shadow-cljs

repository·master·Indexed 25 days ago

https://github.com/thheller/shadow-cljs

A ClojureScript build tool providing support for various output targets including ESM and CommonJS (:npm-module). It includes the shadow.remote architecture for runtime-agnostic REPL communication via a relay and client protocol, and supports advanced integration with Webpack DLLs and external ESM module imports using the esm: prefix.

Tokens
4.2K
Snippets
10
Records
23
Agent score
82%

What's inside shadow-cljs

  1. Use the shadow.remote protocol for client communication

    master

    The shadow.remote protocol uses simple EDN maps exchanged over websockets (with Transit encoding) or TCP. Every message must include an :op keyword.

    Reserved Keywords

    • :op: (Required) Describes the purpose of the message.
    • :to: A single number or a set of numbers. Specifies which client(s) the relay should forward the message to. If omitted, the relay interprets the message.
    • :from: The ID of the client that originated the message (added automatically by the relay).
    • :client-id: A valid relay client ID, used depending on the :op context.

    Connection Lifecycle

    1. Connection: The relay sends {:op :welcome :client-id id} to the client.
    2. Handshake: The client must respond with a :hello message containing :client-info. Failure to do so results in disconnection.
    ;; Example handshake response
    {:op :hello :client-info {:foo 1}}
  2. Understand the shadow.remote architecture

    master

    Overview

    shadow.remote is a new architecture designed to provide a generic, runtime-agnostic alternative to nREPL for ClojureScript. It addresses limitations in existing REPL tools, such as the "Print Problem" (handling extremely large or unprintable values) and the difficulty of running REPL UIs in non-JVM runtimes like browsers, React Native, or Node.js.

    Core Components

    • Relay: A central hub (implemented in CLJ) that handles message routing between clients. A relay is required because certain runtimes (like browsers) cannot accept direct incoming connections and must instead connect to a relay. The relay manages client lifecycles and forwards messages based on routing numbers.
    • Client: Any runtime or tool participating in the protocol. Clients are passive by default and initiate actions. Clients can be runtimes (e.g., a browser app) or tools (e.g., a UI debugger). Clients use :client-info to discover one another via the relay.
  3. Understand the difference between ClosureJS and ESM module formats

    master

    In the context of ClojureScript, it is important to distinguish between the ClosureJS module format and ESM (ECMAScript Modules):

    ClosureJS (The traditional CLJS format)

    • Mechanism: Uses an ad-hoc namespacing system via goog.provide. It creates nested objects in the global scope (e.g., goog.provide("cljs.core") creates var cljs = { core: {} };).
    • Pros: The shared global scope makes implementing REPLs and Hot Module Replacement (HMR) trivial, as you can redeclare variables at runtime. It also allows easy inspection of state via browser devtools.
    • Cons: It is not a web standard, making interop with the broader JavaScript ecosystem difficult and requiring "glue code" or global hacks. It also requires a "Debug Loader" to manage dependency ordering in unoptimized builds.

    ESM (The modern JS standard)

    • Mechanism: Each module has its own private scope. Variables are not shared unless explicitly exported and imported.
    • Pros: It is the industry standard, supported by all modern browsers and runtimes, allowing for seamless interop with regular JavaScript.
    • Cons: Because modules have private scopes, you cannot easily modify or inspect state from the outside. In ClojureScript, strict ESM makes features like the REPL and dynamic HMR more complex because you cannot easily add exports or redeclare bindings after a module has been loaded.
  4. Limitations of publishing CLJS libraries to npm

    master

    While you can publish CLJS libraries to npm using :npm-module or :esm, doing so for multiple packages introduces significant issues:

    • Duplicate Core Libraries: Each package will bundle its own cljs.core, leading to incompatible data structures (e.g., different implementations of {} or []) and large artifact sizes.
    • Macro Support: Compiled JS code cannot consume CLJS macros. The CLJS compiler requires a local version of the source to access macros and analyzer data.
    • Optimization Loss: Publishing modularly as separate npm packages makes it difficult to utilize :advanced optimizations, which are necessary for effective dead-code elimination.

    To incrementally introduce CLJS into an existing JS project, it is recommended to use existing :npm-module or :esm capabilities within a single build context rather than attempting to manage multiple independent CLJS-to-npm packages.

  5. Import external ESM modules at runtime using the esm: prefix

    master

    You can prevent shadow-cljs from bundling a dependency by using the esm: prefix in your :require statements. This tells the compiler to leave the import as a standard ESM import statement, delegating the resolution and loading to the runtime (e.g., the browser).

    Important Note: The path provided must be relative to the :output-dir files created by the build, not relative to the source file.

    Usage Examples:

    1. Loading a package from a CDN at runtime:

    (ns my.app
       (:require ["https://cdn.pika.dev/preact@^10.0.0" :as preact]))

    2. Loading a local ESM file at runtime:

    (ns my.app
      (:require ["esm:../foo.js" :as foo]))
  6. Convert a .p12 certificate to a JKS keystore

    master

    If you have a .p12 certificate file (for example, generated via tools like certsimple), you can convert it into a Java Keystore (.jks) using the keytool command. This is useful if your environment or configuration requires a JKS format.

    Note: Converting to a .jks file via keytool typically requires a password, which may need to be managed in your shadow-cljs configuration.

    keytool -importkeystore -destkeystore keystore.jks -srcstoretype PKCS12 -srckeystore test-cert.p12
  7. Use the :esm target for ES Module output

    master

    The :target :esm option allows you to emit ES Module (ESM) import/export statements instead of CommonJS. This target is designed to reduce boilerplate and work well with code-splitting :modules.

    Key Characteristics:

    • Exports: Instead of providing every namespace as an individual artifact, it uses a configurable :exports option to map CLJS fully qualified names to regular ESM export names.
    • Development Mode: Unoptimized output uses globalThis to expose globals (like cljs), enabling features like hot-reload and REPL. This works in environments like browsers, Node.js, and Deno.
    • Production Mode: Optimized code does not expose globals and does not rely on globalThis, making it compatible with all ESM-compliant systems.
    • Runtime Loading: Because it integrates with standard ESM, you can instruct the runtime (e.g., a browser) to load dependencies directly rather than bundling them with shadow-cljs.
  8. Manage the shadow-cljs server

    master

    The shadow-cljs CLI can manage a background server process to handle commands more efficiently. Use the following commands to control the server lifecycle:

    • Start the server: shadow-cljs start (starts the server if it is not already running).
    • Stop the server: shadow-cljs stop (sends a remote stop signal to the running server).
    • Restart the server: shadow-cljs restart (stops the current server and starts a new one).

    Note: If the server is already running, most other shadow-cljs commands will automatically act as a client and communicate with the existing server.

  9. Run build tasks with shadow/dev, shadow/once, and shadow/release

    master

    Use these commands to execute specific build workflows for a build :foo:

    • (shadow/dev :foo): Starts the build, connects the REPL, and automatically stops the process when :repl/quit is called.
    • (shadow/once :foo): Compiles a development build exactly once.
    • (shadow/release :foo): Creates a production release build for :foo.
    (shadow/dev :foo)
    (shadow/once :foo)
    (shadow/release :foo)
  10. Query object details using Object Flow

    master

    The Object Flow allows a client to incrementally query information or different "views" of an object using an :oid (Object ID).

    Describing an Object

    To get a summary of an object:

    {:op :obj-describe :to id :oid oid}
    
    ;; Success response
    {:op :opj-summary :oid oid :summary {...}}
    
    ;; Not found response
    {:op :opj-not-found :oid oid}

    Requesting Specific Views

    Once you have a summary, you can request specific representations (like EDN, string, or pprint) using :request-op:

    {:op :obj-request :to id :oid oid :request-op :edn}
    {:op :obj-request :to id :oid oid :request-op :str}
    {:op :obj-request :to id :oid oid :request-op :pprint}
    
    ;; Success response
    {:op :obj-result :from id :result <request-op-return-value>}
    
    ;; Error responses
    {:op :obj-not-found :from id :oid oid}
    {:op :obj-request-not-supported :oid oid :request-op kw}
    {:op :obj-request-failed :oid oid :ex-oid ex-oid :msg msg}
  11. Manage dev processes and REPLs with shadow/start-worker, shadow/repl, and shadow/stop-worker

    master

    You can manage individual build processes (workers) and switch between them within a single REPL session using these commands:

    • (shadow/start-worker :build-name): Starts a development process for the specified build :build-name.
    • (shadow/repl :build-name): Switches the current REPL context to the one associated with :build-name.
    • (shadow/stop-worker :build-name): Stops the development process for the specified :build-name.
    (shadow/start-worker :foo)
    (shadow/repl :foo)
    (shadow/stop-worker :foo)