Skip to content

bolasblack/shadow-cljs-vite-plugin

Repository files navigation

shadow-cljs-vite-plugin

Tests

A robust Vite plugin for seamless integration with shadow-cljs.

This plugin bridges the gap between the shadow-cljs build tool and the Vite dev server, allowing you to use Vite's lightning-fast HMR and rich ecosystem while developing ClojureScript applications.

Features

  • Seamless Integration: Automatically starts and manages the shadow-cljs process.
  • Hot Module Replacement (HMR): Correctly delegates HMR to shadow-cljs (for the browser runtime) for a smooth REPL-driven workflow.
  • Cloudflare Workers Ready: Fully tested and works seamlessly with @cloudflare/vite-plugin. Includes specialized logic to handle Google Closure Library namespaces in ESM environments.
  • Zero Configuration: Works out of the box for most standard shadow-cljs setups.

Installation

npm install -D shadow-cljs-vite-plugin
# or
pnpm add -D shadow-cljs-vite-plugin

Usage

Add the plugin to your vite.config.ts (or vite.config.js).

import { defineConfig } from "vite";
import { shadowCljs } from "shadow-cljs-vite-plugin";

export default defineConfig({
  plugins: [
    shadowCljs({
      buildIds: ["app"], // The build ID(s) from your shadow-cljs.edn
      configPath: "./shadow-cljs.edn", // Optional: Path to config
    }),
  ],
});

Then, import the virtual module in your entry HTML or JavaScript file (e.g., main.tsx or index.html):

<!-- index.html -->
<script type="module">
  import "virtual:shadow-cljs/app"; // Matches the build ID provided in config
</script>

Working examples:

Shadow-CLJS Configuration Requirements

To ensure correct integration with Vite's ES module system and avoid runtime errors, your shadow-cljs.edn build configuration MUST use the following settings:

{:target :esm
 :js-options {:js-provider :import}}
  • :target :esm: Tells shadow-cljs to output standard ES modules.
  • :js-options {:js-provider :import}: Ensures that dependencies are imported using native ESM syntax.

Configuration

buildIds (Required)

  • Type: string[]
  • Description: The list of build IDs defined in your shadow-cljs.edn that you want Vite to handle.

configPath (Optional)

  • Type: string
  • Default: shadow-cljs.edn in the project root.
  • Description: The path to your shadow-cljs configuration file.

Hot Module Replacement (HMR)

In dev mode, shadow-cljs handles hot-reloading of ClojureScript code via its own WebSocket + eval() mechanism. The plugin integrates with this by:

  1. Suppressing Vite's default HMR for shadow-cljs output files (re-importing the CLJS module tree would break the stateful ClojureScript runtime).
  2. Auto-refreshing ES module live bindings when shadow-cljs hot-reloads code, so consumers always see fresh values.
  3. Triggering React Fast Refresh (or custom re-render) automatically — no manual event listeners needed.

React (recommended)

With @vitejs/plugin-react, HMR works automatically. Just import and use:

import { greet, add } from "virtual:shadow-cljs/app";

export default function App() {
  return <p>{greet("World")}{add(1, 2)}</p>;
}

Edit your .cljs files and the React component re-renders with fresh values. No useEffect, no event listeners. See examples/cljs-react/.

Vanilla TypeScript/JavaScript

For non-React projects, use import.meta.hot.accept() to re-render:

import { greet, add } from "virtual:shadow-cljs/app";

function render() {
  document.getElementById("app")!.innerHTML = `${greet("World")} ${add(1, 2)}`;
}

render();

if (import.meta.hot) {
  import.meta.hot.accept(() => render());
}

See examples/cljs-ts-mixed/.

Pure ClojureScript (Reagent, etc.)

Use shadow-cljs's standard ^:dev/after-load hook to re-render:

(defn ^:dev/after-load on-reload []
  (render)) ;; re-mount your root component

State in defonce atoms is preserved across reloads. See examples/cljs-reagent/.

How it Works

  1. Dev Server: When you run vite, this plugin spawns shadow-cljs watch <build-id>. Shadow-cljs handles file watching, recompilation, and hot-reload via its own WebSocket. The plugin suppresses Vite's default HMR for shadow-cljs output files, detects eval completion by polling the global namespace, and triggers React Fast Refresh for importers.
  2. Production Build: When you run vite build, it spawns shadow-cljs release <build-id> to generate the optimized assets, which Vite then bundles.

Tests

This project includes a comprehensive test suite, including End-to-End (E2E) tests that simulate real-world build scenarios (including integration with Cloudflare Workers) to ensure reliability.

To run the tests locally:

pnpm test

Projects using this plugin

If you are using this project, feel free to submit a PR to add it here.

Upstream Contributions

Issues discovered during development of this plugin that led to upstream fixes:

  • shadow-cljsthheller/shadow-cljs#1249: Fix ES module import compatibility (merged in v3.3.5)
  • Vitevitejs/vite#22098: import.meta.hot.invalidate() silently fails for virtual modules due to URL mismatch between client (/@id/__x00__...) and server (mod.url)

Contributing

Please see CONTRIBUTING.md for details on the code structure and how to submit changes.

License

MIT

About

A robust Vite plugin for seamless integration with shadow-cljs

Resources

License

Contributing

Stars

Watchers

Forks

Packages