Skip to content

Commit 19da69b

Browse files
committed
perf: implement TS 4.5 auto-import cache logic
close #808
1 parent 573dad2 commit 19da69b

File tree

4 files changed

+274
-2
lines changed

4 files changed

+274
-2
lines changed
+117
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
import type { Path, UserPreferences } from 'typescript/lib/tsserverlibrary';
2+
3+
export interface ModulePath {
4+
path: string;
5+
isInNodeModules: boolean;
6+
isRedirect: boolean;
7+
}
8+
9+
export interface ResolvedModuleSpecifierInfo {
10+
modulePaths: readonly ModulePath[] | undefined;
11+
moduleSpecifiers: readonly string[] | undefined;
12+
isAutoImportable: boolean | undefined;
13+
}
14+
15+
export interface ModuleSpecifierCache {
16+
get(fromFileName: Path, toFileName: Path, preferences: UserPreferences): Readonly<ResolvedModuleSpecifierInfo> | undefined;
17+
set(fromFileName: Path, toFileName: Path, preferences: UserPreferences, modulePaths: readonly ModulePath[], moduleSpecifiers: readonly string[]): void;
18+
setIsAutoImportable(fromFileName: Path, toFileName: Path, preferences: UserPreferences, isAutoImportable: boolean): void;
19+
setModulePaths(fromFileName: Path, toFileName: Path, preferences: UserPreferences, modulePaths: readonly ModulePath[]): void;
20+
clear(): void;
21+
count(): number;
22+
}
23+
24+
// export interface ModuleSpecifierResolutionCacheHost {
25+
// watchNodeModulesForPackageJsonChanges(directoryPath: string): FileWatcher;
26+
// }
27+
28+
export function createModuleSpecifierCache(
29+
// host: ModuleSpecifierResolutionCacheHost
30+
): ModuleSpecifierCache {
31+
// let containedNodeModulesWatchers: Map<string, FileWatcher> | undefined; // TODO
32+
let cache: Map<Path, ResolvedModuleSpecifierInfo> | undefined;
33+
let currentKey: string | undefined;
34+
const result: ModuleSpecifierCache = {
35+
get(fromFileName, toFileName, preferences) {
36+
if (!cache || currentKey !== key(fromFileName, preferences)) return undefined;
37+
return cache.get(toFileName);
38+
},
39+
set(fromFileName, toFileName, preferences, modulePaths, moduleSpecifiers) {
40+
ensureCache(fromFileName, preferences).set(toFileName, createInfo(modulePaths, moduleSpecifiers, /*isAutoImportable*/ true));
41+
42+
// If any module specifiers were generated based off paths in node_modules,
43+
// a package.json file in that package was read and is an input to the cached.
44+
// Instead of watching each individual package.json file, set up a wildcard
45+
// directory watcher for any node_modules referenced and clear the cache when
46+
// it sees any changes.
47+
if (moduleSpecifiers) {
48+
for (const p of modulePaths) {
49+
if (p.isInNodeModules) {
50+
// No trailing slash
51+
// const nodeModulesPath = p.path.substring(0, p.path.indexOf(nodeModulesPathPart) + nodeModulesPathPart.length - 1);
52+
// if (!containedNodeModulesWatchers?.has(nodeModulesPath)) {
53+
// (containedNodeModulesWatchers ||= new Map()).set(
54+
// nodeModulesPath,
55+
// host.watchNodeModulesForPackageJsonChanges(nodeModulesPath),
56+
// );
57+
// }
58+
}
59+
}
60+
}
61+
},
62+
setModulePaths(fromFileName, toFileName, preferences, modulePaths) {
63+
const cache = ensureCache(fromFileName, preferences);
64+
const info = cache.get(toFileName);
65+
if (info) {
66+
info.modulePaths = modulePaths;
67+
}
68+
else {
69+
cache.set(toFileName, createInfo(modulePaths, /*moduleSpecifiers*/ undefined, /*isAutoImportable*/ undefined));
70+
}
71+
},
72+
setIsAutoImportable(fromFileName, toFileName, preferences, isAutoImportable) {
73+
const cache = ensureCache(fromFileName, preferences);
74+
const info = cache.get(toFileName);
75+
if (info) {
76+
info.isAutoImportable = isAutoImportable;
77+
}
78+
else {
79+
cache.set(toFileName, createInfo(/*modulePaths*/ undefined, /*moduleSpecifiers*/ undefined, isAutoImportable));
80+
}
81+
},
82+
clear() {
83+
// containedNodeModulesWatchers?.forEach(watcher => watcher.close());
84+
cache?.clear();
85+
// containedNodeModulesWatchers?.clear();
86+
currentKey = undefined;
87+
},
88+
count() {
89+
return cache ? cache.size : 0;
90+
}
91+
};
92+
// if (Debug.isDebugging) {
93+
// Object.defineProperty(result, "__cache", { get: () => cache });
94+
// }
95+
return result;
96+
97+
function ensureCache(fromFileName: Path, preferences: UserPreferences) {
98+
const newKey = key(fromFileName, preferences);
99+
if (cache && (currentKey !== newKey)) {
100+
result.clear();
101+
}
102+
currentKey = newKey;
103+
return cache ||= new Map();
104+
}
105+
106+
function key(fromFileName: Path, preferences: UserPreferences) {
107+
return `${fromFileName},${preferences.importModuleSpecifierEnding},${preferences.importModuleSpecifierPreference}`;
108+
}
109+
110+
function createInfo(
111+
modulePaths: readonly ModulePath[] | undefined,
112+
moduleSpecifiers: readonly string[] | undefined,
113+
isAutoImportable: boolean | undefined,
114+
): ResolvedModuleSpecifierInfo {
115+
return { modulePaths, moduleSpecifiers, isAutoImportable };
116+
}
117+
}
+92
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
import type { Path, server } from 'typescript/lib/tsserverlibrary';
2+
3+
export const enum PackageJsonDependencyGroup {
4+
Dependencies = 1 << 0,
5+
DevDependencies = 1 << 1,
6+
PeerDependencies = 1 << 2,
7+
OptionalDependencies = 1 << 3,
8+
All = Dependencies | DevDependencies | PeerDependencies | OptionalDependencies,
9+
}
10+
11+
export interface PackageJsonInfo {
12+
fileName: string;
13+
parseable: boolean;
14+
dependencies?: Map<string, string>;
15+
devDependencies?: Map<string, string>;
16+
peerDependencies?: Map<string, string>;
17+
optionalDependencies?: Map<string, string>;
18+
get(dependencyName: string, inGroups?: PackageJsonDependencyGroup): string | undefined;
19+
has(dependencyName: string, inGroups?: PackageJsonDependencyGroup): boolean;
20+
}
21+
22+
export const enum Ternary {
23+
False = 0,
24+
Unknown = 1,
25+
Maybe = 3,
26+
True = -1
27+
}
28+
29+
type ProjectService = server.ProjectService;
30+
31+
export interface PackageJsonCache {
32+
addOrUpdate(fileName: Path): void;
33+
forEach(action: (info: PackageJsonInfo, fileName: Path) => void): void;
34+
delete(fileName: Path): void;
35+
get(fileName: Path): PackageJsonInfo | false | undefined;
36+
getInDirectory(directory: Path): PackageJsonInfo | undefined;
37+
directoryHasPackageJson(directory: Path): Ternary;
38+
searchDirectoryAndAncestors(directory: Path): void;
39+
}
40+
41+
export function createPackageJsonCache(
42+
ts: typeof import('typescript/lib/tsserverlibrary'),
43+
host: ProjectService,
44+
): PackageJsonCache {
45+
const { createPackageJsonInfo, getDirectoryPath, combinePaths, tryFileExists, forEachAncestorDirectory } = ts as any;
46+
const packageJsons = new Map<string, PackageJsonInfo>();
47+
const directoriesWithoutPackageJson = new Map<string, true>();
48+
return {
49+
addOrUpdate,
50+
// @ts-expect-error
51+
forEach: packageJsons.forEach.bind(packageJsons),
52+
get: packageJsons.get.bind(packageJsons),
53+
delete: fileName => {
54+
packageJsons.delete(fileName);
55+
directoriesWithoutPackageJson.set(getDirectoryPath(fileName), true);
56+
},
57+
getInDirectory: directory => {
58+
return packageJsons.get(combinePaths(directory, "package.json")) || undefined;
59+
},
60+
directoryHasPackageJson,
61+
searchDirectoryAndAncestors: directory => {
62+
// @ts-expect-error
63+
forEachAncestorDirectory(directory, ancestor => {
64+
if (directoryHasPackageJson(ancestor) !== Ternary.Maybe) {
65+
return true;
66+
}
67+
const packageJsonFileName = host.toPath(combinePaths(ancestor, "package.json"));
68+
if (tryFileExists(host, packageJsonFileName)) {
69+
addOrUpdate(packageJsonFileName);
70+
}
71+
else {
72+
directoriesWithoutPackageJson.set(ancestor, true);
73+
}
74+
});
75+
},
76+
};
77+
78+
function addOrUpdate(fileName: Path) {
79+
const packageJsonInfo =
80+
// Debug.checkDefined(
81+
createPackageJsonInfo(fileName, host.host)
82+
// );
83+
packageJsons.set(fileName, packageJsonInfo);
84+
directoriesWithoutPackageJson.delete(getDirectoryPath(fileName));
85+
}
86+
87+
function directoryHasPackageJson(directory: Path) {
88+
return packageJsons.has(combinePaths(directory, "package.json")) ? Ternary.True :
89+
directoriesWithoutPackageJson.has(directory) ? Ternary.False :
90+
Ternary.Maybe;
91+
}
92+
}

