Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
81 changes: 58 additions & 23 deletions crates/loro-wasm/scripts/bundler_patch.js
Original file line number Diff line number Diff line change
@@ -1,34 +1,69 @@
// See https://github.com/loro-dev/loro/issues/440
// Without this patch, Cloudflare Worker would raise issue like: "Uncaught TypeError: wasm2.__wbindgen_start is not a function"
import * as wasm from "./loro_wasm_bg.wasm";
import * as imports from "./loro_wasm_bg.js";
import * as rawWasm from './loro_wasm_bg.wasm';
import * as imports from './loro_wasm_bg.js';

if (wasm.__wbindgen_start) {
imports.__wbg_set_wasm(wasm);
wasm.__wbindgen_start();
} else if ("Bun" in globalThis) {
const { instance } = await WebAssembly.instantiateStreaming(
fetch(Bun.pathToFileURL(wasm.default)),
{
"./loro_wasm_bg.js": imports,
},
);
imports.__wbg_set_wasm(instance.exports);
// Normalize how bundlers expose the wasm module/exports.
const toModuleOrExports = (wasm) => {
if (!wasm) return wasm;
if (wasm instanceof WebAssembly.Module) return wasm;
if (typeof wasm === 'object' && 'default' in wasm) {
return wasm.default ?? wasm;
}
// rsbuild doesn't provide a default export when importing wasm.
return wasm;
};

const wasmModuleOrExports = toModuleOrExports(rawWasm);

// Helper: ensure we end up with exports + optionally run externref init.
const finalize = (exports) => {
imports.__wbg_set_wasm(exports);
if (typeof imports.__wbindgen_init_externref_table === 'function') {
imports.__wbindgen_init_externref_table();
}
};

if (wasmModuleOrExports && wasmModuleOrExports.__wbindgen_start) {
// See https://github.com/loro-dev/loro/issues/440
// Without this patch, Cloudflare Worker would raise issue like: "Uncaught TypeError: wasm2.__wbindgen_start is not a function"
// Already the initialized exports object (Cloudflare Workers path).
finalize(wasmModuleOrExports);
wasmModuleOrExports.__wbindgen_start();
} else if ('Bun' in globalThis) {
// Bun's wasm runtime (1.3.0 as of Oct 2025) sometimes reads externref slot 1
// (reserved for booleans by wasm-bindgen) as the global object, causing APIs
// like `LoroText.toDelta()` to return cyclic structures. Re-running the
// wasm-bindgen externref table initializer after instantiation resets the
// table so booleans stay primitives and avoids the infinite recursion seen in
// Bun tests during `pnpm release-wasm`.
if (typeof imports.__wbindgen_init_externref_table === "function") {
imports.__wbindgen_init_externref_table();
let instance;
if (wasmModuleOrExports instanceof WebAssembly.Module) {
({ instance } = await WebAssembly.instantiate(wasmModuleOrExports, {
'./loro_wasm_bg.js': imports,
}));
} else {
const url = Bun.pathToFileURL(wasmModuleOrExports);
({ instance } = await WebAssembly.instantiateStreaming(fetch(url), {
'./loro_wasm_bg.js': imports,
}));
}
finalize(instance.exports);
} else {
const wkmod = await import("./loro_wasm_bg.wasm");
const instance = new WebAssembly.Instance(wkmod.default, {
"./loro_wasm_bg.js": imports,
});
imports.__wbg_set_wasm(instance.exports);
// Browser/node-like bundlers: either we already have exports, or a Module/URL.
const wkmod =
wasmModuleOrExports instanceof WebAssembly.Module
? wasmModuleOrExports
: await import('./loro_wasm_bg.wasm');
const module =
wkmod instanceof WebAssembly.Module
? wkmod
: (wkmod && wkmod.default) || wkmod;
const instance =
module instanceof WebAssembly.Instance
? module
: new WebAssembly.Instance(module, {
'./loro_wasm_bg.js': imports,
});
finalize(instance.exports ?? instance);
}
export * from "./loro_wasm_bg.js";

export * from './loro_wasm_bg.js';
57 changes: 26 additions & 31 deletions crates/loro-wasm/scripts/post-rollup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,45 +73,40 @@ async function rollupBase64() {

const base64IndexPath = "./base64/index.js";
const content = await Deno.readTextFile(base64IndexPath);
const nextContent = injectBase64WasmBranch(content, base64IndexPath);
await Deno.writeTextFile(base64IndexPath, nextContent);

const legacyPattern =
/\{\s*const wkmod = await import\('\.\/loro_wasm_bg-([^']+)\.js'\);\s*const instance = new WebAssembly\.Instance\(wkmod\.default, \{\s*"\.\/loro_wasm_bg\.js": imports,\s*\}\);\s*__wbg_set_wasm\(instance\.exports\);\s*\}/;
const legacyReplacement = (match: string, hash: string) => `
import loro_wasm_bg_js from './loro_wasm_bg-${hash}.js';
const instance = new WebAssembly.Instance(loro_wasm_bg_js(), {
"./loro_wasm_bg.js": imports,
});
__wbg_set_wasm(instance.exports);
`;
await Deno.copyFile("./bundler/loro_wasm.d.ts", "./base64/loro_wasm.d.ts");
}

const modernPattern =
/const wkmod = await Promise\.resolve\(\)\.then\(function \(\) { return wasm\$1; }\);\s*const instance = new WebAssembly\.Instance\(wkmod\.default, \{\s*"\.\/loro_wasm_bg\.js": imports,\s*\}\);\s*__wbg_set_wasm\(instance\.exports\);\s*/;
const modernReplacement = () => `const instance = loro_wasm_bg({
"./loro_wasm_bg.js": imports,
});
__wbg_set_wasm(instance.exports);
`;

let nextContent = content;
let replaced = false;

if (legacyPattern.test(nextContent)) {
nextContent = nextContent.replace(legacyPattern, legacyReplacement);
replaced = true;
} else if (modernPattern.test(nextContent)) {
nextContent = nextContent.replace(modernPattern, modernReplacement);
replaced = true;
function injectBase64WasmBranch(content: string, filePath: string): string {
const alreadyPatched =
content.includes("typeof wasmModuleOrExports === \"function\"");
if (alreadyPatched) {
return content;
}

if (!replaced) {
const bunBranchPattern = /}\s*else if\s*\(\s*(['"])Bun\1\s+in\s+globalThis\s*\)\s*\{/;
if (!bunBranchPattern.test(content)) {
throw new Error(
`Could not find string to replace in ${base64IndexPath}`,
`Could not locate Bun branch while patching ${filePath}`,
);
}

await Deno.writeTextFile(base64IndexPath, nextContent);

await Deno.copyFile("./bundler/loro_wasm.d.ts", "./base64/loro_wasm.d.ts");
const base64Branch = `} else if (typeof wasmModuleOrExports === "function") {
const moduleOrInstance = wasmModuleOrExports({
"./loro_wasm_bg.js": imports,
});
const instance =
moduleOrInstance instanceof WebAssembly.Instance
? moduleOrInstance
: new WebAssembly.Instance(moduleOrInstance, {
"./loro_wasm_bg.js": imports,
});
finalize(instance.exports ?? instance);
} else if ("Bun" in globalThis) {`;

return content.replace(bunBranchPattern, base64Branch);
}

async function main() {
Expand Down
1 change: 1 addition & 0 deletions examples/test-rsbuild/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
dist/
23 changes: 23 additions & 0 deletions examples/test-rsbuild/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
{
"name": "loro-test-rsbuild",
"version": "0.0.0",
"private": true,
"type": "module",
"scripts": {
"dev": "rsbuild dev",
"build": "rsbuild build",
"preview": "rsbuild preview"
},
"dependencies": {
"loro-crdt": "link:../../crates/loro-wasm",
"react": "^18.3.1",
"react-dom": "^18.3.1"
},
"devDependencies": {
"@rsbuild/core": "^1.0.0",
"@rsbuild/plugin-react": "^1.0.0",
"@types/react": "^18.3.3",
"@types/react-dom": "^18.3.0",
"typescript": "^5.6.3"
}
}
Loading
Loading