Skip to content

fix(app-router): ignore very dynamic requests during build analysis#1736

Open
NathanDrake2406 wants to merge 21 commits into
cloudflare:mainfrom
NathanDrake2406:nathan/dynamic-requests
Open

fix(app-router): ignore very dynamic requests during build analysis#1736
NathanDrake2406 wants to merge 21 commits into
cloudflare:mainfrom
NathanDrake2406:nathan/dynamic-requests

Conversation

@NathanDrake2406
Copy link
Copy Markdown
Contributor

@NathanDrake2406 NathanDrake2406 commented Jun 4, 2026

Overview

Field Details
Goal Match Next.js/Turbopack build behaviour for very dynamic import(...) and require(...) requests.
Core change Add a pre-transform that removes only requests with no static path part from build graph analysis.
Main boundary Static or partly static requests still flow through existing Vite/CommonJS analysis. Very dynamic requests get deterministic Next-like runtime failures if executed.
Primary files packages/vinext/src/plugins/ignore-dynamic-requests.ts, packages/vinext/src/index.ts, tests/build-optimization.test.ts
Expected impact App Router page and route builds no longer fail on guarded require(dynamic) / import(dynamic) patterns that Next.js accepts.

Why

App Router production builds must separate graph-analysis inputs from code that is only meaningful at runtime. Next.js/Turbopack treats requests with no static known path part as "very dynamic" and, with ignore_dynamic_requests, avoids adding them to the module graph. Vinext violated that boundary because vite-plugin-commonjs tried to expand require(dynamic) as a static glob, causing a build-time failure even when the code was unreachable.

Area Principle / invariant What this PR changes
App/user build analysis Very dynamic requests should not become required static graph edges. Rewrites only unbound one-argument require(...) calls and dynamic import(...) expressions that have no static request part.
Runtime shape Very dynamic requests should fail deterministically if executed. Emits a synchronous MODULE_NOT_FOUND throw for require(...) and a promise-shaped MODULE_NOT_FOUND rejection for import(...), matching Turbopack codegen shape.
Compatibility Analyzable requests and Vinext internals must keep existing behaviour. Leaves literals, empty literals, non-empty templates, expressions with static parts, wrong-arity require, local require bindings, and Vinext internal runtime modules untouched.

What changed

Scenario Before After
require(dynamic) with exactly one argument and no static path part Build failed in vite-plugin-commonjs with an invalid dynamic import error. Build succeeds; if executed, the expression throws MODULE_NOT_FOUND.
import(dynamic) with no static path part Vite attempted dynamic request analysis. Build succeeds; if executed, the expression returns a rejected promise with MODULE_NOT_FOUND.
require(), require(id, extra) Could be misclassified as ignored very-dynamic requests. Left untouched for the existing analyzer/error path.
require("./" + name) or import(./${name}) Existing static-part analysis path. Unchanged.
Maintainer review path
  1. packages/vinext/src/plugins/ignore-dynamic-requests.ts validates request classification, binding handling, runtime-failure codegen, and plugin module boundaries.
  2. packages/vinext/src/index.ts places the plugin immediately before vite-plugin-commonjs, which is the failing analyzer.
  3. tests/build-optimization.test.ts covers the production build boundary, scope/binding edge cases, TS wrappers, require arity, and Vinext internal runtime exclusion.
Validation
  • vp test run tests/build-optimization.test.ts -t ignoreVeryDynamicRequests
  • vp check packages/vinext/src/plugins/ignore-dynamic-requests.ts tests/build-optimization.test.ts
  • PLAYWRIGHT_PROJECT=pages-router vp exec playwright test tests/e2e/pages-router/router-events.spec.ts tests/e2e/pages-router/navigation.spec.ts --project=pages-router
  • Commit hook: formatting, lint/type checking, full tests/build-optimization.test.ts, and knip
Risk / compatibility
  • Public API impact: none.
  • Runtime impact: only very dynamic app/user request expressions are changed, and only to deterministic Next-like failure expressions.
  • Build output impact: static and partly static request expressions continue to use the existing analyzer path.
  • Framework compatibility: intentionally follows Next.js/Turbopack ignore_dynamic_requests behaviour for requests without a static known part, including the exact-one-argument require boundary.
