|
|
@@ -40,6 +40,7 @@ import { parse } from '../parse'
|
|
|
import { createCache } from '../cache'
|
|
|
import type TS from 'typescript'
|
|
|
import { extname, dirname } from 'path'
|
|
|
+import { minimatch as isMatch } from 'minimatch'
|
|
|
|
|
|
/**
|
|
|
* TypeResolveContext is compatible with ScriptCompileContext
|
|
|
@@ -77,15 +78,19 @@ interface WithScope {
|
|
|
type ScopeTypeNode = Node &
|
|
|
WithScope & { _ns?: TSModuleDeclaration & WithScope }
|
|
|
|
|
|
-export interface TypeScope {
|
|
|
- filename: string
|
|
|
- source: string
|
|
|
- offset: number
|
|
|
- imports: Record<string, Import>
|
|
|
- types: Record<string, ScopeTypeNode>
|
|
|
- exportedTypes: Record<string, ScopeTypeNode>
|
|
|
- declares: Record<string, ScopeTypeNode>
|
|
|
- exportedDeclares: Record<string, ScopeTypeNode>
|
|
|
+export class TypeScope {
|
|
|
+ constructor(
|
|
|
+ public filename: string,
|
|
|
+ public source: string,
|
|
|
+ public offset: number = 0,
|
|
|
+ public imports: Record<string, Import> = Object.create(null),
|
|
|
+ public types: Record<string, ScopeTypeNode> = Object.create(null),
|
|
|
+ public declares: Record<string, ScopeTypeNode> = Object.create(null)
|
|
|
+ ) {}
|
|
|
+
|
|
|
+ resolvedImportSources: Record<string, string> = Object.create(null)
|
|
|
+ exportedTypes: Record<string, ScopeTypeNode> = Object.create(null)
|
|
|
+ exportedDeclares: Record<string, ScopeTypeNode> = Object.create(null)
|
|
|
}
|
|
|
|
|
|
export interface MaybeWithScope {
|
|
|
@@ -716,33 +721,38 @@ function importSourceToScope(
|
|
|
scope
|
|
|
)
|
|
|
}
|
|
|
- let resolved
|
|
|
- if (source.startsWith('.')) {
|
|
|
- // relative import - fast path
|
|
|
- const filename = joinPaths(scope.filename, '..', source)
|
|
|
- resolved = resolveExt(filename, fs)
|
|
|
- } else {
|
|
|
- // module or aliased import - use full TS resolution, only supported in Node
|
|
|
- if (!__NODE_JS__) {
|
|
|
- ctx.error(
|
|
|
- `Type import from non-relative sources is not supported in the browser build.`,
|
|
|
- node,
|
|
|
- scope
|
|
|
- )
|
|
|
+
|
|
|
+ let resolved: string | undefined = scope.resolvedImportSources[source]
|
|
|
+ if (!resolved) {
|
|
|
+ if (source.startsWith('.')) {
|
|
|
+ // relative import - fast path
|
|
|
+ const filename = joinPaths(scope.filename, '..', source)
|
|
|
+ resolved = resolveExt(filename, fs)
|
|
|
+ } else {
|
|
|
+ // module or aliased import - use full TS resolution, only supported in Node
|
|
|
+ if (!__NODE_JS__) {
|
|
|
+ ctx.error(
|
|
|
+ `Type import from non-relative sources is not supported in the browser build.`,
|
|
|
+ node,
|
|
|
+ scope
|
|
|
+ )
|
|
|
+ }
|
|
|
+ if (!ts) {
|
|
|
+ ctx.error(
|
|
|
+ `Failed to resolve import source ${JSON.stringify(source)}. ` +
|
|
|
+ `typescript is required as a peer dep for vue in order ` +
|
|
|
+ `to support resolving types from module imports.`,
|
|
|
+ node,
|
|
|
+ scope
|
|
|
+ )
|
|
|
+ }
|
|
|
+ resolved = resolveWithTS(scope.filename, source, fs)
|
|
|
}
|
|
|
- if (!ts) {
|
|
|
- ctx.error(
|
|
|
- `Failed to resolve import source ${JSON.stringify(source)}. ` +
|
|
|
- `typescript is required as a peer dep for vue in order ` +
|
|
|
- `to support resolving types from module imports.`,
|
|
|
- node,
|
|
|
- scope
|
|
|
- )
|
|
|
+ if (resolved) {
|
|
|
+ resolved = scope.resolvedImportSources[source] = normalizePath(resolved)
|
|
|
}
|
|
|
- resolved = resolveWithTS(scope.filename, source, fs)
|
|
|
}
|
|
|
if (resolved) {
|
|
|
- resolved = normalizePath(resolved)
|
|
|
// (hmr) register dependency file on ctx
|
|
|
;(ctx.deps || (ctx.deps = new Set())).add(resolved)
|
|
|
return fileToScope(ctx, resolved)
|
|
|
@@ -768,10 +778,13 @@ function resolveExt(filename: string, fs: FS) {
|
|
|
)
|
|
|
}
|
|
|
|
|
|
-const tsConfigCache = createCache<{
|
|
|
- options: TS.CompilerOptions
|
|
|
- cache: TS.ModuleResolutionCache
|
|
|
-}>()
|
|
|
+interface CachedConfig {
|
|
|
+ config: TS.ParsedCommandLine
|
|
|
+ cache?: TS.ModuleResolutionCache
|
|
|
+}
|
|
|
+
|
|
|
+const tsConfigCache = createCache<CachedConfig[]>()
|
|
|
+const tsConfigRefMap = new Map<string, string>()
|
|
|
|
|
|
function resolveWithTS(
|
|
|
containingFile: string,
|
|
|
@@ -783,51 +796,102 @@ function resolveWithTS(
|
|
|
// 1. resolve tsconfig.json
|
|
|
const configPath = ts.findConfigFile(containingFile, fs.fileExists)
|
|
|
// 2. load tsconfig.json
|
|
|
- let options: TS.CompilerOptions
|
|
|
- let cache: TS.ModuleResolutionCache | undefined
|
|
|
+ let tsCompilerOptions: TS.CompilerOptions
|
|
|
+ let tsResolveCache: TS.ModuleResolutionCache | undefined
|
|
|
if (configPath) {
|
|
|
+ let configs: CachedConfig[]
|
|
|
const normalizedConfigPath = normalizePath(configPath)
|
|
|
const cached = tsConfigCache.get(normalizedConfigPath)
|
|
|
if (!cached) {
|
|
|
- // The only case where `fs` is NOT `ts.sys` is during tests.
|
|
|
- // parse config host requires an extra `readDirectory` method
|
|
|
- // during tests, which is stubbed.
|
|
|
- const parseConfigHost = __TEST__
|
|
|
- ? {
|
|
|
- ...fs,
|
|
|
- useCaseSensitiveFileNames: true,
|
|
|
- readDirectory: () => []
|
|
|
+ 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
|
|
|
}
|
|
|
- : ts.sys
|
|
|
- const parsed = ts.parseJsonConfigFileContent(
|
|
|
- ts.readConfigFile(configPath, fs.readFile).config,
|
|
|
- parseConfigHost,
|
|
|
- dirname(configPath),
|
|
|
- undefined,
|
|
|
- configPath
|
|
|
- )
|
|
|
- options = parsed.options
|
|
|
- cache = ts.createModuleResolutionCache(
|
|
|
+ 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),
|
|
|
- options
|
|
|
- )
|
|
|
- tsConfigCache.set(normalizedConfigPath, { options, cache })
|
|
|
- } else {
|
|
|
- ;({ options, cache } = cached)
|
|
|
- }
|
|
|
+ tsCompilerOptions
|
|
|
+ ))
|
|
|
} else {
|
|
|
- options = {}
|
|
|
+ tsCompilerOptions = {}
|
|
|
}
|
|
|
|
|
|
// 3. resolve
|
|
|
- const res = ts.resolveModuleName(source, containingFile, options, fs, cache)
|
|
|
+ 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.
|
|
|
+ // parse config host requires an extra `readDirectory` method
|
|
|
+ // during tests, which is stubbed.
|
|
|
+ const parseConfigHost = __TEST__
|
|
|
+ ? {
|
|
|
+ ...fs,
|
|
|
+ useCaseSensitiveFileNames: true,
|
|
|
+ readDirectory: () => []
|
|
|
+ }
|
|
|
+ : ts.sys
|
|
|
+ const config = ts.parseJsonConfigFileContent(
|
|
|
+ ts.readConfigFile(configPath, fs.readFile).config,
|
|
|
+ parseConfigHost,
|
|
|
+ dirname(configPath),
|
|
|
+ undefined,
|
|
|
+ configPath
|
|
|
+ )
|
|
|
+ const res = [config]
|
|
|
+ if (config.projectReferences) {
|
|
|
+ for (const ref of config.projectReferences) {
|
|
|
+ tsConfigRefMap.set(ref.path, configPath)
|
|
|
+ res.unshift(...loadTSConfig(ref.path, fs))
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return res
|
|
|
+}
|
|
|
+
|
|
|
const fileToScopeCache = createCache<TypeScope>()
|
|
|
|
|
|
/**
|
|
|
@@ -837,6 +901,8 @@ export function invalidateTypeCache(filename: string) {
|
|
|
filename = normalizePath(filename)
|
|
|
fileToScopeCache.delete(filename)
|
|
|
tsConfigCache.delete(filename)
|
|
|
+ const affectedConfig = tsConfigRefMap.get(filename)
|
|
|
+ if (affectedConfig) tsConfigCache.delete(affectedConfig)
|
|
|
}
|
|
|
|
|
|
export function fileToScope(
|
|
|
@@ -852,16 +918,7 @@ export function fileToScope(
|
|
|
const fs = ctx.options.fs || ts?.sys
|
|
|
const source = fs.readFile(filename) || ''
|
|
|
const body = parseFile(filename, source, ctx.options.babelParserPlugins)
|
|
|
- const scope: TypeScope = {
|
|
|
- filename,
|
|
|
- source,
|
|
|
- offset: 0,
|
|
|
- imports: recordImports(body),
|
|
|
- types: Object.create(null),
|
|
|
- exportedTypes: Object.create(null),
|
|
|
- declares: Object.create(null),
|
|
|
- exportedDeclares: Object.create(null)
|
|
|
- }
|
|
|
+ const scope = new TypeScope(filename, source, 0, recordImports(body))
|
|
|
recordTypes(ctx, body, scope, asGlobal)
|
|
|
fileToScopeCache.set(filename, scope)
|
|
|
return scope
|
|
|
@@ -923,19 +980,12 @@ function ctxToScope(ctx: TypeResolveContext): TypeScope {
|
|
|
? [...ctx.scriptAst.body, ...ctx.scriptSetupAst!.body]
|
|
|
: ctx.scriptSetupAst!.body
|
|
|
|
|
|
- const scope: TypeScope = {
|
|
|
- filename: ctx.filename,
|
|
|
- source: ctx.source,
|
|
|
- offset: 'startOffset' in ctx ? ctx.startOffset! : 0,
|
|
|
- imports:
|
|
|
- '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)
|
|
|
- }
|
|
|
+ const scope = new TypeScope(
|
|
|
+ ctx.filename,
|
|
|
+ ctx.source,
|
|
|
+ 'startOffset' in ctx ? ctx.startOffset! : 0,
|
|
|
+ 'userImports' in ctx ? Object.create(ctx.userImports) : recordImports(body)
|
|
|
+ )
|
|
|
|
|
|
recordTypes(ctx, body, scope)
|
|
|
|
|
|
@@ -950,14 +1000,15 @@ function moduleDeclToScope(
|
|
|
if (node._resolvedChildScope) {
|
|
|
return node._resolvedChildScope
|
|
|
}
|
|
|
- const scope: TypeScope = {
|
|
|
- ...parentScope,
|
|
|
- imports: Object.create(parentScope.imports),
|
|
|
- types: Object.create(parentScope.types),
|
|
|
- declares: Object.create(parentScope.declares),
|
|
|
- exportedTypes: Object.create(null),
|
|
|
- exportedDeclares: Object.create(null)
|
|
|
- }
|
|
|
+
|
|
|
+ const scope = new TypeScope(
|
|
|
+ parentScope.filename,
|
|
|
+ parentScope.source,
|
|
|
+ parentScope.offset,
|
|
|
+ Object.create(parentScope.imports),
|
|
|
+ Object.create(parentScope.types),
|
|
|
+ Object.create(parentScope.declares)
|
|
|
+ )
|
|
|
|
|
|
if (node.body.type === 'TSModuleDeclaration') {
|
|
|
const decl = node.body as TSModuleDeclaration & WithScope
|