diff --git a/src/harness/harnessLanguageService.ts b/src/harness/harnessLanguageService.ts index be17bbdef0e..5468068d922 100644 --- a/src/harness/harnessLanguageService.ts +++ b/src/harness/harnessLanguageService.ts @@ -994,7 +994,7 @@ namespace Harness.LanguageService { cancellationToken: ts.server.nullCancellationToken, useSingleInferredProject: false, useInferredProjectPerProjectRoot: false, - typingsInstaller: undefined!, // TODO: GH#18217 + typingsInstaller: { ...ts.server.nullTypingsInstaller, globalTypingsCacheLocation: "/Library/Caches/typescript" }, byteLength: Utils.byteLength, hrtime: process.hrtime, logger: serverHost, diff --git a/src/server/project.ts b/src/server/project.ts index 3ecd20cf76f..cabeb6d1e62 100644 --- a/src/server/project.ts +++ b/src/server/project.ts @@ -1974,19 +1974,26 @@ namespace ts.server { } } - // 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; - } + // 2. Try to load from the @types package in the tree and in the global + // typings cache location, if enabled. + const done = forEach([hostProject.currentDirectory, hostProject.getGlobalTypingsCacheLocation()], directory => { + if (directory) { + const typesPackageJson = resolvePackageNameToPackageJson( + `@types/${name}`, + directory, + compilerOptions, + moduleResolutionHost, + program.getModuleResolutionCache()); + if (typesPackageJson) { + const entrypoints = getRootNamesFromPackageJson(typesPackageJson, program, symlinkCache); + rootNames = concatenate(rootNames, entrypoints); + dependenciesAdded += entrypoints?.length ? 1 : 0; + return true; + } + } + }); + + if (done) 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 diff --git a/src/services/exportInfoMap.ts b/src/services/exportInfoMap.ts index 51a0dd2ce23..4004815a4a6 100644 --- a/src/services/exportInfoMap.ts +++ b/src/services/exportInfoMap.ts @@ -59,6 +59,7 @@ namespace ts { export interface CacheableExportInfoMapHost { getCurrentProgram(): Program | undefined; getPackageJsonAutoImportProvider(): Program | undefined; + getGlobalTypingsCacheLocation(): string | undefined; } export function createCacheableExportInfoMap(host: CacheableExportInfoMapHost): ExportInfoMap { @@ -99,7 +100,7 @@ namespace ts { packageName = unmangleScopedPackageName(getPackageNameFromTypesPackageName(moduleFile.fileName.substring(topLevelPackageNameIndex + 1, packageRootIndex))); if (startsWith(importingFile, moduleFile.path.substring(0, topLevelNodeModulesIndex))) { const prevDeepestNodeModulesPath = packages.get(packageName); - const nodeModulesPath = moduleFile.fileName.substring(0, topLevelPackageNameIndex); + const nodeModulesPath = moduleFile.fileName.substring(0, topLevelPackageNameIndex + 1); if (prevDeepestNodeModulesPath) { const prevDeepestNodeModulesIndex = prevDeepestNodeModulesPath.indexOf(nodeModulesPathPart); if (topLevelNodeModulesIndex > prevDeepestNodeModulesIndex) { @@ -272,6 +273,8 @@ namespace ts { function isNotShadowedByDeeperNodeModulesPackage(info: SymbolExportInfo, packageName: string | undefined) { if (!packageName || !info.moduleFileName) return true; + const typingsCacheLocation = host.getGlobalTypingsCacheLocation(); + if (typingsCacheLocation && startsWith(info.moduleFileName, typingsCacheLocation)) return true; const packageDeepestNodeModulesPath = packages.get(packageName); return !packageDeepestNodeModulesPath || startsWith(info.moduleFileName, packageDeepestNodeModulesPath); } @@ -367,6 +370,7 @@ namespace ts { const cache = host.getCachedExportInfoMap?.() || createCacheableExportInfoMap({ getCurrentProgram: () => program, getPackageJsonAutoImportProvider: () => host.getPackageJsonAutoImportProvider?.(), + getGlobalTypingsCacheLocation: () => host.getGlobalTypingsCacheLocation?.(), }); if (cache.isUsableByFile(importingFile.path)) { diff --git a/tests/cases/fourslash/importFixesGlobalTypingsCache.ts b/tests/cases/fourslash/importFixesGlobalTypingsCache.ts index 68eeae6f0fb..690af5820e3 100644 --- a/tests/cases/fourslash/importFixesGlobalTypingsCache.ts +++ b/tests/cases/fourslash/importFixesGlobalTypingsCache.ts @@ -4,11 +4,17 @@ //// { "compilerOptions": { "allowJs": true, "checkJs": true } } // @Filename: /Library/Caches/typescript/node_modules/@types/react-router-dom/package.json -//// { "name": "react-router-dom" } +//// { "name": "@types/react-router-dom", "version": "16.8.4", "types": "index.d.ts" } // @Filename: /Library/Caches/typescript/node_modules/@types/react-router-dom/index.d.ts ////export class BrowserRouter {} +// @Filename: /project/node_modules/react-router-dom/package.json +//// { "name": "react-router-dom", "version": "16.8.4", "main": "index.js" } + +// @Filename: /project/node_modules/react-router-dom/index.js +//// export const BrowserRouter = () => null; + // @Filename: /project/index.js ////BrowserRouter/**/ @@ -16,3 +22,4 @@ goTo.file("/project/index.js"); verify.importFixAtPosition([`const { BrowserRouter } = require("react-router-dom"); BrowserRouter`]); + diff --git a/tests/cases/fourslash/server/autoImportProvider_globalTypingsCache.ts b/tests/cases/fourslash/server/autoImportProvider_globalTypingsCache.ts new file mode 100644 index 00000000000..7e7d22a71ba --- /dev/null +++ b/tests/cases/fourslash/server/autoImportProvider_globalTypingsCache.ts @@ -0,0 +1,41 @@ +/// + +// @Filename: /Library/Caches/typescript/node_modules/@types/react-router-dom/package.json +//// { "name": "@types/react-router-dom", "version": "16.8.4", "types": "index.d.ts" } + +// @Filename: /Library/Caches/typescript/node_modules/@types/react-router-dom/index.d.ts +//// export class BrowserRouterFromDts {} + +// @Filename: /project/package.json +//// { "dependencies": { "react-router-dom": "*" } } + +// @Filename: /project/tsconfig.json +//// { "compilerOptions": { "module": "commonjs", "allowJs": true, "checkJs": true, "maxNodeModuleJsDepth": 2 }, "typeAcquisition": { "enable": true } } + +// @Filename: /project/node_modules/react-router-dom/package.json +//// { "name": "react-router-dom", "version": "16.8.4", "main": "index.js" } + +// @Filename: /project/node_modules/react-router-dom/index.js +//// import "./BrowserRouter"; +//// export {}; + +// @Filename: /project/node_modules/react-router-dom/BrowserRouter.js +//// export const BrowserRouterFromJs = () => null; + +// @Filename: /project/index.js +////BrowserRouter/**/ + +verify.completions({ + marker: "", + exact: completion.globalsInJsPlus([{ + name: "BrowserRouterFromDts", + source: "react-router-dom", + sourceDisplay: "react-router-dom", + hasAction: true, + sortText: completion.SortText.AutoImportSuggestions, + }]), + preferences: { + allowIncompleteCompletions: true, + includeCompletionsForModuleExports: true, + } +});