Skip to content
Merged
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
87 changes: 87 additions & 0 deletions packages/cli/vite.node.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,91 @@ function runtimeRequirePlugin(): Plugin {
}
}

/**
* The Node bundle must not inline native-binding packages whose loaders
* dynamically require a platform .node file: @rollup/plugin-commonjs cannot
* resolve those requires, so the bundle throws during initialisation and
* EVERY command dies before --version (#1615). onnxruntime-node arrives via
* @opencode/core's embeddings-native (a lazy `await import` that
* inlineDynamicImports flattens into the startup graph) and via
* @huggingface/transformers, which also statically imports sharp.
*
* Following the fffNodePlugin precedent above, both specifiers resolve to
* stubs in this bundle ONLY (the Bun build never sees this config): module
* load succeeds, and any actual use throws a clear error. That is safe
* because the native-onnx embeddings backend is opt-in per registration
* (core embeddings.ts: it becomes a backend only by the deliberate #1554
* step-3 flip, which has not happened — every registered model uses the
* HTTP backend), and image loading via sharp is likewise only reached on
* an actual image embed. The NEXT native dependency pulled into core must
* be stubbed or externalised here the same way — the graph guard in
* nativeStubGraphPlugin fails the build if the real packages re-enter.
*/
function nativeStubPlugin(): Plugin {
const message =
"native bindings (onnxruntime-node/sharp) are unavailable in the Node bundle; the native-onnx embeddings backend runs under the Bun build only (#1615)"
return {
name: "opencode:native-stub",
enforce: "pre",
resolveId(id) {
if (id === "onnxruntime-node") return "\0opencode:onnxruntime-node-stub"
if (id === "sharp") return "\0opencode:sharp-stub"
},
transform(code, id) {
// @huggingface/transformers loads onnxruntime-node through a runtime
// createRequire (src/backends/onnx-node.js), which resolveId cannot
// intercept: left alone it requires the real package from beside the
// bundle at startup and throws MODULE_NOT_FOUND. Rewrite that loader
// to the same stub, like runtimeRequirePlugin rewrites Turndown.
const normalized = id.replaceAll("\\", "/")
if (normalized.endsWith("/@huggingface/transformers/src/backends/onnx-node.js"))
return `const unavailable = () => { throw new Error("native bindings (onnxruntime-node/sharp) are unavailable in the Node bundle; the native-onnx embeddings backend runs under the Bun build only (#1615)") }
class Tensor { constructor() { unavailable() } }
const InferenceSession = { create: unavailable }
export default { InferenceSession, Tensor, env: {} }`
// The published package resolves to its prebuilt dist bundle, where
// the same loader survives as a single requireFromHere call assigned
// to onnx_node_default: swap just that call for the stub object.
if (!normalized.endsWith("/@huggingface/transformers/dist/transformers.node.mjs")) return
const stub = `({ InferenceSession: { create: () => { throw new Error("native bindings (onnxruntime-node/sharp) are unavailable in the Node bundle; the native-onnx embeddings backend runs under the Bun build only (#1615)") } }, Tensor: class { constructor() { throw new Error("native bindings (onnxruntime-node/sharp) are unavailable in the Node bundle; the native-onnx embeddings backend runs under the Bun build only (#1615)") } }, env: {} })`
const transformed = code.replace('requireFromHere("onnxruntime-node")', stub)
if (transformed === code) this.error("Failed to rewrite the transformers onnxruntime-node loader")
return transformed
},
load(id) {
if (id === "\0opencode:onnxruntime-node-stub")
return `const unavailable = () => { throw new Error(${JSON.stringify(message)}) }
class Tensor { constructor() { unavailable() } }
const InferenceSession = { create: unavailable }
const env = {}
export { InferenceSession, Tensor, env }
export default { InferenceSession, Tensor, env }`
if (id === "\0opencode:sharp-stub")
return `export default () => { throw new Error(${JSON.stringify(message)}) }`
},
}
}

/** Build-time guard for the stub above (#1615): the real onnxruntime-node /
* sharp packages must never re-enter the Node bundle graph, and the stubs
* must be present — a regression fails here in seconds instead of inside
* the 60-140s serve-handover tests. */
function nativeStubGraphPlugin(): Plugin {
return {
name: "opencode:native-stub-graph",
generateBundle() {
const ids = Array.from(this.getModuleIds(), (id) => id.replaceAll("\\", "/"))
const leaked = ids.filter(
(id) => id.includes("/node_modules/onnxruntime-node/") || id.includes("/node_modules/sharp/"),
)
if (leaked.length > 0)
this.error(`Node bundle graph contains native-binding packages that crash it at startup (#1615): ${leaked.join(", ")}`)
if (!ids.some((id) => id.includes("opencode:onnxruntime-node-stub")))
this.error("Node bundle graph is missing the onnxruntime-node stub (#1615): embeddings-native left the graph or the stub plugin stopped resolving")
},
}
}

function simulationGraphPlugin(): Plugin {
return {
name: "opencode:simulation-graph",
Expand Down Expand Up @@ -264,6 +349,8 @@ export function mainConfig(input: NodeBuildInput): UserConfig {
rawTextPlugin(),
runtimeRequirePlugin(),
fffNodePlugin(),
nativeStubPlugin(),
nativeStubGraphPlugin(),
simulationGraphPlugin(),
solid({
solid: {
Expand Down
10 changes: 10 additions & 0 deletions packages/core/src/embeddings-native.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,16 @@
* Bare dev shells on NixOS need LD_LIBRARY_PATH with the gcc-lib and
* zlib store paths (the same paths ~/local-embed/run.sh carries);
* the packaged server's nix closure must provide them.
*
* Node bundle (packages/cli/vite.node.config.ts, #1615): the native
* bindings cannot be inlined there — onnxruntime-node's loader
* dynamically requires its .node file, which the bundler cannot
* resolve, and the throw lands at bundle initialisation. The Node
* build therefore stubs onnxruntime-node (and sharp, which
* @huggingface/transformers loads) out of the bundle: this backend
* is unavailable under Node and any use throws a clear error there.
* It stays available under the Bun build, which resolves the real
* packages from node_modules.
*/
import { existsSync } from "node:fs"
import { homedir } from "node:os"
Expand Down