Skip to content

Adding different module resolution strategies #4352

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

Merged
merged 8 commits into from
Aug 24, 2015
Merged
Show file tree
Hide file tree
Changes from 5 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
3 changes: 2 additions & 1 deletion Jakefile.js
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,8 @@ var harnessSources = harnessCoreSources.concat([
"convertToBase64.ts",
"transpile.ts",
"reuseProgramStructure.ts",
"cachingInServerLSHost.ts"
"cachingInServerLSHost.ts",
"moduleResolution.ts"
].map(function (f) {
return path.join(unittestsDirectory, f);
})).concat([
Expand Down
11 changes: 10 additions & 1 deletion src/compiler/commandLineParser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -218,7 +218,16 @@ namespace ts {
type: "boolean",
experimental: true,
description: Diagnostics.Enables_experimental_support_for_emitting_type_metadata_for_decorators
}
},
{
name: "moduleResolution",
type: {
"node": ModuleResolutionKind.NodeJs,
"classic": ModuleResolutionKind.Classic
},
experimental: true,
description: Diagnostics.Specifies_module_resolution_strategy_Colon_node_Node_or_classic_TypeScript_pre_1_6
}
];

export function parseCommandLine(commandLine: string[]): ParsedCommandLine {
Expand Down
1 change: 1 addition & 0 deletions src/compiler/diagnosticInformationMap.generated.ts
Original file line number Diff line number Diff line change
Expand Up @@ -574,6 +574,7 @@ namespace ts {
Enables_experimental_support_for_emitting_type_metadata_for_decorators: { code: 6066, category: DiagnosticCategory.Message, key: "Enables experimental support for emitting type metadata for decorators." },
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." },
Enables_experimental_support_for_ES7_async_functions: { code: 6068, category: DiagnosticCategory.Message, key: "Enables experimental support for ES7 async functions." },
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) ." },
Variable_0_implicitly_has_an_1_type: { code: 7005, category: DiagnosticCategory.Error, key: "Variable '{0}' implicitly has an '{1}' type." },
Parameter_0_implicitly_has_an_1_type: { code: 7006, category: DiagnosticCategory.Error, key: "Parameter '{0}' implicitly has an '{1}' type." },
Member_0_implicitly_has_an_1_type: { code: 7008, category: DiagnosticCategory.Error, key: "Member '{0}' implicitly has an '{1}' type." },
Expand Down
5 changes: 4 additions & 1 deletion src/compiler/diagnosticMessages.json
Original file line number Diff line number Diff line change
Expand Up @@ -2286,7 +2286,10 @@
"category": "Message",
"code": 6068
},

