maint: follow-up cherry-pick fix-in-place #9712 (conflict-resolved fallback) (#9892)

* 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 359aba59c7)

* 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/<platform>/<arch>/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 8c98a59f26)

---------

Co-authored-by: Markus Hartung <mail@hartmark.se>
This commit is contained in:
Diego Rodrigues de Sa e Souza
2026-08-09 09:54:24 -03:00
committed by GitHub
parent bd33b4589a
commit bc38965088
4 changed files with 116 additions and 20 deletions

View File

@@ -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/<platform>/<arch>/ — 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()}`),
});
}

View File

@@ -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}`);
}
}

View File

@@ -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/<platform>/<arch>/.
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 });
}
});

View File

@@ -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"]);