Skip to content

Commit

Permalink
"export * as" is no longer implemented using cjs
Browse files Browse the repository at this point in the history
  • Loading branch information
evanw committed Mar 25, 2021
1 parent 24bb2b9 commit 32bc00d
Show file tree
Hide file tree
Showing 6 changed files with 150 additions and 54 deletions.
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,14 @@

With this release, esbuild will no longer do this. Now `import()` expressions will be preserved in the output instead. These expressions can be handled in non-ESM code by arranging for the `import` identifier to be a function that imports ESM code. This is how node works, so it will now be possible to use `import()` with node when the output format is something other than ESM.

* Run-time `export * as` statements no longer convert the file to CommonJS

Certain `export * as` statements require a bundler to evaluate them at run-time instead of at compile-time like the JavaScript specification. This is the case when re-exporting symbols from an external file and a file in CommonJS format.

Previously esbuild would handle this by converting the module containing the `export * as` statement to CommonJS too, since CommonJS exports are evaluated at run-time while ESM exports are evaluated at bundle-time. However, this is undesirable because tree shaking only works for ESM, not for CommonJS, and the CommonJS wrapper causes additional code bloat. Another upcoming problem is that top-level await cannot work within a CommonJS module because CommonJS `require()` is synchronous.

With this release, esbuild will now convert modules containing a run-time `export * as` statement to a special ESM-plus-dynamic-fallback mode. In this mode, named exports present at bundle time can still be imported directly by name, but any imports that don't match one of the explicit named imports present at bundle time will be converted to a property access on the fallback object instead of being a bundle error. These property accesses are then resolved at run-time and will be undefined if the export is missing.

## Unreleased

* Add the ability to set `sourceRoot` in source maps ([#1028](https://github.com/evanw/esbuild/pull/1028))
Expand Down
125 changes: 90 additions & 35 deletions internal/bundler/linker.go
Original file line number Diff line number Diff line change
Expand Up @@ -1310,20 +1310,20 @@ func (c *linkerContext) scanImportsAndExports() {
}
}

// Step 2: Propagate CommonJS status for export star statements that are re-
// exports from a CommonJS module. Exports from a CommonJS module are not
// statically analyzable, so the export star must be evaluated at run time
// instead of at bundle time.
// Step 2: Propagate dynamic export status for export star statements that
// are re-exports from a module whose exports are not statically analyzable.
// In this case the export star must be evaluated at run time instead of at
// bundle time.
for _, sourceIndex := range c.reachableFiles {
if repr, ok := c.files[sourceIndex].repr.(*reprJS); ok && len(repr.ast.ExportStarImportRecords) > 0 {
visited := make(map[uint32]bool)
c.isCommonJSDueToExportStar(sourceIndex, visited)
c.hasDynamicExportsDueToExportStar(sourceIndex, visited)
}
}

// Step 3: Resolve "export * from" statements. This must be done after we
// discover all modules that can be CommonJS because export stars are ignored
// for CommonJS modules.
// discover all modules that can have dynamic exports because export stars
// are ignored for those modules.
exportStarStack := make([]uint32, 0, 32)
for _, sourceIndex := range c.reachableFiles {
file := &c.files[sourceIndex]
Expand Down Expand Up @@ -2221,6 +2221,22 @@ loop:
namedImport.Alias, c.files[nextTracker.sourceIndex].source.PrettyPath))
}

case importDynamicFallback:
// If it's a file with dynamic export fallback, rewrite the import to a property access
trackerFile := &c.files[tracker.sourceIndex]
namedImport := trackerFile.repr.(*reprJS).ast.NamedImports[tracker.importRef]
if result.kind == matchImportNormal {
result.kind = matchImportNormalAndNamespace
result.namespaceRef = nextTracker.importRef
result.alias = namedImport.Alias
} else {
result = matchImportResult{
kind: matchImportNamespace,
namespaceRef: nextTracker.importRef,
alias: namedImport.Alias,
}
}

case importNoMatch:
symbol := c.symbols.Get(tracker.importRef)
trackerFile := &c.files[tracker.sourceIndex]
Expand Down Expand Up @@ -2316,10 +2332,10 @@ loop:
return
}

