Examples
Runnable, self-verifying examples. Each numbered example asserts the behaviour it
demonstrates and exits 0 on success, so npm run examples doubles as a smoke test (CI
runs it). The numbered examples run offline: the npm and JSR registries and the CDNs are
a fake fetch (_offline.mjs) with registry metadata shaped like the real endpoints,
and import() is a fake importer where one is needed. The routing, resolution,
lockfile, health and integrity logic they exercise is exactly what runs against the real
network. The browser pages under the second table talk to the real CDNs.
| Example | Demonstrates |
|---|---|
01-ranges-resolve-once-to-exact-versions.mjs |
react@^19 becomes exactly 19.2.0 through one registry lookup, visible in the trace as lookup → resolved; an exact version skips the lookup; dist-tags and JSR packages (ignoring yanked versions) resolve too; an unsatisfiable range rejects once with ResolutionError and no CDN is blamed. |
02-fallback-skips-a-cdn-that-is-down.mjs |
An array route is fallback(): a CDN answering 503 is a recorded failure, a provider without npm support is skipped without a request, and the next mirror serves the package with its entry file from exports. When every provider fails the rejection is a RoutingError carrying one error per provider and the trace. |
03-race-first-success-wins-losers-are-not-blamed.mjs |
race() takes the first success, so a fast failure does not sink it; slower probes are aborted and get no health entry. With probe: "import" a loser that finishes after the race was decided is traced as aborted with reason "lost the race", never ok. |
04-commonjs-is-skipped-on-raw-cdns.mjs |
React’s CommonJS entry is skipped on jsDelivr and unpkg with a reason, and esm.sh serves it; Preact’s sub-path hooks is mapped through its exports to an .mjs file; allowCommonJS: true turns the check off; entryInfo() applies the detection rules. |
05-lockfile-pins-version-and-build.mjs |
A lockfile brings back the same version and entry with no registry request, and only mirrors of the locked build may serve it: esm.sh and jspm are skipped as locked to "npm" and jsDelivr stands in for unpkg. relock: true ignores the lock for one call. |
06-circuit-breaker-opens-and-resets.mjs |
After two failures esm.sh’s circuit opens and it is skipped without a request; after reset it closes, but its failure streak survives so one more failure reopens it; routers can share a HealthRegistry; snapshot()’s shape. |
07-verified-rejects-a-tampered-mirror.mjs |
verified() rejects a mirror whose bytes don’t match the expected SRI hash even though its availability probe passed, and fails over; without an expected hash it records whatever it downloaded (trust on first use) into the import map’s integrity and the lockfile. |
08-router-import-fails-over-at-runtime.mjs |
router.import() (and createImporter()) excludes a provider whose module fails to import and resolves again, switching from esm.sh to jsDelivr’s +esm build; with build: "esm.sh" or a lockfile pin it rejects with a RoutingError instead; unroutable specifiers are imported as they are. |
09-build-compiles-an-import-map-and-lockfile.mjs |
router.build() compiles exact, sub-path, prefix (lit/, which raw jsDelivr skips because Lit has an exports map), registry-prefixed and scoped entries into one import map, keys the lockfile by the specifier as written, and rejects an unroutable specifier with ResolutionError instead of dropping it. |
10-cli-builds-offline-from-a-config.mjs |
The mport CLI builds importmap.json and mport.lock.json from mport.config.mjs, mport resolve honours a hand-edited lockfile pin (no lookup in the trace), --relock ignores it, and an unknown command exits 1 with the message on stderr. |
11-v1-api-races-origins-on-the-router.mjs |
The 1.x MPortURL() race takes the first import that succeeds (1.x rejected on the first failure), returns [module, url, info] with a trace, parses scoped names, and honours origin arguments. Uses the internal createV1() factory with a fake importer, because Node cannot import() https URLs. |
12-server-renders-an-import-map-with-preloads.mjs |
Server-side HTML from a build: renderImportMap() and renderModulePreload() (with each module’s integrity; prefix mappings are not preloaded), and build() throwing on two versions of one key until the second gets its own scope. |
13-conflicting-versions-get-scopes.mjs |
build() throws when two specifiers want one key at different URLs; with conflicts: "scope" the first listed version keeps imports and a dependent package whose manifest asks for the other version gets a scope keyed by its own directory, with a report of what was scoped and what nothing reaches. |
14-whole-graph-integrity-in-the-lockfile.mjs |
build({ graph: true }) follows an esm.sh-style stub into the files it re-exports, hashes every one into the import map’s integrity and the lockfile’s files, refuses a later build when one file’s bytes changed, and reports a walk cut short by maxFiles as a truncated event. |
15-outdated-and-update-keep-the-lockfile-current.mjs |
outdated() (what mport outdated prints) reports current, wanted (newest the range allows) and latest, skipping what it can’t judge; the mport update CLI re-resolves only the named entry, rewrites the lockfile, and --json reports what moved. |
16-rollup-plugin-routes-bare-imports.mjs |
A real Rollup build with @johnhenry/mport/rollup: external mode turns import "react" into the CDN URL; importmap mode keeps it bare and emits the import map. Needs the rollup dev dependency (installed by npm ci). |
17-local-serves-a-package-that-is-not-on-npm.mjs |
local() alone 404s on a package installed from git (the registry has never heard of it); with registry: installedRegistry({ root }) from @johnhenry/mport/node the version and entry come from the installed package.json, the lockfile records the installed version, nothing touches the network, and a range the installed copy does not satisfy is a ResolutionError. |
18-csp-hash-for-a-static-sites-import-map.mjs |
A static site can’t use a CSP nonce, so the inline import map is allowed by hash: renderImportMapCsp() returns the <script> HTML and its 'sha256-…' from the same string, equal byte for byte to node:crypto’s hash of the text between the tags (and to what real browsers compute under a strict policy, in test/browser/csp.spec.mjs); importMapHash() alone, other algorithms. |
19-dependencies-of-a-raw-cdn-package-join-the-map.mjs |
build(specs, { dependencies: true }) reads each raw-CDN package’s manifest and routes its dependencies too (safe-fragment → dompurify → trusted-types), reporting what it added, what it skipped (a file: range) and what dependencyDepth cut off; the added entries are locked; esm.sh, which rewrites imports itself, is not expanded. |
20-app-owned-prefix-needs-no-registry.mjs |
The app-owned prefix recipe: "components/*": custom("/components/{path}", { name: "app", build: "app" }) maps your own directory (files and the components/ prefix) with no registry lookup, beside CDN packages; the lockfile records build app. |
Browser pages, demos and support files (not part of npm run examples):
| Example | Demonstrates |
|---|---|
playground.html |
Learn. Ten one-click scenarios against the real CDNs and registries, with outages, latency, broken imports and tampered bytes simulated through fetch/importer wrappers: routing by package name into an import map and lockfile, a CDN outage (and why React can’t fall back to a raw CDN), retrying at import time, a race, the circuit breaker, lockfile pinning to a different mirror, choosing by capability, a tampered mirror rejected by verified(), and the 1.x API. Each scenario states what to notice and checks that it happened. |
app.html |
Use it. A Preact + htm app (app-main.mjs, app-view.mjs) loaded either through an injected import map or through router.import(), with esm.sh up, down, or answering but failing to import. Proves an import map cannot fall back at runtime and router.import() can. |
compat.html |
Check it. The 1.x API run through both @johnhenry/mport and @johnhenry/mport/firefox against the real CDNs as a pass/fail table, including a scan proving the Firefox entry never uses two-argument import(). |
index.html |
Redirects to the playground. |
demo.mjs, demo.firefox.mjs, demo-run.mjs |
The 1.x calls against the real CDNs under Deno (npm run demo, npm run demo:firefox), printing results. demo-run.mjs holds the shared calls and imports nothing from mport, so the Firefox demo never loads the standard entry point. |
ui.mjs, shared.css |
Shared header, timeline, verdicts and styles for the three pages. |
_offline.mjs |
The fake network and registry fixtures the numbered examples share. |
Running
Section titled “Running”npm run examples # run all numbered examples in sequencenpm run example:05 # run onenode examples/05-lockfile-pins-version-and-build.mjs
npm run demo:html # serve the repo, then open http://localhost:8712/examples/npm run demo # 1.x calls against the real CDNs (Deno)Browser tests and benchmarks
Section titled “Browser tests and benchmarks”npm run test:browser runs Playwright tests on Chromium, Firefox and WebKit (npx playwright install --with-deps once; CI does this on all three, one job per engine). Pages are served from the repo by a small static server Playwright starts (port 8731, MPORT_TEST_PORT to change it), and every CDN and registry request is answered by page.route stubs, so the suite makes no network requests. It covers the playground (all ten scenarios must pass their own checks), app.html in both modes with esm.sh up, down and broken, compat.html, a strict Content-Security-Policy enforced against the import-map hash, the import map placed before modulepreload, import-map scopes and integrity as each engine enforces them, and the Firefox late-import-map behaviour described under Limitations.
npm run bench (non-gating, also a continue-on-error CI job) measures mport’s own work against a fake fetch, so it shows overhead, not network speed. On an Apple M-series laptop (Node 24):
| cold build, 50 specifiers, esm.sh route, HEAD probe | 0.8 ms |
| cold build, 50 specifiers, jsDelivr raw (entry lookup + probe) | 2.8 ms |
cold build, 50 specifiers, probe: "none" |
0.5 ms |
cold build with graph: true (4 files per module, 200 files hashed and parsed) |
5.2 ms |
| the same cold build with 20 ms per request | 42.5 ms (concurrent: about two round trips, not 100) |
resolve(), pinned by a lockfile, probe: "none" |
~283,000 / s |
resolve(), warm registry memo, probe: "none" |
~165,000 / s |
parseImports() |
~42 MB/s |
A real build is dominated by round trips; these numbers say mport adds well under a millisecond per specifier. Your machine will differ: run it.
Runtime requirements (honest edition)
Section titled “Runtime requirements (honest edition)”The numbered examples run under plain Node >= 26 with no network: they import the
package by its own name (@johnhenry/mport, a self-reference through package.json
exports) and pass a fake fetch, which every router option accepts. Example 10 links
the repo into a temporary directory’s node_modules so its config can import the package
the way an installed one would.
What they deliberately do not cover, and why: Node cannot import() an https: URL, so
probe: "import", router.import() and the v1 functions are shown with a fake
importer (examples 03, 08 and 11) rather than real CDN modules. startup() and
injectImportMap() need a document; app.html exercises them in a real browser, and
test/importmap.test.mjs covers injectImportMap() headlessly. The browser pages and the
Deno demos need the real network and are excluded from npm run examples; that is
intentional, not an oversight.