Skip to content

Commit

Permalink
fix #2853: permit top-level await in dead code
Browse files Browse the repository at this point in the history
  • Loading branch information
evanw committed Jan 20, 2023
1 parent f48391a commit 5fe2125
Show file tree
Hide file tree
Showing 13 changed files with 354 additions and 40 deletions.
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,16 @@

Previously esbuild's serve mode only responded to HTTP `GET` requests. With this release, esbuild's serve mode will also respond to HTTP `HEAD` requests, which are just like HTTP `GET` requests except that the body of the response is omitted.

* Permit top-level await in dead code branches ([#2853](https://github.com/evanw/esbuild/issues/2853))

Adding top-level await to a file has a few consequences with esbuild:

1. It causes esbuild to assume that the input module format is ESM, since top-level await is only syntactically valid in ESM. That prevents you from using `module` and `exports` for exports and also enables strict mode, which disables certain syntax and changes how function hoisting works (among other things).
2. This will cause esbuild to fail the build if either top-level await isn't supported by your language target (e.g. it's not supported in ES2021) or if top-level await isn't supported by the chosen output format (e.g. it's not supported with CommonJS).
3. Doing this will prevent you from using `require()` on this file or on any file that imports this file (even indirectly), since the `require()` function doesn't return a promise and so can't represent top-level await.

This release relaxes these rules slightly: rules 2 and 3 will now no longer apply when esbuild has identified the code branch as dead code, such as when it's behind an `if (false)` check. This should make it possible to use esbuild to convert code into different output formats that only uses top-level await conditionally. This release does not relax rule 1. Top-level await will still cause esbuild to unconditionally consider the input module format to be ESM, even when the top-level `await` is in a dead code branch. This is necessary because whether the input format is ESM or not affects the whole file, not just the dead code branch.

## 0.17.3

