Skip to content

Commit

Permalink
pass-through support for top-level await (#253)
Browse files Browse the repository at this point in the history
  • Loading branch information
evanw committed Aug 2, 2020
1 parent b4f0ce2 commit e87bd67
Show file tree
Hide file tree
Showing 10 changed files with 200 additions and 16 deletions.
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
# Changelog

## Unreleased

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

It seems appropriate for esbuild to support top-level await syntax now that [node is supporting top-level await syntax by default](https://github.com/nodejs/node/issues/34551) (it's the first widely-used platform to do so). This syntax can now be parsed by esbuild and is always passed through untransformed. It's only allowed when the target is `esnext` because the proposal is still in stage 3. It also cannot be used when bundling. Adding support for top-level await to the bundler is complicated since it causes imports to be asynchronous, which has far-reaching implications. This change is mainly for people using esbuild as a library to transform TypeScript into JavaScript one file at a time.

## 0.6.13

* Exclude non-JavaScript files from source maps ([#304](https://github.com/evanw/esbuild/issues/304))
Expand Down
7 changes: 2 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -201,12 +201,9 @@ These syntax features are currently always passed through un-transformed:
| [Async generators](https://github.com/tc39/proposal-async-iteration) | `es2018` | `async function* foo() {}` |
| [BigInt](https://github.com/tc39/proposal-bigint) | `es2020` | `123n` |
| [Hashbang grammar](https://github.com/tc39/proposal-hashbang) | `esnext` | `#!/usr/bin/env node` |
| [Top-level await](https://github.com/tc39/proposal-top-level-await) | `esnext` | `await import(x)` |
These syntax features are not yet supported, and currently cannot be parsed:
| Syntax transform | Language version | Example |
|---------------------------------------------------------------------|------------------|-------------------|
| [Top-level await](https://github.com/tc39/proposal-top-level-await) | `esnext` | `await import(x)` |
Note that while transforming code containing top-level await is supported, bundling code containing top-level await is not yet supported.
See also [the list of finished ECMAScript proposals](https://github.com/tc39/proposals/blob/master/finished-proposals.md) and [the list of active ECMAScript proposals](https://github.com/tc39/proposals/blob/master/README.md).
Expand Down
19 changes: 19 additions & 0 deletions internal/bundler/bundler_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4701,3 +4701,22 @@ console.log("test");
},
})
}

func TestTopLevelAwait(t *testing.T) {
expectBundled(t, bundled{
files: map[string]string{
"/entry.js": `
await foo;
for await (foo of bar) ;
`,
},
entryPaths: []string{"/entry.js"},
options: config.Options{
IsBundling: true,
AbsOutputFile: "/out.js",
},
expectedScanLog: `/entry.js: error: Top-level await is currently not supported when bundling
/entry.js: error: Top-level await is currently not supported when bundling
`,
})
}
3 changes: 3 additions & 0 deletions internal/compat/table.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ const (
OptionalChain
RestArgument
TemplateLiteral
TopLevelAwait
)

func (features Feature) Has(feature Feature) bool {
Expand Down Expand Up @@ -324,6 +325,8 @@ var Table = map[Feature]map[Engine][]int{
Node: {4},
Safari: {9},
},
TopLevelAwait: {
},
}

func isVersionLessThan(a []int, b []int) bool {
Expand Down
85 changes: 80 additions & 5 deletions internal/parser/parser.go
Original file line number Diff line number Diff line change
Expand Up @@ -201,10 +201,12 @@ type scopeOrder struct {

type fnOpts struct {
asyncRange ast.Range
arrowArgErrors *deferredArrowArgErrors
isOutsideFn bool
allowAwait bool
allowYield bool
allowSuperCall bool
isTopLevel bool

// In TypeScript, forward declarations of functions have no bodies
allowMissingBodyForTypeScript bool
Expand Down Expand Up @@ -828,6 +830,45 @@ func (p *parser) logBindingErrors(errors *deferredErrors) {
}
}

// The "await" and "yield" expressions are never allowed in argument lists but
// may or may not be allowed otherwise depending on the details of the enclosing
// function or module. This needs to be handled when parsing an arrow function
// argument list because we don't know if these expressions are not allowed until
// we reach the "=>" token (or discover the absence of one).
//
// Specifically, for await:
//
// // This is ok
// async function foo() { (x = await y) }
//
// // This is an error
// async function foo() { (x = await y) => {} }
//
// And for yield:
//
// // This is ok
// function* foo() { (x = yield y) }
//
// // This is an error
// function* foo() { (x = yield y) => {} }
//
type deferredArrowArgErrors struct {
invalidExprAwait ast.Range
invalidExprYield ast.Range
}

func (p *parser) logArrowArgErrors(errors *deferredArrowArgErrors) {
if errors.invalidExprAwait.Len > 0 {
r := errors.invalidExprAwait
p.log.AddRangeError(&p.source, r, "Cannot use an \"await\" expression here")
}

if errors.invalidExprYield.Len > 0 {
r := errors.invalidExprYield
p.log.AddRangeError(&p.source, r, "Cannot use a \"yield\" expression here")
}
}

type propertyOpts struct {
asyncRange ast.Range
isAsync bool
Expand Down Expand Up @@ -1376,6 +1417,7 @@ type parenExprOpts struct {
func (p *parser) parseParenExpr(loc ast.Loc, opts parenExprOpts) ast.Expr {
items := []ast.Expr{}
errors := deferredErrors{}
arrowArgErrors := deferredArrowArgErrors{}
spreadRange := ast.Range{}
typeColonRange := ast.Range{}

Expand All @@ -1392,6 +1434,10 @@ func (p *parser) parseParenExpr(loc ast.Loc, opts parenExprOpts) ast.Expr {
oldAllowIn := p.allowIn
p.allowIn = true

// Forbid "await" and "yield", but only for arrow functions
oldFnOpts := p.currentFnOpts
p.currentFnOpts.arrowArgErrors = &arrowArgErrors

// Scan over the comma-separated arguments or expressions
for p.lexer.Token != lexer.TCloseParen {
itemLoc := p.lexer.Loc()
Expand Down Expand Up @@ -1446,6 +1492,9 @@ func (p *parser) parseParenExpr(loc ast.Loc, opts parenExprOpts) ast.Expr {
// Restore "in" operator status before we parse the arrow function body
p.allowIn = oldAllowIn

// Also restore "await" and "yield" expression errors
p.currentFnOpts = oldFnOpts

// Are these arguments to an arrow function?
if p.lexer.Token == lexer.TEqualsGreaterThan || opts.forceArrowFn || (p.TS.Parse && p.lexer.Token == lexer.TColon) {
invalidLog := []ast.Loc{}
Expand All @@ -1469,6 +1518,7 @@ func (p *parser) parseParenExpr(loc ast.Loc, opts parenExprOpts) ast.Expr {
if p.lexer.Token == lexer.TEqualsGreaterThan || (len(invalidLog) == 0 &&
p.trySkipTypeScriptArrowReturnTypeWithBacktracking()) || opts.forceArrowFn {
p.logBindingErrors(&errors)
p.logArrowArgErrors(&arrowArgErrors)

// Now that we've decided we're an arrow function, report binding pattern
// conversion errors
Expand Down Expand Up @@ -1726,6 +1776,10 @@ func (p *parser) parsePrefix(level ast.L, errors *deferredErrors, flags exprFlag
panic(lexer.LexerPanic{})
}

if p.currentFnOpts.arrowArgErrors != nil {
p.currentFnOpts.arrowArgErrors.invalidExprYield = p.lexer.Range()
}

p.lexer.Next()

// Parse a yield-from expression, which yields from an iterator
Expand Down Expand Up @@ -1756,12 +1810,19 @@ func (p *parser) parsePrefix(level ast.L, errors *deferredErrors, flags exprFlag
case lexer.TIdentifier:
name := p.lexer.Identifier
nameRange := p.lexer.Range()
raw := p.lexer.Raw()
p.lexer.Next()

// Handle async and await expressions
if name == "async" {
if raw == "async" {
return p.parseAsyncPrefixExpr(nameRange)
} else if p.currentFnOpts.allowAwait && name == "await" {
} else if p.currentFnOpts.allowAwait && raw == "await" {
if p.currentFnOpts.isTopLevel {
p.markSyntaxFeature(compat.TopLevelAwait, nameRange)
}
if p.currentFnOpts.arrowArgErrors != nil {
p.currentFnOpts.arrowArgErrors.invalidExprAwait = nameRange
}
return ast.Expr{Loc: loc, Data: &ast.EAwait{Value: p.parseExpr(ast.LPrefix)}}
}

Expand Down Expand Up @@ -3452,6 +3513,11 @@ func (p *parser) parseFn(name *ast.LocRef, opts fnOpts) (fn ast.Fn, hadBody bool
fn.IsGenerator = opts.allowYield
p.lexer.Expect(lexer.TOpenParen)

// Await and yield are not allowed in function arguments
oldFnOpts := p.currentFnOpts
p.currentFnOpts.allowAwait = false
p.currentFnOpts.allowYield = false

// Reserve the special name "arguments" in this scope. This ensures that it
// shadows any variable called "arguments" in any parent scopes.
fn.ArgumentsRef = p.declareSymbol(ast.SymbolHoisted, ast.Loc{}, "arguments")
Expand Down Expand Up @@ -3558,6 +3624,7 @@ func (p *parser) parseFn(name *ast.LocRef, opts fnOpts) (fn ast.Fn, hadBody bool
}

p.lexer.Expect(lexer.TCloseParen)
p.currentFnOpts = oldFnOpts

// "function foo(): any {}"
if p.TS.Parse && p.lexer.Token == lexer.TColon {
Expand Down Expand Up @@ -4410,11 +4477,15 @@ func (p *parser) parseStmt(opts parseStmtOpts) ast.Stmt {
// "for await (let x of y) {}"
isForAwait := p.lexer.IsContextualKeyword("await")
if isForAwait {
awaitRange := p.lexer.Range()
if !p.currentFnOpts.allowAwait {
p.log.AddRangeError(&p.source, p.lexer.Range(), "Cannot use \"await\" outside an async function")
p.log.AddRangeError(&p.source, awaitRange, "Cannot use \"await\" outside an async function")
isForAwait = false
} else {
p.markSyntaxFeature(compat.ForAwait, p.lexer.Range())
didGenerateError := p.markSyntaxFeature(compat.ForAwait, awaitRange)
if p.currentFnOpts.isTopLevel && !didGenerateError {
p.markSyntaxFeature(compat.TopLevelAwait, awaitRange)
}
}
p.lexer.Next()
}
Expand Down Expand Up @@ -4775,7 +4846,7 @@ func (p *parser) parseStmt(opts parseStmtOpts) ast.Stmt {

// Parse either an async function, an async expression, or a normal expression
var expr ast.Expr
if isIdentifier && name == "async" {
if isIdentifier && p.lexer.Raw() == "async" {
asyncRange := p.lexer.Range()
p.lexer.Next()
if p.lexer.Token == lexer.TFunction {
Expand Down Expand Up @@ -8554,6 +8625,10 @@ func Parse(log logging.Log, source logging.Source, options config.Options) (resu
p.lexer.Next()
}

// Allow top-level await
p.currentFnOpts.allowAwait = true
p.currentFnOpts.isTopLevel = true

// Parse the file in the first pass, but do not bind symbols
stmts := p.parseStmtsUpTo(lexer.TEndOfFile, parseStmtOpts{isModuleScope: true})
p.prepareForVisitPass(&options)
Expand Down
17 changes: 16 additions & 1 deletion internal/parser/parser_lower.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,17 @@ import (
"github.com/evanw/esbuild/internal/lexer"
)

func (p *parser) markSyntaxFeature(feature compat.Feature, r ast.Range) {
func (p *parser) markSyntaxFeature(feature compat.Feature, r ast.Range) (didGenerateError bool) {
didGenerateError = true

if !p.UnsupportedFeatures.Has(feature) {
if feature == compat.TopLevelAwait && p.IsBundling {
p.log.AddRangeError(&p.source, r,
"Top-level await is currently not supported when bundling")
return
}

didGenerateError = false
return
}

Expand Down Expand Up @@ -69,6 +78,11 @@ func (p *parser) markSyntaxFeature(feature compat.Feature, r ast.Range) {
case compat.NestedRestBinding:
name = "non-identifier array rest patterns"

case compat.TopLevelAwait:
p.log.AddRangeError(&p.source, r,
fmt.Sprintf("Top-level await is not available in %s", where))
return

case compat.BigInt:
// Transforming these will never be supported
p.log.AddRangeError(&p.source, r,
Expand All @@ -89,6 +103,7 @@ func (p *parser) markSyntaxFeature(feature compat.Feature, r ast.Range) {

p.log.AddRangeError(&p.source, r,
fmt.Sprintf("Transforming %s to %s is not supported yet", name, where))
return
}

func (p *parser) isPrivateUnsupported(private *ast.EPrivateIdentifier) bool {
Expand Down
70 changes: 69 additions & 1 deletion internal/parser/parser_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -771,6 +771,25 @@ func TestGenerator(t *testing.T) {
expectPrinted(t, "function* foo() { -(yield 100) }", "function* foo() {\n -(yield 100);\n}\n")
}

func TestYield(t *testing.T) {
expectParseError(t, "yield 100", "<stdin>: error: Cannot use \"yield\" outside a generator function\n")

expectParseError(t, "function* bar(x = yield y) {}", "<stdin>: error: Cannot use \"yield\" outside a generator function\n")
expectParseError(t, "(function*(x = yield y) {})", "<stdin>: error: Cannot use \"yield\" outside a generator function\n")
expectParseError(t, "({ *foo(x = yield y) {} })", "<stdin>: error: Cannot use \"yield\" outside a generator function\n")
expectParseError(t, "class Foo { *foo(x = yield y) {} }", "<stdin>: error: Cannot use \"yield\" outside a generator function\n")
expectParseError(t, "(class { *foo(x = yield y) {} })", "<stdin>: error: Cannot use \"yield\" outside a generator function\n")

expectParseError(t, "function *foo() { function bar(x = yield y) {} }", "<stdin>: error: Cannot use \"yield\" outside a generator function\n")
expectParseError(t, "function *foo() { (function(x = yield y) {}) }", "<stdin>: error: Cannot use \"yield\" outside a generator function\n")
expectParseError(t, "function *foo() { ({ foo(x = yield y) {} }) }", "<stdin>: error: Cannot use \"yield\" outside a generator function\n")
expectParseError(t, "function *foo() { class Foo { foo(x = yield y) {} } }", "<stdin>: error: Cannot use \"yield\" outside a generator function\n")
expectParseError(t, "function *foo() { (class { foo(x = yield y) {} }) }", "<stdin>: error: Cannot use \"yield\" outside a generator function\n")
expectParseError(t, "function *foo() { (x = yield y) => {} }", "<stdin>: error: Cannot use a \"yield\" expression here\n")
expectPrinted(t, "function *foo() { (x = yield y) }", "function* foo() {\n x = yield y;\n}\n")
expectParseError(t, "function foo() { (x = yield y) }", "<stdin>: error: Cannot use \"yield\" outside a generator function\n")
}

func TestAsync(t *testing.T) {
expectPrinted(t, "function foo() { await }", "function foo() {\n await;\n}\n")
expectPrinted(t, "async function foo() { await 0 }", "async function foo() {\n await 0;\n}\n")
Expand Down Expand Up @@ -819,7 +838,56 @@ func TestAsync(t *testing.T) {
expectParseError(t, "export default async x => y, z", "<stdin>: error: Expected \";\" but found \",\"\n")
expectParseError(t, "export default async (x) => y, z", "<stdin>: error: Expected \";\" but found \",\"\n")

expectParseError(t, "for await(;;);", "<stdin>: error: Cannot use \"await\" outside an async function\n")
expectParseError(t, "async function bar(x = await y) {}", "<stdin>: error: Expected \")\" but found \"y\"\n")
expectParseError(t, "async (function(x = await y) {})", "<stdin>: error: Expected \")\" but found \"y\"\n")
expectParseError(t, "async ({ foo(x = await y) {} })", "<stdin>: error: Expected \")\" but found \"y\"\n")
expectParseError(t, "class Foo { async foo(x = await y) {} }", "<stdin>: error: Expected \")\" but found \"y\"\n")
expectParseError(t, "(class { async foo(x = await y) {} })", "<stdin>: error: Expected \")\" but found \"y\"\n")

expectParseError(t, "async function foo() { function bar(x = await y) {} }", "<stdin>: error: Expected \")\" but found \"y\"\n")
expectParseError(t, "async function foo() { (function(x = await y) {}) }", "<stdin>: error: Expected \")\" but found \"y\"\n")
expectParseError(t, "async function foo() { ({ foo(x = await y) {} }) }", "<stdin>: error: Expected \")\" but found \"y\"\n")
expectParseError(t, "async function foo() { class Foo { foo(x = await y) {} } }", "<stdin>: error: Expected \")\" but found \"y\"\n")
expectParseError(t, "async function foo() { (class { foo(x = await y) {} }) }", "<stdin>: error: Expected \")\" but found \"y\"\n")
expectParseError(t, "async function foo() { (x = await y) => {} }", "<stdin>: error: Cannot use an \"await\" expression here\n")
expectPrinted(t, "async function foo() { (x = await y) }", "async function foo() {\n x = await y;\n}\n")
expectParseError(t, "function foo() { (x = await y) }", "<stdin>: error: Expected \")\" but found \"y\"\n")

// Top-level await
expectPrinted(t, "await foo;", "await foo;\n")
expectPrinted(t, "for await(foo of bar);", "for await (foo of bar)\n ;\n")
expectParseError(t, "function foo() { await foo }", "<stdin>: error: Expected \";\" but found \"foo\"\n")
expectParseError(t, "function foo() { for await(foo of bar); }", "<stdin>: error: Cannot use \"await\" outside an async function\n")
expectPrinted(t, "function foo(x = await) {}", "function foo(x = await) {\n}\n")
expectParseError(t, "function foo(x = await y) {}", "<stdin>: error: Expected \")\" but found \"y\"\n")
expectPrinted(t, "(function(x = await) {})", "(function(x = await) {\n});\n")
expectParseError(t, "(function(x = await y) {})", "<stdin>: error: Expected \")\" but found \"y\"\n")
expectPrinted(t, "({ foo(x = await) {} })", "({foo(x = await) {\n}});\n")
expectParseError(t, "({ foo(x = await y) {} })", "<stdin>: error: Expected \")\" but found \"y\"\n")
expectPrinted(t, "class Foo { foo(x = await) {} }", "class Foo {\n foo(x = await) {\n }\n}\n")
expectParseError(t, "class Foo { foo(x = await y) {} }", "<stdin>: error: Expected \")\" but found \"y\"\n")
expectPrinted(t, "(class { foo(x = await) {} })", "(class {\n foo(x = await) {\n }\n});\n")
expectParseError(t, "(class { foo(x = await y) {} })", "<stdin>: error: Expected \")\" but found \"y\"\n")
expectParseError(t, "(x = await) => {}", "<stdin>: error: Unexpected \")\"\n")
expectParseError(t, "(x = await y) => {}", "<stdin>: error: Cannot use an \"await\" expression here\n")
expectParseError(t, "(x = await)", "<stdin>: error: Unexpected \")\"\n")
expectPrinted(t, "(x = await y)", "x = await y;\n")
expectParseError(t, "async (x = await) => {}", "<stdin>: error: Unexpected \")\"\n")
expectParseError(t, "async (x = await y) => {}", "<stdin>: error: Cannot use an \"await\" expression here\n")
expectPrinted(t, "async(x = await y)", "async(x = await y);\n")

// Keywords with escapes
expectParseError(t, "\\u0061wait foo", "<stdin>: error: Expected \";\" but found \"foo\"\n")
expectParseError(t, "\\u0061sync function foo() {}", "<stdin>: error: Expected \";\" but found \"function\"\n")
expectParseError(t, "(\\u0061sync function() {})", "<stdin>: error: Expected \")\" but found \"function\"\n")
expectParseError(t, "+\\u0061sync function() {}", "<stdin>: error: Expected \";\" but found \"function\"\n")
expectParseError(t, "\\u0061sync () => {}", "<stdin>: error: Expected \";\" but found \"=>\"\n")
expectParseError(t, "(\\u0061sync () => {})", "<stdin>: error: Expected \")\" but found \"=>\"\n")
expectParseError(t, "+\\u0061sync () => {}", "<stdin>: error: Expected \";\" but found \"=>\"\n")

// For-await
expectParseError(t, "for await(;;);", "<stdin>: error: Unexpected \";\"\n")
expectParseError(t, "for await(x in y);", "<stdin>: error: Expected \"of\" but found \"in\"\n")
expectParseError(t, "async function foo(){for await(;;);}", "<stdin>: error: Unexpected \";\"\n")
expectParseError(t, "async function foo(){for await(let x;;);}", "<stdin>: error: Expected \"of\" but found \";\"\n")
expectPrinted(t, "async function foo(){for await(x of y);}", "async function foo() {\n for await (x of y)\n ;\n}\n")
Expand Down
Loading

0 comments on commit e87bd67

Please sign in to comment.