-
-
Notifications
You must be signed in to change notification settings - Fork 25
fix(prerender): make CJS compat plugin work with Vite 6 nodeRunnerEnv #1735
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,19 +1,228 @@ | ||
| import path from 'node:path' | ||
|
|
||
| import { createServer, isRunnableDevEnvironment, mergeConfig } from 'vite' | ||
| import type { ViteDevServer, RunnableDevEnvironment, UserConfig } from 'vite' | ||
| import type { | ||
| Plugin, | ||
| ViteDevServer, | ||
| RunnableDevEnvironment, | ||
| UserConfig, | ||
| } from 'vite' | ||
|
|
||
| import { getPaths } from '@cedarjs/project-config' | ||
| import { | ||
| cedarCellTransform, | ||
| cedarjsResolveCedarStyleImportsPlugin, | ||
| cedarjsJobPathInjectorPlugin, | ||
| cedarSwapApolloProvider, | ||
| cedarCjsCompatPlugin, | ||
| } from '@cedarjs/vite' | ||
|
|
||
| import { cedarAutoImportsPlugin } from './vite-plugin-cedar-auto-import.js' | ||
| import { cedarImportDirPlugin } from './vite-plugin-cedar-import-dir.js' | ||
|
|
||
| // Initialize cjs-module-lexer eagerly at module load so it's available before | ||
| // any Vite transforms run. buildStart is not guaranteed to fire for all Vite | ||
| // environments (e.g. nodeRunnerEnv), so we can't rely on it for initialization. | ||
| let lexerParse: ((code: string) => { exports: string[] }) | null = null | ||
| const lexerReady: Promise<void> = import('cjs-module-lexer') | ||
| .then(({ init, parse }) => | ||
| init().then(() => { | ||
| lexerParse = parse | ||
| }), | ||
| ) | ||
| .catch(() => { | ||
| // Fall back to extractCjsNamedExports only if cjs-module-lexer is unavailable | ||
| }) | ||
|
|
||
| /** | ||
| * Extracts named exports from CommonJS code without relying on cjs-module-lexer, | ||
| * which fails to detect exports when values are function expressions or other | ||
| * non-trivial expressions (e.g. `module.exports = { handler: function() {} }`). | ||
| * | ||
| * Handles two CJS export patterns: | ||
| * - `exports.key = value` — regex | ||
| * - `module.exports = { key: value, ... }` — brace-counting to find top-level keys | ||
| */ | ||
| function extractCjsNamedExports(code: string): string[] { | ||
| const identifierRe = /^[a-zA-Z_$][a-zA-Z0-9_$]*$/ | ||
| const namedExports = new Set<string>() | ||
|
|
||
| // Pattern 1: exports.key = value | ||
| const exportsAssignRe = /\bexports\.([a-zA-Z_$][a-zA-Z0-9_$]*)\s*=/g | ||
| for (const match of code.matchAll(exportsAssignRe)) { | ||
| if (match[1] !== 'default') { | ||
| namedExports.add(match[1]) | ||
| } | ||
| } | ||
|
|
||
| // Pattern 2: module.exports = { key: value, ... } | ||
| // Brace-counting scan to capture only top-level property keys, correctly | ||
| // skipping over nested objects, function bodies, strings, and comments. | ||
| const assignMatch = code.match(/module\.exports\s*=\s*\{/) | ||
| if (assignMatch?.index !== undefined) { | ||
| const bodyStart = assignMatch.index + assignMatch[0].length | ||
| let depth = 1 | ||
| let pos = bodyStart | ||
|
|
||
| while (pos < code.length && depth > 0) { | ||
| const ch = code[pos] | ||
|
|
||
| // Skip strings (single, double, template) | ||
| if (ch === '"' || ch === "'" || ch === '`') { | ||
| const quote = ch | ||
| pos++ | ||
| while (pos < code.length && code[pos] !== quote) { | ||
| if (code[pos] === '\\') { | ||
| pos++ | ||
| } | ||
| pos++ | ||
| } | ||
| pos++ | ||
| continue | ||
| } | ||
|
|
||
| // Skip line comments | ||
| if (ch === '/' && code[pos + 1] === '/') { | ||
| while (pos < code.length && code[pos] !== '\n') { | ||
| pos++ | ||
| } | ||
| continue | ||
| } | ||
|
|
||
| // Skip block comments | ||
| if (ch === '/' && code[pos + 1] === '*') { | ||
| pos += 2 | ||
| while ( | ||
| pos < code.length && | ||
| !(code[pos] === '*' && code[pos + 1] === '/') | ||
| ) { | ||
| pos++ | ||
| } | ||
| pos += 2 | ||
| continue | ||
| } | ||
|
|
||
| if (ch === '{' || ch === '(' || ch === '[') { | ||
| depth++ | ||
| pos++ | ||
| continue | ||
| } | ||
|
|
||
| if (ch === '}' || ch === ')' || ch === ']') { | ||
| depth-- | ||
| pos++ | ||
| continue | ||
| } | ||
|
|
||
| // At depth 1 (top level of the object literal), look for `identifier:` patterns | ||
| if (depth === 1 && /[a-zA-Z_$]/.test(ch)) { | ||
| const keyMatch = code | ||
| .slice(pos) | ||
| .match(/^([a-zA-Z_$][a-zA-Z0-9_$]*)\s*:/) | ||
| if ( | ||
| keyMatch && | ||
| keyMatch[1] !== 'default' && | ||
| identifierRe.test(keyMatch[1]) | ||
| ) { | ||
| namedExports.add(keyMatch[1]) | ||
| pos += keyMatch[0].length | ||
| continue | ||
| } | ||
| } | ||
|
|
||
| pos++ | ||
| } | ||
| } | ||
|
|
||
| return [...namedExports] | ||
| } | ||
|
|
||
| /** | ||
| * A Vite plugin that transforms CommonJS files to ESM so they work with | ||
| * Vite 6's RunnableDevEnvironment / ESModulesEvaluator, which doesn't | ||
| * understand `module.exports` syntax. | ||
| * | ||
| * Uses two complementary strategies for named export detection: | ||
| * 1. `cjs-module-lexer` (initialized at module load via `lexerReady`) — handles | ||
| * esbuild-compiled packages using the `__export` + `0 && (module.exports = {...})` | ||
| * annotation pattern. | ||
| * 2. `extractCjsNamedExports` — handles hand-written CJS with function values | ||
| * that cjs-module-lexer cannot statically detect. | ||
| * | ||
| * The transform hook is intentionally synchronous: Vite 6's non-default | ||
| * environments (e.g. nodeRunnerEnv) do not reliably call async transform hooks | ||
| * or buildStart, so the lexer must be initialized at module load time instead. | ||
| */ | ||
| function cjsCompatPlugin(): Plugin { | ||
| return { | ||
| name: 'cedar-cjs-compat', | ||
| enforce: 'pre', | ||
|
|
||
| transform(code, id) { | ||
| // Only handle plain .js / .cjs files — TypeScript and JSX are already | ||
| // transformed by Vite's esbuild plugin and will be valid ESM. | ||
| if (!/\.[cm]?js$/.test(id)) { | ||
| return null | ||
| } | ||
|
|
||
| // Quick heuristic: skip files that don't look like CJS | ||
| if (!/\bmodule\.exports\b|\bexports\.\w+/.test(code)) { | ||
| return null | ||
| } | ||
|
|
||
| // Combine both strategies: cjs-module-lexer handles esbuild-compiled | ||
| // packages; extractCjsNamedExports handles hand-written modules with | ||
| // function values that cjs-module-lexer cannot statically detect. | ||
| const namedExports = new Set(extractCjsNamedExports(code)) | ||
| if (lexerParse) { | ||
| try { | ||
| const { exports } = lexerParse(code) | ||
| for (const e of exports) { | ||
| if (/^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(e) && e !== 'default') { | ||
| namedExports.add(e) | ||
| } | ||
| } | ||
| } catch { | ||
| // Ignore — extractCjsNamedExports result is still used | ||
| } | ||
|
Comment on lines
+176
to
+186
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
The named-export filter ( |
||
| } | ||
|
|
||
| const dirPath = JSON.stringify(path.dirname(id)) | ||
| const filePath = JSON.stringify(id) | ||
|
|
||
| const namedExportLines = [...namedExports] | ||
| .map( | ||
| (name) => | ||
| `export const ${name} = __cjs_result__[${JSON.stringify(name)}]`, | ||
| ) | ||
| .join('\n') | ||
|
|
||
| return { | ||
| code: ` | ||
| import { createRequire as __createRequire__ } from 'node:module' | ||
| const require = __createRequire__(${filePath}) | ||
| const module = { exports: {} } | ||
| const exports = module.exports | ||
| const __dirname = ${dirPath} | ||
| const __filename = ${filePath} | ||
| ;(function() { | ||
| ${code} | ||
| }).call(module.exports) | ||
| const __cjs_result__ = module.exports | ||
| export default __cjs_result__ | ||
| ${namedExportLines} | ||
| `, | ||
|
Comment on lines
+192
to
+213
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
The original |
||
| map: null, | ||
| } | ||
| }, | ||
| } | ||
| } | ||
|
|
||
| async function createViteServer(customConfig: UserConfig = {}) { | ||
| // Ensure cjs-module-lexer is initialized before any file transforms run. | ||
| // We can't rely on buildStart for this because it isn't guaranteed to fire | ||
| // for non-default Vite environments (e.g. nodeRunnerEnv). | ||
| await lexerReady | ||
|
|
||
| const defaultConfig: UserConfig = { | ||
| mode: 'production', | ||
| optimizeDeps: { | ||
|
|
@@ -36,7 +245,7 @@ async function createViteServer(customConfig: UserConfig = {}) { | |
| ], | ||
| }, | ||
| plugins: [ | ||
| cedarCjsCompatPlugin(), | ||
| cjsCompatPlugin(), | ||
| cedarImportDirPlugin(), | ||
| cedarAutoImportsPlugin(), | ||
| cedarjsResolveCedarStyleImportsPlugin(), | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The original plugin detected UMD wrappers (
typeof module.exports === 'object',typeof define === 'function', etc.) and threw an explicit, actionable error. The new plugin skips that check entirely: a UMD file passes the heuristic (module.exportsis present), gets wrapped in the CJS shim, and produces broken runtime output with no diagnostic message. Adding the same UMD regex checks from the original plugin before the transform would preserve the fail-fast behavior.