refactor(plugins): centralize npm runtime package planning

This commit is contained in:
Vincent Koc
2026-05-02 22:59:03 -07:00
parent fb6893cf48
commit 25ceffbf25
4 changed files with 86 additions and 94 deletions

View File

@@ -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,

View File

@@ -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,

View File

@@ -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) {

View File

@@ -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<string, unknown> {
return JSON.parse(fs.readFileSync(filePath, "utf8")) as Record<string, unknown>;
}
function isPublishablePluginPackage(packageJson: Record<string, unknown>): 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/**",
]);
});
});