From 84a3252e768b70dda70872b4ca095e66489bf1e0 Mon Sep 17 00:00:00 2001 From: Sheetal Nandi Date: Wed, 25 Mar 2020 16:27:02 -0700 Subject: [PATCH] Handle packages inside another node modules package when auto importing (#37561) Fixes #37542 --- src/compiler/moduleSpecifiers.ts | 73 ++++++++++++------- ...ixesWithPackageJsonInSideAnotherPackage.ts | 36 +++++++++ 2 files changed, 82 insertions(+), 27 deletions(-) create mode 100644 tests/cases/fourslash/importFixesWithPackageJsonInSideAnotherPackage.ts diff --git a/src/compiler/moduleSpecifiers.ts b/src/compiler/moduleSpecifiers.ts index 57df15b5ee0..b173bb13364 100644 --- a/src/compiler/moduleSpecifiers.ts +++ b/src/compiler/moduleSpecifiers.ts @@ -319,33 +319,30 @@ namespace ts.moduleSpecifiers { return undefined; } - let packageJsonContent: any | undefined; - const packageRootPath = moduleFileName.substring(0, parts.packageRootIndex); + // Simplify the full file path to something that can be resolved by Node. + + let moduleSpecifier = moduleFileName; if (!packageNameOnly) { - const packageJsonPath = combinePaths(packageRootPath, "package.json"); - packageJsonContent = host.fileExists(packageJsonPath) - ? JSON.parse(host.readFile(packageJsonPath)!) - : undefined; - const versionPaths = packageJsonContent && packageJsonContent.typesVersions - ? getPackageJsonTypesVersionsPaths(packageJsonContent.typesVersions) - : undefined; - if (versionPaths) { - const subModuleName = moduleFileName.slice(parts.packageRootIndex + 1); - const fromPaths = tryGetModuleNameFromPaths( - removeFileExtension(subModuleName), - removeExtensionAndIndexPostFix(subModuleName, Ending.Minimal, options), - versionPaths.paths - ); - if (fromPaths !== undefined) { - moduleFileName = combinePaths(moduleFileName.slice(0, parts.packageRootIndex), fromPaths); + let packageRootIndex = parts.packageRootIndex; + let moduleFileNameForExtensionless: string | undefined; + while (true) { + // If the module could be imported by a directory name, use that directory's name + const { moduleFileToTry, packageRootPath } = tryDirectoryWithPackageJson(packageRootIndex); + if (packageRootPath) { + moduleSpecifier = packageRootPath; + break; + } + if (!moduleFileNameForExtensionless) moduleFileNameForExtensionless = moduleFileToTry; + + // try with next level of directory + packageRootIndex = moduleFileName.indexOf(directorySeparator, packageRootIndex + 1); + if (packageRootIndex === -1) { + moduleSpecifier = getExtensionlessFileName(moduleFileNameForExtensionless); + break; } } } - // Simplify the full file path to something that can be resolved by Node. - - // If the module could be imported by a directory name, use that directory's name - const moduleSpecifier = packageNameOnly ? moduleFileName : getDirectoryOrExtensionlessFileName(moduleFileName); const globalTypingsCacheLocation = host.getGlobalTypingsCacheLocation && host.getGlobalTypingsCacheLocation(); // Get a path that's relative to node_modules or the importing file's path // if node_modules folder is in this folder or any of its parent folders, no need to keep it. @@ -360,18 +357,40 @@ namespace ts.moduleSpecifiers { // For classic resolution, only allow importing from node_modules/@types, not other node_modules return getEmitModuleResolutionKind(options) !== ModuleResolutionKind.NodeJs && packageName === nodeModulesDirectoryName ? undefined : packageName; - function getDirectoryOrExtensionlessFileName(path: string): string { - // If the file is the main module, it can be imported by the package name - if (packageJsonContent) { + function tryDirectoryWithPackageJson(packageRootIndex: number) { + const packageRootPath = moduleFileName.substring(0, packageRootIndex); + const packageJsonPath = combinePaths(packageRootPath, "package.json"); + let moduleFileToTry = moduleFileName; + if (host.fileExists(packageJsonPath)) { + const packageJsonContent = JSON.parse(host.readFile!(packageJsonPath)!); + const versionPaths = packageJsonContent.typesVersions + ? getPackageJsonTypesVersionsPaths(packageJsonContent.typesVersions) + : undefined; + if (versionPaths) { + const subModuleName = moduleFileName.slice(packageRootPath.length + 1); + const fromPaths = tryGetModuleNameFromPaths( + removeFileExtension(subModuleName), + removeExtensionAndIndexPostFix(subModuleName, Ending.Minimal, options), + versionPaths.paths + ); + if (fromPaths !== undefined) { + moduleFileToTry = combinePaths(packageRootPath, fromPaths); + } + } + + // If the file is the main module, it can be imported by the package name const mainFileRelative = packageJsonContent.typings || packageJsonContent.types || packageJsonContent.main; if (isString(mainFileRelative)) { const mainExportFile = toPath(mainFileRelative, packageRootPath, getCanonicalFileName); - if (removeFileExtension(mainExportFile) === removeFileExtension(getCanonicalFileName(path))) { - return packageRootPath; + if (removeFileExtension(mainExportFile) === removeFileExtension(getCanonicalFileName(moduleFileToTry))) { + return { packageRootPath, moduleFileToTry }; } } } + return { moduleFileToTry }; + } + function getExtensionlessFileName(path: string): string { // We still have a file name - remove the extension const fullModulePathWithoutExtension = removeFileExtension(path); diff --git a/tests/cases/fourslash/importFixesWithPackageJsonInSideAnotherPackage.ts b/tests/cases/fourslash/importFixesWithPackageJsonInSideAnotherPackage.ts new file mode 100644 index 00000000000..73522574b46 --- /dev/null +++ b/tests/cases/fourslash/importFixesWithPackageJsonInSideAnotherPackage.ts @@ -0,0 +1,36 @@ +/// + +// @Filename: /project/tsconfig.json +////{ +//// "compilerOptions": { +//// "jsx": "react", +//// "jsxFactory": "h" +//// } +////} + +// @Filename: /project/app.tsx +////const state = useMemo(() => 'Hello', []); + +// @Filename: /project/component.tsx +////import { useEffect } from "preact/hooks"; + +// @Filename: /project/node_modules/preact/package.json +////{ "name": "preact", "version": "10.3.4", "types": "src/index.d.ts" } + +// @Filename: /project/node_modules/preact/hooks/package.json +////{ "name": "hooks", "version": "0.1.0", "types": "src/index.d.ts" } + +// @Filename: /project/node_modules/preact/hooks/src/index.d.ts +////export function useEffect(): void; +////export function useMemo(factory: () => T, inputs: ReadonlyArray | undefined): T; + +goTo.file("/project/app.tsx"); +verify.importFixAtPosition([ + getImportFixContent("preact/hooks"), +]); + +function getImportFixContent(from: string) { + return `import { useMemo } from "${from}"; + +const state = useMemo(() => 'Hello', []);`; +}