Skip to content

Commit

Permalink
feat(@angular/build): support WASM/ES Module integration proposal
Browse files Browse the repository at this point in the history
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
  • Loading branch information
clydin committed Jul 3, 2024
1 parent d449c9d commit 2cb1fb3
Show file tree
Hide file tree
Showing 6 changed files with 662 additions and 2 deletions.
Original file line number Diff line number Diff line change
@@ -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',
),
}),
);
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -37,6 +38,8 @@ export function createBrowserCodeBundleOptions(
sourceFileCache,
);

const zoneless = isZonelessApp(polyfills);

const buildOptions: BuildOptions = {
...getEsBuildCommonOptions(options),
platform: 'browser',
Expand All @@ -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
Expand Down Expand Up @@ -186,6 +190,8 @@ export function createServerCodeBundleOptions(
entryPoints['server'] = ssrEntryPoint;
}

const zoneless = isZonelessApp(polyfills);

const buildOptions: BuildOptions = {
...getEsBuildCommonOptions(options),
platform: 'node',
Expand All @@ -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
Expand Down
Loading

0 comments on commit 2cb1fb3

Please sign in to comment.