feat(compiler-sfc): support export * when resolving types

This commit is contained in:
Evan You 2023-04-20 15:56:24 +08:00
parent f17a82c769
commit 7c3ca3cc3e
2 changed files with 71 additions and 39 deletions

View File

@ -454,6 +454,24 @@ describe('resolveType', () => {
expect(deps && [...deps]).toStrictEqual(Object.keys(files)) expect(deps && [...deps]).toStrictEqual(Object.keys(files))
}) })
test('relative (chained, export *)', () => {
const files = {
'/foo.ts': `export * from './bar'`,
'/bar.ts': 'export type P = { bar: string }'
}
const { props, deps } = resolve(
`
import { P } from './foo'
defineProps<P>()
`,
files
)
expect(props).toStrictEqual({
bar: ['String']
})
expect(deps && [...deps]).toStrictEqual(Object.keys(files))
})
test('ts module resolve', () => { test('ts module resolve', () => {
const files = { const files = {
'/node_modules/foo/package.json': JSON.stringify({ '/node_modules/foo/package.json': JSON.stringify({
@ -563,10 +581,10 @@ describe('resolveType', () => {
) )
}) })
test('failed improt source resolve', () => { test('failed import source resolve', () => {
expect(() => expect(() =>
resolve(`import { X } from './foo'; defineProps<X>()`) resolve(`import { X } from './foo'; defineProps<X>()`)
).toThrow(`Failed to resolve import source "./foo" for type X`) ).toThrow(`Failed to resolve import source "./foo"`)
}) })
test('should not error on unresolved type when inferring runtime type', () => { test('should not error on unresolved type when inferring runtime type', () => {

View File

@ -542,7 +542,7 @@ function innerResolveTypeReference(
ns = ns._ns ns = ns._ns
} }
if (ns) { if (ns) {
const childScope = moduleDeclToScope(ns, ns._ownerScope || scope) const childScope = moduleDeclToScope(ctx, ns, ns._ownerScope || scope)
return innerResolveTypeReference( return innerResolveTypeReference(
ctx, ctx,
childScope, childScope,
@ -581,7 +581,7 @@ function resolveGlobalScope(ctx: TypeResolveContext): TypeScope[] | undefined {
throw new Error('[vue/compiler-sfc] globalTypeFiles requires fs access.') throw new Error('[vue/compiler-sfc] globalTypeFiles requires fs access.')
} }
return ctx.options.globalTypeFiles.map(file => return ctx.options.globalTypeFiles.map(file =>
fileToScope(normalizePath(file), fs, ctx.options.babelParserPlugins, true) fileToScope(ctx, normalizePath(file), true)
) )
} }
} }
@ -603,23 +603,36 @@ function resolveTypeFromImport(
name: string, name: string,
scope: TypeScope scope: TypeScope
): ScopeTypeNode | undefined { ): ScopeTypeNode | undefined {
const { source, imported } = scope.imports[name]
const resolved = resolveImportSource(ctx, node, scope, source)
return resolveTypeReference(
ctx,
node,
fileToScope(ctx, resolved),
imported,
true
)
}
function resolveImportSource(
ctx: TypeResolveContext,
node: Node,
scope: TypeScope,
source: string
): string {
const fs: FS = ctx.options.fs || ts?.sys const fs: FS = ctx.options.fs || ts?.sys
if (!fs) { if (!fs) {
ctx.error( ctx.error(
`No fs option provided to \`compileScript\` in non-Node environment. ` + `No fs option provided to \`compileScript\` in non-Node environment. ` +
`File system access is required for resolving imported types.`, `File system access is required for resolving imported types.`,
node node,
scope
) )
} }
let resolved
const containingFile = scope.filename
const { source, imported } = scope.imports[name]
let resolved: string | undefined
if (source.startsWith('.')) { if (source.startsWith('.')) {
// relative import - fast path // relative import - fast path
const filename = path.join(containingFile, '..', source) const filename = path.join(scope.filename, '..', source)
resolved = resolveExt(filename, fs) resolved = resolveExt(filename, fs)
} else { } else {
// module or aliased import - use full TS resolution, only supported in Node // module or aliased import - use full TS resolution, only supported in Node
@ -632,36 +645,22 @@ function resolveTypeFromImport(
} }
if (!ts) { if (!ts) {
ctx.error( ctx.error(
`Failed to resolve type ${imported} from module ${JSON.stringify( `Failed to resolve import source ${JSON.stringify(source)}. ` +
source
)}. ` +
`typescript is required as a peer dep for vue in order ` + `typescript is required as a peer dep for vue in order ` +
`to support resolving types from module imports.`, `to support resolving types from module imports.`,
node, node,
scope scope
) )
} }
resolved = resolveWithTS(containingFile, source, fs) resolved = resolveWithTS(scope.filename, source, fs)
} }
if (resolved) { if (resolved) {
resolved = normalizePath(resolved)
// (hmr) register dependency file on ctx // (hmr) register dependency file on ctx
;(ctx.deps || (ctx.deps = new Set())).add(resolved) ;(ctx.deps || (ctx.deps = new Set())).add(resolved)
return normalizePath(resolved)
return resolveTypeReference(
ctx,
node,
fileToScope(resolved, fs, ctx.options.babelParserPlugins),
imported,
true
)
} else { } else {
ctx.error( return ctx.error(
`Failed to resolve import source ${JSON.stringify( `Failed to resolve import source ${JSON.stringify(source)}.`,
source
)} for type ${name}`,
node, node,
scope scope
) )
@ -753,18 +752,18 @@ export function invalidateTypeCache(filename: string) {
} }
export function fileToScope( export function fileToScope(
ctx: TypeResolveContext,
filename: string, filename: string,
fs: FS,
parserPlugins: SFCScriptCompileOptions['babelParserPlugins'],
asGlobal = false asGlobal = false
): TypeScope { ): TypeScope {
const cached = fileToScopeCache.get(filename) const cached = fileToScopeCache.get(filename)
if (cached) { if (cached) {
return cached return cached
} }
// fs should be guaranteed to exist here
const fs = ctx.options.fs || ts?.sys
const source = fs.readFile(filename) || '' const source = fs.readFile(filename) || ''
const body = parseFile(filename, source, parserPlugins) const body = parseFile(filename, source, ctx.options.babelParserPlugins)
const scope: TypeScope = { const scope: TypeScope = {
filename, filename,
source, source,
@ -773,7 +772,7 @@ export function fileToScope(
types: Object.create(null), types: Object.create(null),
exportedTypes: Object.create(null) exportedTypes: Object.create(null)
} }
recordTypes(body, scope, asGlobal) recordTypes(ctx, body, scope, asGlobal)
fileToScopeCache.set(filename, scope) fileToScopeCache.set(filename, scope)
return scope return scope
} }
@ -846,12 +845,13 @@ function ctxToScope(ctx: TypeResolveContext): TypeScope {
exportedTypes: Object.create(null) exportedTypes: Object.create(null)
} }
recordTypes(body, scope) recordTypes(ctx, body, scope)
return (ctx.scope = scope) return (ctx.scope = scope)
} }
function moduleDeclToScope( function moduleDeclToScope(
ctx: TypeResolveContext,
node: TSModuleDeclaration & { _resolvedChildScope?: TypeScope }, node: TSModuleDeclaration & { _resolvedChildScope?: TypeScope },
parentScope: TypeScope parentScope: TypeScope
): TypeScope { ): TypeScope {
@ -872,7 +872,7 @@ function moduleDeclToScope(
const id = getId(decl.id) const id = getId(decl.id)
scope.types[id] = scope.exportedTypes[id] = decl scope.types[id] = scope.exportedTypes[id] = decl
} else { } else {
recordTypes(node.body.body, scope) recordTypes(ctx, node.body.body, scope)
} }
return (node._resolvedChildScope = scope) return (node._resolvedChildScope = scope)
@ -880,7 +880,12 @@ function moduleDeclToScope(
const importExportRE = /^Import|^Export/ const importExportRE = /^Import|^Export/
function recordTypes(body: Statement[], scope: TypeScope, asGlobal = false) { function recordTypes(
ctx: TypeResolveContext,
body: Statement[],
scope: TypeScope,
asGlobal = false
) {
const { types, exportedTypes, imports } = scope const { types, exportedTypes, imports } = scope
const isAmbient = asGlobal const isAmbient = asGlobal
? !body.some(s => importExportRE.test(s.type)) ? !body.some(s => importExportRE.test(s.type))
@ -932,6 +937,15 @@ function recordTypes(body: Statement[], scope: TypeScope, asGlobal = false) {
} }
} }
} }
} else if (stmt.type === 'ExportAllDeclaration') {
const targetFile = resolveImportSource(
ctx,
stmt.source,
scope,
stmt.source.value
)
const targetScope = fileToScope(ctx, targetFile)
Object.assign(scope.exportedTypes, targetScope.exportedTypes)
} }
} }
} }