Skip to content

Commit

Permalink
fix: make wasmbuild work in the browser again (#113)
Browse files Browse the repository at this point in the history
  • Loading branch information
dsherret authored Jan 14, 2024
1 parent 7f16f23 commit 6f06f8d
Show file tree
Hide file tree
Showing 9 changed files with 162 additions and 79 deletions.
3 changes: 3 additions & 0 deletions deno.jsonc
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@
"./tests/target",
"./target"
],
"compilerOptions": {
"checkJs": true
},
"tasks": {
"fmt": "deno fmt && cargo fmt",
"build": "deno run -A ./main.ts -p wasmbuild",
Expand Down
68 changes: 50 additions & 18 deletions lib/pre_build.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { getCargoWorkspace, WasmCrate } from "./manifest.ts";
import { verifyVersions } from "./versions.ts";
import { BindgenOutput, generateBindgen } from "./bindgen.ts";
import { pathExists } from "./helpers.ts";
import { fetchWithRetries } from "../loader/fetch.js";
export type { BindgenOutput } from "./bindgen.ts";

export interface PreBuildOutput {
Expand Down Expand Up @@ -127,11 +128,12 @@ async function getBindingJsOutput(
) {
const sourceHash = await getHash();
const header = `// @generated file from wasmbuild -- do not edit
// @ts-nocheck: generated
// deno-lint-ignore-file
// deno-fmt-ignore-file`;
const genText = bindgenOutput.js.replace(
/\bconst\swasm_url\s.+/ms,
getLoaderText(args, crate, bindgenOutput, bindingJsPath),
await getLoaderText(args, crate, bindgenOutput, bindingJsPath),
);
const bodyText = await getFormattedText(`
// source-hash: ${sourceHash}
Expand Down Expand Up @@ -193,7 +195,7 @@ ${genText}
}
}

function getLoaderText(
async function getLoaderText(
args: CheckCommand | BuildCommand,
crate: WasmCrate,
bindgenOutput: BindgenOutput,
Expand All @@ -203,9 +205,19 @@ function getLoaderText(
case "sync":
return getSyncLoaderText(bindgenOutput);
case "async":
return getAsyncLoaderText(crate, bindgenOutput, false, bindingJsPath);
return await getAsyncLoaderText(
crate,
bindgenOutput,
false,
bindingJsPath,
);
case "async-with-cache":
return getAsyncLoaderText(crate, bindgenOutput, true, bindingJsPath);
return await getAsyncLoaderText(
crate,
bindgenOutput,
true,
bindingJsPath,
);
}
}

Expand Down Expand Up @@ -287,38 +299,42 @@ function parseRelativePath(
return path.isAbsolute(relativeFromTo) ? specifier : relativeFromTo;
}

function getAsyncLoaderText(
async function getAsyncLoaderText(
crate: WasmCrate,
bindgenOutput: BindgenOutput,
useCache: boolean,
bindingJsFileName: string,
) {
const exportNames = getExportNames(bindgenOutput);
const loaderUrl = parseRelativePath(bindingJsFileName, "../loader.ts");

let loaderText = `import { Loader } from "${loaderUrl}";\n`;
const fetchContents = await fetchModuleContents("../loader/fetch.js");
const loaderContents = (await fetchModuleContents("../loader/mod.js"))
.replace(`import { fetchWithRetries } from "./fetch.js";`, "");

let loaderText = fetchContents + "\n" + loaderContents + "\n";

let cacheText = "";
if (useCache) {
const cacheUrl = parseRelativePath(bindingJsFileName, "../cache.ts");
loaderText += `import { cacheToLocalDir } from "${cacheUrl}";\n`;
// If it's Deno or Node (via dnt), then use the cache.
// It's ok that the Node path is importing a .ts file because
// it will be transformed by dnt.
loaderText +=
`const isNodeOrDeno = typeof Deno === "object" || (typeof process !== "undefined" && process.versions != null && process.versions.node != null);\n`;
const cacheUrl = parseRelativePath(bindingJsFileName, "../loader/cache.ts");
cacheText +=
`isNodeOrDeno ? (await import("${cacheUrl}")).cacheToLocalDir : undefined`;
} else {
cacheText = "undefined";
}

loaderText += `
const loader = new Loader({
imports,
cache: ${useCache ? "cacheToLocalDir" : "undefined"},
cache: ${cacheText},
})
`;

loaderText += `/**
* Decompression callback
*
* @callback DecompressCallback
* @param {Uint8Array} compressed
* @return {Uint8Array} decompressed
*/
/**
* Options for instantiating a Wasm instance.
* @typedef {Object} InstantiateOptions
* @property {URL=} url - Optional url to the Wasm file to instantiate.
Expand Down Expand Up @@ -371,6 +387,22 @@ export function isInstantiated() {
return loaderText;
}

async function fetchModuleContents(path: string) {
const url = import.meta.resolve(path);
const dataResponse = await fetchWithRetries(url);
if (!dataResponse.ok) {
throw new Error(
`Failed fetching ${url}: ${dataResponse.statusText} - ${await dataResponse
.text()}`,
);
}
return (await dataResponse.text())
.replace(
"// Copyright 2018-2022 the Deno authors. All rights reserved. MIT license.\n",
"",
);
}

function getExportNames(bindgenOutput: BindgenOutput) {
return Array.from(bindgenOutput.js.matchAll(
/export (function|class) ([^({]+)[({]/g,
Expand Down
5 changes: 3 additions & 2 deletions lib/wasmbuild.generated.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
// @generated file from wasmbuild -- do not edit
// @ts-nocheck: generated
// deno-lint-ignore-file
// deno-fmt-ignore-file
// source-hash: 5bc24f8aa37e22f50b7574e0f9b7626c33201586
Expand Down Expand Up @@ -222,8 +223,8 @@ const imports = {
},
};

import { Loader } from "../loader.ts";
import { cacheToLocalDir } from "../cache.ts";
import { Loader } from "../loader/mod.js";
import { cacheToLocalDir } from "../loader/cache.ts";

const loader = new Loader({
imports,
Expand Down
21 changes: 15 additions & 6 deletions lib/wasmopt.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// Copyright 2018-2022 the Deno authors. All rights reserved. MIT license.
import { fetchWithRetries } from "../cache.ts";
import { fetchWithRetries } from "../loader/fetch.js";
import {
Buffer,
cacheDir,
Expand Down Expand Up @@ -99,11 +99,20 @@ async function downloadBinaryen(tempPath: string) {
}

function binaryenUrl() {
const os = {
"linux": "linux",
"darwin": "macos",
"windows": "windows",
}[Deno.build.os];
function getOs() {
switch (Deno.build.os) {
case "linux":
return "linux";
case "darwin":
return "macos";
case "windows":
return "windows";
default:
throw new Error("Unsupported OS");
}
}

const os = getOs();
const arch = {
"x86_64": "x86_64",
"aarch64": "arm64",
Expand Down
22 changes: 1 addition & 21 deletions cache.ts → loader/cache.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
// Copyright 2018-2022 the Deno authors. All rights reserved. MIT license.
import { default as localDataDir } from "https://deno.land/x/[email protected]/data_local_dir/mod.ts";
import { fetchWithRetries } from "./fetch.js";

export async function cacheToLocalDir(
url: URL,
Expand Down Expand Up @@ -134,24 +135,3 @@ function windowsToFileUrl(path: string): URL {
}
return url;
}

export async function fetchWithRetries(url: URL | string, maxRetries = 5) {
let sleepMs = 250;
let iterationCount = 0;
while (true) {
iterationCount++;
try {
const res = await fetch(url);
if (res.ok || iterationCount > maxRetries) {
return res;
}
} catch (err) {
if (iterationCount > maxRetries) {
throw err;
}
}
console.warn(`Failed fetching. Retrying in ${sleepMs}ms...`);
await new Promise((resolve) => setTimeout(resolve, sleepMs));
sleepMs = Math.min(sleepMs * 2, 10_000);
}
}
23 changes: 23 additions & 0 deletions loader/fetch.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
// Copyright 2018-2022 the Deno authors. All rights reserved. MIT license.

/** @param {URL | string} url */
export async function fetchWithRetries(url, maxRetries = 5) {
let sleepMs = 250;
let iterationCount = 0;
while (true) {
iterationCount++;
try {
const res = await fetch(url);
if (res.ok || iterationCount > maxRetries) {
return res;
}
} catch (err) {
if (iterationCount > maxRetries) {
throw err;
}
}
console.warn(`Failed fetching. Retrying in ${sleepMs}ms...`);
await new Promise((resolve) => setTimeout(resolve, sleepMs));
sleepMs = Math.min(sleepMs * 2, 10_000);
}
}
84 changes: 52 additions & 32 deletions loader.ts → loader/mod.js
Original file line number Diff line number Diff line change
@@ -1,46 +1,60 @@
// Copyright 2018-2022 the Deno authors. All rights reserved. MIT license.
import { fetchWithRetries } from "./cache.ts";
import { fetchWithRetries } from "./fetch.js";

export type DecompressCallback = (bytes: Uint8Array) => Uint8Array;
/**
* @callback DecompressCallback
* @param {Uint8Array} compressed
* @returns {Uint8Array} decompressed
*/

export interface LoaderOptions {
/** The Wasm module's imports. */
imports: WebAssembly.Imports | undefined;
/** A function that caches the Wasm module to a local path so that
* so that a network request isn't required on every load.
*
* Returns an ArrayBuffer with the bytes on download success, but
* cache save failure.
*/
cache?: (
url: URL,
decompress: DecompressCallback | undefined,
) => Promise<URL | Uint8Array>;
}
/**
* @callback CacheCallback
* @param {URL} url
* @param {DecompressCallback | undefined} decompress
* @returns {Promise<URL |Uint8Array>}
*/

/**
* @typedef LoaderOptions
* @property {WebAssembly.Imports | undefined} imports - The Wasm module's imports.
* @property {CacheCallback} [cache] - A function that caches the Wasm module to
* a local path so that a network request isn't required on every load.
*
* Returns an ArrayBuffer with the bytes on download success, but cache save failure.
*/

export class Loader {
#options: LoaderOptions;
#lastLoadPromise:
| Promise<WebAssembly.WebAssemblyInstantiatedSource>
| undefined;
#instantiated: WebAssembly.WebAssemblyInstantiatedSource | undefined;
/** @type {LoaderOptions} */
#options;
/** @type {Promise<WebAssembly.WebAssemblyInstantiatedSource> | undefined} */
#lastLoadPromise;
/** @type {WebAssembly.WebAssemblyInstantiatedSource | undefined} */
#instantiated;

constructor(options: LoaderOptions) {
/** @param {LoaderOptions} options */
constructor(options) {
this.#options = options;
}

/** @returns {WebAssembly.Instance | undefined} */
get instance() {
return this.#instantiated?.instance;
}

/** @returns {WebAssembly.Module | undefined} */
get module() {
return this.#instantiated?.module;
}

/**
* @param {URL} url
* @param {DecompressCallback | undefined} decompress
* @returns {Promise<WebAssembly.WebAssemblyInstantiatedSource>}
*/
load(
url: URL,
decompress: DecompressCallback | undefined,
): Promise<WebAssembly.WebAssemblyInstantiatedSource> {
url,
decompress,
) {
if (this.#instantiated) {
return Promise.resolve(this.#instantiated);
} else if (this.#lastLoadPromise == null) {
Expand All @@ -56,7 +70,11 @@ export class Loader {
return this.#lastLoadPromise;
}

async #instantiate(url: URL, decompress: DecompressCallback | undefined) {
/**
* @param {URL} url
* @param {DecompressCallback | undefined} decompress
*/
async #instantiate(url, decompress) {
const imports = this.#options.imports;
if (this.#options.cache != null && url.protocol !== "file:") {
try {
Expand All @@ -78,8 +96,8 @@ export class Loader {
const isFile = url.protocol === "file:";

// make file urls work in Node via dnt
// deno-lint-ignore no-explicit-any
const isNode = (globalThis as any).process?.versions?.node != null;
const isNode =
(/** @type {any} */ (globalThis)).process?.versions?.node != null;
if (isFile && typeof Deno !== "object") {
throw new Error(
"Loading local files are not supported in this environment",
Expand Down Expand Up @@ -108,10 +126,12 @@ export class Loader {
wasmResponse.headers.get("content-type")?.toLowerCase()
.startsWith("application/wasm")
) {
// Cast to any so there's no type checking issues with dnt
// (https://github.com/denoland/wasmbuild/issues/92)
// deno-lint-ignore no-explicit-any
return WebAssembly.instantiateStreaming(wasmResponse as any, imports);
return WebAssembly.instantiateStreaming(
// Cast to any so there's no type checking issues with dnt
// (https://github.com/denoland/wasmbuild/issues/92)
/** @type {any} */ (wasmResponse),
imports,
);
} else {
return WebAssembly.instantiate(
await wasmResponse.arrayBuffer(),
Expand Down
4 changes: 4 additions & 0 deletions tests/add.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
/**
* @param {number} a
* @param {number} b
*/
export function add(a, b) {
return a + b;
}
Loading

0 comments on commit 6f06f8d

Please sign in to comment.