Skip to content

fix(config): preserve native TypeScript dynamic imports#2353

Open
james-elicx wants to merge 7 commits into
mainfrom
codex/fix-native-ts-config-dynamic-import
Open

fix(config): preserve native TypeScript dynamic imports#2353
james-elicx wants to merge 7 commits into
mainfrom
codex/fix-native-ts-config-dynamic-import

Conversation

@james-elicx

@james-elicx james-elicx commented Jun 25, 2026

Copy link
Copy Markdown
Member

Summary

  • resolve and invoke function-form Next.js configs inside Vite's temporary module runner
  • keep the runner alive while async configs perform deferred dynamic imports
  • preserve existing TypeScript aliases, CommonJS shims, module export semantics, and fresh config evaluation

Next.js parity

Fixes both failures from run 28143992598:

  • test/e2e/app-dir/next-config-ts-native-ts/dynamic-import-esm/next-config-ts-dynamic-import-esm.test.ts
  • test/e2e/app-dir/next-config-ts-native-mts/dynamic-import-esm/next-config-ts-dynamic-import-esm.test.ts

Vinext previously loaded the module namespace with Vite's temporary runnerImport() and invoked the exported async config only after the runner had closed. A later await import('./foo.ts') from the config function then failed with Vite module runner has been closed.

The fix loads a thin virtual module that imports the real config, resolves its ESM/CommonJS export shape, and awaits function-form configs before returning from runnerImport().

Validation

  • vp test run tests/next-config.test.ts — 216 passed
  • focused vp check — clean
  • targeted Next.js v16.2.6 .ts deploy suite — 1 passed
  • targeted Next.js v16.2.6 .mts deploy suite — 1 passed

Full suites are deferred to CI.

@james-elicx

Copy link
Copy Markdown
Member Author

/bigbonk review for issues

@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

@pkg-pr-new

pkg-pr-new Bot commented Jun 25, 2026

Copy link
Copy Markdown

Open in StackBlitz

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

commit: 56831a1

@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: native TypeScript dynamic imports

I verified the fix resolves the Vite module runner has been closed failure (reverting next-config.ts to main and running the new test reproduces the error, and the fix makes it pass). The direction is correct and matches Next.js's intent. But there is one correctness regression and a few parity gaps worth addressing before merge.

1. (blocking) Silent config loss for CJS-style .ts/.mts configs under "type": "module"

The new branch takes priority for all .ts/.mts configs, and only falls back to the Vite path when the native import() throws. A CJS-style config (module.exports = {...}) inside a "type": "module" package does not throw under Node's native type-stripping loader — it silently produces an empty module namespace ({ default: undefined }). unwrapConfig then returns mod.default ?? mod = the empty namespace, so the user's config is dropped with no error.

Reproduction with real Node (not Vitest):

proj/package.json   ->  {"type":"module"}
proj/next.config.ts ->  module.exports = { env: { VIA: "module.exports" } };
PR would return config = {}        // expected { env: { VIA: "module.exports" } }

Before this PR, that config loaded correctly via the Vite path + cjsGlobalsInjectorPlugin. The repo explicitly supports CJS-style .ts configs (see test "exposes a CommonJS module/exports object inside next.config.ts", and the loadNextConfig docstring notes the "type": "module" + CJS shape is "the common shape after vinext init").

This is not caught by the test suite because Vitest intercepts import() with Vite's own pipeline, so the new test never exercises Node's real native loader. (You can confirm: the module.exports-in-.ts test only passes because its tmpdir has no "type":"module", and the dynamic-import test only passes because Vite handles the inner import().)

Suggested fix: only accept the native result when it actually produced a config — e.g. treat a namespace with no usable export as a fall-through, not a success:

if (nativeModule !== undefined) {
  const ns = nativeModule as Record<string, unknown>;
  // A CJS `.ts` under "type":"module" yields an empty namespace under
  // native type-stripping; fall through to the Vite path in that case.
  if (ns.default !== undefined || Object.keys(ns).length > 0) {
    return await unwrapConfig(nativeModule, phase);
  }
}

