Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

PR for version 0.10.0 #1053

Merged
merged 9 commits into from
Mar 25, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 45 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,50 @@
# Changelog

## Breaking changes

* No longer support `module` or `exports` in an ESM file ([#769](https://github.com/evanw/esbuild/issues/769))

This removes support for using CommonJS exports in a file with ESM exports. Previously this worked by converting the ESM file to CommonJS and then mixing the CommonJS and ESM exports into the same `exports` object. But it turns out that supporting this is additional complexity for the bundler, so it has been removed. It's also not something that works in real JavaScript environments since modules will never support both export syntaxes at once.

Note that this doesn't remove support for using `require` in ESM files. Doing this still works (and can be made to work in a real ESM environment by assigning to `globalThis.require`). This also doesn't remove support for using `import` in CommonJS files. Doing this also still works.

* No longer change `import()` to `require()` ([#1029](https://github.com/evanw/esbuild/issues/1029))

Previously esbuild's transform for `import()` matched TypeScript's behavior, which is to transform it into `Promise.resolve().then(() => require())` when the current output format is something other than ESM. This was done when an import is external (i.e. not bundled), either due to the expression not being a string or due to the string matching an external import path.

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.

* Initial support for bundling with top-level await ([#253](https://github.com/evanw/esbuild/issues/253))

Top-level await is a feature that lets you use an `await` expression at the top level (outside of an `async` function). Here is an example:

```js
let promise = fetch('https://www.example.com/data')
export let data = await promise.then(x => x.json())
```

Top-level await only works in ECMAScript modules, and does not work in CommonJS modules. This means that you must use an `import` statement or an `import()` expression to import a module containing top-level await. You cannot use `require()` because it's synchronous while top-level await is asynchronous. There should be a descriptive error message when you try to do this.

This initial release only has limited support for top-level await. It is only supported with the `esm` output format, but not with the `iife` or `cjs` output formats. In addition, the compilation is not correct in that two modules that both contain top-level await and that are siblings in the import graph will be evaluated in serial instead of in parallel. Full support for top-level await will come in a future release.

* Change whether certain files are interpreted as ESM or CommonJS ([#1043](https://github.com/evanw/esbuild/issues/1043))

The bundling algorithm currently doesn't contain any logic that requires flagging modules as CommonJS vs. ESM beforehand. Instead it handles a superset and then sort of decides later if the module should be treated as CommonJS vs. ESM based on whether the module uses the `module` or `exports` variables and/or the `exports` keyword.

With this release, files that follow [node's rules for module types](https://nodejs.org/api/packages.html#packages_type) will be flagged as explicitly ESM. This includes files that end in `.mjs` and files within a package containing `"type": "module"` in the enclosing `package.json` file. The CommonJS `require`, `module`, and `exports` features will be unavailable in these files. This matters most for files without any exports, since then it's otherwise ambiguous what the module type is.

In addition, files without exports should now accurately fall back to being considered CommonJS. They should now generate a `default` export of an empty object when imported using an `import` statement, since that's what happens in node when you import a CommonJS file into an ESM file in node. Previously the default export could be undefined because these export-less files were sort of treated as ESM but with missing import errors turned into warnings instead.

This is an edge case that rarely comes up in practice, since you usually never import things from a module that has no exports.

## Unreleased

* Add the ability to set `sourceRoot` in source maps ([#1028](https://github.com/evanw/esbuild/pull/1028))
Expand Down
4 changes: 4 additions & 0 deletions internal/ast/ast.go
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,10 @@ type ImportRecord struct {
// CommonJS wrapper or not.
ContainsImportStar bool

// If this is true, the import contains an import for the alias "default",
// either via the "import x from" or "import {default as x} from" syntax.
ContainsDefaultAlias bool

// If true, this "export * from 'path'" statement is evaluated at run-time by
// calling the "__exportStar()" helper function
CallsRunTimeExportStarFn bool
Expand Down
113 changes: 108 additions & 5 deletions internal/bundler/bundler.go
Original file line number Diff line number Diff line change
Expand Up @@ -152,10 +152,16 @@ type parseArgs struct {
}

type parseResult struct {
file file
ok bool

file file
resolveResults []*resolver.ResolveResult
tlaCheck tlaCheck
ok bool
}

type tlaCheck struct {
parent ast.Index32
depth uint32
importRecordIndex uint32
}

func parseFile(args parseArgs) {
Expand Down Expand Up @@ -1017,6 +1023,15 @@ func (s *scanner) maybeParseFile(
optionsClone.PreserveUnusedImportsTS = true
}

// Set the module type preference using node's module type rules
if strings.HasSuffix(path.Text, ".mjs") {
optionsClone.ModuleType = config.ModuleESM
} else if strings.HasSuffix(path.Text, ".cjs") {
optionsClone.ModuleType = config.ModuleCommonJS
} else {
optionsClone.ModuleType = resolveResult.ModuleType
}

// Enable bundling for injected files so we always do tree shaking. We
// never want to include unnecessary code from injected files since they
// are essentially bundled. However, if we do this we should skip the
Expand Down Expand Up @@ -1497,14 +1512,102 @@ func (s *scanner) processScannedFiles() []file {
// can't be constructed earlier because we generate new parse results for
// JavaScript stub files for CSS imports above.
files := make([]file, len(s.results))
for i, result := range s.results {
for sourceIndex, result := range s.results {
if result.ok {
files[i] = result.file
s.validateTLA(uint32(sourceIndex))
files[sourceIndex] = result.file
}
}
return files
}

func (s *scanner) validateTLA(sourceIndex uint32) tlaCheck {
result := &s.results[sourceIndex]

if result.ok && result.tlaCheck.depth == 0 {
if repr, ok := result.file.repr.(*reprJS); ok {
result.tlaCheck.depth = 1
if repr.ast.TopLevelAwaitKeyword.Len > 0 {
result.tlaCheck.parent = ast.MakeIndex32(sourceIndex)
}

for importRecordIndex, record := range repr.ast.ImportRecords {
if record.SourceIndex.IsValid() &&
(record.Kind == ast.ImportRequire || record.Kind == ast.ImportStmt ||
(record.Kind == ast.ImportDynamic && !s.options.CodeSplitting)) {
parent := s.validateTLA(record.SourceIndex.GetIndex())
if !parent.parent.IsValid() {
continue
}

// Follow any import chains
if record.Kind == ast.ImportStmt && (!result.tlaCheck.parent.IsValid() || parent.depth < result.tlaCheck.depth) {
result.tlaCheck.depth = parent.depth + 1
result.tlaCheck.parent = record.SourceIndex
result.tlaCheck.importRecordIndex = uint32(importRecordIndex)
continue
}

// Require of a top-level await chain is forbidden. Dynamic import of
// a top-level await chain is also forbidden if code splitting is off.
if record.Kind == ast.ImportRequire || (record.Kind == ast.ImportDynamic && !s.options.CodeSplitting) {
var notes []logger.MsgData
var tlaPrettyPath string
otherSourceIndex := record.SourceIndex.GetIndex()

// Build up a chain of relevant notes for all of the imports
for {
parentResult := &s.results[otherSourceIndex]
parentRepr := parentResult.file.repr.(*reprJS)

if parentRepr.ast.TopLevelAwaitKeyword.Len > 0 {
tlaPrettyPath = parentResult.file.source.PrettyPath
notes = append(notes, logger.RangeData(&parentResult.file.source, parentRepr.ast.TopLevelAwaitKeyword,
fmt.Sprintf("The top-level await in %q is here", tlaPrettyPath)))
break
}

if !parentResult.tlaCheck.parent.IsValid() {
notes = append(notes, logger.MsgData{Text: "unexpected invalid index"})
break
}

otherSourceIndex = parentResult.tlaCheck.parent.GetIndex()

notes = append(notes, logger.RangeData(&parentResult.file.source,
parentRepr.ast.ImportRecords[parent.importRecordIndex].Range,
fmt.Sprintf("The file %q imports the file %q here",
parentResult.file.source.PrettyPath, s.results[otherSourceIndex].file.source.PrettyPath)))
}

var text string
what := "require call"
why := ""
importedPrettyPath := s.results[record.SourceIndex.GetIndex()].file.source.PrettyPath

if record.Kind == ast.ImportDynamic {
what = "dynamic import"
why = " (enable code splitting to allow this)"
}

if importedPrettyPath == tlaPrettyPath {
text = fmt.Sprintf("This %s is not allowed because the imported file %q contains a top-level await%s",
what, importedPrettyPath, why)
} else {
text = fmt.Sprintf("This %s is not allowed because the transitive dependency %q contains a top-level await%s",
what, tlaPrettyPath, why)
}

s.log.AddRangeErrorWithNotes(&result.file.source, record.Range, text, notes)
}
}
}
}
}

return result.tlaCheck
}

func DefaultExtensionToLoaderMap() map[string]config.Loader {
return map[string]config.Loader{
".js": config.LoaderJS,
Expand Down
Loading