]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
fix(compiler-sfc): generate valid TS in script and script setup co-usage with TS
authorEvan You <yyx990803@gmail.com>
Sun, 12 Dec 2021 01:53:52 +0000 (09:53 +0800)
committerEvan You <yyx990803@gmail.com>
Sun, 12 Dec 2021 01:53:52 +0000 (09:53 +0800)
fix #5094

packages/compiler-sfc/__tests__/__snapshots__/compileScript.spec.ts.snap
packages/compiler-sfc/__tests__/compileScript.spec.ts
packages/compiler-sfc/src/compileScript.ts

index 77e8ed01f59414ddfe6b7294ad15a1a554736954..52bda6f48b69a53fc474a2129ffa1d4bec1a18ca 100644 (file)
@@ -4,8 +4,10 @@ exports[`SFC compile <script setup> <script> and <script setup> co-usage script
 "import { x } from './x'
       
       export const n = 1
+
+      const __default__ = {}
       
-export default {
+export default /*#__PURE__*/Object.assign(__default__, {
   setup(__props, { expose }) {
   expose();
 
@@ -14,13 +16,15 @@ export default {
 return { n, x }
 }
 
-}"
+})"
 `;
 
 exports[`SFC compile <script setup> <script> and <script setup> co-usage script setup first 1`] = `
-"import { x } from './x'
+"export const n = 1
+      const __default__ = {}
+      import { x } from './x'
       
-export default {
+export default /*#__PURE__*/Object.assign(__default__, {
   setup(__props, { expose }) {
   expose();
 
@@ -29,29 +33,48 @@ export default {
 return { n, x }
 }
 
-}
-      export const n = 1"
+})"
 `;
 
 exports[`SFC compile <script setup> <script> and <script setup> co-usage script setup first, lang="ts", script block content export default 1`] = `
 "import { defineComponent as _defineComponent } from 'vue'
-import { x } from './x'
+
+      const __default__ = {
+        name: \\"test\\"
+      }
+      import { x } from './x'
       
-function setup(__props, { expose }) {
+export default /*#__PURE__*/_defineComponent({
+  ...__default__,
+  setup(__props, { expose }) {
+  expose();
 
       x()
       
 return { x }
 }
 
+})"
+`;
 
-      const __default__ = {
-        name: \\"test\\"
-      }
+exports[`SFC compile <script setup> <script> and <script setup> co-usage script setup first, named default export 1`] = `
+"export const n = 1
+      const def = {}
       
-export default /*#__PURE__*/_defineComponent({
-  ...__default__,
-  setup})"
+      
+const __default__ = def
+import { x } from './x'
+      
+export default /*#__PURE__*/Object.assign(__default__, {
+  setup(__props, { expose }) {
+  expose();
+
+      x()
+      
+return { n, def, x }
+}
+
+})"
 `;
 
 exports[`SFC compile <script setup> <script> and <script setup> co-usage spaces in ExportDefaultDeclaration node with many spaces and newline 1`] = `
@@ -62,16 +85,15 @@ exports[`SFC compile <script setup> <script> and <script setup> co-usage spaces
           some:'option'
         }
         
