fix(release): handle nested default-wrapped bundled channel entries

This commit is contained in:
Tak Hoffman
2026-04-11 11:33:29 -05:00
parent f3f1ab0a3f
commit 49b42b4a45
5 changed files with 117 additions and 30 deletions

View File

@@ -12,6 +12,7 @@ import {
resolveBundledChannelGeneratedPath,
type BundledChannelPluginMetadata,
} from "../../plugins/bundled-channel-runtime.js";
import { unwrapDefaultModuleExport } from "../../plugins/module-export.js";
import type { PluginRuntime } from "../../plugins/runtime/types.js";
import { isJavaScriptModulePath, loadChannelPluginModule } from "./module-loader.js";
import type { ChannelPlugin } from "./types.plugin.js";
@@ -37,12 +38,7 @@ const OPENCLAW_PACKAGE_ROOT =
function resolveChannelPluginModuleEntry(
moduleExport: unknown,
): BundledChannelEntryContract | null {
const resolved =
moduleExport &&
typeof moduleExport === "object" &&
"default" in (moduleExport as Record<string, unknown>)
? (moduleExport as { default: unknown }).default
: moduleExport;
const resolved = unwrapDefaultModuleExport(moduleExport);
if (!resolved || typeof resolved !== "object") {
return null;
}
@@ -65,12 +61,7 @@ function resolveChannelPluginModuleEntry(
function resolveChannelSetupModuleEntry(
moduleExport: unknown,
): BundledChannelSetupEntryContract | null {
const resolved =
moduleExport &&
typeof moduleExport === "object" &&
"default" in (moduleExport as Record<string, unknown>)
? (moduleExport as { default: unknown }).default
: moduleExport;
const resolved = unwrapDefaultModuleExport(moduleExport);
if (!resolved || typeof resolved !== "object") {
return null;
}

View File

@@ -12,6 +12,7 @@ import { createCapturedPluginRegistration } from "./captured-registration.js";
import { discoverOpenClawPlugins } from "./discovery.js";
import type { PluginLoadOptions } from "./loader.js";
import { loadPluginManifestRegistry } from "./manifest-registry.js";
import { unwrapDefaultModuleExport } from "./module-export.js";
import { createEmptyPluginRegistry } from "./registry-empty.js";
import type { PluginRecord, PluginRegistry } from "./registry.js";
import {
@@ -95,12 +96,7 @@ function resolvePluginModuleExport(moduleExport: unknown): {
definition?: OpenClawPluginDefinition;
register?: OpenClawPluginDefinition["register"];
} {
const resolved =
moduleExport &&
typeof moduleExport === "object" &&
"default" in (moduleExport as Record<string, unknown>)
? (moduleExport as { default: unknown }).default
: moduleExport;
const resolved = unwrapDefaultModuleExport(moduleExport);
if (typeof resolved === "function") {
return {
register: resolved as OpenClawPluginDefinition["register"],

View File

@@ -2781,6 +2781,99 @@ module.exports = { id: "throws-after-import", register() {} };`,
expect(disabled?.status).toBe("disabled");
});
it("loads bundled channel entries through nested default export wrappers", () => {
useNoBundledPlugins();
const pluginDir = makeTempDir();
const fullMarker = path.join(pluginDir, "full-loaded.txt");
fs.writeFileSync(
path.join(pluginDir, "package.json"),
JSON.stringify(
{
name: "@openclaw/nested-default-channel",
openclaw: {
extensions: ["./index.cjs"],
},
},
null,
2,
),
"utf-8",
);
fs.writeFileSync(
path.join(pluginDir, "openclaw.plugin.json"),
JSON.stringify(
{
id: "nested-default-channel",
configSchema: EMPTY_PLUGIN_SCHEMA,
channels: ["nested-default-channel"],
},
null,
2,
),
"utf-8",
);
fs.writeFileSync(
path.join(pluginDir, "index.cjs"),
`module.exports = {
default: {
default: {
id: "nested-default-channel",
kind: "bundled-channel-entry",
name: "Nested Default Channel",
description: "interop-wrapped bundled channel entry",
register(api) {
require("node:fs").writeFileSync(${JSON.stringify(fullMarker)}, "loaded", "utf-8");
api.registerChannel({
plugin: {
id: "nested-default-channel",
meta: {
id: "nested-default-channel",
label: "Nested Default Channel",
selectionLabel: "Nested Default Channel",
docsPath: "/channels/nested-default-channel",
blurb: "interop-wrapped bundled channel entry",
},
capabilities: { chatTypes: ["direct"] },
config: {
listAccountIds: () => ["default"],
resolveAccount: () => ({ accountId: "default", token: "configured" }),
},
outbound: { deliveryMode: "direct" },
},
});
},
},
},
};`,
"utf-8",
);
const registry = loadOpenClawPlugins({
cache: false,
config: {
channels: {
"nested-default-channel": {
enabled: true,
token: "configured",
},
},
plugins: {
load: { paths: [pluginDir] },
allow: ["nested-default-channel"],
},
},
});
expect(fs.existsSync(fullMarker)).toBe(true);
expect(registry.plugins.find((entry) => entry.id === "nested-default-channel")?.status).toBe(
"loaded",
);
expect(registry.channels.some((entry) => entry.plugin.id === "nested-default-channel")).toBe(
true,
);
});
it("does not treat manifest channel ids as scoped plugin id matches", () => {
useNoBundledPlugins();
const target = writePlugin({

View File

@@ -55,6 +55,7 @@ import {
listMemoryPromptSupplements,
restoreMemoryPluginState,
} from "./memory-state.js";
import { unwrapDefaultModuleExport } from "./module-export.js";
import { isPathInside, safeStatSync } from "./path-safety.js";
import { createPluginRegistry, type PluginRecord, type PluginRegistry } from "./registry.js";
import { resolvePluginCacheInputs } from "./roots.js";
@@ -602,12 +603,7 @@ function resolvePluginModuleExport(moduleExport: unknown): {
definition?: OpenClawPluginDefinition;
register?: OpenClawPluginDefinition["register"];
} {
const resolved =
moduleExport &&
typeof moduleExport === "object" &&
"default" in (moduleExport as Record<string, unknown>)
? (moduleExport as { default: unknown }).default
: moduleExport;
const resolved = unwrapDefaultModuleExport(moduleExport);
if (typeof resolved === "function") {
return {
register: resolved as OpenClawPluginDefinition["register"],
@@ -624,12 +620,7 @@ function resolvePluginModuleExport(moduleExport: unknown): {
function resolveSetupChannelRegistration(moduleExport: unknown): {
plugin?: ChannelPlugin;
} {
const resolved =
moduleExport &&
typeof moduleExport === "object" &&
"default" in (moduleExport as Record<string, unknown>)
? (moduleExport as { default: unknown }).default
: moduleExport;
const resolved = unwrapDefaultModuleExport(moduleExport);
if (!resolved || typeof resolved !== "object") {
return {};
}

View File

@@ -0,0 +1,16 @@
export function unwrapDefaultModuleExport(moduleExport: unknown): unknown {
let resolved = moduleExport;
const seen = new Set<unknown>();
while (
resolved &&
typeof resolved === "object" &&
"default" in (resolved as Record<string, unknown>) &&
!seen.has(resolved)
) {
seen.add(resolved);
resolved = (resolved as { default: unknown }).default;
}
return resolved;
}