packages/shared/src/ts.ts

+61-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,67 @@
11
import * as fs from 'fs';
22
import * as path from 'upath';
33
import { normalizeFileName } from './path';
4+
import type * as ts from 'typescript/lib/tsserverlibrary';
5+
import { createModuleSpecifierCache } from './moduleSpecifierCache';
6+
import { createPackageJsonCache, PackageJsonInfo, Ternary } from './packageJsonCache';
7+
8+
export function addCacheLogicToLanguageServiceHost(
9+
ts: typeof import('typescript/lib/tsserverlibrary'),
10+
host: ts.LanguageServiceHost,
11+
service: ts.LanguageService,
12+
) {
13+
14+
const moduleSpecifierCache = createModuleSpecifierCache();
15+
const exportMapCache = (ts as any).createCacheableExportInfoMap({
16+
getCurrentProgram() {
17+
return service.getProgram()
18+
},
19+
getPackageJsonAutoImportProvider() {
20+
return service.getProgram()
21+
},
22+
});
23+
const packageJsonCache = createPackageJsonCache(ts, {
24+
...host,
25+
// @ts-expect-error
26+
host: { ...host },
27+
toPath,
28+
});
29+
30+
// @ts-expect-error
31+
host.getCachedExportInfoMap = () => exportMapCache;
32+
// @ts-expect-error
33+
host.getModuleSpecifierCache = () => moduleSpecifierCache;
34+
// @ts-expect-error
35+
host.getPackageJsonsVisibleToFile = (fileName: string, rootDir?: string) => {
36+
const rootPath = rootDir && toPath(rootDir);
37+
const filePath = toPath(fileName);
38+
const result: PackageJsonInfo[] = [];
39+
const processDirectory = (directory: ts.Path): boolean | undefined => {
40+
switch (packageJsonCache.directoryHasPackageJson(directory)) {
41+
// Sync and check same directory again
42+
case Ternary.Maybe:
43+
packageJsonCache.searchDirectoryAndAncestors(directory);
44+
return processDirectory(directory);
45+
// Check package.json
46+
case Ternary.True:
47+
const packageJsonFileName = (ts as any).combinePaths(directory, "package.json");
48+
// this.watchPackageJsonFile(packageJsonFileName as ts.Path); // TODO
49+
const info = packageJsonCache.getInDirectory(directory);
50+
if (info) result.push(info);
51+
}
52+
if (rootPath && rootPath === directory) {
53+
return true;
54+
}
55+
};
56+
57+
(ts as any).forEachAncestorDirectory((ts as any).getDirectoryPath(filePath), processDirectory);
58+
return result;
59+
};
60+
61+
function toPath(fileName: string) {
62+
return (ts as any).toPath(fileName, host.getCurrentDirectory(), (ts as any).createGetCanonicalFileName(host.useCaseSensitiveFileNames?.()));
63+
}
64+
}
465

