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
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
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 {
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
+}
import {
+ Expression,
Identifier,
- Node as _Node,
+ Node,
Statement,
TSCallSignatureDeclaration,
TSEnumDeclaration,
TSExpressionWithTypeArguments,
TSFunctionType,
+ TSInterfaceDeclaration,
TSMappedType,
TSMethodSignature,
TSModuleBlock,
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
}
}
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 (
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
)
}
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(
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) {
function createProperty(
key: Expression,
- typeAnnotation: TSType
-): TSPropertySignature {
+ typeAnnotation: TSType,
+ scope: TypeScope
+): TSPropertySignature & { _ownerScope: TypeScope } {
return {
type: 'TSPropertySignature',
key,
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]
function resolveMappedType(
ctx: ScriptCompileContext,
- node: TSMappedType
+ node: TSMappedType,
+ scope: TypeScope
): ResolvedElements {
const res: ResolvedElements = { props: {} }
if (!node.typeParameter.constraint) {
type: 'Identifier',
name: key
},
- node.typeAnnotation!
+ node.typeAnnotation!,
+ scope
)
}
return res
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
)
}
}
}
}
+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(
if (node._resolvedChildScope) {
return node._resolvedChildScope
}
- const types: TypeScope['types'] = Object.create(parent.types)
const scope: TypeScope = {
- filename: parent.filename,
- imports: Object.create(parent.imports),
- types: recordTypes((node.body as TSModuleBlock).body, types),
- parent
- }
- for (const key of Object.keys(types)) {
- types[key]._ownerScope = scope
+ ...parent,
+ types: Object.create(parent.types),
+ imports: Object.create(parent.imports)
}
+ recordTypes((node.body as TSModuleBlock).body, scope)
return (node._resolvedChildScope = scope)
}
-function recordTypes(
- body: Statement[],
- types: Record<string, Node> = Object.create(null)
-) {
- for (const s of body) {
- recordType(s, types)
+function recordTypes(body: Statement[], scope: TypeScope) {
+ const { types, exportedTypes, imports } = scope
+ for (const stmt of body) {
+ recordType(stmt, types)
+ }
+ for (const stmt of body) {
+ if (stmt.type === 'ExportNamedDeclaration') {
+ if (stmt.declaration) {
+ recordType(stmt.declaration, types)
+ recordType(stmt.declaration, exportedTypes)
+ } else {
+ for (const spec of stmt.specifiers) {
+ if (spec.type === 'ExportSpecifier') {
+ const local = spec.local.name
+ const exported = getId(spec.exported)
+ if (stmt.source) {
+ // re-export, register an import + export as a type reference
+ imports[local] = {
+ source: stmt.source.value,
+ imported: local
+ }
+ exportedTypes[exported] = {
+ type: 'TSTypeReference',
+ typeName: {
+ type: 'Identifier',
+ name: local
+ },
+ _ownerScope: scope
+ }
+ } else if (types[local]) {
+ // exporting local defined type
+ exportedTypes[exported] = types[local]
+ }
+ }
+ }
+ }
+ }
+ }
+ for (const key of Object.keys(types)) {
+ types[key]._ownerScope = scope
}
- return types
}
function recordType(node: Node, types: Record<string, Node>) {
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) {
}
}
+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) {