diff --git a/src/compiler/commandLineParser.ts b/src/compiler/commandLineParser.ts index 5a6ee2ec619ab..db700a9c57aac 100644 --- a/src/compiler/commandLineParser.ts +++ b/src/compiler/commandLineParser.ts @@ -164,6 +164,13 @@ namespace ts { category: Diagnostics.Command_line_Options, description: Diagnostics.Stylize_errors_and_messages_using_color_and_context_experimental }, + { + name: "showConfig", + type: "boolean", + category: Diagnostics.Command_line_Options, + isCommandLineOnly: true, + description: Diagnostics.Print_the_final_configuration_instead_of_building + }, // Basic { @@ -1654,72 +1661,146 @@ namespace ts { } /** - * Generate tsconfig configuration when running command line "--init" - * @param options commandlineOptions to be generated into tsconfig.json - * @param fileNames array of filenames to be generated into tsconfig.json + * Generate an uncommented, complete tsconfig for use with "--showConfig" + * @param configParseResult options to be generated into tsconfig.json + * @param configFileName name of the parsed config file - output paths will be generated relative to this + * @param host provides current directory and case sensitivity services */ - /* @internal */ - export function generateTSConfig(options: CompilerOptions, fileNames: ReadonlyArray, newLine: string): string { - const compilerOptions = extend(options, defaultInitCompilerOptions); - const compilerOptionsMap = serializeCompilerOptions(compilerOptions); - return writeConfigurations(); + /** @internal */ + export function convertToTSConfig(configParseResult: ParsedCommandLine, configFileName: string, host: { getCurrentDirectory(): string, useCaseSensitiveFileNames: boolean }): object { + const getCanonicalFileName = createGetCanonicalFileName(host.useCaseSensitiveFileNames); + const files = map( + filter( + configParseResult.fileNames, + !configParseResult.configFileSpecs ? _ => false : matchesSpecs( + configFileName, + configParseResult.configFileSpecs.validatedIncludeSpecs, + configParseResult.configFileSpecs.validatedExcludeSpecs + ) + ), + f => getRelativePathFromFile(getNormalizedAbsolutePath(configFileName, host.getCurrentDirectory()), f, getCanonicalFileName) + ); + const optionMap = serializeCompilerOptions(configParseResult.options, { configFilePath: getNormalizedAbsolutePath(configFileName, host.getCurrentDirectory()), useCaseSensitiveFileNames: host.useCaseSensitiveFileNames }); + const config = { + compilerOptions: { + ...arrayFrom(optionMap.entries()).reduce((prev, cur) => ({ ...prev, [cur[0]]: cur[1] }), {}), + showConfig: undefined, + configFile: undefined, + configFilePath: undefined, + help: undefined, + init: undefined, + listFiles: undefined, + listEmittedFiles: undefined, + project: undefined, + }, + references: map(configParseResult.projectReferences, r => ({ ...r, path: r.originalPath, originalPath: undefined })), + files: length(files) ? files : undefined, + ...(configParseResult.configFileSpecs ? { + include: filterSameAsDefaultInclude(configParseResult.configFileSpecs.validatedIncludeSpecs), + exclude: configParseResult.configFileSpecs.validatedExcludeSpecs + } : {}), + compilerOnSave: !!configParseResult.compileOnSave ? true : undefined + }; + return config; + } - function getCustomTypeMapOfCommandLineOption(optionDefinition: CommandLineOption): Map | undefined { - if (optionDefinition.type === "string" || optionDefinition.type === "number" || optionDefinition.type === "boolean") { - // this is of a type CommandLineOptionOfPrimitiveType - return undefined; - } - else if (optionDefinition.type === "list") { - return getCustomTypeMapOfCommandLineOption(optionDefinition.element); - } - else { - return (optionDefinition).type; + function filterSameAsDefaultInclude(specs: ReadonlyArray | undefined) { + if (!length(specs)) return undefined; + if (length(specs) !== 1) return specs; + if (specs![0] === "**/*") return undefined; + return specs; + } + + function matchesSpecs(path: string, includeSpecs: ReadonlyArray | undefined, excludeSpecs: ReadonlyArray | undefined): (path: string) => boolean { + if (!includeSpecs) return _ => false; + const patterns = getFileMatcherPatterns(path, excludeSpecs, includeSpecs, sys.useCaseSensitiveFileNames, sys.getCurrentDirectory()); + const excludeRe = patterns.excludePattern && getRegexFromPattern(patterns.excludePattern, sys.useCaseSensitiveFileNames); + const includeRe = patterns.includeFilePattern && getRegexFromPattern(patterns.includeFilePattern, sys.useCaseSensitiveFileNames); + if (includeRe) { + if (excludeRe) { + return path => includeRe.test(path) && !excludeRe.test(path); } + return path => includeRe.test(path); } + if (excludeRe) { + return path => !excludeRe.test(path); + } + return _ => false; + } - function getNameOfCompilerOptionValue(value: CompilerOptionsValue, customTypeMap: Map): string | undefined { - // There is a typeMap associated with this command-line option so use it to map value back to its name - return forEachEntry(customTypeMap, (mapValue, key) => { - if (mapValue === value) { - return key; - } - }); + function getCustomTypeMapOfCommandLineOption(optionDefinition: CommandLineOption): Map | undefined { + if (optionDefinition.type === "string" || optionDefinition.type === "number" || optionDefinition.type === "boolean") { + // this is of a type CommandLineOptionOfPrimitiveType + return undefined; + } + else if (optionDefinition.type === "list") { + return getCustomTypeMapOfCommandLineOption(optionDefinition.element); + } + else { + return (optionDefinition).type; } + } + + function getNameOfCompilerOptionValue(value: CompilerOptionsValue, customTypeMap: Map): string | undefined { + // There is a typeMap associated with this command-line option so use it to map value back to its name + return forEachEntry(customTypeMap, (mapValue, key) => { + if (mapValue === value) { + return key; + } + }); + } - function serializeCompilerOptions(options: CompilerOptions): Map { - const result = createMap(); - const optionsNameMap = getOptionNameMap().optionNameMap; + function serializeCompilerOptions(options: CompilerOptions, pathOptions?: { configFilePath: string, useCaseSensitiveFileNames: boolean }): Map { + const result = createMap(); + const optionsNameMap = getOptionNameMap().optionNameMap; + const getCanonicalFileName = pathOptions && createGetCanonicalFileName(pathOptions.useCaseSensitiveFileNames); - for (const name in options) { - if (hasProperty(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 - if (optionsNameMap.has(name) && optionsNameMap.get(name)!.category === Diagnostics.Command_line_Options) { - continue; - } - const value = options[name]; - const optionDefinition = optionsNameMap.get(name.toLowerCase()); - if (optionDefinition) { - const customTypeMap = getCustomTypeMapOfCommandLineOption(optionDefinition); - if (!customTypeMap) { - // There is no map associated with this compiler option then use the value as-is - // This is the case if the value is expect to be string, number, boolean or list of string + for (const name in options) { + if (hasProperty(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 + if (optionsNameMap.has(name) && optionsNameMap.get(name)!.category === Diagnostics.Command_line_Options) { + continue; + } + const value = options[name]; + const optionDefinition = optionsNameMap.get(name.toLowerCase()); + if (optionDefinition) { + const customTypeMap = getCustomTypeMapOfCommandLineOption(optionDefinition); + if (!customTypeMap) { + // There is no map associated with this compiler option then use the value as-is + // This is the case if the value is expect to be string, number, boolean or list of string + if (pathOptions && optionDefinition.isFilePath) { + result.set(name, getRelativePathFromFile(pathOptions.configFilePath, getNormalizedAbsolutePath(value as string, getDirectoryPath(pathOptions.configFilePath)), getCanonicalFileName!)); + } + else { result.set(name, value); } + } + else { + if (optionDefinition.type === "list") { + result.set(name, (value as ReadonlyArray).map(element => getNameOfCompilerOptionValue(element, customTypeMap)!)); // TODO: GH#18217 + } else { - if (optionDefinition.type === "list") { - result.set(name, (value as ReadonlyArray).map(element => getNameOfCompilerOptionValue(element, customTypeMap)!)); // TODO: GH#18217 - } - else { - // There is a typeMap associated with this command-line option so use it to map value back to its name - result.set(name, getNameOfCompilerOptionValue(value, customTypeMap)); - } + // There is a typeMap associated with this command-line option so use it to map value back to its name + result.set(name, getNameOfCompilerOptionValue(value, customTypeMap)); } } } } - return result; } + return result; + } + + /** + * Generate tsconfig configuration when running command line "--init" + * @param options commandlineOptions to be generated into tsconfig.json + * @param fileNames array of filenames to be generated into tsconfig.json + */ + /* @internal */ + export function generateTSConfig(options: CompilerOptions, fileNames: ReadonlyArray, newLine: string): string { + const compilerOptions = extend(options, defaultInitCompilerOptions); + const compilerOptionsMap = serializeCompilerOptions(compilerOptions); + return writeConfigurations(); function getDefaultValueForOption(option: CommandLineOption) { switch (option.type) { diff --git a/src/compiler/diagnosticMessages.json b/src/compiler/diagnosticMessages.json index 558c45bf2af13..71256150515d2 100644 --- a/src/compiler/diagnosticMessages.json +++ b/src/compiler/diagnosticMessages.json @@ -1007,6 +1007,10 @@ "category": "Error", "code": 1349 }, + "Print the final configuration instead of building.": { + "category": "Message", + "code": 1350 + }, "Duplicate identifier '{0}'.": { "category": "Error", diff --git a/src/compiler/types.ts b/src/compiler/types.ts index 1c2009bc19f78..86174bc42c1de 100644 --- a/src/compiler/types.ts +++ b/src/compiler/types.ts @@ -4544,6 +4544,7 @@ namespace ts { /*@internal*/ version?: boolean; /*@internal*/ watch?: boolean; esModuleInterop?: boolean; + /* @internal */ showConfig?: boolean; [option: string]: CompilerOptionsValue | TsConfigSourceFile | undefined; } diff --git a/src/testRunner/tsconfig.json b/src/testRunner/tsconfig.json index 42edf83996495..af25aba408dd1 100644 --- a/src/testRunner/tsconfig.json +++ b/src/testRunner/tsconfig.json @@ -75,6 +75,7 @@ "unittests/reuseProgramStructure.ts", "unittests/session.ts", "unittests/semver.ts", + "unittests/showConfig.ts", "unittests/symbolWalker.ts", "unittests/telemetry.ts", "unittests/textChanges.ts", diff --git a/src/testRunner/unittests/showConfig.ts b/src/testRunner/unittests/showConfig.ts new file mode 100644 index 0000000000000..a2b5bb1025880 --- /dev/null +++ b/src/testRunner/unittests/showConfig.ts @@ -0,0 +1,34 @@ +namespace ts { + describe("showTSConfig", () => { + function showTSConfigCorrectly(name: string, commandLinesArgs: string[]) { + describe(name, () => { + const commandLine = parseCommandLine(commandLinesArgs); + const initResult = convertToTSConfig(commandLine, `/${name}/tsconfig.json`, { getCurrentDirectory() { return `/${name}`; }, useCaseSensitiveFileNames: true }); + const outputFileName = `showConfig/${name.replace(/[^a-z0-9\-. ]/ig, "")}/tsconfig.json`; + + it(`Correct output for ${outputFileName}`, () => { + // tslint:disable-next-line:no-null-keyword + Harness.Baseline.runBaseline(outputFileName, JSON.stringify(initResult, null, 4) + "\n"); + }); + }); + } + + showTSConfigCorrectly("Default initialized TSConfig", ["--showConfig"]); + + showTSConfigCorrectly("Show TSConfig with files options", ["--showConfig", "file0.st", "file1.ts", "file2.ts"]); + + showTSConfigCorrectly("Show TSConfig with boolean value compiler options", ["--showConfig", "--noUnusedLocals"]); + + showTSConfigCorrectly("Show TSConfig with enum value compiler options", ["--showConfig", "--target", "es5", "--jsx", "react"]); + + showTSConfigCorrectly("Show TSConfig with list compiler options", ["--showConfig", "--types", "jquery,mocha"]); + + showTSConfigCorrectly("Show TSConfig with list compiler options with enum value", ["--showConfig", "--lib", "es5,es2015.core"]); + + showTSConfigCorrectly("Show TSConfig with incorrect compiler option", ["--showConfig", "--someNonExistOption"]); + + showTSConfigCorrectly("Show TSConfig with incorrect compiler option value", ["--showConfig", "--lib", "nonExistLib,es5,es2015.promise"]); + + showTSConfigCorrectly("Show TSConfig with advanced options", ["--showConfig", "--declaration", "--declarationDir", "lib", "--skipLibCheck", "--noErrorTruncation"]); + }); +} \ No newline at end of file diff --git a/src/tsc/tsc.ts b/src/tsc/tsc.ts index ec2d029e60a01..549f79f9025a6 100644 --- a/src/tsc/tsc.ts +++ b/src/tsc/tsc.ts @@ -132,6 +132,11 @@ namespace ts { const commandLineOptions = commandLine.options; if (configFileName) { const configParseResult = parseConfigFileWithSystem(configFileName, commandLineOptions, sys, reportDiagnostic)!; // TODO: GH#18217 + if (commandLineOptions.showConfig) { + // tslint:disable-next-line:no-null-keyword + sys.write(JSON.stringify(convertToTSConfig(configParseResult, configFileName, sys), null, 4) + sys.newLine); + return sys.exit(ExitStatus.Success); + } updateReportDiagnostic(configParseResult.options); if (isWatchSet(configParseResult.options)) { reportWatchModeWithoutSysSupport(); @@ -142,6 +147,11 @@ namespace ts { } } else { + if (commandLineOptions.showConfig) { + // tslint:disable-next-line:no-null-keyword + sys.write(JSON.stringify(convertToTSConfig(commandLine, combinePaths(sys.getCurrentDirectory(), "tsconfig.json"), sys), null, 4) + sys.newLine); + return sys.exit(ExitStatus.Success); + } updateReportDiagnostic(commandLineOptions); if (isWatchSet(commandLineOptions)) { reportWatchModeWithoutSysSupport(); diff --git a/tests/baselines/reference/showConfig/Default initialized TSConfig/tsconfig.json b/tests/baselines/reference/showConfig/Default initialized TSConfig/tsconfig.json new file mode 100644 index 0000000000000..cd727e8ccdc88 --- /dev/null +++ b/tests/baselines/reference/showConfig/Default initialized TSConfig/tsconfig.json @@ -0,0 +1,3 @@ +{ + "compilerOptions": {} +} diff --git a/tests/baselines/reference/showConfig/Show TSConfig with advanced options/tsconfig.json b/tests/baselines/reference/showConfig/Show TSConfig with advanced options/tsconfig.json new file mode 100644 index 0000000000000..6dbccf64bb9c4 --- /dev/null +++ b/tests/baselines/reference/showConfig/Show TSConfig with advanced options/tsconfig.json @@ -0,0 +1,8 @@ +{ + "compilerOptions": { + "declaration": true, + "declarationDir": "./lib", + "skipLibCheck": true, + "noErrorTruncation": true + } +} diff --git a/tests/baselines/reference/showConfig/Show TSConfig with boolean value compiler options/tsconfig.json b/tests/baselines/reference/showConfig/Show TSConfig with boolean value compiler options/tsconfig.json new file mode 100644 index 0000000000000..650a347f89585 --- /dev/null +++ b/tests/baselines/reference/showConfig/Show TSConfig with boolean value compiler options/tsconfig.json @@ -0,0 +1,5 @@ +{ + "compilerOptions": { + "noUnusedLocals": true + } +} diff --git a/tests/baselines/reference/showConfig/Show TSConfig with enum value compiler options/tsconfig.json b/tests/baselines/reference/showConfig/Show TSConfig with enum value compiler options/tsconfig.json new file mode 100644 index 0000000000000..0052b1327f123 --- /dev/null +++ b/tests/baselines/reference/showConfig/Show TSConfig with enum value compiler options/tsconfig.json @@ -0,0 +1,6 @@ +{ + "compilerOptions": { + "target": "es5", + "jsx": "react" + } +} diff --git a/tests/baselines/reference/showConfig/Show TSConfig with files options/tsconfig.json b/tests/baselines/reference/showConfig/Show TSConfig with files options/tsconfig.json new file mode 100644 index 0000000000000..cd727e8ccdc88 --- /dev/null +++ b/tests/baselines/reference/showConfig/Show TSConfig with files options/tsconfig.json @@ -0,0 +1,3 @@ +{ + "compilerOptions": {} +} diff --git a/tests/baselines/reference/showConfig/Show TSConfig with incorrect compiler option value/tsconfig.json b/tests/baselines/reference/showConfig/Show TSConfig with incorrect compiler option value/tsconfig.json new file mode 100644 index 0000000000000..c75e5d4ae1d96 --- /dev/null +++ b/tests/baselines/reference/showConfig/Show TSConfig with incorrect compiler option value/tsconfig.json @@ -0,0 +1,8 @@ +{ + "compilerOptions": { + "lib": [ + "es5", + "es2015.promise" + ] + } +} diff --git a/tests/baselines/reference/showConfig/Show TSConfig with incorrect compiler option/tsconfig.json b/tests/baselines/reference/showConfig/Show TSConfig with incorrect compiler option/tsconfig.json new file mode 100644 index 0000000000000..cd727e8ccdc88 --- /dev/null +++ b/tests/baselines/reference/showConfig/Show TSConfig with incorrect compiler option/tsconfig.json @@ -0,0 +1,3 @@ +{ + "compilerOptions": {} +} diff --git a/tests/baselines/reference/showConfig/Show TSConfig with list compiler options with enum value/tsconfig.json b/tests/baselines/reference/showConfig/Show TSConfig with list compiler options with enum value/tsconfig.json new file mode 100644 index 0000000000000..6da42b43907bc --- /dev/null +++ b/tests/baselines/reference/showConfig/Show TSConfig with list compiler options with enum value/tsconfig.json @@ -0,0 +1,8 @@ +{ + "compilerOptions": { + "lib": [ + "es5", + "es2015.core" + ] + } +} diff --git a/tests/baselines/reference/showConfig/Show TSConfig with list compiler options/tsconfig.json b/tests/baselines/reference/showConfig/Show TSConfig with list compiler options/tsconfig.json new file mode 100644 index 0000000000000..7b5da8e7d3c71 --- /dev/null +++ b/tests/baselines/reference/showConfig/Show TSConfig with list compiler options/tsconfig.json @@ -0,0 +1,8 @@ +{ + "compilerOptions": { + "types": [ + "jquery", + "mocha" + ] + } +}