]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
feat(compiler-sfc): compileScript parseOnly mode
authorEvan You <yyx990803@gmail.com>
Tue, 29 Jun 2021 21:56:49 +0000 (17:56 -0400)
committerEvan You <yyx990803@gmail.com>
Tue, 29 Jun 2021 21:56:49 +0000 (17:56 -0400)
This is an internal feature meant for IDE support

packages/compiler-sfc/__tests__/compileScriptParseOnlyMode.spec.ts [new file with mode: 0644]
packages/compiler-sfc/src/compileScript.ts
packages/compiler-sfc/src/parse.ts

diff --git a/packages/compiler-sfc/__tests__/compileScriptParseOnlyMode.spec.ts b/packages/compiler-sfc/__tests__/compileScriptParseOnlyMode.spec.ts
new file mode 100644 (file)
index 0000000..50a8a11
--- /dev/null
@@ -0,0 +1,77 @@
+import { TextRange } from '../src/parse'
+import { compileSFCScript } from './utils'
+
+describe('compileScript parseOnly mode', () => {
+  function compile(src: string) {
+    return compileSFCScript(src, { parseOnly: true })
+  }
+
+  function getRange(src: string, range: TextRange) {
+    return src.slice(range.start, range.end)
+  }
+
+  test('bindings', () => {
+    const scriptSrc = `
+    import { foo } from './x'
+    `
+    const scriptSetupSrc = `
+    import { bar } from './x'
+
+    const a = 123
+    function b() {}
+    class c {}
+    `
+    const src = `
+    <script>${scriptSrc}</script>
+    <script setup>${scriptSetupSrc}</script>
+    `
+    const { ranges } = compile(src)
+
+    expect(getRange(scriptSrc, ranges!.scriptBindings[0])).toBe('foo')
+    expect(
+      ranges!.scriptSetupBindings.map(r => getRange(scriptSetupSrc, r))
+    ).toMatchObject(['bar', 'a', 'b', 'c'])
+  })
+
+  test('defineProps', () => {
+    const src = `
+    defineProps({ foo: String })
+    `
+    const { ranges } = compile(`<script setup>${src}</script>`)
+    expect(getRange(src, ranges!.propsRuntimeArg!)).toBe(`{ foo: String }`)
+  })
+
+  test('defineProps (type)', () => {
+    const src = `
+    interface Props { x?: number }
+    defineProps<Props>()
+    `
+    const { ranges } = compile(`<script setup lang="ts">${src}</script>`)
+    expect(getRange(src, ranges!.propsTypeArg!)).toBe(`Props`)
+  })
+
+  test('withDefaults', () => {
+    const src = `
+    interface Props { x?: number }
+    withDefaults(defineProps<Props>(), { x: 1 })
+    `
+    const { ranges } = compile(`<script setup lang="ts">${src}</script>`)
+    expect(getRange(src, ranges!.withDefaultsArg!)).toBe(`{ x: 1 }`)
+  })
+
+  test('defineEmits', () => {
+    const src = `
+    defineEmits(['foo'])
+    `
+    const { ranges } = compile(`<script setup>${src}</script>`)
+    expect(getRange(src, ranges!.emitsRuntimeArg!)).toBe(`['foo']`)
+  })
+
+  test('defineEmits (type)', () => {
+    const src = `
+    defineEmits<{ (e: 'x'): void }>()
+    `
+    const { ranges } = compile(`<script setup lang="ts">${src}</script>`)
+    expect(getRange(src, ranges!.emitsTypeArg!)).toBe(`{ (e: 'x'): void }`)
+  })
+})
index d88736106d233bbd1281d028114ff391ddb2957d..ade6d2d2e0354aa3667d4dc3f2f7a0c4e64c29c2 100644 (file)
@@ -1,6 +1,11 @@
 import MagicString from 'magic-string'
 import { BindingMetadata, BindingTypes, UNREF } from '@vue/compiler-core'
-import { SFCDescriptor, SFCScriptBlock } from './parse'
+import {
+  ScriptSetupTextRanges,
+  SFCDescriptor,
+  SFCScriptBlock,
+  TextRange
+} from './parse'
 import { parse as _parse, ParserOptions, ParserPlugin } from '@babel/parser'
 import { babelParserDefaultPlugins, generateCodeFrame } from '@vue/shared'
 import {
@@ -71,7 +76,31 @@ export interface SFCScriptCompileOptions {
    * from being hot-reloaded separately from component state.
    */
   inlineTemplate?: boolean
+  /**
+   * Options for template compilation when inlining. Note these are options that
+   * would normally be pased to `compiler-sfc`'s own `compileTemplate()`, not
+   * options passed to `compiler-dom`.
+   */
   templateOptions?: Partial<SFCTemplateCompileOptions>
+  /**
+   * Skip codegen and only return AST / binding / text range information.
+   * Also makes the call error-tolerant.
+   * Used for IDE support.
+   */
+  parseOnly?: boolean
+}
+
+interface ImportBinding {
+  isType: boolean
+  imported: string
+  source: string
+  rangeNode: Node
+  isFromSetup: boolean
+}
+
+interface VariableBinding {
+  type: BindingTypes
+  rangeNode: Node
 }
 
 /**
@@ -83,10 +112,22 @@ export function compileScript(
   sfc: SFCDescriptor,
   options: SFCScriptCompileOptions
 ): SFCScriptBlock {
-  const { script, scriptSetup, source, filename } = sfc
+  let { script, scriptSetup, source, filename } = sfc
+  // feature flags
+  const enableRefSugar = !!options.refSugar
+  const parseOnly = !!options.parseOnly
 
   if (scriptSetup) {
-    warnExperimental(`<script setup>`, 227)
+    !parseOnly && warnExperimental(`<script setup>`, 227)
+  } else if (parseOnly) {
+    // in parse-only mode, construct a fake script setup so we still perform
+    // the full parse logic.
+    scriptSetup = {
+      type: 'script',
+      content: '',
+      attrs: {},
+      loc: null as any
+    }
   }
 
   // for backwards compat
@@ -134,7 +175,8 @@ export function compileScript(
     try {
       const scriptAst = _parse(script.content, {
         plugins,
-        sourceType: 'module'
+        sourceType: 'module',
+        errorRecovery: parseOnly
       }).program.body
       const bindings = analyzeScriptBindings(scriptAst)
       let content = script.content
@@ -165,7 +207,8 @@ export function compileScript(
 
   if (script && scriptLang !== scriptSetupLang) {
     throw new Error(
-      `[@vue/compiler-sfc] <script> and <script setup> must have the same language type.`
+      `[@vue/compiler-sfc] <script> and <script setup> must have the same ` +
+        `language type.`
     )
   }
 
@@ -174,22 +217,22 @@ export function compileScript(
     return scriptSetup
   }
 
-  const defaultTempVar = `__default__`
+  // metadata that needs to be returned
   const bindingMetadata: BindingMetadata = {}
+  const ranges: ScriptSetupTextRanges | undefined = parseOnly
+    ? {
+        scriptBindings: [],
+        scriptSetupBindings: []
+      }
+    : undefined
+
+  const defaultTempVar = `__default__`
   const helperImports: Set<string> = new Set()
-  const userImports: Record<
-    string,
-    {
-      isType: boolean
-      imported: string
-      source: string
-    }
-  > = Object.create(null)
+  const userImports: Record<string, ImportBinding> = Object.create(null)
   const userImportAlias: Record<string, string> = Object.create(null)
-  const setupBindings: Record<string, BindingTypes> = Object.create(null)
-  const refBindings: Record<string, BindingTypes> = Object.create(null)
+  const setupBindings: Record<string, VariableBinding> = Object.create(null)
+  const refBindings: Record<string, VariableBinding> = Object.create(null)
   const refIdentifiers: Set<Identifier> = new Set()
-  const enableRefSugar = !!options.refSugar
   let defaultExport: Node | undefined
   let hasDefinePropsCall = false
   let hasDefineEmitCall = false
@@ -197,9 +240,15 @@ export function compileScript(
   let propsRuntimeDecl: Node | undefined
   let propsRuntimeDefaults: Node | undefined
   let propsTypeDecl: TSTypeLiteral | TSInterfaceBody | undefined
+  let propsTypeDeclRaw: Node | undefined
   let propsIdentifier: string | undefined
-  let emitRuntimeDecl: Node | undefined
-  let emitTypeDecl: TSFunctionType | TSTypeLiteral | TSInterfaceBody | undefined
+  let emitsRuntimeDecl: Node | undefined
+  let emitsTypeDecl:
+    | TSFunctionType
+    | TSTypeLiteral
+    | TSInterfaceBody
+    | undefined
+  let emitsTypeDeclRaw: Node | undefined
   let emitIdentifier: string | undefined
   let hasAwait = false
   let hasInlinedSsrRenderFn = false
@@ -227,6 +276,7 @@ export function compileScript(
     offset: number
   ): Statement[] {
     try {
+      options.errorRecovery = parseOnly
       return _parse(input, options).program.body
     } catch (e) {
       e.message = `[@vue/compiler-sfc] ${e.message}\n\n${
@@ -254,7 +304,9 @@ export function compileScript(
     source: string,
     local: string,
     imported: string | false,
-    isType: boolean
+    isType: boolean,
+    isFromSetup: boolean,
+    rangeNode: Node
   ) {
     if (source === 'vue' && imported) {
       userImportAlias[imported] = local
@@ -262,7 +314,9 @@ export function compileScript(
     userImports[local] = {
       isType,
       imported: imported || 'default',
-      source
+      source,
+      rangeNode,
+      isFromSetup
     }
   }
 
@@ -288,8 +342,9 @@ export function compileScript(
         )
       }
 
+      propsTypeDeclRaw = node.typeParameters.params[0]
       propsTypeDecl = resolveQualifiedType(
-        node.typeParameters.params[0],
+        propsTypeDeclRaw,
         node => node.type === 'TSTypeLiteral'
       ) as TSTypeLiteral | TSInterfaceBody | undefined
 
@@ -297,7 +352,7 @@ export function compileScript(
         error(
           `type argument passed to ${DEFINE_PROPS}() must be a literal type, ` +
             `or a reference to an interface or literal type.`,
-          node.typeParameters.params[0]
+          propsTypeDeclRaw
         )
       }
     }
@@ -335,9 +390,9 @@ export function compileScript(
       error(`duplicate ${DEFINE_EMITS}() call`, node)
     }
     hasDefineEmitCall = true
-    emitRuntimeDecl = node.arguments[0]
+    emitsRuntimeDecl = node.arguments[0]
     if (node.typeParameters) {
-      if (emitRuntimeDecl) {
+      if (emitsRuntimeDecl) {
         error(
           `${DEFINE_EMIT}() cannot accept both type and non-type arguments ` +
             `at the same time. Use one or the other.`,
@@ -345,16 +400,17 @@ export function compileScript(
         )
       }
 
-      emitTypeDecl = resolveQualifiedType(
-        node.typeParameters.params[0],
+      emitsTypeDeclRaw = node.typeParameters.params[0]
+      emitsTypeDecl = resolveQualifiedType(
+        emitsTypeDeclRaw,
         node => node.type === 'TSFunctionType' || node.type === 'TSTypeLiteral'
       ) as TSFunctionType | TSTypeLiteral | TSInterfaceBody | undefined
 
-      if (!emitTypeDecl) {
+      if (!emitsTypeDecl) {
         error(
           `type argument passed to ${DEFINE_EMITS}() must be a function type, ` +
             `a literal type with call signatures, or a reference to the above types.`,
-          node.typeParameters.params[0]
+          emitsTypeDeclRaw
         )
       }
     }
@@ -469,7 +525,10 @@ export function compileScript(
     if (id.name[0] === '$') {
       error(`ref variable identifiers cannot start with $.`, id)
     }
-    refBindings[id.name] = setupBindings[id.name] = BindingTypes.SETUP_REF
+    refBindings[id.name] = setupBindings[id.name] = {
+      type: BindingTypes.SETUP_REF,
+      rangeNode: id
+    }
     refIdentifiers.add(id)
   }
 
@@ -635,7 +694,9 @@ export function compileScript(
             node.source.value,
             specifier.local.name,
             imported,
-            node.importKind === 'type'
+            node.importKind === 'type',
+            false,
+            specifier.local
           )
         }
       } else if (node.type === 'ExportDefaultDeclaration') {
@@ -724,7 +785,7 @@ export function compileScript(
       node.body.type === 'ExpressionStatement'
     ) {
       if (enableRefSugar) {
-        warnExperimental(`ref: sugar`, 228)
+        !parseOnly && warnExperimental(`ref: sugar`, 228)
         s.overwrite(
           node.label.start! + startOffset,
           node.body.start! + startOffset,
@@ -795,7 +856,9 @@ export function compileScript(
             source,
             local,
             imported,
-            node.importKind === 'type'
+            node.importKind === 'type',
+            true,
+            specifier.local
           )
         }
       }
@@ -925,6 +988,44 @@ export function compileScript(
     }
   }
 
+  // in parse only mode, we should have collected all the information we need,
+  // return early.
+  if (parseOnly) {
+    for (const key in userImports) {
+      const { rangeNode, isFromSetup } = userImports[key]
+      const bindings = isFromSetup
+        ? ranges!.scriptSetupBindings
+        : ranges!.scriptBindings
+      bindings.push(toTextRange(rangeNode))
+    }
+    for (const key in setupBindings) {
+      ranges!.scriptSetupBindings.push(
+        toTextRange(setupBindings[key].rangeNode)
+      )
+    }
+    if (propsRuntimeDecl) {
+      ranges!.propsRuntimeArg = toTextRange(propsRuntimeDecl)
+    }
+    if (propsTypeDeclRaw) {
+      ranges!.propsTypeArg = toTextRange(propsTypeDeclRaw)
+    }
+    if (emitsRuntimeDecl) {
+      ranges!.emitsRuntimeArg = toTextRange(emitsRuntimeDecl)
+    }
+    if (emitsTypeDeclRaw) {
+      ranges!.emitsTypeArg = toTextRange(emitsTypeDeclRaw)
+    }
+    if (propsRuntimeDefaults) {
+      ranges!.withDefaultsArg = toTextRange(propsRuntimeDefaults)
+    }
+    return {
+      ...scriptSetup,
+      ranges,
+      scriptAst,
+      scriptSetupAst
+    }
+  }
+
   // 3. Do a full walk to rewrite identifiers referencing let exports with ref
   // value access
   if (enableRefSugar && Object.keys(refBindings).length) {
@@ -958,15 +1059,15 @@ export function compileScript(
   if (propsTypeDecl) {
     extractRuntimeProps(propsTypeDecl, typeDeclaredProps, declaredTypes)
   }
-  if (emitTypeDecl) {
-    extractRuntimeEmits(emitTypeDecl, typeDeclaredEmits)
+  if (emitsTypeDecl) {
+    extractRuntimeEmits(emitsTypeDecl, typeDeclaredEmits)
   }
 
   // 5. check useOptions args to make sure it doesn't reference setup scope
   // variables
   checkInvalidScopeReference(propsRuntimeDecl, DEFINE_PROPS)
   checkInvalidScopeReference(propsRuntimeDefaults, DEFINE_PROPS)
-  checkInvalidScopeReference(emitRuntimeDecl, DEFINE_PROPS)
+  checkInvalidScopeReference(emitsRuntimeDecl, DEFINE_PROPS)
 
   // 6. remove non-script content
   if (script) {
@@ -1009,7 +1110,7 @@ export function compileScript(
         : BindingTypes.SETUP_MAYBE_REF
   }
   for (const key in setupBindings) {
-    bindingMetadata[key] = setupBindings[key]
+    bindingMetadata[key] = setupBindings[key].type
   }
 
   // 8. inject `useCssVars` calls
@@ -1051,10 +1152,10 @@ export function compileScript(
   }
   if (destructureElements.length) {
     args += `, { ${destructureElements.join(', ')} }`
-    if (emitTypeDecl) {
+    if (emitsTypeDecl) {
       args += `: { emit: (${scriptSetup.content.slice(
-        emitTypeDecl.start!,
-        emitTypeDecl.end!
+        emitsTypeDecl.start!,
+        emitsTypeDecl.end!
       )}), expose: any, slots: any, attrs: any }`
     }
   }
@@ -1156,11 +1257,11 @@ export function compileScript(
   } else if (propsTypeDecl) {
     runtimeOptions += genRuntimeProps(typeDeclaredProps)
   }
-  if (emitRuntimeDecl) {
+  if (emitsRuntimeDecl) {
     runtimeOptions += `\n  emits: ${scriptSetup.content
-      .slice(emitRuntimeDecl.start!, emitRuntimeDecl.end!)
+      .slice(emitsRuntimeDecl.start!, emitsRuntimeDecl.end!)
       .trim()},`
-  } else if (emitTypeDecl) {
+  } else if (emitsTypeDecl) {
     runtimeOptions += genRuntimeEmits(typeDeclaredEmits)
   }
 
@@ -1230,9 +1331,20 @@ export function compileScript(
   }
 }
 
+function registerBinding(
+  bindings: Record<string, VariableBinding>,
+  node: Identifier,
+  type: BindingTypes
+) {
+  bindings[node.name] = {
+    type,
+    rangeNode: node
+  }
+}
+
 function walkDeclaration(
   node: Declaration,
-  bindings: Record<string, BindingTypes>,
+  bindings: Record<string, VariableBinding>,
   userImportAlias: Record<string, string>
 ) {
   if (node.type === 'VariableDeclaration') {
@@ -1272,7 +1384,7 @@ function walkDeclaration(
         } else {
           bindingType = BindingTypes.SETUP_LET
         }
-        bindings[id.name] = bindingType
+        registerBinding(bindings, id, bindingType)
       } else if (id.type === 'ObjectPattern') {
         walkObjectPattern(id, bindings, isConst, isDefineCall)
       } else if (id.type === 'ArrayPattern') {
@@ -1285,13 +1397,16 @@ function walkDeclaration(
   ) {
     // export function foo() {} / export class Foo {}
     // export declarations must be named.
-    bindings[node.id!.name] = BindingTypes.SETUP_CONST
+    bindings[node.id!.name] = {
+      type: BindingTypes.SETUP_CONST,
+      rangeNode: node.id!
+    }
   }
 }
 
 function walkObjectPattern(
   node: ObjectPattern,
-  bindings: Record<string, BindingTypes>,
+  bindings: Record<string, VariableBinding>,
   isConst: boolean,
   isDefineCall = false
 ) {
@@ -1301,11 +1416,12 @@ function walkObjectPattern(
       if (p.key.type === 'Identifier') {
         if (p.key === p.value) {
           // const { x } = ...
-          bindings[p.key.name] = isDefineCall
+          const type = isDefineCall
             ? BindingTypes.SETUP_CONST
             : isConst
               ? BindingTypes.SETUP_MAYBE_REF
               : BindingTypes.SETUP_LET
+          registerBinding(bindings, p.key, type)
         } else {
           walkPattern(p.value, bindings, isConst, isDefineCall)
         }
@@ -1313,16 +1429,15 @@ function walkObjectPattern(
     } else {
       // ...rest
       // argument can only be identifer when destructuring
-      bindings[(p.argument as Identifier).name] = isConst
-        ? BindingTypes.SETUP_CONST
-        : BindingTypes.SETUP_LET
+      const type = isConst ? BindingTypes.SETUP_CONST : BindingTypes.SETUP_LET
+      registerBinding(bindings, p.argument as Identifier, type)
     }
   }
 }
 
 function walkArrayPattern(
   node: ArrayPattern,
-  bindings: Record<string, BindingTypes>,
+  bindings: Record<string, VariableBinding>,
   isConst: boolean,
   isDefineCall = false
 ) {
@@ -1333,32 +1448,33 @@ function walkArrayPattern(
 
 function walkPattern(
   node: Node,
-  bindings: Record<string, BindingTypes>,
+  bindings: Record<string, VariableBinding>,
   isConst: boolean,
   isDefineCall = false
 ) {
   if (node.type === 'Identifier') {
-    bindings[node.name] = isDefineCall
+    const type = isDefineCall
       ? BindingTypes.SETUP_CONST
       : isConst
         ? BindingTypes.SETUP_MAYBE_REF
         : BindingTypes.SETUP_LET
+    registerBinding(bindings, node, type)
   } else if (node.type === 'RestElement') {
     // argument can only be identifer when destructuring
-    bindings[(node.argument as Identifier).name] = isConst
-      ? BindingTypes.SETUP_CONST
-      : BindingTypes.SETUP_LET
+    const type = isConst ? BindingTypes.SETUP_CONST : BindingTypes.SETUP_LET
+    registerBinding(bindings, node.argument as Identifier, type)
   } else if (node.type === 'ObjectPattern') {
     walkObjectPattern(node, bindings, isConst)
   } else if (node.type === 'ArrayPattern') {
     walkArrayPattern(node, bindings, isConst)
   } else if (node.type === 'AssignmentPattern') {
     if (node.left.type === 'Identifier') {
-      bindings[node.left.name] = isDefineCall
+      const type = isDefineCall
         ? BindingTypes.SETUP_CONST
         : isConst
           ? BindingTypes.SETUP_MAYBE_REF
           : BindingTypes.SETUP_LET
+      registerBinding(bindings, node.left, type)
     } else {
       walkPattern(node.left, bindings, isConst)
     }
@@ -1958,3 +2074,10 @@ function extractIdentifiers(
 
   return nodes
 }
+
+function toTextRange(node: Node): TextRange {
+  return {
+    start: node.start!,
+    end: node.end!
+  }
+}
index dabd227fa04132cac54cd4b38ff0782e83c3e395..ad50baf5b95b8e0de4cf3a68d1c7d79c2975464b 100644 (file)
@@ -42,6 +42,25 @@ export interface SFCScriptBlock extends SFCBlock {
   bindings?: BindingMetadata
   scriptAst?: Statement[]
   scriptSetupAst?: Statement[]
+  ranges?: ScriptSetupTextRanges
+}
+
+/**
+ * Text range data for IDE support
+ */
+export interface ScriptSetupTextRanges {
+  scriptBindings: TextRange[]
+  scriptSetupBindings: TextRange[]
+  propsTypeArg?: TextRange
+  propsRuntimeArg?: TextRange
+  emitsTypeArg?: TextRange
+  emitsRuntimeArg?: TextRange
+  withDefaultsArg?: TextRange
+}
+
+export interface TextRange {
+  start: number
+  end: number
 }
 
 export interface SFCStyleBlock extends SFCBlock {