diff --git a/CHANGELOG.md b/CHANGELOG.md index 90a575451a1..b75133f78d6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -33,6 +33,30 @@ Due to an oversight, the `--metafile` setting didn't work when `--watch` was also specified. This only affected the command-line interface. With this release, the `--metafile` setting should now work in this case. +* Add a hidden `__esModule` property to modules in ESM format ([#1338](https://github.com/evanw/esbuild/pull/1338)) + + Module namespace objects from ESM files will now have a hidden `__esModule` property. This improves compatibility with code that has been converted from ESM syntax to CommonJS by Babel or TypeScript. For example: + + ```js + // Input TypeScript code + import x from "y" + console.log(x) + + // Output JavaScript code from the TypeScript compiler + var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; + }; + Object.defineProperty(exports, "__esModule", { value: true }); + const y_1 = __importDefault(require("y")); + console.log(y_1.default); + ``` + + If the object returned by `require("y")` doesn't have an `__esModule` property, then `y_1` will be the object `{ "default": require("y") }`. If the file `"y"` is in ESM format and has a default export of, say, the value `null`, that means `y_1` will now be `{ "default": { "default": null } }` and you will need to use `y_1.default.default` to access the default value. Adding an automatically-generated `__esModule` property when converting files in ESM format to CommonJS is required to make this code work correctly (i.e. for the value to be accessible via just `y_1.default` instead). + + With this release, code in ESM format will now have an automatically-generated `__esModule` property to satisfy this convention. The property is non-enumerable so it shouldn't show up when iterating over the properties of the object. As a result, the export name `__esModule` is now reserved for use with esbuild. It's now an error to create an export with the name `__esModule`. + + This fix was contributed by [@lbwa](https://github.com/lbwa). + ## 0.12.6 * Improve template literal lowering transformation conformance ([#1327](https://github.com/evanw/esbuild/issues/1327)) diff --git a/internal/bundler/linker.go b/internal/bundler/linker.go index 15fbe7e6e59..98e6c72d36f 100644 --- a/internal/bundler/linker.go +++ b/internal/bundler/linker.go @@ -1740,10 +1740,13 @@ func (c *linkerContext) createExportsForFile(sourceIndex uint32) { } } - // Prefix this part with "var exports = {}" if this isn't a CommonJS module declaredSymbols := []js_ast.DeclaredSymbol{} var nsExportStmts []js_ast.Stmt - if repr.AST.ExportsKind != js_ast.ExportsCommonJS && (!file.IsEntryPoint() || c.options.OutputFormat != config.FormatCommonJS) { + + // Prefix this part with "var exports = {}" if this isn't a CommonJS module + needsExportsVariable := repr.AST.ExportsKind != js_ast.ExportsCommonJS && + (!file.IsEntryPoint() || c.options.OutputFormat != config.FormatCommonJS) + if needsExportsVariable { nsExportStmts = append(nsExportStmts, js_ast.Stmt{Data: &js_ast.SLocal{Decls: []js_ast.Decl{{ Binding: js_ast.Binding{Data: &js_ast.BIdentifier{Ref: repr.AST.ExportsRef}}, ValueOrNil: js_ast.Expr{Data: &js_ast.EObject{}}, @@ -1758,8 +1761,20 @@ func (c *linkerContext) createExportsForFile(sourceIndex uint32) { // "__markAsModule" which sets the "__esModule" property to true. This must // be done before any to "require()" or circular imports of multiple modules // that have been each converted from ESM to CommonJS may not work correctly. - if repr.AST.ExportKeyword.Len > 0 && (repr.AST.ExportsKind == js_ast.ExportsCommonJS || - (file.IsEntryPoint() && c.options.OutputFormat == config.FormatCommonJS)) { + needsMarkAsModule := + (repr.AST.ExportKeyword.Len > 0 && (repr.AST.ExportsKind == js_ast.ExportsCommonJS || + (file.IsEntryPoint() && c.options.OutputFormat == config.FormatCommonJS))) || + needsExportsVariable + + // Avoid calling "__markAsModule" if we call "__export" since the function + // "__export" already calls "__markAsModule". This is an optimization to + // reduce generated code size. + needsExportCall := len(properties) > 0 + if needsMarkAsModule && needsExportCall { + needsMarkAsModule = false + } + + if needsMarkAsModule { runtimeRepr := c.graph.Files[runtime.SourceIndex].InputFile.Repr.(*graph.JSRepr) markAsModuleRef := runtimeRepr.AST.ModuleScope.Members["__markAsModule"].Ref nsExportStmts = append(nsExportStmts, js_ast.Stmt{Data: &js_ast.SExpr{Value: js_ast.Expr{Data: &js_ast.ECall{ @@ -1783,7 +1798,7 @@ func (c *linkerContext) createExportsForFile(sourceIndex uint32) { // "__export(exports, { foo: () => foo })" exportRef := js_ast.InvalidRef - if len(properties) > 0 { + if needsExportCall { runtimeRepr := c.graph.Files[runtime.SourceIndex].InputFile.Repr.(*graph.JSRepr) exportRef = runtimeRepr.AST.ModuleScope.Members["__export"].Ref nsExportStmts = append(nsExportStmts, js_ast.Stmt{Data: &js_ast.SExpr{Value: js_ast.Expr{Data: &js_ast.ECall{ diff --git a/internal/bundler/snapshots/snapshots_default.txt b/internal/bundler/snapshots/snapshots_default.txt index 362e394236e..963f8c1ecfa 100644 --- a/internal/bundler/snapshots/snapshots_default.txt +++ b/internal/bundler/snapshots/snapshots_default.txt @@ -492,6 +492,7 @@ TestEmptyExportClauseBundleAsCommonJSIssue910 ---------- /out.js ---------- // types.mjs var types_exports = {}; +__markAsModule(types_exports); var init_types = __esm({ "types.mjs"() { } @@ -864,6 +865,7 @@ var init_d = __esm({ // e.js var e_exports = {}; +__markAsModule(e_exports); import * as x_star from "x"; var init_e = __esm({ "e.js"() { @@ -1813,21 +1815,21 @@ import("foo");import(foo()); TestMinifiedExportsAndModuleFormatCommonJS ---------- /out.js ---------- // foo/test.js -var o = {}; -s(o, { - foo: () => p +var t = {}; +f(t, { + foo: () => l }); -var p = 123; +var l = 123; // bar/test.js -var t = {}; -s(t, { - bar: () => l +var r = {}; +f(r, { + bar: () => m }); -var l = 123; +var m = 123; // entry.js -console.log(exports, module.exports, o, t); +console.log(exports, module.exports, t, r); ================================================================================ TestMinifyArguments @@ -2093,7 +2095,6 @@ export { TestReExportDefaultExternalCommonJS ---------- /out.js ---------- // entry.js -__markAsModule(exports); __export(exports, { bar: () => import_bar.default, foo: () => import_foo.default @@ -2138,7 +2139,6 @@ export { default as bar } from "./bar"; ================================================================================ TestReExportDefaultNoBundleCommonJS ---------- /out.js ---------- -__markAsModule(exports); __export(exports, { bar: () => import_bar.default, foo: () => import_foo.default @@ -3130,6 +3130,7 @@ TestTopLevelAwaitAllowedImportWithoutSplitting ---------- /out.js ---------- // c.js var c_exports = {}; +__markAsModule(c_exports); var init_c = __esm({ async "c.js"() { await 0; @@ -3138,6 +3139,7 @@ var init_c = __esm({ // b.js var b_exports = {}; +__markAsModule(b_exports); var init_b = __esm({ async "b.js"() { await init_c(); @@ -3146,6 +3148,7 @@ var init_b = __esm({ // a.js var a_exports = {}; +__markAsModule(a_exports); var init_a = __esm({ async "a.js"() { await init_b(); @@ -3154,6 +3157,7 @@ var init_a = __esm({ // entry.js var entry_exports = {}; +__markAsModule(entry_exports); var init_entry = __esm({ async "entry.js"() { init_a(); diff --git a/internal/bundler/snapshots/snapshots_importstar.txt b/internal/bundler/snapshots/snapshots_importstar.txt index 28e6e9e9341..59281fe0faf 100644 --- a/internal/bundler/snapshots/snapshots_importstar.txt +++ b/internal/bundler/snapshots/snapshots_importstar.txt @@ -8,7 +8,6 @@ var require_foo = __commonJS({ }); // entry.js -__markAsModule(exports); __export(exports, { ns: () => ns }); @@ -25,7 +24,6 @@ var require_foo = __commonJS({ }); // entry.js -__markAsModule(exports); __export(exports, { bar: () => import_foo.bar }); @@ -42,7 +40,6 @@ var require_foo = __commonJS({ }); // entry.js -__markAsModule(exports); __export(exports, { y: () => import_foo.x }); @@ -54,7 +51,6 @@ var import_foo = __toModule(require_foo()); TestExportSelfAndImportSelfCommonJS ---------- /out.js ---------- // entry.js -__markAsModule(exports); __export(exports, { foo: () => foo }); @@ -65,7 +61,6 @@ console.log(exports); TestExportSelfAndRequireSelfCommonJS ---------- /out.js ---------- // entry.js -__markAsModule(exports); __export(exports, { foo: () => foo }); @@ -82,7 +77,6 @@ init_entry(); TestExportSelfAsNamespaceCommonJS ---------- /out.js ---------- // entry.js -__markAsModule(exports); __export(exports, { foo: () => foo, ns: () => exports @@ -108,7 +102,6 @@ export { TestExportSelfCommonJS ---------- /out.js ---------- // entry.js -__markAsModule(exports); __export(exports, { foo: () => foo }); @@ -158,7 +151,6 @@ var someName = (() => { TestExportStarDefaultExportCommonJS ---------- /out.js ---------- // entry.js -__markAsModule(exports); __export(exports, { foo: () => foo }); @@ -267,7 +259,6 @@ var require_foo = __commonJS({ }); // entry.js -__markAsModule(exports); __export(exports, { ns: () => ns }); @@ -780,7 +771,6 @@ export { ================================================================================ TestReExportStarAsCommonJSNoBundle ---------- /out.js ---------- -__markAsModule(exports); __export(exports, { out: () => out }); @@ -798,7 +788,6 @@ export { TestReExportStarAsExternalCommonJS ---------- /out.js ---------- // entry.js -__markAsModule(exports); __export(exports, { out: () => out }); @@ -868,6 +857,7 @@ TestReExportStarExternalIIFE var mod = (() => { // entry.js var entry_exports = {}; + __markAsModule(entry_exports); __reExport(entry_exports, __toModule(__require("foo"))); return entry_exports; })(); @@ -877,6 +867,7 @@ TestReExportStarIIFENoBundle ---------- /out.js ---------- var mod = (() => { var entry_exports = {}; + __markAsModule(entry_exports); __reExport(entry_exports, __toModule(require("foo"))); return entry_exports; })(); diff --git a/internal/bundler/snapshots/snapshots_splitting.txt b/internal/bundler/snapshots/snapshots_splitting.txt index 24befdb64e5..e23f392ef9e 100644 --- a/internal/bundler/snapshots/snapshots_splitting.txt +++ b/internal/bundler/snapshots/snapshots_splitting.txt @@ -259,10 +259,16 @@ export { ================================================================================ TestSplittingDynamicCommonJSIntoES6 ---------- /out/entry.js ---------- +import "./chunk-U6GWLSPU.js"; + // entry.js -import("./foo-W3YX6HCT.js").then(({ default: { bar } }) => console.log(bar)); +import("./foo-XBEX5OV6.js").then(({ default: { bar } }) => console.log(bar)); + +---------- /out/foo-XBEX5OV6.js ---------- +import { + __commonJS +} from "./chunk-U6GWLSPU.js"; ----------- /out/foo-W3YX6HCT.js ---------- // foo.js var require_foo = __commonJS({ "foo.js"(exports) { @@ -271,6 +277,11 @@ var require_foo = __commonJS({ }); export default require_foo(); +---------- /out/chunk-U6GWLSPU.js ---------- +export { + __commonJS +}; + ================================================================================ TestSplittingDynamicES6IntoES6 ---------- /out/entry.js ---------- @@ -317,7 +328,7 @@ TestSplittingHybridESMAndCJSIssue617 import { foo, init_a -} from "./chunk-R3U6QWBM.js"; +} from "./chunk-NCWNCRTK.js"; init_a(); export { foo @@ -327,7 +338,7 @@ export { import { a_exports, init_a -} from "./chunk-R3U6QWBM.js"; +} from "./chunk-NCWNCRTK.js"; // b.js var bar = (init_a(), a_exports); @@ -335,7 +346,7 @@ export { bar }; ----------- /out/chunk-R3U6QWBM.js ---------- +---------- /out/chunk-NCWNCRTK.js ---------- // a.js var a_exports = {}; __export(a_exports, { @@ -388,7 +399,7 @@ TestSplittingMissingLazyExport ---------- /out/a.js ---------- import { foo -} from "./chunk-QVTGQSXT.js"; +} from "./chunk-KFPHQBDL.js"; // a.js console.log(foo()); @@ -396,14 +407,15 @@ console.log(foo()); ---------- /out/b.js ---------- import { bar -} from "./chunk-QVTGQSXT.js"; +} from "./chunk-KFPHQBDL.js"; // b.js console.log(bar()); ----------- /out/chunk-QVTGQSXT.js ---------- +---------- /out/chunk-KFPHQBDL.js ---------- // empty.js var empty_exports = {}; +__markAsModule(empty_exports); // common.js function foo() { diff --git a/internal/bundler/snapshots/snapshots_ts.txt b/internal/bundler/snapshots/snapshots_ts.txt index 41c72f7ba09..972830cbc3b 100644 --- a/internal/bundler/snapshots/snapshots_ts.txt +++ b/internal/bundler/snapshots/snapshots_ts.txt @@ -212,6 +212,7 @@ TestTSExportMissingES6 ---------- /out.js ---------- // foo.ts var foo_exports = {}; +__markAsModule(foo_exports); // entry.js console.log(foo_exports); diff --git a/internal/js_parser/js_parser.go b/internal/js_parser/js_parser.go index a7d52b3c954..42ee843fd9c 100644 --- a/internal/js_parser/js_parser.go +++ b/internal/js_parser/js_parser.go @@ -12586,6 +12586,9 @@ func (p *parser) recordExport(loc logger.Loc, alias string, ref js_ast.Ref) { fmt.Sprintf("Multiple exports with the same name %q", alias), []logger.MsgData{logger.RangeData(&p.tracker, js_lexer.RangeOfIdentifier(p.source, name.AliasLoc), fmt.Sprintf("%q was originally exported here", alias))}) + } else if alias == "__esModule" { + p.log.AddRangeError(&p.tracker, js_lexer.RangeOfIdentifier(p.source, loc), + "The export name \"__esModule\" is reserved and cannot be used (it's needed as an export marker when converting ES module syntax to CommonJS)") } else { p.namedExports[alias] = js_ast.NamedExport{AliasLoc: loc, Ref: ref} } diff --git a/internal/js_parser/js_parser_test.go b/internal/js_parser/js_parser_test.go index ddc6866f893..1a9c05e2408 100644 --- a/internal/js_parser/js_parser_test.go +++ b/internal/js_parser/js_parser_test.go @@ -2484,6 +2484,13 @@ func TestExport(t *testing.T) { ": error: This export alias is invalid because it contains the unpaired Unicode surrogate U+DC00\n") expectParseErrorTarget(t, 2020, "export * as '' from 'foo'", ": error: Using a string as a module namespace identifier name is not supported in the configured target environment\n") + + // Exports with the name "__esModule" are forbidden + esModuleError := ": error: The export name \"__esModule\" is reserved and cannot be used " + + "(it's needed as an export marker when converting ES module syntax to CommonJS)\n" + expectParseError(t, "export var __esModule", esModuleError) + expectParseError(t, "export {__esModule}; var __esModule", esModuleError) + expectParseError(t, "export {__esModule} from 'foo'", esModuleError) } func TestExportDuplicates(t *testing.T) { diff --git a/internal/runtime/runtime.go b/internal/runtime/runtime.go index 30965a7b9e6..8f5dddc793b 100644 --- a/internal/runtime/runtime.go +++ b/internal/runtime/runtime.go @@ -169,6 +169,7 @@ func code(isES6 bool) string { // Used to implement ES6 exports to CommonJS export var __export = (target, all) => { + __markAsModule(target) for (var name in all) __defProp(target, name, { get: all[name], enumerable: true }) } diff --git a/scripts/end-to-end-tests.js b/scripts/end-to-end-tests.js index a787b823539..44c6ca86b10 100644 --- a/scripts/end-to-end-tests.js +++ b/scripts/end-to-end-tests.js @@ -754,21 +754,39 @@ 'foo.js': `module.exports = 123`, }), test(['--bundle', 'in.js', '--outfile=node.js'], { - 'in.js': `const out = require('./foo'); if (out.__esModule || out.foo !== 123) throw 'fail'`, + 'in.js': `const out = require('./foo'); if (!out.__esModule || out.foo !== 123) throw 'fail'`, 'foo.js': `export const foo = 123`, }), test(['--bundle', 'in.js', '--outfile=node.js'], { - 'in.js': `const out = require('./foo'); if (out.__esModule || out.default !== 123) throw 'fail'`, + 'in.js': `const out = require('./foo'); if (!out.__esModule || out.default !== 123) throw 'fail'`, 'foo.js': `export default 123`, }), test(['--bundle', 'in.js', '--outfile=node.js'], { - 'in.js': `const out = require('./foo'); if (out.__esModule || out.default !== null) throw 'fail'`, + 'in.js': `const out = require('./foo'); if (!out.__esModule || out.default !== null) throw 'fail'`, 'foo.js': `export default function x() {} x = null`, }), test(['--bundle', 'in.js', '--outfile=node.js'], { - 'in.js': `const out = require('./foo'); if (out.__esModule || out.default !== null) throw 'fail'`, + 'in.js': `const out = require('./foo'); if (!out.__esModule || out.default !== null) throw 'fail'`, 'foo.js': `export default class x {} x = null`, }), + test(['--bundle', 'in.js', '--outfile=node.js'], { + 'in.js': ` + // This is the JavaScript generated by "tsc" for the following TypeScript: + // + // import fn from './foo' + // if (typeof fn !== 'function') throw 'fail' + // + "use strict"; + var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; + }; + Object.defineProperty(exports, "__esModule", { value: true }); + const foo_1 = __importDefault(require("./foo")); + if (typeof foo_1.default !== 'function') + throw 'fail'; + `, + 'foo.js': `export default function fn() {}`, + }), // Self export test(['--bundle', 'in.js', '--outfile=node.js'], { @@ -787,13 +805,13 @@ 'in.js': `export default 123; const out = require('./in'); if (!out.__esModule || out.default !== 123) throw 'fail'`, }), test(['--bundle', 'in.js', '--outfile=node.js', '--format=esm'], { - 'in.js': `export const foo = 123; const out = require('./in'); if (out.__esModule || out.foo !== 123) throw 'fail'`, + 'in.js': `export const foo = 123; const out = require('./in'); if (!out.__esModule || out.foo !== 123) throw 'fail'`, }), test(['--bundle', 'in.js', '--outfile=node.js', '--format=esm', '--minify'], { - 'in.js': `export const foo = 123; const out = require('./in'); if (out.__esModule || out.foo !== 123) throw 'fail'`, + 'in.js': `export const foo = 123; const out = require('./in'); if (!out.__esModule || out.foo !== 123) throw 'fail'`, }), test(['--bundle', 'in.js', '--outfile=node.js', '--format=esm'], { - 'in.js': `export default 123; const out = require('./in'); if (out.__esModule || out.default !== 123) throw 'fail'`, + 'in.js': `export default 123; const out = require('./in'); if (!out.__esModule || out.default !== 123) throw 'fail'`, }), // Test bundled and non-bundled double export star