Skip to content

Commit 9a6db93

Browse files
committed
Merge pull request #4352 from Microsoft/moduleResolutionStrategies
Adding different module resolution strategies
2 parents b8c8dbc + ab6fe49 commit 9a6db93

21 files changed

+517
-8
lines changed

Diff for: Jakefile.js

+2-1
Original file line numberDiff line numberDiff line change
@@ -143,7 +143,8 @@ var harnessSources = harnessCoreSources.concat([
143143
"convertToBase64.ts",
144144
"transpile.ts",
145145
"reuseProgramStructure.ts",
146-
"cachingInServerLSHost.ts"
146+
"cachingInServerLSHost.ts",
147+
"moduleResolution.ts"
147148
].map(function (f) {
148149
return path.join(unittestsDirectory, f);
149150
})).concat([

Diff for: src/compiler/commandLineParser.ts

+10-1
Original file line numberDiff line numberDiff line change
@@ -225,7 +225,16 @@ namespace ts {
225225
type: "boolean",
226226
experimental: true,
227227
description: Diagnostics.Enables_experimental_support_for_emitting_type_metadata_for_decorators
228-
}
228+
},
229+
{
230+
name: "moduleResolution",
231+
type: {
232+
"node": ModuleResolutionKind.NodeJs,
233+
"classic": ModuleResolutionKind.Classic
234+
},
235+
experimental: true,
236+
description: Diagnostics.Specifies_module_resolution_strategy_Colon_node_Node_or_classic_TypeScript_pre_1_6
237+
}
229238
];
230239

