From 3ef80b62cf4bf5a6a5c93796891eb15f0c8e3bb0 Mon Sep 17 00:00:00 2001 From: Andrew Branch Date: Wed, 8 Dec 2021 17:09:18 -0800 Subject: [PATCH 01/12] Have auto-import provider pull in `exports` --- src/compiler/moduleNameResolver.ts | 134 +++++++++++++++++- src/server/project.ts | 66 +++++---- .../fourslash/server/autoImportProvider8.ts | 68 +++++++++ .../server/autoImportProvider_exportMap1.ts | 64 +++++++++ .../server/autoImportProvider_exportMap2.ts | 61 ++++++++ .../server/autoImportProvider_exportMap3.ts | 53 +++++++ 6 files changed, 415 insertions(+), 31 deletions(-) create mode 100644 tests/cases/fourslash/server/autoImportProvider8.ts create mode 100644 tests/cases/fourslash/server/autoImportProvider_exportMap1.ts create mode 100644 tests/cases/fourslash/server/autoImportProvider_exportMap2.ts create mode 100644 tests/cases/fourslash/server/autoImportProvider_exportMap3.ts diff --git a/src/compiler/moduleNameResolver.ts b/src/compiler/moduleNameResolver.ts index b1e3ffbdac6a3..ab539e3babf53 100644 --- a/src/compiler/moduleNameResolver.ts +++ b/src/compiler/moduleNameResolver.ts @@ -340,10 +340,7 @@ namespace ts { } const failedLookupLocations: string[] = []; - const features = - getEmitModuleResolutionKind(options) === ModuleResolutionKind.Node12 ? NodeResolutionFeatures.Node12Default : - getEmitModuleResolutionKind(options) === ModuleResolutionKind.NodeNext ? NodeResolutionFeatures.NodeNextDefault : - NodeResolutionFeatures.None; + const features = getDefaultNodeResolutionFeatures(options); const moduleResolutionState: ModuleResolutionState = { compilerOptions: options, host, traceEnabled, failedLookupLocations, packageJsonInfoCache: cache, features, conditions: ["node", "require", "types"] }; let resolved = primaryLookup(); let primary = true; @@ -433,6 +430,39 @@ namespace ts { } } + function getDefaultNodeResolutionFeatures(options: CompilerOptions) { + return getEmitModuleResolutionKind(options) === ModuleResolutionKind.Node12 ? NodeResolutionFeatures.Node12Default : + getEmitModuleResolutionKind(options) === ModuleResolutionKind.NodeNext ? NodeResolutionFeatures.NodeNextDefault : + NodeResolutionFeatures.None; + } + + /** Does not try `@types/${packageName}` - use a second pass if needed. */ + export function resolvePackageNameToPackageJson( + packageName: string, + containingDirectory: string, + options: CompilerOptions, + host: ModuleResolutionHost, + cache: ModuleResolutionCache | undefined, + ): PackageJsonInfo | undefined { + const moduleResolutionState: ModuleResolutionState = { + compilerOptions: options, + host, + traceEnabled: isTraceEnabled(options, host), + failedLookupLocations: [], + packageJsonInfoCache: cache?.getPackageJsonInfoCache(), + conditions: emptyArray, + features: NodeResolutionFeatures.None, + }; + + return forEachAncestorDirectory(containingDirectory, ancestorDirectory => { + if (getBaseFileName(ancestorDirectory) !== "node_modules") { + const nodeModulesFolder = combinePaths(ancestorDirectory, "node_modules"); + const candidate = combinePaths(nodeModulesFolder, packageName); + return getPackageJsonInfo(candidate, /*onlyRecordFailures*/ false, moduleResolutionState); + } + }); + } + /** * Given a set of options, returns the set of type directive names * that should be included for this program automatically. @@ -1536,6 +1566,102 @@ namespace ts { return withPackageId(packageInfo, loadNodeModuleFromDirectoryWorker(extensions, candidate, onlyRecordFailures, state, packageJsonContent, versionPaths)); } + export function loadEntrypointsFromPackageJsonInfo( + packageJsonInfo: PackageJsonInfo, + options: CompilerOptions, + host: ModuleResolutionHost, + cache: ModuleResolutionCache | undefined, + ): PathAndExtension[] | undefined { + let resolutions: PathAndExtension[] | undefined; + const features = getDefaultNodeResolutionFeatures(options); + const requireState: ModuleResolutionState = { + compilerOptions: options, + host, + traceEnabled: isTraceEnabled(options, host), + failedLookupLocations: [], + packageJsonInfoCache: cache?.getPackageJsonInfoCache(), + conditions: ["node", "require", "types"], + features, + }; + const requireResolution = loadNodeModuleFromDirectoryWorker( + Extensions.TypeScript, + packageJsonInfo.packageDirectory, + /*onlyRecordFailures*/ false, + requireState, + packageJsonInfo.packageJsonContent, + packageJsonInfo.versionPaths); + resolutions = append(resolutions, requireResolution); + + const importState = { ...requireState, failedLookupLocations: [], conditions: ["node", "import", "types"] }; + const importResolution = loadNodeModuleFromDirectoryWorker( + Extensions.TypeScript, + packageJsonInfo.packageDirectory, + /*onlyRecordFailures*/ false, + importState, + packageJsonInfo.packageJsonContent, + packageJsonInfo.versionPaths); + if (importResolution) { + resolutions = appendIfUnique(resolutions, importResolution, (a, b) => a?.path === b?.path); + } + + if (features & NodeResolutionFeatures.Exports && packageJsonInfo.packageJsonContent.exports) { + const exportState = { ...requireState, failedLookupLocations: [], conditions: ["node", "import", "require", "types"] }; + const exportResolutions = loadEntrypointsFromExportMap(packageJsonInfo, packageJsonInfo.packageJsonContent.exports, exportState); + if (exportResolutions) { + for (const resolution of exportResolutions) { + resolutions = appendIfUnique(resolutions, resolution, (a, b) => a?.path === b?.path); + } + } + } + + return resolutions; + } + + function loadEntrypointsFromExportMap(scope: PackageJsonInfo, exports: object, state: ModuleResolutionState): PathAndExtension[] | undefined { + let entrypoints: PathAndExtension[] | undefined; + // eslint-disable-next-line no-null/no-null + if (typeof exports === "object" && exports !== null && allKeysStartWithDot(exports as MapLike)) { + for (const key in exports) { + loadEntrypointsFromTargetExports((exports as MapLike)[key]); + } + } + else { + loadEntrypointsFromTargetExports(exports); + } + return entrypoints; + + function loadEntrypointsFromTargetExports(target: unknown) { + if (typeof target === "string" && startsWith(target, "./") && target.indexOf("*") === -1) { + const parts = getPathComponents(target).slice(1); + const partsAfterFirst = parts.slice(1); + if (partsAfterFirst.indexOf("..") >= 0 || partsAfterFirst.indexOf(".") >= 0 || partsAfterFirst.indexOf("node_modules") >= 0) { + return; + } + const resolvedTarget = combinePaths(scope.packageDirectory, target); + const finalPath = getNormalizedAbsolutePath(resolvedTarget, state.host.getCurrentDirectory?.()); + const result = loadJSOrExactTSFileName(Extensions.TypeScript, finalPath, /*recordOnlyFailures*/ false, state); + if (result) { + entrypoints = appendIfUnique(entrypoints, result, (a, b) => a.path === b.path); + } + } + // eslint-disable-next-line no-null/no-null + else if (typeof target === "object" && target !== null) { + if (Array.isArray(target)) { + for (const elem of target) { + loadEntrypointsFromTargetExports(elem); + } + } + else { + for (const key of getOwnKeys(target as MapLike)) { + if (key === "default" || contains(state.conditions, key) || isApplicableVersionedTypesKey(state.conditions, key)) { + loadEntrypointsFromTargetExports((target as MapLike)[key]); + } + } + } + } + } + } + /*@internal*/ interface PackageJsonInfo { packageDirectory: string; diff --git a/src/server/project.ts b/src/server/project.ts index e6170d086ce81..2cadb22fdbf40 100644 --- a/src/server/project.ts +++ b/src/server/project.ts @@ -1915,6 +1915,7 @@ namespace ts.server { return ts.emptyArray; } + let typesPackageNames: Set | undefined; let dependencyNames: Set | undefined; let rootNames: string[] | undefined; const rootFileName = combinePaths(hostProject.currentDirectory, inferredTypesContainingFile); @@ -1922,36 +1923,41 @@ namespace ts.server { for (const packageJson of packageJsons) { packageJson.dependencies?.forEach((_, dependenyName) => addDependency(dependenyName)); packageJson.peerDependencies?.forEach((_, dependencyName) => addDependency(dependencyName)); - } - - if (dependencyNames) { - const resolutions = mapDefined(arrayFrom(dependencyNames.keys()), name => { - const types = resolveTypeReferenceDirective( - name, - rootFileName, - compilerOptions, - moduleResolutionHost); - - if (types.resolvedTypeReferenceDirective) { - return types.resolvedTypeReferenceDirective; - } - if (compilerOptions.allowJs && compilerOptions.maxNodeModuleJsDepth) { - return tryResolveJSModule(name, hostProject.currentDirectory, moduleResolutionHost); + packageJson.devDependencies?.forEach((_, dependencyName) => { + if (startsWith(dependencyName, "@types")) { + witnessTypesPackage(dependencyName); } }); + } + if (dependencyNames) { const symlinkCache = hostProject.getSymlinkCache(); - for (const resolution of resolutions) { - if (!resolution.resolvedFileName) continue; - const { resolvedFileName, originalPath } = resolution; - if (originalPath) { - symlinkCache.setSymlinkedDirectoryFromSymlinkedFile(originalPath, resolvedFileName); - } - if (!program.getSourceFile(resolvedFileName) && (!originalPath || !program.getSourceFile(originalPath))) { - rootNames = append(rootNames, resolvedFileName); - // Avoid creating a large project that would significantly slow down time to editor interactivity - if (dependencySelection === PackageJsonAutoImportPreference.Auto && rootNames.length > this.maxDependencies) { - return ts.emptyArray; + for (let name of arrayFrom(dependencyNames.keys())) { + // Optimization: don't probe a non-@types package if a @types package for it was also a dependency. + // In all likelihood, the @types package exists because the non-@types package doesn't have types. + name = typesPackageNames?.has(name) ? `@types/${name}` : name; + const packageJson = resolvePackageNameToPackageJson(name, hostProject.currentDirectory, compilerOptions, moduleResolutionHost, program.getModuleResolutionCache()); + if (packageJson) { + const entrypoints = loadEntrypointsFromPackageJsonInfo(packageJson, compilerOptions, moduleResolutionHost, program.getModuleResolutionCache()); + if (entrypoints) { + const real = moduleResolutionHost.realpath?.(packageJson.packageDirectory); + if (real && real !== packageJson.packageDirectory) { + symlinkCache.setSymlinkedDirectory(packageJson.packageDirectory, { + real, + realPath: hostProject.toPath(real), + }); + } + + for (const entrypoint of entrypoints) { + // TODO: need to check if the realpath is in the program too? + if (!program.getSourceFile(entrypoint.path)) { + rootNames = append(rootNames, entrypoint.path); + // Avoid creating a large project that would significantly slow down time to editor interactivity + if (dependencySelection === PackageJsonAutoImportPreference.Auto && rootNames.length > this.maxDependencies) { + return ts.emptyArray; + } + } + } } } } @@ -1960,10 +1966,16 @@ namespace ts.server { return rootNames || ts.emptyArray; function addDependency(dependency: string) { - if (!startsWith(dependency, "@types/")) { + if (startsWith(dependency, "@types/")) { + witnessTypesPackage(dependency); + } + else { (dependencyNames || (dependencyNames = new Set())).add(dependency); } } + function witnessTypesPackage(dependency: string) { + (typesPackageNames || (typesPackageNames = new Set())).add(dependency.substring("@types/".length)); + } } /*@internal*/ diff --git a/tests/cases/fourslash/server/autoImportProvider8.ts b/tests/cases/fourslash/server/autoImportProvider8.ts new file mode 100644 index 0000000000000..28a29e28e9bb2 --- /dev/null +++ b/tests/cases/fourslash/server/autoImportProvider8.ts @@ -0,0 +1,68 @@ +/// + +// @Filename: /tsconfig.json +//// { "compilerOptions": { "module": "commonjs" } } + +// @Filename: /package.json +//// { "dependencies": { "mylib": "file:packages/mylib" } } + +// @Filename: /packages/mylib/package.json +//// { "name": "mylib", "version": "1.0.0" } + +// @Filename: /packages/mylib/index.ts +//// export * from "./mySubDir"; + +// @Filename: /packages/mylib/mySubDir/index.ts +//// export * from "./myClass"; +//// export * from "./myClass2"; + +// @Filename: /packages/mylib/mySubDir/myClass.ts +//// export class MyClass {} + +// @Filename: /packages/mylib/mySubDir/myClass2.ts +//// export class MyClass2 {} + +// @link: /packages/mylib -> /node_modules/mylib + +// @Filename: /src/index.ts +//// +//// const a = new MyClass/*1*/(); +//// const b = new MyClass2/*2*/(); + +goTo.marker("1"); +format.setOption("newLineCharacter", "\n"); + +verify.completions({ + marker: "1", + includes: [{ + name: "MyClass", + source: "mylib", + sourceDisplay: "mylib", + hasAction: true, + sortText: completion.SortText.AutoImportSuggestions, + }], + preferences: { + includeCompletionsForModuleExports: true, + includeInsertTextCompletions: true, + allowIncompleteCompletions: true, + } +}); + +verify.applyCodeActionFromCompletion("1", { + name: "MyClass", + source: "mylib", + description: `Import 'MyClass' from module "mylib"`, + data: { + exportName: "MyClass", + fileName: "/packages/mylib/index.ts", + }, + preferences: { + includeCompletionsForModuleExports: true, + includeCompletionsWithInsertText: true, + allowIncompleteCompletions: true, + }, + newFileContent: `import { MyClass } from "mylib"; + +const a = new MyClass(); +const b = new MyClass2();`, +}); diff --git a/tests/cases/fourslash/server/autoImportProvider_exportMap1.ts b/tests/cases/fourslash/server/autoImportProvider_exportMap1.ts new file mode 100644 index 0000000000000..7f76a8fae02f2 --- /dev/null +++ b/tests/cases/fourslash/server/autoImportProvider_exportMap1.ts @@ -0,0 +1,64 @@ +/// + +// @Filename: /tsconfig.json +//// { +//// "compilerOptions": { +//// "module": "nodenext" +//// } +//// } + +// @Filename: /package.json +//// { +//// "type": "module", +//// "dependencies": { +//// "dependency": "^1.0.0" +//// } +//// } + +// @Filename: /node_modules/dependency/package.json +//// { +//// "type": "module", +//// "name": "dependency", +//// "version": "1.0.0", +//// "exports": { +//// ".": { +//// "types": "./lib/index.d.ts" +//// }, +//// "./lol": { +//// "types": "./lib/lol.d.ts" +//// } +//// } +//// } + +// @Filename: /node_modules/dependency/lib/index.d.ts +//// export function fooFromIndex(): void; + +// @Filename: /node_modules/dependency/lib/lol.d.ts +//// export function fooFromLol(): void; + +// @Filename: /src/foo.ts +//// fooFrom/**/ + +goTo.marker(""); + +verify.completions({ + marker: "", + exact: completion.globalsPlus([{ + name: "fooFromIndex", + source: "dependency", + sourceDisplay: "dependency", + sortText: completion.SortText.AutoImportSuggestions, + hasAction: true, + }, { + name: "fooFromLol", + source: "dependency/lol", + sourceDisplay: "dependency/lol", + sortText: completion.SortText.AutoImportSuggestions, + hasAction: true, + }]), + preferences: { + includeCompletionsForModuleExports: true, + includeInsertTextCompletions: true, + allowIncompleteCompletions: true, + }, +}); \ No newline at end of file diff --git a/tests/cases/fourslash/server/autoImportProvider_exportMap2.ts b/tests/cases/fourslash/server/autoImportProvider_exportMap2.ts new file mode 100644 index 0000000000000..3d01e53006b6e --- /dev/null +++ b/tests/cases/fourslash/server/autoImportProvider_exportMap2.ts @@ -0,0 +1,61 @@ +/// + +// This one uses --module=commonjs, so the export map is not followed. + +// @Filename: /tsconfig.json +//// { +//// "compilerOptions": { +//// "module": "commonjs" +//// } +//// } + +// @Filename: /package.json +//// { +//// "type": "module", +//// "dependencies": { +//// "dependency": "^1.0.0" +//// } +//// } + +// @Filename: /node_modules/dependency/package.json +//// { +//// "type": "module", +//// "name": "dependency", +//// "version": "1.0.0", +//// "types": "./lib/index.d.ts", +//// "exports": { +//// ".": { +//// "types": "./lib/index.d.ts" +//// }, +//// "./lol": { +//// "types": "./lib/lol.d.ts" +//// } +//// } +//// } + +// @Filename: /node_modules/dependency/lib/index.d.ts +//// export function fooFromIndex(): void; + +// @Filename: /node_modules/dependency/lib/lol.d.ts +//// export function fooFromLol(): void; + +// @Filename: /src/foo.ts +//// fooFrom/**/ + +goTo.marker(""); + +verify.completions({ + marker: "", + exact: completion.globalsPlus([{ + name: "fooFromIndex", + source: "dependency", + sourceDisplay: "dependency", + sortText: completion.SortText.AutoImportSuggestions, + hasAction: true, + }]), + preferences: { + includeCompletionsForModuleExports: true, + includeInsertTextCompletions: true, + allowIncompleteCompletions: true, + }, +}); \ No newline at end of file diff --git a/tests/cases/fourslash/server/autoImportProvider_exportMap3.ts b/tests/cases/fourslash/server/autoImportProvider_exportMap3.ts new file mode 100644 index 0000000000000..c85bd000817d8 --- /dev/null +++ b/tests/cases/fourslash/server/autoImportProvider_exportMap3.ts @@ -0,0 +1,53 @@ +/// + +// String exports + +// @Filename: /tsconfig.json +//// { +//// "compilerOptions": { +//// "module": "nodenext" +//// } +//// } + +// @Filename: /package.json +//// { +//// "type": "module", +//// "dependencies": { +//// "dependency": "^1.0.0" +//// } +//// } + +// @Filename: /node_modules/dependency/package.json +//// { +//// "name": "dependency", +//// "version": "1.0.0", +//// "main": "./lib/index.js", +//// "exports": "./lib/lol.d.ts" +//// } + +// @Filename: /node_modules/dependency/lib/index.d.ts +//// export function fooFromIndex(): void; + +// @Filename: /node_modules/dependency/lib/lol.d.ts +//// export function fooFromLol(): void; + +// @Filename: /src/foo.ts +//// fooFrom/**/ + +goTo.marker(""); + +verify.completions({ + marker: "", + exact: completion.globalsPlus([{ + name: "fooFromLol", + source: "dependency", + sourceDisplay: "dependency", + sortText: completion.SortText.AutoImportSuggestions, + hasAction: true, + }]), + preferences: { + includeCompletionsForModuleExports: true, + includeInsertTextCompletions: true, + allowIncompleteCompletions: true, + }, +}); \ No newline at end of file From 32b5f7d443cb5cbac3ca366c5b2874bf829aa62a Mon Sep 17 00:00:00 2001 From: Andrew Branch Date: Wed, 8 Dec 2021 17:10:18 -0800 Subject: [PATCH 02/12] Revert filtering of node_modules relative paths, to do in separate PR --- .../server/autoImportProvider_exportMap3.ts | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/tests/cases/fourslash/server/autoImportProvider_exportMap3.ts b/tests/cases/fourslash/server/autoImportProvider_exportMap3.ts index c85bd000817d8..e4e70c749fefa 100644 --- a/tests/cases/fourslash/server/autoImportProvider_exportMap3.ts +++ b/tests/cases/fourslash/server/autoImportProvider_exportMap3.ts @@ -39,6 +39,16 @@ goTo.marker(""); verify.completions({ marker: "", exact: completion.globalsPlus([{ + // TODO: We should filter this one out due to its bad module specifier, + // but we don't know it's going to be filtered out until we actually + // resolve the module specifier, which is a problem for completions + // that don't have their module specifiers eagerly resolved. + name: "fooFromIndex", + source: "../node_modules/dependency/lib/index.js", + sourceDisplay: "../node_modules/dependency/lib/index.js", + sortText: completion.SortText.AutoImportSuggestions, + hasAction: true, + }, { name: "fooFromLol", source: "dependency", sourceDisplay: "dependency", @@ -50,4 +60,4 @@ verify.completions({ includeInsertTextCompletions: true, allowIncompleteCompletions: true, }, -}); \ No newline at end of file +}); From eb4ec8b6ec8d5b46a58531143cd07b6ceb6bb7fa Mon Sep 17 00:00:00 2001 From: Andrew Branch Date: Thu, 9 Dec 2021 12:14:59 -0800 Subject: [PATCH 03/12] Do @types and JS prioritization correctly --- src/compiler/moduleNameResolver.ts | 46 +++++--- src/compiler/moduleSpecifiers.ts | 8 +- src/server/project.ts | 108 ++++++++++++------ .../server/autoImportProvider_exportMap4.ts | 56 +++++++++ .../server/autoImportProvider_exportMap5.ts | 79 +++++++++++++ .../server/autoImportProvider_exportMap6.ts | 88 ++++++++++++++ 6 files changed, 329 insertions(+), 56 deletions(-) create mode 100644 tests/cases/fourslash/server/autoImportProvider_exportMap4.ts create mode 100644 tests/cases/fourslash/server/autoImportProvider_exportMap5.ts create mode 100644 tests/cases/fourslash/server/autoImportProvider_exportMap6.ts diff --git a/src/compiler/moduleNameResolver.ts b/src/compiler/moduleNameResolver.ts index ab539e3babf53..933f173e53e11 100644 --- a/src/compiler/moduleNameResolver.ts +++ b/src/compiler/moduleNameResolver.ts @@ -1571,8 +1571,10 @@ namespace ts { options: CompilerOptions, host: ModuleResolutionHost, cache: ModuleResolutionCache | undefined, + resolveJs?: boolean, ): PathAndExtension[] | undefined { let resolutions: PathAndExtension[] | undefined; + const extensions = resolveJs ? Extensions.JavaScript : Extensions.TypeScript; const features = getDefaultNodeResolutionFeatures(options); const requireState: ModuleResolutionState = { compilerOptions: options, @@ -1584,7 +1586,7 @@ namespace ts { features, }; const requireResolution = loadNodeModuleFromDirectoryWorker( - Extensions.TypeScript, + extensions, packageJsonInfo.packageDirectory, /*onlyRecordFailures*/ false, requireState, @@ -1594,7 +1596,7 @@ namespace ts { const importState = { ...requireState, failedLookupLocations: [], conditions: ["node", "import", "types"] }; const importResolution = loadNodeModuleFromDirectoryWorker( - Extensions.TypeScript, + extensions, packageJsonInfo.packageDirectory, /*onlyRecordFailures*/ false, importState, @@ -1606,7 +1608,11 @@ namespace ts { if (features & NodeResolutionFeatures.Exports && packageJsonInfo.packageJsonContent.exports) { const exportState = { ...requireState, failedLookupLocations: [], conditions: ["node", "import", "require", "types"] }; - const exportResolutions = loadEntrypointsFromExportMap(packageJsonInfo, packageJsonInfo.packageJsonContent.exports, exportState); + const exportResolutions = loadEntrypointsFromExportMap( + packageJsonInfo, + packageJsonInfo.packageJsonContent.exports, + exportState, + extensions); if (exportResolutions) { for (const resolution of exportResolutions) { resolutions = appendIfUnique(resolutions, resolution, (a, b) => a?.path === b?.path); @@ -1617,10 +1623,20 @@ namespace ts { return resolutions; } - function loadEntrypointsFromExportMap(scope: PackageJsonInfo, exports: object, state: ModuleResolutionState): PathAndExtension[] | undefined { + function loadEntrypointsFromExportMap( + scope: PackageJsonInfo, + exports: object, + state: ModuleResolutionState, + extensions: Extensions, + ): PathAndExtension[] | undefined { let entrypoints: PathAndExtension[] | undefined; + if (isArray(exports)) { + for (const target of exports) { + loadEntrypointsFromTargetExports(target); + } + } // eslint-disable-next-line no-null/no-null - if (typeof exports === "object" && exports !== null && allKeysStartWithDot(exports as MapLike)) { + else if (typeof exports === "object" && exports !== null && allKeysStartWithDot(exports as MapLike)) { for (const key in exports) { loadEntrypointsFromTargetExports((exports as MapLike)[key]); } @@ -1639,25 +1655,19 @@ namespace ts { } const resolvedTarget = combinePaths(scope.packageDirectory, target); const finalPath = getNormalizedAbsolutePath(resolvedTarget, state.host.getCurrentDirectory?.()); - const result = loadJSOrExactTSFileName(Extensions.TypeScript, finalPath, /*recordOnlyFailures*/ false, state); + const result = loadJSOrExactTSFileName(extensions, finalPath, /*recordOnlyFailures*/ false, state); if (result) { entrypoints = appendIfUnique(entrypoints, result, (a, b) => a.path === b.path); } } // eslint-disable-next-line no-null/no-null - else if (typeof target === "object" && target !== null) { - if (Array.isArray(target)) { - for (const elem of target) { - loadEntrypointsFromTargetExports(elem); + else if (typeof target === "object" && target !== null && !Array.isArray(target)) { + forEach(getOwnKeys(target as MapLike), key => { + if (key === "default" || contains(state.conditions, key) || isApplicableVersionedTypesKey(state.conditions, key)) { + loadEntrypointsFromTargetExports((target as MapLike)[key]); + return true; } - } - else { - for (const key of getOwnKeys(target as MapLike)) { - if (key === "default" || contains(state.conditions, key) || isApplicableVersionedTypesKey(state.conditions, key)) { - loadEntrypointsFromTargetExports((target as MapLike)[key]); - } - } - } + }); } } } diff --git a/src/compiler/moduleSpecifiers.ts b/src/compiler/moduleSpecifiers.ts index cb2bad2e27166..4b36113ba60ed 100644 --- a/src/compiler/moduleSpecifiers.ts +++ b/src/compiler/moduleSpecifiers.ts @@ -708,9 +708,13 @@ namespace ts.moduleSpecifiers { const packageJsonContent = JSON.parse(host.readFile!(packageJsonPath)!); // TODO: Inject `require` or `import` condition based on the intended import mode if (getEmitModuleResolutionKind(options) === ModuleResolutionKind.Node12 || getEmitModuleResolutionKind(options) === ModuleResolutionKind.NodeNext) { - const fromExports = packageJsonContent.exports && typeof packageJsonContent.name === "string" ? tryGetModuleNameFromExports(options, path, packageRootPath, packageJsonContent.name, packageJsonContent.exports, ["node", "types"]) : undefined; + const fromExports = packageJsonContent.exports && typeof packageJsonContent.name === "string" + ? tryGetModuleNameFromExports(options, path, packageRootPath, getPackageNameFromTypesPackageName(packageJsonContent.name), packageJsonContent.exports, ["node", "types"]) + : undefined; if (fromExports) { - const withJsExtension = !hasTSFileExtension(fromExports.moduleFileToTry) ? fromExports : { moduleFileToTry: removeFileExtension(fromExports.moduleFileToTry) + tryGetJSExtensionForFile(fromExports.moduleFileToTry, options) }; + const withJsExtension = !hasTSFileExtension(fromExports.moduleFileToTry) + ? fromExports + : { moduleFileToTry: removeFileExtension(fromExports.moduleFileToTry) + tryGetJSExtensionForFile(fromExports.moduleFileToTry, options) }; return { ...withJsExtension, verbatimFromExports: true }; } if (packageJsonContent.exports) { diff --git a/src/server/project.ts b/src/server/project.ts index 2cadb22fdbf40..88a1413bc98fb 100644 --- a/src/server/project.ts +++ b/src/server/project.ts @@ -1915,7 +1915,6 @@ namespace ts.server { return ts.emptyArray; } - let typesPackageNames: Set | undefined; let dependencyNames: Set | undefined; let rootNames: string[] | undefined; const rootFileName = combinePaths(hostProject.currentDirectory, inferredTypesContainingFile); @@ -1923,58 +1922,95 @@ namespace ts.server { for (const packageJson of packageJsons) { packageJson.dependencies?.forEach((_, dependenyName) => addDependency(dependenyName)); packageJson.peerDependencies?.forEach((_, dependencyName) => addDependency(dependencyName)); - packageJson.devDependencies?.forEach((_, dependencyName) => { - if (startsWith(dependencyName, "@types")) { - witnessTypesPackage(dependencyName); - } - }); } if (dependencyNames) { + let dependenciesAdded = 0; const symlinkCache = hostProject.getSymlinkCache(); - for (let name of arrayFrom(dependencyNames.keys())) { - // Optimization: don't probe a non-@types package if a @types package for it was also a dependency. - // In all likelihood, the @types package exists because the non-@types package doesn't have types. - name = typesPackageNames?.has(name) ? `@types/${name}` : name; - const packageJson = resolvePackageNameToPackageJson(name, hostProject.currentDirectory, compilerOptions, moduleResolutionHost, program.getModuleResolutionCache()); + for (const name of arrayFrom(dependencyNames.keys())) { + // Avoid creating a large project that would significantly slow down time to editor interactivity + if (dependencySelection === PackageJsonAutoImportPreference.Auto && dependenciesAdded > this.maxDependencies) { + hostProject.log(`Auto-import provider attempted to add more than ${this.maxDependencies} dependencies.`); + return ts.emptyArray; + } + + // 1. Try to load from the implementation package. For many dependencies, the + // package.json will exist, but the package will not contain any typings, + // so `entrypoints` will be undefined. In that case, or if the dependency + // is missing altogether, we will move on to trying the @types package (2). + const packageJson = resolvePackageNameToPackageJson( + name, + hostProject.currentDirectory, + compilerOptions, + moduleResolutionHost, + program.getModuleResolutionCache()); if (packageJson) { - const entrypoints = loadEntrypointsFromPackageJsonInfo(packageJson, compilerOptions, moduleResolutionHost, program.getModuleResolutionCache()); + const entrypoints = getRootNamesFromPackageJson(packageJson, program, symlinkCache); if (entrypoints) { - const real = moduleResolutionHost.realpath?.(packageJson.packageDirectory); - if (real && real !== packageJson.packageDirectory) { - symlinkCache.setSymlinkedDirectory(packageJson.packageDirectory, { - real, - realPath: hostProject.toPath(real), - }); - } - - for (const entrypoint of entrypoints) { - // TODO: need to check if the realpath is in the program too? - if (!program.getSourceFile(entrypoint.path)) { - rootNames = append(rootNames, entrypoint.path); - // Avoid creating a large project that would significantly slow down time to editor interactivity - if (dependencySelection === PackageJsonAutoImportPreference.Auto && rootNames.length > this.maxDependencies) { - return ts.emptyArray; - } - } - } + rootNames = concatenate(rootNames, entrypoints); + dependenciesAdded += entrypoints.length ? 1 : 0; + continue; } } + + // 2. Try to load from the @types package. + const typesPackageJson = resolvePackageNameToPackageJson( + `@types/${name}`, + hostProject.currentDirectory, + compilerOptions, + moduleResolutionHost, + program.getModuleResolutionCache()); + if (typesPackageJson) { + const entrypoints = getRootNamesFromPackageJson(typesPackageJson, program, symlinkCache); + rootNames = concatenate(rootNames, entrypoints); + dependenciesAdded += entrypoints?.length ? 1 : 0; + continue; + } + + // 3. If the @types package did not exist and the user has settings that + // allow processing JS from node_modules, go back to the implementation + // package and load the JS. + if (packageJson && compilerOptions.allowJs && compilerOptions.maxNodeModuleJsDepth) { + const entrypoints = getRootNamesFromPackageJson(packageJson, program, symlinkCache, /*allowJs*/ true); + rootNames = concatenate(rootNames, entrypoints); + dependenciesAdded += entrypoints?.length ? 1 : 0; + } } } return rootNames || ts.emptyArray; function addDependency(dependency: string) { - if (startsWith(dependency, "@types/")) { - witnessTypesPackage(dependency); - } - else { + if (!startsWith(dependency, "@types/")) { (dependencyNames || (dependencyNames = new Set())).add(dependency); } } - function witnessTypesPackage(dependency: string) { - (typesPackageNames || (typesPackageNames = new Set())).add(dependency.substring("@types/".length)); + + type PackageJsonInfo = Exclude, undefined>; + function getRootNamesFromPackageJson(packageJson: PackageJsonInfo, program: Program, symlinkCache: SymlinkCache, resolveJs?: boolean) { + const entrypoints = loadEntrypointsFromPackageJsonInfo( + packageJson, + compilerOptions, + moduleResolutionHost, + program.getModuleResolutionCache(), + resolveJs); + if (entrypoints) { + const real = moduleResolutionHost.realpath?.(packageJson.packageDirectory); + const isSymlink = real && real !== packageJson.packageDirectory; + if (isSymlink) { + symlinkCache.setSymlinkedDirectory(packageJson.packageDirectory, { + real, + realPath: hostProject.toPath(real), + }); + } + + return mapDefined(entrypoints, entrypoint => { + const resolvedFileName = isSymlink ? entrypoint.path.replace(packageJson.packageDirectory, real) : entrypoint.path; + if (!program.getSourceFile(resolvedFileName) && (!isSymlink || !program.getSourceFile(entrypoint.path))) { + return resolvedFileName; + } + }); + } } } diff --git a/tests/cases/fourslash/server/autoImportProvider_exportMap4.ts b/tests/cases/fourslash/server/autoImportProvider_exportMap4.ts new file mode 100644 index 0000000000000..638bbf70b023b --- /dev/null +++ b/tests/cases/fourslash/server/autoImportProvider_exportMap4.ts @@ -0,0 +1,56 @@ +/// + +// Top-level conditions + +// @Filename: /tsconfig.json +//// { +//// "compilerOptions": { +//// "module": "nodenext" +//// } +//// } + +// @Filename: /package.json +//// { +//// "type": "module", +//// "dependencies": { +//// "dependency": "^1.0.0" +//// } +//// } + +// @Filename: /node_modules/dependency/package.json +//// { +//// "type": "module", +//// "name": "dependency", +//// "version": "1.0.0", +//// "exports": { +//// "types": "./lib/index.d.ts", +//// "require": "./lib/lol.js" +//// } +//// } + +// @Filename: /node_modules/dependency/lib/index.d.ts +//// export function fooFromIndex(): void; + +// @Filename: /node_modules/dependency/lib/lol.d.ts +//// export function fooFromLol(): void; + +// @Filename: /src/foo.ts +//// fooFrom/**/ + +goTo.marker(""); + +verify.completions({ + marker: "", + exact: completion.globalsPlus([{ + name: "fooFromIndex", + source: "dependency", + sourceDisplay: "dependency", + sortText: completion.SortText.AutoImportSuggestions, + hasAction: true, + }]), + preferences: { + includeCompletionsForModuleExports: true, + includeInsertTextCompletions: true, + allowIncompleteCompletions: true, + }, +}); \ No newline at end of file diff --git a/tests/cases/fourslash/server/autoImportProvider_exportMap5.ts b/tests/cases/fourslash/server/autoImportProvider_exportMap5.ts new file mode 100644 index 0000000000000..7bc8946f7f027 --- /dev/null +++ b/tests/cases/fourslash/server/autoImportProvider_exportMap5.ts @@ -0,0 +1,79 @@ +/// + +// @types package lookup + +// @Filename: /tsconfig.json +//// { +//// "compilerOptions": { +//// "module": "nodenext" +//// } +//// } + +// @Filename: /package.json +//// { +//// "type": "module", +//// "dependencies": { +//// "dependency": "^1.0.0" +//// } +//// } + +// @Filename: /node_modules/dependency/package.json +//// { +//// "type": "module", +//// "name": "dependency", +//// "version": "1.0.0", +//// "exports": { +//// ".": "./lib/index.js", +//// "./lol": "./lib/lol.js" +//// } +//// } + +// @Filename: /node_modules/dependency/lib/index.js +//// export function fooFromIndex() {} + +// @Filename: /node_modules/dependency/lib/lol.js +//// export function fooFromLol() {} + +// @Filename: /node_modules/@types/dependency/package.json +//// { +//// "type": "module", +//// "name": "@types/dependency", +//// "version": "1.0.0", +//// "exports": { +//// ".": "./lib/index.d.ts", +//// "./lol": "./lib/lol.d.ts" +//// } +//// } + +// @Filename: /node_modules/@types/dependency/lib/index.d.ts +//// export declare function fooFromIndex(): void; + +// @Filename: /node_modules/@types/dependency/lib/lol.d.ts +//// export declare function fooFromLol(): void; + +// @Filename: /src/foo.ts +//// fooFrom/**/ + +goTo.marker(""); + +verify.completions({ + marker: "", + exact: completion.globalsPlus([{ + name: "fooFromIndex", + source: "dependency", + sourceDisplay: "dependency", + sortText: completion.SortText.AutoImportSuggestions, + hasAction: true, + }, { + name: "fooFromLol", + source: "dependency/lol", + sourceDisplay: "dependency/lol", + sortText: completion.SortText.AutoImportSuggestions, + hasAction: true, + }]), + preferences: { + includeCompletionsForModuleExports: true, + includeInsertTextCompletions: true, + allowIncompleteCompletions: true, + }, +}); diff --git a/tests/cases/fourslash/server/autoImportProvider_exportMap6.ts b/tests/cases/fourslash/server/autoImportProvider_exportMap6.ts new file mode 100644 index 0000000000000..19a32344ae60d --- /dev/null +++ b/tests/cases/fourslash/server/autoImportProvider_exportMap6.ts @@ -0,0 +1,88 @@ +/// + +// @types package should be ignored because implementation package has types + +// @Filename: /tsconfig.json +//// { +//// "compilerOptions": { +//// "module": "nodenext" +//// } +//// } + +// @Filename: /package.json +//// { +//// "type": "module", +//// "dependencies": { +//// "dependency": "^1.0.0" +//// }, +//// "devDependencies": { +//// "@types/dependency": "^1.0.0" +//// } +//// } + +// @Filename: /node_modules/dependency/package.json +//// { +//// "type": "module", +//// "name": "dependency", +//// "version": "1.0.0", +//// "exports": { +//// ".": "./lib/index.js", +//// "./lol": "./lib/lol.js" +//// } +//// } + +// @Filename: /node_modules/dependency/lib/index.js +//// export function fooFromIndex() {} + +// @Filename: /node_modules/dependency/lib/index.d.ts +//// export declare function fooFromIndex(): void + +// @Filename: /node_modules/dependency/lib/lol.js +//// export function fooFromLol() {} + +// @Filename: /node_modules/dependency/lib/lol.d.ts +//// export declare function fooFromLol(): void + +// @Filename: /node_modules/@types/dependency/package.json +//// { +//// "type": "module", +//// "name": "@types/dependency", +//// "version": "1.0.0", +//// "exports": { +//// ".": "./lib/index.d.ts", +//// "./lol": "./lib/lol.d.ts" +//// } +//// } + +// @Filename: /node_modules/@types/dependency/lib/index.d.ts +//// export declare function fooFromAtTypesIndex(): void; + +// @Filename: /node_modules/@types/dependency/lib/lol.d.ts +//// export declare function fooFromAtTypesLol(): void; + +// @Filename: /src/foo.ts +//// fooFrom/**/ + +goTo.marker(""); + +verify.completions({ + marker: "", + exact: completion.globalsPlus([{ + name: "fooFromIndex", + source: "dependency", + sourceDisplay: "dependency", + sortText: completion.SortText.AutoImportSuggestions, + hasAction: true, + }, { + name: "fooFromLol", + source: "dependency/lol", + sourceDisplay: "dependency/lol", + sortText: completion.SortText.AutoImportSuggestions, + hasAction: true, + }]), + preferences: { + includeCompletionsForModuleExports: true, + includeInsertTextCompletions: true, + allowIncompleteCompletions: true, + }, +}); From 6a78118075a6b39ecfdefe80bfb9ad70ed41d483 Mon Sep 17 00:00:00 2001 From: Andrew Branch Date: Thu, 9 Dec 2021 14:32:40 -0800 Subject: [PATCH 04/12] Cache entrypoints on PackageJsonInfo --- src/compiler/moduleNameResolver.ts | 40 +++++++++---------- src/compiler/utilities.ts | 12 ------ src/server/project.ts | 6 +-- .../unittests/tsserver/symlinkCache.ts | 7 +++- 4 files changed, 29 insertions(+), 36 deletions(-) diff --git a/src/compiler/moduleNameResolver.ts b/src/compiler/moduleNameResolver.ts index 933f173e53e11..fb01aeff2f3ed 100644 --- a/src/compiler/moduleNameResolver.ts +++ b/src/compiler/moduleNameResolver.ts @@ -436,7 +436,10 @@ namespace ts { NodeResolutionFeatures.None; } - /** Does not try `@types/${packageName}` - use a second pass if needed. */ + /** + * @internal + * Does not try `@types/${packageName}` - use a second pass if needed. + */ export function resolvePackageNameToPackageJson( packageName: string, containingDirectory: string, @@ -1566,14 +1569,21 @@ namespace ts { return withPackageId(packageInfo, loadNodeModuleFromDirectoryWorker(extensions, candidate, onlyRecordFailures, state, packageJsonContent, versionPaths)); } - export function loadEntrypointsFromPackageJsonInfo( + /* @internal */ + export function getEntrypointsFromPackageJsonInfo( packageJsonInfo: PackageJsonInfo, options: CompilerOptions, host: ModuleResolutionHost, cache: ModuleResolutionCache | undefined, resolveJs?: boolean, - ): PathAndExtension[] | undefined { - let resolutions: PathAndExtension[] | undefined; + ): string[] | false { + if (!resolveJs && packageJsonInfo.resolvedEntrypoints !== undefined) { + // Cached value excludes resolutions to JS files - those could be + // cached separately, but they're used rarely. + return packageJsonInfo.resolvedEntrypoints; + } + + let entrypoints: string[] | undefined; const extensions = resolveJs ? Extensions.JavaScript : Extensions.TypeScript; const features = getDefaultNodeResolutionFeatures(options); const requireState: ModuleResolutionState = { @@ -1592,19 +1602,7 @@ namespace ts { requireState, packageJsonInfo.packageJsonContent, packageJsonInfo.versionPaths); - resolutions = append(resolutions, requireResolution); - - const importState = { ...requireState, failedLookupLocations: [], conditions: ["node", "import", "types"] }; - const importResolution = loadNodeModuleFromDirectoryWorker( - extensions, - packageJsonInfo.packageDirectory, - /*onlyRecordFailures*/ false, - importState, - packageJsonInfo.packageJsonContent, - packageJsonInfo.versionPaths); - if (importResolution) { - resolutions = appendIfUnique(resolutions, importResolution, (a, b) => a?.path === b?.path); - } + entrypoints = append(entrypoints, requireResolution?.path); if (features & NodeResolutionFeatures.Exports && packageJsonInfo.packageJsonContent.exports) { const exportState = { ...requireState, failedLookupLocations: [], conditions: ["node", "import", "require", "types"] }; @@ -1615,12 +1613,12 @@ namespace ts { extensions); if (exportResolutions) { for (const resolution of exportResolutions) { - resolutions = appendIfUnique(resolutions, resolution, (a, b) => a?.path === b?.path); + entrypoints = appendIfUnique(entrypoints, resolution.path); } } } - return resolutions; + return packageJsonInfo.resolvedEntrypoints = entrypoints || false; } function loadEntrypointsFromExportMap( @@ -1677,6 +1675,8 @@ namespace ts { packageDirectory: string; packageJsonContent: PackageJsonPathFields; versionPaths: VersionPaths | undefined; + /** false: resolved to nothing. undefined: not yet resolved */ + resolvedEntrypoints: string[] | false | undefined; } /** @@ -1742,7 +1742,7 @@ namespace ts { trace(host, Diagnostics.Found_package_json_at_0, packageJsonPath); } const versionPaths = readPackageJsonTypesVersionPaths(packageJsonContent, state); - const result = { packageDirectory, packageJsonContent, versionPaths }; + const result = { packageDirectory, packageJsonContent, versionPaths, resolvedEntrypoints: undefined }; state.packageJsonInfoCache?.setPackageJsonInfo(packageJsonPath, result); return result; } diff --git a/src/compiler/utilities.ts b/src/compiler/utilities.ts index 72ce071940dcc..33b18927ef8e6 100644 --- a/src/compiler/utilities.ts +++ b/src/compiler/utilities.ts @@ -6306,8 +6306,6 @@ namespace ts { getSymlinkedFiles(): ReadonlyESMap | undefined; setSymlinkedDirectory(symlink: string, real: SymlinkedDirectory | false): void; setSymlinkedFile(symlinkPath: Path, real: string): void; - /*@internal*/ - setSymlinkedDirectoryFromSymlinkedFile(symlink: string, real: string): void; /** * @internal * Uses resolvedTypeReferenceDirectives from program instead of from files, since files @@ -6345,16 +6343,6 @@ namespace ts { (symlinkedDirectories || (symlinkedDirectories = new Map())).set(symlinkPath, real); } }, - setSymlinkedDirectoryFromSymlinkedFile(symlink, real) { - this.setSymlinkedFile(toPath(symlink, cwd, getCanonicalFileName), real); - const [commonResolved, commonOriginal] = guessDirectorySymlink(real, symlink, cwd, getCanonicalFileName) || emptyArray; - if (commonResolved && commonOriginal) { - this.setSymlinkedDirectory(commonOriginal, { - real: commonResolved, - realPath: toPath(commonResolved, cwd, getCanonicalFileName), - }); - } - }, setSymlinksFromResolutions(files, typeReferenceDirectives) { Debug.assert(!hasProcessedResolutions); hasProcessedResolutions = true; diff --git a/src/server/project.ts b/src/server/project.ts index 88a1413bc98fb..0a35d89c206bc 100644 --- a/src/server/project.ts +++ b/src/server/project.ts @@ -1988,7 +1988,7 @@ namespace ts.server { type PackageJsonInfo = Exclude, undefined>; function getRootNamesFromPackageJson(packageJson: PackageJsonInfo, program: Program, symlinkCache: SymlinkCache, resolveJs?: boolean) { - const entrypoints = loadEntrypointsFromPackageJsonInfo( + const entrypoints = getEntrypointsFromPackageJsonInfo( packageJson, compilerOptions, moduleResolutionHost, @@ -2005,8 +2005,8 @@ namespace ts.server { } return mapDefined(entrypoints, entrypoint => { - const resolvedFileName = isSymlink ? entrypoint.path.replace(packageJson.packageDirectory, real) : entrypoint.path; - if (!program.getSourceFile(resolvedFileName) && (!isSymlink || !program.getSourceFile(entrypoint.path))) { + const resolvedFileName = isSymlink ? entrypoint.replace(packageJson.packageDirectory, real) : entrypoint; + if (!program.getSourceFile(resolvedFileName) && (!isSymlink || !program.getSourceFile(entrypoint))) { return resolvedFileName; } }); diff --git a/src/testRunner/unittests/tsserver/symlinkCache.ts b/src/testRunner/unittests/tsserver/symlinkCache.ts index 7a80a0cc289f2..f9877c4c1b85c 100644 --- a/src/testRunner/unittests/tsserver/symlinkCache.ts +++ b/src/testRunner/unittests/tsserver/symlinkCache.ts @@ -60,7 +60,12 @@ namespace ts.projectSystem { it("works for paths close to the root", () => { const cache = createSymlinkCache("/", createGetCanonicalFileName(/*useCaseSensitiveFileNames*/ false)); - cache.setSymlinkedDirectoryFromSymlinkedFile("/foo", "/one/two/foo"); // Used to crash, #44953 + // Used to crash, #44953 + cache.setSymlinksFromResolutions([], new Map([["foo", { + primary: true, + originalPath: "/foo", + resolvedFileName: "/one/two/foo", + }]])); }); }); From f3be5ead3f1717e319cfd52fa16159fffa20d896 Mon Sep 17 00:00:00 2001 From: Andrew Branch Date: Thu, 9 Dec 2021 14:35:38 -0800 Subject: [PATCH 05/12] Add one more test --- .../server/autoImportProvider_exportMap7.ts | 69 +++++++++++++++++++ 1 file changed, 69 insertions(+) create mode 100644 tests/cases/fourslash/server/autoImportProvider_exportMap7.ts diff --git a/tests/cases/fourslash/server/autoImportProvider_exportMap7.ts b/tests/cases/fourslash/server/autoImportProvider_exportMap7.ts new file mode 100644 index 0000000000000..d1148f824fbe1 --- /dev/null +++ b/tests/cases/fourslash/server/autoImportProvider_exportMap7.ts @@ -0,0 +1,69 @@ +/// + +// Some exports are already in the main program while some are not. + +// @Filename: /tsconfig.json +//// { +//// "compilerOptions": { +//// "module": "nodenext" +//// } +//// } + +// @Filename: /package.json +//// { +//// "type": "module", +//// "dependencies": { +//// "dependency": "^1.0.0" +//// } +//// } + +// @Filename: /node_modules/dependency/package.json +//// { +//// "type": "module", +//// "name": "dependency", +//// "version": "1.0.0", +//// "exports": { +//// ".": { +//// "types": "./lib/index.d.ts" +//// }, +//// "./lol": { +//// "types": "./lib/lol.d.ts" +//// } +//// } +//// } + +// @Filename: /node_modules/dependency/lib/index.d.ts +//// export function fooFromIndex(): void; + +// @Filename: /node_modules/dependency/lib/lol.d.ts +//// export function fooFromLol(): void; + +// @Filename: /src/bar.ts +//// import { fooFromIndex } from "dependency"; + +// @Filename: /src/foo.ts +//// fooFrom/**/ + +goTo.marker(""); + +verify.completions({ + marker: "", + exact: completion.globalsPlus([{ + name: "fooFromIndex", + source: "dependency", + sourceDisplay: "dependency", + sortText: completion.SortText.AutoImportSuggestions, + hasAction: true, + }, { + name: "fooFromLol", + source: "dependency/lol", + sourceDisplay: "dependency/lol", + sortText: completion.SortText.AutoImportSuggestions, + hasAction: true, + }]), + preferences: { + includeCompletionsForModuleExports: true, + includeInsertTextCompletions: true, + allowIncompleteCompletions: true, + }, +}); \ No newline at end of file From b0c649f2e4ee128764dfa93e5a4ff6173ba20eb2 Mon Sep 17 00:00:00 2001 From: Andrew Branch Date: Thu, 9 Dec 2021 14:39:22 -0800 Subject: [PATCH 06/12] Delete unused function --- src/compiler/moduleNameResolver.ts | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/compiler/moduleNameResolver.ts b/src/compiler/moduleNameResolver.ts index fb01aeff2f3ed..c90fce64f45e0 100644 --- a/src/compiler/moduleNameResolver.ts +++ b/src/compiler/moduleNameResolver.ts @@ -1204,11 +1204,6 @@ namespace ts { return resolvedModule.resolvedFileName; } - /* @internal */ - export function tryResolveJSModule(moduleName: string, initialDir: string, host: ModuleResolutionHost) { - return tryResolveJSModuleWorker(moduleName, initialDir, host).resolvedModule; - } - /* @internal */ enum NodeResolutionFeatures { None = 0, From 78db89f6ab01a14971070f468cf5c9b98d010794 Mon Sep 17 00:00:00 2001 From: Andrew Branch Date: Thu, 9 Dec 2021 17:28:38 -0800 Subject: [PATCH 07/12] Fix other tests - dependencies need package.json files --- src/testRunner/unittests/tsserver/exportMapCache.ts | 6 +++++- src/testRunner/unittests/tsserver/moduleSpecifierCache.ts | 6 +++++- .../fourslash/server/completionsImport_mergedReExport.ts | 3 +++ 3 files changed, 13 insertions(+), 2 deletions(-) diff --git a/src/testRunner/unittests/tsserver/exportMapCache.ts b/src/testRunner/unittests/tsserver/exportMapCache.ts index 5eb69c514cd1b..0659c78de2748 100644 --- a/src/testRunner/unittests/tsserver/exportMapCache.ts +++ b/src/testRunner/unittests/tsserver/exportMapCache.ts @@ -19,6 +19,10 @@ namespace ts.projectSystem { path: "/ambient.d.ts", content: "declare module 'ambient' {}" }; + const mobxPackageJson: File = { + path: "/node_modules/mobx/package.json", + content: `{ "name": "mobx", "version": "1.0.0" }` + }; const mobxDts: File = { path: "/node_modules/mobx/index.d.ts", content: "export declare function observable(): unknown;" @@ -118,7 +122,7 @@ namespace ts.projectSystem { }); function setup() { - const host = createServerHost([aTs, bTs, ambientDeclaration, tsconfig, packageJson, mobxDts, exportEqualsMappedType]); + const host = createServerHost([aTs, bTs, ambientDeclaration, tsconfig, packageJson, mobxPackageJson, mobxDts, exportEqualsMappedType]); const session = createSession(host); openFilesForSession([aTs, bTs], session); const projectService = session.getProjectService(); diff --git a/src/testRunner/unittests/tsserver/moduleSpecifierCache.ts b/src/testRunner/unittests/tsserver/moduleSpecifierCache.ts index fd57438a2691e..63434784b0cf4 100644 --- a/src/testRunner/unittests/tsserver/moduleSpecifierCache.ts +++ b/src/testRunner/unittests/tsserver/moduleSpecifierCache.ts @@ -27,6 +27,10 @@ namespace ts.projectSystem { path: "/src/ambient.d.ts", content: "declare module 'ambient' {}" }; + const mobxPackageJson: File = { + path: "/node_modules/mobx/package.json", + content: `{ "name": "mobx", "version": "1.0.0" }` + }; const mobxDts: File = { path: "/node_modules/mobx/index.d.ts", content: "export declare function observable(): unknown;" @@ -120,7 +124,7 @@ namespace ts.projectSystem { }); function setup() { - const host = createServerHost([aTs, bTs, cTs, bSymlink, ambientDeclaration, tsconfig, packageJson, mobxDts]); + const host = createServerHost([aTs, bTs, cTs, bSymlink, ambientDeclaration, tsconfig, packageJson, mobxPackageJson, mobxDts]); const session = createSession(host); openFilesForSession([aTs, bTs, cTs], session); const projectService = session.getProjectService(); diff --git a/tests/cases/fourslash/server/completionsImport_mergedReExport.ts b/tests/cases/fourslash/server/completionsImport_mergedReExport.ts index 63d813a0c9866..a65e2a115990d 100644 --- a/tests/cases/fourslash/server/completionsImport_mergedReExport.ts +++ b/tests/cases/fourslash/server/completionsImport_mergedReExport.ts @@ -6,6 +6,9 @@ // @Filename: /package.json //// { "dependencies": { "@jest/types": "*", "ts-jest": "*" } } +// @Filename: /node_modules/@jest/types/package.json +//// { "name": "@jest/types" } + // @Filename: /node_modules/@jest/types/index.d.ts //// import type * as Config from "./Config"; //// export type { Config }; From 86eb0702d4c9d3485b13385dfbf801d6f44422cb Mon Sep 17 00:00:00 2001 From: Andrew Branch Date: Mon, 13 Dec 2021 15:58:48 -0800 Subject: [PATCH 08/12] Do two passes of exports resolution --- src/compiler/moduleNameResolver.ts | 20 ++-- src/compiler/moduleSpecifiers.ts | 39 +++++--- src/services/getEditsForFileRename.ts | 2 +- src/services/utilities.ts | 2 +- .../server/autoImportProvider_exportMap8.ts | 94 +++++++++++++++++++ 5 files changed, 134 insertions(+), 23 deletions(-) create mode 100644 tests/cases/fourslash/server/autoImportProvider_exportMap8.ts diff --git a/src/compiler/moduleNameResolver.ts b/src/compiler/moduleNameResolver.ts index c90fce64f45e0..2f4404e80c933 100644 --- a/src/compiler/moduleNameResolver.ts +++ b/src/compiler/moduleNameResolver.ts @@ -1600,15 +1600,17 @@ namespace ts { entrypoints = append(entrypoints, requireResolution?.path); if (features & NodeResolutionFeatures.Exports && packageJsonInfo.packageJsonContent.exports) { - const exportState = { ...requireState, failedLookupLocations: [], conditions: ["node", "import", "require", "types"] }; - const exportResolutions = loadEntrypointsFromExportMap( - packageJsonInfo, - packageJsonInfo.packageJsonContent.exports, - exportState, - extensions); - if (exportResolutions) { - for (const resolution of exportResolutions) { - entrypoints = appendIfUnique(entrypoints, resolution.path); + for (const conditions of [["node", "import", "types"], ["node", "require", "types"]]) { + const exportState = { ...requireState, failedLookupLocations: [], conditions }; + const exportResolutions = loadEntrypointsFromExportMap( + packageJsonInfo, + packageJsonInfo.packageJsonContent.exports, + exportState, + extensions); + if (exportResolutions) { + for (const resolution of exportResolutions) { + entrypoints = appendIfUnique(entrypoints, resolution.path); + } } } } diff --git a/src/compiler/moduleSpecifiers.ts b/src/compiler/moduleSpecifiers.ts index 4b36113ba60ed..bfd812c71724c 100644 --- a/src/compiler/moduleSpecifiers.ts +++ b/src/compiler/moduleSpecifiers.ts @@ -59,19 +59,29 @@ namespace ts.moduleSpecifiers { }; } + // `importingSourceFile` and `importingSourceFileName`? Why not just use `importingSourceFile.path`? + // Because when this is called by the file renamer, `importingSourceFile` is the file being renamed, + // while `importingSourceFileName` its *new* name. We need a source file just to get its + // `impliedNodeFormat` and to detect certain preferences from existing import module specifiers. export function updateModuleSpecifier( compilerOptions: CompilerOptions, + importingSourceFile: SourceFile, importingSourceFileName: Path, toFileName: string, host: ModuleSpecifierResolutionHost, oldImportSpecifier: string, ): string | undefined { - const res = getModuleSpecifierWorker(compilerOptions, importingSourceFileName, toFileName, host, getPreferencesForUpdate(compilerOptions, oldImportSpecifier, importingSourceFileName, host), {}); + const res = getModuleSpecifierWorker(compilerOptions, importingSourceFile, importingSourceFile.path, toFileName, host, getPreferencesForUpdate(compilerOptions, oldImportSpecifier, importingSourceFileName, host), {}); if (res === oldImportSpecifier) return undefined; return res; } - // Note: importingSourceFile is just for usesJsExtensionOnImports + // `importingSourceFile` and `importingSourceFileName`? Why not just use `importingSourceFile.path`? + // Because when this is called by the declaration emitter, `importingSourceFile` is the implementation + // file, but `importingSourceFileName` and `toFileName` refer to declaration files (the former to the + // one currently being produced; the latter to the one being imported). We need an implementation file + // just to get its `impliedNodeFormat` and to detect certain preferences from existing import module + // specifiers. export function getModuleSpecifier( compilerOptions: CompilerOptions, importingSourceFile: SourceFile, @@ -79,24 +89,25 @@ namespace ts.moduleSpecifiers { toFileName: string, host: ModuleSpecifierResolutionHost, ): string { - return getModuleSpecifierWorker(compilerOptions, importingSourceFileName, toFileName, host, getPreferences(host, {}, compilerOptions, importingSourceFile), {}); + return getModuleSpecifierWorker(compilerOptions, importingSourceFile, importingSourceFileName, toFileName, host, getPreferences(host, {}, compilerOptions, importingSourceFile), {}); } export function getNodeModulesPackageName( compilerOptions: CompilerOptions, - importingSourceFileName: Path, + importingSourceFile: SourceFile, nodeModulesFileName: string, host: ModuleSpecifierResolutionHost, preferences: UserPreferences, ): string | undefined { - const info = getInfo(importingSourceFileName, host); - const modulePaths = getAllModulePaths(importingSourceFileName, nodeModulesFileName, host, preferences); + const info = getInfo(importingSourceFile.path, host); + const modulePaths = getAllModulePaths(importingSourceFile.path, nodeModulesFileName, host, preferences); return firstDefined(modulePaths, - modulePath => tryGetModuleNameAsNodeModule(modulePath, info, host, compilerOptions, /*packageNameOnly*/ true)); + modulePath => tryGetModuleNameAsNodeModule(modulePath, info, importingSourceFile, host, compilerOptions, /*packageNameOnly*/ true)); } function getModuleSpecifierWorker( compilerOptions: CompilerOptions, + importingSourceFile: SourceFile, importingSourceFileName: Path, toFileName: string, host: ModuleSpecifierResolutionHost, @@ -105,7 +116,7 @@ namespace ts.moduleSpecifiers { ): string { const info = getInfo(importingSourceFileName, host); const modulePaths = getAllModulePaths(importingSourceFileName, toFileName, host, userPreferences); - return firstDefined(modulePaths, modulePath => tryGetModuleNameAsNodeModule(modulePath, info, host, compilerOptions)) || + return firstDefined(modulePaths, modulePath => tryGetModuleNameAsNodeModule(modulePath, info, importingSourceFile, host, compilerOptions)) || getLocalModuleSpecifier(toFileName, info, compilerOptions, host, preferences); } @@ -222,7 +233,7 @@ namespace ts.moduleSpecifiers { let pathsSpecifiers: string[] | undefined; let relativeSpecifiers: string[] | undefined; for (const modulePath of modulePaths) { - const specifier = tryGetModuleNameAsNodeModule(modulePath, info, host, compilerOptions); + const specifier = tryGetModuleNameAsNodeModule(modulePath, info, importingSourceFile, host, compilerOptions); nodeModulesSpecifiers = append(nodeModulesSpecifiers, specifier); if (specifier && modulePath.isRedirect) { // If we got a specifier for a redirect, it was a bare package specifier (e.g. "@foo/bar", @@ -639,7 +650,7 @@ namespace ts.moduleSpecifiers { : removeFileExtension(relativePath); } - function tryGetModuleNameAsNodeModule({ path, isRedirect }: ModulePath, { getCanonicalFileName, sourceDirectory }: Info, host: ModuleSpecifierResolutionHost, options: CompilerOptions, packageNameOnly?: boolean): string | undefined { + function tryGetModuleNameAsNodeModule({ path, isRedirect }: ModulePath, { getCanonicalFileName, sourceDirectory }: Info, importingSourceFile: SourceFile , host: ModuleSpecifierResolutionHost, options: CompilerOptions, packageNameOnly?: boolean): string | undefined { if (!host.fileExists || !host.readFile) { return undefined; } @@ -706,10 +717,14 @@ namespace ts.moduleSpecifiers { let moduleFileToTry = path; if (host.fileExists(packageJsonPath)) { const packageJsonContent = JSON.parse(host.readFile!(packageJsonPath)!); - // TODO: Inject `require` or `import` condition based on the intended import mode if (getEmitModuleResolutionKind(options) === ModuleResolutionKind.Node12 || getEmitModuleResolutionKind(options) === ModuleResolutionKind.NodeNext) { + // `conditions` *could* be made to go against `importingSourceFile.impliedNodeFormat` if something wanted to generate + // an ImportEqualsDeclaration in an ESM-implied file or an ImportCall in a CJS-implied file. But since this function is + // usually called to conjure an import out of thin air, we don't have an existing usage to call `getModeForUsageAtIndex` + // with, so for now we just stick with the mode of the file. + const conditions = ["node", importingSourceFile.impliedNodeFormat === ModuleKind.ESNext ? "import" : "require", "types"]; const fromExports = packageJsonContent.exports && typeof packageJsonContent.name === "string" - ? tryGetModuleNameFromExports(options, path, packageRootPath, getPackageNameFromTypesPackageName(packageJsonContent.name), packageJsonContent.exports, ["node", "types"]) + ? tryGetModuleNameFromExports(options, path, packageRootPath, getPackageNameFromTypesPackageName(packageJsonContent.name), packageJsonContent.exports, conditions) : undefined; if (fromExports) { const withJsExtension = !hasTSFileExtension(fromExports.moduleFileToTry) diff --git a/src/services/getEditsForFileRename.ts b/src/services/getEditsForFileRename.ts index fa0749df0cecc..369f8e8e23f07 100644 --- a/src/services/getEditsForFileRename.ts +++ b/src/services/getEditsForFileRename.ts @@ -156,7 +156,7 @@ namespace ts { // Need an update if the imported file moved, or the importing file moved and was using a relative path. return toImport !== undefined && (toImport.updated || (importingSourceFileMoved && pathIsRelative(importLiteral.text))) - ? moduleSpecifiers.updateModuleSpecifier(program.getCompilerOptions(), getCanonicalFileName(newImportFromPath) as Path, toImport.newFileName, createModuleSpecifierResolutionHost(program, host), importLiteral.text) + ? moduleSpecifiers.updateModuleSpecifier(program.getCompilerOptions(), sourceFile, getCanonicalFileName(newImportFromPath) as Path, toImport.newFileName, createModuleSpecifierResolutionHost(program, host), importLiteral.text) : undefined; }); } diff --git a/src/services/utilities.ts b/src/services/utilities.ts index 593cbc73913f7..d3bdfcc35e47f 100644 --- a/src/services/utilities.ts +++ b/src/services/utilities.ts @@ -3085,7 +3085,7 @@ namespace ts { } const specifier = moduleSpecifiers.getNodeModulesPackageName( host.getCompilationSettings(), - fromFile.path, + fromFile, importedFileName, moduleSpecifierResolutionHost, preferences, diff --git a/tests/cases/fourslash/server/autoImportProvider_exportMap8.ts b/tests/cases/fourslash/server/autoImportProvider_exportMap8.ts new file mode 100644 index 0000000000000..284308909b8d8 --- /dev/null +++ b/tests/cases/fourslash/server/autoImportProvider_exportMap8.ts @@ -0,0 +1,94 @@ +/// + +// Both 'import' and 'require' should be pulled in + +// @Filename: /tsconfig.json +//// { +//// "compilerOptions": { +//// "module": "nodenext" +//// } +//// } + +// @Filename: /package.json +//// { +//// "type": "module", +//// "dependencies": { +//// "dependency": "^1.0.0" +//// } +//// } + +// @Filename: /node_modules/dependency/package.json +//// { +//// "type": "module", +//// "name": "dependency", +//// "version": "1.0.0", +//// "exports": { +//// "./lol": { +//// "import": "./lib/index.js", +//// "require": "./lib/lol.js" +//// } +//// } +//// } + +// @Filename: /node_modules/dependency/lib/index.d.ts +//// export function fooFromIndex(): void; + +// @Filename: /node_modules/dependency/lib/lol.d.ts +//// export function fooFromLol(): void; + +// @Filename: /src/bar.ts +//// import { fooFromIndex } from "dependency"; + +// @Filename: /src/foo.cts +//// fooFrom/*cts*/ + +// @Filename: /src/foo.mts +//// fooFrom/*mts*/ + +goTo.marker("cts"); +verify.completions({ + marker: "cts", + exact: completion.globalsPlus([{ + // TODO: this one will go away (see note in ./autoImportProvider_exportMap3.ts) + name: "fooFromIndex", + source: "../node_modules/dependency/lib/index", + sourceDisplay: "../node_modules/dependency/lib/index", + sortText: completion.SortText.AutoImportSuggestions, + hasAction: true, + }, { + name: "fooFromLol", + source: "dependency/lol", + sourceDisplay: "dependency/lol", + sortText: completion.SortText.AutoImportSuggestions, + hasAction: true, + }]), + preferences: { + includeCompletionsForModuleExports: true, + includeInsertTextCompletions: true, + allowIncompleteCompletions: true, + }, +}); + +// goTo.marker("mts"); +// verify.completions({ +// marker: "mts", +// exact: completion.globalsPlus([{ +// name: "fooFromIndex", +// source: "dependency/lol", +// sourceDisplay: "dependency/lol", +// sortText: completion.SortText.AutoImportSuggestions, +// hasAction: true, +// }, { +// // TODO: this one will go away (see note in ./autoImportProvider_exportMap3.ts) +// name: "fooFromLol", +// source: "../node_modules/dependency/lib/index.js", +// sourceDisplay: "../node_modules/dependency/lib/index.js", +// sortText: completion.SortText.AutoImportSuggestions, +// hasAction: true, +// }]), +// preferences: { +// includeCompletionsForModuleExports: true, +// includeInsertTextCompletions: true, +// allowIncompleteCompletions: true, +// }, +// }); From f18e95d52ce4a9a1047a91bb1a4ecfa6050fedea Mon Sep 17 00:00:00 2001 From: Andrew Branch Date: Mon, 13 Dec 2021 16:22:41 -0800 Subject: [PATCH 09/12] Fix missed refactor --- src/compiler/moduleSpecifiers.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/compiler/moduleSpecifiers.ts b/src/compiler/moduleSpecifiers.ts index bfd812c71724c..c17dfeabcf1e7 100644 --- a/src/compiler/moduleSpecifiers.ts +++ b/src/compiler/moduleSpecifiers.ts @@ -71,7 +71,7 @@ namespace ts.moduleSpecifiers { host: ModuleSpecifierResolutionHost, oldImportSpecifier: string, ): string | undefined { - const res = getModuleSpecifierWorker(compilerOptions, importingSourceFile, importingSourceFile.path, toFileName, host, getPreferencesForUpdate(compilerOptions, oldImportSpecifier, importingSourceFileName, host), {}); + const res = getModuleSpecifierWorker(compilerOptions, importingSourceFile, importingSourceFileName, toFileName, host, getPreferencesForUpdate(compilerOptions, oldImportSpecifier, importingSourceFileName, host), {}); if (res === oldImportSpecifier) return undefined; return res; } From f021d8e3f453ce45070cd4790f5ed39e164780b2 Mon Sep 17 00:00:00 2001 From: Andrew Branch Date: Wed, 15 Dec 2021 14:04:48 -0800 Subject: [PATCH 10/12] Apply suggestions from code review Co-authored-by: Nathan Shively-Sanders <293473+sandersn@users.noreply.github.com> --- src/compiler/moduleNameResolver.ts | 3 +-- src/server/project.ts | 4 ++-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/src/compiler/moduleNameResolver.ts b/src/compiler/moduleNameResolver.ts index 2f4404e80c933..07c1f306e036c 100644 --- a/src/compiler/moduleNameResolver.ts +++ b/src/compiler/moduleNameResolver.ts @@ -1643,8 +1643,7 @@ namespace ts { function loadEntrypointsFromTargetExports(target: unknown) { if (typeof target === "string" && startsWith(target, "./") && target.indexOf("*") === -1) { - const parts = getPathComponents(target).slice(1); - const partsAfterFirst = parts.slice(1); + const partsAfterFirst = getPathComponents(target).slice(2); if (partsAfterFirst.indexOf("..") >= 0 || partsAfterFirst.indexOf(".") >= 0 || partsAfterFirst.indexOf("node_modules") >= 0) { return; } diff --git a/src/server/project.ts b/src/server/project.ts index 0a35d89c206bc..24ceaab91f24e 100644 --- a/src/server/project.ts +++ b/src/server/project.ts @@ -1986,7 +1986,7 @@ namespace ts.server { } } - type PackageJsonInfo = Exclude, undefined>; + type PackageJsonInfo = NonNullable>; function getRootNamesFromPackageJson(packageJson: PackageJsonInfo, program: Program, symlinkCache: SymlinkCache, resolveJs?: boolean) { const entrypoints = getEntrypointsFromPackageJsonInfo( packageJson, @@ -2006,7 +2006,7 @@ namespace ts.server { return mapDefined(entrypoints, entrypoint => { const resolvedFileName = isSymlink ? entrypoint.replace(packageJson.packageDirectory, real) : entrypoint; - if (!program.getSourceFile(resolvedFileName) && (!isSymlink || !program.getSourceFile(entrypoint))) { + if (!program.getSourceFile(resolvedFileName) && !(isSymlink && program.getSourceFile(entrypoint))) { return resolvedFileName; } }); From 766cfaf3fe44aa6e6b1d6b0b40c7b48e21020468 Mon Sep 17 00:00:00 2001 From: Andrew Branch Date: Wed, 15 Dec 2021 14:05:03 -0800 Subject: [PATCH 11/12] Uncomment rest of test --- .../server/autoImportProvider_exportMap8.ts | 46 +++++++++---------- 1 file changed, 23 insertions(+), 23 deletions(-) diff --git a/tests/cases/fourslash/server/autoImportProvider_exportMap8.ts b/tests/cases/fourslash/server/autoImportProvider_exportMap8.ts index 284308909b8d8..b679c2184fd50 100644 --- a/tests/cases/fourslash/server/autoImportProvider_exportMap8.ts +++ b/tests/cases/fourslash/server/autoImportProvider_exportMap8.ts @@ -69,26 +69,26 @@ verify.completions({ }, }); -// goTo.marker("mts"); -// verify.completions({ -// marker: "mts", -// exact: completion.globalsPlus([{ -// name: "fooFromIndex", -// source: "dependency/lol", -// sourceDisplay: "dependency/lol", -// sortText: completion.SortText.AutoImportSuggestions, -// hasAction: true, -// }, { -// // TODO: this one will go away (see note in ./autoImportProvider_exportMap3.ts) -// name: "fooFromLol", -// source: "../node_modules/dependency/lib/index.js", -// sourceDisplay: "../node_modules/dependency/lib/index.js", -// sortText: completion.SortText.AutoImportSuggestions, -// hasAction: true, -// }]), -// preferences: { -// includeCompletionsForModuleExports: true, -// includeInsertTextCompletions: true, -// allowIncompleteCompletions: true, -// }, -// }); +goTo.marker("mts"); +verify.completions({ + marker: "mts", + exact: completion.globalsPlus([{ + name: "fooFromIndex", + source: "dependency/lol", + sourceDisplay: "dependency/lol", + sortText: completion.SortText.AutoImportSuggestions, + hasAction: true, + }, { + // TODO: this one will go away (see note in ./autoImportProvider_exportMap3.ts) + name: "fooFromLol", + source: "../node_modules/dependency/lib/lol.js", + sourceDisplay: "../node_modules/dependency/lib/lol.js", + sortText: completion.SortText.AutoImportSuggestions, + hasAction: true, + }]), + preferences: { + includeCompletionsForModuleExports: true, + includeInsertTextCompletions: true, + allowIncompleteCompletions: true, + }, +}); From 55ba7c969b68e5bf73aeb71e04acc16a12ffb380 Mon Sep 17 00:00:00 2001 From: Andrew Branch Date: Mon, 10 Jan 2022 12:21:49 -0800 Subject: [PATCH 12/12] Handle array targets --- src/compiler/moduleNameResolver.ts | 17 ++++-- .../server/autoImportProvider_exportMap9.ts | 58 +++++++++++++++++++ 2 files changed, 71 insertions(+), 4 deletions(-) create mode 100644 tests/cases/fourslash/server/autoImportProvider_exportMap9.ts diff --git a/src/compiler/moduleNameResolver.ts b/src/compiler/moduleNameResolver.ts index 07c1f306e036c..0e2cff7d00487 100644 --- a/src/compiler/moduleNameResolver.ts +++ b/src/compiler/moduleNameResolver.ts @@ -1641,22 +1641,31 @@ namespace ts { } return entrypoints; - function loadEntrypointsFromTargetExports(target: unknown) { + function loadEntrypointsFromTargetExports(target: unknown): boolean | undefined { if (typeof target === "string" && startsWith(target, "./") && target.indexOf("*") === -1) { const partsAfterFirst = getPathComponents(target).slice(2); if (partsAfterFirst.indexOf("..") >= 0 || partsAfterFirst.indexOf(".") >= 0 || partsAfterFirst.indexOf("node_modules") >= 0) { - return; + return false; } const resolvedTarget = combinePaths(scope.packageDirectory, target); const finalPath = getNormalizedAbsolutePath(resolvedTarget, state.host.getCurrentDirectory?.()); const result = loadJSOrExactTSFileName(extensions, finalPath, /*recordOnlyFailures*/ false, state); if (result) { entrypoints = appendIfUnique(entrypoints, result, (a, b) => a.path === b.path); + return true; + } + } + else if (Array.isArray(target)) { + for (const t of target) { + const success = loadEntrypointsFromTargetExports(t); + if (success) { + return true; + } } } // eslint-disable-next-line no-null/no-null - else if (typeof target === "object" && target !== null && !Array.isArray(target)) { - forEach(getOwnKeys(target as MapLike), key => { + else if (typeof target === "object" && target !== null) { + return forEach(getOwnKeys(target as MapLike), key => { if (key === "default" || contains(state.conditions, key) || isApplicableVersionedTypesKey(state.conditions, key)) { loadEntrypointsFromTargetExports((target as MapLike)[key]); return true; diff --git a/tests/cases/fourslash/server/autoImportProvider_exportMap9.ts b/tests/cases/fourslash/server/autoImportProvider_exportMap9.ts new file mode 100644 index 0000000000000..e7768c984321a --- /dev/null +++ b/tests/cases/fourslash/server/autoImportProvider_exportMap9.ts @@ -0,0 +1,58 @@ +/// + +// Only the first resolution in an array should be used + +// @Filename: /tsconfig.json +//// { +//// "compilerOptions": { +//// "module": "nodenext" +//// } +//// } + +// @Filename: /package.json +//// { +//// "type": "module", +//// "dependencies": { +//// "dependency": "^1.0.0" +//// } +//// } + +// @Filename: /node_modules/dependency/package.json +//// { +//// "type": "module", +//// "name": "dependency", +//// "version": "1.0.0", +//// "exports": { +//// "./lol": ["./lib/index.js", "./lib/lol.js"] +//// } +//// } + +// @Filename: /node_modules/dependency/lib/index.d.ts +//// export function fooFromIndex(): void; + +// @Filename: /node_modules/dependency/lib/lol.d.ts +//// export function fooFromLol(): void; + +// @Filename: /src/bar.ts +//// import { fooFromIndex } from "dependency"; + +// @Filename: /src/foo.ts +//// fooFrom/**/ + + +goTo.marker(""); +verify.completions({ + marker: "", + exact: completion.globalsPlus([{ + name: "fooFromIndex", + source: "dependency/lol", + sourceDisplay: "dependency/lol", + sortText: completion.SortText.AutoImportSuggestions, + hasAction: true, + }]), + preferences: { + includeCompletionsForModuleExports: true, + includeInsertTextCompletions: true, + allowIncompleteCompletions: true, + }, +});