diff --git a/scripts/lib/bundled-extension-manifest.ts b/scripts/lib/bundled-extension-manifest.ts new file mode 100644 index 00000000000..07053e943eb --- /dev/null +++ b/scripts/lib/bundled-extension-manifest.ts @@ -0,0 +1,71 @@ +export type ExtensionPackageJson = { + name?: string; + version?: string; + dependencies?: Record; + optionalDependencies?: Record; + 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; +} diff --git a/scripts/release-check.ts b/scripts/release-check.ts index df79b69f25c..fe2a9a1ea9c 100755 --- a/scripts/release-check.ts +++ b/scripts/release-check.ts @@ -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; - optionalDependencies?: Record; - 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) =>