Skip to content
Open
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
26 changes: 18 additions & 8 deletions npm/packages/ruvector/src/core/onnx-embedder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down Expand Up @@ -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.');
}
Expand All @@ -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);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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 changes: 54 additions & 0 deletions npm/packages/ruvector/test/onnx-node22-loader.test.mjs
Original file line number Diff line number Diff line change
@@ -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