-
Notifications
You must be signed in to change notification settings - Fork 12.6k
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
Configuration Inheritance #9941
Changes from 1 commit
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -700,12 +700,54 @@ 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 if (typeof json["extends"] === "object" && json["extends"].length) { | ||
for (const name of json["extends"]) { | ||
const [tempinclude, tempexclude, tempfiles, tempBase]: [string[], string[], string[], CompilerOptions] = (tryExtendsName(name) || [include, exclude, files, baseOptions]); | ||
include = tempinclude || include; | ||
exclude = tempexclude || exclude; | ||
files = tempfiles || files; | ||
baseOptions = assign({}, baseOptions, tempBase); | ||
} | ||
} | ||
else { | ||
errors.push(createCompilerDiagnostic(Diagnostics.Compiler_option_0_requires_a_value_of_type_1, "extends", "string or 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); | ||
|
@@ -719,6 +761,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")) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. kinda strange that you force them to write There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The reasoning for forbidding modules identifier like paths is, as described in the original issue, to reserve the form for potentially loading configs from |
||
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")) { | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -171,6 +171,20 @@ namespace ts { | |
return result; | ||
} | ||
|
||
export function mapObject<T, U>(object: Map<T>, f: (key: string, x: T) => [string, U]): Map<U> { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. jsdoc would be nice here, specifically calling out what happens when keys are mapped to the same key (looks like last one wins and earlier ones are discarded) |
||
let result: Map<U> = {}; | ||
if (object) { | ||
result = {}; | ||
for (const v of getKeys(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; | ||
|
@@ -357,6 +371,20 @@ namespace ts { | |
return result; | ||
} | ||
|
||
export function assign<T1 extends Map<{}>, T2, T3>(t: T1, arg1: T2, arg2: T3): T1 & T2 & T3; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
The biggest difference is that when property names conflict, |
||
export function assign<T1 extends Map<{}>, T2>(t: T1, arg1: T2): T1 & T2; | ||
export function assign<T1 extends Map<{}>>(t: T1, ...args: any[]): any; | ||
export function assign<T1 extends Map<{}>>(t: T1, ...args: any[]) { | ||
for (const arg of args) { | ||
for (const p of getKeys(arg)) { | ||
if (hasProperty(arg, p)) { | ||
t[p] = arg[p]; | ||
} | ||
} | ||
} | ||
return t; | ||
} | ||
|
||
export function forEachValue<T, U>(map: Map<T>, callback: (value: T) => U): U { | ||
let result: U; | ||
for (const id in map) { | ||
|
@@ -941,7 +969,7 @@ namespace ts { | |
* [^./] # matches everything up to the first . character (excluding directory seperators) | ||
* (\\.(?!min\\.js$))? # matches . characters but not if they are part of the .min.js file extension | ||
*/ | ||
const singleAsteriskRegexFragmentFiles = "([^./]|(\\.(?!min\\.js$))?)*"; | ||
const singleAsteriskRegexFragmentFiles = "([^./]|(\\.(?!min\\.js$))?)*"; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. what's the diff here? |
||
const singleAsteriskRegexFragmentOther = "[^/]*"; | ||
|
||
export function getRegularExpressionForWildcard(specs: string[], basePath: string, usage: "files" | "directories" | "exclude") { | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -1693,6 +1693,8 @@ namespace ts { | |
* @param path The path to test. | ||
*/ | ||
fileExists(path: string): boolean; | ||
|
||
readFile(path: string): string; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. we need to document this as an API breaking change. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @alexeagle, @chuckjaz, @basarat, @adidahiya, @ivogabe, @jbrantly, and @chancancode this is a breaking change in the API, it adds a requirement for a new method There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I've been just passing in There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ❤️ @blakeembrey (ts-node / tsconfig) @cartant / @smrq (tsify) @johnnyreilly (also on ts-loader) @sebastian-lenz (typedoc) Sorry for the mention, thought I'd let you know. Feel free to ignore 💟 🌹 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This breaks us in at least one place: https://github.com/angular/angular/blob/master/tools/@angular/tsc-wrapped/src/tsc.ts#L70 What is the timing of this change? Will it be part of 2.0? If so this will be a challenge for us. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @chuckjaz It looks like you have a readfile method available, you just dont pass it in as part of the host, so it looks easy to update for future versions. Actually, is there a reason you capture the methods on a new object, rather than just passing in There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
no, this is a TS 2.1 feature. but once checked in it will start showing up in There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @mhegazy If it is part of 2.1 on @weswigham This can be better but we do this, in general, to make testing easier. |
||
} | ||
|
||
export interface WriteFileCallback { | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
can you expand on the scenario here? like
[es6.json, module.json, fallthrough.json]
this seems too granular to me.