Merge pull request #9941 from Microsoft/configuration-inheritance
Configuration Inheritance
This commit is contained in:
commit
873850b99e
|
@ -216,6 +216,7 @@ var harnessSources = harnessCoreSources.concat([
|
|||
"moduleResolution.ts",
|
||||
"tsconfigParsing.ts",
|
||||
"commandLineParsing.ts",
|
||||
"configurationExtension.ts",
|
||||
"convertCompilerOptionsFromJson.ts",
|
||||
"convertTypingOptionsFromJson.ts",
|
||||
"tsserverProjectSystem.ts",
|
||||
|
|
|
@ -806,12 +806,45 @@ namespace ts {
|
|||
* @param basePath A root directory to resolve relative path entries in the config
|
||||
* file to. e.g. outDir
|
||||
*/
|
||||
export function parseJsonConfigFileContent(json: any, host: ParseConfigHost, basePath: string, existingOptions: CompilerOptions = {}, configFileName?: string): ParsedCommandLine {
|
||||
export function parseJsonConfigFileContent(json: any, host: ParseConfigHost, basePath: string, existingOptions: CompilerOptions = {}, configFileName?: string, resolutionStack: Path[] = []): ParsedCommandLine {
|
||||
const errors: Diagnostic[] = [];
|
||||
const compilerOptions: CompilerOptions = convertCompilerOptionsFromJsonWorker(json["compilerOptions"], basePath, errors, configFileName);
|
||||
const options = extend(existingOptions, compilerOptions);
|
||||
const getCanonicalFileName = createGetCanonicalFileName(host.useCaseSensitiveFileNames);
|
||||
const resolvedPath = toPath(configFileName || "", basePath, getCanonicalFileName);
|
||||
if (resolutionStack.indexOf(resolvedPath) >= 0) {
|
||||
return {
|
||||
options: {},
|
||||
fileNames: [],
|
||||
typingOptions: {},
|
||||
raw: json,
|
||||
errors: [createCompilerDiagnostic(Diagnostics.Circularity_detected_while_resolving_configuration_Colon_0, [...resolutionStack, resolvedPath].join(" -> "))],
|
||||
wildcardDirectories: {}
|
||||
};
|
||||
}
|
||||
|
||||
let options: CompilerOptions = convertCompilerOptionsFromJsonWorker(json["compilerOptions"], basePath, errors, configFileName);
|
||||
const typingOptions: TypingOptions = convertTypingOptionsFromJsonWorker(json["typingOptions"], basePath, errors, configFileName);
|
||||
|
||||
if (json["extends"]) {
|
||||
let [include, exclude, files, baseOptions]: [string[], string[], string[], CompilerOptions] = [undefined, undefined, undefined, {}];
|
||||
if (typeof json["extends"] === "string") {
|
||||
[include, exclude, files, baseOptions] = (tryExtendsName(json["extends"]) || [include, exclude, files, baseOptions]);
|
||||
}
|
||||
else {
|
||||
errors.push(createCompilerDiagnostic(Diagnostics.Compiler_option_0_requires_a_value_of_type_1, "extends", "string"));
|
||||
}
|
||||
if (include && !json["include"]) {
|
||||
json["include"] = include;
|
||||
}
|
||||
if (exclude && !json["exclude"]) {
|
||||
json["exclude"] = exclude;
|
||||
}
|
||||
if (files && !json["files"]) {
|
||||
json["files"] = files;
|
||||
}
|
||||
options = assign({}, baseOptions, options);
|
||||
}
|
||||
|
||||
options = extend(existingOptions, options);
|
||||
options.configFilePath = configFileName;
|
||||
|
||||
const { fileNames, wildcardDirectories } = getFileNames(errors);
|
||||
|
@ -825,6 +858,39 @@ namespace ts {
|
|||
wildcardDirectories
|
||||
};
|
||||
|
||||
function tryExtendsName(extendedConfig: string): [string[], string[], string[], CompilerOptions] {
|
||||
// If the path isn't a rooted or relative path, don't try to resolve it (we reserve the right to special case module-id like paths in the future)
|
||||
if (!(isRootedDiskPath(extendedConfig) || startsWith(normalizeSlashes(extendedConfig), "./") || startsWith(normalizeSlashes(extendedConfig), "../"))) {
|
||||
errors.push(createCompilerDiagnostic(Diagnostics.The_path_in_an_extends_options_must_be_relative_or_rooted));
|
||||
return;
|
||||
}
|
||||
let extendedConfigPath = toPath(extendedConfig, basePath, getCanonicalFileName);
|
||||
if (!host.fileExists(extendedConfigPath) && !endsWith(extendedConfigPath, ".json")) {
|
||||
extendedConfigPath = `${extendedConfigPath}.json` as Path;
|
||||
if (!host.fileExists(extendedConfigPath)) {
|
||||
errors.push(createCompilerDiagnostic(Diagnostics.File_0_does_not_exist, extendedConfig));
|
||||
return;
|
||||
}
|
||||
}
|
||||
const extendedResult = readConfigFile(extendedConfigPath, path => host.readFile(path));
|
||||
if (extendedResult.error) {
|
||||
errors.push(extendedResult.error);
|
||||
return;
|
||||
}
|
||||
const extendedDirname = getDirectoryPath(extendedConfigPath);
|
||||
const relativeDifference = convertToRelativePath(extendedDirname, basePath, getCanonicalFileName);
|
||||
const updatePath: (path: string) => string = path => isRootedDiskPath(path) ? path : combinePaths(relativeDifference, path);
|
||||
// Merge configs (copy the resolution stack so it is never reused between branches in potential diamond-problem scenarios)
|
||||
const result = parseJsonConfigFileContent(extendedResult.config, host, extendedDirname, /*existingOptions*/undefined, getBaseFileName(extendedConfigPath), resolutionStack.concat([resolvedPath]));
|
||||
errors.push(...result.errors);
|
||||
const [include, exclude, files] = map(["include", "exclude", "files"], key => {
|
||||
if (!json[key] && extendedResult.config[key]) {
|
||||
return map(extendedResult.config[key], updatePath);
|
||||
}
|
||||
});
|
||||
return [include, exclude, files, result.options];
|
||||
}
|
||||
|
||||
function getFileNames(errors: Diagnostic[]): ExpandResult {
|
||||
let fileNames: string[];
|
||||
if (hasProperty(json, "files")) {
|
||||
|
|
|
@ -377,6 +377,20 @@ namespace ts {
|
|||
return result;
|
||||
}
|
||||
|
||||
export function mapObject<T, U>(object: MapLike<T>, f: (key: string, x: T) => [string, U]): MapLike<U> {
|
||||
let result: MapLike<U>;
|
||||
if (object) {
|
||||
result = {};
|
||||
for (const v of getOwnKeys(object)) {
|
||||
const [key, value]: [string, U] = f(v, object[v]) || [undefined, undefined];
|
||||
if (key !== undefined) {
|
||||
result[key] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
export function concatenate<T>(array1: T[], array2: T[]): T[] {
|
||||
if (!array2 || !array2.length) return array1;
|
||||
if (!array1 || !array1.length) return array2;
|
||||
|
@ -639,6 +653,18 @@ namespace ts {
|
|||
}
|
||||
}
|
||||
|
||||
export function assign<T1 extends MapLike<{}>, T2, T3>(t: T1, arg1: T2, arg2: T3): T1 & T2 & T3;
|
||||
export function assign<T1 extends MapLike<{}>, T2>(t: T1, arg1: T2): T1 & T2;
|
||||
export function assign<T1 extends MapLike<{}>>(t: T1, ...args: any[]): any;
|
||||
export function assign<T1 extends MapLike<{}>>(t: T1, ...args: any[]) {
|
||||
for (const arg of args) {
|
||||
for (const p of getOwnKeys(arg)) {
|
||||
t[p] = arg[p];
|
||||
}
|
||||
}
|
||||
return t;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reduce the properties of a map.
|
||||
*
|
||||
|
|
|
@ -3047,5 +3047,14 @@
|
|||
"Unknown typing option '{0}'.": {
|
||||
"category": "Error",
|
||||
"code": 17010
|
||||
},
|
||||
|
||||
"Circularity detected while resolving configuration: {0}": {
|
||||
"category": "Error",
|
||||
"code": 18000
|
||||
},
|
||||
"The path in an 'extends' options must be relative or rooted.": {
|
||||
"category": "Error",
|
||||
"code": 18001
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1807,6 +1807,8 @@ namespace ts {
|
|||
* @param path The path to test.
|
||||
*/
|
||||
fileExists(path: string): boolean;
|
||||
|
||||
readFile(path: string): string;
|
||||
}
|
||||
|
||||
export interface WriteFileCallback {
|
||||
|
|
|
@ -1844,7 +1844,8 @@ namespace Harness {
|
|||
const parseConfigHost: ts.ParseConfigHost = {
|
||||
useCaseSensitiveFileNames: false,
|
||||
readDirectory: (name) => [],
|
||||
fileExists: (name) => true
|
||||
fileExists: (name) => true,
|
||||
readFile: (name) => ts.forEach(testUnitData, data => data.name.toLowerCase() === name.toLowerCase() ? data.content : undefined)
|
||||
};
|
||||
|
||||
// check if project has tsconfig.json in the list of files
|
||||
|
|
|
@ -222,6 +222,7 @@ class ProjectRunner extends RunnerBase {
|
|||
useCaseSensitiveFileNames: Harness.IO.useCaseSensitiveFileNames(),
|
||||
fileExists,
|
||||
readDirectory,
|
||||
readFile
|
||||
};
|
||||
const configParseResult = ts.parseJsonConfigFileContent(configObject, configParseHost, ts.getDirectoryPath(configFileName), compilerOptions);
|
||||
if (configParseResult.errors.length > 0) {
|
||||
|
@ -292,6 +293,10 @@ class ProjectRunner extends RunnerBase {
|
|||
return Harness.IO.fileExists(getFileNameInTheProjectTest(fileName));
|
||||
}
|
||||
|
||||
function readFile(fileName: string): string {
|
||||
return Harness.IO.readFile(getFileNameInTheProjectTest(fileName));
|
||||
}
|
||||
|
||||
function getSourceFileText(fileName: string): string {
|
||||
let text: string = undefined;
|
||||
try {
|
||||
|
|
|
@ -79,6 +79,7 @@ namespace RWC {
|
|||
useCaseSensitiveFileNames: Harness.IO.useCaseSensitiveFileNames(),
|
||||
fileExists: Harness.IO.fileExists,
|
||||
readDirectory: Harness.IO.readDirectory,
|
||||
readFile: Harness.IO.readFile
|
||||
};
|
||||
const configParseResult = ts.parseJsonConfigFileContent(parsedTsconfigFileContents.config, configParseHost, ts.getDirectoryPath(tsconfigFile.path));
|
||||
fileNames = configParseResult.fileNames;
|
||||
|
|
|
@ -86,6 +86,7 @@
|
|||
"./unittests/moduleResolution.ts",
|
||||
"./unittests/tsconfigParsing.ts",
|
||||
"./unittests/commandLineParsing.ts",
|
||||
"./unittests/configurationExtension.ts",
|
||||
"./unittests/convertCompilerOptionsFromJson.ts",
|
||||
"./unittests/convertTypingOptionsFromJson.ts",
|
||||
"./unittests/tsserverProjectSystem.ts",
|
||||
|
|
187
src/harness/unittests/configurationExtension.ts
Normal file
187
src/harness/unittests/configurationExtension.ts
Normal file
|
@ -0,0 +1,187 @@
|
|||
/// <reference path="..\harness.ts" />
|
||||
/// <reference path="..\virtualFileSystem.ts" />
|
||||
|
||||
namespace ts {
|
||||
const testContents = {
|
||||
"/dev/tsconfig.json": `{
|
||||
"extends": "./configs/base",
|
||||
"files": [
|
||||
"main.ts",
|
||||
"supplemental.ts"
|
||||
]
|
||||
}`,
|
||||
"/dev/tsconfig.nostrictnull.json": `{
|
||||
"extends": "./tsconfig",
|
||||
"compilerOptions": {
|
||||
"strictNullChecks": false
|
||||
}
|
||||
}`,
|
||||
"/dev/configs/base.json": `{
|
||||
"compilerOptions": {
|
||||
"allowJs": true,
|
||||
"noImplicitAny": true,
|
||||
"strictNullChecks": true
|
||||
}
|
||||
}`,
|
||||
"/dev/configs/tests.json": `{
|
||||
"compilerOptions": {
|
||||
"preserveConstEnums": true,
|
||||
"removeComments": false,
|
||||
"sourceMap": true
|
||||
},
|
||||
"exclude": [
|
||||
"../tests/baselines",
|
||||
"../tests/scenarios"
|
||||
],
|
||||
"include": [
|
||||
"../tests/**/*.ts"
|
||||
]
|
||||
}`,
|
||||
"/dev/circular.json": `{
|
||||
"extends": "./circular2",
|
||||
"compilerOptions": {
|
||||
"module": "amd"
|
||||
}
|
||||
}`,
|
||||
"/dev/circular2.json": `{
|
||||
"extends": "./circular",
|
||||
"compilerOptions": {
|
||||
"module": "commonjs"
|
||||
}
|
||||
}`,
|
||||
"/dev/missing.json": `{
|
||||
"extends": "./missing2",
|
||||
"compilerOptions": {
|
||||
"types": []
|
||||
}
|
||||
}`,
|
||||
"/dev/failure.json": `{
|
||||
"extends": "./failure2.json",
|
||||
"compilerOptions": {
|
||||
"typeRoots": []
|
||||
}
|
||||
}`,
|
||||
"/dev/failure2.json": `{
|
||||
"excludes": ["*.js"]
|
||||
}`,
|
||||
"/dev/configs/first.json": `{
|
||||
"extends": "./base",
|
||||
"compilerOptions": {
|
||||
"module": "commonjs"
|
||||
},
|
||||
"files": ["../main.ts"]
|
||||
}`,
|
||||
"/dev/configs/second.json": `{
|
||||
"extends": "./base",
|
||||
"compilerOptions": {
|
||||
"module": "amd"
|
||||
},
|
||||
"include": ["../supplemental.*"]
|
||||
}`,
|
||||
"/dev/extends.json": `{ "extends": 42 }`,
|
||||
"/dev/extends2.json": `{ "extends": "configs/base" }`,
|
||||
"/dev/main.ts": "",
|
||||
"/dev/supplemental.ts": "",
|
||||
"/dev/tests/unit/spec.ts": "",
|
||||
"/dev/tests/utils.ts": "",
|
||||
"/dev/tests/scenarios/first.json": "",
|
||||
"/dev/tests/baselines/first/output.ts": ""
|
||||
};
|
||||
|
||||
const caseInsensitiveBasePath = "c:/dev/";
|
||||
const caseInsensitiveHost = new Utils.MockParseConfigHost(caseInsensitiveBasePath, /*useCaseSensitiveFileNames*/ false, mapObject(testContents, (key, content) => [`c:${key}`, content]));
|
||||
|
||||
const caseSensitiveBasePath = "/dev/";
|
||||
const caseSensitiveHost = new Utils.MockParseConfigHost(caseSensitiveBasePath, /*useCaseSensitiveFileNames*/ true, testContents);
|
||||
|
||||
function verifyDiagnostics(actual: Diagnostic[], expected: {code: number, category: DiagnosticCategory, messageText: string}[]) {
|
||||
assert.isTrue(expected.length === actual.length, `Expected error: ${JSON.stringify(expected)}. Actual error: ${JSON.stringify(actual)}.`);
|
||||
for (let i = 0; i < actual.length; i++) {
|
||||
const actualError = actual[i];
|
||||
const expectedError = expected[i];
|
||||
assert.equal(actualError.code, expectedError.code, "Error code mismatch");
|
||||
assert.equal(actualError.category, expectedError.category, "Category mismatch");
|
||||
assert.equal(flattenDiagnosticMessageText(actualError.messageText, "\n"), expectedError.messageText);
|
||||
}
|
||||
}
|
||||
|
||||
describe("Configuration Extension", () => {
|
||||
forEach<[string, string, Utils.MockParseConfigHost], void>([
|
||||
["under a case insensitive host", caseInsensitiveBasePath, caseInsensitiveHost],
|
||||
["under a case sensitive host", caseSensitiveBasePath, caseSensitiveHost]
|
||||
], ([testName, basePath, host]) => {
|
||||
function testSuccess(name: string, entry: string, expected: CompilerOptions, expectedFiles: string[]) {
|
||||
it(name, () => {
|
||||
const {config, error} = ts.readConfigFile(entry, name => host.readFile(name));
|
||||
assert(config && !error, flattenDiagnosticMessageText(error && error.messageText, "\n"));
|
||||
const parsed = ts.parseJsonConfigFileContent(config, host, basePath, {}, entry);
|
||||
assert(!parsed.errors.length, flattenDiagnosticMessageText(parsed.errors[0] && parsed.errors[0].messageText, "\n"));
|
||||
expected.configFilePath = entry;
|
||||
assert.deepEqual(parsed.options, expected);
|
||||
assert.deepEqual(parsed.fileNames, expectedFiles);
|
||||
});
|
||||
}
|
||||
|
||||
function testFailure(name: string, entry: string, expectedDiagnostics: {code: number, category: DiagnosticCategory, messageText: string}[]) {
|
||||
it(name, () => {
|
||||
const {config, error} = ts.readConfigFile(entry, name => host.readFile(name));
|
||||
assert(config && !error, flattenDiagnosticMessageText(error && error.messageText, "\n"));
|
||||
const parsed = ts.parseJsonConfigFileContent(config, host, basePath, {}, entry);
|
||||
verifyDiagnostics(parsed.errors, expectedDiagnostics);
|
||||
});
|
||||
}
|
||||
|
||||
describe(testName, () => {
|
||||
testSuccess("can resolve an extension with a base extension", "tsconfig.json", {
|
||||
allowJs: true,
|
||||
noImplicitAny: true,
|
||||
strictNullChecks: true,
|
||||
}, [
|
||||
combinePaths(basePath, "main.ts"),
|
||||
combinePaths(basePath, "supplemental.ts"),
|
||||
]);
|
||||
|
||||
testSuccess("can resolve an extension with a base extension that overrides options", "tsconfig.nostrictnull.json", {
|
||||
allowJs: true,
|
||||
noImplicitAny: true,
|
||||
strictNullChecks: false,
|
||||
}, [
|
||||
combinePaths(basePath, "main.ts"),
|
||||
combinePaths(basePath, "supplemental.ts"),
|
||||
]);
|
||||
|
||||
testFailure("can report errors on circular imports", "circular.json", [
|
||||
{
|
||||
code: 18000,
|
||||
category: DiagnosticCategory.Error,
|
||||
messageText: `Circularity detected while resolving configuration: ${[combinePaths(basePath, "circular.json"), combinePaths(basePath, "circular2.json"), combinePaths(basePath, "circular.json")].join(" -> ")}`
|
||||
}
|
||||
]);
|
||||
|
||||
testFailure("can report missing configurations", "missing.json", [{
|
||||
code: 6096,
|
||||
category: DiagnosticCategory.Message,
|
||||
messageText: `File './missing2' does not exist.`
|
||||
}]);
|
||||
|
||||
testFailure("can report errors in extended configs", "failure.json", [{
|
||||
code: 6114,
|
||||
category: DiagnosticCategory.Error,
|
||||
messageText: `Unknown option 'excludes'. Did you mean 'exclude'?`
|
||||
}]);
|
||||
|
||||
testFailure("can error when 'extends' is not a string", "extends.json", [{
|
||||
code: 5024,
|
||||
category: DiagnosticCategory.Error,
|
||||
messageText: `Compiler option 'extends' requires a value of type string.`
|
||||
}]);
|
||||
|
||||
testFailure("can error when 'extends' is neither relative nor rooted.", "extends2.json", [{
|
||||
code: 18001,
|
||||
category: DiagnosticCategory.Error,
|
||||
messageText: `The path in an 'extends' options must be relative or rooted.`
|
||||
}]);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
|
@ -10,9 +10,9 @@ namespace Utils {
|
|||
this.name = name;
|
||||
}
|
||||
|
||||
isDirectory() { return false; }
|
||||
isFile() { return false; }
|
||||
isFileSystem() { return false; }
|
||||
isDirectory(): this is VirtualDirectory { return false; }
|
||||
isFile(): this is VirtualFile { return false; }
|
||||
isFileSystem(): this is VirtualFileSystem { return false; }
|
||||
}
|
||||
|
||||
export class VirtualFile extends VirtualFileSystemEntry {
|
||||
|
@ -82,9 +82,8 @@ namespace Utils {
|
|||
return file;
|
||||
}
|
||||
else if (entry.isFile()) {
|
||||
const file = <VirtualFile>entry;
|
||||
file.content = content;
|
||||
return file;
|
||||
entry.content = content;
|
||||
return entry;
|
||||
}
|
||||
else {
|
||||
return undefined;
|
||||
|
@ -196,10 +195,18 @@ namespace Utils {
|
|||
}
|
||||
|
||||
export class MockParseConfigHost extends VirtualFileSystem implements ts.ParseConfigHost {
|
||||
constructor(currentDirectory: string, ignoreCase: boolean, files: string[]) {
|
||||
constructor(currentDirectory: string, ignoreCase: boolean, files: ts.MapLike<string> | string[]) {
|
||||
super(currentDirectory, ignoreCase);
|
||||
for (const file of files) {
|
||||
this.addFile(file);
|
||||
const fileNames = (files instanceof Array) ? files : ts.getOwnKeys(files);
|
||||
for (const file of fileNames) {
|
||||
this.addFile(file, (files as any)[file]);
|
||||
}
|
||||
}
|
||||
|
||||
readFile(path: string): string {
|
||||
const value = this.traversePath(path);
|
||||
if (value && value.isFile()) {
|
||||
return value.content;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in a new issue