Skip to content

Commit 8f7a7f6

Browse files
committed
feat(@angular/build): support WASM/ES Module integration proposal
Browser application builds will now support the direct import of WASM files. The behavior follows the WebAssembly/ES mode integration proposal. The usage of this feature requires the ability to use native async/await and top-level await. Due to this requirement, applications must be zoneless to use this new feature. Applications that use Zone.js are currently incompatible and an error will be generated if the feature is used in a Zone.js application. Manual setup of a WASM file is, however, possible in a Zone.js application if WASM usage is required. Further details for manual setup can be found here: https://developer.mozilla.org/en-US/docs/WebAssembly/Loading_and_running The following is a brief example of using a WASM file in the new feature with the integration proposal behavior: ``` import { multiply } from './example.wasm'; console.log(multiply(4, 5)); ``` NOTE: TypeScript will not automatically understand the types for WASM files. Type definition files will need to be created for each WASM file to allow for an error-free build. These type definition files are specific to each individual WASM file and will either need to be manually created or provided by library authors. The feature relies on an active proposal which may change as it progresses through the standardization process. This may result in behavioral differences between versions. Proposal Details: https://github.com/WebAssembly/esm-integration/tree/main/proposals/esm-integration
1 parent 394e761 commit 8f7a7f6

File tree

4 files changed

+545
-1
lines changed

4 files changed

