├── test ├── e2e │ ├── .gitignore │ ├── fixtures │ │ └── main │ │ │ ├── src │ │ │ ├── assembly │ │ │ │ ├── broken │ │ │ │ │ ├── shared.ts │ │ │ │ │ ├── simple.ts │ │ │ │ │ └── tsconfig.json │ │ │ │ └── correct │ │ │ │ │ ├── shared.ts │ │ │ │ │ ├── bind.ts │ │ │ │ │ ├── simple.ts │ │ │ │ │ ├── tsconfig.json │ │ │ │ │ └── complex.ts │ │ │ ├── async.ts │ │ │ ├── tsconfig.json │ │ │ ├── correct.ts │ │ │ ├── fallback.ts │ │ │ ├── broken.ts │ │ │ ├── bind.ts │ │ │ └── complex.ts │ │ │ ├── package.json │ │ │ └── webpack.config.js │ └── main.spec.ts ├── tsconfig.json └── unit │ ├── options.spec.ts │ └── line-column.spec.ts ├── .gitignore ├── .husky └── pre-commit ├── src ├── loader │ ├── webpack.ts │ ├── options.ts │ ├── line-column.ts │ ├── error.ts │ ├── compiler-host.ts │ ├── schema.json │ └── index.ts └── runtime │ ├── types │ ├── std.ts │ ├── bound.ts │ ├── index.ts │ ├── pointer.ts │ └── runtime.ts │ ├── index.ts │ └── bind.ts ├── tsconfig.json ├── media ├── webpack-logo.svg └── assemblyscript-logo.svg ├── LICENSE ├── .eslintrc.js ├── package.json ├── .github └── workflows │ └── main.yml └── README.md /test/e2e/.gitignore: -------------------------------------------------------------------------------- 1 | __locks__ 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | /loader 3 | /runtime 4 | .idea 5 | .env 6 | .DS_Store 7 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | yarn build 5 | yarn lint 6 | yarn test unit 7 | -------------------------------------------------------------------------------- /test/e2e/fixtures/main/src/assembly/broken/shared.ts: -------------------------------------------------------------------------------- 1 | export function add(a: string, b: i32): i32 { 2 | return a + b; 3 | } 4 | -------------------------------------------------------------------------------- /test/e2e/fixtures/main/src/assembly/correct/shared.ts: -------------------------------------------------------------------------------- 1 | export function add(a: i32, b: i32): i32 { 2 | return a + b; 3 | } 4 | -------------------------------------------------------------------------------- /test/e2e/fixtures/main/src/assembly/correct/bind.ts: -------------------------------------------------------------------------------- 1 | export function hello(name: string): string { 2 | return 'Hello ' + name + '!'; 3 | } 4 | -------------------------------------------------------------------------------- /test/e2e/fixtures/main/src/assembly/broken/simple.ts: -------------------------------------------------------------------------------- 1 | import { add } from "./shared"; 2 | 3 | export function run(): i32 { 4 | return add(5, 10); 5 | } 6 | -------------------------------------------------------------------------------- /test/e2e/fixtures/main/src/assembly/correct/simple.ts: -------------------------------------------------------------------------------- 1 | import { add } from "./shared"; 2 | 3 | export function run(): i32 { 4 | return add(5, 10); 5 | } 6 | -------------------------------------------------------------------------------- /test/e2e/fixtures/main/src/async.ts: -------------------------------------------------------------------------------- 1 | async function loadAndRun() { 2 | const assembly = await import("./assembly/correct/simple"); 3 | 4 | console.log(assembly.run()); 5 | } 6 | 7 | loadAndRun(); 8 | -------------------------------------------------------------------------------- /test/e2e/fixtures/main/src/assembly/broken/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "include": [ 4 | "./simple.ts", 5 | "../../../node_modules/assemblyscript/std/assembly/index.d.ts" 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /test/e2e/fixtures/main/src/assembly/correct/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "include": [ 4 | "./simple.ts", 5 | "../../../node_modules/assemblyscript/std/assembly/index.d.ts" 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /test/e2e/fixtures/main/src/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "module": "esnext", 5 | "moduleResolution": "node", 6 | "lib": ["DOM", "ES6"], 7 | "strict": true 8 | }, 9 | "include": [ 10 | "./async.ts", 11 | "./correct.ts", 12 | "./broken.ts" 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /test/e2e/fixtures/main/src/correct.ts: -------------------------------------------------------------------------------- 1 | import * as fs from 'fs'; 2 | import { instantiate } from "as-loader/runtime"; 3 | 4 | import * as assembly from "./assembly/correct/simple"; 5 | 6 | async function loadAndRun() { 7 | const module = await instantiate( 8 | assembly, 9 | fs.promises.readFile 10 | ); 11 | 12 | console.log(module.exports.run()); 13 | } 14 | 15 | loadAndRun(); 16 | -------------------------------------------------------------------------------- /test/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "module": "commonjs", 5 | "rootDir": "./", 6 | "strict": true, 7 | "moduleResolution": "node", 8 | "resolveJsonModule": true, 9 | "esModuleInterop": true, 10 | "skipLibCheck": true, 11 | "forceConsistentCasingInFileNames": true 12 | }, 13 | "include": [ 14 | "." 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /test/e2e/fixtures/main/src/fallback.ts: -------------------------------------------------------------------------------- 1 | import * as assembly from "./assembly/correct/complex"; 2 | 3 | async function loadAndRun() { 4 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 5 | const module = await (assembly as any).fallback() as typeof assembly; 6 | 7 | const colors = module.getPalette(10); 8 | console.log(colors.map(color => color.toString()).join(',')) 9 | } 10 | 11 | loadAndRun(); 12 | -------------------------------------------------------------------------------- /test/e2e/fixtures/main/src/broken.ts: -------------------------------------------------------------------------------- 1 | import * as fs from "fs"; 2 | import { instantiate } from "@assemblyscript/loader"; 3 | 4 | import * as assembly from "./assembly/broken/simple"; 5 | 6 | async function loadAndRun() { 7 | const module = await instantiate( 8 | fs.promises.readFile((assembly as unknown) as string) 9 | ); 10 | 11 | console.log(module.exports.run()); 12 | } 13 | 14 | loadAndRun(); 15 | -------------------------------------------------------------------------------- /test/e2e/fixtures/main/src/bind.ts: -------------------------------------------------------------------------------- 1 | import * as fs from 'fs'; 2 | import { instantiate } from "as-loader/runtime/bind"; 3 | 4 | import * as assembly from "./assembly/correct/bind"; 5 | 6 | async function loadAndRun() { 7 | const module = await instantiate( 8 | assembly, 9 | fs.promises.readFile 10 | ); 11 | 12 | const { hello } = module.exports; 13 | 14 | console.log(hello('world')); 15 | } 16 | 17 | loadAndRun(); 18 | -------------------------------------------------------------------------------- /src/loader/webpack.ts: -------------------------------------------------------------------------------- 1 | import * as webpack from "webpack"; 2 | 3 | function markModuleAsCompiledToWasm(module: webpack.Module) { 4 | module.buildMeta.asLoaderCompiledToWasm = true; 5 | } 6 | 7 | function isModuleCompiledToWasm(module: webpack.Module): boolean { 8 | return Boolean( 9 | module.buildMeta.asLoaderCompiledToWasm || 10 | (module.issuer && isModuleCompiledToWasm(module.issuer)) 11 | ); 12 | } 13 | 14 | export { markModuleAsCompiledToWasm, isModuleCompiledToWasm }; 15 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "module": "commonjs", 5 | "rootDir": "./src", 6 | "outDir": ".", 7 | "strict": true, 8 | "declaration": true, 9 | "moduleResolution": "node", 10 | "resolveJsonModule": true, 11 | "esModuleInterop": false, 12 | "removeComments": true, 13 | "skipLibCheck": true, 14 | "forceConsistentCasingInFileNames": true, 15 | "importsNotUsedAsValues": "remove" 16 | }, 17 | "include": [ 18 | "src" 19 | ], 20 | "exclude": [ 21 | "node_modules" 22 | ] 23 | } 24 | -------------------------------------------------------------------------------- /test/e2e/fixtures/main/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "as-loader-main-fixture", 3 | "version": "0.0.0", 4 | "description": "Main fixture for as-loader E2E tests", 5 | "main": "dist/index.js", 6 | "repository": "git@github.com:piotr-oles/as-loader.git", 7 | "author": "Piotr Oleś ", 8 | "license": "MIT", 9 | "files": [ 10 | "dist/*" 11 | ], 12 | "dependencies": { 13 | "assemblyscript": "0.19.10", 14 | "ts-loader": "8.0.17", 15 | "typescript": "4.2.2", 16 | "webpack": "5.24.2", 17 | "webpack-cli": "4.5.0" 18 | }, 19 | "engines": { 20 | "node": ">=12" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /media/webpack-logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/runtime/types/std.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-unused-vars */ 2 | 3 | // Types 4 | declare type bool = boolean; 5 | declare type i8 = number; 6 | declare type i16 = number; 7 | declare type i32 = number; 8 | declare type isize = number; 9 | declare type u8 = number; 10 | declare type u16 = number; 11 | declare type u32 = number; 12 | declare type usize = number; 13 | declare type f32 = number; 14 | declare type f64 = number; 15 | 16 | /** Special type evaluating the indexed access index type. */ 17 | declare type indexof = keyof T; 18 | /** Special type evaluating the indexed access value type. */ 19 | declare type valueof = T[0]; 20 | -------------------------------------------------------------------------------- /test/e2e/fixtures/main/src/assembly/correct/complex.ts: -------------------------------------------------------------------------------- 1 | 2 | export class Color { 3 | constructor( 4 | readonly r: u8, 5 | readonly g: u8, 6 | readonly b: u8 7 | ) {} 8 | 9 | toString(): string { 10 | return 'rgb(' + this.r.toString() + ', ' + this.g.toString() + ', ' + this.b.toString() + ')'; 11 | } 12 | } 13 | 14 | export function getPalette(size: u8): Color[] { 15 | const colors: Color[] = []; 16 | let r: u8 = 100; 17 | let g: u8 = 50; 18 | let b: u8 = 20; 19 | 20 | for (let i: u8 = 0; i < size; ++i) { 21 | colors.push(new Color(r, g, b)) 22 | 23 | r = (r + 5) % 255; 24 | g = (g + 1) % 255; 25 | b = (b - 1) % 255; 26 | } 27 | 28 | return colors; 29 | } 30 | -------------------------------------------------------------------------------- /src/loader/options.ts: -------------------------------------------------------------------------------- 1 | type Options = Record; 2 | 3 | function mapAscOptionsToArgs(options: Options): string[] { 4 | const args = []; 5 | const keys = Object.keys(options); 6 | 7 | for (const key of keys) { 8 | const value = options[key]; 9 | 10 | if (typeof value === "boolean") { 11 | if (value) { 12 | // add flag only if value is true 13 | args.push("--" + key); 14 | } 15 | } else if (typeof value === "string" || typeof value === "number") { 16 | args.push("--" + key, String(value)); 17 | } else if (Array.isArray(value)) { 18 | args.push("--" + key, value.join(",")); 19 | } 20 | } 21 | 22 | return args; 23 | } 24 | 25 | export { mapAscOptionsToArgs, Options }; 26 | -------------------------------------------------------------------------------- /test/e2e/fixtures/main/src/complex.ts: -------------------------------------------------------------------------------- 1 | import * as fs from 'fs'; 2 | import { instantiate } from "as-loader/runtime"; 3 | 4 | import * as assembly from "./assembly/correct/complex"; 5 | 6 | async function loadAndRun() { 7 | const module = await instantiate( 8 | assembly, 9 | fs.promises.readFile 10 | ); 11 | 12 | const { 13 | __getArray, 14 | __getString, 15 | __pin, 16 | __unpin, 17 | getPalette, 18 | Color 19 | } = module.exports; 20 | 21 | const colorsPtr = __pin(getPalette(10)); 22 | const colorsPtrs = __getArray(colorsPtr); 23 | const colors = colorsPtrs.map(__pin).map(Color.wrap); 24 | 25 | console.log(colors.map(color => __getString(color.toString())).join(',')); 26 | 27 | __unpin(colorsPtr); 28 | } 29 | 30 | loadAndRun(); 31 | -------------------------------------------------------------------------------- /src/loader/line-column.ts: -------------------------------------------------------------------------------- 1 | interface LineColumn { 2 | // 1-based line 3 | line: number; 4 | // 1-based column 5 | column: number; 6 | } 7 | 8 | function getLineColumnFromIndex( 9 | source: string, 10 | index: number 11 | ): LineColumn | undefined { 12 | if (index < 0 || index >= source.length || isNaN(index)) { 13 | return undefined; 14 | } 15 | 16 | let line = 1; 17 | let prevLineIndex = -1; 18 | let nextLineIndex = source.indexOf("\n"); 19 | 20 | while (nextLineIndex !== -1 && index > nextLineIndex) { 21 | prevLineIndex = nextLineIndex; 22 | nextLineIndex = source.indexOf("\n", prevLineIndex + 1); 23 | ++line; 24 | } 25 | const column = index - prevLineIndex; 26 | 27 | return { 28 | line, 29 | column, 30 | }; 31 | } 32 | 33 | export { getLineColumnFromIndex, LineColumn }; 34 | -------------------------------------------------------------------------------- /test/e2e/fixtures/main/webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | 3 | module.exports = { 4 | mode: "development", 5 | devtool: "source-map", 6 | context: __dirname, 7 | entry: "./src/correct.ts", 8 | target: "node", 9 | output: { 10 | path: path.resolve(__dirname, "./dist"), 11 | publicPath: "./dist/" 12 | }, 13 | resolve: { 14 | extensions: [".ts", ".js"], 15 | }, 16 | module: { 17 | rules: [ 18 | { 19 | test: /\.ts$/, 20 | include: [path.resolve(__dirname, "src/assembly")], 21 | loader: "as-loader", 22 | options: { 23 | name: "[name].wasm", 24 | }, 25 | }, 26 | { 27 | test: /\.ts$/, 28 | exclude: [path.resolve(__dirname, "src/assembly")], 29 | loader: "ts-loader", 30 | options: { 31 | transpileOnly: true, 32 | }, 33 | }, 34 | ], 35 | }, 36 | }; 37 | -------------------------------------------------------------------------------- /test/unit/options.spec.ts: -------------------------------------------------------------------------------- 1 | import { mapAscOptionsToArgs } from "../../loader/options"; 2 | 3 | describe("options", () => { 4 | it.each([ 5 | [{}, []], 6 | [{ optimizeLevel: 3 }, ["--optimizeLevel", "3"]], 7 | [{ coverage: true }, ["--coverage"]], 8 | [{ coverage: false }, []], 9 | [{ runtime: "half" }, ["--runtime", "half"]], 10 | [{ enable: ["bulk-memory", "simd"] }, ["--enable", "bulk-memory,simd"]], 11 | [ 12 | { 13 | optimizeLevel: 2, 14 | shrinkLevel: 1, 15 | noValidate: true, 16 | sharedMemory: false, 17 | disable: ["mutable-globals"], 18 | }, 19 | [ 20 | "--optimizeLevel", 21 | "2", 22 | "--shrinkLevel", 23 | "1", 24 | "--noValidate", 25 | "--disable", 26 | "mutable-globals", 27 | ], 28 | ], 29 | ])("maps options %p to args %p", (options, args) => { 30 | expect(mapAscOptionsToArgs(options)).toEqual(args); 31 | }); 32 | }); 33 | -------------------------------------------------------------------------------- /test/unit/line-column.spec.ts: -------------------------------------------------------------------------------- 1 | import { getLineColumnFromIndex } from "../../loader/line-column"; 2 | 3 | const AS_SOURCE = [ 4 | "export function add(a: i32, b: i32): i32 {", 5 | " return a + b;", 6 | "}", 7 | "", 8 | ].join("\n"); 9 | 10 | describe("line-column", () => { 11 | it.each([ 12 | [0, AS_SOURCE, 1, 1], 13 | [10, AS_SOURCE, 1, 11], 14 | [42, AS_SOURCE, 1, 43], 15 | [43, AS_SOURCE, 2, 1], 16 | [45, AS_SOURCE, 2, 3], 17 | [58, AS_SOURCE, 2, 16], 18 | [59, AS_SOURCE, 3, 1], 19 | [60, AS_SOURCE, 3, 2], 20 | ])( 21 | "get line and column for index %p in %p", 22 | (index, source, line, column) => { 23 | expect(getLineColumnFromIndex(source, index)).toEqual({ line, column }); 24 | } 25 | ); 26 | 27 | it.each([ 28 | [-1, AS_SOURCE], 29 | [10000, AS_SOURCE], 30 | [NaN, AS_SOURCE], 31 | ])("returns undefined for invalid index %p in %p", (index, source) => { 32 | expect(getLineColumnFromIndex(source, index)).toBeUndefined(); 33 | }); 34 | }); 35 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2021 Piotr Oleś 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated 4 | documentation files (the "Software"), to deal in the Software without restriction, including without limitation 5 | the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, 6 | and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 7 | 8 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 9 | 10 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE 11 | WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS 12 | OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT 13 | OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 14 | -------------------------------------------------------------------------------- /src/runtime/types/bound.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /* eslint-disable @typescript-eslint/no-explicit-any */ 3 | 4 | import type { 5 | NonPointerTypes, 6 | PointerCastFunction, 7 | PointerCast, 8 | } from "./pointer"; 9 | 10 | export type BoundNonPointerTypes = 11 | | NonPointerTypes 12 | | string 13 | | number[] 14 | | bigint[] 15 | | string[] 16 | | boolean[] 17 | | number[][] 18 | | bigint[][] 19 | | string[][] 20 | | boolean[][] 21 | | Int8Array 22 | | Uint8Array 23 | | Int16Array 24 | | Uint16Array 25 | | Int32Array 26 | | Uint32Array 27 | | Float32Array 28 | | Float64Array 29 | | BigInt64Array 30 | | BigUint64Array; 31 | 32 | export type BoundFunction any> = 33 | PointerCastFunction; 34 | 35 | export type BoundExports> = T extends Record< 36 | string | symbol | number, 37 | any 38 | > 39 | ? { 40 | [K in keyof T]: T[K] extends (...args: any) => any 41 | ? BoundFunction 42 | : PointerCast; 43 | } 44 | : never; 45 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parser: "@typescript-eslint/parser", 3 | extends: ["plugin:node/recommended", "prettier"], 4 | parserOptions: { 5 | ecmaVersion: 2018, 6 | sourceType: "module", 7 | }, 8 | settings: { 9 | node: { 10 | tryExtensions: [".js", ".json", ".ts", ".d.ts"], 11 | }, 12 | }, 13 | overrides: [ 14 | { 15 | files: ["*.ts"], 16 | extends: ["plugin:@typescript-eslint/recommended", "prettier"], 17 | rules: { 18 | "@typescript-eslint/explicit-function-return-type": "off", 19 | "@typescript-eslint/explicit-module-boundary-types": "off", 20 | "@typescript-eslint/no-use-before-define": "off", 21 | "node/no-unsupported-features/es-syntax": "off", 22 | }, 23 | }, 24 | { 25 | files: ["*.spec.ts"], 26 | rules: { 27 | "@typescript-eslint/no-var-requires": "off", 28 | "node/no-missing-import": "off", 29 | }, 30 | }, 31 | { 32 | files: ["test/e2e/fixtures/**/*.ts"], 33 | rules: { 34 | "node/no-missing-import": "off", 35 | "node/no-extraneous-import": "off" 36 | }, 37 | }, 38 | ], 39 | }; 40 | -------------------------------------------------------------------------------- /src/runtime/types/index.ts: -------------------------------------------------------------------------------- 1 | import type { AsLoaderRuntime } from "./runtime"; 2 | import type { PointerCastObject } from "./pointer"; 3 | import type { BoundExports } from "./bound"; 4 | 5 | export interface AsLoaderModule extends String { 6 | fallback?(): Promise; 7 | } 8 | 9 | export interface WasmModuleInstance { 10 | type: "wasm"; 11 | exports: AsLoaderRuntime & PointerCastObject; 12 | module: WebAssembly.Module; 13 | instance: WebAssembly.Instance; 14 | } 15 | export interface BoundWasmModuleInstance { 16 | type: "wasm-bound"; 17 | exports: AsLoaderRuntime & BoundExports; 18 | unboundExports: AsLoaderRuntime & PointerCastObject; 19 | importObject: TImports; 20 | module: WebAssembly.Module; 21 | instance: WebAssembly.Instance; 22 | } 23 | export interface JsModuleInstance { 24 | type: "js"; 25 | exports: TModule; 26 | } 27 | export type ModuleInstance = 28 | | WasmModuleInstance 29 | | JsModuleInstance; 30 | export type BoundModuleInstance = 31 | | BoundWasmModuleInstance 32 | | JsModuleInstance; 33 | -------------------------------------------------------------------------------- /media/assemblyscript-logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/runtime/types/pointer.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-explicit-any */ 2 | export type NonPointerTypes = number | boolean | bigint; 3 | 4 | export type Pointer = number & { __brand: "pointer"; __type: T }; 5 | export type NullablePointer = T extends undefined | null | void 6 | ? null 7 | : Pointer; 8 | 9 | export type PointerCast = T extends E 10 | ? T 11 | : T extends new (...args: any) => any 12 | ? PointerCastInstance 13 | : T extends (...args: any) => any 14 | ? PointerCastFunction 15 | : T extends any[] 16 | ? Pointer> 17 | : T extends Record 18 | ? Pointer> 19 | : NullablePointer; 20 | export type PointerCastArray = { 21 | [K in keyof T]: PointerCast; 22 | }; 23 | export type PointerCastFunction< 24 | T extends (...args: any) => any, 25 | E = NonPointerTypes 26 | > = T extends (...args: infer A) => infer R 27 | ? (...args: PointerCastArray) => PointerCast 28 | : never; 29 | export type PointerCastObject< 30 | T extends Record, 31 | E = NonPointerTypes 32 | > = T extends Record 33 | ? { 34 | [K in keyof T]: PointerCast; 35 | } 36 | : never; 37 | export type PointerCastInstance< 38 | T extends new (...args: any) => any, 39 | E = NonPointerTypes 40 | > = T extends new (...args: infer A) => infer R 41 | ? (new (...args: PointerCastArray) => PointerCastObject) & { 42 | wrap(ptr: Pointer>): PointerCastObject; 43 | } 44 | : never; 45 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "as-loader", 3 | "version": "0.12.0", 4 | "description": "AssemblyScript loader for webpack", 5 | "main": "loader/index.js", 6 | "repository": "git@github.com:piotr-oles/as-loader.git", 7 | "author": "Piotr Oleś ", 8 | "license": "MIT", 9 | "keywords": [ 10 | "assemblyscript", 11 | "webpack", 12 | "webassembly", 13 | "wasm", 14 | "loader" 15 | ], 16 | "scripts": { 17 | "build": "tsc", 18 | "lint": "eslint src test", 19 | "test": "jest", 20 | "release": "auto shipit", 21 | "prepare": "husky install" 22 | }, 23 | "files": [ 24 | "loader/*", 25 | "runtime/*" 26 | ], 27 | "peerDependencies": { 28 | "assemblyscript": "^0.19.0", 29 | "webpack": "^5.0.0" 30 | }, 31 | "dependencies": { 32 | "@assemblyscript/loader": "^0.19.0", 33 | "as-bind": "^0.8.0", 34 | "loader-utils": "^2.0.0", 35 | "schema-utils": "^3.1.1" 36 | }, 37 | "devDependencies": { 38 | "@types/jest": "^26.0.24", 39 | "@types/loader-utils": "^2.0.3", 40 | "@types/webpack": "^5.28.0", 41 | "@typescript-eslint/eslint-plugin": "^4.29.0", 42 | "@typescript-eslint/parser": "^4.29.0", 43 | "assemblyscript": "^0.19.0", 44 | "auto": "^10.30.0", 45 | "eslint": "^7.32.0", 46 | "eslint-config-prettier": "^8.3.0", 47 | "eslint-plugin-node": "^11.1.0", 48 | "eslint-plugin-prettier": "^3.4.0", 49 | "husky": "^7.0.0", 50 | "jest": "^27.0.6", 51 | "karton": "^0.4.1", 52 | "lint-staged": "^11.1.2", 53 | "prettier": "^2.3.2", 54 | "ts-jest": "^27.0.4", 55 | "typescript": "^4.3.5", 56 | "webpack": "^5.0.0" 57 | }, 58 | "auto": { 59 | "plugins": [ 60 | "npm", 61 | "released" 62 | ], 63 | "shipit": { 64 | "noChangelog": true 65 | } 66 | }, 67 | "lint-staged": { 68 | "*.ts": "eslint --fix" 69 | }, 70 | "jest": { 71 | "preset": "ts-jest", 72 | "testEnvironment": "node" 73 | }, 74 | "engines": { 75 | "node": ">=14" 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/loader/error.ts: -------------------------------------------------------------------------------- 1 | import * as path from "path"; 2 | import type { DiagnosticMessage } from "assemblyscript/cli/asc"; 3 | import { getLineColumnFromIndex, LineColumn } from "./line-column"; 4 | import type { CompilerHost } from "./compiler-host"; 5 | 6 | interface Location { 7 | start?: LineColumn; 8 | end?: LineColumn; 9 | } 10 | 11 | interface ArtificialModule { 12 | identifier(): string; 13 | readableIdentifier(): string; 14 | } 15 | 16 | class AssemblyScriptError extends Error { 17 | readonly loc: Location | undefined; 18 | readonly module: ArtificialModule | undefined; 19 | 20 | constructor(message: string, file?: string, location?: Location) { 21 | super(message); 22 | Object.setPrototypeOf(this, AssemblyScriptError.prototype); 23 | 24 | this.name = "AssemblyScriptError"; 25 | this.message = message; 26 | this.loc = location; 27 | 28 | // webpack quirks... 29 | this.module = { 30 | identifier() { 31 | return file || ""; 32 | }, 33 | readableIdentifier() { 34 | return file || ""; 35 | }, 36 | }; 37 | 38 | Error.captureStackTrace(this, this.constructor); 39 | } 40 | 41 | static fromDiagnostic( 42 | diagnostic: DiagnosticMessage, 43 | host: CompilerHost, 44 | baseDir: string, 45 | context: string 46 | ) { 47 | const fileName = 48 | diagnostic.range && 49 | diagnostic.range.source && 50 | diagnostic.range.source.normalizedPath; 51 | let location: Location | undefined; 52 | 53 | if (fileName) { 54 | const fileContent = host.readFile(fileName, baseDir); 55 | if (fileContent) { 56 | const start = diagnostic.range 57 | ? getLineColumnFromIndex(fileContent, diagnostic.range.start) 58 | : undefined; 59 | const end = diagnostic.range 60 | ? getLineColumnFromIndex(fileContent, diagnostic.range.end) 61 | : undefined; 62 | if (start || end) { 63 | location = { start, end }; 64 | } 65 | } 66 | } 67 | 68 | const baseUrl = path.relative(context, baseDir); 69 | const file = fileName 70 | ? `./${path.join(baseUrl, fileName).replace(/\\/g, "/")}` 71 | : undefined; 72 | 73 | return new AssemblyScriptError(diagnostic.message, file, location); 74 | } 75 | } 76 | 77 | export { AssemblyScriptError }; 78 | -------------------------------------------------------------------------------- /src/loader/compiler-host.ts: -------------------------------------------------------------------------------- 1 | import * as path from "path"; 2 | import * as asc from "assemblyscript/cli/asc"; 3 | import type { DiagnosticMessage, APIOptions } from "assemblyscript/cli/asc"; 4 | 5 | type CompilerHost = Required< 6 | Pick< 7 | APIOptions, 8 | | "stdout" 9 | | "stderr" 10 | | "readFile" 11 | | "writeFile" 12 | | "listFiles" 13 | | "reportDiagnostic" 14 | > 15 | > & { 16 | getDiagnostics(): DiagnosticMessage[]; 17 | }; 18 | 19 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 20 | function createCompilerHost(context: any): CompilerHost { 21 | const memVolume: Record = {}; 22 | const stderr = asc.createMemoryStream(); 23 | const stdout = asc.createMemoryStream(); 24 | const diagnostics: DiagnosticMessage[] = []; 25 | 26 | function readFile(fileName: string, baseDir: string) { 27 | const filePath = baseDir ? path.resolve(baseDir, fileName) : fileName; 28 | 29 | if (memVolume[filePath]) { 30 | return memVolume[filePath]; 31 | } 32 | 33 | try { 34 | const content = context.fs.readFileSync(filePath, "utf8"); 35 | context.addDependency(filePath); 36 | 37 | return typeof content === "string" ? content : content.toString("utf8"); 38 | } catch (error) { 39 | return null; 40 | } 41 | } 42 | 43 | function writeFile(fileName: string, contents: Uint8Array, baseDir: string) { 44 | const filePath = baseDir ? path.resolve(baseDir, fileName) : fileName; 45 | 46 | memVolume[filePath] = Buffer.isBuffer(contents) 47 | ? contents 48 | : Buffer.from(contents); 49 | 50 | return true; 51 | } 52 | 53 | function listFiles(dirName: string, baseDir: string) { 54 | const dirPath = baseDir ? path.resolve(baseDir, dirName) : dirName; 55 | 56 | try { 57 | return context.fs 58 | .readdirSync(dirPath) 59 | .filter( 60 | (file: string) => file.endsWith(".ts") && !file.endsWith(".d.ts") 61 | ); 62 | } catch (error) { 63 | return null; 64 | } 65 | } 66 | 67 | function reportDiagnostic(diagnostic: DiagnosticMessage) { 68 | diagnostics.push(diagnostic); 69 | } 70 | 71 | function getDiagnostics(): DiagnosticMessage[] { 72 | return diagnostics; 73 | } 74 | 75 | return { 76 | readFile, 77 | writeFile, 78 | listFiles, 79 | reportDiagnostic, 80 | getDiagnostics, 81 | stderr, 82 | stdout, 83 | }; 84 | } 85 | 86 | export { createCompilerHost, CompilerHost }; 87 | -------------------------------------------------------------------------------- /src/runtime/index.ts: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line @typescript-eslint/triple-slash-reference 2 | /// 3 | import { 4 | Imports, 5 | instantiate as asLoaderInstantiate, 6 | ResultObject, 7 | } from "@assemblyscript/loader"; 8 | import type { 9 | WasmModuleInstance, 10 | JsModuleInstance, 11 | ModuleInstance, 12 | AsLoaderModule, 13 | } from "./types"; 14 | import type { 15 | Pointer, 16 | NonPointerTypes, 17 | NullablePointer, 18 | PointerCast, 19 | PointerCastArray, 20 | PointerCastFunction, 21 | PointerCastInstance, 22 | PointerCastObject, 23 | } from "./types/pointer"; 24 | import type { AsLoaderRuntime } from "./types/runtime"; 25 | 26 | function instantiate( 27 | module: TModule | string, 28 | load: (url: string) => Promise, 29 | imports?: Imports, 30 | fallback?: false, 31 | supports?: () => boolean 32 | ): Promise>; 33 | function instantiate( 34 | module: TModule | string, 35 | load: (url: string) => Promise, 36 | imports: Imports | undefined, 37 | fallback: true, 38 | supports?: () => boolean 39 | ): Promise>; 40 | function instantiate( 41 | module: TModule | string, 42 | load: (url: string) => Promise, 43 | imports?: Imports, 44 | fallback?: boolean, 45 | supports = () => typeof WebAssembly === "object" 46 | ): Promise> { 47 | if (supports()) { 48 | // WebAssembly is supported 49 | return asLoaderInstantiate( 50 | load(module as string), 51 | imports || {} 52 | ).then( 53 | ( 54 | result: ResultObject & { 55 | exports: AsLoaderRuntime & PointerCastObject; 56 | } 57 | ) => ({ 58 | type: "wasm", 59 | exports: result.exports, 60 | instance: result.instance, 61 | module: result.module, 62 | }) 63 | ); 64 | } else if (fallback && (module as AsLoaderModule).fallback) { 65 | // eslint-disable-next-line 66 | return (module as AsLoaderModule).fallback!().then( 67 | (exports: TModule) => ({ 68 | type: "js", 69 | exports, 70 | }) 71 | ); 72 | } 73 | 74 | return Promise.reject( 75 | new Error( 76 | `Cannot load "${module}" module. WebAssembly is not supported in this environment.` 77 | ) 78 | ); 79 | } 80 | 81 | export { 82 | instantiate, 83 | // types 84 | Imports, 85 | WasmModuleInstance, 86 | JsModuleInstance, 87 | ModuleInstance, 88 | AsLoaderModule, 89 | // pointer types 90 | Pointer, 91 | NonPointerTypes, 92 | NullablePointer, 93 | PointerCast, 94 | PointerCastArray, 95 | PointerCastFunction, 96 | PointerCastInstance, 97 | PointerCastObject, 98 | // runtime types 99 | AsLoaderRuntime, 100 | }; 101 | -------------------------------------------------------------------------------- /src/runtime/bind.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/triple-slash-reference, @typescript-eslint/ban-ts-comment */ 2 | /// 3 | import type { Imports } from "@assemblyscript/loader"; 4 | import * as AsBind from "as-bind"; 5 | import type { 6 | JsModuleInstance, 7 | AsLoaderModule, 8 | BoundModuleInstance, 9 | BoundWasmModuleInstance, 10 | } from "./types"; 11 | import type { 12 | Pointer, 13 | NonPointerTypes, 14 | NullablePointer, 15 | PointerCast, 16 | PointerCastArray, 17 | PointerCastFunction, 18 | PointerCastInstance, 19 | PointerCastObject, 20 | } from "./types/pointer"; 21 | import type { AsLoaderRuntime } from "./types/runtime"; 22 | 23 | function instantiate< 24 | TModule, 25 | TImports extends Imports | undefined = Imports | undefined 26 | >( 27 | module: TModule | string, 28 | load: (url: string) => Promise, 29 | imports: TImports, 30 | fallback: false, 31 | supports?: () => boolean 32 | ): Promise>; 33 | function instantiate< 34 | TModule, 35 | TImports extends Imports | undefined = Imports | undefined 36 | >( 37 | module: TModule | string, 38 | load: (url: string) => Promise, 39 | imports?: TImports, 40 | fallback?: true, 41 | supports?: () => boolean 42 | ): Promise>; 43 | function instantiate< 44 | TModule, 45 | TImports extends Imports | undefined = Imports | undefined 46 | >( 47 | module: TModule | string, 48 | load: (url: string) => Promise, 49 | imports?: TImports, 50 | fallback = true, 51 | supports = () => typeof WebAssembly === "object" 52 | ): Promise> { 53 | if (supports()) { 54 | // WebAssembly is supported 55 | // @ts-ignore invalid as-build typings 56 | return AsBind.instantiate(load(module as string), imports || {}).then( 57 | (result: BoundWasmModuleInstance) => { 58 | result.type = "wasm-bound"; 59 | return result; 60 | } 61 | ); 62 | } else if (fallback && (module as AsLoaderModule).fallback) { 63 | // eslint-disable-next-line 64 | return (module as AsLoaderModule).fallback!().then( 65 | (exports: TModule) => ({ 66 | type: "js", 67 | exports, 68 | }) 69 | ); 70 | } 71 | 72 | return Promise.reject( 73 | new Error( 74 | `Cannot load "${module}" module. WebAssembly is not supported in this environment.` 75 | ) 76 | ); 77 | } 78 | 79 | export { 80 | instantiate, 81 | // types 82 | Imports, 83 | BoundWasmModuleInstance, 84 | JsModuleInstance, 85 | BoundModuleInstance, 86 | AsLoaderModule, 87 | // pointer types 88 | Pointer, 89 | NonPointerTypes, 90 | NullablePointer, 91 | PointerCast, 92 | PointerCastArray, 93 | PointerCastFunction, 94 | PointerCastInstance, 95 | PointerCastObject, 96 | // runtime types 97 | AsLoaderRuntime, 98 | }; 99 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: CI/CD 2 | on: [push] 3 | jobs: 4 | build: 5 | runs-on: ubuntu-latest 6 | steps: 7 | - uses: actions/checkout@v2 8 | 9 | - name: Setup node 10 | uses: actions/setup-node@v2 11 | with: 12 | node-version: ${{ matrix.node }} 13 | 14 | - name: Yarn cache directory 15 | id: yarn-cache 16 | run: echo "::set-output name=dir::$(yarn cache dir)" 17 | 18 | - name: Yarn cache 19 | uses: actions/cache@v2 20 | with: 21 | path: ${{ steps.yarn-cache.outputs.dir }} 22 | key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }} 23 | restore-keys: | 24 | ${{ runner.os }}-yarn- 25 | 26 | - name: Install dependencies 27 | run: yarn install --frozen-lockfile 28 | 29 | - name: Lint 30 | run: yarn lint 31 | 32 | - name: Build 33 | run: yarn build 34 | 35 | - name: Upload build artifact 36 | uses: actions/upload-artifact@v2 37 | with: 38 | name: artifact 39 | path: | 40 | loader 41 | runtime 42 | 43 | test: 44 | runs-on: ${{ matrix.os }} 45 | needs: build 46 | strategy: 47 | matrix: 48 | node: [14, 16] 49 | os: [ubuntu-latest, macos-latest, windows-latest] 50 | steps: 51 | - uses: actions/checkout@v2 52 | 53 | - name: Setup node 54 | uses: actions/setup-node@v2 55 | with: 56 | node-version: ${{ matrix.node }} 57 | 58 | - name: Yarn cache directory 59 | id: yarn-cache 60 | run: echo "::set-output name=dir::$(yarn cache dir)" 61 | 62 | - name: Yarn cache 63 | uses: actions/cache@v2 64 | with: 65 | path: ${{ steps.yarn-cache.outputs.dir }} 66 | key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }} 67 | restore-keys: | 68 | ${{ runner.os }}-yarn- 69 | 70 | - name: Karton cache 71 | uses: actions/cache@v2 72 | with: 73 | path: test/e2e/__locks__ 74 | key: ${{ runner.os }}-karton 75 | 76 | - name: Install dependencies 77 | run: yarn install --frozen-lockfile 78 | 79 | - name: Download build artifact 80 | uses: actions/download-artifact@v2 81 | with: 82 | name: artifact 83 | 84 | - name: Run unit tests 85 | run: yarn test unit 86 | 87 | - name: Run e2e tests 88 | run: yarn test e2e 89 | 90 | release: 91 | runs-on: ubuntu-latest 92 | needs: test 93 | if: "!contains(github.event.head_commit.message, 'ci skip') && !contains(github.event.head_commit.message, 'skip ci')" 94 | steps: 95 | - uses: actions/checkout@v2 96 | 97 | - name: Prepare repository 98 | run: git fetch --unshallow --tags 99 | 100 | - name: Setup node 101 | uses: actions/setup-node@v2 102 | with: 103 | node-version: 14 104 | 105 | - name: Get yarn cache 106 | id: yarn-cache 107 | run: echo "::set-output name=dir::$(yarn cache dir)" 108 | 109 | - uses: actions/cache@v2 110 | with: 111 | path: ${{ steps.yarn-cache.outputs.dir }} 112 | key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }} 113 | restore-keys: | 114 | ${{ runner.os }}-yarn- 115 | 116 | - name: Install dependencies 117 | run: yarn install --frozen-lockfile 118 | 119 | - name: Download build artifact 120 | uses: actions/download-artifact@v2 121 | with: 122 | name: artifact 123 | 124 | - name: Release 125 | env: 126 | GH_TOKEN: ${{ secrets.GH_TOKEN }} 127 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 128 | run: yarn release 129 | 130 | -------------------------------------------------------------------------------- /src/loader/schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft-07/schema#", 3 | "type": "object", 4 | "additionalProperties": false, 5 | "properties": { 6 | "name": { 7 | "type": "string", 8 | "description": "Output file name template, `[name].[contenthash].wasm` by default." 9 | }, 10 | "raw": { 11 | "type": "boolean", 12 | "description": "Return binary instead of emitting file." 13 | }, 14 | "fallback": { 15 | "type": "boolean", 16 | "description": "Use fallback JavaScript file if WebAssembly is not supported." 17 | }, 18 | "bind": { 19 | "type": "boolean", 20 | "description": "Add `as-bind` library files to the compilation (required if you want to use `as-loader/runtime/bind`)." 21 | }, 22 | "optimizeLevel": { 23 | "type": "number", 24 | "minimum": 0, 25 | "maximum": 3, 26 | "description": "How much to focus on optimizing code. [0-3]" 27 | }, 28 | "shrinkLevel": { 29 | "type": "number", 30 | "minimum": 0, 31 | "maximum": 2, 32 | "description": "How much to focus on shrinking code size. [0-2]" 33 | }, 34 | "coverage": { 35 | "type": "boolean", 36 | "description": "Re-optimizes until no further improvements can be made." 37 | }, 38 | "noAssert": { 39 | "type": "boolean", 40 | "description": "Replaces assertions with just their value without trapping." 41 | }, 42 | "runtime": { 43 | "type": "string", 44 | "enum": ["incremental", "minimal", "stub"], 45 | "description": "Specifies the runtime variant to include in the program." 46 | }, 47 | "exportRuntime": { 48 | "type": "boolean", 49 | "description": "Exports the runtime helpers (__new, __collect etc.)." 50 | }, 51 | "debug": { 52 | "type": "boolean", 53 | "description": "Enables debug information in emitted binaries." 54 | }, 55 | "trapMode": { 56 | "type": "string", 57 | "enum": ["allow", "clamp", "js"], 58 | "description": "Sets the trap mode to use." 59 | }, 60 | "noValidate": { 61 | "type": "boolean", 62 | "description": "Skips validating the module using Binaryen." 63 | }, 64 | "importMemory": { 65 | "type": "boolean", 66 | "description": "Imports the memory provided as 'env.memory'." 67 | }, 68 | "noExportMemory": { 69 | "type": "boolean", 70 | "description": "Does not export the memory as 'memory'." 71 | }, 72 | "initialMemory": { 73 | "type": "number", 74 | "description": "Sets the initial memory size in pages." 75 | }, 76 | "maximumMemory": { 77 | "type": "number", 78 | "description": "Sets the maximum memory size in pages." 79 | }, 80 | "sharedMemory": { 81 | "type": "boolean", 82 | "description": "Declare memory as shared. Requires maximumMemory." 83 | }, 84 | "importTable": { 85 | "type": "boolean", 86 | "description": "Imports the function table provided as 'env.table'." 87 | }, 88 | "exportTable": { 89 | "type": "boolean", 90 | "description": "Exports the function table as 'table'." 91 | }, 92 | "explicitStart": { 93 | "type": "boolean", 94 | "description": "Exports an explicit '_start' function to call." 95 | }, 96 | "enable": { 97 | "type": "array", 98 | "items": { 99 | "type": "string", 100 | "enum": [ 101 | "sign-extension", 102 | "bulk-memory", 103 | "simd", 104 | "threads", 105 | "reference-types", 106 | "gc" 107 | ] 108 | }, 109 | "description": "Enables WebAssembly features being disabled by default." 110 | }, 111 | "disable": { 112 | "type": "array", 113 | "items": { 114 | "type": "string", 115 | "enum": ["mutable-globals"] 116 | }, 117 | "description": "Disables WebAssembly features being enabled by default." 118 | }, 119 | "lowMemoryLimit": { 120 | "type": "boolean", 121 | "description": "Enforces very low (<64k) memory constraints." 122 | }, 123 | "memoryBase": { 124 | "type": "number", 125 | "description": "Sets the start offset of emitted memory segments." 126 | }, 127 | "tableBase": { 128 | "type": "number", 129 | "description": "Sets the start offset of emitted table elements." 130 | } 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /src/runtime/types/runtime.ts: -------------------------------------------------------------------------------- 1 | import { Pointer } from "./pointer"; 2 | 3 | // TypeId<> would have to be implemented on the @assemblyscript/loader side to use nominal type 4 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 5 | type TypeId = number; // & { __brand: "type-id"; __type: T }; 6 | 7 | export interface AsLoaderRuntime { 8 | memory?: WebAssembly.Memory; 9 | table?: WebAssembly.Table; 10 | 11 | /** Explicit start function, if requested. */ 12 | _start(): void; 13 | 14 | /** Copies a string's value from the module's memory. */ 15 | __getString(ptr: Pointer): string; 16 | /** Copies an ArrayBuffer's value from the module's memory. */ 17 | __getArrayBuffer(ptr: Pointer): ArrayBuffer; 18 | 19 | /** Copies an array's values from the module's memory. Infers the array type from RTTI. */ 20 | __getArray(ptr: Pointer): T[]; 21 | /** Copies an Int8Array's values from the module's memory. */ 22 | __getInt8Array(ptr: Pointer): Int8Array; 23 | /** Copies an Uint8Array's values from the module's memory. */ 24 | __getUint8Array(ptr: Pointer): Uint8Array; 25 | /** Copies an Uint8ClampedArray's values from the module's memory. */ 26 | __getUint8ClampedArray(ptr: Pointer): Uint8ClampedArray; 27 | /** Copies an Int16Array's values from the module's memory. */ 28 | __getInt16Array(ptr: Pointer): Int16Array; 29 | /** Copies an Uint16Array's values from the module's memory. */ 30 | __getUint16Array(ptr: Pointer): Uint16Array; 31 | /** Copies an Int32Array's values from the module's memory. */ 32 | __getInt32Array(ptr: Pointer): Int32Array; 33 | /** Copies an Uint32Array's values from the module's memory. */ 34 | __getUint32Array(ptr: Pointer): Uint32Array; 35 | /** Copies an Int32Array's values from the module's memory. */ 36 | __getInt64Array?(ptr: Pointer): BigInt64Array; 37 | /** Copies an Uint32Array's values from the module's memory. */ 38 | __getUint64Array?(ptr: Pointer): BigUint64Array; 39 | /** Copies a Float32Array's values from the module's memory. */ 40 | __getFloat32Array(ptr: Pointer): Float32Array; 41 | /** Copies a Float64Array's values from the module's memory. */ 42 | __getFloat64Array(ptr: Pointer): Float64Array; 43 | 44 | /** Gets a live view on an array's values in the module's memory. Infers the array type from RTTI. */ 45 | __getArrayView(ptr: Pointer): ArrayBufferView; 46 | /** Gets a live view on an Int8Array's values in the module's memory. */ 47 | __getInt8ArrayView(ptr: Pointer): Int8Array; 48 | /** Gets a live view on an Uint8Array's values in the module's memory. */ 49 | __getUint8ArrayView(ptr: Pointer): Uint8Array; 50 | /** Gets a live view on an Uint8ClampedArray's values in the module's memory. */ 51 | __getUint8ClampedArrayView( 52 | ptr: Pointer 53 | ): Uint8ClampedArray; 54 | /** Gets a live view on an Int16Array's values in the module's memory. */ 55 | __getInt16ArrayView(ptr: Pointer): Int16Array; 56 | /** Gets a live view on an Uint16Array's values in the module's memory. */ 57 | __getUint16ArrayView(ptr: Pointer): Uint16Array; 58 | /** Gets a live view on an Int32Array's values in the module's memory. */ 59 | __getInt32ArrayView(ptr: Pointer): Int32Array; 60 | /** Gets a live view on an Uint32Array's values in the module's memory. */ 61 | __getUint32ArrayView(ptr: Pointer): Uint32Array; 62 | /** Gets a live view on an Int32Array's values in the module's memory. */ 63 | __getInt64ArrayView?(ptr: Pointer): BigInt64Array; 64 | /** Gets a live view on an Uint32Array's values in the module's memory. */ 65 | __getUint64ArrayView?(ptr: Pointer): BigUint64Array; 66 | /** Gets a live view on a Float32Array's values in the module's memory. */ 67 | __getFloat32ArrayView(ptr: Pointer): Float32Array; 68 | /** Gets a live view on a Float64Array's values in the module's memory. */ 69 | __getFloat64ArrayView(ptr: Pointer): Float64Array; 70 | 71 | /** Tests whether a managed object is an instance of the class represented by the specified base id. */ 72 | __instanceof(ptr: Pointer, id: TypeId): ptr is Pointer; 73 | /** Allocates a new string in the module's memory and returns a reference (pointer) to it. */ 74 | __newString(str: string): Pointer; 75 | /** Allocates a new array in the module's memory and returns a reference (pointer) to it. */ 76 | __newArray>(id: number, values: T): Pointer; 77 | 78 | /** Allocates an instance of the class represented by the specified id. */ 79 | __new(size: number, id: TypeId): Pointer; 80 | /** Pins a managed object externally, preventing it from becoming garbage collected. */ 81 | __pin(ptr: Pointer): Pointer; 82 | /** Unpins a managed object externally, allowing it to become garbage collected. */ 83 | __unpin(ptr: Pointer): void; 84 | /** Performs a full garbage collection cycle. */ 85 | __collect(incremental?: boolean): void; 86 | } 87 | -------------------------------------------------------------------------------- /src/loader/index.ts: -------------------------------------------------------------------------------- 1 | import * as path from "path"; 2 | import * as asc from "assemblyscript/cli/asc"; 3 | import { DiagnosticCategory } from "assemblyscript"; 4 | import { getOptions, interpolateName, OptionObject } from "loader-utils"; 5 | import { validate } from "schema-utils"; 6 | import { Schema } from "schema-utils/declarations/validate"; 7 | import { createCompilerHost } from "./compiler-host"; 8 | import { mapAscOptionsToArgs, Options } from "./options"; 9 | import { AssemblyScriptError } from "./error"; 10 | import * as schema from "./schema.json"; 11 | import { isModuleCompiledToWasm, markModuleAsCompiledToWasm } from "./webpack"; 12 | 13 | const SUPPORTED_EXTENSIONS = [".wasm", ".js"]; 14 | 15 | interface LoaderOptions { 16 | readonly name?: string; 17 | readonly raw?: boolean; 18 | readonly fallback?: boolean; 19 | } 20 | 21 | type CompilerOptions = OptionObject; 22 | 23 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 24 | function loader(this: any, content: string, map?: any, meta?: any) { 25 | const options = getOptions(this); 26 | validate(schema as Schema, options, { 27 | name: "AssemblyScript Loader", 28 | baseDataPath: "options", 29 | }); 30 | 31 | // eslint-disable-next-line @typescript-eslint/no-non-null-assertion 32 | const callback = this.async()!; 33 | let isDone = false; 34 | 35 | const module = this._module; 36 | const context = this.rootContext; 37 | 38 | const { 39 | name = "[name].[contenthash].wasm", 40 | raw = false, 41 | fallback = false, 42 | bind = false, 43 | ...userAscOptions 44 | } = options as LoaderOptions & CompilerOptions; 45 | 46 | if (isModuleCompiledToWasm(module)) { 47 | // skip asc compilation - forward request to a fallback loader 48 | return callback(null, content, map, meta); 49 | } 50 | 51 | if (!SUPPORTED_EXTENSIONS.some((extension) => name.endsWith(extension))) { 52 | throw new Error( 53 | `Unsupported extension in name: "${name}" option in as-loader. ` + 54 | `Supported extensions are ${SUPPORTED_EXTENSIONS.join(", ")}` 55 | ); 56 | } 57 | 58 | if (fallback) { 59 | if (module.type?.startsWith("webassembly")) { 60 | throw new Error( 61 | `Cannot use fallback option together with module type "${module.type}". ` + 62 | `Use standard module type or disable fallback option.` 63 | ); 64 | } else if (raw) { 65 | throw new Error(`Cannot use fallback option together with raw option.`); 66 | } 67 | } 68 | 69 | if (name.endsWith(".js")) { 70 | throw new Error( 71 | `Cannot use .js extension directly. Please use fallback option instead.` 72 | ); 73 | } 74 | 75 | const ascOptions: Options = { 76 | // default options 77 | // when user imports wasm with webassembly type, it's not possible to pass env 78 | runtime: module.type?.startsWith("webassembly") ? "stub" : "incremental", 79 | exportRuntime: 80 | !module.type?.startsWith("webassembly") && 81 | userAscOptions.exportRuntime !== "stub", 82 | debug: this.mode === "development", 83 | optimizeLevel: 3, 84 | shrinkLevel: 1, 85 | noAssert: this.mode === "production", 86 | // user options 87 | ...userAscOptions, 88 | }; 89 | 90 | if (bind) { 91 | // overwrite options for bind 92 | ascOptions.exportRuntime = true; 93 | ascOptions.transform = "as-bind"; 94 | } 95 | 96 | const shouldGenerateSourceMap = this.sourceMap; 97 | const baseDir = path.dirname(this.resourcePath); 98 | const outFileName = interpolateName(this, name, { 99 | context, 100 | content, 101 | }); 102 | const sourceMapFileName = outFileName + ".map"; 103 | 104 | const host = createCompilerHost(this); 105 | 106 | if (shouldGenerateSourceMap) { 107 | ascOptions.sourceMap = true; 108 | } 109 | 110 | const args = [ 111 | path.basename(this.resourcePath), 112 | "--baseDir", 113 | baseDir, 114 | "--outFile", 115 | outFileName, 116 | ...mapAscOptionsToArgs(ascOptions), 117 | ]; 118 | 119 | asc.ready 120 | .then(() => { 121 | asc.main( 122 | args, 123 | { 124 | readFile: host.readFile, 125 | writeFile: host.writeFile, 126 | listFiles: host.listFiles, 127 | reportDiagnostic: host.reportDiagnostic, 128 | stderr: host.stderr, 129 | stdout: host.stdout, 130 | }, 131 | (error) => { 132 | // prevent from multiple callback calls from asc side 133 | if (isDone) { 134 | return 0; 135 | } 136 | isDone = true; 137 | 138 | const diagnostics = host.getDiagnostics(); 139 | 140 | diagnostics.forEach((diagnostic) => { 141 | const error = AssemblyScriptError.fromDiagnostic( 142 | diagnostic, 143 | host, 144 | baseDir, 145 | String(context) 146 | ); 147 | 148 | if (diagnostic.category === DiagnosticCategory.ERROR) { 149 | module.addError(error); 150 | } else { 151 | module.addWarning(error); 152 | } 153 | }); 154 | const errorDiagnostics = diagnostics.filter( 155 | (diagnostic) => diagnostic.category === DiagnosticCategory.ERROR 156 | ); 157 | if (errorDiagnostics.length) { 158 | const errorsWord = 159 | errorDiagnostics.length === 1 ? "error" : "errors"; 160 | callback( 161 | new AssemblyScriptError( 162 | `Compilation failed - found ${errorDiagnostics.length} ${errorsWord}.` 163 | ) 164 | ); 165 | return 1; 166 | } else if (error) { 167 | callback(error); 168 | return 2; 169 | } 170 | 171 | const outFileContent = host.readFile(outFileName, baseDir); 172 | const sourceMapFileContent = shouldGenerateSourceMap 173 | ? host.readFile(sourceMapFileName, baseDir) 174 | : undefined; 175 | 176 | if (!outFileContent) { 177 | callback( 178 | new AssemblyScriptError("Error on compiling AssemblyScript.") 179 | ); 180 | return 3; 181 | } 182 | 183 | if (outFileName.endsWith(".wasm")) { 184 | markModuleAsCompiledToWasm(module); 185 | 186 | if (module.type?.startsWith("webassembly") || raw) { 187 | // uses module type: "webassembly/sync" or "webasssembly/async" or raw: true - 188 | // return binary instead of emitting files 189 | let rawSourceMap: unknown = null; 190 | if (sourceMapFileContent) { 191 | try { 192 | rawSourceMap = JSON.parse(sourceMapFileContent.toString()); 193 | } catch (error) {} 194 | } 195 | callback(null, outFileContent, rawSourceMap); 196 | } else { 197 | const hashedOutFileName = interpolateName(this, name, { 198 | context, 199 | content: Buffer.isBuffer(outFileContent) 200 | ? outFileContent.toString("hex") 201 | : outFileContent, 202 | }); 203 | this.emitFile(hashedOutFileName, outFileContent, null, { 204 | minimized: true, 205 | immutable: /\[([^:\]]+:)?(hash|contenthash)(:[^\]]+)?]/gi.test( 206 | name 207 | ), 208 | sourceFilename: path 209 | .relative(this.rootContext, this.resourcePath) 210 | .replace(/\\/g, "/"), 211 | }); 212 | if (sourceMapFileContent) { 213 | this.emitFile(sourceMapFileName, sourceMapFileContent, null, { 214 | // we can't easily re-write link from wasm to source map and because of that, 215 | // we can't use [contenthash] for source map file name 216 | immutable: false, 217 | development: true, 218 | }); 219 | } 220 | 221 | if (fallback) { 222 | const fallbackRequest = `as-loader?name=${name.replace( 223 | /\.wasm$/, 224 | ".js" 225 | )}!${this.resourcePath}`; 226 | const fallbackChunkName = hashedOutFileName.replace( 227 | /\.wasm$/, 228 | "" 229 | ); 230 | 231 | callback( 232 | null, 233 | [ 234 | `function fallback() {`, 235 | ` return import(`, 236 | ` /* webpackChunkName: ${JSON.stringify( 237 | fallbackChunkName 238 | )} */`, 239 | ` ${JSON.stringify(fallbackRequest)}`, 240 | ` );`, 241 | `}`, 242 | `var path = new String(__webpack_public_path__ + ${JSON.stringify( 243 | hashedOutFileName 244 | )});`, 245 | "path.fallback = fallback;", 246 | `module.exports = path;`, 247 | ].join("\n") 248 | ); 249 | } else { 250 | callback( 251 | null, 252 | `module.exports = __webpack_public_path__ + ${JSON.stringify( 253 | hashedOutFileName 254 | )};` 255 | ); 256 | } 257 | } 258 | } else if (outFileName.endsWith(".js")) { 259 | let rawSourceMap: unknown = null; 260 | if (sourceMapFileContent) { 261 | try { 262 | rawSourceMap = JSON.parse(sourceMapFileContent.toString()); 263 | } catch (error) {} 264 | } 265 | callback(null, outFileContent, rawSourceMap); 266 | } 267 | 268 | return 0; 269 | } 270 | ); 271 | }) 272 | .catch((error) => { 273 | callback(error); 274 | }); 275 | } 276 | 277 | module.exports = loader; 278 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | 3 | AssemblyScript logo 4 | webpack logo 5 | 6 |

as-loader

7 |

AssemblyScript loader for webpack

8 | 9 | [![npm version](https://img.shields.io/npm/v/as-loader.svg)](https://www.npmjs.com/package/as-loader) 10 | [![build status](https://github.com/piotr-oles/as-loader/workflows/CI/CD/badge.svg?branch=main&event=push)](https://github.com/piotr-oles/as-loader/actions?query=branch%3Amain+event%3Apush) 11 | 12 |
13 | 14 | ## Installation 15 | 16 | This loader requires [AssemblyScript ~0.18](https://github.com/AssemblyScript/assemblyscript), 17 | Node.js >= 12 and [webpack 5](https://github.com/webpack/webpack) 18 | 19 | ```sh 20 | # with npm 21 | npm install as-loader 22 | npm install --save-dev assemblyscript 23 | 24 | # with yarn 25 | yarn add as-loader 26 | yarn add --dev assemblyscript 27 | ``` 28 | 29 | The minimal `webpack.config.js`: 30 | 31 | ```js 32 | module.exports = { 33 | entry: "src/index.ts", 34 | resolve: { 35 | extensions: [".ts", ".js"], 36 | }, 37 | module: { 38 | rules: [ 39 | { 40 | test: /\.ts$/, 41 | include: path.resolve(__dirname, "src/assembly"), 42 | loader: "as-loader", 43 | options: { 44 | // optional loader and compiler options 45 | } 46 | }, 47 | { 48 | test: /\.ts$/, 49 | exclude: path.resolve(__dirname, "src/assembly"), 50 | loader: "ts-loader", 51 | }, 52 | ], 53 | }, 54 | }; 55 | ``` 56 | 57 | ## Example repository 58 | 59 | https://stackblitz.com/edit/webpack-webpack-js-org-zl6ung?file=webpack.config.js 60 | 61 | ## Usage 62 | 63 | By default, the loader emits a `.wasm` file (+ `.wasm.map` if source maps are enabled) and 64 | creates CommonJS module that exports URL to the emitted `.wasm` file. 65 | 66 | If you enable `fallback` option, the loader will emit additional `.js` file (+ `.js.map` if source maps are enabled) 67 | and will expose async `fallback()` function which dynamically imports fallback module. 68 | 69 | To simplify loading logic, you can use `as-loader/runtime` loader which uses 70 | [@assemblyscript/loader](https://github.com/AssemblyScript/assemblyscript/tree/master/lib/loader), or 71 | `as-loader/runtime/bind` loader which uses [as-bind](https://github.com/torch2424/as-bind). 72 | These loaders provide correct types, checks for WebAssembly support, and uses fallback if available. 73 | 74 | ```typescript 75 | import * as myModule from "./assembly/myModule"; 76 | import { instantiate } from "as-loader/runtime"; 77 | 78 | async function loadAndRun() { 79 | const { exports } = await instantiate(myModule); 80 | 81 | exports.myFunction(100); 82 | } 83 | 84 | loadAndRun(); 85 | ``` 86 |
87 | Alternatively, you can use exported URL directly: 88 | 89 | ```typescript 90 | import * as myModule from "./assembly/myModule"; 91 | import { instantiate } from "@assemblyscript/loader"; 92 | 93 | async function loadAndRun() { 94 | const { exports } = await instantiate( 95 | // workaround for TypeScript 96 | fetch((myModule as unknown) as string) 97 | ); 98 | 99 | exports.myFunction(100); 100 | } 101 | 102 | loadAndRun(); 103 | 104 | ``` 105 | 106 |
107 | 108 | ### API 109 | > For more details, check [src/runtime](src/runtime) directory 110 | 111 | #### `as-loader/runtime` 112 | This runtime loader uses [@assemblyscript/loader](https://github.com/AssemblyScript/assemblyscript/tree/master/lib/loader) under the hood. 113 | ```typescript 114 | export interface WasmModuleInstance { 115 | type: "wasm"; 116 | exports: AsLoaderRuntime & PointerCastObject; 117 | module: WebAssembly.Module; 118 | instance: WebAssembly.Instance; 119 | } 120 | 121 | export interface JsModuleInstance { 122 | type: "js"; 123 | exports: TModule; 124 | } 125 | 126 | export type ModuleInstance = 127 | | WasmModuleInstance 128 | | JsModuleInstance; 129 | 130 | export function instantiate( 131 | module: TModule, 132 | load: (url: string) => Promise, 133 | imports?: object, 134 | fallback: boolean = false, 135 | supports?: () => boolean 136 | ): Promise> 137 | ``` 138 | 139 |
140 | as-loader/runtime binding code example: 141 | 142 | ```typescript 143 | // ./src/assembly/sayHello.ts 144 | export function sayHello(firstName: string, lastName: string): string { 145 | return `Hello ${firstName} ${lastName}!`; 146 | } 147 | 148 | // ./src/sayHello.ts 149 | import * as sayHelloModule from "./assembly/sayHello"; 150 | import { instantiate } from "as-loader/runtime"; 151 | 152 | export async function loadModule(): Promise { 153 | const { exports } = await instantiate(sayHelloModule, fetch); 154 | const { __pin, __unpin, __newString, __getString } = exports; 155 | 156 | function sayHello(firstName: string, lastName: string): string { 157 | const firstNamePtr = __pin(__newString(firstName)); 158 | const lastNamePtr = __pin(__newString(lastName)); 159 | const result = __getString( 160 | exports.sayHello(firstNamePtr, lastNamePtr) 161 | ); 162 | 163 | __unpin(firstNamePtr); 164 | __unpin(lastNamePtr); 165 | 166 | return result; 167 | } 168 | 169 | return { sayHello }; 170 | } 171 | ``` 172 | 173 |
174 | 175 | 176 | #### `as-loader/runtime/bind` 177 | This runtime loader uses [as-bind](https://github.com/torch2424/as-bind) under the hood. 178 | Requires `bind` option enabled in the webpack loader configuration. 179 | > Keep in mind that currently [it's recommended to manually set `Function.returnType`](https://github.com/torch2424/as-bind#production) 180 | ```typescript 181 | export interface BoundWasmModuleInstance { 182 | type: "wasm-bound"; 183 | exports: AsLoaderRuntime & BoundExports; 184 | unboundExports: AsLoaderRuntime & PointerCastObject; 185 | importObject: TImports; 186 | module: WebAssembly.Module; 187 | instance: WebAssembly.Instance; 188 | } 189 | 190 | export interface JsModuleInstance { 191 | type: "js"; 192 | exports: TModule; 193 | } 194 | 195 | type BoundModuleInstance = 196 | | BoundWasmModuleInstance 197 | | JsModuleInstance; 198 | 199 | export function instantiate( 200 | module: TModule, 201 | load: (url: string) => Promise, 202 | imports?: TImports, 203 | fallback: boolean = false, 204 | supports?: () => boolean 205 | ): Promise> 206 | ``` 207 | 208 |
209 | as-loader/runtime/bind binding code example: 210 | 211 | ```typescript 212 | // ./src/assembly/sayHello.ts 213 | export function sayHello(firstName: string, lastName: string): string { 214 | return `Hello ${firstName} ${lastName}!`; 215 | } 216 | 217 | // ./src/sayHello.ts 218 | import * as sayHelloModule from "./assembly/sayHello"; 219 | import { instantiate } from "as-loader/runtime/bind"; 220 | 221 | export async function loadModule(): Promise { 222 | const module = await instantiate(sayHelloModule, fetch); 223 | 224 | return { sayHello: module.exports.sayHello }; 225 | } 226 | ``` 227 | 228 |
229 | 230 | ## Binding 231 | There are 2 aspects that you have to consider when interacting with a WebAssembly module: 232 | 1. WebAssembly doesn't support function arguments and returns others than `number | boolean | bigint` yet. 233 | Because of that, you have to [manually translate between WebAssembly pointers and JavaScript objects](https://www.assemblyscript.org/loader.html#usage). 234 | 235 | The alternative is to enable the `bind` option and use `as-loader/runtime/bind` loader which uses an [as-bind](https://github.com/torch2424/as-bind) library. 236 | This simplifies passing types like strings and arrays. 237 | 238 | 2. WebAssembly doesn't provide Garbage Collector yet ([proposal](https://github.com/WebAssembly/gc)) - to manage memory, 239 | AssemblyScript offers very lightweight GC implementation. If you use it (see `runtime` option), 240 | you have to [manually `__pin` and `__unpin` pointers](https://www.assemblyscript.org/garbage-collection.html#incremental-runtime) 241 | to instruct GC if given data can be collected or not. 242 | 243 | ## Fallback 244 | If you need to support [older browsers](https://caniuse.com/wasm) like *Internet Explorer* or *Edge* < 16, 245 | you can use the `fallback` option. A fallback module is different from WebAssembly one because you don't have to bind it. 246 | 247 | 248 |
249 | Fallback example: 250 | 251 | ```js 252 | // webpack.config.js 253 | module.exports = { 254 | entry: "src/index.ts", 255 | resolve: { 256 | extensions: [".ts", ".js"], 257 | }, 258 | module: { 259 | rules: [ 260 | { 261 | test: /\.ts$/, 262 | include: path.resolve(__dirname, "src/assembly"), 263 | use: [ 264 | // fallback loader (must be before as-loader) 265 | { 266 | loader: "ts-loader", 267 | options: { 268 | transpileOnly: true 269 | } 270 | }, 271 | // as-loader, apart from building .wasm file, 272 | // will forward assembly script files to the fallback loader above 273 | // to build a .js file 274 | { 275 | loader: "as-loader", 276 | options: { 277 | fallback: true 278 | } 279 | } 280 | ] 281 | }, 282 | { 283 | test: /\.ts$/, 284 | exclude: path.resolve(__dirname, "src/assembly"), 285 | loader: "ts-loader", 286 | }, 287 | ], 288 | }, 289 | }; 290 | ``` 291 | ```typescript 292 | // ./src/assembly/sayHello.ts 293 | export function sayHello(firstName: string, lastName: string): string { 294 | return `Hello ${firstName} ${lastName}!`; 295 | } 296 | 297 | // ./src/sayHello.ts 298 | import * as sayHelloModule from "./assembly/sayHello"; 299 | import { instantiate } from "as-loader/runtime"; 300 | 301 | export async function loadModule(): Promise { 302 | // set fallback option to true (opt-in) 303 | const module = await instantiate(sayHelloModule, fetch, undefined, true); 304 | 305 | if (module.type === 'wasm') { 306 | const { __pin, __unpin, __newString, __getString } = exports; 307 | 308 | function sayHello(firstName: string, lastName: string): string { 309 | const firstNamePtr = __pin(__newString(firstName)); 310 | const lastNamePtr = __pin(__newString(lastName)); 311 | const result = __getString( 312 | exports.sayHello(firstNamePtr, lastNamePtr) 313 | ); 314 | 315 | __unpin(firstNamePtr); 316 | __unpin(lastNamePtr); 317 | 318 | return result; 319 | } 320 | 321 | return { sayHello }; 322 | } else { 323 | return { sayHello: module.exports.sayHello } 324 | } 325 | } 326 | ``` 327 | 328 |
329 | 330 | ## Options 331 | #### Loader Options 332 | 333 | | Name | Type | Description | 334 | |------------|---------| ----------- | 335 | | `name` | string | Output asset name template, `[name].[contenthash].wasm` by default. | 336 | | `bind` | boolean | If true, adds [as-bind](https://github.com/torch2424/as-bind) library files to the compilation (required if you want to use `as-loader/runtime/bind`). | 337 | | `fallback` | boolean | If true, creates additional JavaScript file which can be used if WebAssembly is not supported. | 338 | | `raw` | boolean | If true, returns binary instead of emitting file. Use for chaining with other loaders. | 339 | 340 | #### Compiler Options 341 | 342 | Options passed to the [AssemblyScript compiler](https://www.assemblyscript.org/compiler.html#command-line-options). 343 | 344 | | Name | Type | Description | 345 | |------------------|----------| ----------- | 346 | | `debug` | boolean | Enables debug information in emitted binaries, enabled by default in webpack development mode. | 347 | | `optimizeLevel` | number | How much to focus on optimizing code, 3 by default. [0-3] | 348 | | `shrinkLevel` | number | How much to focus on shrinking code size, 1 by default. [0-2] | 349 | | `coverage` | boolean | Re-optimizes until no further improvements can be made. | 350 | | `noAssert` | boolean | Replaces assertions with just their value without trapping, enabled by default in webpack production mode. | 351 | | `importMemory` | boolean | Imports the memory provided as 'env.memory'. | 352 | | `noExportMemory` | boolean | Does not export the memory as 'memory'. | 353 | | `initialMemory` | number | Sets the initial memory size in pages. | 354 | | `maximumMemory` | number | Sets the maximum memory size in pages. | 355 | | `sharedMemory` | boolean | Declare memory as shared. Requires maximumMemory. | 356 | | `importTable` | boolean | Imports the function table provided as 'env.table'. | 357 | | `exportTable` | boolean | Exports the function table as 'table'. | 358 | | `runtime` | string | Specifies the runtime variant to include in the program. Available runtime are: "incremental" (default), "minimal", "stub" | 359 | | `exportRuntime` | boolean | Exports the runtime helpers (__new, __collect etc.). Enabled by default. | 360 | | `explicitStart` | boolean | Exports an explicit '_start' function to call. | 361 | | `enable` | string[] | Enables WebAssembly features being disabled by default. Available features are: "sign-extension", "bulk-memory", "simd", "threads", "reference-types", "gc" | 362 | | `disable` | string[] | Disables WebAssembly features being enabled by default. Available features are: "mutable-globals" | 363 | | `lowMemoryLimit` | boolean | Enforces very low (<64k) memory constraints. | 364 | | `memoryBase` | number | Sets the start offset of emitted memory segments. | 365 | | `tableBase` | number | Sets the start offset of emitted table elements. | 366 | | `trapMode` | string | Sets the trap mode to use. Available modes are: "allow", "clamp", "js" | 367 | | `noValidate` | boolean | Skips validating the module using Binaryen. | 368 | 369 | ## License 370 | 371 | MIT 372 | -------------------------------------------------------------------------------- /test/e2e/main.spec.ts: -------------------------------------------------------------------------------- 1 | import * as path from "path"; 2 | import { 3 | createSandbox, 4 | Sandbox, 5 | packLocalPackage, 6 | createProcessDriver, 7 | } from "karton"; 8 | import { instantiate } from "@assemblyscript/loader/umd"; 9 | 10 | jest.setTimeout(120000); 11 | 12 | describe("as-loader", () => { 13 | let sandbox: Sandbox; 14 | 15 | beforeAll(async () => { 16 | sandbox = await createSandbox({ 17 | lockDirectory: path.resolve(__dirname, "__locks__"), 18 | fixedDependencies: { 19 | "as-loader": `file:${await packLocalPackage( 20 | path.resolve(__dirname, "../../") 21 | )}`, 22 | }, 23 | }); 24 | }); 25 | afterEach(async () => { 26 | await sandbox.reset(); 27 | }); 28 | afterAll(async () => { 29 | await sandbox.cleanup(); 30 | }); 31 | 32 | describe("single compilation", () => { 33 | it("works without options", async () => { 34 | await sandbox.load(path.resolve(__dirname, "fixtures/main")); 35 | await sandbox.install("yarn", {}); 36 | 37 | const webpackResults = await sandbox.exec("yarn webpack"); 38 | 39 | expect(webpackResults).toContain("simple.wasm"); 40 | expect(webpackResults).toContain("simple.wasm.map"); 41 | expect(webpackResults).toContain("main.js"); 42 | 43 | const simpleWasmInstance = await instantiate< 44 | typeof import("./fixtures/main/src/assembly/correct/simple") 45 | >(await sandbox.read("dist/simple.wasm")); 46 | 47 | expect(simpleWasmInstance.exports.run()).toEqual(15); 48 | 49 | const simpleWasmMap = await sandbox.read("dist/simple.wasm.map", "utf8"); 50 | expect(Object.keys(JSON.parse(simpleWasmMap))).toEqual( 51 | expect.arrayContaining(["version", "sources", "names", "mappings"]) 52 | ); 53 | 54 | const mainResults = await sandbox.exec("node ./dist/main.js"); 55 | expect(mainResults).toEqual("15\n"); 56 | }); 57 | 58 | it("reports errors in project", async () => { 59 | await sandbox.load(path.resolve(__dirname, "fixtures/main")); 60 | await sandbox.install("yarn", {}); 61 | 62 | await sandbox.patch( 63 | "webpack.config.js", 64 | 'entry: "./src/correct.ts",', 65 | 'entry: "./src/broken.ts",' 66 | ); 67 | 68 | const results = await sandbox.exec("yarn webpack", { fail: true }); 69 | 70 | expect(results).toContain( 71 | [ 72 | "ERROR in ./src/assembly/broken/simple.ts", 73 | "Module build failed (from ./node_modules/as-loader/loader/index.js):", 74 | "AssemblyScriptError: Compilation failed - found 3 errors.", 75 | ].join("\n") 76 | ); 77 | expect(results).toContain( 78 | [ 79 | "ERROR in ./src/assembly/broken/simple.ts 4:14-15", 80 | "Type 'i32' is not assignable to type '~lib/string/String'.", 81 | ].join("\n") 82 | ); 83 | expect(results).toContain( 84 | [ 85 | "ERROR in ./src/assembly/broken/shared.ts 2:14-15", 86 | "Type 'i32' is not assignable to type '~lib/string/String'.", 87 | ].join("\n") 88 | ); 89 | expect(results).toContain( 90 | [ 91 | "ERROR in ./src/assembly/broken/shared.ts 2:10-15", 92 | "Type '~lib/string/String' is not assignable to type 'i32'.", 93 | ].join("\n") 94 | ); 95 | }); 96 | 97 | it.each([ 98 | ["webassembly/sync", "syncWebAssembly"], 99 | ["webassembly/async", "asyncWebAssembly"], 100 | ])("loads using %s type", async (type, experiment) => { 101 | await sandbox.load(path.resolve(__dirname, "fixtures/main")); 102 | await sandbox.install("yarn", {}); 103 | 104 | await sandbox.patch( 105 | "webpack.config.js", 106 | 'entry: "./src/correct.ts",', 107 | 'entry: "./src/async.ts",' 108 | ); 109 | await sandbox.patch( 110 | "webpack.config.js", 111 | ' loader: "as-loader",', 112 | [' loader: "as-loader",', ` type: "${type}",`].join("\n") 113 | ); 114 | await sandbox.patch( 115 | "webpack.config.js", 116 | ' mode: "development",', 117 | [ 118 | ' mode: "development",', 119 | " experiments: {", 120 | ` ${experiment}: true,`, 121 | " },", 122 | ].join("\n") 123 | ); 124 | 125 | const webpackResults = await sandbox.exec("yarn webpack"); 126 | 127 | expect(webpackResults).toContain("main.js"); 128 | expect(webpackResults).toContain(".wasm"); 129 | 130 | const distDirents = await sandbox.list("dist"); 131 | const simpleWasmDirent = distDirents.find( 132 | (dirent) => dirent.isFile() && dirent.name.endsWith(".wasm") 133 | ); 134 | expect(simpleWasmDirent).toBeDefined(); 135 | 136 | const simpleWasmInstance = await instantiate< 137 | typeof import("./fixtures/main/src/assembly/correct/simple") 138 | >(await sandbox.read(`dist/${simpleWasmDirent?.name}`)); 139 | 140 | expect(simpleWasmInstance.exports.run()).toEqual(15); 141 | 142 | const mainResults = await sandbox.exec("node ./dist/main.js"); 143 | expect(mainResults).toEqual("15\n"); 144 | }); 145 | 146 | it("creates js file for fallback option", async () => { 147 | await sandbox.load(path.resolve(__dirname, "fixtures/main")); 148 | await sandbox.install("yarn", {}); 149 | 150 | await sandbox.patch( 151 | "webpack.config.js", 152 | ' entry: "./src/correct.ts",', 153 | ' entry: "./src/fallback.ts",' 154 | ); 155 | await sandbox.patch( 156 | "webpack.config.js", 157 | [ 158 | ' loader: "as-loader",', 159 | " options: {", 160 | ' name: "[name].wasm",', 161 | " },", 162 | ].join("\n"), 163 | [ 164 | " use: [", 165 | " {", 166 | ' loader: "ts-loader",', 167 | " options: {", 168 | " transpileOnly: true,", 169 | " }", 170 | " },", 171 | " {", 172 | ' loader: "as-loader",', 173 | " options: {", 174 | ' name: "[name].wasm",', 175 | " fallback: true,", 176 | " },", 177 | " },", 178 | " ],", 179 | ].join("\n") 180 | ); 181 | 182 | const webpackResults = await sandbox.exec("yarn webpack"); 183 | 184 | expect(webpackResults).toContain("complex.js"); 185 | expect(webpackResults).toContain("complex.wasm"); 186 | expect(webpackResults).toContain("complex.wasm.map"); 187 | expect(webpackResults).toContain("main.js"); 188 | 189 | expect(await sandbox.exists("dist/complex.js")).toEqual(true); 190 | expect(await sandbox.exists("dist/complex.wasm")).toEqual(true); 191 | expect(await sandbox.exists("dist/complex.js.map")).toEqual(true); 192 | expect(await sandbox.exists("dist/complex.wasm.map")).toEqual(true); 193 | 194 | const simpleJsMap = await sandbox.read("dist/complex.js.map", "utf8"); 195 | expect(Object.keys(JSON.parse(simpleJsMap))).toEqual( 196 | expect.arrayContaining(["version", "sources", "names", "mappings"]) 197 | ); 198 | 199 | const mainResults = await sandbox.exec("node ./dist/main.js"); 200 | expect(mainResults).toEqual( 201 | "rgb(100, 50, 20),rgb(105, 51, 19),rgb(110, 52, 18),rgb(115, 53, 17),rgb(120, 54, 16),rgb(125, 55, 15),rgb(130, 56, 14),rgb(135, 57, 13),rgb(140, 58, 12),rgb(145, 59, 11)\n" 202 | ); 203 | }); 204 | 205 | it("sets correct [contenthash]", async () => { 206 | await sandbox.load(path.resolve(__dirname, "fixtures/main")); 207 | await sandbox.install("yarn", {}); 208 | 209 | await sandbox.patch( 210 | "webpack.config.js", 211 | ' name: "[name].wasm",', 212 | ' name: "[name].[contenthash].wasm",' 213 | ); 214 | 215 | const webpackResults = await sandbox.exec("yarn webpack"); 216 | 217 | const simpleWasmFileName = path.join( 218 | "dist", 219 | (await sandbox.list("dist")).find((dirent) => 220 | dirent.name.endsWith(".wasm") 221 | )?.name 222 | ); 223 | const simpleWasmSourceMapFileName = path.join( 224 | "dist", 225 | (await sandbox.list("dist")).find((dirent) => 226 | dirent.name.endsWith(".wasm.map") 227 | )?.name 228 | ); 229 | 230 | expect(simpleWasmFileName).toMatch(/simple\.[0-9a-f]+\.wasm$/); 231 | expect(simpleWasmSourceMapFileName).toMatch( 232 | /simple\.[0-9a-f]+\.wasm\.map$/ 233 | ); 234 | 235 | expect(webpackResults).toContain(path.basename(simpleWasmFileName)); 236 | expect(webpackResults).toContain( 237 | path.basename(simpleWasmSourceMapFileName) 238 | ); 239 | 240 | const mainResults = await sandbox.exec("node ./dist/main.js"); 241 | expect(mainResults).toEqual("15\n"); 242 | }); 243 | 244 | it("compiles example with context data types", async () => { 245 | await sandbox.load(path.resolve(__dirname, "fixtures/main")); 246 | await sandbox.install("yarn", {}); 247 | 248 | await sandbox.patch( 249 | "webpack.config.js", 250 | ' entry: "./src/correct.ts",', 251 | ' entry: "./src/complex.ts",' 252 | ); 253 | 254 | const webpackResults = await sandbox.exec("yarn webpack"); 255 | 256 | expect(webpackResults).toContain("complex.wasm"); 257 | expect(webpackResults).toContain("complex.wasm.map"); 258 | expect(webpackResults).toContain("main.js"); 259 | 260 | const mainResults = await sandbox.exec("node ./dist/main.js"); 261 | expect(mainResults).toEqual( 262 | "rgb(100, 50, 20),rgb(105, 51, 19),rgb(110, 52, 18),rgb(115, 53, 17),rgb(120, 54, 16),rgb(125, 55, 15),rgb(130, 56, 14),rgb(135, 57, 13),rgb(140, 58, 12),rgb(145, 59, 11)\n" 263 | ); 264 | }); 265 | 266 | it("compiles example with bind", async () => { 267 | await sandbox.load(path.resolve(__dirname, "fixtures/main")); 268 | await sandbox.install("yarn", {}); 269 | 270 | await sandbox.patch( 271 | "webpack.config.js", 272 | ' entry: "./src/correct.ts",', 273 | ' entry: "./src/bind.ts",' 274 | ); 275 | await sandbox.patch( 276 | "webpack.config.js", 277 | ' name: "[name].wasm",', 278 | [' name: "[name].wasm",', " bind: true,"].join("\n") 279 | ); 280 | 281 | const webpackResults = await sandbox.exec("yarn webpack"); 282 | 283 | expect(webpackResults).toContain("bind.wasm"); 284 | expect(webpackResults).toContain("bind.wasm.map"); 285 | expect(webpackResults).toContain("main.js"); 286 | 287 | const mainResults = await sandbox.exec("node ./dist/main.js"); 288 | expect(mainResults).toEqual("Hello world!\n"); 289 | }); 290 | }); 291 | 292 | describe("watch compilation", () => { 293 | it("re-compiles wasm file on change with", async () => { 294 | await sandbox.load(path.resolve(__dirname, "fixtures/main")); 295 | await sandbox.install("yarn", {}); 296 | 297 | const webpack = createProcessDriver( 298 | await sandbox.spawn("yarn webpack --watch") 299 | ); 300 | 301 | await webpack.waitForStdoutIncludes(["simple.wasm ", "simple.wasm.map "]); 302 | 303 | expect(await sandbox.exists("dist/simple.wasm")).toBe(true); 304 | expect(await sandbox.exists("dist/simple.wasm.map")).toBe(true); 305 | 306 | // update assembly script file 307 | await sandbox.patch("src/assembly/correct/shared.ts", "a + b", "a - b"); 308 | 309 | await webpack.waitForStdoutIncludes(["simple.wasm ", "simple.wasm.map "]); 310 | 311 | const simpleWasmInstance = await instantiate< 312 | typeof import("./fixtures/main/src/assembly/correct/simple") 313 | >(await sandbox.read(`dist/simple.wasm`)); 314 | 315 | expect(simpleWasmInstance.exports.run()).toEqual(-5); 316 | }); 317 | 318 | it("reports errors on change", async () => { 319 | await sandbox.load(path.resolve(__dirname, "fixtures/main")); 320 | await sandbox.install("yarn", {}); 321 | 322 | const webpack = createProcessDriver( 323 | await sandbox.spawn("yarn webpack --watch") 324 | ); 325 | 326 | await webpack.waitForStdoutIncludes("simple.wasm "); 327 | 328 | // update assembly script file 329 | await sandbox.patch( 330 | "src/assembly/correct/shared.ts", 331 | "a: i32", 332 | "a: string" 333 | ); 334 | 335 | await webpack.waitForStdoutIncludes([ 336 | "AssemblyScriptError: Compilation failed - found 3 errors.", 337 | [ 338 | "ERROR in ./src/assembly/correct/simple.ts 4:14-15", 339 | "Type 'i32' is not assignable to type '~lib/string/String'.", 340 | ].join("\n"), 341 | [ 342 | "ERROR in ./src/assembly/correct/shared.ts 2:14-15", 343 | "Type 'i32' is not assignable to type '~lib/string/String'.", 344 | ].join("\n"), 345 | [ 346 | "ERROR in ./src/assembly/correct/shared.ts 2:10-15", 347 | "Type '~lib/string/String' is not assignable to type 'i32'.", 348 | ].join("\n"), 349 | ]); 350 | 351 | await sandbox.patch("src/assembly/correct/shared.ts", "a + b", "a - b"); 352 | 353 | await webpack.waitForStdoutIncludes( 354 | "AssemblyScriptError: Compilation failed - found 2 errors." 355 | ); 356 | 357 | await sandbox.patch( 358 | "src/assembly/correct/shared.ts", 359 | "a: string", 360 | "a: i32" 361 | ); 362 | 363 | await webpack.waitForStdoutIncludes("simple.wasm "); 364 | 365 | const simpleWasm = await sandbox.read(`dist/simple.wasm`); 366 | const simpleWasmInstance = await instantiate< 367 | typeof import("./fixtures/main/src/assembly/correct/simple") 368 | >(simpleWasm); 369 | 370 | expect(simpleWasmInstance.exports.run()).toEqual(-5); 371 | }); 372 | }); 373 | 374 | describe("options", () => { 375 | it("passes options to assemblyscript compiler", async () => { 376 | await sandbox.load(path.resolve(__dirname, "fixtures/main")); 377 | await sandbox.install("yarn", {}); 378 | 379 | await sandbox.patch( 380 | "webpack.config.js", 381 | ' name: "[name].wasm",', 382 | [ 383 | ' name: "[name].wasm",', 384 | " optimizeLevel: 2,", 385 | " shrinkLevel: 1,", 386 | " coverage: true,", 387 | " noAssert: true,", 388 | ' runtime: "minimal",', 389 | " debug: true,", 390 | ' trapMode: "allow",', 391 | " noValidate: true,", 392 | " importMemory: false,", 393 | " noExportMemory: true,", 394 | " initialMemory: 5000,", 395 | " maximumMemory: 10000,", 396 | " sharedMemory: false,", 397 | " importTable: false,", 398 | " exportTable: false,", 399 | " explicitStart: false,", 400 | ' enable: ["simd", "threads"],', 401 | ' disable: ["mutable-globals"],', 402 | " lowMemoryLimit: false,", 403 | ].join("\n") 404 | ); 405 | 406 | const results = await sandbox.exec("yarn webpack"); 407 | expect(results).toContain("simple.wasm"); 408 | expect(results).toContain("simple.wasm.map"); 409 | expect(results).toContain("main.js"); 410 | 411 | const simpleWasmInstance = await instantiate< 412 | typeof import("./fixtures/main/src/assembly/correct/simple") 413 | >(await sandbox.read("dist/simple.wasm")); 414 | 415 | expect(simpleWasmInstance.exports.run()).toEqual(15); 416 | 417 | const simpleWasmMap = await sandbox.read("dist/simple.wasm.map", "utf8"); 418 | expect(Object.keys(JSON.parse(simpleWasmMap))).toEqual( 419 | expect.arrayContaining(["version", "sources", "names", "mappings"]) 420 | ); 421 | }); 422 | }); 423 | }); 424 | --------------------------------------------------------------------------------