Skip to content

Commit 432808d

Browse files
author
Andy Hanson
committed
Allow package.json "main" to specify a directory
1 parent 8144c89 commit 432808d

18 files changed

+393
-79
lines changed

src/compiler/diagnosticMessages.json

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2793,7 +2793,7 @@
27932793
"category": "Message",
27942794
"code": 6099
27952795
},
2796-
"'package.json' does not have a 'types' or 'main' field.": {
2796+
"'package.json' does not have a '{0}' field.": {
27972797
"category": "Message",
27982798
"code": 6100
27992799
},
@@ -2941,10 +2941,6 @@
29412941
"category": "Message",
29422942
"code": 6136
29432943
},
2944-
"No types specified in 'package.json', so returning 'main' value of '{0}'": {
2945-
"category": "Message",
2946-
"code": 6137
2947-
},
29482944
"Property '{0}' is declared but never used.": {
29492945
"category": "Error",
29502946
"code": 6138

src/compiler/moduleNameResolver.ts

Lines changed: 61 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -67,40 +67,32 @@ namespace ts {
6767
}
6868

6969
/** Reads from "main" or "types"/"typings" depending on `extensions`. */
70-
function tryReadPackageJsonMainOrTypes(extensions: Extensions, packageJsonPath: string, baseDirectory: string, state: ModuleResolutionState): string {
70+
function tryReadPackageJsonFields(ts: boolean, packageJsonPath: string, baseDirectory: string, state: ModuleResolutionState): string | undefined {
7171
const jsonContent = readJson(packageJsonPath, state.host);
72+
const file = ts ? tryReadFromField("typings") || tryReadFromField("types") : tryReadFromField("main");
73+
if (!file && state.traceEnabled) {
74+
trace(state.host, Diagnostics.package_json_does_not_have_a_0_field, ts ? "types" : "main");
75+
}
76+
return file;
7277

73-
switch (extensions) {
74-
case Extensions.DtsOnly:
75-
case Extensions.TypeScript:
76-
return tryReadFromField("typings") || tryReadFromField("types");
78+
function tryReadFromField(fieldName: "typings" | "types" | "main"): string | undefined {
79+
if (!hasProperty(jsonContent, fieldName)) {
80+
return;
81+
}
7782

78-
case Extensions.JavaScript:
79-
if (typeof jsonContent.main === "string") {
80-
if (state.traceEnabled) {
81-
trace(state.host, Diagnostics.No_types_specified_in_package_json_so_returning_main_value_of_0, jsonContent.main);
82-
}
83-
return normalizePath(combinePaths(baseDirectory, jsonContent.main));
83+
const fileName = jsonContent[fieldName];
84+
if (typeof fileName !== "string") {
85+
if (state.traceEnabled) {
86+
trace(state.host, Diagnostics.Expected_type_of_0_field_in_package_json_to_be_string_got_1, fieldName, typeof fileName);
8487
}
85-
return undefined;
86-
}
88+
return;
89+
}
8790

88-
function tryReadFromField(fieldName: string) {
89-
if (hasProperty(jsonContent, fieldName)) {
90-
const typesFile = (<any>jsonContent)[fieldName];
91-
if (typeof typesFile === "string") {
92-
const typesFilePath = normalizePath(combinePaths(baseDirectory, typesFile));
93-
if (state.traceEnabled) {
94-
trace(state.host, Diagnostics.package_json_has_0_field_1_that_references_2, fieldName, typesFile, typesFilePath);
95-
}
96-
return typesFilePath;
97-
}
98-
else {
99-
if (state.traceEnabled) {
100-
trace(state.host, Diagnostics.Expected_type_of_0_field_in_package_json_to_be_string_got_1, fieldName, typeof typesFile);
101-
}
102-
}
91+
const path = normalizePath(combinePaths(baseDirectory, fileName));
92+
if (state.traceEnabled) {
93+
trace(state.host, Diagnostics.package_json_has_0_field_1_that_references_2, fieldName, fileName, path);
10394
}
95+
return path;
10496
}
10597
}
10698

@@ -731,7 +723,7 @@ namespace ts {
731723
return real;
732724
}
733725

734-
function nodeLoadModuleByRelativeName(extensions: Extensions, candidate: string, failedLookupLocations: Push<string>, onlyRecordFailures: boolean, state: ModuleResolutionState): Resolved | undefined {
726+
function nodeLoadModuleByRelativeName(extensions: Extensions, candidate: string, failedLookupLocations: Push<string>, onlyRecordFailures: boolean, state: ModuleResolutionState, considerPackageJson = true): Resolved | undefined {
735727
if (state.traceEnabled) {
736728
trace(state.host, Diagnostics.Loading_module_as_file_Slash_folder_candidate_module_location_0_target_file_type_1, candidate, Extensions[extensions]);
737729
}
@@ -759,7 +751,7 @@ namespace ts {
759751
onlyRecordFailures = true;
760752
}
761753
}
762-
return loadNodeModuleFromDirectory(extensions, candidate, failedLookupLocations, onlyRecordFailures, state);
754+
return loadNodeModuleFromDirectory(extensions, candidate, failedLookupLocations, onlyRecordFailures, state, considerPackageJson);
763755
}
764756

765757
/* @internal */
@@ -835,48 +827,56 @@ namespace ts {
835827
return undefined;
836828
}
837829

838-
function loadNodeModuleFromDirectory(extensions: Extensions, candidate: string, failedLookupLocations: Push<string>, onlyRecordFailures: boolean, state: ModuleResolutionState): Resolved | undefined {
839-
const packageJsonPath = pathToPackageJson(candidate);
830+
function loadNodeModuleFromDirectory(extensions: Extensions, candidate: string, failedLookupLocations: Push<string>, onlyRecordFailures: boolean, state: ModuleResolutionState, considerPackageJson = true): Resolved | undefined {
840831
const directoryExists = !onlyRecordFailures && directoryProbablyExists(candidate, state.host);
841832

842-
if (directoryExists && state.host.fileExists(packageJsonPath)) {
843-
if (state.traceEnabled) {
844-
trace(state.host, Diagnostics.Found_package_json_at_0, packageJsonPath);
845-
}
846-
const mainOrTypesFile = tryReadPackageJsonMainOrTypes(extensions, packageJsonPath, candidate, state);
847-
if (mainOrTypesFile) {
848-
const onlyRecordFailures = !directoryProbablyExists(getDirectoryPath(mainOrTypesFile), state.host);
849-
// A package.json "typings" may specify an exact filename, or may choose to omit an extension.
850-
const fromExactFile = tryFile(mainOrTypesFile, failedLookupLocations, onlyRecordFailures, state);
851-
if (fromExactFile) {
852-
const resolved = fromExactFile && resolvedIfExtensionMatches(extensions, fromExactFile);
853-
if (resolved) {
854-
return resolved;
855-
}
856-
if (state.traceEnabled) {
857-
trace(state.host, Diagnostics.File_0_has_an_unsupported_extension_so_skipping_it, fromExactFile);
858-
}
859-
}
860-
const resolved = tryAddingExtensions(mainOrTypesFile, Extensions.TypeScript, failedLookupLocations, onlyRecordFailures, state);
861-
if (resolved) {
862-
return resolved;
833+
if (considerPackageJson) {
834+
const packageJsonPath = pathToPackageJson(candidate);
835+
if (directoryExists && state.host.fileExists(packageJsonPath)) {
836+
const fromPackageJson = loadModuleFromPackageJson(packageJsonPath, extensions, candidate, failedLookupLocations, state);
837+
if (fromPackageJson) {
838+
return fromPackageJson;
863839
}
864840
}
865841
else {
866-
if (state.traceEnabled) {
867-
trace(state.host, Diagnostics.package_json_does_not_have_a_types_or_main_field);
842+
if (directoryExists && state.traceEnabled) {
843+
trace(state.host, Diagnostics.File_0_does_not_exist, packageJsonPath);
868844
}
845+
// record package json as one of failed lookup locations - in the future if this file will appear it will invalidate resolution results
846+
failedLookupLocations.push(packageJsonPath);
869847
}
870848
}
871-
else {
872-
if (directoryExists && state.traceEnabled) {
873-
trace(state.host, Diagnostics.File_0_does_not_exist, packageJsonPath);
849+
850+
return loadModuleFromFile(extensions, combinePaths(candidate, "index"), failedLookupLocations, !directoryExists, state);
851+
}
852+
853+
function loadModuleFromPackageJson(packageJsonPath: string, extensions: Extensions, candidate: string, failedLookupLocations: Push<string>, state: ModuleResolutionState): Resolved | undefined {
854+
if (state.traceEnabled) {
855+
trace(state.host, Diagnostics.Found_package_json_at_0, packageJsonPath);
856+
}
857+
858+
const ts = extensions !== Extensions.JavaScript;
859+
const file = tryReadPackageJsonFields(ts, packageJsonPath, candidate, state);
860+
if (!file) {
861+
return undefined;
862+
}
863+
864+
const onlyRecordFailures = !directoryProbablyExists(getDirectoryPath(file), state.host);
865+
const fromFile = tryFile(file, failedLookupLocations, onlyRecordFailures, state);
866+
if (fromFile) {
867+
const resolved = fromFile && resolvedIfExtensionMatches(extensions, fromFile);
868+
if (resolved) {
869+
return resolved;
870+
}
871+
if (state.traceEnabled) {
872+
trace(state.host, Diagnostics.File_0_has_an_unsupported_extension_so_skipping_it, fromFile);
874873
}
875-
// record package json as one of failed lookup locations - in the future if this file will appear it will invalidate resolution results
876-
failedLookupLocations.push(packageJsonPath);
877874
}
878875

879-
return loadModuleFromFile(extensions, combinePaths(candidate, "index"), failedLookupLocations, !directoryExists, state);
876+
// Even if extensions is DtsOnly, we can still look up a .ts file as a result of package.json "types"
877+
const nextExtensions = extensions === Extensions.DtsOnly ? Extensions.TypeScript : extensions;
878+
// Don't do package.json lookup recursively, because Node.js' package lookup doesn't.
879+
return nodeLoadModuleByRelativeName(nextExtensions, file, failedLookupLocations, onlyRecordFailures, state, /*considerPackageJson*/ false);
880880
}
881881

882882
/** Resolve from an arbitrarily specified file. Return `undefined` if it has an unsupported extension. */
@@ -1040,7 +1040,6 @@ namespace ts {
10401040
return value !== undefined ? { value } : undefined;
10411041
}
10421042

1043-
10441043
/** Calls `callback` on `directory` and every ancestor directory it has, returning the first defined result. */
10451044
function forEachAncestorDirectory<T>(directory: string, callback: (directory: string) => SearchResult<T>): SearchResult<T> {
10461045
while (true) {

tests/baselines/reference/moduleResolutionWithExtensions_unexpected.trace.json

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
"File '/node_modules/normalize.css.tsx' does not exist.",
77
"File '/node_modules/normalize.css.d.ts' does not exist.",
88
"Found 'package.json' at '/node_modules/normalize.css/package.json'.",
9-
"'package.json' does not have a 'types' or 'main' field.",
9+
"'package.json' does not have a 'types' field.",
1010
"File '/node_modules/normalize.css/index.ts' does not exist.",
1111
"File '/node_modules/normalize.css/index.tsx' does not exist.",
1212
"File '/node_modules/normalize.css/index.d.ts' does not exist.",
@@ -15,12 +15,13 @@
1515
"File '/node_modules/normalize.css.js' does not exist.",
1616
"File '/node_modules/normalize.css.jsx' does not exist.",
1717
"Found 'package.json' at '/node_modules/normalize.css/package.json'.",
18-
"No types specified in 'package.json', so returning 'main' value of 'normalize.css'",
18+
"'package.json' has 'main' field 'normalize.css' that references '/node_modules/normalize.css/normalize.css'.",
1919
"File '/node_modules/normalize.css/normalize.css' exist - use it as a name resolution result.",
2020
"File '/node_modules/normalize.css/normalize.css' has an unsupported extension, so skipping it.",
21-
"File '/node_modules/normalize.css/normalize.css.ts' does not exist.",
22-
"File '/node_modules/normalize.css/normalize.css.tsx' does not exist.",
23-
"File '/node_modules/normalize.css/normalize.css.d.ts' does not exist.",
21+
"Loading module as file / folder, candidate module location '/node_modules/normalize.css/normalize.css', target file type 'JavaScript'.",
22+
"File '/node_modules/normalize.css/normalize.css.js' does not exist.",
23+
"File '/node_modules/normalize.css/normalize.css.jsx' does not exist.",
24+
"Directory '/node_modules/normalize.css/normalize.css' does not exist, skipping all lookups in it.",
2425
"File '/node_modules/normalize.css/index.js' does not exist.",
2526
"File '/node_modules/normalize.css/index.jsx' does not exist.",
2627
"======== Module name 'normalize.css' was not resolved. ========"

tests/baselines/reference/moduleResolutionWithExtensions_unexpected2.trace.json

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,15 @@
99
"'package.json' has 'types' field 'foo.js' that references '/node_modules/foo/foo.js'.",
1010
"File '/node_modules/foo/foo.js' exist - use it as a name resolution result.",
1111
"File '/node_modules/foo/foo.js' has an unsupported extension, so skipping it.",
12+
"Loading module as file / folder, candidate module location '/node_modules/foo/foo.js', target file type 'TypeScript'.",
1213
"File '/node_modules/foo/foo.js.ts' does not exist.",
1314
"File '/node_modules/foo/foo.js.tsx' does not exist.",
1415
"File '/node_modules/foo/foo.js.d.ts' does not exist.",
16+
"File name '/node_modules/foo/foo.js' has a '.js' extension - stripping it",
17+
"File '/node_modules/foo/foo.ts' does not exist.",
18+
"File '/node_modules/foo/foo.tsx' does not exist.",
19+
"File '/node_modules/foo/foo.d.ts' does not exist.",
20+
"Directory '/node_modules/foo/foo.js' does not exist, skipping all lookups in it.",
1521
"File '/node_modules/foo/index.ts' does not exist.",
1622
"File '/node_modules/foo/index.tsx' does not exist.",
1723
"File '/node_modules/foo/index.d.ts' does not exist.",
@@ -20,7 +26,7 @@
2026
"File '/node_modules/foo.js' does not exist.",
2127
"File '/node_modules/foo.jsx' does not exist.",
2228
"Found 'package.json' at '/node_modules/foo/package.json'.",
23-
"'package.json' does not have a 'types' or 'main' field.",
29+
"'package.json' does not have a 'main' field.",
2430
"File '/node_modules/foo/index.js' does not exist.",
2531
"File '/node_modules/foo/index.jsx' does not exist.",
2632
"======== Module name 'foo' was not resolved. ========"
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
//// [tests/cases/conformance/moduleResolution/packageJsonMain.ts] ////
2+
3+
//// [package.json]
4+
5+
{ "main": "oof" }
6+
7+
//// [oof.js]
8+
module.exports = 0;
9+
10+
//// [package.json]
11+
{ "main": "rab.js" }
12+
13+
//// [rab.js]
14+
module.exports = 0;
15+
16+
//// [package.json]
17+
{ "main": "zab" }
18+
19+
//// [index.js]
20+
module.exports = 0;
21+
22+
//// [a.ts]
23+
import foo = require("foo");
24+
import bar = require("bar");
25+
import baz = require("baz");
26+
foo + bar + baz;
27+
28+
29+
//// [a.js]
30+
"use strict";
31+
var foo = require("foo");
32+
var bar = require("bar");
33+
var baz = require("baz");
34+
foo + bar + baz;
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
=== /a.ts ===
2+
import foo = require("foo");
3+
>foo : Symbol(foo, Decl(a.ts, 0, 0))
4+
5+
import bar = require("bar");
6+
>bar : Symbol(bar, Decl(a.ts, 0, 28))
7+
8+
import baz = require("baz");
9+
>baz : Symbol(baz, Decl(a.ts, 1, 28))
10+
11+
foo + bar + baz;
12+
>foo : Symbol(foo, Decl(a.ts, 0, 0))
13+
>bar : Symbol(bar, Decl(a.ts, 0, 28))
14+
>baz : Symbol(baz, Decl(a.ts, 1, 28))
15+
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
[
2+
"======== Resolving module 'foo' from '/a.ts'. ========",
3+
"Module resolution kind is not specified, using 'NodeJs'.",
4+
"Loading module 'foo' from 'node_modules' folder, target file type 'TypeScript'.",
5+
"File '/node_modules/foo.ts' does not exist.",
6+
"File '/node_modules/foo.tsx' does not exist.",
7+
"File '/node_modules/foo.d.ts' does not exist.",
8+
"Found 'package.json' at '/node_modules/foo/package.json'.",
9+
"'package.json' does not have a 'types' field.",
10+
"File '/node_modules/foo/index.ts' does not exist.",
11+
"File '/node_modules/foo/index.tsx' does not exist.",
12+
"File '/node_modules/foo/index.d.ts' does not exist.",
13+
"Directory '/node_modules/@types' does not exist, skipping all lookups in it.",
14+
"Loading module 'foo' from 'node_modules' folder, target file type 'JavaScript'.",
15+
"File '/node_modules/foo.js' does not exist.",
16+
"File '/node_modules/foo.jsx' does not exist.",
17+
"Found 'package.json' at '/node_modules/foo/package.json'.",
18+
"'package.json' has 'main' field 'oof' that references '/node_modules/foo/oof'.",
19+
"File '/node_modules/foo/oof' does not exist.",
20+
"Loading module as file / folder, candidate module location '/node_modules/foo/oof', target file type 'JavaScript'.",
21+
"File '/node_modules/foo/oof.js' exist - use it as a name resolution result.",
22+
"Resolving real path for '/node_modules/foo/oof.js', result '/node_modules/foo/oof.js'",
23+
"======== Module name 'foo' was successfully resolved to '/node_modules/foo/oof.js'. ========",
24+
"======== Resolving module 'bar' from '/a.ts'. ========",
25+
"Module resolution kind is not specified, using 'NodeJs'.",
26+
"Loading module 'bar' from 'node_modules' folder, target file type 'TypeScript'.",
27+
"File '/node_modules/bar.ts' does not exist.",
28+
"File '/node_modules/bar.tsx' does not exist.",
29+
"File '/node_modules/bar.d.ts' does not exist.",
30+
"Found 'package.json' at '/node_modules/bar/package.json'.",
31+
"'package.json' does not have a 'types' field.",
32+
"File '/node_modules/bar/index.ts' does not exist.",
33+
"File '/node_modules/bar/index.tsx' does not exist.",
34+
"File '/node_modules/bar/index.d.ts' does not exist.",
35+
"Directory '/node_modules/@types' does not exist, skipping all lookups in it.",
36+
"Loading module 'bar' from 'node_modules' folder, target file type 'JavaScript'.",
37+
"File '/node_modules/bar.js' does not exist.",
38+
"File '/node_modules/bar.jsx' does not exist.",
39+
"Found 'package.json' at '/node_modules/bar/package.json'.",
40+
"'package.json' has 'main' field 'rab.js' that references '/node_modules/bar/rab.js'.",
41+
"File '/node_modules/bar/rab.js' exist - use it as a name resolution result.",
42+
"Resolving real path for '/node_modules/bar/rab.js', result '/node_modules/bar/rab.js'",
43+
"======== Module name 'bar' was successfully resolved to '/node_modules/bar/rab.js'. ========",
44+
"======== Resolving module 'baz' from '/a.ts'. ========",
45+
"Module resolution kind is not specified, using 'NodeJs'.",
46+
"Loading module 'baz' from 'node_modules' folder, target file type 'TypeScript'.",
47+
"File '/node_modules/baz.ts' does not exist.",
48+
"File '/node_modules/baz.tsx' does not exist.",
49+
"File '/node_modules/baz.d.ts' does not exist.",
50+
"Found 'package.json' at '/node_modules/baz/package.json'.",
51+
"'package.json' does not have a 'types' field.",
52+
"File '/node_modules/baz/index.ts' does not exist.",
53+
"File '/node_modules/baz/index.tsx' does not exist.",
54+
"File '/node_modules/baz/index.d.ts' does not exist.",
55+
"Directory '/node_modules/@types' does not exist, skipping all lookups in it.",
56+
"Loading module 'baz' from 'node_modules' folder, target file type 'JavaScript'.",
57+
"File '/node_modules/baz.js' does not exist.",
58+
"File '/node_modules/baz.jsx' does not exist.",
59+
"Found 'package.json' at '/node_modules/baz/package.json'.",
60+
"'package.json' has 'main' field 'zab' that references '/node_modules/baz/zab'.",
61+
"File '/node_modules/baz/zab' does not exist.",
62+
"Loading module as file / folder, candidate module location '/node_modules/baz/zab', target file type 'JavaScript'.",
63+
"File '/node_modules/baz/zab.js' does not exist.",
64+
"File '/node_modules/baz/zab.jsx' does not exist.",
65+
"File '/node_modules/baz/zab/index.js' exist - use it as a name resolution result.",
66+
"Resolving real path for '/node_modules/baz/zab/index.js', result '/node_modules/baz/zab/index.js'",
67+
"======== Module name 'baz' was successfully resolved to '/node_modules/baz/zab/index.js'. ========"
68+
]
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
=== /a.ts ===
2+
import foo = require("foo");
3+
>foo : any
4+
5+
import bar = require("bar");
6+
>bar : any
7+
8+
import baz = require("baz");
9+
>baz : any
10+
11+
foo + bar + baz;
12+
>foo + bar + baz : any
13+
>foo + bar : any
14+
>foo : any
15+
>bar : any
16+
>baz : any
17+

0 commit comments

Comments
 (0)