-
Notifications
You must be signed in to change notification settings - Fork 13.8k
Improve errors on module: node12 and extensionless relative imports #46486
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 3 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,18 @@ 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.
Outdated
|
||
|
|
||
| initializeTypeChecker(); | ||
|
|
||
| return checker; | ||
|
|
@@ -3417,7 +3429,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 +3472,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); // TODO: GH#18217 | ||
|
gabritto marked this conversation as resolved.
Outdated
|
||
| } | ||
| else { | ||
| errorOnImplicitAnyModule(/*isError*/ noImplicitAny && !!moduleNotFoundError, errorNode, resolvedModule, moduleReference); | ||
| errorOnImplicitAnyModule(/*isError*/ noImplicitAny && !!moduleNotFoundError, errorNode, resolvedModule!, moduleReference); // TODO: GH#18217 | ||
| } | ||
| // 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 +3496,9 @@ namespace ts { | |
| } | ||
| else { | ||
| const tsExtension = tryExtractTSExtension(moduleReference); | ||
| const isESMFile = currentSourceFile.impliedNodeFormat === ModuleKind.ESNext; | ||
| const isExtensionlessRelativePathImport = pathIsRelative(moduleReference) && !hasExtension(moduleReference); | ||
| const resolutionIsNode12OrHigher = getEmitModuleResolutionKind(compilerOptions) >= ModuleResolutionKind.Node12; | ||
|
gabritto marked this conversation as resolved.
Outdated
|
||
| 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 +3518,18 @@ namespace ts { | |
| hasJsonModuleEmitEnabled(compilerOptions)) { | ||
| error(errorNode, Diagnostics.Cannot_find_module_0_Consider_using_resolveJsonModule_to_import_module_with_json_extension, moduleReference); | ||
| } | ||
| else if (isESMFile && resolutionIsNode12OrHigher && isExtensionlessRelativePathImport) { | ||
|
gabritto marked this conversation as resolved.
Outdated
|
||
| const absoluteRef = getNormalizedAbsolutePath(moduleReference, getDirectoryPath(currentSourceFile.path)); | ||
| const suggestedExt = suggestedExtensions.find(([actualExt, _importExt]) => host.fileExists(absoluteRef + actualExt))?.[1]; | ||
|
Member
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?
Member
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
Member
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?
Member
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.
Member
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.
Member
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
Member
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
Member
Author
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 Andrew Branch (@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 |
||
| if (suggestedExt) { | ||
| error(errorNode, | ||
| Diagnostics.Cannot_use_a_relative_import_path_without_an_extension_in_an_ES_module_when_moduleResolution_is_node12_or_higher_Did_you_mean_0, | ||
|
gabritto marked this conversation as resolved.
|
||
| moduleReference + suggestedExt); | ||
| } | ||
| else { | ||
| error(errorNode, Diagnostics.Cannot_use_a_relative_import_path_without_an_extension_in_an_ES_module_when_moduleResolution_is_node12_or_higher); | ||
| } | ||
| } | ||
| else { | ||
| error(errorNode, moduleNotFoundError, moduleReference); | ||
| } | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,20 @@ | ||
| /src/bar.mts(1,21): error TS2835: Cannot use a relative import path without an extension in an ES module when '--moduleResolution' is 'node12' or higher. Did you mean './foo.mjs'? | ||
| /src/bar.mts(2,21): error TS2834: Cannot use a relative import path without an extension in an ES module when '--moduleResolution' is 'node12' or higher. | ||
|
|
||
|
|
||
| ==== /src/foo.mts (0 errors) ==== | ||
| export function foo() { | ||
| return ""; | ||
| } | ||
|
|
||
| // Extensionless relative import in an ES module | ||
| ==== /src/bar.mts (2 errors) ==== | ||
| import { foo } from "./foo"; | ||
| ~~~~~~~ | ||
| !!! error TS2835: Cannot use a relative import path without an extension in an ES module when '--moduleResolution' is 'node12' or higher. Did you mean './foo.mjs'? | ||
| import { baz } from "./baz"; | ||
| ~~~~~~~ | ||
| !!! error TS2834: Cannot use a relative import path without an extension in an ES module when '--moduleResolution' is 'node12' or higher. | ||
| foo; | ||
| baz; | ||
|
|
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,25 @@ | ||
| //// [tests/cases/conformance/externalModules/moduleResolutionWithoutExtensions.ts] //// | ||
|
|
||
| //// [foo.mts] | ||
| export function foo() { | ||
| return ""; | ||
| } | ||
|
|
||
| // Extensionless relative import in an ES module | ||
| //// [bar.mts] | ||
| import { foo } from "./foo"; | ||
| import { baz } from "./baz"; | ||
| foo; | ||
| baz; | ||
|
|
||
|
|
||
| //// [foo.mjs] | ||
| export function foo() { | ||
| return ""; | ||
| } | ||
| // Extensionless relative import in an ES module | ||
| //// [bar.mjs] | ||
| import { foo } from "./foo"; | ||
| import { baz } from "./baz"; | ||
| foo; | ||
| baz; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,21 @@ | ||
| === /src/foo.mts === | ||
| export function foo() { | ||
| >foo : Symbol(foo, Decl(foo.mts, 0, 0)) | ||
|
|
||
| return ""; | ||
| } | ||
|
|
||
| // Extensionless relative import in an ES module | ||
| === /src/bar.mts === | ||
| import { foo } from "./foo"; | ||
| >foo : Symbol(foo, Decl(bar.mts, 0, 8)) | ||
|
|
||
| import { baz } from "./baz"; | ||
| >baz : Symbol(baz, Decl(bar.mts, 1, 8)) | ||
|
|
||
| foo; | ||
| >foo : Symbol(foo, Decl(bar.mts, 0, 8)) | ||
|
|
||
| baz; | ||
| >baz : Symbol(baz, Decl(bar.mts, 1, 8)) | ||
|
|
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,22 @@ | ||
| === /src/foo.mts === | ||
| export function foo() { | ||
| >foo : () => string | ||
|
|
||
| return ""; | ||
| >"" : "" | ||
| } | ||
|
|
||
| // Extensionless relative import in an ES module | ||
| === /src/bar.mts === | ||
| import { foo } from "./foo"; | ||
| >foo : any | ||
|
|
||
| import { baz } from "./baz"; | ||
| >baz : any | ||
|
|
||
| foo; | ||
| >foo : any | ||
|
|
||
| baz; | ||
| >baz : any | ||
|
|
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,14 @@ | ||
| // @moduleResolution: node12 | ||
| // @module: node12 | ||
|
|
||
| // @filename: /src/foo.mts | ||
| export function foo() { | ||
| return ""; | ||
| } | ||
|
|
||
| // Extensionless relative import in an ES module | ||
| // @Filename: /src/bar.mts | ||
| import { foo } from "./foo"; | ||
| import { baz } from "./baz"; | ||
| foo; | ||
| baz; |
Uh oh!
There was an error while loading. Please reload this page.