Skip to content

Commit d24b689

Browse files
author
Andy
authored
Merge pull request #13678 from Microsoft/package_json_main_2
Allow package.json "main" to specify a directory
2 parents 2fc634f + 132fa70 commit d24b689

20 files changed

+403
-81
lines changed

src/compiler/diagnosticMessages.json

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2801,7 +2801,7 @@
28012801
"category": "Message",
28022802
"code": 6099
28032803
},
2804-
"'package.json' does not have a 'types' or 'main' field.": {
2804+
"'package.json' does not have a '{0}' field.": {
28052805
"category": "Message",
28062806
"code": 6100
28072807
},
@@ -2949,10 +2949,6 @@
29492949
"category": "Message",
29502950
"code": 6136
29512951
},
2952-
"No types specified in 'package.json', so returning 'main' value of '{0}'": {
2953-
"category": "Message",
2954-
"code": 6137
2955-
},
29562952
"Property '{0}' is declared but never used.": {
29572953
"category": "Error",
29582954
"code": 6138

src/compiler/moduleNameResolver.ts

Lines changed: 62 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -67,40 +67,31 @@ 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(readTypes: boolean, packageJsonPath: string, baseDirectory: string, state: ModuleResolutionState): string | undefined {
7171
const jsonContent = readJson(packageJsonPath, state.host);
72+
return readTypes ? tryReadFromField("typings") || tryReadFromField("types") : tryReadFromField("main");
7273

73-
switch (extensions) {
74-
case Extensions.DtsOnly:
75-
case Extensions.TypeScript:
76-
return tryReadFromField("typings") || tryReadFromField("types");
77-
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));
74+
function tryReadFromField(fieldName: "typings" | "types" | "main"): string | undefined {
75+
if (!hasProperty(jsonContent, fieldName)) {
76+
if (state.traceEnabled) {
77+
trace(state.host, Diagnostics.package_json_does_not_have_a_0_field, fieldName);
8478
}
85-
return undefined;
86-
}
79+
return;
80+
}
8781

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-
}
82+
const fileName = jsonContent[fieldName];
83+
if (typeof fileName !== "string") {
84+
if (state.traceEnabled) {
85+
trace(state.host, Diagnostics.Expected_type_of_0_field_in_package_json_to_be_string_got_1, fieldName, typeof fileName);
10286
}
87+
return;
10388
}
89+
90+
const path = normalizePath(combinePaths(baseDirectory, fileName));
91+
if (state.traceEnabled) {
92+
trace(state.host, Diagnostics.package_json_has_0_field_1_that_references_2, fieldName, fileName, path);
93+
}
94+
return path;
10495
}
10596
}
10697

@@ -698,7 +689,8 @@ namespace ts {
698689
return { resolvedModule: undefined, failedLookupLocations };
699690

700691
function tryResolve(extensions: Extensions): SearchResult<{ resolved: Resolved, isExternalLibraryImport: boolean }> {
701-
const resolved = tryLoadModuleUsingOptionalResolutionSettings(extensions, moduleName, containingDirectory, nodeLoadModuleByRelativeName, failedLookupLocations, state);
692+
const loader: ResolutionKindSpecificLoader = (extensions, candidate, failedLookupLocations, onlyRecordFailures, state) => nodeLoadModuleByRelativeName(extensions, candidate, failedLookupLocations, onlyRecordFailures, state, /*considerPackageJson*/true);
693+
const resolved = tryLoadModuleUsingOptionalResolutionSettings(extensions, moduleName, containingDirectory, loader, failedLookupLocations, state);
702694
if (resolved) {
703695
return toSearchResult({ resolved, isExternalLibraryImport: false });
704696
}
@@ -713,7 +705,7 @@ namespace ts {
713705
}
714706
else {
715707
const candidate = normalizePath(combinePaths(containingDirectory, moduleName));
716-
const resolved = nodeLoadModuleByRelativeName(extensions, candidate, failedLookupLocations, /*onlyRecordFailures*/ false, state);
708+
const resolved = nodeLoadModuleByRelativeName(extensions, candidate, failedLookupLocations, /*onlyRecordFailures*/ false, state, /*considerPackageJson*/true);
717709
return resolved && toSearchResult({ resolved, isExternalLibraryImport: false });
718710
}
719711
}
@@ -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: boolean): 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,55 @@ 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 file = tryReadPackageJsonFields(extensions !== Extensions.JavaScript, packageJsonPath, candidate, state);
859+
if (!file) {
860+
return undefined;
861+
}
862+
863+
const onlyRecordFailures = !directoryProbablyExists(getDirectoryPath(file), state.host);
864+
const fromFile = tryFile(file, failedLookupLocations, onlyRecordFailures, state);
865+
if (fromFile) {
866+
const resolved = fromFile && resolvedIfExtensionMatches(extensions, fromFile);
867+
if (resolved) {
868+
return resolved;
869+
}
870+
if (state.traceEnabled) {
871+
trace(state.host, Diagnostics.File_0_has_an_unsupported_extension_so_skipping_it, fromFile);
874872
}
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);
877873
}
878874

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

882881
/** Resolve from an arbitrarily specified file. Return `undefined` if it has an unsupported extension. */
@@ -1040,7 +1039,6 @@ namespace ts {
10401039
return value !== undefined ? { value } : undefined;
10411040
}
10421041

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

