Skip to content

Commit db75dc5

Browse files
gabrittomprobst
authored andcommitted
Improve errors on module: node12 and extensionless relative imports (microsoft#46486)
* add new error + suggestions * push down assert defined * remove comment * fix esm module import detection, update baselines * add and update new tests * fix review comments * remove renamed baseline references * update node modules test baselines * fix error message, add new tests * update old tests with new error message
1 parent 7a87b4c commit db75dc5

File tree

47 files changed

+854
-404
lines changed

Some content is hidden

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

47 files changed

+854
-404
lines changed

src/compiler/checker.ts

+35-3
Original file line numberDiff line numberDiff line change
@@ -1016,6 +1016,22 @@ namespace ts {
10161016
const builtinGlobals = createSymbolTable();
10171017
builtinGlobals.set(undefinedSymbol.escapedName, undefinedSymbol);
10181018

1019+
// Extensions suggested for path imports when module resolution is node12 or higher.
1020+
// The first element of each tuple is the extension a file has.
1021+
// The second element of each tuple is the extension that should be used in a path import.
1022+
// e.g. if we want to import file `foo.mts`, we should write `import {} from "./foo.mjs".
1023+
const suggestedExtensions: [string, string][] = [
1024+
[".mts", ".mjs"],
1025+
[".ts", ".js"],
1026+
[".cts", ".cjs"],
1027+
[".mjs", ".mjs"],
1028+
[".js", ".js"],
1029+
[".cjs", ".cjs"],
1030+
[".tsx", compilerOptions.jsx === JsxEmit.Preserve ? ".jsx" : ".js"],
1031+
[".jsx", ".jsx"],
1032+
[".json", ".json"],
1033+
];
1034+
10191035
initializeTypeChecker();
10201036

10211037
return checker;
@@ -3443,7 +3459,7 @@ namespace ts {
34433459
(isModuleDeclaration(location) ? location : location.parent && isModuleDeclaration(location.parent) && location.parent.name === location ? location.parent : undefined)?.name ||
34443460
(isLiteralImportTypeNode(location) ? location : undefined)?.argument.literal;
34453461
const mode = contextSpecifier && isStringLiteralLike(contextSpecifier) ? getModeForUsageLocation(currentSourceFile, contextSpecifier) : currentSourceFile.impliedNodeFormat;
3446-
const resolvedModule = getResolvedModule(currentSourceFile, moduleReference, mode)!; // TODO: GH#18217
3462+
const resolvedModule = getResolvedModule(currentSourceFile, moduleReference, mode);
34473463
const resolutionDiagnostic = resolvedModule && getResolutionDiagnostic(compilerOptions, resolvedModule);
34483464
const sourceFile = resolvedModule && !resolutionDiagnostic && host.getSourceFile(resolvedModule.resolvedFileName);
34493465
if (sourceFile) {
@@ -3489,10 +3505,10 @@ namespace ts {
34893505
if (resolvedModule && !resolutionExtensionIsTSOrJson(resolvedModule.extension) && resolutionDiagnostic === undefined || resolutionDiagnostic === Diagnostics.Could_not_find_a_declaration_file_for_module_0_1_implicitly_has_an_any_type) {
34903506
if (isForAugmentation) {
34913507
const diag = Diagnostics.Invalid_module_name_in_augmentation_Module_0_resolves_to_an_untyped_module_at_1_which_cannot_be_augmented;
3492-
error(errorNode, diag, moduleReference, resolvedModule.resolvedFileName);
3508+
error(errorNode, diag, moduleReference, resolvedModule!.resolvedFileName);
34933509
}
34943510
else {
3495-
errorOnImplicitAnyModule(/*isError*/ noImplicitAny && !!moduleNotFoundError, errorNode, resolvedModule, moduleReference);
3511+
errorOnImplicitAnyModule(/*isError*/ noImplicitAny && !!moduleNotFoundError, errorNode, resolvedModule!, moduleReference);
34963512
}
34973513
// Failed imports and untyped modules are both treated in an untyped manner; only difference is whether we give a diagnostic first.
34983514
return undefined;
@@ -3513,6 +3529,10 @@ namespace ts {
35133529
}
35143530
else {
35153531
const tsExtension = tryExtractTSExtension(moduleReference);
3532+
const isExtensionlessRelativePathImport = pathIsRelative(moduleReference) && !hasExtension(moduleReference);
3533+
const moduleResolutionKind = getEmitModuleResolutionKind(compilerOptions);
3534+
const resolutionIsNode12OrNext = moduleResolutionKind === ModuleResolutionKind.Node12 ||
3535+
moduleResolutionKind === ModuleResolutionKind.NodeNext;
35163536
if (tsExtension) {
35173537
const diag = Diagnostics.An_import_path_cannot_end_with_a_0_extension_Consider_importing_1_instead;
35183538
const importSourceWithoutExtension = removeExtension(moduleReference, tsExtension);
@@ -3532,6 +3552,18 @@ namespace ts {
35323552
hasJsonModuleEmitEnabled(compilerOptions)) {
35333553
error(errorNode, Diagnostics.Cannot_find_module_0_Consider_using_resolveJsonModule_to_import_module_with_json_extension, moduleReference);
35343554
}
3555+
else if (mode === ModuleKind.ESNext && resolutionIsNode12OrNext && isExtensionlessRelativePathImport) {
3556+
const absoluteRef = getNormalizedAbsolutePath(moduleReference, getDirectoryPath(currentSourceFile.path));
3557+
const suggestedExt = suggestedExtensions.find(([actualExt, _importExt]) => host.fileExists(absoluteRef + actualExt))?.[1];
3558+
if (suggestedExt) {
3559+
error(errorNode,
3560+
Diagnostics.Relative_import_paths_need_explicit_file_extensions_in_EcmaScript_imports_when_moduleResolution_is_node12_or_nodenext_Did_you_mean_0,
3561+
moduleReference + suggestedExt);
3562+
}
3563+
else {
3564+
error(errorNode, Diagnostics.Relative_import_paths_need_explicit_file_extensions_in_EcmaScript_imports_when_moduleResolution_is_node12_or_nodenext_Consider_adding_an_extension_to_the_import_path);
3565+
}
3566+
}
35353567
else {
35363568
error(errorNode, moduleNotFoundError, moduleReference);
35373569
}

src/compiler/diagnosticMessages.json

+8
Original file line numberDiff line numberDiff line change
@@ -3345,6 +3345,14 @@
33453345
"category": "Error",
33463346
"code": 2833
33473347
},
3348+
"Relative import paths need explicit file extensions in EcmaScript imports when '--moduleResolution' is 'node12' or 'nodenext'. Consider adding an extension to the import path.": {
3349+
"category": "Error",
3350+
"code": 2834
3351+
},
3352+
"Relative import paths need explicit file extensions in EcmaScript imports when '--moduleResolution' is 'node12' or 'nodenext'. Did you mean '{0}'?": {
3353+
"category": "Error",
3354+
"code": 2835
3355+
},
33483356

33493357
"Import declaration '{0}' is using private name '{1}'.": {
33503358
"category": "Error",

src/compiler/types.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -5992,7 +5992,7 @@ namespace ts {
59925992
export enum ModuleResolutionKind {
59935993
Classic = 1,
59945994
NodeJs = 2,
5995-
// Starting with node12, node's module resolver has significant departures from tranditional cjs resolution
5995+
// Starting with node12, node's module resolver has significant departures from traditional cjs resolution
59965996
// to better support ecmascript modules and their use within node - more features are still being added, so
59975997
// we can expect it to change over time, and as such, offer both a `NodeNext` moving resolution target, and a `Node12`
59985998
// version-anchored resolution target
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
/src/bar.mts(2,21): error TS2835: Relative import paths need explicit file extensions in EcmaScript imports when '--moduleResolution' is 'node12' or 'nodenext'. Did you mean './foo.mjs'?
2+
/src/bar.mts(3,21): error TS2834: Relative import paths need explicit file extensions in EcmaScript imports when '--moduleResolution' is 'node12' or 'nodenext'. Consider adding an extension to the import path.
3+
4+
5+
==== /src/foo.mts (0 errors) ====
6+
export function foo() {
7+
return "";
8+
}
9+
10+
==== /src/bar.mts (2 errors) ====
11+
// Extensionless relative path ES import in an ES module
12+
import { foo } from "./foo"; // should error, suggest adding ".mjs"
13+
~~~~~~~
14+
!!! error TS2835: Relative import paths need explicit file extensions in EcmaScript imports when '--moduleResolution' is 'node12' or 'nodenext'. Did you mean './foo.mjs'?
15+
import { baz } from "./baz"; // should error, ask for extension, no extension suggestion
16+
~~~~~~~
17+
!!! error TS2834: Relative import paths need explicit file extensions in EcmaScript imports when '--moduleResolution' is 'node12' or 'nodenext'. Consider adding an extension to the import path.
18+
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
//// [tests/cases/conformance/externalModules/moduleResolutionWithoutExtension1.ts] ////
2+
3+
//// [foo.mts]
4+
export function foo() {
5+
return "";
6+
}
7+
8+
//// [bar.mts]
9+
// Extensionless relative path ES import in an ES module
10+
import { foo } from "./foo"; // should error, suggest adding ".mjs"
11+
import { baz } from "./baz"; // should error, ask for extension, no extension suggestion
12+
13+
14+
//// [foo.mjs]
15+
export function foo() {
16+
return "";
17+
}
18+
//// [bar.mjs]
19+
export {};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
=== /src/foo.mts ===
2+
export function foo() {
3+
>foo : Symbol(foo, Decl(foo.mts, 0, 0))
4+
5+
return "";
6+
}
7+
8+
=== /src/bar.mts ===
9+
// Extensionless relative path ES import in an ES module
10+
import { foo } from "./foo"; // should error, suggest adding ".mjs"
11+
>foo : Symbol(foo, Decl(bar.mts, 1, 8))
12+
13+
import { baz } from "./baz"; // should error, ask for extension, no extension suggestion
14+
>baz : Symbol(baz, Decl(bar.mts, 2, 8))
15+
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
=== /src/foo.mts ===
2+
export function foo() {
3+
>foo : () => string
4+
5+
return "";
6+
>"" : ""
7+
}
8+
9+
=== /src/bar.mts ===
10+
// Extensionless relative path ES import in an ES module
11+
import { foo } from "./foo"; // should error, suggest adding ".mjs"
12+
>foo : any
13+
14+
import { baz } from "./baz"; // should error, ask for extension, no extension suggestion
15+
>baz : any
16+
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
/src/buzz.mts(2,22): error TS2307: Cannot find module './foo' or its corresponding type declarations.
2+
3+
4+
==== /src/buzz.mts (1 errors) ====
5+
// Extensionless relative path cjs import in an ES module
6+
import foo = require("./foo"); // should error, should not ask for extension
7+
~~~~~~~
8+
!!! error TS2307: Cannot find module './foo' or its corresponding type declarations.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
//// [buzz.mts]
2+
// Extensionless relative path cjs import in an ES module
3+
import foo = require("./foo"); // should error, should not ask for extension
4+
5+
//// [buzz.mjs]
6+
export {};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
=== /src/buzz.mts ===
2+
// Extensionless relative path cjs import in an ES module
3+
import foo = require("./foo"); // should error, should not ask for extension
4+
>foo : Symbol(foo, Decl(buzz.mts, 0, 0))
5+
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
=== /src/buzz.mts ===
2+
// Extensionless relative path cjs import in an ES module
3+
import foo = require("./foo"); // should error, should not ask for extension
4+
>foo : any
5+
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
/src/bar.mts(2,21): error TS2835: Relative import paths need explicit file extensions in EcmaScript imports when '--moduleResolution' is 'node12' or 'nodenext'. Did you mean './foo.jsx'?
2+
3+
4+
==== /src/foo.tsx (0 errors) ====
5+
export function foo() {
6+
return "";
7+
}
8+
9+
==== /src/bar.mts (1 errors) ====
10+
// Extensionless relative path ES import in an ES module
11+
import { foo } from "./foo"; // should error, suggest adding ".jsx"
12+
~~~~~~~
13+
!!! error TS2835: Relative import paths need explicit file extensions in EcmaScript imports when '--moduleResolution' is 'node12' or 'nodenext'. Did you mean './foo.jsx'?
14+
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
//// [tests/cases/conformance/externalModules/moduleResolutionWithoutExtension3.ts] ////
2+
3+
//// [foo.tsx]
4+
export function foo() {
5+
return "";
6+
}
7+
8+
//// [bar.mts]
9+
// Extensionless relative path ES import in an ES module
10+
import { foo } from "./foo"; // should error, suggest adding ".jsx"
11+
12+
13+
//// [foo.jsx]
14+
"use strict";
15+
Object.defineProperty(exports, "__esModule", { value: true });
16+
exports.foo = void 0;
17+
function foo() {
18+
return "";
19+
}
20+
exports.foo = foo;
21+
//// [bar.mjs]
22+
export {};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
=== /src/foo.tsx ===
2+
export function foo() {
3+
>foo : Symbol(foo, Decl(foo.tsx, 0, 0))
4+
5+
return "";
6+
}
7+
8+
=== /src/bar.mts ===
9+
// Extensionless relative path ES import in an ES module
10+
import { foo } from "./foo"; // should error, suggest adding ".jsx"
11+
>foo : Symbol(foo, Decl(bar.mts, 1, 8))
12+
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
=== /src/foo.tsx ===
2+
export function foo() {
3+
>foo : () => string
4+
5+
return "";
6+
>"" : ""
7+
}
8+
9+
=== /src/bar.mts ===
10+
// Extensionless relative path ES import in an ES module
11+
import { foo } from "./foo"; // should error, suggest adding ".jsx"
12+
>foo : any
13+
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
/src/bar.mts(2,21): error TS2835: Relative import paths need explicit file extensions in EcmaScript imports when '--moduleResolution' is 'node12' or 'nodenext'. Did you mean './foo.js'?
2+
3+
4+
==== /src/foo.tsx (0 errors) ====
5+
export function foo() {
6+
return "";
7+
}
8+
9+
==== /src/bar.mts (1 errors) ====
10+
// Extensionless relative path ES import in an ES module
11+
import { foo } from "./foo"; // should error, suggest adding ".js"
12+
~~~~~~~
13+
!!! error TS2835: Relative import paths need explicit file extensions in EcmaScript imports when '--moduleResolution' is 'node12' or 'nodenext'. Did you mean './foo.js'?
14+
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
//// [tests/cases/conformance/externalModules/moduleResolutionWithoutExtension4.ts] ////
2+
3+
//// [foo.tsx]
4+
export function foo() {
5+
return "";
6+
}
7+
8+
//// [bar.mts]
9+
// Extensionless relative path ES import in an ES module
10+
import { foo } from "./foo"; // should error, suggest adding ".js"
11+
12+
13+
//// [foo.js]
14+
"use strict";
15+
Object.defineProperty(exports, "__esModule", { value: true });
16+
exports.foo = void 0;
17+
function foo() {
18+
return "";
19+
}
20+
exports.foo = foo;
21+
//// [bar.mjs]
22+
export {};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
=== /src/foo.tsx ===
2+
export function foo() {
3+
>foo : Symbol(foo, Decl(foo.tsx, 0, 0))
4+
5+
return "";
6+
}
7+
8+
=== /src/bar.mts ===
9+
// Extensionless relative path ES import in an ES module
10+
import { foo } from "./foo"; // should error, suggest adding ".js"
11+
>foo : Symbol(foo, Decl(bar.mts, 1, 8))
12+
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
=== /src/foo.tsx ===
2+
export function foo() {
3+
>foo : () => string
4+
5+
return "";
6+
>"" : ""
7+
}
8+
9+
=== /src/bar.mts ===
10+
// Extensionless relative path ES import in an ES module
11+
import { foo } from "./foo"; // should error, suggest adding ".js"
12+
>foo : any
13+
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
/src/buzz.mts(2,8): error TS2834: Relative import paths need explicit file extensions in EcmaScript imports when '--moduleResolution' is 'node12' or 'nodenext'. Consider adding an extension to the import path.
2+
3+
4+
==== /src/buzz.mts (1 errors) ====
5+
// Extensionless relative path dynamic import in an ES module
6+
import("./foo").then(x => x); // should error, ask for extension
7+
~~~~~~~
8+
!!! error TS2834: Relative import paths need explicit file extensions in EcmaScript imports when '--moduleResolution' is 'node12' or 'nodenext'. Consider adding an extension to the import path.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
//// [buzz.mts]
2+
// Extensionless relative path dynamic import in an ES module
3+
import("./foo").then(x => x); // should error, ask for extension
4+
5+
//// [buzz.mjs]
6+
// Extensionless relative path dynamic import in an ES module
7+
import("./foo").then(x => x); // should error, ask for extension
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
=== /src/buzz.mts ===
2+
// Extensionless relative path dynamic import in an ES module
3+
import("./foo").then(x => x); // should error, ask for extension
4+
>import("./foo").then : Symbol(Promise.then, Decl(lib.es5.d.ts, --, --))
5+
>then : Symbol(Promise.then, Decl(lib.es5.d.ts, --, --))
6+
>x : Symbol(x, Decl(buzz.mts, 1, 21))
7+
>x : Symbol(x, Decl(buzz.mts, 1, 21))
8+
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
=== /src/buzz.mts ===
2+
// Extensionless relative path dynamic import in an ES module
3+
import("./foo").then(x => x); // should error, ask for extension
4+
>import("./foo").then(x => x) : Promise<any>
5+
>import("./foo").then : <TResult1 = any, TResult2 = never>(onfulfilled?: (value: any) => TResult1 | PromiseLike<TResult1>, onrejected?: (reason: any) => TResult2 | PromiseLike<TResult2>) => Promise<TResult1 | TResult2>
6+
>import("./foo") : Promise<any>
7+
>"./foo" : "./foo"
8+
>then : <TResult1 = any, TResult2 = never>(onfulfilled?: (value: any) => TResult1 | PromiseLike<TResult1>, onrejected?: (reason: any) => TResult2 | PromiseLike<TResult2>) => Promise<TResult1 | TResult2>
9+
>x => x : (x: any) => any
10+
>x : any
11+
>x : any
12+
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
/src/bar.cts(4,21): error TS2307: Cannot find module './foo' or its corresponding type declarations.
2+
3+
4+
==== /src/bar.cts (1 errors) ====
5+
// Extensionless relative path import statement in a cjs module
6+
// Import statements are not allowed in cjs files,
7+
// but other errors should not assume that they are allowed
8+
import { foo } from "./foo"; // should error, should not ask for extension
9+
~~~~~~~
10+
!!! error TS2307: Cannot find module './foo' or its corresponding type declarations.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
//// [bar.cts]
2+
// Extensionless relative path import statement in a cjs module
3+
// Import statements are not allowed in cjs files,
4+
// but other errors should not assume that they are allowed
5+
import { foo } from "./foo"; // should error, should not ask for extension
6+
7+
//// [bar.cjs]
8+
"use strict";
9+
Object.defineProperty(exports, "__esModule", { value: true });

0 commit comments

Comments
 (0)