Awesome
<div align="center"> <img width="100" height="100" src="media/assemblyscript-logo.svg" alt="AssemblyScript logo"> <img width="100" height="100" src="media/webpack-logo.svg" alt="webpack logo"> <h1>as-loader</h1> <p>AssemblyScript loader for webpack</p> </div>Installation
This loader requires AssemblyScript ~0.18, Node.js >= 12 and webpack 5
# with npm
npm install as-loader
npm install --save-dev assemblyscript
# with yarn
yarn add as-loader
yarn add --dev assemblyscript
The minimal webpack.config.js
:
module.exports = {
entry: "src/index.ts",
resolve: {
extensions: [".ts", ".js"],
},
module: {
rules: [
{
test: /\.ts$/,
include: path.resolve(__dirname, "src/assembly"),
loader: "as-loader",
options: {
// optional loader and compiler options
}
},
{
test: /\.ts$/,
exclude: path.resolve(__dirname, "src/assembly"),
loader: "ts-loader",
},
],
},
};
Usage
By default, the loader emits a .wasm
file (+ .wasm.map
if source maps are enabled) and
creates CommonJS module that exports URL to the emitted .wasm
file.
If you enable fallback
option, the loader will emit additional .js
file (+ .js.map
if source maps are enabled)
and will expose async fallback()
function which dynamically imports fallback module.
To simplify loading logic, you can use as-loader/runtime
loader which uses
@assemblyscript/loader, or
as-loader/runtime/bind
loader which uses as-bind.
These loaders provide correct types, checks for WebAssembly support, and uses fallback if available.
import * as myModule from "./assembly/myModule";
import { instantiate } from "as-loader/runtime";
async function loadAndRun() {
const { exports } = await instantiate(myModule);
exports.myFunction(100);
}
loadAndRun();
<details>
<summary>Alternatively, you can use exported URL directly:</summary>
import * as myModule from "./assembly/myModule";
import { instantiate } from "@assemblyscript/loader";
async function loadAndRun() {
const { exports } = await instantiate<typeof myModule>(
// workaround for TypeScript
fetch((myModule as unknown) as string)
);
exports.myFunction(100);
}
loadAndRun();
</details>
API
For more details, check src/runtime directory
as-loader/runtime
This runtime loader uses @assemblyscript/loader under the hood.
export interface WasmModuleInstance<TModule> {
type: "wasm";
exports: AsLoaderRuntime & PointerCastObject<TModule>;
module: WebAssembly.Module;
instance: WebAssembly.Instance;
}
export interface JsModuleInstance<TModule> {
type: "js";
exports: TModule;
}
export type ModuleInstance<TModule> =
| WasmModuleInstance<TModule>
| JsModuleInstance<TModule>;
export function instantiate<TModule>(
module: TModule,
load: (url: string) => Promise<unknown>,
imports?: object,
fallback: boolean = false,
supports?: () => boolean
): Promise<ModuleInstance<TModule>>
<details>
<summary><code>as-loader/runtime</code> binding code example:</summary>
// ./src/assembly/sayHello.ts
export function sayHello(firstName: string, lastName: string): string {
return `Hello ${firstName} ${lastName}!`;
}
// ./src/sayHello.ts
import * as sayHelloModule from "./assembly/sayHello";
import { instantiate } from "as-loader/runtime";
export async function loadModule(): Promise<typeof sayHelloModule> {
const { exports } = await instantiate(sayHelloModule, fetch);
const { __pin, __unpin, __newString, __getString } = exports;
function sayHello(firstName: string, lastName: string): string {
const firstNamePtr = __pin(__newString(firstName));
const lastNamePtr = __pin(__newString(lastName));
const result = __getString(
exports.sayHello(firstNamePtr, lastNamePtr)
);
__unpin(firstNamePtr);
__unpin(lastNamePtr);
return result;
}
return { sayHello };
}
</details>
as-loader/runtime/bind
This runtime loader uses as-bind under the hood.
Requires bind
option enabled in the webpack loader configuration.
Keep in mind that currently it's recommended to manually set
Function.returnType
export interface BoundWasmModuleInstance<TModule, TImports> {
type: "wasm-bound";
exports: AsLoaderRuntime & BoundExports<TModule>;
unboundExports: AsLoaderRuntime & PointerCastObject<TModule>;
importObject: TImports;
module: WebAssembly.Module;
instance: WebAssembly.Instance;
}
export interface JsModuleInstance<TModule> {
type: "js";
exports: TModule;
}
type BoundModuleInstance<TModule, TImports> =
| BoundWasmModuleInstance<TModule, TImports>
| JsModuleInstance<TModule>;
export function instantiate<TModule, TImports>(
module: TModule,
load: (url: string) => Promise<unknown>,
imports?: TImports,
fallback: boolean = false,
supports?: () => boolean
): Promise<BoundModuleInstance<TModule, TImports>>
<details>
<summary><code>as-loader/runtime/bind</code> binding code example:</summary>
// ./src/assembly/sayHello.ts
export function sayHello(firstName: string, lastName: string): string {
return `Hello ${firstName} ${lastName}!`;
}
// ./src/sayHello.ts
import * as sayHelloModule from "./assembly/sayHello";
import { instantiate } from "as-loader/runtime/bind";
export async function loadModule(): Promise<typeof sayHelloModule> {
const module = await instantiate(sayHelloModule, fetch);
return { sayHello: module.exports.sayHello };
}
</details>
Binding
There are 2 aspects that you have to consider when interacting with a WebAssembly module:
-
WebAssembly doesn't support function arguments and returns others than
number | boolean | bigint
yet. Because of that, you have to manually translate between WebAssembly pointers and JavaScript objects.The alternative is to enable the
bind
option and useas-loader/runtime/bind
loader which uses an as-bind library. This simplifies passing types like strings and arrays. -
WebAssembly doesn't provide Garbage Collector yet (proposal) - to manage memory, AssemblyScript offers very lightweight GC implementation. If you use it (see
runtime
option), you have to manually__pin
and__unpin
pointers to instruct GC if given data can be collected or not.
Fallback
If you need to support older browsers like Internet Explorer or Edge < 16,
you can use the fallback
option. A fallback module is different from WebAssembly one because you don't have to bind it.
// webpack.config.js
module.exports = {
entry: "src/index.ts",
resolve: {
extensions: [".ts", ".js"],
},
module: {
rules: [
{
test: /\.ts$/,
include: path.resolve(__dirname, "src/assembly"),
use: [
// fallback loader (must be before as-loader)
{
loader: "ts-loader",
options: {
transpileOnly: true
}
},
// as-loader, apart from building .wasm file,
// will forward assembly script files to the fallback loader above
// to build a .js file
{
loader: "as-loader",
options: {
fallback: true
}
}
]
},
{
test: /\.ts$/,
exclude: path.resolve(__dirname, "src/assembly"),
loader: "ts-loader",
},
],
},
};
// ./src/assembly/sayHello.ts
export function sayHello(firstName: string, lastName: string): string {
return `Hello ${firstName} ${lastName}!`;
}
// ./src/sayHello.ts
import * as sayHelloModule from "./assembly/sayHello";
import { instantiate } from "as-loader/runtime";
export async function loadModule(): Promise<typeof sayHelloModule> {
// set fallback option to true (opt-in)
const module = await instantiate(sayHelloModule, fetch, undefined, true);
if (module.type === 'wasm') {
const { __pin, __unpin, __newString, __getString } = exports;
function sayHello(firstName: string, lastName: string): string {
const firstNamePtr = __pin(__newString(firstName));
const lastNamePtr = __pin(__newString(lastName));
const result = __getString(
exports.sayHello(firstNamePtr, lastNamePtr)
);
__unpin(firstNamePtr);
__unpin(lastNamePtr);
return result;
}
return { sayHello };
} else {
return { sayHello: module.exports.sayHello }
}
}
</details>
Options
Loader Options
Name | Type | Description |
---|---|---|
name | string | Output asset name template, [name].[contenthash].wasm by default. |
bind | boolean | If true, adds as-bind library files to the compilation (required if you want to use as-loader/runtime/bind ). |
fallback | boolean | If true, creates additional JavaScript file which can be used if WebAssembly is not supported. |
raw | boolean | If true, returns binary instead of emitting file. Use for chaining with other loaders. |
Compiler Options
Options passed to the AssemblyScript compiler.
Name | Type | Description |
---|---|---|
debug | boolean | Enables debug information in emitted binaries, enabled by default in webpack development mode. |
optimizeLevel | number | How much to focus on optimizing code, 3 by default. [0-3] |
shrinkLevel | number | How much to focus on shrinking code size, 1 by default. [0-2] |
coverage | boolean | Re-optimizes until no further improvements can be made. |
noAssert | boolean | Replaces assertions with just their value without trapping, enabled by default in webpack production mode. |
importMemory | boolean | Imports the memory provided as 'env.memory'. |
noExportMemory | boolean | Does not export the memory as 'memory'. |
initialMemory | number | Sets the initial memory size in pages. |
maximumMemory | number | Sets the maximum memory size in pages. |
sharedMemory | boolean | Declare memory as shared. Requires maximumMemory. |
importTable | boolean | Imports the function table provided as 'env.table'. |
exportTable | boolean | Exports the function table as 'table'. |
runtime | string | Specifies the runtime variant to include in the program. Available runtime are: "incremental" (default), "minimal", "stub" |
exportRuntime | boolean | Exports the runtime helpers (__new, __collect etc.). Enabled by default. |
explicitStart | boolean | Exports an explicit '_start' function to call. |
enable | string[] | Enables WebAssembly features being disabled by default. Available features are: "sign-extension", "bulk-memory", "simd", "threads", "reference-types", "gc" |
disable | string[] | Disables WebAssembly features being enabled by default. Available features are: "mutable-globals" |
lowMemoryLimit | boolean | Enforces very low (<64k) memory constraints. |
memoryBase | number | Sets the start offset of emitted memory segments. |
tableBase | number | Sets the start offset of emitted table elements. |
trapMode | string | Sets the trap mode to use. Available modes are: "allow", "clamp", "js" |
noValidate | boolean | Skips validating the module using Binaryen. |
License
MIT