initial implementation of path mapping based module resolution

This commit is contained in:
Vladimir Matveev
2015-11-18 21:46:45 -08:00
parent bd84b844ff
commit d2fd6437d7
90 changed files with 3391 additions and 49 deletions

View File

@@ -250,10 +250,12 @@ namespace ts {
name: "moduleResolution",
type: {
"node": ModuleResolutionKind.NodeJs,
"classic": ModuleResolutionKind.Classic
"classic": ModuleResolutionKind.Classic,
// name is lowercased so we can still use hasProperty(userValue.toLower()) to check if user has entered the right value
"baseurl": ModuleResolutionKind.BaseUrl,
},
description: Diagnostics.Specifies_module_resolution_strategy_Colon_node_Node_js_or_classic_TypeScript_pre_1_6,
error: Diagnostics.Argument_for_moduleResolution_option_must_be_node_or_classic,
error: Diagnostics.Argument_for_moduleResolution_option_must_be_node_classic_or_baseUrl,
},
{
name: "allowUnusedLabels",
@@ -279,6 +281,26 @@ namespace ts {
name: "forceConsistentCasingInFileNames",
type: "boolean",
description: Diagnostics.Disallow_inconsistently_cased_references_to_the_same_file
},
{
name: "baseUrl",
type: "string",
isFilePath: true,
description: Diagnostics.Base_directory_to_resolve_relative_module_names
},
{
// this option can only be specified in tsconfig.json
// use type = object to copy the value as-is
name: "paths",
type: "object",
isTSConfigOnly: true
},
{
// this option can only be specified in tsconfig.json
// use type = object to copy the value as-is
name: "rootDirs",
type: "object",
isTSConfigOnly: true
}
];
@@ -339,31 +361,36 @@ namespace ts {
if (hasProperty(optionNameMap, s)) {
const opt = optionNameMap[s];
// Check to see if no argument was provided (e.g. "--locale" is the last command-line argument).
if (!args[i] && opt.type !== "boolean") {
errors.push(createCompilerDiagnostic(Diagnostics.Compiler_option_0_expects_an_argument, opt.name));
if (opt.isTSConfigOnly) {
errors.push(createCompilerDiagnostic(Diagnostics.Option_0_can_only_be_specified_in_tsconfig_json_file, opt.name));
}
else {
// Check to see if no argument was provided (e.g. "--locale" is the last command-line argument).
if (!args[i] && opt.type !== "boolean") {
errors.push(createCompilerDiagnostic(Diagnostics.Compiler_option_0_expects_an_argument, opt.name));
}
switch (opt.type) {
case "number":
options[opt.name] = parseInt(args[i++]);
break;
case "boolean":
options[opt.name] = true;
break;
case "string":
options[opt.name] = args[i++] || "";
break;
// If not a primitive, the possible types are specified in what is effectively a map of options.
default:
let map = <Map<number>>opt.type;
let key = (args[i++] || "").toLowerCase();
if (hasProperty(map, key)) {
options[opt.name] = map[key];
}
else {
errors.push(createCompilerDiagnostic((<CommandLineOptionOfCustomType>opt).error));
}
switch (opt.type) {
case "number":
options[opt.name] = parseInt(args[i++]);
break;
case "boolean":
options[opt.name] = true;
break;
case "string":
options[opt.name] = args[i++] || "";
break;
// If not a primitive, the possible types are specified in what is effectively a map of options.
default:
let map = <Map<number>>opt.type;
let key = (args[i++] || "").toLowerCase();
if (hasProperty(map, key)) {
options[opt.name] = map[key];
}
else {
errors.push(createCompilerDiagnostic((<CommandLineOptionOfCustomType>opt).error));
}
}
}
}
else {
@@ -466,7 +493,6 @@ namespace ts {
return output;
}
/**
* Parse the contents of a config file (tsconfig.json).
* @param json The contents of the config file to parse
@@ -477,6 +503,9 @@ namespace ts {
export function parseJsonConfigFileContent(json: any, host: ParseConfigHost, basePath: string): ParsedCommandLine {
const { options, errors } = convertCompilerOptionsFromJson(json["compilerOptions"], basePath);
// set basePath as inferredBaseUrl so baseUrl module resolution strategy can still work even if user have not specified baseUrl explicity
options.inferredBaseUrl = basePath;
return {
options,
fileNames: getFileNames(),

View File

@@ -2060,7 +2060,26 @@
"category": "Error",
"code": 5054
},
"Option '{0}' can only be used when option 'moduleResolution' is 'baseUrl'.": {
"category": "Error",
"code": 5055
},
"Pattern '{0}' can have at most one '*' character": {
"category": "Error",
"code": 5056
},
"Substitution '{0}' in pattern '{1}' in can have at most one '*' character": {
"category": "Error",
"code": 5057
},
"'moduleResolution' kind 'baseUrl' cannot be used without specifying '--baseUrl' option.": {
"category": "Error",
"code": 5058
},
"Module resolution kind cannot be determined automatically. Please specify module resolution explicitly via 'moduleResolution' option.": {
"category": "Error",
"code": 5059
},
"Concatenate and emit output to single file.": {
"category": "Message",
"code": 6001
@@ -2253,10 +2272,14 @@
"category": "Error",
"code": 6062
},
"Argument for '--moduleResolution' option must be 'node' or 'classic'.": {
"Argument for '--moduleResolution' option must be 'node', 'classic' or 'baseUrl'.": {
"category": "Error",
"code": 6063
},
"Option '{0}' can only be specified in 'tsconfig.json' file.": {
"category": "Error",
"code": 6064
},
"Enables experimental support for ES7 decorators.": {
"category": "Message",
@@ -2322,6 +2345,10 @@
"category": "Error",
"code": 6082
},
"Base directory to resolve relative module names.": {
"category": "Message",
"code": 6083
},
"Variable '{0}' implicitly has an '{1}' type.": {
"category": "Error",
"code": 7005

View File

@@ -37,23 +37,221 @@ namespace ts {
}
export function resolveModuleName(moduleName: string, containingFile: string, compilerOptions: CompilerOptions, host: ModuleResolutionHost): ResolvedModuleWithFailedLookupLocations {
const moduleResolution = compilerOptions.moduleResolution !== undefined
? compilerOptions.moduleResolution
: compilerOptions.module === ModuleKind.CommonJS ? ModuleResolutionKind.NodeJs : ModuleResolutionKind.Classic;
let moduleResolution = compilerOptions.moduleResolution;
if (moduleResolution === undefined) {
if (compilerOptions.module === ModuleKind.CommonJS) {
moduleResolution = ModuleResolutionKind.NodeJs;
}
else if (compilerOptions.baseUrl !== undefined || compilerOptions.paths || compilerOptions.rootDirs) {
moduleResolution = ModuleResolutionKind.BaseUrl;
}
else {
moduleResolution = ModuleResolutionKind.Classic;
}
}
switch (moduleResolution) {
case ModuleResolutionKind.NodeJs: return nodeModuleNameResolver(moduleName, containingFile, host);
case ModuleResolutionKind.Classic: return classicNameResolver(moduleName, containingFile, compilerOptions, host);
case ModuleResolutionKind.BaseUrl: return baseUrlModuleNameResolver(moduleName, containingFile, compilerOptions, host);
}
}
// Path mapping based module resolution strategy uses base url to resolve relative file names. This resolution strategy can be enabled
// by setting 'moduleResolution' option to 'baseUrl' and as it implied by the name it uses base url to resolve module names.
// Base url can be specified:
// - explicitly via command line option 'baseUrl'. If this value is relative it will be computed relative to current directory.
// - explicitly in 'tsconfig.json' via 'baseUrl' compiler option. If this value is relative it will be computed relative to the location
// of 'tsconfig.json'.
// - if value is not provided explicitly but project uses tsconfig.json then 'baseUrl' will be set of folder that contains 'tsconfig.json'
// If baseUrl is the only provided option then non-relative module names will be resolved relative to baseUrl. Relative module names
// will still be resolved relative to the folder that contains file with import.
// User can specify additional options in tsconfig.json to tweak module resolution.
// - 'paths' option allows to tune how non-relative module names will be resolved based on the content of the module name.
// Structure of 'paths' compiler options
// 'paths': {
// pattern-1: list of substitutions 1,
// pattern-2: list of substitutions 2,
// ...
// pattern-n: list of substitutions-n
// }
// Pattern here is a string that can contain zero or one '*' character. During module resolution module name will be matched against
// all patterns in the list. Matching for patterns that don't contain '*' means that module name must be equal to pattern respecting the case.
// If pattern contains '*' then to match pattern "<prefix>*<suffix>" module name must start with the <prefix> and end with <suffix>.
// <MatchedStar> denotes part of the module name between <prefix> and <suffix>.
// If module name can be matches with multiple patterns then pattern with the longest prefix will be picked.
// After selecting pattern we'll use list of substitutions to get candidate locations of the module and the try to load module from the candidate location.
// Substitiution is a string that can contain zero or one '*'. To get candidate location from substitution we'll pick every substitution in the list
// and replace '*' with <MatchedStar> string. If candidate location is not rooted it will be converted to absolute using baseUrl.
// For example:
// baseUrl: /a/b/c
// "paths": {
// // match all module names
// "*": [
// "*", // use matched name as is,
// // <matched name> will be looked as /a/b/c/<matched name>
//
// "folder1/*" // substitution will convert matched name to 'folder1/<matched name>',
// // since it is not rooted then final candidate location will be /a/b/c/folder1/<matched name>
// ],
// // match module names that start with 'components/'
// "components/*": [ "/root/components/*" ] // substitution will convert /components/folder1/<matched name> to '/root/components/folder1/<matched name>',
// // it is rooted so it will be final candidate location
// }
// 'paths' allows to remap locations of non-relative module names. Relative module names usually are resolved relative to the folder
// that contains import. However sometimes it might be useful to apply mappings to relative module names as well.
// Since the there might be different relative file names that refer to the same file we need to convert relative module name to non-relative
// so we can just apply path mapping to it. This conversion can be performed using 'rootDirs' compiler option - this options stores list of root directories
// for the project (if root directory is not absolute it will be converted to absolute using baseUrl as a base path).
// First - we convert relative module name to absolute using location of file that contains import.
// Then we try to find what element is rootDirs is the longest prefix for this absolute file name (respecting case):
// <absoluteFileName> = <longestRootDir><suffix>
// Once it is done - value of <suffix> is a be non-relative name for initially relative module name.
// Note that if element in rootDirs should always end with '/' so it will be appended automatically if value in configuration does not have it.
// For example:
// baseUrl: /a/b/c
// "paths": {
// "*": [ "*", "generated/*" ] // try to resolve module name as '/a/b/c/<name>' then look it in '/a/b/c/generated/<name>'
// }
// "rootDirs": [
// "./src", // root dir is project baseUrl/src
// "./generated" // root dir is baseUrl/generated
// "/e/f" // root dir is /e/f
// ]
// @file: /a/b/c/src/form1.ts
// import {x} from "./form.content.ts"
// @file: /a/b/c/generated/form1.content.ts
// export var x = ...
// first './form.content.ts' needs to be converted to non-relative name.
// 1. convert it to absolute name '/a/b/c/src/form1.content.ts' ->
// find longest prefix in rootDirs that match this path, it will be './src' (after expansion '/a/b/c/src/') ->
// suffix is form1.content.ts
// 2. apply path mappings to 'form1.content.ts' ->
// '*' pattern will be matched ->
// '*' substitution yields '/a/b/c/form1.content.ts' - file does not exists
// 'generated/*' substitution yields '/a/b/c/generated/form1.content' - file exists - OK
export function baseUrlModuleNameResolver(moduleName: string, containingFile: string, options: CompilerOptions, host: ModuleResolutionHost): ResolvedModuleWithFailedLookupLocations {
const baseUrl = options.baseUrl !== undefined ? options.baseUrl : options.inferredBaseUrl;
Debug.assert(baseUrl !== undefined);
if (isRootedDiskPath(moduleName)) {
return { resolvedModule: { resolvedFileName: moduleName }, failedLookupLocations: emptyArray };
}
if (nameStartsWithDotSlashOrDotDotSlash(moduleName)) {
// relative name
return baseUrlResolveRelativeModuleName(moduleName, containingFile, baseUrl, options, host);
}
else {
// non-relative name
return baseUrlResolveNonRelativeModuleName(moduleName, baseUrl, options, host);
}
}
export function baseUrlResolveRelativeModuleName(moduleName: string, containingFile: string, baseUrl: string, options: CompilerOptions, host: ModuleResolutionHost): ResolvedModuleWithFailedLookupLocations {
const failedLookupLocations: string[] = [];
const containingDirectory = getDirectoryPath(containingFile);
const candidate = normalizePath(combinePaths(containingDirectory, moduleName));
if (options.rootDirs) {
let matchedPrefix: string;
for (const rootDir of options.rootDirs) {
let normalizedRoot = getNormalizedAbsolutePath(rootDir, baseUrl);
if (!endsWith(normalizedRoot, directorySeparator)) {
normalizedRoot += directorySeparator;
}
if (startsWith(candidate, normalizedRoot) && (matchedPrefix === undefined || matchedPrefix.length < normalizedRoot.length)) {
matchedPrefix = normalizedRoot;
}
}
if (matchedPrefix) {
const suffix = candidate.substr(matchedPrefix.length);
return baseUrlResolveNonRelativeModuleName(suffix, baseUrl, options, host);
}
return { resolvedModule: undefined, failedLookupLocations };
}
else {
const resolvedFileName = loadModuleFromFile(supportedExtensions, candidate, failedLookupLocations, host);
return {
resolvedModule: resolvedFileName ? { resolvedFileName } : undefined,
failedLookupLocations
};
}
}
export function baseUrlResolveNonRelativeModuleName(moduleName: string, baseUrl: string, options: CompilerOptions, host: ModuleResolutionHost): ResolvedModuleWithFailedLookupLocations {
let longestMatchPrefixLength = -1;
let matchedPattern: string;
let matchedStar: string;
const failedLookupLocations: string[] = [];
if (options.paths) {
for (const key in options.paths) {
const pattern: string = key;
const indexOfStar = pattern.indexOf("*");
if (indexOfStar !== -1) {
const prefix = pattern.substr(0, indexOfStar);
const suffix = pattern.substr(indexOfStar + 1);
if (moduleName.length >= prefix.length + suffix.length &&
startsWith(moduleName, prefix) &&
endsWith(moduleName, suffix)) {
// use length of prefix as betterness criteria
if (prefix.length > longestMatchPrefixLength) {
longestMatchPrefixLength = prefix.length;
matchedPattern = pattern;
matchedStar = moduleName.substr(prefix.length, moduleName.length - suffix.length);
}
}
}
else if (pattern === moduleName) {
// pattern was matched as is - no need to seatch further
matchedPattern = pattern;
matchedStar = undefined;
break;
}
}
}
if (matchedPattern) {
for (const subst of options.paths[matchedPattern]) {
const path = matchedStar ? subst.replace("\*", matchedStar) : subst;
const candidate = normalizePath(combinePaths(baseUrl, path));
const resolvedFileName = loadModuleFromFile(supportedExtensions, candidate, failedLookupLocations, host);
if (resolvedFileName) {
return { resolvedModule: { resolvedFileName }, failedLookupLocations };
}
}
return { resolvedModule: undefined, failedLookupLocations };
}
else {
const candidate = normalizePath(combinePaths(baseUrl, moduleName));
const resolvedFileName = loadModuleFromFile(supportedExtensions, candidate, failedLookupLocations, host);
return {
resolvedModule: resolvedFileName ? { resolvedFileName } : undefined,
failedLookupLocations
};
}
}
function startsWith(str: string, prefix: string): boolean {
return str.lastIndexOf(prefix, 0) === 0;
}
function endsWith(str: string, suffix: string): boolean {
const expectedPos = str.length - suffix.length;
return str.indexOf(suffix, expectedPos) === expectedPos;
}
export function nodeModuleNameResolver(moduleName: string, containingFile: string, host: ModuleResolutionHost): ResolvedModuleWithFailedLookupLocations {
const containingDirectory = getDirectoryPath(containingFile);
if (getRootLength(moduleName) !== 0 || nameStartsWithDotSlashOrDotDotSlash(moduleName)) {
if (isRootedDiskPath(moduleName) || nameStartsWithDotSlashOrDotDotSlash(moduleName)) {
const failedLookupLocations: string[] = [];
const candidate = normalizePath(combinePaths(containingDirectory, moduleName));
let resolvedFileName = loadNodeModuleFromFile(supportedJsExtensions, candidate, failedLookupLocations, host);
let resolvedFileName = loadModuleFromFile(supportedJsExtensions, candidate, failedLookupLocations, host);
if (resolvedFileName) {
return { resolvedModule: { resolvedFileName }, failedLookupLocations };
@@ -69,7 +267,7 @@ namespace ts {
}
}
function loadNodeModuleFromFile(extensions: string[], candidate: string, failedLookupLocation: string[], host: ModuleResolutionHost): string {
function loadModuleFromFile(extensions: string[], candidate: string, failedLookupLocation: string[], host: ModuleResolutionHost): string {
return forEach(extensions, tryLoad);
function tryLoad(ext: string): string {
@@ -100,7 +298,7 @@ namespace ts {
}
if (jsonContent.typings) {
const result = loadNodeModuleFromFile(extensions, normalizePath(combinePaths(candidate, jsonContent.typings)), failedLookupLocation, host);
const result = loadModuleFromFile(extensions, normalizePath(combinePaths(candidate, jsonContent.typings)), failedLookupLocation, host);
if (result) {
return result;
}
@@ -111,7 +309,7 @@ namespace ts {
failedLookupLocation.push(packageJsonPath);
}
return loadNodeModuleFromFile(extensions, combinePaths(candidate, "index"), failedLookupLocation, host);
return loadModuleFromFile(extensions, combinePaths(candidate, "index"), failedLookupLocation, host);
}
function loadModuleFromNodeModules(moduleName: string, directory: string, host: ModuleResolutionHost): ResolvedModuleWithFailedLookupLocations {
@@ -122,7 +320,7 @@ namespace ts {
if (baseName !== "node_modules") {
const nodeModulesFolder = combinePaths(directory, "node_modules");
const candidate = normalizePath(combinePaths(nodeModulesFolder, moduleName));
let result = loadNodeModuleFromFile(supportedExtensions, candidate, failedLookupLocations, host);
let result = loadModuleFromFile(supportedExtensions, candidate, failedLookupLocations, host);
if (result) {
return { resolvedModule: { resolvedFileName: result, isExternalLibraryImport: true }, failedLookupLocations };
}
@@ -149,6 +347,22 @@ namespace ts {
return i === 0 || (i === 1 && name.charCodeAt(0) === CharacterCodes.dot);
}
function hasZeroOrOneAsteriskCharacter(str: string): boolean {
let seenAsterisk = false;
for (let i = 0; i < str.length; ++i) {
if (str.charCodeAt(i) === CharacterCodes.asterisk) {
if (!seenAsterisk) {
seenAsterisk = true;
}
else {
// have already seen asterisk
return false;
}
}
}
return true;
}
export function classicNameResolver(moduleName: string, containingFile: string, compilerOptions: CompilerOptions, host: ModuleResolutionHost): ResolvedModuleWithFailedLookupLocations {
// module names that contain '!' are used to reference resources and are not resolved to actual files on disk
@@ -997,6 +1211,47 @@ namespace ts {
}
}
if (options.moduleResolution === ModuleResolutionKind.BaseUrl) {
if (options.baseUrl === undefined && options.inferredBaseUrl === undefined) {
programDiagnostics.add(createCompilerDiagnostic(Diagnostics.moduleResolution_kind_baseUrl_cannot_be_used_without_specifying_baseUrl_option));
}
}
else if (options.moduleResolution === undefined) {
// if module resolution kind is not specified it is an error to have baseUrl\paths\rootDirs and module === CommonJs
// since the former one implies moduleResolutionKind to be BaseUrl and the latter one - Node
if (options.module === ModuleKind.CommonJS && (options.baseUrl || options.paths || options.rootDirs)) {
programDiagnostics.add(createCompilerDiagnostic(Diagnostics.Module_resolution_kind_cannot_be_determined_automatically_Please_specify_module_resolution_explicitly_via_moduleResolution_option));
}
}
else {
// here it is known that moduleResolution is not baseurl and is not undefined
if (options.baseUrl !== undefined) {
programDiagnostics.add(createCompilerDiagnostic(Diagnostics.Option_0_can_only_be_used_when_option_moduleResolution_is_baseUrl, "baseUrl"));
}
if (options.paths) {
programDiagnostics.add(createCompilerDiagnostic(Diagnostics.Option_0_can_only_be_used_when_option_moduleResolution_is_baseUrl, "paths"));
}
if (options.rootDirs) {
programDiagnostics.add(createCompilerDiagnostic(Diagnostics.Option_0_can_only_be_used_when_option_moduleResolution_is_baseUrl, "rootDirs"));
}
}
if (options.paths) {
for (const key in options.paths) {
if (!hasProperty(options.paths, key)) {
continue;
}
if (!hasZeroOrOneAsteriskCharacter(key)) {
programDiagnostics.add(createCompilerDiagnostic(Diagnostics.Pattern_0_can_have_at_most_one_Asterisk_character, key));
}
for (const subst of options.paths[key]) {
if (!hasZeroOrOneAsteriskCharacter(subst)) {
programDiagnostics.add(createCompilerDiagnostic(Diagnostics.Substitution_0_in_pattern_1_in_can_have_at_most_one_Asterisk_character, subst, key));
}
}
}
}
if (options.inlineSources) {
if (!options.sourceMap && !options.inlineSourceMap) {
programDiagnostics.add(createCompilerDiagnostic(Diagnostics.Option_inlineSources_can_only_be_used_when_either_option_inlineSourceMap_or_option_sourceMap_is_provided));

View File

@@ -360,7 +360,7 @@ namespace ts {
sys.exit(ExitStatus.DiagnosticsPresent_OutputsSkipped);
return;
}
const configParseResult = parseJsonConfigFileContent(configObject, sys, getDirectoryPath(configFileName));
const configParseResult = parseJsonConfigFileContent(configObject, sys, getNormalizedAbsolutePath(getDirectoryPath(configFileName), sys.getCurrentDirectory()));
if (configParseResult.errors.length > 0) {
reportDiagnostics(configParseResult.errors, /* compilerHost */ undefined);
sys.exit(ExitStatus.DiagnosticsPresent_OutputsSkipped);
@@ -723,7 +723,9 @@ namespace ts {
for (const name in options) {
if (hasProperty(options, name)) {
const value = options[name];
// tsconfig only options cannot be specified via command line,
// so we can assume that only types that can appear here string | number | boolean
const value = <string | number | boolean>options[name];
switch (name) {
case "init":
case "watch":

View File

@@ -2310,9 +2310,14 @@ namespace ts {
export const enum ModuleResolutionKind {
Classic = 1,
NodeJs = 2
NodeJs = 2,
BaseUrl = 3
}
export type RootPaths = string[];
export type PathSubstitutions = Map<string[]>;
export type TsConfigOnlyOptions = RootPaths | PathSubstitutions;
export interface CompilerOptions {
allowNonTsExtensions?: boolean;
charset?: string;
@@ -2360,12 +2365,17 @@ namespace ts {
noImplicitReturns?: boolean;
noFallthroughCasesInSwitch?: boolean;
forceConsistentCasingInFileNames?: boolean;
baseUrl?: string;
paths?: PathSubstitutions;
rootDirs?: RootPaths;
/* @internal */ stripInternal?: boolean;
// Skip checking lib.d.ts to help speed up tests.
/* @internal */ skipDefaultLibCheck?: boolean;
// inferred baseUrl - currently this will be set in 'parseJsonConfigFileContent' to 'baseDir'
/* @internal */ inferredBaseUrl?: string;
[option: string]: string | number | boolean;
[option: string]: string | number | boolean | TsConfigOnlyOptions;
}
export const enum ModuleKind {
@@ -2425,12 +2435,13 @@ namespace ts {
/* @internal */
export interface CommandLineOptionBase {
name: string;
type: "string" | "number" | "boolean" | Map<number>; // a value of a primitive type, or an object literal mapping named values to actual values
type: "string" | "number" | "boolean" | "object" | Map<number>; // a value of a primitive type, or an object literal mapping named values to actual values
isFilePath?: boolean; // True if option value is a path or fileName
shortName?: string; // A short mnemonic for convenience - for instance, 'h' can be used in place of 'help'
description?: DiagnosticMessage; // The message describing what the command line switch does
paramType?: DiagnosticMessage; // The name to be used for a non-boolean option's parameter
experimental?: boolean;
isTSConfigOnly?: boolean; // True if option can only be specified via tsconfig.json file
}
/* @internal */
@@ -2445,7 +2456,12 @@ namespace ts {
}
/* @internal */
export type CommandLineOption = CommandLineOptionOfCustomType | CommandLineOptionOfPrimitiveType;
export interface TsConfigOnlyOption extends CommandLineOptionBase {
type: "object";
}
/* @internal */
export type CommandLineOption = CommandLineOptionOfCustomType | CommandLineOptionOfPrimitiveType | TsConfigOnlyOption;
/* @internal */
export const enum CharacterCodes {

View File

@@ -49,7 +49,6 @@ class CompilerBaselineRunner extends RunnerBase {
// Mocha holds onto the closure environment of the describe callback even after the test is done.
// Everything declared here should be cleared out in the "after" callback.
let justName: string;
let lastUnit: Harness.TestCaseParser.TestUnitData;
let harnessSettings: Harness.TestCaseParser.CompilerSettings;
@@ -63,16 +62,30 @@ class CompilerBaselineRunner extends RunnerBase {
before(() => {
justName = fileName.replace(/^.*[\\\/]/, ""); // strips the fileName from the path.
const content = Harness.IO.readFile(fileName);
const testCaseContent = Harness.TestCaseParser.makeUnitsFromTest(content, fileName);
const rootDir = fileName.indexOf("conformance") === -1 ? "tests/cases/compiler/" : ts.getDirectoryPath(fileName) + "/";
const testCaseContent = Harness.TestCaseParser.makeUnitsFromTest(content, fileName, rootDir);
const units = testCaseContent.testUnitData;
harnessSettings = testCaseContent.settings;
let tsConfigOptions: ts.CompilerOptions;
if (testCaseContent.tsConfig) {
assert.equal(testCaseContent.tsConfig.fileNames.length, 0, `list of files in tsconfig is not currently supported`);
tsConfigOptions = ts.clone(testCaseContent.tsConfig.options);
}
else {
const baseUrl = harnessSettings["baseUrl"];
if (baseUrl !== undefined && !ts.isRootedDiskPath(baseUrl)) {
harnessSettings["baseUrl"] = ts.getNormalizedAbsolutePath(baseUrl, rootDir);
}
}
lastUnit = units[units.length - 1];
const rootDir = lastUnit.originalFilePath.indexOf("conformance") === -1 ? "tests/cases/compiler/" : lastUnit.originalFilePath.substring(0, lastUnit.originalFilePath.lastIndexOf("/")) + "/";
// We need to assemble the list of input files for the compiler and other related files on the 'filesystem' (ie in a multi-file test)
// If the last file in a test uses require or a triple slash reference we'll assume all other files will be brought in via references,
// otherwise, assume all files are just meant to be in the same compilation session without explicit references to one another.
toBeCompiled = [];
otherFiles = [];
if (/require\(/.test(lastUnit.content) || /reference\spath/.test(lastUnit.content)) {
toBeCompiled.push({ unitName: this.makeUnitName(lastUnit.name, rootDir), content: lastUnit.content });
units.forEach(unit => {
@@ -88,7 +101,7 @@ class CompilerBaselineRunner extends RunnerBase {
}
const output = Harness.Compiler.compileFiles(
toBeCompiled, otherFiles, harnessSettings, /* options */ undefined, /* currentDirectory */ undefined);
toBeCompiled, otherFiles, harnessSettings, /*options*/ tsConfigOptions, /*currentDirectory*/ undefined);
options = output.options;
result = output.result;

View File

@@ -1000,6 +1000,9 @@ namespace Harness {
if (options.useCaseSensitiveFileNames !== undefined) {
useCaseSensitiveFileNames = options.useCaseSensitiveFileNames;
}
if (options.inferredBaseUrl) {
options.inferredBaseUrl = ts.getNormalizedAbsolutePath(options.inferredBaseUrl, currentDirectory);
}
const programFiles: TestFile[] = inputFiles.slice();
// Files from built\local that are requested by test "@includeBuiltFiles" to be in the context.
@@ -1372,7 +1375,7 @@ namespace Harness {
}
/** Given a test file containing // @FileName directives, return an array of named units of code to be added to an existing compiler instance */
export function makeUnitsFromTest(code: string, fileName: string): { settings: CompilerSettings; testUnitData: TestUnitData[]; } {
export function makeUnitsFromTest(code: string, fileName: string, rootDir?: string): { settings: CompilerSettings; testUnitData: TestUnitData[]; tsConfig: ts.ParsedCommandLine } {
const settings = extractCompilerSettings(code);
// List of all the subfiles we've parsed out
@@ -1450,7 +1453,31 @@ namespace Harness {
};
testUnitData.push(newTestFile2);
return { settings, testUnitData };
// unit tests always list files explicitly
const parseConfigHost: ts.ParseConfigHost = {
readDirectory: (name) => []
};
// check if project has tsconfig.json in the list of files
let tsConfig: ts.ParsedCommandLine;
for (let i = 0; i < testUnitData.length; ++i) {
const data = testUnitData[i];
if (ts.getBaseFileName(data.name).toLowerCase() === "tsconfig.json") {
const configJson = ts.parseConfigFileTextToJson(data.name, data.content);
assert.isTrue(configJson.config !== undefined);
let baseDir = ts.normalizePath(ts.getDirectoryPath(data.name));
if (rootDir) {
baseDir = ts.getNormalizedAbsolutePath(baseDir, rootDir);
}
tsConfig = ts.parseJsonConfigFileContent(configJson.config, parseConfigHost, baseDir);
// delete entry from the list
testUnitData.splice(i, 1);
break;
}
}
return { settings, testUnitData, tsConfig };
}
}