-function setup(__props, { expose }) {
+export default /*#__PURE__*/Object.assign(__default__, {
+  setup(__props, { expose }) {
+  expose();
 
         x()
         
 return { n, x }
 }
 
-
-export default /*#__PURE__*/ Object.assign(__default__, {
-  setup
 })"
 `;
 
@@ -83,16 +105,15 @@ exports[`SFC compile <script setup> <script> and <script setup> co-usage spaces
           some:'option'
         }
         
-function setup(__props, { expose }) {
+export default /*#__PURE__*/Object.assign(__default__, {
+  setup(__props, { expose }) {
+  expose();
 
         x()
         
 return { n, x }
 }
 
-
-export default /*#__PURE__*/ Object.assign(__default__, {
-  setup
 })"
 `;
 
@@ -980,7 +1001,12 @@ return () => {}
 `;
 
 exports[`SFC compile <script setup> should expose top level declarations 1`] = `
-"import { x } from './x'
+"import { xx } from './x'
+      let aa = 1
+      const bb = 2
+      function cc() {}
+      class dd {}
+      import { x } from './x'
       
 export default {
   setup(__props, { expose }) {
@@ -994,12 +1020,7 @@ export default {
 return { aa, bb, cc, dd, a, b, c, d, xx, x }
 }
 
-}
-      import { xx } from './x'
-      let aa = 1
-      const bb = 2
-      function cc() {}
-      class dd {}"
+}"
 `;
 
 exports[`SFC compile <script setup> with TypeScript const Enum 1`] = `
index df2d99348ce6c1089ce0b04102aac629467b2012..93373b2b4dca309ff8182c86d57ce877f49e6674 100644 (file)
@@ -169,47 +169,12 @@ defineExpose({ foo: 123 })
   })
 
   describe('<script> and <script setup> co-usage', () => {
-    describe('spaces in ExportDefaultDeclaration node', () => {
-      // #4371
-      test('with many spaces and newline', () => {
-        // #4371
-        const { content } = compile(`
-        <script>
-        export const n = 1
-        export        default
-        {
-          some:'option'
-        }
-        </script>
-        <script setup>
-        import { x } from './x'
-        x()
-        </script>
-        `)
-        assertCode(content)
-      })
-
-      test('with minimal spaces', () => {
-        const { content } = compile(`
-        <script>
-        export const n = 1
-        export default{
-          some:'option'
-        }
-        </script>
-        <script setup>
-        import { x } from './x'
-        x()
-        </script>
-        `)
-        assertCode(content)
-      })
-    })
-
     test('script first', () => {
       const { content } = compile(`
       <script>
       export const n = 1
+
+      export default {}
       </script>
       <script setup>
       import { x } from './x'
@@ -227,6 +192,22 @@ defineExpose({ foo: 123 })
       </script>
       <script>
       export const n = 1
+      export default {}
+      </script>
+      `)
+      assertCode(content)
+    })
+
+    test('script setup first, named default export', () => {
+      const { content } = compile(`
+      <script setup>
+      import { x } from './x'
+      x()
+      </script>
+      <script>
+      export const n = 1
+      const def = {}
+      export { def as default }
       </script>
       `)
       assertCode(content)
@@ -249,6 +230,43 @@ defineExpose({ foo: 123 })
       expect(content).toMatch(/const __default__[\S\s]*\.\.\.__default__/m)
       assertCode(content)
     })
+
+    describe('spaces in ExportDefaultDeclaration node', () => {
+      // #4371
+      test('with many spaces and newline', () => {
+        // #4371
+        const { content } = compile(`
+        <script>
+        export const n = 1
+        export        default
+        {
+          some:'option'
+        }
+        </script>
+        <script setup>
+        import { x } from './x'
+        x()
+        </script>
+        `)
+        assertCode(content)
+      })
+
+      test('with minimal spaces', () => {
+        const { content } = compile(`
+        <script>
+        export const n = 1
+        export default{
+          some:'option'
+        }
+        </script>
+        <script setup>
+        import { x } from './x'
+        x()
+        </script>
+        `)
+        assertCode(content)
+      })
+    })
   })
 
   describe('imports', () => {
index 9aa108118fdcdab676278f0d1e59d1a74c7db766..75203d42b53bcabd85fa3e7b67fbed722602329a 100644 (file)
@@ -59,6 +59,9 @@ const DEFINE_EMITS = 'defineEmits'
 const DEFINE_EXPOSE = 'defineExpose'
 const WITH_DEFAULTS = 'withDefaults'
 
+// constants
+const DEFAULT_VAR = `__default__`
+
 const isBuiltInDir = makeMap(
   `once,memo,if,else,else-if,slot,text,html,on,bind,model,show,cloak,is`
 )
@@ -214,14 +217,14 @@ export function compileScript(
         }
       }
       if (cssVars.length) {
-        content = rewriteDefault(content, `__default__`, plugins)
+        content = rewriteDefault(content, DEFAULT_VAR, plugins)
         content += genNormalScriptCssVarsCode(
           cssVars,
           bindings,
           scopeId,
           isProd
         )
-        content += `\nexport default __default__`
+        content += `\nexport default ${DEFAULT_VAR}`
       }
       return {
         ...script,
@@ -251,7 +254,6 @@ export function compileScript(
 
   // metadata that needs to be returned
   const bindingMetadata: BindingMetadata = {}
-  const defaultTempVar = `__default__`
   const helperImports: Set<string> = new Set()
   const userImports: Record<string, ImportBinding> = Object.create(null)
   const userImportAlias: Record<string, string> = Object.create(null)
@@ -780,7 +782,6 @@ export function compileScript(
   // 1. process normal <script> first if it exists
   let scriptAst: Program | undefined
   if (script) {
-    // import dedupe between <script> and <script setup>
     scriptAst = parse(
       script.content,
       {
@@ -809,9 +810,10 @@ export function compileScript(
       } else if (node.type === 'ExportDefaultDeclaration') {
         // export default
         defaultExport = node
+        // export default { ... } --> const __default__ = { ... }
         const start = node.start! + scriptStartOffset!
         const end = node.declaration.start! + scriptStartOffset!
-        s.overwrite(start, end, `const ${defaultTempVar} = `)
+        s.overwrite(start, end, `const ${DEFAULT_VAR} = `)
       } else if (node.type === 'ExportNamedDeclaration') {
         const defaultSpecifier = node.specifiers.find(
           s => s.exported.type === 'Identifier' && s.exported.name === 'default'
@@ -835,13 +837,14 @@ export function compileScript(
             // rewrite to `import { x as __default__ } from './x'` and
             // add to top
             s.prepend(
-              `import { ${defaultSpecifier.local.name} as ${defaultTempVar} } from '${node.source.value}'\n`
+              `import { ${defaultSpecifier.local.name} as ${DEFAULT_VAR} } from '${node.source.value}'\n`
             )
           } else {
             // export { x as default }
             // rewrite to `const __default__ = x` and move to end
-            s.append(
-              `\nconst ${defaultTempVar} = ${defaultSpecifier.local.name}\n`
+            s.appendLeft(
+              scriptEndOffset!,
+              `\nconst ${DEFAULT_VAR} = ${defaultSpecifier.local.name}\n`
             )
           }
         }
@@ -871,6 +874,13 @@ export function compileScript(
         helperImports.add(h)
       }
     }
+
+    // <script> after <script setup>
+    // we need to move the block up so that `const __default__` is
+    // declared before being used in the actual component definition
+    if (scriptStartOffset! > startOffset) {
+      s.move(scriptStartOffset!, scriptEndOffset!, 0)
+    }
   }
 
   // 2. parse <script setup> and  walk over top level statements
@@ -1384,46 +1394,33 @@ export function compileScript(
   // explicitly call `defineExpose`, call expose() with no args.
   const exposeCall =
     hasDefineExposeCall || options.inlineTemplate ? `` : `  expose();\n`
+  // wrap setup code with function.
   if (isTS) {
     // for TS, make sure the exported type is still valid type with
     // correct props information
     // we have to use object spread for types to be merged properly
     // user's TS setting should compile it down to proper targets
-    const def = defaultExport ? `\n  ...${defaultTempVar},` : ``
-    // wrap setup code with function.
-    // export the content of <script setup> as a named export, `setup`.
-    // this allows `import { setup } from '*.vue'` for testing purposes.
-    if (defaultExport) {
-      s.prependLeft(
-        startOffset,
-        `\n${hasAwait ? `async ` : ``}function setup(${args}) {\n`
-      )
-      s.append(
-        `\nexport default /*#__PURE__*/${helper(
-          `defineComponent`
-        )}({${def}${runtimeOptions}\n  setup})`
-      )
-    } else {
-      s.prependLeft(
-        startOffset,
-        `\nexport default /*#__PURE__*/${helper(
-          `defineComponent`
-        )}({${def}${runtimeOptions}\n  ${
-          hasAwait ? `async ` : ``
-        }setup(${args}) {\n${exposeCall}`
-      )
-      s.appendRight(endOffset, `})`)
-    }
+    // export default defineComponent({ ...__default__, ... })
+    const def = defaultExport ? `\n  ...${DEFAULT_VAR},` : ``
+    s.prependLeft(
+      startOffset,
+      `\nexport default /*#__PURE__*/${helper(
+        `defineComponent`
+      )}({${def}${runtimeOptions}\n  ${
+        hasAwait ? `async ` : ``
+      }setup(${args}) {\n${exposeCall}`
+    )
+    s.appendRight(endOffset, `})`)
   } else {
     if (defaultExport) {
-      // can't rely on spread operator in non ts mode
+      // without TS, can't rely on rest spread, so we use Object.assign
+      // export default Object.assign(__default__, { ... })
       s.prependLeft(
         startOffset,
-        `\n${hasAwait ? `async ` : ``}function setup(${args}) {\n`
-      )
-      s.append(
-        `\nexport default /*#__PURE__*/ Object.assign(${defaultTempVar}, {${runtimeOptions}\n  setup\n})\n`
+        `\nexport default /*#__PURE__*/Object.assign(${DEFAULT_VAR}, {${runtimeOptions}\n  ` +
+          `${hasAwait ? `async ` : ``}setup(${args}) {\n${exposeCall}`
       )
+      s.appendRight(endOffset, `})`)
     } else {
       s.prependLeft(
         startOffset,