-
Notifications
You must be signed in to change notification settings - Fork 12k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
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)
- Loading branch information
Showing
6 changed files
with
662 additions
and
2 deletions.
There are no files selected for viewing
275 changes: 275 additions & 0 deletions
275
packages/angular/build/src/builders/application/tests/behavior/wasm-esm_spec.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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', | ||
), | ||
}), | ||
); | ||
}); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.