From aefccb69b01a35b8ee8d36c3e3eeabe9e98bc579 Mon Sep 17 00:00:00 2001 From: Sheetal Nandi Date: Thu, 3 Dec 2020 15:21:44 -0800 Subject: [PATCH] Fix the output file names api to use the correct common source directory Fixes #41801 and #41780 --- src/compiler/emitter.ts | 74 ++++++++++++++----- src/compiler/program.ts | 45 +++++------ .../when-rootDir-is-not-specified.js | 7 +- 3 files changed, 76 insertions(+), 50 deletions(-) diff --git a/src/compiler/emitter.ts b/src/compiler/emitter.ts index 83a0a7ed9d845..353a45db739a7 100644 --- a/src/compiler/emitter.ts +++ b/src/compiler/emitter.ts @@ -130,36 +130,32 @@ namespace ts { return Extension.Js; } - function rootDirOfOptions(configFile: ParsedCommandLine) { - return configFile.options.rootDir || getDirectoryPath(Debug.checkDefined(configFile.options.configFilePath)); - } - - function getOutputPathWithoutChangingExt(inputFileName: string, configFile: ParsedCommandLine, ignoreCase: boolean, outputDir: string | undefined) { + function getOutputPathWithoutChangingExt(inputFileName: string, configFile: ParsedCommandLine, ignoreCase: boolean, outputDir: string | undefined, getCommonSourceDirectory?: () => string) { return outputDir ? resolvePath( outputDir, - getRelativePathFromDirectory(rootDirOfOptions(configFile), inputFileName, ignoreCase) + getRelativePathFromDirectory(getCommonSourceDirectory ? getCommonSourceDirectory() : getCommonSourceDirectoryOfConfig(configFile, ignoreCase), inputFileName, ignoreCase) ) : inputFileName; } /* @internal */ - export function getOutputDeclarationFileName(inputFileName: string, configFile: ParsedCommandLine, ignoreCase: boolean) { + export function getOutputDeclarationFileName(inputFileName: string, configFile: ParsedCommandLine, ignoreCase: boolean, getCommonSourceDirectory?: () => string) { Debug.assert(!fileExtensionIs(inputFileName, Extension.Dts) && !fileExtensionIs(inputFileName, Extension.Json)); return changeExtension( - getOutputPathWithoutChangingExt(inputFileName, configFile, ignoreCase, configFile.options.declarationDir || configFile.options.outDir), + getOutputPathWithoutChangingExt(inputFileName, configFile, ignoreCase, configFile.options.declarationDir || configFile.options.outDir, getCommonSourceDirectory), Extension.Dts ); } - function getOutputJSFileName(inputFileName: string, configFile: ParsedCommandLine, ignoreCase: boolean) { + function getOutputJSFileName(inputFileName: string, configFile: ParsedCommandLine, ignoreCase: boolean, getCommonSourceDirectory?: () => string) { if (configFile.options.emitDeclarationOnly) return undefined; const isJsonFile = fileExtensionIs(inputFileName, Extension.Json); const outputFileName = changeExtension( - getOutputPathWithoutChangingExt(inputFileName, configFile, ignoreCase, configFile.options.outDir), + getOutputPathWithoutChangingExt(inputFileName, configFile, ignoreCase, configFile.options.outDir, getCommonSourceDirectory), isJsonFile ? Extension.Json : - fileExtensionIs(inputFileName, Extension.Tsx) && configFile.options.jsx === JsxEmit.Preserve ? + configFile.options.jsx === JsxEmit.Preserve && (fileExtensionIs(inputFileName, Extension.Tsx) || fileExtensionIs(inputFileName, Extension.Jsx)) ? Extension.Jsx : Extension.Js ); @@ -190,16 +186,16 @@ namespace ts { addOutput(buildInfoPath); } - function getOwnOutputFileNames(configFile: ParsedCommandLine, inputFileName: string, ignoreCase: boolean, addOutput: ReturnType["addOutput"]) { + function getOwnOutputFileNames(configFile: ParsedCommandLine, inputFileName: string, ignoreCase: boolean, addOutput: ReturnType["addOutput"], getCommonSourceDirectory?: () => string) { if (fileExtensionIs(inputFileName, Extension.Dts)) return; - const js = getOutputJSFileName(inputFileName, configFile, ignoreCase); + const js = getOutputJSFileName(inputFileName, configFile, ignoreCase, getCommonSourceDirectory); addOutput(js); if (fileExtensionIs(inputFileName, Extension.Json)) return; if (js && configFile.options.sourceMap) { addOutput(`${js}.map`); } if (getEmitDeclarations(configFile.options)) { - const dts = getOutputDeclarationFileName(inputFileName, configFile, ignoreCase); + const dts = getOutputDeclarationFileName(inputFileName, configFile, ignoreCase, getCommonSourceDirectory); addOutput(dts); if (configFile.options.declarationMap) { addOutput(`${dts}.map`); @@ -207,6 +203,48 @@ namespace ts { } } + /*@internal*/ + export function getCommonSourceDirectory( + options: CompilerOptions, + emittedFiles: () => readonly string[], + currentDirectory: string, + getCanonicalFileName: GetCanonicalFileName, + checkSourceFilesBelongToPath?: (commonSourceDirectory: string) => void + ): string { + let commonSourceDirectory; + if (options.rootDir) { + // If a rootDir is specified use it as the commonSourceDirectory + commonSourceDirectory = getNormalizedAbsolutePath(options.rootDir, currentDirectory); + checkSourceFilesBelongToPath?.(commonSourceDirectory); + } + else if (options.composite && options.configFilePath) { + // Project compilations never infer their root from the input source paths + commonSourceDirectory = getDirectoryPath(normalizeSlashes(options.configFilePath)); + checkSourceFilesBelongToPath?.(commonSourceDirectory); + } + else { + commonSourceDirectory = computeCommonSourceDirectoryOfFilenames(emittedFiles(), currentDirectory, getCanonicalFileName); + } + + if (commonSourceDirectory && commonSourceDirectory[commonSourceDirectory.length - 1] !== directorySeparator) { + // Make sure directory path ends with directory separator so this string can directly + // used to replace with "" to get the relative path of the source file and the relative path doesn't + // start with / making it rooted path + commonSourceDirectory += directorySeparator; + } + return commonSourceDirectory; + } + + /*@internal*/ + export function getCommonSourceDirectoryOfConfig({ options, fileNames }: ParsedCommandLine, ignoreCase: boolean): string { + return getCommonSourceDirectory( + options, + () => filter(fileNames, file => !(options.noEmitForJsFiles && fileExtensionIsOneOf(file, supportedJSExtensions)) && !fileExtensionIs(file, Extension.Dts)), + getDirectoryPath(normalizeSlashes(Debug.checkDefined(options.configFilePath))), + createGetCanonicalFileName(!ignoreCase) + ); + } + /*@internal*/ export function getAllProjectOutputs(configFile: ParsedCommandLine, ignoreCase: boolean): readonly string[] { const { addOutput, getOutputs } = createAddOutput(); @@ -214,8 +252,9 @@ namespace ts { getSingleOutputFileNames(configFile, addOutput); } else { + const getCommonSourceDirectory = memoize(() => getCommonSourceDirectoryOfConfig(configFile, ignoreCase)); for (const inputFileName of configFile.fileNames) { - getOwnOutputFileNames(configFile, inputFileName, ignoreCase, addOutput); + getOwnOutputFileNames(configFile, inputFileName, ignoreCase, addOutput, getCommonSourceDirectory); } addOutput(getTsBuildInfoEmitOutputFilePath(configFile.options)); } @@ -242,13 +281,14 @@ namespace ts { return Debug.checkDefined(jsFilePath, `project ${configFile.options.configFilePath} expected to have at least one output`); } + const getCommonSourceDirectory = memoize(() => getCommonSourceDirectoryOfConfig(configFile, ignoreCase)); for (const inputFileName of configFile.fileNames) { if (fileExtensionIs(inputFileName, Extension.Dts)) continue; - const jsFilePath = getOutputJSFileName(inputFileName, configFile, ignoreCase); + const jsFilePath = getOutputJSFileName(inputFileName, configFile, ignoreCase, getCommonSourceDirectory); if (jsFilePath) return jsFilePath; if (fileExtensionIs(inputFileName, Extension.Json)) continue; if (getEmitDeclarations(configFile.options)) { - return getOutputDeclarationFileName(inputFileName, configFile, ignoreCase); + return getOutputDeclarationFileName(inputFileName, configFile, ignoreCase, getCommonSourceDirectory); } } const buildInfoPath = getTsBuildInfoEmitOutputFilePath(configFile.options); diff --git a/src/compiler/program.ts b/src/compiler/program.ts index d3cbb6a940b4e..bd668768ca25a 100644 --- a/src/compiler/program.ts +++ b/src/compiler/program.ts @@ -13,7 +13,7 @@ namespace ts { } /* @internal */ - export function computeCommonSourceDirectoryOfFilenames(fileNames: string[], currentDirectory: string, getCanonicalFileName: GetCanonicalFileName): string { + export function computeCommonSourceDirectoryOfFilenames(fileNames: readonly string[], currentDirectory: string, getCanonicalFileName: GetCanonicalFileName): string { let commonPathComponents: string[] | undefined; const failed = forEach(fileNames, sourceFile => { // Each file contributes into common source file path @@ -899,9 +899,15 @@ namespace ts { processSourceFile(changeExtension(out, ".d.ts"), /*isDefaultLib*/ false, /*ignoreNoDefaultLib*/ false, /*packageId*/ undefined); } else if (getEmitModuleKind(parsedRef.commandLine.options) === ModuleKind.None) { + const getCommonSourceDirectory = memoize(() => getCommonSourceDirectoryOfConfig(parsedRef.commandLine, !host.useCaseSensitiveFileNames())); for (const fileName of parsedRef.commandLine.fileNames) { if (!fileExtensionIs(fileName, Extension.Dts) && !fileExtensionIs(fileName, Extension.Json)) { - processSourceFile(getOutputDeclarationFileName(fileName, parsedRef.commandLine, !host.useCaseSensitiveFileNames()), /*isDefaultLib*/ false, /*ignoreNoDefaultLib*/ false, /*packageId*/ undefined); + processSourceFile( + getOutputDeclarationFileName(fileName, parsedRef.commandLine, !host.useCaseSensitiveFileNames(), getCommonSourceDirectory), + /*isDefaultLib*/ false, + /*ignoreNoDefaultLib*/ false, + /*packageId*/ undefined + ); } } } @@ -1127,26 +1133,13 @@ namespace ts { function getCommonSourceDirectory() { if (commonSourceDirectory === undefined) { const emittedFiles = filter(files, file => sourceFileMayBeEmitted(file, program)); - if (options.rootDir) { - // If a rootDir is specified use it as the commonSourceDirectory - commonSourceDirectory = getNormalizedAbsolutePath(options.rootDir, currentDirectory); - checkSourceFilesBelongToPath(emittedFiles, commonSourceDirectory); - } - else if (options.composite && options.configFilePath) { - // Project compilations never infer their root from the input source paths - commonSourceDirectory = getDirectoryPath(normalizeSlashes(options.configFilePath)); - checkSourceFilesBelongToPath(emittedFiles, commonSourceDirectory); - } - else { - commonSourceDirectory = computeCommonSourceDirectory(emittedFiles); - } - - if (commonSourceDirectory && commonSourceDirectory[commonSourceDirectory.length - 1] !== directorySeparator) { - // Make sure directory path ends with directory separator so this string can directly - // used to replace with "" to get the relative path of the source file and the relative path doesn't - // start with / making it rooted path - commonSourceDirectory += directorySeparator; - } + commonSourceDirectory = ts.getCommonSourceDirectory( + options, + () => mapDefined(emittedFiles, file => file.isDeclarationFile ? undefined : file.fileName), + currentDirectory, + getCanonicalFileName, + commonSourceDirectory => checkSourceFilesBelongToPath(emittedFiles, commonSourceDirectory) + ); } return commonSourceDirectory; } @@ -2708,9 +2701,10 @@ namespace ts { mapFromToProjectReferenceRedirectSource!.set(toPath(outputDts), true); } else { + const getCommonSourceDirectory = memoize(() => getCommonSourceDirectoryOfConfig(resolvedRef.commandLine, !host.useCaseSensitiveFileNames())); forEach(resolvedRef.commandLine.fileNames, fileName => { if (!fileExtensionIs(fileName, Extension.Dts) && !fileExtensionIs(fileName, Extension.Json)) { - const outputDts = getOutputDeclarationFileName(fileName, resolvedRef.commandLine, host.useCaseSensitiveFileNames()); + const outputDts = getOutputDeclarationFileName(fileName, resolvedRef.commandLine, !host.useCaseSensitiveFileNames(), getCommonSourceDirectory); mapFromToProjectReferenceRedirectSource!.set(toPath(outputDts), fileName); } }); @@ -2974,11 +2968,6 @@ namespace ts { } } - function computeCommonSourceDirectory(sourceFiles: SourceFile[]): string { - const fileNames = mapDefined(sourceFiles, file => file.isDeclarationFile ? undefined : file.fileName); - return computeCommonSourceDirectoryOfFilenames(fileNames, currentDirectory, getCanonicalFileName); - } - function checkSourceFilesBelongToPath(sourceFiles: readonly SourceFile[], rootDirectory: string): boolean { let allFilesBelongToPath = true; const absoluteRootDirectoryPath = host.getCanonicalFileName(getNormalizedAbsolutePath(rootDirectory, currentDirectory)); diff --git a/tests/baselines/reference/tsbuild/outputPaths/initial-build/when-rootDir-is-not-specified.js b/tests/baselines/reference/tsbuild/outputPaths/initial-build/when-rootDir-is-not-specified.js index e957d7fb2373d..11648d4fe14c2 100644 --- a/tests/baselines/reference/tsbuild/outputPaths/initial-build/when-rootDir-is-not-specified.js +++ b/tests/baselines/reference/tsbuild/outputPaths/initial-build/when-rootDir-is-not-specified.js @@ -27,7 +27,7 @@ Output:: [12:01:00 AM] Projects in this build: * src/tsconfig.json -[12:01:00 AM] Project 'src/tsconfig.json' is out of date because output file 'src/dist/src/index.js' does not exist +[12:01:00 AM] Project 'src/tsconfig.json' is out of date because output file 'src/dist/index.js' does not exist [12:01:00 AM] Building project '/src/tsconfig.json'... @@ -52,14 +52,11 @@ Output:: [12:04:00 AM] Projects in this build: * src/tsconfig.json -[12:04:00 AM] Project 'src/tsconfig.json' is out of date because output file 'src/dist/src/index.js' does not exist - -[12:04:00 AM] Building project '/src/tsconfig.json'... +[12:04:00 AM] Project 'src/tsconfig.json' is up to date because newest input 'src/src/index.ts' is older than oldest output 'src/dist/index.js' exitCode:: ExitStatus.Success -//// [/src/dist/index.js] file written with same contents Change:: Normal build without change, that does not block emit on error to show files that get emitted