diff --git a/scripts/check-plugin-npm-runtime-builds.mjs b/scripts/check-plugin-npm-runtime-builds.mjs index c9d04a640a2..b5a6c2d62de 100644 --- a/scripts/check-plugin-npm-runtime-builds.mjs +++ b/scripts/check-plugin-npm-runtime-builds.mjs @@ -5,32 +5,11 @@ import path from "node:path"; import { pathToFileURL } from "node:url"; import { buildPluginNpmRuntime, + listPluginNpmRuntimeBuildOutputs, + listPublishablePluginPackageDirs, resolvePluginNpmRuntimeBuildPlan, } from "./lib/plugin-npm-runtime-build.mjs"; -function readJsonFile(filePath) { - return JSON.parse(fs.readFileSync(filePath, "utf8")); -} - -function isPublishablePluginPackage(packageJson) { - return packageJson.openclaw?.release?.publishToNpm === true; -} - -function listPublishablePluginPackageDirs(repoRoot) { - const extensionsRoot = path.join(repoRoot, "extensions"); - return fs - .readdirSync(extensionsRoot, { withFileTypes: true }) - .filter((entry) => entry.isDirectory()) - .map((entry) => path.join("extensions", entry.name)) - .filter((packageDir) => { - const packageJsonPath = path.join(repoRoot, packageDir, "package.json"); - return ( - fs.existsSync(packageJsonPath) && isPublishablePluginPackage(readJsonFile(packageJsonPath)) - ); - }) - .toSorted((left, right) => left.localeCompare(right)); -} - function parseArgs(argv) { const packageDirs = []; for (let index = 0; index < argv.length; index += 1) { @@ -51,18 +30,12 @@ function parseArgs(argv) { return { packageDirs }; } -function listMissingRuntimeOutputs(plan) { - return Object.keys(plan.entry) - .map((entryKey) => path.join(plan.outDir, `${entryKey}.js`)) - .filter((filePath) => !fs.existsSync(filePath)); -} - export async function checkPluginNpmRuntimeBuilds(params = {}) { const repoRoot = path.resolve(params.repoRoot ?? "."); const packageDirs = params.packageDirs?.length > 0 ? params.packageDirs - : listPublishablePluginPackageDirs(repoRoot); + : listPublishablePluginPackageDirs({ repoRoot }); const rows = []; for (const packageDir of packageDirs) { const plan = resolvePluginNpmRuntimeBuildPlan({ repoRoot, packageDir }); @@ -74,13 +47,12 @@ export async function checkPluginNpmRuntimeBuilds(params = {}) { packageDir, logLevel: params.logLevel ?? "warn", }); - const missing = listMissingRuntimeOutputs(result); + const missing = listPluginNpmRuntimeBuildOutputs(result).filter( + (runtimePath) => + !fs.existsSync(path.join(result.packageDir, runtimePath.replace(/^\.\//u, ""))), + ); if (missing.length > 0) { - throw new Error( - `${packageDir} missing built runtime outputs: ${missing - .map((filePath) => path.relative(repoRoot, filePath)) - .join(", ")}`, - ); + throw new Error(`${packageDir} missing built runtime outputs: ${missing.join(", ")}`); } rows.push({ pluginDir: result.pluginDir, diff --git a/scripts/lib/plugin-npm-package-manifest.mjs b/scripts/lib/plugin-npm-package-manifest.mjs index 72283b8054c..b1d1b4daa19 100644 --- a/scripts/lib/plugin-npm-package-manifest.mjs +++ b/scripts/lib/plugin-npm-package-manifest.mjs @@ -3,7 +3,10 @@ import fs from "node:fs"; import path from "node:path"; import { pathToFileURL } from "node:url"; import JSON5 from "json5"; -import { resolvePluginNpmRuntimeBuildPlan } from "./plugin-npm-runtime-build.mjs"; +import { + listPluginNpmRuntimeBuildOutputs, + resolvePluginNpmRuntimeBuildPlan, +} from "./plugin-npm-runtime-build.mjs"; const GENERATED_BUNDLED_CHANNEL_CONFIG_METADATA_PATH = "src/config/bundled-channel-config-metadata.generated.ts"; @@ -28,34 +31,8 @@ function packageRelativePathExists(packageDir, relativePath) { return fs.existsSync(path.join(packageDir, relativePath)); } -function mergePackageFiles(packageDir, files) { - const merged = new Set( - Array.isArray(files) ? files.filter((entry) => typeof entry === "string") : [], - ); - merged.add("dist/**"); - if (packageRelativePathExists(packageDir, "openclaw.plugin.json")) { - merged.add("openclaw.plugin.json"); - } - if (packageRelativePathExists(packageDir, "README.md")) { - merged.add("README.md"); - } - if (packageRelativePathExists(packageDir, "SKILL.md")) { - merged.add("SKILL.md"); - } - if (packageRelativePathExists(packageDir, "skills")) { - merged.add("skills/**"); - } - return [...merged]; -} - -function listRuntimeBuildOutputs(plan) { - return Object.keys(plan.entry) - .map((entryKey) => `./dist/${entryKey}.js`) - .toSorted((left, right) => left.localeCompare(right)); -} - function assertPluginNpmRuntimeBuildExists(plan) { - const missing = listRuntimeBuildOutputs(plan).filter( + const missing = listPluginNpmRuntimeBuildOutputs(plan).filter( (runtimePath) => !packageRelativePathExists(plan.packageDir, runtimePath.replace(/^\.\//u, "")), ); if (missing.length > 0) { @@ -98,7 +75,7 @@ export function resolveAugmentedPluginNpmPackageJson(params) { const packageJson = { ...plan.packageJson, - files: mergePackageFiles(packageDir, plan.packageJson.files), + files: plan.packageFiles, openclaw: { ...plan.packageJson.openclaw, runtimeExtensions: plan.runtimeExtensions, diff --git a/scripts/lib/plugin-npm-runtime-build.mjs b/scripts/lib/plugin-npm-runtime-build.mjs index 3d9af19ce5d..fbff5d162ec 100644 --- a/scripts/lib/plugin-npm-runtime-build.mjs +++ b/scripts/lib/plugin-npm-runtime-build.mjs @@ -16,6 +16,10 @@ function readJsonFile(filePath) { return JSON.parse(fs.readFileSync(filePath, "utf8")); } +export function isPublishablePluginPackage(packageJson) { + return packageJson.openclaw?.release?.publishToNpm === true; +} + function normalizePackageEntry(value) { return typeof value === "string" ? value.trim().replaceAll("\\", "/") : ""; } @@ -64,6 +68,54 @@ function resolvePackageDir(repoRoot, packageDir) { return path.isAbsolute(packageDir) ? packageDir : path.resolve(repoRoot, packageDir); } +function packageRelativePathExists(packageDir, relativePath) { + return fs.existsSync(path.join(packageDir, relativePath)); +} + +export function listPublishablePluginPackageDirs(params = {}) { + const repoRoot = path.resolve(params.repoRoot ?? "."); + const extensionsRoot = path.join(repoRoot, "extensions"); + return fs + .readdirSync(extensionsRoot, { withFileTypes: true }) + .filter((entry) => entry.isDirectory()) + .map((entry) => path.join("extensions", entry.name)) + .filter((packageDir) => { + const packageJsonPath = path.join(repoRoot, packageDir, "package.json"); + return ( + fs.existsSync(packageJsonPath) && isPublishablePluginPackage(readJsonFile(packageJsonPath)) + ); + }) + .toSorted((left, right) => left.localeCompare(right)); +} + +export function listPluginNpmRuntimeBuildOutputs(plan) { + return Object.keys(plan.entry) + .map((entryKey) => `./dist/${entryKey}.js`) + .toSorted((left, right) => left.localeCompare(right)); +} + +export function resolvePluginNpmRuntimePackageFiles(plan) { + const merged = new Set( + Array.isArray(plan.packageJson.files) + ? plan.packageJson.files.filter((entry) => typeof entry === "string") + : [], + ); + merged.add("dist/**"); + if (packageRelativePathExists(plan.packageDir, "openclaw.plugin.json")) { + merged.add("openclaw.plugin.json"); + } + if (packageRelativePathExists(plan.packageDir, "README.md")) { + merged.add("README.md"); + } + if (packageRelativePathExists(plan.packageDir, "SKILL.md")) { + merged.add("SKILL.md"); + } + if (packageRelativePathExists(plan.packageDir, "skills")) { + merged.add("skills/**"); + } + return [...merged]; +} + export function resolvePluginNpmRuntimeBuildPlan(params) { const repoRoot = path.resolve(params.repoRoot ?? "."); const packageDir = resolvePackageDir(repoRoot, params.packageDir); @@ -72,7 +124,7 @@ export function resolvePluginNpmRuntimeBuildPlan(params) { return null; } const packageJson = readJsonFile(packageJsonPath); - if (packageJson.openclaw?.release?.publishToNpm !== true) { + if (!isPublishablePluginPackage(packageJson)) { return null; } @@ -96,7 +148,7 @@ export function resolvePluginNpmRuntimeBuildPlan(params) { ]), ); - return { + const plan = { repoRoot, packageDir, pluginDir, @@ -115,6 +167,11 @@ export function resolvePluginNpmRuntimeBuildPlan(params) { ? toPackageRuntimeEntry(packageJson.openclaw.setupEntry) : undefined, }; + return { + ...plan, + runtimeBuildOutputs: listPluginNpmRuntimeBuildOutputs(plan), + packageFiles: resolvePluginNpmRuntimePackageFiles(plan), + }; } export async function buildPluginNpmRuntime(params) { diff --git a/test/plugin-npm-runtime-build.test.ts b/test/plugin-npm-runtime-build.test.ts index 77f37e0835b..b1f3102e24c 100644 --- a/test/plugin-npm-runtime-build.test.ts +++ b/test/plugin-npm-runtime-build.test.ts @@ -1,37 +1,15 @@ -import fs from "node:fs"; import path from "node:path"; import { describe, expect, it } from "vitest"; -import { resolvePluginNpmRuntimeBuildPlan } from "../scripts/lib/plugin-npm-runtime-build.mjs"; +import { + listPublishablePluginPackageDirs, + resolvePluginNpmRuntimeBuildPlan, +} from "../scripts/lib/plugin-npm-runtime-build.mjs"; const repoRoot = path.resolve(import.meta.dirname, ".."); -function readJsonFile(filePath: string): Record { - return JSON.parse(fs.readFileSync(filePath, "utf8")) as Record; -} - -function isPublishablePluginPackage(packageJson: Record): boolean { - const openclaw = packageJson.openclaw as { release?: { publishToNpm?: unknown } } | undefined; - return openclaw?.release?.publishToNpm === true; -} - -function listPublishablePluginPackageDirs(): string[] { - const extensionsRoot = path.join(repoRoot, "extensions"); - return fs - .readdirSync(extensionsRoot, { withFileTypes: true }) - .filter((dirent) => dirent.isDirectory()) - .map((dirent) => path.join(extensionsRoot, dirent.name)) - .filter((packageDir) => { - const packageJsonPath = path.join(packageDir, "package.json"); - return ( - fs.existsSync(packageJsonPath) && isPublishablePluginPackage(readJsonFile(packageJsonPath)) - ); - }) - .toSorted((left, right) => left.localeCompare(right)); -} - describe("plugin npm runtime build planning", () => { it("plans package-local runtime entries for every publishable plugin package", () => { - const packageDirs = listPublishablePluginPackageDirs(); + const packageDirs = listPublishablePluginPackageDirs({ repoRoot }); expect(packageDirs.length).toBeGreaterThan(0); const plans = packageDirs.map((packageDir) => @@ -46,6 +24,8 @@ describe("plugin npm runtime build planning", () => { for (const plan of plans) { expect(plan?.outDir).toBe(path.join(plan?.packageDir ?? "", "dist")); expect(plan?.runtimeExtensions.every((entry) => entry.startsWith("./dist/"))).toBe(true); + expect(plan?.runtimeBuildOutputs.every((entry) => entry.startsWith("./dist/"))).toBe(true); + expect(plan?.packageFiles).toContain("dist/**"); } }); @@ -75,5 +55,11 @@ describe("plugin npm runtime build planning", () => { "runtime-api": path.join(repoRoot, "extensions", "diffs", "runtime-api.ts"), }), ); + expect(diffsPlan?.packageFiles).toEqual([ + "dist/**", + "openclaw.plugin.json", + "README.md", + "skills/**", + ]); }); });