Skip to content

node consumers: node-lts interpreter only at runtime, node-lts to build (fixes #751) - #752

Merged
bryan-minimal merged 5 commits into
mainfrom
node-runtime-subset
Sep 23, 2026
Merged

bryan-minimal merged 5 commits into
mainfrom
node-runtime-subset

Conversation

@bryan-minimal

@bryan-minimal bryan-minimal commented Sep 23, 2026 •

Copy link
Copy Markdown
Member

Fixes #751. Follow-up to #665/#670. Scope updated per @mitodrummer's review.

What

Karl's #751 has the repro. With node-lts plus agent-browser or cf in one session, /usr/lib/node_modules/npm becomes a blend of two npm versions. Every npm/npx then dies with Class extends value undefined. That breaks webapp's min session activate, which runs npx astro dev.

Every package that runs under Node now:

  1. At runtime, takes the interpreter only, via subsetOf node-lts ["node"]. No package can contribute an npm tree to a session or build closure anymore. This is the fix(node-lts): repair the bundled npm's shadowed minipass — every npm install is broken #670 fix generalized from pnpm to all of them.
  2. Uses node-lts for build, runtime and tests, not node. node is 25.8.2, an odd release line that never becomes LTS and is past end-of-life. No package here needs 25: the strictest engines is agent-browser's >=24. With one flavor across the catalog, every package ships the same /usr/bin/node file, so there is no first-writer race on the interpreter either. The only sessions left with two Node binaries are ones that explicitly add node.

node stays in the catalog as an explicit user opt-in for the newest Node.

Packages (19): agent-browser, bash-language-server, capy*, cf, emscripten, eslint, json-server, lerna, mermaid-cli, next, pi, pnpm, prettier, pyright, serve, typescript-language-server, typescript, vlt*, wrangler, yarn
* already on node-lts; only the runtime subset changes.

Why build deps move too, not just runtime: pnpm's runtime interpreter enters the build closure of everything built with pnpm (next, agent-browser). If pnpm moved to node-lts while those builds stayed on node, those sandboxes would get two /usr/bin/node binaries, and which one wins would be arbitrary.

Behaviour change

A project that listed only one of these packages (e.g. next, typescript, eslint) with no node/node-lts used to get npm/npx transitively. After this PR it gets neither, so npm install fails with "command not found". A project that needs npm should declare a Node (node-lts, or node for Current). lerna is the sharpest case, because lerna run/publish spawn the npm client.

Verification

Companion PR

The structural guard Karl suggested, plus moving cf, wrangler, next, mermaid-cli and capy out of the shared usr/lib/node_modules into private usr/libexec/<pkg> directories, comes in a separate PR. It will go red until this one lands. Dry-run on the full catalog: main fails 19 packages, and this branch plus that PR passes all 531.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Chores
    • Updated Node.js-based tools to use the LTS release for builds and runtime execution.
    • Runtime configurations now include only the Node.js component.
    • Improved Next.js build validation for its image-processing support and platform-specific packages.

pkgs#670 fixed pnpm by depending on `subsetOf node ["node"]`. The other
17 packages that run under node still pulled in the whole `node`
package at runtime: npm, npx and /usr/lib/node_modules/npm included.

In any session that also contains node-lts, two npm trees get
hardlinked first-writer-wins per file. Since node-lts 24.19.0 moved its
bundled npm ahead of node's, the blend no longer loads: every npm/npx
dies with "Class extends value undefined". webapp's activation hook
(`npx astro dev`) hits this, so webapp is pinned to an older pkgs.

None of these packages invoke npm at runtime. Build deps keep the full
package, since builds need npm.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@bryan-minimal
bryan-minimal requested a review from a team as a code owner September 23, 2026 18:08
@coderabbitai

coderabbitai Bot commented Sep 23, 2026 •

Copy link
Copy Markdown
Contributor

Review in Change Stack →

Navigate logical layers of code changes, visualize relationships, and explore their blast radius.

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository: gominimal/pkgs/.coderabbit.yaml

Review profile: CHILL

Plan: Essentials

Run ID: e441d2a4-b583-499e-938a-14920e1d34f0

📥 Commits

Reviewing files that changed from the base of the PR and between b74b0cb and c283d1e.

📒 Files selected for processing (6)
  • packages/agent-browser/build.ncl
  • packages/cf/build.ncl
  • packages/mermaid-cli/build.ncl
  • packages/next/build.sh
  • packages/vlt/build.ncl
  • packages/wrangler/build.ncl
🚧 Files skipped from review as they are similar to previous changes (5)
  • packages/cf/build.ncl
  • packages/wrangler/build.ncl
  • packages/agent-browser/build.ncl
  • packages/mermaid-cli/build.ncl
  • packages/vlt/build.ncl

Included review availability: 0 reviews are currently available. Your included PR review attempts over the past 7 days set your current allowance at 2 reviews per hour.


📝 Walkthrough

Walkthrough

Package build specifications switch applicable Node build dependencies to Node LTS and select only its Node output for runtime dependencies. The Next.js build script also changes how it replaces and validates sharp, and removes platform-specific sharp-* packages.

Changes

Node runtime dependencies

Layer / File(s) Summary
Use Node LTS with Node-only runtime dependencies
packages/*/build.ncl
Package build specifications replace generic Node build dependencies with Node LTS where applicable and select only its Node output for runtime dependencies. Capy and vlt narrow their existing Node LTS runtime dependencies. pnpm’s comments describe LTS use and Node as an explicit opt-in.

Next.js sharp build

Layer / File(s) Summary
Replace and validate sharp
packages/next/build.sh
The build script removes the installed sharp directory before copying the source-built package. It removes sharp-* entries from both @img directories, copies @img/colour only when the destination is absent, and checks that sharp loads an addon from its installed src/build/Release directory and reports a libvips version.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Suggested reviewers: mitodrummer

Merge Risk: ⚪ Minimal · up to c283d

No actionable merge-blocking issue is established. Projects that need npm or npx should declare Node explicitly.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: Node consumers use the node-lts interpreter at runtime and node-lts for builds. It is specific and related to the npm-tree conflict fix.
Linked Issues check ✅ Passed The changes satisfy the coding objective in [#751]. packages/agent-browser/build.ncl and packages/cf/build.ncl use node-lts for builds and subsetOf node-lts ["node"] at runtime. The other chan…
Out of Scope Changes check ✅ Passed The changed package declarations address the same Node runtime and npm-tree closure problem described in [#751]. The next sharp changes fix the build output for a migrated Node consumer and verify t…
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check. Docstring coverage is scoped to functions touched by this diff. Analyzed 0 functions across 1…
✨ Finishing Touches
📝 Generate docstrings
  • Commit to this branch
  • Create a new PR
🧪 Generate unit tests (beta)
  • Commit to this branch
  • Create a new PR

Comment @coderabbitai help to get the list of available commands.

Mirror image of the previous commit: capy and vlt pulled in the whole
node-lts package at runtime, so a session with `node` plus either of them
got the same two-npm blend. Interpreter only.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

@mitodrummer mitodrummer left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for turning this around quickly. The subsetOf … ["node"] approach is right: the node output is only usr/bin/node, so these packages stop contributing an npm tree, which is the cause of #751.

On the convention call you left for Tom and me: these runtime deps should use node-lts, not node.

  • node is pinned to 25.8.2. Odd-numbered Node lines never become LTS, and 25.x is past end-of-life, so every tool here currently runs on a Node that no longer gets security fixes. node-lts (24.19.0) is a supported LTS line, and that's what these projects target upstream.
  • No package here needs 25. The strictest engines range I found is agent-browser's >=24; wrangler needs >=22 and next >=20.9.
  • node-lts has the same node output, so subsetOf node-lts ["node"] works as a drop-in replacement. It also mostly settles your open two-/usr/bin/node item. A session that includes node-lts (e.g. webapp) would then get the same usr/bin/node file from every package, so there's no first-writer-wins race. The collision would only remain in sessions that choose node explicitly.

Suggested scope:

  1. Change runtime_deps to subsetOf node-lts ["node"] for all 17 packages here, plus pnpm (#670 pointed it at node too).
  2. Moving build_deps to node-lts is optional and not needed for this bug, but it would keep each package's build and runtime on the same Node.
  3. Keep node as the explicit opt-in for anyone who wants the newest Node.

One behaviour change to call out in the PR description, broader than the lerna note: a project that listed only one of these packages (e.g. next, typescript, eslint) with no node/node-lts used to get npm/npx transitively. After this PR it has neither, and npm install fails with "command not found". I think that trade is right, since a project that needs npm should declare a Node, but it should be stated plainly.

Once CI has built the branch, I'll run the #751 repro against the head commit.

Per Karl's review: `node` is 25.8.2, an odd line that never becomes LTS
and is past end-of-life, so every tool here ran on a Node without
security fixes. No package needs 25 (strictest `engines` is
agent-browser's >=24).

One runtime flavor across the catalog also removes the remaining
two-/usr/bin/node race: every package now ships the same interpreter
file, so the only sessions with two are ones that explicitly add `node`.

Build deps move too. That is required, not just tidier: pnpm's runtime
interpreter enters the build closure of everything built with pnpm
(next, agent-browser), so leaving builds on `node` would put two
/usr/bin/node binaries in those sandboxes.

`node` stays in the catalog as an explicit user opt-in.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@bryan-minimal bryan-minimal changed the title node consumers: depend on the node binary only at runtime (fixes #751) node consumers: node-lts interpreter only at runtime, node-lts to build (fixes #751) Sep 23, 2026
bryan-minimal added a commit that referenced this pull request Sep 23, 2026
…ame (review)

Addresses Karl's review on #753:

- shared-tree now also rejects a broad recursive glob (`usr/**`,
  `usr/lib/**`, `**`) whose literal prefix is an ancestor of
  usr/lib/node_modules, in any package that BUILDS with a node runtime.
  Such a glob would capture an npm install and ship it. Non-node packages
  are exempt, so the 11 C libraries using `usr/**` are untouched.
  - Synthetic cases: usr/**, usr/lib/** and ** are flagged; usr/bin/*,
    usr/lib/*.so, usr/share/** and a C lib's usr/** are not.
  - Full catalog: main still fails the same 20 packages; with #752 it
    passes all 531.
- AGENTS.md now claims only what the guard checks: build and runtime
  deps. node-lts for test_deps is stated as convention, since `minimal
  dump` has no tests to check.
- capy: rename the output node_modules -> libexec to match the other
  four, and drop the stale "same shape as cf's node_modules" comment.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Five import lines had `subsetOf` prepended ahead of `standaloneTest`.
Lowercase identifiers sort alphabetically, so the import-line checker
failed them.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@bryan-minimal

Copy link
Copy Markdown
Member Author

Thanks, Karl. Everything from your review is in.

  1. Runtime is now subsetOf node-lts ["node"] for all 17 packages plus pnpm, in b74b0cb. capy and vlt were already on node-lts but took the whole package at runtime, which is the mirror image of agent-browser and cf still blend a second npm into sessions with node-lts (follow-up to #665/#670) #751 for sessions that pick node. They now take the interpreter only too (5adbad9). That makes 19 packages.
  2. build_deps (and test deps) are on node-lts too. I treated this as required, not optional: pnpm's runtime interpreter enters the build closure of everything built with pnpm (next, agent-browser), so leaving those builds on node would put two /usr/bin/node binaries in their sandboxes.
  3. node stays as the explicit opt-in. This PR doesn't touch the node or node-lts packages.
  4. The behaviour change is stated plainly in the description: a project that listed only e.g. next/typescript/eslint no longer gets npm/npx transitively, with lerna called out as the sharpest case.

Also since your review:

Checked: mip dump on amd64 and arm64 evaluates all 531, and no package outside the two runtimes references node in build or runtime deps. CI's minimal-check is rebuilding after the import-line fix. Once it's green, the head commit is ready for your #751 repro.

@mitodrummer mitodrummer left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approving. minimal-check was still running at approval time, so please merge only once it's green. After it merges I'll re-pin gominimal/webapp to the new pkgs commit and confirm the #751 repro is fixed.

@mitodrummer

Copy link
Copy Markdown
Member

Correction to my approval note: minimal-check had already passed, so this is clear to merge.

@bryan-minimal
bryan-minimal added this pull request to the merge queue Sep 23, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to failed status checks Sep 23, 2026
…uilts on every arch

sharp is one of next's optionalDependencies, so `npm install -g next.tgz`
already creates node_modules/sharp, and `cp -r` of the source-built sharp
nested it at sharp/sharp/ where nothing loads it. The npm copy won:
- amd64: its prebuilt @img/sharp-linux-x64 had been deleted, so sharp
  failed to load at all. This is the sharp_uses_system_libvips failure on
  #752.
- arm64: @img/sharp-linux-arm64 was never deleted, so sharp quietly
  loaded the prebuilt addon with bundled libvips, and the test passed
  without testing its name.

Pre-existing, not caused by the node-lts move. Main's CI Skips next's
standalone tests unless next is rebuilt, and #752 forces the rebuild.

The fix:
- rm the npm sharp before copying the source build in;
- remove @img/sharp-* for every arch;
- guard the @img/colour copy the same way;
- check at build time that sharp loads its OWN src/build/Release addon
  with a libvips version.

Verified in the Linux sandbox (arm64):
- fixed: `sharp 0.35.4 vips 8.18.6 addon sharp-linux-arm64-0.35.4.node`
- negative control, without the rm: the build FAILS with
  `Could not load the "sharp" module using the linux-arm64 runtime`

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@bryan-minimal

Copy link
Copy Markdown
Member Author

Fix for the next sharp_uses_system_libvips failure is in c283d1e.

Root cause (pre-existing, not the node-lts move): sharp is one of next's optionalDependencies, so npm install -g next.tgz already creates node_modules/sharp. build.sh's cp -r of the source-built sharp therefore nested it at sharp/sharp/, and the npm copy won:

  • amd64: its @img/sharp-linux-x64 prebuilt had been deleted, so sharp loaded nothing. That's the CI failure.
  • arm64: @img/sharp-linux-arm64 was never deleted, so sharp quietly loaded the prebuilt addon with bundled libvips, and the test passed without testing what its name says.

Main never caught it because CI Skips next's standalone tests unless next is rebuilt, and this PR forces that rebuild.

Fix:

  • rm the npm sharp before copying the source build in.
  • Remove @img/sharp-* prebuilts on every arch, not just x64.
  • A build-time assertion that sharp loads its own src/build/Release addon, with a libvips version.

Verified in the Linux sandbox (arm64):

  • The fixed build prints sharp 0.35.4 vips 8.18.6 addon sharp-linux-arm64-0.35.4.node.
  • Negative control, without the rm: the build fails with Could not load the "sharp" module using the linux-arm64 runtime.

amd64 is covered by CI on this push. Once it's green, the head is ready for your #751 repro.

@bryan-minimal
bryan-minimal added this pull request to the merge queue Sep 23, 2026
Merged via the queue into main with commit 79d15b4 Sep 23, 2026
12 checks passed
@bryan-minimal
bryan-minimal deleted the node-runtime-subset branch September 23, 2026 19:23
cvince pushed a commit to capysc/pkgs that referenced this pull request Oct 2, 2026
…ominimal#751) (gominimal#753)

* node packages: private libexec prefixes + a guard against a second npm (gominimal#751)

Two follow-ups to gominimal#752:

1. cf, wrangler, next, mermaid-cli and capy installed into the shared
   usr/lib/node_modules, the tree the node runtime's own npm lives in
   (Tom's gominimal#370 warning). They now install into usr/libexec/<pkg> with
   relative usr/bin symlinks, like vlt and pnpm. A `test -e` on each
   link fails the build if an upstream bin is renamed, rather than
   shipping a dangling link. mermaid-cli's wrapper and next's
   sharp/test paths follow the move.

2. node-runtime-guard (BLOCKING, as Karl suggested in gominimal#751). It
   evaluates the whole catalog on amd64 and arm64 and rejects:
   - outputs under usr/lib/node_modules;
   - runtime deps that carry a node runtime's npm;
   - packages on `node` rather than `node-lts`.
   A dump failure exits 2 and fails the check, so an unverified run
   cannot go green.

Full-catalog dry run:
- main: FAILS 20 packages (34 flavor, 19 runtime-npm, 5 shared-tree).
- main + gominimal#752 + this: PASSES all 531 packages.
This check is red until gominimal#752 lands.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* AGENTS.md: document the Node pattern the guard enforces

The rules that node-runtime-guard enforces are now written down where
package authors and agents look first:
- node-lts, not node;
- interpreter-only at runtime;
- a private libexec prefix, with a worked build.sh and build.ncl.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* node-runtime-guard: catch broad globs; align AGENTS.md; capy output name (review)

Addresses Karl's review on gominimal#753:

- shared-tree now also rejects a broad recursive glob (`usr/**`,
  `usr/lib/**`, `**`) whose literal prefix is an ancestor of
  usr/lib/node_modules, in any package that BUILDS with a node runtime.
  Such a glob would capture an npm install and ship it. Non-node packages
  are exempt, so the 11 C libraries using `usr/**` are untouched.
  - Synthetic cases: usr/**, usr/lib/** and ** are flagged; usr/bin/*,
    usr/lib/*.so, usr/share/** and a C lib's usr/** are not.
  - Full catalog: main still fails the same 20 packages; with gominimal#752 it
    passes all 531.
- AGENTS.md now claims only what the guard checks: build and runtime
  deps. node-lts for test_deps is stated as convention, since `minimal
  dump` has no tests to check.
- capy: rename the output node_modules -> libexec to match the other
  four, and drop the stale "same shape as cf's node_modules" comment.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

agent-browser and cf still blend a second npm into sessions with node-lts (follow-up to #665/#670)

2 participants