mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 11:00:42 +00:00
fix(plugins): build package-local npm runtimes
This commit is contained in:
@@ -2,6 +2,7 @@ import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
||||
import { dirname, join } from "node:path";
|
||||
import { afterEach, describe, expect, it } from "vitest";
|
||||
import {
|
||||
resolveAugmentedPluginNpmPackageJson,
|
||||
resolveAugmentedPluginNpmManifest,
|
||||
withAugmentedPluginNpmManifestForPackage,
|
||||
} from "../scripts/lib/plugin-npm-package-manifest.mjs";
|
||||
@@ -48,6 +49,28 @@ function writeFileText(filePath: string, text: string): void {
|
||||
writeFileSync(filePath, text, "utf8");
|
||||
}
|
||||
|
||||
function writePublishablePluginPackage(repoDir: string): string {
|
||||
const packageDir = join(repoDir, "extensions", "diffs");
|
||||
mkdirSync(packageDir, { recursive: true });
|
||||
writeJsonFile(join(packageDir, "package.json"), {
|
||||
name: "@openclaw/diffs",
|
||||
version: "2026.5.3",
|
||||
type: "module",
|
||||
openclaw: {
|
||||
extensions: ["./index.ts"],
|
||||
setupEntry: "./setup-entry.ts",
|
||||
release: {
|
||||
publishToNpm: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
writeJsonFile(join(packageDir, "openclaw.plugin.json"), { id: "diffs" });
|
||||
writeFileText(join(packageDir, "README.md"), "# Diffs\n");
|
||||
writeFileText(join(packageDir, "SKILL.md"), "# Diffs Skill\n");
|
||||
writeFileText(join(packageDir, "skills", "diffs", "SKILL.md"), "# Diffs Skill\n");
|
||||
return packageDir;
|
||||
}
|
||||
|
||||
describe("plugin npm package manifest staging", () => {
|
||||
it("overlays generated channel configs while packing and restores source manifest", () => {
|
||||
const repoDir = makeTempRepoRoot(tempDirs, "openclaw-plugin-npm-package-manifest-");
|
||||
@@ -90,4 +113,49 @@ describe("plugin npm package manifest staging", () => {
|
||||
});
|
||||
expect(readFileSync(join(packageDir, "openclaw.plugin.json"), "utf8")).toBe(originalText);
|
||||
});
|
||||
|
||||
it("overlays package-local runtime metadata while packing and restores source package json", () => {
|
||||
const repoDir = makeTempRepoRoot(tempDirs, "openclaw-plugin-npm-package-runtime-");
|
||||
const packageDir = writePublishablePluginPackage(repoDir);
|
||||
writeFileText(join(packageDir, "dist", "index.js"), "export {};\n");
|
||||
writeFileText(join(packageDir, "dist", "setup-entry.js"), "export {};\n");
|
||||
|
||||
const resolved = resolveAugmentedPluginNpmPackageJson({
|
||||
repoRoot: repoDir,
|
||||
packageDir,
|
||||
});
|
||||
expect(resolved.changed).toBe(true);
|
||||
expect(resolved.packageJson).toMatchObject({
|
||||
files: ["dist/**", "openclaw.plugin.json", "README.md", "SKILL.md", "skills/**"],
|
||||
openclaw: {
|
||||
runtimeExtensions: ["./dist/index.js"],
|
||||
runtimeSetupEntry: "./dist/setup-entry.js",
|
||||
},
|
||||
});
|
||||
|
||||
const originalText = readFileSync(join(packageDir, "package.json"), "utf8");
|
||||
withAugmentedPluginNpmManifestForPackage({ repoRoot: repoDir, packageDir }, () => {
|
||||
const stagedPackageJson = JSON.parse(readFileSync(join(packageDir, "package.json"), "utf8"));
|
||||
expect(stagedPackageJson.openclaw.extensions).toEqual(["./index.ts"]);
|
||||
expect(stagedPackageJson.openclaw.runtimeExtensions).toEqual(["./dist/index.js"]);
|
||||
expect(stagedPackageJson.openclaw.runtimeSetupEntry).toBe("./dist/setup-entry.js");
|
||||
expect(stagedPackageJson.files).toContain("dist/**");
|
||||
expect(stagedPackageJson.files).toContain("skills/**");
|
||||
});
|
||||
expect(readFileSync(join(packageDir, "package.json"), "utf8")).toBe(originalText);
|
||||
});
|
||||
|
||||
it("refuses to pack publishable plugins before package-local runtime files exist", () => {
|
||||
const repoDir = makeTempRepoRoot(tempDirs, "openclaw-plugin-npm-package-runtime-missing-");
|
||||
const packageDir = writePublishablePluginPackage(repoDir);
|
||||
|
||||
expect(() =>
|
||||
resolveAugmentedPluginNpmPackageJson({
|
||||
repoRoot: repoDir,
|
||||
packageDir,
|
||||
}),
|
||||
).toThrow(
|
||||
"package-local plugin runtime is missing for diffs: ./dist/index.js, ./dist/setup-entry.js",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
79
test/plugin-npm-runtime-build.test.ts
Normal file
79
test/plugin-npm-runtime-build.test.ts
Normal file
@@ -0,0 +1,79 @@
|
||||
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";
|
||||
|
||||
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();
|
||||
expect(packageDirs.length).toBeGreaterThan(0);
|
||||
|
||||
const plans = packageDirs.map((packageDir) =>
|
||||
resolvePluginNpmRuntimeBuildPlan({
|
||||
repoRoot,
|
||||
packageDir,
|
||||
}),
|
||||
);
|
||||
expect(plans.filter(Boolean).map((plan) => plan?.pluginDir)).toEqual(
|
||||
packageDirs.map((packageDir) => path.basename(packageDir)),
|
||||
);
|
||||
for (const plan of plans) {
|
||||
expect(plan?.outDir).toBe(path.join(plan?.packageDir ?? "", "dist"));
|
||||
expect(plan?.runtimeExtensions.every((entry) => entry.startsWith("./dist/"))).toBe(true);
|
||||
}
|
||||
});
|
||||
|
||||
it("includes top-level public runtime surfaces and root-build-excluded plugins", () => {
|
||||
const qqbotPlan = resolvePluginNpmRuntimeBuildPlan({
|
||||
repoRoot,
|
||||
packageDir: path.join(repoRoot, "extensions", "qqbot"),
|
||||
});
|
||||
expect(qqbotPlan?.entry).toEqual(
|
||||
expect.objectContaining({
|
||||
index: path.join(repoRoot, "extensions", "qqbot", "index.ts"),
|
||||
"runtime-api": path.join(repoRoot, "extensions", "qqbot", "runtime-api.ts"),
|
||||
"setup-entry": path.join(repoRoot, "extensions", "qqbot", "setup-entry.ts"),
|
||||
}),
|
||||
);
|
||||
expect(qqbotPlan?.runtimeExtensions).toEqual(["./dist/index.js"]);
|
||||
expect(qqbotPlan?.runtimeSetupEntry).toBe("./dist/setup-entry.js");
|
||||
|
||||
const diffsPlan = resolvePluginNpmRuntimeBuildPlan({
|
||||
repoRoot,
|
||||
packageDir: path.join(repoRoot, "extensions", "diffs"),
|
||||
});
|
||||
expect(diffsPlan?.entry).toEqual(
|
||||
expect.objectContaining({
|
||||
api: path.join(repoRoot, "extensions", "diffs", "api.ts"),
|
||||
index: path.join(repoRoot, "extensions", "diffs", "index.ts"),
|
||||
"runtime-api": path.join(repoRoot, "extensions", "diffs", "runtime-api.ts"),
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user