From 024caf1e5142f170b5f945cf1f6641b14fecdb31 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Mon, 10 Aug 2026 02:52:28 -0700 Subject: [PATCH] fix(plugin-sdk): external plugins typecheck across SDK entrypoints (#121512) * fix(plugin-sdk): dedupe declaration dependency types Keep Zod declarations owned by the published dependency across partitioned SDK declaration builds, and compile an isolated external consumer that mixes the documented runtime and schema entrypoints. * fix(plugin-sdk): narrow package type regression guard Keep the cross-entrypoint assignment and exported-schema checks while excluding unrelated declaration-library diagnostics. Inline the temporary consumer and carry the current-main unused-import repair required for green CI. --- scripts/check-plugin-sdk-exports.mts | 107 +++++++++++++++++++++++++-- tsdown.config.ts | 8 +- 2 files changed, 106 insertions(+), 9 deletions(-) diff --git a/scripts/check-plugin-sdk-exports.mts b/scripts/check-plugin-sdk-exports.mts index 0e49a80df4eb..5d8304e56b79 100755 --- a/scripts/check-plugin-sdk-exports.mts +++ b/scripts/check-plugin-sdk-exports.mts @@ -3,12 +3,23 @@ /** * Verifies that public plugin-sdk subpaths are present in the compiled dist output. * - * Run after `pnpm build` to catch missing exports or leaked repo-only type aliases + * Run after the package build to catch missing exports or leaked repo-only type aliases * before release. */ -import { readFileSync, existsSync, statSync } from "node:fs"; -import { resolve, dirname, relative, sep } from "node:path"; +import { spawnSync } from "node:child_process"; +import { + existsSync, + mkdirSync, + mkdtempSync, + readFileSync, + rmSync, + statSync, + symlinkSync, + writeFileSync, +} from "node:fs"; +import { tmpdir } from "node:os"; +import { dirname, join, relative, resolve, sep } from "node:path"; import { fileURLToPath, pathToFileURL } from "node:url"; import { MAX_PRIVATE_QA_PUBLIC_PLUGIN_SDK_DECLARATION_BYTES, @@ -20,6 +31,19 @@ import { import { publicPluginSdkEntrypoints, publicPluginSdkSubpaths } from "./lib/plugin-sdk-entries.mts"; const scriptDir = dirname(fileURLToPath(import.meta.url)); +const repoRoot = resolve(scriptDir, ".."); +const nativePreviewPackageJsonPath = resolve( + repoRoot, + "node_modules/@typescript/native-preview/package.json", +); +const nativePreviewPackageJson = JSON.parse(readFileSync(nativePreviewPackageJsonPath, "utf8")) as { + bin?: { tsgo?: string }; +}; +const nativePreviewTsgoBin = nativePreviewPackageJson.bin?.tsgo; +if (!nativePreviewTsgoBin) { + throw new Error("@typescript/native-preview does not declare the tsgo binary"); +} +const tsgoPath = resolve(dirname(nativePreviewPackageJsonPath), nativePreviewTsgoBin); const forbiddenPublicDeclarationSpecifiers = ["@openclaw/llm-core"]; const FORBIDDEN_PUBLIC_PROTOCOL_REGISTRY_RE = /\bdeclare\s+const\s+ProtocolSchemas(?:\$\d+)?\b/u; const RELATIVE_DECLARATION_SPECIFIER_RE = /\b(?:from|import)\s*(?:\(\s*)?["']([^"']+)["']/gu; @@ -36,6 +60,77 @@ const requiredSubpathExports: Record = { let missing = 0; +{ + const tempRoot = mkdtempSync(join(tmpdir(), "openclaw-plugin-sdk-consumer-")); + const consumerRoot = join(tempRoot, "consumer"); + try { + mkdirSync(consumerRoot, { recursive: true }); + writeFileSync( + join(consumerRoot, "index.ts"), + `import { buildChannelConfigSchema, DmPolicySchema } from "openclaw/plugin-sdk/channel-config-schema"; +import { defineChannelPluginEntry } from "openclaw/plugin-sdk/core"; +import { createPluginRuntimeStore, type PluginRuntime } from "openclaw/plugin-sdk/runtime-store"; +import { z } from "openclaw/plugin-sdk/zod"; + +const runtimeStore = createPluginRuntimeStore({ + pluginId: "package-consumer", + errorMessage: "package consumer runtime not initialized", +}); +export const configSchema = buildChannelConfigSchema( + z.object({ dmPolicy: DmPolicySchema.optional() }), +); + +declare const plugin: Parameters[0]["plugin"]; +export default defineChannelPluginEntry({ + id: "package-consumer", + name: "Package Consumer", + description: "Published Plugin SDK declaration compatibility fixture", + plugin, + setRuntime: runtimeStore.setRuntime, +}); +`, + ); + writeFileSync(join(consumerRoot, "package.json"), '{"private":true,"type":"module"}\n'); + writeFileSync( + join(consumerRoot, "tsconfig.json"), + `{ + "compilerOptions": { + "lib": ["DOM", "DOM.Iterable", "ES2023"], + "module": "NodeNext", + "moduleResolution": "NodeNext", + "noEmit": true, + "skipLibCheck": true, + "strict": true, + "types": [] + }, + "include": ["index.ts"] +} +`, + ); + const openclawPackagePath = join(consumerRoot, "node_modules", "openclaw"); + mkdirSync(dirname(openclawPackagePath), { recursive: true }); + symlinkSync(repoRoot, openclawPackagePath, process.platform === "win32" ? "junction" : "dir"); + + const result = spawnSync( + process.execPath, + [tsgoPath, "-p", join(consumerRoot, "tsconfig.json"), "--pretty", "false"], + { cwd: consumerRoot, encoding: "utf8" }, + ); + if (result.error) { + console.error("BROKEN PLUGIN SDK CONSUMER: failed to start tsgo"); + console.error(result.error.message); + missing += 1; + } else if (result.status !== 0) { + console.error("BROKEN PLUGIN SDK CONSUMER: mixed public subpaths are not assignable"); + process.stderr.write(result.stdout || ""); + process.stderr.write(result.stderr || ""); + missing += 1; + } + } finally { + rmSync(tempRoot, { force: true, recursive: true }); + } +} + for (const entry of publicPluginSdkSubpaths) { const jsPath = resolve(scriptDir, "..", "dist", "plugin-sdk", `${entry}.js`); const dtsPath = resolve(scriptDir, "..", "dist", "plugin-sdk", `${entry}.d.ts`); @@ -151,11 +246,9 @@ if (declarationBudget.shouldFail) { } if (missing > 0) { - console.error( - `\nERROR: ${missing} required plugin-sdk artifact(s) missing (named exports or subpath files).`, - ); + console.error(`\nERROR: ${missing} plugin-sdk artifact check(s) failed.`); console.error("This will break published plugin-sdk artifacts."); - console.error("Check generated d.ts rewrites, subpath entries, and rebuild."); + console.error("Check generated d.ts rewrites, subpath entries, type compatibility, and rebuild."); process.exit(1); } diff --git a/tsdown.config.ts b/tsdown.config.ts index de062fe60e19..4ee8cb9b3e46 100644 --- a/tsdown.config.ts +++ b/tsdown.config.ts @@ -233,6 +233,10 @@ function shouldNeverBundleDependency(id: string): boolean { }); } +function shouldNeverBundleDeclarationDependency(id: string): boolean { + return shouldNeverBundleDependency(id) || id === "zod" || id.startsWith("zod/"); +} + function shouldAlwaysBundleDependency(id: string): boolean { return ( id === "openclaw/plugin-sdk/ssrf-runtime-internal" || @@ -613,8 +617,8 @@ const unifiedDistEntries = buildUnifiedDistEntries(); const unifiedDeps = { alwaysBundle: shouldAlwaysBundleDependency, neverBundle: shouldNeverBundleDependency, - // Keep dts generation from inlining externalized package types. - dts: { neverBundle: shouldNeverBundleDependency }, + // Keep dependency-owned types canonical across independently emitted declaration graphs. + dts: { neverBundle: shouldNeverBundleDeclarationDependency }, }; const configs = [