mirror of
https://github.com/openclaw/openclaw.git
synced 2026-03-27 09:21:35 +07:00
fix(release): ship bundled plugins in pack artifacts
This commit is contained in:
4
.github/workflows/ci.yml
vendored
4
.github/workflows/ci.yml
vendored
@@ -187,6 +187,9 @@ jobs:
|
||||
- name: Build dist
|
||||
run: pnpm build
|
||||
|
||||
- name: Build Control UI
|
||||
run: pnpm ui:build
|
||||
|
||||
- name: Upload dist artifact
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
@@ -261,6 +264,7 @@ jobs:
|
||||
cache_key_suffix: "node22"
|
||||
command: |
|
||||
pnpm build
|
||||
pnpm ui:build
|
||||
node openclaw.mjs --help
|
||||
node openclaw.mjs status --json --timeout 1
|
||||
pnpm test:build:singleton
|
||||
|
||||
@@ -189,7 +189,11 @@ export function copyBundledPluginMetadata(params = {}) {
|
||||
const pluginDir = path.join(extensionsRoot, dirent.name);
|
||||
const manifestPath = path.join(pluginDir, "openclaw.plugin.json");
|
||||
const distPluginDir = path.join(distExtensionsRoot, dirent.name);
|
||||
if (!shouldBuildBundledCluster(dirent.name, env)) {
|
||||
const packageJsonPath = path.join(pluginDir, "package.json");
|
||||
const packageJson = fs.existsSync(packageJsonPath)
|
||||
? JSON.parse(fs.readFileSync(packageJsonPath, "utf8"))
|
||||
: undefined;
|
||||
if (!shouldBuildBundledCluster(dirent.name, env, { packageJson })) {
|
||||
removePathIfExists(distPluginDir);
|
||||
continue;
|
||||
}
|
||||
@@ -219,13 +223,10 @@ export function copyBundledPluginMetadata(params = {}) {
|
||||
: manifest;
|
||||
writeTextFileIfChanged(distManifestPath, `${JSON.stringify(bundledManifest, null, 2)}\n`);
|
||||
|
||||
const packageJsonPath = path.join(pluginDir, "package.json");
|
||||
if (!fs.existsSync(packageJsonPath)) {
|
||||
removeFileIfExists(distPackageJsonPath);
|
||||
continue;
|
||||
}
|
||||
|
||||
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf8"));
|
||||
if (packageJson.openclaw && "extensions" in packageJson.openclaw) {
|
||||
packageJson.openclaw = {
|
||||
...packageJson.openclaw,
|
||||
|
||||
95
scripts/lib/bundled-plugin-build-entries.mjs
Normal file
95
scripts/lib/bundled-plugin-build-entries.mjs
Normal file
@@ -0,0 +1,95 @@
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { shouldBuildBundledCluster } from "./optional-bundled-clusters.mjs";
|
||||
|
||||
function readBundledPluginPackageJson(packageJsonPath) {
|
||||
if (!fs.existsSync(packageJsonPath)) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
return JSON.parse(fs.readFileSync(packageJsonPath, "utf8"));
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function collectPluginSourceEntries(packageJson) {
|
||||
let packageEntries = Array.isArray(packageJson?.openclaw?.extensions)
|
||||
? packageJson.openclaw.extensions.filter(
|
||||
(entry) => typeof entry === "string" && entry.trim().length > 0,
|
||||
)
|
||||
: [];
|
||||
const setupEntry =
|
||||
typeof packageJson?.openclaw?.setupEntry === "string" &&
|
||||
packageJson.openclaw.setupEntry.trim().length > 0
|
||||
? packageJson.openclaw.setupEntry
|
||||
: undefined;
|
||||
if (setupEntry) {
|
||||
packageEntries = Array.from(new Set([...packageEntries, setupEntry]));
|
||||
}
|
||||
return packageEntries.length > 0 ? packageEntries : ["./index.ts"];
|
||||
}
|
||||
|
||||
export function collectBundledPluginBuildEntries(params = {}) {
|
||||
const cwd = params.cwd ?? process.cwd();
|
||||
const env = params.env ?? process.env;
|
||||
const extensionsRoot = path.join(cwd, "extensions");
|
||||
const entries = [];
|
||||
|
||||
for (const dirent of fs.readdirSync(extensionsRoot, { withFileTypes: true })) {
|
||||
if (!dirent.isDirectory()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const pluginDir = path.join(extensionsRoot, dirent.name);
|
||||
const manifestPath = path.join(pluginDir, "openclaw.plugin.json");
|
||||
if (!fs.existsSync(manifestPath)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const packageJsonPath = path.join(pluginDir, "package.json");
|
||||
const packageJson = readBundledPluginPackageJson(packageJsonPath);
|
||||
if (!shouldBuildBundledCluster(dirent.name, env, { packageJson })) {
|
||||
continue;
|
||||
}
|
||||
|
||||
entries.push({
|
||||
id: dirent.name,
|
||||
hasPackageJson: packageJson !== null,
|
||||
packageJson,
|
||||
sourceEntries: collectPluginSourceEntries(packageJson),
|
||||
});
|
||||
}
|
||||
|
||||
return entries;
|
||||
}
|
||||
|
||||
export function listBundledPluginBuildEntries(params = {}) {
|
||||
return Object.fromEntries(
|
||||
collectBundledPluginBuildEntries(params).flatMap(({ id, sourceEntries }) =>
|
||||
sourceEntries.map((entry) => {
|
||||
const normalizedEntry = entry.replace(/^\.\//, "");
|
||||
const entryKey = `extensions/${id}/${normalizedEntry.replace(/\.[^.]+$/u, "")}`;
|
||||
return [entryKey, path.join("extensions", id, normalizedEntry)];
|
||||
}),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
export function listBundledPluginPackArtifacts(params = {}) {
|
||||
const entries = collectBundledPluginBuildEntries(params);
|
||||
const artifacts = new Set();
|
||||
|
||||
for (const { id, hasPackageJson, sourceEntries } of entries) {
|
||||
artifacts.add(`dist/extensions/${id}/openclaw.plugin.json`);
|
||||
if (hasPackageJson) {
|
||||
artifacts.add(`dist/extensions/${id}/package.json`);
|
||||
}
|
||||
for (const entry of sourceEntries) {
|
||||
const normalizedEntry = entry.replace(/^\.\//, "").replace(/\.[^.]+$/u, "");
|
||||
artifacts.add(`dist/extensions/${id}/${normalizedEntry}.js`);
|
||||
}
|
||||
}
|
||||
|
||||
return [...artifacts].toSorted((left, right) => left.localeCompare(right));
|
||||
}
|
||||
@@ -3,4 +3,9 @@ export const optionalBundledClusterSet: Set<string>;
|
||||
export const OPTIONAL_BUNDLED_BUILD_ENV: "OPENCLAW_INCLUDE_OPTIONAL_BUNDLED";
|
||||
export function isOptionalBundledCluster(cluster: string): boolean;
|
||||
export function shouldIncludeOptionalBundledClusters(env?: NodeJS.ProcessEnv): boolean;
|
||||
export function shouldBuildBundledCluster(cluster: string, env?: NodeJS.ProcessEnv): boolean;
|
||||
export function hasReleasedBundledInstall(packageJson: unknown): boolean;
|
||||
export function shouldBuildBundledCluster(
|
||||
cluster: string,
|
||||
env?: NodeJS.ProcessEnv,
|
||||
options?: { packageJson?: unknown },
|
||||
): boolean;
|
||||
|
||||
7
scripts/lib/optional-bundled-clusters.d.ts
vendored
7
scripts/lib/optional-bundled-clusters.d.ts
vendored
@@ -3,4 +3,9 @@ export const optionalBundledClusterSet: Set<string>;
|
||||
export const OPTIONAL_BUNDLED_BUILD_ENV: "OPENCLAW_INCLUDE_OPTIONAL_BUNDLED";
|
||||
export function isOptionalBundledCluster(cluster: string): boolean;
|
||||
export function shouldIncludeOptionalBundledClusters(env?: NodeJS.ProcessEnv): boolean;
|
||||
export function shouldBuildBundledCluster(cluster: string, env?: NodeJS.ProcessEnv): boolean;
|
||||
export function hasReleasedBundledInstall(packageJson: unknown): boolean;
|
||||
export function shouldBuildBundledCluster(
|
||||
cluster: string,
|
||||
env?: NodeJS.ProcessEnv,
|
||||
options?: { packageJson?: unknown },
|
||||
): boolean;
|
||||
|
||||
@@ -26,6 +26,16 @@ export function shouldIncludeOptionalBundledClusters(env = process.env) {
|
||||
return env[OPTIONAL_BUNDLED_BUILD_ENV] === "1";
|
||||
}
|
||||
|
||||
export function shouldBuildBundledCluster(cluster, env = process.env) {
|
||||
export function hasReleasedBundledInstall(packageJson) {
|
||||
return (
|
||||
typeof packageJson?.openclaw?.install?.npmSpec === "string" &&
|
||||
packageJson.openclaw.install.npmSpec.trim().length > 0
|
||||
);
|
||||
}
|
||||
|
||||
export function shouldBuildBundledCluster(cluster, env = process.env, options = {}) {
|
||||
if (hasReleasedBundledInstall(options.packageJson)) {
|
||||
return true;
|
||||
}
|
||||
return shouldIncludeOptionalBundledClusters(env) || !isOptionalBundledCluster(cluster);
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
type BundledExtension,
|
||||
type ExtensionPackageJson as PackageJson,
|
||||
} from "./lib/bundled-extension-manifest.ts";
|
||||
import { listBundledPluginPackArtifacts } from "./lib/bundled-plugin-build-entries.mjs";
|
||||
import { listPluginSdkDistArtifacts } from "./lib/plugin-sdk-entries.mjs";
|
||||
import { sparkleBuildFloorsFromShortVersion, type SparkleBuildFloors } from "./sparkle-build.ts";
|
||||
|
||||
@@ -21,9 +22,11 @@ const requiredPathGroups = [
|
||||
["dist/index.js", "dist/index.mjs"],
|
||||
["dist/entry.js", "dist/entry.mjs"],
|
||||
...listPluginSdkDistArtifacts(),
|
||||
...listBundledPluginPackArtifacts(),
|
||||
"dist/plugin-sdk/compat.js",
|
||||
"dist/plugin-sdk/root-alias.cjs",
|
||||
"dist/build-info.json",
|
||||
"dist/control-ui/index.html",
|
||||
];
|
||||
const forbiddenPrefixes = ["dist-runtime/", "dist/OpenClaw.app/"];
|
||||
// 2026.3.12 ballooned to ~213.6 MiB unpacked and correlated with low-memory
|
||||
@@ -85,7 +88,7 @@ export function collectForbiddenPackPaths(paths: Iterable<string>): string[] {
|
||||
forbiddenPrefixes.some((prefix) => path.startsWith(prefix)) ||
|
||||
(/node_modules\//.test(path) && !isAllowedBundledPluginNodeModulesPath(path)),
|
||||
)
|
||||
.toSorted();
|
||||
.toSorted((left, right) => left.localeCompare(right));
|
||||
}
|
||||
|
||||
function formatMiB(bytes: number): string {
|
||||
@@ -303,7 +306,7 @@ async function main() {
|
||||
}
|
||||
return paths.has(group) ? [] : [group];
|
||||
})
|
||||
.toSorted();
|
||||
.toSorted((left, right) => left.localeCompare(right));
|
||||
const forbidden = collectForbiddenPackPaths(paths);
|
||||
const sizeErrors = collectPackUnpackedSizeErrors(results);
|
||||
|
||||
|
||||
@@ -40,6 +40,9 @@ describe("tsdown config", () => {
|
||||
"plugin-sdk/compat",
|
||||
"plugin-sdk/index",
|
||||
"extensions/openai/index",
|
||||
"extensions/matrix/index",
|
||||
"extensions/msteams/index",
|
||||
"extensions/whatsapp/index",
|
||||
"bundled/boot-md/handler",
|
||||
]),
|
||||
);
|
||||
|
||||
@@ -361,4 +361,25 @@ describe("copyBundledPluginMetadata", () => {
|
||||
|
||||
expect(fs.existsSync(path.join(repoRoot, "dist", "extensions", "acpx"))).toBe(false);
|
||||
});
|
||||
|
||||
it("still bundles previously released optional plugins without the opt-in env", () => {
|
||||
const repoRoot = makeRepoRoot("openclaw-bundled-plugin-released-optional-");
|
||||
const pluginDir = path.join(repoRoot, "extensions", "whatsapp");
|
||||
fs.mkdirSync(pluginDir, { recursive: true });
|
||||
writeJson(path.join(pluginDir, "openclaw.plugin.json"), {
|
||||
id: "whatsapp",
|
||||
configSchema: { type: "object" },
|
||||
});
|
||||
writeJson(path.join(pluginDir, "package.json"), {
|
||||
name: "@openclaw/whatsapp",
|
||||
openclaw: {
|
||||
extensions: ["./index.ts"],
|
||||
install: { npmSpec: "@openclaw/whatsapp" },
|
||||
},
|
||||
});
|
||||
|
||||
copyBundledPluginMetadataWithEnv({ repoRoot, env: {} });
|
||||
|
||||
expect(fs.existsSync(path.join(repoRoot, "dist", "extensions", "whatsapp"))).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { defineConfig, type UserConfig } from "tsdown";
|
||||
import { shouldBuildBundledCluster } from "./scripts/lib/optional-bundled-clusters.mjs";
|
||||
import { listBundledPluginBuildEntries } from "./scripts/lib/bundled-plugin-build-entries.mjs";
|
||||
import { buildPluginSdkEntrySources } from "./scripts/lib/plugin-sdk-entries.mjs";
|
||||
|
||||
type InputOptionsFactory = Extract<NonNullable<UserConfig["inputOptions"]>, Function>;
|
||||
@@ -80,60 +80,6 @@ function nodeBuildConfig(config: UserConfig): UserConfig {
|
||||
};
|
||||
}
|
||||
|
||||
function listBundledPluginBuildEntries(): Record<string, string> {
|
||||
const extensionsRoot = path.join(process.cwd(), "extensions");
|
||||
const entries: Record<string, string> = {};
|
||||
|
||||
for (const dirent of fs.readdirSync(extensionsRoot, { withFileTypes: true })) {
|
||||
if (!dirent.isDirectory()) {
|
||||
continue;
|
||||
}
|
||||
if (!shouldBuildBundledCluster(dirent.name, process.env)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const pluginDir = path.join(extensionsRoot, dirent.name);
|
||||
const manifestPath = path.join(pluginDir, "openclaw.plugin.json");
|
||||
if (!fs.existsSync(manifestPath)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const packageJsonPath = path.join(pluginDir, "package.json");
|
||||
let packageEntries: string[] = [];
|
||||
if (fs.existsSync(packageJsonPath)) {
|
||||
try {
|
||||
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf8")) as {
|
||||
openclaw?: { extensions?: unknown; setupEntry?: unknown };
|
||||
};
|
||||
packageEntries = Array.isArray(packageJson.openclaw?.extensions)
|
||||
? packageJson.openclaw.extensions.filter(
|
||||
(entry): entry is string => typeof entry === "string" && entry.trim().length > 0,
|
||||
)
|
||||
: [];
|
||||
const setupEntry =
|
||||
typeof packageJson.openclaw?.setupEntry === "string" &&
|
||||
packageJson.openclaw.setupEntry.trim().length > 0
|
||||
? packageJson.openclaw.setupEntry
|
||||
: undefined;
|
||||
if (setupEntry) {
|
||||
packageEntries = Array.from(new Set([...packageEntries, setupEntry]));
|
||||
}
|
||||
} catch {
|
||||
packageEntries = [];
|
||||
}
|
||||
}
|
||||
|
||||
const sourceEntries = packageEntries.length > 0 ? packageEntries : ["./index.ts"];
|
||||
for (const entry of sourceEntries) {
|
||||
const normalizedEntry = entry.replace(/^\.\//, "");
|
||||
const entryKey = `extensions/${dirent.name}/${normalizedEntry.replace(/\.[^.]+$/u, "")}`;
|
||||
entries[entryKey] = path.join("extensions", dirent.name, normalizedEntry);
|
||||
}
|
||||
}
|
||||
|
||||
return entries;
|
||||
}
|
||||
|
||||
const bundledPluginBuildEntries = listBundledPluginBuildEntries();
|
||||
|
||||
function buildBundledHookEntries(): Record<string, string> {
|
||||
|
||||
Reference in New Issue
Block a user