566
export function getWorkspaceTypescriptPath(tsdk: string, workspaceFolderFsPaths: string[]) {
667
if (path.isAbsolute(tsdk)) {
@@ -108,8 +169,6 @@ export function getTypeScriptVersion(serverPath: string): string | undefined {
108169
return desc.version;
109170
}
110171

111-
export type { SourceFile as TsSourceFile } from 'typescript/lib/tsserverlibrary';
112-
113172
export function createParsedCommandLine(
114173
ts: typeof import('typescript/lib/tsserverlibrary'),
115174
parseConfigHost: ts.ParseConfigHost,

packages/vscode-vue-languageservice/src/languageService.ts

+4
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,10 @@ export function createLanguageService(
159159
const scriptTsHost = createTsLsHost('script');
160160
const templateTsLsRaw = ts.createLanguageService(templateTsHost);
161161
const scriptTsLsRaw = ts.createLanguageService(scriptTsHost);
162+
163+
shared.addCacheLogicToLanguageServiceHost(ts, templateTsHost, templateTsLsRaw);
164+
shared.addCacheLogicToLanguageServiceHost(ts, scriptTsHost, scriptTsLsRaw);
165+
162166
const templateTsLs = ts2.createLanguageService(ts, templateTsHost, templateTsLsRaw);
163167
const scriptTsLs = ts2.createLanguageService(ts, scriptTsHost, scriptTsLsRaw);
164168
const localTypesScript = ts.ScriptSnapshot.fromString(localTypes.getTypesCode(isVue2));

0 commit comments

Comments
 (0)