]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
refactor: simplify sfc script transform usage
authorEvan You <yyx990803@gmail.com>
Thu, 9 Jul 2020 22:18:46 +0000 (18:18 -0400)
committerEvan You <yyx990803@gmail.com>
Thu, 9 Jul 2020 22:18:46 +0000 (18:18 -0400)
packages/compiler-sfc/__tests__/compileScript.spec.ts
packages/compiler-sfc/src/compileScript.ts
packages/compiler-sfc/src/index.ts
packages/compiler-sfc/src/parse.ts

index 178c5922aeab9e133f64f22aab69c6561c2b59ec..90dc31bdb4c329ea6e1dd64341882dc037f4bc33 100644 (file)
@@ -1,10 +1,9 @@
-import { parse, compileScriptSetup, SFCScriptCompileOptions } from '../src'
+import { parse, 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)
+  return parse(src, options).descriptor.scriptTransformed!
 }
 
 function assertCode(code: string) {
@@ -23,17 +22,19 @@ function assertCode(code: string) {
 
 describe('SFC compile <script setup>', () => {
   test('should hoist imports', () => {
-    assertCode(compile(`<script setup>import { ref } from 'vue'</script>`).code)
+    assertCode(
+      compile(`<script setup>import { ref } from 'vue'</script>`).content
+    )
   })
 
   test('explicit setup signature', () => {
     assertCode(
-      compile(`<script setup="props, { emit }">emit('foo')</script>`).code
+      compile(`<script setup="props, { emit }">emit('foo')</script>`).content
     )
   })
 
   test('import dedupe between <script> and <script setup>', () => {
-    const code = compile(`
+    const { content } = compile(`
       <script>
       import { x } from './x'
       </script>
@@ -41,30 +42,30 @@ describe('SFC compile <script setup>', () => {
       import { x } from './x'
       x()
       </script>
-      `).code
-    assertCode(code)
-    expect(code.indexOf(`import { x }`)).toEqual(
-      code.lastIndexOf(`import { x }`)
+      `)
+    assertCode(content)
+    expect(content.indexOf(`import { x }`)).toEqual(
+      content.lastIndexOf(`import { x }`)
     )
   })
 
   describe('exports', () => {
     test('export const x = ...', () => {
-      const { code, bindings } = compile(
+      const { content, bindings } = compile(
         `<script setup>export const x = 1</script>`
       )
-      assertCode(code)
+      assertCode(content)
       expect(bindings).toStrictEqual({
         x: 'setup'
       })
     })
 
     test('export const { x } = ... (destructuring)', () => {
-      const { code, bindings } = compile(`<script setup>
+      const { content, bindings } = compile(`<script setup>
           export const [a = 1, { b } = { b: 123 }, ...c] = useFoo()
           export const { d = 2, _: [e], ...f } = useBar()
         </script>`)
-      assertCode(code)
+      assertCode(content)
       expect(bindings).toStrictEqual({
         a: 'setup',
         b: 'setup',
@@ -76,34 +77,34 @@ describe('SFC compile <script setup>', () => {
     })
 
     test('export function x() {}', () => {
-      const { code, bindings } = compile(
+      const { content, bindings } = compile(
         `<script setup>export function x(){}</script>`
       )
-      assertCode(code)
+      assertCode(content)
       expect(bindings).toStrictEqual({
         x: 'setup'
       })
     })
 
     test('export class X() {}', () => {
-      const { code, bindings } = compile(
+      const { content, bindings } = compile(
         `<script setup>export class X {}</script>`
       )
-      assertCode(code)
+      assertCode(content)
       expect(bindings).toStrictEqual({
         X: 'setup'
       })
     })
 
     test('export { x }', () => {
-      const { code, bindings } = compile(
+      const { content, bindings } = compile(
         `<script setup>
            const x = 1
            const y = 2
            export { x, y }
           </script>`
       )
-      assertCode(code)
+      assertCode(content)
       expect(bindings).toStrictEqual({
         x: 'setup',
         y: 'setup'
@@ -111,12 +112,12 @@ describe('SFC compile <script setup>', () => {
     })
 
     test(`export { x } from './x'`, () => {
-      const { code, bindings } = compile(
+      const { content, bindings } = compile(
         `<script setup>
            export { x, y } from './x'
           </script>`
       )
-      assertCode(code)
+      assertCode(content)
       expect(bindings).toStrictEqual({
         x: 'setup',
         y: 'setup'
@@ -124,52 +125,52 @@ describe('SFC compile <script setup>', () => {
     })
 
     test(`export default from './x'`, () => {
-      const { code, bindings } = compile(
+      const { content, bindings } = compile(
         `<script setup>
           export default from './x'
           </script>`,
         {
-          parserPlugins: ['exportDefaultFrom']
+          babelParserPlugins: ['exportDefaultFrom']
         }
       )
-      assertCode(code)
+      assertCode(content)
       expect(bindings).toStrictEqual({})
     })
 
     test(`export { x as default }`, () => {
-      const { code, bindings } = compile(
+      const { content, bindings } = compile(
         `<script setup>
           import x from './x'
           const y = 1
           export { x as default, y }
           </script>`
       )
-      assertCode(code)
+      assertCode(content)
       expect(bindings).toStrictEqual({
         y: 'setup'
       })
     })
 
     test(`export { x as default } from './x'`, () => {
-      const { code, bindings } = compile(
+      const { content, bindings } = compile(
         `<script setup>
           export { x as default, y } from './x'
           </script>`
       )
-      assertCode(code)
+      assertCode(content)
       expect(bindings).toStrictEqual({
         y: 'setup'
       })
     })
 
     test(`export * from './x'`, () => {
-      const { code, bindings } = compile(
+      const { content, bindings } = compile(
         `<script setup>
           export * from './x'
           export const y = 1
           </script>`
       )
-      assertCode(code)
+      assertCode(content)
       expect(bindings).toStrictEqual({
         y: 'setup'
         // in this case we cannot extract bindings from ./x so it falls back
@@ -178,7 +179,7 @@ describe('SFC compile <script setup>', () => {
     })
 
     test('export default in <script setup>', () => {
-      const { code, bindings } = compile(
+      const { content, bindings } = compile(
         `<script setup>
           export default {
             props: ['foo']
@@ -186,7 +187,7 @@ describe('SFC compile <script setup>', () => {
           export const y = 1
           </script>`
       )
-      assertCode(code)
+      assertCode(content)
       expect(bindings).toStrictEqual({
         y: 'setup'
       })
@@ -195,18 +196,18 @@ describe('SFC compile <script setup>', () => {
 
   describe('<script setup lang="ts">', () => {
     test('hoist type declarations', () => {
-      const { code, bindings } = compile(`
+      const { content, bindings } = compile(`
       <script setup lang="ts">
         export interface Foo {}
         type Bar = {}
         export const a = 1
       </script>`)
-      assertCode(code)
+      assertCode(content)
       expect(bindings).toStrictEqual({ a: 'setup' })
     })
 
     test('extract props', () => {
-      const { code } = compile(`
+      const { content } = compile(`
       <script setup="myProps" lang="ts">
       interface Test {}
 
@@ -237,59 +238,57 @@ describe('SFC compile <script setup>', () => {
         intersection: Test & {}
       }
       </script>`)
-      assertCode(code)
-      expect(code).toMatch(`string: { type: String, required: true }`)
-      expect(code).toMatch(`number: { type: Number, required: true }`)
-      expect(code).toMatch(`boolean: { type: Boolean, required: true }`)
-      expect(code).toMatch(`object: { type: Object, required: true }`)
-      expect(code).toMatch(`objectLiteral: { type: Object, required: true }`)
-      expect(code).toMatch(`fn: { type: Function, required: true }`)
-      expect(code).toMatch(`functionRef: { type: Function, required: true }`)
-      expect(code).toMatch(`objectRef: { type: Object, required: true }`)
-      expect(code).toMatch(`array: { type: Array, required: true }`)
-      expect(code).toMatch(`arrayRef: { type: Array, required: true }`)
-      expect(code).toMatch(`tuple: { type: Array, required: true }`)
-      expect(code).toMatch(`set: { type: Set, required: true }`)
-      expect(code).toMatch(`literal: { type: String, required: true }`)
-      expect(code).toMatch(`optional: { type: null, required: false }`)
-      expect(code).toMatch(`recordRef: { type: Object, required: true }`)
-      expect(code).toMatch(`interface: { type: Object, required: true }`)
-      expect(code).toMatch(`alias: { type: Array, required: true }`)
-      expect(code).toMatch(`union: { type: [String, Number], required: true }`)
-      expect(code).toMatch(
+      assertCode(content)
+      expect(content).toMatch(`string: { type: String, required: true }`)
+      expect(content).toMatch(`number: { type: Number, required: true }`)
+      expect(content).toMatch(`boolean: { type: Boolean, required: true }`)
+      expect(content).toMatch(`object: { type: Object, required: true }`)
+      expect(content).toMatch(`objectLiteral: { type: Object, required: true }`)
+      expect(content).toMatch(`fn: { type: Function, required: true }`)
+      expect(content).toMatch(`functionRef: { type: Function, required: true }`)
+      expect(content).toMatch(`objectRef: { type: Object, required: true }`)
+      expect(content).toMatch(`array: { type: Array, required: true }`)
+      expect(content).toMatch(`arrayRef: { type: Array, required: true }`)
+      expect(content).toMatch(`tuple: { type: Array, required: true }`)
+      expect(content).toMatch(`set: { type: Set, required: true }`)
+      expect(content).toMatch(`literal: { type: String, required: true }`)
+      expect(content).toMatch(`optional: { type: null, required: false }`)
+      expect(content).toMatch(`recordRef: { type: Object, required: true }`)
+      expect(content).toMatch(`interface: { type: Object, required: true }`)
+      expect(content).toMatch(`alias: { type: Array, required: true }`)
+      expect(content).toMatch(
+        `union: { type: [String, Number], required: true }`
+      )
+      expect(content).toMatch(
         `literalUnion: { type: [String, String], required: true }`
       )
-      expect(code).toMatch(
+      expect(content).toMatch(
         `literalUnionMixed: { type: [String, Number, Boolean], required: true }`
       )
-      expect(code).toMatch(`intersection: { type: Object, required: true }`)
+      expect(content).toMatch(`intersection: { type: Object, required: true }`)
     })
 
     test('extract emits', () => {
-      const { code } = compile(`
+      const { content } = compile(`
       <script setup="_, { emit: myEmit }" lang="ts">
       declare function myEmit(e: 'foo' | 'bar'): void
       declare function myEmit(e: 'baz', id: number): void
       </script>
       `)
-      assertCode(code)
-      expect(code).toMatch(`declare function __emit__(e: 'foo' | 'bar'): void`)
-      expect(code).toMatch(
+      assertCode(content)
+      expect(content).toMatch(
+        `declare function __emit__(e: 'foo' | 'bar'): void`
+      )
+      expect(content).toMatch(
         `declare function __emit__(e: 'baz', id: number): void`
       )
-      expect(code).toMatch(
+      expect(content).toMatch(
         `emits: ["foo", "bar", "baz"] as unknown as undefined`
       )
     })
   })
 
   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>`)
@@ -342,7 +341,7 @@ describe('SFC compile <script setup>', () => {
               }
             }
           }
-        </script>`).code
+        </script>`).content
       )
     })
 
@@ -358,7 +357,7 @@ describe('SFC compile <script setup>', () => {
               }
             }
           }
-        </script>`).code
+        </script>`).content
       )
     })
 
@@ -373,7 +372,7 @@ describe('SFC compile <script setup>', () => {
               }
             }
           }
-        </script>`).code
+        </script>`).content
       )
     })
 
index 42ee651c29d9ea22ffda5146c1366d693f7b2975..2f8fbe407d51b91060819d0045faa6e7b99e0aff 100644 (file)
@@ -1,4 +1,4 @@
-import MagicString, { SourceMap } from 'magic-string'
+import MagicString from 'magic-string'
 import { SFCDescriptor, SFCScriptBlock } from './parse'
 import { parse, ParserPlugin } from '@babel/parser'
 import { babelParserDefautPlugins, generateCodeFrame } from '@vue/shared'
@@ -17,13 +17,14 @@ import {
   TSDeclareFunction
 } from '@babel/types'
 import { walk } from 'estree-walker'
+import { RawSourceMap } from 'source-map'
 
-export interface BindingMetadata {
-  [key: string]: 'data' | 'props' | 'setup' | 'ctx'
+export interface SFCScriptCompileOptions {
+  babelParserPlugins?: ParserPlugin[]
 }
 
-export interface SFCScriptCompileOptions {
-  parserPlugins?: ParserPlugin[]
+export interface BindingMetadata {
+  [key: string]: 'data' | 'props' | 'setup' | 'ctx'
 }
 
 let hasWarned = false
@@ -33,10 +34,10 @@ let hasWarned = false
  * It requires the whole SFC descriptor because we need to handle and merge
  * normal `<script>` + `<script setup>` if both are present.
  */
-export function compileScriptSetup(
+export function compileScript(
   sfc: SFCDescriptor,
   options: SFCScriptCompileOptions = {}
-) {
+): SFCScriptBlock {
   if (__DEV__ && !__TEST__ && !hasWarned) {
     hasWarned = true
     console.log(
@@ -47,7 +48,13 @@ export function compileScriptSetup(
 
   const { script, scriptSetup, source, filename } = sfc
   if (!scriptSetup) {
-    throw new Error('SFC has no <script setup>.')
+    if (!script) {
+      throw new Error(`SFC contains no <script> tags.`)
+    }
+    return {
+      ...script,
+      bindings: analyzeScriptBindings(script)
+    }
   }
 
   if (script && script.lang !== scriptSetup.lang) {
@@ -86,7 +93,7 @@ export function compileScriptSetup(
 
   const isTS = scriptSetup.lang === 'ts'
   const plugins: ParserPlugin[] = [
-    ...(options.parserPlugins || []),
+    ...(options.babelParserPlugins || []),
     ...babelParserDefautPlugins,
     ...(isTS ? (['typescript'] as const) : [])
   ]
@@ -154,7 +161,8 @@ export function compileScriptSetup(
   }
 
   // 2. check <script setup="xxx"> function signature
-  const hasExplicitSignature = typeof scriptSetup.setup === 'string'
+  const setupValue = scriptSetup.attrs.setup
+  const hasExplicitSignature = typeof setupValue === 'string'
 
   let propsVar: string | undefined
   let emitVar: string | undefined
@@ -179,8 +187,8 @@ export function compileScriptSetup(
     // <script setup="xxx" lang="ts">
     // parse the signature to extract the props/emit variables the user wants
     // we need them to find corresponding type declarations.
-    const signatureAST = parse(`(${scriptSetup.setup})=>{}`, { plugins })
-      .program.body[0]
+    const signatureAST = parse(`(${setupValue})=>{}`, { plugins }).program
+      .body[0]
     const params = ((signatureAST as ExpressionStatement)
       .expression as ArrowFunctionExpression).params
     if (params[0] && params[0].type === 'Identifier') {
@@ -464,7 +472,7 @@ export function compileScriptSetup(
 }`
     if (hasExplicitSignature) {
       // inject types to user signature
-      args = scriptSetup.setup as string
+      args = setupValue as string
       const ss = new MagicString(args)
       if (propsASTNode) {
         // compensate for () wraper offset
@@ -476,7 +484,7 @@ export function compileScriptSetup(
       args = ss.toString()
     }
   } else {
-    args = hasExplicitSignature ? (scriptSetup.setup as string) : ``
+    args = hasExplicitSignature ? (setupValue as string) : ``
   }
 
   // 6. wrap setup code with function.
@@ -530,13 +538,14 @@ export function compileScriptSetup(
 
   s.trim()
   return {
+    ...scriptSetup,
     bindings,
-    code: s.toString(),
-    map: s.generateMap({
+    content: s.toString(),
+    map: (s.generateMap({
       source: filename,
       hires: true,
       includeContent: true
-    }) as SourceMap
+    }) as unknown) as RawSourceMap
   }
 }
 
index e1d7b15de14ec69ab0b0097bafdb2da6bc3225c1..86636a4d9cc97b94394428cf846525ee882e6e9b 100644 (file)
@@ -2,7 +2,7 @@
 export { parse } from './parse'
 export { compileTemplate } from './compileTemplate'
 export { compileStyle, compileStyleAsync } from './compileStyle'
-export { compileScriptSetup, analyzeScriptBindings } from './compileScript'
+export { compileScript, analyzeScriptBindings } from './compileScript'
 
 // Types
 export {
@@ -23,7 +23,7 @@ export {
   SFCAsyncStyleCompileOptions,
   SFCStyleCompileResults
 } from './compileStyle'
-export { SFCScriptCompileOptions } from './compileScript'
+export { SFCScriptCompileOptions, BindingMetadata } from './compileScript'
 export {
   CompilerOptions,
   CompilerError,
index f0e2001c3ca7af118e12ce7985e88dfa9ec009a4..1985b9b60d2cf794af3d5c81553631c4f9f7d300 100644 (file)
@@ -5,10 +5,12 @@ import {
   CompilerError,
   TextModes
 } from '@vue/compiler-core'
+import * as CompilerDOM from '@vue/compiler-dom'
 import { RawSourceMap, SourceMapGenerator } from 'source-map'
 import { generateCodeFrame } from '@vue/shared'
 import { TemplateCompiler } from './compileTemplate'
-import * as CompilerDOM from '@vue/compiler-dom'
+import { compileScript, BindingMetadata } from './compileScript'
+import { ParserPlugin } from '@babel/parser'
 
 export interface SFCParseOptions {
   filename?: string
@@ -16,6 +18,7 @@ export interface SFCParseOptions {
   sourceRoot?: string
   pad?: boolean | 'line' | 'space'
   compiler?: TemplateCompiler
+  babelParserPlugins?: ParserPlugin[]
 }
 
 export interface SFCBlock {
@@ -35,7 +38,7 @@ export interface SFCTemplateBlock extends SFCBlock {
 
 export interface SFCScriptBlock extends SFCBlock {
   type: 'script'
-  setup?: boolean | string
+  bindings?: BindingMetadata
 }
 
 export interface SFCStyleBlock extends SFCBlock {
@@ -50,6 +53,7 @@ export interface SFCDescriptor {
   template: SFCTemplateBlock | null
   script: SFCScriptBlock | null
   scriptSetup: SFCScriptBlock | null
+  scriptTransformed: SFCScriptBlock | null
   styles: SFCStyleBlock[]
   customBlocks: SFCBlock[]
 }
@@ -75,7 +79,8 @@ export function parse(
     filename = 'component.vue',
     sourceRoot = '',
     pad = false,
-    compiler = CompilerDOM
+    compiler = CompilerDOM,
+    babelParserPlugins
   }: SFCParseOptions = {}
 ): SFCParseResult {
   const sourceKey =
@@ -91,6 +96,7 @@ export function parse(
     template: null,
     script: null,
     scriptSetup: null,
+    scriptTransformed: null,
     styles: [],
     customBlocks: []
   }
@@ -146,15 +152,16 @@ export function parse(
         break
       case 'script':
         const block = createBlock(node, source, pad) as SFCScriptBlock
-        if (block.setup && !descriptor.scriptSetup) {
+        const isSetup = !!block.attrs.setup
+        if (isSetup && !descriptor.scriptSetup) {
           descriptor.scriptSetup = block
           break
         }
-        if (!block.setup && !descriptor.script) {
+        if (!isSetup && !descriptor.script) {
           descriptor.script = block
           break
         }
-        warnDuplicateBlock(source, filename, node, !!block.setup)
+        warnDuplicateBlock(source, filename, node, isSetup)
         break
       case 'style':
         descriptor.styles.push(createBlock(node, source, pad) as SFCStyleBlock)
@@ -182,6 +189,16 @@ export function parse(
     descriptor.styles.forEach(genMap)
   }
 
+  if (descriptor.script || descriptor.scriptSetup) {
+    try {
+      descriptor.scriptTransformed = compileScript(descriptor, {
+        babelParserPlugins
+      })
+    } catch (e) {
+      errors.push(e)
+    }
+  }
+
   const result = {
     descriptor,
     errors
@@ -252,8 +269,6 @@ function createBlock(
         }
       } else if (type === 'template' && p.name === 'functional') {
         ;(block as SFCTemplateBlock).functional = true
-      } else if (type === 'script' && p.name === 'setup') {
-        ;(block as SFCScriptBlock).setup = attrs.setup || true
       }
     }
   })