Skip to content

Commit 89e8e78

Browse files
committed
perf(typescript): cache module resolution result for tsc
close vuejs/language-tools#4177
1 parent c6a538c commit 89e8e78

File tree

4 files changed

+70
-85
lines changed

4 files changed

+70
-85
lines changed

packages/typescript/lib/node/decorateLanguageServiceHost.ts

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ export function decorateLanguageServiceHost(
1111

1212
let extraProjectVersion = 0;
1313

14-
const exts = language.plugins
14+
const extensions = language.plugins
1515
.map(plugin => plugin.typescript?.extraFileExtensions.map(ext => '.' + ext.extension) ?? [])
1616
.flat();
1717
const scripts = new Map<string, [version: string, {
@@ -31,7 +31,7 @@ export function decorateLanguageServiceHost(
3131
if (readDirectory) {
3232
languageServiceHost.readDirectory = (path, extensions, exclude, include, depth) => {
3333
if (extensions) {
34-
for (const ext of exts) {
34+
for (const ext of extensions) {
3535
if (!extensions.includes(ext)) {
3636
extensions = [...extensions, ...ext];
3737
}
@@ -41,9 +41,13 @@ export function decorateLanguageServiceHost(
4141
};
4242
}
4343

44-
if (language.plugins.some(language => language.typescript?.extraFileExtensions.length)) {
44+
if (extensions.length) {
4545

4646
const resolveModuleName = createResolveModuleName(ts, languageServiceHost, language.plugins, fileName => language.scripts.get(fileName));
47+
const getCanonicalFileName = languageServiceHost.useCaseSensitiveFileNames?.()
48+
? (fileName: string) => fileName
49+
: (fileName: string) => fileName.toLowerCase();
50+
const moduleResolutionCache = ts.createModuleResolutionCache(languageServiceHost.getCurrentDirectory(), getCanonicalFileName, languageServiceHost.getCompilationSettings());
4751

4852
if (resolveModuleNameLiterals) {
4953
languageServiceHost.resolveModuleNameLiterals = (
@@ -53,11 +57,11 @@ export function decorateLanguageServiceHost(
5357
options,
5458
...rest
5559
) => {
56-
if (moduleLiterals.every(name => !exts.some(ext => name.text.endsWith(ext)))) {
60+
if (moduleLiterals.every(name => !extensions.some(ext => name.text.endsWith(ext)))) {
5761
return resolveModuleNameLiterals(moduleLiterals, containingFile, redirectedReference, options, ...rest);
5862
}
5963
return moduleLiterals.map(moduleLiteral => {
60-
return resolveModuleName(moduleLiteral.text, containingFile, options, undefined, redirectedReference);
64+
return resolveModuleName(moduleLiteral.text, containingFile, options, moduleResolutionCache, redirectedReference);
6165
});
6266
};
6367
}
@@ -70,11 +74,11 @@ export function decorateLanguageServiceHost(
7074
options,
7175
containingSourceFile
7276
) => {
73-
if (moduleNames.every(name => !exts.some(ext => name.endsWith(ext)))) {
77+
if (moduleNames.every(name => !extensions.some(ext => name.endsWith(ext)))) {
7478
return resolveModuleNames(moduleNames, containingFile, reusedNames, redirectedReference, options, containingSourceFile);
7579
}
7680
return moduleNames.map(moduleName => {
77-
return resolveModuleName(moduleName, containingFile, options, undefined, redirectedReference).resolvedModule;
81+
return resolveModuleName(moduleName, containingFile, options, moduleResolutionCache, redirectedReference).resolvedModule;
7882
});
7983
};
8084
}

packages/typescript/lib/node/proxyCreateProgram.ts

Lines changed: 46 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import type * as ts from 'typescript';
22
import { decorateProgram } from './decorateProgram';
33
import { LanguagePlugin, createLanguage } from '@volar/language-core';
4+
import { createResolveModuleName } from '../resolveModuleName';
45

56
export function proxyCreateProgram(
67
ts: typeof import('typescript'),
@@ -24,8 +25,7 @@ export function proxyCreateProgram(
2425
ts.sys.useCaseSensitiveFileNames,
2526
fileName => {
2627
let snapshot: ts.IScriptSnapshot | undefined;
27-
assert(originalSourceFiles.has(fileName), `originalSourceFiles.has(${fileName})`);
28-
const sourceFile = originalSourceFiles.get(fileName);
28+
const sourceFile = originalHost.getSourceFile(fileName, 99 satisfies ts.ScriptTarget.ESNext);
2929
if (sourceFile) {
3030
snapshot = sourceFileToSnapshotMap.get(sourceFile);
3131
if (!snapshot) {
@@ -51,35 +51,17 @@ export function proxyCreateProgram(
5151
}
5252
}
5353
);
54-
const originalSourceFiles = new Map<string, ts.SourceFile | undefined>();
5554
const parsedSourceFiles = new WeakMap<ts.SourceFile, ts.SourceFile>();
56-
const arbitraryExtensions = extensions.map(ext => `.d${ext}.ts`);
5755
const originalHost = options.host;
58-
const moduleResolutionHost: ts.ModuleResolutionHost = {
59-
...originalHost,
60-
fileExists(fileName) {
61-
for (let i = 0; i < arbitraryExtensions.length; i++) {
62-
if (fileName.endsWith(arbitraryExtensions[i])) {
63-
return originalHost.fileExists(fileName.slice(0, -arbitraryExtensions[i].length) + extensions[i]);
64-
}
65-
}
66-
return originalHost.fileExists(fileName);
67-
},
68-
};
6956

7057
options.host = { ...originalHost };
71-
options.options.allowArbitraryExtensions = true;
7258
options.host.getSourceFile = (
7359
fileName,
7460
languageVersionOrOptions,
7561
onError,
7662
shouldCreateNewSourceFile,
7763
) => {
78-
7964
const originalSourceFile = originalHost.getSourceFile(fileName, languageVersionOrOptions, onError, shouldCreateNewSourceFile);
80-
81-
originalSourceFiles.set(fileName, originalSourceFile);
82-
8365
if (originalSourceFile && extensions.some(ext => fileName.endsWith(ext))) {
8466
let sourceFile2 = parsedSourceFiles.get(originalSourceFile);
8567
if (!sourceFile2) {
@@ -88,14 +70,14 @@ export function proxyCreateProgram(
8870
let patchedText = originalSourceFile.text.split('\n').map(line => ' '.repeat(line.length)).join('\n');
8971
let scriptKind = ts.ScriptKind.TS;
9072
if (sourceScript.generated?.languagePlugin.typescript) {
91-
const { getServiceScript: getScript, getExtraServiceScripts: getExtraScripts } = sourceScript.generated.languagePlugin.typescript;
92-
const serviceScript = getScript(sourceScript.generated.root);
73+
const { getServiceScript, getExtraServiceScripts } = sourceScript.generated.languagePlugin.typescript;
74+
const serviceScript = getServiceScript(sourceScript.generated.root);
9375
if (serviceScript) {
9476
scriptKind = serviceScript.scriptKind;
9577
patchedText += serviceScript.code.snapshot.getText(0, serviceScript.code.snapshot.getLength());
9678
}
97-
if (getExtraScripts) {
98-
console.warn('getExtraScripts() is not available in this use case.');
79+
if (getExtraServiceScripts) {
80+
console.warn('getExtraServiceScripts() is not available in this use case.');
9981
}
10082
}
10183
sourceFile2 = ts.createSourceFile(
@@ -114,57 +96,54 @@ export function proxyCreateProgram(
11496

11597
return originalSourceFile;
11698
};
117-
options.host.resolveModuleNameLiterals = (
118-
moduleNames,
119-
containingFile,
120-
redirectedReference,
121-
options,
122-
) => {
123-
return moduleNames.map<ts.ResolvedModuleWithFailedLookupLocations>(name => {
124-
return resolveModuleName(name.text, containingFile, options, redirectedReference);
125-
});
126-
};
127-
options.host.resolveModuleNames = (
128-
moduleNames,
129-
containingFile,
130-
_reusedNames,
131-
redirectedReference,
132-
options,
133-
) => {
134-
return moduleNames.map<ts.ResolvedModule | undefined>(name => {
135-
return resolveModuleName(name, containingFile, options, redirectedReference).resolvedModule;
136-
});
137-
};
13899

139-
const program = Reflect.apply(target, thisArg, [options]) as ts.Program;
100+
if (extensions.length) {
101+
options.options.allowArbitraryExtensions = true;
102+
103+
const resolveModuleName = createResolveModuleName(ts, originalHost, language.plugins, fileName => language.scripts.get(fileName));
104+
const resolveModuleNameLiterals = originalHost.resolveModuleNameLiterals;
105+
const resolveModuleNames = originalHost.resolveModuleNames;
106+
const moduleResolutionCache = ts.createModuleResolutionCache(originalHost.getCurrentDirectory(), originalHost.getCanonicalFileName, options.options);
107+
108+
options.host.resolveModuleNameLiterals = (
109+
moduleLiterals,
110+
containingFile,
111+
redirectedReference,
112+
compilerOptions,
113+
...rest
114+
) => {
115+
if (resolveModuleNameLiterals && moduleLiterals.every(name => !extensions.some(ext => name.text.endsWith(ext)))) {
116+
return resolveModuleNameLiterals(moduleLiterals, containingFile, redirectedReference, compilerOptions, ...rest);
117+
}
118+
return moduleLiterals.map(moduleLiteral => {
119+
return resolveModuleName(moduleLiteral.text, containingFile, compilerOptions, moduleResolutionCache, redirectedReference);
120+
});
121+
};
122+
options.host.resolveModuleNames = (
123+
moduleNames,
124+
containingFile,
125+
reusedNames,
126+
redirectedReference,
127+
compilerOptions,
128+
containingSourceFile
129+
) => {
130+
if (resolveModuleNames && moduleNames.every(name => !extensions.some(ext => name.endsWith(ext)))) {
131+
return resolveModuleNames(moduleNames, containingFile, reusedNames, redirectedReference, compilerOptions, containingSourceFile);
132+
}
133+
return moduleNames.map(moduleName => {
134+
return resolveModuleName(moduleName, containingFile, compilerOptions, moduleResolutionCache, redirectedReference).resolvedModule;
135+
});
136+
};
137+
}
138+
139+
const program = Reflect.apply(target, thisArg, args) as ts.Program;
140140

141141
decorateProgram(language, program);
142142

143143
// TODO: #128
144144
(program as any).__volar__ = { language };
145145

146146
return program;
147-
148-
function resolveModuleName(name: string, containingFile: string, options: ts.CompilerOptions, redirectedReference: ts.ResolvedProjectReference | undefined) {
149-
const resolved = ts.resolveModuleName(
150-
name,
151-
containingFile,
152-
options,
153-
moduleResolutionHost,
154-
originalHost.getModuleResolutionCache?.(),
155-
redirectedReference
156-
);
157-
if (resolved.resolvedModule) {
158-
for (let i = 0; i < arbitraryExtensions.length; i++) {
159-
if (resolved.resolvedModule.resolvedFileName.endsWith(arbitraryExtensions[i])) {
160-
const sourceFileName = resolved.resolvedModule.resolvedFileName.slice(0, -arbitraryExtensions[i].length) + extensions[i];
161-
resolved.resolvedModule.resolvedFileName = sourceFileName;
162-
resolved.resolvedModule.extension = extensions[i];
163-
}
164-
}
165-
}
166-
return resolved;
167-
}
168147
},
169148
});
170149
}

packages/typescript/lib/protocol/createProject.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ export function createTypeScriptLanguage(
6060
// TODO: can this share between monorepo packages?
6161
const moduleCache = ts.createModuleResolutionCache(
6262
languageServiceHost.getCurrentDirectory(),
63-
languageServiceHost.useCaseSensitiveFileNames ? s => s : s => s.toLowerCase(),
63+
languageServiceHost.useCaseSensitiveFileNames?.() ? s => s : s => s.toLowerCase(),
6464
languageServiceHost.getCompilationSettings()
6565
);
6666
const resolveModuleName = createResolveModuleName(ts, languageServiceHost, languagePlugins, fileName => language.scripts.get(projectHost.fileNameToScriptId(fileName)));

packages/typescript/lib/resolveModuleName.ts

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,18 +3,20 @@ import type * as ts from 'typescript';
33

44
export function createResolveModuleName(
55
ts: typeof import('typescript'),
6-
languageServiceHost: ts.LanguageServiceHost,
6+
host: ts.ModuleResolutionHost,
77
languagePlugins: LanguagePlugin<any>[],
88
getSourceScript: (fileName: string) => SourceScript | undefined,
99
) {
1010
const toPatchResults = new Map<string, string>();
1111
const moduleResolutionHost: ts.ModuleResolutionHost = {
12-
readFile: languageServiceHost.readFile.bind(languageServiceHost),
13-
directoryExists: languageServiceHost.directoryExists?.bind(languageServiceHost),
14-
realpath: languageServiceHost.realpath?.bind(languageServiceHost),
15-
getCurrentDirectory: languageServiceHost.getCurrentDirectory.bind(languageServiceHost),
16-
getDirectories: languageServiceHost.getDirectories?.bind(languageServiceHost),
17-
useCaseSensitiveFileNames: languageServiceHost.useCaseSensitiveFileNames?.bind(languageServiceHost),
12+
readFile: host.readFile.bind(host),
13+
directoryExists: host.directoryExists?.bind(host),
14+
realpath: host.realpath?.bind(host),
15+
getCurrentDirectory: host.getCurrentDirectory?.bind(host),
16+
getDirectories: host.getDirectories?.bind(host),
17+
useCaseSensitiveFileNames: typeof host.useCaseSensitiveFileNames === 'function'
18+
? host.useCaseSensitiveFileNames.bind(host)
19+
: host.useCaseSensitiveFileNames,
1820
fileExists(fileName) {
1921
for (const { typescript } of languagePlugins) {
2022
if (!typescript) {
@@ -30,7 +32,7 @@ export function createResolveModuleName(
3032
}
3133
}
3234
}
33-
return languageServiceHost.fileExists(fileName);
35+
return host.fileExists(fileName);
3436
},
3537
};
3638
return (
@@ -66,8 +68,8 @@ export function createResolveModuleName(
6668

6769
// fix https://github.com/vuejs/language-tools/issues/3332
6870
function fileExists(fileName: string) {
69-
if (languageServiceHost.fileExists(fileName)) {
70-
const fileSize = ts.sys.getFileSize?.(fileName) ?? languageServiceHost.readFile(fileName)?.length ?? 0;
71+
if (host.fileExists(fileName)) {
72+
const fileSize = ts.sys.getFileSize?.(fileName) ?? host.readFile(fileName)?.length ?? 0;
7173
return fileSize < 4 * 1024 * 1024;
7274
}
7375
return false;

0 commit comments

Comments
 (0)