feat(compiler-sfc): support project references when resolving types

close #8140
This commit is contained in:
Evan You 2023-04-25 16:30:11 +08:00
parent a370e8006a
commit 1c0be5c744
4 changed files with 194 additions and 95 deletions

View File

@ -607,6 +607,44 @@ describe('resolveType', () => {
]) ])
}) })
test('ts module resolve w/ project reference & extends', () => {
const files = {
'/tsconfig.json': JSON.stringify({
references: [
{
path: './tsconfig.app.json'
}
]
}),
'/tsconfig.app.json': JSON.stringify({
include: ['**/*.ts', '**/*.vue'],
extends: './tsconfig.web.json'
}),
'/tsconfig.web.json': JSON.stringify({
compilerOptions: {
composite: true,
paths: {
bar: ['./user.ts']
}
}
}),
'/user.ts': 'export type User = { bar: string }'
}
const { props, deps } = resolve(
`
import { User } from 'bar'
defineProps<User>()
`,
files
)
expect(props).toStrictEqual({
bar: ['String']
})
expect(deps && [...deps]).toStrictEqual(['/user.ts'])
})
test('global types', () => { test('global types', () => {
const files = { const files = {
// ambient // ambient

View File

@ -51,6 +51,7 @@
"hash-sum": "^2.0.0", "hash-sum": "^2.0.0",
"lru-cache": "^5.1.1", "lru-cache": "^5.1.1",
"merge-source-map": "^1.1.0", "merge-source-map": "^1.1.0",
"minimatch": "^9.0.0",
"postcss-modules": "^4.0.0", "postcss-modules": "^4.0.0",
"postcss-selector-parser": "^6.0.4", "postcss-selector-parser": "^6.0.4",
"pug": "^3.0.1", "pug": "^3.0.1",

View File

@ -40,6 +40,7 @@ import { parse } from '../parse'
import { createCache } from '../cache' import { createCache } from '../cache'
import type TS from 'typescript' import type TS from 'typescript'
import { extname, dirname } from 'path' import { extname, dirname } from 'path'
import { minimatch as isMatch } from 'minimatch'
/** /**
* TypeResolveContext is compatible with ScriptCompileContext * TypeResolveContext is compatible with ScriptCompileContext
@ -77,15 +78,19 @@ interface WithScope {
type ScopeTypeNode = Node & type ScopeTypeNode = Node &
WithScope & { _ns?: TSModuleDeclaration & WithScope } WithScope & { _ns?: TSModuleDeclaration & WithScope }
export interface TypeScope { export class TypeScope {
filename: string constructor(
source: string public filename: string,
offset: number public source: string,
imports: Record<string, Import> public offset: number = 0,
types: Record<string, ScopeTypeNode> public imports: Record<string, Import> = Object.create(null),
exportedTypes: Record<string, ScopeTypeNode> public types: Record<string, ScopeTypeNode> = Object.create(null),
declares: Record<string, ScopeTypeNode> public declares: Record<string, ScopeTypeNode> = Object.create(null)
exportedDeclares: Record<string, ScopeTypeNode> ) {}
resolvedImportSources: Record<string, string> = Object.create(null)
exportedTypes: Record<string, ScopeTypeNode> = Object.create(null)
exportedDeclares: Record<string, ScopeTypeNode> = Object.create(null)
} }
export interface MaybeWithScope { export interface MaybeWithScope {
@ -716,7 +721,9 @@ function importSourceToScope(
scope scope
) )
} }
let resolved
let resolved: string | undefined = scope.resolvedImportSources[source]
if (!resolved) {
if (source.startsWith('.')) { if (source.startsWith('.')) {
// relative import - fast path // relative import - fast path
const filename = joinPaths(scope.filename, '..', source) const filename = joinPaths(scope.filename, '..', source)
@ -742,7 +749,10 @@ function importSourceToScope(
resolved = resolveWithTS(scope.filename, source, fs) resolved = resolveWithTS(scope.filename, source, fs)
} }
if (resolved) { if (resolved) {
resolved = normalizePath(resolved) resolved = scope.resolvedImportSources[source] = normalizePath(resolved)
}
}
if (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 fileToScope(ctx, resolved) return fileToScope(ctx, resolved)
@ -768,10 +778,13 @@ function resolveExt(filename: string, fs: FS) {
) )
} }
const tsConfigCache = createCache<{ interface CachedConfig {
options: TS.CompilerOptions config: TS.ParsedCommandLine
cache: TS.ModuleResolutionCache cache?: TS.ModuleResolutionCache
}>() }
const tsConfigCache = createCache<CachedConfig[]>()
const tsConfigRefMap = new Map<string, string>()
function resolveWithTS( function resolveWithTS(
containingFile: string, containingFile: string,
@ -783,12 +796,75 @@ function resolveWithTS(
// 1. resolve tsconfig.json // 1. resolve tsconfig.json
const configPath = ts.findConfigFile(containingFile, fs.fileExists) const configPath = ts.findConfigFile(containingFile, fs.fileExists)
// 2. load tsconfig.json // 2. load tsconfig.json
let options: TS.CompilerOptions let tsCompilerOptions: TS.CompilerOptions
let cache: TS.ModuleResolutionCache | undefined let tsResolveCache: TS.ModuleResolutionCache | undefined
if (configPath) { if (configPath) {
let configs: CachedConfig[]
const normalizedConfigPath = normalizePath(configPath) const normalizedConfigPath = normalizePath(configPath)
const cached = tsConfigCache.get(normalizedConfigPath) const cached = tsConfigCache.get(normalizedConfigPath)
if (!cached) { if (!cached) {
configs = loadTSConfig(configPath, fs).map(config => ({ config }))
tsConfigCache.set(normalizedConfigPath, configs)
} else {
configs = cached
}
let matchedConfig: CachedConfig | undefined
if (configs.length === 1) {
matchedConfig = configs[0]
} else {
// resolve which config matches the current file
for (const c of configs) {
const base = normalizePath(
(c.config.options.pathsBasePath as string) ||
dirname(c.config.options.configFilePath as string)
)
const included: string[] = c.config.raw?.include
const excluded: string[] = c.config.raw?.exclude
if (
(!included && (!base || containingFile.startsWith(base))) ||
included.some(p => isMatch(containingFile, joinPaths(base, p)))
) {
if (
excluded &&
excluded.some(p => isMatch(containingFile, joinPaths(base, p)))
) {
continue
}
matchedConfig = c
break
}
}
if (!matchedConfig) {
matchedConfig = configs[configs.length - 1]
}
}
tsCompilerOptions = matchedConfig.config.options
tsResolveCache =
matchedConfig.cache ||
(matchedConfig.cache = ts.createModuleResolutionCache(
process.cwd(),
createGetCanonicalFileName(ts.sys.useCaseSensitiveFileNames),
tsCompilerOptions
))
} else {
tsCompilerOptions = {}
}
// 3. resolve
const res = ts.resolveModuleName(
source,
containingFile,
tsCompilerOptions,
fs,
tsResolveCache
)
if (res.resolvedModule) {
return res.resolvedModule.resolvedFileName
}
}
function loadTSConfig(configPath: string, fs: FS): TS.ParsedCommandLine[] {
// The only case where `fs` is NOT `ts.sys` is during tests. // The only case where `fs` is NOT `ts.sys` is during tests.
// parse config host requires an extra `readDirectory` method // parse config host requires an extra `readDirectory` method
// during tests, which is stubbed. // during tests, which is stubbed.
@ -799,33 +875,21 @@ function resolveWithTS(
readDirectory: () => [] readDirectory: () => []
} }
: ts.sys : ts.sys
const parsed = ts.parseJsonConfigFileContent( const config = ts.parseJsonConfigFileContent(
ts.readConfigFile(configPath, fs.readFile).config, ts.readConfigFile(configPath, fs.readFile).config,
parseConfigHost, parseConfigHost,
dirname(configPath), dirname(configPath),
undefined, undefined,
configPath configPath
) )
options = parsed.options const res = [config]
cache = ts.createModuleResolutionCache( if (config.projectReferences) {
process.cwd(), for (const ref of config.projectReferences) {
createGetCanonicalFileName(ts.sys.useCaseSensitiveFileNames), tsConfigRefMap.set(ref.path, configPath)
options res.unshift(...loadTSConfig(ref.path, fs))
)
tsConfigCache.set(normalizedConfigPath, { options, cache })
} else {
;({ options, cache } = cached)
} }
} else {
options = {}
}
// 3. resolve
const res = ts.resolveModuleName(source, containingFile, options, fs, cache)
if (res.resolvedModule) {
return res.resolvedModule.resolvedFileName
} }
return res
} }
const fileToScopeCache = createCache<TypeScope>() const fileToScopeCache = createCache<TypeScope>()
@ -837,6 +901,8 @@ export function invalidateTypeCache(filename: string) {
filename = normalizePath(filename) filename = normalizePath(filename)
fileToScopeCache.delete(filename) fileToScopeCache.delete(filename)
tsConfigCache.delete(filename) tsConfigCache.delete(filename)
const affectedConfig = tsConfigRefMap.get(filename)
if (affectedConfig) tsConfigCache.delete(affectedConfig)
} }
export function fileToScope( export function fileToScope(
@ -852,16 +918,7 @@ export function fileToScope(
const fs = ctx.options.fs || ts?.sys const fs = ctx.options.fs || ts?.sys
const source = fs.readFile(filename) || '' const source = fs.readFile(filename) || ''
const body = parseFile(filename, source, ctx.options.babelParserPlugins) const body = parseFile(filename, source, ctx.options.babelParserPlugins)
const scope: TypeScope = { const scope = new TypeScope(filename, source, 0, recordImports(body))
filename,
source,
offset: 0,
imports: recordImports(body),
types: Object.create(null),
exportedTypes: Object.create(null),
declares: Object.create(null),
exportedDeclares: Object.create(null)
}
recordTypes(ctx, body, scope, asGlobal) recordTypes(ctx, body, scope, asGlobal)
fileToScopeCache.set(filename, scope) fileToScopeCache.set(filename, scope)
return scope return scope
@ -923,19 +980,12 @@ function ctxToScope(ctx: TypeResolveContext): TypeScope {
? [...ctx.scriptAst.body, ...ctx.scriptSetupAst!.body] ? [...ctx.scriptAst.body, ...ctx.scriptSetupAst!.body]
: ctx.scriptSetupAst!.body : ctx.scriptSetupAst!.body
const scope: TypeScope = { const scope = new TypeScope(
filename: ctx.filename, ctx.filename,
source: ctx.source, ctx.source,
offset: 'startOffset' in ctx ? ctx.startOffset! : 0, 'startOffset' in ctx ? ctx.startOffset! : 0,
imports: 'userImports' in ctx ? Object.create(ctx.userImports) : recordImports(body)
'userImports' in ctx )
? Object.create(ctx.userImports)
: recordImports(body),
types: Object.create(null),
exportedTypes: Object.create(null),
declares: Object.create(null),
exportedDeclares: Object.create(null)
}
recordTypes(ctx, body, scope) recordTypes(ctx, body, scope)
@ -950,14 +1000,15 @@ function moduleDeclToScope(
if (node._resolvedChildScope) { if (node._resolvedChildScope) {
return node._resolvedChildScope return node._resolvedChildScope
} }
const scope: TypeScope = {
...parentScope, const scope = new TypeScope(
imports: Object.create(parentScope.imports), parentScope.filename,
types: Object.create(parentScope.types), parentScope.source,
declares: Object.create(parentScope.declares), parentScope.offset,
exportedTypes: Object.create(null), Object.create(parentScope.imports),
exportedDeclares: Object.create(null) Object.create(parentScope.types),
} Object.create(parentScope.declares)
)
if (node.body.type === 'TSModuleDeclaration') { if (node.body.type === 'TSModuleDeclaration') {
const decl = node.body as TSModuleDeclaration & WithScope const decl = node.body as TSModuleDeclaration & WithScope

View File

@ -136,6 +136,7 @@ importers:
lru-cache: ^5.1.1 lru-cache: ^5.1.1
magic-string: ^0.30.0 magic-string: ^0.30.0
merge-source-map: ^1.1.0 merge-source-map: ^1.1.0
minimatch: ^9.0.0
postcss: ^8.1.10 postcss: ^8.1.10
postcss-modules: ^4.0.0 postcss-modules: ^4.0.0
postcss-selector-parser: ^6.0.4 postcss-selector-parser: ^6.0.4
@ -161,6 +162,7 @@ importers:
hash-sum: 2.0.0 hash-sum: 2.0.0
lru-cache: 5.1.1 lru-cache: 5.1.1
merge-source-map: 1.1.0 merge-source-map: 1.1.0
minimatch: 9.0.0
postcss-modules: 4.3.1_postcss@8.4.21 postcss-modules: 4.3.1_postcss@8.4.21
postcss-selector-parser: 6.0.11 postcss-selector-parser: 6.0.11
pug: 3.0.2 pug: 3.0.2
@ -3759,6 +3761,13 @@ packages:
brace-expansion: 2.0.1 brace-expansion: 2.0.1
dev: true dev: true
/minimatch/9.0.0:
resolution: {integrity: sha512-0jJj8AvgKqWN05mrwuqi8QYKx1WmYSUoKSxu5Qhs9prezTz10sxAHGNZe9J9cqIJzta8DWsleh2KaVaLl6Ru2w==}
engines: {node: '>=16 || 14 >=14.17'}
dependencies:
brace-expansion: 2.0.1
dev: true
/minimist-options/4.1.0: /minimist-options/4.1.0:
resolution: {integrity: sha512-Q4r8ghd80yhO/0j1O3B2BjweX3fiHg9cdOwjJd2J76Q135c+NDxGCqdYKQ1SKBuFfgWbAUzBfvYjPUEeNgqN1A==} resolution: {integrity: sha512-Q4r8ghd80yhO/0j1O3B2BjweX3fiHg9cdOwjJd2J76Q135c+NDxGCqdYKQ1SKBuFfgWbAUzBfvYjPUEeNgqN1A==}
engines: {node: '>= 6'} engines: {node: '>= 6'}