diff --git a/docs/implementation-docs/2026-03-26-cedarjs-project-overview.md b/docs/implementation-docs/2026-03-26-cedarjs-project-overview.md index 552a82da27..0298d08bd9 100644 --- a/docs/implementation-docs/2026-03-26-cedarjs-project-overview.md +++ b/docs/implementation-docs/2026-03-26-cedarjs-project-overview.md @@ -13,7 +13,7 @@ │ USER PROJECT: api/src/ │ web/src/ │ cedar.toml │ Routes.tsx │ Cells │ └──────────────┬──────────────────────────────┬───────────────────────┘ │ │ -┌──────────────▼─────────────────────────────▼───────────────────────┐ +┌──────────────▼──────────────────────────────▼────────────────────────┐ │ CORE: cli│router│auth│web│api│graphql-server│vite│forms│prerender │ │ realtime│jobs│mailer│storage│record│codemods │ ├──────────────────────────────────────────────────────────────────────┤ diff --git a/packages/cli/package.json b/packages/cli/package.json index cbdc041152..22acbc38e9 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -42,6 +42,7 @@ "@cedarjs/structure": "workspace:*", "@cedarjs/telemetry": "workspace:*", "@cedarjs/utils": "workspace:*", + "@cedarjs/vite": "workspace:*", "@cedarjs/web-server": "workspace:*", "@listr2/prompt-adapter-enquirer": "4.2.1", "@opentelemetry/api": "1.9.0", @@ -88,7 +89,6 @@ "title-case": "3.0.3", "unionfs": "4.6.0", "uuid": "11.1.0", - "vite-node": "3.2.4", "yargs": "17.7.2" }, "devDependencies": { diff --git a/packages/cli/src/lib/exec.js b/packages/cli/src/lib/exec.js index 849b1298b7..1207fa40ea 100644 --- a/packages/cli/src/lib/exec.js +++ b/packages/cli/src/lib/exec.js @@ -1,9 +1,7 @@ +import { existsSync } from 'node:fs' import path from 'node:path' -import { createServer, version as viteVersion } from 'vite' -import { ViteNodeRunner } from 'vite-node/client' -import { ViteNodeServer } from 'vite-node/server' -import { installSourcemapsSupport } from 'vite-node/source-map' +import { createServer, isRunnableDevEnvironment } from 'vite' import { getPaths, importStatementPath } from '@cedarjs/project-config' import { @@ -13,8 +11,27 @@ import { cedarSwapApolloProvider, cedarImportDirPlugin, cedarAutoImportsPlugin, + cedarCjsCompatPlugin, } from '@cedarjs/vite' +// When the customResolver returns an id, that id is final — Vite won't try +// alternative extensions on it. This helper resolves the actual file on disk, +// handling both bare paths (src/lib/jobs) and .js/.jsx paths that map to .ts +// files in a TypeScript project (e.g. db.js → db.ts). +function resolveExtension(id) { + if (existsSync(id)) { + return id + } + // Strip .js/.jsx extension if present, then try TypeScript and JS extensions + const withoutExt = /\.jsx?$/.test(id) ? id.replace(/\.jsx?$/, '') : id + for (const ext of ['.ts', '.tsx', '.js', '.jsx']) { + if (existsSync(withoutExt + ext)) { + return withoutExt + ext + } + } + return id +} + export async function runScriptFunction({ path: scriptPath, functionName, @@ -28,10 +45,16 @@ export async function runScriptFunction({ const server = await createServer({ mode: 'production', optimizeDeps: { - // This is recommended in the vite-node readme noDiscovery: true, include: undefined, }, + server: { + hmr: false, + watch: null, + }, + environments: { + nodeRunnerEnv: {}, + }, resolve: { alias: [ { @@ -63,23 +86,18 @@ export async function runScriptFunction({ // from scripts/ because it doesn't know what the src/ alias is. // So we have to tell it to use the correct path based on what file // is doing the importing. - // Also, to support both imports like 'src/lib/db.js' and - // 'src/lib/db' in ts files we need to have special treatment for - // the .js extension. + // Also, to support imports like 'src/lib/db.js' in TS projects + // where only a .ts file exists, we resolve the correct extension + // ourselves — the customResolver result is final and Vite won't + // try alternative extensions on it. if (importer.startsWith(apiImportBase)) { const apiImportSrc = importStatementPath(getPaths().api.src) - let resolvedId = id.replace('src', apiImportSrc) - if (importer.endsWith('.ts') || importer.endsWith('.tsx')) { - resolvedId = resolvedId.replace(/\.jsx?$/, '') - } - return { id: resolvedId } + const resolvedId = id.replace('src', apiImportSrc) + return { id: resolveExtension(resolvedId) } } else if (importer.startsWith(webImportBase)) { const webImportSrc = importStatementPath(getPaths().web.src) - let resolvedId = id.replace('src', webImportSrc) - if (importer.endsWith('.ts') || importer.endsWith('.tsx')) { - resolvedId = resolvedId.replace(/\.jsx?$/, '') - } - return { id: resolvedId } + const resolvedId = id.replace('src', webImportSrc) + return { id: resolveExtension(resolvedId) } } return null @@ -88,6 +106,7 @@ export async function runScriptFunction({ ], }, plugins: [ + cedarCjsCompatPlugin(), cedarjsResolveCedarStyleImportsPlugin(), cedarCellTransform(), cedarjsJobPathInjectorPlugin(), @@ -97,49 +116,24 @@ export async function runScriptFunction({ ], }) - // For old Vite, this is needed to initialize the plugins. - if (Number(viteVersion.split('.')[0]) < 6) { - await server.pluginContainer.buildStart({}) + const env = server.environments.nodeRunnerEnv + if (!env || !isRunnableDevEnvironment(env)) { + await server.close() + throw new Error('Vite environment is not runnable.') } - const node = new ViteNodeServer(server, { - transformMode: { - ssr: [/.*/], - web: [/\/web\//], - }, - deps: { - fallbackCJS: true, - }, - }) - - // fixes stacktraces in Errors - installSourcemapsSupport({ - getSourceMap: (source) => node.getSourceMap(source), - }) - - const runner = new ViteNodeRunner({ - root: server.config.root, - base: server.config.base, - fetchModule(id) { - return node.fetchModule(id) - }, - resolveId(id, importer) { - return node.resolveId(id, importer) - }, - }) - let returnValue let scriptError = null try { - const script = await runner.executeFile(scriptPath) + const script = await env.runner.import(scriptPath) returnValue = await script[functionName](args) } catch (error) { scriptError = error } try { - const { db } = await runner.executeFile(path.join(getPaths().api.lib, 'db')) + const { db } = await env.runner.import(path.join(getPaths().api.lib, 'db')) db.$disconnect() } catch (e) { // silence diff --git a/packages/prerender/package.json b/packages/prerender/package.json index ea84f1ea80..be3863feb9 100644 --- a/packages/prerender/package.json +++ b/packages/prerender/package.json @@ -91,8 +91,7 @@ "rollup-plugin-swc3": "0.12.1", "unimport": "5.7.0", "unplugin-auto-import": "19.3.0", - "vite": "7.3.2", - "vite-node": "3.2.4" + "vite": "7.3.2" }, "devDependencies": { "@cedarjs/framework-tools": "workspace:*", diff --git a/packages/prerender/src/graphql/node-runner.ts b/packages/prerender/src/graphql/node-runner.ts index 193ea33bc0..91aed90a1f 100644 --- a/packages/prerender/src/graphql/node-runner.ts +++ b/packages/prerender/src/graphql/node-runner.ts @@ -1,8 +1,5 @@ -import { createServer, version as viteVersion, mergeConfig } from 'vite' -import type { ViteDevServer, UserConfig } from 'vite' -import { ViteNodeRunner } from 'vite-node/client' -import { ViteNodeServer } from 'vite-node/server' -import { installSourcemapsSupport } from 'vite-node/source-map' +import { createServer, isRunnableDevEnvironment, mergeConfig } from 'vite' +import type { ViteDevServer, RunnableDevEnvironment, UserConfig } from 'vite' import { getPaths } from '@cedarjs/project-config' import { @@ -10,6 +7,7 @@ import { cedarjsResolveCedarStyleImportsPlugin, cedarjsJobPathInjectorPlugin, cedarSwapApolloProvider, + cedarCjsCompatPlugin, } from '@cedarjs/vite' import { cedarAutoImportsPlugin } from './vite-plugin-cedar-auto-import.js' @@ -19,10 +17,16 @@ async function createViteServer(customConfig: UserConfig = {}) { const defaultConfig: UserConfig = { mode: 'production', optimizeDeps: { - // This is recommended in the vite-node readme noDiscovery: true, include: undefined, }, + server: { + hmr: false, + watch: null, + }, + environments: { + nodeRunnerEnv: {}, + }, resolve: { alias: [ { @@ -32,6 +36,7 @@ async function createViteServer(customConfig: UserConfig = {}) { ], }, plugins: [ + cedarCjsCompatPlugin(), cedarImportDirPlugin(), cedarAutoImportsPlugin(), cedarjsResolveCedarStyleImportsPlugin(), @@ -45,17 +50,12 @@ async function createViteServer(customConfig: UserConfig = {}) { const server = await createServer(mergedConfig) - // For old Vite, this is needed to initialize the plugins. - if (Number(viteVersion.split('.')[0]) < 6) { - await server.pluginContainer.buildStart({}) - } - return server } export class NodeRunner { private viteServer?: ViteDevServer = undefined - private runner?: ViteNodeRunner = undefined + private env?: RunnableDevEnvironment = undefined private readonly customViteConfig: UserConfig constructor(customViteConfig: UserConfig = {}) { @@ -64,39 +64,27 @@ export class NodeRunner { async init() { this.viteServer = await createViteServer(this.customViteConfig) - const nodeServer = new ViteNodeServer(this.viteServer, { - transformMode: { - ssr: [/.*/], - web: [/\/web\//], - }, - deps: { - fallbackCJS: true, - }, - }) - // fixes stacktraces in Errors - installSourcemapsSupport({ - getSourceMap: (source) => nodeServer?.getSourceMap(source), - }) + const env = this.viteServer.environments.nodeRunnerEnv + if (!env || !isRunnableDevEnvironment(env)) { + await this.viteServer.close() + throw new Error('Vite environment is not runnable.') + } - this.runner = new ViteNodeRunner({ - root: this.viteServer.config.root, - base: this.viteServer.config.base, - fetchModule(id) { - return nodeServer.fetchModule(id) - }, - resolveId(id, importer) { - return nodeServer.resolveId(id, importer) - }, - }) + this.env = env } async importFile(filePath: string) { - if (!this.runner) { + if (!this.env) { await this.init() } - return this.runner?.executeFile(filePath) + const env = this.env + if (!env) { + throw new Error('NodeRunner failed to initialize') + } + + return env.runner.import(filePath) } async close() { diff --git a/packages/vite/package.json b/packages/vite/package.json index 9a0be568b4..5b157bc234 100644 --- a/packages/vite/package.json +++ b/packages/vite/package.json @@ -92,6 +92,7 @@ "@vitejs/plugin-react": "4.7.0", "@whatwg-node/fetch": "0.10.13", "@whatwg-node/server": "0.10.18", + "acorn": "8.16.0", "acorn-loose": "8.5.2", "ansis": "4.2.0", "buffer": "6.0.3", diff --git a/packages/vite/src/index.ts b/packages/vite/src/index.ts index 70e4b734b0..e6d5476fee 100644 --- a/packages/vite/src/index.ts +++ b/packages/vite/src/index.ts @@ -21,6 +21,7 @@ import { cedarMergedConfig } from './plugins/vite-plugin-merged-config.js' import { cedarSwapApolloProvider } from './plugins/vite-plugin-swap-apollo-provider.js' export { cedarAutoImportsPlugin } from './plugins/vite-plugin-cedar-auto-import.js' +export { cedarCjsCompatPlugin } from './plugins/vite-plugin-cedar-cjs-compat.js' export { cedarCellTransform } from './plugins/vite-plugin-cedar-cell.js' export { cedarEntryInjectionPlugin } from './plugins/vite-plugin-cedar-entry-injection.js' export { cedarHtmlEnvPlugin } from './plugins/vite-plugin-cedar-html-env.js' diff --git a/packages/vite/src/plugins/vite-plugin-cedar-cjs-compat.ts b/packages/vite/src/plugins/vite-plugin-cedar-cjs-compat.ts new file mode 100644 index 0000000000..4e120bc71d --- /dev/null +++ b/packages/vite/src/plugins/vite-plugin-cedar-cjs-compat.ts @@ -0,0 +1,549 @@ +import path from 'node:path' + +import { parse } from 'acorn' +import type { Plugin } from 'vite' + +// UMD heuristics borrowed from @chialab/cjs-to-esm — matches patterns like +// `typeof module.exports === 'object'` and `typeof define === 'function'`. +const UMD_REGEXES = [ + /\btypeof\s+(module\.exports|module|exports)\s*===?\s*['"]object['"]/, + /['"]object['"]\s*===?\s*typeof\s+(module\.exports|module|exports)/, + /\btypeof\s+define\s*===?\s*['"]function['"]/, + /['"]function['"]\s*===?\s*typeof\s+define/, +] + +/** + * Minimal AST node type used during manual traversal. Every property is + * typed as `unknown` so we are forced to validate before use. + */ +interface AstNode { + type: string + [key: string]: unknown +} + +/** + * Type guard that checks whether a value is an AST node (i.e. an object + * with a `type` string property). + */ +function isAstNode(value: unknown): value is AstNode { + return value !== null && typeof value === 'object' && 'type' in value +} + +/** + * Type guard that checks whether an object has a specific property key. + */ +function hasProperty( + obj: object, + key: K, +): obj is Record { + return key in obj +} + +/** + * Walk an acorn AST and call the visitor for every node. The visitor may + * return `false` to skip descending into that node's children. + */ +function walkAst(node: unknown, visitor: (node: AstNode) => false | void) { + if (Array.isArray(node)) { + for (const child of node) { + walkAst(child, visitor) + } + return + } + + if (!isAstNode(node)) { + return + } + + const shouldDescend = visitor(node) + if (shouldDescend === false) { + return + } + + for (const key of Object.keys(node)) { + if (key === 'type' || key === 'loc' || key === 'range') { + continue + } + const child = node[key] + if (child && typeof child === 'object') { + walkAst(child, visitor) + } + } +} + +/** + * Extract the name of an Identifier node, or return `null`. + */ +function getIdentifierName(node: unknown): string | null { + if (!isAstNode(node)) { + return null + } + return node.type === 'Identifier' && typeof node.name === 'string' + ? node.name + : null +} + +/** + * Check whether a node represents `module.exports`. + */ +function isModuleExports(node: unknown): boolean { + if (!isAstNode(node)) { + return false + } + return ( + node.type === 'MemberExpression' && + getIdentifierName(node.object) === 'module' && + getIdentifierName(node.property) === 'exports' + ) +} + +/** + * Check whether a node represents a re-export: + * `module.exports = require(...)`. + */ +function isReExport(node: unknown): boolean { + if (!isAstNode(node) || node.type !== 'AssignmentExpression') { + return false + } + + return ( + isModuleExports(node.left) && + isAstNode(node.right) && + node.right.type === 'CallExpression' && + getIdentifierName(node.right.callee) === 'require' + ) +} + +/** + * Check whether a node represents `Object.defineProperty(exports, key, { get: … })` + * or a setter descriptor. Plain value descriptors (e.g. `__esModule` markers) + * are allowed because they are harmless and are handled correctly by the + * generated wrapper at runtime. + * + * If the descriptor is not a statically analyzable object literal we return + * `false` — a false negative is safer than breaking on harmless code. + */ +function isObjectDefinePropertyWithGetterOnExports(node: unknown): boolean { + if (!isAstNode(node) || node.type !== 'CallExpression') { + return false + } + + if (!isObjectDefineProperty(node)) { + return false + } + + const args = node.arguments + if ( + !Array.isArray(args) || + args.length < 3 || + getIdentifierName(args[0]) !== 'exports' + ) { + return false + } + + const descriptor = args[2] + if (!isAstNode(descriptor) || descriptor.type !== 'ObjectExpression') { + return false + } + + const props = descriptor.properties + if (!Array.isArray(props)) { + return false + } + + return props.some((prop) => { + if (!isAstNode(prop) || prop.type !== 'Property') { + return false + } + const name = getIdentifierName(prop.key) + return name === 'get' || name === 'set' + }) +} + +/** + * Check whether a node represents `module.exports = { ... }` (an object + * literal assignment). If it does, returns the ObjectExpression node so + * the caller can extract named exports from it. + */ +function getModuleExportsObjectLiteral(node: unknown): AstNode | null { + if (!isAstNode(node) || node.type !== 'AssignmentExpression') { + return null + } + + if ( + isModuleExports(node.left) && + isAstNode(node.right) && + node.right.type === 'ObjectExpression' + ) { + return node.right + } + + return null +} + +/** + * Check whether a node represents `Object.defineProperty(...)`. + */ +function isObjectDefineProperty(node: unknown): boolean { + if (!isAstNode(node) || node.type !== 'CallExpression') { + return false + } + + const callee = node.callee + if (!isAstNode(callee) || callee.type !== 'MemberExpression') { + return false + } + + return ( + getIdentifierName(callee.object) === 'Object' && + getIdentifierName(callee.property) === 'defineProperty' + ) +} + +/** + * Build a location string from an acorn node for error messages. + */ +function formatLoc(node: AstNode): string { + const loc = node.loc + if (typeof loc !== 'object' || loc === null || !hasProperty(loc, 'start')) { + return 'unknown location' + } + + const start = loc.start + if ( + typeof start !== 'object' || + start === null || + !hasProperty(start, 'line') || + !hasProperty(start, 'column') + ) { + return 'unknown location' + } + + const line = start.line + const column = start.column + if (typeof line !== 'number' || typeof column !== 'number') { + return 'unknown location' + } + + return `line ${line}, column ${column}` +} + +/** + * 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 `cjs-module-lexer` (a Vite transitive dependency) to detect named + * exports so they are individually re-exported and accessible without going + * through `.default`. + * + * Known limitations (documented inline where relevant): + * - No source-map support (`map: null`). + * - Object.defineProperty(exports, key, { get: () => ... }) with getter or + * setter descriptors are evaluated eagerly at module-load time rather than + * lazily; plain value descriptors (e.g. __esModule) are allowed. + * - Properties added to a function/class after `module.exports = fn` are + * not re-exported. + * - Circular dependencies rely on Node's native behaviour via + * `createRequire` and are not handled as robustly as Rollup's synthetic + * namespace objects. + */ +export function cedarCjsCompatPlugin(): Plugin { + let lexerInitialized = false + + return { + name: 'cedar-cjs-compat', + enforce: 'pre', + async 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 + } + + // Fast bail-out: if there's no CJS-looking syntax, skip parsing. + if (!/\bmodule\.exports\b|\bexports\.[a-zA-Z_$]/.test(code)) { + return null + } + + // Detect UMD wrappers via regex before parsing — these are rare and + // the regex is sufficient for the common patterns. + const isUmd = UMD_REGEXES.some((re) => re.test(code)) + if (isUmd) { + throw new Error( + `CedarJS CJS compat plugin does not support UMD modules. ` + + `File: ${id}\n` + + `If you need to load this file in a Vite RunnableDevEnvironment, ` + + `consider converting it to pure ESM or using a pre-bundled ESM ` + + `build from the package author.`, + ) + } + + let ast: ReturnType + try { + ast = parse(code, { ecmaVersion: 'latest', sourceType: 'script' }) + } catch { + // If acorn can't parse it, it's probably not plain JS (or it's + // malformed). Skip and let Vite handle it. + return null + } + + /** + * Scan the AST for unsupported CJS patterns and return the + * `module.exports = { ... }` ObjectExpression node if found. + */ + function scanForUnsupportedPatterns(astBody: unknown): AstNode | null { + let objectLiteral: AstNode | null = null + + walkAst(astBody, (node) => { + // Stop descending into nested functions — we only care about top-level + // module-scoped statements. + if ( + node.type === 'FunctionDeclaration' || + node.type === 'FunctionExpression' || + node.type === 'ArrowFunctionExpression' + ) { + return false + } + + if (node.type !== 'ExpressionStatement') { + return undefined + } + + const expr = node.expression + if (!isAstNode(expr)) { + return undefined + } + + // 1) Re-export detection: module.exports = require(...) + if (isReExport(expr)) { + throw new Error( + `CedarJS CJS compat plugin does not support re-exports ` + + `(module.exports = require(...)). File: ${id}\n` + + `Named exports from the re-exported module would be lost. ` + + `Convert the file to explicit named exports, or import the ` + + `target module directly in the consumer.`, + ) + } + + // 2) Object.defineProperty(exports, key, { get: () => ... }) — + // getter/setter descriptors are evaluated eagerly. Plain value + // descriptors (e.g. __esModule markers) are allowed. + if (isObjectDefinePropertyWithGetterOnExports(expr)) { + throw new Error( + `CedarJS CJS compat plugin does not support Object.defineProperty ` + + `with getter/setter descriptors on exports because they would ` + + `be evaluated eagerly at load time rather than lazily. ` + + `File: ${id}\n` + + `Convert the file to plain property assignments ` + + `(exports.foo = ...) or use an ESM build of the package.`, + ) + } + + // 3) module.exports = { ... } object literal + const objectLiteralExpr = getModuleExportsObjectLiteral(expr) + if (objectLiteralExpr) { + objectLiteral = objectLiteralExpr + // Don't descend into the object literal — we already have it. + return false + } + + return undefined + }) + + return objectLiteral + } + + const objectLiteralAssignment = scanForUnsupportedPatterns(ast) + + // 4) Local `exports` shadowing — scan top-level declarations. + walkAst(ast, (node) => { + if ( + node.type === 'FunctionDeclaration' || + node.type === 'FunctionExpression' || + node.type === 'ArrowFunctionExpression' + ) { + return false + } + + if (node.type !== 'VariableDeclaration') { + return undefined + } + + const declarations = node.declarations + if (!Array.isArray(declarations)) { + return undefined + } + + for (const decl of declarations) { + if (!isAstNode(decl)) { + continue + } + + const declId = decl.id + if (!isAstNode(declId)) { + continue + } + + if ( + declId.type === 'Identifier' && + typeof declId.name === 'string' && + declId.name === 'exports' + ) { + throw new Error( + `CedarJS CJS compat plugin does not support local variables ` + + `named 'exports' because they shadow the injected CJS ` + + `globals. File: ${id}\n` + + `Rename the local variable to something else.`, + ) + } + + if (declId.type === 'ObjectPattern') { + const props = declId.properties + if (Array.isArray(props)) { + for (const prop of props) { + if ( + isAstNode(prop) && + prop.type === 'Property' && + getIdentifierName(prop.value) === 'exports' + ) { + throw new Error( + `CedarJS CJS compat plugin does not support destructuring ` + + `into a local variable named 'exports' because it shadows ` + + `the injected CJS globals. File: ${id}\n` + + `Rename the local variable to something else.`, + ) + } + } + } + } + } + + return undefined + }) + + // Use cjs-module-lexer to statically extract named exports from + // `exports.foo = ...` and `Object.defineProperty(exports, 'foo', ...)`. + let namedExports: string[] = [] + try { + if (!lexerInitialized) { + const { init } = await import('cjs-module-lexer') + await init() + lexerInitialized = true + } + const { parse: parseLexer } = await import('cjs-module-lexer') + const { exports } = parseLexer(code) + namedExports = exports.filter( + (e) => /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(e) && e !== 'default', + ) + } catch { + // If the lexer fails, fall back to default-only export + } + + // Fallback: extract named exports from `module.exports = { ... }`. + if (namedExports.length === 0 && objectLiteralAssignment) { + const props = objectLiteralAssignment.properties + if (Array.isArray(props)) { + for (const prop of props) { + if (!isAstNode(prop)) { + continue + } + + if (prop.type === 'SpreadElement') { + throw new Error( + `CedarJS CJS compat plugin encountered an unsupported ` + + `pattern in module.exports = { ... } at ` + + `${formatLoc(prop)} (spread element (...)). File: ${id}\n` + + `Convert the object literal to plain property assignments ` + + `(exports.foo = ...) so that cjs-module-lexer can detect ` + + `the named exports, or use an ESM build of the package.`, + ) + } + + if (prop.computed) { + throw new Error( + `CedarJS CJS compat plugin encountered an unsupported ` + + `pattern in module.exports = { ... } at ` + + `${formatLoc(prop)} (computed property key ([expr])). ` + + `File: ${id}\n` + + `Convert the object literal to plain property assignments ` + + `(exports.foo = ...) so that cjs-module-lexer can detect ` + + `the named exports, or use an ESM build of the package.`, + ) + } + + if (prop.method) { + throw new Error( + `CedarJS CJS compat plugin encountered an unsupported ` + + `pattern in module.exports = { ... } at ` + + `${formatLoc(prop)} (method shorthand). File: ${id}\n` + + `Convert the object literal to plain property assignments ` + + `(exports.foo = ...) so that cjs-module-lexer can detect ` + + `the named exports, or use an ESM build of the package.`, + ) + } + + if (prop.shorthand) { + throw new Error( + `CedarJS CJS compat plugin encountered an unsupported ` + + `pattern in module.exports = { ... } at ` + + `${formatLoc(prop)} (shorthand property). File: ${id}\n` + + `Convert the object literal to plain property assignments ` + + `(exports.foo = ...) so that cjs-module-lexer can detect ` + + `the named exports, or use an ESM build of the package.`, + ) + } + + const keyName = getIdentifierName(prop.key) + if (keyName && keyName !== 'default') { + namedExports.push(keyName) + } + } + } + } + + const dirPath = JSON.stringify(path.dirname(id)) + const filePath = JSON.stringify(id) + + const hasEsModuleFlag = namedExports.includes('__esModule') + const safeNamedExports = namedExports.filter( + (e) => e !== '__esModule' && e !== 'default', + ) + + const namedExportLines = safeNamedExports + .map( + (name) => + `export const ${name} = __cjs_result__[${JSON.stringify(name)}]`, + ) + .join('\n') + + // If the module sets __esModule (typical of transpiled ESM→CJS), + // unwrap the .default so that `import foo from './file'` returns the + // actual default export rather than the wrapper object. + const defaultExportLine = hasEsModuleFlag + ? `export default (__cjs_result__ != null && typeof __cjs_result__ === 'object' && 'default' in __cjs_result__ ? __cjs_result__.default : __cjs_result__)` + : `export default __cjs_result__` + + 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 + ${defaultExportLine} + ${namedExportLines} + `, + // Source maps are not generated. If you hit a break-point issue inside + // a CJS file loaded through this plugin, the line numbers will be off + // by the number of lines in the wrapper preamble (~10). + map: null, + } + }, + } +} diff --git a/yarn.lock b/yarn.lock index 8821d6f9d3..59e93ed43b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2899,6 +2899,7 @@ __metadata: "@cedarjs/structure": "workspace:*" "@cedarjs/telemetry": "workspace:*" "@cedarjs/utils": "workspace:*" + "@cedarjs/vite": "workspace:*" "@cedarjs/web-server": "workspace:*" "@listr2/prompt-adapter-enquirer": "npm:4.2.1" "@opentelemetry/api": "npm:1.9.0" @@ -2951,7 +2952,6 @@ __metadata: typescript: "npm:5.9.3" unionfs: "npm:4.6.0" uuid: "npm:11.1.0" - vite-node: "npm:3.2.4" vitest: "npm:3.2.4" yargs: "npm:17.7.2" bin: @@ -3487,7 +3487,6 @@ __metadata: unimport: "npm:5.7.0" unplugin-auto-import: "npm:19.3.0" vite: "npm:7.3.2" - vite-node: "npm:3.2.4" vitest: "npm:3.2.4" peerDependencies: react: 19.2.3 @@ -3785,6 +3784,7 @@ __metadata: "@vitejs/plugin-react": "npm:4.7.0" "@whatwg-node/fetch": "npm:0.10.13" "@whatwg-node/server": "npm:0.10.18" + acorn: "npm:8.16.0" acorn-loose: "npm:8.5.2" ansis: "npm:4.2.0" buffer: "npm:6.0.3" @@ -12008,7 +12008,7 @@ __metadata: languageName: node linkType: hard -"acorn@npm:^8.1.0, acorn@npm:^8.11.0, acorn@npm:^8.12.1, acorn@npm:^8.14.1, acorn@npm:^8.15.0, acorn@npm:^8.16.0, acorn@npm:^8.4.1, acorn@npm:^8.8.1, acorn@npm:^8.9.0": +"acorn@npm:8.16.0, acorn@npm:^8.1.0, acorn@npm:^8.11.0, acorn@npm:^8.12.1, acorn@npm:^8.14.1, acorn@npm:^8.15.0, acorn@npm:^8.16.0, acorn@npm:^8.4.1, acorn@npm:^8.8.1, acorn@npm:^8.9.0": version: 8.16.0 resolution: "acorn@npm:8.16.0" bin: