mirror of https://github.com/vuejs/core.git
feat(compiler-sfc): support specifying global types for sfc macros
ref: https://github.com/vuejs/core/pull/8083#issuecomment-1508468713
This commit is contained in:
parent
f22e32e365
commit
4e028b9669
|
@ -1,5 +1,5 @@
|
|||
import { Identifier } from '@babel/types'
|
||||
import { parse } from '../../src'
|
||||
import { SFCScriptCompileOptions, parse } from '../../src'
|
||||
import { ScriptCompileContext } from '../../src/script/context'
|
||||
import {
|
||||
inferRuntimeType,
|
||||
|
@ -410,6 +410,32 @@ describe('resolveType', () => {
|
|||
'/pp.ts'
|
||||
])
|
||||
})
|
||||
|
||||
test('global types', () => {
|
||||
const files = {
|
||||
// ambient
|
||||
'/app.d.ts':
|
||||
'declare namespace App { interface User { name: string } }',
|
||||
// module - should only respect the declare global block
|
||||
'/global.d.ts': `
|
||||
declare type PP = { bar: number }
|
||||
declare global {
|
||||
type PP = { bar: string }
|
||||
}
|
||||
export {}
|
||||
`
|
||||
}
|
||||
|
||||
const { props, deps } = resolve(`defineProps<App.User & PP>()`, files, {
|
||||
globalTypeFiles: Object.keys(files)
|
||||
})
|
||||
|
||||
expect(props).toStrictEqual({
|
||||
name: ['String'],
|
||||
bar: ['String']
|
||||
})
|
||||
expect(deps && [...deps]).toStrictEqual(Object.keys(files))
|
||||
})
|
||||
})
|
||||
|
||||
describe('errors', () => {
|
||||
|
@ -444,7 +470,11 @@ describe('resolveType', () => {
|
|||
})
|
||||
})
|
||||
|
||||
function resolve(code: string, files: Record<string, string> = {}) {
|
||||
function resolve(
|
||||
code: string,
|
||||
files: Record<string, string> = {},
|
||||
options?: Partial<SFCScriptCompileOptions>
|
||||
) {
|
||||
const { descriptor } = parse(`<script setup lang="ts">\n${code}\n</script>`, {
|
||||
filename: '/Test.vue'
|
||||
})
|
||||
|
@ -457,7 +487,8 @@ function resolve(code: string, files: Record<string, string> = {}) {
|
|||
readFile(file) {
|
||||
return files[file]
|
||||
}
|
||||
}
|
||||
},
|
||||
...options
|
||||
})
|
||||
|
||||
for (const file in files) {
|
||||
|
|
|
@ -72,6 +72,11 @@ export interface SFCScriptCompileOptions {
|
|||
* https://babeljs.io/docs/en/babel-parser#plugins
|
||||
*/
|
||||
babelParserPlugins?: ParserPlugin[]
|
||||
/**
|
||||
* A list of files to parse for global types to be made available for type
|
||||
* resolving in SFC macros. The list must be fully resolved file system paths.
|
||||
*/
|
||||
globalTypeFiles?: string[]
|
||||
/**
|
||||
* Compile the template and inline the resulting render function
|
||||
* directly inside setup().
|
||||
|
|
|
@ -24,6 +24,7 @@ export class ScriptCompileContext {
|
|||
|
||||
// import / type analysis
|
||||
scope?: TypeScope
|
||||
globalScopes?: TypeScope[]
|
||||
userImports: Record<string, ImportBinding> = Object.create(null)
|
||||
|
||||
// macros presence check
|
||||
|
@ -101,7 +102,7 @@ export class ScriptCompileContext {
|
|||
sourceType: 'module'
|
||||
}).program
|
||||
} catch (e: any) {
|
||||
e.message = `[@vue/compiler-sfc] ${e.message}\n\n${
|
||||
e.message = `[vue/compiler-sfc] ${e.message}\n\n${
|
||||
descriptor.filename
|
||||
}\n${generateCodeFrame(
|
||||
descriptor.source,
|
||||
|
@ -113,15 +114,12 @@ export class ScriptCompileContext {
|
|||
}
|
||||
|
||||
this.scriptAst =
|
||||
this.descriptor.script &&
|
||||
parse(
|
||||
this.descriptor.script.content,
|
||||
this.descriptor.script.loc.start.offset
|
||||
)
|
||||
descriptor.script &&
|
||||
parse(descriptor.script.content, descriptor.script.loc.start.offset)
|
||||
|
||||
this.scriptSetupAst =
|
||||
this.descriptor.scriptSetup &&
|
||||
parse(this.descriptor.scriptSetup!.content, this.startOffset!)
|
||||
descriptor.scriptSetup &&
|
||||
parse(descriptor.scriptSetup!.content, this.startOffset!)
|
||||
}
|
||||
|
||||
getString(node: Node, scriptSetup = true): string {
|
||||
|
|
|
@ -56,7 +56,7 @@ export type SimpleTypeResolveContext = Pick<
|
|||
// required
|
||||
'source' | 'filename' | 'error' | 'options'
|
||||
> &
|
||||
Partial<Pick<ScriptCompileContext, 'scope' | 'deps'>> & {
|
||||
Partial<Pick<ScriptCompileContext, 'scope' | 'globalScopes' | 'deps'>> & {
|
||||
ast: Statement[]
|
||||
}
|
||||
|
||||
|
@ -64,25 +64,18 @@ export type TypeResolveContext = ScriptCompileContext | SimpleTypeResolveContext
|
|||
|
||||
type Import = Pick<ImportBinding, 'source' | 'imported'>
|
||||
|
||||
type ScopeTypeNode = Node & {
|
||||
// scope types always has ownerScope attached
|
||||
_ownerScope: TypeScope
|
||||
}
|
||||
|
||||
export interface TypeScope {
|
||||
filename: string
|
||||
source: string
|
||||
offset: number
|
||||
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
|
||||
}
|
||||
>
|
||||
types: Record<string, ScopeTypeNode>
|
||||
exportedTypes: Record<string, ScopeTypeNode>
|
||||
}
|
||||
|
||||
export interface WithScope {
|
||||
|
@ -492,12 +485,12 @@ function resolveBuiltin(
|
|||
function resolveTypeReference(
|
||||
ctx: TypeResolveContext,
|
||||
node: (TSTypeReference | TSExpressionWithTypeArguments) & {
|
||||
_resolvedReference?: Node
|
||||
_resolvedReference?: ScopeTypeNode
|
||||
},
|
||||
scope?: TypeScope,
|
||||
name?: string,
|
||||
onlyExported = false
|
||||
): (Node & WithScope) | undefined {
|
||||
): ScopeTypeNode | undefined {
|
||||
if (node._resolvedReference) {
|
||||
return node._resolvedReference
|
||||
}
|
||||
|
@ -516,13 +509,26 @@ function innerResolveTypeReference(
|
|||
name: string | string[],
|
||||
node: TSTypeReference | TSExpressionWithTypeArguments,
|
||||
onlyExported: boolean
|
||||
): Node | undefined {
|
||||
): ScopeTypeNode | undefined {
|
||||
if (typeof name === 'string') {
|
||||
if (scope.imports[name]) {
|
||||
return resolveTypeFromImport(ctx, node, name, scope)
|
||||
} else {
|
||||
const types = onlyExported ? scope.exportedTypes : scope.types
|
||||
return types[name]
|
||||
if (types[name]) {
|
||||
return types[name]
|
||||
} else {
|
||||
// fallback to global
|
||||
const globalScopes = resolveGlobalScope(ctx)
|
||||
if (globalScopes) {
|
||||
for (const s of globalScopes) {
|
||||
if (s.types[name]) {
|
||||
;(ctx.deps || (ctx.deps = new Set())).add(s.filename)
|
||||
return s.types[name]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
const ns = innerResolveTypeReference(
|
||||
|
@ -539,7 +545,7 @@ function innerResolveTypeReference(
|
|||
childScope,
|
||||
name.length > 2 ? name.slice(1) : name[name.length - 1],
|
||||
node,
|
||||
true
|
||||
!ns.declare
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -564,6 +570,19 @@ function qualifiedNameToPath(node: Identifier | TSQualifiedName): string[] {
|
|||
}
|
||||
}
|
||||
|
||||
function resolveGlobalScope(ctx: TypeResolveContext): TypeScope[] | undefined {
|
||||
if (ctx.options.globalTypeFiles) {
|
||||
const fs: FS = ctx.options.fs || ts?.sys
|
||||
if (!fs) {
|
||||
throw new Error('[vue/compiler-sfc] globalTypeFiles requires fs access.')
|
||||
}
|
||||
return ctx.options.globalTypeFiles.map(file =>
|
||||
// TODO: differentiate ambient vs non-ambient module
|
||||
fileToScope(file, fs, ctx.options.babelParserPlugins, true)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
let ts: typeof TS
|
||||
|
||||
/**
|
||||
|
@ -580,7 +599,7 @@ function resolveTypeFromImport(
|
|||
node: TSTypeReference | TSExpressionWithTypeArguments,
|
||||
name: string,
|
||||
scope: TypeScope
|
||||
): Node | undefined {
|
||||
): ScopeTypeNode | undefined {
|
||||
const fs: FS = ctx.options.fs || ts?.sys
|
||||
if (!fs) {
|
||||
ctx.error(
|
||||
|
@ -629,7 +648,7 @@ function resolveTypeFromImport(
|
|||
return resolveTypeReference(
|
||||
ctx,
|
||||
node,
|
||||
fileToScope(ctx, resolved, fs),
|
||||
fileToScope(resolved, fs, ctx.options.babelParserPlugins),
|
||||
imported,
|
||||
true
|
||||
)
|
||||
|
@ -726,10 +745,11 @@ export function invalidateTypeCache(filename: string) {
|
|||
tsConfigCache.delete(filename)
|
||||
}
|
||||
|
||||
function fileToScope(
|
||||
ctx: TypeResolveContext,
|
||||
export function fileToScope(
|
||||
filename: string,
|
||||
fs: FS
|
||||
fs: FS,
|
||||
parserPlugins: SFCScriptCompileOptions['babelParserPlugins'],
|
||||
asGlobal = false
|
||||
): TypeScope {
|
||||
const cached = fileToScopeCache.get(filename)
|
||||
if (cached) {
|
||||
|
@ -737,33 +757,30 @@ function fileToScope(
|
|||
}
|
||||
|
||||
const source = fs.readFile(filename) || ''
|
||||
const body = parseFile(ctx, filename, source)
|
||||
const body = parseFile(filename, source, parserPlugins)
|
||||
const scope: TypeScope = {
|
||||
filename,
|
||||
source,
|
||||
offset: 0,
|
||||
imports: recordImports(body),
|
||||
types: Object.create(null),
|
||||
exportedTypes: Object.create(null),
|
||||
imports: recordImports(body)
|
||||
exportedTypes: Object.create(null)
|
||||
}
|
||||
recordTypes(body, scope)
|
||||
recordTypes(body, scope, asGlobal)
|
||||
|
||||
fileToScopeCache.set(filename, scope)
|
||||
return scope
|
||||
}
|
||||
|
||||
function parseFile(
|
||||
ctx: TypeResolveContext,
|
||||
filename: string,
|
||||
content: string
|
||||
content: string,
|
||||
parserPlugins?: SFCScriptCompileOptions['babelParserPlugins']
|
||||
): Statement[] {
|
||||
const ext = extname(filename)
|
||||
if (ext === '.ts' || ext === '.tsx') {
|
||||
return babelParse(content, {
|
||||
plugins: resolveParserPlugins(
|
||||
ext.slice(1),
|
||||
ctx.options.babelParserPlugins
|
||||
),
|
||||
plugins: resolveParserPlugins(ext.slice(1), parserPlugins),
|
||||
sourceType: 'module'
|
||||
}).program.body
|
||||
} else if (ext === '.vue') {
|
||||
|
@ -792,7 +809,7 @@ function parseFile(
|
|||
}
|
||||
const lang = script?.lang || scriptSetup?.lang
|
||||
return babelParse(scriptContent, {
|
||||
plugins: resolveParserPlugins(lang!, ctx.options.babelParserPlugins),
|
||||
plugins: resolveParserPlugins(lang!, parserPlugins),
|
||||
sourceType: 'module'
|
||||
}).program.body
|
||||
}
|
||||
|
@ -830,52 +847,71 @@ function ctxToScope(ctx: TypeResolveContext): TypeScope {
|
|||
|
||||
function moduleDeclToScope(
|
||||
node: TSModuleDeclaration & { _resolvedChildScope?: TypeScope },
|
||||
parent: TypeScope
|
||||
parentScope: TypeScope
|
||||
): TypeScope {
|
||||
if (node._resolvedChildScope) {
|
||||
return node._resolvedChildScope
|
||||
}
|
||||
const scope: TypeScope = {
|
||||
...parent,
|
||||
types: Object.create(parent.types),
|
||||
imports: Object.create(parent.imports)
|
||||
...parentScope,
|
||||
types: Object.create(parentScope.types),
|
||||
imports: Object.create(parentScope.imports)
|
||||
}
|
||||
recordTypes((node.body as TSModuleBlock).body, scope)
|
||||
return (node._resolvedChildScope = scope)
|
||||
}
|
||||
|
||||
function recordTypes(body: Statement[], scope: TypeScope) {
|
||||
const importExportRE = /^Import|^Export/
|
||||
|
||||
function recordTypes(body: Statement[], scope: TypeScope, asGlobal = false) {
|
||||
const { types, exportedTypes, imports } = scope
|
||||
const isAmbient = asGlobal
|
||||
? !body.some(s => importExportRE.test(s.type))
|
||||
: false
|
||||
for (const stmt of body) {
|
||||
recordType(stmt, types)
|
||||
if (asGlobal) {
|
||||
if (isAmbient) {
|
||||
if ((stmt as any).declare) {
|
||||
recordType(stmt, types)
|
||||
}
|
||||
} else if (stmt.type === 'TSModuleDeclaration' && stmt.global) {
|
||||
for (const s of (stmt.body as TSModuleBlock).body) {
|
||||
recordType(s, types)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
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
|
||||
if (!asGlobal) {
|
||||
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]
|
||||
}
|
||||
exportedTypes[exported] = {
|
||||
type: 'TSTypeReference',
|
||||
typeName: {
|
||||
type: 'Identifier',
|
||||
name: local
|
||||
},
|
||||
_ownerScope: scope
|
||||
}
|
||||
} else if (types[local]) {
|
||||
// exporting local defined type
|
||||
exportedTypes[exported] = types[local]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue