mirror of
https://github.com/microsoft/TypeScript.git
synced 2026-02-15 03:23:08 -06:00
initial implementation of module resolution for node/requirejs
This commit is contained in:
parent
405db829a0
commit
3b95ea460e
@ -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([
|
||||
|
||||
@ -36,11 +36,165 @@ 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);
|
||||
switch(compilerOptions.moduleResolution) {
|
||||
case ModuleResolutionKind.NodeJs: return nodeModuleNameResolver(moduleName, containingFile, compilerOptions, host);
|
||||
case ModuleResolutionKind.BaseUrl: return baseUrlModuleNameResolver(moduleName, containingFile, compilerOptions, host);
|
||||
default: return legacyNameResolver(moduleName, containingFile, compilerOptions, host);
|
||||
}
|
||||
}
|
||||
|
||||
export function nodeModuleNameResolver(moduleName: string, containingFile: string, compilerOptions: CompilerOptions, host: ModuleResolutionHost): ResolvedModule {
|
||||
let containingDirectory = getDirectoryPath(containingFile);
|
||||
|
||||
if (getRootLength(moduleName) !== 0 || nameStartsWithDotSlashOrDotDotSlash(moduleName)) {
|
||||
let failedLookupLocations: string[] = [];
|
||||
let candidate = normalizePath(combinePaths(containingDirectory, moduleName));
|
||||
let result = loadNodeModuleFromFile(candidate, failedLookupLocations, host);
|
||||
|
||||
if (result) {
|
||||
return { resolvedFileName: result, failedLookupLocations };
|
||||
}
|
||||
|
||||
result = loadNodeModuleFromDirectory(candidate, failedLookupLocations, host);
|
||||
return { resolvedFileName: result, failedLookupLocations };
|
||||
}
|
||||
else {
|
||||
return loadModuleModuleFromNodeModules(moduleName, containingDirectory, host);
|
||||
}
|
||||
}
|
||||
|
||||
function loadNodeModuleFromFile(candidate: string, failedLookupLocation: string[], host: ModuleResolutionHost): string {
|
||||
// load only .d.ts files
|
||||
let fileName = fileExtensionIs(candidate, ".d.ts") ? candidate : candidate + ".d.ts";
|
||||
if (!host.fileExists(fileName)) {
|
||||
failedLookupLocation.push(fileName);
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return fileName;
|
||||
}
|
||||
|
||||
function loadNodeModuleFromDirectory(candidate: string, failedLookupLocation: string[], host: ModuleResolutionHost): string {
|
||||
let packageJsonPath = combinePaths(candidate, "package.json");
|
||||
if (host.fileExists(packageJsonPath)) {
|
||||
let jsonText = host.readFile(packageJsonPath);
|
||||
let jsonContent = jsonText ? <{ typings?: string, main?: string }>JSON.parse(jsonText) : { typings: undefined, main:undefined };
|
||||
if (jsonContent.typings) {
|
||||
let result = loadNodeModuleFromFile(normalizePath(combinePaths(candidate, jsonContent.typings)), failedLookupLocation, host);
|
||||
if (result) {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
if (jsonContent.main) {
|
||||
let mainFile = removeFileExtension(jsonContent.main);
|
||||
let result = loadNodeModuleFromFile(normalizePath(combinePaths(candidate, mainFile)), 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"), failedLookupLocation, host);
|
||||
}
|
||||
|
||||
function loadModuleModuleFromNodeModules(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, failedLookupLocations, host);
|
||||
if (result) {
|
||||
return { resolvedFileName: result, failedLookupLocations };
|
||||
}
|
||||
|
||||
result = loadNodeModuleFromDirectory(candidate, failedLookupLocations, host);
|
||||
if (result) {
|
||||
return { resolvedFileName: result, failedLookupLocations };
|
||||
}
|
||||
}
|
||||
|
||||
let parentPath = getDirectoryPath(directory);
|
||||
if (parentPath === directory) {
|
||||
break;
|
||||
}
|
||||
|
||||
directory = parentPath;
|
||||
}
|
||||
|
||||
return { resolvedFileName: undefined, failedLookupLocations };
|
||||
}
|
||||
|
||||
export function baseUrlModuleNameResolver(moduleName: string, containingFile: string, compilerOptions: CompilerOptions, host: ModuleResolutionHost): ResolvedModule {
|
||||
Debug.assert(compilerOptions.baseUrl !== undefined, "baseUrl is mandatory when using this module resolution strategy");
|
||||
|
||||
let normalizedModuleName = normalizeSlashes(moduleName);
|
||||
|
||||
// treat module name as url that is relative to containing file if
|
||||
let basePart = useBaseUrl(moduleName) ? compilerOptions.baseUrl : getDirectoryPath(containingFile);
|
||||
let candidate = normalizePath(combinePaths(basePart, moduleName));
|
||||
|
||||
|
||||
let failedLookupLocations: string[] = [];
|
||||
// first - try to load file as is
|
||||
let result = tryLoadFile(candidate);
|
||||
if (result) {
|
||||
return result;
|
||||
}
|
||||
// then try all supported extension
|
||||
for(let ext of supportedExtensions) {
|
||||
let result = tryLoadFile(candidate + ext);
|
||||
if (result) {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
return { 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 rooted
|
||||
if (getRootLength(moduleName) !== 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// module name starts with './' or '../'
|
||||
if (nameStartsWithDotSlashOrDotDotSlash(moduleName)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// module name has one of supported extesions
|
||||
for(let ext of supportedExtensions ) {
|
||||
if (fileExtensionIs(moduleName, ext)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function legacyNameResolver(moduleName: string, containingFile: string, compilerOptions: CompilerOptions, host: ModuleResolutionHost): ResolvedModule {
|
||||
export function legacyNameResolver(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) {
|
||||
|
||||
@ -2004,7 +2004,13 @@ namespace ts {
|
||||
Error,
|
||||
Message,
|
||||
}
|
||||
|
||||
|
||||
export const enum ModuleResolutionKind {
|
||||
Legacy = 1,
|
||||
NodeJs = 2,
|
||||
BaseUrl = 3
|
||||
}
|
||||
|
||||
export interface CompilerOptions {
|
||||
allowNonTsExtensions?: boolean;
|
||||
charset?: string;
|
||||
@ -2043,6 +2049,8 @@ namespace ts {
|
||||
experimentalDecorators?: boolean;
|
||||
experimentalAsyncFunctions?: boolean;
|
||||
emitDecoratorMetadata?: boolean;
|
||||
moduleResolution?: ModuleResolutionKind
|
||||
baseUrl?: string;
|
||||
/* @internal */ stripInternal?: boolean;
|
||||
|
||||
// Skip checking lib.d.ts to help speed up tests.
|
||||
|
||||
254
tests/cases/unittests/moduleResolution.ts
Normal file
254
tests/cases/unittests/moduleResolution.ts
Normal file
@ -0,0 +1,254 @@
|
||||
/// <reference path="..\..\..\src\harness\external\mocha.d.ts" />
|
||||
/// <reference path='..\..\..\src\harness\harness.ts' />
|
||||
|
||||
declare namespace chai.assert {
|
||||
function deepEqual(actual: any, expected: any): void;
|
||||
}
|
||||
|
||||
module ts {
|
||||
|
||||
interface Directory {
|
||||
name: string;
|
||||
children: Map<File | Directory>;
|
||||
}
|
||||
|
||||
interface File {
|
||||
name: string
|
||||
content?: string
|
||||
}
|
||||
|
||||
function createModuleResolutionHost(...files: File[]): ModuleResolutionHost {
|
||||
let root = makeFS(files);
|
||||
|
||||
return { fileExists, readFile };
|
||||
|
||||
function fileExists(path: string): boolean {
|
||||
return findFile(path, root) !== undefined;
|
||||
}
|
||||
|
||||
function readFile(path: string): string {
|
||||
let f = findFile(path, root);
|
||||
return f && f.content;
|
||||
}
|
||||
|
||||
function findFile(path: string, fse: File | Directory): File {
|
||||
if (!fse) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (isDirectory(fse)) {
|
||||
let {dir, rel} = splitPath(path);
|
||||
return findFile(rel, (<Directory>fse).children[dir]);
|
||||
}
|
||||
else {
|
||||
return !path && <File>fse;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function isDirectory(fse: Directory | File): boolean {
|
||||
return (<Directory>fse).children !== undefined;
|
||||
}
|
||||
|
||||
function createDirectory(name: string): Directory {
|
||||
return { name, children: {} }
|
||||
}
|
||||
|
||||
function makeFS(files: File[]): Directory {
|
||||
// create root
|
||||
let {dir} = splitPath(files[0].name);
|
||||
let root: Directory = createDirectory(dir);
|
||||
|
||||
for(let f of files) {
|
||||
addFile(f.name, f.content, root);
|
||||
}
|
||||
|
||||
function addFile(path: string, content: string, parent: Directory) {
|
||||
Debug.assert(parent !== undefined);
|
||||
|
||||
let {dir, rel} = splitPath(path);
|
||||
if (rel) {
|
||||
let d = parent.children[dir] || (parent.children[dir] = createDirectory(dir));
|
||||
Debug.assert(isDirectory(d))
|
||||
addFile(rel, content, <Directory>d);
|
||||
}
|
||||
else {
|
||||
parent.children[dir] = <File>{ name: dir, content };
|
||||
}
|
||||
}
|
||||
|
||||
return root;
|
||||
}
|
||||
|
||||
function splitPath(path: string): { dir: string; rel: string } {
|
||||
let index = path.indexOf(directorySeparator);
|
||||
return index === -1
|
||||
? { dir: path, rel: undefined }
|
||||
: { dir: path.substr(0, index), rel: path.substr(index + 1) };
|
||||
}
|
||||
|
||||
let opts: CompilerOptions = { moduleResolution: ModuleResolutionKind.NodeJs };
|
||||
|
||||
describe("Node module resolution - relative paths", () => {
|
||||
|
||||
function testLoadAsFile(containingFileName: string, moduleFileNameNoExt: string, moduleName: string): void {
|
||||
{
|
||||
// loading only .d.ts files
|
||||
|
||||
let containingFile = { name: containingFileName, content: ""}
|
||||
let moduleFile = { name: moduleFileNameNoExt + ".d.ts", content: "var x;"}
|
||||
let resolution = nodeModuleNameResolver(moduleName, containingFile.name, opts, createModuleResolutionHost(containingFile, moduleFile));
|
||||
|
||||
assert.equal(resolution.resolvedFileName, moduleFile.name);
|
||||
assert.isTrue(resolution.failedLookupLocations.length === 0);
|
||||
}
|
||||
{
|
||||
// does not try to load .ts files
|
||||
|
||||
let containingFile = { name: containingFileName, content: ""}
|
||||
let moduleFile = { name: moduleFileNameNoExt + ".ts", content: "var x;"}
|
||||
let resolution = nodeModuleNameResolver(moduleName, containingFile.name, opts, createModuleResolutionHost(containingFile, moduleFile));
|
||||
|
||||
assert.equal(resolution.resolvedFileName, undefined);
|
||||
assert.equal(resolution.failedLookupLocations.length, 3);
|
||||
assert.deepEqual(resolution.failedLookupLocations, [
|
||||
moduleFileNameNoExt + ".d.ts",
|
||||
moduleFileNameNoExt + "/package.json",
|
||||
moduleFileNameNoExt + "/index.d.ts"
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
it("module name that starts with './' resolved as relative file name", () => {
|
||||
testLoadAsFile("/foo/bar/baz.ts", "/foo/bar/foo", "./foo");
|
||||
});
|
||||
|
||||
it("module name that starts with '../' resolved as relative file name", () => {
|
||||
testLoadAsFile("/foo/bar/baz.ts", "/foo/foo", "../foo");
|
||||
});
|
||||
|
||||
it("module name that starts with '/' script extension resolved as relative file name", () => {
|
||||
testLoadAsFile("/foo/bar/baz.ts", "/foo", "/foo");
|
||||
});
|
||||
|
||||
function testLoadingFromPackageJson(containingFileName: string, packageJsonFileName: string, fieldName: string, fieldRef: string, moduleFileName: string, moduleName: string): void {
|
||||
let containingFile = { name: containingFileName };
|
||||
let packageJson = { name: packageJsonFileName, content: JSON.stringify({ [fieldName]: fieldRef }) };
|
||||
let moduleFile = { name: moduleFileName };
|
||||
let resolution = nodeModuleNameResolver(moduleName, containingFile.name, opts, createModuleResolutionHost(containingFile, packageJson, moduleFile));
|
||||
assert.equal(resolution.resolvedFileName, moduleFile.name);
|
||||
// expect one failed lookup location - attempt to load module as file
|
||||
assert.equal(resolution.failedLookupLocations.length, 1);
|
||||
}
|
||||
|
||||
it("module name as directory - load from typings", () => {
|
||||
testLoadingFromPackageJson("/a/b/c/d.ts", "/a/b/c/bar/package.json", "typings", "c/d/e.d.ts", "/a/b/c/bar/c/d/e.d.ts", "./bar");
|
||||
testLoadingFromPackageJson("/a/b/c/d.ts", "/a/bar/package.json", "typings", "e.d.ts", "/a/bar/e.d.ts", "../../bar");
|
||||
testLoadingFromPackageJson("/a/b/c/d.ts", "/bar/package.json", "typings", "e.d.ts", "/bar/e.d.ts", "/bar");
|
||||
});
|
||||
|
||||
it("module name as directory - load from main", () => {
|
||||
testLoadingFromPackageJson("/a/b/c/d.ts", "/a/b/c/bar/package.json", "main", "c/d/e.d.ts", "/a/b/c/bar/c/d/e.d.ts", "./bar");
|
||||
testLoadingFromPackageJson("/a/b/c/d.ts", "/a/bar/package.json", "main", "e.d.ts", "/a/bar/e.d.ts", "../../bar");
|
||||
testLoadingFromPackageJson("/a/b/c/d.ts", "/bar/package.json", "main", "e.d.ts", "/bar/e.d.ts", "/bar");
|
||||
});
|
||||
|
||||
it ("module name as directory - load index.d.ts", () => {
|
||||
let containingFile = {name: "/a/b/c.ts"};
|
||||
let packageJson = {name: "/a/b/foo/package.json", content: JSON.stringify({main: "/c/d"})};
|
||||
let indexFile = { name: "/a/b/foo/index.d.ts" };
|
||||
let resolution = nodeModuleNameResolver("./foo", containingFile.name, opts, createModuleResolutionHost(containingFile, packageJson, indexFile));
|
||||
assert.equal(resolution.resolvedFileName, indexFile.name);
|
||||
// expect 2 failed lookup locations:
|
||||
assert.deepEqual(resolution.failedLookupLocations, [
|
||||
"/a/b/foo.d.ts",
|
||||
"/c/d.d.ts"
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Node module resolution - non-relative paths", () => {
|
||||
it("load module as file - ts files not loaded", () => {
|
||||
let containingFile = { name: "/a/b/c/d/e.ts" };
|
||||
let moduleFile = { name: "/a/b/node_modules/foo.ts" };
|
||||
let resolution = nodeModuleNameResolver("foo", containingFile.name, opts, createModuleResolutionHost(containingFile, moduleFile));
|
||||
assert.equal(resolution.resolvedFileName, undefined);
|
||||
assert.deepEqual(resolution.failedLookupLocations, [
|
||||
"/a/b/c/d/node_modules/foo.d.ts",
|
||||
"/a/b/c/d/node_modules/foo/package.json",
|
||||
"/a/b/c/d/node_modules/foo/index.d.ts",
|
||||
"/a/b/c/node_modules/foo.d.ts",
|
||||
"/a/b/c/node_modules/foo/package.json",
|
||||
"/a/b/c/node_modules/foo/index.d.ts",
|
||||
"/a/b/node_modules/foo.d.ts",
|
||||
"/a/b/node_modules/foo/package.json",
|
||||
"/a/b/node_modules/foo/index.d.ts",
|
||||
"/a/node_modules/foo.d.ts",
|
||||
"/a/node_modules/foo/package.json",
|
||||
"/a/node_modules/foo/index.d.ts",
|
||||
"/node_modules/foo.d.ts",
|
||||
"/node_modules/foo/package.json",
|
||||
"/node_modules/foo/index.d.ts"
|
||||
])
|
||||
});
|
||||
|
||||
it("load module as file", () => {
|
||||
let containingFile = { name: "/a/b/c/d/e.ts" };
|
||||
let moduleFile = { name: "/a/b/node_modules/foo.d.ts" };
|
||||
let resolution = nodeModuleNameResolver("foo", containingFile.name, opts, createModuleResolutionHost(containingFile, moduleFile));
|
||||
assert.equal(resolution.resolvedFileName, moduleFile.name);
|
||||
});
|
||||
|
||||
it("load module as directory", () => {
|
||||
let containingFile = { name: "/a/node_modules/b/c/node_modules/d/e.ts" };
|
||||
let moduleFile = { name: "/a/node_modules/foo/index.d.ts" };
|
||||
let resolution = nodeModuleNameResolver("foo", containingFile.name, opts, createModuleResolutionHost(containingFile, moduleFile));
|
||||
assert.equal(resolution.resolvedFileName, moduleFile.name);
|
||||
assert.deepEqual(resolution.failedLookupLocations, [
|
||||
"/a/node_modules/b/c/node_modules/d/node_modules/foo.d.ts",
|
||||
"/a/node_modules/b/c/node_modules/d/node_modules/foo/package.json",
|
||||
"/a/node_modules/b/c/node_modules/d/node_modules/foo/index.d.ts",
|
||||
"/a/node_modules/b/c/node_modules/foo.d.ts",
|
||||
"/a/node_modules/b/c/node_modules/foo/package.json",
|
||||
"/a/node_modules/b/c/node_modules/foo/index.d.ts",
|
||||
"/a/node_modules/b/node_modules/foo.d.ts",
|
||||
"/a/node_modules/b/node_modules/foo/package.json",
|
||||
"/a/node_modules/b/node_modules/foo/index.d.ts",
|
||||
"/a/node_modules/foo.d.ts",
|
||||
"/a/node_modules/foo/package.json"
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("BaseUrl mode", () => {
|
||||
function getCompilerOptions(baseUrl: string): CompilerOptions {
|
||||
return { baseUrl, moduleResolution: ModuleResolutionKind.BaseUrl };
|
||||
}
|
||||
|
||||
it ("load module as relative url", () => {
|
||||
function test(containingFileName: string, moduleFileName: string, moduleName: string): void {
|
||||
let containingFile = {name: containingFileName };
|
||||
let moduleFile = { name: moduleFileName };
|
||||
let resolution = baseUrlModuleNameResolver(moduleName, containingFile.name, getCompilerOptions("<some-value>"), createModuleResolutionHost(containingFile, moduleFile));
|
||||
assert.equal(resolution.resolvedFileName, moduleFile.name);
|
||||
}
|
||||
|
||||
test("/a/b/c/d.ts", "/foo.ts", "/foo.ts");
|
||||
test("/a/b/c/d.ts", "/foo.ts", "/foo");
|
||||
test("/a/b/c/d.ts", "/foo.d.ts", "/foo");
|
||||
test("/a/b/c/d.ts", "/foo.tsx", "/foo");
|
||||
|
||||
test("/a/b/c/d.ts", "/a/b/c/foo.ts", "./foo");
|
||||
test("/a/b/c/d.ts", "/a/b/c/foo.d.ts", "./foo");
|
||||
test("/a/b/c/d.ts", "/a/b/c/foo.tsx", "./foo");
|
||||
|
||||
test("/a/b/c/d.ts", "/a/b/foo.ts", "../foo");
|
||||
test("/a/b/c/d.ts", "/a/b/foo.d.ts", "../foo");
|
||||
test("/a/b/c/d.ts", "/a/b/foo.tsx", "../foo");
|
||||
|
||||
test("/a/b/c/d.ts", "/a/b/c/foo.ts", "foo.ts");
|
||||
test("/a/b/c/d.ts", "/a/b/c/foo.tsx", "foo.tsx");
|
||||
test("/a/b/c/d.ts", "/a/b/c/foo.d.ts", "foo.d.ts");
|
||||
});
|
||||
});
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user