Skip to content

Commit 84a3252

Browse files
authored
Handle packages inside another node modules package when auto importing (microsoft#37561)
Fixes microsoft#37542
1 parent fd9e602 commit 84a3252

File tree

2 files changed

+82
-27
lines changed

2 files changed

+82
-27
lines changed

src/compiler/moduleSpecifiers.ts

+46-27
Original file line numberDiff line numberDiff line change
@@ -319,33 +319,30 @@ namespace ts.moduleSpecifiers {
319319
return undefined;
320320
}
321321

322-
let packageJsonContent: any | undefined;
323-
const packageRootPath = moduleFileName.substring(0, parts.packageRootIndex);
322+
// Simplify the full file path to something that can be resolved by Node.
323+
324+
let moduleSpecifier = moduleFileName;
324325
if (!packageNameOnly) {
325-
const packageJsonPath = combinePaths(packageRootPath, "package.json");
326-
packageJsonContent = host.fileExists(packageJsonPath)
327-
? JSON.parse(host.readFile(packageJsonPath)!)
328-
: undefined;
329-
const versionPaths = packageJsonContent && packageJsonContent.typesVersions
330-
? getPackageJsonTypesVersionsPaths(packageJsonContent.typesVersions)
331-
: undefined;
332-
if (versionPaths) {
333-
const subModuleName = moduleFileName.slice(parts.packageRootIndex + 1);
334-
const fromPaths = tryGetModuleNameFromPaths(
335-
removeFileExtension(subModuleName),
336-
removeExtensionAndIndexPostFix(subModuleName, Ending.Minimal, options),
337-
versionPaths.paths
338-
);
339-
if (fromPaths !== undefined) {
340-
moduleFileName = combinePaths(moduleFileName.slice(0, parts.packageRootIndex), fromPaths);
326+
let packageRootIndex = parts.packageRootIndex;
327+
let moduleFileNameForExtensionless: string | undefined;
328+
while (true) {
329+
// If the module could be imported by a directory name, use that directory's name
330+
const { moduleFileToTry, packageRootPath } = tryDirectoryWithPackageJson(packageRootIndex);
331+
if (packageRootPath) {
332+
moduleSpecifier = packageRootPath;
333+
break;
334+
}
335+
if (!moduleFileNameForExtensionless) moduleFileNameForExtensionless = moduleFileToTry;
336+
337+
// try with next level of directory
338+
packageRootIndex = moduleFileName.indexOf(directorySeparator, packageRootIndex + 1);
339+
if (packageRootIndex === -1) {
340+
moduleSpecifier = getExtensionlessFileName(moduleFileNameForExtensionless);
341+
break;
341342
}
342343
}
343344
}
344345

345-
// Simplify the full file path to something that can be resolved by Node.
346-
347-
// If the module could be imported by a directory name, use that directory's name
348-
const moduleSpecifier = packageNameOnly ? moduleFileName : getDirectoryOrExtensionlessFileName(moduleFileName);
349346
const globalTypingsCacheLocation = host.getGlobalTypingsCacheLocation && host.getGlobalTypingsCacheLocation();
350347
// Get a path that's relative to node_modules or the importing file's path
351348
// 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 {
360357
// For classic resolution, only allow importing from node_modules/@types, not other node_modules
361358
return getEmitModuleResolutionKind(options) !== ModuleResolutionKind.NodeJs && packageName === nodeModulesDirectoryName ? undefined : packageName;
362359

363-
function getDirectoryOrExtensionlessFileName(path: string): string {
364-
// If the file is the main module, it can be imported by the package name
365-
if (packageJsonContent) {
360+
function tryDirectoryWithPackageJson(packageRootIndex: number) {
361+
const packageRootPath = moduleFileName.substring(0, packageRootIndex);
362+
const packageJsonPath = combinePaths(packageRootPath, "package.json");
363+
let moduleFileToTry = moduleFileName;
364+
if (host.fileExists(packageJsonPath)) {
365+
const packageJsonContent = JSON.parse(host.readFile!(packageJsonPath)!);
366+
const versionPaths = packageJsonContent.typesVersions
367+
? getPackageJsonTypesVersionsPaths(packageJsonContent.typesVersions)
368+
: undefined;
369+
if (versionPaths) {
370+
const subModuleName = moduleFileName.slice(packageRootPath.length + 1);
371+
const fromPaths = tryGetModuleNameFromPaths(
372+
removeFileExtension(subModuleName),
373+
removeExtensionAndIndexPostFix(subModuleName, Ending.Minimal, options),
374+
versionPaths.paths
375+
);
376+
if (fromPaths !== undefined) {
377+
moduleFileToTry = combinePaths(packageRootPath, fromPaths);
378+
}
379+
}
380+
381+
// If the file is the main module, it can be imported by the package name
366382
const mainFileRelative = packageJsonContent.typings || packageJsonContent.types || packageJsonContent.main;
367383
if (isString(mainFileRelative)) {
368384
const mainExportFile = toPath(mainFileRelative, packageRootPath, getCanonicalFileName);
369-
if (removeFileExtension(mainExportFile) === removeFileExtension(getCanonicalFileName(path))) {
370-
return packageRootPath;
385+
if (removeFileExtension(mainExportFile) === removeFileExtension(getCanonicalFileName(moduleFileToTry))) {
386+
return { packageRootPath, moduleFileToTry };
371387
}
372388
}
373389
}
390+
return { moduleFileToTry };
391+
}
374392

393+
function getExtensionlessFileName(path: string): string {
375394
// We still have a file name - remove the extension
376395
const fullModulePathWithoutExtension = removeFileExtension(path);
377396

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
/// <reference path="fourslash.ts" />
2+
3+
// @Filename: /project/tsconfig.json
4+
////{
5+
//// "compilerOptions": {
6+
//// "jsx": "react",
7+
//// "jsxFactory": "h"
8+
//// }
9+
////}
10+
11+
// @Filename: /project/app.tsx
12+
////const state = useMemo(() => 'Hello', []);
13+
14+
// @Filename: /project/component.tsx
15+
////import { useEffect } from "preact/hooks";
16+
17+
// @Filename: /project/node_modules/preact/package.json
18+
////{ "name": "preact", "version": "10.3.4", "types": "src/index.d.ts" }
19+
20+
// @Filename: /project/node_modules/preact/hooks/package.json
21+
////{ "name": "hooks", "version": "0.1.0", "types": "src/index.d.ts" }
22+
23+
// @Filename: /project/node_modules/preact/hooks/src/index.d.ts
24+
////export function useEffect(): void;
25+
////export function useMemo<T>(factory: () => T, inputs: ReadonlyArray<unknown> | undefined): T;
26+
27+
goTo.file("/project/app.tsx");
28+
verify.importFixAtPosition([
29+
getImportFixContent("preact/hooks"),
30+
]);
31+
32+
function getImportFixContent(from: string) {
33+
return `import { useMemo } from "${from}";
34+
35+
const state = useMemo(() => 'Hello', []);`;
36+
}

0 commit comments

Comments
 (0)