Non-goals
  • This PR does not implement Turbopack broader handling for dynamic new URL, filesystem, or child process request patterns.
  • This PR does not change the existing CommonJS transform for analyzable request expressions.
  • This PR does not split the binding-aware scope walker into shared modules before another caller needs it.

References

Reference Why it matters
Next.js dynamic-requests e2e test Upstream contract: guarded dynamic requests in pages and routes should not error.
Next.js ignore_dynamic_requests option Documents the intended semantics for requests without a static known part.
Turbopack dynamic import(...) handling Shows very dynamic imports become runtime dynamic expressions when ignored.
Turbopack dynamic require(...) handling Shows the ignored very-dynamic require path and the exact-arity boundary.

Closes #1508

App Router production builds currently fail when a page or route module contains guarded require(dynamic) calls. That differs from Next.js, where requests with no static request part are ignored during graph analysis and only fail if executed at runtime.

The violated invariant was that bundler analysis should not turn unreachable, very dynamic requests into build-time failures. vite-plugin-commonjs tried to expand require(dynamic) as a static glob before the branch could remain runtime-only code.

Add a pre-transform that preserves directives, removes only very dynamic require calls from CJS graph analysis, and marks matching dynamic import calls with @vite-ignore. The regression builds both an App Router page and route module ported from Next.js dynamic-requests coverage.
@pkg-pr-new
Copy link
Copy Markdown

pkg-pr-new Bot commented Jun 4, 2026

Open in StackBlitz

npm i https://pkg.pr.new/@vinext/cloudflare@1736
npm i https://pkg.pr.new/vinext@1736

commit: a3f506f

…equest rewrite

- Parse .js/.mjs/.cjs as JSX so App Router files with JSX are not silently
  skipped before the JSX-in-JS transform runs.
- Track lexical bindings and only rewrite unbound/CommonJS-style require(...).
  Skips rewriting when require is declared as a local variable, function,
  parameter, import specifier, or catch binding.
- Unwrap TypeScript-transparent expression nodes (TSAsExpression,
  TSTypeAssertion, TSNonNullExpression, TSInstantiationExpression,
  ParenthesizedExpression, ChainExpression) before classifying static parts,
  ensuring expressions like require(('./' + name) as string) keep flowing
  through existing analysis.
- Add focused unit and integration tests for the three blocking cases.
@NathanDrake2406
Copy link
Copy Markdown
Contributor Author

All three blocking issues are valid. Implemented fixes and pushed.

Blocking issue 1 — .js with JSX: Valid. The plugin runs before vinext:jsx-in-js, so .js files containing JSX would fail to parse with lang: "js" and silently fall through. Fixed by returning lang: "jsx" for .js/.mjs/.cjs extensions in parserLangForId. Added integration test that builds an app/page.js with JSX + guarded dynamic requests.

Blocking issue 2 — name-based require matching: Valid. Rewriting every call whose callee is named require changes program semantics when the user has a local binding. Fixed by adding walkAstWithBindings, which tracks lexical scopes (function params, declarations, imports, catch bindings, destructuring patterns). isVeryDynamicRequireCall now checks isRequireBound() and skips rewriting when require is shadowed. Added unit tests for local const require, function parameter require, imported require, and function-named-require.

Blocking issue 3 — TS wrapper expressions hiding static parts: Valid. TSAsExpression, TSTypeAssertion, TSNonNullExpression, TSInstantiationExpression, ParenthesizedExpression, and ChainExpression were treated as unknown nodes, causing misclassification. Fixed by adding unwrapTransparentExpression that recurses through these wrappers before requestHasStaticPart evaluates the expression. Added unit tests for require(('./' + name) as string), import((`./${name}`) as string), and require(path!).

All new and existing tests in tests/build-optimization.test.ts and tests/cjs.test.ts pass. vp check is clean.

…uire shadowing

Rewrites walkAstWithBindings as a proper two-phase scope pass:

1. Collect hoisted declarations (FunctionDeclaration, var, imports) before visiting any expressions in a scope body.
2. Walk statements and transform expressions with the full binding context already in place.

