Skip to content

Refactor module resolution logic to have configurable goal extensions #9430

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 9 commits into from
Closed
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions src/compiler/diagnosticMessages.json
Original file line number Diff line number Diff line change
Expand Up @@ -2660,7 +2660,7 @@
"category": "Message",
"code": 6099
},
"'package.json' does not have 'types' field.": {
"'package.json' does not have '{0}' field.": {
"category": "Message",
"code": 6100
},
Expand All @@ -2680,7 +2680,7 @@
"category": "Message",
"code": 6104
},
"Expected type of '{0}' field in 'package.json' to be 'string', got '{1}'.": {
"Expected type of '{0}' field in 'package.json' to be '{1}', got '{2}'.": {
"category": "Message",
"code": 6105
},
Expand Down
91 changes: 40 additions & 51 deletions src/compiler/program.ts
Original file line number Diff line number Diff line change
Expand Up @@ -129,57 +129,49 @@ namespace ts {
skipTsx: boolean;
}

function tryReadTypesSection(packageJsonPath: string, baseDirectory: string, state: ModuleResolutionState): string {
let jsonContent: { typings?: string, types?: string, main?: string };
try {
const jsonText = state.host.readFile(packageJsonPath);
jsonContent = jsonText ? <{ typings?: string, types?: string, main?: string }>JSON.parse(jsonText) : {};
function getPackageEntry(packageJson: any, key: string, tag: string, state: ModuleResolutionState) {
const value = packageJson[key];
if (typeof value === tag) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

when do we need something that is not a string?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

At this point, never. This was mostly @rbuckton's suggestion. I can specialize the function, if you'd prefer it.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We discussed the possibility of augmenting the package.json for extensions, which could leverage this if we decided to lean in that direction.

return value;
}
catch (e) {
// gracefully handle if readFile fails or returns not JSON
jsonContent = {};
if (state.traceEnabled) {
trace(state.host, Diagnostics.Expected_type_of_0_field_in_package_json_to_be_1_got_2, key, tag, typeof value);
}
return undefined;
}

let typesFile: string;
let fieldName: string;
// first try to read content of 'typings' section (backward compatibility)
if (jsonContent.typings) {
if (typeof jsonContent.typings === "string") {
fieldName = "typings";
typesFile = jsonContent.typings;
}
else {
if (state.traceEnabled) {
trace(state.host, Diagnostics.Expected_type_of_0_field_in_package_json_to_be_string_got_1, "typings", typeof jsonContent.typings);
}
}
function getPackageEntryAsPath(packageJson: any, packageJsonPath: string, key: string, state: ModuleResolutionState) {
const value = getPackageEntry(packageJson, key, "string", state);
const path = value ? normalizePath(combinePaths(getDirectoryPath(packageJsonPath), value)) : undefined;
if (path && state.traceEnabled) {
trace(state.host, Diagnostics.package_json_has_0_field_1_that_references_2, key, value, path);
}
// then read 'types'
if (!typesFile && jsonContent.types) {
if (typeof jsonContent.types === "string") {
fieldName = "types";
typesFile = jsonContent.types;
}
else {
if (state.traceEnabled) {
trace(state.host, Diagnostics.Expected_type_of_0_field_in_package_json_to_be_string_got_1, "types", typeof jsonContent.types);
}
}
return path;
}

function getPackageTypes(packageJsonPath: string, state: ModuleResolutionState) {
const { config } = readConfigFile(packageJsonPath, state.host.readFile);
if (config) {
return getPackageEntryAsPath(config, packageJsonPath, "typings", state)
|| getPackageEntryAsPath(config, packageJsonPath, "types", state);
}
if (typesFile) {
const typesFilePath = normalizePath(combinePaths(baseDirectory, typesFile));
else {
if (state.traceEnabled) {
trace(state.host, Diagnostics.package_json_has_0_field_1_that_references_2, fieldName, typesFile, typesFilePath);
trace(state.host, Diagnostics.package_json_does_not_have_0_field, "types");
}
return typesFilePath;
}
// Use the main module for inferring types if no types package specified and the allowJs is set
if (state.compilerOptions.allowJs && jsonContent.main && typeof jsonContent.main === "string") {
return undefined;
}

function getPackageMain(packageJsonPath: string, state: ModuleResolutionState) {
const { config } = readConfigFile(packageJsonPath, state.host.readFile);
if (config) {
return getPackageEntryAsPath(config, packageJsonPath, "main", state);
}
else {
if (state.traceEnabled) {
trace(state.host, Diagnostics.No_types_specified_in_package_json_but_allowJs_is_set_so_returning_main_value_of_0, jsonContent.main);
trace(state.host, Diagnostics.package_json_does_not_have_0_field, "main");
}
const mainFilePath = normalizePath(combinePaths(baseDirectory, jsonContent.main));
return mainFilePath;
}
return undefined;
}
Expand Down Expand Up @@ -727,25 +719,20 @@ namespace ts {
}
}

function loadNodeModuleFromDirectory(extensions: string[], candidate: string, failedLookupLocation: string[], onlyRecordFailures: boolean, state: ModuleResolutionState): string {
function loadNodeModuleFromDirectory(extensions: string[], candidate: string, failedLookupLocation: string[], onlyRecordFailures: boolean, state: ModuleResolutionState, loadJS?: boolean): string {
const packageJsonPath = combinePaths(candidate, "package.json");
const directoryExists = !onlyRecordFailures && directoryProbablyExists(candidate, state.host);
if (directoryExists && state.host.fileExists(packageJsonPath)) {
if (state.traceEnabled) {
trace(state.host, Diagnostics.Found_package_json_at_0, packageJsonPath);
}
const typesFile = tryReadTypesSection(packageJsonPath, candidate, state);
const typesFile = loadJS ? getPackageMain(packageJsonPath, state) : getPackageTypes(packageJsonPath, state);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This won't work if the package.json doesn't have a types or main field. It'll still just fall through and return the default index.js file at the bottom of this function.

if (typesFile) {
const result = loadModuleFromFile(typesFile, extensions, failedLookupLocation, !directoryProbablyExists(getDirectoryPath(typesFile), state.host), state);
if (result) {
return result;
}
}
else {
if (state.traceEnabled) {
trace(state.host, Diagnostics.package_json_does_not_have_types_field);
}
}
}
else {
if (state.traceEnabled) {
Expand All @@ -758,7 +745,7 @@ namespace ts {
return loadModuleFromFile(combinePaths(candidate, "index"), extensions, failedLookupLocation, !directoryExists, state);
}

function loadModuleFromNodeModulesFolder(moduleName: string, directory: string, failedLookupLocations: string[], state: ModuleResolutionState): string {
function loadModuleFromNodeModulesFolder(moduleName: string, directory: string, failedLookupLocations: string[], state: ModuleResolutionState, loadJS?: boolean): string {
const nodeModulesFolder = combinePaths(directory, "node_modules");
const nodeModulesFolderExists = directoryProbablyExists(nodeModulesFolder, state.host);
const candidate = normalizePath(combinePaths(nodeModulesFolder, moduleName));
Expand All @@ -768,7 +755,7 @@ namespace ts {
if (result) {
return result;
}
result = loadNodeModuleFromDirectory(supportedExtensions, candidate, failedLookupLocations, !nodeModulesFolderExists, state);
result = loadNodeModuleFromDirectory(supportedExtensions, candidate, failedLookupLocations, !nodeModulesFolderExists, state, loadJS);
if (result) {
return result;
}
Expand All @@ -783,7 +770,9 @@ namespace ts {
// first: try to load module as-is
loadModuleFromNodeModulesFolder(moduleName, directory, failedLookupLocations, state) ||
// second: try to load module from the scope '@types'
loadModuleFromNodeModulesFolder(combinePaths("@types", moduleName), directory, failedLookupLocations, state);
loadModuleFromNodeModulesFolder(combinePaths("@types", moduleName), directory, failedLookupLocations, state) ||
// third: if allowJS try to load the JS module
(state.compilerOptions.allowJs && loadModuleFromNodeModulesFolder(moduleName, directory, failedLookupLocations, state, true))
if (result) {
return result;
}
Expand Down
1 change: 1 addition & 0 deletions tests/baselines/reference/library-reference-12.trace.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
"File '/a/node_modules/jquery.ts' does not exist.",
"File '/a/node_modules/jquery.d.ts' does not exist.",
"Found 'package.json' at '/a/node_modules/jquery/package.json'.",
"Expected type of 'typings' field in 'package.json' to be 'string', got 'undefined'.",
"'package.json' has 'types' field 'dist/jquery.d.ts' that references '/a/node_modules/jquery/dist/jquery.d.ts'.",
"File '/a/node_modules/jquery/dist/jquery.d.ts' exist - use it as a name resolution result.",
"======== Type reference directive 'jquery' was successfully resolved to '/a/node_modules/jquery/dist/jquery.d.ts', primary: false. ========"
Expand Down
2 changes: 2 additions & 0 deletions tests/baselines/reference/library-reference-2.trace.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,14 @@
"======== Resolving type reference directive 'jquery', containing file '/consumer.ts', root directory '/types'. ========",
"Resolving with primary search path '/types'",
"Found 'package.json' at '/types/jquery/package.json'.",
"Expected type of 'typings' field in 'package.json' to be 'string', got 'undefined'.",
"'package.json' has 'types' field 'jquery.d.ts' that references '/types/jquery/jquery.d.ts'.",
"File '/types/jquery/jquery.d.ts' exist - use it as a name resolution result.",
"======== Type reference directive 'jquery' was successfully resolved to '/types/jquery/jquery.d.ts', primary: true. ========",
"======== Resolving type reference directive 'jquery', containing file '/__inferred type names__.ts', root directory '/types'. ========",
"Resolving with primary search path '/types'",
"Found 'package.json' at '/types/jquery/package.json'.",
"Expected type of 'typings' field in 'package.json' to be 'string', got 'undefined'.",
"'package.json' has 'types' field 'jquery.d.ts' that references '/types/jquery/jquery.d.ts'.",
"File '/types/jquery/jquery.d.ts' exist - use it as a name resolution result.",
"======== Type reference directive 'jquery' was successfully resolved to '/types/jquery/jquery.d.ts', primary: true. ========"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
define(["require", "exports", "m1"], function (require, exports, m1) {
"use strict";
exports.m1 = m1;
var val = m1("works", 42);
void (m1.name + ": " + val);
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
{
"scenario": "Realistic npm package types",
"projectRoot": "tests/cases/projects/realisticNpmTypes",
"inputFiles": [
"index.ts"
],
"declaration": true,
"baselineCheck": true,
"moduleResolution": "node",
"resolvedInputFiles": [
"lib.d.ts",
"node_modules/@types/m1/index.d.ts",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So this file doesn't exist, but it's referenced?

Copy link
Member Author

@weswigham weswigham Jun 29, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hah. It didn't get committed because it's in a node_modules folder. That's funny. And annoying. Let me fix that.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, see the last line I added to the gitignore file on my prior merge to master. That nearly got me too.

"index.ts"
],
"emittedFiles": [
"index.js",
"index.d.ts"
]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
"use strict";
var m1 = require("m1");
exports.m1 = m1;
var val = m1("works", 42);
void (m1.name + ": " + val);
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
{
"scenario": "Realistic npm package types",
"projectRoot": "tests/cases/projects/realisticNpmTypes",
"inputFiles": [
"index.ts"
],
"declaration": true,
"baselineCheck": true,
"moduleResolution": "node",
"resolvedInputFiles": [
"lib.d.ts",
"node_modules/@types/m1/index.d.ts",
"index.ts"
],
"emittedFiles": [
"index.js",
"index.d.ts"
]
}
10 changes: 10 additions & 0 deletions tests/cases/project/realisticNpmTypes.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"scenario": "Realistic npm package types",
"projectRoot": "tests/cases/projects/realisticNpmTypes",
"inputFiles": [
"index.ts"
],
"declaration": true,
"baselineCheck": true,
"moduleResolution": "node"
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You are not specifying allowJs, that's why this is working even with the above bug, as you are never even checking for .js files.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

'doh. It's just added by default in so many places that I thought this may be one of them. Added and fixed.

6 changes: 6 additions & 0 deletions tests/cases/projects/realisticNpmTypes/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import m1 = require("m1");

const val = m1("works", 42);
void `${m1.name}: ${val}`;

export {m1};