Skip to content

Commit 628d87a

Browse files
committedJul 3, 2024·
feat(@angular/build): support WASM/ES Module integration proposal
Application builds will now support the direct import of WASM files. The behavior follows the WebAssembly/ES module 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 For more information regarding zoneless applications, you can visit https://angular.dev/guide/experimental/zoneless (cherry picked from commit 2cb1fb3)
1 parent 3091956 commit 628d87a

File tree

6 files changed

+662
-2
lines changed

6 files changed

+662
-2
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

+9-2
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, cache: sourceFileCache?.loadResultCache }),
5357
createSourcemapIgnorelistPlugin(),
5458
createCompilerPlugin(
5559
// JS/TS options
@@ -186,6 +190,8 @@ export function createServerCodeBundleOptions(
186190
entryPoints['server'] = ssrEntryPoint;
187191
}
188192

193+
const zoneless = isZonelessApp(polyfills);
194+
189195
const buildOptions: BuildOptions = {
190196
...getEsBuildCommonOptions(options),
191197
platform: 'node',
@@ -202,8 +208,9 @@ export function createServerCodeBundleOptions(
202208
js: `import './polyfills.server.mjs';`,
203209
},
204210
entryPoints,
205-
supported: getFeatureSupport(target, isZonelessApp(polyfills)),
211+
supported: getFeatureSupport(target, zoneless),
206212
plugins: [
213+
createWasmPlugin({ allowAsync: zoneless, cache: sourceFileCache?.loadResultCache }),
207214
createSourcemapIgnorelistPlugin(),
208215
createCompilerPlugin(
209216
// JS/TS options
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,255 @@
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 type { Plugin, ResolveOptions } from 'esbuild';
10+
import assert from 'node:assert';
11+
import { createHash } from 'node:crypto';
12+
import { readFile } from 'node:fs/promises';
13+
import { basename, dirname, join } from 'node:path';
14+
import { assertIsError } from '../../utils/error';
15+
import { LoadResultCache, createCachedLoad } from './load-result-cache';
16+
17+
/**
18+
* Options for the Angular WASM esbuild plugin
19+
* @see createWasmPlugin
20+
*/
21+
export interface WasmPluginOptions {
22+
/** Allow generation of async (proposal compliant) WASM imports. This requires zoneless to enable async/await. */
23+
allowAsync?: boolean;
24+
/** Load results cache. */
25+
cache?: LoadResultCache;
26+
}
27+
28+
const WASM_INIT_NAMESPACE = 'angular:wasm:init';
29+
const WASM_CONTENTS_NAMESPACE = 'angular:wasm:contents';
30+
const WASM_RESOLVE_SYMBOL = Symbol('WASM_RESOLVE_SYMBOL');
31+
32+
// See: https://github.com/tc39/proposal-regexp-unicode-property-escapes/blob/fe6d07fad74cd0192d154966baa1e95e7cda78a1/README.md#other-examples
33+
const ecmaIdentifierNameRegExp = /^(?:[$_\p{ID_Start}])(?:[$_\u200C\u200D\p{ID_Continue}])*$/u;
34+
35+
/**
36+
* Creates an esbuild plugin to use WASM files with import statements and expressions.
37+
* The default behavior follows the WebAssembly/ES mode integration proposal found at
38+
* https://github.com/WebAssembly/esm-integration/tree/main/proposals/esm-integration.
39+
* This behavior requires top-level await support which is only available in zoneless
40+
* Angular applications.
41+
* @returns An esbuild plugin.
42+
*/
43+
export function createWasmPlugin(options: WasmPluginOptions): Plugin {
44+
const { allowAsync = false, cache } = options;
45+
46+
return {
47+
name: 'angular-wasm',
48+
setup(build): void {
49+
build.onResolve({ filter: /.wasm$/ }, async (args) => {
50+
// Skip if already resolving the WASM file to avoid infinite resolution
51+
if (args.pluginData?.[WASM_RESOLVE_SYMBOL]) {
52+
return;
53+
}
54+
// Skip if not an import statement or expression
55+
if (args.kind !== 'import-statement' && args.kind !== 'dynamic-import') {
56+
return;
57+
}
58+
59+
// When in the initialization namespace, the content has already been resolved
60+
// and only needs to be loaded for use with the initialization code.
61+
if (args.namespace === WASM_INIT_NAMESPACE) {
62+
return {
63+
namespace: WASM_CONTENTS_NAMESPACE,
64+
path: join(args.resolveDir, args.path),
65+
pluginData: args.pluginData,
66+
};
67+
}
68+
69+
// Skip if a custom loader is defined
70+
if (build.initialOptions.loader?.['.wasm'] || args.with['loader']) {
71+
return;
72+
}
73+
74+
// Attempt full resolution of the WASM file
75+
const resolveOptions: ResolveOptions & { path?: string } = {
76+
...args,
77+
pluginData: { [WASM_RESOLVE_SYMBOL]: true },
78+
};
79+
// The "path" property will cause an error if used in the resolve call
80+
delete resolveOptions.path;
81+
82+
const result = await build.resolve(args.path, resolveOptions);
83+
84+
// Skip if there are errors, is external, or another plugin resolves to a custom namespace
85+
if (result.errors.length > 0 || result.external || result.namespace !== 'file') {
86+
// Reuse already resolved result
87+
return result;
88+
}
89+
90+
return {
91+
...result,
92+
namespace: WASM_INIT_NAMESPACE,
93+
};
94+
});
95+
96+
build.onLoad(
97+
{ filter: /.wasm$/, namespace: WASM_INIT_NAMESPACE },
98+
createCachedLoad(cache, async (args) => {
99+
// Ensure async mode is supported
100+
if (!allowAsync) {
101+
return {
102+
errors: [
103+
{
104+
text: 'WASM/ES module integration imports are not supported with Zone.js applications',
105+
notes: [
106+
{
107+
text: 'Information about zoneless Angular applications can be found here: https://angular.dev/guide/experimental/zoneless',
108+
},
109+
],
110+
},
111+
],
112+
};
113+
}
114+
115+
const wasmContents = await readFile(args.path);
116+
// Inline WASM code less than 10kB
117+
const inlineWasm = wasmContents.byteLength < 10_000;
118+
119+
// Add import of WASM contents
120+
let initContents = `import ${inlineWasm ? 'wasmData' : 'wasmPath'} from ${JSON.stringify(basename(args.path))}`;
121+
initContents += inlineWasm ? ' with { loader: "binary" };' : ';\n\n';
122+
123+
// Read from the file system when on Node.js (SSR) and not inline
124+
if (!inlineWasm && build.initialOptions.platform === 'node') {
125+
initContents += 'import { readFile } from "node:fs/promises";\n';
126+
initContents += 'const wasmData = await readFile(wasmPath);\n';
127+
}
128+
129+
// Create initialization function
130+
initContents += generateInitHelper(
131+
!inlineWasm && build.initialOptions.platform !== 'node',
132+
wasmContents,
133+
);
134+
135+
// Analyze WASM for imports and exports
136+
let importModuleNames, exportNames;
137+
try {
138+
const wasm = await WebAssembly.compile(wasmContents);
139+
importModuleNames = new Set(
140+
WebAssembly.Module.imports(wasm).map((value) => value.module),
141+
);
142+
exportNames = WebAssembly.Module.exports(wasm).map((value) => value.name);
143+
} catch (error) {
144+
assertIsError(error);
145+
146+
return {
147+
errors: [{ text: 'Unable to analyze WASM file', notes: [{ text: error.message }] }],
148+
};
149+
}
150+
151+
// Ensure export names are valid JavaScript identifiers
152+
const invalidExportNames = exportNames.filter(
153+
(name) => !ecmaIdentifierNameRegExp.test(name),
154+
);
155+
if (invalidExportNames.length > 0) {
156+
return {
157+
errors: invalidExportNames.map((name) => ({
158+
text: 'WASM export names must be valid JavaScript identifiers',
159+
notes: [
160+
{
161+
text: `The export "${name}" is not valid. The WASM file should be updated to remove this error.`,
162+
},
163+
],
164+
})),
165+
};
166+
}
167+
168+
// Add import statements and setup import object
169+
initContents += 'const importObject = Object.create(null);\n';
170+
let importIndex = 0;
171+
for (const moduleName of importModuleNames) {
172+
// Add a namespace import for each module name
173+
initContents += `import * as wasm_import_${++importIndex} from ${JSON.stringify(moduleName)};\n`;
174+
// Add the namespace object to the import object
175+
initContents += `importObject[${JSON.stringify(moduleName)}] = wasm_import_${importIndex};\n`;
176+
}
177+
178+
// Instantiate the module
179+
initContents += 'const instance = await init(importObject);\n';
180+
181+
// Add exports
182+
const exportNameList = exportNames.join(', ');
183+
initContents += `const { ${exportNameList} } = instance.exports;\n`;
184+
initContents += `export { ${exportNameList} }\n`;
185+
186+
return {
187+
contents: initContents,
188+
loader: 'js',
189+
resolveDir: dirname(args.path),
190+
pluginData: { wasmContents },
191+
watchFiles: [args.path],
192+
};
193+
}),
194+
);
195+
196+
build.onLoad({ filter: /.wasm$/, namespace: WASM_CONTENTS_NAMESPACE }, async (args) => {
197+
const contents = args.pluginData.wasmContents ?? (await readFile(args.path));
198+
199+
let loader: 'binary' | 'file' = 'file';
200+
if (args.with.loader) {
201+
assert(
202+
args.with.loader === 'binary' || args.with.loader === 'file',
203+
'WASM loader type should only be binary or file.',
204+
);
205+
loader = args.with.loader;
206+
}
207+
208+
return {
209+
contents,
210+
loader,
211+
watchFiles: [args.path],
212+
};
213+
});
214+
},
215+
};
216+
}
217+
218+
/**
219+
* Generates the string content of the WASM initialization helper function.
220+
* This function supports both file fetching and inline byte data depending on
221+
* the preferred option for the WASM file. When fetching, an integrity hash is
222+
* also generated and used with the fetch action.
223+
*
224+
* @param streaming Uses fetch and WebAssembly.instantiateStreaming.
225+
* @param wasmContents The binary contents to generate an integrity hash.
226+
* @returns A string containing the initialization function.
227+
*/
228+
function generateInitHelper(streaming: boolean, wasmContents: Uint8Array) {
229+
let resultContents;
230+
if (streaming) {
231+
const fetchOptions = {
232+
integrity: 'sha256-' + createHash('sha-256').update(wasmContents).digest('base64'),
233+
};
234+
const fetchContents = `fetch(new URL(wasmPath, import.meta.url), ${JSON.stringify(fetchOptions)})`;
235+
resultContents = `await WebAssembly.instantiateStreaming(${fetchContents}, imports)`;
236+
} else {
237+
resultContents = 'await WebAssembly.instantiate(wasmData, imports)';
238+
}
239+
240+
const contents = `
241+
let mod;
242+
async function init(imports) {
243+
if (mod) {
244+
return await WebAssembly.instantiate(mod, imports);
245+
}
246+
247+
const result = ${resultContents};
248+
mod = result.module;
249+
250+
return result.instance;
251+
}
252+
`;
253+
254+
return contents;
255+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
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+
/** @fileoverview
10+
* TypeScript does not provide a separate lib for WASM types and the Node.js
11+
* types (`@types/node`) does not contain them either. This type definition
12+
* file provides type information for the subset of functionality required
13+
* by the Angular build process. Ideally this can be removed when the WASM
14+
* type situation has improved.
15+
*/
16+
17+
declare namespace WebAssembly {
18+
class Module {
19+
constructor(data: Uint8Array);
20+
21+
static imports(mod: Module): { module: string; name: string }[];
22+
static exports(mode: Module): { name: string }[];
23+
}
24+
function compile(data: Uint8Array): Promise<Module>;
25+
}

‎tests/legacy-cli/e2e.bzl

+1
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ WEBPACK_IGNORE_TESTS = [
4545
"tests/commands/serve/ssr-http-requests-assets.js",
4646
"tests/build/prerender/http-requests-assets.js",
4747
"tests/build/prerender/error-with-sourcemaps.js",
48+
"tests/build/wasm-esm.js",
4849
]
4950

5051
def _to_glob(patterns):
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
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+
import { writeFile } from 'node:fs/promises';
9+
import { ng } from '../../utils/process';
10+
import { prependToFile, replaceInFile } from '../../utils/fs';
11+
import { updateJsonFile } from '../../utils/project';
12+
13+
/**
14+
* Compiled and base64 encoded WASM file for the following WAT:
15+
* ```
16+
* (module
17+
* (import "./values" "getValue" (func $getvalue (result i32)))
18+
* (export "multiply" (func $multiply))
19+
* (export "subtract1" (func $subtract))
20+
* (func $multiply (param i32 i32) (result i32)
21+
* local.get 0
22+
* local.get 1
23+
* i32.mul
24+
* )
25+
* (func $subtract (param i32) (result i32)
26+
* call $getvalue
27+
* local.get 0
28+
* i32.sub
29+
* )
30+
* )
31+
* ```
32+
*/
33+
const importWasmBase64 =
34+
'AGFzbQEAAAABEANgAAF/YAJ/fwF/YAF/AX8CFQEILi92YWx1ZXMIZ2V0VmFsdWUAAAMDAgECBxgCCG11bHRpcGx5AAEJc3VidHJhY3QxAAIKEQIHACAAIAFsCwcAEAAgAGsLAC8EbmFtZQEfAwAIZ2V0dmFsdWUBCG11bHRpcGx5AghzdWJ0cmFjdAIHAwAAAQACAA==';
35+
const importWasmBytes = Buffer.from(importWasmBase64, 'base64');
36+
37+
export default async function () {
38+
// Add WASM file to project
39+
await writeFile('src/app/multiply.wasm', importWasmBytes);
40+
await writeFile(
41+
'src/app/multiply.wasm.d.ts',
42+
'export declare function multiply(a: number, b: number): number; export declare function subtract1(a: number): number;',
43+
);
44+
45+
// Add requested WASM import file
46+
await writeFile('src/app/values.js', 'export function getValue() { return 100; }');
47+
48+
// Use WASM file in project
49+
await prependToFile(
50+
'src/app/app.component.ts',
51+
`
52+
import { multiply, subtract1 } from './multiply.wasm';
53+
`,
54+
);
55+
await replaceInFile(
56+
'src/app/app.component.ts',
57+
"'test-project'",
58+
'multiply(4, 5) + subtract1(88)',
59+
);
60+
61+
// Remove Zone.js from polyfills and make zoneless
62+
await updateJsonFile('angular.json', (json) => {
63+
// Remove bundle budgets to avoid a build error due to the expected increased output size
64+
// of a JIT production build.
65+
json.projects['test-project'].architect.build.options.polyfills = [];
66+
});
67+
await replaceInFile(
68+
'src/app/app.config.ts',
69+
'provideZoneChangeDetection',
70+
'provideExperimentalZonelessChangeDetection',
71+
);
72+
await replaceInFile(
73+
'src/app/app.config.ts',
74+
'provideZoneChangeDetection({ eventCoalescing: true })',
75+
'provideExperimentalZonelessChangeDetection()',
76+
);
77+
78+
await ng('build');
79+
80+
// Update E2E test to check for WASM execution
81+
await writeFile(
82+
'e2e/src/app.e2e-spec.ts',
83+
`
84+
import { AppPage } from './app.po';
85+
import { browser, logging } from 'protractor';
86+
describe('WASM execution', () => {
87+
it('should log WASM result messages', async () => {
88+
const page = new AppPage();
89+
await page.navigateTo();
90+
expect(await page.getTitleText()).toEqual('Hello, 32');
91+
});
92+
});
93+
`,
94+
);
95+
96+
await ng('e2e');
97+
}

0 commit comments

Comments
 (0)
Please sign in to comment.