This fixes two JavaScript-correctness issues:
- Function declarations are hoisted, so local require is now visible throughout its enclosing scope.
- Block-scoped let/const/class bindings push their own scope frames (BlockStatement, CatchClause, SwitchCase), so they never leak out and poison the parent scope.

Adds tests for hoisted function declaration named require and block-scoped require not leaking to outer scope.

Loosens the fast-path regex to handle comments between the callee and the paren.
… and exports

The previous two-phase pre-collection passed 'block' scope type when
descending into nested blocks from module/function scope.  This caused
two bugs:

1. TDZ false negative: a 'const require' at the top level of a function
   body was not collected, so a 'require(id)' before the declaration was
   incorrectly rewritten even though the function-scope const shadows
   the global.  Fix: for function scope, collect all var kinds (let,
   const, var) at the top level of the function body — the function body
   itself is the enclosing block, so let/const there ARE function-scoped.

2. Block shadow leak: a 'const require' inside an 'if' or other nested
   block was being promoted into the enclosing module/function scope
   via the block-scope descent, so a 'require(id)' outside the block
   was incorrectly skipped.  Fix: when descending from module/function
   scope into nested blocks, only collect 'var' (the only kind that
   hoists out of a block into the enclosing function/module scope).

The new logic splits the two concerns:
- collectDeclarationsForScope handles the top level of a scope and
  delegates to collectVarDeclarationsFromNestedBlocks when descending
  into blocks from module/function scope.
- collectVarDeclarationsFromNestedBlocks walks into blocks looking
  ONLY for 'var' declarations (stopping at nested function boundaries).

Also adds three regression tests:
- const binding in function body shadows global require (TDZ)
- exported function declaration named require
- exported const binding named require
…lexical scope

Two scope-analysis gaps surfaced by review:

1. ForStatement / ForInStatement / ForOfStatement with a let/const
   header introduced a per-iteration lexical scope that covered
   init/test/update/body (or left/right/body) but was not modeled.
   A 'const require' in a 'for (...)' header was therefore invisible
   inside the loop body, so a 'require(id)' in the body was
   incorrectly rewritten even though it resolved to the loop binding.

   Fix: when the header is a VariableDeclaration with kind 'let' or
   'const', push a new scope, pre-collect those bindings (including
   destructuring patterns via addBindings), and walk init/test/update/
   body under that scope. 'var' headers remain function-scoped and are
   already collected by the enclosing function-scope pre-pass, so no
   extra scope is pushed for them.

2. SwitchStatement cases share the lexical scope of the SwitchStatement
   itself unless the user wraps a case body in explicit braces. The
   walker previously created a fresh scope per SwitchCase, so a
   'const require' in a later case did not shadow a 'require(id)' in an
   earlier case (the earlier case saw an empty scope and the call was
   rewritten).

   Fix: SwitchStatement now pushes one shared scope, pre-collects
   'let'/'const'/'class'/'function' from every case consequent at its
   top level, and walks each case's consequent under that same shared
   scope. The standalone SwitchCase handler is retained as a defensive
   fallback that walks the consequent without pushing a scope, so a
   stray SwitchCase (parser edge case) cannot double-scope.

Adds the three suggested regression tests:
- for-of lexical binding shadows global require
- for-initializer lexical binding shadows global require
- switch-wide lexical binding shadows require in a prior case
… static block scope

Two traversal gaps surfaced by review:

1. Function parameter default RHS expressions were never walked.
   'addBindings' intentionally records binding names but does not
   traverse default-value expressions, so an executable
   'require(dynamic)' or 'import(dynamic)' hidden in a parameter
   default was invisible to the pre-transform. That left the original
   build-analysis failure mode alive in code like:

     function load(x = require(id)) { return x; }

   Fix: after collecting parameter bindings into the function scope,
   walk each parameter node. Because the bindings are already in
   scope, a default like 'function f(require = require(id))' is
   correctly skipped (the parameter binding named 'require' is the
   resolved binding for the RHS call), while 'function load(x =
   require(id))' is rewritten normally.

