]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
feat(compiler-sfc): support relative imported types in macros
authorEvan You <yyx990803@gmail.com>
Thu, 13 Apr 2023 12:49:16 +0000 (20:49 +0800)
committerEvan You <yyx990803@gmail.com>
Sat, 15 Apr 2023 14:08:39 +0000 (22:08 +0800)
packages/compiler-core/src/babelUtils.ts
packages/compiler-sfc/__tests__/compileScript/resolveType.spec.ts
packages/compiler-sfc/src/compileScript.ts
packages/compiler-sfc/src/script/context.ts
packages/compiler-sfc/src/script/resolveType.ts
packages/compiler-sfc/src/script/utils.ts

index 7d96ec519285a2ea2962258fab47496948c94940..4b8f4182d2c881d527ccadd8faa04ca89cd96b4e 100644 (file)
@@ -6,10 +6,7 @@ import type {
   Function,
   ObjectProperty,
   BlockStatement,
-  Program,
-  ImportDefaultSpecifier,
-  ImportNamespaceSpecifier,
-  ImportSpecifier
+  Program
 } from '@babel/types'
 import { walk } from 'estree-walker'
 
@@ -246,17 +243,6 @@ export const isStaticProperty = (node: Node): node is ObjectProperty =>
 export const isStaticPropertyKey = (node: Node, parent: Node) =>
   isStaticProperty(parent) && parent.key === node
 