+545
-1
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,275 @@
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+
/**
13+
* Compiled and base64 encoded WASM file for the following WAT:
14+
* ```
15+
* (module
16+
* (export "multiply" (func $multiply))
17+
* (func $multiply (param i32 i32) (result i32)
18+
* local.get 0
19+
* local.get 1
20+
* i32.mul
21+
* )
22+
* )
23+
* ```
24+
*/
25+
const exportWasmBase64 =
26+
'AGFzbQEAAAABBwFgAn9/AX8DAgEABwwBCG11bHRpcGx5AAAKCQEHACAAIAFsCwAXBG5hbWUBCwEACG11bHRpcGx5AgMBAAA=';
27+
const exportWasmBytes = Buffer.from(exportWasmBase64, 'base64');
28+
29+
/**
30+
* Compiled and base64 encoded WASM file for the following WAT:
31+
* ```
32+
* (module
33+
* (import "./values" "getValue" (func $getvalue (result i32)))
34+
* (export "multiply" (func $multiply))
35+
* (export "subtract1" (func $subtract))
36+
* (func $multiply (param i32 i32) (result i32)
37+
* local.get 0
38+
* local.get 1
39+
* i32.mul
40+
* )
41+
* (func $subtract (param i32) (result i32)
42+
* call $getvalue
43+
* local.get 0
44+
* i32.sub
45+
* )
46+
* )
47+
* ```
48+
*/
49+
const importWasmBase64 =
50+
'AGFzbQEAAAABEANgAAF/YAJ/fwF/YAF/AX8CFQEILi92YWx1ZXMIZ2V0VmFsdWUAAAMDAgECBxgCCG11bHRpcGx5AAEJc3VidHJhY3QxAAIKEQIHACAAIAFsCwcAEAAgAGsLAC8EbmFtZQEfAwAIZ2V0dmFsdWUBCG11bHRpcGx5AghzdWJ0cmFjdAIHAwAAAQACAA==';
51+
const importWasmBytes = Buffer.from(importWasmBase64, 'base64');
52+
53+
describeBuilder(buildApplication, APPLICATION_BUILDER_INFO, (harness) => {
54+
describe('Behavior: "Supports WASM/ES module integration"', () => {
55+
it('should inject initialization code and add an export', async () => {
56+
harness.useTarget('build', {
57+
...BASE_OPTIONS,
58+
});
59+
60+
// Create WASM file
61+
await harness.writeFile('src/multiply.wasm', exportWasmBytes);
62+
63+
// Create main file that uses the WASM file
64+
await harness.writeFile(
65+
'src/main.ts',
66+
`
67+
// @ts-ignore
68+
import { multiply } from './multiply.wasm';
69+
70+
console.log(multiply(4, 5));
71+
`,
72+
);
73+
74+
const { result } = await harness.executeOnce();
75+
expect(result?.success).toBeTrue();
76+
77+
// Ensure initialization code and export name is present in output code
78+
harness.expectFile('dist/browser/main.js').content.toContain('WebAssembly.instantiate');
79+
harness.expectFile('dist/browser/main.js').content.toContain('multiply');
80+
});
81+
82+
it('should compile successfully with a provided type definition file', async () => {
83+
harness.useTarget('build', {
84+
...BASE_OPTIONS,
85+
});
86+
87+
// Create WASM file
88+
await harness.writeFile('src/multiply.wasm', exportWasmBytes);
89+
await harness.writeFile(
90+
'src/multiply.wasm.d.ts',
91+
'export declare function multiply(a: number, b: number): number;',
92+
);
93+
94+
// Create main file that uses the WASM file
95+
await harness.writeFile(
96+
'src/main.ts',
97+
`
98+
import { multiply } from './multiply.wasm';
99+
100+
console.log(multiply(4, 5));
101+
`,
102+
);
103+
104+
const { result } = await harness.executeOnce();
105+
expect(result?.success).toBeTrue();
106+
107+
// Ensure initialization code and export name is present in output code
108+
harness.expectFile('dist/browser/main.js').content.toContain('WebAssembly.instantiate');
109+
harness.expectFile('dist/browser/main.js').content.toContain('multiply');
110+
});
111+
112+
it('should add WASM defined imports and include resolved TS file for import', async () => {
113+
harness.useTarget('build', {
114+
...BASE_OPTIONS,
115+
});
116+
117+
// Create WASM file
118+
await harness.writeFile('src/subtract.wasm', importWasmBytes);
119+
120+
// Create TS file that is expect by WASM file
121+
await harness.writeFile(
122+
'src/values.ts',
123+
`
124+
export function getValue(): number { return 100; }
125+
`,
126+
);
127+
// The file is not imported into any actual TS files so it needs to be manually added to the TypeScript program
128+
await harness.modifyFile('src/tsconfig.app.json', (content) =>
129+
content.replace('"main.ts",', '"main.ts","values.ts",'),
130+
);
131+
132+
// Create main file that uses the WASM file
133+
await harness.writeFile(
134+
'src/main.ts',
135+
`
136+
// @ts-ignore
137+
import { subtract1 } from './subtract.wasm';
138+
139+
console.log(subtract1(5));
140+
`,
141+
);
142+
143+
const { result } = await harness.executeOnce();
144+
expect(result?.success).toBeTrue();
145+
146+
// Ensure initialization code and export name is present in output code
147+
harness.expectFile('dist/browser/main.js').content.toContain('WebAssembly.instantiate');
148+
harness.expectFile('dist/browser/main.js').content.toContain('subtract1');
149+
harness.expectFile('dist/browser/main.js').content.toContain('./values');
150+
harness.expectFile('dist/browser/main.js').content.toContain('getValue');
151+
});
152+
153+
it('should add WASM defined imports and include resolved JS file for import', async () => {
154+
harness.useTarget('build', {
155+
...BASE_OPTIONS,
156+
});
157+
158+
// Create WASM file
159+
await harness.writeFile('src/subtract.wasm', importWasmBytes);
160+
161+
// Create JS file that is expect by WASM file
162+
await harness.writeFile(
163+
'src/values.js',
164+
`
165+
export function getValue() { return 100; }
166+
`,
167+
);
168+
169+
// Create main file that uses the WASM file
170+
await harness.writeFile(
171+
'src/main.ts',
172+
`
173+
// @ts-ignore
174+
import { subtract1 } from './subtract.wasm';
175+
176+
console.log(subtract1(5));
177+
`,
178+
);
179+
180+
const { result } = await harness.executeOnce();
181+
expect(result?.success).toBeTrue();
182+
183+
// Ensure initialization code and export name is present in output code
184+
harness.expectFile('dist/browser/main.js').content.toContain('WebAssembly.instantiate');
185+
harness.expectFile('dist/browser/main.js').content.toContain('subtract1');
186+
harness.expectFile('dist/browser/main.js').content.toContain('./values');
187+
harness.expectFile('dist/browser/main.js').content.toContain('getValue');
188+
});
189+
190+
it('should inline WASM files less than 10kb', async () => {
191+
harness.useTarget('build', {
192+
...BASE_OPTIONS,
193+
});
194+
195+
// Create WASM file
196+
await harness.writeFile('src/multiply.wasm', exportWasmBytes);
197+
198+
// Create main file that uses the WASM file
199+
await harness.writeFile(
200+
'src/main.ts',
201+
`
202+
// @ts-ignore
203+
import { multiply } from './multiply.wasm';
204+
205+
console.log(multiply(4, 5));
206+
`,
207+
);
208+
209+
const { result } = await harness.executeOnce();
210+
expect(result?.success).toBeTrue();
211+
212+
// Ensure WASM is present in output code
213+
harness.expectFile('dist/browser/main.js').content.toContain(exportWasmBase64);
214+
});
215+
216+
it('should show an error on invalid WASM file', async () => {
217+
harness.useTarget('build', {
218+
...BASE_OPTIONS,
219+
});
220+
221+
// Create WASM file
222+
await harness.writeFile('src/multiply.wasm', 'NOT_WASM');
223+
224+
// Create main file that uses the WASM file
225+
await harness.writeFile(
226+
'src/main.ts',
227+
`
228+
// @ts-ignore
229+
import { multiply } from './multiply.wasm';
230+
231+
console.log(multiply(4, 5));
232+
`,
233+
);
234+
235+
const { result, logs } = await harness.executeOnce({ outputLogsOnFailure: false });
236+
expect(result?.success).toBeFalse();
237+
expect(logs).toContain(
238+
jasmine.objectContaining({
239+
message: jasmine.stringMatching('Unable to analyze WASM file'),
240+
}),
241+
);
242+
});
243+
244+
it('should show an error if using Zone.js', async () => {
245+
harness.useTarget('build', {
246+
...BASE_OPTIONS,
247+
polyfills: ['zone.js'],
248+
});
249+
250+
// Create WASM file
251+
await harness.writeFile('src/multiply.wasm', importWasmBytes);
252+
253+
// Create main file that uses the WASM file
254+
await harness.writeFile(
255+
'src/main.ts',
256+
`
257+
// @ts-ignore
258+
import { multiply } from './multiply.wasm';
259+
260+
console.log(multiply(4, 5));
261+
`,
262+
);
263+
264+
const { result, logs } = await harness.executeOnce({ outputLogsOnFailure: false });
265+
expect(result?.success).toBeFalse();
266+
expect(logs).toContain(
267+
jasmine.objectContaining({
268+
message: jasmine.stringMatching(
269+
'WASM/ES module integration imports are not supported with Zone.js applications',
270+
),
271+
}),
272+
);
273+
});
274+
});
275+
});

