diff --git a/CHANGELOG.md b/CHANGELOG.md index 29c10549cb2..ca7ffc76562 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,65 @@ # Changelog +## Unreleased + +* Fix `for await` transform when a label is present + + This release fixes a bug where the `for await` transform, which wraps the loop in a `try` statement, previously failed to also move the loop's label into the `try` statement. This bug only affects code that uses both of these features in combination. Here's an example of some affected code: + + ```js + // Original code + async function test() { + outer: for await (const x of [Promise.resolve([0, 1])]) { + for (const y of x) if (y) break outer + throw 'fail' + } + } + + // Old output (with --target=es6) + function test() { + return __async(this, null, function* () { + outer: try { + for (var iter = __forAwait([Promise.resolve([0, 1])]), more, temp, error; more = !(temp = yield iter.next()).done; more = false) { + const x = temp.value; + for (const y of x) if (y) break outer; + throw "fail"; + } + } catch (temp) { + error = [temp]; + } finally { + try { + more && (temp = iter.return) && (yield temp.call(iter)); + } finally { + if (error) + throw error[0]; + } + } + }); + } + + // New output (with --target=es6) + function test() { + return __async(this, null, function* () { + try { + outer: for (var iter = __forAwait([Promise.resolve([0, 1])]), more, temp, error; more = !(temp = yield iter.next()).done; more = false) { + const x = temp.value; + for (const y of x) if (y) break outer; + throw "fail"; + } + } catch (temp) { + error = [temp]; + } finally { + try { + more && (temp = iter.return) && (yield temp.call(iter)); + } finally { + if (error) + throw error[0]; + } + } + }); + } + ``` + ## 0.21.3 * Implement the decorator metadata proposal ([#3760](https://github.com/evanw/esbuild/issues/3760)) diff --git a/internal/bundler_tests/bundler_lower_test.go b/internal/bundler_tests/bundler_lower_test.go index c5cf8484460..c43d9f6a959 100644 --- a/internal/bundler_tests/bundler_lower_test.go +++ b/internal/bundler_tests/bundler_lower_test.go @@ -2337,6 +2337,8 @@ func TestLowerForAwait2017(t *testing.T) { async () => { for await (x.y of y) z(x) }, async () => { for await (let x of y) z(x) }, async () => { for await (const x of y) z(x) }, + async () => { label: for await (const x of y) break label }, + async () => { label: for await (const x of y) continue label }, ] `, }, @@ -2358,6 +2360,8 @@ func TestLowerForAwait2015(t *testing.T) { async () => { for await (x.y of y) z(x) }, async () => { for await (let x of y) z(x) }, async () => { for await (const x of y) z(x) }, + async () => { label: for await (const x of y) break label }, + async () => { label: for await (const x of y) continue label }, ] `, }, diff --git a/internal/bundler_tests/snapshots/snapshots_lower.txt b/internal/bundler_tests/snapshots/snapshots_lower.txt index e28e61f08c8..4c653cc40db 100644 --- a/internal/bundler_tests/snapshots/snapshots_lower.txt +++ b/internal/bundler_tests/snapshots/snapshots_lower.txt @@ -2060,6 +2060,40 @@ export default [ throw error[0]; } } + }), + () => __async(void 0, null, function* () { + try { + label: for (var iter = __forAwait(y), more, temp, error; more = !(temp = yield iter.next()).done; more = false) { + const x2 = temp.value; + break label; + } + } catch (temp) { + error = [temp]; + } finally { + try { + more && (temp = iter.return) && (yield temp.call(iter)); + } finally { + if (error) + throw error[0]; + } + } + }), + () => __async(void 0, null, function* () { + try { + label: for (var iter = __forAwait(y), more, temp, error; more = !(temp = yield iter.next()).done; more = false) { + const x2 = temp.value; + continue label; + } + } catch (temp) { + error = [temp]; + } finally { + try { + more && (temp = iter.return) && (yield temp.call(iter)); + } finally { + if (error) + throw error[0]; + } + } }) ]; @@ -2134,6 +2168,40 @@ export default [ throw error[0]; } } + }, + async () => { + try { + label: for (var iter = __forAwait(y), more, temp, error; more = !(temp = await iter.next()).done; more = false) { + const x2 = temp.value; + break label; + } + } catch (temp) { + error = [temp]; + } finally { + try { + more && (temp = iter.return) && await temp.call(iter); + } finally { + if (error) + throw error[0]; + } + } + }, + async () => { + try { + label: for (var iter = __forAwait(y), more, temp, error; more = !(temp = await iter.next()).done; more = false) { + const x2 = temp.value; + continue label; + } + } catch (temp) { + error = [temp]; + } finally { + try { + more && (temp = iter.return) && await temp.call(iter); + } finally { + if (error) + throw error[0]; + } + } } ]; diff --git a/internal/js_parser/js_parser.go b/internal/js_parser/js_parser.go index d6880880a59..574b26e2c1b 100644 --- a/internal/js_parser/js_parser.go +++ b/internal/js_parser/js_parser.go @@ -10284,6 +10284,18 @@ func (p *parser) visitAndAppendStmt(stmts []js_ast.Stmt, stmt js_ast.Stmt) []js_ } } + // Handle "for await" that has been lowered by moving this label inside the "try" + if try, ok := s.Stmt.Data.(*js_ast.STry); ok && len(try.Block.Stmts) > 0 { + if _, ok := try.Block.Stmts[0].Data.(*js_ast.SFor); ok { + try.Block.Stmts[0] = js_ast.Stmt{Loc: stmt.Loc, Data: &js_ast.SLabel{ + Stmt: try.Block.Stmts[0], + Name: s.Name, + IsSingleLineStmt: s.IsSingleLineStmt, + }} + return append(stmts, s.Stmt) + } + } + case *js_ast.SLocal: // Silently remove unsupported top-level "await" in dead code branches if s.Kind == js_ast.LocalAwaitUsing && p.fnOrArrowDataVisit.isOutsideFnOrArrow { diff --git a/scripts/end-to-end-tests.js b/scripts/end-to-end-tests.js index c2370ba65cc..c3dea204db4 100644 --- a/scripts/end-to-end-tests.js +++ b/scripts/end-to-end-tests.js @@ -7149,6 +7149,23 @@ for (let flags of [[], ['--target=es2017'], ['--target=es6']]) { } `, }, { async: true }), + + // https://github.com/arogozine/LinqToTypeScript/issues/29 + test(['in.js', '--outfile=node.js'].concat(flags), { + 'in.js': ` + exports.async = async () => { + let total = 0 + outer: + for await (const n of [Promise.resolve(1), Promise.resolve(2), Promise.resolve(5)]) { + for (let i = 1; i <= n; i++) { + if (i === 4) continue outer + total += i + } + } + if (total !== 1 + (1 + 2) + (1 + 2 + 3)) throw 'fail' + } + `, + }, { async: true }), ) }