func (c *linkerContext) isCommonJSDueToExportStar(sourceIndex uint32, visited map[uint32]bool) bool {
// Terminate the traversal now if this file is CommonJS
func (c *linkerContext) hasDynamicExportsDueToExportStar(sourceIndex uint32, visited map[uint32]bool) bool {
// Terminate the traversal now if this file already has dynamic exports
repr := c.files[sourceIndex].repr.(*reprJS)
if repr.ast.ExportsKind == js_ast.ExportsCommonJS {
if repr.ast.ExportsKind == js_ast.ExportsCommonJS || repr.ast.ExportsKind == js_ast.ExportsESMWithDynamicFallback {
return true
}

Expand All @@ -2333,12 +2349,12 @@ func (c *linkerContext) isCommonJSDueToExportStar(sourceIndex uint32, visited ma
for _, importRecordIndex := range repr.ast.ExportStarImportRecords {
record := &repr.ast.ImportRecords[importRecordIndex]

// This file is CommonJS if the exported imports are from a file that is
// either CommonJS directly or transitively by itself having an export star
// from a CommonJS file.
// This file has dynamic exports if the exported imports are from a file
// that either has dynamic exports directly or transitively by itself
// having an export star from a file with dynamic exports.
if (!record.SourceIndex.IsValid() && (!c.files[sourceIndex].isEntryPoint || !c.options.OutputFormat.KeepES6ImportExportSyntax())) ||
(record.SourceIndex.IsValid() && record.SourceIndex.GetIndex() != sourceIndex && c.isCommonJSDueToExportStar(record.SourceIndex.GetIndex(), visited)) {
repr.ast.ExportsKind = js_ast.ExportsCommonJS
(record.SourceIndex.IsValid() && record.SourceIndex.GetIndex() != sourceIndex && c.hasDynamicExportsDueToExportStar(record.SourceIndex.GetIndex(), visited)) {
repr.ast.ExportsKind = js_ast.ExportsESMWithDynamicFallback
return true
}
}
Expand Down Expand Up @@ -2377,7 +2393,7 @@ func (c *linkerContext) addExportsForExportStar(
// re-exports as property accesses off of a generated require() call.
otherRepr := c.files[otherSourceIndex].repr.(*reprJS)
if otherRepr.ast.ExportsKind == js_ast.ExportsCommonJS {
// This will be resolved at run time instead
// All exports will be resolved at run time instead
continue
}

Expand Down Expand Up @@ -2446,6 +2462,9 @@ const (
// The imported file is CommonJS and has unknown exports
importCommonJS

// The import is missing but there is a dynamic fallback object
importDynamicFallback

// The import was treated as a CommonJS import but the file is known to have no exports
importCommonJSWithoutExports

Expand Down Expand Up @@ -2509,6 +2528,11 @@ func (c *linkerContext) advanceImportTracker(tracker importTracker) (importTrack
}, importFound, matchingExport.potentiallyAmbiguousExportStarRefs
}

// Is this a file with dynamic exports?
if otherRepr.ast.ExportsKind == js_ast.ExportsESMWithDynamicFallback {
return importTracker{sourceIndex: otherSourceIndex, importRef: otherRepr.ast.ExportsRef}, importDynamicFallback, nil
}

// Missing re-exports in TypeScript files are indistinguishable from types
if file.loader.IsTypeScript() && namedImport.IsExported {
return importTracker{}, importProbablyTypeScriptType, nil
Expand Down Expand Up @@ -2830,23 +2854,30 @@ func (c *linkerContext) includePart(sourceIndex uint32, partIndex uint32, entryP

otherSourceIndex := record.SourceIndex.GetIndex()
otherRepr := c.files[otherSourceIndex].repr.(*reprJS)
if record.Kind == ast.ImportStmt && otherRepr.ast.ExportsKind != js_ast.ExportsCommonJS {
// Skip this since it's not a require() import
continue
}

// This is a require() import
c.includeFile(otherSourceIndex, entryPointBit, distanceFromEntryPoint)
if record.Kind == ast.ImportRequire || record.Kind == ast.ImportDynamic ||
(record.Kind == ast.ImportStmt && otherRepr.ast.ExportsKind == js_ast.ExportsCommonJS) {
// This is a require() import
c.includeFile(otherSourceIndex, entryPointBit, distanceFromEntryPoint)

// Depend on the automatically-generated require wrapper symbol
wrapperRef := otherRepr.ast.WrapperRef
c.generateUseOfSymbolForInclude(part, &repr.meta, 1, wrapperRef, otherSourceIndex)
// Depend on the automatically-generated require wrapper symbol
wrapperRef := otherRepr.ast.WrapperRef
c.generateUseOfSymbolForInclude(part, &repr.meta, 1, wrapperRef, otherSourceIndex)

// This is an ES6 import of a CommonJS module, so it needs the
// "__toModule" wrapper as long as it's not a bare "require()"
if record.Kind != ast.ImportRequire && otherRepr.ast.ExportKeyword.Len == 0 {
record.WrapWithToModule = true
toModuleUses++
// This is an ES6 import of a CommonJS module, so it needs the
// "__toModule" wrapper as long as it's not a bare "require()"
if record.Kind != ast.ImportRequire && otherRepr.ast.ExportKeyword.Len == 0 {
record.WrapWithToModule = true
toModuleUses++
}
} else if record.Kind == ast.ImportStmt && otherRepr.ast.ExportsKind == js_ast.ExportsESMWithDynamicFallback {
// This is an import of a module that has a dynamic export fallback
// object. In that case we need to depend on that object in case
// something ends up needing to use it later. This could potentially
// be omitted in some cases with more advanced analysis if this
// dynamic export fallback object doesn't end up being needed.
c.generateUseOfSymbolForInclude(part, &repr.meta, 1, otherRepr.ast.ExportsRef, otherSourceIndex)
c.includePart(otherSourceIndex, otherRepr.meta.nsExportPartIndex, entryPointBit, distanceFromEntryPoint)
}
}

Expand All @@ -2861,9 +2892,23 @@ func (c *linkerContext) includePart(sourceIndex uint32, partIndex uint32, entryP
record := &repr.ast.ImportRecords[importRecordIndex]

// Is this export star evaluated at run time?
if (!record.SourceIndex.IsValid() && (!file.isEntryPoint || !c.options.OutputFormat.KeepES6ImportExportSyntax())) ||
(record.SourceIndex.IsValid() && record.SourceIndex.GetIndex() != sourceIndex &&
c.files[record.SourceIndex.GetIndex()].repr.(*reprJS).ast.ExportsKind == js_ast.ExportsCommonJS) {
happensAtRunTime := !record.SourceIndex.IsValid() && (!file.isEntryPoint || !c.options.OutputFormat.KeepES6ImportExportSyntax())
if record.SourceIndex.IsValid() {
otherSourceIndex := record.SourceIndex.GetIndex()
otherRepr := c.files[otherSourceIndex].repr.(*reprJS)
if otherSourceIndex != sourceIndex && otherRepr.ast.ExportsKind.IsDynamic() {
happensAtRunTime = true
}
if otherRepr.ast.ExportsKind == js_ast.ExportsESMWithDynamicFallback {
// This looks like "__exportStar(exports_a, exports_b)". Make sure to
// pull in the "exports_b" symbol into this export star. This matters
// in code splitting situations where the "export_b" symbol might live
// in a different chunk than this export star.
c.generateUseOfSymbolForInclude(part, &repr.meta, 1, otherRepr.ast.ExportsRef, otherSourceIndex)
c.includePart(otherSourceIndex, otherRepr.meta.nsExportPartIndex, entryPointBit, distanceFromEntryPoint)
}
}
if happensAtRunTime {
record.CallsRunTimeExportStarFn = true
repr.ast.UsesExportsRef = true
exportStarUses++
Expand Down Expand Up @@ -3330,15 +3375,25 @@ func (c *linkerContext) convertStmtsForChunk(sourceIndex uint32, stmtList *stmtL
}
} else {
if record.CallsRunTimeExportStarFn {
// Prefix this module with "__exportStar(exports, require(path))"
var target js_ast.E
if record.SourceIndex.IsValid() {
if repr := c.files[record.SourceIndex.GetIndex()].repr.(*reprJS); repr.ast.ExportsKind == js_ast.ExportsESMWithDynamicFallback {
// Prefix this module with "__exportStar(exports, otherExports)"
target = &js_ast.EIdentifier{Ref: repr.ast.ExportsRef}
}
}
if target == nil {
// Prefix this module with "__exportStar(exports, require(path))"
target = &js_ast.ERequire{ImportRecordIndex: s.ImportRecordIndex}
}
exportStarRef := c.files[runtime.SourceIndex].repr.(*reprJS).ast.ModuleScope.Members["__exportStar"].Ref
stmtList.prefixStmts = append(stmtList.prefixStmts, js_ast.Stmt{
Loc: stmt.Loc,
Data: &js_ast.SExpr{Value: js_ast.Expr{Loc: stmt.Loc, Data: &js_ast.ECall{
Target: js_ast.Expr{Loc: stmt.Loc, Data: &js_ast.EIdentifier{Ref: exportStarRef}},
Args: []js_ast.Expr{
{Loc: stmt.Loc, Data: &js_ast.EIdentifier{Ref: repr.ast.ExportsRef}},
{Loc: record.Range.Loc, Data: &js_ast.ERequire{ImportRecordIndex: s.ImportRecordIndex}},
{Loc: record.Range.Loc, Data: target},
},
}}},
})
Expand Down
13 changes: 4 additions & 9 deletions internal/bundler/snapshots/snapshots_default.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2465,13 +2465,6 @@ var require_es6_expr_import_dynamic = __commonJS((exports) => {
console.log(exports);
});

// es6-export-star.js
var require_es6_export_star = __commonJS((exports) => {
__markAsModule(exports);
__exportStar(exports, require_dummy());
console.log(void 0);
});

// es6-export-assign.ts
var require_es6_export_assign = __commonJS((exports, module) => {
console.log(exports);
Expand Down Expand Up @@ -2626,8 +2619,10 @@ console.log(void 0);
var import_dummy2 = require_dummy();
console.log(void 0);

// entry.js
var import_es6_export_star = require_es6_export_star();
// es6-export-star.js
var es6_export_star_exports = {};
__exportStar(es6_export_star_exports, require_dummy());
console.log(void 0);

// es6-export-star-as.js
var ns = require_dummy();
Expand Down
16 changes: 6 additions & 10 deletions internal/bundler/snapshots/snapshots_importstar.txt
Original file line number Diff line number Diff line change
Expand Up @@ -841,22 +841,18 @@ TestReExportStarExternalIIFE
---------- /out.js ----------
var mod = (() => {
// entry.js
var require_entry = __commonJS((exports) => {
__markAsModule(exports);
__exportStar(exports, __toModule(require("foo")));
});
return require_entry();
var entry_exports = {};
__exportStar(entry_exports, __toModule(require("foo")));
return entry_exports;
})();

================================================================================
TestReExportStarIIFENoBundle
---------- /out.js ----------
var mod = (() => {
var require_entry = __commonJS((exports) => {
__markAsModule(exports);
__exportStar(exports, __toModule(require("foo")));
});
return require_entry();
var entry_exports = {};
__exportStar(entry_exports, __toModule(require("foo")));
return entry_exports;
})();

================================================================================
Expand Down
15 changes: 15 additions & 0 deletions internal/js_ast/js_ast.go
Original file line number Diff line number Diff line change
Expand Up @@ -1593,8 +1593,23 @@ const (
// export names. Named imports to this module are only allowed if they are
// in the set of export names.
ExportsESM

// Some export names are known explicitly, but others fall back to a dynamic
// run-time object. This is necessary when using the "export * from" syntax
// with either a CommonJS module or an external module (i.e. a module whose
// export names are not known at compile-time).
//
// Calling "require()" on this module generates an exports object (stored in
// "exports") with getters for the export names. All named imports to this
// module are allowed. Direct named imports reference the corresponding export
// directly. Other imports go through property accesses on "exports".
ExportsESMWithDynamicFallback
)

func (kind ExportsKind) IsDynamic() bool {
return kind == ExportsCommonJS || kind == ExportsESMWithDynamicFallback
}

type AST struct {
ApproximateLineCount int32
NestedScopeSlotCounts SlotCounts
Expand Down
27 changes: 27 additions & 0 deletions scripts/end-to-end-tests.js
Original file line number Diff line number Diff line change
Expand Up @@ -452,6 +452,33 @@
export let b = 'b'
`,
}),
test(['entry1.js', 'entry2.js', '--splitting', '--bundle', '--format=esm', '--outdir=out'], {
'entry1.js': `
import { abc, def, xyz } from './a'
export default [abc, def, xyz]
`,
'entry2.js': `
import * as x from './b'
export default x
`,
'a.js': `
export let abc = 'abc'
export * from './b'
`,
'b.js': `
export * from './c'
export const def = 'def'
`,
'c.js': `
exports.xyz = 'xyz'
`,
'node.js': `
import entry1 from './out/entry1.js'
import entry2 from './out/entry2.js'
if (entry1[0] !== 'abc' || entry1[1] !== 'def' || entry1[2] !== 'xyz') throw 'fail'
if (entry2.def !== 'def' || entry2.xyz !== 'xyz') throw 'fail'
`,
}),

// Complex circular bundled and non-bundled import case (https://github.com/evanw/esbuild/issues/758)
test(['node.ts', '--bundle', '--format=cjs', '--outdir=.'], {
Expand Down

0 comments on commit 32bc00d

Please sign in to comment.