test(e2e): stop the elision-off block depending on a live CDN - #1229
test(e2e): stop the elision-off block depending on a live CDN#1229vivek7405 wants to merge 10 commits into
Conversation
|
Mechanism: the elision-OFF page hydrates only if the public internet answers, and the ON page never asks The title's framing is wrong and the issue said not to trust it, so here is what the evidence actually shows. There is no server bug and no gate race. The OFF build makes two network calls to jspm that the ON build does not make at all, and either one failing kills hydration for the whole page, not just for the component that needed it. Measured on both builds, same app, same machine, back to back:
ES module instantiation is all or nothing over the whole graph, so a failure at either point means Both halves reproduce on demand. Failing the browser fetch ( Failing the server's resolve (a Note the second one produces the reported symptom with no 404 at all, which is worth knowing: an OFF run can die this way and leave nothing in the network tab to find. The 404 in the original report is the first mode. This accounts for every property the issue lists. It is OFF-only, because ON makes neither call. It is independent of CPU load, because it is a network dependency rather than a race. It flips between runs minutes apart on one idle machine, because CDN edges and API endpoints do that. And it is not CI-specific. Falsified along the way, so nobody re-chases it. Local runs where the failure does NOT appear, for the record: 25 warm loads, 15 cold-server loads, 8 two-server runs shaped like the e2e block, 20 runs of the block pinned to two cores, and two full e2e suites (one pinned to two cores). The differential block passed in all of them. Nothing about CPU budget or server lifecycle moves this, which is consistent with the cause being off-box. |
… CDN The differential elision block redded on and off from 2026-08-02 with "OFF page never upgraded <my-counter>", which read as an elision defect and was not one. Elision ON drops components/vendor-badge.ts, the blog's only vendor consumer, so the ON server never calls api.jspm.io and the ON page never fetches from a CDN. Elision OFF ships that component, which adds two internet dependencies the ON page does not have: a blocking api.jspm.io/generate POST on the server's cold first request, and a ga.jspm.io module fetch inside app/page.ts's graph in the browser. A module graph instantiates as a unit, so a failure at either point means app/page.ts never evaluates and nothing it imports registers, including the counter these tests drive. The counter's own module fetches fine every time, which is why the symptom pointed at elision. Both halves reproduced on demand by failing each call in turn. Stubbing the API call closes both, since the URL the browser fetches is whatever that map says; pointing dayjs at a data: URL carrying this repo's own copy leaves nothing for the network to break. The stub serves only the packages it lists and passes anything else through to the real API, so a vendor added later is never silently faked. The hydration failure message now names the off-origin and failed resources on the page, since "define never ran" means the graph did not evaluate, not that the component's own module was missing.
A vendor added to the blog later needs a LOCAL_VENDORS entry or the block silently goes back to depending on jspm, and the preload flag differs per runtime, so a Node-only spelling would skip the Bun e2e job without failing it. Neither is discoverable from the diff.
|
Decision: WEBJS_ELIDE=0 keeps shipping the page's throw-at-load The issue asked for a call on this, so here it is. Two reasons. The flag's whole job is to serve the un-elided build so the differential block has something to compare against. Teaching And the throw is harmless where it lands. ES modules evaluate post-order, so every one of Worth being precise about what this does NOT say. The stub still throws, so |
The first cut counted resolved entries against install count, which got two cases wrong. An unparseable body parsed to zero installs and zero imports, which compared equal, so it answered with an EMPTY map. That is the silent death the fixture exists to prevent: an absent importmap entry is an unresolved-bare-specifier error that takes down the whole page graph. And two installs of one package (a bare install plus a subpath) collapsed to one key and read as unserviceable, sending a request the repo could have served to the network. It now refuses explicitly, per install, and a subpath install counts as unserviceable since it needs its own entry pointing at its own file. The unit test also pins the load-bearing trick, that dayjs.min.js is UMD and takes its global branch in module scope, by importing the emitted data: URL and formatting a date through it. A map naming a module that exports nothing would pass every structural check and still leave the page unhydrated.
The fixture read node_modules/dayjs at the workspace root, which only works while npm hoists it there. If a future install kept it under examples/blog/node_modules the read would fail, the stub would pass through, and the block would quietly go back to depending on jspm with nothing failing to say so. It now resolves the package from the blog's own package.json and reads whatever its main names.
Bun reads a data: URL specifier of this size as a filesystem path and raises NameTooLong, so importing the stub's emitted module fails there while every other assertion about it runs fine. That is a limitation of importing a data: URL from Bun, not of the fixture, whose URL is imported by Chromium and is exercised for real by the Bun e2e job. Per the matrix's own rule for a file that mixes runtime-agnostic and node-only tests, the node-only one moves to its own file and that file goes on the DENYLIST with its reason and where Bun covers the behaviour.
vivek7405
left a comment
There was a problem hiding this comment.
Read the whole diff cold. The mechanism holds up and the fixture is the right shape: stubbing the API call rather than the CDN fetch is what makes it cover both halves, and refusing to answer a partial map is the detail that would otherwise reintroduce the exact silent failure being fixed. Splitting the node-only data: URL import onto the DENYLIST is the convention the matrix asks for, and passing the preload flag as argv rather than NODE_OPTIONS is what keeps the Bun e2e job honest.
Two things wrong, one of them outside the diff.
The PR title is fix(server): stop the elision-off boot from 404ing a module, and this PR touches zero files under packages/, so the server scope names a package it does not change. The subject also asserts a boot that 404s a module, which the body and the code comments now both refute. Six commits means the squash subject comes from the title, so that description lands on main permanently. It needs to be a test: change that says what it actually does.
The other is inline.
The module-import assertion moved to e2e-vendor-stub-module.test.mjs when it was split off the Bun matrix, but the fixture kept naming the file it left, which now says in its own words that it does not carry it.
|
Title fix: the Retitled to |
vivek7405
left a comment
There was a problem hiding this comment.
Delta pass over the comment repoint. The cross-references line up everywhere now, but reading the guard that comment points at turned up a real problem in it, plus one more stale label of the same kind the repoint was fixing.
Both inline.
dayjs formats in local time and nothing pins TZ, so asserting an epoch formatted to 'Jan 1, 2025' only held at or east of UTC: America/New_York and America/Los_Angeles both produced 'Dec 31, 2024'. CI stayed green because GitHub runners default to UTC, so this was an npm test failure reserved for contributors in the Americas, in a test whose whole job is to fail for exactly one reason. Parsing a date STRING round-trips through local midnight in every zone, and the utc plugin is not an option since a data: URL cannot resolve it. Also relabels the empty-install case, which sends parseable JSON and never reaches the guard around init.body, and adds the cases that do reach it: no body, a non-string body, and malformed JSON. Each must pass through, since answering any of them with an empty map is an importmap missing every entry the page needs.
vivek7405
left a comment
There was a problem hiding this comment.
Delta pass over the timezone fix. The fix itself holds, and the three new pass-through cases each reach the guard they name.
One thing it did not go far enough on, path-level because the line is unchanged and the API will not anchor there: test/e2e/e2e.test.mjs:1832 carries the same defect. assert.match(badgeText, /\b2026\b/, ...) reads a badge that examples/blog/components/vendor-badge.ts:25 formats from 2026-01-01T00:00:00.000Z, a UTC instant, and dayjs formats in local time, so west of UTC the badge SSRs as Dec 31, 2025 and the match fails. TZ=America/Los_Angeles reproduces it. Pre-existing rather than introduced by this PR, but it is the same green-on-UTC-runners-only profile, in a file this PR edits, and it defeats the point of making this block deterministic off CI.
vendor-badge formatted a UTC instant, and dayjs formats in local time, so west of UTC the badge SSR'd as Dec 31, 2025 and the e2e assertion on its year failed. Same green-on-UTC-runners-only profile as the vendor-stub guard fixed a commit ago, reproduced with TZ=America/Los_Angeles. A release date is a date, not an instant, so a plain date string is also the more honest value: it parses as local midnight and formats to the same day wherever the app is served from.
|
Scope call: fixed the pre-existing timezone bug rather than deferring it
Fixed at the source in |
vivek7405
left a comment
There was a problem hiding this comment.
Delta pass over the badge fix. The fix is right and the year assertion now holds in every zone.
One leftover, path-level since the line is unchanged and the API will not anchor there: examples/blog/components/vendor-badge.ts:7 still says the component formats a fixed EPOCH, which is exactly what that commit stopped it doing, while the comment 18 lines below now says the opposite in as many words. The file contradicts itself about the one value the commit changed.
The docstring still described the value the previous commit replaced, while the comment beside that value said the opposite.
Closes #1228
Summary
The
differential elision (#181)e2e block redded on and off from 2026-08-02 withOFF page never upgraded <my-counter>, which reads as an elision defect and is not one. The elision-OFF build's hydration depends on the public internet twice and the ON build does not depend on it at all, so any jspm hiccup takes down every component on the OFF page and gets reported as elision dropping a module.Elision ON drops
components/vendor-badge.ts, the blog's only vendor consumer, soscanBareImportsfinds nothing,api.jspm.iois never called,dayjsnever enters the importmap, and the browser never contacts a third party. That is the #170 property and another test already asserts it. Elision OFF ships that component, so the same page picks up a blockingapi.jspm.io/generatePOST on the server's cold first request and ahttps://ga.jspm.io/...module fetch insideapp/page.ts's graph in the browser. An ES module graph instantiates as a unit, so a failure at either point meansapp/page.tsnever evaluates and nothing it imports registers.#components/counter.tsis imported on line 2 and fetches successfully every time, yetcustomElements.definenever runs for it, which is exactly why the symptom pointed at elision.The OFF server now boots with
test/e2e/fixtures/stub-jspm.mjspreloaded. It answers theapi.jspm.io/generatecall from this repo'snode_modulesand pointsdayjsat adata:URL carrying those bytes, which closes both holes at once because the URL the browser fetches is whatever that map says. The ON server is untouched, since it resolves nothing. The stub serves only the packages in itsLOCAL_VENDORSmap and passes anything else through to the real API, so a vendor added later is never silently faked.The hydration failure message also stopped guessing.
definenever running means the module graph did not evaluate, so it now says that and lists the off-origin and failed resources on the page instead of naming elision as the likely cause.Measured asymmetry, same app, back to back:
Full evidence, including both failure halves reproduced on demand and the false lead that a bare
curlof the CDN url returns 404, is in the first comment on this PR. The.server.tsdesign call the issue asked for is recorded in the second.Deliberately not done
packages/is wrong here. A vendor package fetched over the internet is a single point of failure for a page's whole hydration, which is inherent to importmaps without a bundler, and the framework already ships the production answer (webjs vendor pin)..webjs/vendor/importmap.jsonbroke the Add e2e network probes for vendor-never-fetched + inert-route zero-JS elision #170 elision e2e once already.WEBJS_ELIDE=0still ships the page's throw-at-load.server.tsimports. Reasoning in the second comment.Test plan
differential elision (#181)green over 25 consecutive runs9adc972f: with the stub wiring removed and the jspm API failing, the block reds with the exact CI message, and the third test still passes, matching CI's failure fingerprint precisely. With the stub wired, the same outage is a no-op.ea37bb96: makinglocalImportsForanswer a partial map instead of refusing reds two of the unit tests.test/repo-health/e2e-vendor-stub{,-module}.test.mjs, six tests, covering the install parsing, the refusal, the pass-through, and that the emitteddata:URL really is a module that formats a date.WEBJS_E2E_RUNTIME=bunruns the block green,bun --preloadwas verified to load the stub into the server process, and the Bun matrix passes overtest/repo-healthwith the one node-only file skipped by DENYLIST. Bun honours neither--importnorNODE_OPTIONS, so a Node-only spelling would have skipped the Bun e2e job without failing it.E2E: form actions, which reproduce identically on a clean checkout ofmainat this same commit (/feedback/triage500s there too) and pass in CI, so they are a local environment problem on this machine, not a regression from this branch.npm test: seven failures, all of which also fail on a clean checkout ofmainat this commit, and all of which pass in CI's Unit + integration job.packages/changedframework-dev.mdupdated. No other surface, since no user-facing behaviour changed