Resilient CDN loading: shared loadWithFallback engine + per-package fallback + robust use-m/load bootstrap (#58)#59
Conversation
Adding .gitkeep for PR creation (default mode). This file will be removed when the task is complete. Issue: #58
The recommended bootstrap
const { use } = eval(await (await fetch('https://unpkg.com/use-m/use.js')).text());
crashes with a cryptic 'SyntaxError: Unexpected identifier' when a CDN
returns an error body (e.g. 'Internal Server Error') instead of the module
source, because eval() parses that text as JavaScript.
loadUseM() validates the HTTP status and response body before eval(),
retries each source, falls back across unpkg -> jsDelivr -> esm.sh, and on
total failure throws a clear, actionable error listing every attempt.
Refs #58
tests/load.test.{mjs,cjs} run under Jest, Bun and Deno via the test
adapter. They cover the looksLikeUseModule heuristic, mirror fallback,
per-source retries, the per-attempt timeout, and — the core regression for
issue #58 — that an error body surfaces a clear aggregated error and never
a SyntaxError. The real shipped use.js is also eval'd into a working module.
experiments/ reproduce the cryptic SyntaxError and verify the loader's
behavior across ESM and CJS.
Refs #58
Adds load.mjs and load.cjs to the published files, an exports entry mapping use-m/load to the ESM/CJS builds, and bumps the minor version so the next release ships the new robust loader. Refs #58
Adds a 'Robust loading' section to the README (packaged use-m/load helper plus a dependency-free self-contained snippet), a pointer from the interactive-shell one-liner, runnable examples/load demos, and a changelog entry. Removes the placeholder .gitkeep now that the task is complete. Refs #58
Working session summaryDone. Issue #58 is fully resolved and PR #59 is finalized and ready for review. SummaryPR #59: #59 — "Add robust CDN loader (use-m/load) to fix cryptic SyntaxError on CDN errors (#58)" — now ready for review (no longer draft), base What the fix doesThe recommended bootstrap
Delivered (4 atomic commits, +1023 lines)
Verification
The PR closes #58 on merge. This summary was automatically extracted from the AI working session output. |
🤖 Solution Draft LogThis log file contains the complete execution trace of the AI solution draft process. 💰 Cost estimation:
📊 Context and tokens usage:Claude Opus 4.8: (3 sub-sessions)
Total: (29.8K new + 325.1K cache writes + 8.0M cache reads) input tokens, 129.7K output tokens, $9.433901 cost 🤖 Models used:
📎 Log file uploaded as Gist (3482KB)Now working session is ended, feel free to review and add any feedback on the solution draft. |
✅ Ready to mergeThis pull request is now ready to be merged:
Monitored by hive-mind with --auto-restart-until-mergeable flag |
|
Why loader was not changed? We cannot at least reuse the code? Should we do the same loading for each package inside use-m? May be it should be more generic fetch with retry? I think we should use it across the codebase in all relevant places. |
|
🤖 AI Work Session Started Starting automated work session at 2026-06-14T21:15:25.861Z The PR has been converted to draft mode while work is in progress. This comment marks the beginning of an AI work session. Please wait for the session to finish, and provide your feedback. |
Add a shared, mechanism-agnostic loadWithFallback(sources, load, options) engine to the use-m core: try each source in order, optionally retry with linear backoff, and fail with ONE aggregated error listing every attempt instead of the cryptic error of a single failing source (issue #58). Wire it into makeUse so network package loads are resilient: in the browser, in Deno, and from http(s) entry points, use() now falls back across distinct CDN hosts (esm.sh -> jspm.dev -> cdn.skypack.dev; Deno tries its esm.sh target first) rather than depending on one host. Local npm/bun loading and any single explicit resolver keep their previous single-shot behavior and raw errors (a one-entry chain bypasses the engine). New makeUse options: specifierResolvers (ordered chain) and import (injectable low-level importer). New exports: loadWithFallback, networkResolverChain, denoResolverChain. Synced across use.mjs, use.cjs, and the self-contained use.js eval build.
loadUseM previously carried its own retry/fallback loop. Delegate it to the shared loadWithFallback engine so the bootstrap and the rest of the codebase use one mechanism. The loader-specific work (fetch -> validate body -> eval -> check the use export) stays here; only the retry/fallback loop is shared. No behavior change: the aggregated error message is byte-for-byte identical (same label and transient-outage hint), and all existing load tests pass unchanged. Addresses the maintainer's request to reuse the loader code across the codebase instead of duplicating it.
Cover the shared engine (first-source success, fallback to a later source, retry-then-succeed, aggregated error listing every attempt, empty-sources and missing-load guards, custom describeSource/label/hint) and makeUse per-package CDN fallback offline via the new specifierResolvers + import DI seams (mirror fallback, aggregated all-mirrors-fail error, single explicit resolver imports directly with its raw error, single-entry chain bypasses the engine). Dual .mjs/.cjs files per repo convention; pass under Node (Jest), Bun, and Deno. Includes an experiments/ smoke test for the eval'd use.js build.
Add a README "Resilient package loading (shared fallback engine)" section covering the built-in per-package CDN fallback (with a mirror-chain table and the aggregated-error format) and the exported loadWithFallback engine, plus a runnable examples/load/shared-fallback-engine.mjs demonstration. Update the CHANGELOG and sync package-lock.json to 8.14.0.
|
Good call — I reworked this to do exactly that. Point-by-point: "Why was the loader not changed? Can we reuse the code?" // exported from use-m
loadWithFallback(sources, load, options)
// "try each source in order, optionally retry with linear backoff,
// and fail with ONE aggregated error listing every attempt"
"Should we do the same loading for each package inside use-m? Maybe a more generic fetch with retry?"
So if a package 502s on one CDN, "Use it across the codebase in all relevant places." New public surface: Tests & verification: 22 new cross-runtime tests in The PR title/description are updated to reflect the expanded scope. Marking ready for review. |
🤖 Solution Draft LogThis log file contains the complete execution trace of the AI solution draft process. 💰 Cost estimation:
📊 Context and tokens usage:Claude Opus 4.8: (4 sub-sessions)
Total: (40.2K new + 421.1K cache writes + 12.2M cache reads) input tokens, 161.9K output tokens, $12.975264 cost 🤖 Models used:
📎 Log file uploaded as Gist (5389KB)Now working session is ended, feel free to review and add any feedback on the solution draft. |
✅ Ready to mergeThis pull request is now ready to be merged:
Monitored by hive-mind with --auto-restart-until-mergeable flag |
|
Put all js, mjs, cjs files at root of repository into src folder, and make sure we fully check them all. Why loader.js is not changed? I think you ignoring our codebase again. |
|
🤖 AI Work Session Started Starting automated work session at 2026-06-15T06:53:10.935Z The PR has been converted to draft mode while work is in progress. This comment marks the beginning of an AI work session. Please wait for the session to finish, and provide your feedback. |
… loadWithFallback This addresses two requests on PR #59: 1. "Put all js, mjs, cjs files at root of repository into src folder." Moved use.{mjs,cjs,js}, load.{mjs,cjs}, loader.js, cli.mjs and test-adapter.{mjs,cjs} into src/. The package "exports" map, "main", "bin", and "files" fields were updated so existing imports (use-m, use-m/load) keep working unchanged; only direct CDN URLs gain a src/ segment (e.g. https://unpkg.com/use-m/src/use.js). Updated every parent-relative import across tests/, examples/, experiments/, docs/, bun-issues/ (66 files via per-pattern sed), plus path-pinned tests: - tests/filename.test.{mjs,cjs}: './package.json' -> '../package.json' and matching expected values, because use.{mjs,cjs} now live one level deeper. - tests/script-sync.test.mjs: file list now src/use.{mjs,cjs,js}. - tests/load.test.{mjs,cjs}: read 'src/use.js'. - experiments/verify-cjs.cjs: join(__dirname,'..','src','use.js'). README.md and CONTRIBUTING.md updated to show the new src/ paths. 2. "Why loader.js is not changed? We cannot at least reuse the code?" Refactored src/loader.js so the "try the default resolver, then the use-m npm resolver" handshake is expressed as a two-source fallback chain via the shared loadWithFallback engine. loadWithFallback is now the single retry/fallback mechanism used at all three call sites: per-package CDN loading (src/use.{mjs,cjs}), the use-m/load bootstrap (src/load.{mjs,cjs}), and the Node --loader hook (src/loader.js). Added tests/loader.test.mjs covering: default-resolver success, fallback to the npm resolver, and aggregated-error message when both fail. network-imports example URL pinned to [email protected]/use.mjs until the next release publishes the new src/ layout (TODO comment in the example flags the post-release switch back to use-m/src/use.mjs). Verified: 316/316 jest tests pass, 316/316 bun tests pass, 30 deno test files (159 steps) pass.
Working session summaryBoth review asks on PR #59 are now addressed end-to-end:
Test results: 316/316 Jest, 316/316 Bun, 30 files / 159 steps Deno — all green. Pushed as commit This summary was automatically extracted from the AI working session output. |
🤖 Solution Draft LogThis log file contains the complete execution trace of the AI solution draft process. 💰 Cost estimation:
📊 Context and tokens usage:Claude Opus 4.7: (3 sub-sessions)
Total: (6.7K new + 326.9K cache writes + 8.2M cache reads) input tokens, 72.1K output tokens, $7.998757 cost 🤖 Models used:
📎 Log file uploaded as Gist (3442KB)Now working session is ended, feel free to review and add any feedback on the solution draft. |
✅ Ready to mergeThis pull request is now ready to be merged:
Monitored by hive-mind with --auto-restart-until-mergeable flag |
Problem
The bootstrap snippet recommended for loading
use-m:crashes with a cryptic, misleading error whenever the CDN hiccups. When unpkg
(or its upstream) returns an error body — e.g. the plain text
Internal Server Error,Bad Gateway, or an HTML error page — instead of the module source,eval()parsesthat text as JavaScript and throws:
eval('Internal Server Error')→Internalis parsed as an identifier, thenServeris "unexpected". The stack trace points at the
evalline with no hint that the realproblem is a transient network/CDN failure. This was hit downstream in
gh-pull-all.Fix — one generic mechanism, reused at three call sites
Rather than a one-off bootstrap loader, this PR extracts a single generic engine and
reuses it everywhere
use-mperforms a "try then fall back" load:loadWithFallback(sources, load, options)— a mechanism-agnostic core (exportedfrom
use-m): try each source in order, optionally retry with linear backoff, andfail with one aggregated error listing every attempt instead of the cryptic error
of a single failing source.
It is now the shared backbone of three call sites:
Per-package CDN loading inside
use()— whenuse()fetches a package over thenetwork (browser, Deno, or
http(s)entry point) it falls back across distinct CDNhosts instead of depending on one:
http(s)esm.sh→jspm.dev→cdn.skypack.devesm.sh(deno target) →jspm.dev→cdn.skypack.devnpm i -g), BunLocal npm/bun loading and any single explicit resolver keep their previous
single-shot behavior and raw errors (a one-entry chain bypasses the engine), so this
is additive and backward-compatible.
The
use-m/loadbootstrap (loadUseM) — delegates its retry/fallback loop tothe same engine instead of a private copy. The loader-specific work
(fetch → validate body →
eval→ check theuseexport) stays local; only the loopis shared. No behavior change — the aggregated error message is byte-for-byte
identical, and all existing load tests pass unchanged.
The Node.js
--loaderhook (src/loader.js) — the "try the default resolver,then the use-m npm resolver" handshake is a two-source fallback chain, so it now
reuses
loadWithFallbackinstead of bespoke try/catch wiring. Same engine, sameaggregated error shape — one mechanism across the whole codebase.
New public surface:
loadWithFallback,networkResolverChain,denoResolverChain(exported), and
makeUseoptionsspecifierResolvers(ordered chain) +import(injectable low-level importer for custom chains and offline testing).
Source reorganization
All root-level source files have moved into
src/:package.jsonmain/exports/bin/fileswere updated to point at the newlocations, so all consumer-facing imports keep working unchanged:
import { use } from 'use-m'✅require('use-m')✅import { loadUseM } from 'use-m/load'✅use-mCLI ✅Only direct CDN URLs gain a
src/segment (e.g.https://unpkg.com/use-m/src/use.js). All examples, experiments, tests, and docs wereupdated accordingly (~66 files via bulk sed, plus path-pinned tests for the deeper
location).
The original bootstrap fix (still here)
loadUseMmakes the recommended bootstrap resilient, shipped two ways:Packaged helper
use-m/load(src/load.mjs+src/load.cjs), via the packageexportsmap for ESM (import { loadUseM } from 'use-m/load') and CJS:It validates
response.okand the body beforeeval()(rejecting HTML pages andshort plain-text error bodies via
looksLikeUseModule), retries each source, fallsback across
unpkg→jsDelivr→esm.sh, and fails with a clear, actionable errorlisting every attempt. Options:
fetch,sources,maxAttemptsPerSource(3),retryDelayMs(250),timeoutMs(10000, AbortController-backed),evaluate.Dependency-free self-contained snippet documented in the README for standalone
scripts that fetch
use-mdirectly (no install).Tests
All synced across
src/use.mjs,src/use.cjs, and the self-containedsrc/use.jseval build, and run across Node (Jest), Bun, and Deno via the existing
test-adapter.
tests/fallback.test.{mjs,cjs}— 22 tests (11 each) for the shared engineand per-package fallback.
tests/load.test.{mjs,cjs}— 34 tests forloadUseM/looksLikeUseModule,including the core regression: when every mirror returns
Internal Server Error,the thrown error lists every attempt and is not a
SyntaxError.tests/loader.test.mjs— 3 new tests for the refactored Node--loaderhook: default-resolver success, fallback to the use-m npm resolver, and aggregated
error message when both resolvers fail (covering the new third call site of
loadWithFallback).Full suite: 316 tests green on Node, 316 on Bun, and 30 files / 159 steps
on Deno.
Examples
examples/load/shared-fallback-engine.mjs— runnable, dependency-free demonstrationof the generic
loadWithFallbackengine and per-packageuse()fallback(deterministic, no network).
examples/load/using-helper.mjs—import { loadUseM } from 'use-m/load'.examples/load/robust-loading.mjs— the dependency-free self-contained loader.Other changes
entry + mirror-chain table + aggregated-error format), alongside the existing
"Robust loading (resilient CDN bootstrap)" section; all CDN URLs updated to
src/use.js.src/layout.
[Unreleased]for both the engine reuse and thesrc/reorganization.8.13.8→8.14.0;main,exports,bin, andfilesupdated forsrc/.Verification
--experimental-vm-modules): 43 suites / 316 tests passCloses #58