packages/angular/build/src/tools/esbuild/application-code-bundle.ts

+5-1
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import { createRxjsEsmResolutionPlugin } from './rxjs-esm-resolution-plugin';
2323
import { createSourcemapIgnorelistPlugin } from './sourcemap-ignorelist-plugin';
2424
import { getFeatureSupport, isZonelessApp } from './utils';
2525
import { createVirtualModulePlugin } from './virtual-module-plugin';
26+
import { createWasmPlugin } from './wasm-plugin';
2627

2728
export function createBrowserCodeBundleOptions(
2829
options: NormalizedApplicationBuildOptions,
@@ -37,6 +38,8 @@ export function createBrowserCodeBundleOptions(
3738
sourceFileCache,
3839
);
3940

41+
const zoneless = isZonelessApp(polyfills);
42+
4043
const buildOptions: BuildOptions = {
4144
...getEsBuildCommonOptions(options),
4245
platform: 'browser',
@@ -48,8 +51,9 @@ export function createBrowserCodeBundleOptions(
4851
entryNames: outputNames.bundles,
4952
entryPoints,
5053
target,
51-
supported: getFeatureSupport(target, isZonelessApp(polyfills)),
54+
supported: getFeatureSupport(target, zoneless),
5255
plugins: [
56+
createWasmPlugin({ allowAsync: zoneless }),
5357
createSourcemapIgnorelistPlugin(),
5458
createCompilerPlugin(
5559
// JS/TS options

0 commit comments

Comments
 (0)