Skip to content

Commit

Permalink
fix #2486, fix #3029: node re-export annotations
Browse files Browse the repository at this point in the history
  • Loading branch information
evanw committed Apr 1, 2023
1 parent 9fbf1fd commit b86e581
Show file tree
Hide file tree
Showing 5 changed files with 144 additions and 19 deletions.
29 changes: 29 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down
24 changes: 24 additions & 0 deletions internal/bundler_tests/bundler_default_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"},
Expand All @@ -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,
},
},
},
},
})
}
Expand All @@ -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"},
Expand All @@ -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,
},
},
},
},
})
}
Expand Down
24 changes: 24 additions & 0 deletions internal/bundler_tests/snapshots/snapshots_default.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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 ----------
Expand Down
51 changes: 32 additions & 19 deletions internal/linker/linker.go
Original file line number Diff line number Diff line change
Expand Up @@ -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:
//
Expand All @@ -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
Expand All @@ -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:
Expand Down
35 changes: 35 additions & 0 deletions scripts/end-to-end-tests.js
Original file line number Diff line number Diff line change
Expand Up @@ -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': `
Expand Down

0 comments on commit b86e581

Please sign in to comment.