Cherry

repository·main·Indexed 20 days ago

https://github.com/squint-cljs/cherry

An experimental ClojureScript to ES6 module compiler designed for modern JavaScript tooling. Cherry compiles .cljs files into ES6-compatible .mjs files, enabling integration with workflows like Vite, Node.js, Deno, and Next.js without the traditional Google Closure toolchain. It features a tree-shaking transform for esbuild to reduce cljs.core.js bundle sizes, nREPL support for editor connectivity, and a Vite plugin for browser-side evaluation with HMR.

Tokens
6.2K
Snippets
24
Records
34
Agent score
71%

What's inside cherry

  1. How the tree-shaking transform works

    main

    The transform converts the side-effect-heavy Closure output into a tree-shakeable format through several stages:

    1. Variable Conversion: Converts singly-assigned $APP.p = rhs properties into local var p = rhs declarations.
    2. Arity Folding: Folds contiguous arity installs (p.h = ..) into pure IIFEs: var p = /* @__PURE__ */ (function(){var $self=..;..;return $self})().
    3. Type Constructor Merging: Merges type constructors with their prototype stamps into single /* @__PURE__ */ initializers.
    4. Hoisting Cleanup: Converts hoisted var names with exactly one top-level assignment into declaration-at-assignment.
    5. Statement Splitting: Splits multi-declarator var statements and folds remaining cursor segments into type initializers.
    6. Pure Annotation: Marks remaining call/new initializers (dispatch-builders, singletons, etc.) with the /* @__PURE__ */ annotation.
  2. Understand the relationship between Cherry and Squint

    main

    Cherry and Squint are related dialects that share much of their underlying logic. Most code is shared via the squint git dependency to prevent duplication and rot.

    Shared Logic

    Core components like the emitter core (squint.compiler-common), configuration/path resolution (squint.internal.node.utils), and certain internal utilities are shared between both projects.

    When to Fork

    A fork (separate implementation) is only used when the dialects have different runtime semantics. For example:

    • Function/Type Emittance: Cherry emits CLJS-style names (e.g., cljs$core$IFn$_invoke$arity$N) to support the cljs.core runtime, whereas Squint emits native scheme-style names (e.g., squint$lang$variadic).
    • Compiler Drivers: While both use compiler-common, the top-level drivers differ to accommodate their respective runtimes.

    If you are contributing or extending the project, aim to use shared namespaces in the squint repository rather than duplicating logic in cherry.

  3. How Cherry handles PROTOCOL_SENTINEL sharing with other CLJS runtimes

    main

    Cherry uses a patching mechanism to ensure that its cljs.core.PROTOCOL_SENTINEL is shared with other coexisting ClojureScript (CLJS) runtimes in the same JavaScript realm.

    The Problem

    CLJS dispatches protocols by attaching marker properties to native prototypes (e.g., Date.prototype.cljs$core$IComparable$). These markers are checked for identity against a PROTOCOL_SENTINEL. Because each runtime typically mints its own private sentinel, if two runtimes (like a standard shadow-cljs build and a Cherry build) coexist, the one that loads last overwrites the marker on the prototype. This causes the first runtime to fail protocol checks (e.g., throwing Cannot compare).

    The Solution

    Cherry implements a build-time patch (patch-protocol-sentinel) that modifies the initialized lib/cljs.core.js. Instead of minting a private sentinel, the patched code follows a three-step resolution to find or create a shared token:

    1. Reuse: Use globalThis.cljs.core.PROTOCOL_SENTINEL if it already exists.
    2. Sniff: If not present, adopt the value from an existing marker (e.g., Date.prototype.cljs$core$IEquiv$) to detect if a foreign runtime has already stamped the prototype.
    3. Mint: If no existing sentinel is found, mint a new one and publish it to globalThis.cljs.core.

    This ensures that all subsequent protocol stamps in cljs.core use the same shared identity.

  4. Use `preserve-ns` to expose CLJS namespaces to cherry

    main

    When using advanced compilation, cherry's output might not be able to find certain CLJS functions unless they are explicitly preserved. Use the preserve-ns macro to make an entire namespace available as globals.

    If you do not want to preserve an entire namespace, you can instead use the ^:export metadata on specific functions or values within your CLJS namespace to make them available:

    (defn ^:export foo [])
    (cherry/preserve-ns 'cljs.core)
  5. Packaging: Public facade over internal module

    main

    To prevent users from accidentally importing unstable internal names that change with every build, Cherry uses a facade pattern:

    • Internal Module: The actual definitions and ~916 internal names are moved to an internal module (e.g., lib/internal/cljs.core.js).
    • Public Facade: lib/cljs.core.js acts as a facade that re-exports only the stable public API.
    • Compatibility: User-compiled code continues to import 'cljs.core.js' unchanged. Bundlers like esbuild can shake through the facade to the internal implementation with zero overhead.
  6. Understand the Cherry tree-shaking transform for esbuild

    main

    Cherry uses a specialized production transform to re-massage the Closure output of cljs.core.js. This allows bundlers like esbuild to effectively tree-shake the core library, which is otherwise difficult to shake due to its reliance on a global $APP object and side-effect-heavy property assignments.

    Key Benefits

    • Drastic Bundle Reduction: A bare import of cljs.core.js can be reduced from ~364 KB to ~2.0 KB.
    • Granular Imports: Importing specific functions (e.g., import { str }) results in much smaller bundles (e.g., ~133 KB) compared to the full core.
    • Wire Parity with Vanilla CLJS: While vanilla CLJS with advanced optimizations is smaller raw, the tree-shaken Cherry approach achieves near-parity in compressed (gzip/brotli) sizes, while keeping user builds free from the Closure compiler.
  7. Extend the CLI via the dialect map mechanism

    main

    To avoid duplicating CLI logic (like argument validation, error reporting, and command scaffolding), Cherry uses a shared CLI namespace (squint.internal.cli-common) parameterized by a dialect map.

    When adding new CLI features, they should be implemented in the shared squint namespace and exposed to Cherry by updating its dialect map. This ensures fixes and features land in one place.

    ;; Example of the dialect map structure used to parameterize the shared CLI
    {:prog "cherry" 
     :log-prefix "[cherry]" 
     :config-file "cherry.edn" 
     :compile-file cherry.compiler.node/compile-file 
     :resolve-ns ... 
     :commands #{:run :compile :watch :eval}}
  8. Run the Cherry Vite demo

    main

    To run the demonstration project that uses Cherry with Vite.js, install the dependencies using npm and then use the bb (Babashka) task runner to start the development server.

    To see changes reflected in the browser, edit the cherry.cljs file.

    npm install
    bb dev
  9. Use hot reload lifecycle hooks in Cherry

    main

    When saving .cljs or .cljc files, Cherry hot-swaps the module in the browser without a full page reload. You can execute code specifically before or after this swap using metadata tags on functions:

    • ^:dev/before-load: Runs before the new code is loaded.
    • ^:dev/after-load: Runs after the new code is loaded.

    This is useful for re-initializing state or re-rendering components after a code change.

    (defn ^:dev/after-load re-render []
      (swap! store identity))
  10. Use the Cherry Vite REPL with HMR

    main

    Cherry provides support for a browser-based REPL integrated with Vite's Hot Module Replacement (HMR) via WebSockets. This setup allows for live evaluation in the browser.

    Cherry's vite.js implementation uses a dialect adapter that connects to squint's makeVitePlugin. This enables a development workflow featuring:

    • A live Vite dev server
    • Browser evaluation over nREPL
    • Hot-reload hooks
  11. Run the Replicant Tic-Tac-Toe example

    main

    To run the Replicant tic-tac-toe demo locally, which uses cherry to compile replicant and is served via vite with the cherry plugin, follow these steps:

    1. Install dependencies using npm install.
    2. Start the development server with npm run dev.

    This setup includes a browser REPL for interactive development.

    npm install
    npm run dev