Files
openclaw/src/infra/tsdown-config.test.ts
2026-05-26 19:40:12 +01:00

327 lines
11 KiB
TypeScript

import { readFileSync } from "node:fs";
import { bundledPluginRoot } from "openclaw/plugin-sdk/test-fixtures";
import { describe, expect, it } from "vitest";
import tsdownConfig from "../../tsdown.config.ts";
type TsdownConfigEntry = {
deps?: {
alwaysBundle?: string[] | ((id: string) => boolean);
neverBundle?: string[] | ((id: string) => boolean);
};
entry?: Record<string, string> | string[];
inputOptions?: TsdownInputOptions;
outDir?: string;
};
type TsdownLog = {
code?: string;
message?: string;
id?: string;
importer?: string;
plugin?: string;
};
type TsdownOnLog = (
level: string,
log: TsdownLog,
defaultHandler: (level: string, log: TsdownLog) => void,
) => void;
type TsdownInputOptions = (
options: { external?: TsdownExternalOption; onLog?: TsdownOnLog },
format?: unknown,
context?: unknown,
) => { external?: TsdownExternalOption; onLog?: TsdownOnLog } | undefined;
type TsdownExternalOption = string | RegExp | Array<string | RegExp> | TsdownExternalFunction;
type TsdownExternalFunction = (
id: string,
parentId: string | undefined,
isResolved: boolean,
) => boolean | null | undefined;
function asConfigArray(config: unknown): TsdownConfigEntry[] {
return Array.isArray(config) ? (config as TsdownConfigEntry[]) : [config as TsdownConfigEntry];
}
function entryKeys(config: TsdownConfigEntry): string[] {
if (!config.entry || Array.isArray(config.entry)) {
return [];
}
return Object.keys(config.entry);
}
function entrySources(config: TsdownConfigEntry): Record<string, string> {
if (!config.entry || Array.isArray(config.entry)) {
return {};
}
return config.entry;
}
function bundledEntry(pluginId: string): string {
return `${bundledPluginRoot(pluginId)}/index`;
}
function unifiedDistGraph(): TsdownConfigEntry | undefined {
return asConfigArray(tsdownConfig).find((config) =>
entryKeys(config).includes("plugins/runtime/index"),
);
}
function requireUnifiedDistGraph(): TsdownConfigEntry {
const distGraph = unifiedDistGraph();
if (!distGraph) {
throw new Error("expected unified dist graph");
}
return distGraph;
}
function readGatewayRunLoopSource(): string {
return readFileSync(new URL("../cli/gateway-cli/run-loop.ts", import.meta.url), "utf8");
}
function readPiModelDiscoveryCacheSource(): string {
return readFileSync(
new URL("../agents/pi-embedded-runner/model-discovery-cache.ts", import.meta.url),
"utf8",
);
}
describe("tsdown config", () => {
it("keeps core, plugin runtime, plugin-sdk, bundled root plugins, and bundled hooks in one dist graph", () => {
const distGraph = requireUnifiedDistGraph();
const keys = entryKeys(distGraph);
for (const entry of [
"acp/control-plane/manager",
"agents/auth-profiles.runtime",
"agents/model-catalog.runtime",
"agents/models-config.runtime",
"cli/gateway-lifecycle.runtime",
"plugins/memory-state",
"subagent-registry.runtime",
"task-registry-control.runtime",
"agents/pi-model-discovery-runtime",
"link-understanding/apply.runtime",
"media-understanding/apply.runtime",
"index",
"commands/status.summary.runtime",
"provider-dispatcher.runtime",
"plugins/hook-runner-global",
"plugins/provider-discovery.runtime",
"plugins/provider-runtime.runtime",
"plugins/runtime/index",
"plugins/synthetic-auth.runtime",
"web-fetch/runtime",
"plugin-sdk/compat",
"plugin-sdk/index",
bundledEntry("active-memory"),
"bundled/boot-md/handler",
]) {
expect(keys).toContain(entry);
}
});
it("keeps root-package-excluded external plugins out of the root dist graph", () => {
const distGraph = requireUnifiedDistGraph();
const keys = entryKeys(distGraph);
const hasPluginEntry = (pluginId: string) =>
keys.some((entry) => entry.startsWith(`${bundledPluginRoot(pluginId)}/`));
expect(hasPluginEntry("amazon-bedrock")).toBe(false);
expect(hasPluginEntry("amazon-bedrock-mantle")).toBe(false);
});
it("keeps gateway lifecycle lazy runtime behind one stable dist entry", () => {
const distGraph = requireUnifiedDistGraph();
expect(entrySources(distGraph)["cli/gateway-lifecycle.runtime"]).toBe(
"src/cli/gateway-cli/lifecycle.runtime.ts",
);
});
it("keeps reply dispatcher lazy runtime behind one root stable dist entry", () => {
const distGraph = requireUnifiedDistGraph();
expect(entrySources(distGraph)["provider-dispatcher.runtime"]).toBe(
"src/auto-reply/reply/provider-dispatcher.runtime.ts",
);
});
it("keeps gateway shutdown hook runner behind one stable dist entry", () => {
const distGraph = requireUnifiedDistGraph();
expect(entrySources(distGraph)["plugins/hook-runner-global"]).toBe(
"src/plugins/hook-runner-global.ts",
);
});
it("keeps PI model discovery synthetic auth refs behind one stable runtime dist entry", () => {
const distGraph = requireUnifiedDistGraph();
const importSpecifiers = [
...readPiModelDiscoveryCacheSource().matchAll(
/from ["']([^"']*synthetic-auth\.runtime\.js)["']/gu,
),
].map((match) => match[1]);
expect(importSpecifiers).toEqual(["../../plugins/synthetic-auth.runtime.js"]);
expect(entrySources(distGraph)["plugins/synthetic-auth.runtime"]).toBe(
"src/plugins/synthetic-auth.runtime.ts",
);
});
it("keeps Telegram ingress worker behind one root stable dist entry", () => {
const distGraph = requireUnifiedDistGraph();
expect(entrySources(distGraph)["telegram-ingress-worker.runtime"]).toBe(
"extensions/telegram/src/telegram-ingress-worker.runtime.ts",
);
});
it("routes gateway run-loop lifecycle imports through the stable runtime boundary", () => {
const importSpecifiers = [
...readGatewayRunLoopSource().matchAll(/import\(["']([^"']+)["']\)/gu),
].map((match) => match[1]);
expect(new Set(importSpecifiers)).toEqual(new Set(["./lifecycle.runtime.js"]));
});
it("keeps bundled plugins out of separate dependency-staging graphs", () => {
const extensionGraphs = asConfigArray(tsdownConfig).filter(
(config) => typeof config.outDir === "string" && config.outDir.startsWith("dist/extensions/"),
);
expect(extensionGraphs).toStrictEqual([]);
});
it("does not emit plugin-sdk or hooks from a separate dist graph", () => {
const configs = asConfigArray(tsdownConfig);
const hookEntries = configs.flatMap((config) =>
Array.isArray(config.entry)
? config.entry.filter((entry) => entry.includes("src/hooks/"))
: [],
);
expect(configs.map((config) => config.outDir)).not.toContain("dist/plugin-sdk");
expect(hookEntries).toStrictEqual([]);
});
it("externalizes known heavy native and declaration-fragile dependencies", () => {
const unifiedGraph = unifiedDistGraph();
const neverBundle = unifiedGraph?.deps?.neverBundle;
const external = unifiedGraph?.inputOptions?.({})?.external;
if (typeof neverBundle === "function") {
expect(neverBundle("@anthropic-ai/vertex-sdk")).toBe(true);
expect(neverBundle("@discordjs/voice")).toBe(true);
expect(neverBundle("@lancedb/lancedb")).toBe(true);
expect(neverBundle("@larksuiteoapi/node-sdk")).toBe(true);
expect(neverBundle("@matrix-org/matrix-sdk-crypto-nodejs")).toBe(true);
expect(neverBundle("@slack/bolt")).toBe(true);
expect(neverBundle("@slack/web-api")).toBe(true);
expect(neverBundle("@vitest/expect")).toBe(true);
expect(neverBundle("matrix-js-sdk/lib/client.js")).toBe(true);
expect(neverBundle("qrcode-terminal/lib/main.js")).toBe(true);
expect(neverBundle("vitest")).toBe(true);
expect(neverBundle("not-a-runtime-dependency")).toBe(false);
} else {
for (const dependency of [
"@anthropic-ai/vertex-sdk",
"@discordjs/voice",
"@lancedb/lancedb",
"@larksuiteoapi/node-sdk",
"@slack/bolt",
"@slack/web-api",
"@vitest/expect",
"matrix-js-sdk",
"qrcode-terminal",
"vitest",
]) {
expect(neverBundle).toContain(dependency);
}
}
if (typeof external !== "function") {
throw new Error("expected unified graph external predicate");
}
const externalize = external;
expect(externalize("qrcode-terminal/lib/main.js", undefined, false)).toBe(true);
});
it("always bundles plugin SDK package-local runtime dependencies", () => {
const unifiedGraph = requireUnifiedDistGraph();
const alwaysBundle = unifiedGraph.deps?.alwaysBundle;
if (typeof alwaysBundle !== "function") {
throw new Error("expected unified graph alwaysBundle predicate");
}
expect(alwaysBundle("@openclaw/fs-safe")).toBe(true);
expect(alwaysBundle("@openclaw/fs-safe/path")).toBe(true);
expect(alwaysBundle("zod")).toBe(true);
expect(alwaysBundle("zod/v4/core")).toBe(true);
expect(alwaysBundle("not-a-runtime-dependency")).toBe(false);
});
it("suppresses unresolved imports from extension source", () => {
const configured = unifiedDistGraph()?.inputOptions?.({})?.onLog;
const handled: TsdownLog[] = [];
configured?.(
"warn",
{
code: "UNRESOLVED_IMPORT",
message: "Could not resolve '@azure/identity' in extensions/msteams/src/sdk.ts",
},
(_level, log) => handled.push(log),
);
expect(handled).toStrictEqual([]);
});
it("keeps unresolved imports outside extension source visible", () => {
const configured = unifiedDistGraph()?.inputOptions?.({})?.onLog;
const handled: TsdownLog[] = [];
const log = {
code: "UNRESOLVED_IMPORT",
message: "Could not resolve 'missing-dependency' in src/index.ts",
};
configured?.("warn", log, (_level, forwardedLog) => handled.push(forwardedLog));
expect(handled).toEqual([log]);
});
it("suppresses rolldown-plugin-dts CommonJS dts warnings from bundled zod locales", () => {
const configured = unifiedDistGraph()?.inputOptions?.({})?.onLog;
const handled: TsdownLog[] = [];
configured?.(
"warn",
{
code: "PLUGIN_WARNING",
plugin: "rolldown-plugin-dts:fake-js",
message:
"/abs/path/node_modules/zod/v4/locales/ur.d.cts uses CommonJS dts syntax. CommonJS dts modules cannot be reliably bundled by rolldown-plugin-dts. Please mark this module as external in your Rolldown config.",
},
(_level, log) => handled.push(log),
);
expect(handled).toStrictEqual([]);
});
it("keeps other rolldown-plugin-dts warnings visible", () => {
const configured = unifiedDistGraph()?.inputOptions?.({})?.onLog;
const handled: TsdownLog[] = [];
const log = {
code: "PLUGIN_WARNING",
plugin: "rolldown-plugin-dts:fake-js",
message: "some other dts warning that should not be hidden",
};
configured?.("warn", log, (_level, forwardedLog) => handled.push(forwardedLog));
expect(handled).toEqual([log]);
});
});