Skip to content

Commit dadf000

Browse files
committed
fix(plugins): alias bundled public surfaces in source loaders
1 parent b2f0f67 commit dadf000

3 files changed

Lines changed: 201 additions & 0 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ Docs: https://docs.openclaw.ai
4848
### Fixes
4949

5050
- Agents/session status: keep semantic `session_status({ sessionKey: "current" })` on the live run session even before that run has a persisted session-store entry, instead of falling back to the sandbox policy key. Thanks @vincentkoc.
51+
- QA/Slack: resolve bundled official plugin public-surface package aliases during source-mode QA runs, so release Slack live validation can load `@openclaw/slack/api.js` without workspace symlinks. Thanks @vincentkoc.
5152
- Codex: pass the live run session key into app-server dynamic tools when sandbox policy uses a separate session key, so `session_status({ sessionKey: "current" })` reports the active run instead of the sandbox policy key. Thanks @vincentkoc.
5253
- Plugins/tools: mark manifest-optional sibling tools as optional even when they come from a shared non-optional factory, so cached/status/MCP metadata keeps opt-in tool policy accurate. Thanks @vincentkoc.
5354
- Matrix: keep `streaming.progress.toolProgress` scoped to progress draft mode, so partial and quiet Matrix previews do not lose tool progress unless `streaming.preview.toolProgress` is disabled. Thanks @vincentkoc.

src/plugins/sdk-alias.test.ts

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -198,6 +198,39 @@ function createPluginSdkAliasTargetFixture(params?: {
198198
};
199199
}
200200

201+
function createBundledPluginPackagePublicSurfaceAliasFixture() {
202+
const fixture = createPluginSdkAliasTargetFixture();
203+
const extensionRoot = path.join(fixture.fixture.root, bundledPluginRoot("slack"));
204+
const distExtensionRoot = path.join(fixture.fixture.root, "dist", "extensions", "slack");
205+
mkdirSafeDir(extensionRoot);
206+
mkdirSafeDir(distExtensionRoot);
207+
fs.writeFileSync(
208+
path.join(extensionRoot, "package.json"),
209+
JSON.stringify({ name: "@openclaw/slack", type: "module" }, null, 2),
210+
"utf-8",
211+
);
212+
const sourceApiPath = path.join(extensionRoot, "api.ts");
213+
const sourceRuntimeApiPath = path.join(extensionRoot, "runtime-api.ts");
214+
const distApiPath = path.join(distExtensionRoot, "api.js");
215+
const distRuntimeApiPath = path.join(distExtensionRoot, "runtime-api.js");
216+
fs.writeFileSync(sourceApiPath, "export const slackApi = 'source';\n", "utf-8");
217+
fs.writeFileSync(sourceRuntimeApiPath, "export const slackRuntimeApi = 'source';\n", "utf-8");
218+
fs.writeFileSync(distApiPath, "export const slackApi = 'dist';\n", "utf-8");
219+
fs.writeFileSync(distRuntimeApiPath, "export const slackRuntimeApi = 'dist';\n", "utf-8");
220+
fs.writeFileSync(
221+
path.join(extensionRoot, "internal.ts"),
222+
"export const internal = true;\n",
223+
"utf-8",
224+
);
225+
return {
226+
...fixture,
227+
distApiPath,
228+
distRuntimeApiPath,
229+
sourceApiPath,
230+
sourceRuntimeApiPath,
231+
};
232+
}
233+
201234
function writePluginEntry(root: string, relativePath: string) {
202235
const pluginEntry = path.join(root, relativePath);
203236
fs.mkdirSync(path.dirname(pluginEntry), { recursive: true });
@@ -777,6 +810,47 @@ describe("plugin sdk alias helpers", () => {
777810
});
778811
});
779812

