Files
openclaw/scripts/lib/bundled-plugin-root-runtime-mirrors.mjs
2026-04-08 15:15:44 +01:00

178 lines
5.7 KiB
JavaScript

import fs from "node:fs";
import path from "node:path";
const JS_EXTENSIONS = new Set([".cjs", ".js", ".mjs"]);
export function collectRuntimeDependencySpecs(packageJson = {}) {
return new Map(
[
...Object.entries(packageJson.dependencies ?? {}),
...Object.entries(packageJson.optionalDependencies ?? {}),
].filter((entry) => typeof entry[1] === "string" && entry[1].length > 0),
);
}
export function packageNameFromSpecifier(specifier) {
if (
typeof specifier !== "string" ||
specifier.startsWith(".") ||
specifier.startsWith("/") ||
specifier.startsWith("node:") ||
specifier.startsWith("#")
) {
return null;
}
const [first, second] = specifier.split("/");
if (!first) {
return null;
}
return first.startsWith("@") && second ? `${first}/${second}` : first;
}
function readJson(filePath) {
return JSON.parse(fs.readFileSync(filePath, "utf8"));
}
function collectPackageJsonPaths(rootDir) {
if (!fs.existsSync(rootDir)) {
return [];
}
return fs
.readdirSync(rootDir, { withFileTypes: true })
.filter((entry) => entry.isDirectory())
.map((entry) => path.join(rootDir, entry.name, "package.json"))
.filter((packageJsonPath) => fs.existsSync(packageJsonPath))
.toSorted((left, right) => left.localeCompare(right));
}
export function collectBundledPluginRuntimeDependencySpecs(bundledPluginsDir) {
const specs = new Map();
for (const packageJsonPath of collectPackageJsonPaths(bundledPluginsDir)) {
const packageJson = readJson(packageJsonPath);
const pluginId = path.basename(path.dirname(packageJsonPath));
for (const [name, spec] of collectRuntimeDependencySpecs(packageJson)) {
const existing = specs.get(name);
if (existing) {
if (existing.spec !== spec) {
existing.conflicts.push({ pluginId, spec });
} else if (!existing.pluginIds.includes(pluginId)) {
existing.pluginIds.push(pluginId);
}
continue;
}
specs.set(name, { conflicts: [], pluginIds: [pluginId], spec });
}
}
return specs;
}
function walkJavaScriptFiles(rootDir) {
const files = [];
if (!fs.existsSync(rootDir)) {
return files;
}
const queue = [rootDir];
while (queue.length > 0) {
const current = queue.shift();
for (const entry of fs.readdirSync(current, { withFileTypes: true })) {
const fullPath = path.join(current, entry.name);
if (entry.isDirectory()) {
if (fullPath.split(path.sep).includes("extensions")) {
continue;
}
queue.push(fullPath);
continue;
}
if (entry.isFile() && JS_EXTENSIONS.has(path.extname(entry.name))) {
files.push(fullPath);
}
}
}
return files.toSorted((left, right) => left.localeCompare(right));
}
function extractModuleSpecifiers(source) {
const specifiers = new Set();
const patterns = [
/\bfrom\s*["']([^"']+)["']/g,
/\bimport\s*["']([^"']+)["']/g,
/\bimport\s*\(\s*["']([^"']+)["']\s*\)/g,
/\brequire\s*\(\s*["']([^"']+)["']\s*\)/g,
];
for (const pattern of patterns) {
for (const match of source.matchAll(pattern)) {
if (match[1]) {
specifiers.add(match[1]);
}
}
}
return specifiers;
}
export function collectRootDistBundledRuntimeMirrors(params) {
const distDir = params.distDir;
const bundledSpecs = params.bundledRuntimeDependencySpecs;
const mirrors = new Map();
for (const filePath of walkJavaScriptFiles(distDir)) {
const source = fs.readFileSync(filePath, "utf8");
const relativePath = path.relative(distDir, filePath).replaceAll(path.sep, "/");
for (const specifier of extractModuleSpecifiers(source)) {
const dependencyName = packageNameFromSpecifier(specifier);
if (!dependencyName || !bundledSpecs.has(dependencyName)) {
continue;
}
const bundledSpec = bundledSpecs.get(dependencyName);
const existing = mirrors.get(dependencyName);
if (existing) {
existing.importers.add(relativePath);
continue;
}
mirrors.set(dependencyName, {
importers: new Set([relativePath]),
pluginIds: bundledSpec.pluginIds,
spec: bundledSpec.spec,
});
}
}
return mirrors;
}
export function collectBundledPluginRootRuntimeMirrorErrors(params) {
const rootRuntimeDeps = collectRuntimeDependencySpecs(params.rootPackageJson);
const errors = [];
for (const [dependencyName, record] of params.bundledRuntimeDependencySpecs) {
for (const conflict of record.conflicts) {
errors.push(
`bundled runtime dependency '${dependencyName}' has conflicting plugin specs: ${record.pluginIds.join(", ")} use '${record.spec}', ${conflict.pluginId} uses '${conflict.spec}'.`,
);
}
}
for (const [dependencyName, mirror] of params.requiredRootMirrors) {
const rootSpec = rootRuntimeDeps.get(dependencyName);
const importers = [...mirror.importers].toSorted((left, right) => left.localeCompare(right));
const importerLabel = importers.join(", ");
const pluginLabel = mirror.pluginIds
.toSorted((left, right) => left.localeCompare(right))
.join(", ");
if (typeof rootSpec !== "string" || rootSpec.length === 0) {
errors.push(
`root dist imports bundled plugin runtime dependency '${dependencyName}' from ${importerLabel}; mirror '${dependencyName}: ${mirror.spec}' in root package.json (declared by ${pluginLabel}).`,
);
continue;
}
if (rootSpec !== mirror.spec) {
errors.push(
`root dist imports bundled plugin runtime dependency '${dependencyName}' from ${importerLabel}; root package.json has '${rootSpec}' but plugin manifest declares '${mirror.spec}' (${pluginLabel}).`,
);
}
}
return errors;
}