2. Class static blocks ('static { ... }') introduce their own
   lexical scope. The generic walker previously treated them as
   ordinary object recursion, so a 'const require' inside a static
   block did not shadow an earlier 'require(id)' in the same block
   (the earlier call was rewritten instead of resolving to the local
   binding via TDZ).

   Fix: handle 'StaticBlock' as a block scope. Push a new scope,
   pre-collect 'let'/'const'/'class'/'function' from the static block
   body, walk the body, and pop. 'var' declarations in static blocks
   still hoist to the enclosing function scope and are collected by
   the function-scope pre-pass.

Adds the suggested regression tests:
- unbound require(dynamic) in a function parameter default is rewritten
- unbound import(dynamic) in a function parameter default gets
  @vite-ignore
- parameter default that resolves to a parameter named require is NOT
  rewritten
- require shadowed by a class static block lexical binding is NOT
  rewritten
…antics

Class static blocks ('static { ... }') have var-as-static-block
semantics: a 'var' declared inside a static block is scoped to the
static block itself, not hoisted to the enclosing function/module.
This differs from ordinary 'var', which is function-scoped.

The previous 'StaticBlock' handling reused the 'block' scope type,
which only pre-collects 'let'/'const' (not 'var'). That meant a
'var require' inside a static block was invisible to the static
block scope, so an earlier 'require(id)' in the same static block
was incorrectly rewritten even though it resolves to the local
'var require' binding (TDZ/undefined at runtime, not global
CommonJS require).

Fix: introduce a distinct 'static-block' scope type. It behaves
like 'function' for collection purposes — collects 'var'/'let'/
'const'/'class'/'function' at the top level and hoists 'var' from
nested control-flow blocks — but the name makes the intent clear
and keeps the two cases from being conflated.

The function/module scope pre-pass is unaffected: 'var' inside a
static block stays in the static block scope and never leaks to
the enclosing function/module scope, because the function-scope
pre-pass already does not recurse into class bodies or static
blocks.

Adds the suggested regression tests:
- 'var require' inside a static block shadows an earlier
  'require(id)' in the same static block
- 'var require' inside a static block does NOT leak to an
  outer 'require(id)' at the module level
…dup walker

Two correctness follow-ups and a verbosity cleanup for the
binding-aware walker.

## Fixes

### Named class expressions
A named class expression (e.g. `const C = class require { static {
require(id) } }`) binds its name in the class's own scope, and
static blocks inside see that name (per ES2022).  The walker was
falling through to the generic recursion for ClassExpression, so a
`require` static block binding was not detected.  Add an explicit
ClassExpression handler that:

  * Walks `superClass` and decorators in the enclosing scope
    (they are evaluated before the class is bound).
  * Pushes a new scope, binds the class name, walks the body, and
    pops the scope.  Methods don't see the class name, but they
    create their own function scopes when walked, so the extra
    class scope is harmless for them.
  * For unnamed ClassExpression, just walks the body in the
    enclosing scope (the generic recursion path).

### TS `declare` declarations
A `declare const require: unknown` (or `declare function`,
``declare class`, etc.) is type-only and has no runtime binding.
OXC reports it with `declare: true` on the declaration node.  The
walker was treating it as a runtime binding and skipping the
rewrite, leaving the call for the CJS plugin.  Add an early
`node.declare === true` return in `collectDeclarationsForScope`
so type-only declarations never shadow the global `require`.

## Refactor
  * Hoist `SCOPE_RECURSE_INTO` (the block-introducing node set)
    to module level.  It was duplicated in
    `collectVarDeclarationsFromNestedBlocks` and
    `collectDeclarationsForScope`.
  * Add `forEachChild` helper for the generic child-iteration
    pattern.  Was duplicated in
    `collectVarDeclarationsFromNestedBlocks` and the fallback
    branch of `walkNode`.
  * Add `isFunctionLike`, `isExportWrapper`, `isHoistingScope`
    helpers.  The 3-clause OR for function-like and the
    export-wrapper type check were duplicated in 2-3 places each.
  * Add `ScopeType` type alias.  Replaces 4-fold union repetition
    in function signatures.

Net effect: ~50 lines of duplication removed, ~60 lines of named
helpers (with comments) added.  The walker is shorter overall and
the intent of each scope-introducing case is clearer.

## Tests
  * Named class expression shadows an earlier `require(id)` in
    the same static block.
  * Named class expression with `extends` and a method named
    `require` still shadows correctly.
  * `declare const require: unknown; require(id)` rewrites the
    call to the runtime helper (type-only declare does not
    shadow the global require).
  * Same for `declare function require` and `declare class
    require`.

All 33 `ignoreVeryDynamicRequests` tests pass (was 28).
@NathanDrake2406
Copy link
Copy Markdown
Contributor Author

Addressed the latest REQUEST_CHANGES review and pushed 5b3e9406.

Assessment: all three comments were valid.

  1. var require inside catch bodies: valid. The hoist pre-pass now recurses through CatchClause, so var require in a catch body binds the enclosing hoisting scope. Added regressions for both the in-catch call and the post-catch call.

  2. import type runtime binding: valid. Runtime scope collection now skips ImportDeclaration nodes with importKind === "type" and individual specifiers with importKind === "type", while preserving value imports. Added regressions for full type-only imports and mixed type/value imports.

  3. satisfies transparent wrapper: valid. TSSatisfiesExpression now unwraps through the same path as as, non-null, parenthesized, and instantiation wrappers. Added regressions for both require and dynamic import with static path parts.

Also merged current upstream/main to use the new shared ast-utils and replaced the plugin-local AST range/child/binding helpers with that utility.

Verification:

  • vp test run tests/build-optimization.test.ts -t "catch-body|import type|type-only import specifier|satisfies" passed: 6 tests.
  • vp check tests/build-optimization.test.ts packages/vinext/src/plugins/ignore-dynamic-requests.ts passed: format, lint, type checks clean.
  • vp test run tests/build-optimization.test.ts passed: 123 tests.

@NathanDrake2406
Copy link
Copy Markdown
Contributor Author

Addressed the latest REQUEST_CHANGES review and pushed 41e776f4.

Assessment: both comments were valid.

  1. var require under a labelled statement: valid. Labels do not introduce a new scope, but the var-hoist pre-pass was not recursing through LabeledStatement. Added it to the hoist recursion set and added regressions for the in-labelled-block call and the post-labelled-block call.

  2. Empty string literals: valid. require("") and import("") are statically known literal requests, not very dynamic requests. requestHasStaticPart() now treats any string literal as static while leaving the non-empty template-quasi boundary unchanged. Added regressions for both forms.

Verification:

  • Red check before fix: vp test run tests/build-optimization.test.ts -t "labelled-block|static literal request" failed on the four new regressions.
  • After fix: same command passed, 4 tests.
  • vp test run tests/build-optimization.test.ts passed, 127 tests.
  • vp check tests/build-optimization.test.ts packages/vinext/src/plugins/ignore-dynamic-requests.ts passed: format, lint, and type checks clean.

@NathanDrake2406 NathanDrake2406 marked this pull request as ready for review June 5, 2026 16:01
@james-elicx
Copy link
Copy Markdown
Member

/bigbonk review for issues

1 similar comment
@james-elicx
Copy link
Copy Markdown
Member

/bigbonk review for issues

@ask-bonk
Copy link
Copy Markdown
Contributor

ask-bonk Bot commented Jun 5, 2026

@james-elicx Bonk workflow was cancelled.

View workflow run · To retry, trigger Bonk again.

@james-elicx
Copy link
Copy Markdown
Member

/bigbonk review for issues

@james-elicx
Copy link
Copy Markdown
Member

/bigbonk re-review for issues

1 similar comment
@james-elicx
Copy link
Copy Markdown
Member

/bigbonk re-review for issues

@james-elicx
Copy link
Copy Markdown
Member

/bigbonk re-review for issues

@NathanDrake2406
Copy link
Copy Markdown
Contributor Author

@codex adversarial review

@chatgpt-codex-connector
Copy link
Copy Markdown

To use Codex here, create an environment for this repo.

@NathanDrake2406
Copy link
Copy Markdown
Contributor Author

@codex review

@chatgpt-codex-connector
Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Delightful!

ℹ️ About Codex in GitHub

Your team has set up Codex to 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 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@james-elicx
Copy link
Copy Markdown
Member

/bigbonk re-review for issues

@james-elicx
Copy link
Copy Markdown
Member

/bigbonk review this pr for issues

@james-elicx
Copy link
Copy Markdown
Member

/bigbonk review for issues

@james-elicx
Copy link
Copy Markdown
Member

/bigbonk review for issues

@james-elicx
Copy link
Copy Markdown
Member

/bigbonk re-review for issues

Copy link
Copy Markdown
Contributor

@ask-bonk ask-bonk Bot left a comment

Choose a reason for hiding this comment

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

Re-review for issues. The scope/binding analysis is impressively thorough and the prior review rounds resolved the substantive concerns (JSX-in-.js parsing, require shadowing across function/block/catch/static-block/labelled/switch scopes, TS transparent wrappers, declare/import type runtime-binding handling, empty-literal classification). I verified the full ignoreVeryDynamicRequests suite passes (49 tests) and vp check is clean on the plugin.

I ran a few additional adversarial probes against the actual parser. A couple of edge cases rewrite where the documented ignore_dynamic_requests boundary arguably shouldn't, but all are low-severity and rare. No blocking issues found. Details inline.

Comment thread packages/vinext/src/plugins/ignore-dynamic-requests.ts
Comment thread packages/vinext/src/plugins/ignore-dynamic-requests.ts
Comment thread packages/vinext/src/plugins/ignore-dynamic-requests.ts Outdated
Comment thread packages/vinext/src/plugins/ignore-dynamic-requests.ts
@ask-bonk
Copy link
Copy Markdown
Contributor

ask-bonk Bot commented Jun 6, 2026

Review submitted successfully. Here's a summary of my re-review of PR #1736:

Re-review summary

The PR adds a vinext:ignore-dynamic-requests pre-transform that mirrors Next.js/Turbopack's ignore_dynamic_requests behavior, rewriting "very dynamic" require(...)/import(...) calls (no static path part) into deterministic MODULE_NOT_FOUND failure expressions before vite-plugin-commonjs chokes on them.

Verification performed:

  • Ran the full ignoreVeryDynamicRequests suite via vp — 49 tests pass
  • vp check clean on the plugin
  • Confirmed prior review rounds resolved the substantive concerns (JSX-in-.js parsing, require shadowing across all scope kinds, TS transparent wrappers, declare/import type runtime-binding handling, empty-literal classification)
  • Ran adversarial parser probes for spread args, optional calls, with blocks, member-call require, tagged templates, sequence expressions

Findings (all non-blocking):

  1. Spread argumentrequire(...args) gets rewritten to a throw, but a SpreadElement isn't a single positional request string per Turbopack's arity boundary. Suggested a guard in singleArgument.
  2. with blockrequire(id) inside with is rewritten even though require could resolve dynamically; effectively dead code since with is illegal in ESM. Flagged for completeness.
  3. Source map source — generated with un-cleaned id while parse used cleanModuleId(id); cosmetic.
  4. Dev/build parity — confirmed the lack of apply: "build" is correct (the analyzer it guards also runs in dev); suggested a comment so it's not "fixed" later.

No blocking issues. The scope/binding analysis is thorough and well-tested.

github run

@james-elicx
Copy link
Copy Markdown
Member

/bigbonk review for issues

1 similar comment
@james-elicx
Copy link
Copy Markdown
Member

/bigbonk review for issues

Copy link
Copy Markdown
Contributor

@ask-bonk ask-bonk Bot left a comment

Choose a reason for hiding this comment

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

Re-review for issues

Went through the plugin, the scope/binding walker, the failure-expression codegen, and the plugin ordering in index.ts. The implementation is thorough and the prior review rounds resolved the substantive concerns (JSX-in-.js parsing ordering, require shadowing across function/block/catch/static-block/labelled/switch/for-header scopes, TS transparent wrappers, declare/import type runtime-binding handling, empty-literal classification, SpreadElement arity, source-map source, dev/build parity comment). I confirmed the new plugin sits before vinext:jsx-in-js (both enforce: "pre", runs in array order) which is why parsing .js as jsx in this plugin is necessary and correct.

No blocking issues. A few non-blocking observations below for completeness — all are on unreachable/failing paths and match Turbopack's ignore_dynamic_requests framing, so they may be intentional.


function isVeryDynamicImportExpression(node: AstRecord): boolean {
if (node.type !== "ImportExpression") return false;
return !requestHasStaticPart(toAstRecord(node.source));
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Non-blocking: isVeryDynamicImportExpression rewrites the entire ImportExpression, which discards the second argument of import(id, options) (import attributes / with). Since the expression is replaced by a rejected promise, any side effects in options are dropped. This only happens on the failing path (the import is being ignored anyway), so it matches the Turbopack "very dynamic, fails at runtime" framing — flagging for completeness, not as a defect.

Same applies to a SequenceExpression request like require((sideEffect(), id)): it has no static part, so it's rewritten and the sideEffect() is dropped. Again only on the failing path.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Leaving this unchanged intentionally. For the ignored very-dynamic path, the contract is to replace the whole request expression with the deterministic runtime failure expression. That matches the Turbopack-style framing: no graph edge is created and execution fails at that expression rather than evaluating subexpressions that are part of an unanalyzable request. Preserving option/request side effects would make this branch less like a direct expression replacement and would add complexity for a path whose observable result is failure.

return MODULE_EXTENSIONS.has(path.extname(cleanId));
}

export function mayContainVeryDynamicRequestTarget(code: string): boolean {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Minor: the mayContainVeryDynamicRequestTarget fast-path regex matches import( even inside string literals or comments (e.g. a doc comment mentioning import(...)). That only forces an extra parseAst pass for those files — the AST walk then finds nothing to rewrite and returns null — so it's a perf nit, not a correctness issue. Fine as-is.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Leaving this as-is. The regex is only a cheap precheck to avoid parsing ordinary static-import modules; false positives in comments/strings pay one parse and then the AST walk returns null. Narrowing this further would either become a lexer-lite path or risk missing real call-form targets, so the current cost/correctness boundary is intentional.

@ask-bonk
Copy link
Copy Markdown
Contributor

ask-bonk Bot commented Jun 7, 2026

Review submitted. Here's a summary:

Re-review summary for PR #1736

The PR adds a vinext:ignore-dynamic-requests pre-transform that mirrors Next.js/Turbopack's ignore_dynamic_requests, rewriting "very dynamic" require(...)/import(...) calls (no static path part) into deterministic MODULE_NOT_FOUND failure expressions before vite-plugin-commonjs chokes on them.

What I verified:

  • The scope/binding walker (walkAstWithBindings) correctly tracks require shadowing across module/function/block/catch/static-block/labelled/switch/for-header scopes, var-hoisting, named class/function expressions, and declare/import type (type-only, no runtime binding).
  • TS transparent wrappers (as, satisfies, non-null, parenthesized, instantiation, chain) are unwrapped before static-part classification.
  • Plugin ordering: the new plugin runs before vinext:jsx-in-js (both enforce: "pre", array order), which is why it parses .js as jsx itself — correct and tested.
  • require.resolve(...), wrong-arity require, SpreadElement, empty-string literals, and node_modules are all correctly excluded.
  • Prior review rounds resolved every substantive concern (verified against the resolved comments in commit a3f506f).

Findings: No blocking issues. I left two non-blocking notes:

  1. import(id, options) / SequenceExpression requests drop the second arg / side effects when rewritten — but only on the failing path, matching Turbopack framing.
  2. The fast-path regex matches import( in strings/comments — a perf nit, not a correctness issue.

The implementation is high quality, the test coverage is extensive, and the adversarial review concerns from earlier rounds were all addressed.

github run

@james-elicx
Copy link
Copy Markdown
Member

/bigbonk review for issues

5 similar comments
@james-elicx
Copy link
Copy Markdown
Member

/bigbonk review for issues

@james-elicx
Copy link
Copy Markdown
Member

/bigbonk review for issues

@james-elicx
Copy link
Copy Markdown
Member

/bigbonk review for issues

@james-elicx
Copy link
Copy Markdown
Member

/bigbonk review for issues

@james-elicx
Copy link
Copy Markdown
Member

/bigbonk review for issues

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.

Build failure: dynamic-requests test app fails to build

2 participants