From b066ffb3a3657d40cfad2c9a388d20616d135487 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=89=E5=92=B2=E6=99=BA=E5=AD=90=20Kevin=20Deng?= Date: Tue, 28 Mar 2023 11:54:22 +0800 Subject: [PATCH] fix(compiler-sfc): rewrite default export with AST analysis instead of regex (#7068) closes #7038 closes #7041 closes #7078 --- .../__tests__/rewriteDefault.spec.ts | 115 ++++++++++++------ packages/compiler-sfc/src/compileScript.ts | 7 +- packages/compiler-sfc/src/index.ts | 2 +- packages/compiler-sfc/src/parse.ts | 2 + packages/compiler-sfc/src/rewriteDefault.ts | 112 +++++++++-------- 5 files changed, 149 insertions(+), 89 deletions(-) diff --git a/packages/compiler-sfc/__tests__/rewriteDefault.spec.ts b/packages/compiler-sfc/__tests__/rewriteDefault.spec.ts index 40561da17db..bed27e3ae81 100644 --- a/packages/compiler-sfc/__tests__/rewriteDefault.spec.ts +++ b/packages/compiler-sfc/__tests__/rewriteDefault.spec.ts @@ -2,8 +2,9 @@ import { rewriteDefault } from '../src' describe('compiler sfc: rewriteDefault', () => { test('without export default', () => { - expect(rewriteDefault(`export a = {}`, 'script')).toMatchInlineSnapshot(` - "export a = {} + expect(rewriteDefault(`export const a = {}`, 'script')) + .toMatchInlineSnapshot(` + "export const a = {} const script = {}" `) }) @@ -14,6 +15,14 @@ describe('compiler sfc: rewriteDefault', () => { ).toMatchInlineSnapshot(`"const script = {}"`) }) + test('rewrite variable value default', () => { + expect(rewriteDefault(`export const foo = 'default'`, 'script')) + .toMatchInlineSnapshot(` + "export const foo = 'default' + const script = {}" + `) + }) + test('rewrite export named default', () => { expect( rewriteDefault( @@ -36,6 +45,18 @@ describe('compiler sfc: rewriteDefault', () => { export { a as b, a as c} const script = a" `) + + expect( + rewriteDefault( + `const a = 1 \n export { a as b } \n export { a as default, a as c }`, + 'script' + ) + ).toMatchInlineSnapshot(` + "const a = 1 + export { a as b } + export { a as c } + const script = a" + `) }) test('w/ comments', async () => { @@ -52,7 +73,7 @@ describe('compiler sfc: rewriteDefault', () => { ).toMatchInlineSnapshot(` "let App = {} export { - + } const _sfc_main = App" `) @@ -96,25 +117,25 @@ describe('compiler sfc: rewriteDefault', () => { expect( rewriteDefault(`export { default, foo } from './index.js'`, 'script') ).toMatchInlineSnapshot(` - "import { default as __VUE_DEFAULT__ } from './index.js' - export { foo } from './index.js' - const script = __VUE_DEFAULT__" + "import { default as __VUE_DEFAULT__ } from './index.js' + export { foo } from './index.js' + const script = __VUE_DEFAULT__" `) expect( rewriteDefault(`export { default , foo } from './index.js'`, 'script') ).toMatchInlineSnapshot(` - "import { default as __VUE_DEFAULT__ } from './index.js' - export { foo } from './index.js' - const script = __VUE_DEFAULT__" + "import { default as __VUE_DEFAULT__ } from './index.js' + export { foo } from './index.js' + const script = __VUE_DEFAULT__" `) expect( rewriteDefault(`export { foo, default } from './index.js'`, 'script') ).toMatchInlineSnapshot(` - "import { default as __VUE_DEFAULT__ } from './index.js' - export { foo, } from './index.js' - const script = __VUE_DEFAULT__" + "import { default as __VUE_DEFAULT__ } from './index.js' + export { foo, } from './index.js' + const script = __VUE_DEFAULT__" `) expect( @@ -123,9 +144,9 @@ describe('compiler sfc: rewriteDefault', () => { 'script' ) ).toMatchInlineSnapshot(` - "import { foo } from './index.js' - export { bar } from './index.js' - const script = foo" + "import { foo as __VUE_DEFAULT__ } from './index.js' + export { bar } from './index.js' + const script = __VUE_DEFAULT__" `) expect( @@ -134,9 +155,9 @@ describe('compiler sfc: rewriteDefault', () => { 'script' ) ).toMatchInlineSnapshot(` - "import { foo } from './index.js' - export { bar } from './index.js' - const script = foo" + "import { foo as __VUE_DEFAULT__ } from './index.js' + export { bar } from './index.js' + const script = __VUE_DEFAULT__" `) expect( @@ -145,18 +166,42 @@ describe('compiler sfc: rewriteDefault', () => { 'script' ) ).toMatchInlineSnapshot(` - "import { foo } from './index.js' - export { bar, } from './index.js' - const script = foo" + "import { foo as __VUE_DEFAULT__ } from './index.js' + export { bar, } from './index.js' + const script = __VUE_DEFAULT__" + `) + + expect( + rewriteDefault( + `export { foo as default } from './index.js' \n const foo = 1`, + 'script' + ) + ).toMatchInlineSnapshot(` + "import { foo as __VUE_DEFAULT__ } from './index.js' + export { } from './index.js' + const foo = 1 + const script = __VUE_DEFAULT__" + `) + + expect( + rewriteDefault( + `const a = 1 \nexport { a as default } from 'xxx'`, + 'script' + ) + ).toMatchInlineSnapshot(` + "import { a as __VUE_DEFAULT__ } from 'xxx' + const a = 1 + export { } from 'xxx' + const script = __VUE_DEFAULT__" `) }) test('export default class', async () => { expect(rewriteDefault(`export default class Foo {}`, 'script')) .toMatchInlineSnapshot(` - "class Foo {} - const script = Foo" - `) + " class Foo {} + const script = Foo" + `) }) test('export default class w/ comments', async () => { @@ -164,7 +209,7 @@ describe('compiler sfc: rewriteDefault', () => { rewriteDefault(`// export default\nexport default class Foo {}`, 'script') ).toMatchInlineSnapshot(` "// export default - class Foo {} + class Foo {} const script = Foo" `) }) @@ -190,16 +235,18 @@ describe('compiler sfc: rewriteDefault', () => { ).toMatchInlineSnapshot(` "/* export default class Foo {}*/ - class Bar {} + class Bar {} const script = Bar" `) }) test('@Component\nexport default class', async () => { - expect(rewriteDefault(`@Component\nexport default class Foo {}`, 'script')) - .toMatchInlineSnapshot(` - "@Component - class Foo {} + expect( + rewriteDefault(`@Component\nexport default class Foo {}`, 'script', [ + 'decorators-legacy' + ]) + ).toMatchInlineSnapshot(` + "@Component class Foo {} const script = Foo" `) }) @@ -208,12 +255,12 @@ describe('compiler sfc: rewriteDefault', () => { expect( rewriteDefault( `// export default\n@Component\nexport default class Foo {}`, - 'script' + 'script', + ['decorators-legacy'] ) ).toMatchInlineSnapshot(` "// export default - @Component - class Foo {} + @Component class Foo {} const script = Foo" `) }) @@ -242,7 +289,7 @@ describe('compiler sfc: rewriteDefault', () => { "/* @Component export default class Foo {}*/ - class Bar {} + class Bar {} const script = Bar" `) }) diff --git a/packages/compiler-sfc/src/compileScript.ts b/packages/compiler-sfc/src/compileScript.ts index 8f2a5adeefd..9722174f290 100644 --- a/packages/compiler-sfc/src/compileScript.ts +++ b/packages/compiler-sfc/src/compileScript.ts @@ -53,7 +53,7 @@ import { } from './cssVars' import { compileTemplate, SFCTemplateCompileOptions } from './compileTemplate' import { warnOnce } from './warn' -import { rewriteDefault } from './rewriteDefault' +import { rewriteDefaultAST } from './rewriteDefault' import { createCache } from './cache' import { shouldTransform, transformAST } from '@vue/reactivity-transform' @@ -231,7 +231,9 @@ export function compileScript( } } if (cssVars.length) { - content = rewriteDefault(content, DEFAULT_VAR, plugins) + const s = new MagicString(content) + rewriteDefaultAST(scriptAst.body, s, DEFAULT_VAR) + content = s.toString() content += genNormalScriptCssVarsCode( cssVars, bindings, @@ -1759,6 +1761,7 @@ export function compileScript( return { ...scriptSetup, + s, bindings: bindingMetadata, imports: userImports, content: s.toString(), diff --git a/packages/compiler-sfc/src/index.ts b/packages/compiler-sfc/src/index.ts index c56b1266220..e42678de4bb 100644 --- a/packages/compiler-sfc/src/index.ts +++ b/packages/compiler-sfc/src/index.ts @@ -3,7 +3,7 @@ export { parse } from './parse' export { compileTemplate } from './compileTemplate' export { compileStyle, compileStyleAsync } from './compileStyle' export { compileScript } from './compileScript' -export { rewriteDefault } from './rewriteDefault' +export { rewriteDefault, rewriteDefaultAST } from './rewriteDefault' export { shouldTransform as shouldTransformRef, transform as transformRef, diff --git a/packages/compiler-sfc/src/parse.ts b/packages/compiler-sfc/src/parse.ts index 79065fc667e..b36d133eb10 100644 --- a/packages/compiler-sfc/src/parse.ts +++ b/packages/compiler-sfc/src/parse.ts @@ -12,6 +12,7 @@ import { TemplateCompiler } from './compileTemplate' import { parseCssVars } from './cssVars' import { createCache } from './cache' import { hmrShouldReload, ImportBinding } from './compileScript' +import MagicString from 'magic-string' export const DEFAULT_FILENAME = 'anonymous.vue' @@ -41,6 +42,7 @@ export interface SFCTemplateBlock extends SFCBlock { export interface SFCScriptBlock extends SFCBlock { type: 'script' + s: MagicString setup?: string | boolean bindings?: BindingMetadata imports?: Record diff --git a/packages/compiler-sfc/src/rewriteDefault.ts b/packages/compiler-sfc/src/rewriteDefault.ts index 3efd8cefac2..ae5e7366bde 100644 --- a/packages/compiler-sfc/src/rewriteDefault.ts +++ b/packages/compiler-sfc/src/rewriteDefault.ts @@ -1,55 +1,55 @@ -import { parse, ParserPlugin } from '@babel/parser' +import { parse } from '@babel/parser' import MagicString from 'magic-string' +import type { ParserPlugin } from '@babel/parser' +import type { Identifier, Statement } from '@babel/types' -const defaultExportRE = /((?:^|\n|;)\s*)export(\s*)default/ -const namedDefaultExportRE = /((?:^|\n|;)\s*)export(.+)(?:as)?(\s*)default/s -const exportDefaultClassRE = - /((?:^|\n|;)\s*)export\s+default\s+class\s+([\w$]+)/ - -/** - * Utility for rewriting `export default` in a script block into a variable - * declaration so that we can inject things into it - */ export function rewriteDefault( input: string, as: string, parserPlugins?: ParserPlugin[] ): string { - if (!hasDefaultExport(input)) { - return input + `\nconst ${as} = {}` - } + const ast = parse(input, { + sourceType: 'module', + plugins: parserPlugins + }).program.body + const s = new MagicString(input) - let replaced: string | undefined + rewriteDefaultAST(ast, s, as) - const classMatch = input.match(exportDefaultClassRE) - if (classMatch) { - replaced = - input.replace(exportDefaultClassRE, '$1class $2') + - `\nconst ${as} = ${classMatch[2]}` - } else { - replaced = input.replace(defaultExportRE, `$1const ${as} =`) - } - if (!hasDefaultExport(replaced)) { - return replaced + return s.toString() +} + +/** + * Utility for rewriting `export default` in a script block into a variable + * declaration so that we can inject things into it + */ +export function rewriteDefaultAST( + ast: Statement[], + s: MagicString, + as: string +): void { + if (!hasDefaultExport(ast)) { + s.append(`\nconst ${as} = {}`) + return } // if the script somehow still contains `default export`, it probably has // multi-line comments or template strings. fallback to a full parse. - const s = new MagicString(input) - const ast = parse(input, { - sourceType: 'module', - plugins: parserPlugins - }).program.body ast.forEach(node => { if (node.type === 'ExportDefaultDeclaration') { if (node.declaration.type === 'ClassDeclaration') { - s.overwrite(node.start!, node.declaration.id.start!, `class `) + let start: number = + node.declaration.decorators && node.declaration.decorators.length > 0 + ? node.declaration.decorators[ + node.declaration.decorators.length - 1 + ].end! + : node.start! + s.overwrite(start, node.declaration.id.start!, ` class `) s.append(`\nconst ${as} = ${node.declaration.id.name}`) } else { s.overwrite(node.start!, node.declaration.start!, `const ${as} = `) } - } - if (node.type === 'ExportNamedDeclaration') { + } else if (node.type === 'ExportNamedDeclaration') { for (const specifier of node.specifiers) { if ( specifier.type === 'ExportSpecifier' && @@ -58,56 +58,64 @@ export function rewriteDefault( ) { if (node.source) { if (specifier.local.name === 'default') { - const end = specifierEnd(input, specifier.local.end!, node.end!) s.prepend( `import { default as __VUE_DEFAULT__ } from '${node.source.value}'\n` ) - s.overwrite(specifier.start!, end, ``) + const end = specifierEnd(s, specifier.local.end!, node.end!) + s.remove(specifier.start!, end) s.append(`\nconst ${as} = __VUE_DEFAULT__`) continue } else { - const end = specifierEnd( - input, - specifier.exported.end!, - node.end! - ) s.prepend( - `import { ${input.slice( + `import { ${s.slice( specifier.local.start!, specifier.local.end! - )} } from '${node.source.value}'\n` + )} as __VUE_DEFAULT__ } from '${node.source.value}'\n` ) - s.overwrite(specifier.start!, end, ``) - s.append(`\nconst ${as} = ${specifier.local.name}`) + const end = specifierEnd(s, specifier.exported.end!, node.end!) + s.remove(specifier.start!, end) + s.append(`\nconst ${as} = __VUE_DEFAULT__`) continue } } - const end = specifierEnd(input, specifier.end!, node.end!) - s.overwrite(specifier.start!, end, ``) + + const end = specifierEnd(s, specifier.end!, node.end!) + s.remove(specifier.start!, end) s.append(`\nconst ${as} = ${specifier.local.name}`) } } } }) - return s.toString() } -export function hasDefaultExport(input: string): boolean { - return defaultExportRE.test(input) || namedDefaultExportRE.test(input) +export function hasDefaultExport(ast: Statement[]): boolean { + for (const stmt of ast) { + if (stmt.type === 'ExportDefaultDeclaration') { + return true + } else if ( + stmt.type === 'ExportNamedDeclaration' && + stmt.specifiers.some( + spec => (spec.exported as Identifier).name === 'default' + ) + ) { + return true + } + } + return false } -function specifierEnd(input: string, end: number, nodeEnd: number | null) { +function specifierEnd(s: MagicString, end: number, nodeEnd: number | null) { // export { default , foo } ... let hasCommas = false let oldEnd = end while (end < nodeEnd!) { - if (/\s/.test(input.charAt(end))) { + if (/\s/.test(s.slice(end, end + 1))) { end++ - } else if (input.charAt(end) === ',') { + } else if (s.slice(end, end + 1) === ',') { end++ hasCommas = true break - } else if (input.charAt(end) === '}') { + } else if (s.slice(end, end + 1) === '}') { break } }