shadow-cljs.edn
will also need a :builds
section. Builds should be a map of builds keyed by build ID:
A configuration file with a build map.
{:dependencies [[some-library "1.2.1"] ...]
:source-paths ["src"]
:builds
{:app {:target :browser
... browser-specific options ...}
:tests {:target :karma
... karma-specific options ...}}}
Each build describes artifacts that the compiler will build. The build target is an extensible feature of shadow-cljs
, and the compiler comes with quite a few of them already.
Each build in shadow-cljs
must define a :target
which defines where you intend your code to be executed. There are default built-ins for the browser and node.js
. They all share the basic concept of having :dev
and :release
modes. :dev
mode provides all the usual development goodies like fast compilation, live code reloading and a REPL. :release
mode will produce optimized output intended for production.
Targets are covered in separate chapters.
Here are some of them:
:browser
Output code suitable for running in a web browser.
:bootstrap
Output code suitable for running in bootstrapped cljs environment.
:browser-test
Scan for tests to determine required files, and output tests suitable for running in the browser.
:karma
Scan for tests to determine required files, and output karma-runner compatible tests. See Karma.
:node-library
Output code suitable for use as a node library.
:node-script
Output code suitable for use as a node script.
:npm-module
Output code suitable for use as an NPM module.
Each target is covered in more detail in its own chapter since the remaining build options vary on the target you select.
6.2. Development OptionsEach build :target
typically provides some development support. They are grouped under the :devtools
key for each :build
.
When running watch
code for the REPL is injected automatically and usually does not require additional configuration. Additional options are available to control REPL behavior:
:repl-init-ns
allows configuring which namespace the REPL will start in. It defaults to cljs.user
.
:repl-pprint
makes the REPL use cljs.pprint
instead of the regular pr-str
when printing eval results. Defaults to false.
{...
:builds
{:app {...
:devtools {:repl-init-ns my.app
:repl-pprint true
...}}}}
6.2.2. Preloads
As a developer most of your time is spent in development mode. You’re probably familiar with tools like figwheel
, boot-reload
, and devtools
. It’s almost certain that you want one or more of these in your builds.
Preloads are used to force certain namespaces into the front of your generated Javascript. This is generally used to inject tools and instrumentation before the application actually loads and runs. The preloads option is simply a list of namespaces either in the :devtools
/:preloads
section of shadow-cljs.edn
or within the :preloads
key of a specific module:
{...
:builds
{:app {...
:devtools {:preloads [fulcro.inspect.preload]
...}}}}
For example to only include the preloads within a main module during development, and not in a web worker:
{...
:builds
{:app {...
:modules {:main {...
:preloads
[com.fulcrologic.fulcro.inspect.preload
com.fulcrologic.fulcro.inspect.dom-picker-preload]
:depends-on #{:shared}}
:shared {:entries []}
:web-worker {...
:depends-on #{:shared}
:web-worker true}}}}}
:preloads
are only applied to development builds and will not be applied to release builds.
Note
Since version 2.0.130 shadow-cljs automatically addscljs-devtools
to the preloads in watch
and compile
if they are on the classpath. All you need to do is make sure binaryage/devtools
is in your dependencies
list. (Note, not binaryage/cljs-devtools.) If you don’t want to have cljs-devtools
in specific targets, you can suppress this by adding :console-support false
to the :devtools
section of those targets. 6.2.3. Hot Code Reload
The React and ClojureScript ecosystems combine to make this kind of thing super useful. The shadow-cljs
system includes everything you need to do your hot code reload, without needing to resort to external tools.
In order to use it you simply run:
shadow-cljs watch build-id
Hot Reload of Transitive Dependents
By default, compiled files and files explicitly requiring those are reloaded. This approach may not be sufficient eg. during development for :react-native
target. To reload also all transitive dependents, use :reload-strategy
option with value :full
as follows:
Important
This may become slow for larger apps, only use it if you really need it.{...
:builds
{:app
{:target :react-native
:init-fn some.app/init
:output-dir "app"
...
:devtools
{:reload-strategy :full}}}}
6.2.4. Lifecycle Hooks
You can configure the compiler to run functions just before hot code reload brings in updated code, and just after. These are useful for stopping/starting things that would otherwise close over old code.
These can be configured via the :devtools
section in your build config or directly in your code via metadata tags.
You can set certain metadata on normal CLJS defn
vars to inform the compiler that these functions should be called at a certain time when live reloading.
hook config via metadata
(ns my.app)
(defn ^:dev/before-load stop []
(js/console.log "stop"))
(defn ^:dev/after-load start []
(js/console.log "start"))
This would call my.app/stop
before loading any new code and my.app/start
when all new code was loaded. You can tag multiple functions like this and they will be called in dependency order of their namespaces.
There are also async variants of these in case you need to do some async work that should complete before proceeding with the reload process.
async hooks example
(ns my.app)
(defn ^:dev/before-load-async stop [done]
(js/console.log "stop")
(js/setTimeout
(fn []
(js/console.log "stop complete")
(done)))
(defn ^:dev/after-load-async start [done]
(js/console.log "start")
(js/setTimeout
(fn []
(js/console.log "start complete")
(done)))
Important
The functions will receive one callback function that must be called when their work is completed. If the callback function is not called the reload process will not proceed.It is possible to tag namespaces with metadata so they will never be reloaded even if they are recompiled.
A non-reloadable ns
(ns ^:dev/once my.thing)
(js/console.warn "will only execute once")
Namespaces can also be tagged to always reload.
An always-reloadable ns
(ns ^:dev/always my.thing)
(js/console.warn "will execute on every code change")
Config
In addition to the metadata you can configure the lifecycle hooks via shadow-cljs.edn
.
:before-load
A symbol (with namespace) of a function to run just before refreshing files that have been recompiled. This function must be synchronous in nature.
:before-load-async
A symbol (with namespace) of a function (fn [done])
to run just before refreshing. This function can do async processing, but must call (done)
to indicate it is complete.
:after-load
A symbol (with namespace) of a function to run after hot code reload is complete.
:after-load-async
A symbol (with namespace) of a function (fn [done])
to run after hot code reload is complete. This function can do async processing, but must call (done)
to indicate it is complete.
:autoload
A boolean controlling whether code should be hot loaded. Implicitly set to true
if either of the callbacks is set. Always enabled for the :browser
target by default, set to false
to disable.
:ignore-warnings
A boolean controlling whether code with warnings should be reloaded. Defaults to false
.
A sample of lifecycle hooks.
{...
:builds
{:app {...
:devtools {:before-load my.app/stop
:after-load my.app/start
...}}}}
Important
Hooks cannot be declared in thecljs.user
namespace. Hooks are only used if the namespace containing them is actually included in the build. If you use an extra namespace make sure to include it via :preloads
.
Tip
If neither:after-load
nor :before-load
are set the compiler will only attempt to hot reload the code in the :browser
target. If you still want hot reloading but don’t need any of the callbacks you can set :autoload true
instead. 6.3. Build Hooks
It is sometimes desirable to execute some custom code at a specific stage in the compilation pipeline. :build-hooks
let you declare which functions should be called and they have full access to the build state at that time. This is quite powerful and opens up many possible tool options.
They are configured per build under the :build-hooks
key
Exampe :build-hooks
{...
:builds
{:app {:target ...
:build-hooks
[(my.util/hook 1 2 3)]
...}}}}
Example hook code
(ns my.util)
(defn hook
{:shadow.build/stage :flush}
[build-state & args]
(prn [:hello-world args])
build-state)
This example would call (my.util/hook build-state 1 2 3)
after the build completed the :flush
stage (ie. written to disk). The example would print [:hello-world (1 2 3)]
but please do something more useful in actual hooks.
The hook is a just a normal Clojure function with some additional metadata. The {:shadow.build/stage :flush}
metadata informs the compiler to call this hook for :flush
only. You may instead configure {:shadow.build/stages #{:configure :flush}}
if the hook should be called after multiple stages. At least one configured stage is required since the hook otherwise would never do anything.
All build hooks will be called after the :target
work is done. They will receive the build-state
(a clojure map with all the current build data) as their first argument and must return this build-state
modified or unmodified. When using multiple stages you can add additional data to the build-state
that later stages can see. It is strongly advised to use namespaced keys only to ensure not accidentally breaking the entire build.
The build-state
has some important entries which might be useful for your hooks:
:shadow.build/build-id
- the id of the current build (eg. :app
)
:shadow.build/mode
- :dev
or :release
:shadow.build/stage
- the current stage
:shadow.build/config
- the build config. You can either store config data for the hook in the build config directly or pass it as arguments in the hook itself
Important
With a runningwatch
all hooks will be called repeatedly for each build. Avoid doing too much work as they can considerably impact your build performance. 6.3.1. Compilation Stages
The possible stages the :build-hooks
can use are:
:configure
- initial :target
specific configuration
:compile-prepare
- called before any compilation is done
:compile-finish
- called after all compilation finishes
:optimize-prepare
- called before running the Closure Compiler optimization phase (:release
only)
:optimize-finish
- called after Closure is done (:release
only)
:flush
- called after everything was flushed to disk
With a running watch
the :configure
is only called once. Any of the others may be called again (in order) for each re-compile. The build-state
will be re-used until the build config changes at which point it will be thrown away and a fresh one will be created.
shadow-cljs
will cache all compilation results by default. The cache is invalidated whenever anything relevant to the individual source files changes (eg. changed compiler setting, changed dependencies, etc.). This greatly improves the developer experience since incremental compilation will be much faster than starting from scratch.
Invalidating the cache however can not always be done reliably if you are using a lot of macros with side-effects (reading files, storing things outside the compiler state, etc.). In those cases you might need to disable caching entirely.
Namespaces that are known to include side-effecting macros can be blocked from caching. They won’t be cached themselves and namespaces requiring them will not be cached as well. The clara-rules library has side-effecting macros and is blocked by default. You can specify which namespaces to block globally via the :cache-blockers
configuration. It expects a set of namespace symbols.
clara.rules cache blocking example (this is done by default)
{...
:cache-blockers #{clara.rules}
:builds {...}}
In addition you can control how much caching is done more broadly via the :build-options
:cache-level
entry. The supported options are:
:all
The default, all CLJS files are cached
:jars
Only caches files from libraries, ie. source files in .jar
files
:off
Does not cache any CLJS compilation results (by far the slowest option)
Compiling without Cache
{...
:builds
{:app
{:target :browser
...
:build-options
{:cache-level :off}}}}
The cache files are stored in a dedicated directory for each build so the cache is never shared between builds. A build with the id :app
will have the :dev
cache in the directory:
Cache location for cljs/core.cljs
target/shadow-cljs/builds/app/dev/ana/cljs/core.cljs.cache.transit.json
The :cache-root
setting defaults to target/shadow-cljs
and controls where ALL cache files will be written. It can only be configured globally, not per build.
{:source-paths [...]
:dependencies [...]
:cache-root ".shadow-cljs"
:builds ...}
The :cache-root
is always resolved relative to the project directory. You can also specify absolute paths (eg. /tmp/shadow-cljs
).
The Closure Library & Compiler allow you to define variables that are essentially compile time constants. You can use these to configure certain features of your build. Since the Closure compiler treats these as constants when running :advanced
optimizations they are fully supported in the Dead-Code-Elimination passes and can be used to remove certain parts of the code that should not be included in release
builds.
You can define them in your code
(ns your.app)
(goog-define VERBOSE false)
(when VERBOSE
(println "Hello World"))
This defines the your.app/VERBOSE
variable as false
by default. This will cause the println
to be removed in :advanced
compilation. You can toggle this to true
via the :closure-defines
options which will enable the println
. This can either be done for development only or always.
{...
:builds
{:app
{:target :browser
...
:modules {:app {:entries [your.app]}}
:dev {:closure-defines {your.app/VERBOSE true}}
:closure-defines {your.app/VERBOSE true}
:release {:closure-defines {your.app/VERBOSE true}}
}}
Tip
It is generally safer to use the "disabled" variant as the default since it makes things less likely to be included in arelease
build when they shouldn’t be. Forgetting to set a :closure-defines
variable should almost always result in less code being used not more.
Closure Defines from the Closure Library
goog.DEBUG
: The Closure Library uses this for many development features. shadow-cljs
automatically sets this to false
for release
builds.
goog.LOCALE
can be used to configure certain localization features like goog.i18n.DateTimeFormat
. It accepts a standard locale string and defaults to en
. Pretty much all locales are supported, see here and here.
The CLJS compiler supports several options to influence how some code is generated. For the most part shadow-cljs
will pick some good defaults for each :target
but you might occasionally want to change some of them.
These are all grouped under the :compiler-options
key in your build config.
{:dependencies [...]
:builds
{:app
{:target :browser
...
:compiler-options {:fn-invoke-direct true}}}}
Most of the standard ClojureScript Compiler Options are either enabled by default or do not apply. So very few of them actually have an effect. A lot of them are also specific to certain :target
types and do not apply universally (e.g. :compiler-options {:output-wrapper true}
is only relevant for :target :browser
).
Currently supported options include
:optimizations
supports :advanced
, :simple
or :whitespace
, defaults to :advanced
. :none
is the default for development and cannot be set manually. release
with :none
won’t work.
:infer-externs
:all
, :auto
, true
or false
, defaults to :auto
:static-fns
(Boolean) defaults to true
:fn-invoke-direct
(Boolean) defaults to false
:elide-asserts
(Boolean) default to false
in development and true
in release
builds
:pretty-print
and :pseudo-names
default to false
. You can use shadow-cljs release app --debug
to enable both temporarily without touching your config. This is very useful when running into problem with release
builds
:source-map
(Boolean) defaults to true
during development, false
for release
.
:source-map-include-sources-content
(Boolean) defaults to true
and decides whether source maps should contains their sources in the .map
files directly.
:source-map-detail-level
:all
or :symbols
(:symbols
reduces overall size a bit but also a bit less accurate)
:externs
vector of paths, defaults to []
:checked-arrays
(Boolean), defaults to false
:anon-fn-naming-policy
:rename-prefix
and :rename-prefix-namespace
:warnings
as a map of {warning-type true|false}
, eg. :warnings {:undeclared-var false}
to turn off specific warnings.
Unsupported or non-applicable Options
Options that don’t have any effect at all include
:verbose
is controlled by running shadow-cljs compile app --verbose
not in the build config.
:foreign-libs
and :libs
:stable-names
always enabled, cannot be disabled
:install-deps
:source-map-path
, :source-asset-path
and :source-map-timestamp
:cache-analysis
always enabled, cannot be disabled.
:recompile-dependents
:preamble
:hashbang
(the :node-script
target supports this, others don’t)
:compiler-stats
use --verbose
to get detailed information instead
:optimize-constants
always done for release
builds, cannot be disabled
:parallel-build
always enabled
:aot-cache
:package-json-resolution
see :js-options :resolve instead
:watch-fn
:process-shim
It is sometimes desireable to fail a build with warnings rather than continuing with the build (eg. in CI envs). You can use the :warnings-as-errors
compiler options to customize how that is handled.
Treat all warnings as errors
{...
:builds
{:app
{...
:compiler-options {:warnings-as-errors true}}}}
Only throw certain warnings
{...
:builds
{:app
{...
:compiler-options {:warnings-as-errors #{:undeclared-var}}}}
A set of possible warning-type keywords can be found here.
Only throw for certain namespaces
{...
:builds
{:app
{...
:compiler-options {:warnings-as-errors {:ignore #{some.ns some.library.*}
:warnings-types #{:undeclared-var}}}
:ignore
takes a set of symbols refering to namespaces. Either direct matches or .*
wildcards are allowed. :warning-types
has the same functionality as above, not specifying it means all warnings will throw except the ignored namespaces.
By default the generated JS output will be compatible with ES6 and all "newer" features will be transpiled to compatible code using polyfills. This is currently the safest default and supports most browsers in active use (including IE10+).
You can select other output options if you only care about more modern environments and want to keep the original code without replacements (eg. node
, Chrome Extensions, …)
Important
Note that this mostly affects imported JS code from npm or.js
files from the classpath. CLJS will currently only generate ES5 output and is not affected by setting higher options.
You can configure this via the :output-feature-set
in :compiler-options
. The older :language-out
option should not be used as :output-feature-set
replaced it.
Supported options are:
:bare-minimum
:es3
:es5
:es6
- class
, const
, let
, …
:es7
- exponent **
operator
:es8
- async/await
, generators
, shared memory and atomics
:es2018
- async iteration, Promise.finally
, several RegExp
features, spread syntax in object literals
:es2019
:es2020
- AsyncIterator
, BigInt
, ??
operator, dynamic imports
:es-next
- all the features the Closure Compiler currently supports
:browser-2020
- :es2019
minus several RegExp features
:browser-2021
- :es2020
minus RegExp Unicode properties
Example
{...
:builds
{:script
{:target :node-script
:main foo.bar/main
...
:compiler-options {:output-feature-set :es7}}}}
Documentation on these options is a bit sparse and is mostly documented in the code here.
6.8. Conditional ReadingCaution
This feature only works inshadow-cljs
. It was officially rejected by the ClojureScript project. It will still compile fine in CLJS but only the official branches work (e.g. :cljs
). It might still be supported one day but as of now it is not.
shadow-cljs
lets you configure additional reader features in .cljc
files. By default you can only use reader conditionals to generate separate code for :clj
, :cljs
or :cljr
. In many CLJS builds however it is also desirable to select which code is generated based on your :target
.
Example: Some npm
packages only work when targeting the :browser
, but you may have a ns
that you also want to use in a :node-script
build. This might happen frequently when trying to use Server-Side Rendering (SSR) with your React App. codemirror
is one such package.
(ns my.awesome.component
(:require
["react" :as react]
["codemirror" :as CodeMirror]))
(defn init-cm [dom-node]
(let [cm (CodeMirror/fromTextArea dom-node #js {...})]
...))
...
This namespace will compile fine for both builds (:node-script
and :browser
) but when trying to run the :node-script
it will fail since the codemirror
package tries to access the DOM. Since react-dom/server
does not use refs the init-cm
function will never be called anyways.
While you can use :closure-defines to conditionally compile away the init-cm
fn you can not use it to get rid of the extra :require
. Reader conditionals let you do this easily.
(ns my.awesome.component
(:require
["react" :as react]
#?@(:node [[]]
:cljs [["codemirror" :as CodeMirror]])))
#?(:node
(defn init-cm [dom-node]
:no-op)
:cljs
(defn init-cm [dom-node]
... actual impl ...))
...
:reader-features
config examples
{...
:builds
{:app
{:target :browser
...}
:server
{:target :node-script
:compiler-options
{:reader-features #{:node}}}}}
The :server
build will then no longer have the codemirror
require and the init-cm
function is removed. Becoming only
(ns my.awesome.component
(:require
["react" :as react]))
(defn init-cm [dom-node] :no-op)
...
Important
This feature is only available in.cljc
files and will fail in .cljs
files. 6.9. Overriding from the CLI
It is sometimes desirable to make small adjustments to the build configuration from the command line with values that can’t be added statically to the shadow-cljs.edn
config or may change depending on the environment you are in.
You can pass additional config data via the --config-merge {:some "data"}
command line option which will be merged into the build config. Data added from the CLI will override data from the shadow-cljs.edn
file.
Example shadow-cljs.edn
config
{...
:builds
{:app
{:target :browser
:output-dir "public/js"
...}}}
Overriding the :output-dir
from the CLI
$ shadow-cljs release app --config-merge '{:output-dir "somewhere/else"}'
Overriding the :closure-defines
from the CLI
$ shadow-cljs release app --config-merge '{:closure-defines {your.app/DEBUG true}}'
--config-merge
expects one EDN map and can be used multiple times, they will be merged left to right. The data added is also visible to build-hooks. It will also accept a file path like --config-merge a/path.edn
or --config-merge classpath:a/resource.edn
.
Important
If you specify multiple build ids the data will be merged into all specified builds.shadow-cljs release frontend backend --config-merge '{:hello "world"}'
will be applied to both. 6.10. Using Environment Variables
It is possible to use environment variables to set configuration values in shadow-cljs.edn
but you should consider using --config-merge
instead. If you really must use an environment variable you can do so via the #shadow/env "FOO"
reader tag. You can also use the shorter #env
.
Example shadow-cljs.edn
config
{...
:builds
{:app
{:target :browser
:output-dir "public/js"
:closure-defines {your.app/URL #shadow/env "APP_URL"}
...}}}
The are also a few more supported forms that you can use #shadow/env
with.
#shadow/env "APP_URL"
#shadow/env ["APP_URL"]
#shadow/env ["APP_URL" "default-value"]
#shadow/env ["APP_URL" :default "default-value"]
#shadow/env ["PORT" :as :int :default 8080]
Supported :as
coercions are :int
, :bool
, :keyword
, :symbol
. Supplied :default
values will not be converted and are expected to be in the correct type already.
Important
The environment variables used when theshadow-cljs
process was started are used. If a server process is used its environment variables will be used over those potentially set by other commands. This is mostly relevant during development but may be confusing. --config-merge
does not have this limitation. 6.11. Build and Target defaults
It is possible to use set defaults that will be used for all builds, or for all targets of a certain type.
Configuration merge order is as follows :build-defaults
→ :target-defaults
→ actual build config → extra config overrides.
Example shadow-cljs.edn
config
{...
:build-defaults
{:closure-defines
{your.app/VERBOSE true}}
:target-defaults
{:browser
{:js-options
{:resolve {"react" {:target :global
:global "React"}}}}}
:builds
{:app
{:target :browser
...}}}
In this example the :app
target will inherit both :build-defaults
and the :target-defaults
for :browser
.
Important
Configs later in the merge order can override, but not remove previous configuration items. Once a default is set, the only way to remove it is by overriding it.RetroSearch is an open source project built by @garambo | Open a GitHub Issue
Search and Browse the WWW like it's 1997 | Search results from DuckDuckGo
HTML:
3.2
| Encoding:
UTF-8
| Version:
0.7.4