diff --git a/package.json b/package.json index 791e113d85e..4d1a1803801 100644 --- a/package.json +++ b/package.json @@ -1424,6 +1424,7 @@ "lint:extensions:bundled": "node scripts/run-bundled-extension-oxlint.mjs", "lint:extensions:channels": "node scripts/run-extension-channel-oxlint.mjs", "lint:extensions:no-plugin-sdk-internal": "node scripts/check-extension-plugin-sdk-boundary.mjs --mode=plugin-sdk-internal", + "lint:extensions:no-plugin-sdk-wildcard-reexports": "node scripts/check-plugin-sdk-wildcard-reexports.mjs", "lint:extensions:no-relative-outside-package": "node scripts/check-extension-plugin-sdk-boundary.mjs --mode=relative-outside-package", "lint:extensions:no-src-outside-plugin-sdk": "node scripts/check-extension-plugin-sdk-boundary.mjs --mode=src-outside-plugin-sdk", "lint:fix": "node scripts/run-oxlint.mjs --fix && pnpm format", diff --git a/scripts/check-changed.mjs b/scripts/check-changed.mjs index 5f98b35fd08..158893f7efa 100644 --- a/scripts/check-changed.mjs +++ b/scripts/check-changed.mjs @@ -55,6 +55,7 @@ export function createChangedCheckPlan(result, options = {}) { add("conflict markers", ["check:no-conflict-markers"]); add("changelog attributions", ["check:changelog-attributions"]); + add("plugin-sdk wildcard re-exports", ["lint:extensions:no-plugin-sdk-wildcard-reexports"]); if (result.docsOnly) { return { diff --git a/scripts/check-plugin-sdk-wildcard-reexports.mjs b/scripts/check-plugin-sdk-wildcard-reexports.mjs new file mode 100644 index 00000000000..fa48fadaf79 --- /dev/null +++ b/scripts/check-plugin-sdk-wildcard-reexports.mjs @@ -0,0 +1,85 @@ +#!/usr/bin/env node + +import fs from "node:fs/promises"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; + +const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."); +const extensionsRoot = path.join(repoRoot, "extensions"); + +const WILDCARD_PLUGIN_SDK_REEXPORT_PATTERN = + /^\s*export\s+(?:type\s+)?\*\s+from\s+["']openclaw\/plugin-sdk\//u; + +async function listExtensionApiFiles(rootDir = extensionsRoot) { + const entries = await fs.readdir(rootDir, { withFileTypes: true }); + const files = []; + for (const entry of entries) { + if (!entry.isDirectory()) { + continue; + } + for (const fileName of ["api.ts", "runtime-api.ts"]) { + const filePath = path.join(rootDir, entry.name, fileName); + try { + const stat = await fs.stat(filePath); + if (stat.isFile()) { + files.push(filePath); + } + } catch (error) { + if (!error || typeof error !== "object" || !("code" in error) || error.code !== "ENOENT") { + throw error; + } + } + } + } + return files.toSorted((left, right) => left.localeCompare(right)); +} + +export function findPluginSdkWildcardReexports(source) { + return source + .split(/\r?\n/u) + .map((text, index) => ({ line: index + 1, text })) + .filter(({ text }) => WILDCARD_PLUGIN_SDK_REEXPORT_PATTERN.test(text)); +} + +export async function collectPluginSdkWildcardReexports(rootDir = repoRoot) { + const files = await listExtensionApiFiles(path.join(rootDir, "extensions")); + const violations = []; + for (const filePath of files) { + const source = await fs.readFile(filePath, "utf8"); + for (const match of findPluginSdkWildcardReexports(source)) { + violations.push({ + file: path.relative(rootDir, filePath).split(path.sep).join("/"), + line: match.line, + text: match.text.trim(), + }); + } + } + return violations; +} + +export async function main(argv = process.argv.slice(2), io = process) { + const json = argv.includes("--json"); + const violations = await collectPluginSdkWildcardReexports(); + + if (json) { + io.stdout.write(`${JSON.stringify(violations, null, 2)}\n`); + return violations.length === 0 ? 0 : 1; + } + + if (violations.length === 0) { + io.stdout.write("No plugin-sdk wildcard re-exports found in extension API barrels.\n"); + return 0; + } + + io.stderr.write("Found plugin-sdk wildcard re-exports in extension API barrels:\n"); + for (const violation of violations) { + io.stderr.write(`- ${violation.file}:${violation.line} ${violation.text}\n`); + } + io.stderr.write("Use explicit named exports from the narrow SDK subpath instead.\n"); + return 1; +} + +if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) { + const exitCode = await main(); + process.exit(exitCode); +} diff --git a/scripts/check.mjs b/scripts/check.mjs index ccab6e4f411..6bb4f4aafa0 100644 --- a/scripts/check.mjs +++ b/scripts/check.mjs @@ -31,6 +31,10 @@ export async function main(argv = process.argv.slice(2)) { commands: [ { name: "conflict markers", args: ["check:no-conflict-markers"] }, { name: "changelog attributions", args: ["check:changelog-attributions"] }, + { + name: "plugin-sdk wildcard re-exports", + args: ["lint:extensions:no-plugin-sdk-wildcard-reexports"], + }, { name: "tool display", args: ["tool-display:check"] }, { name: "host env policy", args: ["check:host-env-policy:swift"] }, ], diff --git a/test/scripts/changed-lanes.test.ts b/test/scripts/changed-lanes.test.ts index f50e04ab415..fc153b34e05 100644 --- a/test/scripts/changed-lanes.test.ts +++ b/test/scripts/changed-lanes.test.ts @@ -259,6 +259,7 @@ describe("scripts/changed-lanes", () => { expect(plan.commands.map((command) => command.name)).toEqual([ "conflict markers", "changelog attributions", + "plugin-sdk wildcard re-exports", "typecheck core tests", "lint core", "lint scripts", @@ -546,6 +547,7 @@ describe("scripts/changed-lanes", () => { expect(plan.commands.map((command) => command.args[0])).toEqual([ "check:no-conflict-markers", "check:changelog-attributions", + "lint:extensions:no-plugin-sdk-wildcard-reexports", "release-metadata:check", "ios:version:check", "config:schema:check", @@ -677,6 +679,10 @@ describe("scripts/changed-lanes", () => { expect(plan.commands).toEqual([ { name: "conflict markers", args: ["check:no-conflict-markers"] }, { name: "changelog attributions", args: ["check:changelog-attributions"] }, + { + name: "plugin-sdk wildcard re-exports", + args: ["lint:extensions:no-plugin-sdk-wildcard-reexports"], + }, ]); }); @@ -688,6 +694,10 @@ describe("scripts/changed-lanes", () => { expect(plan.commands).toEqual([ { name: "conflict markers", args: ["check:no-conflict-markers"] }, { name: "changelog attributions", args: ["check:changelog-attributions"] }, + { + name: "plugin-sdk wildcard re-exports", + args: ["lint:extensions:no-plugin-sdk-wildcard-reexports"], + }, ]); }); }); diff --git a/test/scripts/check-plugin-sdk-wildcard-reexports.test.ts b/test/scripts/check-plugin-sdk-wildcard-reexports.test.ts new file mode 100644 index 00000000000..635a9ce1dc7 --- /dev/null +++ b/test/scripts/check-plugin-sdk-wildcard-reexports.test.ts @@ -0,0 +1,31 @@ +import { describe, expect, it } from "vitest"; +import { findPluginSdkWildcardReexports } from "../../scripts/check-plugin-sdk-wildcard-reexports.mjs"; + +describe("check-plugin-sdk-wildcard-reexports", () => { + it("flags wildcard re-exports from plugin-sdk subpaths", () => { + expect( + findPluginSdkWildcardReexports( + [ + 'export * from "openclaw/plugin-sdk/foo";', + 'export type * from "openclaw/plugin-sdk/bar";', + 'export { named } from "openclaw/plugin-sdk/foo";', + ].join("\n"), + ), + ).toEqual([ + { line: 1, text: 'export * from "openclaw/plugin-sdk/foo";' }, + { line: 2, text: 'export type * from "openclaw/plugin-sdk/bar";' }, + ]); + }); + + it("allows explicit SDK exports and local wildcard barrels", () => { + expect( + findPluginSdkWildcardReexports( + [ + 'export { named } from "openclaw/plugin-sdk/foo";', + 'export type { Named } from "openclaw/plugin-sdk/foo";', + 'export * from "./src/runtime-api.js";', + ].join("\n"), + ), + ).toEqual([]); + }); +});