diff --git a/package.json b/package.json index 2e529c8032b..1ecf252da04 100644 --- a/package.json +++ b/package.json @@ -241,6 +241,10 @@ "types": "./dist/plugin-sdk/imessage.d.ts", "default": "./dist/plugin-sdk/imessage.js" }, + "./plugin-sdk/imessage-core": { + "types": "./dist/plugin-sdk/imessage-core.d.ts", + "default": "./dist/plugin-sdk/imessage-core.js" + }, "./plugin-sdk/open-prose": { "types": "./dist/plugin-sdk/open-prose.d.ts", "default": "./dist/plugin-sdk/open-prose.js" @@ -538,7 +542,7 @@ "build:plugin-sdk:dts": "tsc -p tsconfig.plugin-sdk.dts.json", "build:strict-smoke": "pnpm canvas:a2ui:bundle && node scripts/tsdown-build.mjs && node scripts/runtime-postbuild.mjs && pnpm build:plugin-sdk:dts", "canvas:a2ui:bundle": "bash scripts/bundle-a2ui.sh", - "check": "pnpm check:host-env-policy:swift && pnpm check:bundled-provider-auth-env-vars && pnpm format:check && pnpm tsgo && pnpm plugin-sdk:check-exports && pnpm lint && pnpm lint:tmp:no-random-messaging && pnpm lint:tmp:channel-agnostic-boundaries && pnpm lint:tmp:no-raw-channel-fetch && pnpm lint:agent:ingress-owner && pnpm lint:plugins:no-register-http-handler && pnpm lint:plugins:no-monolithic-plugin-sdk-entry-imports && pnpm lint:plugins:no-extension-src-imports && pnpm lint:plugins:no-extension-test-core-imports && pnpm lint:plugins:no-extension-imports && pnpm lint:extensions:no-src-outside-plugin-sdk && pnpm lint:extensions:no-plugin-sdk-internal && pnpm lint:extensions:no-relative-outside-package && pnpm lint:web-search-provider-boundaries && pnpm lint:webhook:no-low-level-body-read && pnpm lint:auth:no-pairing-store-group && pnpm lint:auth:pairing-account-scope", + "check": "pnpm check:host-env-policy:swift && pnpm check:bundled-provider-auth-env-vars && pnpm format:check && pnpm tsgo && pnpm plugin-sdk:check-exports && pnpm lint && pnpm lint:tmp:no-random-messaging && pnpm lint:tmp:channel-agnostic-boundaries && pnpm lint:tmp:no-raw-channel-fetch && pnpm lint:agent:ingress-owner && pnpm lint:plugins:no-register-http-handler && pnpm lint:plugins:no-monolithic-plugin-sdk-entry-imports && pnpm lint:plugins:no-extension-src-imports && pnpm lint:plugins:no-extension-test-core-imports && pnpm lint:plugins:no-extension-imports && pnpm lint:plugins:plugin-sdk-subpaths-exported && pnpm lint:extensions:no-src-outside-plugin-sdk && pnpm lint:extensions:no-plugin-sdk-internal && pnpm lint:extensions:no-relative-outside-package && pnpm lint:web-search-provider-boundaries && pnpm lint:webhook:no-low-level-body-read && pnpm lint:auth:no-pairing-store-group && pnpm lint:auth:pairing-account-scope", "check:bundled-provider-auth-env-vars": "node scripts/generate-bundled-provider-auth-env-vars.mjs --check", "check:docs": "pnpm format:docs:check && pnpm lint:docs && pnpm docs:check-i18n-glossary && pnpm docs:check-links", "check:host-env-policy:swift": "node scripts/generate-host-env-security-policy-swift.mjs --check", @@ -599,6 +603,7 @@ "lint:plugins:no-extension-test-core-imports": "node --import tsx scripts/check-no-extension-test-core-imports.ts", "lint:plugins:no-monolithic-plugin-sdk-entry-imports": "node --import tsx scripts/check-no-monolithic-plugin-sdk-entry-imports.ts", "lint:plugins:no-register-http-handler": "node scripts/check-no-register-http-handler.mjs", + "lint:plugins:plugin-sdk-subpaths-exported": "node scripts/check-plugin-sdk-subpath-exports.mjs", "lint:swift": "swiftlint lint --config .swiftlint.yml && (cd apps/ios && swiftlint lint --config .swiftlint.yml)", "lint:tmp:channel-agnostic-boundaries": "node scripts/check-channel-agnostic-boundaries.mjs", "lint:tmp:no-random-messaging": "node scripts/check-no-random-messaging-tmp.mjs", diff --git a/scripts/check-plugin-sdk-subpath-exports.mjs b/scripts/check-plugin-sdk-subpath-exports.mjs new file mode 100644 index 00000000000..07094e18a3b --- /dev/null +++ b/scripts/check-plugin-sdk-subpath-exports.mjs @@ -0,0 +1,146 @@ +#!/usr/bin/env node + +import { readFileSync } from "node:fs"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; +import ts from "typescript"; +import { + collectTypeScriptFilesFromRoots, + resolveSourceRoots, + toLine, +} from "./lib/ts-guard-utils.mjs"; + +const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."); +const scanRoots = resolveSourceRoots(repoRoot, ["src", "extensions", "scripts", "test"]); + +function readPackageExports() { + const packageJson = JSON.parse(readFileSync(path.join(repoRoot, "package.json"), "utf8")); + return new Set( + Object.keys(packageJson.exports ?? {}) + .filter((key) => key.startsWith("./plugin-sdk/")) + .map((key) => key.slice("./plugin-sdk/".length)), + ); +} + +function readEntrypoints() { + const entrypoints = JSON.parse( + readFileSync(path.join(repoRoot, "scripts/lib/plugin-sdk-entrypoints.json"), "utf8"), + ); + return new Set(entrypoints.filter((entry) => entry !== "index")); +} + +function normalizePath(filePath) { + return path.relative(repoRoot, filePath).split(path.sep).join("/"); +} + +function parsePluginSdkSubpath(specifier) { + if (!specifier.startsWith("openclaw/plugin-sdk/")) { + return null; + } + const subpath = specifier.slice("openclaw/plugin-sdk/".length); + return subpath || null; +} + +function compareEntries(left, right) { + return ( + left.file.localeCompare(right.file) || + left.line - right.line || + left.kind.localeCompare(right.kind) || + left.specifier.localeCompare(right.specifier) || + left.subpath.localeCompare(right.subpath) + ); +} + +async function collectViolations() { + const entrypoints = readEntrypoints(); + const exports = readPackageExports(); + const files = (await collectTypeScriptFilesFromRoots(scanRoots, { includeTests: true })).toSorted( + (left, right) => normalizePath(left).localeCompare(normalizePath(right)), + ); + const violations = []; + + for (const filePath of files) { + const sourceText = readFileSync(filePath, "utf8"); + const sourceFile = ts.createSourceFile( + filePath, + sourceText, + ts.ScriptTarget.Latest, + true, + filePath.endsWith(".tsx") ? ts.ScriptKind.TSX : ts.ScriptKind.TS, + ); + + function push(kind, specifierNode, specifier) { + const subpath = parsePluginSdkSubpath(specifier); + if (!subpath) { + return; + } + + const missingFrom = []; + if (!entrypoints.has(subpath)) { + missingFrom.push("scripts/lib/plugin-sdk-entrypoints.json"); + } + if (!exports.has(subpath)) { + missingFrom.push("package.json exports"); + } + if (missingFrom.length === 0) { + return; + } + + violations.push({ + file: normalizePath(filePath), + line: toLine(sourceFile, specifierNode), + kind, + specifier, + subpath, + missingFrom, + }); + } + + function visit(node) { + if (ts.isImportDeclaration(node) && ts.isStringLiteral(node.moduleSpecifier)) { + push("import", node.moduleSpecifier, node.moduleSpecifier.text); + } else if ( + ts.isExportDeclaration(node) && + node.moduleSpecifier && + ts.isStringLiteral(node.moduleSpecifier) + ) { + push("export", node.moduleSpecifier, node.moduleSpecifier.text); + } else if ( + ts.isCallExpression(node) && + node.expression.kind === ts.SyntaxKind.ImportKeyword && + node.arguments.length === 1 && + ts.isStringLiteral(node.arguments[0]) + ) { + push("dynamic-import", node.arguments[0], node.arguments[0].text); + } + ts.forEachChild(node, visit); + } + + visit(sourceFile); + } + + return violations.toSorted(compareEntries); +} + +async function main() { + const violations = await collectViolations(); + if (violations.length === 0) { + console.log("OK: all referenced openclaw/plugin-sdk/ imports are exported."); + return; + } + + console.error( + "Rule: every referenced openclaw/plugin-sdk/ must exist in the public package exports.", + ); + for (const violation of violations) { + console.error( + `- ${violation.file}:${violation.line} [${violation.kind}] ${violation.specifier} missing from ${violation.missingFrom.join(" and ")}`, + ); + } + process.exit(1); +} + +main().catch((error) => { + console.error(error); + process.exit(1); +}); diff --git a/scripts/lib/plugin-sdk-entrypoints.json b/scripts/lib/plugin-sdk-entrypoints.json index 97658712de2..da2395758c5 100644 --- a/scripts/lib/plugin-sdk-entrypoints.json +++ b/scripts/lib/plugin-sdk-entrypoints.json @@ -50,6 +50,7 @@ "slack", "slack-core", "imessage", + "imessage-core", "open-prose", "phone-control", "qwen-portal-auth",