Skip to content

Islands: CommonJS island requires, builtin-class handles, JavaScript widened bindings - #1907

Merged
ctate merged 31 commits into
mainfrom
ni/i10-js-island-requires
Oct 29, 2026
Merged

ctate merged 31 commits into
mainfrom
ni/i10-js-island-requires

Conversation

@cramforce

@cramforce cramforce commented Oct 23, 2026 •

Copy link
Copy Markdown
Contributor

Stacked on #1906; tenth of eleven Node islands PRs. This is #1874, which was opened against int/islands, rebased onto the stack without changes. The stack's final tree equals int/islands plus #1874, minus the separately upstreamed #1834–#1837.

This fixes the compiler gaps that blocked a Next.js standalone server (.next/standalone/server.js) from building with automatic Node islands. With these changes, a Next 15 App Router app builds with --node-islands=auto and needs no environment workarounds. Against node server.js, it answers 26 of 26 checked requests (pages, route handlers, server action, middleware) with the same status, headers and body. The only difference is a per-request random id in one RSC payload, and that id also differs between two Node runs.

Changes

  • JavaScript requires of island packages
    • Declaration-less packages. A user .js file may require a package that ships no declarations: TS7016 is now relaxed in JavaScript, like TS2307. Standalone output strips every .d.ts.
    • Destructured requires. const { a, b: c } = require("<island>") binds the names as member reads of the island module. Before, every use threw the runtime fence "a binding form with no lowering".
    • Load at the statement. Top-level const/let requires of island modules in JavaScript now load the module at the statement instead of in the %init prelude. Environment writes before the require (process.env.NODE_ENV = …) are now visible to the module's evaluation, as in Node. var keeps the prelude load.
    • Nested requires. require("<island>") inside a function or an expression of a CommonJS module loads the island module at the call. The package scout counts such requires for packages without own declarations, so those packages become islands.
  • Builtin-class island values stay handles. Island values typed with builtin classes (URLSearchParams, Headers, URL, Request, Response, …) now stay island handles. Before, request.nextUrl.searchParams.get() reached a native checked cast and failed with an internal compiler error (dynDesc of non-JSON type searchParams).
  • JavaScript widened bindings. A JavaScript let/var binding that is assigned a value outside its declared type (relaxed TS2322, for example let t = parseInt(s); if (isNaN(t)) t = undefined) gets checked-dynamic storage, and reads keep the stored value. Every default Next standalone server.js hit this through KEEP_ALIVE_TIMEOUT.
  • Readonly writes in JavaScript. TS2540, a write to a member declared readonly, is relaxed in JavaScript. Next's types mark process.env.NODE_ENV readonly.

Evidence

New corpus programs. Each runs under Node and as a compiled binary, and must match byte for byte:

Program Covers
6880-node-island-cjs-entry A server.js-shaped CommonJS entry: environment writes, then bare, identifier and destructured requires of declaration-less packages, then an async call that resolves and rejects
6881-js-let-widened-assignment.js Module-level and function-local widened bindings, and a binding that stays typed
6882-node-island-nested-require Nested requires as an expression, a binding and a getter
6883-node-island-builtin-handles An island class exposing URL, Headers and URLSearchParams, round-tripped to the island
6884-js-readonly-declared-write A write to a readonly-declared member

Results in a Linux sandbox with the libnode dev pack (24.22.1):

  • Island test set (node-island*.test.ts, npm.test.ts, packages/compiler/src/node-islands): 236/236.
  • Differential 6800–6889: 62/62.
  • Every JavaScript corpus program in differential.test.ts: 854/854.
  • pnpm test:ts7 --baselines-only passes. Order baselines are recorded for the five new programs, and readable-stream-3/-disable lose their TS7016 diagnostic.
  • Zero-cost gate (--quick) against int/islands ee72385: PASS, 33 identical.
  • Self-hosting IR build: the same five pre-existing diagnostics as the base.
  • oxlint is clean on the touched files.

Shortcuts

  • Destructured island bindings read the member at each use, rather than copying it once at the declaration.
  • A hoisted var require keeps the prelude load.
  • A function whose checker-inferred return type is narrower than a widened binding it returns still checks its return value against that inferred type.
  • Coverage still reports island decisions as "would-be", and it lists fences that the island build removes.

