feat(compiler-sfc): support relative imported types in macros

This commit is contained in:
Evan You 2023-04-13 20:49:16 +08:00
parent 1c06fe1d02
commit 8aa4ea81d6
6 changed files with 473 additions and 153 deletions

View File

@ -6,10 +6,7 @@ import type {
Function, Function,
ObjectProperty, ObjectProperty,
BlockStatement, BlockStatement,
Program, Program
ImportDefaultSpecifier,
ImportNamespaceSpecifier,
ImportSpecifier
} from '@babel/types' } from '@babel/types'
import { walk } from 'estree-walker' import { walk } from 'estree-walker'
@ -246,17 +243,6 @@ export const isStaticProperty = (node: Node): node is ObjectProperty =>
export const isStaticPropertyKey = (node: Node, parent: Node) => export const isStaticPropertyKey = (node: Node, parent: Node) =>
isStaticProperty(parent) && parent.key === 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 * 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) * To avoid runtime dependency on @babel/types (which includes process references)

View File

@ -3,6 +3,7 @@ import { parse } from '../../src'
import { ScriptCompileContext } from '../../src/script/context' import { ScriptCompileContext } from '../../src/script/context'
import { import {
inferRuntimeType, inferRuntimeType,
recordImports,
resolveTypeElements resolveTypeElements
} from '../../src/script/resolveType' } 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', () => { describe('errors', () => {
test('error on computed keys', () => { test('error on computed keys', () => {
expect(() => resolve(`type Target = { [Foo]: string }`)).toThrow( expect(() => resolve(`type Target = { [Foo]: string }`)).toThrow(
@ -255,9 +335,26 @@ describe('resolveType', () => {
}) })
}) })
function resolve(code: string) { function resolve(code: string, files: Record<string, string> = {}) {
const { descriptor } = parse(`<script setup lang="ts">${code}</script>`) const { descriptor } = parse(`<script setup lang="ts">${code}</script>`, {
const ctx = new ScriptCompileContext(descriptor, { id: 'test' }) 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( const targetDecl = ctx.scriptSetupAst!.body.find(
s => s.type === 'TSTypeAliasDeclaration' && s.id.name === 'Target' s => s.type === 'TSTypeAliasDeclaration' && s.id.name === 'Target'
) as TSTypeAliasDeclaration ) as TSTypeAliasDeclaration

View File

@ -2,8 +2,7 @@ import {
BindingTypes, BindingTypes,
UNREF, UNREF,
isFunctionType, isFunctionType,
walkIdentifiers, walkIdentifiers
getImportedName
} from '@vue/compiler-dom' } from '@vue/compiler-dom'
import { DEFAULT_FILENAME, SFCDescriptor, SFCScriptBlock } from './parse' import { DEFAULT_FILENAME, SFCDescriptor, SFCScriptBlock } from './parse'
import { parse as _parse, ParserPlugin } from '@babel/parser' 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 { DEFINE_OPTIONS, processDefineOptions } from './script/defineOptions'
import { processDefineSlots } from './script/defineSlots' import { processDefineSlots } from './script/defineSlots'
import { DEFINE_MODEL, processDefineModel } from './script/defineModel' 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 { analyzeScriptBindings } from './script/analyzeScriptBindings'
import { isImportUsed } from './script/importUsageCheck' import { isImportUsed } from './script/importUsageCheck'
import { processAwait } from './script/topLevelAwait' import { processAwait } from './script/topLevelAwait'
@ -106,6 +110,13 @@ export interface SFCScriptCompileOptions {
* (**Experimental**) Enable macro `defineModel` * (**Experimental**) Enable macro `defineModel`
*/ */
defineModel?: boolean defineModel?: boolean
/**
*
*/
fs?: {
fileExists(file: string): boolean
readFile(file: string): string
}
} }
export interface ImportBinding { export interface ImportBinding {

View File

@ -1,13 +1,13 @@
import { Node, ObjectPattern, Program } from '@babel/types' import { Node, ObjectPattern, Program } from '@babel/types'
import { SFCDescriptor } from '../parse' import { SFCDescriptor } from '../parse'
import { generateCodeFrame } from '@vue/shared' 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 { ImportBinding, SFCScriptCompileOptions } from '../compileScript'
import { PropsDestructureBindings } from './defineProps' import { PropsDestructureBindings } from './defineProps'
import { ModelDecl } from './defineModel' import { ModelDecl } from './defineModel'
import { BindingMetadata } from '../../../compiler-core/src' import { BindingMetadata } from '../../../compiler-core/src'
import MagicString from 'magic-string' import MagicString from 'magic-string'
import { TypeScope } from './resolveType' import { TypeScope, WithScope } from './resolveType'
export class ScriptCompileContext { export class ScriptCompileContext {
isJS: boolean isJS: boolean
@ -83,31 +83,17 @@ export class ScriptCompileContext {
scriptSetupLang === 'tsx' scriptSetupLang === 'tsx'
// resolve parser plugins // resolve parser plugins
const plugins: ParserPlugin[] = [] const plugins: ParserPlugin[] = resolveParserPlugins(
if (!this.isTS || scriptLang === 'tsx' || scriptSetupLang === 'tsx') { (scriptLang || scriptSetupLang)!,
plugins.push('jsx') options.babelParserPlugins
} 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')
}
}
function parse( function parse(input: string, offset: number): Program {
input: string,
options: ParserOptions,
offset: number
): Program {
try { try {
return babelParse(input, options).program return babelParse(input, {
plugins,
sourceType: 'module'
}).program
} catch (e: any) { } catch (e: any) {
e.message = `[@vue/compiler-sfc] ${e.message}\n\n${ e.message = `[@vue/compiler-sfc] ${e.message}\n\n${
descriptor.filename descriptor.filename
@ -124,23 +110,12 @@ export class ScriptCompileContext {
this.descriptor.script && this.descriptor.script &&
parse( parse(
this.descriptor.script.content, this.descriptor.script.content,
{
plugins,
sourceType: 'module'
},
this.descriptor.script.loc.start.offset this.descriptor.script.loc.start.offset
) )
this.scriptSetupAst = this.scriptSetupAst =
this.descriptor.scriptSetup && this.descriptor.scriptSetup &&
parse( parse(this.descriptor.scriptSetup!.content, this.startOffset!)
this.descriptor.scriptSetup!.content,
{
plugins: [...plugins, 'topLevelAwait'],
sourceType: 'module'
},
this.startOffset!
)
} }
getString(node: Node, scriptSetup = true): string { getString(node: Node, scriptSetup = true): string {
@ -150,19 +125,39 @@ export class ScriptCompileContext {
return block.content.slice(node.start!, node.end!) return block.content.slice(node.start!, node.end!)
} }
error( error(msg: string, node: Node & WithScope, scope?: TypeScope): never {
msg: string,
node: Node,
end: number = node.end! + this.startOffset!
): never {
throw new Error( throw new Error(
`[@vue/compiler-sfc] ${msg}\n\n${ `[@vue/compiler-sfc] ${msg}\n\n${
this.descriptor.filename this.descriptor.filename
}\n${generateCodeFrame( }\n${generateCodeFrame(
this.descriptor.source, this.descriptor.source,
node.start! + this.startOffset!, 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
}

View File

@ -1,11 +1,13 @@
import { import {
Expression,
Identifier, Identifier,
Node as _Node, Node,
Statement, Statement,
TSCallSignatureDeclaration, TSCallSignatureDeclaration,
TSEnumDeclaration, TSEnumDeclaration,
TSExpressionWithTypeArguments, TSExpressionWithTypeArguments,
TSFunctionType, TSFunctionType,
TSInterfaceDeclaration,
TSMappedType, TSMappedType,
TSMethodSignature, TSMethodSignature,
TSModuleBlock, TSModuleBlock,
@ -18,81 +20,108 @@ import {
TSTypeReference, TSTypeReference,
TemplateLiteral TemplateLiteral
} from '@babel/types' } from '@babel/types'
import { UNKNOWN_TYPE } from './utils' import { UNKNOWN_TYPE, getId, getImportedName } from './utils'
import { ScriptCompileContext } from './context' import { ScriptCompileContext, resolveParserPlugins } from './context'
import { ImportBinding } from '../compileScript' import { ImportBinding, SFCScriptCompileOptions } from '../compileScript'
import { TSInterfaceDeclaration } from '@babel/types'
import { capitalize, hasOwn } from '@vue/shared' 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 { export interface TypeScope {
filename: string filename: string
imports: Record<string, ImportBinding> source: string
types: Record<string, Node> imports: Record<string, Import>
parent?: TypeScope 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 _ownerScope?: TypeScope
} }
interface ResolvedElements { interface ResolvedElements {
props: Record<string, (TSPropertySignature | TSMethodSignature) & WithScope> props: Record<
string,
(TSPropertySignature | TSMethodSignature) & {
// resolved props always has ownerScope attached
_ownerScope: TypeScope
}
>
calls?: (TSCallSignatureDeclaration | TSFunctionType)[] calls?: (TSCallSignatureDeclaration | TSFunctionType)[]
} }
type Node = _Node &
WithScope & {
_resolvedElements?: ResolvedElements
}
/** /**
* Resolve arbitrary type node to a list of type elements that can be then * Resolve arbitrary type node to a list of type elements that can be then
* mapped to runtime props or emits. * mapped to runtime props or emits.
*/ */
export function resolveTypeElements( export function resolveTypeElements(
ctx: ScriptCompileContext, ctx: ScriptCompileContext,
node: Node node: Node & WithScope & { _resolvedElements?: ResolvedElements },
scope?: TypeScope
): ResolvedElements { ): ResolvedElements {
if (node._resolvedElements) { if (node._resolvedElements) {
return node._resolvedElements return node._resolvedElements
} }
return (node._resolvedElements = innerResolveTypeElements(ctx, node)) return (node._resolvedElements = innerResolveTypeElements(
ctx,
node,
node._ownerScope || scope || ctxToScope(ctx)
))
} }
function innerResolveTypeElements( function innerResolveTypeElements(
ctx: ScriptCompileContext, ctx: ScriptCompileContext,
node: Node node: Node,
scope: TypeScope
): ResolvedElements { ): ResolvedElements {
switch (node.type) { switch (node.type) {
case 'TSTypeLiteral': case 'TSTypeLiteral':
return typeElementsToMap(ctx, node.members, node._ownerScope) return typeElementsToMap(ctx, node.members, scope)
case 'TSInterfaceDeclaration': case 'TSInterfaceDeclaration':
return resolveInterfaceMembers(ctx, node) return resolveInterfaceMembers(ctx, node, scope)
case 'TSTypeAliasDeclaration': case 'TSTypeAliasDeclaration':
case 'TSParenthesizedType': case 'TSParenthesizedType':
return resolveTypeElements(ctx, node.typeAnnotation) return resolveTypeElements(ctx, node.typeAnnotation, scope)
case 'TSFunctionType': { case 'TSFunctionType': {
return { props: {}, calls: [node] } return { props: {}, calls: [node] }
} }
case 'TSUnionType': case 'TSUnionType':
case 'TSIntersectionType': case 'TSIntersectionType':
return mergeElements( return mergeElements(
node.types.map(t => resolveTypeElements(ctx, t)), node.types.map(t => resolveTypeElements(ctx, t, scope)),
node.type node.type
) )
case 'TSMappedType': case 'TSMappedType':
return resolveMappedType(ctx, node) return resolveMappedType(ctx, node, scope)
case 'TSIndexedAccessType': { case 'TSIndexedAccessType': {
if ( if (
node.indexType.type === 'TSLiteralType' && node.indexType.type === 'TSLiteralType' &&
node.indexType.literal.type === 'StringLiteral' 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 key = node.indexType.literal.value
const targetType = resolved.props[key].typeAnnotation const targetType = resolved.props[key].typeAnnotation
if (targetType) { if (targetType) {
return resolveTypeElements(ctx, targetType.typeAnnotation) return resolveTypeElements(
ctx,
targetType.typeAnnotation,
resolved.props[key]._ownerScope
)
} else { } else {
break break
} }
@ -105,9 +134,9 @@ function innerResolveTypeElements(
} }
case 'TSExpressionWithTypeArguments': // referenced by interface extends case 'TSExpressionWithTypeArguments': // referenced by interface extends
case 'TSTypeReference': { case 'TSTypeReference': {
const resolved = resolveTypeReference(ctx, node) const resolved = resolveTypeReference(ctx, node, scope)
if (resolved) { if (resolved) {
return resolveTypeElements(ctx, resolved) return resolveTypeElements(ctx, resolved, resolved._ownerScope)
} else { } else {
const typeName = getReferenceName(node) const typeName = getReferenceName(node)
if ( if (
@ -118,7 +147,7 @@ function innerResolveTypeElements(
return resolveBuiltin(ctx, node, typeName as any) return resolveBuiltin(ctx, node, typeName as any)
} }
ctx.error( 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 node
) )
} }
@ -135,18 +164,13 @@ function typeElementsToMap(
const res: ResolvedElements = { props: {} } const res: ResolvedElements = { props: {} }
for (const e of elements) { for (const e of elements) {
if (e.type === 'TSPropertySignature' || e.type === 'TSMethodSignature') { if (e.type === 'TSPropertySignature' || e.type === 'TSMethodSignature') {
;(e as Node)._ownerScope = scope ;(e as WithScope)._ownerScope = scope
const name = const name = getId(e.key)
e.key.type === 'Identifier'
? e.key.name
: e.key.type === 'StringLiteral'
? e.key.value
: null
if (name && !e.computed) { if (name && !e.computed) {
res.props[name] = e res.props[name] = e as ResolvedElements['props'][string]
} else if (e.key.type === 'TemplateLiteral') { } else if (e.key.type === 'TemplateLiteral') {
for (const key of resolveTemplateKeys(ctx, e.key)) { for (const key of resolveTemplateKeys(ctx, e.key)) {
res.props[key] = e res.props[key] = e as ResolvedElements['props'][string]
} }
} else { } else {
ctx.error( ctx.error(
@ -172,11 +196,15 @@ function mergeElements(
if (!hasOwn(baseProps, key)) { if (!hasOwn(baseProps, key)) {
baseProps[key] = props[key] baseProps[key] = props[key]
} else { } else {
baseProps[key] = createProperty(baseProps[key].key, { baseProps[key] = createProperty(
type, baseProps[key].key,
// @ts-ignore {
types: [baseProps[key], props[key]] type,
}) // @ts-ignore
types: [baseProps[key], props[key]]
},
baseProps[key]._ownerScope
)
} }
} }
if (calls) { if (calls) {
@ -188,8 +216,9 @@ function mergeElements(
function createProperty( function createProperty(
key: Expression, key: Expression,
typeAnnotation: TSType typeAnnotation: TSType,
): TSPropertySignature { scope: TypeScope
): TSPropertySignature & { _ownerScope: TypeScope } {
return { return {
type: 'TSPropertySignature', type: 'TSPropertySignature',
key, key,
@ -197,18 +226,20 @@ function createProperty(
typeAnnotation: { typeAnnotation: {
type: 'TSTypeAnnotation', type: 'TSTypeAnnotation',
typeAnnotation typeAnnotation
} },
_ownerScope: scope
} }
} }
function resolveInterfaceMembers( function resolveInterfaceMembers(
ctx: ScriptCompileContext, ctx: ScriptCompileContext,
node: TSInterfaceDeclaration & WithScope node: TSInterfaceDeclaration & WithScope,
scope: TypeScope
): ResolvedElements { ): ResolvedElements {
const base = typeElementsToMap(ctx, node.body.body, node._ownerScope) const base = typeElementsToMap(ctx, node.body.body, node._ownerScope)
if (node.extends) { if (node.extends) {
for (const ext of node.extends) { for (const ext of node.extends) {
const { props } = resolveTypeElements(ctx, ext) const { props } = resolveTypeElements(ctx, ext, scope)
for (const key in props) { for (const key in props) {
if (!hasOwn(base.props, key)) { if (!hasOwn(base.props, key)) {
base.props[key] = props[key] base.props[key] = props[key]
@ -221,7 +252,8 @@ function resolveInterfaceMembers(
function resolveMappedType( function resolveMappedType(
ctx: ScriptCompileContext, ctx: ScriptCompileContext,
node: TSMappedType node: TSMappedType,
scope: TypeScope
): ResolvedElements { ): ResolvedElements {
const res: ResolvedElements = { props: {} } const res: ResolvedElements = { props: {} }
if (!node.typeParameter.constraint) { if (!node.typeParameter.constraint) {
@ -234,7 +266,8 @@ function resolveMappedType(
type: 'Identifier', type: 'Identifier',
name: key name: key
}, },
node.typeAnnotation! node.typeAnnotation!,
scope
) )
} }
return res return res
@ -357,32 +390,52 @@ function resolveTypeReference(
node: (TSTypeReference | TSExpressionWithTypeArguments) & { node: (TSTypeReference | TSExpressionWithTypeArguments) & {
_resolvedReference?: Node _resolvedReference?: Node
}, },
scope = ctxToScope(ctx) scope?: TypeScope,
): Node | undefined { name?: string,
onlyExported = false
): (Node & WithScope) | undefined {
if (node._resolvedReference) { if (node._resolvedReference) {
return node._resolvedReference return node._resolvedReference
} }
const name = getReferenceName(node) return (node._resolvedReference = innerResolveTypeReference(
return (node._resolvedReference = innerResolveTypeReference(scope, name)) ctx,
scope || ctxToScope(ctx),
name || getReferenceName(node),
node,
onlyExported
))
} }
function innerResolveTypeReference( function innerResolveTypeReference(
ctx: ScriptCompileContext,
scope: TypeScope, scope: TypeScope,
name: string | string[] name: string | string[],
node: TSTypeReference | TSExpressionWithTypeArguments,
onlyExported: boolean
): Node | undefined { ): Node | undefined {
if (typeof name === 'string') { if (typeof name === 'string') {
if (scope.imports[name]) { if (scope.imports[name]) {
// TODO external import return resolveTypeFromImport(ctx, scope, scope.imports[name], node)
} else if (scope.types[name]) { } else {
return scope.types[name] const types = onlyExported ? scope.exportedTypes : scope.types
return types[name]
} }
} else { } else {
const ns = innerResolveTypeReference(scope, name[0]) const ns = innerResolveTypeReference(
ctx,
scope,
name[0],
node,
onlyExported
)
if (ns && ns.type === 'TSModuleDeclaration') { if (ns && ns.type === 'TSModuleDeclaration') {
const childScope = moduleDeclToScope(ns, scope) const childScope = moduleDeclToScope(ns, scope)
return innerResolveTypeReference( return innerResolveTypeReference(
ctx,
childScope, 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 { function ctxToScope(ctx: ScriptCompileContext): TypeScope {
if (ctx.scope) { if (ctx.scope) {
return 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 const body = ctx.scriptAst
? [...ctx.scriptAst.body, ...ctx.scriptSetupAst!.body] ? [...ctx.scriptAst.body, ...ctx.scriptSetupAst!.body]
: ctx.scriptSetupAst!.body : ctx.scriptSetupAst!.body
return (ctx.scope = { recordTypes(body, scope)
filename: ctx.descriptor.filename,
imports: ctx.userImports, return (ctx.scope = scope)
types: recordTypes(body)
})
} }
function moduleDeclToScope( function moduleDeclToScope(
@ -430,27 +588,56 @@ function moduleDeclToScope(
if (node._resolvedChildScope) { if (node._resolvedChildScope) {
return node._resolvedChildScope return node._resolvedChildScope
} }
const types: TypeScope['types'] = Object.create(parent.types)
const scope: TypeScope = { const scope: TypeScope = {
filename: parent.filename, ...parent,
imports: Object.create(parent.imports), types: Object.create(parent.types),
types: recordTypes((node.body as TSModuleBlock).body, types), imports: Object.create(parent.imports)
parent }
recordTypes((node.body as TSModuleBlock).body, scope)
return (node._resolvedChildScope = scope)
}
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)) { for (const key of Object.keys(types)) {
types[key]._ownerScope = scope types[key]._ownerScope = scope
} }
return (node._resolvedChildScope = scope)
}
function recordTypes(
body: Statement[],
types: Record<string, Node> = Object.create(null)
) {
for (const s of body) {
recordType(s, types)
}
return types
} }
function recordType(node: Node, types: Record<string, Node>) { function recordType(node: Node, types: Record<string, Node>) {
@ -465,12 +652,6 @@ function recordType(node: Node, types: Record<string, Node>) {
case 'TSTypeAliasDeclaration': case 'TSTypeAliasDeclaration':
types[node.id.name] = node.typeAnnotation types[node.id.name] = node.typeAnnotation
break break
case 'ExportNamedDeclaration': {
if (node.declaration) {
recordType(node.declaration, types)
}
break
}
case 'VariableDeclaration': { case 'VariableDeclaration': {
if (node.declare) { if (node.declare) {
for (const decl of node.declarations) { 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( export function inferRuntimeType(
ctx: ScriptCompileContext, ctx: ScriptCompileContext,
node: Node, node: Node & WithScope,
scope = node._ownerScope || ctxToScope(ctx) scope = node._ownerScope || ctxToScope(ctx)
): string[] { ): string[] {
switch (node.type) { switch (node.type) {

View 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' import { TS_NODE_TYPES } from '@vue/compiler-dom'
export const UNKNOWN_TYPE = 'Unknown' export const UNKNOWN_TYPE = 'Unknown'
@ -48,3 +57,24 @@ export function isCallOf(
export function toRuntimeTypeString(types: string[]) { export function toRuntimeTypeString(types: string[]) {
return types.length > 1 ? `[${types.join(', ')}]` : types[0] 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
}