Even better: mirror Next.js v16.2.6, which only uses native import as a fast path and otherwise transpiles (handleCJS). See transpileConfig — native import is gated behind __NEXT_NODE_NATIVE_TS_LOADER_ENABLED === 'true' AND process.features.typescript, and any non-throwing-but-empty / require-based config still routes through SWC. A focused test with "type":"module" + module.exports would lock this down.

2. Parity: native gate is broader than Next.js

Next.js gates the native path on both process.env.__NEXT_NODE_NATIVE_TS_LOADER_ENABLED === 'true' and process.features.typescript (transpile-config.ts:148-156). This PR gates only on process.features?.typescript. On Node 22.18+ the feature is on by default, so the PR will take the native path for every .ts/.mts, whereas Next.js still requires the env opt-in. This widens the surface where the regression in (1) bites and diverges from Next.js parity. Worth matching the env gate, or at least documenting the deliberate divergence per AGENTS.md.

3. Parity / interop: .cts is excluded

The branch handles .ts and .mts but not .cts. Node's native TS support covers .cts too. Confirm whether this is intentional — if .cts is meant to keep flowing through the Vite/CJS path, a one-line comment explaining why would help (the existing comments at lines 984/993 mention .cts for the Vite path, so the omission reads as accidental).

4. unwrapConfig vs Next.js interopDefault

Next.js consistently extracts .default from the native module (config.ts:1722 interopDefault, transpile-config.ts:156 .default). unwrapConfig's mod.default ?? mod is looser and is what enables the silent-empty-config issue in (1). Tightening this (only use .default, and treat its absence as a fall-through) would align with Next.js and fix the regression in one place.

Minor

  • The native path uses raw configPath for pathToFileURL(configPath), while the rest of the function uses safeRealpath(path.resolve(configPath)) (normalizedConfigPath) for symlink-correct comparisons. Not a correctness bug here (import resolves the path itself), just noting the inconsistency.
  • The empty catch {} swallows all native-import errors, including genuine syntax/type errors in the config, which then silently re-run through Vite. Next.js surfaces these (warnOnce + rethrow on real failure). Consider distinguishing "native loader unavailable" from "user config is broken".

Validation done

  • Reverted impl to main: new test fails with Vite module runner has been closed — confirms the test is meaningful and the fix works in-suite.
  • vp test run tests/next-config.test.ts — 211 passed.
  • vp check on both files — clean.
  • Standalone Node repro confirms the CJS-.ts-under-type:module regression (returns {}).

