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 = [