diff --git a/.github/workflows/build-rvf-node.yml b/.github/workflows/build-rvf-node.yml new file mode 100644 index 00000000..f69d2fc0 --- /dev/null +++ b/.github/workflows/build-rvf-node.yml @@ -0,0 +1,187 @@ +name: Build RVF Node Native Modules + +on: + push: + branches: [main] + paths: + - 'crates/rvf/rvf-node/**' + - 'crates/rvf/rvf-runtime/**' + - 'npm/packages/rvf-node/**' + pull_request: + branches: [main] + paths: + - 'crates/rvf/rvf-node/**' + - 'crates/rvf/rvf-runtime/**' + - 'npm/packages/rvf-node/**' + workflow_dispatch: + +env: + CARGO_TERM_COLOR: always + +jobs: + build: + strategy: + fail-fast: false + matrix: + settings: + - host: ubuntu-22.04 + target: x86_64-unknown-linux-gnu + platform: linux-x64-gnu + - host: ubuntu-22.04 + target: aarch64-unknown-linux-gnu + platform: linux-arm64-gnu + - host: macos-14 + target: x86_64-apple-darwin + platform: darwin-x64 + - host: macos-14 + target: aarch64-apple-darwin + platform: darwin-arm64 + - host: windows-2022 + target: x86_64-pc-windows-msvc + platform: win32-x64-msvc + + name: Build ${{ matrix.settings.platform }} + runs-on: ${{ matrix.settings.host }} + + steps: + - uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '18' + + - name: Setup Rust + uses: dtolnay/rust-toolchain@stable + with: + toolchain: stable + targets: ${{ matrix.settings.target }} + + - name: Cache Rust + uses: Swatinem/rust-cache@v2 + with: + key: rvf-node-${{ matrix.settings.target }} + + - name: Install cross-compilation tools (Linux ARM64) + if: matrix.settings.platform == 'linux-arm64-gnu' + run: | + sudo apt-get update + sudo apt-get install -y gcc-aarch64-linux-gnu g++-aarch64-linux-gnu + + - name: Install NAPI-RS CLI + run: npm install -g @napi-rs/cli@^2.18.0 + + - name: Build rvf-node native module + working-directory: crates/rvf/rvf-node + run: napi build --platform --release --target ${{ matrix.settings.target }} + env: + CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER: aarch64-linux-gnu-gcc + + - name: List built artifacts + shell: bash + run: | + echo "=== Built .node files ===" + find crates/rvf/rvf-node -name "*.node" -type f -exec ls -lh {} \; + + - name: Copy binary to platform package + shell: bash + run: | + SRC=$(find crates/rvf/rvf-node -name "rvf-node.*.node" -type f | head -1) + FNAME=$(basename "$SRC") + + # Copy to npm platform package + PLAT_DIR="crates/rvf/rvf-node/npm/${{ matrix.settings.platform }}" + mkdir -p "$PLAT_DIR" + cp -v "$SRC" "$PLAT_DIR/$FNAME" + + # Copy to main rvf-node package + cp -v "$SRC" "npm/packages/rvf-node/$FNAME" + + echo "=== Platform package ===" + ls -lh "$PLAT_DIR/" + echo "=== Main package ===" + ls -lh "npm/packages/rvf-node/$FNAME" + + - name: Test native module (native platform only) + if: | + (matrix.settings.platform == 'linux-x64-gnu') || + (matrix.settings.platform == 'darwin-arm64' && runner.arch == 'ARM64') || + (matrix.settings.platform == 'darwin-x64' && runner.arch == 'X64') || + (matrix.settings.platform == 'win32-x64-msvc' && runner.os == 'Windows') + continue-on-error: true + shell: bash + run: | + NODE_FILE=$(find crates/rvf/rvf-node -name "rvf-node.*.node" -type f | head -1) + if [ -f "$NODE_FILE" ]; then + echo "Testing: $NODE_FILE" + node -e " + const m = require('./$NODE_FILE'); + console.log('Exports:', Object.keys(m)); + if (m.RvfDatabase) { + console.log('RvfDatabase methods:', Object.getOwnPropertyNames(m.RvfDatabase.prototype || {})); + console.log('RvfDatabase static:', Object.keys(m.RvfDatabase)); + } + console.log('OK'); + " + fi + + - name: Upload artifact + uses: actions/upload-artifact@v4 + with: + name: rvf-node-${{ matrix.settings.platform }} + path: | + crates/rvf/rvf-node/npm/${{ matrix.settings.platform }}/rvf-node.*.node + if-no-files-found: error + + commit-binaries: + name: Commit RVF Node Binaries + runs-on: ubuntu-22.04 + needs: build + if: github.event_name == 'push' && github.ref == 'refs/heads/main' + permissions: + contents: write + + steps: + - uses: actions/checkout@v4 + with: + ref: main + + - name: Download all artifacts + uses: actions/download-artifact@v4 + with: + path: artifacts + pattern: rvf-node-* + + - name: Copy binaries to packages + run: | + for dir in artifacts/rvf-node-*/; do + platform=$(basename "$dir" | sed 's/rvf-node-//') + mkdir -p "crates/rvf/rvf-node/npm/${platform}" + cp -v "$dir"/rvf-node.*.node "crates/rvf/rvf-node/npm/${platform}/" + cp -v "$dir"/rvf-node.*.node "npm/packages/rvf-node/" + done + + - name: Show binary sizes + run: | + echo "=== RVF Node Binaries ===" + find crates/rvf/rvf-node/npm -name "*.node" -exec ls -lh {} \; + echo "=== Main package ===" + find npm/packages/rvf-node -name "*.node" -exec ls -lh {} \; + + - name: Commit and push + run: | + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + git add -f crates/rvf/rvf-node/npm/ npm/packages/rvf-node/*.node + if git diff --staged --quiet; then + echo "No changes to commit" + else + git commit -m "chore: Update RVF NAPI-RS binaries for all platforms + + Built from commit ${{ github.sha }} + + Platforms: linux-x64-gnu, linux-arm64-gnu, darwin-x64, darwin-arm64, win32-x64-msvc + + Co-Authored-By: claude-flow " + git push + fi diff --git a/.github/workflows/wasm-dedup-check.yml b/.github/workflows/wasm-dedup-check.yml index 8f54a8e1..0d2a4545 100644 --- a/.github/workflows/wasm-dedup-check.yml +++ b/.github/workflows/wasm-dedup-check.yml @@ -12,8 +12,10 @@ jobs: - uses: actions/setup-node@v4 with: node-version: 20 - - run: npm install + - run: npm install --ignore-scripts --omit=optional 2>&1 || true working-directory: npm + env: + npm_config_optional: false - name: Check for duplicate WASM artifacts run: | count=$(find node_modules -name "rvf_wasm_bg.wasm" 2>/dev/null | wc -l) diff --git a/npm/packages/rvf-wasm/package.json b/npm/packages/rvf-wasm/package.json index 4af973be..9cc2dcd4 100644 --- a/npm/packages/rvf-wasm/package.json +++ b/npm/packages/rvf-wasm/package.json @@ -1,10 +1,17 @@ { "name": "@ruvector/rvf-wasm", - "version": "0.1.4", + "version": "0.1.5", "description": "RuVector Format WASM microkernel for browser and edge vector operations", - "type": "module", "main": "pkg/rvf_wasm.js", "types": "pkg/rvf_wasm.d.ts", + "exports": { + ".": { + "types": "./pkg/rvf_wasm.d.ts", + "import": "./pkg/rvf_wasm.mjs", + "require": "./pkg/rvf_wasm.js", + "default": "./pkg/rvf_wasm.js" + } + }, "files": [ "pkg/" ], @@ -15,5 +22,9 @@ "repository": { "type": "git", "url": "https://github.com/ruvnet/ruvector" + }, + "publishConfig": { + "registry": "https://registry.npmjs.org/", + "access": "public" } } diff --git a/npm/packages/rvf-wasm/pkg/rvf_wasm.js b/npm/packages/rvf-wasm/pkg/rvf_wasm.js index 3afd12da..61e589a2 100644 --- a/npm/packages/rvf-wasm/pkg/rvf_wasm.js +++ b/npm/packages/rvf-wasm/pkg/rvf_wasm.js @@ -3,46 +3,70 @@ * * Loads the .wasm binary and re-exports all C-ABI functions plus the * WASM linear memory object. + * + * Works in Node.js (CJS/ESM) and browsers. */ -let wasmInstance = null; +var wasmInstance = null; + +var _isNode = typeof process !== 'undefined' && + typeof process.versions !== 'undefined' && + typeof process.versions.node === 'string'; /** * Initialize the WASM module. * Returns the exports object with all rvf_* functions and `memory`. + * + * @param {ArrayBuffer|BufferSource|WebAssembly.Module|string} [input] + * Optional pre-loaded bytes, Module, or file path override. */ -export default async function init(input) { +async function init(input) { if (wasmInstance) return wasmInstance; - let wasmBytes; + var wasmBytes; if (input instanceof ArrayBuffer || ArrayBuffer.isView(input)) { wasmBytes = input; - } else if (input instanceof WebAssembly.Module) { - const instance = await WebAssembly.instantiate(input, {}); - wasmInstance = instance.exports; + } else if (typeof WebAssembly !== 'undefined' && input instanceof WebAssembly.Module) { + var inst = await WebAssembly.instantiate(input, {}); + wasmInstance = inst.exports; return wasmInstance; - } else { - // Default: load from adjacent .wasm file - const url = new URL('rvf_wasm_bg.wasm', import.meta.url); - if (typeof fetch === 'function') { - const resp = await fetch(url); - if (typeof WebAssembly.instantiateStreaming === 'function') { - const { instance } = await WebAssembly.instantiateStreaming(resp, {}); - wasmInstance = instance.exports; - return wasmInstance; - } - wasmBytes = await resp.arrayBuffer(); + } else if (_isNode) { + // Node.js: always use readFile (fetch on file:// is unreliable) + var fs = await import('node:fs/promises'); + var url = await import('node:url'); + var path = await import('node:path'); + var wasmPath; + if (typeof input === 'string') { + wasmPath = input; + } else if (typeof __dirname !== 'undefined') { + // CJS context + wasmPath = path.default.join(__dirname, 'rvf_wasm_bg.wasm'); } else { - // Node.js fallback - const { readFile } = await import('node:fs/promises'); - const { fileURLToPath } = await import('node:url'); - const path = fileURLToPath(url); - wasmBytes = await readFile(path); + // ESM context — import.meta.url available + var thisDir = path.default.dirname(url.default.fileURLToPath(import.meta.url)); + wasmPath = path.default.join(thisDir, 'rvf_wasm_bg.wasm'); } + wasmBytes = await fs.default.readFile(wasmPath); + } else { + // Browser: use fetch + instantiateStreaming + var wasmUrl = new URL('rvf_wasm_bg.wasm', import.meta.url); + if (typeof WebAssembly.instantiateStreaming === 'function') { + var resp = await fetch(wasmUrl); + var result = await WebAssembly.instantiateStreaming(resp, {}); + wasmInstance = result.instance.exports; + return wasmInstance; + } + var resp2 = await fetch(wasmUrl); + wasmBytes = await resp2.arrayBuffer(); } - const { instance } = await WebAssembly.instantiate(wasmBytes, {}); - wasmInstance = instance.exports; + var compiled = await WebAssembly.instantiate(wasmBytes, {}); + wasmInstance = compiled.instance.exports; return wasmInstance; } + +// Support both ESM (export default) and CJS (module.exports) +init.default = init; +if (typeof module !== 'undefined') module.exports = init; +export default init; diff --git a/npm/packages/rvf-wasm/pkg/rvf_wasm.mjs b/npm/packages/rvf-wasm/pkg/rvf_wasm.mjs new file mode 100644 index 00000000..d62265eb --- /dev/null +++ b/npm/packages/rvf-wasm/pkg/rvf_wasm.mjs @@ -0,0 +1,6 @@ +/** + * @ruvector/rvf-wasm ESM entry point. + * Re-exports the init function from the CJS-compatible module. + */ +import init from './rvf_wasm.js'; +export default init;