diff --git a/CHANGELOG.md b/CHANGELOG.md index 7fd6b2cd21e..1af37824854 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,35 @@ type C = { [readonly in Foo]: number } ``` +* Add annotations for re-exported modules in node ([#2486](https://github.com/evanw/esbuild/issues/2486), [#3029](https://github.com/evanw/esbuild/issues/3029)) + + Node lets you import named imports from a CommonJS module using ESM import syntax. However, the allowed names aren't derived from the properties of the CommonJS module. Instead they are derived from an arbitrary syntax-only analysis of the CommonJS module's JavaScript AST. + + To accommodate node doing this, esbuild's ESM-to-CommonJS conversion adds a special non-executable "annotation" for node that describes the exports that node should expose in this scenario. It takes the form `0 && (module.exports = { ... })` and comes at the end of the file (`0 && expr` means `expr` is never evaluated). + + Previously esbuild didn't do this for modules re-exported using the `export * from` syntax. Annotations for these re-exports will now be added starting with this release: + + ```js + // Original input + export { foo } from './foo' + export * from './bar' + + // Old output (with --format=cjs --platform=node) + ... + 0 && (module.exports = { + foo + }); + + // New output (with --format=cjs --platform=node) + ... + 0 && (module.exports = { + foo, + ...require("./bar") + }); + ``` + + Note that you need to specify both `--format=cjs` and `--platform=node` to get these node-specific annotations. + ## 0.17.14 * Allow the TypeScript 5.0 `const` modifier in object type declarations ([#3021](https://github.com/evanw/esbuild/issues/3021)) diff --git a/internal/bundler_tests/bundler_default_test.go b/internal/bundler_tests/bundler_default_test.go index 82b2a44e6e5..69065bde48c 100644 --- a/internal/bundler_tests/bundler_default_test.go +++ b/internal/bundler_tests/bundler_default_test.go @@ -1615,6 +1615,11 @@ func TestExportWildcardFSNodeES6(t *testing.T) { files: map[string]string{ "/entry.js": ` export * from 'fs' + export * from './internal' + export * from './external' + `, + "/internal.js": ` + export let foo = 123 `, }, entryPaths: []string{"/entry.js"}, @@ -1623,6 +1628,13 @@ func TestExportWildcardFSNodeES6(t *testing.T) { OutputFormat: config.FormatESModule, AbsOutputFile: "/out.js", Platform: config.PlatformNode, + ExternalSettings: config.ExternalSettings{ + PreResolve: config.ExternalMatchers{ + Exact: map[string]bool{ + "./external": true, + }, + }, + }, }, }) } @@ -1632,6 +1644,11 @@ func TestExportWildcardFSNodeCommonJS(t *testing.T) { files: map[string]string{ "/entry.js": ` export * from 'fs' + export * from './internal' + export * from './external' + `, + "/internal.js": ` + export let foo = 123 `, }, entryPaths: []string{"/entry.js"}, @@ -1640,6 +1657,13 @@ func TestExportWildcardFSNodeCommonJS(t *testing.T) { OutputFormat: config.FormatCommonJS, AbsOutputFile: "/out.js", Platform: config.PlatformNode, + ExternalSettings: config.ExternalSettings{ + PreResolve: config.ExternalMatchers{ + Exact: map[string]bool{ + "./external": true, + }, + }, + }, }, }) } diff --git a/internal/bundler_tests/snapshots/snapshots_default.txt b/internal/bundler_tests/snapshots/snapshots_default.txt index 2d315a0584a..8213e3dc55d 100644 --- a/internal/bundler_tests/snapshots/snapshots_default.txt +++ b/internal/bundler_tests/snapshots/snapshots_default.txt @@ -1505,15 +1505,39 @@ TestExportWildcardFSNodeCommonJS ---------- /out.js ---------- // entry.js var entry_exports = {}; +__export(entry_exports, { + foo: () => foo +}); module.exports = __toCommonJS(entry_exports); __reExport(entry_exports, require("fs"), module.exports); +// internal.js +var foo = 123; + +// entry.js +__reExport(entry_exports, require("./external"), module.exports); +// Annotate the CommonJS export names for ESM import in node: +0 && (module.exports = { + foo, + ...require("fs"), + ...require("./external") +}); + ================================================================================ TestExportWildcardFSNodeES6 ---------- /out.js ---------- // entry.js export * from "fs"; +// internal.js +var foo = 123; + +// entry.js +export * from "./external"; +export { + foo +}; + ================================================================================ TestExportsAndModuleFormatCommonJS ---------- /out.js ---------- diff --git a/internal/linker/linker.go b/internal/linker/linker.go index 86d85ad6d97..cd116a4d4ef 100644 --- a/internal/linker/linker.go +++ b/internal/linker/linker.go @@ -4208,7 +4208,7 @@ func (c *linkerContext) generateEntryPointTailJS( // of this parser, which the node project uses to detect named exports in // CommonJS files: https://github.com/guybedford/cjs-module-lexer. Think of // this code as an annotation for that parser. - if c.options.Platform == config.PlatformNode && len(repr.Meta.SortedAndFilteredExportAliases) > 0 { + if c.options.Platform == config.PlatformNode { // Add a comment since otherwise people will surely wonder what this is. // This annotation means you can do this and have it work: // @@ -4228,11 +4228,6 @@ func (c *linkerContext) generateEntryPointTailJS( // instead of "__export" but support for that would need to be added to // "cjs-module-lexer" and then we would need to be ok with not supporting // older versions of node that don't have that newly-added support. - if !c.options.MinifyWhitespace { - stmts = append(stmts, - js_ast.Stmt{Data: &js_ast.SComment{Text: `// Annotate the CommonJS export names for ESM import in node:`}}, - ) - } // "{a, b, if: null}" var moduleExports []js_ast.Property @@ -4259,20 +4254,38 @@ func (c *linkerContext) generateEntryPointTailJS( }) } - // "0 && (module.exports = {a, b, if: null});" - expr := js_ast.Expr{Data: &js_ast.EBinary{ - Op: js_ast.BinOpLogicalAnd, - Left: js_ast.Expr{Data: &js_ast.ENumber{Value: 0}}, - Right: js_ast.Assign( - js_ast.Expr{Data: &js_ast.EDot{ - Target: js_ast.Expr{Data: &js_ast.EIdentifier{Ref: c.unboundModuleRef}}, - Name: "exports", - }}, - js_ast.Expr{Data: &js_ast.EObject{Properties: moduleExports}}, - ), - }} + // Add annotations for re-exports: "{...require('./foo')}" + for _, importRecordIndex := range repr.AST.ExportStarImportRecords { + if record := &repr.AST.ImportRecords[importRecordIndex]; !record.SourceIndex.IsValid() { + moduleExports = append(moduleExports, js_ast.Property{ + Kind: js_ast.PropertySpread, + ValueOrNil: js_ast.Expr{Data: &js_ast.ERequireString{ImportRecordIndex: importRecordIndex}}, + }) + } + } + + if len(moduleExports) > 0 { + // "0 && (module.exports = {a, b, if: null});" + expr := js_ast.Expr{Data: &js_ast.EBinary{ + Op: js_ast.BinOpLogicalAnd, + Left: js_ast.Expr{Data: &js_ast.ENumber{Value: 0}}, + Right: js_ast.Assign( + js_ast.Expr{Data: &js_ast.EDot{ + Target: js_ast.Expr{Data: &js_ast.EIdentifier{Ref: c.unboundModuleRef}}, + Name: "exports", + }}, + js_ast.Expr{Data: &js_ast.EObject{Properties: moduleExports}}, + ), + }} + + if !c.options.MinifyWhitespace { + stmts = append(stmts, + js_ast.Stmt{Data: &js_ast.SComment{Text: `// Annotate the CommonJS export names for ESM import in node:`}}, + ) + } - stmts = append(stmts, js_ast.Stmt{Data: &js_ast.SExpr{Value: expr}}) + stmts = append(stmts, js_ast.Stmt{Data: &js_ast.SExpr{Value: expr}}) + } } case config.FormatESModule: diff --git a/scripts/end-to-end-tests.js b/scripts/end-to-end-tests.js index 4001a3002c8..b7eff50e11f 100644 --- a/scripts/end-to-end-tests.js +++ b/scripts/end-to-end-tests.js @@ -1987,6 +1987,41 @@ tests.push( `, }, { async: true }), + // https://github.com/evanw/esbuild/issues/3029 + test([ + 'node_modules/util-ex/src/index.js', + 'node_modules/util-ex/src/fn1.js', + 'node_modules/util-ex/src/fn2.js', + '--outdir=node_modules/util-ex/lib', + '--format=cjs', + '--platform=node', + ], { + 'node_modules/util-ex/src/index.js': ` + export * from './fn1' + export * from './fn2' + `, + 'node_modules/util-ex/src/fn1.js': ` + export function fn1() { return 1 } + export default fn1 + `, + 'node_modules/util-ex/src/fn2.js': ` + export function fn2() { return 2 } + export default fn2 + `, + 'node_modules/util-ex/package.json': `{ + "main": "./lib/index.js", + "type": "commonjs" + }`, + 'node.js': ` + import { fn1, fn2 } from 'util-ex' + if (fn1() !== 1) throw 'fail 1' + if (fn2() !== 2) throw 'fail 2' + `, + 'package.json': `{ + "type": "module" + }`, + }), + // ESM => IIFE test(['in.js', '--outfile=node.js', '--format=iife'], { 'in.js': `