mirror of https://github.com/vuejs/core.git
feat(compiler-sfc): support project references when resolving types
close #8140
This commit is contained in:
parent
a370e8006a
commit
1c0be5c744
|
@ -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
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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'}
|
||||||
|
|
Loading…
Reference in New Issue