-export function getImportedName(
-  specifier: ImportSpecifier | ImportDefaultSpecifier | ImportNamespaceSpecifier
-) {
-  if (specifier.type === 'ImportSpecifier')
-    return specifier.imported.type === 'Identifier'
-      ? specifier.imported.name
-      : specifier.imported.value
-  else if (specifier.type === 'ImportNamespaceSpecifier') return '*'
-  return 'default'
-}
-
 /**
  * Copied from https://github.com/babel/babel/blob/main/packages/babel-types/src/validators/isReferenced.ts
  * To avoid runtime dependency on @babel/types (which includes process references)
index 38bcdf988ee452cb3c598eaf40bc43a9b0d81f71..7f25ae4888db3140452bd925ffd4da0040183903 100644 (file)
@@ -3,6 +3,7 @@ import { parse } from '../../src'
 import { ScriptCompileContext } from '../../src/script/context'
 import {
   inferRuntimeType,
+  recordImports,
   resolveTypeElements
 } from '../../src/script/resolveType'
 
@@ -246,6 +247,85 @@ describe('resolveType', () => {
     })
   })
 
+  describe('external type imports', () => {
+    test('relative ts', () => {
+      expect(
+        resolve(
+          `
+        import { P } from './foo'
+        import { Y as PP } from './bar'
+        type Target = P & PP
+        `,
+          {
+            'foo.ts': 'export type P = { foo: number }',
+            'bar.d.ts': 'type X = { bar: string }; export { X as Y }'
+          }
+        ).props
+      ).toStrictEqual({
+        foo: ['Number'],
+        bar: ['String']
+      })
+    })
+
+    test('relative vue', () => {
+      expect(
+        resolve(
+          `
+        import { P } from './foo.vue'
+        import { P as PP } from './bar.vue'
+        type Target = P & PP
+        `,
+          {
+            'foo.vue':
+              '<script lang="ts">export type P = { foo: number }</script>',
+            'bar.vue':
+              '<script setup lang="tsx">export type P = { bar: string }</script>'
+          }
+        ).props
+      ).toStrictEqual({
+        foo: ['Number'],
+        bar: ['String']
+      })
+    })
+
+    test('relative (chained)', () => {
+      expect(
+        resolve(
+          `
+        import { P } from './foo'
+        type Target = P
+        `,
+          {
+            'foo.ts': `import type { P as PP } from './nested/bar.vue'
+              export type P = { foo: number } & PP`,
+            'nested/bar.vue':
+              '<script setup lang="ts">export type P = { bar: string }</script>'
+          }
+        ).props
+      ).toStrictEqual({
+        foo: ['Number'],
+        bar: ['String']
+      })
+    })
+
+    test('relative (chained, re-export)', () => {
+      expect(
+        resolve(
+          `
+        import { PP as P } from './foo'
+        type Target = P
+        `,
+          {
+            'foo.ts': `export { P as PP } from './bar'`,
+            'bar.ts': 'export type P = { bar: string }'
+          }
+        ).props
+      ).toStrictEqual({
+        bar: ['String']
+      })
+    })
+  })
+
   describe('errors', () => {
     test('error on computed keys', () => {
       expect(() => resolve(`type Target = { [Foo]: string }`)).toThrow(
@@ -255,9 +335,26 @@ describe('resolveType', () => {
   })
 })
 
-function resolve(code: string) {
-  const { descriptor } = parse(`<script setup lang="ts">${code}</script>`)
-  const ctx = new ScriptCompileContext(descriptor, { id: 'test' })
+function resolve(code: string, files: Record<string, string> = {}) {
+  const { descriptor } = parse(`<script setup lang="ts">${code}</script>`, {
+    filename: 'Test.vue'
+  })
+  const ctx = new ScriptCompileContext(descriptor, {
+    id: 'test',
+    fs: {
+      fileExists(file) {
+        return !!files[file]
+      },
+      readFile(file) {
+        return files[file]
+      }
+    }
+  })
+
+  // ctx.userImports is collected when calling compileScript(), but we are
+  // skipping that here, so need to manually register imports
+  ctx.userImports = recordImports(ctx.scriptSetupAst!.body) as any
+
   const targetDecl = ctx.scriptSetupAst!.body.find(
     s => s.type === 'TSTypeAliasDeclaration' && s.id.name === 'Target'
   ) as TSTypeAliasDeclaration
index c828d77f6d5414efafdd67a036540c03165d75b6..593e8e072c638b2fe67c6077d47ae876aff56dd8 100644 (file)
@@ -2,8 +2,7 @@ import {
   BindingTypes,
   UNREF,
   isFunctionType,
-  walkIdentifiers,
-  getImportedName
+  walkIdentifiers
 } from '@vue/compiler-dom'
 import { DEFAULT_FILENAME, SFCDescriptor, SFCScriptBlock } from './parse'
 import { parse as _parse, ParserPlugin } from '@babel/parser'
@@ -45,7 +44,12 @@ import { DEFINE_EXPOSE, processDefineExpose } from './script/defineExpose'
 import { DEFINE_OPTIONS, processDefineOptions } from './script/defineOptions'
 import { processDefineSlots } from './script/defineSlots'
 import { DEFINE_MODEL, processDefineModel } from './script/defineModel'
-import { isLiteralNode, unwrapTSNode, isCallOf } from './script/utils'
+import {
+  isLiteralNode,
+  unwrapTSNode,
+  isCallOf,
+  getImportedName
+} from './script/utils'
 import { analyzeScriptBindings } from './script/analyzeScriptBindings'
 import { isImportUsed } from './script/importUsageCheck'
 import { processAwait } from './script/topLevelAwait'
@@ -106,6 +110,13 @@ export interface SFCScriptCompileOptions {
    * (**Experimental**) Enable macro `defineModel`
    */
   defineModel?: boolean
