Skip to content

Commit

Permalink
add annotations for "cjs-module-lexer" and node
Browse files Browse the repository at this point in the history
  • Loading branch information
evanw committed Mar 12, 2021
1 parent 24811f1 commit 20fc8a8
Show file tree
Hide file tree
Showing 3 changed files with 90 additions and 1 deletion.
20 changes: 20 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,26 @@

There are 16 different special at-rules that can be nested inside the `@page` rule. They are defined in [this specification](https://www.w3.org/TR/css-page-3/#syntax-page-selector). Previously esbuild treated these as unknown rules, but with this release esbuild will now treat these as known rules. The only real difference in behavior is that esbuild will no longer warn about these rules being unknown.

* Add export name annotations to CommonJS output for node

When you import a CommonJS file using an ESM `import` statement in node, the `default` import is the value of `module.exports` in the CommonJS file. In addition, node attempts to generate named exports for properties of the `module.exports` object.

Except that node doesn't actually ever look at the properties of that object to determine the export names. Instead it parses the CommonJS file and scans the AST for certain syntax patterns. A full list of supported patterns can be found in the [documentation for the `cjs-module-lexer` package](https://github.com/guybedford/cjs-module-lexer#grammar). This library doesn't currently support the syntax patterns used by esbuild.

While esbuild could adapt its syntax to these patterns, the patterns are less compact than the ones used by esbuild and doing this would lead to code bloat. Supporting two separate ways of generating export getters would also complicate esbuild's internal implementation, which is undesirable. Another alternative could be to update the implementation of `cjs-module-lexer` to support the specific patterns used by esbuild. This is also undesirable because this pattern detection would break when minification is enabled, this would tightly couple esbuild's output format with node and prevent esbuild from changing it, and it wouldn't work for existing and previous versions of node that still have the old version of this library.

Instead, esbuild will now add additional code to "annotate" ESM files that have been converted to CommonJS when esbuild's platform has been set to `node`. The annotation is dead code but is still detected by the `cjs-module-lexer` library. If the original ESM file has the exports `foo` and `bar`, the additional annotation code will look like this:

```js
0 && (module.exports = {foo, bar});
```

This allows you to use named imports with an ESM `import` statement in node (previously you could only use the `default` import):

```js
import { foo, bar } from './file-built-by-esbuild.cjs'
```

## 0.9.0

**This release contains backwards-incompatible changes.** Since esbuild is before version 1.0.0, these changes have been released as a new minor version to reflect this (as [recommended by npm](https://docs.npmjs.com/cli/v6/using-npm/semver/)). You should either be pinning the exact version of `esbuild` in your `package.json` file or be using a version range syntax that only accepts patch upgrades such as `^0.8.0`. See the documentation about [semver](https://docs.npmjs.com/cli/v6/using-npm/semver/) for more information.
Expand Down
56 changes: 56 additions & 0 deletions internal/bundler/linker.go
Original file line number Diff line number Diff line change
Expand Up @@ -1820,6 +1820,62 @@ func (c *linkerContext) createExportsForFile(sourceIndex uint32) {
}
}

// If we are generating CommonJS for node, encode the known export names in
// a form that node can understand them. This relies on the specific behavior
// 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 file.isEntryPoint && c.options.Platform == config.PlatformNode &&
c.options.OutputFormat == config.FormatCommonJS && len(repr.meta.resolvedExports) > 0 {
// Add a comment since otherwise people will surely wonder what this is.
// This annotation means you can do this and have it work:
//
// import { name } from './file-from-esbuild.cjs'
//
// when "file-from-esbuild.cjs" looks like this:
//
// __export(exports, { name: () => name });
// 0 && (module.exports = {name});
//
// The maintainer of "cjs-module-lexer" is receptive to adding esbuild-
// friendly patterns to this library. However, this library has already
// shipped in node and using existing patterns instead of defining new
// patterns is maximally compatible.
//
// An alternative to doing this could be to use "Object.defineProperties"
// 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.RemoveWhitespace {
entryPointExportStmts = append(entryPointExportStmts,
js_ast.Stmt{Data: &js_ast.SComment{Text: `// Annotate the CommonJS export names for ESM import in node:`}},
)
}

// "{a, b}"
var moduleExports []js_ast.Property
for _, export := range repr.meta.sortedAndFilteredExportAliases {
moduleExports = append(moduleExports, js_ast.Property{
Key: js_ast.Expr{Data: &js_ast.EString{Value: js_lexer.StringToUTF16(export)}},
})
}

// "0 && (module.exports = {a, b});"
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: repr.ast.ModuleRef}},
Name: "exports",
}},
js_ast.Expr{Data: &js_ast.EObject{Properties: moduleExports}},
),
}}

entryPointExportStmts = append(entryPointExportStmts, js_ast.Stmt{Data: &js_ast.SExpr{Value: expr}})
}

if len(entryPointExportStmts) > 0 || cjsWrapStmt.Data != nil {
// Trigger evaluation of the CommonJS wrapper
if cjsWrapStmt.Data != nil {
Expand Down
15 changes: 14 additions & 1 deletion scripts/end-to-end-tests.js
Original file line number Diff line number Diff line change
Expand Up @@ -196,7 +196,6 @@
test(['in.js', '--outfile=node.js', '--target=es6'], {
'in.js': `
let v, o = {b: 3, c: 5}, e = ({b: v, ...o} = o);
console.log(JSON.stringify([o !== e, o, e, v]));
if (o === e || o.b !== void 0 || o.c !== 5 || e.b !== 3 || e.c !== 5 || v !== 3) throw 'fail'
`,
}),
Expand Down Expand Up @@ -889,6 +888,20 @@
if (out.foo !== 'abc' || out.default() !== 123) throw 'fail'
`,
}),
test(['in.js', '--outfile=out.cjs', '--format=cjs', '--platform=node'], {
'in.js': `
export let foo = 123
export let bar = 234
`,
'node.js': `
exports.async = async () => {
let out = await import('./out.cjs')
let keys = Object.keys(out)
if (keys.length !== 3 || keys[0] !== 'bar' || keys[1] !== 'default' || keys[2] !== 'foo') throw 'fail'
if (out.foo !== 123 || out.bar !== 234 || out.default.foo !== 123 || out.default.bar !== 234) throw 'fail'
}
`,
}, { async: true }),

// ESM => IIFE
test(['in.js', '--outfile=node.js', '--format=iife'], {
Expand Down

0 comments on commit 20fc8a8

Please sign in to comment.