Skip to content

Commit 2b4a104

Browse files
Andyuniqueiniquity
Andy
authored andcommitted
For duplicate source files of the same package, make one redirect to the other (microsoft#16274)
* For duplicate source files of the same package, make one redirect to the other * Add reuseProgramStructure tests * Copy `sourceFileToPackageId` and `isSourceFileTargetOfRedirect` only if we completely reuse old structure * Use fallthrough instead of early exit from loop * Use a set to efficiently detect duplicate package names * Move map setting outside of createRedirectSourceFile * Correctly handle seenPackageNames set * sourceFileToPackageId -> sourceFileToPackageName * Renames * Respond to PR comments * Fix bug where `oldSourceFile !== newSourceFile` because oldSourceFile was a redirect * Clean up redirectInfo * Respond to PR comments
1 parent c80cb3f commit 2b4a104

16 files changed

+696
-106
lines changed

src/compiler/core.ts

+7-8
Original file line numberDiff line numberDiff line change
@@ -2210,20 +2210,14 @@ namespace ts {
22102210
/** Must have ".d.ts" first because if ".ts" goes first, that will be detected as the extension instead of ".d.ts". */
22112211
export const supportedTypescriptExtensionsForExtractExtension: ReadonlyArray<Extension> = [Extension.Dts, Extension.Ts, Extension.Tsx];
22122212
export const supportedJavascriptExtensions: ReadonlyArray<Extension> = [Extension.Js, Extension.Jsx];
2213-
const allSupportedExtensions = [...supportedTypeScriptExtensions, ...supportedJavascriptExtensions];
2213+
const allSupportedExtensions: ReadonlyArray<Extension> = [...supportedTypeScriptExtensions, ...supportedJavascriptExtensions];
22142214

22152215
export function getSupportedExtensions(options?: CompilerOptions, extraFileExtensions?: ReadonlyArray<JsFileExtensionInfo>): ReadonlyArray<string> {
22162216
const needAllExtensions = options && options.allowJs;
22172217
if (!extraFileExtensions || extraFileExtensions.length === 0 || !needAllExtensions) {
22182218
return needAllExtensions ? allSupportedExtensions : supportedTypeScriptExtensions;
22192219
}
2220-
const extensions: string[] = allSupportedExtensions.slice(0);
2221-
for (const extInfo of extraFileExtensions) {
2222-
if (extensions.indexOf(extInfo.extension) === -1) {
2223-
extensions.push(extInfo.extension);
2224-
}
2225-
}
2226-
return extensions;
2220+
return deduplicate([...allSupportedExtensions, ...extraFileExtensions.map(e => e.extension)]);
22272221
}
22282222

22292223
export function hasJavaScriptFileExtension(fileName: string) {
@@ -2590,6 +2584,11 @@ namespace ts {
25902584
}
25912585
Debug.fail(`File ${path} has unknown extension.`);
25922586
}
2587+
2588+
export function isAnySupportedFileExtension(path: string): boolean {
2589+
return tryGetExtensionFromPath(path) !== undefined;
2590+
}
2591+
25932592
export function tryGetExtensionFromPath(path: string): Extension | undefined {
25942593
return find<Extension>(supportedTypescriptExtensionsForExtractExtension, e => fileExtensionIs(path, e)) || find(supportedJavascriptExtensions, e => fileExtensionIs(path, e));
25952594
}

src/compiler/moduleNameResolver.ts

+76-34
Original file line numberDiff line numberDiff line change
@@ -13,13 +13,32 @@ namespace ts {
1313
return compilerOptions.traceResolution && host.trace !== undefined;
1414
}
1515

16-
/**
17-
* Result of trying to resolve a module.
18-
* At least one of `ts` and `js` should be defined, or the whole thing should be `undefined`.
19-
*/
16+
/** Array that is only intended to be pushed to, never read. */
17+
/* @internal */
18+
export interface Push<T> {
19+
push(value: T): void;
20+
}
21+
22+
function withPackageId(packageId: PackageId | undefined, r: PathAndExtension | undefined): Resolved {
23+
return r && { path: r.path, extension: r.ext, packageId };
24+
}
25+
26+
function noPackageId(r: PathAndExtension | undefined): Resolved {
27+
return withPackageId(/*packageId*/ undefined, r);
28+
}
29+
30+
/** Result of trying to resolve a module. */
2031
interface Resolved {
2132
path: string;
2233
extension: Extension;
34+
packageId: PackageId | undefined;
35+
}
36+
37+
/** Result of trying to resolve a module at a file. Needs to have 'packageId' added later. */
38+
interface PathAndExtension {
39+
path: string;
40+
// (Use a different name than `extension` to make sure Resolved isn't assignable to PathAndExtension.)
41+
ext: Extension;
2342
}
2443

2544
/**
@@ -43,7 +62,7 @@ namespace ts {
4362

4463
function createResolvedModuleWithFailedLookupLocations(resolved: Resolved | undefined, isExternalLibraryImport: boolean, failedLookupLocations: string[]): ResolvedModuleWithFailedLookupLocations {
4564
return {
46-
resolvedModule: resolved && { resolvedFileName: resolved.path, extension: resolved.extension, isExternalLibraryImport },
65+
resolvedModule: resolved && { resolvedFileName: resolved.path, extension: resolved.extension, isExternalLibraryImport, packageId: resolved.packageId },
4766
failedLookupLocations
4867
};
4968
}
@@ -54,9 +73,16 @@ namespace ts {
5473
traceEnabled: boolean;
5574
}
5675

76+
interface PackageJson {
77+
name?: string;
78+
version?: string;
79+
typings?: string;
80+
types?: string;
81+
main?: string;
82+
}
83+
5784
/** Reads from "main" or "types"/"typings" depending on `extensions`. */
58-
function tryReadPackageJsonFields(readTypes: boolean, packageJsonPath: string, baseDirectory: string, state: ModuleResolutionState): string | undefined {
59-
const jsonContent = readJson(packageJsonPath, state.host);
85+
function tryReadPackageJsonFields(readTypes: boolean, jsonContent: PackageJson, baseDirectory: string, state: ModuleResolutionState): string | undefined {
6086
return readTypes ? tryReadFromField("typings") || tryReadFromField("types") : tryReadFromField("main");
6187

6288
function tryReadFromField(fieldName: "typings" | "types" | "main"): string | undefined {
@@ -83,7 +109,7 @@ namespace ts {
83109
}
84110
}
85111

86-
function readJson(path: string, host: ModuleResolutionHost): { typings?: string, types?: string, main?: string } {
112+
function readJson(path: string, host: ModuleResolutionHost): PackageJson {
87113
try {
88114
const jsonText = host.readFile(path);
89115
return jsonText ? JSON.parse(jsonText) : {};
@@ -646,7 +672,7 @@ namespace ts {
646672
if (extension !== undefined) {
647673
const path = tryFile(candidate, failedLookupLocations, /*onlyRecordFailures*/ false, state);
648674
if (path !== undefined) {
649-
return { path, extension };
675+
return { path, extension, packageId: undefined };
650676
}
651677
}
652678

@@ -709,7 +735,7 @@ namespace ts {
709735
}
710736
const resolved = loadModuleFromNodeModules(extensions, moduleName, containingDirectory, failedLookupLocations, state, cache);
711737
// For node_modules lookups, get the real path so that multiple accesses to an `npm link`-ed module do not create duplicate files.
712-
return resolved && { value: resolved.value && { resolved: { path: realpath(resolved.value.path, host, traceEnabled), extension: resolved.value.extension }, isExternalLibraryImport: true } };
738+
return resolved && { value: resolved.value && { resolved: { ...resolved.value, path: realpath(resolved.value.path, host, traceEnabled) }, isExternalLibraryImport: true } };
713739
}
714740
else {
715741
const candidate = normalizePath(combinePaths(containingDirectory, moduleName));
@@ -747,7 +773,7 @@ namespace ts {
747773
}
748774
const resolvedFromFile = loadModuleFromFile(extensions, candidate, failedLookupLocations, onlyRecordFailures, state);
749775
if (resolvedFromFile) {
750-
return resolvedFromFile;
776+
return noPackageId(resolvedFromFile);
751777
}
752778
}
753779
if (!onlyRecordFailures) {
@@ -768,11 +794,15 @@ namespace ts {
768794
return !host.directoryExists || host.directoryExists(directoryName);
769795
}
770796

797+
function loadModuleFromFileNoPackageId(extensions: Extensions, candidate: string, failedLookupLocations: Push<string>, onlyRecordFailures: boolean, state: ModuleResolutionState): Resolved {
798+
return noPackageId(loadModuleFromFile(extensions, candidate, failedLookupLocations, onlyRecordFailures, state));
799+
}
800+
771801
/**
772802
* @param {boolean} onlyRecordFailures - if true then function won't try to actually load files but instead record all attempts as failures. This flag is necessary
773803
* in cases when we know upfront that all load attempts will fail (because containing folder does not exists) however we still need to record all failed lookup locations.
774804
*/
775-
function loadModuleFromFile(extensions: Extensions, candidate: string, failedLookupLocations: Push<string>, onlyRecordFailures: boolean, state: ModuleResolutionState): Resolved | undefined {
805+
function loadModuleFromFile(extensions: Extensions, candidate: string, failedLookupLocations: Push<string>, onlyRecordFailures: boolean, state: ModuleResolutionState): PathAndExtension | undefined {
776806
// First, try adding an extension. An import of "foo" could be matched by a file "foo.ts", or "foo.js" by "foo.js.ts"
777807
const resolvedByAddingExtension = tryAddingExtensions(candidate, extensions, failedLookupLocations, onlyRecordFailures, state);
778808
if (resolvedByAddingExtension) {
@@ -792,7 +822,7 @@ namespace ts {
792822
}
793823

794824
/** Try to return an existing file that adds one of the `extensions` to `candidate`. */
795-
function tryAddingExtensions(candidate: string, extensions: Extensions, failedLookupLocations: Push<string>, onlyRecordFailures: boolean, state: ModuleResolutionState): Resolved | undefined {
825+
function tryAddingExtensions(candidate: string, extensions: Extensions, failedLookupLocations: Push<string>, onlyRecordFailures: boolean, state: ModuleResolutionState): PathAndExtension | undefined {
796826
if (!onlyRecordFailures) {
797827
// check if containing folder exists - if it doesn't then just record failures for all supported extensions without disk probing
798828
const directory = getDirectoryPath(candidate);
@@ -810,9 +840,9 @@ namespace ts {
810840
return tryExtension(Extension.Js) || tryExtension(Extension.Jsx);
811841
}
812842

813-
function tryExtension(extension: Extension): Resolved | undefined {
814-
const path = tryFile(candidate + extension, failedLookupLocations, onlyRecordFailures, state);
815-
return path && { path, extension };
843+
function tryExtension(ext: Extension): PathAndExtension | undefined {
844+
const path = tryFile(candidate + ext, failedLookupLocations, onlyRecordFailures, state);
845+
return path && { path, ext };
816846
}
817847
}
818848

@@ -838,12 +868,23 @@ namespace ts {
838868
function loadNodeModuleFromDirectory(extensions: Extensions, candidate: string, failedLookupLocations: Push<string>, onlyRecordFailures: boolean, state: ModuleResolutionState, considerPackageJson = true): Resolved | undefined {
839869
const directoryExists = !onlyRecordFailures && directoryProbablyExists(candidate, state.host);
840870

871+
let packageId: PackageId | undefined;
872+
841873
if (considerPackageJson) {
842874
const packageJsonPath = pathToPackageJson(candidate);
843875
if (directoryExists && state.host.fileExists(packageJsonPath)) {
844-
const fromPackageJson = loadModuleFromPackageJson(packageJsonPath, extensions, candidate, failedLookupLocations, state);
876+
if (state.traceEnabled) {
877+
trace(state.host, Diagnostics.Found_package_json_at_0, packageJsonPath);
878+
}
879+
const jsonContent = readJson(packageJsonPath, state.host);
880+
881+
if (typeof jsonContent.name === "string" && typeof jsonContent.version === "string") {
882+
packageId = { name: jsonContent.name, version: jsonContent.version };
883+
}
884+
885+
const fromPackageJson = loadModuleFromPackageJson(jsonContent, extensions, candidate, failedLookupLocations, state);
845886
if (fromPackageJson) {
846-
return fromPackageJson;
887+
return withPackageId(packageId, fromPackageJson);
847888
}
848889
}
849890
else {
@@ -855,15 +896,11 @@ namespace ts {
855896
}
856897
}
857898

858-
return loadModuleFromFile(extensions, combinePaths(candidate, "index"), failedLookupLocations, !directoryExists, state);
899+
return withPackageId(packageId, loadModuleFromFile(extensions, combinePaths(candidate, "index"), failedLookupLocations, !directoryExists, state));
859900
}
860901

861-
function loadModuleFromPackageJson(packageJsonPath: string, extensions: Extensions, candidate: string, failedLookupLocations: Push<string>, state: ModuleResolutionState): Resolved | undefined {
862-
if (state.traceEnabled) {
863-
trace(state.host, Diagnostics.Found_package_json_at_0, packageJsonPath);
864-
}
865-
866-
const file = tryReadPackageJsonFields(extensions !== Extensions.JavaScript, packageJsonPath, candidate, state);
902+
function loadModuleFromPackageJson(jsonContent: PackageJson, extensions: Extensions, candidate: string, failedLookupLocations: Push<string>, state: ModuleResolutionState): PathAndExtension | undefined {
903+
const file = tryReadPackageJsonFields(extensions !== Extensions.JavaScript, jsonContent, candidate, state);
867904
if (!file) {
868905
return undefined;
869906
}
@@ -883,13 +920,18 @@ namespace ts {
883920
// Even if extensions is DtsOnly, we can still look up a .ts file as a result of package.json "types"
884921
const nextExtensions = extensions === Extensions.DtsOnly ? Extensions.TypeScript : extensions;
885922
// Don't do package.json lookup recursively, because Node.js' package lookup doesn't.
886-
return nodeLoadModuleByRelativeName(nextExtensions, file, failedLookupLocations, onlyRecordFailures, state, /*considerPackageJson*/ false);
923+
const result = nodeLoadModuleByRelativeName(nextExtensions, file, failedLookupLocations, onlyRecordFailures, state, /*considerPackageJson*/ false);
924+
if (result) {
925+
// It won't have a `packageId` set, because we disabled `considerPackageJson`.
926+
Debug.assert(result.packageId === undefined);
927+
return { path: result.path, ext: result.extension };
928+
}
887929
}
888930

889931
/** Resolve from an arbitrarily specified file. Return `undefined` if it has an unsupported extension. */
890-
function resolvedIfExtensionMatches(extensions: Extensions, path: string): Resolved | undefined {
891-
const extension = tryGetExtensionFromPath(path);
892-
return extension !== undefined && extensionIsOk(extensions, extension) ? { path, extension } : undefined;
932+
function resolvedIfExtensionMatches(extensions: Extensions, path: string): PathAndExtension | undefined {
933+
const ext = tryGetExtensionFromPath(path);
934+
return ext !== undefined && extensionIsOk(extensions, ext) ? { path, ext } : undefined;
893935
}
894936

895937
/** True if `extension` is one of the supported `extensions`. */
@@ -911,7 +953,7 @@ namespace ts {
911953
function loadModuleFromNodeModulesFolder(extensions: Extensions, moduleName: string, nodeModulesFolder: string, nodeModulesFolderExists: boolean, failedLookupLocations: Push<string>, state: ModuleResolutionState): Resolved | undefined {
912954
const candidate = normalizePath(combinePaths(nodeModulesFolder, moduleName));
913955

914-
return loadModuleFromFile(extensions, candidate, failedLookupLocations, !nodeModulesFolderExists, state) ||
956+
return loadModuleFromFileNoPackageId(extensions, candidate, failedLookupLocations, !nodeModulesFolderExists, state) ||
915957
loadNodeModuleFromDirectory(extensions, candidate, failedLookupLocations, !nodeModulesFolderExists, state);
916958
}
917959

@@ -996,7 +1038,7 @@ namespace ts {
9961038
if (traceEnabled) {
9971039
trace(host, Diagnostics.Resolution_for_module_0_was_found_in_cache, moduleName);
9981040
}
999-
return { value: result.resolvedModule && { path: result.resolvedModule.resolvedFileName, extension: result.resolvedModule.extension } };
1041+
return { value: result.resolvedModule && { path: result.resolvedModule.resolvedFileName, extension: result.resolvedModule.extension, packageId: result.resolvedModule.packageId } };
10001042
}
10011043
}
10021044

@@ -1010,7 +1052,7 @@ namespace ts {
10101052
return createResolvedModuleWithFailedLookupLocations(resolved && resolved.value, /*isExternalLibraryImport*/ false, failedLookupLocations);
10111053

10121054
function tryResolve(extensions: Extensions): SearchResult<Resolved> {
1013-
const resolvedUsingSettings = tryLoadModuleUsingOptionalResolutionSettings(extensions, moduleName, containingDirectory, loadModuleFromFile, failedLookupLocations, state);
1055+
const resolvedUsingSettings = tryLoadModuleUsingOptionalResolutionSettings(extensions, moduleName, containingDirectory, loadModuleFromFileNoPackageId, failedLookupLocations, state);
10141056
if (resolvedUsingSettings) {
10151057
return { value: resolvedUsingSettings };
10161058
}
@@ -1024,7 +1066,7 @@ namespace ts {
10241066
return resolutionFromCache;
10251067
}
10261068
const searchName = normalizePath(combinePaths(directory, moduleName));
1027-
return toSearchResult(loadModuleFromFile(extensions, searchName, failedLookupLocations, /*onlyRecordFailures*/ false, state));
1069+
return toSearchResult(loadModuleFromFileNoPackageId(extensions, searchName, failedLookupLocations, /*onlyRecordFailures*/ false, state));
10281070
});
10291071
if (resolved) {
10301072
return resolved;
@@ -1036,7 +1078,7 @@ namespace ts {
10361078
}
10371079
else {
10381080
const candidate = normalizePath(combinePaths(containingDirectory, moduleName));
1039-
return toSearchResult(loadModuleFromFile(extensions, candidate, failedLookupLocations, /*onlyRecordFailures*/ false, state));
1081+
return toSearchResult(loadModuleFromFileNoPackageId(extensions, candidate, failedLookupLocations, /*onlyRecordFailures*/ false, state));
10401082
}
10411083
}
10421084
}

0 commit comments

Comments
 (0)