+  /**
+   *
+   */
+  fs?: {
+    fileExists(file: string): boolean
+    readFile(file: string): string
+  }
 }
 
 export interface ImportBinding {
index 718f23da5cad35b8a294de7b5b6504b3a0ebd804..1928ea900fc1a1f52bccd2fa9cc433dcb6c5229e 100644 (file)
@@ -1,13 +1,13 @@
 import { Node, ObjectPattern, Program } from '@babel/types'
 import { SFCDescriptor } from '../parse'
 import { generateCodeFrame } from '@vue/shared'
-import { parse as babelParse, ParserOptions, ParserPlugin } from '@babel/parser'
+import { parse as babelParse, ParserPlugin } from '@babel/parser'
 import { ImportBinding, SFCScriptCompileOptions } from '../compileScript'
 import { PropsDestructureBindings } from './defineProps'
 import { ModelDecl } from './defineModel'
 import { BindingMetadata } from '../../../compiler-core/src'
 import MagicString from 'magic-string'
-import { TypeScope } from './resolveType'
+import { TypeScope, WithScope } from './resolveType'
 
 export class ScriptCompileContext {
   isJS: boolean
@@ -83,31 +83,17 @@ export class ScriptCompileContext {
       scriptSetupLang === 'tsx'
 
     // resolve parser plugins
-    const plugins: ParserPlugin[] = []
-    if (!this.isTS || scriptLang === 'tsx' || scriptSetupLang === 'tsx') {
-      plugins.push('jsx')
-    } else {
-      // If don't match the case of adding jsx, should remove the jsx from the babelParserPlugins
-      if (options.babelParserPlugins)
-        options.babelParserPlugins = options.babelParserPlugins.filter(
-          n => n !== 'jsx'
-        )
-    }
-    if (options.babelParserPlugins) plugins.push(...options.babelParserPlugins)
-    if (this.isTS) {
-      plugins.push('typescript')
-      if (!plugins.includes('decorators')) {
-        plugins.push('decorators-legacy')
-      }
-    }
+    const plugins: ParserPlugin[] = resolveParserPlugins(
+      (scriptLang || scriptSetupLang)!,
+      options.babelParserPlugins
+    )
 
-    function parse(
-      input: string,
-      options: ParserOptions,
-      offset: number
-    ): Program {
+    function parse(input: string, offset: number): Program {
       try {
-        return babelParse(input, options).program
+        return babelParse(input, {
+          plugins,
+          sourceType: 'module'
+        }).program
       } catch (e: any) {
         e.message = `[@vue/compiler-sfc] ${e.message}\n\n${
           descriptor.filename
@@ -124,23 +110,12 @@ export class ScriptCompileContext {
       this.descriptor.script &&
       parse(
         this.descriptor.script.content,
-        {
-          plugins,
-          sourceType: 'module'
-        },
         this.descriptor.script.loc.start.offset
       )
 
     this.scriptSetupAst =
       this.descriptor.scriptSetup &&
-      parse(
-        this.descriptor.scriptSetup!.content,
-        {
-          plugins: [...plugins, 'topLevelAwait'],
-          sourceType: 'module'
-        },
-        this.startOffset!
-      )
+      parse(this.descriptor.scriptSetup!.content, this.startOffset!)
   }
 
   getString(node: Node, scriptSetup = true): string {
@@ -150,19 +125,39 @@ export class ScriptCompileContext {
     return block.content.slice(node.start!, node.end!)
   }
 
-  error(
-    msg: string,
-    node: Node,
-    end: number = node.end! + this.startOffset!
-  ): never {
+  error(msg: string, node: Node & WithScope, scope?: TypeScope): never {
     throw new Error(
       `[@vue/compiler-sfc] ${msg}\n\n${
         this.descriptor.filename
       }\n${generateCodeFrame(
         this.descriptor.source,
         node.start! + this.startOffset!,
-        end
+        node.end! + this.startOffset!
       )}`
     )
   }
 }
+
+export function resolveParserPlugins(
+  lang: string,
+  userPlugins?: ParserPlugin[]
+) {
+  const plugins: ParserPlugin[] = []
+  if (lang === 'jsx' || lang === 'tsx') {
+    plugins.push('jsx')
+  } else if (userPlugins) {
+    // If don't match the case of adding jsx
+    // should remove the jsx from user options
+    userPlugins = userPlugins.filter(p => p !== 'jsx')
+  }
+  if (lang === 'ts' || lang === 'tsx') {
+    plugins.push('typescript')
+    if (!plugins.includes('decorators')) {
+      plugins.push('decorators-legacy')
+    }
+  }
+  if (userPlugins) {
+    plugins.push(...userPlugins)
+  }
+  return plugins
+}
index f3b20a7ee1569d5d6912d8e11acdd6d668b494e2..1e82d2c83264f7f95979bef52f1dfa09d2978233 100644 (file)
@@ -1,11 +1,13 @@
 import {
+  Expression,
   Identifier,
-  Node as _Node,
+  Node,
   Statement,
   TSCallSignatureDeclaration,
   TSEnumDeclaration,
   TSExpressionWithTypeArguments,
   TSFunctionType,
+  TSInterfaceDeclaration,
   TSMappedType,
   TSMethodSignature,
   TSModuleBlock,
@@ -18,81 +20,108 @@ import {
   TSTypeReference,
   TemplateLiteral
 } from '@babel/types'
-import { UNKNOWN_TYPE } from './utils'
-import { ScriptCompileContext } from './context'
-import { ImportBinding } from '../compileScript'
-import { TSInterfaceDeclaration } from '@babel/types'
+import { UNKNOWN_TYPE, getId, getImportedName } from './utils'
+import { ScriptCompileContext, resolveParserPlugins } from './context'
+import { ImportBinding, SFCScriptCompileOptions } from '../compileScript'
 import { capitalize, hasOwn } from '@vue/shared'
-import { Expression } from '@babel/types'
+import path from 'path'
+import { parse as babelParse } from '@babel/parser'
+import { parse } from '../parse'
+
+type Import = Pick<ImportBinding, 'source' | 'imported'>
 
 export interface TypeScope {
   filename: string
-  imports: Record<string, ImportBinding>
-  types: Record<string, Node>
-  parent?: TypeScope
+  source: string
+  imports: Record<string, Import>
+  types: Record<
+    string,
+    Node & {
+      // scope types always has ownerScope attached
+      _ownerScope: TypeScope
+    }
+  >
+  exportedTypes: Record<
+    string,
+    Node & {
+      // scope types always has ownerScope attached
+      _ownerScope: TypeScope
+    }
+  >
 }
 
-interface WithScope {
+export interface WithScope {
   _ownerScope?: TypeScope
 }
 
 interface ResolvedElements {
-  props: Record<string, (TSPropertySignature | TSMethodSignature) & WithScope>
+  props: Record<
+    string,
+    (TSPropertySignature | TSMethodSignature) & {
+      // resolved props always has ownerScope attached
+      _ownerScope: TypeScope
+    }
+  >
   calls?: (TSCallSignatureDeclaration | TSFunctionType)[]
 }
 
-type Node = _Node &
-  WithScope & {
-    _resolvedElements?: ResolvedElements
-  }
-
 /**
  * Resolve arbitrary type node to a list of type elements that can be then
  * mapped to runtime props or emits.
  */
 export function resolveTypeElements(
   ctx: ScriptCompileContext,
-  node: Node
+  node: Node & WithScope & { _resolvedElements?: ResolvedElements },
+  scope?: TypeScope
 ): ResolvedElements {
   if (node._resolvedElements) {
     return node._resolvedElements
   }
-  return (node._resolvedElements = innerResolveTypeElements(ctx, node))
+  return (node._resolvedElements = innerResolveTypeElements(
+    ctx,
+    node,
+    node._ownerScope || scope || ctxToScope(ctx)
+  ))
 }
 
 function innerResolveTypeElements(
   ctx: ScriptCompileContext,
-  node: Node
+  node: Node,
+  scope: TypeScope
 ): ResolvedElements {
   switch (node.type) {
     case 'TSTypeLiteral':
-      return typeElementsToMap(ctx, node.members, node._ownerScope)
+      return typeElementsToMap(ctx, node.members, scope)
     case 'TSInterfaceDeclaration':
-      return resolveInterfaceMembers(ctx, node)
+      return resolveInterfaceMembers(ctx, node, scope)
     case 'TSTypeAliasDeclaration':
     case 'TSParenthesizedType':
-      return resolveTypeElements(ctx, node.typeAnnotation)
+      return resolveTypeElements(ctx, node.typeAnnotation, scope)
     case 'TSFunctionType': {
       return { props: {}, calls: [node] }
     }
     case 'TSUnionType':
     case 'TSIntersectionType':
       return mergeElements(
-        node.types.map(t => resolveTypeElements(ctx, t)),
+        node.types.map(t => resolveTypeElements(ctx, t, scope)),
         node.type
       )
     case 'TSMappedType':
-      return resolveMappedType(ctx, node)
+      return resolveMappedType(ctx, node, scope)
     case 'TSIndexedAccessType': {
       if (
         node.indexType.type === 'TSLiteralType' &&
         node.indexType.literal.type === 'StringLiteral'
       ) {
-        const resolved = resolveTypeElements(ctx, node.objectType)
+        const resolved = resolveTypeElements(ctx, node.objectType, scope)
         const key = node.indexType.literal.value
         const targetType = resolved.props[key].typeAnnotation
         if (targetType) {
-          return resolveTypeElements(ctx, targetType.typeAnnotation)
+          return resolveTypeElements(
+            ctx,
+            targetType.typeAnnotation,
+            resolved.props[key]._ownerScope
+          )
         } else {
           break
         }
@@ -105,9 +134,9 @@ function innerResolveTypeElements(
     }
     case 'TSExpressionWithTypeArguments': // referenced by interface extends
     case 'TSTypeReference': {
-      const resolved = resolveTypeReference(ctx, node)
+      const resolved = resolveTypeReference(ctx, node, scope)
       if (resolved) {
-        return resolveTypeElements(ctx, resolved)
+        return resolveTypeElements(ctx, resolved, resolved._ownerScope)
       } else {
         const typeName = getReferenceName(node)
         if (
@@ -118,7 +147,7 @@ function innerResolveTypeElements(
           return resolveBuiltin(ctx, node, typeName as any)
         }
         ctx.error(
-          `Failed to resolved type reference, or unsupported built-in utlility type.`,
+          `Failed to resolve type reference, or unsupported built-in utlility type.`,
           node
         )
       }
@@ -135,18 +164,13 @@ function typeElementsToMap(
   const res: ResolvedElements = { props: {} }
   for (const e of elements) {
     if (e.type === 'TSPropertySignature' || e.type === 'TSMethodSignature') {
-      ;(e as Node)._ownerScope = scope
-      const name =
-        e.key.type === 'Identifier'
-          ? e.key.name
-          : e.key.type === 'StringLiteral'
-          ? e.key.value
-          : null
+      ;(e as WithScope)._ownerScope = scope
+      const name = getId(e.key)
       if (name && !e.computed) {
-        res.props[name] = e
+        res.props[name] = e as ResolvedElements['props'][string]
       } else if (e.key.type === 'TemplateLiteral') {
         for (const key of resolveTemplateKeys(ctx, e.key)) {
-          res.props[key] = e
+          res.props[key] = e as ResolvedElements['props'][string]
         }
       } else {
         ctx.error(
@@ -172,11 +196,15 @@ function mergeElements(
       if (!hasOwn(baseProps, key)) {
         baseProps[key] = props[key]
       } else {
-        baseProps[key] = createProperty(baseProps[key].key, {
-          type,
-          // @ts-ignore
-          types: [baseProps[key], props[key]]
-        })
+        baseProps[key] = createProperty(
+          baseProps[key].key,
+          {
+            type,
+            // @ts-ignore
+            types: [baseProps[key], props[key]]
+          },
+          baseProps[key]._ownerScope
+        )
       }
     }
     if (calls) {
@@ -188,8 +216,9 @@ function mergeElements(
 
 function createProperty(
   key: Expression,
-  typeAnnotation: TSType
-): TSPropertySignature {
+  typeAnnotation: TSType,
+  scope: TypeScope
+): TSPropertySignature & { _ownerScope: TypeScope } {
   return {
     type: 'TSPropertySignature',
     key,
@@ -197,18 +226,20 @@ function createProperty(
     typeAnnotation: {
       type: 'TSTypeAnnotation',
       typeAnnotation
-    }
+    },
+    _ownerScope: scope
   }
 }
 
 function resolveInterfaceMembers(
   ctx: ScriptCompileContext,
-  node: TSInterfaceDeclaration & WithScope
+  node: TSInterfaceDeclaration & WithScope,
+  scope: TypeScope
 ): ResolvedElements {
   const base = typeElementsToMap(ctx, node.body.body, node._ownerScope)
   if (node.extends) {
     for (const ext of node.extends) {
-      const { props } = resolveTypeElements(ctx, ext)
+      const { props } = resolveTypeElements(ctx, ext, scope)
       for (const key in props) {
         if (!hasOwn(base.props, key)) {
           base.props[key] = props[key]
@@ -221,7 +252,8 @@ function resolveInterfaceMembers(
 
 function resolveMappedType(
   ctx: ScriptCompileContext,
-  node: TSMappedType
+  node: TSMappedType,
+  scope: TypeScope
 ): ResolvedElements {
   const res: ResolvedElements = { props: {} }
   if (!node.typeParameter.constraint) {
@@ -234,7 +266,8 @@ function resolveMappedType(
         type: 'Identifier',
         name: key
       },
-      node.typeAnnotation!
+      node.typeAnnotation!,
+      scope
     )
   }
   return res
@@ -357,32 +390,52 @@ function resolveTypeReference(
   node: (TSTypeReference | TSExpressionWithTypeArguments) & {
     _resolvedReference?: Node
   },
-  scope = ctxToScope(ctx)
-): Node | undefined {
+  scope?: TypeScope,
+  name?: string,
+  onlyExported = false
+): (Node & WithScope) | undefined {
   if (node._resolvedReference) {
     return node._resolvedReference
   }
-  const name = getReferenceName(node)
-  return (node._resolvedReference = innerResolveTypeReference(scope, name))
+  return (node._resolvedReference = innerResolveTypeReference(
+    ctx,
+    scope || ctxToScope(ctx),
+    name || getReferenceName(node),
+    node,
+    onlyExported
+  ))
 }
 
 function innerResolveTypeReference(
+  ctx: ScriptCompileContext,
   scope: TypeScope,
-  name: string | string[]
+  name: string | string[],
+  node: TSTypeReference | TSExpressionWithTypeArguments,
+  onlyExported: boolean
 ): Node | undefined {
   if (typeof name === 'string') {
     if (scope.imports[name]) {
-      // TODO external import
-    } else if (scope.types[name]) {
-      return scope.types[name]
+      return resolveTypeFromImport(ctx, scope, scope.imports[name], node)
+    } else {
+      const types = onlyExported ? scope.exportedTypes : scope.types
+      return types[name]
     }
   } else {
-    const ns = innerResolveTypeReference(scope, name[0])
+    const ns = innerResolveTypeReference(
+      ctx,
+      scope,
+      name[0],
+      node,
+      onlyExported
+    )
     if (ns && ns.type === 'TSModuleDeclaration') {
       const childScope = moduleDeclToScope(ns, scope)
       return innerResolveTypeReference(
+        ctx,
         childScope,
-        name.length > 2 ? name.slice(1) : name[name.length - 1]
+        name.length > 2 ? name.slice(1) : name[name.length - 1],
+        node,
+        true
       )
     }
   }
@@ -407,20 +460,125 @@ function qualifiedNameToPath(node: Identifier | TSQualifiedName): string[] {
   }
 }
 
+function resolveTypeFromImport(
+  ctx: ScriptCompileContext,
+  scope: TypeScope,
+  { source, imported }: Import,
+  node: TSTypeReference | TSExpressionWithTypeArguments
+): Node | undefined {
+  const fs = ctx.options.fs
+  if (!fs) {
+    ctx.error(
+      `fs options for compileScript are required for resolving imported types`,
+      node
+    )
+  }
+  // TODO (hmr) register dependency file on ctx
+  const containingFile = scope.filename
+  if (source.startsWith('.')) {
+    // relative import - fast path
+    const filename = path.join(containingFile, '..', source)
+    const resolved = resolveExt(filename, fs)
+    if (resolved) {
+      return resolveTypeReference(
+        ctx,
+        node,
+        fileToScope(ctx, resolved, fs),
+        imported,
+        true
+      )
+    } else {
+      ctx.error(`Failed to resolve import source for type`, node)
+    }
+  } else {
+    // TODO module or aliased import - use full TS resolution
+    return
+  }
+}
+
+function resolveExt(
+  filename: string,
+  fs: NonNullable<SFCScriptCompileOptions['fs']>
+) {
+  const tryResolve = (filename: string) => {
+    if (fs.fileExists(filename)) return filename
+  }
+  return (
+    tryResolve(filename) ||
+    tryResolve(filename + `.ts`) ||
+    tryResolve(filename + `.d.ts`) ||
+    tryResolve(filename + `/index.ts`) ||
+    tryResolve(filename + `/index.d.ts`)
+  )
+}
+
+function fileToScope(
+  ctx: ScriptCompileContext,
+  filename: string,
+  fs: NonNullable<SFCScriptCompileOptions['fs']>
+): TypeScope {
+  // TODO cache
+  const source = fs.readFile(filename)
+  const body = parseFile(ctx, filename, source)
+  const scope: TypeScope = {
+    filename,
+    source,
+    types: Object.create(null),
+    exportedTypes: Object.create(null),
+    imports: recordImports(body)
+  }
+  recordTypes(body, scope)
+  return scope
+}
+
+function parseFile(
+  ctx: ScriptCompileContext,
+  filename: string,
+  content: string
+): Statement[] {
+  const ext = path.extname(filename)
+  if (ext === '.ts' || ext === '.tsx') {
+    return babelParse(content, {
+      plugins: resolveParserPlugins(
+        ext.slice(1),
+        ctx.options.babelParserPlugins
+      ),
+      sourceType: 'module'
+    }).program.body
+  } else if (ext === '.vue') {
+    const {
+      descriptor: { script, scriptSetup }
+    } = parse(content)
+    const scriptContent = (script?.content || '') + (scriptSetup?.content || '')
+    const lang = script?.lang || scriptSetup?.lang
+    return babelParse(scriptContent, {
+      plugins: resolveParserPlugins(lang!, ctx.options.babelParserPlugins),
+      sourceType: 'module'
+    }).program.body
+  }
+  return []
+}
+
 function ctxToScope(ctx: ScriptCompileContext): TypeScope {
   if (ctx.scope) {
     return ctx.scope
   }
 
+  const scope: TypeScope = {
+    filename: ctx.descriptor.filename,
+    source: ctx.descriptor.source,
+    imports: Object.create(ctx.userImports),
+    types: Object.create(null),
+    exportedTypes: Object.create(null)
+  }
+
   const body = ctx.scriptAst
     ? [...ctx.scriptAst.body, ...ctx.scriptSetupAst!.body]
     : ctx.scriptSetupAst!.body
 
-  return (ctx.scope = {
-    filename: ctx.descriptor.filename,
-    imports: ctx.userImports,
-    types: recordTypes(body)
-  })
+  recordTypes(body, scope)
+
+  return (ctx.scope = scope)
 }
 
 function moduleDeclToScope(
@@ -430,27 +588,56 @@ function moduleDeclToScope(
   if (node._resolvedChildScope) {
     return node._resolvedChildScope
   }
-  const types: TypeScope['types'] = Object.create(parent.types)
   const scope: TypeScope = {
-    filename: parent.filename,
-    imports: Object.create(parent.imports),
-    types: recordTypes((node.body as TSModuleBlock).body, types),
-    parent
-  }
-  for (const key of Object.keys(types)) {
-    types[key]._ownerScope = scope
+    ...parent,
+    types: Object.create(parent.types),
+    imports: Object.create(parent.imports)
   }
+  recordTypes((node.body as TSModuleBlock).body, scope)
   return (node._resolvedChildScope = scope)
 }
 
-function recordTypes(
-  body: Statement[],
-  types: Record<string, Node> = Object.create(null)
-) {
-  for (const s of body) {
-    recordType(s, types)
+function recordTypes(body: Statement[], scope: TypeScope) {
+  const { types, exportedTypes, imports } = scope
+  for (const stmt of body) {
+    recordType(stmt, types)
+  }
+  for (const stmt of body) {
+    if (stmt.type === 'ExportNamedDeclaration') {
+      if (stmt.declaration) {
+        recordType(stmt.declaration, types)
+        recordType(stmt.declaration, exportedTypes)
+      } else {
+        for (const spec of stmt.specifiers) {
+          if (spec.type === 'ExportSpecifier') {
+            const local = spec.local.name
+            const exported = getId(spec.exported)
+            if (stmt.source) {
+              // re-export, register an import + export as a type reference
+              imports[local] = {
+                source: stmt.source.value,
+                imported: local
+              }
+              exportedTypes[exported] = {
+                type: 'TSTypeReference',
+                typeName: {
+                  type: 'Identifier',
+                  name: local
+                },
+                _ownerScope: scope
+              }
+            } else if (types[local]) {
+              // exporting local defined type
+              exportedTypes[exported] = types[local]
+            }
+          }
+        }
+      }
+    }
+  }
+  for (const key of Object.keys(types)) {
+    types[key]._ownerScope = scope
   }
-  return types
 }
 
 function recordType(node: Node, types: Record<string, Node>) {
@@ -465,12 +652,6 @@ function recordType(node: Node, types: Record<string, Node>) {
     case 'TSTypeAliasDeclaration':
       types[node.id.name] = node.typeAnnotation
       break
-    case 'ExportNamedDeclaration': {
-      if (node.declaration) {
-        recordType(node.declaration, types)
-      }
-      break
-    }
     case 'VariableDeclaration': {
       if (node.declare) {
         for (const decl of node.declarations) {
@@ -486,9 +667,29 @@ function recordType(node: Node, types: Record<string, Node>) {
   }
 }
 
+export function recordImports(body: Statement[]) {
+  const imports: TypeScope['imports'] = Object.create(null)
+  for (const s of body) {
+    recordImport(s, imports)
+  }
+  return imports
+}
+
+function recordImport(node: Node, imports: TypeScope['imports']) {
+  if (node.type !== 'ImportDeclaration') {
+    return
+  }
+  for (const s of node.specifiers) {
+    imports[s.local.name] = {
+      imported: getImportedName(s),
+      source: node.source.value
+    }
+  }
+}
+
 export function inferRuntimeType(
   ctx: ScriptCompileContext,
-  node: Node,
+  node: Node & WithScope,
   scope = node._ownerScope || ctxToScope(ctx)
 ): string[] {
   switch (node.type) {
index 11bc011820e5e44de756dd3be4438f9ca6a72a19..780c780e2ccb125d88ae52d76914057d4c0fdd4d 100644 (file)
@@ -1,4 +1,13 @@
-import { CallExpression, Node } from '@babel/types'
+import {
+  CallExpression,
+  Expression,
+  Identifier,
+  ImportDefaultSpecifier,
+  ImportNamespaceSpecifier,
+  ImportSpecifier,
+  Node,
+  StringLiteral
+} from '@babel/types'
 import { TS_NODE_TYPES } from '@vue/compiler-dom'
 
 export const UNKNOWN_TYPE = 'Unknown'
@@ -48,3 +57,24 @@ export function isCallOf(
 export function toRuntimeTypeString(types: string[]) {
   return types.length > 1 ? `[${types.join(', ')}]` : types[0]
 }
+
+export function getImportedName(
+  specifier: ImportSpecifier | ImportDefaultSpecifier | ImportNamespaceSpecifier
+) {
+  if (specifier.type === 'ImportSpecifier')
+    return specifier.imported.type === 'Identifier'
+      ? specifier.imported.name
+      : specifier.imported.value
+  else if (specifier.type === 'ImportNamespaceSpecifier') return '*'
+  return 'default'
+}
+
+export function getId(node: Identifier | StringLiteral): string
+export function getId(node: Expression): string | null
+export function getId(node: Expression) {
+  return node.type === 'Identifier'
+    ? node.name
+    : node.type === 'StringLiteral'
+    ? node.value
+    : null
+}