]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
fix(compiler-sfc): rewrite default export with AST analysis instead of regex (#7068)
author三咲智子 Kevin Deng <sxzz@sxzz.moe>
Tue, 28 Mar 2023 03:54:22 +0000 (11:54 +0800)
committerGitHub <noreply@github.com>
Tue, 28 Mar 2023 03:54:22 +0000 (11:54 +0800)
closes #7038
closes #7041
closes #7078

packages/compiler-sfc/__tests__/rewriteDefault.spec.ts
packages/compiler-sfc/src/compileScript.ts
packages/compiler-sfc/src/index.ts
packages/compiler-sfc/src/parse.ts
packages/compiler-sfc/src/rewriteDefault.ts

index 40561da17dbdc7052a551302f23ec8c979f911a2..bed27e3ae81b2c09c63d5fe5f14a7a8987473ed1 100644 (file)
@@ -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"
     `)
   })
index 8f2a5adeefd9cd5df3aebd2cb46011e494b96007..9722174f2901454deab7b5979b21c84be57bc560 100644 (file)
@@ -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(),
index c56b1266220e4d03942ab1ea57aaf9d891280e24..e42678de4bba1cede5954ec67ee64a0120691f3f 100644 (file)
@@ -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,
index 79065fc667eb2d1821d058530b276d7e8c6bdac1..b36d133eb101b175de53310c56e48c0d4837a814 100644 (file)
@@ -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<string, ImportBinding>
index 3efd8cefac28326cd9e94f32a46c11baeb6e8a73..ae5e7366bdec2994b38b031df1f4b99c1a168b0a 100644 (file)
@@ -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
     }
   }