Recommendation: address (1) before merge (it's a silent data-loss regression for a documented supported config shape); (2)–(4) are parity/robustness improvements that would be good to fold in while here.

@james-elicx

Copy link
Copy Markdown
Member Author

/bigbonk review for issues

@ask-bonk

ask-bonk Bot commented Jun 25, 2026

Copy link
Copy Markdown
Contributor

Review posted to PR #2353. Summary of what I found:

Findings

1. (Blocking) Silent config loss regression — The new native-import branch takes priority for all .ts/.mts configs but only falls back when import() throws. A CJS-style config (module.exports = {...}) inside a "type": "module" package does not throw under Node's native type-stripping loader — it returns an empty module namespace. unwrapConfig's mod.default ?? mod then returns the empty object, silently discarding the user's config. I reproduced this in real Node: the PR returns {} instead of { env: { VIA: "module.exports" } }.

This is a documented supported config shape (per the loadNextConfig docstring, it's "the common shape after vinext init") and an existing test covers it — but the test only passes because (a) its tmpdir lacks "type":"module", and (b) Vitest intercepts import() with Vite's pipeline, so the new tests never actually exercise Node's native loader.

2. Parity — Next.js gates native import on both __NEXT_NODE_NATIVE_TS_LOADER_ENABLED === 'true' AND process.features.typescript; the PR checks only the latter, widening the surface.

3. .cts excluded — handled for the Vite path but not the new native path; likely accidental.

4. unwrapConfig vs interopDefault — Next.js consistently extracts .default; the looser mod.default ?? mod is what enables the regression.

Plus two minor notes (realpath inconsistency, empty catch swallowing genuine config errors).

I verified the fix is genuinely effective (reverting the impl reproduces the original Vite module runner has been closed error) and that vp test/vp check pass — the core direction is correct, but item (1) is a silent data-loss regression that should be fixed before merge.

github run

@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: native TypeScript dynamic imports

I re-reviewed against the current head (c550645), which is past the earlier ask-bonk review. The three follow-up commits (retain CJS fallback, detect native CJS references precisely, cover CJS native loader fallback) add AST-based referencesUnboundCjsGlobals gating, which resolves the previously-blocking silent-config-loss concern. I verified the core behavior in real Node 24 (not just Vitest, where import() is intercepted), and the fix is sound.

Verified working (standalone Node, process.features.typescript === 'strip')

Scenario Native import() Routing Result
dynamic-import async config (await import('./foo.ts')) under type:module succeeds, runner stays alive native { FOO: "foo" } — the bug this PR fixes
module.exports = {...} .ts under type:module throws module is not defined AST detects unbound moduleVite path ✅ config preserved
pure-ESM .ts succeeds native
tsconfig @/foo alias config throws ERR_MODULE_NOT_FOUND catchVite path
extensionless sibling import (./helper) throws ERR_MODULE_NOT_FOUND catchVite path
require mentioned only in a comment/string n/a AST finds no unbound ref → native ✅ (PR test covers this)

So the earlier blocking finding (silent empty config for CJS-.ts-under-type:module) does not reproduce on the current code: native strip throws cleanly for module.exports, and even before the throw, referencesUnboundCjsGlobals proactively routes those configs to the Vite path. vp check and tests/next-config.test.ts (214) both pass.

On the earlier parity notes

I pulled Next.js transpile-config.ts (canary). Two clarifications on the prior review's parity items:

  1. __NEXT_NODE_NATIVE_TS_LOADER_ENABLED gate — Next.js sets this env var to 'true' unconditionally in packages/next/src/bin/next.ts at CLI startup. It's an internal kill-switch (flipped to 'false' after a failure within a session), not a user opt-in. So gating only on process.features.typescript, as this PR does, is behaviorally equivalent to Next.js's default path. Not a real divergence. A one-line comment noting this would preempt future confusion, but it's not blocking.

  2. CJS pre-check is a deliberate (good) divergence — Next.js does not inspect for CJS globals; it just runs import() and falls back on throw. vinext's AST pre-check is stricter and necessary, because vinext's Vite fallback path supports CJS-global injection that Next.js handles via SWC. This is the right call and matches the AGENTS.md "deliberate, documented divergence" bar.

Minor (non-blocking)

  • .cts not on the native path (only .ts/.mts). Node's native loader supports .cts, but .cts is CJS-by-extension and almost always references module/exports, so it'd be routed to Vite anyway. Behavior is correct; a brief comment on the extension list would make the omission read as intentional rather than accidental.
  • Double read of the config file for TS configs (fs.readFileSync here, then again inside the Vite plugin transform on fallback). Negligible, only on the TS path.
  • catch {} scope — I confirmed it only wraps the import() call (module load/parse), not the async-config invocation in unwrapConfig. Genuine load failures still surface via the Vite fallback's warnConfigLoadFailure, and runtime errors thrown by an async config function propagate normally. No errors are silently lost.

Verdict

No blocking issues on the current head. The implementation is correct, well-tested, and matches Next.js's effective default behavior. Recommend merge after CI green; the minor comments above are optional polish.

@ask-bonk

ask-bonk Bot commented Jun 25, 2026

Copy link
Copy Markdown
Contributor

Review posted to PR #2353.

Summary

I reviewed the current head (c550645), which is past the earlier ask-bonk review. The three follow-up commits added AST-based CJS detection that resolves the previously-blocking concern. I verified behavior in real Node 24 (not just Vitest, where import() is intercepted), and the fix is sound.

Key findings:

  1. The earlier blocking finding does not reproduce. The previously-flagged "silent empty config for CJS-.ts-under-type:module" is fixed two ways: (a) native strip actually throws module is not defined for module.exports (not a silent empty namespace), and (b) the new referencesUnboundCjsGlobals AST check proactively routes any config referencing unbound module/exports/require to the Vite path before native import is even attempted.

  2. The dynamic-import fix works end-to-end — I reproduced { FOO: "foo" } from an async config calling await import('./foo.ts') in real Node, confirming the module runner stays alive.

  3. Parity clarification — Next.js sets __NEXT_NODE_NATIVE_TS_LOADER_ENABLED='true' unconditionally at CLI startup (internal kill-switch, not a user opt-in), so gating only on process.features.typescript is behaviorally equivalent to Next.js's default. The prior review's parity concern was overstated.

  4. Minor, non-blocking notes: .cts omission from the native path (correct, but a comment would help), a negligible double file-read on the TS path, and confirmation that the catch {} only wraps module load (genuine errors still surface).

Verdict: No blocking issues on the current head. vp check clean, 214 config tests pass. Recommended merge after CI green.

github run

@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: native TypeScript dynamic imports

Reviewed the current head (6acc0b0 fix(config): harden native CJS detection), which is one commit past the last ask-bonk review. I verified the core behavior in real Node 24 (process.features.typescript === 'strip'), where import() is not intercepted by Vitest, and re-checked the CJS-detection logic against a battery of edge cases. No blocking issues on the current head.

What I verified in real Node (not Vitest)

Scenario Native import() Detection routes to Result
async config doing await import('./foo.ts') under type:module succeeds, runner stays alive native { VALUE: "from-dynamic-import" } — the bug this PR fixes
module.exports = {...} .ts under type:module does not throw — yields empty namespace {} AST detects unbound moduleVite path ✅ config preserved

Important correction to the earlier review thread: on Node 24, module.exports = {...} in a .ts under "type":"module" does not throw module is not defined — native type-stripping returns an empty module namespace silently. So the safety here rests entirely on referencesUnboundCjsGlobals routing it to Vite before native import is attempted. It does so correctly, which is what closes the original blocking concern — but the throw-based fallback (catch {}) would not have caught this case on its own.

CJS detection is robust (probed for false negatives)

A false negative (CJS config wrongly routed to native → silent empty config) would be the dangerous failure mode. I ran the detection logic against the parser and confirmed it correctly routes to the Vite path for: module.exports assignment, bare require(), require in a default parameter, typeof require, module.foo member access, bare exports, __dirname, require in template literals, spread ...require(), decorators, getters, labeled/static blocks, with statements, and module inside TSAsExpression/TSNonNullExpression. It correctly allows the native path for genuinely-shadowed bindings (catch param, for-of binding, destructure alias) and for CJS tokens that only appear in comments/strings or as TS type names (interface module, type require). The new isErasedTypeScriptNode + default-parameter visitation from this last commit are sound.

Parity confirmed against Next.js transpile-config.ts

  • __NEXT_NODE_NATIVE_TS_LOADER_ENABLED is set to 'true' unconditionally in packages/next/src/bin/next.ts at CLI startup (it's an internal kill-switch flipped to 'false' after a failure, not a user opt-in). So gating only on process.features.typescript is behaviorally equivalent to Next.js's default. The earlier parity concern was overstated.
  • Next.js does (await import(...)).default and falls back to SWC (handleCJS) on throw. The PR's catch {} → Vite path mirrors this. The AST pre-check is a deliberate, stricter divergence required because vinext's Vite fallback does CJS-global injection that Next.js does via SWC — this is the right call and meets the AGENTS.md "deliberate, documented divergence" bar.

Non-blocking notes (optional polish)

  1. Silent catch {} diverges from Next.js's warnOnce. Next.js emits a one-time warning when native import fails before falling back; the PR swallows the error silently. For a config with a genuine error (not a tsconfig-alias / extensionless-import fallback), the user gets no signal from the native attempt — the Vite path's own error reporting is the only surface. Consider a debug-level log to aid diagnosis. Confirmed this catch only wraps the import() call, so runtime errors thrown by an async config function still propagate.

  2. unwrapConfig's mod.default ?? mod is looser than Next.js's strict .default. It's harmless on the native path only because CJS configs are pre-filtered to Vite, so the native branch always has a default export. A short comment noting that invariant would prevent a future refactor from reintroducing the silent-empty-config risk if the pre-filter is ever weakened.

  3. .cts is intentionally not on the native path (only .ts/.mts). This is correct — .cts is CJS-by-extension and would be routed to Vite anyway — but the extension list reads as accidental. A one-line comment would help, matching the existing .cts comments on the Vite path (lines ~1161).

  4. Double file read on the TS path (fs.readFileSync here, then again in the Vite transform on fallback). Negligible, TS-path only.

Validation

  • Real-Node repro of the dynamic-import fix: ✅ returns the imported value.
  • Real-Node repro of module.exports-under-type:module: empty namespace, but AST correctly routes to Vite → config preserved.
  • referencesUnboundCjsGlobals probed against ~25 edge cases — no false negatives found.
  • vp test run tests/next-config.test.ts — 216 passed.

Verdict: Correct, well-tested, and matches Next.js's effective default. Recommend merge after CI green; notes above are optional.

Reviewed at 6acc0b0.

@ask-bonk

ask-bonk Bot commented Jun 25, 2026

Copy link
Copy Markdown
Contributor

Review posted to PR #2353.

Summary

I reviewed the current head (6acc0b0), one commit past the last review, and verified behavior in real Node 24 (where import() is not intercepted by Vitest). No blocking issues.

Key findings:

  1. The dynamic-import fix works — confirmed in real Node that an async config calling await import('./foo.ts') succeeds because native import() keeps the module loader alive (vs Vite's runnerImport closing its runner too early).

  2. Important correction to the prior review thread: On Node 24, module.exports = {...} in a .ts under "type":"module" does not throw module is not defined — it silently yields an empty namespace. So the throw-based catch {} fallback would not have caught the original blocking case on its own. The safety rests entirely on the AST referencesUnboundCjsGlobals pre-check routing it to Vite first — which it does correctly.

  3. CJS detection is robust. I probed ~25 edge cases for false negatives (the dangerous failure mode). It correctly routes all genuine unbound CJS references to the Vite path and correctly allows the native path for shadowed bindings and comment/string/type-name-only mentions.

  4. Parity confirmed against Next.js transpile-config.ts: __NEXT_NODE_NATIVE_TS_LOADER_ENABLED is set unconditionally at CLI startup (kill-switch, not opt-in), so gating on process.features.typescript matches Next.js's default. The AST pre-check is a deliberate, justified divergence.

  5. Non-blocking notes: silent catch {} vs Next.js's warnOnce; looser mod.default ?? mod (harmless given the pre-filter, but worth a comment); intentional .cts omission; minor double file read.

vp test run tests/next-config.test.ts — 216 passed.

github run

@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: preserve native TypeScript dynamic imports

Reviewed the current head (1c0d28d fix(config): resolve async configs inside runner), which is past all three earlier ask-bonk reviews in this thread. Those reviews covered the native-import() + referencesUnboundCjsGlobals AST approach (commits ab161c1..6acc0b0). That entire approach was reverted in the final commit. The current code contains no native import(), no process.features.typescript gate, and no AST CJS detection — so the prior parity discussion, the silent-empty-config concern, and the .cts/catch {} notes are all stale and no longer apply.

What the current head actually does

Instead of native import, it now loads a thin virtual module (virtual:vinext-resolved-next-config) through Vite's runnerImport. That virtual module import * as's the real config, applies the CJS/ESM unwrap rules, and awaits a function-form config before runnerImport returns — which is what keeps the Vite runner alive across a deferred await import('./foo.ts'). Everything stays inside Vite's pipeline (cjsGlobalsInjectorPlugin + vite-plugin-commonjs), so the native type-stripping loader is never involved.

This is strictly more robust than the abandoned native approach, and it sidesteps every concern raised earlier in the thread. No blocking issues.

Verified in real Node 24 (process.features.typescript === 'strip'), runner not stubbed

Scenario Result
async config doing await import('./foo.ts') under type:module { FOO: "foo" } — the bug this PR fixes
module.exports = {...} .ts under type:module { VIA: "module.exports" }the prior "blocking regression" does not reproduce; goes through Vite + injector
function-form module.exports = (phase) => ({...}) ✅ phase passed correctly
async config that throws ✅ error propagates (and warnConfigLoadFailure fires) — not silently swallowed
pure-ESM named-exports-only (namespace fallback) { ONLY_NAMED }, no __vinext_cjs_* leakage
default + extra named exports ✅ prefers default, named exports don't pollute config
module.exports mentioned only in a string (false positive) ✅ falls through to ESM default (sentinel identity check works through the virtual module)
extensionless sibling import (./helper) ✅ — failed under the old native loader, works now
tsconfig @/* alias import
__dirname + fs.readFileSync (node-api-cjs style)
fresh evaluation across two loads of a mutated config 1 → 2, no staleness despite the fixed virtualConfigId

vp test run tests/next-config.test.ts216 passed. vp check on the changed file — clean. Reverting next-config.ts to main reproduces Vite module runner has been closed, confirming the test is meaningful and the fix works in-suite (and, unlike the native approach, the test now genuinely exercises the runner because Vitest's runnerImport is the runner under test).

One non-blocking maintainability note

The unwrap logic is now duplicated. The main path inlines the CJS/ESM resolution + function invocation as a generated string in the virtual module's load hook (lines ~1011–1016):

const cjsValue = cjsExports != null && (cjsExports !== cjsInitial || (typeof cjsExports === "object" && Object.keys(cjsExports).length > 0)) ? cjsExports : undefined;
const value = cjsValue ?? configModule.default ?? configModule;
export default typeof value === "function" ? await value(<phase>, { defaultConfig: {} }) : value;

while unwrapConfig (lines 654–672, still used by the loadConfigViaRequire CJS fallback at lines 896/899) and resolveConfigValue express the same rules in TypeScript. I confirmed the two are currently behaviorally equivalent (including the cjsInitial identity sentinel and function-form invocation), but they now have to be kept in sync by hand, and the inline copy is an untyped template string that won't be caught by vp check if it drifts. Per AGENTS.md ("generated entry modules should stay thin"), consider having the virtual module import and call a small shared helper (e.g. export the unwrap rules from a typed module and reference them) rather than re-encoding the sentinel logic as a string. Optional, not blocking.

Verdict

Correct, well-tested, and a cleaner approach than the reverted native-import path. The earlier blocking finding in this thread targeted code that no longer exists. Recommend merge after CI green; the duplicate-unwrap note above is optional polish.

Reviewed at 1c0d28d.

@ask-bonk

ask-bonk Bot commented Jun 25, 2026

Copy link
Copy Markdown
Contributor

Review posted to PR #2353.

Summary

I reviewed the current head (1c0d28d), which is past all three earlier reviews in this thread. The key finding is that those reviews are now stale: the final commit reverted the entire native-import() + AST-CJS-detection approach (commits ab161c1..6acc0b0) and replaced it with a virtual-module-through-Vite-runner approach. There is no longer any process.features.typescript gate, native import(), or referencesUnboundCjsGlobals — so the prior "blocking silent-config-loss" finding and all the parity/.cts/catch {} notes no longer apply to the code being merged.

What the new code does: loads a thin virtual module that imports the real config, applies the CJS/ESM unwrap rules, and awaits a function-form config before runnerImport returns — keeping the Vite runner alive across a deferred await import('./foo.ts'). Everything stays in Vite's pipeline, so the native type-stripping loader (the source of all the earlier concerns) is never touched.

Verification (real Node 24, runner not stubbed):

  • The dynamic-import fix works ({ FOO: "foo" }).
  • The previously-flagged CJS-module.exports-under-type:module regression does not reproduce — it correctly returns the config.
  • Error propagation, namespace fallback, false-positive sentinel handling, extensionless imports, aliases, __dirname, and fresh re-evaluation all verified correct.
  • 216 tests pass; vp check clean; reverting the impl reproduces Vite module runner has been closed.

No blocking issues. One optional maintainability note: the unwrap logic is now duplicated between unwrapConfig (typed, still used by the CJS-require fallback) and an untyped template string in the virtual module's load hook — they're currently equivalent but must be kept in sync by hand.

github run

@github-actions

Copy link
Copy Markdown
Contributor

Performance benchmarks

Compared 1c0d28d against base de014d4 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 117.1 KB 117.1 KB ⚫ +0.0%
Client entry size (gzip) vinext 112.5 KB 112.5 KB ⚫ +0.0%
Dev server cold start vinext 2.49 s 2.50 s ⚫ +0.3%
Production build time vinext 2.99 s 3.02 s ⚫ +0.7%
RSC entry closure size (gzip) vinext 93.0 KB 93.0 KB ⚫ +0.0%
Server bundle size (gzip) vinext 156.9 KB 156.9 KB ⚫ +0.0%

View detailed results and traces

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

@james-elicx james-elicx marked this pull request as ready for review June 25, 2026 17:55
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.

1 participant