refactor: extract bundled extension manifest parser

This commit is contained in:
Peter Steinberger
2026-03-08 17:15:16 +00:00
parent 52bc809143
commit d307a7ca1a
2 changed files with 79 additions and 68 deletions

View File

@@ -0,0 +1,71 @@
export type ExtensionPackageJson = {
name?: string;
version?: string;
dependencies?: Record<string, string>;
optionalDependencies?: Record<string, string>;
openclaw?: {
install?: {
npmSpec?: string;
};
releaseChecks?: {
rootDependencyMirrorAllowlist?: string[];
};
};
};
export type BundledExtension = { id: string; packageJson: ExtensionPackageJson };
export type BundledExtensionMetadata = BundledExtension & {
npmSpec?: string;
rootDependencyMirrorAllowlist: string[];
};
export function normalizeBundledExtensionMetadata(
extensions: BundledExtension[],
): BundledExtensionMetadata[] {
return extensions.map((extension) => ({
...extension,
npmSpec:
typeof extension.packageJson.openclaw?.install?.npmSpec === "string"
? extension.packageJson.openclaw.install.npmSpec.trim()
: undefined,
rootDependencyMirrorAllowlist:
extension.packageJson.openclaw?.releaseChecks?.rootDependencyMirrorAllowlist?.filter(
(entry): entry is string => typeof entry === "string" && entry.trim().length > 0,
) ?? [],
}));
}
export function collectBundledExtensionManifestErrors(extensions: BundledExtension[]): string[] {
const errors: string[] = [];
for (const extension of extensions) {
const install = extension.packageJson.openclaw?.install;
if (
install &&
(!install.npmSpec || typeof install.npmSpec !== "string" || !install.npmSpec.trim())
) {
errors.push(
`bundled extension '${extension.id}' manifest invalid | openclaw.install.npmSpec must be a non-empty string`,
);
}
const allowlist = extension.packageJson.openclaw?.releaseChecks?.rootDependencyMirrorAllowlist;
if (allowlist === undefined) {
continue;
}
if (!Array.isArray(allowlist)) {
errors.push(
`bundled extension '${extension.id}' manifest invalid | openclaw.releaseChecks.rootDependencyMirrorAllowlist must be an array of non-empty strings`,
);
continue;
}
const invalidEntries = allowlist.filter((entry) => typeof entry !== "string" || !entry.trim());
if (invalidEntries.length > 0) {
errors.push(
`bundled extension '${extension.id}' manifest invalid | openclaw.releaseChecks.rootDependencyMirrorAllowlist must contain only non-empty strings`,
);
}
}
return errors;
}

View File

@@ -4,29 +4,18 @@ import { execSync } from "node:child_process";
import { readdirSync, readFileSync } from "node:fs";
import { join, resolve } from "node:path";
import { pathToFileURL } from "node:url";
import {
collectBundledExtensionManifestErrors,
normalizeBundledExtensionMetadata,
type BundledExtension,
type ExtensionPackageJson as PackageJson,
} from "./lib/bundled-extension-manifest.ts";
import { sparkleBuildFloorsFromShortVersion, type SparkleBuildFloors } from "./sparkle-build.ts";
export { collectBundledExtensionManifestErrors } from "./lib/bundled-extension-manifest.ts";
type PackFile = { path: string };
type PackResult = { files?: PackFile[] };
type PackageJson = {
name?: string;
version?: string;
dependencies?: Record<string, string>;
optionalDependencies?: Record<string, string>;
openclaw?: {
install?: {
npmSpec?: string;
};
releaseChecks?: {
rootDependencyMirrorAllowlist?: string[];
};
};
};
type BundledExtension = { id: string; packageJson: PackageJson };
type BundledExtensionMetadata = BundledExtension & {
npmSpec?: string;
rootDependencyMirrorAllowlist: string[];
};
const requiredPathGroups = [
["dist/index.js", "dist/index.mjs"],
@@ -175,55 +164,6 @@ export function collectBundledExtensionRootDependencyGapErrors(params: {
return errors;
}
function normalizeBundledExtensionMetadata(
extensions: BundledExtension[],
): BundledExtensionMetadata[] {
return extensions.map((extension) => ({
...extension,
npmSpec:
typeof extension.packageJson.openclaw?.install?.npmSpec === "string"
? extension.packageJson.openclaw.install.npmSpec.trim()
: undefined,
rootDependencyMirrorAllowlist:
extension.packageJson.openclaw?.releaseChecks?.rootDependencyMirrorAllowlist?.filter(
(entry): entry is string => typeof entry === "string" && entry.trim().length > 0,
) ?? [],
}));
}
export function collectBundledExtensionManifestErrors(extensions: BundledExtension[]): string[] {
const errors: string[] = [];
for (const extension of extensions) {
const install = extension.packageJson.openclaw?.install;
if (
install &&
(!install.npmSpec || typeof install.npmSpec !== "string" || !install.npmSpec.trim())
) {
errors.push(
`bundled extension '${extension.id}' manifest invalid | openclaw.install.npmSpec must be a non-empty string`,
);
}
const allowlist = extension.packageJson.openclaw?.releaseChecks?.rootDependencyMirrorAllowlist;
if (allowlist === undefined) {
continue;
}
if (!Array.isArray(allowlist)) {
errors.push(
`bundled extension '${extension.id}' manifest invalid | openclaw.releaseChecks.rootDependencyMirrorAllowlist must be an array of non-empty strings`,
);
continue;
}
const invalidEntries = allowlist.filter((entry) => typeof entry !== "string" || !entry.trim());
if (invalidEntries.length > 0) {
errors.push(
`bundled extension '${extension.id}' manifest invalid | openclaw.releaseChecks.rootDependencyMirrorAllowlist must contain only non-empty strings`,
);
}
}
return errors;
}
function collectBundledExtensions(): BundledExtension[] {
const extensionsDir = resolve("extensions");
const entries = readdirSync(extensionsDir, { withFileTypes: true }).filter((entry) =>