Fallback to Jiti when bun is unavailable

This commit is contained in:
Tak Hoffman 2026-03-27 15:11:01 -05:00
parent fa89d68e7a
commit c3d45fbb19
No known key found for this signature in database
2 changed files with 72 additions and 1 deletions

View File

@ -77,6 +77,13 @@ function shouldRetryViaIsolatedCopy(error: unknown): boolean {
return code === "ERR_MODULE_NOT_FOUND" && message.includes(`${path.sep}node_modules${path.sep}`);
}
function isMissingExecutableError(error: unknown): boolean {
if (!error || typeof error !== "object") {
return false;
}
return "code" in error && error.code === "ENOENT";
}
const SOURCE_FILE_EXTENSIONS = [".ts", ".tsx", ".mts", ".cts", ".js", ".jsx", ".mjs", ".cjs"];
function resolveImportCandidates(basePath: string): string[] {
@ -232,6 +239,12 @@ export async function loadChannelConfigSurfaceModule(
OPENCLAW_CONFIG_SURFACE_MODULE: path.resolve(candidatePath),
},
});
if (result.error) {
if (isMissingExecutableError(result.error)) {
return null;
}
throw result.error;
}
if (result.status !== 0) {
throw new Error(result.stderr || result.stdout || `bun loader failed for ${candidatePath}`);
}

View File

@ -1,7 +1,7 @@
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { afterEach, describe, expect, it } from "vitest";
import { afterEach, describe, expect, it, vi } from "vitest";
import { loadChannelConfigSurfaceModule } from "../../scripts/load-channel-config-surface.ts";
const tempDirs: string[] = [];
@ -12,6 +12,25 @@ function makeTempRoot(prefix: string): string {
return root;
}
async function importLoaderWithMissingBun() {
vi.resetModules();
const spawnSync = vi.fn(() => ({
error: Object.assign(new Error("bun not found"), { code: "ENOENT" }),
status: null,
stdout: "",
stderr: "",
}));
vi.doMock("node:child_process", () => ({ spawnSync }));
try {
const imported = await import("../../scripts/load-channel-config-surface.ts");
return { loadChannelConfigSurfaceModule: imported.loadChannelConfigSurfaceModule, spawnSync };
} finally {
vi.doUnmock("node:child_process");
vi.resetModules();
}
}
afterEach(() => {
for (const dir of tempDirs.splice(0, tempDirs.length)) {
fs.rmSync(dir, { recursive: true, force: true });
@ -19,6 +38,45 @@ afterEach(() => {
});
describe("loadChannelConfigSurfaceModule", () => {
it("falls back to Jiti when bun is unavailable", async () => {
const repoRoot = makeTempRoot("openclaw-config-surface-");
const packageRoot = path.join(repoRoot, "extensions", "demo");
const modulePath = path.join(packageRoot, "src", "config-schema.js");
fs.mkdirSync(path.join(packageRoot, "src"), { recursive: true });
fs.writeFileSync(
path.join(packageRoot, "package.json"),
JSON.stringify({ name: "@openclaw/demo", type: "module" }, null, 2),
"utf8",
);
fs.writeFileSync(
modulePath,
[
"export const DemoChannelConfigSchema = {",
" schema: {",
" type: 'object',",
" properties: { ok: { type: 'string' } },",
" },",
"};",
"",
].join("\n"),
"utf8",
);
const { loadChannelConfigSurfaceModule: loadWithMissingBun, spawnSync } =
await importLoaderWithMissingBun();
await expect(loadWithMissingBun(modulePath, { repoRoot })).resolves.toMatchObject({
schema: {
type: "object",
properties: {
ok: { type: "string" },
},
},
});
expect(spawnSync).toHaveBeenCalledWith("bun", expect.any(Array), expect.any(Object));
});
it("retries from an isolated package copy when extension-local node_modules is broken", async () => {
const repoRoot = makeTempRoot("openclaw-config-surface-");
const packageRoot = path.join(repoRoot, "extensions", "demo");