Skip to content

Commit d87f243

Browse files
committed
fix(@angular/build): prevent incorrect catch binding removal in downleveled for-await
When `async-await` is disabled (e.g., in Zone.js applications) and the build target is ES2019 or higher, esbuild may incorrectly remove the catch binding of a downleveled `for await...of` loop during minification. This change explicitly disables the `optional-catch-binding` feature in esbuild when `async-await` support is disabled, forcing esbuild to retain the catch binding and avoiding the minification bug. A new integration test has been added to verify that the catch binding is preserved in the optimized output.
1 parent 7b33b80 commit d87f243

File tree

2 files changed

+78
-0
lines changed

2 files changed

+78
-0
lines changed
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
/**
2+
* @license
3+
* Copyright Google LLC All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.dev/license
7+
*/
8+
9+
import { buildApplication } from '../../index';
10+
import { APPLICATION_BUILDER_INFO, BASE_OPTIONS, describeBuilder } from '../setup';
11+
12+
describeBuilder(buildApplication, APPLICATION_BUILDER_INFO, (harness) => {
13+
describe('Behavior: "Esbuild for-await"', () => {
14+
it('should properly downlevel for-await loops with optimization enabled', async () => {
15+
// Setup a for-await loop that triggers the esbuild minification bug when async/await is downleveled.
16+
await harness.writeFile(
17+
'src/main.ts',
18+
`
19+
async function test() {
20+
const someAsyncIterable = {
21+
[Symbol.asyncIterator]() {
22+
return {
23+
next() {
24+
return Promise.resolve({ done: true, value: undefined });
25+
}
26+
};
27+
}
28+
};
29+
for await(const item of someAsyncIterable) {
30+
console.log(item);
31+
}
32+
}
33+
test();
34+
`,
35+
);
36+
37+
// Ensure target is ES2022 so that optional catch binding is supported natively.
38+
await harness.modifyFile('src/tsconfig.app.json', (content) => {
39+
const tsConfig = JSON.parse(content);
40+
tsConfig.compilerOptions.target = 'ES2022';
41+
return JSON.stringify(tsConfig);
42+
});
43+
44+
harness.useTarget('build', {
45+
...BASE_OPTIONS,
46+
optimization: true,
47+
polyfills: ['zone.js'],
48+
});
49+
50+
const { result } = await harness.executeOnce();
51+
expect(result?.success).toBe(true);
52+
53+
// We expect the output to contain a catch block that captures the error in a variable,
54+
// even if that variable is mangled.
55+
// The pattern for the downleveled for-await catch block is roughly:
56+
// } catch (temp) { error = [temp]; }
57+
//
58+
// With the bug, esbuild (when minifying) would optimize away the catch binding if it thought it was unused,
59+
// resulting in: } catch { ... } which breaks the logic requiring the error object.
60+
//
61+
// The regex matches:
62+
// catch \s* -> catch keyword and whitespace
63+
// \( [a-zA-Z_$][\w$]* \) -> (variable)
64+
// \s* { \s* -> { and whitespace
65+
// [a-zA-Z_$][\w$]* -> error array variable
66+
// \s* = \s* -> assignment
67+
// \[ [a-zA-Z_$][\w$]* \] -> [variable]
68+
harness
69+
.expectFile('dist/browser/main.js')
70+
.content.toMatch(
71+
/catch\s*\([a-zA-Z_$][\w$]*\)\s*\{\s*[a-zA-Z_$][\w$]*\s*=\s*\[[a-zA-Z_$][\w$]*\]/,
72+
);
73+
});
74+
});
75+
});

packages/angular/build/src/tools/esbuild/utils.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -204,6 +204,9 @@ export function getFeatureSupport(
204204
// Native async/await is not supported with Zone.js. Disabling support here will cause
205205
// esbuild to downlevel async/await, async generators, and for await...of to a Zone.js supported form.
206206
'async-await': nativeAsyncAwait,
207+
// Workaround for an esbuild minification bug when async-await is disabled and the target is es2019+.
208+
// The catch binding for downleveled for-await will be incorrectly removed in this specific situation.
209+
...(!nativeAsyncAwait ? { 'optional-catch-binding': false } : {}),
207210
// V8 currently has a performance defect involving object spread operations that can cause signficant
208211
// degradation in runtime performance. By not supporting the language feature here, a downlevel form
209212
// will be used instead which provides a workaround for the performance issue.

0 commit comments

Comments
 (0)