Skip to content

Commit 41aca7c

Browse files
authored
Add a new compiler option moduleSuffixes to expand the node module resolver's search algorithm (#48189)
* Add moduleSuffixes compiler option and related tests. Update baselines for compiler options tests. * Add a flag to the command-line parser which allows "list" params to preserve "falsy" values such as empty strings. Falsy values are normally stripped out. * Add tests. Rework resolver logic to only run module-suffix code when needed. * PR feedback * Add test * Remove unnecessary conditional.
1 parent c639d3a commit 41aca7c

File tree

105 files changed

+1728
-2
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

105 files changed

+1728
-2
lines changed

Diff for: src/compiler/commandLineParser.ts

+14-2
Original file line numberDiff line numberDiff line change
@@ -884,6 +884,18 @@ namespace ts {
884884
description: Diagnostics.Allow_accessing_UMD_globals_from_modules,
885885
defaultValueDescription: false,
886886
},
887+
{
888+
name: "moduleSuffixes",
889+
type: "list",
890+
element: {
891+
name: "suffix",
892+
type: "string",
893+
},
894+
listPreserveFalsyValues: true,
895+
affectsModuleResolution: true,
896+
category: Diagnostics.Modules,
897+
description: Diagnostics.List_of_file_name_suffixes_to_search_when_resolving_a_module,
898+
},
887899

