Skip to content
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

--moduleResolution bundler: Require ESM for module and remove node from hard-coded conditions #52940

Merged
merged 3 commits into from
Feb 24, 2023

Conversation

andrewbranch
Copy link
Member

Discussion: #52926, #51669 (comment)

Also fixes some missing traces and makes some minor error message tweaks.

@typescript-bot typescript-bot added Author: Team For Uncommitted Bug PR for untriaged, rejected, closed or missing bug labels Feb 23, 2023
Comment on lines 5021 to 5028
if (emitModuleKindIsNonNodeESM(moduleKind) || mode === ModuleKind.ESNext) {
return importSourceWithoutExtension + (tsExtension === Extension.Mts ? ".mjs" : tsExtension === Extension.Cts ? ".cjs" : ".js");
const preferTs = isDeclarationFileName(moduleReference) && shouldAllowImportingTsExtension(compilerOptions);
const ext =
tsExtension === Extension.Mts || tsExtension === Extension.Dmts ? preferTs ? ".mts" : ".mjs" :
tsExtension === Extension.Cts || tsExtension === Extension.Dmts ? preferTs ? ".cts" : ".cjs" :
preferTs ? ".ts" : ".js";
return importSourceWithoutExtension + ext;
}
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It has never made a ton of sense to predicate adding an extension to the suggested import source based on module and not moduleResolution, but rather than rethink that whole issue, I just made a narrow fix for when .ts is allowed and intended but the message suggests .js.

Comment on lines -43966 to 43971
else if (!(node.flags & NodeFlags.Ambient) && getEmitModuleResolutionKind(compilerOptions) === ModuleResolutionKind.Bundler) {
grammarErrorOnNode(node, Diagnostics.Import_assignment_is_not_allowed_when_moduleResolution_is_set_to_bundler_Consider_using_import_Asterisk_as_ns_from_mod_import_a_from_mod_import_d_from_mod_or_another_module_format_instead);
}
}
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can drop this error because it’s redundant with the one that already existed for using import= in --module esnext. That makes me feel even more like this is a coherent move.

Comment on lines 708 to 713
const conditions = esmMode || getEmitModuleResolutionKind(options) === ModuleResolutionKind.Bundler
? ["node", "import"]
: ["node", "require"];
? ["import"]
: ["require"];
if (getEmitModuleResolutionKind(options) !== ModuleResolutionKind.Bundler) {
conditions.unshift("node");
}
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Someone suggested dropping node from the hard-coded set of conditions when moduleResolution is bundler and I think that makes sense. I didn’t do it at first because Webpack includes the node condition, oddly enough, but esbuild doesn’t unless its target is set to Node. It definitely makes more sense to leave it up to customConditions.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Small nit suggestion: Instead of unshift can we first add "node" and then others so we arent moving array elements

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think the order matters; I just didn’t want to change a bunch of baselines unnecessarily. I did unshift because I remember Ron saying something about if you make an empty array and then push a non-number into it, that does a deopt. Unshift is probably worse though... I think I’ll just push node on last and change some traces.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Interestingly conditions can be a Set|undefined i think.. we always check for contains(array, key) so may be set is better.. But thats not important for this PR.. just mentioning.

@@ -83,5 +83,5 @@ error TS6054: File '/project/e.txt' has an unsupported extension. The only suppo
import {} from "./a.ts";
import {} from "./a.d.ts";
~~~~~~~~~~
!!! error TS2846: A declaration file cannot be imported without 'import type'. Did you mean to import an implementation file './a' instead?
!!! error TS2846: A declaration file cannot be imported without 'import type'. Did you mean to import an implementation file './a.ts' instead?
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the error message that changed based on my small addition in the checker. It was suggesting ./a.js before, which is ok, but looks a bit weird when the user has explicitly opted into allowing .ts extensions.

@Andarist
Copy link
Contributor

Out of curiosity - did you consider any special handling for the module condition? It has some special meaning in bundlers - both ESM imports and CJS requires are allowed to load it (even though its code is authored in ESM)

@andrewbranch
Copy link
Member Author

I didn’t, but because we lack the ability to remove conditions, I feel like the best move is to be conservative. Do all bundlers always set the module condition? Even if this is true today, I wouldn’t want to count on it.

@Andarist
Copy link
Contributor

Do all bundlers always set the module condition? Even if this is true today, I wouldn’t want to count on it.

Definitely not all of them. I know that it's supported by:

  • webpack5
  • the Rollup versions that understand conditions
  • esbuild (only in recent versions, previously you could enable it just like any other condition)
  • bun

I can't say - without further testing - that it works exactly in the same way in all of them. But generally speaking most bundlers are more permissive when it comes to mixing CJS and ESM, so when that's enabled I would expect all of them to allow requiring a source that is written in ESM. This is the very goal of this condition - bundlers didn't want to not have any escape hatch from the node's strict rules. For that reason, I ship the module condition in all libs that I maintain (or at least in those that target browsers)

@andrewbranch
Copy link
Member Author

--moduleResolution bundler doesn’t check requires at all. But I can tell you from testing many bundlers that there is no coherent pattern across most bundlers as to whether using require in a given file is allowed.

@andrewbranch
Copy link
Member Author

This is totally unrelated to the default set of conditions though. Conditions cannot affect how their target module kind is interpreted. An import condition pointing to a CJS module is a violation of convention only.

Comment on lines 708 to 713
const conditions = esmMode || getEmitModuleResolutionKind(options) === ModuleResolutionKind.Bundler
? ["node", "import"]
: ["node", "require"];
? ["import"]
: ["require"];
if (getEmitModuleResolutionKind(options) !== ModuleResolutionKind.Bundler) {
conditions.unshift("node");
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Small nit suggestion: Instead of unshift can we first add "node" and then others so we arent moving array elements

@andrewbranch andrewbranch merged commit e9868e9 into microsoft:main Feb 24, 2023
@andrewbranch andrewbranch deleted the bundler-updates branch February 24, 2023 23:49
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Author: Team For Uncommitted Bug PR for untriaged, rejected, closed or missing bug
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants