Skip to content

fix(build): bundle dynamic URL imports - #2893

Open
james-elicx wants to merge 13 commits into
mainfrom
codex/cover-react-version-conditions
Open

fix(build): bundle dynamic URL imports#2893
james-elicx wants to merge 13 commits into
mainfrom
codex/cover-react-version-conditions

Conversation

@james-elicx

Copy link
Copy Markdown
Member

Summary

  • normalize literal import(new URL("./module", import.meta.url).href) dependencies before vinext's generic dynamic-request fallback
  • add Vite RSC + Wrangler coverage for React export conditions across App server/client pages, App route handlers, Pages pages, and Pages APIs
  • classify the exact react-version suite as supported now that all assertions run and pass

Root cause

The generic dynamic-request transform ran before Vite could recognize the literal module URL. It replaced the import with vinext's deterministic MODULE_NOT_FOUND fallback, so the Next.js suite failed during prerender before any React-condition assertion ran.

Validation

  • vp test run tests/import-meta-url.test.ts tests/compatibility-support.test.ts (50 passed)
  • PLAYWRIGHT_PROJECT=cloudflare-encoded-paths pnpm run test:e2e -- react-conditions.spec.ts (5 passed against Wrangler)
  • REPO="$(pwd)" NEXTJS_DIR="/Users/jamesanderson/Developer/vinext/.nextjs-ref" NEXT_TEST_CONCURRENCY=1 ./scripts/run-targeted-nextjs-e2e.sh test/e2e/react-version/react-version.test.ts (5 passed)
  • targeted vp check (clean)
  • vp run knip (clean)

Refs the non-cache failures from Actions run 31439707085.

@pkg-pr-new

pkg-pr-new Bot commented Aug 11, 2026

Copy link
Copy Markdown

Open in StackBlitz

npm i https://pkg.pr.new/@vinext/cloudflare@2893
npm i https://pkg.pr.new/create-vinext-app@2893
npm i https://pkg.pr.new/@vinext/types@2893
npm i https://pkg.pr.new/vinext@2893

commit: 08819bc

@github-actions

Copy link
Copy Markdown
Contributor
Example Preview Production Original
app-router-cloudflare preview production
pages-router-cloudflare preview production
app-router-playground preview production original
realworld-api-rest preview production
nextra-docs-template preview production
benchmarks preview production
hackernews preview production original
workers-cache preview production
web preview production

@james-elicx

Copy link
Copy Markdown
Member Author

/bigbonk review for issues

@github-actions

github-actions Bot commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

Performance benchmarks

Compared 08819bc against base d558d62 using alternating same-runner rounds. Next.js was unchanged and skipped.

0 improved · 0 regressed · 6 within ±1.5%

Scenario Framework Baseline Current Change
Client bundle size (gzip) vinext 135.2 KB 135.2 KB ⚫ -0.0%
Client entry size (gzip) vinext 122.7 KB 122.7 KB ⚫ -0.0%
Dev server cold start vinext 3.09 s 3.08 s ⚫ -0.3%
Production build time vinext 3.38 s 3.41 s ⚫ +1.1%
RSC entry closure size (gzip) vinext 114.2 KB 114.2 KB ⚫ -0.0%
Server bundle size (gzip) vinext 193.0 KB 193.0 KB ⚫ 0.0%

View detailed results and traces

🟢 improvement · 🔴 regression · ⚫ change below 1.5% · paired base/head

@ask-bonk ask-bonk Bot left a comment

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.

Review: fix(build): bundle dynamic URL imports

Reviewed the new vinext:dynamic-import-url pre-transform plugin, the index.ts wiring, the ported react-version fixtures/E2E, and the compatibility-support reclassification. This is a focused, well-tested change and I could not find any correctness issues. Targeted tests pass locally (vp test run tests/import-meta-url.test.ts tests/compatibility-support.test.ts → 52 passed) and vp check on the plugin is clean.

What I verified (all correct)

  • Plugin ordering is the crux of the fix and is right. createDynamicImportUrlPlugin() is enforce: "pre" and is placed in the plugin array before createIgnoreDynamicRequestsPlugin (also pre), so the literal new URL(...).href import is normalized to a bare relative specifier before the generic dynamic-request fallback can replace it with the MODULE_NOT_FOUND runtime error. Array position between two pre plugins is what determines this, and it's correct here.
  • Specifier guarding is conservative and semantically safe. getDynamicImportUrlSpecifier only rewrites when the first new URL arg is a string literal starting with .//../, there are exactly 2 args, and the base is import.meta.url (or the optional-chained form). Bare specifiers ("foo/bar.js") and absolute specifiers ("/style.css") are correctly rejected — both would change resolution semantics if rewritten to import(...). The "preserves non-literal and absolute" test locks this in.
  • new URL detection matches Vite's own scope. isNewUrlExpression only matches bare URL, not globalThis.URL/window.URL, consistent with Vite's asset detection and the existing import.meta.url handling in this file.
  • Parser-language selection is consistent with the repo. parserLanguageForModuleId (jsx fallback, ts for .ts/.mts/.cts, tsx for .tsx) mirrors parserLanguage in middleware-export-validation.ts. The added it.each raw-ts/tsx cases guard the 4ab0128 fix where parsing plain-.js as the default failed on type-annotated .ts/JSX sources.
  • CSS fixture placement is safe. pages/api/react-conditions.css will not be picked up as an API route — the pages/api scanner filters through createValidFileMatcher(pageExtensions), which excludes .css.

