From 533d54ad1e43616e02707e88a9bf40680183c2d3 Mon Sep 17 00:00:00 2001 From: weixiaoing <1537476031@qq.com> Date: Sat, 22 Aug 2026 09:06:22 +0800 Subject: [PATCH] fix(esm-shim): avoid duplicate declarations close #921 --- src/plugins/esmShim.ts | 112 ++++++++++++++++++++++++++++++++--------- 1 file changed, 88 insertions(+), 24 deletions(-) diff --git a/src/plugins/esmShim.ts b/src/plugins/esmShim.ts index 3829846..3643d4b 100644 --- a/src/plugins/esmShim.ts +++ b/src/plugins/esmShim.ts @@ -6,29 +6,14 @@ import MagicString from 'magic-string' import type { SourceMapInput } from 'rollup' -import type { Plugin } from 'vite' +import type { ESTree, Plugin } from 'vite' import { supportImportMetaPaths } from '../electron' -const CJSyntaxRe = /__filename|__dirname|require\(|require\.resolve\(/ - -const CJSShim_normal = ` -// -- CommonJS Shims -- -import __cjs_url__ from 'node:url'; -import __cjs_path__ from 'node:path'; -import __cjs_mod__ from 'node:module'; -const __filename = __cjs_url__.fileURLToPath(import.meta.url); -const __dirname = __cjs_path__.dirname(__filename); -const require = __cjs_mod__.createRequire(import.meta.url); -` - -const CJSShim_node_20_11 = ` -// -- CommonJS Shims -- -import __cjs_mod__ from 'node:module'; -const __filename = import.meta.filename; -const __dirname = import.meta.dirname; -const require = __cjs_mod__.createRequire(import.meta.url); -` +const CJSShimMarker = '// -- CommonJS Shims --' +const CJSFilenameRe = /__filename/ +const CJSDirnameRe = /__dirname/ +const CJSRequireRe = /require\(|require\.resolve\(/ const ESMStaticImportRe = /(?<=\s|^|;)import\s*([\s"']*(?[\p{L}\p{M}\w\t\n\r $*,/{}@.]+)from\s*)?["']\s*(?(?<="\s*)[^"]*[^\s"](?=\s*")|(?<='\s*)[^']*[^\s'](?=\s*'))\s*["'][\s;]*/gmu @@ -45,19 +30,98 @@ function findStaticImports(code: string): StaticImport[] { return matches } -export default function esmShimPlugin(): Plugin { - const CJSShim = supportImportMetaPaths() ? CJSShim_node_20_11 : CJSShim_normal +function addBindingIdentifiers(pattern: ESTree.BindingPattern, bindings: Set): void { + if (pattern.type === 'Identifier') { + bindings.add(pattern.name) + } else if (pattern.type === 'ObjectPattern') { + for (const property of pattern.properties) { + addBindingIdentifiers(property.type === 'RestElement' ? property.argument : property.value, bindings) + } + } else if (pattern.type === 'ArrayPattern') { + for (const element of pattern.elements) { + if (element) addBindingIdentifiers(element.type === 'RestElement' ? element.argument : element, bindings) + } + } else { + addBindingIdentifiers(pattern.left, bindings) + } +} +function getUniqueBindingName(base: string, bindings: Set): string { + let name = base + let index = 1 + while (bindings.has(name)) name = `${base}${index++}` + bindings.add(name) + return name +} + +function createCJSShim(code: string, bindings: Set): string | null { + const needsFilename = !bindings.has('__filename') && CJSFilenameRe.test(code) + const needsDirname = !bindings.has('__dirname') && CJSDirnameRe.test(code) + const needsRequire = !bindings.has('require') && CJSRequireRe.test(code) + + if (!needsFilename && !needsDirname && !needsRequire) { + return null + } + + const imports: string[] = [] + const declarations: string[] = [] + + if (supportImportMetaPaths()) { + if (needsFilename) declarations.push('const __filename = import.meta.filename;') + if (needsDirname) declarations.push('const __dirname = import.meta.dirname;') + } else if (needsFilename || needsDirname) { + const urlBinding = getUniqueBindingName('__cjs_url__', bindings) + imports.push(`import ${urlBinding} from 'node:url';`) + if (needsFilename) declarations.push(`const __filename = ${urlBinding}.fileURLToPath(import.meta.url);`) + if (needsDirname) { + const pathBinding = getUniqueBindingName('__cjs_path__', bindings) + imports.push(`import ${pathBinding} from 'node:path';`) + declarations.push(`const __dirname = ${pathBinding}.dirname(${urlBinding}.fileURLToPath(import.meta.url));`) + } + } + + if (needsRequire) { + const moduleBinding = getUniqueBindingName('__cjs_mod__', bindings) + imports.push(`import ${moduleBinding} from 'node:module';`) + declarations.push(`const require = ${moduleBinding}.createRequire(import.meta.url);`) + } + + return `\n${CJSShimMarker}\n${imports.join('\n')}\n${declarations.join('\n')}\n` +} + +export default function esmShimPlugin(): Plugin { return { name: 'vite:esm-shim', apply: 'build', enforce: 'post', renderChunk(code, _chunk, { format, sourcemap }): { code: string; map?: SourceMapInput } | null { if (format === 'es') { - if (code.includes(CJSShim) || !CJSyntaxRe.test(code)) { - return null + const bindings = new Set() + for (const node of this.parse(code).body) { + if (node.type === 'ImportDeclaration') { + for (const specifier of node.specifiers) { + bindings.add(specifier.local.name) + } + continue + } + + const declaration = + node.type === 'ExportNamedDeclaration' || node.type === 'ExportDefaultDeclaration' ? node.declaration : node + if (declaration?.type === 'VariableDeclaration') { + for (const declarator of declaration.declarations) { + addBindingIdentifiers(declarator.id, bindings) + } + } else if ( + (declaration?.type === 'FunctionDeclaration' || declaration?.type === 'ClassDeclaration') && + declaration.id + ) { + bindings.add(declaration.id.name) + } } + const CJSShim = createCJSShim(code, bindings) + if (!CJSShim) return null + const lastESMImport = findStaticImports(code).pop() const indexToAppend = lastESMImport ? lastESMImport.end : 0 const s = new MagicString(code)