Skip to content

Commit 9a89ff3

Browse files
perf(install): ship a lockfile per template, cutting cold installs by ~90%
Templates shipped no lockfile, so npm's Arborist resolved from ranges and fetched the FULL packument of every package in the graph. On react-ts that was 143 of 155.6 MiB: 92% of a cold install spent deciding what to install. fix(locks): refuse peer-split locks, and gate the lock rules on a bare checkout Eight template gates hung on `Cannot find native binding`. rolldown 1.2.1-1.2.3 let its wasm binding pin @emnapi at alpha.3 while @napi-rs/wasm-runtime wanted alpha.4, so npm nested a second copy and the binding could not find its runtime. A lock encoding that split is now refused rather than shipped, which also stops a stale lock from freezing a defect 1.2.4 has since fixed. fix(ci): provision the shipped locks from the spikes, and judge a stall by the serving window Two jobs ran the same lock-reading spikes and only one of them had locks. `template-gate` resolved them in a step; `spikes-net` did not, so `vendor/locks/` was empty there, `template-locks` died in 0.2s on the first missing file, and the eight template spikes quietly installed from RANGES — two jobs exercising different code paths for the same template. Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent f390cde commit 9a89ff3

41 files changed

Lines changed: 8746 additions & 301 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/ci.yml

Lines changed: 86 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,74 @@ jobs:
6464
# that nor an over-eager expiry shows up as an error anywhere.
6565
- run: npm run probe:cookie-jar
6666

67+
# --- the same gate, on the OLDEST Node package.json admits -----------------
68+
# Every other job pins `node-version: 22`, and `.nvmrc` says `22`. Both resolve
69+
# to the newest 22.x, so before this job existed NO MACHINE ANYWHERE ran the
70+
# regime `engines.node` promises to support — not CI, not a contributor using
71+
# nvm. That is not a hypothetical gap: `npm run dev` shipped broken on
72+
# 22.0-22.17 twice, behind three rounds of green gates, because Node only
73+
# strips TypeScript types unprompted from 22.18 and every runner had it.
74+
#
75+
# Pinned to the exact floor rather than a range, so that raising `engines.node`
76+
# without raising this line leaves an untested version admitted, and lowering
77+
# it without lowering this line is a promise nothing checks. The two are meant
78+
# to be edited together; scripts/spike-install-latency.mjs asserts they match.
79+
#
80+
# Cheap by construction: it is the Wasm-free offline tier, the same one
81+
# `toolchain-gate` runs, so it costs a second checkout and about a minute.
82+
# Without the crates, run-spikes skips the `needsWasm` spikes — which is what
83+
# keeps this job honest about Node versions rather than about OpenSSL: the
84+
# `constants` spike compares the host's bundled OpenSSL and zlib against the
85+
# VM's and legitimately differs on any Node but the newest.
86+
toolchain-floor:
87+
if: github.event_name != 'schedule'
88+
runs-on: ubuntu-latest
89+
steps:
90+
- uses: actions/checkout@v7
91+
- uses: actions/setup-node@v7
92+
with:
93+
# Keep in step with `engines.node`.
94+
node-version: "22.15.0"
95+
# Named separately from the tier below so that the failure this job exists
96+
# to catch reads as itself. Thirteen scripts import the studio's templates
97+
# out of a .ts file and two of them run on every `npm run dev`; when that
98+
# broke, the generators failed soft and the only visible symptom was a dev
99+
# server that was quietly slow.
100+
- name: the studio templates must import on the floor, not only on latest
101+
run: |
102+
node --input-type=module -e "
103+
const { loadShippedTemplates } = await import('./scripts/lib/shipped-templates.mjs');
104+
const t = await loadShippedTemplates();
105+
if (!Array.isArray(t) || t.length === 0) throw new Error('loaded no templates');
106+
console.log('loaded ' + t.length + ' templates on Node ' + process.versions.node);
107+
"
108+
- run: node scripts/run-spikes.mjs --offline
109+
110+
# The corpus gate that installs each shipped lock in the VM lives in
111+
# `template-gate`, which is schedule/dispatch only because it needs the Rust
112+
# toolchain and an hour. That leaves merges unguarded against the change most
113+
# likely to poison a lock: an edit to templates.ts, to the generator, or to
114+
# the kernel's alias table. RESOLVING all of them needs none of that — no
115+
# Wasm, no VM, ~35s — and resolution is where both guards run, so a template
116+
# that starts pulling in an aliased native is refused here rather than on the
117+
# deploy that ships it. It is not the in-VM gate and does not replace it; it
118+
# is the part of that gate which is cheap enough to block a merge.
119+
#
120+
# `--strict`, because producing them IS this job's subject. The cost is that a
121+
# registry outage reddens a PR — the same trade `template-gate` already makes
122+
# for the same command, against the alternative of hearing it from a nightly.
123+
lock-resolution-gate:
124+
if: github.event_name != 'schedule'
125+
runs-on: ubuntu-latest
126+
timeout-minutes: 15
127+
steps:
128+
- uses: actions/checkout@v7
129+
- uses: actions/setup-node@v7
130+
with:
131+
node-version: 22
132+
- name: every shipped lockfile must still resolve, and pass both guards
133+
run: npm run vendor:locks -- --strict
134+
67135
# SDK packaging + public-API contract guard (offline, no Wasm, no browser).
68136
# Also regression-checks the shipped preview sw.js branding + examples/basic.
69137
sdk-smoke:
@@ -111,7 +179,7 @@ jobs:
111179
# but missing here runs nowhere at all: skipped in toolchain-gate for want of
112180
# the crates, and never selected in the only job that has them.
113181
- name: offline spikes that need the Wasm VFS
114-
run: node scripts/run-spikes.mjs --offline dep-cache bun http-binary-body http-response-bytes cookie-session fs-cp fs-errors worker-pool node-cli repl stack-traces readline watch child-stdin fs-metadata crypto-jwk zlib-brotli port-liveness constants diag-liveness fatal-errors net-close-order net-blocklist
182+
run: node scripts/run-spikes.mjs --offline dep-cache depcache-shipped seed-swc bun http-binary-body http-response-bytes cookie-session fs-cp fs-errors worker-pool node-cli repl stack-traces readline watch child-stdin fs-metadata crypto-jwk zlib-brotli port-liveness constants diag-liveness fatal-errors net-close-order net-blocklist
115183
- run: npm run verify
116184

117185
# --- package-manager gate: the North Star, gated on its own ----------------
@@ -214,6 +282,23 @@ jobs:
214282
# a bare `lit` also selects sqlite and pglite: this step was running two in-VM
215283
# database spikes it does not name, on a job whose failures are supposed to mean
216284
# "a shipped template stopped booting". Nine ran where seven were asked for.
285+
# There is deliberately no `vendor:locks` step here. It used to be one, and
286+
# a step is the wrong place for it: `spikes-net` runs the same lock-reading
287+
# spikes with no such step, so `vendor/locks/` was empty there and
288+
# `template-locks` died in 0.2s on the first missing file while the eight
289+
# template spikes silently installed from RANGES — two jobs exercising
290+
# different code paths for the same template, which is the one thing a
291+
# second job must not do. Provisioning now belongs to the spikes that need
292+
# it (`VENDORS.locks` in scripts/run-spikes.mjs), so every job that selects
293+
# one gets the locks, and a job added tomorrow cannot forget.
294+
# Every id in COVERAGE, installed in the VM from the lock the runner
295+
# provisions for it. This is the gate that made COVERAGE forty templates
296+
# instead of one: a spike per template would have been ~50 near-identical
297+
# files, so this is one spike driven over the list. It installs and checks
298+
# the tree is usable rather than booting a server — the step below is what
299+
# boots servers, for the eight frameworks it names.
300+
- name: every shipped lockfile must install into a usable tree
301+
run: node scripts/run-spikes.mjs --net template-locks$
217302
- name: framework templates must start their dev server
218303
run: node scripts/run-spikes.mjs --net react$ preact$ lit$ solid$ vue$ svelte$ qwik$ ember$
219304
# The two the step above used to pick up by accident, kept blocking on purpose.

0 commit comments

Comments
 (0)