refactor(channels): simplify bundled root scope helpers

This commit is contained in:
Gustavo Madeira Santana
2026-04-15 12:17:42 -04:00
parent 718eff80eb
commit 6aeef1ff94
9 changed files with 327 additions and 106 deletions

View File

@@ -1,6 +1,6 @@
import { normalizeOptionalString } from "../../shared/string-coerce.js";
import { listBundledChannelPluginIdsForRoot } from "./bundled-ids.js";
import { resolveBundledChannelPackageRoot } from "./bundled-root.js";
import { resolveBundledChannelRootScope } from "./bundled-root.js";
import {
getBundledChannelPlugin,
getBundledChannelSecrets,
@@ -69,11 +69,11 @@ function mergeBootstrapPlugin(
}
function buildBootstrapPlugins(
packageRoot: string,
cacheKey: string,
env: NodeJS.ProcessEnv = process.env,
): CachedBootstrapPlugins {
return {
sortedIds: listBundledChannelPluginIdsForRoot(packageRoot, env),
sortedIds: listBundledChannelPluginIdsForRoot(cacheKey, env),
byId: new Map(),
secretsById: new Map(),
missingIds: new Set(),
@@ -81,20 +81,20 @@ function buildBootstrapPlugins(
}
function getBootstrapPlugins(
packageRoot = resolveBundledChannelPackageRoot(),
cacheKey = resolveBundledChannelRootScope().cacheKey,
env: NodeJS.ProcessEnv = process.env,
): CachedBootstrapPlugins {
const cached = cachedBootstrapPluginsByRoot.get(packageRoot);
const cached = cachedBootstrapPluginsByRoot.get(cacheKey);
if (cached) {
return cached;
}
const created = buildBootstrapPlugins(packageRoot, env);
cachedBootstrapPluginsByRoot.set(packageRoot, created);
const created = buildBootstrapPlugins(cacheKey, env);
cachedBootstrapPluginsByRoot.set(cacheKey, created);
return created;
}
function resolveActiveBootstrapPlugins(): CachedBootstrapPlugins {
return getBootstrapPlugins(resolveBundledChannelPackageRoot());
return getBootstrapPlugins(resolveBundledChannelRootScope().cacheKey);
}
export function listBootstrapChannelPluginIds(): readonly string[] {

View File

@@ -1,5 +1,5 @@
import { listChannelCatalogEntries } from "../../plugins/channel-catalog-registry.js";
import { resolveBundledChannelPackageRoot } from "./bundled-root.js";
import { resolveBundledChannelRootScope } from "./bundled-root.js";
const bundledChannelPluginIdsByRoot = new Map<string, string[]>();
@@ -19,5 +19,5 @@ export function listBundledChannelPluginIdsForRoot(
}
export function listBundledChannelPluginIds(): string[] {
return listBundledChannelPluginIdsForRoot(resolveBundledChannelPackageRoot());
return listBundledChannelPluginIdsForRoot(resolveBundledChannelRootScope().cacheKey);
}

View File

@@ -79,11 +79,11 @@ describe("bundled root-aware caches", () => {
const rootB = makeBundledRoot("openclaw-bootstrap-b-");
vi.doMock("./bundled-ids.js", () => ({
listBundledChannelPluginIdsForRoot: (packageRoot: string) => {
if (packageRoot === rootA.root) {
listBundledChannelPluginIdsForRoot: (cacheKey: string) => {
if (cacheKey === rootA.pluginsDir) {
return ["alpha"];
}
if (packageRoot === rootB.root) {
if (cacheKey === rootB.pluginsDir) {
return ["beta"];
}
return [];

View File

@@ -13,12 +13,14 @@ const OPENCLAW_PACKAGE_ROOT =
? path.resolve(fileURLToPath(new URL("../../..", import.meta.url)))
: process.cwd());
export function derivePackageRootFromBundledPluginsDir(pluginsDir: string): string {
const resolvedDir = path.resolve(pluginsDir);
if (path.basename(resolvedDir) !== "extensions") {
return resolvedDir;
}
const parentDir = path.dirname(resolvedDir);
export type BundledChannelRootScope = {
packageRoot: string;
cacheKey: string;
pluginsDir?: string;
};
function derivePackageRootFromExtensionsDir(extensionsDir: string): string {
const parentDir = path.dirname(extensionsDir);
const parentBase = path.basename(parentDir);
if (parentBase === "dist" || parentBase === "dist-runtime") {
return path.dirname(parentDir);
@@ -26,10 +28,23 @@ export function derivePackageRootFromBundledPluginsDir(pluginsDir: string): stri
return parentDir;
}
export function resolveBundledChannelPackageRoot(env: NodeJS.ProcessEnv = process.env): string {
export function resolveBundledChannelRootScope(
env: NodeJS.ProcessEnv = process.env,
): BundledChannelRootScope {
const bundledPluginsDir = resolveBundledPluginsDir(env);
if (bundledPluginsDir) {
return derivePackageRootFromBundledPluginsDir(bundledPluginsDir);
if (!bundledPluginsDir) {
return {
packageRoot: OPENCLAW_PACKAGE_ROOT,
cacheKey: OPENCLAW_PACKAGE_ROOT,
};
}
return OPENCLAW_PACKAGE_ROOT;
const resolvedPluginsDir = path.resolve(bundledPluginsDir);
return {
packageRoot:
path.basename(resolvedPluginsDir) === "extensions"
? derivePackageRootFromExtensionsDir(resolvedPluginsDir)
: resolvedPluginsDir,
cacheKey: resolvedPluginsDir,
pluginsDir: resolvedPluginsDir,
};
}

View File

@@ -165,6 +165,97 @@ describe("bundled channel entry shape guards", () => {
}
});
it("treats direct bundled plugin-tree overrides as scan roots", async () => {
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-bundled-direct-override-"));
const previousBundledPluginsDir = process.env.OPENCLAW_BUNDLED_PLUGINS_DIR;
const pluginsRoot = path.join(tempRoot, "bundled-plugins");
const pluginDir = path.join(pluginsRoot, "alpha");
fs.mkdirSync(pluginDir, { recursive: true });
fs.writeFileSync(
path.join(pluginDir, "index.js"),
[
"globalThis.__bundledOverrideRuntime = undefined;",
"const plugin = { id: 'alpha', meta: {}, capabilities: {}, config: {} };",
"export default {",
" kind: 'bundled-channel-entry',",
" id: 'alpha',",
" name: 'Alpha',",
" description: 'Alpha',",
" register() {},",
" loadChannelPlugin() { return plugin; },",
" setChannelRuntime(runtime) { globalThis.__bundledOverrideRuntime = runtime.marker; },",
"};",
"",
].join("\n"),
"utf8",
);
let metadataScanDir: string | undefined;
let generatedRootDir: string | undefined;
let generatedScanDir: string | undefined;
vi.doMock("../../plugins/bundled-channel-runtime.js", () => ({
listBundledChannelPluginMetadata: (params?: { rootDir?: string; scanDir?: string }) => {
metadataScanDir = params?.scanDir;
return [
{
dirName: "alpha",
manifest: {
id: "alpha",
channels: ["alpha"],
},
source: {
source: "./index.js",
built: "./index.js",
},
},
];
},
resolveBundledChannelGeneratedPath: (
rootDir: string,
entry: { built?: string; source?: string },
pluginDirName?: string,
scanDir?: string,
) => {
generatedRootDir = rootDir;
generatedScanDir = scanDir;
return path.join(
scanDir ?? path.join(rootDir, "dist", "extensions"),
pluginDirName ?? "alpha",
(entry.built ?? entry.source ?? "./index.js").replace(/^\.\//u, ""),
);
},
}));
try {
process.env.OPENCLAW_BUNDLED_PLUGINS_DIR = pluginsRoot;
const bundled = await importFreshModule<typeof import("./bundled.js")>(
import.meta.url,
"./bundled.js?scope=bundled-direct-override-root",
);
bundled.setBundledChannelRuntime("alpha", { marker: "ok" } as never);
const testGlobal = globalThis as typeof globalThis & {
__bundledOverrideRuntime?: unknown;
};
expect(metadataScanDir).toBe(pluginsRoot);
expect(generatedRootDir).toBe(pluginsRoot);
expect(generatedScanDir).toBe(pluginsRoot);
expect(testGlobal.__bundledOverrideRuntime).toBe("ok");
expect(bundled.requireBundledChannelPlugin("alpha").id).toBe("alpha");
} finally {
if (previousBundledPluginsDir === undefined) {
delete process.env.OPENCLAW_BUNDLED_PLUGINS_DIR;
} else {
process.env.OPENCLAW_BUNDLED_PLUGINS_DIR = previousBundledPluginsDir;
}
fs.rmSync(tempRoot, { recursive: true, force: true });
delete (globalThis as { __bundledOverrideRuntime?: unknown }).__bundledOverrideRuntime;
}
});
it("partitions bundled channel lazy caches by active bundled root without re-importing", async () => {
const rootA = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-bundled-root-a-"));
const rootB = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-bundled-root-b-"));

View File

@@ -8,7 +8,7 @@ import {
} from "../../plugins/bundled-channel-runtime.js";
import { unwrapDefaultModuleExport } from "../../plugins/module-export.js";
import type { PluginRuntime } from "../../plugins/runtime/types.js";
import { resolveBundledChannelPackageRoot } from "./bundled-root.js";
import { resolveBundledChannelRootScope, type BundledChannelRootScope } from "./bundled-root.js";
import { isJavaScriptModulePath, loadChannelPluginModule } from "./module-loader.js";
import type { ChannelPlugin } from "./types.plugin.js";
import type { ChannelId } from "./types.public.js";
@@ -102,9 +102,20 @@ function hasSetupEntryFeature(
function resolveBundledChannelBoundaryRoot(params: {
packageRoot: string;
pluginsDir?: string;
metadata: BundledChannelPluginMetadata;
modulePath: string;
}): string {
const overrideRoot = params.pluginsDir
? path.resolve(params.pluginsDir, params.metadata.dirName)
: null;
if (
overrideRoot &&
(params.modulePath === overrideRoot ||
params.modulePath.startsWith(`${overrideRoot}${path.sep}`))
) {
return overrideRoot;
}
const distRoot = path.resolve(params.packageRoot, "dist", "extensions", params.metadata.dirName);
if (params.modulePath === distRoot || params.modulePath.startsWith(`${distRoot}${path.sep}`)) {
return distRoot;
@@ -112,27 +123,28 @@ function resolveBundledChannelBoundaryRoot(params: {
return path.resolve(params.packageRoot, "extensions", params.metadata.dirName);
}
function resolveBundledChannelScanDir(rootScope: BundledChannelRootScope): string | undefined {
return rootScope.pluginsDir;
}
function resolveGeneratedBundledChannelModulePath(params: {
packageRoot: string;
rootScope: BundledChannelRootScope;
metadata: BundledChannelPluginMetadata;
entry: BundledChannelPluginMetadata["source"] | BundledChannelPluginMetadata["setupSource"];
}): string | null {
if (!params.entry) {
return null;
}
const resolved = resolveBundledChannelGeneratedPath(
params.packageRoot,
return resolveBundledChannelGeneratedPath(
params.rootScope.packageRoot,
params.entry,
params.metadata.dirName,
resolveBundledChannelScanDir(params.rootScope),
);
if (resolved) {
return resolved;
}
return null;
}
function loadGeneratedBundledChannelModule(params: {
packageRoot: string;
rootScope: BundledChannelRootScope;
metadata: BundledChannelPluginMetadata;
entry: BundledChannelPluginMetadata["source"] | BundledChannelPluginMetadata["setupSource"];
}): unknown {
@@ -140,8 +152,10 @@ function loadGeneratedBundledChannelModule(params: {
if (!modulePath) {
throw new Error(`missing generated module for bundled channel ${params.metadata.manifest.id}`);
}
const scanDir = resolveBundledChannelScanDir(params.rootScope);
const boundaryRoot = resolveBundledChannelBoundaryRoot({
packageRoot: params.packageRoot,
packageRoot: params.rootScope.packageRoot,
...(scanDir ? { pluginsDir: scanDir } : {}),
metadata: params.metadata,
modulePath,
});
@@ -155,14 +169,14 @@ function loadGeneratedBundledChannelModule(params: {
}
function loadGeneratedBundledChannelEntry(params: {
packageRoot: string;
rootScope: BundledChannelRootScope;
metadata: BundledChannelPluginMetadata;
includeSetup: boolean;
}): GeneratedBundledChannelEntry | null {
try {
const entry = resolveChannelPluginModuleEntry(
loadGeneratedBundledChannelModule({
packageRoot: params.packageRoot,
rootScope: params.rootScope,
metadata: params.metadata,
entry: params.metadata.source,
}),
@@ -177,7 +191,7 @@ function loadGeneratedBundledChannelEntry(params: {
params.includeSetup && params.metadata.setupSource
? resolveChannelSetupModuleEntry(
loadGeneratedBundledChannelModule({
packageRoot: params.packageRoot,
rootScope: params.rootScope,
metadata: params.metadata,
entry: params.metadata.setupSource,
}),
@@ -211,65 +225,69 @@ function createBundledChannelCacheContext(): BundledChannelCacheContext {
};
}
function getBundledChannelCacheContext(packageRoot: string): BundledChannelCacheContext {
const cached = bundledChannelCacheContexts.get(packageRoot);
function getBundledChannelCacheContext(cacheKey: string): BundledChannelCacheContext {
const cached = bundledChannelCacheContexts.get(cacheKey);
if (cached) {
return cached;
}
const created = createBundledChannelCacheContext();
bundledChannelCacheContexts.set(packageRoot, created);
bundledChannelCacheContexts.set(cacheKey, created);
return created;
}
function resolveActiveBundledChannelCacheScope(): {
packageRoot: string;
rootScope: BundledChannelRootScope;
cacheContext: BundledChannelCacheContext;
} {
const packageRoot = resolveBundledChannelPackageRoot();
const rootScope = resolveBundledChannelRootScope();
return {
packageRoot,
cacheContext: getBundledChannelCacheContext(packageRoot),
rootScope,
cacheContext: getBundledChannelCacheContext(rootScope.cacheKey),
};
}
function listBundledChannelMetadata(
packageRoot = resolveBundledChannelPackageRoot(),
rootScope = resolveBundledChannelRootScope(),
): readonly BundledChannelPluginMetadata[] {
const cached = cachedBundledChannelMetadata.get(packageRoot);
const cached = cachedBundledChannelMetadata.get(rootScope.cacheKey);
if (cached) {
return cached;
}
const scanDir = resolveBundledChannelScanDir(rootScope);
const loaded = listBundledChannelPluginMetadata({
rootDir: packageRoot,
rootDir: rootScope.packageRoot,
...(scanDir ? { scanDir } : {}),
includeChannelConfigs: false,
includeSyntheticChannelConfigs: false,
}).filter((metadata) => (metadata.manifest.channels?.length ?? 0) > 0);
cachedBundledChannelMetadata.set(packageRoot, loaded);
cachedBundledChannelMetadata.set(rootScope.cacheKey, loaded);
return loaded;
}
function listBundledChannelPluginIdsForRoot(packageRoot: string): readonly ChannelId[] {
return listBundledChannelMetadata(packageRoot)
function listBundledChannelPluginIdsForRoot(
rootScope: BundledChannelRootScope,
): readonly ChannelId[] {
return listBundledChannelMetadata(rootScope)
.map((metadata) => metadata.manifest.id)
.toSorted((left, right) => left.localeCompare(right));
}
export function listBundledChannelPluginIds(): readonly ChannelId[] {
return listBundledChannelPluginIdsForRoot(resolveBundledChannelPackageRoot());
return listBundledChannelPluginIdsForRoot(resolveBundledChannelRootScope());
}
function resolveBundledChannelMetadata(
id: ChannelId,
packageRoot: string,
rootScope: BundledChannelRootScope,
): BundledChannelPluginMetadata | undefined {
return listBundledChannelMetadata(packageRoot).find(
return listBundledChannelMetadata(rootScope).find(
(metadata) => metadata.manifest.id === id || metadata.manifest.channels?.includes(id),
);
}
function getLazyGeneratedBundledChannelEntryForRoot(
id: ChannelId,
packageRoot: string,
rootScope: BundledChannelRootScope,
cacheContext: BundledChannelCacheContext,
params?: { includeSetup?: boolean },
): GeneratedBundledChannelEntry | null {
@@ -280,7 +298,7 @@ function getLazyGeneratedBundledChannelEntryForRoot(
if (cached === null && !params?.includeSetup) {
return null;
}
const metadata = resolveBundledChannelMetadata(id, packageRoot);
const metadata = resolveBundledChannelMetadata(id, rootScope);
if (!metadata) {
cacheContext.lazyEntriesById.set(id, null);
return null;
@@ -291,7 +309,7 @@ function getLazyGeneratedBundledChannelEntryForRoot(
cacheContext.entryLoadInProgressIds.add(id);
try {
const entry = loadGeneratedBundledChannelEntry({
packageRoot,
rootScope,
metadata,
includeSetup: params?.includeSetup === true,
});
@@ -307,7 +325,7 @@ function getLazyGeneratedBundledChannelEntryForRoot(
function getBundledChannelPluginForRoot(
id: ChannelId,
packageRoot: string,
rootScope: BundledChannelRootScope,
cacheContext: BundledChannelCacheContext,
): ChannelPlugin | undefined {
const cached = cacheContext.lazyPluginsById.get(id);
@@ -317,7 +335,7 @@ function getBundledChannelPluginForRoot(
if (cacheContext.pluginLoadInProgressIds.has(id)) {
return undefined;
}
const entry = getLazyGeneratedBundledChannelEntryForRoot(id, packageRoot, cacheContext)?.entry;
const entry = getLazyGeneratedBundledChannelEntryForRoot(id, rootScope, cacheContext)?.entry;
if (!entry) {
return undefined;
}
@@ -333,26 +351,26 @@ function getBundledChannelPluginForRoot(
function getBundledChannelSecretsForRoot(
id: ChannelId,
packageRoot: string,
rootScope: BundledChannelRootScope,
cacheContext: BundledChannelCacheContext,
): ChannelPlugin["secrets"] | undefined {
if (cacheContext.lazySecretsById.has(id)) {
return cacheContext.lazySecretsById.get(id) ?? undefined;
}
const entry = getLazyGeneratedBundledChannelEntryForRoot(id, packageRoot, cacheContext)?.entry;
const entry = getLazyGeneratedBundledChannelEntryForRoot(id, rootScope, cacheContext)?.entry;
if (!entry) {
return undefined;
}
const secrets =
entry.loadChannelSecrets?.() ??
getBundledChannelPluginForRoot(id, packageRoot, cacheContext)?.secrets;
getBundledChannelPluginForRoot(id, rootScope, cacheContext)?.secrets;
cacheContext.lazySecretsById.set(id, secrets ?? null);
return secrets;
}
function getBundledChannelSetupPluginForRoot(
id: ChannelId,
packageRoot: string,
rootScope: BundledChannelRootScope,
cacheContext: BundledChannelCacheContext,
): ChannelPlugin | undefined {
const cached = cacheContext.lazySetupPluginsById.get(id);
@@ -362,7 +380,7 @@ function getBundledChannelSetupPluginForRoot(
if (cacheContext.setupPluginLoadInProgressIds.has(id)) {
return undefined;
}
const entry = getLazyGeneratedBundledChannelEntryForRoot(id, packageRoot, cacheContext, {
const entry = getLazyGeneratedBundledChannelEntryForRoot(id, rootScope, cacheContext, {
includeSetup: true,
})?.setupEntry;
if (!entry) {
@@ -380,13 +398,13 @@ function getBundledChannelSetupPluginForRoot(
function getBundledChannelSetupSecretsForRoot(
id: ChannelId,
packageRoot: string,
rootScope: BundledChannelRootScope,
cacheContext: BundledChannelCacheContext,
): ChannelPlugin["secrets"] | undefined {
if (cacheContext.lazySetupSecretsById.has(id)) {
return cacheContext.lazySetupSecretsById.get(id) ?? undefined;
}
const entry = getLazyGeneratedBundledChannelEntryForRoot(id, packageRoot, cacheContext, {
const entry = getLazyGeneratedBundledChannelEntryForRoot(id, rootScope, cacheContext, {
includeSetup: true,
})?.setupEntry;
if (!entry) {
@@ -394,23 +412,23 @@ function getBundledChannelSetupSecretsForRoot(
}
const secrets =
entry.loadSetupSecrets?.() ??
getBundledChannelSetupPluginForRoot(id, packageRoot, cacheContext)?.secrets;
getBundledChannelSetupPluginForRoot(id, rootScope, cacheContext)?.secrets;
cacheContext.lazySetupSecretsById.set(id, secrets ?? null);
return secrets;
}
export function listBundledChannelPlugins(): readonly ChannelPlugin[] {
const { packageRoot, cacheContext } = resolveActiveBundledChannelCacheScope();
return listBundledChannelPluginIdsForRoot(packageRoot).flatMap((id) => {
const plugin = getBundledChannelPluginForRoot(id, packageRoot, cacheContext);
const { rootScope, cacheContext } = resolveActiveBundledChannelCacheScope();
return listBundledChannelPluginIdsForRoot(rootScope).flatMap((id) => {
const plugin = getBundledChannelPluginForRoot(id, rootScope, cacheContext);
return plugin ? [plugin] : [];
});
}
export function listBundledChannelSetupPlugins(): readonly ChannelPlugin[] {
const { packageRoot, cacheContext } = resolveActiveBundledChannelCacheScope();
return listBundledChannelPluginIdsForRoot(packageRoot).flatMap((id) => {
const plugin = getBundledChannelSetupPluginForRoot(id, packageRoot, cacheContext);
const { rootScope, cacheContext } = resolveActiveBundledChannelCacheScope();
return listBundledChannelPluginIdsForRoot(rootScope).flatMap((id) => {
const plugin = getBundledChannelSetupPluginForRoot(id, rootScope, cacheContext);
return plugin ? [plugin] : [];
});
}
@@ -418,37 +436,37 @@ export function listBundledChannelSetupPlugins(): readonly ChannelPlugin[] {
export function listBundledChannelSetupPluginsByFeature(
feature: keyof NonNullable<BundledChannelSetupEntryRuntimeContract["features"]>,
): readonly ChannelPlugin[] {
const { packageRoot, cacheContext } = resolveActiveBundledChannelCacheScope();
return listBundledChannelPluginIdsForRoot(packageRoot).flatMap((id) => {
const setupEntry = getLazyGeneratedBundledChannelEntryForRoot(id, packageRoot, cacheContext, {
const { rootScope, cacheContext } = resolveActiveBundledChannelCacheScope();
return listBundledChannelPluginIdsForRoot(rootScope).flatMap((id) => {
const setupEntry = getLazyGeneratedBundledChannelEntryForRoot(id, rootScope, cacheContext, {
includeSetup: true,
})?.setupEntry;
if (!hasSetupEntryFeature(setupEntry, feature)) {
return [];
}
const plugin = getBundledChannelSetupPluginForRoot(id, packageRoot, cacheContext);
const plugin = getBundledChannelSetupPluginForRoot(id, rootScope, cacheContext);
return plugin ? [plugin] : [];
});
}
export function getBundledChannelPlugin(id: ChannelId): ChannelPlugin | undefined {
const { packageRoot, cacheContext } = resolveActiveBundledChannelCacheScope();
return getBundledChannelPluginForRoot(id, packageRoot, cacheContext);
const { rootScope, cacheContext } = resolveActiveBundledChannelCacheScope();
return getBundledChannelPluginForRoot(id, rootScope, cacheContext);
}
export function getBundledChannelSecrets(id: ChannelId): ChannelPlugin["secrets"] | undefined {
const { packageRoot, cacheContext } = resolveActiveBundledChannelCacheScope();
return getBundledChannelSecretsForRoot(id, packageRoot, cacheContext);
const { rootScope, cacheContext } = resolveActiveBundledChannelCacheScope();
return getBundledChannelSecretsForRoot(id, rootScope, cacheContext);
}
export function getBundledChannelSetupPlugin(id: ChannelId): ChannelPlugin | undefined {
const { packageRoot, cacheContext } = resolveActiveBundledChannelCacheScope();
return getBundledChannelSetupPluginForRoot(id, packageRoot, cacheContext);
const { rootScope, cacheContext } = resolveActiveBundledChannelCacheScope();
return getBundledChannelSetupPluginForRoot(id, rootScope, cacheContext);
}
export function getBundledChannelSetupSecrets(id: ChannelId): ChannelPlugin["secrets"] | undefined {
const { packageRoot, cacheContext } = resolveActiveBundledChannelCacheScope();
return getBundledChannelSetupSecretsForRoot(id, packageRoot, cacheContext);
const { rootScope, cacheContext } = resolveActiveBundledChannelCacheScope();
return getBundledChannelSetupSecretsForRoot(id, rootScope, cacheContext);
}
export function requireBundledChannelPlugin(id: ChannelId): ChannelPlugin {
@@ -460,8 +478,8 @@ export function requireBundledChannelPlugin(id: ChannelId): ChannelPlugin {
}
export function setBundledChannelRuntime(id: ChannelId, runtime: PluginRuntime): void {
const { packageRoot, cacheContext } = resolveActiveBundledChannelCacheScope();
const setter = getLazyGeneratedBundledChannelEntryForRoot(id, packageRoot, cacheContext)?.entry
const { rootScope, cacheContext } = resolveActiveBundledChannelCacheScope();
const setter = getLazyGeneratedBundledChannelEntryForRoot(id, rootScope, cacheContext)?.entry
.setChannelRuntime;
if (!setter) {
throw new Error(`missing bundled channel runtime setter: ${id}`);

View File

@@ -9,6 +9,7 @@ export type BundledChannelPluginMetadata = BundledPluginMetadata;
export function listBundledChannelPluginMetadata(params?: {
rootDir?: string;
scanDir?: string;
includeChannelConfigs?: boolean;
includeSyntheticChannelConfigs?: boolean;
}): readonly BundledChannelPluginMetadata[] {
@@ -19,12 +20,14 @@ export function resolveBundledChannelGeneratedPath(
rootDir: string,
entry: BundledPluginMetadata["source"] | BundledPluginMetadata["setupSource"],
pluginDirName?: string,
scanDir?: string,
): string | null {
return resolveBundledPluginGeneratedPath(rootDir, entry, pluginDirName);
return resolveBundledPluginGeneratedPath(rootDir, entry, pluginDirName, scanDir);
}
export function resolveBundledChannelWorkspacePath(params: {
rootDir: string;
scanDir?: string;
pluginId: string;
}): string | null {
return resolveBundledPluginWorkspaceSourcePath(params);

View File

@@ -274,6 +274,45 @@ describe("bundled plugin metadata", () => {
);
});
it("scans direct plugin-tree overrides and resolves generated paths from that scan dir", () => {
const tempRoot = createGeneratedPluginTempRoot("openclaw-bundled-plugin-direct-tree-");
const pluginsDir = path.join(tempRoot, "bundled-plugins");
const pluginRoot = path.join(pluginsDir, "alpha");
writeJson(path.join(pluginRoot, "package.json"), {
name: "@openclaw/alpha",
version: "0.0.1",
openclaw: {
extensions: ["./index.ts"],
},
});
writeJson(path.join(pluginRoot, "openclaw.plugin.json"), {
id: "alpha",
channels: ["alpha"],
configSchema: { type: "object" },
});
fs.writeFileSync(path.join(pluginRoot, "index.ts"), "export const source = true;\n", "utf8");
clearBundledPluginMetadataCache();
expect(
listBundledPluginMetadata({
rootDir: tempRoot,
scanDir: pluginsDir,
}).map((entry) => entry.manifest.id),
).toEqual(["alpha"]);
expect(
resolveBundledPluginGeneratedPath(
tempRoot,
{
source: "./index.ts",
built: "index.js",
},
"alpha",
pluginsDir,
),
).toBe(path.join(pluginRoot, "index.ts"));
});
it("resolves bundled repo entry paths from dist before workspace source", () => {
const tempRoot = createGeneratedPluginTempRoot("openclaw-bundled-plugin-repo-entry-");
const pluginRoot = path.join(tempRoot, "extensions", "alpha");

View File

@@ -67,26 +67,44 @@ function readPackageManifest(pluginDir: string): PackageManifest | undefined {
}
}
function collectBundledPluginMetadataForPackageRoot(
function resolveBundledPluginMetadataScanDir(
packageRoot: string,
includeChannelConfigs: boolean,
includeSyntheticChannelConfigs: boolean,
): readonly BundledPluginMetadata[] {
const scanDir = resolveBundledPluginScanDir({
scanDir?: string,
): string | undefined {
if (scanDir) {
return path.resolve(scanDir);
}
return resolveBundledPluginScanDir({
packageRoot,
runningFromBuiltArtifact: RUNNING_FROM_BUILT_ARTIFACT,
});
if (!scanDir || !fs.existsSync(scanDir)) {
}
function resolveBundledPluginLookupParams(params: { rootDir: string; scanDir?: string }): {
rootDir: string;
scanDir?: string;
} {
return params.scanDir ? params : { rootDir: params.rootDir };
}
function collectBundledPluginMetadata(
packageRoot: string,
includeChannelConfigs: boolean,
includeSyntheticChannelConfigs: boolean,
scanDir?: string,
): readonly BundledPluginMetadata[] {
const resolvedScanDir = resolveBundledPluginMetadataScanDir(packageRoot, scanDir);
if (!resolvedScanDir || !fs.existsSync(resolvedScanDir)) {
return [];
}
const entries: BundledPluginMetadata[] = [];
for (const dirName of fs
.readdirSync(scanDir, { withFileTypes: true })
.readdirSync(resolvedScanDir, { withFileTypes: true })
.filter((entry) => entry.isDirectory())
.map((entry) => entry.name)
.toSorted((left, right) => left.localeCompare(right))) {
const pluginDir = path.join(scanDir, dirName);
const pluginDir = path.join(resolvedScanDir, dirName);
const manifestResult = loadPluginManifest(pluginDir, false);
if (!manifestResult.ok) {
continue;
@@ -165,15 +183,18 @@ function collectBundledPluginMetadataForPackageRoot(
export function listBundledPluginMetadata(params?: {
rootDir?: string;
scanDir?: string;
includeChannelConfigs?: boolean;
includeSyntheticChannelConfigs?: boolean;
}): readonly BundledPluginMetadata[] {
const rootDir = path.resolve(params?.rootDir ?? OPENCLAW_PACKAGE_ROOT);
const scanDir = params?.scanDir ? path.resolve(params.scanDir) : undefined;
const includeChannelConfigs = params?.includeChannelConfigs ?? !RUNNING_FROM_BUILT_ARTIFACT;
const includeSyntheticChannelConfigs =
params?.includeSyntheticChannelConfigs ?? includeChannelConfigs;
const cacheKey = JSON.stringify({
rootDir,
scanDir,
includeChannelConfigs,
includeSyntheticChannelConfigs,
});
@@ -182,10 +203,11 @@ export function listBundledPluginMetadata(params?: {
return cached;
}
const entries = Object.freeze(
collectBundledPluginMetadataForPackageRoot(
collectBundledPluginMetadata(
rootDir,
includeChannelConfigs,
includeSyntheticChannelConfigs,
scanDir,
),
);
bundledPluginMetadataCache.set(cacheKey, entries);
@@ -194,26 +216,50 @@ export function listBundledPluginMetadata(params?: {
export function findBundledPluginMetadataById(
pluginId: string,
params?: { rootDir?: string },
params?: { rootDir?: string; scanDir?: string },
): BundledPluginMetadata | undefined {
return listBundledPluginMetadata(params).find((entry) => entry.manifest.id === pluginId);
}
export function resolveBundledPluginWorkspaceSourcePath(params: {
rootDir: string;
scanDir?: string;
pluginId: string;
}): string | null {
const metadata = findBundledPluginMetadataById(params.pluginId, { rootDir: params.rootDir });
const metadata = findBundledPluginMetadataById(
params.pluginId,
resolveBundledPluginLookupParams({
rootDir: params.rootDir,
scanDir: params.scanDir,
}),
);
if (!metadata) {
return null;
}
if (params.scanDir) {
return path.resolve(params.scanDir, metadata.dirName);
}
return path.resolve(params.rootDir, "extensions", metadata.dirName);
}
function listBundledPluginEntryBaseDirs(params: {
rootDir: string;
pluginDirName?: string;
scanDir?: string;
}): string[] {
const baseDirs = [
path.resolve(params.rootDir, "dist", "extensions", params.pluginDirName ?? ""),
path.resolve(params.rootDir, "extensions", params.pluginDirName ?? ""),
...(params.scanDir ? [path.resolve(params.scanDir, params.pluginDirName ?? "")] : []),
];
return baseDirs.filter((entry, index, all) => all.indexOf(entry) === index);
}
export function resolveBundledPluginGeneratedPath(
rootDir: string,
entry: BundledPluginPathPair | undefined,
pluginDirName?: string,
scanDir?: string,
): string | null {
if (!entry) {
return null;
@@ -221,10 +267,11 @@ export function resolveBundledPluginGeneratedPath(
const entryOrder = [entry.built, entry.source].filter(
(candidate): candidate is string => typeof candidate === "string" && candidate.length > 0,
);
const baseDirs = [
path.resolve(rootDir, "dist", "extensions", pluginDirName ?? ""),
path.resolve(rootDir, "extensions", pluginDirName ?? ""),
];
const baseDirs = listBundledPluginEntryBaseDirs({
rootDir,
pluginDirName,
...(scanDir ? { scanDir } : {}),
});
for (const baseDir of baseDirs) {
for (const entryPath of entryOrder) {
const candidate = path.resolve(baseDir, normalizeRelativePluginEntryPath(entryPath));
@@ -244,8 +291,15 @@ export function resolveBundledPluginRepoEntryPath(params: {
rootDir: string;
pluginId: string;
preferBuilt?: boolean;
scanDir?: string;
}): string | null {
const metadata = findBundledPluginMetadataById(params.pluginId, { rootDir: params.rootDir });
const metadata = findBundledPluginMetadataById(
params.pluginId,
resolveBundledPluginLookupParams({
rootDir: params.rootDir,
scanDir: params.scanDir,
}),
);
if (!metadata) {
return null;
}
@@ -253,10 +307,11 @@ export function resolveBundledPluginRepoEntryPath(params: {
const entryOrder = params.preferBuilt
? [metadata.source.built, metadata.source.source]
: [metadata.source.source, metadata.source.built];
const baseDirs = [
path.resolve(params.rootDir, "dist", "extensions", metadata.dirName),
path.resolve(params.rootDir, "extensions", metadata.dirName),
];
const baseDirs = listBundledPluginEntryBaseDirs({
rootDir: params.rootDir,
pluginDirName: metadata.dirName,
...(params.scanDir ? { scanDir: params.scanDir } : {}),
});
for (const baseDir of baseDirs) {
for (const entryPath of entryOrder) {