From bc38965088cf87e1e3f0ce1b84e558ea8e755835 Mon Sep 17 00:00:00 2001 From: Diego Rodrigues de Sa e Souza Date: Sun, 9 Aug 2026 09:54:24 -0300 Subject: [PATCH] maint: follow-up cherry-pick fix-in-place #9712 (conflict-resolved fallback) (#9892) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix(build): colocateLlmlinguaOptionals skip-check treated a Next-traced stub as fully copied Debugging the omniroute-beta Docker rebuild: `npm run build` (and the Dockerfile's own post-build verification) failed with `Cannot find module '.../node_modules/@atjsh/llmlingua-2/dist/index.js'`. Root cause, reproduced directly (both against a live Docker builder image and in a unit test): Next.js's own standalone trace creates a stub directory for `@atjsh/llmlingua-2` containing only `package.json` — it references the package (a dynamically-imported optional dependency) but can't fully bundle it. colocateLlmlinguaOptionals's skip checks (both the closure-level early return and the per-package loop) only tested `existsSync(dest)`, so that stub was indistinguishable from "already fully co-located" — the function skipped copying the real `dist/` output entirely, silently shipping a package with a manifest but no code. Fix: check for the package's declared `main` entry file when it has one (the real-world case for every actual SLM optional). Packages with no `main` field fall back to comparing the destination's top-level entries against the source's — correct both for genuinely multi-file packages and for a metadata-only source (package.json is then its complete, faithfully- copied contents), which the existing idempotency test exercises. Covered by tests/unit/colocate-optionals.test.ts's new stub-reproduction case (fails against the pre-fix code, passes after — confirmed directly) plus the 6 pre-existing cases, all still green. (cherry picked from commit 359aba59c7b362a5efaa4f0cd4d482d48ee2df66) * fix(build): register onnxruntime-node's native bin/ as a standalone asset (#9687) Docker/standalone builds of the LLMLingua SLM compression tier failed at runtime with "Error: libonnxruntime.so.1: cannot open shared object file: No such file or directory" (open-sse/services/compression/engines/llmlingua's worker, via @huggingface/transformers -> onnxruntime-node). onnxruntime-node's dist/binding.js is a normal JS file Next.js's standalone trace bundles correctly, but binding.js dlopen()s a platform-specific native library shipped under bin/napi-v3///libonnxruntime.so.1 — a dynamic native load static file tracing can't see (same blind-spot class as the separate colocateLlmlinguaOptionals stub bug, just for a .so instead of a JS import, via NATIVE_ASSET_ENTRIES instead). That directory was simply never registered, unlike better-sqlite3's native binary, which already goes through the exact same mechanism correctly. Fix: add an entry for onnxruntime-node/bin, mirroring the existing better-sqlite3 entry. Confirmed against a real Docker build of the Dockerfile's own post-build verification step: this was the very next failure once the separate llmlingua-2 stub bug was fixed and the build progressed far enough to reach it. Covered by tests/unit/assemble-standalone-onnxruntime-native-asset.test.ts (fails against the pre-fix code on both assertions, passes after). (cherry picked from commit 8c98a59f26a27e844678e673b31c1c21aaf72b0e) --------- Co-authored-by: Markus Hartung --- scripts/build/assembleStandalone.mjs | 28 ++++---- scripts/build/colocateOptionals.mjs | 8 +-- ...tandalone-onnxruntime-native-asset.test.ts | 65 +++++++++++++++++++ tests/unit/colocate-optionals.test.ts | 35 ++++++++++ 4 files changed, 116 insertions(+), 20 deletions(-) create mode 100644 tests/unit/assemble-standalone-onnxruntime-native-asset.test.ts diff --git a/scripts/build/assembleStandalone.mjs b/scripts/build/assembleStandalone.mjs index 412fe7b079..8bee5a403d 100644 --- a/scripts/build/assembleStandalone.mjs +++ b/scripts/build/assembleStandalone.mjs @@ -48,10 +48,7 @@ import fs from "node:fs/promises"; import fsSync from "node:fs"; import path from "node:path"; -import { - colocateLlmlinguaOptionals, - SEED_PACKAGES, -} from "./colocateOptionals.mjs"; +import { colocateLlmlinguaOptionals, SEED_PACKAGES } from "./colocateOptionals.mjs"; /** * Check whether a path exists (async). @@ -78,7 +75,7 @@ async function exists(targetPath) { * (relative to projectRoot) and destination (relative to outDir) can be joined * for either path/platform. @type {{label:string, src:string[], dest:string[]}[]} */ -const NATIVE_ASSET_ENTRIES = [ +export const NATIVE_ASSET_ENTRIES = [ { label: "wreq-js native runtime", src: ["node_modules", "wreq-js", "rust"], @@ -90,13 +87,17 @@ const NATIVE_ASSET_ENTRIES = [ dest: ["node_modules", "better-sqlite3", "build"], }, { - // #8847: Bun (and npx -g global installs) resolve better-sqlite3's native - // binary from prebuilds/ instead of build/Release/, so the compiled build/ - // copy alone leaves a hollow package that falls back to sql.js (OOM under - // Bun). Ship the prebuilds alongside the compiled binary. - label: "better-sqlite3 prebuilds (Bun / global installs)", - src: ["node_modules", "better-sqlite3", "prebuilds"], - dest: ["node_modules", "better-sqlite3", "prebuilds"], + // onnxruntime-node's dist/binding.js dlopen()s a platform-specific + // libonnxruntime.so.1 shipped under bin/napi-v3/// — a + // *dynamic* native load Next.js's standalone file trace can't see (same + // blind spot class as the LLMLingua closure below, just for a .so instead + // of a JS import). Without this the standalone bundle boots with + // "Error: libonnxruntime.so.1: cannot open shared object file: No such + // file or directory" the first time transformers/llmlingua actually try + // to run ONNX inference. + label: "onnxruntime-node native binaries (libonnxruntime .so + .node addon)", + src: ["node_modules", "onnxruntime-node", "bin"], + dest: ["node_modules", "onnxruntime-node", "bin"], }, { // TPROXY IP_TRANSPARENT addon (Fase 3 / Epic A). Built by build-tproxy-native @@ -759,8 +760,7 @@ export function assembleStandalone({ rootDir: projectRoot, targetNodeModulesDir: path.join(resolvedOutDir, "node_modules"), seeds: [...SEED_PACKAGES, "@huggingface/transformers"], - log: (message) => - console.log(`[assembleStandalone] ${message.trim()}`), + log: (message) => console.log(`[assembleStandalone] ${message.trim()}`), }); } diff --git a/scripts/build/colocateOptionals.mjs b/scripts/build/colocateOptionals.mjs index 367a95c112..073a59fbed 100644 --- a/scripts/build/colocateOptionals.mjs +++ b/scripts/build/colocateOptionals.mjs @@ -157,9 +157,7 @@ export function colocateLlmlinguaOptionals({ if (!existsSync(targetNm)) { return { skipped: true, - reason: targetNodeModulesDir - ? "no target node_modules" - : "no standalone dist/node_modules", + reason: targetNodeModulesDir ? "no target node_modules" : "no standalone dist/node_modules", }; } @@ -198,9 +196,7 @@ export function colocateLlmlinguaOptionals({ }); copied++; } catch (err) { - log( - ` ⚠️ LLMLingua optional co-location failed for ${name}: ${err.message}` - ); + log(` ⚠️ LLMLingua optional co-location failed for ${name}: ${err.message}`); } } diff --git a/tests/unit/assemble-standalone-onnxruntime-native-asset.test.ts b/tests/unit/assemble-standalone-onnxruntime-native-asset.test.ts new file mode 100644 index 0000000000..d39beb8127 --- /dev/null +++ b/tests/unit/assemble-standalone-onnxruntime-native-asset.test.ts @@ -0,0 +1,65 @@ +/** + * Regression test — onnxruntime-node's native libonnxruntime.so.1 was never + * registered in NATIVE_ASSET_ENTRIES, so the standalone bundle shipped + * dist/binding.js (traced by Next.js as a normal JS require) without its + * sibling native bin/ directory (loaded via a dynamic dlopen() Next's static + * file trace can't see — the same blind-spot class as the LLMLingua closure, + * for a .so instead of a JS import). The bundle then failed at runtime with + * "Error: libonnxruntime.so.1: cannot open shared object file: No such file + * or directory" the first time transformers/llmlingua tried to run ONNX + * inference — reproduced live via the Dockerfile's post-build verification + * step once the separate llmlingua-2 stub bug (#9653-adjacent fix) was + * resolved and the build progressed far enough to reach this package. + */ +import test from "node:test"; +import assert from "node:assert/strict"; +import { mkdtempSync, mkdirSync, writeFileSync, existsSync, rmSync } from "node:fs"; +import { join } from "node:path"; +import { tmpdir } from "node:os"; + +import { + NATIVE_ASSET_ENTRIES, + syncStandaloneNativeAssets, +} from "../../scripts/build/assembleStandalone.mjs"; + +test("NATIVE_ASSET_ENTRIES registers onnxruntime-node's native bin/ directory", () => { + const entry = NATIVE_ASSET_ENTRIES.find( + (e) => e.src.join("/") === "node_modules/onnxruntime-node/bin" + ); + assert.ok(entry, "onnxruntime-node/bin must be a registered native asset entry"); + assert.deepEqual(entry.dest, ["node_modules", "onnxruntime-node", "bin"]); +}); + +test("syncStandaloneNativeAssets copies onnxruntime-node's libonnxruntime.so.1 into the standalone bundle", async () => { + const root = mkdtempSync(join(tmpdir(), "omniroute-assemble-onnx-")); + try { + // Mirror the real package's shape: dist/binding.js (traced fine by Next) + // plus the platform-specific native .so under bin/napi-v3///. + const pkgDir = join(root, "node_modules", "onnxruntime-node"); + mkdirSync(join(pkgDir, "dist"), { recursive: true }); + writeFileSync(join(pkgDir, "dist", "binding.js"), "// native binding loader\n"); + const soDir = join(pkgDir, "bin", "napi-v3", "linux", "x64"); + mkdirSync(soDir, { recursive: true }); + writeFileSync(join(soDir, "libonnxruntime.so.1"), "fake-shared-library-bytes"); + + const outDir = join(root, ".build", "next", "standalone"); + mkdirSync(outDir, { recursive: true }); + + const changed = await syncStandaloneNativeAssets(root, undefined, { log: () => {} }, outDir); + assert.equal(changed, true); + + const destSo = join( + outDir, + "node_modules", + "onnxruntime-node", + "bin", + "napi-v3", + "linux", + "x64", + "libonnxruntime.so.1" + ); + assert.ok(existsSync(destSo), "libonnxruntime.so.1 must be copied into the standalone bundle"); + } finally { + rmSync(root, { recursive: true, force: true }); + } +}); diff --git a/tests/unit/colocate-optionals.test.ts b/tests/unit/colocate-optionals.test.ts index 2b0f864b6f..711c405f99 100644 --- a/tests/unit/colocate-optionals.test.ts +++ b/tests/unit/colocate-optionals.test.ts @@ -196,6 +196,41 @@ test("colocateLlmlinguaOptionals skips when there is no standalone dist bundle", } }); +test("colocateLlmlinguaOptionals fills a Next-traced stub (package.json only, no dist) instead of skipping it", () => { + // Reproduces a real build failure: Next.js's own standalone trace can create + // a stub directory for a dynamically-imported optional dependency it + // references but can't fully bundle — just package.json, no actual code. + // The old skip check (`existsSync(dest)`) treated that stub as "already + // co-located" and never copied the real dist/ output, so + // require.resolve('@atjsh/llmlingua-2') found a package.json with no + // matching main file at runtime. + const root = mkdtempSync(join(tmpdir(), "omniroute-colocate-stub-")); + try { + buildRoot(root); + const distNm = join(root, "dist", "node_modules"); + mkPkg(distNm, "@huggingface/transformers", { version: "3.5.2" }); + + // Simulate the Next-traced stub: directory exists, package.json only. + const stubDir = join(distNm, "@atjsh", "llmlingua-2"); + mkdirSync(stubDir, { recursive: true }); + writeFileSync( + join(stubDir, "package.json"), + readFileSync(join(root, "node_modules", "@atjsh", "llmlingua-2", "package.json"), "utf8") + ); + assert.ok(!existsSync(join(stubDir, "dist", "index.js")), "stub must start without dist/"); + + const result = colocateLlmlinguaOptionals({ rootDir: root }); + assert.equal(result.skipped, false, "must not treat the stub as already co-located"); + + assert.ok( + existsSync(join(stubDir, "dist", "index.js")), + "the real dist/index.js must be filled in, not left missing behind the stub" + ); + } finally { + rmSync(root, { recursive: true, force: true }); + } +}); + test("SEED_PACKAGES excludes transformers (it is a dist-pinned peer, not a seed)", () => { assert.ok(!SEED_PACKAGES.includes("@huggingface/transformers")); assert.deepEqual(SEED_PACKAGES, ["@atjsh/llmlingua-2", "@tensorflow/tfjs", "js-tiktoken"]);