Repository navigation
fix(transforms): make leaked node: named imports fail at the call site (depends on #2999) - #3004
Conversation
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 0e15e161a0
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| const fromIndex = statement.lastIndexOf(" from "); | ||
| if (fromIndex === -1) return null; // side-effect-only import |
There was a problem hiding this comment.
Match minified import syntax before skipping
In production browser transforms, compilePlugin runs esbuild with minify: !ctx.dev, and esbuild emits imports without spaces such as import{createHash as h}from"node:crypto";. This exact " from " lookup returns -1 for that valid static named import, so the new stage skips it as if it were side-effect-only; resolveImportsPlugin then still points the named import at node-noop.js, preserving the link-time does not provide an export named failure for production builds even though dev builds are fixed.
Useful? React with 👍 / 👎.
d5634cc to
5a874ae
Compare
0e15e16 to
fb33fd4
Compare
kwakayama
left a comment
There was a problem hiding this comment.
Score: 78/100
Requesting changes before this lands. The approach is reasonable for moving node-builtin failures from link time to call site, but the transform needs one more hardening pass.
Blocking concern:
src/transforms/pipeline/stages/browser-node-builtin-imports.tsgenerates names like__vf_node_builtin_0without checking existing user bindings. A source file that already declares/imports that name can be transformed into duplicate or conflicting bindings.
Please generate a non-conflicting namespace identifier by scanning existing identifiers/import bindings, and add a regression where __vf_node_builtin_0 already exists in the module.
Also note this PR is stacked on lower open PRs and currently only shows CLA in the status rollup, so it is not merge-ready yet regardless.
5a874ae to
f61c8f5
Compare
fb33fd4 to
25c1742
Compare
f61c8f5 to
260b4c0
Compare
25c1742 to
6cd7316
Compare
|
Addressed. The namespace identifier is now collision-free.
Regressions added:
Each asserts the destructure is still correct and that the user's own name is bound exactly once. Separately, this commit also fixes a review finding that made the original transform dev-only: the import clause is now read from the lexer's offsets instead of searching for Verification: 24 steps passing, |
6cd7316 to
cdb77c3
Compare
260b4c0 to
7ad24e2
Compare
kwakayama
left a comment
There was a problem hiding this comment.
Follow-up after fixes: approving. Namespace generation now avoids collisions, including minified inputs. Local verification: \running 1 test from ./src/transforms/pipeline/stages/browser-node-builtin-imports.test.ts
browser-node-builtin-imports ...
converts a named import into a namespace import plus destructure ... ok (2ms)
preserves aliases ... ok (0ms)
handles multiple bindings ... ok (0ms)
keeps a default binding alongside the namespace ... ok (0ms)
leaves a default-only import alone ... ok (0ms)
leaves a namespace import alone ... ok (0ms)
leaves a side-effect-only import alone ... ok (0ms)
leaves non-node imports alone ... ok (0ms)
numbers each rewritten import uniquely ... ok (0ms)
minified input ...
rewrites a minified named import ... ok (0ms)
rewrites a minified import with a default binding ... ok (0ms)
leaves a minified side-effect-only import alone ... ok (0ms)
leaves a minified namespace import alone ... ok (0ms)
rewrites both imports in a minified module ... ok (0ms)
handles a single-quoted specifier ... ok (0ms)
minified input ... ok (1ms)
name collisions ...
avoids a name the module already declares ... ok (0ms)
avoids a name the module already imports ... ok (0ms)
avoids a name aliased in by another import ... ok (0ms)
keeps stepping away until the name is free ... ok (0ms)
avoids a collision in minified source ... ok (0ms)
keeps the numbering unique across imports once renamed ... ok (0ms)
name collisions ... ok (1ms)
only runs for the browser target ... ok (0ms)
browser-node-builtin-imports ... ok (6ms)
ok | 1 passed (24 steps) | 0 failed (9ms). Score: 93/100. Next step: merge after base stack and checks are green.
|
Clean follow-up after the approval above: Score: 93/100. Verification:
Next step: merge after the base stack and refreshed checks are green. |
7ad24e2 to
169effb
Compare
cdb77c3 to
0393447
Compare
8e91ec2 to
b5cc4b7
Compare
020ce0d to
2f612f0
Compare
b5cc4b7 to
80bc47b
Compare
2f612f0 to
3dd5424
Compare
80bc47b to
a519123
Compare
a0360c7 to
83bde72
Compare
a519123 to
310ae9b
Compare
The browser noop polyfill for Node built-ins documents its contract as: the
import succeeds, and any actual use of the API fails at the call site, which
surfaces the problem clearly instead of a cryptic resolution error.
That cannot hold for a named import. ESM resolves named imports at link time, so
`import { createHash } from "node:crypto"` in browser-bound code took down the
entire module graph before a line of it ran, with an error naming the polyfill
rather than the offending import:
SyntaxError: The requested module '.../node-noop.js' does not provide an
export named 'createHash'
Named imports of node: builtins are now rewritten to a namespace import plus a
destructure, so linking succeeds and the failure lands where the API is called
("createHash is not a function"). This also makes the source and compiled-binary
node-noop variants, which export different names, behave identically.
The import clause is read from the lexer's own offsets. Searching for " from "
would find nothing in `import{createHash as h}from"node:crypto";`, which is
what esbuild emits whenever the build is minified, so the rewrite would have
applied in dev only and every production build would have kept the failure.
83bde72 to
adff4b8
Compare
kwakayama
left a comment
There was a problem hiding this comment.
Follow-up approval after #3003 merged and #3004 was rebased onto main.
Score: 92/100.
Rationale: the transform keeps browser node-builtin named imports from failing at module-link time, pushing failure to the call site while preserving existing coverage after the rebase.
Verification:
- deno task audit
- combined targeted regression suite: 21 tests, 334 steps, 0 failed
Next step: wait for refreshed GitHub checks and required reviewer gate, then merge when green.
Summary
The browser noop polyfill for Node built-ins documents its own contract:
That contract cannot hold for a named import. ESM resolves named imports at link time, so
import { createHash } from "node:crypto"in browser-bound code took down the entire module graph before a line of it ran — and the error named the polyfill rather than the offending import:Named imports of
node:builtins are now rewritten to a namespace import plus a destructure, so linking succeeds and the failure lands where the API is actually called.This also resolves a real source/binary divergence:
src/platform/polyfills/node-noop.tsexports{ nodeNoop, default }while the compiled-binary copy inmodule-server.tsexports onlydefault, so identical source could link or fail depending on how the framework was served. Via a namespace import, both behave identically.Reproduction
g-transitive-ts-ext.tsx,app/use-server/page.tsx— components that callnode:cryptoat render time, rather than inside a data hooksrc/transforms/pipeline/stages/browser-node-builtin-imports.test.tsTest evidence
Covers aliases, multiple bindings, a default binding alongside named ones, and the untouched cases (default-only, namespace, side-effect-only, non-node).
Wider suite:
deno task test:unit→ 2525 passed | 0 failed.Client evidence
Before — one leaked builtin kills the whole page, blaming the polyfill:
After — the module links, and the error names the real API at its real call site:
These three routes should still fail: they genuinely invoke
node:cryptoduring client render, which no polyfill can make work. The point is that the framework now says so precisely, exactly as the polyfill's documentation promises, instead of emitting a link error against a file the author never wrote.Related
Chain: this PR is part of a 13-PR chain fixing the bugs catalogued in
veryfront-router-testing.
Its base is the previous PR in the chain, so the diff shows only this fix.
The root of the chain is #2999 (
fix/ssr-lazy-import-graceful-degrade) — merge #2999 first, thenrebase the chain onto
main.Regression gate:
deno task test:unit→ 2525 passed | 0 failed;deno task lint,deno task fmt:checkanddeno task typecheckall clean. The reproducer's full 56-routematrix (
ROUTES.txt+sweep.sh) was re-run after every fix: 7 routes improved, 0 regressed.A 46-route Chromium hydration sweep (
client-sweep.mjs) backs the client-side claims.