mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 06:10:44 +00:00
refactor(channels): simplify bundled root scope helpers
This commit is contained in:
@@ -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[] {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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 [];
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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-"));
|
||||
|
||||
@@ -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}`);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user