Skip to content

Commit

Permalink
perf: support auto-import cache for multiple TS version (#1406)
Browse files Browse the repository at this point in the history
  • Loading branch information
johnsoncodehk committed Jun 6, 2022
1 parent ebc22aa commit 54a7048
Show file tree
Hide file tree
Showing 18 changed files with 499 additions and 100 deletions.
21 changes: 21 additions & 0 deletions packages/typescript-faster/LICENSE
@@ -0,0 +1,21 @@
MIT License

Copyright (c) 2021-present Johnson Chu

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
5 changes: 5 additions & 0 deletions packages/typescript-faster/README.md
@@ -0,0 +1,5 @@
# @volar/typescript-faster

TypeScript Language Service Completion API is slow when calculate auto-import.

This package make it faster by ported tsserver auto-import caching logic.
22 changes: 22 additions & 0 deletions packages/typescript-faster/package.json
@@ -0,0 +1,22 @@
{
"name": "@volar/typescript-faster",
"version": "0.37.0",
"main": "out/index.js",
"license": "MIT",
"files": [
"out/**/*.js",
"out/**/*.d.ts"
],
"repository": {
"type": "git",
"url": "https://github.com/johnsoncodehk/volar.git",
"directory": "packages/typescript-faster"
},
"devDependencies": {
"@types/semver": "^7.3.9",
"typescript": "latest"
},
"dependencies": {
"semver": "^7.3.7"
}
}
11 changes: 11 additions & 0 deletions packages/typescript-faster/src/4_0/index.ts
@@ -0,0 +1,11 @@
export default function (
ts: typeof import('typescript/lib/tsserverlibrary'),
host: ts.LanguageServiceHost,
service: ts.LanguageService,
) {
// @ts-expect-error
const importSuggestionsCache = ts.Completions?.createImportSuggestionsForFileCache?.();
// @ts-expect-error
// TODO: crash on 'addListener' from 'node:process', reuse because TS has same problem
host.getImportSuggestionsCache = () => importSuggestionsCache;
}
77 changes: 77 additions & 0 deletions packages/typescript-faster/src/4_4/index.ts
@@ -0,0 +1,77 @@
import { createModuleSpecifierCache } from './moduleSpecifierCache';
import { createPackageJsonCache, canCreatePackageJsonCache, PackageJsonInfo, Ternary } from './packageJsonCache';

export default function (
ts: typeof import('typescript/lib/tsserverlibrary'),
host: ts.LanguageServiceHost,
service: ts.LanguageService,
) {

const _createCacheableExportInfoMap = (ts as any).createCacheableExportInfoMap;
const _combinePaths = (ts as any).combinePaths;
const _forEachAncestorDirectory = (ts as any).forEachAncestorDirectory;
const _getDirectoryPath = (ts as any).getDirectoryPath;
const _toPath = (ts as any).toPath;
const _createGetCanonicalFileName = (ts as any).createGetCanonicalFileName;

if (
!_createCacheableExportInfoMap
|| !_combinePaths
|| !_forEachAncestorDirectory
|| !_getDirectoryPath
|| !_toPath
|| !_createGetCanonicalFileName
|| !canCreatePackageJsonCache(ts)
) return;

const moduleSpecifierCache = createModuleSpecifierCache();
const exportMapCache = _createCacheableExportInfoMap({
getCurrentProgram() {
return service.getProgram();
},
getPackageJsonAutoImportProvider() {
return service.getProgram();
},
});
const packageJsonCache = createPackageJsonCache(ts, {
...host,
// @ts-expect-error
host: { ...host },
toPath,
});

// @ts-expect-error
host.getCachedExportInfoMap = () => exportMapCache;
// @ts-expect-error
host.getModuleSpecifierCache = () => moduleSpecifierCache;
// @ts-expect-error
host.getPackageJsonsVisibleToFile = (fileName: string, rootDir?: string) => {
const rootPath = rootDir && toPath(rootDir);
const filePath = toPath(fileName);
const result: PackageJsonInfo[] = [];
const processDirectory = (directory: ts.Path): boolean | undefined => {
switch (packageJsonCache.directoryHasPackageJson(directory)) {
// Sync and check same directory again
case Ternary.Maybe:
packageJsonCache.searchDirectoryAndAncestors(directory);
return processDirectory(directory);
// Check package.json
case Ternary.True:
const packageJsonFileName = _combinePaths(directory, "package.json");
// this.watchPackageJsonFile(packageJsonFileName as ts.Path); // TODO
const info = packageJsonCache.getInDirectory(directory);
if (info) result.push(info);
}
if (rootPath && rootPath === directory) {
return true;
}
};

_forEachAncestorDirectory(_getDirectoryPath(filePath), processDirectory);
return result;
};

function toPath(fileName: string) {
return _toPath(fileName, host.getCurrentDirectory(), _createGetCanonicalFileName(host.useCaseSensitiveFileNames?.()));
}
}
117 changes: 117 additions & 0 deletions packages/typescript-faster/src/4_4/moduleSpecifierCache.ts
@@ -0,0 +1,117 @@
import type { Path, UserPreferences } from 'typescript/lib/tsserverlibrary';

export interface ModulePath {
path: string;
isInNodeModules: boolean;
isRedirect: boolean;
}

export interface ResolvedModuleSpecifierInfo {
modulePaths: readonly ModulePath[] | undefined;
moduleSpecifiers: readonly string[] | undefined;
isAutoImportable: boolean | undefined;
}

export interface ModuleSpecifierCache {
get(fromFileName: Path, toFileName: Path, preferences: UserPreferences): Readonly<ResolvedModuleSpecifierInfo> | undefined;
set(fromFileName: Path, toFileName: Path, preferences: UserPreferences, modulePaths: readonly ModulePath[], moduleSpecifiers: readonly string[]): void;
setIsAutoImportable(fromFileName: Path, toFileName: Path, preferences: UserPreferences, isAutoImportable: boolean): void;
setModulePaths(fromFileName: Path, toFileName: Path, preferences: UserPreferences, modulePaths: readonly ModulePath[]): void;
clear(): void;
count(): number;
}

// export interface ModuleSpecifierResolutionCacheHost {
// watchNodeModulesForPackageJsonChanges(directoryPath: string): FileWatcher;
// }

export function createModuleSpecifierCache(
// host: ModuleSpecifierResolutionCacheHost
): ModuleSpecifierCache {
// let containedNodeModulesWatchers: Map<string, FileWatcher> | undefined; // TODO
let cache: Map<Path, ResolvedModuleSpecifierInfo> | undefined;
let currentKey: string | undefined;
const result: ModuleSpecifierCache = {
get(fromFileName, toFileName, preferences) {
if (!cache || currentKey !== key(fromFileName, preferences)) return undefined;
return cache.get(toFileName);
},
set(fromFileName, toFileName, preferences, modulePaths, moduleSpecifiers) {
ensureCache(fromFileName, preferences).set(toFileName, createInfo(modulePaths, moduleSpecifiers, /*isAutoImportable*/ true));

// If any module specifiers were generated based off paths in node_modules,
// a package.json file in that package was read and is an input to the cached.
// Instead of watching each individual package.json file, set up a wildcard
// directory watcher for any node_modules referenced and clear the cache when
// it sees any changes.
if (moduleSpecifiers) {
for (const p of modulePaths) {
if (p.isInNodeModules) {
// No trailing slash
// const nodeModulesPath = p.path.substring(0, p.path.indexOf(nodeModulesPathPart) + nodeModulesPathPart.length - 1);
// if (!containedNodeModulesWatchers?.has(nodeModulesPath)) {
// (containedNodeModulesWatchers ||= new Map()).set(
// nodeModulesPath,
// host.watchNodeModulesForPackageJsonChanges(nodeModulesPath),
// );
// }
}
}
}
},
setModulePaths(fromFileName, toFileName, preferences, modulePaths) {
const cache = ensureCache(fromFileName, preferences);
const info = cache.get(toFileName);
if (info) {
info.modulePaths = modulePaths;
}
else {
cache.set(toFileName, createInfo(modulePaths, /*moduleSpecifiers*/ undefined, /*isAutoImportable*/ undefined));
}
},
setIsAutoImportable(fromFileName, toFileName, preferences, isAutoImportable) {
const cache = ensureCache(fromFileName, preferences);
const info = cache.get(toFileName);
if (info) {
info.isAutoImportable = isAutoImportable;
}
else {
cache.set(toFileName, createInfo(/*modulePaths*/ undefined, /*moduleSpecifiers*/ undefined, isAutoImportable));
}
},
clear() {
// containedNodeModulesWatchers?.forEach(watcher => watcher.close());
cache?.clear();
// containedNodeModulesWatchers?.clear();
currentKey = undefined;
},
count() {
return cache ? cache.size : 0;
}
};
// if (Debug.isDebugging) {
// Object.defineProperty(result, "__cache", { get: () => cache });
// }
return result;

function ensureCache(fromFileName: Path, preferences: UserPreferences) {
const newKey = key(fromFileName, preferences);
if (cache && (currentKey !== newKey)) {
result.clear();
}
currentKey = newKey;
return cache ||= new Map();
}

function key(fromFileName: Path, preferences: UserPreferences) {
return `${fromFileName},${preferences.importModuleSpecifierEnding},${preferences.importModuleSpecifierPreference}`;
}

function createInfo(
modulePaths: readonly ModulePath[] | undefined,
moduleSpecifiers: readonly string[] | undefined,
isAutoImportable: boolean | undefined,
): ResolvedModuleSpecifierInfo {
return { modulePaths, moduleSpecifiers, isAutoImportable };
}
}
96 changes: 96 additions & 0 deletions packages/typescript-faster/src/4_4/packageJsonCache.ts
@@ -0,0 +1,96 @@
import type { Path, server } from 'typescript/lib/tsserverlibrary';

export const enum PackageJsonDependencyGroup {
Dependencies = 1 << 0,
DevDependencies = 1 << 1,
PeerDependencies = 1 << 2,
OptionalDependencies = 1 << 3,
All = Dependencies | DevDependencies | PeerDependencies | OptionalDependencies,
}

export interface PackageJsonInfo {
fileName: string;
parseable: boolean;
dependencies?: Map<string, string>;
devDependencies?: Map<string, string>;
peerDependencies?: Map<string, string>;
optionalDependencies?: Map<string, string>;
get(dependencyName: string, inGroups?: PackageJsonDependencyGroup): string | undefined;
has(dependencyName: string, inGroups?: PackageJsonDependencyGroup): boolean;
}

export const enum Ternary {
False = 0,
Unknown = 1,
Maybe = 3,
True = -1
}

type ProjectService = server.ProjectService;

export interface PackageJsonCache {
addOrUpdate(fileName: Path): void;
forEach(action: (info: PackageJsonInfo, fileName: Path) => void): void;
delete(fileName: Path): void;
get(fileName: Path): PackageJsonInfo | false | undefined;
getInDirectory(directory: Path): PackageJsonInfo | undefined;
directoryHasPackageJson(directory: Path): Ternary;
searchDirectoryAndAncestors(directory: Path): void;
}

export function canCreatePackageJsonCache(ts: typeof import('typescript/lib/tsserverlibrary')) {
return 'createPackageJsonInfo' in ts && 'getDirectoryPath' in ts && 'combinePaths' in ts && 'tryFileExists' in ts && 'forEachAncestorDirectory' in ts;
}

export function createPackageJsonCache(
ts: typeof import('typescript/lib/tsserverlibrary'),
host: ProjectService,
): PackageJsonCache {
const { createPackageJsonInfo, getDirectoryPath, combinePaths, tryFileExists, forEachAncestorDirectory } = ts as any;
const packageJsons = new Map<string, PackageJsonInfo>();
const directoriesWithoutPackageJson = new Map<string, true>();
return {
addOrUpdate,
// @ts-expect-error
forEach: packageJsons.forEach.bind(packageJsons),
get: packageJsons.get.bind(packageJsons),
delete: fileName => {
packageJsons.delete(fileName);
directoriesWithoutPackageJson.set(getDirectoryPath(fileName), true);
},
getInDirectory: directory => {
return packageJsons.get(combinePaths(directory, "package.json")) || undefined;
},
directoryHasPackageJson,
searchDirectoryAndAncestors: directory => {
// @ts-expect-error
forEachAncestorDirectory(directory, ancestor => {
if (directoryHasPackageJson(ancestor) !== Ternary.Maybe) {
return true;
}
const packageJsonFileName = host.toPath(combinePaths(ancestor, "package.json"));
if (tryFileExists(host, packageJsonFileName)) {
addOrUpdate(packageJsonFileName);
}
else {
directoriesWithoutPackageJson.set(ancestor, true);
}
});
},
};

function addOrUpdate(fileName: Path) {
const packageJsonInfo =
// Debug.checkDefined(
createPackageJsonInfo(fileName, host.host);
// );
packageJsons.set(fileName, packageJsonInfo);
directoriesWithoutPackageJson.delete(getDirectoryPath(fileName));
}

function directoryHasPackageJson(directory: Path) {
return packageJsons.has(combinePaths(directory, "package.json")) ? Ternary.True :
directoriesWithoutPackageJson.has(directory) ? Ternary.False :
Ternary.Maybe;
}
}

0 comments on commit 54a7048

Please sign in to comment.