-
Notifications
You must be signed in to change notification settings - Fork 12.9k
Improve errors on module: node12 and extensionless relative imports #46486
New issue
Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? # to your account
Changes from all commits
8811903
ffcaf66
9c9899c
7166cbe
998dbb1
e8a1b7f
4e4a168
685e98f
f89332e
4b0da27
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -1015,6 +1015,22 @@ namespace ts { | |
const builtinGlobals = createSymbolTable(); | ||
builtinGlobals.set(undefinedSymbol.escapedName, undefinedSymbol); | ||
|
||
// Extensions suggested for path imports when module resolution is node12 or higher. | ||
// The first element of each tuple is the extension a file has. | ||
// The second element of each tuple is the extension that should be used in a path import. | ||
// e.g. if we want to import file `foo.mts`, we should write `import {} from "./foo.mjs". | ||
const suggestedExtensions: [string, string][] = [ | ||
[".mts", ".mjs"], | ||
[".ts", ".js"], | ||
[".cts", ".cjs"], | ||
[".mjs", ".mjs"], | ||
[".js", ".js"], | ||
[".cjs", ".cjs"], | ||
gabritto marked this conversation as resolved.
Show resolved
Hide resolved
|
||
[".tsx", compilerOptions.jsx === JsxEmit.Preserve ? ".jsx" : ".js"], | ||
[".jsx", ".jsx"], | ||
[".json", ".json"], | ||
]; | ||
|
||
initializeTypeChecker(); | ||
|
||
return checker; | ||
|
@@ -3417,7 +3433,7 @@ namespace ts { | |
(isModuleDeclaration(location) ? location : location.parent && isModuleDeclaration(location.parent) && location.parent.name === location ? location.parent : undefined)?.name || | ||
(isLiteralImportTypeNode(location) ? location : undefined)?.argument.literal; | ||
const mode = contextSpecifier && isStringLiteralLike(contextSpecifier) ? getModeForUsageLocation(currentSourceFile, contextSpecifier) : currentSourceFile.impliedNodeFormat; | ||
const resolvedModule = getResolvedModule(currentSourceFile, moduleReference, mode)!; // TODO: GH#18217 | ||
const resolvedModule = getResolvedModule(currentSourceFile, moduleReference, mode); | ||
const resolutionDiagnostic = resolvedModule && getResolutionDiagnostic(compilerOptions, resolvedModule); | ||
const sourceFile = resolvedModule && !resolutionDiagnostic && host.getSourceFile(resolvedModule.resolvedFileName); | ||
if (sourceFile) { | ||
|
@@ -3460,10 +3476,10 @@ namespace ts { | |
if (resolvedModule && !resolutionExtensionIsTSOrJson(resolvedModule.extension) && resolutionDiagnostic === undefined || resolutionDiagnostic === Diagnostics.Could_not_find_a_declaration_file_for_module_0_1_implicitly_has_an_any_type) { | ||
if (isForAugmentation) { | ||
const diag = Diagnostics.Invalid_module_name_in_augmentation_Module_0_resolves_to_an_untyped_module_at_1_which_cannot_be_augmented; | ||
error(errorNode, diag, moduleReference, resolvedModule.resolvedFileName); | ||
error(errorNode, diag, moduleReference, resolvedModule!.resolvedFileName); | ||
} | ||
else { | ||
errorOnImplicitAnyModule(/*isError*/ noImplicitAny && !!moduleNotFoundError, errorNode, resolvedModule, moduleReference); | ||
errorOnImplicitAnyModule(/*isError*/ noImplicitAny && !!moduleNotFoundError, errorNode, resolvedModule!, moduleReference); | ||
} | ||
// Failed imports and untyped modules are both treated in an untyped manner; only difference is whether we give a diagnostic first. | ||
return undefined; | ||
|
@@ -3484,6 +3500,10 @@ namespace ts { | |
} | ||
else { | ||
gabritto marked this conversation as resolved.
Show resolved
Hide resolved
|
||
const tsExtension = tryExtractTSExtension(moduleReference); | ||
const isExtensionlessRelativePathImport = pathIsRelative(moduleReference) && !hasExtension(moduleReference); | ||
const moduleResolutionKind = getEmitModuleResolutionKind(compilerOptions); | ||
const resolutionIsNode12OrNext = moduleResolutionKind === ModuleResolutionKind.Node12 || | ||
moduleResolutionKind === ModuleResolutionKind.NodeNext; | ||
if (tsExtension) { | ||
const diag = Diagnostics.An_import_path_cannot_end_with_a_0_extension_Consider_importing_1_instead; | ||
const importSourceWithoutExtension = removeExtension(moduleReference, tsExtension); | ||
|
@@ -3503,6 +3523,18 @@ namespace ts { | |
hasJsonModuleEmitEnabled(compilerOptions)) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This probes the FS - do we have any concerns there from a performance standpoint? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Generally the answer is “no” when we’re about to emit an error, but it is kind of notable that nowhere else in the checker uses On the other hand, this error message could be a pretty hot path if you take a big codebase and flip it from There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is this something we can move into the program construction phase then? Do we ever build errors there? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If you're going to probe a bunch of paths in the same directory, it can be faster to use readdir and then compare against the results yourself. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There's some danger that people will see this thousands of times when they first change the setting, but I'm a little reluctant to "optimize" it without some way to establish that the change helped. Probably better to keep it simple until we measure a problem. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
After thinking about this more, I think maybe the ideal implementation would probe the filesystem during module resolution—when ESM-mode module resolution fails, it could try CJS-mode resolution and attach the result to the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It's probably worth noting that the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think @andrewbranch's suggestion is a good one for suggesting extensions. I don't think I could get this ready for the RC, though. Does it still make sense to include this PR as is? I think the error message without the suggestion is going to remain as is, so that should go in. I think it would only make sense to remove the extension suggestion message/check if we have performance concerns over calling |
||
error(errorNode, Diagnostics.Cannot_find_module_0_Consider_using_resolveJsonModule_to_import_module_with_json_extension, moduleReference); | ||
} | ||
else if (mode === ModuleKind.ESNext && resolutionIsNode12OrNext && isExtensionlessRelativePathImport) { | ||
gabritto marked this conversation as resolved.
Show resolved
Hide resolved
|
||
const absoluteRef = getNormalizedAbsolutePath(moduleReference, getDirectoryPath(currentSourceFile.path)); | ||
const suggestedExt = suggestedExtensions.find(([actualExt, _importExt]) => host.fileExists(absoluteRef + actualExt))?.[1]; | ||
if (suggestedExt) { | ||
error(errorNode, | ||
Diagnostics.Relative_import_paths_need_explicit_file_extensions_in_EcmaScript_imports_when_moduleResolution_is_node12_or_nodenext_Did_you_mean_0, | ||
moduleReference + suggestedExt); | ||
} | ||
else { | ||
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); | ||
} | ||
} | ||
else { | ||
error(errorNode, moduleNotFoundError, moduleReference); | ||
} | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
/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'? | ||
/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. | ||
|
||
|
||
==== /src/foo.mts (0 errors) ==== | ||
export function foo() { | ||
return ""; | ||
} | ||
|
||
==== /src/bar.mts (2 errors) ==== | ||
// Extensionless relative path ES import in an ES module | ||
import { foo } from "./foo"; // should error, suggest adding ".mjs" | ||
~~~~~~~ | ||
!!! error TS2835: Relative import paths need explicit file extensions in EcmaScript imports when '--moduleResolution' is 'node12' or 'nodenext'. Did you mean './foo.mjs'? | ||
import { baz } from "./baz"; // should error, ask for extension, no extension suggestion | ||
~~~~~~~ | ||
!!! 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 number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
//// [tests/cases/conformance/externalModules/moduleResolutionWithoutExtension1.ts] //// | ||
|
||
//// [foo.mts] | ||
export function foo() { | ||
return ""; | ||
} | ||
|
||
//// [bar.mts] | ||
// Extensionless relative path ES import in an ES module | ||
import { foo } from "./foo"; // should error, suggest adding ".mjs" | ||
import { baz } from "./baz"; // should error, ask for extension, no extension suggestion | ||
|
||
|
||
//// [foo.mjs] | ||
export function foo() { | ||
return ""; | ||
} | ||
//// [bar.mjs] | ||
export {}; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
=== /src/foo.mts === | ||
export function foo() { | ||
>foo : Symbol(foo, Decl(foo.mts, 0, 0)) | ||
|
||
return ""; | ||
} | ||
|
||
=== /src/bar.mts === | ||
// Extensionless relative path ES import in an ES module | ||
import { foo } from "./foo"; // should error, suggest adding ".mjs" | ||
>foo : Symbol(foo, Decl(bar.mts, 1, 8)) | ||
|
||
import { baz } from "./baz"; // should error, ask for extension, no extension suggestion | ||
>baz : Symbol(baz, Decl(bar.mts, 2, 8)) | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
=== /src/foo.mts === | ||
export function foo() { | ||
>foo : () => string | ||
|
||
return ""; | ||
>"" : "" | ||
} | ||
|
||
=== /src/bar.mts === | ||
// Extensionless relative path ES import in an ES module | ||
import { foo } from "./foo"; // should error, suggest adding ".mjs" | ||
>foo : any | ||
|
||
import { baz } from "./baz"; // should error, ask for extension, no extension suggestion | ||
>baz : any | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
/src/buzz.mts(2,22): error TS2307: Cannot find module './foo' or its corresponding type declarations. | ||
|
||
|
||
==== /src/buzz.mts (1 errors) ==== | ||
// Extensionless relative path cjs import in an ES module | ||
import foo = require("./foo"); // should error, should not ask for extension | ||
~~~~~~~ | ||
!!! error TS2307: Cannot find module './foo' or its corresponding type declarations. |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
//// [buzz.mts] | ||
// Extensionless relative path cjs import in an ES module | ||
import foo = require("./foo"); // should error, should not ask for extension | ||
|
||
//// [buzz.mjs] | ||
export {}; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
=== /src/buzz.mts === | ||
// Extensionless relative path cjs import in an ES module | ||
import foo = require("./foo"); // should error, should not ask for extension | ||
>foo : Symbol(foo, Decl(buzz.mts, 0, 0)) | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
=== /src/buzz.mts === | ||
// Extensionless relative path cjs import in an ES module | ||
import foo = require("./foo"); // should error, should not ask for extension | ||
>foo : any | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
/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'? | ||
|
||
|
||
==== /src/foo.tsx (0 errors) ==== | ||
export function foo() { | ||
return ""; | ||
} | ||
|
||
==== /src/bar.mts (1 errors) ==== | ||
// Extensionless relative path ES import in an ES module | ||
import { foo } from "./foo"; // should error, suggest adding ".jsx" | ||
~~~~~~~ | ||
!!! error TS2835: Relative import paths need explicit file extensions in EcmaScript imports when '--moduleResolution' is 'node12' or 'nodenext'. Did you mean './foo.jsx'? | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
//// [tests/cases/conformance/externalModules/moduleResolutionWithoutExtension3.ts] //// | ||
|
||
//// [foo.tsx] | ||
export function foo() { | ||
return ""; | ||
} | ||
|
||
//// [bar.mts] | ||
// Extensionless relative path ES import in an ES module | ||
import { foo } from "./foo"; // should error, suggest adding ".jsx" | ||
|
||
|
||
//// [foo.jsx] | ||
"use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
exports.foo = void 0; | ||
function foo() { | ||
return ""; | ||
} | ||
exports.foo = foo; | ||
//// [bar.mjs] | ||
export {}; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
=== /src/foo.tsx === | ||
export function foo() { | ||
>foo : Symbol(foo, Decl(foo.tsx, 0, 0)) | ||
|
||
return ""; | ||
} | ||
|
||
=== /src/bar.mts === | ||
// Extensionless relative path ES import in an ES module | ||
import { foo } from "./foo"; // should error, suggest adding ".jsx" | ||
>foo : Symbol(foo, Decl(bar.mts, 1, 8)) | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
=== /src/foo.tsx === | ||
export function foo() { | ||
>foo : () => string | ||
|
||
return ""; | ||
>"" : "" | ||
} | ||
|
||
=== /src/bar.mts === | ||
// Extensionless relative path ES import in an ES module | ||
import { foo } from "./foo"; // should error, suggest adding ".jsx" | ||
>foo : any | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
/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'? | ||
|
||
|
||
==== /src/foo.tsx (0 errors) ==== | ||
export function foo() { | ||
return ""; | ||
} | ||
|
||
==== /src/bar.mts (1 errors) ==== | ||
// Extensionless relative path ES import in an ES module | ||
import { foo } from "./foo"; // should error, suggest adding ".js" | ||
~~~~~~~ | ||
!!! error TS2835: Relative import paths need explicit file extensions in EcmaScript imports when '--moduleResolution' is 'node12' or 'nodenext'. Did you mean './foo.js'? | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
//// [tests/cases/conformance/externalModules/moduleResolutionWithoutExtension4.ts] //// | ||
|
||
//// [foo.tsx] | ||
export function foo() { | ||
return ""; | ||
} | ||
|
||
//// [bar.mts] | ||
// Extensionless relative path ES import in an ES module | ||
import { foo } from "./foo"; // should error, suggest adding ".js" | ||
|
||
|
||
//// [foo.js] | ||
"use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
exports.foo = void 0; | ||
function foo() { | ||
return ""; | ||
} | ||
exports.foo = foo; | ||
//// [bar.mjs] | ||
export {}; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
=== /src/foo.tsx === | ||
export function foo() { | ||
>foo : Symbol(foo, Decl(foo.tsx, 0, 0)) | ||
|
||
return ""; | ||
} | ||
|
||
=== /src/bar.mts === | ||
// Extensionless relative path ES import in an ES module | ||
import { foo } from "./foo"; // should error, suggest adding ".js" | ||
>foo : Symbol(foo, Decl(bar.mts, 1, 8)) | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
=== /src/foo.tsx === | ||
export function foo() { | ||
>foo : () => string | ||
|
||
return ""; | ||
>"" : "" | ||
} | ||
|
||
=== /src/bar.mts === | ||
// Extensionless relative path ES import in an ES module | ||
import { foo } from "./foo"; // should error, suggest adding ".js" | ||
>foo : any | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
/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. | ||
|
||
|
||
==== /src/buzz.mts (1 errors) ==== | ||
// Extensionless relative path dynamic import in an ES module | ||
import("./foo").then(x => x); // should error, ask for extension | ||
~~~~~~~ | ||
!!! 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 number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
//// [buzz.mts] | ||
// Extensionless relative path dynamic import in an ES module | ||
import("./foo").then(x => x); // should error, ask for extension | ||
|
||
//// [buzz.mjs] | ||
// Extensionless relative path dynamic import in an ES module | ||
import("./foo").then(x => x); // should error, ask for extension |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
=== /src/buzz.mts === | ||
// Extensionless relative path dynamic import in an ES module | ||
import("./foo").then(x => x); // should error, ask for extension | ||
>import("./foo").then : Symbol(Promise.then, Decl(lib.es5.d.ts, --, --)) | ||
>then : Symbol(Promise.then, Decl(lib.es5.d.ts, --, --)) | ||
>x : Symbol(x, Decl(buzz.mts, 1, 21)) | ||
>x : Symbol(x, Decl(buzz.mts, 1, 21)) | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
=== /src/buzz.mts === | ||
// Extensionless relative path dynamic import in an ES module | ||
import("./foo").then(x => x); // should error, ask for extension | ||
>import("./foo").then(x => x) : Promise<any> | ||
>import("./foo").then : <TResult1 = any, TResult2 = never>(onfulfilled?: (value: any) => TResult1 | PromiseLike<TResult1>, onrejected?: (reason: any) => TResult2 | PromiseLike<TResult2>) => Promise<TResult1 | TResult2> | ||
>import("./foo") : Promise<any> | ||
>"./foo" : "./foo" | ||
>then : <TResult1 = any, TResult2 = never>(onfulfilled?: (value: any) => TResult1 | PromiseLike<TResult1>, onrejected?: (reason: any) => TResult2 | PromiseLike<TResult2>) => Promise<TResult1 | TResult2> | ||
>x => x : (x: any) => any | ||
>x : any | ||
>x : any | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
/src/bar.cts(4,21): error TS2307: Cannot find module './foo' or its corresponding type declarations. | ||
|
||
|
||
==== /src/bar.cts (1 errors) ==== | ||
// Extensionless relative path import statement in a cjs module | ||
// Import statements are not allowed in cjs files, | ||
// but other errors should not assume that they are allowed | ||
import { foo } from "./foo"; // should error, should not ask for extension | ||
~~~~~~~ | ||
!!! error TS2307: Cannot find module './foo' or its corresponding type declarations. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. For future reference, I think it's a bit weird that the error message here is TS2307 "Cannot find module", instead of something like TS1471: "Module '{0}' cannot be imported using this construct". |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
//// [bar.cts] | ||
// Extensionless relative path import statement in a cjs module | ||
// Import statements are not allowed in cjs files, | ||
// but other errors should not assume that they are allowed | ||
import { foo } from "./foo"; // should error, should not ask for extension | ||
|
||
//// [bar.cjs] | ||
"use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); |
Uh oh!
There was an error while loading. Please reload this page.