diff --git a/src/compiler/emitter.ts b/src/compiler/emitter.ts index c60fd927872..3f823833451 100644 --- a/src/compiler/emitter.ts +++ b/src/compiler/emitter.ts @@ -664,6 +664,21 @@ export function getCommonSourceDirectory( return commonSourceDirectory; } +export function getCommonSourceDirectory60(options: CompilerOptions): string | undefined { + if (!options.rootDir && !options.composite && !options.outFile && options.configFilePath) { + // Project compilations never infer their root from the input source paths + let commonSourceDirectory = getDirectoryPath(normalizeSlashes(options.configFilePath)); + + 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( diff --git a/src/compiler/moduleNameResolver.ts b/src/compiler/moduleNameResolver.ts index 3fa7da5d5c9..a5183856daa 100644 --- a/src/compiler/moduleNameResolver.ts +++ b/src/compiler/moduleNameResolver.ts @@ -42,6 +42,7 @@ import { getBaseFileName, GetCanonicalFileName, getCommonSourceDirectory, + getCommonSourceDirectory60, getCompilerOptionValue, getDirectoryPath, GetEffectiveTypeRootsHost, @@ -80,6 +81,7 @@ import { normalizePath, normalizeSlashes, PackageId, + packageIdIsEqual, packageIdToString, ParsedPatterns, Path, @@ -148,6 +150,16 @@ function removeIgnoredPackageId(r: Resolved | undefined): PathAndExtension | und } } +function resolvedIsEqual(a: Resolved | undefined, b: Resolved | undefined) { + return a === b || + !!a && !!b && + a.path === b.path && + a.extension === b.extension && + packageIdIsEqual(a.packageId, b.packageId) && + a.originalPath === b.originalPath && + a.resolvedUsingTsExtension === b.resolvedUsingTsExtension; +} + /** Result of trying to resolve a module. */ interface Resolved { path: string; @@ -2932,23 +2944,46 @@ function getLoadModuleFromTargetExportOrImport(extensions: Extensions, state: Mo packagePath, )); } - for (const commonSourceDirGuess of commonSourceDirGuesses) { - const candidateDirectories = getOutputDirectoriesForBaseDirectory(commonSourceDirGuess); - for (const candidateDir of candidateDirectories) { - if (containsPath(candidateDir, finalPath, !useCaseSensitiveFileNames(state))) { - // The matched export is looking up something in either the out declaration or js dir, now map the written path back into the source dir and source extension - const pathFragment = finalPath.slice(candidateDir.length + 1); // +1 to also remove directory seperator - const possibleInputBase = combinePaths(commonSourceDirGuess, pathFragment); - const jsAndDtsExtensions = [Extension.Mjs, Extension.Cjs, Extension.Js, Extension.Json, Extension.Dmts, Extension.Dcts, Extension.Dts]; - for (const ext of jsAndDtsExtensions) { - if (fileExtensionIs(possibleInputBase, ext)) { - const inputExts = getPossibleOriginalInputExtensionForExtension(possibleInputBase); - for (const possibleExt of inputExts) { - if (!extensionIsOk(extensions, possibleExt)) continue; - const possibleInputWithInputExtension = changeAnyExtension(possibleInputBase, possibleExt, ext, !useCaseSensitiveFileNames(state)); - if (state.host.fileExists(possibleInputWithInputExtension)) { - return toSearchResult(withPackageId(scope, loadFileNameFromPackageJsonField(extensions, possibleInputWithInputExtension, /*packageJsonValue*/ undefined, /*onlyRecordFailures*/ false, state), state)); - } + const result = guessFromCommonDirs(commonSourceDirGuesses, finalPath); + const commonDir60 = toAbsolutePath(getCommonSourceDirectory60(state.compilerOptions)); + if (commonDir60) { + if (!arrayIsEqualTo(commonSourceDirGuesses, [commonDir60])) { + const result60 = guessFromCommonDirs([commonDir60], finalPath); + // Compare and if not same report -- and add made up diagnostics + if (!searchResultIsEqual(result, result60, resolvedIsEqual)) { + state.reportDiagnostic(createCompilerDiagnostic( + isImports + ? Diagnostics.The_project_root_is_ambiguous_but_is_required_to_resolve_import_map_entry_0_in_file_1_Supply_the_rootDir_compiler_option_to_disambiguate + : Diagnostics.The_project_root_is_ambiguous_but_is_required_to_resolve_export_map_entry_0_in_file_1_Supply_the_rootDir_compiler_option_to_disambiguate, + entry === "" ? "." : entry, // replace empty string with `.` - the reverse of the operation done when entries are built - so main entrypoint errors don't look weird + packagePath + "\nSheetal:: Change in behaviour: guessed " + commonSourceDirGuesses.join(", ") + " will be in 6.0::" + commonDir60 + + "\nResult " + JSON.stringify(result) + "\n Result.6.0: " + JSON.stringify(result60), + )); + } + } + } + return result; + } + return undefined; + } + + function guessFromCommonDirs(commonSourceDirGuesses: string[], finalPath: string) { + for (const commonSourceDirGuess of commonSourceDirGuesses) { + const candidateDirectories = getOutputDirectoriesForBaseDirectory(commonSourceDirGuess); + for (const candidateDir of candidateDirectories) { + if (containsPath(candidateDir, finalPath, !useCaseSensitiveFileNames(state))) { + // The matched export is looking up something in either the out declaration or js dir, now map the written path back into the source dir and source extension + const pathFragment = finalPath.slice(candidateDir.length + 1); // +1 to also remove directory seperator + const possibleInputBase = combinePaths(commonSourceDirGuess, pathFragment); + const jsAndDtsExtensions = [Extension.Mjs, Extension.Cjs, Extension.Js, Extension.Json, Extension.Dmts, Extension.Dcts, Extension.Dts]; + for (const ext of jsAndDtsExtensions) { + if (fileExtensionIs(possibleInputBase, ext)) { + const inputExts = getPossibleOriginalInputExtensionForExtension(possibleInputBase); + for (const possibleExt of inputExts) { + if (!extensionIsOk(extensions, possibleExt)) continue; + const possibleInputWithInputExtension = changeAnyExtension(possibleInputBase, possibleExt, ext, !useCaseSensitiveFileNames(state)); + if (state.host.fileExists(possibleInputWithInputExtension)) { + return toSearchResult(withPackageId(scope, loadFileNameFromPackageJsonField(extensions, possibleInputWithInputExtension, /*packageJsonValue*/ undefined, /*onlyRecordFailures*/ false, state), state)); } } } @@ -2956,7 +2991,6 @@ function getLoadModuleFromTargetExportOrImport(extensions: Extensions, state: Mo } } } - return undefined; function getOutputDirectoriesForBaseDirectory(commonSourceDirGuess: string) { // Config file ouput paths are processed to be relative to the host's current directory, while @@ -3416,3 +3450,7 @@ function useCaseSensitiveFileNames(state: ModuleResolutionState) { typeof state.host.useCaseSensitiveFileNames === "boolean" ? state.host.useCaseSensitiveFileNames : state.host.useCaseSensitiveFileNames(); } + +function searchResultIsEqual(a: SearchResult | undefined, b: SearchResult | undefined, compareValue: (a: T | undefined, b: T | undefined) => boolean) { + return a === b || !!a && !!b && compareValue(a.value, b.value); +} diff --git a/src/compiler/program.ts b/src/compiler/program.ts index 560b9e2c5d6..4e063716bc7 100644 --- a/src/compiler/program.ts +++ b/src/compiler/program.ts @@ -63,6 +63,8 @@ import { DiagnosticWithLocation, directorySeparator, DirectoryStructureHost, + emitDetachedComments, + emitFileNamesIsEqual, emitFiles, EmitHost, emitModuleKindIsNonNodeESM, @@ -110,6 +112,7 @@ import { getBaseFileName, GetCanonicalFileName, getCommonSourceDirectory as ts_getCommonSourceDirectory, + getCommonSourceDirectory60, getCommonSourceDirectoryOfConfig, getDeclarationDiagnostics as ts_getDeclarationDiagnostics, getDefaultLibFileName, @@ -131,6 +134,7 @@ import { getNormalizedAbsolutePathWithoutRoot, getNormalizedPathComponents, getOutputDeclarationFileName, + getOutputPathsFor, getPackageScopeForPath, getPathFromPathComponents, getPositionOfLineAndCharacter, @@ -233,6 +237,7 @@ import { NodeWithTypeArguments, noop, normalizePath, + normalizeSlashes, notImplementedResolver, noTransformers, ObjectLiteralExpression, @@ -293,6 +298,7 @@ import { SourceFile, sourceFileAffectingCompilerOptions, sourceFileMayBeEmitted, + sourceFileMayBeEmitted60, startsWith, Statement, StringLiteral, @@ -2149,6 +2155,7 @@ export function createProgram(_rootNamesOrOptions: readonly string[] | CreatePro return commonSourceDirectory; } const emittedFiles = filter(files, file => sourceFileMayBeEmitted(file, program)); + commonSourceDirectory = ts_getCommonSourceDirectory( options, () => mapDefined(emittedFiles, file => file.isDeclarationFile ? undefined : file.fileName), @@ -2156,6 +2163,24 @@ export function createProgram(_rootNamesOrOptions: readonly string[] | CreatePro getCanonicalFileName, commonSourceDirectory => checkSourceFilesBelongToPath(emittedFiles, commonSourceDirectory), ); + + const commonDir60 = getCommonSourceDirectory60(options); + if (commonDir60) { + const emittedFiles60 = filter(files, file => sourceFileMayBeEmitted60(file, program)); + const commonDir2 = getDirectoryPath(normalizeSlashes(options.configFilePath!)); + const result = checkSourceFilesBelongToPathWorker(emittedFiles60, commonDir2); + if (!result.allFilesBelongToPath) { + result.filesWithError?.forEach(sourceFile => { + programDiagnostics.addLazyConfigDiagnostic( + sourceFile, + Diagnostics.File_0_is_not_under_rootDir_1_rootDir_is_expected_to_contain_all_source_files, + sourceFile.fileName, + "!!! Sheetal CommonDir computed: " + commonSourceDirectory + " commonDir in 6.0 : " + commonDir60, + ); + }); + } + } + programDiagnostics.setCommonSourceDirectory(commonSourceDirectory); return commonSourceDirectory; } @@ -4009,24 +4034,33 @@ export function createProgram(_rootNamesOrOptions: readonly string[] | CreatePro } function checkSourceFilesBelongToPath(sourceFiles: readonly SourceFile[], rootDirectory: string): boolean { + const result = checkSourceFilesBelongToPathWorker(sourceFiles, rootDirectory); + result.filesWithError?.forEach(sourceFile => { + programDiagnostics.addLazyConfigDiagnostic( + sourceFile, + Diagnostics.File_0_is_not_under_rootDir_1_rootDir_is_expected_to_contain_all_source_files, + sourceFile.fileName, + rootDirectory, + ); + }); + return result.allFilesBelongToPath; + } + + function checkSourceFilesBelongToPathWorker(sourceFiles: readonly SourceFile[], rootDirectory: string) { let allFilesBelongToPath = true; + let filesWithError: SourceFile[] | undefined; const absoluteRootDirectoryPath = host.getCanonicalFileName(getNormalizedAbsolutePath(rootDirectory, currentDirectory)); for (const sourceFile of sourceFiles) { if (!sourceFile.isDeclarationFile) { const absoluteSourceFilePath = host.getCanonicalFileName(getNormalizedAbsolutePath(sourceFile.fileName, currentDirectory)); if (absoluteSourceFilePath.indexOf(absoluteRootDirectoryPath) !== 0) { - programDiagnostics.addLazyConfigDiagnostic( - sourceFile, - Diagnostics.File_0_is_not_under_rootDir_1_rootDir_is_expected_to_contain_all_source_files, - sourceFile.fileName, - rootDirectory, - ); + (filesWithError ??= []).push(sourceFile); allFilesBelongToPath = false; } } } - return allFilesBelongToPath; + return { allFilesBelongToPath, filesWithError }; } function parseProjectReferenceConfigFile(ref: ProjectReference): ResolvedProjectReference | undefined { @@ -4403,6 +4437,36 @@ export function createProgram(_rootNamesOrOptions: readonly string[] | CreatePro verifyEmitFilePath(emitFileNames.declarationFilePath, emitFilesSeen); }); } + { + const commonDirWithConfig = getCommonSourceDirectory60(options); + if (commonDirWithConfig) { + const emitHost = getEmitHost(); + const emitHostWithConfig = { + ...emitHost, + getCommonSourceDirectory: () => commonDirWithConfig, + }; + + for (const sourceFile of emitHost.getSourceFiles()) { + const canBeEmitted = sourceFileMayBeEmitted(sourceFile, emitHost); + const canBeEmitted60 = sourceFileMayBeEmitted60(sourceFile, emitHostWithConfig); + const outputPaths = canBeEmitted ? + getOutputPathsFor(sourceFile, emitHost, /*forceDtsPaths*/ false) : + undefined; + const outputPaths60 = canBeEmitted60 ? + getOutputPathsFor(sourceFile, emitHostWithConfig, /*forceDtsPaths*/ false) : + undefined; + if (!emitFileNamesIsEqual(outputPaths, outputPaths60)) { + // Report error + programDiagnostics.addConfigDiagnostic(createCompilerDiagnostic( + Diagnostics.Cannot_write_file_0_because_it_would_be_overwritten_by_multiple_input_files, + "!!! Sheetal: Output layout chaned for file: " + sourceFile.fileName + + "\n outputPaths:: " + JSON.stringify(outputPaths) + + "\n Output paths in 6.0: " + JSON.stringify(outputPaths60), + )); + } + } + } + } // Verify that all the emit files are unique and don't overwrite input files function verifyEmitFilePath(emitFileName: string | undefined, emitFilesSeen: Set) { diff --git a/src/compiler/utilities.ts b/src/compiler/utilities.ts index 4687bb8796f..7f13c7483f2 100644 --- a/src/compiler/utilities.ts +++ b/src/compiler/utilities.ts @@ -170,6 +170,7 @@ import { getCombinedModifierFlags, getCombinedNodeFlags, getCommonSourceDirectory, + getCommonSourceDirectory60, getContainerFlags, getDirectoryPath, getImpliedNodeFormatForEmitWorker, @@ -897,7 +898,8 @@ export function createModeMismatchDetails(currentSourceFile: SourceFile): Diagno return result; } -function packageIdIsEqual(a: PackageId | undefined, b: PackageId | undefined): boolean { +/** @internal */ +export function packageIdIsEqual(a: PackageId | undefined, b: PackageId | undefined): boolean { return a === b || !!a && !!b && a.name === b.name && a.subModuleName === b.subModuleName && a.version === b.version && a.peerDependencies === b.peerDependencies; } @@ -6549,6 +6551,16 @@ export interface EmitFileNames { buildInfoPath?: string | undefined; } +/** @internal */ +export function emitFileNamesIsEqual(a: EmitFileNames | undefined, b: EmitFileNames | undefined): boolean { + return a === b || !!a && !!b && + a.jsFilePath === b.jsFilePath && + a.sourceMapFilePath === b.jsFilePath && + a.declarationFilePath === b.declarationFilePath && + a.declarationMapPath === b.declarationMapPath && + a.buildInfoPath === b.buildInfoPath; +} + /** * Gets the source files that are expected to have an emit output. * @@ -6616,6 +6628,35 @@ export function sourceFileMayBeEmitted(sourceFile: SourceFile, host: SourceFileM return true; } +export function sourceFileMayBeEmitted60(sourceFile: SourceFile, host: SourceFileMayBeEmittedHost, forceDtsEmit?: boolean): boolean { + const options = host.getCompilerOptions(); + // Js files are emitted only if option is enabled + if (options.noEmitForJsFiles && isSourceFileJS(sourceFile)) return false; + // Declaration files are not emitted + if (sourceFile.isDeclarationFile) return false; + // Source file from node_modules are not emitted + if (host.isSourceFileFromExternalLibrary(sourceFile)) return false; + // forcing dts emit => file needs to be emitted + if (forceDtsEmit) return true; + // Check other conditions for file emit + // Source files from referenced projects are not emitted + if (host.isSourceOfProjectReferenceRedirect(sourceFile.fileName)) return false; + // Any non json file should be emitted + if (!isJsonSourceFile(sourceFile)) return true; + if (host.getRedirectFromSourceFile(sourceFile.fileName)) return false; + // Emit json file if outFile is specified + if (options.outFile) return true; + // Json file is not emitted if outDir is not specified + if (!options.outDir) return false; + // Otherwise if rootDir or composite config file, we know common sourceDir and can check if file would be emitted in same location + if (!options.rootDir && !options.composite && options.configFilePath) { + const commonDir = getNormalizedAbsolutePath(getCommonSourceDirectory60(options)!, host.getCurrentDirectory()); + const outputPath = getSourceFilePathInNewDirWorker(sourceFile.fileName, options.outDir, host.getCurrentDirectory(), commonDir, host.getCanonicalFileName); + if (comparePaths(sourceFile.fileName, outputPath, host.getCurrentDirectory(), !host.useCaseSensitiveFileNames()) === Comparison.EqualTo) return false; + } + return true; +} + /** @internal */ export function getSourceFilePathInNewDir(fileName: string, host: EmitHost, newDirPath: string): string { return getSourceFilePathInNewDirWorker(fileName, newDirPath, host.getCurrentDirectory(), host.getCommonSourceDirectory(), f => host.getCanonicalFileName(f));