231240
export function parseCommandLine(commandLine: string[]): ParsedCommandLine {

Diff for: src/compiler/diagnosticInformationMap.generated.ts

+1
Original file line numberDiff line numberDiff line change
@@ -565,6 +565,7 @@ namespace ts {
565565
Enables_experimental_support_for_emitting_type_metadata_for_decorators: { code: 6066, category: DiagnosticCategory.Message, key: "Enables experimental support for emitting type metadata for decorators." },
566566
Option_experimentalAsyncFunctions_cannot_be_specified_when_targeting_ES5_or_lower: { code: 6067, category: DiagnosticCategory.Message, key: "Option 'experimentalAsyncFunctions' cannot be specified when targeting ES5 or lower." },
567567
Enables_experimental_support_for_ES7_async_functions: { code: 6068, category: DiagnosticCategory.Message, key: "Enables experimental support for ES7 async functions." },
568+
Specifies_module_resolution_strategy_Colon_node_Node_or_classic_TypeScript_pre_1_6: { code: 6069, category: DiagnosticCategory.Message, key: "Specifies module resolution strategy: 'node' (Node) or 'classic' (TypeScript pre 1.6) ." },
568569
Variable_0_implicitly_has_an_1_type: { code: 7005, category: DiagnosticCategory.Error, key: "Variable '{0}' implicitly has an '{1}' type." },
569570
Parameter_0_implicitly_has_an_1_type: { code: 7006, category: DiagnosticCategory.Error, key: "Parameter '{0}' implicitly has an '{1}' type." },
570571
Member_0_implicitly_has_an_1_type: { code: 7008, category: DiagnosticCategory.Error, key: "Member '{0}' implicitly has an '{1}' type." },

Diff for: src/compiler/diagnosticMessages.json

+5-1
Original file line numberDiff line numberDiff line change
@@ -2250,7 +2250,11 @@
22502250
"category": "Message",
22512251
"code": 6068
22522252
},
2253-
2253+
"Specifies module resolution strategy: 'node' (Node) or 'classic' (TypeScript pre 1.6) .": {
2254+
"category": "Message",
2255+
"code": 6069
2256+
},
2257+
22542258
"Variable '{0}' implicitly has an '{1}' type.": {
22552259
"category": "Error",
22562260
"code": 7005

Diff for: src/compiler/program.ts

+142-3
Original file line numberDiff line numberDiff line change
@@ -36,11 +36,150 @@ namespace ts {
3636
}
3737

3838
export function resolveModuleName(moduleName: string, containingFile: string, compilerOptions: CompilerOptions, host: ModuleResolutionHost): ResolvedModule {
39-
// TODO: use different resolution strategy based on compiler options
40-
return legacyNameResolver(moduleName, containingFile, compilerOptions, host);
39+
let moduleResolution = compilerOptions.moduleResolution !== undefined
40+
? compilerOptions.moduleResolution
41+
: compilerOptions.module === ModuleKind.CommonJS ? ModuleResolutionKind.NodeJs : ModuleResolutionKind.Classic;
42+
43+
switch (moduleResolution) {
44+
case ModuleResolutionKind.NodeJs: return nodeModuleNameResolver(moduleName, containingFile, host);
45+
case ModuleResolutionKind.Classic: return classicNameResolver(moduleName, containingFile, compilerOptions, host);
46+
}
47+
}
48+
49+
export function nodeModuleNameResolver(moduleName: string, containingFile: string, host: ModuleResolutionHost): ResolvedModule {
50+
let containingDirectory = getDirectoryPath(containingFile);
51+
52+
if (getRootLength(moduleName) !== 0 || nameStartsWithDotSlashOrDotDotSlash(moduleName)) {
53+
let failedLookupLocations: string[] = [];
54+
let candidate = normalizePath(combinePaths(containingDirectory, moduleName));
55+
let resolvedFileName = loadNodeModuleFromFile(candidate, /* loadOnlyDts */ false, failedLookupLocations, host);
56+
57+
if (resolvedFileName) {
58+
return { resolvedFileName, failedLookupLocations };
59+
}
60+
61+
resolvedFileName = loadNodeModuleFromDirectory(candidate, /* loadOnlyDts */ false, failedLookupLocations, host);
62+
return { resolvedFileName, failedLookupLocations };
63+
}
64+
else {
65+
return loadModuleFromNodeModules(moduleName, containingDirectory, host);
66+
}
67+
}
68+
69+
function loadNodeModuleFromFile(candidate: string, loadOnlyDts: boolean, failedLookupLocation: string[], host: ModuleResolutionHost): string {
70+
if (loadOnlyDts) {
71+
return tryLoad(".d.ts");
72+
}
73+
else {
74+
return forEach(supportedExtensions, tryLoad);
75+
}
76+
77+
function tryLoad(ext: string): string {
78+
let fileName = fileExtensionIs(candidate, ext) ? candidate : candidate + ext;
79+
if (host.fileExists(fileName)) {
80+
return fileName;
81+
}
82+
else {
83+
failedLookupLocation.push(fileName);
84+
return undefined;
85+
}
86+
}
87+
}
88+
89+
function loadNodeModuleFromDirectory(candidate: string, loadOnlyDts: boolean, failedLookupLocation: string[], host: ModuleResolutionHost): string {
90+
let packageJsonPath = combinePaths(candidate, "package.json");
91+
if (host.fileExists(packageJsonPath)) {
92+
93+
let jsonContent: { typings?: string };
94+
95+
try {
96+
let jsonText = host.readFile(packageJsonPath);
97+
jsonContent = jsonText ? <{ typings?: string }>JSON.parse(jsonText) : { typings: undefined };
98+
}
99+
catch (e) {
100+
// gracefully handle if readFile fails or returns not JSON
101+
jsonContent = { typings: undefined };
102+
}
103+
104+
if (jsonContent.typings) {
105+
let result = loadNodeModuleFromFile(normalizePath(combinePaths(candidate, jsonContent.typings)), loadOnlyDts, failedLookupLocation, host);
106+
if (result) {
107+
return result;
108+
}
109+
}
110+
}
111+
else {
112+
// record package json as one of failed lookup locations - in the future if this file will appear it will invalidate resolution results
113+
failedLookupLocation.push(packageJsonPath);
114+
}
115+
116+
return loadNodeModuleFromFile(combinePaths(candidate, "index"), loadOnlyDts, failedLookupLocation, host);
117+
}
118+
119+
function loadModuleFromNodeModules(moduleName: string, directory: string, host: ModuleResolutionHost): ResolvedModule {
120+
let failedLookupLocations: string[] = [];
121+
directory = normalizeSlashes(directory);
122+
while (true) {
123+
let baseName = getBaseFileName(directory);
124+
if (baseName !== "node_modules") {
125+
let nodeModulesFolder = combinePaths(directory, "node_modules");
126+
let candidate = normalizePath(combinePaths(nodeModulesFolder, moduleName));
127+
let result = loadNodeModuleFromFile(candidate, /* loadOnlyDts */ true, failedLookupLocations, host);
128+
if (result) {
129+
return { resolvedFileName: result, failedLookupLocations };
130+
}
131+
132+
result = loadNodeModuleFromDirectory(candidate, /* loadOnlyDts */ true, failedLookupLocations, host);
133+
if (result) {
134+
return { resolvedFileName: result, failedLookupLocations };
135+
}
136+
}
137+
138+
let parentPath = getDirectoryPath(directory);
139+
if (parentPath === directory) {
140+
break;
141+
}
142+
143+
directory = parentPath;
144+
}
145+
146+
return { resolvedFileName: undefined, failedLookupLocations };
147+
}
148+
149+
export function baseUrlModuleNameResolver(moduleName: string, containingFile: string, baseUrl: string, host: ModuleResolutionHost): ResolvedModule {
150+
Debug.assert(baseUrl !== undefined);
151+
152+
let normalizedModuleName = normalizeSlashes(moduleName);
153+
let basePart = useBaseUrl(moduleName) ? baseUrl : getDirectoryPath(containingFile);
154+
let candidate = normalizePath(combinePaths(basePart, moduleName));
155+
156+
let failedLookupLocations: string[] = [];
157+
158+
return forEach(supportedExtensions, ext => tryLoadFile(candidate + ext)) || { resolvedFileName: undefined, failedLookupLocations };
159+
160+
function tryLoadFile(location: string): ResolvedModule {
161+
if (host.fileExists(location)) {
162+
return { resolvedFileName: location, failedLookupLocations };
163+
}
164+
else {
165+
failedLookupLocations.push(location);
166+
return undefined;
167+
}
168+
}
169+
}
170+
171+
function nameStartsWithDotSlashOrDotDotSlash(name: string) {
172+
let i = name.lastIndexOf("./", 1);
173+
return i === 0 || (i === 1 && name.charCodeAt(0) === CharacterCodes.dot);
174+
}
175+
176+
function useBaseUrl(moduleName: string): boolean {
177+
// path is not rooted
178+
// module name does not start with './' or '../'
179+
return getRootLength(moduleName) === 0 && !nameStartsWithDotSlashOrDotDotSlash(moduleName);
41180
}
42181

43-
function legacyNameResolver(moduleName: string, containingFile: string, compilerOptions: CompilerOptions, host: ModuleResolutionHost): ResolvedModule {
182+
export function classicNameResolver(moduleName: string, containingFile: string, compilerOptions: CompilerOptions, host: ModuleResolutionHost): ResolvedModule {
44183

45184
// module names that contain '!' are used to reference resources and are not resolved to actual files on disk
46185
if (moduleName.indexOf('!') != -1) {

Diff for: src/compiler/types.ts

+7-1
Original file line numberDiff line numberDiff line change
@@ -2009,7 +2009,12 @@ namespace ts {
20092009
Error,
20102010
Message,
20112011
}
2012-
2012+
2013+
export const enum ModuleResolutionKind {
2014+
Classic = 1,
2015+
NodeJs = 2
2016+
}
2017+
20132018
export interface CompilerOptions {
20142019
allowNonTsExtensions?: boolean;
20152020
charset?: string;
@@ -2049,6 +2054,7 @@ namespace ts {
20492054
experimentalDecorators?: boolean;
20502055
experimentalAsyncFunctions?: boolean;
20512056
emitDecoratorMetadata?: boolean;
2057+
moduleResolution?: ModuleResolutionKind
20522058
/* @internal */ stripInternal?: boolean;
20532059

20542060
// Skip checking lib.d.ts to help speed up tests.

Diff for: src/harness/harness.ts

+17-1
Original file line numberDiff line numberDiff line change
@@ -1049,6 +1049,13 @@ module Harness {
10491049
options.module = ts.ModuleKind.UMD;
10501050
} else if (setting.value.toLowerCase() === "commonjs") {
10511051
options.module = ts.ModuleKind.CommonJS;
1052+
if (options.moduleResolution === undefined) {
1053+
// TODO: currently we have relative module names pretty much in all tests that use CommonJS module target.
1054+
// Such names could never be resolved in Node however classic resolution strategy still can handle them.
1055+
// Changing all module names to relative will be a major overhaul in code (but we'll do this anyway) so as a temporary measure
1056+
// we'll use ts.ModuleResolutionKind.Classic for CommonJS modules.
1057+
options.moduleResolution = ts.ModuleResolutionKind.Classic;
1058+
}
10521059
} else if (setting.value.toLowerCase() === "system") {
10531060
options.module = ts.ModuleKind.System;
10541061
} else if (setting.value.toLowerCase() === "unspecified") {
@@ -1060,7 +1067,16 @@ module Harness {
10601067
options.module = <any>setting.value;
10611068
}
10621069
break;
1063-
1070+
case "moduleresolution":
1071+
switch((setting.value || "").toLowerCase()) {
1072+
case "classic":
1073+
options.moduleResolution = ts.ModuleResolutionKind.Classic;
1074+
break;
1075+
case "node":
1076+
options.moduleResolution = ts.ModuleResolutionKind.NodeJs;
1077+
break;
1078+
}
1079+
break;
10641080
case "target":
10651081
case "codegentarget":
10661082
if (typeof setting.value === "string") {

Diff for: src/harness/projectsRunner.ts

+1
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,7 @@ class ProjectRunner extends RunnerBase {
163163
mapRoot: testCase.resolveMapRoot && testCase.mapRoot ? Harness.IO.resolvePath(testCase.mapRoot) : testCase.mapRoot,
164164
sourceRoot: testCase.resolveSourceRoot && testCase.sourceRoot ? Harness.IO.resolvePath(testCase.sourceRoot) : testCase.sourceRoot,
165165
module: moduleKind,
166+
moduleResolution: ts.ModuleResolutionKind.Classic, // currently all tests use classic module resolution kind, this will change in the future
166167
noResolve: testCase.noResolve,
167168
rootDir: testCase.rootDir
168169
};

Diff for: tests/baselines/reference/nodeResolution1.js

+12
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
//// [tests/cases/compiler/nodeResolution1.ts] ////
2+
3+
//// [a.ts]
4+
5+
export var x = 1;
6+
7+
//// [b.ts]
8+
import y = require("./a");
9+
10+
//// [a.js]
11+
exports.x = 1;
12+
//// [b.js]

Diff for: tests/baselines/reference/nodeResolution1.symbols

+9
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
=== tests/cases/compiler/b.ts ===
2+
import y = require("./a");
3+
>y : Symbol(y, Decl(b.ts, 0, 0))
4+
5+
=== tests/cases/compiler/a.ts ===
6+
7+
export var x = 1;
8+
>x : Symbol(x, Decl(a.ts, 1, 10))
9+

Diff for: tests/baselines/reference/nodeResolution1.types

+10
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
=== tests/cases/compiler/b.ts ===
2+
import y = require("./a");
3+
>y : typeof y
4+
5+
=== tests/cases/compiler/a.ts ===
6+
7+
export var x = 1;
8+
>x : number
9+
>1 : number
10+

Diff for: tests/baselines/reference/nodeResolution2.js

+10
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
//// [tests/cases/compiler/nodeResolution2.ts] ////
2+
3+
//// [a.d.ts]
4+
5+
export var x: number;
6+
7+
//// [b.ts]
8+
import y = require("a");
9+
10+
//// [b.js]

Diff for: tests/baselines/reference/nodeResolution2.symbols

+9
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
=== tests/cases/compiler/b.ts ===
2+
import y = require("a");
3+
>y : Symbol(y, Decl(b.ts, 0, 0))
4+
5+
=== tests/cases/compiler/node_modules/a.d.ts ===
6+
7+
export var x: number;
8+
>x : Symbol(x, Decl(a.d.ts, 1, 10))
9+

Diff for: tests/baselines/reference/nodeResolution2.types

+9
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
=== tests/cases/compiler/b.ts ===
2+
import y = require("a");
3+
>y : typeof y
4+
5+
=== tests/cases/compiler/node_modules/a.d.ts ===
6+
7+
export var x: number;
8+
>x : number
9+

Diff for: tests/baselines/reference/nodeResolution3.js

+10
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
//// [tests/cases/compiler/nodeResolution3.ts] ////
2+
3+
//// [index.d.ts]
4+
5+
export var x: number;
6+
7+
//// [a.ts]
8+
import y = require("b");
9+
10+
//// [a.js]

Diff for: tests/baselines/reference/nodeResolution3.symbols

+9
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
=== tests/cases/compiler/a.ts ===
2+
import y = require("b");
3+
>y : Symbol(y, Decl(a.ts, 0, 0))
4+
5+
=== tests/cases/compiler/node_modules/b/index.d.ts ===
6+
7+
export var x: number;
8+
>x : Symbol(x, Decl(index.d.ts, 1, 10))
9+

Diff for: tests/baselines/reference/nodeResolution3.types

+9
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
=== tests/cases/compiler/a.ts ===
2+
import y = require("b");
3+
>y : typeof y
4+
5+
=== tests/cases/compiler/node_modules/b/index.d.ts ===
6+
7+
export var x: number;
8+
>x : number
9+

Diff for: tests/cases/compiler/nodeResolution1.ts

+8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
// @module: commonjs
2+
// @moduleResolution: node
3+
4+
// @filename: a.ts
5+
export var x = 1;
6+
7+
// @filename: b.ts
8+
import y = require("./a");

Diff for: tests/cases/compiler/nodeResolution2.ts

+8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
// @module: commonjs
2+
// @moduleResolution: node
3+
4+
// @filename: node_modules/a.d.ts
5+
export var x: number;
6+
7+
// @filename: b.ts
8+
import y = require("a");

Diff for: tests/cases/compiler/nodeResolution3.ts

+8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
// @module: commonjs
2+
// @moduleResolution: node
3+
4+
// @filename: node_modules/b/index.d.ts
5+
export var x: number;
6+
7+
// @filename: a.ts
8+
import y = require("b");

0 commit comments

Comments
 (0)