From 5fe21253ee75fb4c5ea395a0877b2a5ab51c3575 Mon Sep 17 00:00:00 2001 From: Evan Wallace Date: Thu, 19 Jan 2023 21:24:34 -0500 Subject: [PATCH] fix #2853: permit top-level await in dead code --- CHANGELOG.md | 10 ++ internal/bundler/bundler.go | 6 +- .../bundler_tests/bundler_default_test.go | 148 +++++++++++++++++- .../snapshots/snapshots_default.txt | 118 +++++++++++++- internal/config/config.go | 2 +- internal/js_ast/js_ast.go | 13 +- internal/js_parser/js_parser.go | 50 ++++-- internal/js_parser/js_parser_lower.go | 2 +- internal/js_parser/js_parser_lower_test.go | 5 + internal/js_parser/js_parser_test.go | 16 ++ internal/js_printer/js_printer.go | 4 +- internal/linker/linker.go | 18 +-- internal/resolver/resolver.go | 2 +- 13 files changed, 354 insertions(+), 40 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b1f22336ba5..e94a8b1b88d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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)) diff --git a/internal/bundler/bundler.go b/internal/bundler/bundler.go index c8ab7a2a26e..6f86bb6f5da 100644 --- a/internal/bundler/bundler.go +++ b/internal/bundler/bundler.go @@ -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) } @@ -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 } diff --git a/internal/bundler_tests/bundler_default_test.go b/internal/bundler_tests/bundler_default_test.go index 2a94d636958..fb691400141 100644 --- a/internal/bundler_tests/bundler_default_test.go +++ b/internal/bundler_tests/bundler_default_test.go @@ -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{ @@ -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{ @@ -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{ @@ -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": ` @@ -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{ @@ -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{ @@ -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{ @@ -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{ diff --git a/internal/bundler_tests/snapshots/snapshots_default.txt b/internal/bundler_tests/snapshots/snapshots_default.txt index 5fae281d922..8412070cfa9 100644 --- a/internal/bundler_tests/snapshots/snapshots_default.txt +++ b/internal/bundler_tests/snapshots/snapshots_default.txt @@ -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 ---------- @@ -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 ---------- @@ -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 ---------- diff --git a/internal/config/config.go b/internal/config/config.go index b2530ce0e6e..1aaf82667e8 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -184,7 +184,7 @@ const ( FormatESModule ) -func (f Format) KeepES6ImportExportSyntax() bool { +func (f Format) KeepESMImportExportSyntax() bool { return f == FormatPreserve || f == FormatESModule } diff --git a/internal/js_ast/js_ast.go b/internal/js_ast/js_ast.go index 48a0255ca11..90f1cd14e04 100644 --- a/internal/js_ast/js_ast.go +++ b/internal/js_ast/js_ast.go @@ -972,10 +972,10 @@ type SForIn struct { } type SForOf struct { - Init Stmt // May be a SConst, SLet, SVar, or SExpr - Value Expr - Body Stmt - IsAwait bool + Init Stmt // May be a SConst, SLet, SVar, or SExpr + Value Expr + Body Stmt + Await logger.Range } type SDoWhile struct { @@ -1864,8 +1864,9 @@ type AST struct { // This is a list of ES6 features. They are ranges instead of booleans so // that they can be used in log messages. Check to see if "Len > 0". - ExportKeyword logger.Range // Does not include TypeScript-specific syntax - TopLevelAwaitKeyword logger.Range + ExportKeyword logger.Range // Does not include TypeScript-specific syntax + TopLevelAwaitKeyword logger.Range + LiveTopLevelAwaitKeyword logger.Range // Excludes top-level await in dead branches ExportsRef Ref ModuleRef Ref diff --git a/internal/js_parser/js_parser.go b/internal/js_parser/js_parser.go index 2820b0ce98a..7f48ad314b7 100644 --- a/internal/js_parser/js_parser.go +++ b/internal/js_parser/js_parser.go @@ -225,6 +225,7 @@ type parser struct { esmExportKeyword logger.Range enclosingClassKeyword logger.Range topLevelAwaitKeyword logger.Range + liveTopLevelAwaitKeyword logger.Range latestArrowArgLoc logger.Loc forbidSuffixAfterAsLoc logger.Loc @@ -3213,7 +3214,6 @@ func (p *parser) parsePrefix(level js_ast.L, errors *deferredErrors, flags exprF } else { if p.fnOrArrowDataParse.isTopLevel { p.topLevelAwaitKeyword = nameRange - p.markSyntaxFeature(compat.TopLevelAwait, nameRange) } if p.fnOrArrowDataParse.arrowArgErrors != nil { p.fnOrArrowDataParse.arrowArgErrors.invalidExprAwait = nameRange @@ -6783,17 +6783,16 @@ func (p *parser) parseStmt(opts parseStmtOpts) js_ast.Stmt { p.lexer.Next() // "for await (let x of y) {}" - isForAwait := p.lexer.IsContextualKeyword("await") - if isForAwait { - awaitRange := p.lexer.Range() + var awaitRange logger.Range + if p.lexer.IsContextualKeyword("await") { + awaitRange = p.lexer.Range() if p.fnOrArrowDataParse.await != allowExpr { p.log.AddError(&p.tracker, awaitRange, "Cannot use \"await\" outside an async function") - isForAwait = false + awaitRange = logger.Range{} } else { didGenerateError := false if p.fnOrArrowDataParse.isTopLevel { p.topLevelAwaitKeyword = awaitRange - didGenerateError = p.markSyntaxFeature(compat.TopLevelAwait, awaitRange) } if !didGenerateError && p.options.unsupportedJSFeatures.Has(compat.AsyncAwait) && p.options.unsupportedJSFeatures.Has(compat.Generator) { // If for-await loops aren't supported, then we only support lowering @@ -6842,7 +6841,7 @@ func (p *parser) parseStmt(opts parseStmtOpts) js_ast.Stmt { expr, stmt, decls = p.parseExprOrLetStmt(parseStmtOpts{ lexicalDecl: lexicalDeclAllowAll, isForLoopInit: true, - isForAwaitLoopInit: isForAwait, + isForAwaitLoopInit: awaitRange.Len > 0, }) if stmt.Data != nil { badLetRange = logger.Range{} @@ -6856,11 +6855,11 @@ func (p *parser) parseStmt(opts parseStmtOpts) js_ast.Stmt { p.allowIn = true // Detect for-of loops - if p.lexer.IsContextualKeyword("of") || isForAwait { + if p.lexer.IsContextualKeyword("of") || awaitRange.Len > 0 { if badLetRange.Len > 0 { p.log.AddError(&p.tracker, badLetRange, "\"let\" must be wrapped in parentheses to be used as an expression here:") } - if isForAwait && !p.lexer.IsContextualKeyword("of") { + if awaitRange.Len > 0 && !p.lexer.IsContextualKeyword("of") { if initOrNil.Data != nil { p.lexer.ExpectedString("\"of\"") } else { @@ -6873,7 +6872,7 @@ func (p *parser) parseStmt(opts parseStmtOpts) js_ast.Stmt { value := p.parseExpr(js_ast.LComma) p.lexer.Expect(js_lexer.TCloseParen) body := p.parseStmt(parseStmtOpts{}) - return js_ast.Stmt{Loc: loc, Data: &js_ast.SForOf{IsAwait: isForAwait, Init: initOrNil, Value: value, Body: body}} + return js_ast.Stmt{Loc: loc, Data: &js_ast.SForOf{Await: awaitRange, Init: initOrNil, Value: value, Body: body}} } // Detect for-in loops @@ -9859,6 +9858,16 @@ func (p *parser) visitAndAppendStmt(stmts []js_ast.Stmt, stmt js_ast.Stmt) []js_ p.lowerObjectRestInForLoopInit(s.Init, &s.Body) case *js_ast.SForOf: + // Silently remove unsupported top-level "await" in dead code branches + if s.Await.Len > 0 && p.fnOrArrowDataVisit.isOutsideFnOrArrow { + if p.isControlFlowDead && (p.options.unsupportedJSFeatures.Has(compat.TopLevelAwait) || !p.options.outputFormat.KeepESMImportExportSyntax()) { + s.Await = logger.Range{} + } else { + p.liveTopLevelAwaitKeyword = s.Await + p.markSyntaxFeature(compat.TopLevelAwait, s.Await) + } + } + p.pushScopeForVisitPass(js_ast.ScopeBlock, stmt.Loc) p.visitForLoopInit(s.Init, true) s.Value = p.visitExpr(s.Value) @@ -9876,7 +9885,7 @@ func (p *parser) visitAndAppendStmt(stmts []js_ast.Stmt, stmt js_ast.Stmt) []js_ p.lowerObjectRestInForLoopInit(s.Init, &s.Body) - if s.IsAwait && p.options.unsupportedJSFeatures.Has(compat.ForAwait) { + if s.Await.Len > 0 && p.options.unsupportedJSFeatures.Has(compat.ForAwait) { return p.lowerForAwaitLoop(stmt.Loc, s, stmts) } @@ -11599,7 +11608,7 @@ func (p *parser) valueForThis( func (p *parser) valueForImportMeta(loc logger.Loc) (js_ast.Expr, bool) { if p.options.unsupportedJSFeatures.Has(compat.ImportMeta) || - (p.options.mode != config.ModePassThrough && !p.options.outputFormat.KeepES6ImportExportSyntax()) { + (p.options.mode != config.ModePassThrough && !p.options.outputFormat.KeepESMImportExportSyntax()) { // Generate the variable if it doesn't exist yet if p.importMetaRef == js_ast.InvalidRef { p.importMetaRef = p.newSymbol(js_ast.SymbolOther, "import_meta") @@ -11909,7 +11918,7 @@ func (p *parser) visitExprInOut(expr js_ast.Expr, in exprIn) (js_ast.Expr, exprO if p.options.unsupportedJSFeatures.Has(compat.ImportMeta) { r := logger.Range{Loc: expr.Loc, Len: e.RangeLen} p.markSyntaxFeature(compat.ImportMeta, r) - } else if p.options.mode != config.ModePassThrough && !p.options.outputFormat.KeepES6ImportExportSyntax() { + } else if p.options.mode != config.ModePassThrough && !p.options.outputFormat.KeepESMImportExportSyntax() { r := logger.Range{Loc: expr.Loc, Len: e.RangeLen} kind := logger.Warning if p.suppressWarningsAboutWeirdCode || p.fnOrArrowDataVisit.tryBodyCount > 0 { @@ -13332,6 +13341,16 @@ func (p *parser) visitExprInOut(expr js_ast.Expr, in exprIn) (js_ast.Expr, exprO } case *js_ast.EAwait: + // Silently remove unsupported top-level "await" in dead code branches + if p.fnOrArrowDataVisit.isOutsideFnOrArrow { + if p.isControlFlowDead && (p.options.unsupportedJSFeatures.Has(compat.TopLevelAwait) || !p.options.outputFormat.KeepESMImportExportSyntax()) { + return p.visitExprInOut(e.Value, in) + } else { + p.liveTopLevelAwaitKeyword = logger.Range{Loc: expr.Loc, Len: 5} + p.markSyntaxFeature(compat.TopLevelAwait, logger.Range{Loc: expr.Loc, Len: 5}) + } + } + p.awaitTarget = e.Value.Data e.Value = p.visitExpr(e.Value) @@ -16075,7 +16094,8 @@ func (p *parser) toAST(before, parts, after []js_ast.Part, hashbang string, dire ExportsKind: exportsKind, // ES6 features - ExportKeyword: p.esmExportKeyword, - TopLevelAwaitKeyword: p.topLevelAwaitKeyword, + ExportKeyword: p.esmExportKeyword, + TopLevelAwaitKeyword: p.topLevelAwaitKeyword, + LiveTopLevelAwaitKeyword: p.liveTopLevelAwaitKeyword, } } diff --git a/internal/js_parser/js_parser_lower.go b/internal/js_parser/js_parser_lower.go index 6f664bb8249..120e4b7f6b5 100644 --- a/internal/js_parser/js_parser_lower.go +++ b/internal/js_parser/js_parser_lower.go @@ -49,7 +49,7 @@ func (p *parser) markSyntaxFeature(feature compat.JSFeature, r logger.Range) (di didGenerateError = true if !p.options.unsupportedJSFeatures.Has(feature) { - if feature == compat.TopLevelAwait && !p.options.outputFormat.KeepES6ImportExportSyntax() { + if feature == compat.TopLevelAwait && !p.options.outputFormat.KeepESMImportExportSyntax() { p.log.AddError(&p.tracker, r, fmt.Sprintf( "Top-level await is currently not supported with the %q output format", p.options.outputFormat.String())) return diff --git a/internal/js_parser/js_parser_lower_test.go b/internal/js_parser/js_parser_lower_test.go index 5461770a802..ab0ab2c972e 100644 --- a/internal/js_parser/js_parser_lower_test.go +++ b/internal/js_parser/js_parser_lower_test.go @@ -736,4 +736,9 @@ func TestForAwait(t *testing.T) { // Can't use for-await at the top-level without top-level await err = ": ERROR: Top-level await is not available in the configured target environment\n" expectParseErrorWithUnsupportedFeatures(t, compat.TopLevelAwait, "for await (x of y) ;", err) + expectParseErrorWithUnsupportedFeatures(t, compat.TopLevelAwait, "if (true) for await (x of y) ;", err) + expectPrintedWithUnsupportedFeatures(t, compat.TopLevelAwait, "if (false) for await (x of y) ;", "if (false)\n for (x of y)\n ;\n") + expectParseErrorWithUnsupportedFeatures(t, compat.TopLevelAwait, "with (x) y; if (false) for await (x of y) ;", + ": ERROR: With statements cannot be used in an ECMAScript module\n"+ + ": NOTE: This file is considered to be an ECMAScript module because of the top-level \"await\" keyword here:\n") } diff --git a/internal/js_parser/js_parser_test.go b/internal/js_parser/js_parser_test.go index 7c95e7c7049..1c44ae8faae 100644 --- a/internal/js_parser/js_parser_test.go +++ b/internal/js_parser/js_parser_test.go @@ -44,6 +44,13 @@ func expectParseErrorTarget(t *testing.T, esVersion int, contents string, expect }) } +func expectPrintedWithUnsupportedFeatures(t *testing.T, unsupportedJSFeatures compat.JSFeature, contents string, expected string) { + t.Helper() + expectPrintedCommon(t, contents, expected, config.Options{ + UnsupportedJSFeatures: unsupportedJSFeatures, + }) +} + func expectParseErrorWithUnsupportedFeatures(t *testing.T, unsupportedJSFeatures compat.JSFeature, contents string, expected string) { t.Helper() expectParseErrorCommon(t, contents, expected, config.Options{ @@ -678,6 +685,15 @@ func TestAwait(t *testing.T) { : NOTE: This file is considered to be an ECMAScript module because of the top-level "await" keyword here: `) expectPrinted(t, "async function f() { await delete x }", "async function f() {\n await delete x;\n}\n") + + // Can't use await at the top-level without top-level await + err := ": ERROR: Top-level await is not available in the configured target environment\n" + expectParseErrorWithUnsupportedFeatures(t, compat.TopLevelAwait, "await x;", err) + expectParseErrorWithUnsupportedFeatures(t, compat.TopLevelAwait, "if (true) await x;", err) + expectPrintedWithUnsupportedFeatures(t, compat.TopLevelAwait, "if (false) await x;", "if (false)\n x;\n") + expectParseErrorWithUnsupportedFeatures(t, compat.TopLevelAwait, "with (x) y; if (false) await x;", + ": ERROR: With statements cannot be used in an ECMAScript module\n"+ + ": NOTE: This file is considered to be an ECMAScript module because of the top-level \"await\" keyword here:\n") } func TestRegExp(t *testing.T) { diff --git a/internal/js_printer/js_printer.go b/internal/js_printer/js_printer.go index c78891909fe..22a3cfa9cd9 100644 --- a/internal/js_printer/js_printer.go +++ b/internal/js_printer/js_printer.go @@ -3913,7 +3913,7 @@ func (p *printer) printStmt(stmt js_ast.Stmt, flags printStmtFlags) { p.printIndent() p.printSpaceBeforeIdentifier() p.print("for") - if s.IsAwait { + if s.Await.Len > 0 { p.print(" await") } p.printSpace() @@ -3921,7 +3921,7 @@ func (p *printer) printStmt(stmt js_ast.Stmt, flags printStmtFlags) { hasInitComment := p.willPrintExprCommentsAtLoc(s.Init.Loc) hasValueComment := p.willPrintExprCommentsAtLoc(s.Value.Loc) flags := forbidIn | isFollowedByOf - if s.IsAwait { + if s.Await.Len > 0 { flags |= isInsideForAwait } if hasInitComment || hasValueComment { diff --git a/internal/linker/linker.go b/internal/linker/linker.go index 45bc50e0674..aed6e20500f 100644 --- a/internal/linker/linker.go +++ b/internal/linker/linker.go @@ -277,7 +277,7 @@ func Link( // format is not ESM-compatible since that avoids generating the ESM-to-CJS // machinery. if repr.AST.HasLazyExport && (c.options.Mode == config.ModePassThrough || - (c.options.Mode == config.ModeConvertFormat && !c.options.OutputFormat.KeepES6ImportExportSyntax())) { + (c.options.Mode == config.ModeConvertFormat && !c.options.OutputFormat.KeepESMImportExportSyntax())) { repr.AST.ExportsKind = js_ast.ExportsCommonJS } @@ -1716,7 +1716,7 @@ func (c *linkerContext) scanImportsAndExports() { // Don't follow external imports (this includes import() expressions) if !record.SourceIndex.IsValid() || c.isExternalDynamicImport(record, sourceIndex) { // This is an external import. Check if it will be a "require()" call. - if record.Kind == ast.ImportRequire || !c.options.OutputFormat.KeepES6ImportExportSyntax() || + if record.Kind == ast.ImportRequire || !c.options.OutputFormat.KeepESMImportExportSyntax() || (record.Kind == ast.ImportDynamic && c.options.UnsupportedJSFeatures.Has(compat.DynamicImport)) { // We should use "__require" instead of "require" if we're not // generating a CommonJS output file, since it won't exist otherwise @@ -1815,7 +1815,7 @@ func (c *linkerContext) scanImportsAndExports() { record := &repr.AST.ImportRecords[importRecordIndex] // Is this export star evaluated at run time? - happensAtRunTime := !record.SourceIndex.IsValid() && (!file.IsEntryPoint() || !c.options.OutputFormat.KeepES6ImportExportSyntax()) + happensAtRunTime := !record.SourceIndex.IsValid() && (!file.IsEntryPoint() || !c.options.OutputFormat.KeepESMImportExportSyntax()) if record.SourceIndex.IsValid() { otherSourceIndex := record.SourceIndex.GetIndex() otherRepr := c.graph.Files[otherSourceIndex].InputFile.Repr.(*graph.JSRepr) @@ -2322,7 +2322,7 @@ loop: nextTracker, status, potentiallyAmbiguousExportStarRefs := c.advanceImportTracker(tracker) switch status { case importCommonJS, importCommonJSWithoutExports, importExternal, importDisabled: - if status == importExternal && c.options.OutputFormat.KeepES6ImportExportSyntax() { + if status == importExternal && c.options.OutputFormat.KeepESMImportExportSyntax() { // Imports from external modules should not be converted to CommonJS // if the output format preserves the original ES6 import statements break @@ -2534,7 +2534,7 @@ func (c *linkerContext) hasDynamicExportsDueToExportStar(sourceIndex uint32, vis // 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.graph.Files[sourceIndex].IsEntryPoint() || !c.options.OutputFormat.KeepES6ImportExportSyntax())) || + if (!record.SourceIndex.IsValid() && (!c.graph.Files[sourceIndex].IsEntryPoint() || !c.options.OutputFormat.KeepESMImportExportSyntax())) || (record.SourceIndex.IsValid() && record.SourceIndex.GetIndex() != sourceIndex && c.hasDynamicExportsDueToExportStar(record.SourceIndex.GetIndex(), visited)) { repr.AST.ExportsKind = js_ast.ExportsESMWithDynamicFallback return true @@ -3426,7 +3426,7 @@ func (c *linkerContext) shouldRemoveImportExportStmt( // Is this an external import? if !record.SourceIndex.IsValid() { // Keep the "import" statement if "import" statements are supported - if c.options.OutputFormat.KeepES6ImportExportSyntax() { + if c.options.OutputFormat.KeepESMImportExportSyntax() { return false } @@ -3556,7 +3556,7 @@ func (c *linkerContext) convertStmtsForChunk(sourceIndex uint32, stmtList *stmtL record := &repr.AST.ImportRecords[s.ImportRecordIndex] // Is this export star evaluated at run time? - if !record.SourceIndex.IsValid() && c.options.OutputFormat.KeepES6ImportExportSyntax() { + if !record.SourceIndex.IsValid() && c.options.OutputFormat.KeepESMImportExportSyntax() { if record.Flags.Has(ast.CallsRunTimeReExportFn) { // Turn this statement into "import * as ns from 'path'" stmt.Data = &js_ast.SImport{ @@ -4602,7 +4602,7 @@ func (c *linkerContext) renameSymbolsInChunk(chunk *chunkInfo, filesInOrder []ui // wrapper if the output format supports import statements. We need to // add those symbols to the top-level scope to avoid causing name // collisions. This code special-cases only those symbols. - if c.options.OutputFormat.KeepES6ImportExportSyntax() { + if c.options.OutputFormat.KeepESMImportExportSyntax() { for _, part := range repr.AST.Parts { for _, stmt := range part.Stmts { switch s := stmt.Data.(type) { @@ -4897,7 +4897,7 @@ func (c *linkerContext) generateChunkJS(chunkIndex int, chunkWaitGroup *sync.Wai // Print exports jMeta.AddString("],\n \"exports\": [") var aliases []string - if c.options.OutputFormat.KeepES6ImportExportSyntax() { + if c.options.OutputFormat.KeepESMImportExportSyntax() { if chunk.isEntryPoint { if fileRepr := c.graph.Files[chunk.sourceIndex].InputFile.Repr.(*graph.JSRepr); fileRepr.Meta.Wrap == graph.WrapCJS { aliases = []string{"default"} diff --git a/internal/resolver/resolver.go b/internal/resolver/resolver.go index 7a1220a10da..5823b81abe1 100644 --- a/internal/resolver/resolver.go +++ b/internal/resolver/resolver.go @@ -404,7 +404,7 @@ func (res *Resolver) Resolve(sourceDir string, importPath string, kind ast.Impor } // Check whether the path will end up as "import" or "require" - convertImportToRequire := !r.options.OutputFormat.KeepES6ImportExportSyntax() + convertImportToRequire := !r.options.OutputFormat.KeepESMImportExportSyntax() isImport := !convertImportToRequire && (kind == ast.ImportStmt || kind == ast.ImportDynamic) isRequire := kind == ast.ImportRequire || kind == ast.ImportRequireResolve || (convertImportToRequire && (kind == ast.ImportStmt || kind == ast.ImportDynamic))