Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
perf: support auto-import cache for multiple TS version (#1406)
- Loading branch information
1 parent
ebc22aa
commit 54a7048
Showing
18 changed files
with
499 additions
and
100 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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" | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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
117
packages/typescript-faster/src/4_4/moduleSpecifierCache.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 }; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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; | ||
} | ||
} |
Oops, something went wrong.