Skip to content

Commit 03e4d9a

Browse files
committed
fix: bundle plugin SDK zod artifact
1 parent eaea56d commit 03e4d9a

6 files changed

Lines changed: 189 additions & 2 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,7 @@ Docs: https://docs.openclaw.ai
114114
- 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.
115115
- CLI/status: render extra gateway-like service diagnostics as warning/info output instead of error output. Fixes #46930. (#82922) thanks @giodl73-repo.
116116
- 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.
117+
- 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.
117118

118119
## 2026.5.17
119120

scripts/openclaw-npm-postpublish-verify.ts

Lines changed: 93 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import {
1313
import { builtinModules } from "node:module";
1414
import { createRequire } from "node:module";
1515
import { tmpdir } from "node:os";
16-
import { isAbsolute, join, relative } from "node:path";
16+
import { dirname, isAbsolute, join, relative } from "node:path";
1717
import { pathToFileURL } from "node:url";
1818
import { formatErrorMessage } from "../src/infra/errors.ts";
1919
import { BUNDLED_RUNTIME_SIDECAR_PATHS } from "../src/plugins/runtime-sidecar-paths.ts";
@@ -129,6 +129,7 @@ export function collectInstalledPackageErrors(params: {
129129
}
130130

131131
errors.push(...collectInstalledContextEngineRuntimeErrors(params.packageRoot));
132+
errors.push(...collectInstalledPluginSdkZodArtifactErrors(params.packageRoot));
132133
errors.push(...collectInstalledRootDependencyManifestErrors(params.packageRoot));
133134

134135
return errors;
@@ -214,6 +215,97 @@ export function collectInstalledContextEngineRuntimeErrors(packageRoot: string):
214215
return errors;
215216
}
216217

218+
function resolveInstalledDistRelativeImport(params: {
219+
distRoot: string;
220+
importerPath: string;
221+
specifier: string;
222+
}): string | null {
223+
if (!params.specifier.startsWith(".")) {
224+
return null;
225+
}
226+
227+
const candidatePath = join(dirname(params.importerPath), params.specifier);
228+
const candidatePaths = [
229+
candidatePath,
230+
`${candidatePath}.js`,
231+
`${candidatePath}.mjs`,
232+
`${candidatePath}.cjs`,
233+
join(candidatePath, "index.js"),
234+
join(candidatePath, "index.mjs"),
235+
join(candidatePath, "index.cjs"),
236+
];
237+
238+
for (const resolvedPath of candidatePaths) {
239+
const relativePath = relative(params.distRoot, resolvedPath);
240+
if (
241+
relativePath.length === 0 ||
242+
relativePath.startsWith("..") ||
243+
isAbsolute(relativePath) ||
244+
!existsSync(resolvedPath)
245+
) {
246+
continue;
247+
}
248+
return resolvedPath;
249+
}
250+
251+
return null;
252+
}
253+
254+
export function collectInstalledPluginSdkZodArtifactErrors(packageRoot: string): string[] {
255+
const distRoot = join(packageRoot, "dist");
256+
const entryRelativePath = "dist/plugin-sdk/zod.js";
257+
const entryPath = join(packageRoot, entryRelativePath);
258+
const pending = [entryPath];
259+
const visited = new Set<string>();
260+
261+
while (pending.length > 0) {
262+
const filePath = pending.pop();
263+
if (!filePath || visited.has(filePath)) {
264+
continue;
265+
}
266+
visited.add(filePath);
267+
268+
if (!existsSync(filePath)) {
269+
return [`installed package is missing required plugin SDK artifact: ${entryRelativePath}`];
270+
}
271+
272+
const relativePath = relative(packageRoot, filePath).replaceAll("\\", "/");
273+
const fileStat = lstatSync(filePath);
274+
if (!fileStat.isFile() || fileStat.size > MAX_INSTALLED_ROOT_DIST_JS_BYTES) {
275+
return [
276+
`installed package plugin SDK artifact '${relativePath}' is invalid or exceeds ${MAX_INSTALLED_ROOT_DIST_JS_BYTES} bytes.`,
277+
];
278+
}
279+
280+
const source = readFileSync(filePath, "utf8");
281+
const parsedSpecifiers = extractJavaScriptImportSpecifiers(source);
282+
if (!parsedSpecifiers.ok) {
283+
return [
284+
`installed package plugin SDK artifact '${relativePath}' could not be parsed for runtime dependency verification: ${parsedSpecifiers.error}.`,
285+
];
286+
}
287+
288+
for (const specifier of parsedSpecifiers.specifiers) {
289+
if (specifier === "zod" || specifier.startsWith("zod/")) {
290+
return [
291+
`installed package plugin SDK zod artifact must be self-contained but ${relativePath} imports ${specifier}.`,
292+
];
293+
}
294+
295+
const resolvedPath = resolveInstalledDistRelativeImport({
296+
distRoot,
297+
importerPath: filePath,
298+
specifier,
299+
});
300+
if (resolvedPath) {
301+
pending.push(resolvedPath);
302+
}
303+
}
304+
}
305+
306+
return [];
307+
}
308+
217309
function listInstalledRootDistJavaScriptFiles(packageRoot: string): string[] {
218310
return listDistJavaScriptFiles(packageRoot, {
219311
skipRelativePath: (relativePath) => relativePath.startsWith("extensions/"),

src/infra/tsdown-config.test.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import tsdownConfig from "../../tsdown.config.ts";
55

66
type TsdownConfigEntry = {
77
deps?: {
8+
alwaysBundle?: string[] | ((id: string) => boolean);
89
neverBundle?: string[] | ((id: string) => boolean);
910
};
1011
entry?: Record<string, string> | string[];
@@ -226,6 +227,21 @@ describe("tsdown config", () => {
226227
expect(externalize("qrcode-terminal/lib/main.js", undefined, false)).toBe(true);
227228
});
228229

230+
it("always bundles plugin SDK package-local runtime dependencies", () => {
231+
const unifiedGraph = requireUnifiedDistGraph();
232+
const alwaysBundle = unifiedGraph.deps?.alwaysBundle;
233+
234+
if (typeof alwaysBundle !== "function") {
235+
throw new Error("expected unified graph alwaysBundle predicate");
236+
}
237+
238+
expect(alwaysBundle("@openclaw/fs-safe")).toBe(true);
239+
expect(alwaysBundle("@openclaw/fs-safe/path")).toBe(true);
240+
expect(alwaysBundle("zod")).toBe(true);
241+
expect(alwaysBundle("zod/v4/core")).toBe(true);
242+
expect(alwaysBundle("not-a-runtime-dependency")).toBe(false);
243+
});
244+
229245
it("suppresses unresolved imports from extension source", () => {
230246
const configured = unifiedDistGraph()?.inputOptions?.({})?.onLog;
231247
const handled: TsdownLog[] = [];

test/openclaw-npm-postpublish-verify.test.ts

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {
77
buildPublishedInstallScenarios,
88
collectInstalledBundledRuntimeSidecarPaths,
99
collectInstalledContextEngineRuntimeErrors,
10+
collectInstalledPluginSdkZodArtifactErrors,
1011
collectInstalledRootDependencyManifestErrors,
1112
collectInstalledPackageErrors,
1213
normalizeInstalledBinaryVersion,
@@ -146,6 +147,77 @@ describe("collectInstalledContextEngineRuntimeErrors", () => {
146147
});
147148
});
148149

150+
describe("collectInstalledPluginSdkZodArtifactErrors", () => {
151+
function withInstalledPackageRoot(run: (packageRoot: string) => void): void {
152+
const packageRoot = mkdtempSync(join(tmpdir(), "openclaw-postpublish-zod-sdk-"));
153+
try {
154+
run(packageRoot);
155+
} finally {
156+
rmSync(packageRoot, { recursive: true, force: true });
157+
}
158+
}
159+
160+
function writeInstalledFile(packageRoot: string, relativePath: string, contents: string): void {
161+
const filePath = join(packageRoot, ...relativePath.split("/"));
162+
mkdirSync(dirname(filePath), { recursive: true });
163+
writeFileSync(filePath, contents, "utf8");
164+
}
165+
166+
it("requires the plugin-sdk zod artifact", () => {
167+
withInstalledPackageRoot((packageRoot) => {
168+
expect(collectInstalledPluginSdkZodArtifactErrors(packageRoot)).toEqual([
169+
"installed package is missing required plugin SDK artifact: dist/plugin-sdk/zod.js",
170+
]);
171+
});
172+
});
173+
174+
it("rejects plugin-sdk zod artifacts with a bare zod export", () => {
175+
withInstalledPackageRoot((packageRoot) => {
176+
writeInstalledFile(
177+
packageRoot,
178+
"dist/plugin-sdk/zod.js",
179+
'import "../zod-D2c0iocA.js";\nexport * from "zod";\n',
180+
);
181+
182+
expect(collectInstalledPluginSdkZodArtifactErrors(packageRoot)).toEqual([
183+
"installed package plugin SDK zod artifact must be self-contained but dist/plugin-sdk/zod.js imports zod.",
184+
]);
185+
});
186+
});
187+
188+
it("rejects plugin-sdk zod artifacts when a reachable local chunk imports zod", () => {
189+
withInstalledPackageRoot((packageRoot) => {
190+
writeInstalledFile(
191+
packageRoot,
192+
"dist/plugin-sdk/zod.js",
193+
'export { z } from "../zod-D2c0iocA.js";\n',
194+
);
195+
writeInstalledFile(
196+
packageRoot,
197+
"dist/zod-D2c0iocA.js",
198+
'import * as zodCore from "zod/v4/core";\nexport const z = zodCore;\n',
199+
);
200+
201+
expect(collectInstalledPluginSdkZodArtifactErrors(packageRoot)).toEqual([
202+
"installed package plugin SDK zod artifact must be self-contained but dist/zod-D2c0iocA.js imports zod/v4/core.",
203+
]);
204+
});
205+
});
206+
207+
it("accepts plugin-sdk zod artifacts that only import package-local chunks", () => {
208+
withInstalledPackageRoot((packageRoot) => {
209+
writeInstalledFile(
210+
packageRoot,
211+
"dist/plugin-sdk/zod.js",
212+
'export { z } from "../zod-D2c0iocA.js";\n',
213+
);
214+
writeInstalledFile(packageRoot, "dist/zod-D2c0iocA.js", "export const z = {};\n");
215+
216+
expect(collectInstalledPluginSdkZodArtifactErrors(packageRoot)).toEqual([]);
217+
});
218+
});
219+
});
220+
149221
describe("normalizeInstalledBinaryVersion", () => {
150222
it("accepts decorated CLI version output", () => {
151223
expect(normalizeInstalledBinaryVersion("OpenClaw 2026.4.8 (9ece252)")).toBe("2026.4.8");

test/release-check.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -553,6 +553,7 @@ describe("collectMissingPackPaths", () => {
553553
packageRoot,
554554
}),
555555
).toEqual([
556+
"installed package is missing required plugin SDK artifact: dist/plugin-sdk/zod.js",
556557
"installed package root dist file 'typescript-compiler.js' is invalid or exceeds 6291456 bytes.",
557558
]);
558559
} finally {

tsdown.config.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -183,7 +183,12 @@ function shouldNeverBundleDependency(id: string): boolean {
183183
}
184184

185185
function shouldAlwaysBundleDependency(id: string): boolean {
186-
return id === "@openclaw/fs-safe" || id.startsWith("@openclaw/fs-safe/") || id === "zod";
186+
return (
187+
id === "@openclaw/fs-safe" ||
188+
id.startsWith("@openclaw/fs-safe/") ||
189+
id === "zod" ||
190+
id.startsWith("zod/")
191+
);
187192
}
188193

189194
function listBundledPluginEntrySources(

0 commit comments

Comments
 (0)