Non-blocking observations

  1. collectDynamicImportUrlSpecifiers early-returns without visiting import.meta.url inside the matched new URL. This is fine (a matched literal specifier can't contain a nested rewritable dynamic import), just noting the intent: once an ImportExpression matches, its subtree is skipped. Nested import() inside a non-matching expression is still visited via forEachAstChild, which is what we want.
  2. Import attributes are preserved by construction. Because only the source-argument node range (.source) is overwritten, an import(new URL(...).href, { with: { type: "json" } }) keeps its options. No handling needed, but worth being aware there's no node.options guard here (unlike extensionless-dynamic-import.ts), and that's acceptable.
  3. Documented semantic shift (URL import → bundled specifier). import(new URL(...).href) resolves a runtime URL string in Next.js, whereas import("./x") goes through the bundler graph. That's exactly the intended behavior for a bundled Workers deployment and the code comment explains it well — flagging only so future readers don't mistake it for an accidental divergence.

Suggestion (optional)

The compatibility reclassification (react-version → supported, NON_SUPPORTED_SUITES 33→32, needs-vite-equivalent 3→2) is asserted in tests/compatibility-support.test.ts, but those hardcoded counts are brittle across unrelated future edits. Not a change to make here — just a standing maintenance cost of the count-based assertions.

LGTM. CI is green.

@ask-bonk

ask-bonk Bot commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

Review posted to PR #2893.

Summary

I reviewed the changes in PR #2893 (fix(build): bundle dynamic URL imports) and found no correctness issues. The change is focused and well-tested.

What the PR does: Adds a new enforce: "pre" plugin (vinext:dynamic-import-url) that normalizes import(new URL("./x", import.meta.url).href)import("./x") before the generic dynamic-request fallback replaces it with a MODULE_NOT_FOUND runtime error. Plus ported react-version fixtures/E2E and a compatibility-suite reclassification.

Key things I verified as correct:

  • Plugin ordering is the crux — the new pre-plugin is placed before createIgnoreDynamicRequestsPlugin in the array, so normalization wins.
  • Specifier guarding is conservative: only literal .//../ specifiers with import.meta.url base are rewritten; bare and absolute specifiers correctly rejected (they'd change resolution semantics).
  • parserLanguageForModuleId matches the established pattern in middleware-export-validation.ts.
  • The pages/api/react-conditions.css fixture won't be misinterpreted as an API route (scanner filters by page extensions).

Non-blocking notes I raised: the intentional URL→bundled-specifier semantic shift (well-documented in the code), the absence of an import.options guard (acceptable — options are preserved by construction), and the brittleness of the hardcoded compatibility counts in compatibility-support.test.ts.

Verified locally: targeted tests pass (52 passed), vp check clean, and CI is green.

github run

@james-elicx
james-elicx marked this pull request as ready for review August 11, 2026 05:00
@james-elicx
james-elicx marked this pull request as draft August 11, 2026 05:42
@james-elicx

Copy link
Copy Markdown
Member Author

/bigbonk review for issues

@ask-bonk ask-bonk Bot left a comment

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.

Review: fix(build): bundle dynamic URL imports

I independently reviewed the new vinext:dynamic-import-url pre-transform, the index.ts wiring, the ported react-version fixtures/E2E, and the compatibility reclassification. No correctness or security issues found — this is a focused, well-tested change. Targeted suites pass locally (tests/import-meta-url.test.ts 49 passed, tests/compatibility-support.test.ts 4 passed), and I verified the edge-case behavior with an out-of-band script.

Verified correct

  • Ordering is the fix and it is right. createDynamicImportUrlPlugin() (enforce: "pre") is placed before createIgnoreDynamicRequestsPlugin (also pre) in the array, so the literal new URL(...).href import is normalized to a relative specifier before the generic dynamic-request fallback can replace it with the MODULE_NOT_FOUND runtime error. Between two pre plugins, array position decides, and it is correct here.
  • Specifier guarding is conservative and safe. getDynamicImportUrlSpecifier rewrites only when arg0 is a string literal starting with .//../, there are exactly 2 args, and arg1 is import.meta.url (or the optional-chained form). Bare and absolute (/style.css) specifiers are correctly rejected — both would change resolution semantics. The "preserves non-literal and absolute" test locks this in.
  • Import options are preserved by construction. Only the .source node range is overwritten, so import(new URL(...).href, { with: { type: "json" } }) keeps its options. I confirmed this directly: import("./a.js", { with: { type: "json" } }).
  • Nested and multiple imports handled. I confirmed foo(import(new URL(...).href)) and Promise.all([import(...), import(...)]) both rewrite every occurrence. collectDynamicImportUrlSpecifiers early-returning after a match only skips a matched literal's subtree (which can't contain a rewritable import), and still visits siblings.
  • parserLanguageForModuleId matches the repo pattern in middleware-export-validation.ts, and the added raw-ts/tsx it.each cases guard the 4ab0128 parse-by-language fix.
  • CSS fixture placement is safe. pages/api/react-conditions.css will not be picked up as an API route — the pages/api scanner filters through createValidFileMatcher(pageExtensions), which excludes .css.