On the stack: the nested-require scout skips files without a literal require(. That keeps pipeline.test.ts's minimal loads working; without it they crashed in moduleSourceCandidates. The npm harness normalizes the Node.js v<version> line of Node's uncaught-error report, because CI's reference Node is 24.22.0 and the libnode pack is 24.22.1.

Gate results

  • Island set in a Vercel Sandbox, with the libnode dev pack rebuilt from this branch's bridge (node-island*.test.ts, npm.test.ts, packages/compiler/src/node-islands, differential 68xx): plain 231/231 + diff 74/74; sanitized 231/231 + 74/74; full differential corpus 3734/3738: jose 6331 and 6610 (fixed in Route more packages and values through Node islands (NI-4c–4g) #1904 since, see below), and 5963-date-to-string and process-version-pin, which depend on the Sandbox's tzdata and Node 24.22.1 and fail on the base too.
  • pnpm lint, pnpm format:check and pnpm test:ts7 --baselines-only pass.
  • The self-hosted compiler (packages/compiler/src/native/cli.ts --emit=llvm) builds.
  • Zero-cost gate: 2837 programs identical (benchmarks, self-hosted compiler, TypeScript corpus) against the base branch. The only DIFFERS is corpus/fs-extra-package (%m0.settleVoid). It differs in every gate of the stack, including Bound V8's stack scan by Node's own stack in Node islands #1898, which does not touch the compiler, so it is nondeterministic on the base. both-fail covers only the island programs, because the gate host has no libnode pack. Compile time stays within tolerance.

CI on the first push found failures outside the focused island set: the ABI in bridge.test.ts, the runtime ABI header declarations, the effect gate on the ts7 allowlist, weak collections in the compiler, the pipeline scout and the Node version line. The fixes are folded into the PRs that introduce each piece, and they are re-checked in a Sandbox on the final tree.

Stack

#1898
#1899
#1900
#1901
#1902
#1903
#1904
#1905
#1906
→ #1907
#1908

🤖 Generated with Claude Code

@vercel

vercel Bot commented Oct 23, 2026 •

Copy link
Copy Markdown
Contributor

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
scriptc Ready Ready Preview, v0 Oct 28, 2026 7:01pm UTC

…widened bindings

Node islands stack, part 10 of 11 (scriptc-perf/results/islands-prs/PLAN.md).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
# Conflicts:
#	packages/compiler/src/frontend/lowering/lower-stmts.ts
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@cramforce

Copy link
Copy Markdown
Contributor Author

All review threads on this PR are resolved: function-local require bindings keep their own storage (8752390), the module-source scan tolerates whitespace and comments before the require parenthesis (8752390), and the pipeline test loader's empty source is skipped (00e88c2).

The fixed parent branch #1906 (d061550) is merged in f4702d6; CI is running.

🤖 Generated with Claude Code

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
cramforce and others added 5 commits October 28, 2026 08:48
@cramforce

Copy link
Copy Markdown
Contributor Author

Stack maintenance round; head is now 8e65aa86e.

CI root cause. test (Linux, plain|san, 10/20) failed in unit-journal.test.ts with expected [ 'jsWidenedBindings' ] to deeply equal []. jsWidenedBindings is computed by the whole-program prepass analyzeJsWidenedBindings before lowering starts — a pure function of the program's modules — so it is not journal state. Fixed here in 538b78f59 by listing it in Lowerer.unjournaled with that reason.

Carried up: f2cee4d5f merges the current ni/i9-recorded-deferral, 8e65aa86e carries the lazy-traps divergence classification.

Sandbox validation with a libnode pack built from the top branch's bridge (bridgeAbi 10): unit-journal, the eight node-island*.test.ts files (100 tests), the 64 island corpus programs, and the npm suite pass.

The journal classification is read only by unit-journal.test.ts, so it is codegen-inert.

🤖 Generated with Claude Code

6883-node-island-builtin-handles builds a Headers inside an island, undici
loads node:tls for the builtin classes it backs, and OpenSSL parses the
system CA bundle through PEM/X509. That state is freed only by
OPENSSL_cleanup at exit, which NI-7c keeps off, and the stacks carry no
node::crypto frame, so the existing entries cannot match. Name OpenSSL's
allocator entry points, as the file already does for the rest of libnode's
process-lifetime state.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…/i10-js-island-requires

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@cramforce

Copy link
Copy Markdown
Contributor Author

Restacked after main moved. #1902 was merged at 17:41Z (merge commit 2f3b926de), so this branch now merges origin/ni/i9-recorded-deferral and the merge was clean (no conflicts, no hand edits; pnpm manifest reproduces the merged surface manifest byte for byte). Head is now b1ff156ea and a fresh CI run is in progress on it.

🤖 Generated with Claude Code

Base automatically changed from ni/i9-recorded-deferral to main October 29, 2026 20:18
@ctate
ctate merged commit b8a6d38 into main Oct 29, 2026
70 checks passed
@ctate
ctate deleted the ni/i10-js-island-requires branch October 29, 2026 20:27

This branch was successfully deployed

1 active deployment
Preview — b1ff156e Deployed Oct 28, 2026 by vercel[bot]
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.

2 participants