Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
316 changes: 226 additions & 90 deletions package-lock.json

Large diffs are not rendered by default.

3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -114,5 +114,8 @@
"volta": {
"node": "20.1.0",
"npm": "8.19.4"
},
"dependencies": {
"@sheetalkamat/ts-fix-rootdir": "^1.0.1"
}
}
15 changes: 15 additions & 0 deletions src/compiler/emitter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
25 changes: 25 additions & 0 deletions src/compiler/executeCommandLine.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { fixRootDirSync } from "@sheetalkamat/ts-fix-rootdir";
import {
arrayFrom,
BuilderProgram,
Expand Down Expand Up @@ -48,6 +49,7 @@ import {
formatMessage,
generateTSConfig,
getBuildOrderFromAnyBuildOrder,
getCompilerOptionsOfBuildOptions,
getConfigFileParsingDiagnostics,
getDiagnosticText,
getErrorSummaryText,
Expand Down Expand Up @@ -636,6 +638,17 @@ function executeCommandLineWorker(
fileName => getNormalizedAbsolutePath(fileName, currentDirectory),
);
if (configFileName) {
const fixRootDirLogs: string[] = [];
try {
const fixes = fixRootDirSync(configFileName, commandLineOptions as any, log => fixRootDirLogs.push(log));
for (const [fileName, text] of Object.entries(fixes)) {
sys.writeFile(fileName, text);
}
}
catch (e) {
throw new Error([...fixRootDirLogs, `Error: ${e instanceof Error ? e.message : e}`].join(sys.newLine));
}

const extendedConfigCache = new Map<string, ExtendedConfigCacheEntry>();
const configParseResult = parseConfigFileWithSystem(configFileName, commandLineOptions, extendedConfigCache, commandLine.watchOptions, sys, reportDiagnostic)!; // TODO: GH#18217
if (commandLineOptions.showConfig) {
Expand Down Expand Up @@ -746,6 +759,18 @@ export function executeCommandLine(
): void | SolutionBuilder<EmitAndSemanticDiagnosticsBuilderProgram> | WatchOfConfigFile<EmitAndSemanticDiagnosticsBuilderProgram> {
if (isBuildCommand(commandLineArgs)) {
const { buildOptions, watchOptions, projects, errors } = parseBuildCommand(commandLineArgs);
const fixRootDirLogs: string[] = [];
try {
for (const project of projects) {
const fixes = fixRootDirSync(project, getCompilerOptionsOfBuildOptions(buildOptions) as any, log => fixRootDirLogs.push(log));
for (const [fileName, text] of Object.entries(fixes)) {
sys.writeFile(fileName, text);
}
}
}
catch (e) {
throw new Error([...fixRootDirLogs, `Error: ${e instanceof Error ? e.message : e}`].join(sys.newLine));
}
if (buildOptions.generateCpuProfile && system.enableCPUProfiler) {
system.enableCPUProfiler(buildOptions.generateCpuProfile, () =>
performBuild(
Expand Down
74 changes: 56 additions & 18 deletions src/compiler/moduleNameResolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ import {
getBaseFileName,
GetCanonicalFileName,
getCommonSourceDirectory,
getCommonSourceDirectory60,
getCompilerOptionValue,
getDirectoryPath,
GetEffectiveTypeRootsHost,
Expand Down Expand Up @@ -80,6 +81,7 @@ import {
normalizePath,
normalizeSlashes,
PackageId,
packageIdIsEqual,
packageIdToString,
ParsedPatterns,
Path,
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -2932,31 +2944,53 @@ 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));
}
}
}
}
}
}
}
return undefined;

function getOutputDirectoriesForBaseDirectory(commonSourceDirGuess: string) {
// Config file ouput paths are processed to be relative to the host's current directory, while
Expand Down Expand Up @@ -3416,3 +3450,7 @@ function useCaseSensitiveFileNames(state: ModuleResolutionState) {
typeof state.host.useCaseSensitiveFileNames === "boolean" ? state.host.useCaseSensitiveFileNames :
state.host.useCaseSensitiveFileNames();
}

function searchResultIsEqual<T>(a: SearchResult<T> | undefined, b: SearchResult<T> | undefined, compareValue: (a: T | undefined, b: T | undefined) => boolean) {
return a === b || !!a && !!b && compareValue(a.value, b.value);
}
78 changes: 71 additions & 7 deletions src/compiler/program.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ import {
DiagnosticWithLocation,
directorySeparator,
DirectoryStructureHost,
emitFileNamesIsEqual,
emitFiles,
EmitHost,
emitModuleKindIsNonNodeESM,
Expand Down Expand Up @@ -110,6 +111,7 @@ import {
getBaseFileName,
GetCanonicalFileName,
getCommonSourceDirectory as ts_getCommonSourceDirectory,
getCommonSourceDirectory60,
getCommonSourceDirectoryOfConfig,
getDeclarationDiagnostics as ts_getDeclarationDiagnostics,
getDefaultLibFileName,
Expand All @@ -131,6 +133,7 @@ import {
getNormalizedAbsolutePathWithoutRoot,
getNormalizedPathComponents,
getOutputDeclarationFileName,
getOutputPathsFor,
getPackageScopeForPath,
getPathFromPathComponents,
getPositionOfLineAndCharacter,
Expand Down Expand Up @@ -233,6 +236,7 @@ import {
NodeWithTypeArguments,
noop,
normalizePath,
normalizeSlashes,
notImplementedResolver,
noTransformers,
ObjectLiteralExpression,
Expand Down Expand Up @@ -293,6 +297,7 @@ import {
SourceFile,
sourceFileAffectingCompilerOptions,
sourceFileMayBeEmitted,
sourceFileMayBeEmitted60,
startsWith,
Statement,
StringLiteral,
Expand Down Expand Up @@ -2149,13 +2154,32 @@ 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),
currentDirectory,
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;
}
Expand Down Expand Up @@ -4009,24 +4033,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 {
Expand Down Expand Up @@ -4403,6 +4436,37 @@ export function createProgram(_rootNamesOrOptions: readonly string[] | CreatePro
verifyEmitFilePath(emitFileNames.declarationFilePath, emitFilesSeen);
});
}
{
const commonDir60 = getCommonSourceDirectory60(options);
if (commonDir60) {
const emitHost = getEmitHost();
const emitHost60 = {
...emitHost,
getCommonSourceDirectory: () => commonDir60,
};

for (const sourceFile of emitHost.getSourceFiles()) {
const canBeEmitted = sourceFileMayBeEmitted(sourceFile, emitHost);
const canBeEmitted60 = sourceFileMayBeEmitted60(sourceFile, emitHost60);
const outputPaths = canBeEmitted ?
getOutputPathsFor(sourceFile, emitHost, /*forceDtsPaths*/ false) :
undefined;
const outputPaths60 = canBeEmitted60 ?
getOutputPathsFor(sourceFile, emitHost60, /*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 commonDir: " + getCommonSourceDirectory() + " commonDir 6.0:: " + commonDir60 +
"\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<string>) {
Expand Down
3 changes: 2 additions & 1 deletion src/compiler/tsbuildPublic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -328,7 +328,8 @@ export function createSolutionBuilderWithWatchHost<T extends BuilderProgram = Em
return host;
}

function getCompilerOptionsOfBuildOptions(buildOptions: BuildOptions): CompilerOptions {
/** @internal */
export function getCompilerOptionsOfBuildOptions(buildOptions: BuildOptions): CompilerOptions {
const result = {} as CompilerOptions;
commonOptionsWithBuild.forEach(option => {
if (hasProperty(buildOptions, option.name)) result[option.name] = buildOptions[option.name];
Expand Down
Loading
Loading