🚧 CDN pivot: stop building the webpack Pages bundle, serve eyereasoner from a CDN + redirect old Pages URLs - #1957
Draft
jeswr wants to merge 2 commits into
Draft
🚧 CDN pivot: stop building the webpack Pages bundle, serve eyereasoner from a CDN + redirect old Pages URLs#1957jeswr wants to merge 2 commits into
jeswr wants to merge 2 commits into
Conversation
…d of building the webpack bundle Stop building and publishing the ~4MB webpack bundle to the `pages` branch on every release (which grew the branch to ~7.5GB). Instead: - README and the prebuilt example now use a public ESM CDN (esm.sh, with jsDelivr as a drop-in alternative) for both ESM `import` and classic `<script>` usage. The package is self-contained (WASM + EYE image inlined), so there are no separate .wasm/.data assets to host. - Two tiny, self-deriving redirect stubs (redirects/index.js classic global, redirects/dynamic-import.js ESM) derive the requested version from their own URL and load it from the CDN. Broadcast byte-identically to every path they collapse to 2 unique blobs, so old Pages URLs keep working with ~0 growth. - scripts/generate-redirects.ts replaces the webpack + post-webpack Pages emission in the release workflow and also writes the `.nojekyll` marker; the gh-pages publish now ships the stub tree (dotfiles enabled). Refs #1845. Supersedes the build-and-prune approach of #1955. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ackfill Revisions to the CDN pivot per review: - docs/example: the recommended browser delivery now serves the SWI-Prolog WebAssembly as a separate binary asset (import map + ?external=swipl-wasm swaps the package's inlined SWIPL build for swipl-wasm's split web build; a small wrapper points locateFile at the CDN and normalises preRun to an array). The single-URL inlined delivery stays documented as the zero-config alternative, with measured wire sizes for both. The idea of adding a new IIFE artifact to the npm package is dropped: it could never serve versions that are already published, so the classic global is the async import() + Proxy shim in every case. - redirects: the stubs are now explicitly a ONE-TIME historical backfill for already-published version URLs. generate-redirects gained --all (generates the stub tree for every version published to npm, from the registry) and is deliberately not wired into CI. - release: stop publishing anything to the pages branch per release (drop the stub-generation step and the @qiwi/semantic-release-gh-pages-plugin); new versions need no Pages files because the docs point at the CDN directly. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
🚧 Stop building the webpack Pages bundle; serve
eyereasonerfrom a CDN and backfill the old Pages URLsReferences #1845. Supersedes / re-frames #1955: rather than keep building the bundle and prune it every release, this stops building it at all.
The pivot
Today each release builds a ~4MB webpack bundle and publishes it to the
pagesbranch, which has grown to ~7.5GB (~7× the 1GB Pages limit) and keeps breaking the Pages deployment. This PR removes the cause:examples/prebuilt/index.htmlnow document two deliveries, both zero-build and working for every already-published version:?external=swipl-wasmswaps eyereasoner's internal WASM-inlined SWIPL dependency forswipl-wasm's split web build, and a 5-line wrapper pointslocateFileat the CDN. The browser then fetchesswipl-web.wasmas a real binary (application/wasm→ streaming-compiled, cached independently of the eyereasoner release) instead of parsing it out of a multi-MB JavaScript string.import { n3reasoner } from 'https://esm.sh/eyereasoner'(self-contained, WASM inlined) stays documented as the no-setup variant.redirects/index.jsclassic global,redirects/dynamic-import.jsESM) read their own URL, work out the requested version, and load it from the CDN.scripts/generate-redirects --allgenerates them for every version ever published to npm (validated: 847 versions → 2,116 files that git dedups to 2 unique blobs). This is run once to replace the existing bundle tree — it is deliberately not wired into CI.@qiwi/semantic-release-gh-pages-pluginis removed from the release config: since the docs point new consumers at the CDN, new versions need no redirect stubs and no Pages files at all.The delivery investigation (what changed and why)
The previous draft recommended the esm.sh single URL, which ships SWI-Prolog's WASM inlined in JavaScript. Per review, the first-class delivery should serve the WASM as a separate, streamable, separately-cacheable binary. That reopened the "can the runtime locate its assets cross-origin?" question, so it was verified end-to-end:
How the split build locates assets (
swipl-wasm/dist/swipl/swipl-web.js): the.wasmresolves relative to the script's own URL (document.currentScript/__dirname) — CDN-friendly for classic scripts — but the.datapackage resolves relative to the page, and module scripts have nocurrentScript. So a CDN delivery must passModule.locateFile.eyereasonerdoesn't forward emscripten options, butn3reasoner(..., { SWIPL })accepts a factory, so a wrapper does it. One genuine boot-blocker was found and solved: eyereasoner passespreRunas a function, and the split build's data loader requires an array (Module.preRun.push) — the wrapper normalises it. Without that, boot throwsTypeError: Module.preRun.push is not a function.Proof it boots and reasons (headless node, gated/niced):
eyereasoner@21.1.10+ the split driver/.wasm/.databyte-for-byte as served by jsDelivr (md5-verified against npm) viaoptions.SWIPLinjection → boots, infersSocrates a Mortalin 354ms.?external=swipl-wasmmodule + esm.sh-transformedswipl-web.js, with a node loader hook emulating the exact import map → boots, reasons, 319ms.application/wasm(streaming-compile eligible),access-control-allow-origin: *, immutable cache on both CDNs.swipl-bundle-no-databuild embeds exactlyswipl-web.wasm(md51cee4be…, 2,190,887B), so the EYE.pvmimage is compatible with the split build by construction — provided theswipl-wasmversion matches the one the eyereasoner release pins (documented as a lockstep note in the README).Measured wire sizes (uncompressed / brotli):
application/wasm)Honest caveat: the split delivery is ~0.9MB more over the wire, not less. The inlined build never shipped the 1.6MB SWI-Prolog
.data(that's what-no-data+ the EYE.pvmimage achieves), and the splitswipl-webbuild downloads it unconditionally even though boot uses the.pvm. The split.wasmitself transfers ~0.4MB smaller than its JS-inlined form; what you buy for the extra.databytes is streaming compilation, no multi-MB JS parse + string→ArrayBuffer decode on the main thread, and swipl-wasm assets that stay cached across eyereasoner upgrades. See open question 2 for the upstream fix that would make the split delivery strictly smaller.The classic
<script>global (Option B dropped)The previous draft floated adding an
esbuild --format=iifeartifact to the npm package for a true synchronous global. Dropped: a new npm artifact can never apply to versions already published, so it cannot serve the historical Pages URLs — the very thing the redirects are for. The classic global therefore comes from the CDN's own transform of any published version, via dynamicimport():window.n3reasoner = n3reasoner(explicitly documented as the only way to get a global from an already-published CJS build — and it is async).import()+ aProxyglobal soawait eyereasoner.n3reasoner(...)keeps working on legacy pages. A consumer reading a non-callable property synchronously would see a change — documented. The stubs deliberately import the self-contained inlined build: legacy pages can't be assumed to carry the import map the split delivery needs.Everything above was verified headless in node; the browser-only bits (import-map resolution,
instantiateStreaming, the esm.sh module running in a real module context) were not. Openexamples/prebuilt/index.htmlfrom this branch (it now uses the recommended split delivery), click Execute, and confirm::Socrates a :Mortalderivation;swipl-web.wasm(~2.19MB,application/wasm) andswipl-web.data(~1.64MB) fetched fromcdn.jsdelivr.netwith no 404s, and noswipl-bundle-no-datadownload;What's in this PR
README.mdexamples/prebuilt/index.html?external+locateFile/preRunwrapper)redirects/index.jsredirects/dynamic-import.jseyereasoner) — marked one-time backfillscripts/generate-redirects.ts--allgenerates stubs for every npm-published version (registry-driven); not wired into CIscripts/post-webpack.ts.github/workflows/nodejs.ymlpackage.jsonbackfill:redirectsscript;@qiwi/semantic-release-gh-pages-pluginremoved from release config and devDependencieswebpack.config.js/bundle:webpackare kept — still used by the browser E2E/memory tests. See open question 1.Validation (headless, gated
flock+nice)Socrates a Mortal).npm run lint: 0 errors;tsc --noEmitonscripts/generate-redirects.ts: clean.generate-redirects --name=v2.3.14: exact tree (2/3/14/,2/3/latest/,2/latest/,latest/, rootindex.js,.nojekyll), stubs md5-identical toredirects/*.js.generate-redirects --all: 847 npm versions → 1,058index.js+ 1,057dynamic-import.js+.nojekyll, 2 unique blobs, 210latestdirs.node --check-parse; URL→CDN derivation re-checked for root/latest/@M/@M.m/@M.m.p/query-string shapes.application/wasm+access-control-allow-origin: *on the split assets; all URLs in the docs return 200.Follow-ups (need @jeswr sign-off — NOT in this PR)
generate-redirects --all, commit the result as a fresh orphan commit onpages(plusexample/,dev/bench/), force-push. That installs every historical redirect and shrinks the branch to a few MB atomically. Force-push needs explicit approval — deliberately excluded here..nojekyllmarker (the prune logic is moot once nothing is published per release).Open questions
swipl-wasmimprovement (you maintain it): the split delivery would be strictly better than the inlined one (~0.7MB brotli total, smallest of all options) ifswipl-wasmshipped a split no-data web build (swipl-web-no-data.js+ standalone.wasm) — external WASM without the 1.6MB.datathat eyereasoner's.pvmboot doesn't need. Worth an issue on SWI-Prolog/npm-swipl-wasm? Existing releases would still use the recipe in this PR.latest/@M/@M.mlive, so a bad future npm publish would propagate to old URLs. Acceptable, or pin?🤖 Generated with Claude Code