Skip to content

Commit

Permalink
Add a new compiler option moduleSuffixes to expand the node module …
Browse files Browse the repository at this point in the history
…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.
  • Loading branch information
afoxman authored Mar 30, 2022
1 parent c639d3a commit 41aca7c
Show file tree
Hide file tree
Showing 105 changed files with 1,728 additions and 2 deletions.
16 changes: 14 additions & 2 deletions src/compiler/commandLineParser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -884,6 +884,18 @@ namespace ts {
description: Diagnostics.Allow_accessing_UMD_globals_from_modules,
defaultValueDescription: false,
},
{
name: "moduleSuffixes",
type: "list",
element: {
name: "suffix",
type: "string",
},
listPreserveFalsyValues: true,
affectsModuleResolution: true,
category: Diagnostics.Modules,
description: Diagnostics.List_of_file_name_suffixes_to_search_when_resolving_a_module,
},

// Source Maps
{
Expand Down Expand Up @@ -3192,7 +3204,7 @@ namespace ts {
if (option.type === "list") {
const listOption = option;
if (listOption.element.isFilePath || !isString(listOption.element.type)) {
return filter(map(value, v => normalizeOptionValue(listOption.element, basePath, v)), v => !!v) as CompilerOptionsValue;
return filter(map(value, v => normalizeOptionValue(listOption.element, basePath, v)), v => listOption.listPreserveFalsyValues ? true : !!v) as CompilerOptionsValue;
}
return value;
}
Expand Down Expand Up @@ -3233,7 +3245,7 @@ namespace ts {
}

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

/**
Expand Down
4 changes: 4 additions & 0 deletions src/compiler/diagnosticMessages.json
Original file line number Diff line number Diff line change
Expand Up @@ -5878,6 +5878,10 @@
"category": "Message",
"code": 6930
},
"List of file name suffixes to search when resolving a module." : {
"category": "Error",
"code": 6931
},

"Variable '{0}' implicitly has an '{1}' type.": {
"category": "Error",
Expand Down
10 changes: 10 additions & 0 deletions src/compiler/moduleNameResolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1576,6 +1576,16 @@ namespace ts {

/** Return the file if it exists. */
function tryFile(fileName: string, onlyRecordFailures: boolean, state: ModuleResolutionState): string | undefined {
if (!state.compilerOptions.moduleSuffixes?.length) {
return tryFileLookup(fileName, onlyRecordFailures, state);
}

const ext = tryGetExtensionFromPath(fileName) ?? "";
const fileNameNoExtension = ext ? removeExtension(fileName, ext) : fileName;
return forEach(state.compilerOptions.moduleSuffixes, suffix => tryFileLookup(fileNameNoExtension + suffix + ext, onlyRecordFailures, state));
}

function tryFileLookup(fileName: string, onlyRecordFailures: boolean, state: ModuleResolutionState): string | undefined {
if (!onlyRecordFailures) {
if (state.host.fileExists(fileName)) {
if (state.traceEnabled) {
Expand Down
2 changes: 2 additions & 0 deletions src/compiler/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6149,6 +6149,7 @@ namespace ts {
maxNodeModuleJsDepth?: number;
module?: ModuleKind;
moduleResolution?: ModuleResolutionKind;
moduleSuffixes?: string[];
moduleDetection?: ModuleDetectionKind;
newLine?: NewLineKind;
noEmit?: boolean;
Expand Down Expand Up @@ -6453,6 +6454,7 @@ namespace ts {
export interface CommandLineOptionOfListType extends CommandLineOptionBase {
type: "list";
element: CommandLineOptionOfCustomType | CommandLineOptionOfStringType | CommandLineOptionOfNumberType | CommandLineOptionOfBooleanType | TsConfigOnlyOption;
listPreserveFalsyValues?: boolean;
}

/* @internal */
Expand Down
64 changes: 64 additions & 0 deletions src/testRunner/unittests/config/convertCompilerOptionsFromJson.ts
Original file line number Diff line number Diff line change
Expand Up @@ -420,6 +420,70 @@ namespace ts {
);
});

it("Convert empty string option of moduleSuffixes to compiler-options ", () => {
assertCompilerOptions(
{
compilerOptions: {
moduleSuffixes: [".ios", ""]
}
}, "tsconfig.json",
{
compilerOptions: {
moduleSuffixes: [".ios", ""]
},
errors: []
}
);
});

it("Convert empty string option of moduleSuffixes to compiler-options ", () => {
assertCompilerOptions(
{
compilerOptions: {
moduleSuffixes: [""]
}
}, "tsconfig.json",
{
compilerOptions: {
moduleSuffixes: [""]
},
errors: []
}
);
});

it("Convert trailing-whitespace string option of moduleSuffixes to compiler-options ", () => {
assertCompilerOptions(
{
compilerOptions: {
moduleSuffixes: [" "]
}
}, "tsconfig.json",
{
compilerOptions: {
moduleSuffixes: [" "]
},
errors: []
}
);
});

it("Convert empty option of moduleSuffixes to compiler-options ", () => {
assertCompilerOptions(
{
compilerOptions: {
moduleSuffixes: []
}
}, "tsconfig.json",
{
compilerOptions: {
moduleSuffixes: []
},
errors: []
}
);
});

it("Convert incorrectly format tsconfig.json to compiler-options ", () => {
assertCompilerOptions(
{
Expand Down
1 change: 1 addition & 0 deletions tests/baselines/reference/api/tsserverlibrary.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2977,6 +2977,7 @@ declare namespace ts {
maxNodeModuleJsDepth?: number;
module?: ModuleKind;
moduleResolution?: ModuleResolutionKind;
moduleSuffixes?: string[];
moduleDetection?: ModuleDetectionKind;
newLine?: NewLineKind;
noEmit?: boolean;
Expand Down
1 change: 1 addition & 0 deletions tests/baselines/reference/api/typescript.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2977,6 +2977,7 @@ declare namespace ts {
maxNodeModuleJsDepth?: number;
module?: ModuleKind;
moduleResolution?: ModuleResolutionKind;
moduleSuffixes?: string[];
moduleDetection?: ModuleDetectionKind;
newLine?: NewLineKind;
noEmit?: boolean;
Expand Down
17 changes: 17 additions & 0 deletions tests/baselines/reference/moduleResolutionWithSuffixes_empty.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
//// [tests/cases/compiler/moduleResolutionWithSuffixes_empty.ts] ////

//// [index.ts]
import { base } from "./foo";
//// [foo.ts]
export function base() {}


//// [foo.js]
"use strict";
exports.__esModule = true;
exports.base = void 0;
function base() { }
exports.base = base;
//// [index.js]
"use strict";
exports.__esModule = true;
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
=== /index.ts ===
import { base } from "./foo";
>base : Symbol(base, Decl(index.ts, 0, 8))

=== /foo.ts ===
export function base() {}
>base : Symbol(base, Decl(foo.ts, 0, 0))

Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
[
"======== Resolving module './foo' from '/index.ts'. ========",
"Explicitly specified module resolution kind: 'NodeJs'.",
"Loading module as file / folder, candidate module location '/foo', target file type 'TypeScript'.",
"File '/foo.ts' exist - use it as a name resolution result.",
"======== Module name './foo' was successfully resolved to '/foo.ts'. ========"
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
=== /index.ts ===
import { base } from "./foo";
>base : () => void

=== /foo.ts ===
export function base() {}
>base : () => void

Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
//// [tests/cases/compiler/moduleResolutionWithSuffixes_notSpecified.ts] ////

//// [index.ts]
import { base } from "./foo";
//// [foo.ts]
export function base() {}


//// [foo.js]
"use strict";
exports.__esModule = true;
exports.base = void 0;
function base() { }
exports.base = base;
//// [index.js]
"use strict";
exports.__esModule = true;
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
=== /index.ts ===
import { base } from "./foo";
>base : Symbol(base, Decl(index.ts, 0, 8))

=== /foo.ts ===
export function base() {}
>base : Symbol(base, Decl(foo.ts, 0, 0))

Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
[
"======== Resolving module './foo' from '/index.ts'. ========",
"Explicitly specified module resolution kind: 'NodeJs'.",
"Loading module as file / folder, candidate module location '/foo', target file type 'TypeScript'.",
"File '/foo.ts' exist - use it as a name resolution result.",
"======== Module name './foo' was successfully resolved to '/foo.ts'. ========"
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
=== /index.ts ===
import { base } from "./foo";
>base : () => void

=== /foo.ts ===
export function base() {}
>base : () => void

25 changes: 25 additions & 0 deletions tests/baselines/reference/moduleResolutionWithSuffixes_one.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
//// [tests/cases/compiler/moduleResolutionWithSuffixes_one.ts] ////

//// [index.ts]
import { ios } from "./foo";
//// [foo.ios.ts]
export function ios() {}
//// [foo.ts]
export function base() {}


//// [foo.ios.js]
"use strict";
exports.__esModule = true;
exports.ios = void 0;
function ios() { }
exports.ios = ios;
//// [index.js]
"use strict";
exports.__esModule = true;
//// [foo.js]
"use strict";
exports.__esModule = true;
exports.base = void 0;
function base() { }
exports.base = base;
12 changes: 12 additions & 0 deletions tests/baselines/reference/moduleResolutionWithSuffixes_one.symbols
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
=== /index.ts ===
import { ios } from "./foo";
>ios : Symbol(ios, Decl(index.ts, 0, 8))

=== /foo.ios.ts ===
export function ios() {}
>ios : Symbol(ios, Decl(foo.ios.ts, 0, 0))

=== /foo.ts ===
export function base() {}
>base : Symbol(base, Decl(foo.ts, 0, 0))

Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
[
"======== Resolving module './foo' from '/index.ts'. ========",
"Explicitly specified module resolution kind: 'NodeJs'.",
"Loading module as file / folder, candidate module location '/foo', target file type 'TypeScript'.",
"File '/foo.ios.ts' exist - use it as a name resolution result.",
"======== Module name './foo' was successfully resolved to '/foo.ios.ts'. ========"
]
12 changes: 12 additions & 0 deletions tests/baselines/reference/moduleResolutionWithSuffixes_one.types
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
=== /index.ts ===
import { ios } from "./foo";
>ios : () => void

=== /foo.ios.ts ===
export function ios() {}
>ios : () => void

=== /foo.ts ===
export function base() {}
>base : () => void

17 changes: 17 additions & 0 deletions tests/baselines/reference/moduleResolutionWithSuffixes_oneBlank.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
//// [tests/cases/compiler/moduleResolutionWithSuffixes_oneBlank.ts] ////

//// [index.ts]
import { base } from "./foo";
//// [foo.ts]
export function base() {}


//// [foo.js]
"use strict";
exports.__esModule = true;
exports.base = void 0;
function base() { }
exports.base = base;
//// [index.js]
"use strict";
exports.__esModule = true;
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
=== /index.ts ===
import { base } from "./foo";
>base : Symbol(base, Decl(index.ts, 0, 8))

=== /foo.ts ===
export function base() {}
>base : Symbol(base, Decl(foo.ts, 0, 0))

Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
[
"======== Resolving module './foo' from '/index.ts'. ========",
"Explicitly specified module resolution kind: 'NodeJs'.",
"Loading module as file / folder, candidate module location '/foo', target file type 'TypeScript'.",
"File '/foo.ts' exist - use it as a name resolution result.",
"======== Module name './foo' was successfully resolved to '/foo.ts'. ========"
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
=== /index.ts ===
import { base } from "./foo";
>base : () => void

=== /foo.ts ===
export function base() {}
>base : () => void

Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
/index.ts(1,21): error TS2307: Cannot find module './foo' or its corresponding type declarations.


==== /tsconfig.json (0 errors) ====
// moduleSuffixes has one entry but there isn't a matching file. Module resolution should fail.

{
"compilerOptions": {
"moduleResolution": "node",
"traceResolution": true,
"moduleSuffixes": [".ios"]
}
}

==== /index.ts (1 errors) ====
import { ios } from "./foo";
~~~~~~~
!!! error TS2307: Cannot find module './foo' or its corresponding type declarations.
==== /foo.ts (0 errors) ====
export function base() {}

Loading

0 comments on commit 41aca7c

Please sign in to comment.