mirror of
https://github.com/openclaw/openclaw.git
synced 2026-03-16 12:30:49 +00:00
206 lines
7.1 KiB
JavaScript
206 lines
7.1 KiB
JavaScript
import fs from "node:fs";
|
|
import path from "node:path";
|
|
import { pathToFileURL } from "node:url";
|
|
import {
|
|
removeFileIfExists,
|
|
removePathIfExists,
|
|
writeTextFileIfChanged,
|
|
} from "./runtime-postbuild-shared.mjs";
|
|
|
|
const GENERATED_BUNDLED_SKILLS_DIR = "bundled-skills";
|
|
|
|
export function rewritePackageExtensions(entries) {
|
|
if (!Array.isArray(entries)) {
|
|
return undefined;
|
|
}
|
|
|
|
return entries
|
|
.filter((entry) => typeof entry === "string" && entry.trim().length > 0)
|
|
.map((entry) => {
|
|
const normalized = entry.replace(/^\.\//, "");
|
|
const rewritten = normalized.replace(/\.[^.]+$/u, ".js");
|
|
return `./${rewritten}`;
|
|
});
|
|
}
|
|
|
|
function rewritePackageEntry(entry) {
|
|
if (typeof entry !== "string" || entry.trim().length === 0) {
|
|
return undefined;
|
|
}
|
|
const normalized = entry.replace(/^\.\//, "");
|
|
const rewritten = normalized.replace(/\.[^.]+$/u, ".js");
|
|
return `./${rewritten}`;
|
|
}
|
|
|
|
function ensurePathInsideRoot(rootDir, rawPath) {
|
|
const resolved = path.resolve(rootDir, rawPath);
|
|
const relative = path.relative(rootDir, resolved);
|
|
if (
|
|
relative === "" ||
|
|
relative === "." ||
|
|
(!relative.startsWith(`..${path.sep}`) && relative !== ".." && !path.isAbsolute(relative))
|
|
) {
|
|
return resolved;
|
|
}
|
|
throw new Error(`path escapes plugin root: ${rawPath}`);
|
|
}
|
|
|
|
function normalizeManifestRelativePath(rawPath) {
|
|
return rawPath.replaceAll("\\", "/").replace(/^\.\//u, "");
|
|
}
|
|
|
|
function resolveDeclaredSkillSourcePath(params) {
|
|
const normalized = normalizeManifestRelativePath(params.rawPath);
|
|
const pluginLocalPath = ensurePathInsideRoot(params.pluginDir, normalized);
|
|
if (fs.existsSync(pluginLocalPath)) {
|
|
return pluginLocalPath;
|
|
}
|
|
if (!/^node_modules(?:\/|$)/u.test(normalized)) {
|
|
return pluginLocalPath;
|
|
}
|
|
return ensurePathInsideRoot(params.repoRoot, normalized);
|
|
}
|
|
|
|
function resolveBundledSkillTarget(rawPath) {
|
|
const normalized = normalizeManifestRelativePath(rawPath);
|
|
if (/^node_modules(?:\/|$)/u.test(normalized)) {
|
|
// Bundled dist/plugin roots must not publish nested node_modules trees. Relocate
|
|
// dependency-backed skill assets into a dist-owned directory and rewrite the manifest.
|
|
const trimmed = normalized.replace(/^node_modules\/?/u, "");
|
|
if (!trimmed) {
|
|
throw new Error(`node_modules skill path must point to a package: ${rawPath}`);
|
|
}
|
|
const bundledRelativePath = `${GENERATED_BUNDLED_SKILLS_DIR}/${trimmed}`;
|
|
return {
|
|
manifestPath: `./${bundledRelativePath}`,
|
|
outputPath: bundledRelativePath,
|
|
};
|
|
}
|
|
return {
|
|
manifestPath: rawPath,
|
|
outputPath: normalized,
|
|
};
|
|
}
|
|
|
|
function copyDeclaredPluginSkillPaths(params) {
|
|
const skills = Array.isArray(params.manifest.skills) ? params.manifest.skills : [];
|
|
const copiedSkills = [];
|
|
for (const raw of skills) {
|
|
if (typeof raw !== "string" || raw.trim().length === 0) {
|
|
continue;
|
|
}
|
|
const sourcePath = resolveDeclaredSkillSourcePath({
|
|
rawPath: raw,
|
|
pluginDir: params.pluginDir,
|
|
repoRoot: params.repoRoot,
|
|
});
|
|
const target = resolveBundledSkillTarget(raw);
|
|
if (!fs.existsSync(sourcePath)) {
|
|
// Some Docker/lightweight builds intentionally omit optional plugin-local
|
|
// dependencies. Only advertise skill paths that were actually bundled.
|
|
console.warn(
|
|
`[bundled-plugin-metadata] skipping missing skill path ${sourcePath} (plugin ${params.manifest.id ?? path.basename(params.pluginDir)})`,
|
|
);
|
|
continue;
|
|
}
|
|
const targetPath = ensurePathInsideRoot(params.distPluginDir, target.outputPath);
|
|
removePathIfExists(targetPath);
|
|
fs.mkdirSync(path.dirname(targetPath), { recursive: true });
|
|
const shouldExcludeNestedNodeModules = /^node_modules(?:\/|$)/u.test(
|
|
normalizeManifestRelativePath(raw),
|
|
);
|
|
fs.cpSync(sourcePath, targetPath, {
|
|
dereference: true,
|
|
force: true,
|
|
recursive: true,
|
|
filter: (candidatePath) => {
|
|
if (!shouldExcludeNestedNodeModules || candidatePath === sourcePath) {
|
|
return true;
|
|
}
|
|
const relativeCandidate = path.relative(sourcePath, candidatePath).replaceAll("\\", "/");
|
|
return !relativeCandidate.split("/").includes("node_modules");
|
|
},
|
|
});
|
|
copiedSkills.push(target.manifestPath);
|
|
}
|
|
return copiedSkills;
|
|
}
|
|
|
|
export function copyBundledPluginMetadata(params = {}) {
|
|
const repoRoot = params.cwd ?? params.repoRoot ?? process.cwd();
|
|
const extensionsRoot = path.join(repoRoot, "extensions");
|
|
const distExtensionsRoot = path.join(repoRoot, "dist", "extensions");
|
|
if (!fs.existsSync(extensionsRoot)) {
|
|
return;
|
|
}
|
|
|
|
const sourcePluginDirs = new Set();
|
|
for (const dirent of fs.readdirSync(extensionsRoot, { withFileTypes: true })) {
|
|
if (!dirent.isDirectory()) {
|
|
continue;
|
|
}
|
|
sourcePluginDirs.add(dirent.name);
|
|
|
|
const pluginDir = path.join(extensionsRoot, dirent.name);
|
|
const manifestPath = path.join(pluginDir, "openclaw.plugin.json");
|
|
const distPluginDir = path.join(distExtensionsRoot, dirent.name);
|
|
const distManifestPath = path.join(distPluginDir, "openclaw.plugin.json");
|
|
const distPackageJsonPath = path.join(distPluginDir, "package.json");
|
|
if (!fs.existsSync(manifestPath)) {
|
|
removePathIfExists(distPluginDir);
|
|
continue;
|
|
}
|
|
|
|
const manifest = JSON.parse(fs.readFileSync(manifestPath, "utf8"));
|
|
// Generated skill assets live under a dedicated dist-owned directory. Also
|
|
// remove the older bad node_modules tree so release packs cannot pick it up.
|
|
removePathIfExists(path.join(distPluginDir, GENERATED_BUNDLED_SKILLS_DIR));
|
|
removePathIfExists(path.join(distPluginDir, "node_modules"));
|
|
const copiedSkills = copyDeclaredPluginSkillPaths({
|
|
manifest,
|
|
pluginDir,
|
|
distPluginDir,
|
|
repoRoot,
|
|
});
|
|
const bundledManifest = Array.isArray(manifest.skills)
|
|
? { ...manifest, skills: copiedSkills }
|
|
: 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,
|
|
extensions: rewritePackageExtensions(packageJson.openclaw.extensions),
|
|
...(typeof packageJson.openclaw.setupEntry === "string"
|
|
? { setupEntry: rewritePackageEntry(packageJson.openclaw.setupEntry) }
|
|
: {}),
|
|
};
|
|
}
|
|
|
|
writeTextFileIfChanged(distPackageJsonPath, `${JSON.stringify(packageJson, null, 2)}\n`);
|
|
}
|
|
|
|
if (!fs.existsSync(distExtensionsRoot)) {
|
|
return;
|
|
}
|
|
|
|
for (const dirent of fs.readdirSync(distExtensionsRoot, { withFileTypes: true })) {
|
|
if (!dirent.isDirectory() || sourcePluginDirs.has(dirent.name)) {
|
|
continue;
|
|
}
|
|
const distPluginDir = path.join(distExtensionsRoot, dirent.name);
|
|
removePathIfExists(distPluginDir);
|
|
}
|
|
}
|
|
|
|
if (import.meta.url === pathToFileURL(process.argv[1] ?? "").href) {
|
|
copyBundledPluginMetadata();
|
|
}
|