Skip to content

fix: support CommonJS node_modules in Pages Router dev#665

Open
southpolesteve wants to merge 1 commit intomainfrom
codex/pages-router-cjs-node-modules
Open

fix: support CommonJS node_modules in Pages Router dev#665
southpolesteve wants to merge 1 commit intomainfrom
codex/pages-router-cjs-node-modules

Conversation

@southpolesteve
Copy link
Collaborator

@southpolesteve southpolesteve commented Mar 23, 2026

Summary

  • add a CommonJS-aware evaluator to the direct dev module runner used by Pages Router requests
  • limit that compatibility path to raw node_modules and .cjs modules, while preserving Vite's normal ESM evaluator everywhere else
  • add a regression test covering a Pages Router page that imports a nested CommonJS package from node_modules

This matches the Pages Router bug reported in #585. In Next.js, Pages Router dependencies are externalized by default unless bundlePagesRouterDependencies is enabled, so raw CommonJS packages should not crash dev SSR with module is not defined.

Fixes #585.

Verification

  • vp test run tests/cjs.test.ts tests/node-modules-css.test.ts
  • vp check packages/vinext/src/server/dev-module-runner.ts tests/cjs.test.ts

Notes

@pkg-pr-new
Copy link

pkg-pr-new bot commented Mar 23, 2026

Open in StackBlitz

npm i https://pkg.pr.new/vinext@665

commit: ff56b46

@github-actions
Copy link

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

@southpolesteve
Copy link
Collaborator Author

/bigbonk review

Copy link
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.

Good approach overall. The noExternal: true config that vinext sets for non-Cloudflare environments means all node_modules are forced through Vite's transform pipeline. Since vite-plugin-commonjs skips node_modules by default, and ssrTransform only handles ESM syntax (not require()/module.exports), CJS patterns survive into runInlinedModule where ESModulesEvaluator can't handle them. The evaluator-level fix is the right layer for this.

The scoping to node_modules + .cjs is appropriately conservative. The test creates a realistic scenario with inter-file require() between CJS modules. The replaceNamespaceExports/toCommonJsNamespace conversion correctly handles the ESM namespace shape that the module runner expects.

One real bug (Windows path separators) needs fixing before merge. A couple minor items noted inline.

ESModulesEvaluator,
createNodeImportMeta,
ssrDynamicImportKey,
ssrExportAllKey,
Copy link
Contributor

Choose a reason for hiding this comment

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

Bug: path.sep is \\ on Windows, but Vite normalizes all module paths to forward slashes via normalizePath() (which calls path.posix.normalize(slash(id))). So mod.file will always contain forward slashes like C:/Users/dev/project/node_modules/foo/index.js.

This means on Windows the check becomes modulePath.includes("\\node_modules\\") which will never match — the entire CJS compat layer silently becomes a no-op.

Suggested change
ssrExportAllKey,
(modulePath.includes("/node_modules/") || modulePath.endsWith(".cjs"))

const COMMONJS_MARKERS =
/\bmodule\.exports\b|\bexports(?:\s*\[|\.[A-Za-z_$])|\brequire\s*\(|\b__dirname\b|\b__filename\b/;
const AsyncFunction = async function () {}.constructor as new (
...args: string[]
Copy link
Contributor

Choose a reason for hiding this comment

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

Nit: Worth a brief inline comment that createRequire loads files from disk directly, bypassing Vite's module graph. This is fine for node_modules (no HMR needed, no user plugins relevant), but the trade-off is worth documenting since require("./core.js") inside CJS packages will resolve sub-modules natively rather than through Vite's transform pipeline.

Something like:

// createRequire resolves sub-modules natively (bypasses Vite's module graph).
// This is intentional for node_modules — third-party deps don't need HMR
// or Vite plugin transforms.
const require = createRequire(pathToFileURL(modulePath));

*/

import { ModuleRunner, ESModulesEvaluator, createNodeImportMeta } from "vite/module-runner";
import path from "node:path";
Copy link
Contributor

Choose a reason for hiding this comment

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

Minor: This regex tests the raw code string, so it can false-positive on CJS markers in string literals or comments (e.g., a JSDoc mentioning require()). Combined with the node_modules path gate, the practical risk is very low — the worst case is adding unused CJS params to an already-ESM function, which is harmless. Not blocking, just noting.

@ask-bonk
Copy link
Contributor

ask-bonk bot commented Mar 23, 2026

The review has been posted. Here's a summary of what I found and commented:

Review Summary for PR #665

Verdict: Request Changes (one bug, two minor items)

Bug: Windows path separator (must fix)

shouldUseCommonJsCompat at line 99 uses path.sep to check for /node_modules/ in the module path. Vite normalizes all paths to forward slashes, but path.sep is \\ on Windows. This makes the entire CJS compat layer silently inoperative on Windows. Fix: use literal "/node_modules/" instead of template with path.sep.

Minor: createRequire bypasses Vite module graph

The require created at line 167 resolves sub-modules via Node.js native resolution, not Vite. This is fine for node_modules scope but worth a brief inline comment.

Minor: Regex false positives

The COMMONJS_MARKERS regex tests raw code strings and could match CJS patterns in comments/strings. Low risk given the node_modules path gate.

What's good

  • The architectural approach is correct — noExternal: true forces node_modules through Vite's pipeline, vite-plugin-commonjs skips node_modules, ssrTransform doesn't touch CJS syntax, so the evaluator level is the right place for this fix
  • Scoping to node_modules + .cjs is conservative and correct
  • replaceNamespaceExports/toCommonJsNamespace correctly shape CJS exports into ESM namespace format
  • Test creates a realistic two-file CJS package with inter-module require()
  • Object.seal behavior matches ESModulesEvaluator pattern

github run

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.

SSR dev server fails with "module is not defined" for CJS dependencies (void-elements, highlight.js, etc.)

1 participant