"Specifies module resolution strategy: 'node' (Node) or 'classic' (TypeScript pre 1.6) .": {
"category": "Message",
"code": 6069
},
"Variable '{0}' implicitly has an '{1}' type.": {
"category": "Error",
"code": 7005
Expand Down
145 changes: 142 additions & 3 deletions src/compiler/program.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,11 +36,150 @@ namespace ts {
}

export function resolveModuleName(moduleName: string, containingFile: string, compilerOptions: CompilerOptions, host: ModuleResolutionHost): ResolvedModule {
// TODO: use different resolution strategy based on compiler options
return legacyNameResolver(moduleName, containingFile, compilerOptions, host);
let moduleResolution = compilerOptions.moduleResolution !== undefined
? compilerOptions.moduleResolution
: compilerOptions.module === ModuleKind.CommonJS ? ModuleResolutionKind.NodeJs : ModuleResolutionKind.Classic;

switch (moduleResolution) {
case ModuleResolutionKind.NodeJs: return nodeModuleNameResolver(moduleName, containingFile, host);
case ModuleResolutionKind.Classic: return classicNameResolver(moduleName, containingFile, compilerOptions, host);
}
}

export function nodeModuleNameResolver(moduleName: string, containingFile: string, host: ModuleResolutionHost): ResolvedModule {
let containingDirectory = getDirectoryPath(containingFile);

if (getRootLength(moduleName) !== 0 || nameStartsWithDotSlashOrDotDotSlash(moduleName)) {
let failedLookupLocations: string[] = [];
Copy link
Contributor

Choose a reason for hiding this comment

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

Is this used to give out any error or it is mainly use for testing?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

this is used as the part of output

let candidate = normalizePath(combinePaths(containingDirectory, moduleName));
let resolvedFileName = loadNodeModuleFromFile(candidate, /* loadOnlyDts */ false, failedLookupLocations, host);

if (resolvedFileName) {
return { resolvedFileName, failedLookupLocations };
}

resolvedFileName = loadNodeModuleFromDirectory(candidate, /* loadOnlyDts */ false, failedLookupLocations, host);
return { resolvedFileName, failedLookupLocations };
}
else {
return loadModuleFromNodeModules(moduleName, containingDirectory, host);
}
}

function loadNodeModuleFromFile(candidate: string, loadOnlyDts: boolean, failedLookupLocation: string[], host: ModuleResolutionHost): string {
if (loadOnlyDts) {
return tryLoad(".d.ts");
}
else {
return forEach(supportedExtensions, tryLoad);
}

function tryLoad(ext: string): string {
let fileName = fileExtensionIs(candidate, ext) ? candidate : candidate + ext;
if (host.fileExists(fileName)) {
return fileName;
}
else {
failedLookupLocation.push(fileName);
return undefined;
}
}
}

function loadNodeModuleFromDirectory(candidate: string, loadOnlyDts: boolean, failedLookupLocation: string[], host: ModuleResolutionHost): string {
let packageJsonPath = combinePaths(candidate, "package.json");
if (host.fileExists(packageJsonPath)) {

let jsonContent: { typings?: string };

try {
let jsonText = host.readFile(packageJsonPath);
jsonContent = jsonText ? <{ typings?: string }>JSON.parse(jsonText) : { typings: undefined };
}
catch (e) {
// gracefully handle if readFile fails or returns not JSON
jsonContent = { typings: undefined };
}

if (jsonContent.typings) {
let result = loadNodeModuleFromFile(normalizePath(combinePaths(candidate, jsonContent.typings)), loadOnlyDts, failedLookupLocation, host);
if (result) {
return result;
}
}
}
else {
// record package json as one of failed lookup locations - in the future if this file will appear it will invalidate resolution results
failedLookupLocation.push(packageJsonPath);
}

return loadNodeModuleFromFile(combinePaths(candidate, "index"), loadOnlyDts, failedLookupLocation, host);
}

function loadModuleFromNodeModules(moduleName: string, directory: string, host: ModuleResolutionHost): ResolvedModule {
let failedLookupLocations: string[] = [];
directory = normalizeSlashes(directory);
while (true) {
let baseName = getBaseFileName(directory);
if (baseName !== "node_modules") {
let nodeModulesFolder = combinePaths(directory, "node_modules");
let candidate = normalizePath(combinePaths(nodeModulesFolder, moduleName));
let result = loadNodeModuleFromFile(candidate, /* loadOnlyDts */ true, failedLookupLocations, host);
if (result) {
return { resolvedFileName: result, failedLookupLocations };
}

result = loadNodeModuleFromDirectory(candidate, /* loadOnlyDts */ true, failedLookupLocations, host);
if (result) {
return { resolvedFileName: result, failedLookupLocations };
}
}

let parentPath = getDirectoryPath(directory);
if (parentPath === directory) {
Copy link
Member

Choose a reason for hiding this comment

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

Add a comment to explain what's going on here.

break;
}

directory = parentPath;
}

return { resolvedFileName: undefined, failedLookupLocations };
}

export function baseUrlModuleNameResolver(moduleName: string, containingFile: string, baseUrl: string, host: ModuleResolutionHost): ResolvedModule {
Debug.assert(baseUrl !== undefined);
Copy link
Contributor

Choose a reason for hiding this comment

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

Should there be a check and give out compilerOptions error if the baseUrl is not defined?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

this check will be in verifyCompilerOptions - it is not there yet


let normalizedModuleName = normalizeSlashes(moduleName);
let basePart = useBaseUrl(moduleName) ? baseUrl : getDirectoryPath(containingFile);
let candidate = normalizePath(combinePaths(basePart, moduleName));

let failedLookupLocations: string[] = [];

return forEach(supportedExtensions, ext => tryLoadFile(candidate + ext)) || { resolvedFileName: undefined, failedLookupLocations };

function tryLoadFile(location: string): ResolvedModule {
if (host.fileExists(location)) {
return { resolvedFileName: location, failedLookupLocations };
}
else {
failedLookupLocations.push(location);
return undefined;
}
}
}

function nameStartsWithDotSlashOrDotDotSlash(name: string) {
let i = name.lastIndexOf("./", 1);
return i === 0 || (i === 1 && name.charCodeAt(0) === CharacterCodes.dot);
}

function useBaseUrl(moduleName: string): boolean {
// path is not rooted
// module name does not start with './' or '../'
return getRootLength(moduleName) === 0 && !nameStartsWithDotSlashOrDotDotSlash(moduleName);
}

function legacyNameResolver(moduleName: string, containingFile: string, compilerOptions: CompilerOptions, host: ModuleResolutionHost): ResolvedModule {
export function classicNameResolver(moduleName: string, containingFile: string, compilerOptions: CompilerOptions, host: ModuleResolutionHost): ResolvedModule {

// module names that contain '!' are used to reference resources and are not resolved to actual files on disk
if (moduleName.indexOf('!') != -1) {
Expand Down
8 changes: 7 additions & 1 deletion src/compiler/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2008,7 +2008,12 @@ namespace ts {
Error,
Message,
}


export const enum ModuleResolutionKind {
Classic = 1,
NodeJs = 2
}

export interface CompilerOptions {
allowNonTsExtensions?: boolean;
charset?: string;
Expand Down Expand Up @@ -2047,6 +2052,7 @@ namespace ts {
experimentalDecorators?: boolean;
experimentalAsyncFunctions?: boolean;
emitDecoratorMetadata?: boolean;
moduleResolution?: ModuleResolutionKind
/* @internal */ stripInternal?: boolean;

// Skip checking lib.d.ts to help speed up tests.
Expand Down
14 changes: 13 additions & 1 deletion src/harness/harness.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1025,6 +1025,9 @@ module Harness {
options.module = ts.ModuleKind.UMD;
} else if (setting.value.toLowerCase() === "commonjs") {
options.module = ts.ModuleKind.CommonJS;
if (options.moduleResolution === undefined) {
options.moduleResolution = ts.ModuleResolutionKind.Classic;
Copy link
Contributor

Choose a reason for hiding this comment

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

can you add a comment

}
} else if (setting.value.toLowerCase() === "system") {
options.module = ts.ModuleKind.System;
} else if (setting.value.toLowerCase() === "unspecified") {
Expand All @@ -1036,7 +1039,16 @@ module Harness {
options.module = <any>setting.value;
}
break;

case "moduleresolution":
switch((setting.value || "").toLowerCase()) {
case "classic":
options.moduleResolution = ts.ModuleResolutionKind.Classic;
break;
case "node":
options.moduleResolution = ts.ModuleResolutionKind.NodeJs;
break;
}
break;
case "target":
case "codegentarget":
if (typeof setting.value === "string") {
Expand Down
1 change: 1 addition & 0 deletions src/harness/projectsRunner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,7 @@ class ProjectRunner extends RunnerBase {
mapRoot: testCase.resolveMapRoot && testCase.mapRoot ? ts.sys.resolvePath(testCase.mapRoot) : testCase.mapRoot,
sourceRoot: testCase.resolveSourceRoot && testCase.sourceRoot ? ts.sys.resolvePath(testCase.sourceRoot) : testCase.sourceRoot,
module: moduleKind,
moduleResolution: ts.ModuleResolutionKind.Classic, // currently all tests use classic module resolution kind, this will change in the future
noResolve: testCase.noResolve,
rootDir: testCase.rootDir
};
Expand Down
12 changes: 12 additions & 0 deletions tests/baselines/reference/nodeResolution1.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
//// [tests/cases/compiler/nodeResolution1.ts] ////

//// [a.ts]

export var x = 1;

//// [b.ts]
import y = require("./a");

//// [a.js]
exports.x = 1;
//// [b.js]
9 changes: 9 additions & 0 deletions tests/baselines/reference/nodeResolution1.symbols
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
=== tests/cases/compiler/b.ts ===
import y = require("./a");
>y : Symbol(y, Decl(b.ts, 0, 0))

=== tests/cases/compiler/a.ts ===

export var x = 1;
>x : Symbol(x, Decl(a.ts, 1, 10))

10 changes: 10 additions & 0 deletions tests/baselines/reference/nodeResolution1.types
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
=== tests/cases/compiler/b.ts ===
import y = require("./a");
>y : typeof y

=== tests/cases/compiler/a.ts ===

export var x = 1;
>x : number
>1 : number

10 changes: 10 additions & 0 deletions tests/baselines/reference/nodeResolution2.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
//// [tests/cases/compiler/nodeResolution2.ts] ////

//// [a.d.ts]

export var x: number;

//// [b.ts]
import y = require("a");

//// [b.js]
9 changes: 9 additions & 0 deletions tests/baselines/reference/nodeResolution2.symbols
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
=== tests/cases/compiler/b.ts ===
import y = require("a");
>y : Symbol(y, Decl(b.ts, 0, 0))

=== tests/cases/compiler/node_modules/a.d.ts ===

export var x: number;
>x : Symbol(x, Decl(a.d.ts, 1, 10))

9 changes: 9 additions & 0 deletions tests/baselines/reference/nodeResolution2.types
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
=== tests/cases/compiler/b.ts ===
import y = require("a");
>y : typeof y

=== tests/cases/compiler/node_modules/a.d.ts ===

export var x: number;
>x : number

10 changes: 10 additions & 0 deletions tests/baselines/reference/nodeResolution3.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
//// [tests/cases/compiler/nodeResolution3.ts] ////

//// [index.d.ts]

export var x: number;

//// [a.ts]
import y = require("b");

//// [a.js]
9 changes: 9 additions & 0 deletions tests/baselines/reference/nodeResolution3.symbols
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
=== tests/cases/compiler/a.ts ===
import y = require("b");
>y : Symbol(y, Decl(a.ts, 0, 0))

=== tests/cases/compiler/node_modules/b/index.d.ts ===

export var x: number;
>x : Symbol(x, Decl(index.d.ts, 1, 10))

9 changes: 9 additions & 0 deletions tests/baselines/reference/nodeResolution3.types
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
=== tests/cases/compiler/a.ts ===
import y = require("b");
>y : typeof y

=== tests/cases/compiler/node_modules/b/index.d.ts ===

export var x: number;
>x : number

8 changes: 8 additions & 0 deletions tests/cases/compiler/nodeResolution1.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
// @module: commonjs
// @moduleResolution: node

// @filename: a.ts
export var x = 1;

// @filename: b.ts
import y = require("./a");
8 changes: 8 additions & 0 deletions tests/cases/compiler/nodeResolution2.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
// @module: commonjs
// @moduleResolution: node

// @filename: node_modules/a.d.ts
export var x: number;

// @filename: b.ts
import y = require("a");
8 changes: 8 additions & 0 deletions tests/cases/compiler/nodeResolution3.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
// @module: commonjs
// @moduleResolution: node

// @filename: node_modules/b/index.d.ts
export var x: number;

// @filename: a.ts
import y = require("b");
Loading