mirror of
https://github.com/ruvnet/RuVector.git
synced 2026-05-29 19:33:34 +00:00
fix(ruvector): Node 22 LTS-compatible ONNX WASM loader (#323)
The bundled `pkg/ruvector_onnx_embeddings_wasm.js` is wasm-bindgen's
`--target bundler` output. Its first line is
import * as wasm from "./ruvector_onnx_embeddings_wasm_bg.wasm";
Node 22 LTS rejects static `.wasm` imports without
`--experimental-wasm-modules`, throwing
`Unknown file extension ".wasm"` and tearing down `initOnnxEmbedder()`
before any user code runs. Node 25+ accepts it natively, which masked
the regression.
Add a Node-friendly sibling entry that uses `fs.readFileSync` +
`WebAssembly.compile/instantiate` and routes the host imports through
the existing `_bg.js` (the import module name `./*_bg.js` was
verified by inspecting the .wasm import section). The bundler-target
file is left untouched so webpack/vite/rollup consumers and `--target
bundler`-style toolchains keep working.
`onnx-embedder.ts` now prefers the new `_node.mjs` entry when present
and falls back to the bundler entry, so older bundles stay loadable.
Verified on Node v22.22.2:
$ node test/onnx-node22-loader.test.mjs
ok: Node-friendly loader exists in src/core/onnx/pkg/
ok: loader produced a module namespace
ok: re-exports __wbg_set_wasm
ok: re-exports WasmEmbedder
ok: re-exports WasmEmbedderConfig
Node v22.22.2 ONNX loader smoke OK
Note: end-to-end `initOnnxEmbedder()` from a clean install also
requires the build script to copy `src/core/onnx/pkg/` into `dist/`
(tracked separately as #354 / #417). This PR is the loader fix only.
Closes #323
Co-Authored-By: claude-flow <ruv@ruv.net>
This commit is contained in:
parent
e383476014
commit
304a77259a
3 changed files with 109 additions and 8 deletions
|
|
@ -97,8 +97,11 @@ const DEFAULT_MODEL = 'all-MiniLM-L6-v2';
|
|||
*/
|
||||
export function isOnnxAvailable(): boolean {
|
||||
try {
|
||||
// Prefer the Node-friendly loader (#323); fall back to the bundler entry
|
||||
// for older bundles that don't ship the .mjs sibling yet.
|
||||
const nodePkgPath = path.join(__dirname, 'onnx', 'pkg', 'ruvector_onnx_embeddings_wasm_node.mjs');
|
||||
const pkgPath = path.join(__dirname, 'onnx', 'pkg', 'ruvector_onnx_embeddings_wasm.js');
|
||||
return fs.existsSync(pkgPath);
|
||||
return fs.existsSync(nodePkgPath) || fs.existsSync(pkgPath);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
|
|
@ -176,10 +179,16 @@ export async function initOnnxEmbedder(config: OnnxEmbedderConfig = {}): Promise
|
|||
|
||||
loadPromise = (async () => {
|
||||
try {
|
||||
// Paths to bundled ONNX files
|
||||
const pkgPath = path.join(__dirname, 'onnx', 'pkg', 'ruvector_onnx_embeddings_wasm.js');
|
||||
// Prefer the Node-friendly loader (resolves issue #323 — the
|
||||
// bundler-target index does `import * as wasm from "./*.wasm"`,
|
||||
// which Node 22 LTS rejects without --experimental-wasm-modules).
|
||||
// Fall back to the bundler entry only if the .mjs is absent (older
|
||||
// bundles), so existing installs aren't broken by this change.
|
||||
const nodePkgPath = path.join(__dirname, 'onnx', 'pkg', 'ruvector_onnx_embeddings_wasm_node.mjs');
|
||||
const bundlerPkgPath = path.join(__dirname, 'onnx', 'pkg', 'ruvector_onnx_embeddings_wasm.js');
|
||||
const loaderPath = path.join(__dirname, 'onnx', 'loader.js');
|
||||
|
||||
const pkgPath = fs.existsSync(nodePkgPath) ? nodePkgPath : bundlerPkgPath;
|
||||
if (!fs.existsSync(pkgPath)) {
|
||||
throw new Error('ONNX WASM files not bundled. The onnx/ directory is missing.');
|
||||
}
|
||||
|
|
@ -188,13 +197,14 @@ export async function initOnnxEmbedder(config: OnnxEmbedderConfig = {}): Promise
|
|||
const pkgUrl = pathToFileURL(pkgPath).href;
|
||||
const loaderUrl = pathToFileURL(loaderPath).href;
|
||||
|
||||
// Dynamic import of bundled modules using file:// URLs
|
||||
// Dynamic import of bundled modules using file:// URLs.
|
||||
// The Node loader instantiates the WASM at module-load time via
|
||||
// fs.readFileSync, so no separate default() init is needed.
|
||||
wasmModule = await dynamicImport(pkgUrl);
|
||||
|
||||
// Initialize WASM module (loads the .wasm file)
|
||||
const wasmPath = path.join(__dirname, 'onnx', 'pkg', 'ruvector_onnx_embeddings_wasm_bg.wasm');
|
||||
if (wasmModule.default && typeof wasmModule.default === 'function') {
|
||||
// For bundler-style initialization, pass the wasm buffer
|
||||
// Legacy bundler-target path still needs an explicit init().
|
||||
if (pkgPath === bundlerPkgPath && wasmModule.default && typeof wasmModule.default === 'function') {
|
||||
const wasmPath = path.join(__dirname, 'onnx', 'pkg', 'ruvector_onnx_embeddings_wasm_bg.wasm');
|
||||
const wasmBytes = fs.readFileSync(wasmPath);
|
||||
await wasmModule.default(wasmBytes);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,37 @@
|
|||
// Node-friendly entry for the wasm-bindgen output (regression guard for #323).
|
||||
//
|
||||
// Background: the autogenerated `ruvector_onnx_embeddings_wasm.js` is the
|
||||
// `--target bundler` output, whose first line is
|
||||
// import * as wasm from "./ruvector_onnx_embeddings_wasm_bg.wasm";
|
||||
// Node 22 LTS rejects static `.wasm` imports without
|
||||
// `--experimental-wasm-modules`, throwing `Unknown file extension ".wasm"`.
|
||||
// This file replaces that load path with `fs.readFileSync` +
|
||||
// `WebAssembly.instantiate`, which works on Node 18+ unchanged.
|
||||
//
|
||||
// The bundler-target file is left alone so browser/webpack/vite consumers
|
||||
// keep working with their existing toolchains.
|
||||
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { dirname, join } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
import * as bg from './ruvector_onnx_embeddings_wasm_bg.js';
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
const wasmPath = join(__dirname, 'ruvector_onnx_embeddings_wasm_bg.wasm');
|
||||
|
||||
const bytes = readFileSync(wasmPath);
|
||||
const compiled = await WebAssembly.compile(bytes);
|
||||
|
||||
// wasm-bindgen labels the host-import module by the bg.js path; keep this
|
||||
// key in sync if the binary is ever regenerated with a different name.
|
||||
const instance = await WebAssembly.instantiate(compiled, {
|
||||
'./ruvector_onnx_embeddings_wasm_bg.js': bg,
|
||||
});
|
||||
|
||||
bg.__wbg_set_wasm(instance.exports);
|
||||
if (typeof instance.exports.__wbindgen_start === 'function') {
|
||||
instance.exports.__wbindgen_start();
|
||||
}
|
||||
|
||||
export * from './ruvector_onnx_embeddings_wasm_bg.js';
|
||||
54
npm/packages/ruvector/test/onnx-node22-loader.test.mjs
Normal file
54
npm/packages/ruvector/test/onnx-node22-loader.test.mjs
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
// Regression test for issue #323 — ONNX embedder failed on Node 22 LTS
|
||||
// because `pkg/ruvector_onnx_embeddings_wasm.js` (the wasm-bindgen
|
||||
// `--target bundler` output) does `import * as wasm from "./*.wasm"`,
|
||||
// which Node 22 rejects without `--experimental-wasm-modules`.
|
||||
//
|
||||
// This test loads the new Node-friendly entry that uses
|
||||
// `fs.readFileSync` + `WebAssembly.instantiate` and asserts the bindings
|
||||
// surface looks healthy. Run with:
|
||||
//
|
||||
// node test/onnx-node22-loader.test.mjs
|
||||
|
||||
import { existsSync } from 'node:fs';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { dirname, join } from 'node:path';
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
const repoRoot = join(__dirname, '..');
|
||||
const nodeLoader = join(repoRoot, 'src/core/onnx/pkg/ruvector_onnx_embeddings_wasm_node.mjs');
|
||||
|
||||
let failures = 0;
|
||||
function check(cond, msg) {
|
||||
if (!cond) {
|
||||
console.error('FAIL:', msg);
|
||||
failures++;
|
||||
} else {
|
||||
console.log(' ok:', msg);
|
||||
}
|
||||
}
|
||||
|
||||
console.log('Node version:', process.version);
|
||||
console.log('Loader path:', nodeLoader);
|
||||
|
||||
check(existsSync(nodeLoader), 'Node-friendly loader exists in src/core/onnx/pkg/');
|
||||
|
||||
// The actual smoke: dynamically import the loader. If Node still rejected
|
||||
// the .wasm static import this would throw `Unknown file extension ".wasm"`
|
||||
// and the test would fail.
|
||||
const mod = await import(nodeLoader);
|
||||
|
||||
check(typeof mod === 'object', 'loader produced a module namespace');
|
||||
|
||||
// wasm-bindgen `--target bundler` re-exports everything via _bg.js, so the
|
||||
// loader should re-export the same surface. Pick a couple of well-known
|
||||
// wasm-bindgen runtime exports as a sanity check.
|
||||
const expectedSymbols = ['__wbg_set_wasm', 'WasmEmbedder', 'WasmEmbedderConfig'];
|
||||
for (const sym of expectedSymbols) {
|
||||
check(typeof mod[sym] !== 'undefined', `re-exports ${sym}`);
|
||||
}
|
||||
|
||||
if (failures > 0) {
|
||||
console.error(`\n${failures} check(s) failed`);
|
||||
process.exit(1);
|
||||
}
|
||||
console.log(`\nNode ${process.version} ONNX loader smoke OK`);
|
||||
Loading…
Add table
Add a link
Reference in a new issue