]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
wip: tests for compileScriptSetup
authorEvan You <yyx990803@gmail.com>
Thu, 9 Jul 2020 01:11:57 +0000 (21:11 -0400)
committerEvan You <yyx990803@gmail.com>
Thu, 9 Jul 2020 16:17:28 +0000 (12:17 -0400)
packages/compiler-core/src/transforms/transformExpression.ts
packages/compiler-sfc/__tests__/__snapshots__/compileScript.spec.ts.snap [new file with mode: 0644]
packages/compiler-sfc/__tests__/compileScript.spec.ts [new file with mode: 0644]
packages/compiler-sfc/src/compileScript.ts
packages/compiler-sfc/src/index.ts
packages/shared/src/index.ts

index 97e8fece2365798388a667ed3ca4da8924e0acb0..1e223067c2a54c879e0bff8b16d5faa63bc43d39 100644 (file)
@@ -30,7 +30,6 @@ import {
 import { createCompilerError, ErrorCodes } from '../errors'
 import { Node, Function, Identifier, ObjectProperty } from '@babel/types'
 import { validateBrowserExpression } from '../validateExpression'
-import { ParserPlugin } from '@babel/parser'
 
 const isLiteralWhitelisted = /*#__PURE__*/ makeMap('true,false,null,this')
 
@@ -130,10 +129,7 @@ export function processExpression(
     : `(${rawExp})${asParams ? `=>{}` : ``}`
   try {
     ast = parseJS(source, {
-      plugins: [
-        ...context.expressionPlugins,
-        ...(babelParserDefautPlugins as ParserPlugin[])
-      ]
+      plugins: [...context.expressionPlugins, ...babelParserDefautPlugins]
     }).program
   } catch (e) {
     context.onError(
diff --git a/packages/compiler-sfc/__tests__/__snapshots__/compileScript.spec.ts.snap b/packages/compiler-sfc/__tests__/__snapshots__/compileScript.spec.ts.snap
new file mode 100644 (file)
index 0000000..cf681ef
--- /dev/null
@@ -0,0 +1,248 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`SFC compile <script setup> <script setup lang="ts"> hoist type declarations 1`] = `
+"import { defineComponent as __define__ } from 'vue'
+import { Slots as __Slots__ } from 'vue'
+export interface Foo {}
+        type Bar = {}
+        
+export function setup() {
+
+        const a = 1
+      
+return { a }
+}
+
+export default __define__({
+  setup
+})"
+`;
+
+exports[`SFC compile <script setup> errors should allow export default referencing imported binding 1`] = `
+"import { bar } from './bar'
+          
+export function setup() {
+
+          
+return { bar }
+}
+
+const __default__ = {
+            props: {
+              foo: {
+                default: () => bar
+              }
+            }
+          }
+        __default__.setup = setup
+export default __default__"
+`;
+
+exports[`SFC compile <script setup> errors should allow export default referencing re-exported binding 1`] = `
+"import { bar } from './bar'
+          
+export function setup() {
+
+          
+return { bar }
+}
+
+const __default__ = {
+            props: {
+              foo: {
+                default: () => bar
+              }
+            }
+          }
+        __default__.setup = setup
+export default __default__"
+`;
+
+exports[`SFC compile <script setup> errors should allow export default referencing scope var 1`] = `
+"export function setup() {
+
+          const bar = 1
+          
+return {  }
+}
+
+const __default__ = {
+            props: {
+              foo: {
+                default: bar => bar + 1
+              }
+            }
+          }
+        __default__.setup = setup
+export default __default__"
+`;
+
+exports[`SFC compile <script setup> explicit setup signature 1`] = `
+"export function setup(props, { emit }) {
+emit('foo')
+return {  }
+}
+
+export default { setup }"
+`;
+
+exports[`SFC compile <script setup> exports export * from './x' 1`] = `
+"import { toRefs as __toRefs__ } from 'vue'
+import * as __export_all_0__ from './x'
+          
+export function setup() {
+
+          const y = 1
+          
+return Object.assign(
+  { y },
+  __toRefs__(__export_all_0__)
+)
+}
+
+export default { setup }"
+`;
+
+exports[`SFC compile <script setup> exports export { x } 1`] = `
+"export function setup() {
+
+           const x = 1
+           const y = 2
+           
+return { x, y }
+}
+
+export default { setup }"
+`;
+
+exports[`SFC compile <script setup> exports export { x } from './x' 1`] = `
+"import { x, y } from './x'
+          
+export function setup() {
+
+           
+return { x, y }
+}
+
+export default { setup }"
+`;
+
+exports[`SFC compile <script setup> exports export { x as default } 1`] = `
+"import x from './x'
+          
+export function setup() {
+
+          const y = 1
+          
+return { y }
+}
+
+
+const __default__ = x
+__default__.setup = setup
+export default __default__"
+`;
+
+exports[`SFC compile <script setup> exports export { x as default } from './x' 1`] = `
+"import { x as __default__ } from './x'
+import { y } from './x'
+          
+export function setup() {
+
+          
+return { y }
+}
+
+__default__.setup = setup
+export default __default__"
+`;
+
+exports[`SFC compile <script setup> exports export class X() {} 1`] = `
+"export function setup() {
+class X {}
+return { X }
+}
+
+export default { setup }"
+`;
+
+exports[`SFC compile <script setup> exports export const { x } = ... (destructuring) 1`] = `
+"export function setup() {
+
+          const [a = 1, { b } = { b: 123 }, ...c] = useFoo()
+          const { d = 2, _: [e], ...f } = useBar()
+        
+return { a, b, c, d, e, f }
+}
+
+export default { setup }"
+`;
+
+exports[`SFC compile <script setup> exports export const x = ... 1`] = `
+"export function setup() {
+const x = 1
+return { x }
+}
+
+export default { setup }"
+`;
+
+exports[`SFC compile <script setup> exports export default from './x' 1`] = `
+"import __default__ from './x'
+          
+export function setup() {
+
+          
+return {  }
+}
+
+__default__.setup = setup
+export default __default__"
+`;
+
+exports[`SFC compile <script setup> exports export default in <script setup> 1`] = `
+"export function setup() {
+
+          const y = 1
+          
+return { y }
+}
+
+const __default__ = {
+            props: ['foo']
+          }
+          __default__.setup = setup
+export default __default__"
+`;
+
+exports[`SFC compile <script setup> exports export function x() {} 1`] = `
+"export function setup() {
+function x(){}
+return { x }
+}
+
+export default { setup }"
+`;
+
+exports[`SFC compile <script setup> import dedupe between <script> and <script setup> 1`] = `
+"import { x } from './x'
+      
+export function setup() {
+
+      x()
+      
+return {  }
+}
+
+export default { setup }"
+`;
+
+exports[`SFC compile <script setup> should hoist imports 1`] = `
+"import { ref } from 'vue'
+export function setup() {
+
+return {  }
+}
+
+export default { setup }"
+`;
diff --git a/packages/compiler-sfc/__tests__/compileScript.spec.ts b/packages/compiler-sfc/__tests__/compileScript.spec.ts
new file mode 100644 (file)
index 0000000..e609ab0
--- /dev/null
@@ -0,0 +1,366 @@
+import { parse, compileScriptSetup, SFCScriptCompileOptions } from '../src'
+import { parse as babelParse } from '@babel/parser'
+import { babelParserDefautPlugins } from '@vue/shared'
+
+function compile(src: string, options?: SFCScriptCompileOptions) {
+  const { descriptor } = parse(src)
+  return compileScriptSetup(descriptor, options)
+}
+
+function assertCode(code: string) {
+  // parse the generated code to make sure it is valid
+  try {
+    babelParse(code, {
+      sourceType: 'module',
+      plugins: [...babelParserDefautPlugins, 'typescript']
+    })
+  } catch (e) {
+    console.log(code)
+    throw e
+  }
+  expect(code).toMatchSnapshot()
+}
+
+describe('SFC compile <script setup>', () => {
+  test('should hoist imports', () => {
+    assertCode(compile(`<script setup>import { ref } from 'vue'</script>`).code)
+  })
+
+  test('explicit setup signature', () => {
+    assertCode(
+      compile(`<script setup="props, { emit }">emit('foo')</script>`).code
+    )
+  })
+
+  test('import dedupe between <script> and <script setup>', () => {
+    const code = compile(`
+      <script>
+      import { x } from './x'
+      </script>
+      <script setup>
+      import { x } from './x'
+      x()
+      </script>
+      `).code
+    assertCode(code)
+    expect(code.indexOf(`import { x }`)).toEqual(
+      code.lastIndexOf(`import { x }`)
+    )
+  })
+
+  describe('exports', () => {
+    test('export const x = ...', () => {
+      const { code, bindings } = compile(
+        `<script setup>export const x = 1</script>`
+      )
+      assertCode(code)
+      expect(bindings).toStrictEqual({
+        x: 'setup'
+      })
+    })
+
+    test('export const { x } = ... (destructuring)', () => {
+      const { code, bindings } = compile(`<script setup>
+          export const [a = 1, { b } = { b: 123 }, ...c] = useFoo()
+          export const { d = 2, _: [e], ...f } = useBar()
+        </script>`)
+      assertCode(code)
+      expect(bindings).toStrictEqual({
+        a: 'setup',
+        b: 'setup',
+        c: 'setup',
+        d: 'setup',
+        e: 'setup',
+        f: 'setup'
+      })
+    })
+
+    test('export function x() {}', () => {
+      const { code, bindings } = compile(
+        `<script setup>export function x(){}</script>`
+      )
+      assertCode(code)
+      expect(bindings).toStrictEqual({
+        x: 'setup'
+      })
+    })
+
+    test('export class X() {}', () => {
+      const { code, bindings } = compile(
+        `<script setup>export class X {}</script>`
+      )
+      assertCode(code)
+      expect(bindings).toStrictEqual({
+        X: 'setup'
+      })
+    })
+
+    test('export { x }', () => {
+      const { code, bindings } = compile(
+        `<script setup>
+           const x = 1
+           const y = 2
+           export { x, y }
+          </script>`
+      )
+      assertCode(code)
+      expect(bindings).toStrictEqual({
+        x: 'setup',
+        y: 'setup'
+      })
+    })
+
+    test(`export { x } from './x'`, () => {
+      const { code, bindings } = compile(
+        `<script setup>
+           export { x, y } from './x'
+          </script>`
+      )
+      assertCode(code)
+      expect(bindings).toStrictEqual({
+        x: 'setup',
+        y: 'setup'
+      })
+    })
+
+    test(`export default from './x'`, () => {
+      const { code, bindings } = compile(
+        `<script setup>
+          export default from './x'
+          </script>`,
+        {
+          parserPlugins: ['exportDefaultFrom']
+        }
+      )
+      assertCode(code)
+      expect(bindings).toStrictEqual({})
+    })
+
+    test(`export { x as default }`, () => {
+      const { code, bindings } = compile(
+        `<script setup>
+          import x from './x'
+          const y = 1
+          export { x as default, y }
+          </script>`
+      )
+      assertCode(code)
+      expect(bindings).toStrictEqual({
+        y: 'setup'
+      })
+    })
+
+    test(`export { x as default } from './x'`, () => {
+      const { code, bindings } = compile(
+        `<script setup>
+          export { x as default, y } from './x'
+          </script>`
+      )
+      assertCode(code)
+      expect(bindings).toStrictEqual({
+        y: 'setup'
+      })
+    })
+
+    test(`export * from './x'`, () => {
+      const { code, bindings } = compile(
+        `<script setup>
+          export * from './x'
+          export const y = 1
+          </script>`
+      )
+      assertCode(code)
+      expect(bindings).toStrictEqual({
+        y: 'setup'
+        // in this case we cannot extract bindings from ./x so it falls back
+        // to runtime proxy dispatching
+      })
+    })
+
+    test('export default in <script setup>', () => {
+      const { code, bindings } = compile(
+        `<script setup>
+          export default {
+            props: ['foo']
+          }
+          export const y = 1
+          </script>`
+      )
+      assertCode(code)
+      expect(bindings).toStrictEqual({
+        y: 'setup'
+      })
+    })
+  })
+
+  describe('<script setup lang="ts">', () => {
+    test('hoist type declarations', () => {
+      const { code, bindings } = compile(`
+      <script setup lang="ts">
+        export interface Foo {}
+        type Bar = {}
+        export const a = 1
+      </script>`)
+      assertCode(code)
+      expect(bindings).toStrictEqual({ a: 'setup' })
+    })
+
+    test('extract props', () => {})
+
+    test('extract emits', () => {})
+  })
+
+  describe('errors', () => {
+    test('must have <script setup>', () => {
+      expect(() => compile(`<script>foo()</script>`)).toThrow(
+        `SFC has no <script setup>`
+      )
+    })
+
+    test('<script> and <script setup> must have same lang', () => {
+      expect(() =>
+        compile(`<script>foo()</script><script setup lang="ts">bar()</script>`)
+      ).toThrow(`<script> and <script setup> must have the same language type`)
+    })
+
+    test('export local as default', () => {
+      expect(() =>
+        compile(`<script setup>
+          const bar = 1
+          export { bar as default }
+        </script>`)
+      ).toThrow(`Cannot export locally defined variable as default`)
+    })
+
+    test('export default referencing local var', () => {
+      expect(() =>
+        compile(`<script setup>
+          const bar = 1
+          export default {
+            props: {
+              foo: {
+                default: () => bar
+              }
+            }
+          }
+        </script>`)
+      ).toThrow(`cannot reference locally declared variables`)
+    })
+
+    test('export default referencing exports', () => {
+      expect(() =>
+        compile(`<script setup>
+        export const bar = 1
+        export default {
+          props: bar
+        }
+      </script>`)
+      ).toThrow(`cannot reference locally declared variables`)
+    })
+
+    test('should allow export default referencing scope var', () => {
+      assertCode(
+        compile(`<script setup>
+          const bar = 1
+          export default {
+            props: {
+              foo: {
+                default: bar => bar + 1
+              }
+            }
+          }
+        </script>`).code
+      )
+    })
+
+    test('should allow export default referencing imported binding', () => {
+      assertCode(
+        compile(`<script setup>
+          import { bar } from './bar'
+          export { bar }
+          export default {
+            props: {
+              foo: {
+                default: () => bar
+              }
+            }
+          }
+        </script>`).code
+      )
+    })
+
+    test('should allow export default referencing re-exported binding', () => {
+      assertCode(
+        compile(`<script setup>
+          export { bar } from './bar'
+          export default {
+            props: {
+              foo: {
+                default: () => bar
+              }
+            }
+          }
+        </script>`).code
+      )
+    })
+
+    test('error on duplicated defalut export', () => {
+      expect(() =>
+        compile(`
+      <script>
+      export default {}
+      </script>
+      <script setup>
+      export default {}
+      </script>
+      `)
+      ).toThrow(`Default export is already declared`)
+
+      expect(() =>
+        compile(`
+      <script>
+      export default {}
+      </script>
+      <script setup>
+      const x = {}
+      export { x as default }
+      </script>
+      `)
+      ).toThrow(`Default export is already declared`)
+
+      expect(() =>
+        compile(`
+      <script>
+      export default {}
+      </script>
+      <script setup>
+      export { x as default } from './y'
+      </script>
+      `)
+      ).toThrow(`Default export is already declared`)
+
+      expect(() =>
+        compile(`
+      <script>
+      export { x as default } from './y'
+      </script>
+      <script setup>
+      export default {}
+      </script>
+      `)
+      ).toThrow(`Default export is already declared`)
+
+      expect(() =>
+        compile(`
+      <script>
+      const x = {}
+      export { x as default }
+      </script>
+      <script setup>
+      export default {}
+      </script>
+      `)
+      ).toThrow(`Default export is already declared`)
+    })
+  })
+})
index bc789e25f0df211666e2343dd5445bd7b8ab4adb..aabf4b3a79e0eda17b59daa3a057b6d3cb9a4e56 100644 (file)
@@ -51,7 +51,21 @@ export function compileScriptSetup(
   const setupExports: Record<string, boolean> = {}
   let exportAllIndex = 0
   let defaultExport: Node | undefined
-  let needDefaultExportCheck: boolean = false
+  let needDefaultExportRefCheck: boolean = false
+
+  const checkDuplicateDefaultExport = (node: Node) => {
+    if (defaultExport) {
+      // <script> already has export default
+      throw new Error(
+        `Default export is already declared in normal <script>.\n\n` +
+          generateCodeFrame(
+            source,
+            node.start! + startOffset,
+            node.start! + startOffset + `export default`.length
+          )
+      )
+    }
+  }
 
   const s = new MagicString(source)
   const startOffset = scriptSetup.loc.start.offset
@@ -62,7 +76,7 @@ export function compileScriptSetup(
   const isTS = scriptSetup.lang === 'ts'
   const plugins: ParserPlugin[] = [
     ...(options.parserPlugins || []),
-    ...(babelParserDefautPlugins as ParserPlugin[]),
+    ...babelParserDefautPlugins,
     ...(isTS ? (['typescript'] as const) : [])
   ]
 
@@ -130,10 +144,11 @@ export function compileScriptSetup(
 
   // 2. check <script setup="xxx"> function signature
   const hasExplicitSignature = typeof scriptSetup.setup === 'string'
-  let propsVar = `$props`
-  let emitVar = `$emit`
-  let slotsVar = `$slots`
-  let attrsVar = `$attrs`
+
+  let propsVar: string | undefined
+  let emitVar: string | undefined
+  let slotsVar: string | undefined
+  let attrsVar: string | undefined
 
   let propsType = `{}`
   let emitType = `(e: string, ...args: any[]) => void`
@@ -239,24 +254,32 @@ export function compileScriptSetup(
           s.remove(start, end)
         }
         for (const specifier of node.specifiers) {
-          if (specifier.type == 'ExportDefaultSpecifier') {
+          if (specifier.type === 'ExportDefaultSpecifier') {
             // export default from './x'
             // rewrite to `import __default__ from './x'`
+            checkDuplicateDefaultExport(node)
             defaultExport = node
             s.overwrite(
               specifier.exported.start! + startOffset,
               specifier.exported.start! + startOffset + 7,
               '__default__'
             )
-          } else if (specifier.type == 'ExportSpecifier') {
+          } else if (specifier.type === 'ExportSpecifier') {
             if (specifier.exported.name === 'default') {
+              checkDuplicateDefaultExport(node)
               defaultExport = node
               // 1. remove specifier
               if (node.specifiers.length > 1) {
-                s.remove(
-                  specifier.start! + startOffset,
-                  specifier.end! + startOffset
-                )
+                // removing the default specifier from a list of specifiers.
+                // look ahead until we reach the first non , or whitespace char.
+                let end = specifier.end! + startOffset
+                while (end < source.length) {
+                  if (/[^,\s]/.test(source.charAt(end))) {
+                    break
+                  }
+                  end++
+                }
+                s.remove(specifier.start! + startOffset, end)
               } else {
                 s.remove(node.start! + startOffset!, node.end! + startOffset!)
               }
@@ -288,6 +311,9 @@ export function compileScriptSetup(
               }
             } else {
               setupExports[specifier.exported.name] = true
+              if (node.source) {
+                imports[specifier.exported.name] = node.source.value
+              }
             }
           }
         }
@@ -305,30 +331,15 @@ export function compileScriptSetup(
     }
 
     if (node.type === 'ExportDefaultDeclaration') {
-      if (defaultExport) {
-        // <script> already has export default
-        throw new Error(
-          `Default export is already declared in normal <script>.\n\n` +
-            generateCodeFrame(
-              source,
-              node.start! + startOffset,
-              node.start! + startOffset + `export default`.length
-            )
-        )
-      } else {
-        // export default {} inside <script setup>
-        // this should be kept in module scope - move it to the end
-        s.move(start, end, source.length)
-        s.overwrite(
-          start,
-          start + `export default`.length,
-          `const __default__ =`
-        )
-        // save it for analysis when all imports and variable declarations have
-        // been recorded
-        defaultExport = node
-        needDefaultExportCheck = true
-      }
+      checkDuplicateDefaultExport(node)
+      // export default {} inside <script setup>
+      // this should be kept in module scope - move it to the end
+      s.move(start, end, source.length)
+      s.overwrite(start, start + `export default`.length, `const __default__ =`)
+      // save it for analysis when all imports and variable declarations have
+      // been recorded
+      defaultExport = node
+      needDefaultExportRefCheck = true
     }
 
     if (
@@ -397,7 +408,7 @@ export function compileScriptSetup(
 
   // check default export to make sure it doesn't reference setup scope
   // variables
-  if (needDefaultExportCheck) {
+  if (needDefaultExportRefCheck) {
     checkDefaultExport(
       defaultExport!,
       setupScopeVars,
@@ -428,7 +439,7 @@ export function compileScriptSetup(
 
   // wrap setup code with function
   // finalize the argument signature.
-  let args
+  let args = ``
   if (isTS) {
     if (slotsType === '__Slots__') {
       s.prepend(`import { Slots as __Slots__ } from 'vue'\n`)
@@ -450,13 +461,9 @@ export function compileScriptSetup(
         ss.appendRight(setupCtxASTNode.end! - 1!, `: ${ctxType}`)
       }
       args = ss.toString()
-    } else {
-      args = `$props: ${propsType}, { emit: $emit, slots: $slots, attrs: $attrs }: ${ctxType}`
     }
   } else {
-    args = hasExplicitSignature
-      ? scriptSetup.setup
-      : `$props, { emit: $emit, slots: $slots, attrs: $attrs }`
+    args = hasExplicitSignature ? (scriptSetup.setup as string) : ``
   }
 
   // export the content of <script setup> as a named export, `setup`.
@@ -602,6 +609,7 @@ function walkPattern(node: Node, bindings: Record<string, boolean>) {
 }
 
 function extractProps(node: TSTypeLiteral, props: Set<string>) {
+  // TODO generate type/required checks in dev
   for (const m of node.members) {
     if (m.type === 'TSPropertySignature' && m.key.type === 'Identifier') {
       props.add(m.key.name)
index 40c2ce7d86cafac5889ce67d2b2b99713fdfbe51..e1d7b15de14ec69ab0b0097bafdb2da6bc3225c1 100644 (file)
@@ -23,6 +23,7 @@ export {
   SFCAsyncStyleCompileOptions,
   SFCStyleCompileResults
 } from './compileStyle'
+export { SFCScriptCompileOptions } from './compileScript'
 export {
   CompilerOptions,
   CompilerError,
index 96b3f9157748bc49d64a9d9857122b08ba448ea9..91c173459fc7d50c63c16848d182167f394dfdaa 100644 (file)
@@ -23,7 +23,7 @@ export const babelParserDefautPlugins = [
   'bigInt',
   'optionalChaining',
   'nullishCoalescingOperator'
-]
+] as const
 
 export const EMPTY_OBJ: { readonly [key: string]: any } = __DEV__
   ? Object.freeze({})