mirror of https://github.com/vuejs/core.git
feat(compiler-sfc): support relative imported types in macros
This commit is contained in:
parent
1c06fe1d02
commit
8aa4ea81d6
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
...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[], 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 (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>) {
|
||||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue