Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.dev/license
*/

import { buildApplication } from '../../index';
import { APPLICATION_BUILDER_INFO, BASE_OPTIONS, describeBuilder } from '../setup';

describeBuilder(buildApplication, APPLICATION_BUILDER_INFO, (harness) => {
describe('Behavior: "Esbuild for-await"', () => {
it('should properly downlevel for-await loops with optimization enabled', async () => {
// Setup a for-await loop that triggers the esbuild minification bug when async/await is downleveled.
await harness.writeFile(
'src/main.ts',
`
async function test() {
const someAsyncIterable = {
[Symbol.asyncIterator]() {
return {
next() {
return Promise.resolve({ done: true, value: undefined });
}
};
}
};
for await(const item of someAsyncIterable) {
console.log(item);
}
}
test();
`,
);

// Ensure target is ES2022 so that optional catch binding is supported natively.
await harness.modifyFile('src/tsconfig.app.json', (content) => {
const tsConfig = JSON.parse(content);
tsConfig.compilerOptions.target = 'ES2022';
return JSON.stringify(tsConfig);
});

harness.useTarget('build', {
...BASE_OPTIONS,
optimization: true,
polyfills: ['zone.js'],
});

const { result } = await harness.executeOnce();
expect(result?.success).toBe(true);

// We expect the output to contain a catch block that captures the error in a variable,
// even if that variable is mangled.
// The pattern for the downleveled for-await catch block is roughly:
// } catch (temp) { error = [temp]; }
//
// With the bug, esbuild (when minifying) would optimize away the catch binding if it thought it was unused,
// resulting in: } catch { ... } which breaks the logic requiring the error object.
//
// The regex matches:
// catch \s* -> catch keyword and whitespace
// \( [a-zA-Z_$][\w$]* \) -> (variable)
// \s* { \s* -> { and whitespace
// [a-zA-Z_$][\w$]* -> error array variable
// \s* = \s* -> assignment
// \[ [a-zA-Z_$][\w$]* \] -> [variable]
harness
.expectFile('dist/browser/main.js')
.content.toMatch(
/catch\s*\([a-zA-Z_$][\w$]*\)\s*\{\s*[a-zA-Z_$][\w$]*\s*=\s*\[[a-zA-Z_$][\w$]*\]/,
);
});
});
});
3 changes: 3 additions & 0 deletions packages/angular/build/src/tools/esbuild/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,9 @@ export function getFeatureSupport(
// Native async/await is not supported with Zone.js. Disabling support here will cause
// esbuild to downlevel async/await, async generators, and for await...of to a Zone.js supported form.
'async-await': nativeAsyncAwait,
// Workaround for an esbuild minification bug when async-await is disabled and the target is es2019+.
// The catch binding for downleveled for-await will be incorrectly removed in this specific situation.
...(!nativeAsyncAwait ? { 'optional-catch-binding': false } : {}),
// V8 currently has a performance defect involving object spread operations that can cause signficant
// degradation in runtime performance. By not supporting the language feature here, a downlevel form
// will be used instead which provides a workaround for the performance issue.
Expand Down