- 1. Introduction
- 2. Installation
- 3. Usage
- 4. REPL
- 5. Configuration
- 6. Build Configuration
- 7. Targeting the Browser
- 8. Targeting JavaScript Modules
- 9. Targeting React Native
- 10. Targeting node.js
- 11. Embedding in the JS Ecosystem The :npm-module Target
- 12. Testing
- 13. JavaScript Integration
- 14. Generating Production Code All Targets
- 15. Editor Integration
- 16. Troubleshooting
- 17. Publishing Libraries
- 18. What to do when things don’t work?
- 19. Hacking
8. Targeting JavaScript Modules
The :target :esm
emits files that can be used in any ESM environment.
ESM, short for ECMAscript Modules, or just JavaScript Modules, is the modernized standard for JavaScript files. Most modern platforms support this out of the box, and more and more of the JS ecosystem is moving this way. Each Module generated this way can specify "exports" which other files importing this can reference.
ESM much like the :target :browser is driven by the :modules
config option. Each module can declare its :exports
for others to access.
As with other modes the main configuration options apply and can be set. The additional target-specific options are:
:output-dir | (optional, default |
:runtime | (optional , default |
:modules | required, a map of keyword to module configuration |
8.1. Module Configuration
Each module has its own set of options that control how the module is constructed. Specifying multiple modules will mean that the code is split between them.
:init-fn | optional, a |
:entries | optional, a vector of namespace symbols to load in this module |
:exports | required, a map of symbol to fully qualified symbols |
:depends-on | required when using multiple |
8.1.1. Module Exports
Controlling what code is actually exported is done via :exports
.
{:source-paths ["src/main"]
:dev-http {8000 "public"}
:builds
{:app
{:target :esm
:output-dir "public/js"
:modules {:demo {:exports {hello demo.lib/hello}}}}}}
This will generate the public/js/demo.js
file. The name is decided by taking the :output-dir
and combining it with the key :demo
in the :modules
map.
(ns demo.lib)
(defn hello []
(js/console.og "hello world"))
(defn not-exported []
(js/console.log "foo"))
It will be loadable directly in any ESM environment. For example the Browser. Putting this into a public/index.html
and loading it via http://localhost:8000.
<script type="module">
import { hello } from "/js/demo.js";
hello();
</script>
With npx shadow-cljs watch app
you should see the hello world
logged to the browser console when loading the page.
Note that only hello
is accessible here since it was declared in the :exports
. The (defn not-exported [] …)
will not be accessible and will most likely be removed entirely in :advanced
release builds.
Module Default Exports
ES Module have this one "special" default
export, which you’ll often see used in JS examples. This can be expressed by defining the default
exports like any other.
{:source-paths ["src/main"]
:dev-http {8000 "public"}
:builds
{:app
{:target :esm
:output-dir "public/js"
:modules {:demo {:exports {default demo.lib/hello}}}}}}
And the import side changing to
<script type="module">
import hello from "/js/demo.js";
hello();
</script>
Many platforms or systems apply special meaning to this default
export, but it is declared like any other in the build config.
Module :init-fn
Sometimes you may not require any :exports
and instead just want the code to run automatically when the module is loaded. This can be done via :init-fn
.
{:source-paths ["src/main"]
:dev-http {8000 "public"}
:builds
{:app
{:target :esm
:output-dir "public/js"
:modules {:demo {:init-fn demo.lib/hello}}}}}
And the HTML
<script type="module" src="js/demo.js"></script>
In can also be combined with :exports
to run a function and still provide :exports
{:source-paths ["src/main"]
:dev-http {8000 "public"}
:builds
{:app
{:target :esm
:output-dir "public/js"
:modules
{:demo
{:init-fn demo.lib/hello
:exports {hello demo.lib/hello}}}}}}
Keeping this HTML will essentially just log twice on page load.
<script type="module">
import hello from "/js/demo.js";
hello();
</script>
8.2. Module Splitting
{:source-paths ["src/main"]
:dev-http {8000 "public"}
:builds
{:app
{:target :esm
:output-dir "public/js"
:modules
{:base
{:entries []}
:hello
{:exports {hello demo.lib/hello}
:depends-on #{:base}}
:other
{:exports {foo demo.foo/foo}
:depends-on #{:base}}
}}}}
And adding
(ns demo.foo)
(defn foo []
(js/console.log "foo"))
Here we declare 3 modules with one :base
module and two other modules which both depend on the :base
module. The :base
module declared an empty :entries []
vector which is a convenience to say that it should extract all the namespaces that both of the other modules share (eg. cljs.core
in this case).
You may now load each :module
independently in the HTML.
<script type="module">
import hello from "/js/hello.js";
hello();
</script>
The browser will automatically load the /js/base.js
as well, but not the /js/other.js
as the code above doesn’t need it. You can use :modules
to split code for separate sections of your website for example.
8.3. Dynamic Module Import
Modules can also be loaded dynamically at runtime via the provided shadow.esm/dynamic-import
helper.
(ns my.app
(:require
[shadow.esm :refer (dynamic-import)]
[shadow.cljs.modern :refer (js-await)]))
(defn foo []
(js-await [mod (dynamic-import "https://cdn.pika.dev/preact@^10.0.0")]
(js/console.log "loaded module" mod)))
This would load an external ESM module dynamically at runtime without it ever being part of the build. You can of course also load your own :modules
dynamically this way too.
8.4. Third Party Tool Integration
In the default :runtime :browser
setup all dependencies are bundled and provided by shadow-cljs
. This is done so the output is directly loadable in the Browser. When importing the :target :esm
output into another build tool environment (eg. webpack) that may lead to duplicated dependencies.
Instead, you can configure shadow-cljs to not bundle any JS dependencies and instead leave that to the other tool.
This is done by setting :js-provider
in your build config.
{:source-paths ["src/main"]
:dev-http {8000 "public"}
:builds
{:app
{:target :esm
:output-dir "public/js"
:js-options {:js-provider :import}
:modules {:demo {:exports {default demo.lib/hello}}}}}}
For this build shadow-cljs will only compile and bundle CLJS code, but leave all other JS code to be provided by some other tool later. Note that if you have (:require ["react"])
or any other npm
dependency in your build the output from shadow-cljs
MUST be processed by another tool first before it becomes loadable in the Browser. Only set this if some other tool is actually going to provide the required dependencies.
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论