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 commit 8ea4829587.

* 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 commit 8ea4829587.

* 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:
Andrew Branch
2019-09-27 13:38:31 -07:00
committed by GitHub
parent 558ece72cb
commit 304fcee09b
42 changed files with 1963 additions and 210 deletions

View File

@@ -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)) {

View 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;
}
}
}

View File

@@ -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 shouldnt get this far, but on the off chance the file was added or removed,
// we cant 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 doesnt 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> {

View File

@@ -576,7 +576,7 @@ namespace ts.server {
markContainingProjectsAsDirty() {
for (const p of this.containingProjects) {
p.markAsDirty();
p.markFileAsDirty(this.path);
}
}

View File

@@ -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"
]
}

View File

@@ -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"
}
}