This page is not created by, affiliated with, or supported by Slack Technologies, Inc.
2019-02-05
Channels
- # adventofcode (8)
- # aleph (42)
- # announcements (4)
- # beginners (157)
- # boot (4)
- # calva (1)
- # cider (6)
- # cljdoc (8)
- # cljs-dev (1)
- # cljsrn (3)
- # clojure (50)
- # clojure-europe (9)
- # clojure-italy (16)
- # clojure-nl (2)
- # clojure-russia (6)
- # clojure-spec (59)
- # clojure-uk (25)
- # clojurescript (20)
- # core-async (41)
- # cursive (30)
- # data-science (9)
- # datomic (20)
- # fulcro (28)
- # kaocha (9)
- # nrepl (1)
- # off-topic (3)
- # om (3)
- # pathom (10)
- # re-frame (8)
- # reagent (2)
- # ring-swagger (38)
- # rum (9)
- # shadow-cljs (209)
- # spacemacs (7)
- # tools-deps (11)
- # vim (6)
- # yada (2)
hmm, was :external-config
at some point explained in the docs? I recall reading about it at some point, and it saved my bacon yesterday, but I can’t find a good explanation anywhere
I’ve posted a gist of one way I have been handling injection of environment variables using a shadow-cljs build hook and juxt/aero: https://gist.github.com/mhuebert/a87795a74bf3f3452e6b032f1c7ee25d
It seems like a bit more ceremony than I would like but is the cleanest way I have found (so far) to satisfy all my requirements.. I would be very happy to discover ways to simplify it
I guess it is time I finally write down how to do runtime configuration for CLJS 🙂
:external-config
and the macro approaches break the assumptions made by the caching stuff so it very likely breaks caching in some way
I tried another approach where I compared the prev-config to current-config, and conditionally added just the env namespace to :cache-blockers if things had changed, but that never worked for the first compile
so changes to the config while the server wasn’t running, or re-running with a different release flag, wouldn’t get picked up
that actually looked like it might be a bug or nuance with how :cache-blockers works
you can skip the hook though and just
(defn release
"Build :browser release, with advanced compilation"
([] (release "local"))
([release-flag]
(-> (shadow-config/get-build! :browser)
;; note, we add ::release-flag to our build-config, we need this later.
(assoc-in [:compiler-options :external-config ::env] (read-env :release))
(shadow/release* {}))))
well the problem here is that you are treating runtime configuration like you would in clojure
the idea is to have a single config map which gives you the same benefits as goog-define
-ing things
read via aero
so you can pull in system environment variables or keep things in the edn directly
(defmacro goog-define*
"Like goog-define, but reads `k` from `env/config` at compile time "
[name k default]
`(~'goog-define ~name ~(get config k default)))
so the single config map is exposed as a plain map in cljs-land, and then i only use that goog-define*
bit when I want DCE. We also have some clj scripts which rely on the config being available in clojure-land (generating static html from hiccup, some other deploy-related tasks)
re: the custom build function, i just couldn’t figure out how to pass in my own command line argument
alright 2second example: in a browser app I recommend passing runtime config via the HTML
things like the url you use in your example should probably come from the html directly since the server probably knows these things
or if just static stuff you just have a index-dev.html
index-staging.html
index.html
for prod
yeah, server or compile environment. this is all just deployed to s3 but we generate the index.html from hiccup in a post-compile build hook
I tried describing my approach a long time ago https://code.thheller.com/blog/web/2014/12/20/better-javascript-html-integration.html
and currently that build hook can seamlessly read from the same app.env/config
var that is exposed to cljs
and we can also put things like :devtools false
in the same config map, which needs to be a compile-time flag to keep stuff out of prod builds
do you have an example of something that should actually be a compile time constant?
we have an additional tab that is only visible in dev and sometimes staging, and includes a bunch of extra stuff that we don’t want bundled always
sure. you don't actually gain anything by making it a goog-define since you are never redefining it
just something that ends up as (def devtools? false)
would have the same effect as far as closure is concerned
if the get-config
is a macro and just emits literals you don't need extra goog-define gimmicks
yeah, sometimes I use a prod
release flag if i want my local build to talk to the prod api server
hm. if that is done in a build hook, would shadow notice changes to that file “in time” to invalidate its cache if it has changed?
if config variables are read from a static/unchanging map, can DCE still happen? eg. (when (:devtools static-config) ...)
generating something like
(ns app.env)
(def devtools? false)
(def config {:url "foo.bar"})
is easy enoughyeah.. that code-generation looks to me more complicated than what we are doing, where there is a straightforward config map
yeah. although, the macro trickery has a straightforward API once it’s set up, and there is no uncertainty about where a given environment variable is coming from. if you know the release flag, there is just one edn
file where you can always look things up. and changes are picked up during recompile
so maybe it is the most minimal thing that solves for the constraints/requirements we’ve accepted
:cache-blockers
complete blocks all cache for the namespaces mentioned. yes it works on the first compile
npm run
lets you add command line parameters after a --
, which are then passed to the underlying script, eg. npm run watch -- staging
. that’s actually what we have set up. i could imagine something similar for shadow, allowing parameters at the end to be passed in as command-line-args or something, but I haven’t thought a lot about that.
ok. I think there might be a bug there, as my previous attempt was dynamically setting :cache-blockers
in :compile-prepare
stage, and on the first compile it wasn’t recompiling everything that I added to it
hmm yeah but the watch
logic will not trigger recompiles if you modify the config there
hm. i guess that could explain why it wasn’t working on the 1st compile, but seemed to work later
(-> build-state
(assoc ::prev-config public-config)
(update-in [:shadow.build/config :cache-blockers] (if env-changed?
#(conj % 'web3.env)
#(remove #{'web3.env} %))))
if the file is touched on the filesystem the version in the compiler state is invalidated
if a file is in the compiler state and not otherwise affected it won't be recompiled
hmm no I can't see how changing cache-blockers affects anything once a file is compiled and in memory
you mean this part
(let [output (get-in state [:output resource-id])]
;; skip compilation if output is already present from previous compile
;; always recompile files with warnings
(if (and output (not (seq (:warnings output))))
output
(maybe-compile-cljs state src)
))
so seq-compile-cljs-sources
uses that code, par-compile-cljs-sources
calls maybe-compile-cljs
directly without that output check but is not used for partial incremental compiles
well, this isn’t so important anyway, for our purposes :external-config
is fine, and we can live with recompiling everything when config changes, as that is not so often. I guess in a hyper-optimised world we would figure out how to add a particular namespace to the list of things-to-recompile from within a :compile-prepare
hook.
would something like shadow-cljs release app --config-hook "(your.ns/some-fn 1 2 3)"
help?
so (your.ns/some-fn config-from-file 1 2 3)
would be called whenever the config is loaded?
(shadow.build.data/get-source-id-by-provide state 'app.env)
will you you the proper resource-id
probably should use (shadow.build.data/remove-source-by-id state resource-id)
instead of just removing the output
if the cache logic decides that the disk cache was ok then no recompiles happen at all
hmm. so this wouldn’t really trigger a recompile then, because the file isn’t changing
yes - not just the DCE stuff (ie. in macros) but also scripts that generate HTML, interop with Sentry
like
:build-hooks [(app.build/load-env)
(app.build/compile-static-assets-hook)
(app.sentry/push-source-maps!)]
why not
(defn release
"Build :browser release, with advanced compilation"
([] (release "local"))
([release-flag]
(-> (shadow/get-build-config! :browser)
;; note, we add ::release-flag to our build-config, we need this later.
(assoc ::release-flag release-flag)
(shadow/release* {})
(compile-static-assets)
(send-source-maps-to-sentry)
)))
seems overkill to run all the hooks all the time in watch
? granted I don't know what you do in them but probably the first thing you do is check if they should actually run?
yeah, the sentry fn starts with (when (= :release (:shadow.build/mode build-state)) ...)
the static assets are affected by changes to config, so i want those to recompile, and all the heavier bits are memoized so it adds max handful-of-milliseconds
before, there were multiple commands that had to be run / coordinated at the right time, in order to get a correct build
now, running the shadow build ensures that everything is created declaratively / at the right time / using a consistent environment
it is not really possible to compile the app without simultaneously creating all the appropriate assets
it has made it easier for onboarding / booting up in new environments, there is little to remember
I think at one point I was hoping to do away with the custom release/watch scripts altogether,
overall the build hooks let me substantially simplify our whole build/deploy process
the scripts are hid behind npm scripts, so in practice we run npm run watch -- staging
and then it delegates to shadow’s clj-run
that has been helpful as i have been able to evolve how this stuff works without breaking anyone’s habits
hmm that could be useful. would that be added to shadow’s config and validated according to that schema, or could we put arbitrary keys there
if you want to see some real abuse of build hooks, https://github.com/mhuebert/js-interop-example/blob/master/shadow-cljs.edn#L15
I would probably have outsourced the html generation to an external config somewhere but if you like it there that is fine 🙂
I've been thinking about this whole static site generation business a lot the past few weeks
and 90% of the time for these cljs projects, all i need in terms of HTML is a title, and a couple style tags and script elements
I have probably never written anything that didn't have a massive backend that was at least as big as the frontend
that function in the build hook, write-assets!
, makes use of the :output-dir
and :asset-path
in the shadow build config, and in :release
mode it appends content hashes to paths
but the "get something simple out" part for CLJ(S) still sucks compared to something like gatsby
or so
I'm about to transform what's now a tiny cms with nginx-clojure to a static site. First goal is just static html, which should be quite easy, since the back-end is already set up to generate the whole page with just the key of the main content. The cljs part is really simple apart from the cms stuff, just some menu things and image modals.
My current app is basically a static thing hosted on Firebase hosting and using Firebase firestore. The goal will be to generate static HTML for each route and use code splitting and module loading per route change, basically like https://zeit.co/now, except on firebase. I'm looking into ways to generate the HTML. I'll probably wind up doing it the same way I did last time with a headless browser that spits out the HTML.
Also this looks really interesting: https://github.com/faceyspacey/react-universal-component
I did some experiments with zeit but as of now I wouldn't want to build a builder for it
you are only allowed 100mb cache for builds which isn't even enough for the JVM you'd need to download
or via CI of course. eg. https://github.com/jntn/haiku/blob/master/.travis.yml
I always use the manifest.edn
for that https://shadow-cljs.github.io/docs/UsersGuide.html#BrowserManifest
this project has a big backend for the api server, but it doesn’t generate any html, the web app is all on s3
so deployment happens via aws CodeBuild, a remote box performs the shadow build and spits all the html / js to disk
I have used manifest.edn
as well. i ended up writing something more general because we needed the same cache-busting stuff for generated html and css also
our hosting is configured to cache everything except index.html
et al very aggressively
My JVM returns
(System/getProperty "java.version")
=> "11.0.1"
There is 2 ways to get "11"
in my system
{"java.specification.version" "11"
"java.vm.specification.version" "11"}
https://github.com/thheller/shadow-cljs/blob/1485cbefc62d781ca5bc1ddf3464833c75771f2e/src/main/shadow/cljs/devtools/server/npm_deps.clj#L12