Files
openclaw/scripts/lib/plugin-package-dependencies.mts
Peter Steinberger c70aee247e refactor(scripts): migrate JavaScript tools to TypeScript (#121005)
* refactor(scripts): migrate JavaScript tools to TypeScript

* fix(ci): keep changed-scope preflight zero-install

* fix(ci): preserve zero-install script owners

* fix(ci): complete script migration follow-through

* fix(release): keep stable closeout zero-install

* fix(scripts): preserve standalone execution boundaries

* fix(scripts): repair standalone loader boundaries

* fix(scripts): normalize gateway observation ids

* fix(scripts): keep Docker packager standalone

* test(scripts): preserve rebase cleanup helpers

* test(sessions): use tracked temp directory
2026-08-09 07:21:35 -07:00

86 lines
2.8 KiB
TypeScript

// Collects runtime dependency specs from bundled plugin packages.
import fs from "node:fs";
import path from "node:path";
type RuntimeDependencyPackageJson = {
dependencies?: Record<string, unknown>;
devDependencies?: Record<string, unknown>;
optionalDependencies?: Record<string, unknown>;
};
/** Collect dependencies and optionalDependencies needed at plugin runtime. */
export function collectRuntimeDependencySpecs(packageJson: RuntimeDependencyPackageJson = {}) {
return new Map(
[
...Object.entries(packageJson.dependencies ?? {}),
...Object.entries(packageJson.optionalDependencies ?? {}),
].filter((entry): entry is [string, string] => {
return typeof entry[1] === "string" && entry[1].length > 0;
}),
);
}
/** Extract an npm package name from a bare module specifier. */
export function packageNameFromSpecifier(specifier: string) {
if (
typeof specifier !== "string" ||
specifier.startsWith(".") ||
specifier.startsWith("/") ||
specifier.startsWith("node:") ||
specifier.startsWith("#")
) {
return null;
}
const [first, second] = specifier.split("/");
if (!first) {
return null;
}
if (first.startsWith("@")) {
return second ? `${first}/${second}` : null;
}
return first;
}
/** Collect runtime dependency specs across bundled plugin packages and note conflicts. */
export function collectBundledPluginPackageDependencySpecs(bundledPluginsDir: string) {
const specs = new Map<
string,
{ conflicts: Array<{ pluginId: string; spec: string }>; pluginIds: string[]; spec: string }
>();
if (!fs.existsSync(bundledPluginsDir)) {
return specs;
}
const packageJsonPaths = fs
.readdirSync(bundledPluginsDir, { withFileTypes: true })
.filter((entry) => entry.isDirectory())
.map((entry) => path.join(bundledPluginsDir, entry.name, "package.json"))
.filter((packageJsonPath) => fs.existsSync(packageJsonPath))
.toSorted((left, right) => left.localeCompare(right));
for (const packageJsonPath of packageJsonPaths) {
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf8"));
// External official plugins own isolated npm projects, so their dependency
// specs do not need to match packages bundled into the root distribution.
if (packageJson.openclaw?.build?.bundledDist === false) {
continue;
}
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;
}