fix(server): support Node production entry contracts - #2749
Conversation
|
/bigbonk review for issues |
commit: |
|
There was a problem hiding this comment.
Review — fix(server): support Node production entry contracts
Reviewed the diff against dev/prod parity and the constant-folding logic. Everything is well-tested (helper unit tests, entry-loader regression, Worker-handler regression, and a real app-basic production fixture) and vp check is clean on the two source files. Verified the targeted tests + the char-code integration test pass locally.
Correctness — looks good
- The
require(String.fromCharCode(<literal code units>))fold correctly gates on: unboundString(hasAstBinding(scope, "String")), non-computed member access,fromCharCodeproperty, and integer literal args in[0, 0xffff]. Spread args and shadowedStringfall through to the existing dynamic-request handling — both covered bytests/dynamic-requests-build.test.ts. - Overwriting only the argument range (not the whole
CallExpression) plus the earlyreturnavoids a MagicString overlap with the nestedString.fromCharCodenode. Good. globalThis.requireis set once before evaluating the entry, matching the existing escape-hatch pattern inotel-tracer-extension.ts/client-trace-metadata.ts. Within a single server run only one entry path (RSC or Pages) is loaded, so thecreateRequirebase is consistent.- Passing
process.env(instead ofundefined) to Worker-stylefetch(request, env, ctx)is a sensible Node-side equivalent of the CF bindings object.
Minor observations (non-blocking)
-
No
hasSignificantPathPart/empty-specifier guard instringFromCharCodeValue.require(String.fromCharCode())folds torequire("")andrequire(String.fromCharCode(47))folds torequire("/"). The existingrequestHasStaticPartpath rejects a bare"/"(see line 565) and would emit the dynamic-request error instead. These are pathological inputs unlikely in real encoded imports, but for parity with the string path you may want toreturn nullwhen the resolved request is""or normalizes to"/", letting it fall through to the dynamic replacement rather than emitting an unresolvablerequire(""). -
globalThis.requireis never restored after the process sets it. Fine for a long-running prod server, but worth a one-line comment noting it's intentionally process-lifetime (the loader test has to save/restore it precisely because it leaks). Not a functional problem.
Nothing here blocks merge — the fold is conservative (fails closed) and the parity changes are correct. The two items above are optional hardening.
|
/bigbonk review for issues |
|
Review posted to PR #2749. SummaryI reviewed PR #2749 ( Verified correct:
Two non-blocking observations flagged in the review:
Neither blocks merge — the fold fails closed and the parity fixes are correct. |
3c95c7d to
9dff1a4
Compare
|
/bigbonk review for issues |
9dff1a4 to
bd6ec65
Compare
|
/bigbonk review for issues |
|
@james-elicx Bonk workflow failed. Check the logs for details. View workflow run · To retry, trigger Bonk again. |
|
UnknownError: ProviderInitError |
|
/bigbonk review for issues |
There was a problem hiding this comment.
Review — fix(server): support Node production entry contracts
Re-reviewed the current diff (note: it differs substantially from the earlier ask-bonk pass — the prod-server.ts change is now an AsyncLocalStorage-scoped globalThis.require dispatcher, not a plain assignment). Verified vp check is clean on both source files and the targeted suites pass locally (prod-server-entry-import, app-router-worker-entry, dynamic-requests-build → 55/55).
Correctness — looks good
String.fromCharCodefold gates correctly on:CallExpression→ non-computedMemberExpression→ unboundString(!hasAstBinding(scope, "String")) →fromCharCodeproperty → every arg an integerLiteralin[0, 0xffff]. Spread args and shadowedStringfall through to the existing dynamic path, both covered by tests. Overwriting only the argument range (not the wholeCallExpression) plus the earlyreturnavoids MagicString overlap with the nested node.- Empty/root parity is actually handled now. The
!== "/"guard sendsString.fromCharCode(47)back throughrequestHasStaticPart(→ dynamic replacement), andString.fromCharCode()folds torequire("")— which matches the literal string path (line 565 treats""as static too). The newdynamic-requests-build.test.tscase asserts exactly this (require("")+ oneMODULE_NOT_FOUND). So the earlier review's "parity concern #1" is resolved. - ALS dispatcher design is sound. I independently verified that
AsyncLocalStorage.run(store, () => import(url))propagates the store into the imported ESM module's top-level evaluation and isolates concurrent imports — so thePromise.allisolation the tests rely on is a real guarantee, not luck. Per-requestrequirecalls insidehandleRequestalso stay in-context because ALS survives awaits, andwaitUntilpromises are created in-context. - Only one entry (RSC or Pages) loads per
startProdServerprocess, so the single process-global dispatcher + entry-scoped resolver is consistent. - Passing
process.envto Worker-stylefetch(request, env, ctx)is the right Node analog of the CF bindings object. - No dev/CF parity gap: the fold lives in the shared
ignore-dynamic-requestsplugin (dev + build), and on Workers the encodedrequireis resolved statically at build time, so no runtimeglobalThis.requireprovisioning is needed there. Scoping the provisioning to the Node prod server is deliberate and correct.
Minor observations (non-blocking)
-
globalThis.requirebecomes a throwing dispatcher for the process lifetime. In a pure-ESM prod serverglobalThis.requirestartsundefined, soinheritedGlobalRequireisundefined. After the firstrunWithServerEntryRequire,globalThis.requireis permanently the dispatcher, which throws"require() was called outside a Node production server entry context"for any call made off an ALS context. That's a behavioral change from the priorTypeError: ... is not a function. Practically harmless (bundledrequireruns at load or in-request, both in-context), but a fully-detached callback (e.g. a baresetTimeoutin user code) would now get the vinext error instead. The existing comment covers the process-lifetime intent; consider a one-liner noting the out-of-context throw is intentional. -
runWithServerEntryRequirenever restoresglobalThis.require— fine for a long-lived server (the tests save/restore precisely because it leaks into the shared Vitest process).
Nothing here blocks merge. The fold fails closed, the ALS propagation is verified, and the parity items from the previous review are addressed.
|
Review posted to PR #2749. SummaryRe-reviewed PR #2749 ( Verified correct:
Two non-blocking observations flagged:
Nothing blocks merge — the fold fails closed and the ALS design is verified correct. |
bd6ec65 to
d265caa
Compare
Performance benchmarksCompared 0 improved · 0 regressed · 6 within ±1.5%
View detailed results and traces 🟢 improvement · 🔴 regression · ⚫ change below 1.5% · paired base/head |
Summary
String.fromCharCode(<literal code units>)inside CommonJS requests so statically encoded imports remain bundleableglobalThis.requirebefore evaluating Node production server entriesprocess.envto Worker-style handlers run by the Node production serverapp-basicproduction fixture, plus focused entry-loader and Worker-handler regressionsDownstream patch: nodejs/nodejs.org@30ca201#diff-f89185437eff5248b9d539ae73a8d6dd9b0836c5d1786c7dd71bae46e02f1f16
Validation
vp check packages/vinext/src/plugins/ignore-dynamic-requests.ts packages/vinext/src/server/prod-server.ts tests/dynamic-requests-build.test.ts tests/prod-server-entry-import.test.ts tests/app-router-worker-entry.test.ts tests/app-router-production-server.test.ts tests/fixtures/app-basic/app/char-code-requirevp test run tests/dynamic-requests-build.test.ts tests/prod-server-entry-import.test.ts tests/app-router-worker-entry.test.tsvp test run tests/app-router-production-server.test.ts -t "bundles a static CommonJS request encoded"vp run vinext#build