-
Notifications
You must be signed in to change notification settings - Fork 12.6k
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
Respect package.json "type"
and module-format-specific file extensions in more module
modes
#57896
Respect package.json "type"
and module-format-specific file extensions in more module
modes
#57896
Conversation
Looks like you're introducing a change to the public API surface area. If this includes breaking changes, please document them on our wiki's API Breaking Changes page. Also, please make sure @DanielRosenwasser and @RyanCavanaugh are aware of the changes, just as a heads up. |
"type"
and module-format-specific file extensions in more module
modes
@typescript-bot test top400 |
@andrewbranch Here they are:
tscComparison Report - baseline..pr
System info unknown
Hosts
Scenarios
tsserverComparison Report - baseline..pr
System info unknown
Hosts
Scenarios
startupComparison Report - baseline..pr
System info unknown
Hosts
Scenarios
Developer Information: |
@andrewbranch Here are the results of running the top 400 repos comparing Something interesting changed - please have a look. Details
|
FWIW this does not match the behavior of esbuild or bun; both accept import statements with those file extensions: $ cat index.cts
import fs from "fs";
console.log("my code is:");
console.log(fs.readFileSync(__filename, "utf8"));
$ bun run index.cts
my code is:
import fs from "fs";
console.log("my code is:");
console.log(fs.readFileSync(__filename, "utf8"));
$ bunx esbuild --platform=node --bundle index.cts
var __create = Object.create;
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __getProtoOf = Object.getPrototypeOf;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __markAsModule = (target) => __defProp(target, "__esModule", { value: true });
var __reExport = (target, module2, copyDefault, desc) => {
if (module2 && typeof module2 === "object" || typeof module2 === "function") {
for (let key of __getOwnPropNames(module2))
if (!__hasOwnProp.call(target, key) && (copyDefault || key !== "default"))
__defProp(target, key, { get: () => module2[key], enumerable: !(desc = __getOwnPropDesc(module2, key)) || desc.enumerable });
}
return target;
};
var __toESM = (module2, isNodeMode) => {
return __reExport(__markAsModule(__defProp(module2 != null ? __create(__getProtoOf(module2)) : {}, "default", !isNodeMode && module2 && module2.__esModule ? { get: () => module2.default, enumerable: true } : { value: module2, enumerable: true })), module2);
};
// index.cts
var import_fs = __toESM(require("fs"));
console.log("my code is:");
console.log(import_fs.default.readFileSync(__filename, "utf8")); I'm not totally sure how widespread this is, though. But it does in general make me nervous about making files stricter based on |
@jakebailey but if you make those file extensions |
Works the same: $ bun run index.cjs
my code is:
import fs from "fs";
console.log("my code is:");
console.log(fs.readFileSync(__filename, "utf8"));
$ bunx esbuild --platform=node --bundle index.cjs
var __create = Object.create;
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __getProtoOf = Object.getPrototypeOf;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __markAsModule = (target) => __defProp(target, "__esModule", { value: true });
var __reExport = (target, module2, copyDefault, desc) => {
if (module2 && typeof module2 === "object" || typeof module2 === "function") {
for (let key of __getOwnPropNames(module2))
if (!__hasOwnProp.call(target, key) && (copyDefault || key !== "default"))
__defProp(target, key, { get: () => module2[key], enumerable: !(desc = __getOwnPropDesc(module2, key)) || desc.enumerable });
}
return target;
};
var __toESM = (module2, isNodeMode) => {
return __reExport(__markAsModule(__defProp(module2 != null ? __create(__getProtoOf(module2)) : {}, "default", !isNodeMode && module2 && module2.__esModule ? { get: () => module2.default, enumerable: true } : { value: module2, enumerable: true })), module2);
};
// index.cjs
var import_fs = __toESM(require("fs"));
console.log("my code is:");
console.log(import_fs.default.readFileSync(__filename, "utf8")); |
I think it’s ok for us to be more restrictive and opinionated than any single bundler or runtime here. It’s in Bun’s best interest to make plausibly interpretable input code execute without errors. |
Sure, the CJS thing isn't so bad; from my reading this PR doesn't prevent people from using the icky "set module=esnext to emit ESM" trick, so I don't think there are any extra cases I'm concerned about off the top of my head. |
// source file. | ||
return transformImpliedNodeFormatDependentModule; | ||
case ModuleKind.System: | ||
return transformSystemModule; | ||
default: | ||
return transformModule; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should we just make this switch exhaustive? This last case is really only for ModuleKind.None
, right?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
AMD/UMD too
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
With #57931 I think this is all good.
tests/baselines/reference/tsbuild/amdModulesWithOut/prepend-reports-deprecation-error.js
Show resolved
Hide resolved
…c file extensions in more module modes) (#58831)
… extensions in more `module` modes (microsoft#57896)" This reverts commit 585a9af.
… extensions in more `module` modes (microsoft#57896)" This reverts commit 585a9af.
… extensions in more `module` modes (microsoft#57896)" This reverts commit 585a9af.
…fic file extensions in more `module` modes (microsoft#57896)"" This reverts commit d3c8f08.
Fixes #54752
Fixes #50647
Closes #54788
Related #55221
Prerequisite for #54102
Suggest reviewing individual commits:
This PR allows us to use module-format-specific file extensions and package.json
"type"
fields in allmodule
modes—not just innode16
/nodenext
as today—but the way we use them can vary bymodule
/moduleResolution
mode. This has two noticeable effects:Emit respects file extension / package.json
"type"
more often.module
ises2015
–esnext
, we will now treat.cts
/.cjs
files, as well as.ts
/.js
files when the nearest package.json has"type": "commonjs"
, as CommonJS and emit CommonJS outputs accordingly. Note that unlike in--module nodenext
, a lack of package.json"type"
does not cause.ts
/.js
files to be treated as CommonJS. We only override the default behavior of themodule
setting when"type"
is explicitly set to"commonjs"
, or when the file extension is.cts
/.cjs
.module
iscommonjs
, we will now treat.mts
/.mjs
files, as well as.ts
/.js
files when the nearest package.json has"type": "module"
, as ESM and output ESM outputs accordingly.module
ispreserve
, ESM import/export declarations are now forbidden in.cts
/.cjs
files, as well as in.ts
/.js
files when the nearest package.json has"type": "commonjs"
. We want to ensure that--module preserve
means the module syntax you write gets emitted without significant transformation, but we also want to prevent ESM syntax from being used in unambiguously CommonJS files, so this situation acts much likeverbatimModuleSyntax
is enabled.Unambiguously ESM-mode declaration files do not get synthesized default exports
This is ESM-only packages come with synthesized default in
moduleResolution: bundler
#54752. Now that we always collect information about the package.json"type"
and file extension, we gain the ability to notice when a declaration file, likely in a node_modules dependency, must represent an ESM JavaScript file, which changes how a default import targeting that file works. Previously, under allmodule
modes exceptnode16
/nodenext
, this was allowed:Since
foo.d.mts
has an unambiguously-ESM file extension, we should not allow a default import from a module that doesn’t declare a default export. However, it was allowed because we believed thatfoo.d.mts
might actually represent a CommonJS module with amodule.exports.x
property, in which case a default import should be allowed and would link to themodule.exports
object. With this PR, when a declaration file has an ESM file extension or exists in a"type": "module"
context, we use that to prevent us from letting a default import happen where it shouldn’t.This work also unlocks a fix for #54102. Webpack and esbuild use module-format-specific file extensions and package.json
"type"
to vary their ESM/CJS interop rules, so if we want to provide correct types for projects using those bundlers, we will need to be able to leverage that module format info under--module esnext
or--module preserve
.There is one test failing that represents an pre-existing bug that was not exposed until now because the test did not previously look up package.json files, and a particular sequence of edits causes a failure in combination with that. It has been isolated to a failure that doesn’t rely on this PR’s changes in #57757 so it can be investigated separately.