Skip to content

Commit 2cb0403

Browse files
author
Andy
authored
Support 'package.json' not in package root (#19133)
* Support 'package.json' not in package root * Test "foo/@bar" * More tests, and don't use "types" from the root package.json if not loading the root module
1 parent 40222d1 commit 2cb0403

21 files changed

+288
-12
lines changed

src/compiler/moduleNameResolver.ts

Lines changed: 28 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -77,16 +77,20 @@ namespace ts {
7777
traceEnabled: boolean;
7878
}
7979

80-
interface PackageJson {
81-
name?: string;
82-
version?: string;
80+
/** Just the fields that we use for module resolution. */
81+
interface PackageJsonPathFields {
8382
typings?: string;
8483
types?: string;
8584
main?: string;
8685
}
8786

87+
interface PackageJson extends PackageJsonPathFields {
88+
name?: string;
89+
version?: string;
90+
}
91+
8892
/** Reads from "main" or "types"/"typings" depending on `extensions`. */
89-
function tryReadPackageJsonFields(readTypes: boolean, jsonContent: PackageJson, baseDirectory: string, state: ModuleResolutionState): string | undefined {
93+
function tryReadPackageJsonFields(readTypes: boolean, jsonContent: PackageJsonPathFields, baseDirectory: string, state: ModuleResolutionState): string | undefined {
9094
return readTypes ? tryReadFromField("typings") || tryReadFromField("types") : tryReadFromField("main");
9195

9296
function tryReadFromField(fieldName: "typings" | "types" | "main"): string | undefined {
@@ -886,7 +890,7 @@ namespace ts {
886890
return withPackageId(packageId, loadNodeModuleFromDirectoryWorker(extensions, candidate, failedLookupLocations, onlyRecordFailures, state, packageJsonContent));
887891
}
888892

889-
function loadNodeModuleFromDirectoryWorker(extensions: Extensions, candidate: string, failedLookupLocations: Push<string>, onlyRecordFailures: boolean, state: ModuleResolutionState, packageJsonContent: PackageJson | undefined): PathAndExtension | undefined {
893+
function loadNodeModuleFromDirectoryWorker(extensions: Extensions, candidate: string, failedLookupLocations: Push<string>, onlyRecordFailures: boolean, state: ModuleResolutionState, packageJsonContent: PackageJsonPathFields | undefined): PathAndExtension | undefined {
890894
const fromPackageJson = packageJsonContent && loadModuleFromPackageJson(packageJsonContent, extensions, candidate, failedLookupLocations, state);
891895
if (fromPackageJson) {
892896
return fromPackageJson;
@@ -901,7 +905,7 @@ namespace ts {
901905
failedLookupLocations: Push<string>,
902906
onlyRecordFailures: boolean,
903907
{ host, traceEnabled }: ModuleResolutionState,
904-
): { packageJsonContent: PackageJson | undefined, packageId: PackageId | undefined } {
908+
): { found: boolean, packageJsonContent: PackageJsonPathFields | undefined, packageId: PackageId | undefined } {
905909
const directoryExists = !onlyRecordFailures && directoryProbablyExists(nodeModuleDirectory, host);
906910
const packageJsonPath = pathToPackageJson(nodeModuleDirectory);
907911
if (directoryExists && host.fileExists(packageJsonPath)) {
@@ -912,19 +916,19 @@ namespace ts {
912916
const packageId: PackageId = typeof packageJsonContent.name === "string" && typeof packageJsonContent.version === "string"
913917
? { name: packageJsonContent.name, subModuleName, version: packageJsonContent.version }
914918
: undefined;
915-
return { packageJsonContent, packageId };
919+
return { found: true, packageJsonContent, packageId };
916920
}
917921
else {
918922
if (directoryExists && traceEnabled) {
919923
trace(host, Diagnostics.File_0_does_not_exist, packageJsonPath);
920924
}
921925
// record package json as one of failed lookup locations - in the future if this file will appear it will invalidate resolution results
922926
failedLookupLocations.push(packageJsonPath);
923-
return { packageJsonContent: undefined, packageId: undefined };
927+
return { found: false, packageJsonContent: undefined, packageId: undefined };
924928
}
925929
}
926930

927-
function loadModuleFromPackageJson(jsonContent: PackageJson, extensions: Extensions, candidate: string, failedLookupLocations: Push<string>, state: ModuleResolutionState): PathAndExtension | undefined {
931+
function loadModuleFromPackageJson(jsonContent: PackageJsonPathFields, extensions: Extensions, candidate: string, failedLookupLocations: Push<string>, state: ModuleResolutionState): PathAndExtension | undefined {
928932
const file = tryReadPackageJsonFields(extensions !== Extensions.JavaScript, jsonContent, candidate, state);
929933
if (!file) {
930934
return undefined;
@@ -976,10 +980,22 @@ namespace ts {
976980
}
977981

978982
function loadModuleFromNodeModulesFolder(extensions: Extensions, moduleName: string, nodeModulesFolder: string, nodeModulesFolderExists: boolean, failedLookupLocations: Push<string>, state: ModuleResolutionState): Resolved | undefined {
979-
const { packageName, rest } = getPackageName(moduleName);
980-
const packageRootPath = combinePaths(nodeModulesFolder, packageName);
981-
const { packageJsonContent, packageId } = getPackageJsonInfo(packageRootPath, rest, failedLookupLocations, !nodeModulesFolderExists, state);
982983
const candidate = normalizePath(combinePaths(nodeModulesFolder, moduleName));
984+
// First look for a nested package.json, as in `node_modules/foo/bar/package.json`.
985+
let packageJsonContent: PackageJsonPathFields | undefined;
986+
let packageId: PackageId | undefined;
987+
const packageInfo = getPackageJsonInfo(candidate, "", failedLookupLocations, /*onlyRecordFailures*/ !nodeModulesFolderExists, state);
988+
if (packageInfo.found) {
989+
({ packageJsonContent, packageId } = packageInfo);
990+
}
991+
else {
992+
const { packageName, rest } = getPackageName(moduleName);
993+
if (rest !== "") { // If "rest" is empty, we just did this search above.
994+
const packageRootPath = combinePaths(nodeModulesFolder, packageName);
995+
// Don't use a "types" or "main" from here because we're not loading the root, but a subdirectory -- just here for the packageId.
996+
packageId = getPackageJsonInfo(packageRootPath, rest, failedLookupLocations, !nodeModulesFolderExists, state).packageId;
997+
}
998+
}
983999
const pathAndExtension = loadModuleFromFile(extensions, candidate, failedLookupLocations, !nodeModulesFolderExists, state) ||
9841000
loadNodeModuleFromDirectoryWorker(extensions, candidate, failedLookupLocations, !nodeModulesFolderExists, state, packageJsonContent);
9851001
return withPackageId(packageId, pathAndExtension);
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
//// [tests/cases/compiler/moduleResolution_packageJson_notAtPackageRoot.ts] ////
2+
3+
//// [package.json]
4+
// Loads from a "fake" nested package.json, not from the one at the root.
5+
6+
{ "types": "types.d.ts" }
7+
8+
//// [package.json]
9+
{}
10+
11+
//// [types.d.ts]
12+
export const x: number;
13+
14+
//// [a.ts]
15+
import { x } from "foo/bar";
16+
17+
18+
//// [a.js]
19+
"use strict";
20+
exports.__esModule = true;
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
=== /a.ts ===
2+
import { x } from "foo/bar";
3+
>x : Symbol(x, Decl(a.ts, 0, 8))
4+
5+
=== /node_modules/foo/bar/types.d.ts ===
6+
export const x: number;
7+
>x : Symbol(x, Decl(types.d.ts, 0, 12))
8+
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
[
2+
"======== Resolving module 'foo/bar' from '/a.ts'. ========",
3+
"Module resolution kind is not specified, using 'NodeJs'.",
4+
"Loading module 'foo/bar' from 'node_modules' folder, target file type 'TypeScript'.",
5+
"Found 'package.json' at '/node_modules/foo/bar/package.json'.",
6+
"File '/node_modules/foo/bar.ts' does not exist.",
7+
"File '/node_modules/foo/bar.tsx' does not exist.",
8+
"File '/node_modules/foo/bar.d.ts' does not exist.",
9+
"'package.json' does not have a 'typings' field.",
10+
"'package.json' has 'types' field 'types.d.ts' that references '/node_modules/foo/bar/types.d.ts'.",
11+
"File '/node_modules/foo/bar/types.d.ts' exist - use it as a name resolution result.",
12+
"Resolving real path for '/node_modules/foo/bar/types.d.ts', result '/node_modules/foo/bar/types.d.ts'.",
13+
"======== Module name 'foo/bar' was successfully resolved to '/node_modules/foo/bar/types.d.ts'. ========"
14+
]
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
=== /a.ts ===
2+
import { x } from "foo/bar";
3+
>x : number
4+
5+
=== /node_modules/foo/bar/types.d.ts ===
6+
export const x: number;
7+
>x : number
8+
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
//// [tests/cases/compiler/moduleResolution_packageJson_notAtPackageRoot_fakeScopedPackage.ts] ////
2+
3+
//// [package.json]
4+
// Copy of `moduleResolution_packageJson_notAtPackageRoot` with `foo/@bar` instead of `foo/bar`. Should behave identically.
5+
6+
{ "types": "types.d.ts" }
7+
8+
//// [package.json]
9+
{}
10+
11+
//// [types.d.ts]
12+
export const x: number;
13+
14+
//// [a.ts]
15+
import { x } from "foo/@bar";
16+
17+
18+
//// [a.js]
19+
"use strict";
20+
exports.__esModule = true;
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
=== /a.ts ===
2+
import { x } from "foo/@bar";
3+
>x : Symbol(x, Decl(a.ts, 0, 8))
4+
5+
=== /node_modules/foo/@bar/types.d.ts ===
6+
export const x: number;
7+
>x : Symbol(x, Decl(types.d.ts, 0, 12))
8+
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
[
2+
"======== Resolving module 'foo/@bar' from '/a.ts'. ========",
3+
"Module resolution kind is not specified, using 'NodeJs'.",
4+
"Loading module 'foo/@bar' from 'node_modules' folder, target file type 'TypeScript'.",
5+
"Found 'package.json' at '/node_modules/foo/@bar/package.json'.",
6+
"File '/node_modules/foo/@bar.ts' does not exist.",
7+
"File '/node_modules/foo/@bar.tsx' does not exist.",
8+
"File '/node_modules/foo/@bar.d.ts' does not exist.",
9+
"'package.json' does not have a 'typings' field.",
10+
"'package.json' has 'types' field 'types.d.ts' that references '/node_modules/foo/@bar/types.d.ts'.",
11+
"File '/node_modules/foo/@bar/types.d.ts' exist - use it as a name resolution result.",
12+
"Resolving real path for '/node_modules/foo/@bar/types.d.ts', result '/node_modules/foo/@bar/types.d.ts'.",
13+
"======== Module name 'foo/@bar' was successfully resolved to '/node_modules/foo/@bar/types.d.ts'. ========"
14+
]
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
=== /a.ts ===
2+
import { x } from "foo/@bar";
3+
>x : number
4+
5+
=== /node_modules/foo/@bar/types.d.ts ===
6+
export const x: number;
7+
>x : number
8+
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
//// [tests/cases/compiler/moduleResolution_packageJson_yesAtPackageRoot.ts] ////
2+
3+
//// [index.js]
4+
not read
5+
6+
//// [package.json]
7+
{ "name": "foo", "version": "1.2.3", "types": "types.d.ts" }
8+
9+
//// [types.d.ts]
10+
export const x = 0;
11+
12+
//// [a.ts]
13+
import { x } from "foo/bar";
14+
15+
16+
//// [a.js]
17+
"use strict";
18+
exports.__esModule = true;

0 commit comments

Comments
 (0)