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,
|
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)
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -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) {
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue