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.
- Seamless Integration: Automatically starts and manages the
shadow-cljsprocess. - 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.
npm install -D shadow-cljs-vite-plugin
# or
pnpm add -D shadow-cljs-vite-pluginAdd 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:
- examples/cljs-react/ — CLJS business logic + React UI with auto HMR
- examples/cljs-ts-mixed/ — CLJS + TypeScript mixed project with HMR
- examples/cljs-reagent/ — Pure Reagent (ClojureScript) app with HMR
- examples/cljs-cloudflare-worker/ — CLJS shared between browser + Cloudflare Worker (SSR)
- tests/e2e/fixtures/simple-project/ — E2E test fixture (Cloudflare Workers)
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.
- Type:
string[] - Description: The list of build IDs defined in your
shadow-cljs.ednthat you want Vite to handle.
- Type:
string - Default:
shadow-cljs.ednin the project root. - Description: The path to your shadow-cljs configuration file.
In dev mode, shadow-cljs handles hot-reloading of ClojureScript code via its own WebSocket + eval() mechanism. The plugin integrates with this by:
- Suppressing Vite's default HMR for shadow-cljs output files (re-importing the CLJS module tree would break the stateful ClojureScript runtime).
- Auto-refreshing ES module live bindings when shadow-cljs hot-reloads code, so consumers always see fresh values.
- Triggering React Fast Refresh (or custom re-render) automatically — no manual event listeners needed.
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/.
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());
}Use shadow-cljs's standard ^:dev/after-load hook to re-render:
(defn ^:dev/after-load on-reload []
(render)) ;; re-mount your root componentState in defonce atoms is preserved across reloads. See examples/cljs-reagent/.
- Dev Server: When you run
vite, this plugin spawnsshadow-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. - Production Build: When you run
vite build, it spawnsshadow-cljs release <build-id>to generate the optimized assets, which Vite then bundles.
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 testIf you are using this project, feel free to submit a PR to add it here.
Issues discovered during development of this plugin that led to upstream fixes:
- shadow-cljs — thheller/shadow-cljs#1249: Fix ES module import compatibility (merged in v3.3.5)
- Vite — vitejs/vite#22098:
import.meta.hot.invalidate()silently fails for virtual modules due to URL mismatch between client (/@id/__x00__...) and server (mod.url)
Please see CONTRIBUTING.md for details on the code structure and how to submit changes.
MIT