fix(build): bundle zod inline to fix pnpm global install resolution (#78515)

Merged via squash.

Prepared head SHA: c925d1afab
Co-authored-by: ggzeng <20488795+ggzeng@users.noreply.github.com>
Co-authored-by: altaywtf <9790196+altaywtf@users.noreply.github.com>
Reviewed-by: @altaywtf
This commit is contained in:
Gavin Zeng
2026-05-18 00:20:42 +08:00
committed by GitHub
parent ac848d318d
commit ea72414e1c
6 changed files with 189 additions and 2 deletions

View File

@@ -114,6 +114,7 @@ Docs: https://docs.openclaw.ai
- Android: prompt before replacing a changed Gateway TLS thumbprint, showing the old and new SHA-256 fingerprints so users can accept expected certificate rotations instead of hard failing on pin mismatch. (#83077) Thanks @sliekens.
- CLI/status: render extra gateway-like service diagnostics as warning/info output instead of error output. Fixes #46930. (#82922) thanks @giodl73-repo.
- Agents/failover: classify Moonshot/Kimi exhausted-balance HTTP 429 payloads as billing instead of generic rate limits, preserving billing guidance and fallback behavior. Fixes #43447. (#83079) Thanks @leno23.
- Plugin SDK: bundle `openclaw/plugin-sdk/zod` into the published package artifact and verify the packed zod subpath stays self-contained, so pnpm global installs can register plugins without a package-local `zod` symlink. Fixes #78398. (#78515) Thanks @ggzeng.
## 2026.5.17

View File

@@ -13,7 +13,7 @@ import {
import { builtinModules } from "node:module";
import { createRequire } from "node:module";
import { tmpdir } from "node:os";
import { isAbsolute, join, relative } from "node:path";
import { dirname, isAbsolute, join, relative } from "node:path";
import { pathToFileURL } from "node:url";
import { formatErrorMessage } from "../src/infra/errors.ts";
import { BUNDLED_RUNTIME_SIDECAR_PATHS } from "../src/plugins/runtime-sidecar-paths.ts";
@@ -129,6 +129,7 @@ export function collectInstalledPackageErrors(params: {
}
errors.push(...collectInstalledContextEngineRuntimeErrors(params.packageRoot));
errors.push(...collectInstalledPluginSdkZodArtifactErrors(params.packageRoot));
errors.push(...collectInstalledRootDependencyManifestErrors(params.packageRoot));
return errors;
@@ -214,6 +215,97 @@ export function collectInstalledContextEngineRuntimeErrors(packageRoot: string):
return errors;
}
function resolveInstalledDistRelativeImport(params: {
distRoot: string;
importerPath: string;
specifier: string;
}): string | null {
if (!params.specifier.startsWith(".")) {
return null;
}
const candidatePath = join(dirname(params.importerPath), params.specifier);
const candidatePaths = [
candidatePath,
`${candidatePath}.js`,
`${candidatePath}.mjs`,
`${candidatePath}.cjs`,
join(candidatePath, "index.js"),
join(candidatePath, "index.mjs"),
join(candidatePath, "index.cjs"),
];
for (const resolvedPath of candidatePaths) {
const relativePath = relative(params.distRoot, resolvedPath);
if (
relativePath.length === 0 ||
relativePath.startsWith("..") ||
isAbsolute(relativePath) ||
!existsSync(resolvedPath)
) {
continue;
}
return resolvedPath;
}
return null;
}
export function collectInstalledPluginSdkZodArtifactErrors(packageRoot: string): string[] {
const distRoot = join(packageRoot, "dist");
const entryRelativePath = "dist/plugin-sdk/zod.js";
const entryPath = join(packageRoot, entryRelativePath);
const pending = [entryPath];
const visited = new Set<string>();
while (pending.length > 0) {
const filePath = pending.pop();
if (!filePath || visited.has(filePath)) {
continue;
}
visited.add(filePath);
if (!existsSync(filePath)) {
return [`installed package is missing required plugin SDK artifact: ${entryRelativePath}`];
}
const relativePath = relative(packageRoot, filePath).replaceAll("\\", "/");
const fileStat = lstatSync(filePath);
if (!fileStat.isFile() || fileStat.size > MAX_INSTALLED_ROOT_DIST_JS_BYTES) {
return [
`installed package plugin SDK artifact '${relativePath}' is invalid or exceeds ${MAX_INSTALLED_ROOT_DIST_JS_BYTES} bytes.`,
];
}
const source = readFileSync(filePath, "utf8");
const parsedSpecifiers = extractJavaScriptImportSpecifiers(source);
if (!parsedSpecifiers.ok) {
return [
`installed package plugin SDK artifact '${relativePath}' could not be parsed for runtime dependency verification: ${parsedSpecifiers.error}.`,
];
}
for (const specifier of parsedSpecifiers.specifiers) {
if (specifier === "zod" || specifier.startsWith("zod/")) {
return [
`installed package plugin SDK zod artifact must be self-contained but ${relativePath} imports ${specifier}.`,
];
}
const resolvedPath = resolveInstalledDistRelativeImport({
distRoot,
importerPath: filePath,
specifier,
});
if (resolvedPath) {
pending.push(resolvedPath);
}
}
}
return [];
}
function listInstalledRootDistJavaScriptFiles(packageRoot: string): string[] {
return listDistJavaScriptFiles(packageRoot, {
skipRelativePath: (relativePath) => relativePath.startsWith("extensions/"),

View File

@@ -5,6 +5,7 @@ import tsdownConfig from "../../tsdown.config.ts";
type TsdownConfigEntry = {
deps?: {
alwaysBundle?: string[] | ((id: string) => boolean);
neverBundle?: string[] | ((id: string) => boolean);
};
entry?: Record<string, string> | string[];
@@ -226,6 +227,21 @@ describe("tsdown config", () => {
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[] = [];

View File

@@ -7,6 +7,7 @@ import {
buildPublishedInstallScenarios,
collectInstalledBundledRuntimeSidecarPaths,
collectInstalledContextEngineRuntimeErrors,
collectInstalledPluginSdkZodArtifactErrors,
collectInstalledRootDependencyManifestErrors,
collectInstalledPackageErrors,
normalizeInstalledBinaryVersion,
@@ -146,6 +147,77 @@ describe("collectInstalledContextEngineRuntimeErrors", () => {
});
});
describe("collectInstalledPluginSdkZodArtifactErrors", () => {
function withInstalledPackageRoot(run: (packageRoot: string) => void): void {
const packageRoot = mkdtempSync(join(tmpdir(), "openclaw-postpublish-zod-sdk-"));
try {
run(packageRoot);
} finally {
rmSync(packageRoot, { recursive: true, force: true });
}
}
function writeInstalledFile(packageRoot: string, relativePath: string, contents: string): void {
const filePath = join(packageRoot, ...relativePath.split("/"));
mkdirSync(dirname(filePath), { recursive: true });
writeFileSync(filePath, contents, "utf8");
}
it("requires the plugin-sdk zod artifact", () => {
withInstalledPackageRoot((packageRoot) => {
expect(collectInstalledPluginSdkZodArtifactErrors(packageRoot)).toEqual([
"installed package is missing required plugin SDK artifact: dist/plugin-sdk/zod.js",
]);
});
});
it("rejects plugin-sdk zod artifacts with a bare zod export", () => {
withInstalledPackageRoot((packageRoot) => {
writeInstalledFile(
packageRoot,
"dist/plugin-sdk/zod.js",
'import "../zod-D2c0iocA.js";\nexport * from "zod";\n',
);
expect(collectInstalledPluginSdkZodArtifactErrors(packageRoot)).toEqual([
"installed package plugin SDK zod artifact must be self-contained but dist/plugin-sdk/zod.js imports zod.",
]);
});
});
it("rejects plugin-sdk zod artifacts when a reachable local chunk imports zod", () => {
withInstalledPackageRoot((packageRoot) => {
writeInstalledFile(
packageRoot,
"dist/plugin-sdk/zod.js",
'export { z } from "../zod-D2c0iocA.js";\n',
);
writeInstalledFile(
packageRoot,
"dist/zod-D2c0iocA.js",
'import * as zodCore from "zod/v4/core";\nexport const z = zodCore;\n',
);
expect(collectInstalledPluginSdkZodArtifactErrors(packageRoot)).toEqual([
"installed package plugin SDK zod artifact must be self-contained but dist/zod-D2c0iocA.js imports zod/v4/core.",
]);
});
});
it("accepts plugin-sdk zod artifacts that only import package-local chunks", () => {
withInstalledPackageRoot((packageRoot) => {
writeInstalledFile(
packageRoot,
"dist/plugin-sdk/zod.js",
'export { z } from "../zod-D2c0iocA.js";\n',
);
writeInstalledFile(packageRoot, "dist/zod-D2c0iocA.js", "export const z = {};\n");
expect(collectInstalledPluginSdkZodArtifactErrors(packageRoot)).toEqual([]);
});
});
});
describe("normalizeInstalledBinaryVersion", () => {
it("accepts decorated CLI version output", () => {
expect(normalizeInstalledBinaryVersion("OpenClaw 2026.4.8 (9ece252)")).toBe("2026.4.8");

View File

@@ -553,6 +553,7 @@ describe("collectMissingPackPaths", () => {
packageRoot,
}),
).toEqual([
"installed package is missing required plugin SDK artifact: dist/plugin-sdk/zod.js",
"installed package root dist file 'typescript-compiler.js' is invalid or exceeds 6291456 bytes.",
]);
} finally {

View File

@@ -183,7 +183,12 @@ function shouldNeverBundleDependency(id: string): boolean {
}
function shouldAlwaysBundleDependency(id: string): boolean {
return id === "@openclaw/fs-safe" || id.startsWith("@openclaw/fs-safe/");
return (
id === "@openclaw/fs-safe" ||
id.startsWith("@openclaw/fs-safe/") ||
id === "zod" ||
id.startsWith("zod/")
);
}
function listBundledPluginEntrySources(