888900
// Source Maps
889901
{
@@ -3192,7 +3204,7 @@ namespace ts {
31923204
if (option.type === "list") {
31933205
const listOption = option;
31943206
if (listOption.element.isFilePath || !isString(listOption.element.type)) {
3195-
return filter(map(value, v => normalizeOptionValue(listOption.element, basePath, v)), v => !!v) as CompilerOptionsValue;
3207+
return filter(map(value, v => normalizeOptionValue(listOption.element, basePath, v)), v => listOption.listPreserveFalsyValues ? true : !!v) as CompilerOptionsValue;
31963208
}
31973209
return value;
31983210
}
@@ -3233,7 +3245,7 @@ namespace ts {
32333245
}
32343246

32353247
function convertJsonOptionOfListType(option: CommandLineOptionOfListType, values: readonly any[], basePath: string, errors: Push<Diagnostic>): any[] {
3236-
return filter(map(values, v => convertJsonOption(option.element, v, basePath, errors)), v => !!v);
3248+
return filter(map(values, v => convertJsonOption(option.element, v, basePath, errors)), v => option.listPreserveFalsyValues ? true : !!v);
32373249
}
32383250

32393251
/**

Diff for: src/compiler/diagnosticMessages.json

+4
Original file line numberDiff line numberDiff line change
@@ -5878,6 +5878,10 @@
58785878
"category": "Message",
58795879
"code": 6930
58805880
},
5881+
"List of file name suffixes to search when resolving a module." : {
5882+
"category": "Error",
5883+
"code": 6931
5884+
},
58815885

58825886
"Variable '{0}' implicitly has an '{1}' type.": {
58835887
"category": "Error",

Diff for: src/compiler/moduleNameResolver.ts

+10
Original file line numberDiff line numberDiff line change
@@ -1576,6 +1576,16 @@ namespace ts {
15761576

15771577
/** Return the file if it exists. */
15781578
function tryFile(fileName: string, onlyRecordFailures: boolean, state: ModuleResolutionState): string | undefined {
1579+
if (!state.compilerOptions.moduleSuffixes?.length) {
1580+
return tryFileLookup(fileName, onlyRecordFailures, state);
1581+
}
1582+
1583+
const ext = tryGetExtensionFromPath(fileName) ?? "";
1584+
const fileNameNoExtension = ext ? removeExtension(fileName, ext) : fileName;
1585+
return forEach(state.compilerOptions.moduleSuffixes, suffix => tryFileLookup(fileNameNoExtension + suffix + ext, onlyRecordFailures, state));
1586+
}
1587+
1588+
function tryFileLookup(fileName: string, onlyRecordFailures: boolean, state: ModuleResolutionState): string | undefined {
15791589
if (!onlyRecordFailures) {
15801590
if (state.host.fileExists(fileName)) {
15811591
if (state.traceEnabled) {

Diff for: src/compiler/types.ts

+2
Original file line numberDiff line numberDiff line change
@@ -6149,6 +6149,7 @@ namespace ts {
61496149
maxNodeModuleJsDepth?: number;
61506150
module?: ModuleKind;
61516151
moduleResolution?: ModuleResolutionKind;
6152+
moduleSuffixes?: string[];
61526153
moduleDetection?: ModuleDetectionKind;
61536154
newLine?: NewLineKind;
61546155
noEmit?: boolean;
@@ -6453,6 +6454,7 @@ namespace ts {
64536454
export interface CommandLineOptionOfListType extends CommandLineOptionBase {
64546455
type: "list";
64556456
element: CommandLineOptionOfCustomType | CommandLineOptionOfStringType | CommandLineOptionOfNumberType | CommandLineOptionOfBooleanType | TsConfigOnlyOption;
6457+
listPreserveFalsyValues?: boolean;
64566458
}
64576459

64586460
/* @internal */

Diff for: src/testRunner/unittests/config/convertCompilerOptionsFromJson.ts

+64
Original file line numberDiff line numberDiff line change
@@ -420,6 +420,70 @@ namespace ts {
420420
);
421421
});
422422

423+
it("Convert empty string option of moduleSuffixes to compiler-options ", () => {
424+
assertCompilerOptions(
425+
{
426+
compilerOptions: {
427+
moduleSuffixes: [".ios", ""]
428+
}
429+
}, "tsconfig.json",
430+
{
431+
compilerOptions: {
432+
moduleSuffixes: [".ios", ""]
433+
},
434+
errors: []
435+
}
436+
);
437+
});
438+
439+
it("Convert empty string option of moduleSuffixes to compiler-options ", () => {
440+
assertCompilerOptions(
441+
{
442+
compilerOptions: {
443+
moduleSuffixes: [""]
444+
}
445+
}, "tsconfig.json",
446+
{
447+
compilerOptions: {
448+
moduleSuffixes: [""]
449+
},
450+
errors: []
451+
}
452+
);
453+
});
454+
455+
it("Convert trailing-whitespace string option of moduleSuffixes to compiler-options ", () => {
456+
assertCompilerOptions(
457+
{
458+
compilerOptions: {
459+
moduleSuffixes: [" "]
460+
}
461+
}, "tsconfig.json",
462+
{
463+
compilerOptions: {
464+
moduleSuffixes: [" "]
465+
},
466+
errors: []
467+
}
468+
);
469+
});
470+
471+
it("Convert empty option of moduleSuffixes to compiler-options ", () => {
472+
assertCompilerOptions(
473+
{
474+
compilerOptions: {
475+
moduleSuffixes: []
476+
}
477+
}, "tsconfig.json",
478+
{
479+
compilerOptions: {
480+
moduleSuffixes: []
481+
},
482+
errors: []
483+
}
484+
);
485+
});
486+
423487
it("Convert incorrectly format tsconfig.json to compiler-options ", () => {
424488
assertCompilerOptions(
425489
{

Diff for: tests/baselines/reference/api/tsserverlibrary.d.ts

+1
Original file line numberDiff line numberDiff line change
@@ -2977,6 +2977,7 @@ declare namespace ts {
29772977
maxNodeModuleJsDepth?: number;
29782978
module?: ModuleKind;
29792979
moduleResolution?: ModuleResolutionKind;
2980+
moduleSuffixes?: string[];
29802981
moduleDetection?: ModuleDetectionKind;
29812982
newLine?: NewLineKind;
29822983
noEmit?: boolean;

Diff for: tests/baselines/reference/api/typescript.d.ts

+1
Original file line numberDiff line numberDiff line change
@@ -2977,6 +2977,7 @@ declare namespace ts {
29772977
maxNodeModuleJsDepth?: number;
29782978
module?: ModuleKind;
29792979
moduleResolution?: ModuleResolutionKind;
2980+
moduleSuffixes?: string[];
29802981
moduleDetection?: ModuleDetectionKind;
29812982
newLine?: NewLineKind;
29822983
noEmit?: boolean;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
//// [tests/cases/compiler/moduleResolutionWithSuffixes_empty.ts] ////
2+
3+
//// [index.ts]
4+
import { base } from "./foo";
5+
//// [foo.ts]
6+
export function base() {}
7+
8+
9+
//// [foo.js]
10+
"use strict";
11+
exports.__esModule = true;
12+
exports.base = void 0;
13+
function base() { }
14+
exports.base = base;
15+
//// [index.js]
16+
"use strict";
17+
exports.__esModule = true;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
=== /index.ts ===
2+
import { base } from "./foo";
3+
>base : Symbol(base, Decl(index.ts, 0, 8))
4+
5+
=== /foo.ts ===
6+
export function base() {}
7+
>base : Symbol(base, Decl(foo.ts, 0, 0))
8+
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
[
2+
"======== Resolving module './foo' from '/index.ts'. ========",
3+
"Explicitly specified module resolution kind: 'NodeJs'.",
4+
"Loading module as file / folder, candidate module location '/foo', target file type 'TypeScript'.",
5+
"File '/foo.ts' exist - use it as a name resolution result.",
6+
"======== Module name './foo' was successfully resolved to '/foo.ts'. ========"
7+
]
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
=== /index.ts ===
2+
import { base } from "./foo";
3+
>base : () => void
4+
5+
=== /foo.ts ===
6+
export function base() {}
7+
>base : () => void
8+
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
//// [tests/cases/compiler/moduleResolutionWithSuffixes_notSpecified.ts] ////
2+
3+
//// [index.ts]
4+
import { base } from "./foo";
5+
//// [foo.ts]
6+
export function base() {}
7+
8+
9+
//// [foo.js]
10+
"use strict";
11+
exports.__esModule = true;
12+
exports.base = void 0;
13+
function base() { }
14+
exports.base = base;
15+
//// [index.js]
16+
"use strict";
17+
exports.__esModule = true;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
=== /index.ts ===
2+
import { base } from "./foo";
3+
>base : Symbol(base, Decl(index.ts, 0, 8))
4+
5+
=== /foo.ts ===
6+
export function base() {}
7+
>base : Symbol(base, Decl(foo.ts, 0, 0))
8+
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
[
2+
"======== Resolving module './foo' from '/index.ts'. ========",
3+
"Explicitly specified module resolution kind: 'NodeJs'.",
4+
"Loading module as file / folder, candidate module location '/foo', target file type 'TypeScript'.",
5+
"File '/foo.ts' exist - use it as a name resolution result.",
6+
"======== Module name './foo' was successfully resolved to '/foo.ts'. ========"
7+
]
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
=== /index.ts ===
2+
import { base } from "./foo";
3+
>base : () => void
4+
5+
=== /foo.ts ===
6+
export function base() {}
7+
>base : () => void
8+
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
//// [tests/cases/compiler/moduleResolutionWithSuffixes_one.ts] ////
2+
3+
//// [index.ts]
4+
import { ios } from "./foo";
5+
//// [foo.ios.ts]
6+
export function ios() {}
7+
//// [foo.ts]
8+
export function base() {}
9+
10+
11+
//// [foo.ios.js]
12+
"use strict";
13+
exports.__esModule = true;
14+
exports.ios = void 0;
15+
function ios() { }
16+
exports.ios = ios;
17+
//// [index.js]
18+
"use strict";
19+
exports.__esModule = true;
20+
//// [foo.js]
21+
"use strict";
22+
exports.__esModule = true;
23+
exports.base = void 0;
24+
function base() { }
25+
exports.base = base;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
=== /index.ts ===
2+
import { ios } from "./foo";
3+
>ios : Symbol(ios, Decl(index.ts, 0, 8))
4+
5+
=== /foo.ios.ts ===
6+
export function ios() {}
7+
>ios : Symbol(ios, Decl(foo.ios.ts, 0, 0))
8+
9+
=== /foo.ts ===
10+
export function base() {}
11+
>base : Symbol(base, Decl(foo.ts, 0, 0))
12+
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
[
2+
"======== Resolving module './foo' from '/index.ts'. ========",
3+
"Explicitly specified module resolution kind: 'NodeJs'.",
4+
"Loading module as file / folder, candidate module location '/foo', target file type 'TypeScript'.",
5+
"File '/foo.ios.ts' exist - use it as a name resolution result.",
6+
"======== Module name './foo' was successfully resolved to '/foo.ios.ts'. ========"
7+
]
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
=== /index.ts ===
2+
import { ios } from "./foo";
3+
>ios : () => void
4+
5+
=== /foo.ios.ts ===
6+
export function ios() {}
7+
>ios : () => void
8+
9+
=== /foo.ts ===
10+
export function base() {}
11+
>base : () => void
12+
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
//// [tests/cases/compiler/moduleResolutionWithSuffixes_oneBlank.ts] ////
2+
3+
//// [index.ts]
4+
import { base } from "./foo";
5+
//// [foo.ts]
6+
export function base() {}
7+
8+
9+
//// [foo.js]
10+
"use strict";
11+
exports.__esModule = true;
12+
exports.base = void 0;
13+
function base() { }
14+
exports.base = base;
15+
//// [index.js]
16+
"use strict";
17+
exports.__esModule = true;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
=== /index.ts ===
2+
import { base } from "./foo";
3+
>base : Symbol(base, Decl(index.ts, 0, 8))
4+
5+
=== /foo.ts ===
6+
export function base() {}
7+
>base : Symbol(base, Decl(foo.ts, 0, 0))
8+
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
[
2+
"======== Resolving module './foo' from '/index.ts'. ========",
3+
"Explicitly specified module resolution kind: 'NodeJs'.",
4+
"Loading module as file / folder, candidate module location '/foo', target file type 'TypeScript'.",
5+
"File '/foo.ts' exist - use it as a name resolution result.",
6+
"======== Module name './foo' was successfully resolved to '/foo.ts'. ========"
7+
]
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
=== /index.ts ===
2+
import { base } from "./foo";
3+
>base : () => void
4+
5+
=== /foo.ts ===
6+
export function base() {}
7+
>base : () => void
8+
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
/index.ts(1,21): error TS2307: Cannot find module './foo' or its corresponding type declarations.
2+
3+
4+
==== /tsconfig.json (0 errors) ====
5+
// moduleSuffixes has one entry but there isn't a matching file. Module resolution should fail.
6+
7+
{
8+
"compilerOptions": {
9+
"moduleResolution": "node",
10+
"traceResolution": true,
11+
"moduleSuffixes": [".ios"]
12+
}
13+
}
14+
15+
==== /index.ts (1 errors) ====
16+
import { ios } from "./foo";
17+
~~~~~~~
18+
!!! error TS2307: Cannot find module './foo' or its corresponding type declarations.
19+
==== /foo.ts (0 errors) ====
20+
export function base() {}
21+

0 commit comments

Comments
 (0)