813+
it("aliases bundled plugin package public surfaces for source plugin transforms", () => {
814+
const { fixture, sourceApiPath, sourceRuntimeApiPath } =
815+
createBundledPluginPackagePublicSurfaceAliasFixture();
816+
const sourcePluginEntry = writePluginEntry(
817+
fixture.root,
818+
bundledPluginFile("qa-lab", "src/live-transports/slack/slack-live.runtime.ts"),
819+
);
820+
821+
const aliases = withEnv({ NODE_ENV: undefined }, () =>
822+
buildPluginLoaderAliasMap(sourcePluginEntry),
823+
);
824+
825+
expect(fs.realpathSync(aliases["@openclaw/slack/api.js"] ?? "")).toBe(
826+
fs.realpathSync(sourceApiPath),
827+
);
828+
expect(fs.realpathSync(aliases["@openclaw/slack/runtime-api.js"] ?? "")).toBe(
829+
fs.realpathSync(sourceRuntimeApiPath),
830+
);
831+
expect(aliases["@openclaw/slack/internal.js"]).toBeUndefined();
832+
});
833+
834+
it("aliases bundled plugin package public surfaces to dist when dist resolution is requested", () => {
835+
const { fixture, distApiPath, distRuntimeApiPath } =
836+
createBundledPluginPackagePublicSurfaceAliasFixture();
837+
const sourcePluginEntry = writePluginEntry(
838+
fixture.root,
839+
bundledPluginFile("qa-lab", "src/live-transports/slack/slack-live.runtime.ts"),
840+
);
841+
842+
const aliases = withEnv({ NODE_ENV: undefined }, () =>
843+
buildPluginLoaderAliasMap(sourcePluginEntry, undefined, undefined, "dist"),
844+
);
845+
846+
expect(fs.realpathSync(aliases["@openclaw/slack/api.js"] ?? "")).toBe(
847+
fs.realpathSync(distApiPath),
848+
);
849+
expect(fs.realpathSync(aliases["@openclaw/slack/runtime-api.js"] ?? "")).toBe(
850+
fs.realpathSync(distRuntimeApiPath),
851+
);
852+
});
853+
780854
it("falls back to source plugin-sdk subpath aliases when dist chunks are stale", () => {
781855
const fixture = createPluginSdkAliasFixture({
782856
srcFile: "provider-entry.ts",

src/plugins/sdk-alias.ts

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -274,6 +274,7 @@ const PLUGIN_SDK_SOURCE_CANDIDATE_EXTENSIONS = [
274274
".cts",
275275
".cjs",
276276
] as const;
277+
const BUNDLED_PLUGIN_PUBLIC_SURFACE_SOURCE_PATTERN = /^(?:api|runtime-api|test-api|.+-api)$/u;
277278
const JS_STATIC_RELATIVE_DEPENDENCY_PATTERN =
278279
/(?:\bfrom\s*["']|\bimport\s*\(\s*["']|\brequire\s*\(\s*["'])(\.{1,2}\/[^"']+)["']/g;
279280

@@ -320,6 +321,125 @@ function readPrivateLocalOnlyPluginSdkSubpaths(packageRoot: string): string[] {
320321
}
321322
}
322323

324+
function readBundledPluginPackageName(packageJsonPath: string): string | null {
325+
try {
326+
const parsed = JSON.parse(fs.readFileSync(packageJsonPath, "utf-8")) as { name?: unknown };
327+
const name = typeof parsed.name === "string" ? parsed.name.trim() : "";
328+
return name.startsWith("@openclaw/") ? name : null;
329+
} catch {
330+
return null;
331+
}
332+
}
333+
334+
function listBundledPluginPublicSurfaceSourceBasenames(extensionSourceRoot: string): string[] {
335+
try {
336+
return fs
337+
.readdirSync(extensionSourceRoot, { withFileTypes: true })
338+
.filter((entry) => entry.isFile())
339+
.map((entry) => entry.name)
340+
.flatMap((fileName) => {
341+
const ext = PLUGIN_SDK_SOURCE_CANDIDATE_EXTENSIONS.find((candidateExt) =>
342+
fileName.endsWith(candidateExt),
343+
);
344+
if (!ext) {
345+
return [];
346+
}
347+
const basename = fileName.slice(0, -ext.length);
348+
return BUNDLED_PLUGIN_PUBLIC_SURFACE_SOURCE_PATTERN.test(basename) ? [basename] : [];
349+
})
350+
.toSorted();
351+
} catch {
352+
return [];
353+
}
354+
}
355+
356+
function resolveBundledPluginPublicSurfaceAliasTarget(params: {
357+
packageRoot: string;
358+
dirName: string;
359+
basename: string;
360+
orderedKinds: PluginSdkAliasCandidateKind[];
361+
}): string | null {
362+
for (const kind of params.orderedKinds) {
363+
if (kind === "dist") {
364+
const candidate = path.join(
365+
params.packageRoot,
366+
"dist",
367+
"extensions",
368+
params.dirName,
369+
`${params.basename}.js`,
370+
);
371+
if (fs.existsSync(candidate)) {
372+
return candidate;
373+
}
374+
continue;
375+
}
376+
for (const ext of PLUGIN_SDK_SOURCE_CANDIDATE_EXTENSIONS) {
377+
const candidate = path.join(
378+
params.packageRoot,
379+
"extensions",
380+
params.dirName,
381+
`${params.basename}${ext}`,
382+
);
383+
if (fs.existsSync(candidate)) {
384+
return candidate;
385+
}
386+
}
387+
}
388+
return null;
389+
}
390+
391+
function resolveBundledPluginPackagePublicSurfaceAliasMap(params: {
392+
modulePath: string;
393+
argv1?: string;
394+
moduleUrl?: string;
395+
pluginSdkResolution: PluginSdkResolutionPreference;
396+
}): Record<string, string> {
397+
const packageRoot = resolveLoaderPluginSdkPackageRoot(params);
398+
if (!packageRoot) {
399+
return {};
400+
}
401+
const extensionsRoot = path.join(packageRoot, "extensions");
402+
let extensionDirs: fs.Dirent[];
403+
try {
404+
extensionDirs = fs.readdirSync(extensionsRoot, { withFileTypes: true });
405+
} catch {
406+
return {};
407+
}
408+
const orderedKinds = resolvePluginSdkAliasCandidateOrder({
409+
modulePath: params.modulePath,
410+
isProduction: process.env.NODE_ENV === "production",
411+
pluginSdkResolution: params.pluginSdkResolution,
412+
});
413+
const aliasMap: Record<string, string> = {};
414+
for (const entry of extensionDirs) {
415+
if (!entry.isDirectory()) {
416+
continue;
417+
}
418+
const dirName = entry.name;
419+
const packageName = readBundledPluginPackageName(
420+
path.join(extensionsRoot, dirName, "package.json"),
421+
);
422+
if (!packageName) {
423+
continue;
424+
}
425+
for (const basename of listBundledPluginPublicSurfaceSourceBasenames(
426+
path.join(extensionsRoot, dirName),
427+
)) {
428+
const target = resolveBundledPluginPublicSurfaceAliasTarget({
429+
packageRoot,
430+
dirName,
431+
basename,
432+
orderedKinds,
433+
});
434+
if (!target) {
435+
continue;
436+
}
437+
aliasMap[`${packageName}/${basename}.js`] = normalizeJitiAliasTargetPath(target);
438+
}
439+
}
440+
return aliasMap;
441+
}
442+
323443
function shouldIncludePrivateLocalOnlyPluginSdkSubpaths() {
324444
return process.env.OPENCLAW_ENABLE_PRIVATE_QA_CLI === "1";
325445
}
@@ -626,6 +746,12 @@ export function buildPluginLoaderAliasMap(
626746
...(extensionApiAlias
627747
? { "openclaw/extension-api": normalizeJitiAliasTargetPath(extensionApiAlias) }
628748
: {}),
749+
...resolveBundledPluginPackagePublicSurfaceAliasMap({
750+
modulePath,
751+
argv1,
752+
moduleUrl,
753+
pluginSdkResolution,
754+
}),
629755
...(pluginSdkAlias
630756
? Object.fromEntries(
631757
PLUGIN_SDK_PACKAGE_NAMES.map((packageName) => [

0 commit comments

Comments
 (0)