From 7b431942c077bc33793103f8dcdbc55ccef5266c Mon Sep 17 00:00:00 2001 From: Ben Papillon Date: Fri, 22 May 2026 12:08:02 -0700 Subject: [PATCH 1/2] inline rules-engine WASM binary at build time MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The wasm-bindgen-generated `dist/wasm/rulesengine.js` loads its `.wasm` sibling at runtime via `fs.readFileSync(${__dirname}/rulesengine_bg.wasm)`. That works in plain Node, but breaks the moment a downstream bundler follows the require chain — webpack rewrites `__dirname` to point inside the bundle output, where the `.wasm` sibling never gets copied. Symptom in a Next.js consumer: ENOENT: no such file or directory, open '.next/dev/server/vendor-chunks/rulesengine_bg.wasm' …and the SDK silently falls back to API-only checks, disabling DataStream and credit-lease paths. This adds a build step that reads the WASM binary, base64-encodes it, and rewrites `dist/wasm/rulesengine.js` to instantiate from an inlined `Buffer.from(BASE64, 'base64')` instead of touching the filesystem. The standalone `.wasm` is then removed from `dist/` since nothing reads it at runtime anymore. Tarball delta is +138 KB (base64 overhead on the ~414 KB binary). For consumers that use creditLeases / DataStream the net bundle size is unchanged — the WASM bytes are in either form. For consumers that don't, practical tree-shaking ends up the same regardless of inlining: the require chain is reachable from the package's main entry point and the WASM init runs as a module-level side effect. Materially reducing the WASM cost for non-credit-lease consumers would require splitting credit leases into a separate entry point — out of scope here. If wasm-bindgen output ever stops matching the regex this script keys on, the build throws with a clear pointer instead of silently shipping a broken loader. --- build.js | 55 ++++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 54 insertions(+), 1 deletion(-) diff --git a/build.js b/build.js index 7ff25664..d839279b 100644 --- a/build.js +++ b/build.js @@ -1,7 +1,7 @@ // build.js const esbuild = require('esbuild'); const { execSync } = require('child_process'); -const { cpSync, mkdirSync, existsSync } = require('fs'); +const { cpSync, mkdirSync, existsSync, readFileSync, writeFileSync, rmSync } = require('fs'); const sharedConfig = { entryPoints: ['src/index.ts'], @@ -52,6 +52,8 @@ async function build() { } console.log('✅ WASM artifacts copied to dist/wasm/'); + inlineWasmBinary(); + // Generate TypeScript declarations with tsc console.log('🔧 Generating TypeScript declarations...'); execSync('tsc --emitDeclarationOnly --outDir dist', { stdio: 'inherit' }); @@ -65,4 +67,55 @@ async function build() { } } +// Inline the WASM binary into dist/wasm/rulesengine.js so the runtime no +// longer reads it off disk via `fs.readFileSync(${__dirname}/...)`. +// +// The wasm-bindgen-generated loader resolves the .wasm file relative to +// its own `__dirname`, which breaks the moment a downstream bundler +// (webpack, Next.js, Vite, etc.) follows the require chain and rewrites +// `__dirname` to point inside the bundle output — there's no `.wasm` +// sibling there, so initialization fails with a misleading ENOENT. +// See the linked Next.js failure mode at +// .next/dev/server/vendor-chunks/rulesengine_bg.wasm → ENOENT. +// +// Inlining the bytes as a base64 string sidesteps the whole class of +// `__dirname`-aware loaders. Costs ~+138 KB on the tarball (550 KB +// base64 string in JS replaces a 414 KB .wasm + the loader stub) but +// makes the SDK bundler-agnostic for free. We delete the standalone +// `.wasm` after rewriting since nothing reads it at runtime anymore. +function inlineWasmBinary() { + const loaderPath = 'dist/wasm/rulesengine.js'; + const binaryPath = 'dist/wasm/rulesengine_bg.wasm'; + if (!existsSync(loaderPath) || !existsSync(binaryPath)) { + console.warn('⚠️ Skipping WASM inlining — files not found:', { loaderPath, binaryPath }); + return; + } + + const wasmBase64 = readFileSync(binaryPath).toString('base64'); + const loaderSource = readFileSync(loaderPath, 'utf8'); + + // Match the wasm-bindgen-emitted block that reads the binary off disk. + // Captures any whitespace/comments wasm-bindgen emits between the two + // statements so future loader updates don't silently bypass this step. + const loaderPattern = /const wasmPath = `\$\{__dirname\}\/rulesengine_bg\.wasm`;\s*\nconst wasmBytes = require\('fs'\)\.readFileSync\(wasmPath\);/; + if (!loaderPattern.test(loaderSource)) { + throw new Error( + 'WASM inlining failed: expected `const wasmPath = `${__dirname}/...`; const wasmBytes = require(\'fs\').readFileSync(wasmPath);` in ' + + loaderPath + + '. wasm-bindgen output shape changed — update inlineWasmBinary() to match.' + ); + } + + const inlined = loaderSource.replace( + loaderPattern, + `// WASM binary inlined at build time (see build.js inlineWasmBinary).\nconst wasmBytes = Buffer.from('${wasmBase64}', 'base64');` + ); + writeFileSync(loaderPath, inlined); + + // Standalone .wasm is dead weight once the bytes are embedded. Drop + // it so we don't ship two copies of the binary. + rmSync(binaryPath); + console.log(`✅ WASM inlined into ${loaderPath} (${wasmBase64.length} base64 chars, .wasm removed)`); +} + build(); From 4196c8544180598916707a3681daa1c2a84270b1 Mon Sep 17 00:00:00 2001 From: Ben Papillon Date: Fri, 22 May 2026 12:14:03 -0700 Subject: [PATCH 2/2] update verify-package CI check for inlined WASM The verify-package step asserted on `dist/wasm/rulesengine_bg.wasm` presence, which the inlining step now intentionally removes. Replace those checks with a content-sentinel grep on the inlined comment so a future change that silently reverts to disk-based loading still fails CI, plus an explicit assertion that the standalone .wasm is gone. --- .github/workflows/ci.yml | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4a1e8091..4eb6961f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -85,16 +85,25 @@ jobs: echo "ERROR: dist/index.js not found" exit 1 fi - if [ ! -f dist/wasm/rulesengine_bg.wasm ]; then - echo "ERROR: dist/wasm/rulesengine_bg.wasm not found" - echo "The build may have failed to copy WASM artifacts" - exit 1 - fi if [ ! -f dist/wasm/rulesengine.js ]; then echo "ERROR: dist/wasm/rulesengine.js not found" exit 1 fi - echo "Verified: WASM artifacts present in dist/wasm/" + # The .wasm binary is base64-inlined into rulesengine.js at build + # time (see build.js inlineWasmBinary), so the standalone .wasm + # is intentionally absent from dist/. Verify the inlining sentinel + # is present instead — guards against a future build change that + # silently reverts to the disk-loaded path. + if ! grep -q "WASM binary inlined at build time" dist/wasm/rulesengine.js; then + echo "ERROR: dist/wasm/rulesengine.js does not contain the inlined WASM sentinel" + echo "The build may have failed to run inlineWasmBinary()" + exit 1 + fi + if [ -f dist/wasm/rulesengine_bg.wasm ]; then + echo "ERROR: dist/wasm/rulesengine_bg.wasm should have been removed by inlineWasmBinary()" + exit 1 + fi + echo "Verified: WASM inlined into dist/wasm/rulesengine.js" publish: needs: [ compile, test, verify-package ]