* Fix incorrect CSS minification for certain rules ([#2838](https://github.com/evanw/esbuild/issues/2838))
Expand Down
6 changes: 3 additions & 3 deletions internal/bundler/bundler.go
Original file line number Diff line number Diff line change
Expand Up @@ -2233,7 +2233,7 @@ func (s *scanner) validateTLA(sourceIndex uint32) tlaCheck {
if result.ok && result.tlaCheck.depth == 0 {
if repr, ok := result.file.inputFile.Repr.(*graph.JSRepr); ok {
result.tlaCheck.depth = 1
if repr.AST.TopLevelAwaitKeyword.Len > 0 {
if repr.AST.LiveTopLevelAwaitKeyword.Len > 0 {
result.tlaCheck.parent = ast.MakeIndex32(sourceIndex)
}

Expand Down Expand Up @@ -2263,10 +2263,10 @@ func (s *scanner) validateTLA(sourceIndex uint32) tlaCheck {
parentResult := &s.results[otherSourceIndex]
parentRepr := parentResult.file.inputFile.Repr.(*graph.JSRepr)

if parentRepr.AST.TopLevelAwaitKeyword.Len > 0 {
if parentRepr.AST.LiveTopLevelAwaitKeyword.Len > 0 {
tlaPrettyPath = parentResult.file.inputFile.Source.PrettyPath
tracker := logger.MakeLineColumnTracker(&parentResult.file.inputFile.Source)
notes = append(notes, tracker.MsgData(parentRepr.AST.TopLevelAwaitKeyword,
notes = append(notes, tracker.MsgData(parentRepr.AST.LiveTopLevelAwaitKeyword,
fmt.Sprintf("The top-level await in %q is here:", tlaPrettyPath)))
break
}
Expand Down
148 changes: 147 additions & 1 deletion internal/bundler_tests/bundler_default_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3797,6 +3797,23 @@ entry.js: ERROR: Top-level await is currently not supported with the "iife" outp
})
}

func TestTopLevelAwaitIIFEDeadBranch(t *testing.T) {
default_suite.expectBundled(t, bundled{
files: map[string]string{
"/entry.js": `
if (false) await foo;
if (false) for await (foo of bar) ;
`,
},
entryPaths: []string{"/entry.js"},
options: config.Options{
Mode: config.ModeBundle,
OutputFormat: config.FormatIIFE,
AbsOutputFile: "/out.js",
},
})
}

func TestTopLevelAwaitCJS(t *testing.T) {
default_suite.expectBundled(t, bundled{
files: map[string]string{
Expand All @@ -3817,6 +3834,23 @@ entry.js: ERROR: Top-level await is currently not supported with the "cjs" outpu
})
}

func TestTopLevelAwaitCJSDeadBranch(t *testing.T) {
default_suite.expectBundled(t, bundled{
files: map[string]string{
"/entry.js": `
if (false) await foo;
if (false) for await (foo of bar) ;
`,
},
entryPaths: []string{"/entry.js"},
options: config.Options{
Mode: config.ModeBundle,
OutputFormat: config.FormatCommonJS,
AbsOutputFile: "/out.js",
},
})
}

func TestTopLevelAwaitESM(t *testing.T) {
default_suite.expectBundled(t, bundled{
files: map[string]string{
Expand All @@ -3834,6 +3868,23 @@ func TestTopLevelAwaitESM(t *testing.T) {
})
}

func TestTopLevelAwaitESMDeadBranch(t *testing.T) {
default_suite.expectBundled(t, bundled{
files: map[string]string{
"/entry.js": `
if (false) await foo;
if (false) for await (foo of bar) ;
`,
},
entryPaths: []string{"/entry.js"},
options: config.Options{
Mode: config.ModeBundle,
OutputFormat: config.FormatESModule,
AbsOutputFile: "/out.js",
},
})
}

func TestTopLevelAwaitNoBundle(t *testing.T) {
default_suite.expectBundled(t, bundled{
files: map[string]string{
Expand All @@ -3849,7 +3900,22 @@ func TestTopLevelAwaitNoBundle(t *testing.T) {
})
}

func TestTopLevelAwaitNoBundleES6(t *testing.T) {
func TestTopLevelAwaitNoBundleDeadBranch(t *testing.T) {
default_suite.expectBundled(t, bundled{
files: map[string]string{
"/entry.js": `
if (false) await foo;
if (false) for await (foo of bar) ;
`,
},
entryPaths: []string{"/entry.js"},
options: config.Options{
AbsOutputFile: "/out.js",
},
})
}

func TestTopLevelAwaitNoBundleESM(t *testing.T) {
default_suite.expectBundled(t, bundled{
files: map[string]string{
"/entry.js": `
Expand All @@ -3866,6 +3932,23 @@ func TestTopLevelAwaitNoBundleES6(t *testing.T) {
})
}

func TestTopLevelAwaitNoBundleESMDeadBranch(t *testing.T) {
default_suite.expectBundled(t, bundled{
files: map[string]string{
"/entry.js": `
if (false) await foo;
if (false) for await (foo of bar) ;
`,
},
entryPaths: []string{"/entry.js"},
options: config.Options{
OutputFormat: config.FormatESModule,
Mode: config.ModeConvertFormat,
AbsOutputFile: "/out.js",
},
})
}

func TestTopLevelAwaitNoBundleCommonJS(t *testing.T) {
default_suite.expectBundled(t, bundled{
files: map[string]string{
Expand All @@ -3886,6 +3969,23 @@ entry.js: ERROR: Top-level await is currently not supported with the "cjs" outpu
})
}

func TestTopLevelAwaitNoBundleCommonJSDeadBranch(t *testing.T) {
default_suite.expectBundled(t, bundled{
files: map[string]string{
"/entry.js": `
if (false) await foo;
if (false) for await (foo of bar) ;
`,
},
entryPaths: []string{"/entry.js"},
options: config.Options{
OutputFormat: config.FormatCommonJS,
Mode: config.ModeConvertFormat,
AbsOutputFile: "/out.js",
},
})
}

func TestTopLevelAwaitNoBundleIIFE(t *testing.T) {
default_suite.expectBundled(t, bundled{
files: map[string]string{
Expand All @@ -3906,6 +4006,23 @@ entry.js: ERROR: Top-level await is currently not supported with the "iife" outp
})
}

func TestTopLevelAwaitNoBundleIIFEDeadBranch(t *testing.T) {
default_suite.expectBundled(t, bundled{
files: map[string]string{
"/entry.js": `
if (false) await foo;
if (false) for await (foo of bar) ;
`,
},
entryPaths: []string{"/entry.js"},
options: config.Options{
OutputFormat: config.FormatIIFE,
Mode: config.ModeConvertFormat,
AbsOutputFile: "/out.js",
},
})
}

func TestTopLevelAwaitForbiddenRequire(t *testing.T) {
default_suite.expectBundled(t, bundled{
files: map[string]string{
Expand Down Expand Up @@ -3947,6 +4064,35 @@ entry.js: NOTE: The top-level await in "entry.js" is here:
})
}

func TestTopLevelAwaitForbiddenRequireDeadBranch(t *testing.T) {
default_suite.expectBundled(t, bundled{
files: map[string]string{
"/entry.js": `
require('./a')
require('./b')
require('./c')
require('./entry')
if (false) for await (let x of y) await 0
`,
"/a.js": `
import './b'
`,
"/b.js": `
import './c'
`,
"/c.js": `
if (false) for await (let x of y) await 0
`,
},
entryPaths: []string{"/entry.js"},
options: config.Options{
Mode: config.ModeBundle,
OutputFormat: config.FormatIIFE,
AbsOutputFile: "/out.js",
},
})
}

func TestTopLevelAwaitAllowedImportWithoutSplitting(t *testing.T) {
default_suite.expectBundled(t, bundled{
files: map[string]string{
Expand Down
118 changes: 117 additions & 1 deletion internal/bundler_tests/snapshots/snapshots_default.txt
Original file line number Diff line number Diff line change
Expand Up @@ -5548,6 +5548,16 @@ var init_entry = __esm({
});
await init_entry();

================================================================================
TestTopLevelAwaitCJSDeadBranch
---------- /out.js ----------
// entry.js
if (false)
foo;
if (false)
for (foo of bar)
;

================================================================================
TestTopLevelAwaitESM
---------- /out.js ----------
Expand All @@ -5556,6 +5566,74 @@ await foo;
for await (foo of bar)
;

================================================================================
TestTopLevelAwaitESMDeadBranch
---------- /out.js ----------
// entry.js
if (false)
await foo;
if (false)
for await (foo of bar)
;

================================================================================
TestTopLevelAwaitForbiddenRequireDeadBranch
---------- /out.js ----------
(() => {
// c.js
var c_exports = {};
var init_c = __esm({
"c.js"() {
if (false)
for (let x of y)
;
}
});

// b.js
var b_exports = {};
var init_b = __esm({
"b.js"() {
init_c();
}
});

// a.js
var a_exports = {};
var init_a = __esm({
"a.js"() {
init_b();
}
});

// entry.js
var entry_exports = {};
var init_entry = __esm({
"entry.js"() {
init_a();
init_b();
init_c();
init_entry();
if (false)
for (let x of y)
;
}
});
init_entry();
})();

================================================================================
TestTopLevelAwaitIIFEDeadBranch
---------- /out.js ----------
(() => {
// entry.js
if (false)
foo;
if (false)
for (foo of bar)
;
})();

================================================================================
TestTopLevelAwaitNoBundle
---------- /out.js ----------
Expand All @@ -5564,12 +5642,50 @@ for await (foo of bar)
;

================================================================================
TestTopLevelAwaitNoBundleES6
TestTopLevelAwaitNoBundleCommonJSDeadBranch
---------- /out.js ----------
if (false)
foo;
if (false)
for (foo of bar)
;

================================================================================
TestTopLevelAwaitNoBundleDeadBranch
---------- /out.js ----------
if (false)
await foo;
if (false)
for await (foo of bar)
;

================================================================================
TestTopLevelAwaitNoBundleESM
---------- /out.js ----------
await foo;
for await (foo of bar)
;

================================================================================
TestTopLevelAwaitNoBundleESMDeadBranch
---------- /out.js ----------
if (false)
await foo;
if (false)
for await (foo of bar)
;

================================================================================
TestTopLevelAwaitNoBundleIIFEDeadBranch
---------- /out.js ----------
(() => {
if (false)
foo;
if (false)
for (foo of bar)
;
})();

================================================================================
TestUseStrictDirectiveBundleCJSIssue2264
---------- /out.js ----------
Expand Down
2 changes: 1 addition & 1 deletion internal/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -184,7 +184,7 @@ const (
FormatESModule
)

func (f Format) KeepES6ImportExportSyntax() bool {
func (f Format) KeepESMImportExportSyntax() bool {
return f == FormatPreserve || f == FormatESModule
}

Expand Down
Loading

0 comments on commit 5fe2125

Please sign in to comment.