diff --git a/deno.lock b/deno.lock index 2cfe7d53bde..3d5970098a8 100644 --- a/deno.lock +++ b/deno.lock @@ -1,7 +1,6 @@ { "version": "5", "specifiers": { - "jsr:@astral/astral@0.5.6": "0.5.6", "jsr:@astral/astral@~0.5.6": "0.5.6", "jsr:@deno-library/progress@^1.5.1": "1.5.1", "jsr:@deno/cache-dir@0.14": "0.14.0", diff --git a/packages/plugin-vite/demo/fixtures/commonjs_mod.cjs b/packages/plugin-vite/demo/fixtures/commonjs_mod.cjs new file mode 100644 index 00000000000..1f6a76bc478 --- /dev/null +++ b/packages/plugin-vite/demo/fixtures/commonjs_mod.cjs @@ -0,0 +1 @@ +exports.value = "ok"; diff --git a/packages/plugin-vite/demo/fixtures/commonjs_mod.js b/packages/plugin-vite/demo/fixtures/commonjs_mod.js deleted file mode 100644 index 44d44847d02..00000000000 --- a/packages/plugin-vite/demo/fixtures/commonjs_mod.js +++ /dev/null @@ -1 +0,0 @@ -export const value = "ok"; diff --git a/packages/plugin-vite/demo/fixtures/maxmind.cjs b/packages/plugin-vite/demo/fixtures/maxmind.cjs new file mode 100644 index 00000000000..bbef3806916 --- /dev/null +++ b/packages/plugin-vite/demo/fixtures/maxmind.cjs @@ -0,0 +1,8 @@ +"use strict"; +// deno-lint-ignore no-var +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +const assert_1 = __importDefault(require("assert")); +(0, assert_1.default)(true); diff --git a/packages/plugin-vite/demo/fixtures/maxmind.js b/packages/plugin-vite/demo/fixtures/maxmind.js deleted file mode 100644 index 8710fa40a4c..00000000000 --- a/packages/plugin-vite/demo/fixtures/maxmind.js +++ /dev/null @@ -1,2 +0,0 @@ -import assert from "node:assert"; -assert(true); diff --git a/packages/plugin-vite/demo/routes/tests/cjs_npm.tsx b/packages/plugin-vite/demo/routes/tests/cjs_npm.tsx deleted file mode 100644 index e8a333346ba..00000000000 --- a/packages/plugin-vite/demo/routes/tests/cjs_npm.tsx +++ /dev/null @@ -1,6 +0,0 @@ -import qs from "qs"; - -export default function Page() { - const parsed = qs.parse("a=1&b=2"); - return

{parsed.a === "1" ? "qs-ok" : "qs-fail"}

; -} diff --git a/packages/plugin-vite/demo/routes/tests/commonjs.tsx b/packages/plugin-vite/demo/routes/tests/commonjs.tsx index 32308633baf..8f73c79b934 100644 --- a/packages/plugin-vite/demo/routes/tests/commonjs.tsx +++ b/packages/plugin-vite/demo/routes/tests/commonjs.tsx @@ -1,4 +1,4 @@ -import { value } from "../../fixtures/commonjs_mod.js"; +import { value } from "../../fixtures/commonjs_mod.cjs"; export default function Page() { return

{value}

; diff --git a/packages/plugin-vite/demo/routes/tests/maxmind.tsx b/packages/plugin-vite/demo/routes/tests/maxmind.tsx index 8ac0424445b..b42a8ada37f 100644 --- a/packages/plugin-vite/demo/routes/tests/maxmind.tsx +++ b/packages/plugin-vite/demo/routes/tests/maxmind.tsx @@ -1,4 +1,4 @@ -import * as maxmind from "../../fixtures/maxmind.js"; +import * as maxmind from "../../fixtures/maxmind.cjs"; export default function Page() { // deno-lint-ignore no-console diff --git a/packages/plugin-vite/src/mod.ts b/packages/plugin-vite/src/mod.ts index 8fde99478c6..c49c244526c 100644 --- a/packages/plugin-vite/src/mod.ts +++ b/packages/plugin-vite/src/mod.ts @@ -82,43 +82,15 @@ export function fresh(config?: FreshViteConfig): Plugin[] { }); let isDev = false; - let freshMode = "development"; const plugins: Plugin[] = [ { name: "fresh", sharedDuringBuild: true, - async config(config, env) { + config(config, env) { isDev = env.command === "serve"; - freshMode = isDev ? "development" : "production"; - - // Load env files early so define entries are available - const root = config.root ? path.resolve(config.root) : Deno.cwd(); - const envDir = config.envDir ? path.resolve(root, config.envDir) : root; - await loadEnvFile(path.join(envDir, ".env")); - await loadEnvFile(path.join(envDir, ".env.local")); - await loadEnvFile(path.join(envDir, `.env.${freshMode}`)); - await loadEnvFile(path.join(envDir, `.env.${freshMode}.local`)); - - // Build define map for FRESH_PUBLIC_* env vars - // Replaces the Babel inlineEnvVarsPlugin with Vite's native define - const envDefine: Record = {}; - for (const [key, value] of Object.entries(Deno.env.toObject())) { - if (key.startsWith("FRESH_PUBLIC_")) { - envDefine[`process.env.${key}`] = JSON.stringify(value); - envDefine[`import.meta.env.${key}`] = JSON.stringify(value); - } - } return { - define: envDefine, - ssr: { - // Bundle all deps in SSR so that resolve.alias - // (react -> preact/compat) is applied consistently. - // CJS packages are handled by the deno plugin's load - // hook which wraps them in an ESM-compatible shim. - noExternal: true, - }, server: { watch: { // Ignore temp files, editor swap files, and Vite timestamp @@ -147,15 +119,14 @@ export function fresh(config?: FreshViteConfig): Plugin[] { "react-dom": "preact/compat", react: "preact/compat", }, + // Disallow externals, because it leads to duplicate + // modules with `preact` vs `npm:preact@*` in the server + // environment. + noExternal: true, }, - optimizeDeps: { - // Disable dep optimizer because deno.ts handles all - // module resolution. The optimizer causes duplicate - // module instances when remote (JSR) islands resolve - // deps to /@fs/ paths while the optimizer bundles to - // /.vite/deps/. CJS packages in client-side islands - // are handled by deno.ts's load hook. + // Optimize deps somehow leads to duplicate modules or them + // being placed in the wrong chunks... noDiscovery: true, }, @@ -221,6 +192,14 @@ export function fresh(config?: FreshViteConfig): Plugin[] { return; } + // Ignore commonjs optional exports + if ( + warning.code === "MISSING_EXPORT" && + warning.message.includes("__require") + ) { + return; + } + // Ignore this warnings if (warning.code === "THIS_IS_UNDEFINED") { return; @@ -242,7 +221,7 @@ export function fresh(config?: FreshViteConfig): Plugin[] { }, }; }, - configResolved(vConfig) { + async configResolved(vConfig) { // Run update check in background updateCheck(UPDATE_INTERVAL).catch(() => {}); @@ -257,45 +236,19 @@ export function fresh(config?: FreshViteConfig): Plugin[] { const name = fConfig.namer.getUniqueName(specName); fConfig.islandSpecifiers.set(spec, name); }); - }, - }, - // Lightweight replacement for Deno.env.get() calls with FRESH_PUBLIC_* - // and NODE_ENV values. Replaces the Babel inlineEnvVarsPlugin for this - // pattern which can't be handled by Vite's define (it's a call expression). - { - name: "fresh:deno-env", - sharedDuringBuild: true, - applyToEnvironment() { - return true; - }, - transform: { - filter: { - id: /\.([tj]sx?|[mc]?[tj]s)(\?.*)?$/, - }, - handler(code) { - if (!code.includes("Deno.env.get(")) return; - const allEnv = Deno.env.toObject(); - let modified = false; - const result = code.replace( - /Deno\.env\.get\(\s*["']([^"']+)["']\s*\)/g, - (match: string, name: string) => { - if (name === "NODE_ENV") { - modified = true; - return JSON.stringify(freshMode); - } - if (name.startsWith("FRESH_PUBLIC_") && name in allEnv) { - modified = true; - return JSON.stringify(allEnv[name]); - } - return match; - }, - ); + const envDir = pathWithRoot( + vConfig.envDir || vConfig.root, + vConfig.root, + ); - if (modified) return { code: result }; - }, + await loadEnvFile(path.join(envDir, ".env")); + await loadEnvFile(path.join(envDir, ".env.local")); + const mode = isDev ? "development" : "production"; + await loadEnvFile(path.join(envDir, `.env.${mode}`)); + await loadEnvFile(path.join(envDir, `.env.${mode}.local`)); }, - } satisfies Plugin, + }, serverEntryPlugin(fConfig), patches(), ...serverSnapshot(fConfig), diff --git a/packages/plugin-vite/src/plugins/deno.ts b/packages/plugin-vite/src/plugins/deno.ts index 2ab1e22ac95..6cefed7e463 100644 --- a/packages/plugin-vite/src/plugins/deno.ts +++ b/packages/plugin-vite/src/plugins/deno.ts @@ -9,7 +9,7 @@ import { import * as path from "@std/path"; import * as babel from "@babel/core"; import { httpAbsolute } from "./patches/http_absolute.ts"; -import { JSX_REG } from "../utils.ts"; +import { JS_REG, JSX_REG } from "../utils.ts"; import { builtinModules } from "node:module"; // @ts-ignore Workaround for https://github.com/denoland/deno/issues/30850 @@ -17,43 +17,16 @@ const { default: babelReact } = await import("@babel/preset-react"); const BUILTINS = new Set(builtinModules); +interface DenoState { + type: RequestedModuleType; +} + export function deno(): Plugin { let ssrLoader: Loader; let browserLoader: Loader; let isDev = false; - // Cache for package.json "type" field lookups. Per Node.js semantics, - // .js files in node_modules are CJS unless the nearest package.json - // has "type": "module". - const pkgTypeCache = new Map(); - async function isEsmPackage(filePath: string): Promise { - let dir = path.dirname(filePath); - while (true) { - const cached = pkgTypeCache.get(dir); - if (cached !== undefined) return cached; - - try { - const text = await Deno.readTextFile(path.join(dir, "package.json")); - const isEsm = JSON.parse(text).type === "module"; - pkgTypeCache.set(dir, isEsm); - return isEsm; - } catch { - const parent = path.dirname(dir); - if (parent === dir) break; - dir = parent; - } - } - return false; - } - - // Detect actual ESM export/import statements at the statement level. - // More robust than code.includes("export ") which matches comments - // and strings (e.g. "// Remove export in next version" would trick it). - // Handles both formatted and minified ESM (e.g. "import{" with no space). - const ESM_STMT_RE = - /(?:^|[\n;])\s*(?:export\s*[{*]|export\s+(?:default|const|let|var|function|class|async)\b|import\s*[{*"']|import\s+[a-zA-Z_$])/; - return { name: "deno", sharedDuringBuild: true, @@ -112,21 +85,6 @@ export function deno(): Plugin { id = `${url.origin}${id}`; } - // Apply resolve.alias before Deno resolution so that - // react -> preact/compat works even in externalized packages. - // Vite normalizes alias config to { find, replacement }[] format. - const aliases = this.environment?.config?.resolve?.alias; - if (aliases) { - const list = Array.isArray(aliases) ? aliases : []; - for (const alias of list) { - const find = alias.find; - if (typeof find === "string" ? find === id : find?.test?.(id)) { - id = typeof alias.replacement === "string" ? alias.replacement : id; - break; - } - } - } - // We still want to allow other plugins to participate in // resolution, with us being in front due to `enforce: "pre"`. // But we still want to ignore everything `vite:resolve` does @@ -197,13 +155,14 @@ export function deno(): Plugin { resolved = path.fromFileUrl(resolved); } - // For file:// resolved modules (npm packages in node_modules, - // local files), let Vite handle loading natively. This allows - // Vite to externalize CJS packages in SSR mode (Node.js handles - // them with native require()) and avoids needing a custom CJS - // transform. Only \0deno:: virtual modules (jsr:, non-default - // types) need Fresh's custom load hook. - return { id: resolved }; + return { + id: resolved, + meta: { + deno: { + type, + }, + }, + }; } catch { // ignore } @@ -213,87 +172,6 @@ export function deno(): Plugin { ? ssrLoader : browserLoader; - // In dev mode, CJS files need to be wrapped in an ESM shim: - // - SSR: module runner evaluates as ESM, needs module/exports/require - // - Client: browser evaluates as ESM, needs module/exports - // In build mode, Rollup's @rollup/plugin-commonjs handles CJS. - // - // CJS detection uses Node.js semantics (package.json "type" field) - // instead of content heuristics, which can be fooled by comments - // or strings containing "export"/"import". - if ( - isDev && - !id.startsWith("\0") && - id.includes("node_modules") && - /\.(c?js|cjs)$/.test(id) - ) { - // .cjs is always CJS. For .js files, check the nearest - // package.json "type" field first (Node.js semantics), then - // fall back to content-based detection for dual CJS/ESM - // packages that ship ESM in .js without "type": "module". - if (id.endsWith(".cjs") || !(await isEsmPackage(id))) { - try { - const code = await Deno.readTextFile(id); - - // Skip if the file contains actual ESM syntax. Some packages - // (e.g. @opentelemetry/api) ship both CJS and ESM as .js - // without "type": "module" in package.json. - if (!ESM_STMT_RE.test(code)) { - const isServer = this.environment.config.consumer === "server"; - - if (isServer) { - // SSR: use Node.js createRequire for full CJS compat - const wrapped = ` -import { createRequire as __cjs_createRequire } from "node:module"; -import { fileURLToPath as __cjs_fileURLToPath } from "node:url"; -import { dirname as __cjs_dirname } from "node:path"; -var __filename = __cjs_fileURLToPath(import.meta.url); -var __dirname = __cjs_dirname(__filename); -var require = __cjs_createRequire(import.meta.url); -var module = { exports: {} }; -var exports = module.exports; - -${code} - -export default module.exports; -`; - return { code: wrapped }; - } - - // Client: convert require() calls to ESM imports so - // browsers can load them. Hoist static require() calls - // to import statements at the top. - const imports: string[] = []; - let idx = 0; - const transformed = code.replace( - /\brequire\(["']([^"']+)["']\)/g, - (_match: string, spec: string) => { - const varName = `__cjs_import_${idx++}`; - imports.push( - `import ${varName} from ${JSON.stringify(spec)};`, - ); - return `(${varName}.default ?? ${varName})`; - }, - ); - - const wrapped = `${imports.join("\n")} -var module = { exports: {} }; -var exports = module.exports; -var __filename = ""; -var __dirname = ""; - -${transformed} - -export default module.exports; -`; - return { code: wrapped }; - } - } catch { - // Fall through to default loading - } - } - } - if (isDenoSpecifier(id)) { const { type, specifier } = parseDenoSpecifier(id); @@ -319,6 +197,49 @@ export default module.exports; code, }; } + + if (id.startsWith("\0")) { + id = id.slice(1); + } + + const meta = this.getModuleInfo(id)?.meta.deno as + | DenoState + | undefined + | null; + + if (meta === null || meta === undefined) return; + + // Skip for non-js files like `.css` + if ( + meta.type === RequestedModuleType.Default && + !JS_REG.test(id) + ) { + return; + } + + const url = path.toFileUrl(id); + + const result = await loader.load(url.href, meta.type); + if (result.kind === "external") { + return null; + } + + const code = new TextDecoder().decode(result.code); + + const maybeJsx = babelTransform({ + ssr: this.environment.config.consumer === "server", + media: result.mediaType, + id, + code, + isDev, + }); + if (maybeJsx) { + return maybeJsx; + } + + return { + code, + }; }, transform: { filter: { diff --git a/packages/plugin-vite/src/plugins/patches.ts b/packages/plugin-vite/src/plugins/patches.ts index b6970bdbc42..167ac7bc862 100644 --- a/packages/plugin-vite/src/plugins/patches.ts +++ b/packages/plugin-vite/src/plugins/patches.ts @@ -1,6 +1,8 @@ import type { Plugin } from "vite"; import * as babel from "@babel/core"; +import { cjsPlugin } from "./patches/commonjs.ts"; import { jsxComments } from "./patches/jsx_comment.ts"; +import { inlineEnvVarsPlugin } from "./patches/inline_env_vars.ts"; import { removePolyfills } from "./patches/remove_polyfills.ts"; import { JS_REG, JSX_REG } from "../utils.ts"; import { codeEvalPlugin } from "./patches/code_eval.ts"; @@ -39,8 +41,10 @@ export function patches(): Plugin { const plugins: babel.PluginItem[] = [ codeEvalPlugin(this.environment.config.consumer, env), + cjsPlugin, removePolyfills, jsxComments, + inlineEnvVarsPlugin(env, Deno.env.toObject()), ]; const res = babel.transformSync(code, { diff --git a/packages/plugin-vite/src/plugins/patches/commonjs.ts b/packages/plugin-vite/src/plugins/patches/commonjs.ts new file mode 100644 index 00000000000..83a58bb2d5b --- /dev/null +++ b/packages/plugin-vite/src/plugins/patches/commonjs.ts @@ -0,0 +1,959 @@ +import type { NodePath, PluginObj, types } from "@babel/core"; +import { builtinModules } from "node:module"; + +const BUILTINS = new Set(builtinModules); + +export function cjsPlugin( + { types: t }: { types: typeof types }, +): PluginObj { + const HAS_ES_MODULE = "esModule"; + const REQUIRE_CALLS = "requireCalls"; + const ROOT_SCOPE = "rootScope"; + const EXPORTED = "exported"; + const EXPORTED_NAMESPACES = "exported_namespaces"; + const ALIASED = "aliased"; + const REEXPORT = "re-export"; + const NEEDS_REQUIRE_IMPORT = "needsRequireImport"; + const NEEDS_DIRNAME_IMPORT = "needsDirnameImport"; + const IS_ESM = "isESM"; + + return { + name: "fresh-cjs-esm", + pre(file) { + const filename = file.opts.filename; + if (filename) { + if (filename.endsWith(".mjs") || filename.endsWith(".mts")) { + this.set(IS_ESM, true); + } else if (filename.endsWith(".cjs") || filename.endsWith(".cts")) { + this.set(IS_ESM, false); + } + } + }, + visitor: { + Program: { + enter(path, state) { + state.set(ROOT_SCOPE, path.scope); + state.set(EXPORTED, new Set()); + state.set(EXPORTED_NAMESPACES, new Set()); + state.set(REEXPORT, null); + + path.traverse({ + Import(_path, state) { + state.set(IS_ESM, true); + }, + ImportDeclaration(_path, state) { + state.set(IS_ESM, true); + }, + ExportAllDeclaration(_path, state) { + state.set(IS_ESM, true); + }, + ExportDefaultDeclaration(_path, state) { + state.set(IS_ESM, true); + }, + ExportNamedDeclaration(_path, state) { + state.set(IS_ESM, true); + }, + }, state); + }, + exit(path, state) { + const isESM = state.get(IS_ESM); + if (isESM) return; + + const body = path.get("body"); + const requires = state.get(REQUIRE_CALLS); + if (requires !== undefined) { + for (let i = 0; i < requires.length; i++) { + const { specifier, id } = requires[i]; + path.unshiftContainer( + "body", + t.importDeclaration( + [t.importNamespaceSpecifier(id)], + specifier, + ), + ); + } + } + + const reexport = state.get(REEXPORT); + const exported = state.get(EXPORTED); + const exportedNs = state.get(EXPORTED_NAMESPACES); + const needsRequireImport = state.get(NEEDS_REQUIRE_IMPORT); + const hasEsModule = state.get(HAS_ES_MODULE); + + if (needsRequireImport) { + // Inject: + // ```ts + // import { createRequire } from "node:module"; + // const require = createRequire(import.meta.url); + // ``` + const id = t.identifier("createRequire"); + path.unshiftContainer( + "body", + t.variableDeclaration("const", [ + t.variableDeclarator( + t.identifier("require"), + t.callExpression(t.identifier("createRequire"), [ + t.memberExpression( + t.metaProperty( + t.identifier("import"), + t.identifier("meta"), + ), + t.identifier("url"), + ), + ]), + ), + ]), + ); + path.unshiftContainer( + "body", + t.importDeclaration( + [t.importSpecifier(id, id)], + t.stringLiteral("node:module"), + ), + ); + } + + const needsDirnameImport = state.get(NEEDS_DIRNAME_IMPORT); + if (needsDirnameImport) { + // Inject: + // ```ts + // import { fileURLToPath as __cjs_fileURLToPath } from "node:url"; + // import { dirname as __cjs_dirname } from "node:path"; + // const __filename = __cjs_fileURLToPath(import.meta.url); + // const __dirname = __cjs_dirname(__filename); + // ``` + const fileURLToPathId = t.identifier("__cjs_fileURLToPath"); + const dirnameId = t.identifier("__cjs_dirname"); + const importMetaUrl = t.memberExpression( + t.metaProperty( + t.identifier("import"), + t.identifier("meta"), + ), + t.identifier("url"), + ); + + path.unshiftContainer( + "body", + t.variableDeclaration("var", [ + t.variableDeclarator( + t.identifier("__dirname"), + t.callExpression(dirnameId, [t.identifier("__filename")]), + ), + ]), + ); + path.unshiftContainer( + "body", + t.variableDeclaration("var", [ + t.variableDeclarator( + t.identifier("__filename"), + t.callExpression(fileURLToPathId, [importMetaUrl]), + ), + ]), + ); + path.unshiftContainer( + "body", + t.importDeclaration( + [t.importSpecifier(dirnameId, t.identifier("dirname"))], + t.stringLiteral("node:path"), + ), + ); + path.unshiftContainer( + "body", + t.importDeclaration( + [ + t.importSpecifier( + fileURLToPathId, + t.identifier("fileURLToPath"), + ), + ], + t.stringLiteral("node:url"), + ), + ); + } + + if (reexport !== null) { + path.unshiftContainer( + "body", + t.exportAllDeclaration(t.cloneNode(reexport, true)), + ); + } + + const mappedNs: string[] = []; + + for (const spec of exportedNs.values()) { + const id = path.scope.generateUidIdentifier("__ns"); + mappedNs.push(id.name); + + path.unshiftContainer( + "body", + t.importDeclaration( + [t.importNamespaceSpecifier(id)], + t.stringLiteral(spec), + ), + ); + } + + if (exported.size > 0 || exportedNs.size > 0 || hasEsModule) { + path.unshiftContainer( + "body", + t.expressionStatement( + t.callExpression( + t.memberExpression( + t.identifier("Object"), + t.identifier("defineProperty"), + ), + [ + t.identifier("exports"), + t.stringLiteral("__esModule"), + t.objectExpression([ + t.objectProperty( + t.identifier("value"), + t.booleanLiteral(true), + ), + ]), + ], + ), + ), + ); + path.unshiftContainer( + "body", + t.expressionStatement( + t.callExpression( + t.memberExpression( + t.identifier("Object"), + t.identifier("defineProperty"), + ), + [ + t.identifier("module"), + t.stringLiteral("exports"), + t.objectExpression([ + t.objectMethod( + "method", + t.identifier("get"), + [], + t.blockStatement([ + t.returnStatement(t.identifier("exports")), + ]), + ), + t.objectMethod( + "method", + t.identifier("set"), + [t.identifier("value")], + t.blockStatement([ + t.expressionStatement( + t.assignmentExpression( + "=", + t.identifier("exports"), + t.identifier("value"), + ), + ), + ]), + ), + ]), + ], + ), + ), + ); + path.unshiftContainer( + "body", + t.variableDeclaration("var", [ + t.variableDeclarator( + t.identifier("exports"), + t.objectExpression([]), + ), + t.variableDeclarator( + t.identifier("module"), + t.objectExpression([]), + ), + ]), + ); + } + + const idExports: types.ExportSpecifier[] = []; + for (const name of exported) { + if (name === "default") { + continue; + } + + const id = path.scope.generateUidIdentifier(name); + + path.pushContainer( + "body", + t.variableDeclaration( + "var", + [t.variableDeclarator( + id, + t.memberExpression( + t.identifier("exports"), + t.identifier(name), + ), + )], + ), + ); + idExports.push( + t.exportSpecifier(id, t.identifier(name)), + ); + } + + if (idExports.length > 0) { + path.pushContainer( + "body", + t.exportNamedDeclaration(null, idExports), + ); + } + + if (exported.size > 0 || exportedNs.size > 0 || hasEsModule) { + const id = path.scope.generateUidIdentifier("__default"); + + // Use `var` instead of `const` to avoid TDZ errors when + // Rollup reorders declarations in the bundled output. + path.pushContainer( + "body", + t.variableDeclaration("var", [ + t.variableDeclarator( + id, + ), + ]), + ); + + path.pushContainer( + "body", + t.ifStatement( + t.logicalExpression( + "&&", + t.logicalExpression( + "&&", + t.binaryExpression( + "===", + t.unaryExpression("typeof", t.identifier("exports")), + t.stringLiteral("object"), + ), + t.binaryExpression( + "!==", + t.identifier("exports"), + t.nullLiteral(), + ), + ), + t.binaryExpression( + "in", + t.stringLiteral("default"), + t.identifier("exports"), + ), + ), + t.blockStatement([ + t.expressionStatement( + t.assignmentExpression( + "=", + id, + t.memberExpression( + t.identifier("exports"), + t.identifier("default"), + ), + ), + ), + ]), + t.blockStatement([ + t.expressionStatement( + t.assignmentExpression("=", id, t.identifier("exports")), + ), + ]), + ), + ); + + for (let i = 0; i < mappedNs.length; i++) { + const mapped = mappedNs[i]; + + const key = path.scope.generateUid("k"); + // Only spread namespace properties when the module has no + // explicit default export (i.e. "default" not in exports). + path.pushContainer( + "body", + t.ifStatement( + t.logicalExpression( + "&&", + t.logicalExpression( + "&&", + t.binaryExpression( + "===", + t.unaryExpression("typeof", t.identifier("exports")), + t.stringLiteral("object"), + ), + t.binaryExpression( + "!==", + t.identifier("exports"), + t.nullLiteral(), + ), + ), + t.unaryExpression( + "!", + t.binaryExpression( + "in", + t.stringLiteral("default"), + t.identifier("exports"), + ), + ), + ), + t.forInStatement( + t.variableDeclaration("var", [ + t.variableDeclarator(t.identifier(key)), + ]), + t.identifier(mapped), + t.ifStatement( + t.logicalExpression( + "&&", + t.logicalExpression( + "&&", + t.binaryExpression( + "!==", + t.identifier(key), + t.stringLiteral("default"), + ), + t.binaryExpression( + "!==", + t.identifier(key), + t.stringLiteral("__esModule"), + ), + ), + t.callExpression( + t.memberExpression( + t.memberExpression( + t.memberExpression( + t.identifier("Object"), + t.identifier("prototype"), + ), + t.identifier("hasOwnProperty"), + ), + t.identifier("call"), + ), + [t.identifier(mapped), t.identifier(key)], + ), + ), + t.expressionStatement( + t.assignmentExpression( + "=", + t.memberExpression( + t.cloneNode(id, true), + t.identifier(key), + true, + ), + t.memberExpression( + t.identifier(mapped), + t.identifier(key), + true, + ), + ), + ), + ), + ), + ), + ); + } + + path.pushContainer("body", t.exportDefaultDeclaration(id)); + path.pushContainer( + "body", + t.exportNamedDeclaration( + t.variableDeclaration("var", [ + t.variableDeclarator( + t.identifier("__require"), + t.identifier("exports"), + ), + ]), + ), + ); + } + + if (body.length === 0 && hasEsModule) { + path.pushContainer("body", t.exportNamedDeclaration(null)); + } else if (hasEsModule) { + path.pushContainer( + "body", + t.exportNamedDeclaration( + t.variableDeclaration( + "var", + [t.variableDeclarator( + t.identifier("__esModule"), + t.memberExpression( + t.identifier("exports"), + t.identifier("__esModule"), + ), + )], + ), + ), + ); + } + }, + }, + CallExpression(path, state) { + if (state.get(IS_ESM)) return; + const exported = state.get(EXPORTED); + + if (isObjEsModuleFlag(t, path.node)) { + state.set(HAS_ES_MODULE, true); + return; + } + + // Handle require.resolve() by injecting createRequire + if ( + t.isMemberExpression(path.node.callee) && + t.isIdentifier(path.node.callee.object) && + path.node.callee.object.name === "require" && + t.isIdentifier(path.node.callee.property) && + path.node.callee.property.name === "resolve" + ) { + state.set(NEEDS_REQUIRE_IMPORT, true); + return; + } + + if ( + t.isIdentifier(path.node.callee) && + path.node.callee.name === "require" + ) { + const root = state.get(ROOT_SCOPE); + const id = root.generateUidIdentifier("mod"); + + const mods = state.get(REQUIRE_CALLS) ?? []; + state.set(REQUIRE_CALLS, mods); + + const source = path.node.arguments[0]; + if (t.isStringLiteral(source)) { + // Check if we can hoist it or if we need to keep it. + let canImport = true; + let parent: NodePath | null = path.parentPath; + while (parent !== null) { + if ( + t.isTryStatement(parent.node) || t.isIfStatement(parent.node) || + t.isConditionalExpression(parent.node) + ) { + canImport = false; + break; + } + parent = parent.parentPath; + } + + if (!canImport) { + state.set(NEEDS_REQUIRE_IMPORT, true); + return; + } + + mods.push({ + id, + specifier: t.cloneNode(path.node.arguments[0], true), + }); + + if ( + path.parentPath?.isVariableDeclarator() && + path.parentPath?.get("id").isIdentifier() || + path.parentPath?.isCallExpression() + ) { + // Vite json processing always adds a default property. + if (source.value.endsWith(".json")) { + path.replaceWith( + t.logicalExpression( + "??", + t.memberExpression( + t.cloneNode(id, true), + t.identifier("default"), + ), + t.cloneNode(id, true), + ), + ); + } else if ( + path.parentPath?.isCallExpression() && + t.isIdentifier(path.parentPath.node.callee) && + path.parentPath.node.callee.name === "__importDefault" + ) { + if (isNodeBuiltin(source.value)) { + path.replaceWith(t.objectExpression([ + t.objectProperty( + t.identifier("__esModule"), + t.booleanLiteral(true), + ), + t.objectProperty( + t.identifier("default"), + t.logicalExpression( + "??", + t.memberExpression( + t.cloneNode(id, true), + t.identifier("default"), + ), + t.cloneNode(id, true), + ), + ), + ])); + } else { + path.replaceWith(t.cloneNode(id, true)); + } + } else { + path.replaceWith( + t.logicalExpression( + "??", + t.memberExpression( + t.cloneNode(id, true), + t.identifier("__require"), + ), + t.logicalExpression( + "??", + t.memberExpression( + t.cloneNode(id, true), + t.identifier("default"), + ), + t.cloneNode(id, true), + ), + ), + ); + } + return; + } + + path.replaceWith(t.cloneNode(id, true)); + } else { + state.set(NEEDS_REQUIRE_IMPORT, true); + } + } else if ( + t.isMemberExpression(path.node.callee) && + t.isIdentifier(path.node.callee.object) && + path.node.callee.object.name === "Object" && + t.isIdentifier(path.node.callee.property) && + path.node.callee.property.name === "defineProperty" && + path.node.arguments.length > 0 && + t.isIdentifier(path.node.arguments[0]) && + path.node.arguments[0].name === "exports" && + t.isStringLiteral(path.node.arguments[1]) + ) { + const name = path.node.arguments[1].value; + exported.add(name); + } + }, + EmptyStatement(path) { + path.remove(); + }, + MemberExpression: { + exit(path, state) { + if (state.get(IS_ESM)) return; + if ( + t.isIdentifier(path.node.property) && + path.node.property.name !== "__esModule" + ) { + // Track both `exports.X` and `module.exports.X` + if ( + t.isIdentifier(path.node.object) && + path.node.object.name === "exports" + ) { + state.get(EXPORTED).add(path.node.property.name); + } else if ( + t.isMemberExpression(path.node.object) && + isModuleExports(t, path.node.object) + ) { + state.get(EXPORTED).add(path.node.property.name); + } + } + }, + }, + ExpressionStatement: { + enter(path, state) { + if (state.get(IS_ESM)) return; + // Check: Object.defineProperty(module.exports) "__esModule" ...) + // Check: Object.defineProperty(exports) "__esModule" ...) + // Check: a({}, "__esModule", ...) + if ( + t.isCallExpression(path.node.expression) && + path.node.expression.arguments.length === 3 && + t.isStringLiteral(path.node.expression.arguments[1]) && + path.node.expression.arguments[1].value === "__esModule" + ) { + state.set(HAS_ES_MODULE, true); + return; + } + + if ( + t.isExpressionStatement(path.node) && + t.isCallExpression(path.node.expression) && + t.isIdentifier(path.node.expression.callee) && + path.node.expression.callee.name === "__exportStar" && + path.node.expression.arguments.length > 0 && + t.isCallExpression(path.node.expression.arguments[0]) && + t.isIdentifier(path.node.expression.arguments[0].callee) && + path.node.expression.arguments[0].callee.name === "require" && + t.isStringLiteral(path.node.expression.arguments[0].arguments[0]) + ) { + const spec = t.cloneNode( + path.node.expression.arguments[0].arguments[0], + true, + ); + state.get(EXPORTED_NAMESPACES).add(spec.value); + path.replaceWith(t.exportAllDeclaration(spec)); + } else if ( + t.isExpressionStatement(path.node) && + t.isCallExpression(path.node.expression) && + t.isFunctionExpression(path.node.expression.callee) + ) { + if ( + path.node.expression.callee.params.length > 0 && + t.isIdentifier(path.node.expression.callee.params[0]) + ) { + const alias = path.node.expression.callee.params[0].name; + state.set(ALIASED, alias); + } + } else if ( + // Check: Object.defineProperty(exports, "foo", { enumerable: true, get: function () { return foo; } }); + t.isCallExpression(path.node.expression) && + t.isMemberExpression(path.node.expression.callee) && + t.isIdentifier(path.node.expression.callee.object) && + path.node.expression.callee.object.name === "Object" && + t.isIdentifier(path.node.expression.callee.property) && + path.node.expression.callee.property.name === "defineProperty" && + path.node.expression.arguments.length >= 2 && + t.isIdentifier(path.node.expression.arguments[0]) && + path.node.expression.arguments[0].name === "exports" && + t.isStringLiteral(path.node.expression.arguments[1]) && + t.isObjectExpression(path.node.expression.arguments[2]) + ) { + const exported = path.node.expression.arguments[1].value; + const obj = path.node.expression.arguments[2]; + for (let i = 0; i < obj.properties.length; i++) { + const prop = obj.properties[i]; + + if ( + t.isObjectProperty(prop) && t.isIdentifier(prop.key) && + prop.key.name === "get" && t.isFunctionExpression(prop.value) && + t.isBlockStatement(prop.value.body) && + prop.value.body.body.length === 1 && + t.isReturnStatement(prop.value.body.body[0]) + ) { + const expr = prop.value.body.body[0].argument; + if (expr !== null && expr !== undefined) { + path.replaceWith( + t.assignmentExpression( + "=", + t.memberExpression( + t.identifier("exports"), + t.identifier(exported), + ), + t.cloneNode(expr, true), + ), + ); + } + } else if ( + t.isObjectMethod(prop) && t.isIdentifier(prop.key) && + prop.key.name === "get" && t.isBlockStatement(prop.body) && + prop.body.body.length === 1 && + t.isReturnStatement(prop.body.body[0]) + ) { + const expr = prop.body.body[0].argument; + if (expr !== null && expr !== undefined) { + path.replaceWith( + t.assignmentExpression( + "=", + t.memberExpression( + t.identifier("exports"), + t.identifier(exported), + ), + t.cloneNode(expr, true), + ), + ); + } + } + } + } else if ( + // Check: module.exports = require(...) + t.isAssignmentExpression(path.node.expression) && + t.isMemberExpression(path.node.expression.left) && + t.isIdentifier(path.node.expression.left.object) && + t.isIdentifier(path.node.expression.left.property) && + path.node.expression.left.object.name === "module" && + path.node.expression.left.property.name === "exports" && + t.isCallExpression(path.node.expression.right) && + t.isIdentifier(path.node.expression.right.callee) && + path.node.expression.right.callee.name === "require" && + path.node.expression.right.arguments.length === 1 && + t.isStringLiteral(path.node.expression.right.arguments[0]) + ) { + const source = path.node.expression.right.arguments[0]; + state.set(REEXPORT, source); + } else { + let depth = 0; + let current = path.node.expression; + + while ( + t.isAssignmentExpression(current) && + t.isMemberExpression(current.left) && + t.isIdentifier(current.left.object) && + current.left.object.name === "exports" + ) { + if ( + t.isUnaryExpression(current.right) && + current.right.operator === "void" && + t.isNumericLiteral(current.right.argument) && + current.right.argument.value === 0 + ) { + if (depth > 0) { + path.remove(); + } + + break; + } + + depth++; + current = current.right; + } + } + }, + exit(path, state) { + if (state.get(IS_ESM)) return; + const exported = state.get(EXPORTED); + const expr = path.get("expression"); + + if (expr.isAssignmentExpression()) { + const left = expr.get("left"); + + if (isEsModuleFlag(t, expr.node)) { + state.set(HAS_ES_MODULE, true); + } else if (left.isMemberExpression()) { + if (isModuleExports(t, left.node)) { + // Should always try to create synthetic default export in this case. + exported.add("default"); + + if (t.isObjectExpression(expr.node.right)) { + const properties = expr.node.right.properties; + for (let i = 0; i < properties.length; i++) { + const prop = properties[i]; + if (t.isObjectProperty(prop)) { + if (t.isIdentifier(prop.key)) { + if (prop.key.name === "__esModule") { + continue; + } + + exported.add(prop.key.name); + } + } + } + } + } else { + const named = getExportsAssignName(t, left.node); + if (named === null) return; + exported.add(named); + } + } + } else if (expr.isCallExpression()) { + if (isObjEsModuleFlag(t, expr.node)) { + state.set(HAS_ES_MODULE, true); + } + } + }, + }, + VariableDeclaration(path) { + if (path.node.declarations.length === 0) { + path.remove(); + } + }, + ConditionalExpression(path, state) { + if (state.get(IS_ESM)) return; + + if ( + t.isBinaryExpression(path.node.test) && + t.isUnaryExpression(path.node.test.left) && + path.node.test.left.operator === "typeof" && + t.isIdentifier(path.node.test.left.argument) && + path.node.test.left.argument.name === "exports" && + path.node.test.operator === "===" + ) { + path.replaceWith(t.cloneNode(path.node.alternate, true)); + } + }, + Identifier(path, state) { + if (state.get(IS_ESM)) return; + + const name = path.node.name; + if (name !== "__dirname" && name !== "__filename") return; + + // Skip if this is already a declaration (e.g. our own polyfill) + if ( + path.parentPath?.isVariableDeclarator() && + path.parentPath.get("id") === path + ) return; + + state.set(NEEDS_DIRNAME_IMPORT, true); + }, + AssignmentExpression(path, state) { + if (state.get(IS_ESM)) return; + + const exported = state.get(EXPORTED); + const aliased = state.get(ALIASED); + if (aliased === undefined) return; + + if ( + path.node.operator === "=" && t.isMemberExpression(path.node.left) && + t.isIdentifier(path.node.left.object) && + path.node.left.object.name === aliased && + t.isIdentifier(path.node.left.property) + ) { + const name = path.node.left.property.name; + exported.add(name); + } + }, + }, + }; +} + +function isModuleExports( + t: typeof types, + node: types.MemberExpression, +): boolean { + return t.isIdentifier(node.object) && node.object.name === "module" && + t.isIdentifier(node.property) && node.property.name === "exports"; +} + +function getExportsAssignName( + t: typeof types, + node: types.MemberExpression, +): string | null { + if ( + (t.isMemberExpression(node.object) && + isModuleExports(t, node.object) || + t.isIdentifier(node.object) && node.object.name === "exports") && + t.isIdentifier(node.property) + ) { + return node.property.name; + } + + return null; +} + +/** + * Detect `exports.__esModule = true;` + */ +function isEsModuleFlag( + t: typeof types, + node: types.AssignmentExpression, +): boolean { + if (!t.isMemberExpression(node.left)) return false; + + const { left, right } = node; + return (t.isMemberExpression(left.object) && + isModuleExports(t, left.object) || + t.isIdentifier(left.object) && left.object.name === "exports") && + t.isIdentifier(left.property) && left.property.name === "__esModule" && + t.isBooleanLiteral(right); +} + +/** + * Check for `Object.defineProperty(exports, '__esModule', { value: true })` + */ +function isObjEsModuleFlag( + t: typeof types, + node: types.CallExpression, +): boolean { + return node.arguments.length === 3 && + t.isStringLiteral(node.arguments[1]) && + node.arguments[1].value === "__esModule" && + t.isObjectExpression(node.arguments[2]); +} + +function isNodeBuiltin(specifier: string): boolean { + return BUILTINS.has(specifier) || ( + specifier.startsWith("node:") + ? BUILTINS.has(specifier.slice("node:".length)) + : BUILTINS.has(`node:${specifier}`) + ); +} diff --git a/packages/plugin-vite/src/plugins/patches/commonjs_test.ts b/packages/plugin-vite/src/plugins/patches/commonjs_test.ts new file mode 100644 index 00000000000..4c337babca2 --- /dev/null +++ b/packages/plugin-vite/src/plugins/patches/commonjs_test.ts @@ -0,0 +1,835 @@ +import { expect } from "@std/expect/expect"; +import * as babel from "@babel/core"; +import { cjsPlugin } from "../patches/commonjs.ts"; + +function runTest( + options: { input: string; expected: string; filename?: string }, +) { + const res = babel.transformSync(options.input, { + filename: options.filename ?? "foo.js", + babelrc: false, + plugins: [cjsPlugin], + }); + + const output = res?.code ?? ""; + expect(output).toEqual(options.expected); +} + +const INIT = `var exports = {}, + module = {}; +Object.defineProperty(module, "exports", { + get() { + return exports; + }, + set(value) { + exports = value; + } +}); +Object.defineProperty(exports, "__esModule", { + value: true +});`; + +const DEFAULT_EXPORT = `var _default; +if (typeof exports === "object" && exports !== null && "default" in exports) { + _default = exports.default; +} else { + _default = exports; +}`; + +const DEFAULT_EXPORT_END = `export default _default; +export var __require = exports;`; +const IMPORT_REQUIRE = `import { createRequire } from "node:module"; +const require = createRequire(import.meta.url);`; +const EXPORT_ES_MODULE = `export var __esModule = exports.__esModule;`; + +Deno.test("commonjs - module.exports default", () => { + runTest({ + input: `module.exports = async function () {};`, + expected: `${INIT} +module.exports = async function () {}; +${DEFAULT_EXPORT} +${DEFAULT_EXPORT_END}`, + }); +}); + +Deno.test("commonjs - module.exports default primitive", () => { + runTest({ + input: `module.exports = 42;`, + expected: `${INIT} +module.exports = 42; +${DEFAULT_EXPORT} +${DEFAULT_EXPORT_END}`, + }); +}); + +Deno.test("commonjs - exports with default + named", () => { + runTest({ + input: `exports.__esModule = true; +exports.default = 'x'; +exports.foo = 'foo';`, + expected: `${INIT} +exports.__esModule = true; +exports.default = 'x'; +exports.foo = 'foo'; +var _foo = exports.foo; +export { _foo as foo }; +${DEFAULT_EXPORT} +${DEFAULT_EXPORT_END} +${EXPORT_ES_MODULE}`, + }); +}); + +Deno.test("commonjs - module.exports with default + named", () => { + runTest({ + input: `module.exports.__esModule = true; +module.exports.default = 'x'; +module.exports.foo = 'foo';`, + expected: `${INIT} +module.exports.__esModule = true; +module.exports.default = 'x'; +module.exports.foo = 'foo'; +var _foo = exports.foo; +export { _foo as foo }; +${DEFAULT_EXPORT} +${DEFAULT_EXPORT_END} +${EXPORT_ES_MODULE}`, + }); +}); + +Deno.test("commonjs - Object es module flag with named clash", () => { + runTest({ + input: `Object.defineProperty(exports, '__esModule', { value: true }); +exports.foo = 'bar'; +const foo = 'also bar'; +`, + expected: `${INIT} +Object.defineProperty(exports, '__esModule', { + value: true +}); +exports.foo = 'bar'; +const foo = 'also bar'; +var _foo = exports.foo; +export { _foo as foo }; +${DEFAULT_EXPORT} +${DEFAULT_EXPORT_END} +${EXPORT_ES_MODULE}`, + }); +}); + +Deno.test("commonjs - Object es module flag with named + default", () => { + runTest({ + input: `Object.defineProperty(exports, '__esModule', { value: true }); +exports.default = 'foo'; +exports.foo = 'bar'; +`, + expected: `${INIT} +Object.defineProperty(exports, '__esModule', { + value: true +}); +exports.default = 'foo'; +exports.foo = 'bar'; +var _foo = exports.foo; +export { _foo as foo }; +${DEFAULT_EXPORT} +${DEFAULT_EXPORT_END} +${EXPORT_ES_MODULE}`, + }); +}); + +Deno.test("commonjs - esModule flag only", () => { + runTest({ + input: `Object.defineProperty(exports, "__esModule", { value: true });`, + expected: `${INIT} +Object.defineProperty(exports, "__esModule", { + value: true +}); +${DEFAULT_EXPORT} +${DEFAULT_EXPORT_END} +${EXPORT_ES_MODULE}`, + }); +}); + +Deno.test("commonjs - esModule flag only #2", () => { + runTest({ + input: + `Object.defineProperty(module.exports, "__esModule", { value: true });`, + expected: `${INIT} +Object.defineProperty(module.exports, "__esModule", { + value: true +}); +${DEFAULT_EXPORT} +${DEFAULT_EXPORT_END} +${EXPORT_ES_MODULE}`, + }); +}); + +Deno.test("commonjs - esModule flag only minified #3", () => { + runTest({ + input: `Object.defineProperty(exports, '__esModule', { value: !0 });`, + expected: `${INIT} +Object.defineProperty(exports, '__esModule', { + value: !0 +}); +${DEFAULT_EXPORT} +${DEFAULT_EXPORT_END} +${EXPORT_ES_MODULE}`, + }); +}); + +Deno.test("commonjs - exports only named", () => { + runTest({ + input: `Object.defineProperty(exports, '__esModule', { value: true }); +exports.foo = 'bar'; +exports.bar = 'foo'; +`, + expected: `${INIT} +Object.defineProperty(exports, '__esModule', { + value: true +}); +exports.foo = 'bar'; +exports.bar = 'foo'; +var _foo = exports.foo; +var _bar = exports.bar; +export { _foo as foo, _bar as bar }; +${DEFAULT_EXPORT} +${DEFAULT_EXPORT_END} +${EXPORT_ES_MODULE}`, + }); +}); + +Deno.test("commonjs - require", () => { + runTest({ + input: `var foo = require("tape"); +console.log(foo); +`, + expected: `import * as _mod from "tape"; +var foo = _mod.__require ?? _mod.default ?? _mod; +console.log(foo);`, + }); +}); + +Deno.test("commonjs - require destructure", () => { + runTest({ + input: `var { foo } = require("tape"); +console.log(foo); +`, + expected: `import * as _mod from "tape"; +var { + foo +} = _mod; +console.log(foo);`, + }); +}); + +Deno.test("commonjs - require assign", () => { + runTest({ + input: `foo = require("tape"); +console.log(foo); +`, + expected: `import * as _mod from "tape"; +foo = _mod; +console.log(foo);`, + }); +}); + +Deno.test("commonjs - require assign pattern", () => { + runTest({ + input: `foo = require("tape"); +console.log(foo); +`, + expected: `import * as _mod from "tape"; +foo = _mod; +console.log(foo);`, + }); +}); + +Deno.test("commonjs - require function call", () => { + runTest({ + input: `var a = require('./a')()`, + expected: `import * as _mod from './a'; +var a = (_mod.__require ?? _mod.default ?? _mod)();`, + }); +}); + +Deno.test("commonjs - require var decls", () => { + runTest({ + input: `var a = require('./a'), b = 42;`, + expected: `import * as _mod from './a'; +var a = _mod.__require ?? _mod.default ?? _mod, + b = 42;`, + }); +}); + +Deno.test("commonjs - duplicate exports", () => { + runTest({ + input: `Object.defineProperty(exports, "__esModule", { value: true }); +exports.trace = void 0; +exports.trace = 'foo'`, + expected: `${INIT} +Object.defineProperty(exports, "__esModule", { + value: true +}); +exports.trace = void 0; +exports.trace = 'foo'; +var _trace = exports.trace; +export { _trace as trace }; +${DEFAULT_EXPORT} +${DEFAULT_EXPORT_END} +${EXPORT_ES_MODULE}`, + }); +}); + +Deno.test("commonjs - cleared exports", () => { + runTest({ + input: `Object.defineProperty(exports, "__esModule", { value: true }); +exports.foo = exports.bar = void 0; +exports.foo = 'foo'`, + expected: `${INIT} +Object.defineProperty(exports, "__esModule", { + value: true +}); +exports.foo = 'foo'; +var _foo = exports.foo; +export { _foo as foo }; +${DEFAULT_EXPORT} +${DEFAULT_EXPORT_END} +${EXPORT_ES_MODULE}`, + }); +}); + +Deno.test("commonjs - define exports", () => { + runTest({ + input: `var utils_1 = require("./bar"); +Object.defineProperty(exports, "foo", { enumerable: true, get: function () { return utils_1.foo; } });`, + expected: `${INIT} +import * as _mod from "./bar"; +var utils_1 = _mod.__require ?? _mod.default ?? _mod; +exports.foo = utils_1.foo; +var _foo = exports.foo; +export { _foo as foo }; +${DEFAULT_EXPORT} +${DEFAULT_EXPORT_END}`, + }); +}); + +Deno.test("commonjs - define exports #2", () => { + runTest({ + input: `var utils_1 = require("./bar"); +Object.defineProperty(exports, "foo", { enumerable: true, get() { return utils_1.foo; } });`, + expected: `${INIT} +import * as _mod from "./bar"; +var utils_1 = _mod.__require ?? _mod.default ?? _mod; +exports.foo = utils_1.foo; +var _foo = exports.foo; +export { _foo as foo }; +${DEFAULT_EXPORT} +${DEFAULT_EXPORT_END}`, + }); +}); + +Deno.test("commonjs - define exports #3", () => { + runTest({ + input: `Object.defineProperty(exports, "__esModule", { value: true }); +exports._globalThis = void 0; +exports._globalThis = typeof globalThis === 'object' ? globalThis : global;`, + expected: `${INIT} +Object.defineProperty(exports, "__esModule", { + value: true +}); +exports._globalThis = void 0; +exports._globalThis = typeof globalThis === 'object' ? globalThis : global; +var _globalThis = exports._globalThis; +export { _globalThis }; +${DEFAULT_EXPORT} +${DEFAULT_EXPORT_END} +${EXPORT_ES_MODULE}`, + }); +}); + +Deno.test("commonjs - named function", () => { + runTest({ + input: `Object.defineProperty(exports, "__esModule", { value: true }); +function foo() {}; +exports.foo = foo;`, + expected: `${INIT} +Object.defineProperty(exports, "__esModule", { + value: true +}); +function foo() {} +exports.foo = foo; +var _foo = exports.foo; +export { _foo as foo }; +${DEFAULT_EXPORT} +${DEFAULT_EXPORT_END} +${EXPORT_ES_MODULE}`, + }); +}); + +Deno.test("commonjs - detect esbuild shims", () => { + runTest({ + input: `__exportStar(require("./globalThis"), exports);`, + expected: `${INIT} +import * as _ns from "./globalThis"; +export * from "./globalThis"; +${DEFAULT_EXPORT} +if (typeof exports === "object" && exports !== null && !("default" in exports)) for (var _k in _ns) if (_k !== "default" && _k !== "__esModule" && Object.prototype.hasOwnProperty.call(_ns, _k)) _default[_k] = _ns[_k]; +${DEFAULT_EXPORT_END}`, + }); +}); + +Deno.test("commonjs - exports.default", () => { + runTest({ + input: `exports.default = {}`, + expected: `${INIT} +exports.default = {}; +${DEFAULT_EXPORT} +${DEFAULT_EXPORT_END}`, + }); +}); + +Deno.test("commonjs - multiple same name", () => { + runTest({ + input: `exports.VERSION = void 0; +exports.VERSION = '1.9.0';`, + expected: `${INIT} +exports.VERSION = void 0; +exports.VERSION = '1.9.0'; +var _VERSION = exports.VERSION; +export { _VERSION as VERSION }; +${DEFAULT_EXPORT} +${DEFAULT_EXPORT_END}`, + }); +}); + +Deno.test("commonjs - export enum", () => { + runTest({ + input: `Object.defineProperty(exports, "__esModule", { value: true }); +exports.DiagLogLevel = void 0; +var DiagLogLevel; +(function (DiagLogLevel) { + DiagLogLevel[DiagLogLevel["ALL"] = 9999] = "ALL"; +})(DiagLogLevel = exports.DiagLogLevel || (exports.DiagLogLevel = {}));`, + expected: `${INIT} +Object.defineProperty(exports, "__esModule", { + value: true +}); +exports.DiagLogLevel = void 0; +var DiagLogLevel; +(function (DiagLogLevel) { + DiagLogLevel[DiagLogLevel["ALL"] = 9999] = "ALL"; +})(DiagLogLevel = exports.DiagLogLevel || (exports.DiagLogLevel = {})); +var _DiagLogLevel = exports.DiagLogLevel; +export { _DiagLogLevel as DiagLogLevel }; +${DEFAULT_EXPORT} +${DEFAULT_EXPORT_END} +${EXPORT_ES_MODULE}`, + }); +}); + +Deno.test("commonjs - require", () => { + runTest({ + input: `module.exports = { __esModule: true, default: { foo: 'bar' }}`, + expected: `${INIT} +module.exports = { + __esModule: true, + default: { + foo: 'bar' + } +}; +${DEFAULT_EXPORT} +${DEFAULT_EXPORT_END}`, + }); +}); + +Deno.test("commonjs - export default object", () => { + runTest({ + input: `Object.defineProperty(exports, '__esModule', { value: true }); +module.exports = { foo: 'bar' }; +`, + expected: `${INIT} +Object.defineProperty(exports, '__esModule', { + value: true +}); +module.exports = { + foo: 'bar' +}; +var _foo = exports.foo; +export { _foo as foo }; +${DEFAULT_EXPORT} +${DEFAULT_EXPORT_END} +${EXPORT_ES_MODULE}`, + }); +}); + +Deno.test("commonjs - detect iife wrapper", () => { + runTest({ + input: `;(function (sax) { + sax.foo = "foo"; +})(typeof exports === 'undefined' ? this.sax = {} : exports);`, + expected: `${INIT} +(function (sax) { + sax.foo = "foo"; +})(exports); +var _foo = exports.foo; +export { _foo as foo }; +${DEFAULT_EXPORT} +${DEFAULT_EXPORT_END}`, + }); +}); + +Deno.test("commonjs - re-export", () => { + runTest({ + input: + `;var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } }); +}) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +})); +var __exportStar = (this && this.__exportStar) || function(m, exports) { + for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p); +}; +Object.defineProperty(exports, "__esModule", { value: true }); +__exportStar(require("./node"), exports);`, + expected: `${INIT} +import * as _ns from "./node"; +var __createBinding = this && this.__createBinding || (Object.create ? function (o, m, k, k2) { + if (k2 === undefined) k2 = k; + Object.defineProperty(o, k2, { + enumerable: true, + get: function () { + return m[k]; + } + }); +} : function (o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +}); +var __exportStar = this && this.__exportStar || function (m, exports) { + for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p); +}; +Object.defineProperty(exports, "__esModule", { + value: true +}); +export * from "./node"; +${DEFAULT_EXPORT} +if (typeof exports === "object" && exports !== null && !("default" in exports)) for (var _k in _ns) if (_k !== "default" && _k !== "__esModule" && Object.prototype.hasOwnProperty.call(_ns, _k)) _default[_k] = _ns[_k]; +${DEFAULT_EXPORT_END} +${EXPORT_ES_MODULE}`, + }); +}); + +Deno.test("commonjs - assign module.exports", () => { + runTest({ + input: `module.exports = { foo: 1 };`, + expected: `${INIT} +module.exports = { + foo: 1 +}; +var _foo = exports.foo; +export { _foo as foo }; +${DEFAULT_EXPORT} +${DEFAULT_EXPORT_END}`, + }); +}); + +Deno.test("commonjs - require non-analyzable arg", () => { + runTest({ + input: `const pkg = require(path.join(basedir, "package.json"))`, + expected: `import { createRequire } from "node:module"; +const require = createRequire(import.meta.url); +const pkg = require(path.join(basedir, "package.json"));`, + }); +}); + +Deno.test("commonjs - keep binding", () => { + runTest({ + input: `export var __createBinding = Object.create ? 1 : 2;`, + expected: `export var __createBinding = Object.create ? 1 : 2;`, + }); +}); + +Deno.test("commonjs - require lazy import", () => { + runTest({ + input: `if (typeof process.env.NODE_PG_FORCE_NATIVE !== 'undefined') { + module.exports = new PG(require('./native')) +} else { + module.exports = new PG(Client) + + // lazy require native module...the native module may not have installed + Object.defineProperty(module.exports, 'native', { + configurable: true, + enumerable: false, + get() { + let native = null + try { + native = new PG(require('./native')) + } catch (err) { + if (err.code !== 'MODULE_NOT_FOUND') { + throw err + } + } + + // overwrite module.exports.native so that getter is never called again + Object.defineProperty(module.exports, 'native', { + value: native, + }) + + return native + }, + }) +}`, + expected: `${INIT} +${IMPORT_REQUIRE} +if (typeof process.env.NODE_PG_FORCE_NATIVE !== 'undefined') { + module.exports = new PG(require('./native')); +} else { + module.exports = new PG(Client); + + // lazy require native module...the native module may not have installed + Object.defineProperty(module.exports, 'native', { + configurable: true, + enumerable: false, + get() { + let native = null; + try { + native = new PG(require('./native')); + } catch (err) { + if (err.code !== 'MODULE_NOT_FOUND') { + throw err; + } + } + + // overwrite module.exports.native so that getter is never called again + Object.defineProperty(module.exports, 'native', { + value: native + }); + return native; + } + }); +} +${DEFAULT_EXPORT} +${DEFAULT_EXPORT_END}`, + }); +}); + +Deno.test("commonjs - wrapped iife binding", () => { + runTest({ + input: `"production" !== process.env.NODE_ENV && (function() { + exports.foo = 123 +})()`, + expected: `${INIT} +"production" !== process.env.NODE_ENV && function () { + exports.foo = 123; +}(); +var _foo = exports.foo; +export { _foo as foo }; +${DEFAULT_EXPORT} +${DEFAULT_EXPORT_END}`, + }); +}); + +Deno.test("commonjs - re-export #2", () => { + runTest({ + input: `module.exports = require("foo");`, + expected: `${INIT} +export * from "foo"; +import * as _mod from "foo"; +module.exports = _mod; +${DEFAULT_EXPORT} +${DEFAULT_EXPORT_END}`, + }); +}); + +Deno.test("commonjs - keep require() in .mjs", () => { + runTest({ + filename: "foo.mjs", + input: `try { require("foo"); } catch {}`, + expected: `try { + require("foo"); +} catch {}`, + }); +}); + +Deno.test("commonjs - keep conditional require() in ESM file", () => { + runTest({ + filename: "foo.mjs", + input: `try { require("foo"); } catch {}; +export {};`, + expected: `try { + require("foo"); +} catch {} +export {};`, + }); +}); + +Deno.test("commonjs - CJS turned ESM module", () => { + runTest({ + filename: "foo.mjs", + input: `module.exports.create = confettiCannon; +export default module.exports; +export var create = module.exports.create;`, + expected: `module.exports.create = confettiCannon; +export default module.exports; +export var create = module.exports.create;`, + }); +}); + +Deno.test("commonjs - minified __esModule", () => { + runTest({ + filename: "foo.js", + input: ` +const m = module.exports; +const a = Object.defineProperty; +a(m, "__esModule", { value: !0 });`, + expected: `${INIT} +const m = module.exports; +const a = Object.defineProperty; +a(m, "__esModule", { + value: !0 +}); +${DEFAULT_EXPORT} +${DEFAULT_EXPORT_END} +export var __esModule = exports.__esModule;`, + }); +}); + +Deno.test("commonjs - esbuild __importDefault", () => { + runTest({ + input: + `var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +const node_events_1 = __importDefault(require("node:events"));`, + expected: `import * as _mod from "node:events"; +var __importDefault = this && this.__importDefault || function (mod) { + return mod && mod.__esModule ? mod : { + "default": mod + }; +}; +const node_events_1 = __importDefault({ + __esModule: true, + default: _mod.default ?? _mod +});`, + }); +}); + +// --- New tests for CJS transform fixes --- + +Deno.test("commonjs - require.resolve injects createRequire", () => { + runTest({ + input: `var resolved = require.resolve("some-package");`, + expected: `${IMPORT_REQUIRE} +var resolved = require.resolve("some-package");`, + }); +}); + +Deno.test("commonjs - require.resolve with require() both inject createRequire once", () => { + runTest({ + input: `var resolved = require.resolve("some-package"); +if (true) { + var mod = require("other"); +}`, + expected: `${IMPORT_REQUIRE} +var resolved = require.resolve("some-package"); +if (true) { + var mod = require("other"); +}`, + }); +}); + +Deno.test("commonjs - .mts file treated as ESM", () => { + runTest({ + filename: "foo.mts", + input: `export const x = 1;`, + expected: `export const x = 1;`, + }); +}); + +Deno.test("commonjs - .cts file treated as CJS", () => { + runTest({ + filename: "foo.cts", + input: `module.exports = 42;`, + expected: `${INIT} +module.exports = 42; +${DEFAULT_EXPORT} +${DEFAULT_EXPORT_END}`, + }); +}); + +Deno.test("commonjs - __dirname and __filename polyfill", () => { + const DIRNAME_IMPORT = + `import { fileURLToPath as __cjs_fileURLToPath } from "node:url"; +import { dirname as __cjs_dirname } from "node:path"; +var __filename = __cjs_fileURLToPath(import.meta.url); +var __dirname = __cjs_dirname(__filename);`; + runTest({ + input: `var dir = __dirname; +var file = __filename; +module.exports = { dir: dir, file: file };`, + expected: `${INIT} +${DIRNAME_IMPORT} +var dir = __dirname; +var file = __filename; +module.exports = { + dir: dir, + file: file +}; +var _dir = exports.dir; +var _file = exports.file; +export { _dir as dir, _file as file }; +${DEFAULT_EXPORT} +${DEFAULT_EXPORT_END}`, + }); +}); + +Deno.test("commonjs - module.exports.X tracked as named export", () => { + runTest({ + input: `module.exports = function parse() {}; +module.exports.parse = module.exports; +module.exports.stringify = function stringify() {};`, + expected: `${INIT} +module.exports = function parse() {}; +module.exports.parse = module.exports; +module.exports.stringify = function stringify() {}; +var _parse = exports.parse; +var _stringify = exports.stringify; +export { _parse as parse, _stringify as stringify }; +${DEFAULT_EXPORT} +${DEFAULT_EXPORT_END}`, + }); +}); + +Deno.test("commonjs imitating esm - default export exists", () => { + runTest({ + input: `module.exports = { + 'default': 'string', + otherExport: 1 +}; +`, + expected: `${INIT} +module.exports = { + 'default': 'string', + otherExport: 1 +}; +var _otherExport = exports.otherExport; +export { _otherExport as otherExport }; +${DEFAULT_EXPORT} +${DEFAULT_EXPORT_END}`, + }); +}); + +Deno.test("commonjs - primitive module.exports with namespace re-export guards assignment", () => { + runTest({ + input: `__exportStar(require("./utils"), exports); +module.exports = "RFC3986";`, + expected: `${INIT} +import * as _ns from "./utils"; +export * from "./utils"; +module.exports = "RFC3986"; +${DEFAULT_EXPORT} +if (typeof exports === "object" && exports !== null && !("default" in exports)) for (var _k in _ns) if (_k !== "default" && _k !== "__esModule" && Object.prototype.hasOwnProperty.call(_ns, _k)) _default[_k] = _ns[_k]; +${DEFAULT_EXPORT_END}`, + }); +}); diff --git a/packages/plugin-vite/src/plugins/patches/inline_env_vars.ts b/packages/plugin-vite/src/plugins/patches/inline_env_vars.ts new file mode 100644 index 00000000000..820247d1345 --- /dev/null +++ b/packages/plugin-vite/src/plugins/patches/inline_env_vars.ts @@ -0,0 +1,78 @@ +import type { NodePath, PluginObj, types } from "@babel/core"; + +export function inlineEnvVarsPlugin(mode: string, env: Record) { + const allowed = new Map(); + for (const [name, value] of Object.entries(env)) { + if (name.startsWith("FRESH_PUBLIC_")) { + allowed.set(name, value); + } + } + + allowed.set("NODE_ENV", mode); + + return ( + { types: t }: { types: typeof types }, + ): PluginObj => { + function replace(path: NodePath, name: string) { + if (allowed.has(name)) { + const value = allowed.get(name); + + if (value !== undefined) { + path.replaceWith(t.stringLiteral(value)); + } else { + path.replaceWith(t.identifier("undefined")); + } + } + } + + return { + name: "fresh-env-var", + visitor: { + MemberExpression(path) { + // Check: process.env.* + if ( + t.isMemberExpression(path.node.object) && + t.isIdentifier(path.node.object.object) && + path.node.object.object.name === "process" && + t.isIdentifier(path.node.object.property) && + path.node.object.property.name === "env" && + t.isIdentifier(path.node.property) + ) { + const name = path.node.property.name; + replace(path, name); + } + + // Check: import.meta.env.* + if ( + t.isIdentifier(path.node.property) && + t.isMemberExpression(path.node.object) && + t.isIdentifier(path.node.object.property) && + path.node.object.property.name === "env" && + t.isMetaProperty(path.node.object.object) + ) { + const name = path.node.property.name; + replace(path, name); + } + }, + CallExpression(path) { + // Check: Deno.env.get("") + if ( + t.isMemberExpression(path.node.callee) && + t.isMemberExpression(path.node.callee.object) && + t.isIdentifier(path.node.callee.object.object) && + path.node.callee.object.object.name === "Deno" && + t.isIdentifier(path.node.callee.object.property) && + path.node.callee.object.property.name === "env" && + t.isIdentifier(path.node.callee.property) && + path.node.callee.property.name === "get" && + path.node.arguments.length > 0 && + t.isStringLiteral(path.node.arguments[0]) + ) { + const name = path.node.arguments[0].value; + replace(path, name); + } + }, + }, + }; + }; +} diff --git a/packages/plugin-vite/src/plugins/patches/inline_env_vars_test.ts b/packages/plugin-vite/src/plugins/patches/inline_env_vars_test.ts new file mode 100644 index 00000000000..424fb2501fd --- /dev/null +++ b/packages/plugin-vite/src/plugins/patches/inline_env_vars_test.ts @@ -0,0 +1,79 @@ +import { expect } from "@std/expect/expect"; +import * as babel from "@babel/core"; +import { inlineEnvVarsPlugin } from "./inline_env_vars.ts"; + +function runTest( + options: { + input: string; + expected: string; + mode?: string; + env?: Record; + }, +) { + const res = babel.transformSync(options.input, { + filename: "foo.js", + babelrc: false, + plugins: [ + inlineEnvVarsPlugin(options.mode ?? "development", options.env ?? {}), + ], + }); + + const output = res?.code ?? ""; + expect(output).toEqual(options.expected); +} + +Deno.test("env vars - inline NODE_ENV mode", () => { + runTest({ + input: `() => process.env.NODE_ENV`, + expected: `() => "asdf";`, + mode: "asdf", + }); +}); + +Deno.test("env vars - inline custom process.env.*", () => { + runTest({ + input: `() => process.env.FRESH_PUBLIC_FOO`, + expected: `() => "a";`, + env: { + FRESH_PUBLIC_FOO: "a", + }, + }); +}); + +Deno.test("env vars - inline Deno.env.get()", () => { + runTest({ + input: `() => Deno.env.get("FRESH_PUBLIC_FOO")`, + expected: `() => "b";`, + env: { + FRESH_PUBLIC_FOO: "b", + }, + }); +}); + +Deno.test("env vars - inline Deno.env.get(NODE_ENV)", () => { + runTest({ + input: `() => Deno.env.get("NODE_ENV")`, + expected: `() => "c";`, + mode: "c", + }); +}); + +Deno.test("env vars - inline const _ = Deno.env.get()", () => { + runTest({ + input: `const deno = Deno.env.get("FRESH_PUBLIC_FOO");`, + expected: `const deno = "test";`, + env: { + FRESH_PUBLIC_FOO: "test", + }, + }); +}); + +Deno.test("env vars - inline import.meta.env.FRESH_PUBLIC_FOO", () => { + runTest({ + input: `() => import.meta.env.FRESH_PUBLIC_FOO;`, + expected: `() => "test";`, + env: { + FRESH_PUBLIC_FOO: "test", + }, + }); +}); diff --git a/packages/plugin-vite/tests/config_test.ts b/packages/plugin-vite/tests/config_test.ts index 366275c4bd6..cd650105de8 100644 --- a/packages/plugin-vite/tests/config_test.ts +++ b/packages/plugin-vite/tests/config_test.ts @@ -2,7 +2,7 @@ import { expect } from "@std/expect"; import { fresh } from "../src/mod.ts"; import type { Plugin } from "vite"; -Deno.test("fresh plugin - sets server.watch.ignored patterns", async () => { +Deno.test("fresh plugin - sets server.watch.ignored patterns", () => { const plugins = fresh() as Plugin[]; const freshPlugin = plugins.find((p) => p.name === "fresh"); expect(freshPlugin).toBeDefined(); @@ -10,7 +10,7 @@ Deno.test("fresh plugin - sets server.watch.ignored patterns", async () => { // Call the config hook as Vite would during dev // deno-lint-ignore no-explicit-any const configFn = freshPlugin!.config as any; - const result = await configFn({}, { command: "serve" }); + const result = configFn({}, { command: "serve" }); const ignored = result?.server?.watch?.ignored; expect(ignored).toBeDefined(); diff --git a/packages/plugin-vite/tests/dev_server_test.ts b/packages/plugin-vite/tests/dev_server_test.ts index 3eb1d8f6bd9..ffc30f92ee2 100644 --- a/packages/plugin-vite/tests/dev_server_test.ts +++ b/packages/plugin-vite/tests/dev_server_test.ts @@ -452,17 +452,6 @@ Deno.test({ sanitizeResources: false, }); -Deno.test({ - name: "vite dev - CJS npm package (qs)", - fn: async () => { - const res = await fetch(`${demoServer.address()}/tests/cjs_npm`); - const text = await res.text(); - expect(text).toContain("

qs-ok

"); - }, - sanitizeOps: false, - sanitizeResources: false, -}); - // issue: https://github.com/denoland/fresh/issues/3666 integrationTest( "vite dev - basePath does not intercept Vite URLs",