Non-blocking observations

  1. code filter misses import(/* comment */ new URL(...)). The filter regex /\bimport\s*\(\s*new\s+URL\s*\(/ does not tolerate a comment between import( and new URL( (newlines are fine). In that case the pre-transform is skipped and ignore-dynamic-requests — which parses the AST and ignores comments — would still replace the expression with the MODULE_NOT_FOUND fallback. The underlying function rewriteDynamicImportUrls handles the comment form correctly (I verified), so only the filter is narrower than the implementation. This is an unusual source pattern and not a regression, just an incomplete corner of the fix. Consider relaxing the filter to /\bimport\s*\(/ (still cheap) if you want the fix to cover it.
  2. Client-environment behavior now changes too. The new plugin has no environment guard, so it rewrites user client code as well. Previously ignore-dynamic-requests left user client code untouched (it only transforms node_modules there). Rewriting import(new URL(...).href)import("./x") in the client build is consistent with the intended bundle-the-module semantics and matches what the server does, so this is reasonable — flagging only so the uniform behavior is a conscious choice rather than incidental.
  3. Documented semantic shift (URL import → bundled specifier). import(new URL(...).href) resolves a runtime URL string in Next.js, whereas import("./x") goes through the bundler graph. That is exactly the intended behavior for a bundled Workers deployment and the code comment explains it well.
  4. Brittle count assertions. The compatibility reclassification asserts hardcoded totals (NON_SUPPORTED_SUITES 33→32, needs-vite-equivalent 3→2) in tests/compatibility-support.test.ts. Not a change to make here — a standing maintenance cost of count-based assertions across unrelated future edits.

LGTM.

@ask-bonk

ask-bonk Bot commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

Review posted to PR #2893.

Summary

I reviewed PR #2893 (fix(build): bundle dynamic URL imports) and found no correctness or security issues. The change is well-scoped and well-tested. Verified locally: tests/import-meta-url.test.ts (49 passed) and tests/compatibility-support.test.ts (4 passed), plus out-of-band edge-case verification of the rewrite function.

What the PR does: Adds an enforce: "pre" plugin (vinext:dynamic-import-url) that normalizes import(new URL("./x", import.meta.url).href)import("./x") before the generic dynamic-request fallback replaces it with a MODULE_NOT_FOUND runtime error. Plus ported react-version fixtures/E2E and a compatibility reclassification.

Key things I verified as correct:

  • Plugin ordering (the crux) — pre-plugin is placed before createIgnoreDynamicRequestsPlugin so normalization wins.
  • Conservative specifier guarding (only literal .//../ with import.meta.url base; bare/absolute rejected).
  • Import options preserved by construction, nested/multiple imports handled correctly.
  • .css fixture in pages/api/ won't be misread as an API route (scanner filters by page extensions).

Non-blocking notes I raised:

  1. The code filter regex misses import(/* comment */ new URL(...)) (the underlying function handles it; only the filter is narrower). Suggested relaxing to /\bimport\s*\(/.
  2. The plugin now rewrites client-environment user code too (previously untouched by ignore-dynamic-requests) — consistent and reasonable, flagged for awareness.
  3. The documented URL→bundled-specifier semantic shift.
  4. Brittle hardcoded compatibility counts in compatibility-support.test.ts.

LGTM.

github run

@james-elicx

Copy link
Copy Markdown
Member Author

Independent review of immutable head 9867589a5fe6088e3a14dc81ecb2e543519cc625: NO FINDINGS.

Validation evidence:

  • Focused unit/build coverage: 53/53 passed
  • Exact Next.js test/e2e/react-version/react-version.test.ts: 5/5 passed
  • Cloudflare Worker react-conditions.spec.ts: 5/5 passed

@james-elicx

Copy link
Copy Markdown
Member Author

/bigbonk review for issues

@ask-bonk ask-bonk Bot left a comment

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.

Review: fix(build): bundle dynamic URL imports

Independently reviewed the new vinext:dynamic-import-url pre-transform, the index.ts wiring, the ported react-version fixtures/E2E, and the compatibility reclassification. No correctness or security issues found — the fix is well-scoped and well-tested.

Verified locally on head 03f3980:

  • tests/import-meta-url.test.ts + tests/compatibility-support.test.ts53 passed
  • vp check packages/vinext/src/plugins/import-meta-url.ts → clean
  • Out-of-band edge-case probing of rewriteDynamicImportUrls (see below)

Verified correct

  • Ordering is the fix and it's right. createDynamicImportUrlPlugin() (enforce: "pre") is placed in the plugin array before createIgnoreDynamicRequestsPlugin (also pre), so the literal new URL(...).href import is normalized to a relative specifier before the generic dynamic-request fallback replaces it with the MODULE_NOT_FOUND runtime error. Between two pre plugins array position decides, and it's correct.
  • Conservative, correctly-scoped rewriting. I probed the matcher directly. It rewrites only import(new URL("./x", import.meta.url).href) with a .//../ string literal, exactly 2 args, and import.meta.url (or import.meta?.url) base. It correctly leaves alone: ?.href optional-chained property, .toString(), computed ["href"], 3-arg new URL, template-literal specifiers, non-literal specifiers, and absolute /x specifiers. Critically, it does not touch bare new Worker(new URL("./worker.js", import.meta.url)) or const u = new URL(...).href; import(u) — only the exact import(<newURL>.href) shape. Import options are preserved (import("./a.js", { with: { type: "json" } })), and nested/multiple occurrences (Promise.all([...])) all rewrite.
  • Comment-form covered. import(/* webpackMode: "eager" */ new URL(...).href) is handled by both the code: /\bimport\s*\(/ filter and rewriteDynamicImportUrls; the build-integration test locks this in.
  • parserLanguageForModuleId matches the repo pattern in middleware-export-validation.ts; the raw-ts/tsx it.each cases guard the 4ab0128 parse-by-language fix.
  • Fixtures are a genuine integration test. pages/api/pages-react-conditions.ts carries the real import(new URL("./react-conditions.css", import.meta.url).href), and the cloudflare-encoded-paths Playwright project builds cf-app-basic with vp build + wrangler dev, so the fix is exercised end-to-end against Workers. The .css sibling won't be misread as an API route (pages/api scanner filters through createValidFileMatcher(pageExtensions)).

Non-blocking observations

  1. Plugin lacks the environment/scope guards its sibling has — flagging for a conscious decision, not a bug. createIgnoreDynamicRequestsPlugin gates via shouldTransformVeryDynamicRequests: on the client consumer it only transforms node_modules (minus transpiled packages), and it skips vinext source + @vitejs/plugin-rsc. The new createDynamicImportUrlPlugin has none of these — it runs on all JS/TS modules in every environment. In practice this is fine (I grepped: no vinext source or plugin-rsc module uses the import(new URL(...).href) shape, so nothing new gets rewritten there), and rewriting user client code to a bundled import("./x") is consistent with the intended semantics. But it is a real behavior change for client user code that ignore-dynamic-requests previously left untouched, so it should be a deliberate choice rather than incidental.

  2. No transform cache. Unlike both sibling pre/post transforms (ignore-dynamic-requests uses createTransformCache, import-meta-url has its own cache), the new plugin re-runs parseAst on every module matching code: /\bimport\s*\(/ on every transform. Given AGENTS.md's emphasis on keeping the common request/transform path light in large route graphs, consider reusing createTransformCache.

  3. Documented URL→bundled-specifier semantic shift. import(new URL(...).href) resolves a runtime URL string in Next.js, whereas import("./x") goes through the bundler graph. That's the intended behavior for a bundled Workers deployment and the code comment (lines 267-274) explains it well.

  4. Brittle count assertions. tests/compatibility-support.test.ts asserts hardcoded totals (NON_SUPPORTED_SUITES 33→32, needs-vite-equivalent 3→2). Standing maintenance cost across unrelated future edits — not a change for this PR.

LGTM.

@ask-bonk

ask-bonk Bot commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

Review posted to PR #2893.

Summary

I independently reviewed PR #2893 (fix(build): bundle dynamic URL imports) at head 03f3980 and found no correctness or security issues. The fix is well-scoped and well-tested.

What the PR does: Adds an enforce: "pre" plugin (vinext:dynamic-import-url) that normalizes import(new URL("./x", import.meta.url).href)import("./x") before the generic dynamic-request fallback (createIgnoreDynamicRequestsPlugin) would replace it with a MODULE_NOT_FOUND runtime error. Plus ported react-version fixtures + a Wrangler E2E spec and a compatibility-suite reclassification.

Verified locally:

  • tests/import-meta-url.test.ts + tests/compatibility-support.test.ts → 53 passed
  • vp check on the plugin → clean
  • Direct edge-case probing of the matcher: correctly conservative (rejects ?.href, .toString(), computed ["href"], 3-arg, template-literal, non-literal, absolute specifiers) and correctly scoped (doesn't touch bare new Worker(new URL(...)) or variable-assigned URLs — only the exact import(<newURL>.href) shape). Import options and nested/multiple imports handled.

Substantive non-blocking observation the prior reviews only partially covered: the new plugin lacks the environment/scope guards its sibling createIgnoreDynamicRequestsPlugin has (shouldTransformVeryDynamicRequests restricts client-consumer transforms to node_modules and skips vinext source + plugin-rsc). The new plugin runs on all JS/TS modules in every environment. I grepped and confirmed no vinext source or plugin-rsc module uses this pattern, so it's harmless in practice — but it's a real behavior change for client user code and should be a conscious decision. I also flagged the absence of a transform cache (both siblings cache; this one re-parses on every matching module), the documented URL→bundled semantic shift, and the brittle hardcoded compatibility counts.

Overall: LGTM.

github run

@james-elicx james-elicx left a comment

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Review of exact head 03f39802439956e5eeee8b7dc9b3f44704ebfa21:

[P2] Keep the native dynamic-URL prescan narrow. createDynamicImportUrlPlugin() now uses code: /\bimport\s*\(/, which sends every JS/TS module containing any dynamic import — including dependencies — through rewriteDynamicImportUrls() and a full parseAst() walk. Only literal import(/* comments */ new URL(..., import.meta.url).href) forms can change, so this widens common-path work in exactly the performance-sensitive plugin path that the repository guidelines require filtering before JavaScript. The webpack-comment regression does not require that widening: I independently built with Rolldown using a comment-aware native filter (/\bimport\s*\(\s*(?:(?:\/\*[\s\S]*?\*\/|\/\/[^\r\n]*(?:\r?\n|$))\s*)*new\s+URL\s*\(/) and it invoked the handler for the commented target while skipping an ordinary import("virtual:dep"). Please narrow the native code filter to candidate dynamic-URL imports and retain the ordering regression before the ignore fallback.

Functional verification on this SHA is otherwise clean: focused tests 53/53, exact Next.js react-version.test.ts 5/5, Cloudflare Worker react-conditions.spec.ts 5/5, and vp check passed.

@james-elicx

Copy link
Copy Markdown
Member Author

/bigbonk review for issues

Comment thread packages/vinext/src/plugins/import-meta-url.ts Fixed
@james-elicx

Copy link
Copy Markdown
Member Author

/bigbonk review for issues

@james-elicx

Copy link
Copy Markdown
Member Author

/bigbonk review for issues

ask-bonk[bot]
ask-bonk Bot previously requested changes Aug 11, 2026

@ask-bonk ask-bonk Bot left a comment

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.

Review: fix(build): bundle dynamic URL imports

Focused re-review at head ed0ca7a, after the perf(build): narrow dynamic URL import filter commit that addressed the earlier [P2]. The core fix (pre-plugin ordering + conservative AST rewrite) remains correct and well-tested. However, the narrowed code filter introduced in ed0ca7a has a catastrophic-backtracking (ReDoS) vulnerability that can hang the build.

[P1] The narrowed code filter regex has catastrophic backtracking

packages/vinext/src/plugins/import-meta-url.ts:72

code: /\bimport\s*\(\s*(?:\/(?:\*[\s\S]*?\*\/|\/[^\r\n]*(?:\r?\n|$))\s*)*new\s+URL\s*\(/,

The construct \s*(?:comment\s*)* has overlapping whitespace consumers (\s* after (, and the \s* that trails each comment inside the repeated group). When the input has a run of comment-like sequences that never reaches new URL(, the engine backtracks exponentially trying every partition of the whitespace/comments. Since this filter runs against the full module source of every JS/TS module in every environment, a single module with a import(/* a */ /* b */ ... expr) prologue is enough to stall the build.

Measured with the exact production regex (import( + N /*x*/ blocks + a non-new URL token):

N comments time
15 3 ms
20 20 ms
25 600 ms

Roughly 30x per 5 comments (2^N). At ~35 comments the filter alone runs for minutes; a pathological/minified vendor file (or a deliberately crafted dependency) turns a build-time filter into a DoS. This is a regression introduced by ed0ca7a — the previous /\bimport\s*\(/ filter was linear.

Suggested fix — replace the comment-grammar with a bounded, linear lazy gap:

code: /\bimport\s*\([\s\S]{0,200}?\bnew\s+URL\s*\(/,

This is O(n) (10 ms on a 2M-char input vs. a hang), still matches every relevant form — plain, block-comment, line-comment, and multiple magic comments between import( and new URL( — and still rejects ordinary import("dep"). The {0,200} cap keeps it from bridging an unrelated import(...) to a far-away new URL( while comfortably covering real webpack-magic-comment prologues; the AST-based rewriteDynamicImportUrls remains the source of truth for whether a rewrite actually happens, so a slightly looser filter is harmless. (I verified all the current comment fixtures still match and import("virtual:dep") / bare new URL(...) still skip.)

Please add a ReDoS guard test (e.g. assert the filter returns within a tight time budget on "import(" + "/*x*/ ".repeat(50) + "x)"), since the existing it.each comment cases don't exercise the failing-to-match backtracking path.

Everything else verified correct

  • Plugin ordering (the crux) is right. createDynamicImportUrlPlugin() (enforce: "pre") is placed before createIgnoreDynamicRequestsPlugin in index.ts, so the literal new URL(...).href import is normalized before the generic dynamic-request fallback replaces it with MODULE_NOT_FOUND.
  • getDynamicImportUrlSpecifier is conservative and semantically safe — only .//../ string literals, exactly 2 args, import.meta.url (or optional-chained) base; bare/absolute specifiers and non-literal forms correctly rejected.
  • Import options preserved by construction (only .source range overwritten); nested/multiple imports handled.
  • parserLanguageForModuleId matches the middleware-export-validation.ts pattern; the raw-ts/tsx cases guard the 4ab0128 fix.
  • pages/api/react-conditions.css won't be misread as an API route (pages/api scanner filters via createValidFileMatcher(pageExtensions)).

Non-blocking

  • The new plugin lacks the environment/scope guards its sibling createIgnoreDynamicRequestsPlugin has (client → node_modules only, skips vinext source + plugin-rsc) and has no transform cache. Harmless in practice (no vinext/plugin-rsc source uses this shape) but a deliberate uniform-behavior choice worth noting — already raised in prior reviews.
  • Brittle hardcoded compatibility counts in tests/compatibility-support.test.ts (33→32, 3→2).

Blocking on the [P1] ReDoS only.

Comment thread packages/vinext/src/plugins/import-meta-url.ts Fixed
@ask-bonk

ask-bonk Bot commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

Review posted to PR #2893.

Summary

I re-reviewed PR #2893 (fix(build): bundle dynamic URL imports) at head ed0ca7a and found one blocking issue introduced by the most recent commit (perf(build): narrow dynamic URL import filter).

[P1] ReDoS in the narrowed code filter — import-meta-url.ts:72

The comment-aware filter regex that was added to address the earlier [P2] performance note has catastrophic (exponential) backtracking:

code: /\bimport\s*\(\s*(?:\/(?:\*[\s\S]*?\*\/|\/[^\r\n]*(?:\r?\n|$))\s*)*new\s+URL\s*\(/,

The \s*(?:comment\s*)* construct has overlapping whitespace consumers. On input like import(/* a */ /* b */ ... expr) that never reaches new URL(, the engine backtracks exponentially:

N comments time
15 3 ms
20 20 ms
25 600 ms

~30x per 5 comments. Since the filter runs against the full source of every JS/TS module, a single pathological/minified/crafted module hangs the build. The prior /\bimport\s*\(/ filter was linear, so this is a regression from ed0ca7a.

Fix suggested: a bounded lazy gap, which I verified is O(n) (10 ms on 2M chars) and still matches every comment form while rejecting ordinary imports:

code: /\bimport\s*\([\s\S]{0,200}?\bnew\s+URL\s*\(/,

Also recommended adding a ReDoS-guard test, since the existing it.each cases only exercise the matching path, not the exponential failing-to-match path.

Everything else (plugin ordering, conservative AST rewrite, import-option preservation, parser-language selection, fixture placement) verified correct. Non-blocking items (missing env/scope guards, no transform cache, brittle count assertions) noted but already raised in prior reviews.

I posted this as a request-changes review blocking only on the ReDoS.

github run

@james-elicx

Copy link
Copy Markdown
Member Author

Independent CodeQL/ReDoS and native-filter review of immutable head f4ad11152db8dacac1f3c0201641d819717f6fd5: NO FINDINGS.

I re-reviewed the complete dynamic URL import filter follow-up, including the prior false-negative report.

Security and correctness checks:

  • the block-comment matcher is an unrolled loop over non-star spans and star runs; it contains no ambiguous dot-star or nested empty repetition;
  • the line-comment branch excludes every ECMAScript line terminator from its body and consumes CRLF, bare CR, LF, U+2028, or U+2029 explicitly;
  • each outer comment iteration consumes a leading slash and a complete comment, while surrounding whitespace is disjoint, keeping the overall scan linear;
  • the pattern uses no lookaround, backreferences, or unsupported flags and is exercised through real Vite native hook filtering;
  • separate real dependency builds pass for block comments and all five line-terminator forms;
  • repeated/internal star runs and mixed repeated comments match correctly;
  • ordinary literal/dynamic imports do not match; the real build test verifies the transform handler is invoked zero times;
  • the AST handler accepts and rewrites all five line-terminator forms, so the previous filter false negatives are closed.

Independent validation on this exact SHA:

  • tests/import-meta-url.test.ts: 55/55 passed
  • tests/compatibility-support.test.ts: 4/4 passed
  • scoped format/lint/types and git diff --check: clean
  • adversarial two-million-character unterminated block and line-comment probes completed in approximately 4 ms and 3 ms
  • fresh CodeQL run 31463946824: JavaScript/TypeScript and Actions analyses both completed successfully

The local and remote heads still match and the worktree is clean.

@james-elicx

Copy link
Copy Markdown
Member Author

/bigbonk review for issues

@ask-bonk

ask-bonk Bot commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

UnknownError: ProviderInitError

github run

@ask-bonk

ask-bonk Bot commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

@james-elicx Bonk workflow was cancelled.

View workflow run · To retry, trigger Bonk again.

@james-elicx

Copy link
Copy Markdown
Member Author

/bigbonk review for issues

@ask-bonk

ask-bonk Bot commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

UnknownError: ProviderInitError

github run

@james-elicx

Copy link
Copy Markdown
Member Author

/bigbonk review for issues

@james-elicx james-elicx left a comment

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Review of exact head 08819bc9328cadaea27b9527f2b8e47e37953109:

NO FINDINGS.

The earlier performance and parser-boundary findings are resolved. The native code filter now performs only a bounded prefix check: direct import(new URL( candidates and leading-comment candidates reach AST validation, ordinary no-comment dynamic imports do not. Real-build coverage proves an ordinary import followed by an unrelated new URL(...) invokes the handler zero times, while commented ordinary imports take the documented one-parse validation path. Block comments, mixed comments, and LF/CRLF/CR/U+2028/U+2029 line terminators are all exercised through real Vite builds before the ignore fallback. Direct adversarial JavaScript-filter probes are linear/bounded, and CodeQL is green.

Independent validation on this SHA: focused import-meta/compatibility tests 62/62; exact Next.js test/e2e/react-version/react-version.test.ts 5/5 via the targeted wrapper; Cloudflare Worker react-conditions.spec.ts 5/5; vp check; and git diff --check. The currently failed E2E (app-router-client-cache) check is in the separately excluded cache-behavior domain and is not attributable to this change.

@james-elicx

Copy link
Copy Markdown
Member Author

/bigbonk review for issues

1 similar comment
@james-elicx

Copy link
Copy Markdown
Member Author

/bigbonk review for issues

@ask-bonk ask-bonk Bot left a comment

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.

Review: fix(build): bundle dynamic URL imports

Independent review at head 08819bc. No correctness, security, or performance issues found. The prior [P1] ReDoS (from ed0ca7a) and [P2] filter-widening findings are both resolved by the current coarse prefix filter. The change is well-scoped and thoroughly tested.

Verified correct

  • Plugin ordering (the crux) is right. createDynamicImportUrlPlugin() (enforce: "pre") sits before createIgnoreDynamicRequestsPlugin (also pre) in the plugin array (index.ts), so import(new URL("./x", import.meta.url).href) is normalized to import("./x") before the generic dynamic-request fallback would replace it with the MODULE_NOT_FOUND runtime error. Between two pre plugins, array position decides — and it's correct.

  • The AST rewrite is precise and conservative. I probed rewriteDynamicImportUrls directly (11 cases via the test harness). It rewrites only the exact import(new URL("./"|"../", import.meta.url|import.meta?.url).href) shape, and correctly skips every near-miss:

    • import(new URL("./x", import.meta.url)?.href) — optional-chained .href → skip
    • import(new URL("./x", import.meta.url)["href"]) — computed access → skip
    • import(new URL("./x", import.meta.url).toString()) → skip
    • template-literal specifier, absolute /abs, bare "bare", missing base, globalThis.URL → all skip
    • import(new URL("./x", import.meta?.url).href) → rewrite (optional-chained base handled)
  • Import options preserved by construction (only the .source range is overwritten); nested/multiple imports handled.

  • ReDoS resolved (linear filter). The current code: /\bimport\s*\(\s*(?:new\s+URL\s*\(|\/)/ is a bounded prefix check with no overlapping/nested quantifiers. I stress-tested the exact production regex against the old backtracking vectors — import( + 500k /*x*/, import(/ + 500k \r\n//, 2M-char non-matching prefixes — all complete in <8ms. CodeQL findings 282/283 were against the removed comment-grammar regex in ed0ca7a, not this one.

  • Coarse-filter tradeoff is documented and tested. The filter now admits import(/* … */ "./ordinary") too, which costs one extra parseAst (no rewrite). The only parses commented ordinary dynamic imports for AST validation test locks this to exactly one parse, and does not invoke the URL import transform for ordinary dynamic imports verifies zero handler calls for uncommented ordinary imports.

  • parserLanguageForModuleId matches the middleware-export-validation.ts pattern; the raw-ts/tsx it.each cases guard the 4ab0128 parse-by-language fix.

  • Fixture is a genuine end-to-end test. pages/api/pages-react-conditions.ts carries the real import(new URL("./react-conditions.css", import.meta.url).href), and the cloudflare-encoded-paths Playwright project builds cf-app-basic and runs it under Wrangler, so the fix is exercised against Workers. The .css sibling in pages/api/ won't be misread as an API route — the pages/api scanner filters through createValidFileMatcher(pageExtensions), which excludes .css (confirmed in routing/pages-router.ts).

  • Compatibility reclassification is consistent. Moving react-version from VITE_RUNTIME_CONDITIONS (needs-vite-equivalent) to supported reduces needs-vite-equivalent 3→2 and NON_SUPPORTED_SUITES 33→32; the asserted totals match.

Non-blocking observations (already raised in prior reviews)

  1. No env/scope guard on the new plugin. Unlike its sibling createIgnoreDynamicRequestsPlugin (which via shouldTransformVeryDynamicRequests restricts the client consumer to node_modules, and skips vinext source + @vitejs/plugin-rsc + build output), the new plugin runs on all JS/TS user modules in every environment. I independently grepped packages/vinext/src and node_modules/@vitejs/plugin-rsc — no source uses the import(new URL(...).href) shape, so nothing new gets rewritten there in practice. Rewriting user client code to a bundled import("./x") is consistent with the intended semantics, so this is fine — flagging only so the uniform behavior is a conscious choice.

  2. No transform cache. Both sibling transforms cache (ignore-dynamic-requests uses createTransformCache; import-meta-url has its own). The new plugin re-parses on every matching module. The coarse filter keeps the matching set small (only modules with import( immediately followed by new URL( or a comment), so impact is minimal, but reusing createTransformCache would align it with the repo's hot-path guidance.

  3. Documented URL→bundled-specifier semantic shift. import(new URL(...).href) resolves a runtime URL string in Next.js, whereas import("./x") goes through the bundler graph. That's the intended behavior for a bundled Workers deployment and the code comment (lines 267-274) explains it well.

  4. Brittle hardcoded compatibility counts in tests/compatibility-support.test.ts (33→32, 3→2) — a standing maintenance cost, not a change for this PR.

Validation performed locally

  • vp test run tests/import-meta-url.test.ts tests/compatibility-support.test.ts62 passed
  • Out-of-band edge-case probe of rewriteDynamicImportUrls11/11 behave conservatively
  • Adversarial ReDoS stress of the production filter → linear (<8ms on 2M-char inputs)
  • vp check on import-meta-url.ts + index.ts → clean

LGTM.

@ask-bonk

ask-bonk Bot commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

Review posted to PR #2893.

Summary

I independently reviewed PR #2893 (fix(build): bundle dynamic URL imports) at head 08819bc and found no correctness, security, or performance issues. The earlier [P1] ReDoS (from ed0ca7a) and [P2] filter-widening findings are both resolved by the current coarse prefix filter.

What the PR does: Adds an enforce: "pre" plugin (vinext:dynamic-import-url) that normalizes import(new URL("./x", import.meta.url).href)import("./x") before createIgnoreDynamicRequestsPlugin would replace it with a MODULE_NOT_FOUND runtime error. Plus ported react-version fixtures + a Wrangler E2E spec and a compatibility reclassification.

Key things I verified myself (not just trusting prior reviews):

  • Plugin ordering — confirmed createDynamicImportUrlPlugin() is placed before createIgnoreDynamicRequestsPlugin in index.ts.
  • AST rewrite precision — ran 11 edge cases through the test harness; only the exact import(new URL("./"|"../", import.meta.url|import.meta?.url).href) shape rewrites, and every near-miss (optional-chained .href, computed access, .toString(), template literals, absolute/bare specifiers, globalThis.URL) is correctly skipped.
  • ReDoS resolution — stress-tested the exact production regex against the old backtracking vectors (500k /*x*/, 500k \r\n//, 2M-char non-matches); all complete in <8ms. The current filter has no nested quantifiers.
  • Fixture/route safety — confirmed the .css file in pages/api/ is excluded by createValidFileMatcher(pageExtensions), and grepped vinext source + plugin-rsc to confirm no source uses the rewritten pattern.

Local validation: 62 tests pass, edge probe 11/11, ReDoS stress linear, vp check clean.

Non-blocking notes (already raised in prior reviews): missing env/scope guard vs. the sibling plugin, no transform cache, the documented URL→bundled semantic shift, and brittle hardcoded compatibility counts.

LGTM.

github run

@james-elicx
james-elicx dismissed ask-bonk[bot]’s stale review August 11, 2026 06:33

Addressed on 08819bc; fresh Big Bonk run 31465181133 reports no findings.

@james-elicx
james-elicx marked this pull request as ready for review August 11, 2026 06:33
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