Using @johnhenry packages in the browser
Most @johnhenry packages are plain ES modules, so a page can load them from a CDN with no bundler and no build step. Where a package runs in a browser, the install block on its docs page has a CDN tab next to npm (and JSR, for the packages published there). Those snippets are generated from the published npm packages, pinned to the current version, and each one was loaded in headless Chromium before it appeared here. A snippet that failed that check is left out.
Which packages run in a browser
Section titled “Which packages run in a browser”- Browser-ready: every library entry point loads in a browser. Any CLI that ships with the package is still Node.
- Partial: some entry points load in a browser and others need Node. The package’s install block names both.
- Node only: the package needs Node built-ins (
node:http,node:fs,child_process) or a native addon.
65 packages: 47 browser-ready, 11 partial (some entry points only), 7 Node only. Versions as of 2026-10-07.
| Package | npm | JSR | Browser | CDN snippet | Why |
|---|---|---|---|---|---|
@johnhenry/a2a-query | 0.0.0 | — | Browser-ready | import map + esm.sh | Library entries (., ./react, ./testing) are plain ESM with no Node built-ins; @a2a-js/sdk comes in through the import map. Only the a2a-query-codegen CLI is Node. |
@johnhenry/a2a-query-tanstack | 0.0.0 | — | Browser-ready | import map + esm.sh | React hooks over a2a-query; react and @tanstack/react-query come in through the import map (react via jsDelivr's /+esm build, since it ships CommonJS). |
@johnhenry/acp-query | 0.0.2 | — | Browser-ready | import map + esm.sh | Plain ESM with no Node built-ins; @agentclientprotocol/sdk comes in through the import map. |
@johnhenry/agent-query-core | 0.1.0 | — | Browser-ready | import map + esm.sh | Plain ESM, no dependencies; ./react needs react. |
@johnhenry/mcp-gate | 0.4.0 | — | Partial | import map + esm.sh | The package has a "browser" export condition whose build carries only the policy helpers (compilePolicy, policyListFilter, redact, validateGateConfig). createGate, the proxy itself, and the mcp-gate CLI are Node. |
@johnhenry/mcp-query | 0.2.1 | — | Partial | import map + esm.sh | Every entry loads in a browser except ./transports, the stdio transport (node:process, node:stream, cross-spawn). ./otel loads from esm.sh but not raw from jsDelivr: @opentelemetry/api's ESM build uses extensionless relative imports. |
@johnhenry/mcp-query-tanstack | 0.0.0 | — | Browser-ready | import map + esm.sh | React hooks over mcp-query; the MCP client SDK, react and @tanstack/react-query come in through the import map. |
@johnhenry/aimatey-backend | 0.5.0 | — | Browser-ready | import map + esm.sh | Plain ESM, every provider subpath loads in a browser. Calling a remote provider from a page is still subject to that provider's CORS policy and puts your API key in the page. |
@johnhenry/aimatey-core | 0.6.0 | — | Browser-ready | import map + esm.sh | Plain ESM (an ESM and a CJS build); its three @johnhenry/aimatey-* dependencies come in through the import map. |
@johnhenry/aimatey-frontend | 0.4.0 | — | Browser-ready | import map + esm.sh | Plain ESM, every adapter subpath loads in a browser. |
@johnhenry/aimatey-middleware | 0.3.0 | — | Browser-ready | import map + esm.sh | Plain ESM. ./opentelemetry imports the @opentelemetry/* peers lazily, so map them only if you use it. |
@johnhenry/aimatey-middleware-andbox | 0.0.3 | — | Browser-ready | import map + esm.sh | Plain ESM, no Node built-ins. @johnhenry/andbox is a peer you add yourself. |
@johnhenry/andbox | 0.0.6 | — | Browser-ready | import map + esm.sh | Plain ESM, no dependencies, no Node built-ins. |
@johnhenry/apple-foundation-models | 0.0.0 | — | Node only | — | A Node bridge to a Swift helper built at install time: child_process, net, fs, readline. macOS only (`"os": ["darwin"]`). |
@johnhenry/browsermesh-kernel | 0.2.0 | — | Browser-ready | import map + esm.sh | Plain ESM, no dependencies. |
@johnhenry/browsermesh-netway | 0.0.1 | — | Browser-ready | import map + esm.sh | Plain ESM, no dependencies. |
@johnhenry/browsermesh-primitives | 0.2.0 | — | Browser-ready | import map + esm.sh | Plain ESM, no dependencies. |
@johnhenry/canvas-fx | 0.0.1 | — | Browser-ready | import map + esm.sh | Plain ESM custom elements with no dependencies. global.mjs registers every element. |
@johnhenry/css-signals | 0.0.0 | — | Browser-ready | import map + esm.sh | Plain ESM and CSS files, no dependencies. |
@johnhenry/data-plot | 0.0.0 | — | Browser-ready | import map + esm.sh | Plain ESM custom elements with no dependencies. global.mjs registers every element. |
@johnhenry/dialback | 0.0.5 | — | Node only | — | The root imports node:http, node:crypto and ws's server. ./browsermesh imports node:events, so it loads raw only in Node; esm.sh's events polyfill gets it to import. |
@johnhenry/domable | 0.0.1 | — | Browser-ready | import map + esm.sh | Plain ESM, no dependencies. |
@johnhenry/domkit | 0.0.4 | — | Browser-ready | — | Source ESM. These docs describe domkit 0.1.0, whose module layout differs from the 0.0.4 on npm, so no CDN snippet is generated until 0.1.0 is published. |
@johnhenry/ecmanim | 0.1.0 | — | Partial | import map + esm.sh | The root, ./browser, ./browser-three, ./authoring, ./studio and ./physics/* load in a browser. Optional renderers (three, mathjax-full, mp4-muxer, …) are imported lazily, so map them only if you use them. ./node and ./service (ffmpeg, worker_threads, http) are Node. |
@johnhenry/fileable | 0.0.4 | — | Partial | import map + esm.sh | The root renders to disk (node:fs, child_process, zlib) and is Node-only. ./browser and the JSX runtimes load in a browser. |
@johnhenry/hashish | 0.0.0 | — | Browser-ready | import map + esm.sh | Plain ESM, no dependencies. |
@johnhenry/hostable | 0.0.2 | — | Partial | import map + esm.sh | The root, the JSX runtimes and the Deno/Bun/Cloudflare adapters are Fetch-API code that loads in a browser (a service worker, say); servable's #imports resolve through an import-map scope. ./adapters/node needs node:http. |
@johnhenry/html-modules | 0.0.0 | — | Partial | import map + esm.sh | The bootstrap, runtime, compiler and safe-fragment bridge load in a browser. ./dev (a node:http dev server), ./vite and the html-module CLI are Node. |
@johnhenry/http-converter | 0.0.1 | — | Browser-ready | import map + esm.sh | Plain ESM, no dependencies. |
@johnhenry/http-fields | 0.1.0 | — | Browser-ready | import map + esm.sh | Plain ESM, no dependencies. |
@johnhenry/isomorphic-jj | 1.13.0 | — | Browser-ready | import map + esm.sh | The root and ./browser load in a browser. isomorphic-git (a peer) and its CommonJS dependencies come in through jsDelivr's /+esm builds; @isomorphic-git/lightning-fs is optional. The isojj CLI is Node. |
@johnhenry/jth | 0.0.0 | — | Node only | — | The jth CLI and compiler driver: esbuild, fs, child_process. |
@johnhenry/jth-stdlib | 0.0.0 | — | Browser-ready | import map + esm.sh | Plain ESM; jth-runtime and jth-types come in through the import map. |
@johnhenry/laya | 0.3.2 | — | Browser-ready | import map + esm.sh | The root loads in a browser; its #io import resolves to the browser build through an import-map scope. Pair it with @johnhenry/backend-webgpu (or backend-cpu), which it imports lazily. |
@johnhenry/leserve | 0.1.0 | — | Node only | — | An HTTP server on node:http/https and ws. A few helper subpaths (./body, ./compose, ./auth) import cleanly in a browser, but the package exists to run a Node server. |
@johnhenry/letterpress | 0.2.1 | — | Partial | import map + esm.sh | The router and ./rewrite are runtime-agnostic and load in a browser. ./fs, the filesystem router, needs node:fs. |
@johnhenry/iteration | 0.0.0 | 0.0.0 | Browser-ready | import map + esm.sh | Plain ESM, no dependencies. |
@johnhenry/math | 0.0.1 | 0.0.1 | Browser-ready | import map + esm.sh | Plain ESM, no dependencies. |
@johnhenry/math-plus-tensor-core | 0.2.1 | 0.2.1 | Browser-ready | import map + esm.sh | Plain ESM; @johnhenry/math-plus-special comes in through the import map. |
@johnhenry/math-prototype-patch | 0.0.1 | — | Browser-ready | import map + esm.sh | Plain ESM; @johnhenry/math comes in through the import map. |
@johnhenry/mport | 0.0.0 | — | Partial | import map + esm.sh | The router (., ./core, ./firefox) is plain ESM with no dependencies. ./node, ./vite, ./rollup and the CLI read and write files. |
@johnhenry/oat-bootstrap | 0.1.1 | — | Browser-ready | import map + esm.sh | Plain ESM. |
@johnhenry/oat-protocol | 0.1.1 | — | Browser-ready | import map + esm.sh | Plain ESM; cbor-x and @noble/* come in through the import map. |
@johnhenry/oat-receiver | 0.1.1 | — | Browser-ready | import map + esm.sh | Plain ESM; jsqr (CommonJS) comes in through jsDelivr's /+esm build. |
@johnhenry/oat-sender | 0.1.1 | — | Browser-ready | import map + esm.sh | Plain ESM; qrcode (CommonJS) comes in through jsDelivr's /+esm build. |
@johnhenry/oat-sim | 0.1.1 | — | Browser-ready | import map + esm.sh | Plain ESM. |
@johnhenry/objectify | 0.0.1 | — | Node only | — | A CLI on better-sqlite3 (a native addon), fs and child_process. |
@johnhenry/packfile | 0.0.1 | — | Partial | import map + esm.sh | ./browser and ./blob-preview load in a browser. The root and ./compat, ./cache, ./hash, ./compression, ./web-bundle use node:fs, node:crypto or zlib. |
@johnhenry/raijin-consensus | 0.2.0 | — | Browser-ready | import map + esm.sh | Plain ESM. |
@johnhenry/raijin-core | 0.2.0 | — | Browser-ready | import map + esm.sh | Plain ESM, no dependencies. |
@johnhenry/raijin-da | 0.0.4 | — | Browser-ready | import map + esm.sh | Plain ESM. viem (peer) and fflate (optional) are imported only by the features that need them. |
@johnhenry/raijin-mempool | 0.1.1 | — | Browser-ready | import map + esm.sh | Plain ESM. |
@johnhenry/raijin-sdk | 0.1.0 | — | Browser-ready | import map + esm.sh | Plain ESM. |
@johnhenry/raijin-validator | 0.2.0 | — | Browser-ready | import map + esm.sh | Plain ESM. |
@johnhenry/safe-fragment | 0.0.1 | — | Browser-ready | import map + esm.sh | Plain ESM. dompurify is imported lazily (only without Element.setHTML), and a raw CDN cannot resolve that bare import, so the import map includes it. |
@johnhenry/semantic-chunker | 0.0.2 | — | Partial | import map + esm.sh | The chunker and ./embed/xenova load in a browser. ./embed/ollama imports node:fs. |
@johnhenry/servable | 0.0.3 | — | Partial | import map + esm.sh | The root (it has a "browser" export condition), the JSX runtimes and the Deno/Bun/Cloudflare adapters load in a browser; its #imports resolve through an import-map scope. ./adapters/node needs node:http. |
@johnhenry/servant | 0.3.0 | — | Node only | — | Controls a Node HTTP server: node:http, node:https, node:stream, ws. |
@johnhenry/signalle | 0.1.2 | — | Browser-ready | import map + esm.sh | Plain ESM, no dependencies. |
@johnhenry/spintax | 0.0.1 | — | Browser-ready | import map + esm.sh | Plain ESM, no dependencies. |
@johnhenry/temporals | 0.0.3 | — | Browser-ready | import map + esm.sh | Plain ESM; temporal-polyfill (a peer) comes in through the import map. |
@johnhenry/tester | 0.0.1 | — | Browser-ready | import map + esm.sh | Plain ESM, no dependencies. |
@johnhenry/webwire | 0.0.1 | — | Node only | — | Converts between Node's http objects and Fetch API Request/Response. Some subpaths import in a browser, but they only make sense with Node objects. |
@johnhenry/window-algebra | 0.0.0 | — | Browser-ready | import map + esm.sh | Plain ESM, no dependencies; every entry point loads in a browser. |
@johnhenry/wsh | 0.23.0 | — | Browser-ready | import map + esm.sh | Plain ESM, no dependencies; @noble/post-quantum is an optional, lazily imported extra. |
Each classification comes from reading the package’s exports the way a browser bundler does (the browser,
import and default conditions), following its module graph for node: and other built-in imports, and then
loading the generated snippet in Chromium.
A clean import from esm.sh is not proof of browser support. esm.sh replaces Node built-ins with browser
polyfills or stubs. Several Node-only entry points (fileable’s root, packfile’s root, webwire, objectify, jth,
mport’s ./node) import from esm.sh without an error and then fail when you call them. It can go the other way
too: @johnhenry/mcp-query/otel loads from esm.sh but not as raw files, because @opentelemetry/api’s ESM build
uses extensionless relative imports.
Three ways to load a package
Section titled “Three ways to load a package”An import map and raw files (jsDelivr or unpkg). These CDNs serve npm’s files byte for byte. They do not rewrite
a bare import "dompurify" inside a package, so the page’s import map has to name every bare specifier the modules
reach, dependencies included. safe-fragment is a typical case: it imports dompurify lazily, and only on browsers
without Element.setHTML (Safari today, per its docs). A map that leaves
dompurify out appears to work wherever setHTML exists and fails where it doesn’t.
<!-- In <head>, before any <script type="module"> or modulepreload. --><script type="importmap"> { "imports": { "@johnhenry/safe-fragment": "https://cdn.jsdelivr.net/npm/@johnhenry/[email protected]/dist/index.js", "dompurify": "https://cdn.jsdelivr.net/npm/[email protected]/dist/purify.es.mjs" } }</script><script type="module"> import * as safeFragment from "@johnhenry/safe-fragment";</script>Two more details apply to raw files:
- A package’s internal
#imports(servable, hostable, laya) are private to that package, so they go in the map’sscopes, keyed by the package’s CDN URL. The generated snippets include them. - A dependency that ships only CommonJS, such as react, qrcode or jsqr, cannot load raw. The snippets point it at
jsDelivr’s
/+esmbuild of the same version, at the same URL jsDelivr’s own bundles use (/npm/react@<version>/+esm). Two spellings of one module would load two copies, which breaks React.
esm.sh. A single URL, no map. esm.sh rewrites every import to another esm.sh URL and picks dependency versions from the package’s own ranges:
Its output is a build, not npm’s bytes. It is generated per request (target, polyfills), which means you cannot pin it with SRI, and two packages share a dependency only when esm.sh resolves both to the same URL.
jsDelivr’s /+esm. https://cdn.jsdelivr.net/npm/<name>@<version>/+esm is jsDelivr’s own bundle of a package,
with its imports rewritten to other /+esm URLs. Like esm.sh it is generated, so it does not take SRI either.
Pin exact versions
Section titled “Pin exact versions”Write @johnhenry/[email protected], not @0, @^0.0.0 or @latest. A CDN resolves a range to whatever is
newest when its cache expires, so the code on your page changes without a deploy. SRI needs a fixed file anyway.
Several of these packages are at 0.0.x, and before 1.0 a caret range means little: ^0.0.0 matches only
0.0.0, while ^0.2.0 accepts any 0.2.x.
The snippets on this site are pinned to npm’s latest on 2026-10-07. The version source is
src/data/package-versions.json: npm run sync:versions refreshes it from npm and JSR, and
npm run verify:cdn loads the snippets in a browser again.
Put the import map before any module script
Section titled “Put the import map before any module script”Place <script type="importmap"> in <head>, ahead of every <script type="module"> and every
<link rel="modulepreload">. Firefox ignores an import map that arrives after a module load or preload has
started. It logs “Import maps are not allowed after a module load or preload has started”, and every bare import on
the page then fails. In our test, Chromium 143 accepted a map after a module script and after a modulepreload,
but the page then works in Chromium and breaks in Firefox. mport’s
limitations page records the per-engine results.
Subresource integrity
Section titled “Subresource integrity”A raw file at an exact version never changes, so you can pin it. An import map’s integrity key takes a hash for
each module URL. Unlike <script integrity>, which covers only that one script, it applies to every module the page
loads from a listed URL, including files reached through relative imports. Here is the safe-fragment map with every
file pinned. Chromium blocked the module when we changed one character of a hash.
<!-- In <head>, before any <script type="module"> or modulepreload. --><script type="importmap"> { "imports": { "@johnhenry/safe-fragment": "https://cdn.jsdelivr.net/npm/@johnhenry/[email protected]/dist/index.js", "dompurify": "https://cdn.jsdelivr.net/npm/[email protected]/dist/purify.es.mjs" }, "integrity": { "https://cdn.jsdelivr.net/npm/@johnhenry/[email protected]/dist/index.js": "sha384-Wdbb2pEsDBQuAiBEvhTrQQYJnCKK60Ow2saI8VrCdb17+IVQq4N59qexggWvH7en", "https://cdn.jsdelivr.net/npm/[email protected]/dist/purify.es.mjs": "sha384-a/XFwrpeq88LPRbyWBWlLnv0sUPkA6mAADSFODl/XWtg+PHClcleY9DmdjDED9+Z" } }</script><script type="module"> import * as safeFragment from "@johnhenry/safe-fragment";</script>To hash a file yourself:
| openssl dgst -sha384 -binary | openssl base64 -AIntegrity only works for raw files. esm.sh output and jsDelivr’s /+esm are generated, and jsDelivr asks you not to
use SRI with them. Browsers that don’t implement import-map integrity ignore the key, so it adds protection where
it is supported and changes nothing elsewhere.
Content Security Policy
Section titled “Content Security Policy”An inline <script type="importmap"> counts as an inline script. Under a script-src without 'unsafe-inline'
the browser blocks it unless it carries a nonce or the policy lists the hash of its exact text. Every bare import
then fails. script-src must also allow the CDN origins the modules load from:
Content-Security-Policy: script-src 'nonce-RANDOM' https://cdn.jsdelivr.net https://esm.sh<script type="importmap" nonce="RANDOM">{ "imports": { "…": "…" } }</script>A static site cannot send a fresh nonce on each response, so it lists the map’s hash instead
('sha256-…', computed over the exact bytes between the tags). Any change to the map, including whitespace,
changes the hash. In Chromium, a map without a nonce under that policy was blocked, and the same map with a nonce
loaded.
Generating maps with mport
Section titled “Generating maps with mport”mport is the family’s tool for building import maps. It resolves version ranges to exact versions with
a lockfile, routes each package to a CDN with fallback across providers (esm.sh, jsDelivr, unpkg and others), can
hash the whole module graph into the map’s integrity, and returns the CSP hash for the map it renders. See
Import maps, lockfiles and the CLI and In the browser.
The snippets on this site come from a smaller script in this repository (scripts/sync-package-versions.mjs), not
from mport.