mirror of
https://github.com/microsoft/TypeScript.git
synced 2026-05-17 21:06:50 -05:00
Updated: Only auto-import from package.json (#32517)
* Move package.json related utils to utilities * Add failing test * Make first test pass * Don’t filter when there’s no package.json, fix scoped package imports * Use type acquisition as a heuristic for whether a JS project is using node core * Make same fix in getCompletionDetails * Fix re-exporting * Change JS node core module heuristic to same-file utilization * Remove unused method * Remove other unused method * Remove unused triple-slash ref * Update comment * Refactor findAlias to forEachAlias to reduce iterations * Really fix re-exporting * Use getModuleSpecifier instead of custom hack * Fix offering auto imports to paths within node modules * Rename things and make comments better * Add another reexport test * Inline `symbolHasBeenSeen` * Simplify forEachAlias to findAlias * Add note that symbols is mutated * Symbol order doesn’t matter here * Style nits * Add test with nested package.jsons * Fix and add tests for export * re-exports * Don’t fail when alias isn’t found * Make some easy optimizations * Clean up memoization when done * Remove unnecessary semicolon * Make getSymbolsFromOtherSourceFileExports pure * Cache auto imports * Revert "Cache auto imports" This reverts commit8ea4829587. * Handle merged symbols through cache * Be safer with symbol declarations, add logging * Improve cache invalidation for imports and exports * Check symbol presence first * Only run cache invalidation logic if there’s something to clear * Consolidate cache invalidation logic * Fix reuseProgramStructure test * Add more logging * Only clear cache if symbols are different * Refactor ambient module handling * Start caching package.json stuff * Support package.json searching in fourslash * Move import suggestions cache to Project * Start making more module specifier work available without having the importing file * Going to backtrack some from here * Get rid of dumb cache, fix node core modules stuff * Start determining changes to a file have invalidated its own auto imports * Move package.json related utils to utilities * Add failing test * Make first test pass * Don’t filter when there’s no package.json, fix scoped package imports * Use type acquisition as a heuristic for whether a JS project is using node core * Make same fix in getCompletionDetails * Fix re-exporting * Change JS node core module heuristic to same-file utilization * Remove unused method * Remove other unused method * Remove unused triple-slash ref * Update comment * Refactor findAlias to forEachAlias to reduce iterations * Really fix re-exporting * Use getModuleSpecifier instead of custom hack * Fix offering auto imports to paths within node modules * Rename things and make comments better * Add another reexport test * Inline `symbolHasBeenSeen` * Simplify forEachAlias to findAlias * Add note that symbols is mutated * Symbol order doesn’t matter here * Style nits * Add test with nested package.jsons * Fix and add tests for export * re-exports * Don’t fail when alias isn’t found * Make some easy optimizations * Clean up memoization when done * Remove unnecessary semicolon * Make getSymbolsFromOtherSourceFileExports pure * Cache auto imports * Revert "Cache auto imports" This reverts commit8ea4829587. * Handle merged symbols through cache * Be safer with symbol declarations, add logging * Improve cache invalidation for imports and exports * Check symbol presence first * Only run cache invalidation logic if there’s something to clear * Consolidate cache invalidation logic * Fix reuseProgramStructure test * Add more logging * Only clear cache if symbols are different * Refactor ambient module handling * Finish(?) sourceFileHasChangedOwnImportSuggestions * Make package.json info model better * Fix misplaced paren * Use file structure cache for package.json detection when possible * Revert unnecessary changes in moduleSpecifiers * Revert more unnecessary changes * Don’t watch package.jsons inside node_modules, fix tests * Work around declaration emit bug * Sync submodules? * Delete unused type * Add server cache tests * Fix server fourslash editing * Fix packageJsonInfo tests * Add node core modules cache test and fix more fourslash * Clean up symlink caching * Improve logging * Function name doesn’t make any sense anymore * Move symlinks cache to host * Fix markFileAsDirty from ScriptInfo * Mark new Project members internal * Use Path instead of fileName * Rename AutoImportSuggestionsCache * Improve WatchType description * Remove entries() from packageJsonCache * Fix path/fileName bug * Also cache symlinks on Program for benefit of d.ts emit * Let language service use Program’s symlink cache
This commit is contained in:
@@ -1005,7 +1005,7 @@ namespace ts.server {
|
||||
directory,
|
||||
fileOrDirectory => {
|
||||
const fileOrDirectoryPath = this.toPath(fileOrDirectory);
|
||||
project.getCachedDirectoryStructureHost().addOrDeleteFileOrDirectory(fileOrDirectory, fileOrDirectoryPath);
|
||||
const fsResult = project.getCachedDirectoryStructureHost().addOrDeleteFileOrDirectory(fileOrDirectory, fileOrDirectoryPath);
|
||||
|
||||
// don't trigger callback on open, existing files
|
||||
if (project.fileIsOpen(fileOrDirectoryPath)) {
|
||||
@@ -1015,6 +1015,13 @@ namespace ts.server {
|
||||
if (isPathIgnored(fileOrDirectoryPath)) return;
|
||||
const configFilename = project.getConfigFilePath();
|
||||
|
||||
if (getBaseFileName(fileOrDirectoryPath) === "package.json" && !isInsideNodeModules(fileOrDirectoryPath) &&
|
||||
(fsResult && fsResult.fileExists || !fsResult && this.host.fileExists(fileOrDirectoryPath))
|
||||
) {
|
||||
this.logger.info(`Project: ${configFilename} Detected new package.json: ${fileOrDirectory}`);
|
||||
project.onAddPackageJson(fileOrDirectoryPath);
|
||||
}
|
||||
|
||||
// If the the added or created file or directory is not supported file name, ignore the file
|
||||
// But when watched directory is added/removed, we need to reload the file list
|
||||
if (fileOrDirectoryPath !== directory && hasExtension(fileOrDirectoryPath) && !isSupportedSourceFileName(fileOrDirectory, project.getCompilationSettings(), this.hostConfiguration.extraFileExtensions)) {
|
||||
|
||||
54
src/server/packageJsonCache.ts
Normal file
54
src/server/packageJsonCache.ts
Normal file
@@ -0,0 +1,54 @@
|
||||
/*@internal*/
|
||||
namespace ts.server {
|
||||
export interface PackageJsonCache {
|
||||
addOrUpdate(fileName: Path): void;
|
||||
delete(fileName: Path): void;
|
||||
getInDirectory(directory: Path): PackageJsonInfo | undefined;
|
||||
directoryHasPackageJson(directory: Path): Ternary;
|
||||
searchDirectoryAndAncestors(directory: Path): void;
|
||||
}
|
||||
|
||||
export function createPackageJsonCache(project: Project): PackageJsonCache {
|
||||
const packageJsons = createMap<PackageJsonInfo>();
|
||||
const directoriesWithoutPackageJson = createMap<true>();
|
||||
return {
|
||||
addOrUpdate,
|
||||
delete: fileName => {
|
||||
packageJsons.delete(fileName);
|
||||
directoriesWithoutPackageJson.set(getDirectoryPath(fileName), true);
|
||||
},
|
||||
getInDirectory: directory => {
|
||||
return packageJsons.get(combinePaths(directory, "package.json"));
|
||||
},
|
||||
directoryHasPackageJson,
|
||||
searchDirectoryAndAncestors: directory => {
|
||||
forEachAncestorDirectory(directory, ancestor => {
|
||||
if (directoryHasPackageJson(ancestor) !== Ternary.Maybe) {
|
||||
return true;
|
||||
}
|
||||
const packageJsonFileName = project.toPath(combinePaths(ancestor, "package.json"));
|
||||
if (tryFileExists(project, packageJsonFileName)) {
|
||||
addOrUpdate(packageJsonFileName);
|
||||
}
|
||||
else {
|
||||
directoriesWithoutPackageJson.set(ancestor, true);
|
||||
}
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
function addOrUpdate(fileName: Path) {
|
||||
const packageJsonInfo = createPackageJsonInfo(fileName, project);
|
||||
if (packageJsonInfo) {
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -127,6 +127,9 @@ namespace ts.server {
|
||||
private generatedFilesMap: GeneratedFileWatcherMap | undefined;
|
||||
private plugins: PluginModuleWithName[] = [];
|
||||
|
||||
/*@internal*/
|
||||
private packageJsonFilesMap: Map<FileWatcher> | undefined;
|
||||
|
||||
/*@internal*/
|
||||
/**
|
||||
* This is map from files to unresolved imports in it
|
||||
@@ -234,6 +237,16 @@ namespace ts.server {
|
||||
/*@internal*/
|
||||
public readonly getCanonicalFileName: GetCanonicalFileName;
|
||||
|
||||
/*@internal*/
|
||||
readonly packageJsonCache: PackageJsonCache;
|
||||
|
||||
/*@internal*/
|
||||
private importSuggestionsCache = Completions.createImportSuggestionsForFileCache();
|
||||
/*@internal*/
|
||||
private dirtyFilesForSuggestions: Map<true> | undefined;
|
||||
/*@internal*/
|
||||
private symlinks: ReadonlyMap<string> | undefined;
|
||||
|
||||
/*@internal*/
|
||||
constructor(
|
||||
/*@internal*/ readonly projectName: string,
|
||||
@@ -284,6 +297,7 @@ namespace ts.server {
|
||||
}
|
||||
this.markAsDirty();
|
||||
this.projectService.pendingEnsureProjectForOpenFiles = true;
|
||||
this.packageJsonCache = createPackageJsonCache(this);
|
||||
}
|
||||
|
||||
isKnownTypesPackageName(name: string): boolean {
|
||||
@@ -297,6 +311,14 @@ namespace ts.server {
|
||||
return this.projectService.typingsCache;
|
||||
}
|
||||
|
||||
/*@internal*/
|
||||
getProbableSymlinks(files: readonly SourceFile[]): ReadonlyMap<string> {
|
||||
return this.symlinks || (this.symlinks = discoverProbableSymlinks(
|
||||
files,
|
||||
this.getCanonicalFileName,
|
||||
this.getCurrentDirectory()));
|
||||
}
|
||||
|
||||
// Method of LanguageServiceHost
|
||||
getCompilationSettings() {
|
||||
return this.compilerOptions;
|
||||
@@ -673,6 +695,10 @@ namespace ts.server {
|
||||
clearMap(this.missingFilesMap, closeFileWatcher);
|
||||
this.missingFilesMap = undefined!;
|
||||
}
|
||||
if (this.packageJsonFilesMap) {
|
||||
clearMap(this.packageJsonFilesMap, closeFileWatcher);
|
||||
this.packageJsonFilesMap = undefined;
|
||||
}
|
||||
this.clearGeneratedFileWatch();
|
||||
|
||||
// signal language service to release source files acquired from document registry
|
||||
@@ -847,6 +873,14 @@ namespace ts.server {
|
||||
(this.updatedFileNames || (this.updatedFileNames = createMap<true>())).set(fileName, true);
|
||||
}
|
||||
|
||||
/*@internal*/
|
||||
markFileAsDirty(changedFile: Path) {
|
||||
this.markAsDirty();
|
||||
if (!this.importSuggestionsCache.isEmpty()) {
|
||||
(this.dirtyFilesForSuggestions || (this.dirtyFilesForSuggestions = createMap())).set(changedFile, true);
|
||||
}
|
||||
}
|
||||
|
||||
markAsDirty() {
|
||||
if (!this.dirty) {
|
||||
this.projectStateVersion++;
|
||||
@@ -1008,6 +1042,29 @@ namespace ts.server {
|
||||
}
|
||||
}
|
||||
|
||||
if (!this.importSuggestionsCache.isEmpty()) {
|
||||
if (this.hasAddedorRemovedFiles || oldProgram && !oldProgram.structureIsReused) {
|
||||
this.importSuggestionsCache.clear();
|
||||
}
|
||||
else if (this.dirtyFilesForSuggestions && oldProgram && this.program) {
|
||||
forEachKey(this.dirtyFilesForSuggestions, fileName => {
|
||||
const oldSourceFile = oldProgram.getSourceFile(fileName);
|
||||
const sourceFile = this.program!.getSourceFile(fileName);
|
||||
if (this.sourceFileHasChangedOwnImportSuggestions(oldSourceFile, sourceFile)) {
|
||||
this.importSuggestionsCache.clear();
|
||||
return true;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
if (this.dirtyFilesForSuggestions) {
|
||||
this.dirtyFilesForSuggestions.clear();
|
||||
}
|
||||
|
||||
if (this.hasAddedorRemovedFiles) {
|
||||
this.symlinks = undefined;
|
||||
}
|
||||
|
||||
const oldExternalFiles = this.externalFiles || emptyArray as SortedReadonlyArray<string>;
|
||||
this.externalFiles = this.getExternalFiles();
|
||||
enumerateInsertsAndDeletes<string, string>(this.externalFiles, oldExternalFiles, getStringComparer(!this.useCaseSensitiveFileNames()),
|
||||
@@ -1031,6 +1088,54 @@ namespace ts.server {
|
||||
return hasNewProgram;
|
||||
}
|
||||
|
||||
/*@internal*/
|
||||
private sourceFileHasChangedOwnImportSuggestions(oldSourceFile: SourceFile | undefined, newSourceFile: SourceFile | undefined) {
|
||||
if (!oldSourceFile && !newSourceFile) {
|
||||
return false;
|
||||
}
|
||||
// Probably shouldn’t get this far, but on the off chance the file was added or removed,
|
||||
// we can’t reliably tell anything about it.
|
||||
if (!oldSourceFile || !newSourceFile) {
|
||||
return true;
|
||||
}
|
||||
|
||||
Debug.assertEqual(oldSourceFile.fileName, newSourceFile.fileName);
|
||||
// If ATA is enabled, auto-imports uses existing imports to guess whether you want auto-imports from node.
|
||||
// Adding or removing imports from node could change the outcome of that guess, so could change the suggestions list.
|
||||
if (this.getTypeAcquisition().enable && consumesNodeCoreModules(oldSourceFile) !== consumesNodeCoreModules(newSourceFile)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Module agumentation and ambient module changes can add or remove exports available to be auto-imported.
|
||||
// Changes elsewhere in the file can change the *type* of an export in a module augmentation,
|
||||
// but type info is gathered in getCompletionEntryDetails, which doesn’t use the cache.
|
||||
if (
|
||||
!arrayIsEqualTo(oldSourceFile.moduleAugmentations, newSourceFile.moduleAugmentations) ||
|
||||
!this.ambientModuleDeclarationsAreEqual(oldSourceFile, newSourceFile)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/*@internal*/
|
||||
private ambientModuleDeclarationsAreEqual(oldSourceFile: SourceFile, newSourceFile: SourceFile) {
|
||||
if (!arrayIsEqualTo(oldSourceFile.ambientModuleNames, newSourceFile.ambientModuleNames)) {
|
||||
return false;
|
||||
}
|
||||
let oldFileStatementIndex = -1;
|
||||
let newFileStatementIndex = -1;
|
||||
for (const ambientModuleName of newSourceFile.ambientModuleNames) {
|
||||
const isMatchingModuleDeclaration = (node: Statement) => isNonGlobalAmbientModule(node) && node.name.text === ambientModuleName;
|
||||
oldFileStatementIndex = findIndex(oldSourceFile.statements, isMatchingModuleDeclaration, oldFileStatementIndex + 1);
|
||||
newFileStatementIndex = findIndex(newSourceFile.statements, isMatchingModuleDeclaration, newFileStatementIndex + 1);
|
||||
if (oldSourceFile.statements[oldFileStatementIndex] !== newSourceFile.statements[newFileStatementIndex]) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private detachScriptInfoFromProject(uncheckedFileName: string, noRemoveResolution?: boolean) {
|
||||
const scriptInfoToDetach = this.projectService.getScriptInfo(uncheckedFileName);
|
||||
if (scriptInfoToDetach) {
|
||||
@@ -1341,6 +1446,71 @@ namespace ts.server {
|
||||
refreshDiagnostics() {
|
||||
this.projectService.sendProjectsUpdatedInBackgroundEvent();
|
||||
}
|
||||
|
||||
/*@internal*/
|
||||
getPackageJsonsVisibleToFile(fileName: string, rootDir?: string): readonly PackageJsonInfo[] {
|
||||
const packageJsonCache = this.packageJsonCache;
|
||||
const watchPackageJsonFile = this.watchPackageJsonFile.bind(this);
|
||||
const toPath = this.toPath.bind(this);
|
||||
const rootPath = rootDir && toPath(rootDir);
|
||||
const filePath = toPath(fileName);
|
||||
const result: PackageJsonInfo[] = [];
|
||||
forEachAncestorDirectory(getDirectoryPath(filePath), function processDirectory(directory): 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");
|
||||
watchPackageJsonFile(packageJsonFileName);
|
||||
result.push(Debug.assertDefined(packageJsonCache.getInDirectory(directory)));
|
||||
}
|
||||
if (rootPath && rootPath === toPath(directory)) {
|
||||
return true;
|
||||
}
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/*@internal*/
|
||||
onAddPackageJson(path: Path) {
|
||||
this.packageJsonCache.addOrUpdate(path);
|
||||
this.watchPackageJsonFile(path);
|
||||
}
|
||||
|
||||
/*@internal*/
|
||||
getImportSuggestionsCache() {
|
||||
return this.importSuggestionsCache;
|
||||
}
|
||||
|
||||
private watchPackageJsonFile(path: Path) {
|
||||
const watchers = this.packageJsonFilesMap || (this.packageJsonFilesMap = createMap());
|
||||
if (!watchers.has(path)) {
|
||||
watchers.set(path, this.projectService.watchFactory.watchFile(
|
||||
this.projectService.host,
|
||||
path,
|
||||
(fileName, eventKind) => {
|
||||
const path = this.toPath(fileName);
|
||||
switch (eventKind) {
|
||||
case FileWatcherEventKind.Created:
|
||||
return Debug.fail();
|
||||
case FileWatcherEventKind.Changed:
|
||||
this.packageJsonCache.addOrUpdate(path);
|
||||
break;
|
||||
case FileWatcherEventKind.Deleted:
|
||||
this.packageJsonCache.delete(path);
|
||||
watchers.get(path)!.close();
|
||||
watchers.delete(path);
|
||||
}
|
||||
},
|
||||
PollingInterval.Low,
|
||||
WatchType.PackageJsonFile,
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function getUnresolvedImports(program: Program, cachedUnresolvedImportsPerFile: Map<readonly string[]>): SortedReadonlyArray<string> {
|
||||
|
||||
@@ -576,7 +576,7 @@ namespace ts.server {
|
||||
|
||||
markContainingProjectsAsDirty() {
|
||||
for (const p of this.containingProjects) {
|
||||
p.markAsDirty();
|
||||
p.markFileAsDirty(this.path);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,27 +1,28 @@
|
||||
{
|
||||
"extends": "../tsconfig-base",
|
||||
"compilerOptions": {
|
||||
"removeComments": false,
|
||||
"outFile": "../../built/local/server.js",
|
||||
"preserveConstEnums": true,
|
||||
"types": [
|
||||
"node"
|
||||
]
|
||||
},
|
||||
"references": [
|
||||
{ "path": "../compiler" },
|
||||
{ "path": "../jsTyping" },
|
||||
{ "path": "../services" }
|
||||
],
|
||||
"files": [
|
||||
"types.ts",
|
||||
"utilities.ts",
|
||||
"protocol.ts",
|
||||
"scriptInfo.ts",
|
||||
"typingsCache.ts",
|
||||
"project.ts",
|
||||
"editorServices.ts",
|
||||
"session.ts",
|
||||
"scriptVersionCache.ts"
|
||||
]
|
||||
}
|
||||
{
|
||||
"extends": "../tsconfig-base",
|
||||
"compilerOptions": {
|
||||
"removeComments": false,
|
||||
"outFile": "../../built/local/server.js",
|
||||
"preserveConstEnums": true,
|
||||
"types": [
|
||||
"node"
|
||||
]
|
||||
},
|
||||
"references": [
|
||||
{ "path": "../compiler" },
|
||||
{ "path": "../jsTyping" },
|
||||
{ "path": "../services" }
|
||||
],
|
||||
"files": [
|
||||
"types.ts",
|
||||
"utilities.ts",
|
||||
"protocol.ts",
|
||||
"scriptInfo.ts",
|
||||
"typingsCache.ts",
|
||||
"project.ts",
|
||||
"editorServices.ts",
|
||||
"packageJsonCache.ts",
|
||||
"session.ts",
|
||||
"scriptVersionCache.ts"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -231,6 +231,7 @@ namespace ts {
|
||||
NodeModulesForClosedScriptInfo = "node_modules for closed script infos in them",
|
||||
MissingSourceMapFile = "Missing source map file",
|
||||
NoopConfigFileForInferredRoot = "Noop Config file for the inferred project root",
|
||||
MissingGeneratedFile = "Missing generated file"
|
||||
MissingGeneratedFile = "Missing generated file",
|
||||
PackageJsonFile = "package.json file for import suggestions"
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user