From 628d87a9474ad2792b69bfbc501a2c5960b27db9 Mon Sep 17 00:00:00 2001 From: Charles Lyding <19598772+clydin@users.noreply.github.com> Date: Mon, 1 Jul 2024 14:31:47 -0400 Subject: [PATCH] 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 2cb1fb350b93e0908e5b011cae6000aae524dcb3) --- .../tests/behavior/wasm-esm_spec.ts | 275 ++++++++++++++++++ .../tools/esbuild/application-code-bundle.ts | 11 +- .../build/src/tools/esbuild/wasm-plugin.ts | 255 ++++++++++++++++ .../angular/build/src/tools/esbuild/wasm.d.ts | 25 ++ tests/legacy-cli/e2e.bzl | 1 + tests/legacy-cli/e2e/tests/build/wasm-esm.ts | 97 ++++++ 6 files changed, 662 insertions(+), 2 deletions(-) create mode 100644 packages/angular/build/src/builders/application/tests/behavior/wasm-esm_spec.ts create mode 100644 packages/angular/build/src/tools/esbuild/wasm-plugin.ts create mode 100644 packages/angular/build/src/tools/esbuild/wasm.d.ts create mode 100644 tests/legacy-cli/e2e/tests/build/wasm-esm.ts diff --git a/packages/angular/build/src/builders/application/tests/behavior/wasm-esm_spec.ts b/packages/angular/build/src/builders/application/tests/behavior/wasm-esm_spec.ts new file mode 100644 index 000000000000..5ae62f020c1c --- /dev/null +++ b/packages/angular/build/src/builders/application/tests/behavior/wasm-esm_spec.ts @@ -0,0 +1,275 @@ +/** + * @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'; + +/** + * Compiled and base64 encoded WASM file for the following WAT: + * ``` + * (module + * (export "multiply" (func $multiply)) + * (func $multiply (param i32 i32) (result i32) + * local.get 0 + * local.get 1 + * i32.mul + * ) + * ) + * ``` + */ +const exportWasmBase64 = + 'AGFzbQEAAAABBwFgAn9/AX8DAgEABwwBCG11bHRpcGx5AAAKCQEHACAAIAFsCwAXBG5hbWUBCwEACG11bHRpcGx5AgMBAAA='; +const exportWasmBytes = Buffer.from(exportWasmBase64, 'base64'); + +/** + * Compiled and base64 encoded WASM file for the following WAT: + * ``` + * (module + * (import "./values" "getValue" (func $getvalue (result i32))) + * (export "multiply" (func $multiply)) + * (export "subtract1" (func $subtract)) + * (func $multiply (param i32 i32) (result i32) + * local.get 0 + * local.get 1 + * i32.mul + * ) + * (func $subtract (param i32) (result i32) + * call $getvalue + * local.get 0 + * i32.sub + * ) + * ) + * ``` + */ +const importWasmBase64 = + 'AGFzbQEAAAABEANgAAF/YAJ/fwF/YAF/AX8CFQEILi92YWx1ZXMIZ2V0VmFsdWUAAAMDAgECBxgCCG11bHRpcGx5AAEJc3VidHJhY3QxAAIKEQIHACAAIAFsCwcAEAAgAGsLAC8EbmFtZQEfAwAIZ2V0dmFsdWUBCG11bHRpcGx5AghzdWJ0cmFjdAIHAwAAAQACAA=='; +const importWasmBytes = Buffer.from(importWasmBase64, 'base64'); + +describeBuilder(buildApplication, APPLICATION_BUILDER_INFO, (harness) => { + describe('Behavior: "Supports WASM/ES module integration"', () => { + it('should inject initialization code and add an export', async () => { + harness.useTarget('build', { + ...BASE_OPTIONS, + }); + + // Create WASM file + await harness.writeFile('src/multiply.wasm', exportWasmBytes); + + // Create main file that uses the WASM file + await harness.writeFile( + 'src/main.ts', + ` + // @ts-ignore + import { multiply } from './multiply.wasm'; + + console.log(multiply(4, 5)); + `, + ); + + const { result } = await harness.executeOnce(); + expect(result?.success).toBeTrue(); + + // Ensure initialization code and export name is present in output code + harness.expectFile('dist/browser/main.js').content.toContain('WebAssembly.instantiate'); + harness.expectFile('dist/browser/main.js').content.toContain('multiply'); + }); + + it('should compile successfully with a provided type definition file', async () => { + harness.useTarget('build', { + ...BASE_OPTIONS, + }); + + // Create WASM file + await harness.writeFile('src/multiply.wasm', exportWasmBytes); + await harness.writeFile( + 'src/multiply.wasm.d.ts', + 'export declare function multiply(a: number, b: number): number;', + ); + + // Create main file that uses the WASM file + await harness.writeFile( + 'src/main.ts', + ` + import { multiply } from './multiply.wasm'; + + console.log(multiply(4, 5)); + `, + ); + + const { result } = await harness.executeOnce(); + expect(result?.success).toBeTrue(); + + // Ensure initialization code and export name is present in output code + harness.expectFile('dist/browser/main.js').content.toContain('WebAssembly.instantiate'); + harness.expectFile('dist/browser/main.js').content.toContain('multiply'); + }); + + it('should add WASM defined imports and include resolved TS file for import', async () => { + harness.useTarget('build', { + ...BASE_OPTIONS, + }); + + // Create WASM file + await harness.writeFile('src/subtract.wasm', importWasmBytes); + + // Create TS file that is expect by WASM file + await harness.writeFile( + 'src/values.ts', + ` + export function getValue(): number { return 100; } + `, + ); + // The file is not imported into any actual TS files so it needs to be manually added to the TypeScript program + await harness.modifyFile('src/tsconfig.app.json', (content) => + content.replace('"main.ts",', '"main.ts","values.ts",'), + ); + + // Create main file that uses the WASM file + await harness.writeFile( + 'src/main.ts', + ` + // @ts-ignore + import { subtract1 } from './subtract.wasm'; + + console.log(subtract1(5)); + `, + ); + + const { result } = await harness.executeOnce(); + expect(result?.success).toBeTrue(); + + // Ensure initialization code and export name is present in output code + harness.expectFile('dist/browser/main.js').content.toContain('WebAssembly.instantiate'); + harness.expectFile('dist/browser/main.js').content.toContain('subtract1'); + harness.expectFile('dist/browser/main.js').content.toContain('./values'); + harness.expectFile('dist/browser/main.js').content.toContain('getValue'); + }); + + it('should add WASM defined imports and include resolved JS file for import', async () => { + harness.useTarget('build', { + ...BASE_OPTIONS, + }); + + // Create WASM file + await harness.writeFile('src/subtract.wasm', importWasmBytes); + + // Create JS file that is expect by WASM file + await harness.writeFile( + 'src/values.js', + ` + export function getValue() { return 100; } + `, + ); + + // Create main file that uses the WASM file + await harness.writeFile( + 'src/main.ts', + ` + // @ts-ignore + import { subtract1 } from './subtract.wasm'; + + console.log(subtract1(5)); + `, + ); + + const { result } = await harness.executeOnce(); + expect(result?.success).toBeTrue(); + + // Ensure initialization code and export name is present in output code + harness.expectFile('dist/browser/main.js').content.toContain('WebAssembly.instantiate'); + harness.expectFile('dist/browser/main.js').content.toContain('subtract1'); + harness.expectFile('dist/browser/main.js').content.toContain('./values'); + harness.expectFile('dist/browser/main.js').content.toContain('getValue'); + }); + + it('should inline WASM files less than 10kb', async () => { + harness.useTarget('build', { + ...BASE_OPTIONS, + }); + + // Create WASM file + await harness.writeFile('src/multiply.wasm', exportWasmBytes); + + // Create main file that uses the WASM file + await harness.writeFile( + 'src/main.ts', + ` + // @ts-ignore + import { multiply } from './multiply.wasm'; + + console.log(multiply(4, 5)); + `, + ); + + const { result } = await harness.executeOnce(); + expect(result?.success).toBeTrue(); + + // Ensure WASM is present in output code + harness.expectFile('dist/browser/main.js').content.toContain(exportWasmBase64); + }); + + it('should show an error on invalid WASM file', async () => { + harness.useTarget('build', { + ...BASE_OPTIONS, + }); + + // Create WASM file + await harness.writeFile('src/multiply.wasm', 'NOT_WASM'); + + // Create main file that uses the WASM file + await harness.writeFile( + 'src/main.ts', + ` + // @ts-ignore + import { multiply } from './multiply.wasm'; + + console.log(multiply(4, 5)); + `, + ); + + const { result, logs } = await harness.executeOnce({ outputLogsOnFailure: false }); + expect(result?.success).toBeFalse(); + expect(logs).toContain( + jasmine.objectContaining({ + message: jasmine.stringMatching('Unable to analyze WASM file'), + }), + ); + }); + + it('should show an error if using Zone.js', async () => { + harness.useTarget('build', { + ...BASE_OPTIONS, + polyfills: ['zone.js'], + }); + + // Create WASM file + await harness.writeFile('src/multiply.wasm', importWasmBytes); + + // Create main file that uses the WASM file + await harness.writeFile( + 'src/main.ts', + ` + // @ts-ignore + import { multiply } from './multiply.wasm'; + + console.log(multiply(4, 5)); + `, + ); + + const { result, logs } = await harness.executeOnce({ outputLogsOnFailure: false }); + expect(result?.success).toBeFalse(); + expect(logs).toContain( + jasmine.objectContaining({ + message: jasmine.stringMatching( + 'WASM/ES module integration imports are not supported with Zone.js applications', + ), + }), + ); + }); + }); +}); diff --git a/packages/angular/build/src/tools/esbuild/application-code-bundle.ts b/packages/angular/build/src/tools/esbuild/application-code-bundle.ts index 27cc5cccbaf0..366ba18da019 100644 --- a/packages/angular/build/src/tools/esbuild/application-code-bundle.ts +++ b/packages/angular/build/src/tools/esbuild/application-code-bundle.ts @@ -23,6 +23,7 @@ import { createRxjsEsmResolutionPlugin } from './rxjs-esm-resolution-plugin'; import { createSourcemapIgnorelistPlugin } from './sourcemap-ignorelist-plugin'; import { getFeatureSupport, isZonelessApp } from './utils'; import { createVirtualModulePlugin } from './virtual-module-plugin'; +import { createWasmPlugin } from './wasm-plugin'; export function createBrowserCodeBundleOptions( options: NormalizedApplicationBuildOptions, @@ -37,6 +38,8 @@ export function createBrowserCodeBundleOptions( sourceFileCache, ); + const zoneless = isZonelessApp(polyfills); + const buildOptions: BuildOptions = { ...getEsBuildCommonOptions(options), platform: 'browser', @@ -48,8 +51,9 @@ export function createBrowserCodeBundleOptions( entryNames: outputNames.bundles, entryPoints, target, - supported: getFeatureSupport(target, isZonelessApp(polyfills)), + supported: getFeatureSupport(target, zoneless), plugins: [ + createWasmPlugin({ allowAsync: zoneless, cache: sourceFileCache?.loadResultCache }), createSourcemapIgnorelistPlugin(), createCompilerPlugin( // JS/TS options @@ -186,6 +190,8 @@ export function createServerCodeBundleOptions( entryPoints['server'] = ssrEntryPoint; } + const zoneless = isZonelessApp(polyfills); + const buildOptions: BuildOptions = { ...getEsBuildCommonOptions(options), platform: 'node', @@ -202,8 +208,9 @@ export function createServerCodeBundleOptions( js: `import './polyfills.server.mjs';`, }, entryPoints, - supported: getFeatureSupport(target, isZonelessApp(polyfills)), + supported: getFeatureSupport(target, zoneless), plugins: [ + createWasmPlugin({ allowAsync: zoneless, cache: sourceFileCache?.loadResultCache }), createSourcemapIgnorelistPlugin(), createCompilerPlugin( // JS/TS options diff --git a/packages/angular/build/src/tools/esbuild/wasm-plugin.ts b/packages/angular/build/src/tools/esbuild/wasm-plugin.ts new file mode 100644 index 000000000000..8ad6fc262522 --- /dev/null +++ b/packages/angular/build/src/tools/esbuild/wasm-plugin.ts @@ -0,0 +1,255 @@ +/** + * @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 type { Plugin, ResolveOptions } from 'esbuild'; +import assert from 'node:assert'; +import { createHash } from 'node:crypto'; +import { readFile } from 'node:fs/promises'; +import { basename, dirname, join } from 'node:path'; +import { assertIsError } from '../../utils/error'; +import { LoadResultCache, createCachedLoad } from './load-result-cache'; + +/** + * Options for the Angular WASM esbuild plugin + * @see createWasmPlugin + */ +export interface WasmPluginOptions { + /** Allow generation of async (proposal compliant) WASM imports. This requires zoneless to enable async/await. */ + allowAsync?: boolean; + /** Load results cache. */ + cache?: LoadResultCache; +} + +const WASM_INIT_NAMESPACE = 'angular:wasm:init'; +const WASM_CONTENTS_NAMESPACE = 'angular:wasm:contents'; +const WASM_RESOLVE_SYMBOL = Symbol('WASM_RESOLVE_SYMBOL'); + +// See: https://github.com/tc39/proposal-regexp-unicode-property-escapes/blob/fe6d07fad74cd0192d154966baa1e95e7cda78a1/README.md#other-examples +const ecmaIdentifierNameRegExp = /^(?:[$_\p{ID_Start}])(?:[$_\u200C\u200D\p{ID_Continue}])*$/u; + +/** + * Creates an esbuild plugin to use WASM files with import statements and expressions. + * The default behavior follows the WebAssembly/ES mode integration proposal found at + * https://github.com/WebAssembly/esm-integration/tree/main/proposals/esm-integration. + * This behavior requires top-level await support which is only available in zoneless + * Angular applications. + * @returns An esbuild plugin. + */ +export function createWasmPlugin(options: WasmPluginOptions): Plugin { + const { allowAsync = false, cache } = options; + + return { + name: 'angular-wasm', + setup(build): void { + build.onResolve({ filter: /.wasm$/ }, async (args) => { + // Skip if already resolving the WASM file to avoid infinite resolution + if (args.pluginData?.[WASM_RESOLVE_SYMBOL]) { + return; + } + // Skip if not an import statement or expression + if (args.kind !== 'import-statement' && args.kind !== 'dynamic-import') { + return; + } + + // When in the initialization namespace, the content has already been resolved + // and only needs to be loaded for use with the initialization code. + if (args.namespace === WASM_INIT_NAMESPACE) { + return { + namespace: WASM_CONTENTS_NAMESPACE, + path: join(args.resolveDir, args.path), + pluginData: args.pluginData, + }; + } + + // Skip if a custom loader is defined + if (build.initialOptions.loader?.['.wasm'] || args.with['loader']) { + return; + } + + // Attempt full resolution of the WASM file + const resolveOptions: ResolveOptions & { path?: string } = { + ...args, + pluginData: { [WASM_RESOLVE_SYMBOL]: true }, + }; + // The "path" property will cause an error if used in the resolve call + delete resolveOptions.path; + + const result = await build.resolve(args.path, resolveOptions); + + // Skip if there are errors, is external, or another plugin resolves to a custom namespace + if (result.errors.length > 0 || result.external || result.namespace !== 'file') { + // Reuse already resolved result + return result; + } + + return { + ...result, + namespace: WASM_INIT_NAMESPACE, + }; + }); + + build.onLoad( + { filter: /.wasm$/, namespace: WASM_INIT_NAMESPACE }, + createCachedLoad(cache, async (args) => { + // Ensure async mode is supported + if (!allowAsync) { + return { + errors: [ + { + text: 'WASM/ES module integration imports are not supported with Zone.js applications', + notes: [ + { + text: 'Information about zoneless Angular applications can be found here: https://angular.dev/guide/experimental/zoneless', + }, + ], + }, + ], + }; + } + + const wasmContents = await readFile(args.path); + // Inline WASM code less than 10kB + const inlineWasm = wasmContents.byteLength < 10_000; + + // Add import of WASM contents + let initContents = `import ${inlineWasm ? 'wasmData' : 'wasmPath'} from ${JSON.stringify(basename(args.path))}`; + initContents += inlineWasm ? ' with { loader: "binary" };' : ';\n\n'; + + // Read from the file system when on Node.js (SSR) and not inline + if (!inlineWasm && build.initialOptions.platform === 'node') { + initContents += 'import { readFile } from "node:fs/promises";\n'; + initContents += 'const wasmData = await readFile(wasmPath);\n'; + } + + // Create initialization function + initContents += generateInitHelper( + !inlineWasm && build.initialOptions.platform !== 'node', + wasmContents, + ); + + // Analyze WASM for imports and exports + let importModuleNames, exportNames; + try { + const wasm = await WebAssembly.compile(wasmContents); + importModuleNames = new Set( + WebAssembly.Module.imports(wasm).map((value) => value.module), + ); + exportNames = WebAssembly.Module.exports(wasm).map((value) => value.name); + } catch (error) { + assertIsError(error); + + return { + errors: [{ text: 'Unable to analyze WASM file', notes: [{ text: error.message }] }], + }; + } + + // Ensure export names are valid JavaScript identifiers + const invalidExportNames = exportNames.filter( + (name) => !ecmaIdentifierNameRegExp.test(name), + ); + if (invalidExportNames.length > 0) { + return { + errors: invalidExportNames.map((name) => ({ + text: 'WASM export names must be valid JavaScript identifiers', + notes: [ + { + text: `The export "${name}" is not valid. The WASM file should be updated to remove this error.`, + }, + ], + })), + }; + } + + // Add import statements and setup import object + initContents += 'const importObject = Object.create(null);\n'; + let importIndex = 0; + for (const moduleName of importModuleNames) { + // Add a namespace import for each module name + initContents += `import * as wasm_import_${++importIndex} from ${JSON.stringify(moduleName)};\n`; + // Add the namespace object to the import object + initContents += `importObject[${JSON.stringify(moduleName)}] = wasm_import_${importIndex};\n`; + } + + // Instantiate the module + initContents += 'const instance = await init(importObject);\n'; + + // Add exports + const exportNameList = exportNames.join(', '); + initContents += `const { ${exportNameList} } = instance.exports;\n`; + initContents += `export { ${exportNameList} }\n`; + + return { + contents: initContents, + loader: 'js', + resolveDir: dirname(args.path), + pluginData: { wasmContents }, + watchFiles: [args.path], + }; + }), + ); + + build.onLoad({ filter: /.wasm$/, namespace: WASM_CONTENTS_NAMESPACE }, async (args) => { + const contents = args.pluginData.wasmContents ?? (await readFile(args.path)); + + let loader: 'binary' | 'file' = 'file'; + if (args.with.loader) { + assert( + args.with.loader === 'binary' || args.with.loader === 'file', + 'WASM loader type should only be binary or file.', + ); + loader = args.with.loader; + } + + return { + contents, + loader, + watchFiles: [args.path], + }; + }); + }, + }; +} + +/** + * Generates the string content of the WASM initialization helper function. + * This function supports both file fetching and inline byte data depending on + * the preferred option for the WASM file. When fetching, an integrity hash is + * also generated and used with the fetch action. + * + * @param streaming Uses fetch and WebAssembly.instantiateStreaming. + * @param wasmContents The binary contents to generate an integrity hash. + * @returns A string containing the initialization function. + */ +function generateInitHelper(streaming: boolean, wasmContents: Uint8Array) { + let resultContents; + if (streaming) { + const fetchOptions = { + integrity: 'sha256-' + createHash('sha-256').update(wasmContents).digest('base64'), + }; + const fetchContents = `fetch(new URL(wasmPath, import.meta.url), ${JSON.stringify(fetchOptions)})`; + resultContents = `await WebAssembly.instantiateStreaming(${fetchContents}, imports)`; + } else { + resultContents = 'await WebAssembly.instantiate(wasmData, imports)'; + } + + const contents = ` +let mod; +async function init(imports) { + if (mod) { + return await WebAssembly.instantiate(mod, imports); + } + + const result = ${resultContents}; + mod = result.module; + + return result.instance; +} +`; + + return contents; +} diff --git a/packages/angular/build/src/tools/esbuild/wasm.d.ts b/packages/angular/build/src/tools/esbuild/wasm.d.ts new file mode 100644 index 000000000000..e3b45032eccc --- /dev/null +++ b/packages/angular/build/src/tools/esbuild/wasm.d.ts @@ -0,0 +1,25 @@ +/** + * @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 + */ + +/** @fileoverview + * TypeScript does not provide a separate lib for WASM types and the Node.js + * types (`@types/node`) does not contain them either. This type definition + * file provides type information for the subset of functionality required + * by the Angular build process. Ideally this can be removed when the WASM + * type situation has improved. + */ + +declare namespace WebAssembly { + class Module { + constructor(data: Uint8Array); + + static imports(mod: Module): { module: string; name: string }[]; + static exports(mode: Module): { name: string }[]; + } + function compile(data: Uint8Array): Promise; +} diff --git a/tests/legacy-cli/e2e.bzl b/tests/legacy-cli/e2e.bzl index 97f3e149beed..139433f97858 100644 --- a/tests/legacy-cli/e2e.bzl +++ b/tests/legacy-cli/e2e.bzl @@ -45,6 +45,7 @@ WEBPACK_IGNORE_TESTS = [ "tests/commands/serve/ssr-http-requests-assets.js", "tests/build/prerender/http-requests-assets.js", "tests/build/prerender/error-with-sourcemaps.js", + "tests/build/wasm-esm.js", ] def _to_glob(patterns): diff --git a/tests/legacy-cli/e2e/tests/build/wasm-esm.ts b/tests/legacy-cli/e2e/tests/build/wasm-esm.ts new file mode 100644 index 000000000000..f605930e80f2 --- /dev/null +++ b/tests/legacy-cli/e2e/tests/build/wasm-esm.ts @@ -0,0 +1,97 @@ +/** + * @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 { writeFile } from 'node:fs/promises'; +import { ng } from '../../utils/process'; +import { prependToFile, replaceInFile } from '../../utils/fs'; +import { updateJsonFile } from '../../utils/project'; + +/** + * Compiled and base64 encoded WASM file for the following WAT: + * ``` + * (module + * (import "./values" "getValue" (func $getvalue (result i32))) + * (export "multiply" (func $multiply)) + * (export "subtract1" (func $subtract)) + * (func $multiply (param i32 i32) (result i32) + * local.get 0 + * local.get 1 + * i32.mul + * ) + * (func $subtract (param i32) (result i32) + * call $getvalue + * local.get 0 + * i32.sub + * ) + * ) + * ``` + */ +const importWasmBase64 = + 'AGFzbQEAAAABEANgAAF/YAJ/fwF/YAF/AX8CFQEILi92YWx1ZXMIZ2V0VmFsdWUAAAMDAgECBxgCCG11bHRpcGx5AAEJc3VidHJhY3QxAAIKEQIHACAAIAFsCwcAEAAgAGsLAC8EbmFtZQEfAwAIZ2V0dmFsdWUBCG11bHRpcGx5AghzdWJ0cmFjdAIHAwAAAQACAA=='; +const importWasmBytes = Buffer.from(importWasmBase64, 'base64'); + +export default async function () { + // Add WASM file to project + await writeFile('src/app/multiply.wasm', importWasmBytes); + await writeFile( + 'src/app/multiply.wasm.d.ts', + 'export declare function multiply(a: number, b: number): number; export declare function subtract1(a: number): number;', + ); + + // Add requested WASM import file + await writeFile('src/app/values.js', 'export function getValue() { return 100; }'); + + // Use WASM file in project + await prependToFile( + 'src/app/app.component.ts', + ` + import { multiply, subtract1 } from './multiply.wasm'; + `, + ); + await replaceInFile( + 'src/app/app.component.ts', + "'test-project'", + 'multiply(4, 5) + subtract1(88)', + ); + + // Remove Zone.js from polyfills and make zoneless + await updateJsonFile('angular.json', (json) => { + // Remove bundle budgets to avoid a build error due to the expected increased output size + // of a JIT production build. + json.projects['test-project'].architect.build.options.polyfills = []; + }); + await replaceInFile( + 'src/app/app.config.ts', + 'provideZoneChangeDetection', + 'provideExperimentalZonelessChangeDetection', + ); + await replaceInFile( + 'src/app/app.config.ts', + 'provideZoneChangeDetection({ eventCoalescing: true })', + 'provideExperimentalZonelessChangeDetection()', + ); + + await ng('build'); + + // Update E2E test to check for WASM execution + await writeFile( + 'e2e/src/app.e2e-spec.ts', + ` + import { AppPage } from './app.po'; + import { browser, logging } from 'protractor'; + describe('WASM execution', () => { + it('should log WASM result messages', async () => { + const page = new AppPage(); + await page.navigateTo(); + expect(await page.getTitleText()).toEqual('Hello, 32'); + }); + }); + `, + ); + + await ng('e2e'); +}