tests/baselines/reference/library-reference-12.trace.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
"Directory '/a/b/node_modules' does not exist, skipping all lookups in it.",
66
"File '/a/node_modules/jquery.d.ts' does not exist.",
77
"Found 'package.json' at '/a/node_modules/jquery/package.json'.",
8+
"'package.json' does not have a 'typings' field.",
89
"'package.json' has 'types' field 'dist/jquery.d.ts' that references '/a/node_modules/jquery/dist/jquery.d.ts'.",
910
"File '/a/node_modules/jquery/dist/jquery.d.ts' exist - use it as a name resolution result.",
1011
"Resolving real path for '/a/node_modules/jquery/dist/jquery.d.ts', result '/a/node_modules/jquery/dist/jquery.d.ts'",

tests/baselines/reference/library-reference-2.trace.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,15 @@
22
"======== Resolving type reference directive 'jquery', containing file '/consumer.ts', root directory '/types'. ========",
33
"Resolving with primary search path '/types'",
44
"Found 'package.json' at '/types/jquery/package.json'.",
5+
"'package.json' does not have a 'typings' field.",
56
"'package.json' has 'types' field 'jquery.d.ts' that references '/types/jquery/jquery.d.ts'.",
67
"File '/types/jquery/jquery.d.ts' exist - use it as a name resolution result.",
78
"Resolving real path for '/types/jquery/jquery.d.ts', result '/types/jquery/jquery.d.ts'",
89
"======== Type reference directive 'jquery' was successfully resolved to '/types/jquery/jquery.d.ts', primary: true. ========",
910
"======== Resolving type reference directive 'jquery', containing file 'test/__inferred type names__.ts', root directory '/types'. ========",
1011
"Resolving with primary search path '/types'",
1112
"Found 'package.json' at '/types/jquery/package.json'.",
13+
"'package.json' does not have a 'typings' field.",
1214
"'package.json' has 'types' field 'jquery.d.ts' that references '/types/jquery/jquery.d.ts'.",
1315
"File '/types/jquery/jquery.d.ts' exist - use it as a name resolution result.",
1416
"Resolving real path for '/types/jquery/jquery.d.ts', result '/types/jquery/jquery.d.ts'",

tests/baselines/reference/moduleResolutionWithExtensions_unexpected.trace.json

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@
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 'typings' field.",
10+
"'package.json' does not have a 'types' field.",
1011
"File '/node_modules/normalize.css/index.ts' does not exist.",
1112
"File '/node_modules/normalize.css/index.tsx' does not exist.",
1213
"File '/node_modules/normalize.css/index.d.ts' does not exist.",
@@ -15,12 +16,13 @@
1516
"File '/node_modules/normalize.css.js' does not exist.",
1617
"File '/node_modules/normalize.css.jsx' does not exist.",
1718
"Found 'package.json' at '/node_modules/normalize.css/package.json'.",
18-
"No types specified in 'package.json', so returning 'main' value of 'normalize.css'",
19+
"'package.json' has 'main' field 'normalize.css' that references '/node_modules/normalize.css/normalize.css'.",
1920
"File '/node_modules/normalize.css/normalize.css' exist - use it as a name resolution result.",
2021
"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.",
22+
"Loading module as file / folder, candidate module location '/node_modules/normalize.css/normalize.css', target file type 'JavaScript'.",
23+
"File '/node_modules/normalize.css/normalize.css.js' does not exist.",
24+
"File '/node_modules/normalize.css/normalize.css.jsx' does not exist.",
25+
"Directory '/node_modules/normalize.css/normalize.css' does not exist, skipping all lookups in it.",
2426
"File '/node_modules/normalize.css/index.js' does not exist.",
2527
"File '/node_modules/normalize.css/index.jsx' does not exist.",
2628
"======== Module name 'normalize.css' was not resolved. ========"

tests/baselines/reference/moduleResolutionWithExtensions_unexpected2.trace.json

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,19 @@
66
"File '/node_modules/foo.tsx' does not exist.",
77
"File '/node_modules/foo.d.ts' does not exist.",
88
"Found 'package.json' at '/node_modules/foo/package.json'.",
9+
"'package.json' does not have a 'typings' field.",
910
"'package.json' has 'types' field 'foo.js' that references '/node_modules/foo/foo.js'.",
1011
"File '/node_modules/foo/foo.js' exist - use it as a name resolution result.",
1112
"File '/node_modules/foo/foo.js' has an unsupported extension, so skipping it.",
13+
"Loading module as file / folder, candidate module location '/node_modules/foo/foo.js', target file type 'TypeScript'.",
1214
"File '/node_modules/foo/foo.js.ts' does not exist.",
1315
"File '/node_modules/foo/foo.js.tsx' does not exist.",
1416
"File '/node_modules/foo/foo.js.d.ts' does not exist.",
17+
"File name '/node_modules/foo/foo.js' has a '.js' extension - stripping it",
18+
"File '/node_modules/foo/foo.ts' does not exist.",
19+
"File '/node_modules/foo/foo.tsx' does not exist.",
20+
"File '/node_modules/foo/foo.d.ts' does not exist.",
21+
"Directory '/node_modules/foo/foo.js' does not exist, skipping all lookups in it.",
1522
"File '/node_modules/foo/index.ts' does not exist.",
1623
"File '/node_modules/foo/index.tsx' does not exist.",
1724
"File '/node_modules/foo/index.d.ts' does not exist.",
@@ -20,7 +27,7 @@
2027
"File '/node_modules/foo.js' does not exist.",
2128
"File '/node_modules/foo.jsx' does not exist.",
2229
"Found 'package.json' at '/node_modules/foo/package.json'.",
23-
"'package.json' does not have a 'types' or 'main' field.",
30+
"'package.json' does not have a 'main' field.",
2431
"File '/node_modules/foo/index.js' does not exist.",
2532
"File '/node_modules/foo/index.jsx' does not exist.",
2633
"======== 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+

0 commit comments

Comments
 (0)