mirror of
https://github.com/openclaw/openclaw.git
synced 2026-06-06 05:51:15 +08:00
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:
@@ -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
|
||||
|
||||
|
||||
@@ -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/"),
|
||||
|
||||
@@ -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[] = [];
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user