fix(msteams): emit native cjs runtime entrypoints (#102366)

Co-authored-by: Galin Iliev <Galin.Iliev@microsoft.com>
This commit is contained in:
Galin Iliev
2026-07-11 23:48:05 -07:00
committed by GitHub
parent 8d39c1baa5
commit 68dcfe633d
4 changed files with 137 additions and 35 deletions

View File

@@ -59,7 +59,8 @@
"pluginApi": ">=2026.7.2"
},
"build": {
"openclawVersion": "2026.7.2"
"openclawVersion": "2026.7.2",
"runtimeFormat": "cjs"
},
"release": {
"publishToClawHub": true,

View File

@@ -2,8 +2,8 @@
export function listPublishablePluginPackageDirs(params?: Record<string, unknown>): string[];
/** List package-local runtime output files expected from a runtime build plan. */
export function listPluginNpmRuntimeBuildOutputs(plan: unknown): string[];
/** Resolve the package-local runtime build plan for one publishable plugin package. */
export function resolvePluginNpmRuntimeBuildPlan(params: unknown): {
export type PluginNpmRuntimeBuildFormat = "esm" | "cjs";
export type PluginNpmRuntimeBuildPlan = {
runtimeBuildOutputs: string[];
packageFiles: string[];
packagePeerMetadata: {
@@ -27,34 +27,20 @@ export function resolvePluginNpmRuntimeBuildPlan(params: unknown): {
[k: string]: string;
};
outDir: string;
runtimeFormat: PluginNpmRuntimeBuildFormat;
runtimeExtensions: string[];
runtimeSetupEntry: string | undefined;
} | null;
};
/** Resolve the package-local runtime build plan for one publishable plugin package. */
export function resolvePluginNpmRuntimeBuildPlan(params: unknown): PluginNpmRuntimeBuildPlan | null;
/** Build package-local runtime files and static assets for one plugin package. */
export function buildPluginNpmRuntime(params: unknown): Promise<{
assetBuildCommand: string | null;
copiedStaticAssets: string[];
runtimeBuildOutputs: string[];
packageFiles: unknown[];
packagePeerMetadata: {
peerDependencies: {
openclaw: string;
};
peerDependenciesMeta: unknown;
};
repoRoot: string;
packageDir: unknown;
pluginDir: string;
packageJson: unknown;
rootPackageJson: unknown;
sourceEntries: unknown[];
entry: {
[k: string]: string;
};
outDir: string;
runtimeExtensions: unknown;
runtimeSetupEntry: string | undefined;
} | null>;
export function buildPluginNpmRuntime(params: unknown): Promise<
| (PluginNpmRuntimeBuildPlan & {
assetBuildCommand: string | null;
copiedStaticAssets: string[];
})
| null
>;
export function parseArgs(argv: unknown):
| {
help: boolean;

View File

@@ -37,9 +37,17 @@ function isTypeScriptEntry(entry) {
return /\.(?:c|m)?ts$/u.test(entry);
}
function toPackageRuntimeEntry(entry) {
function resolveRuntimeBuildFormat(packageJson) {
return packageJson.openclaw?.build?.runtimeFormat === "cjs" ? "cjs" : "esm";
}
function runtimeBuildExtension(runtimeFormat) {
return runtimeFormat === "cjs" ? ".cjs" : ".js";
}
function toPackageRuntimeEntry(entry, runtimeFormat = "esm") {
const normalized = normalizePackageEntry(entry).replace(/^\.\//u, "");
return `./dist/${normalized.replace(/\.[^.]+$/u, ".js")}`;
return `./dist/${normalized.replace(/\.[^.]+$/u, runtimeBuildExtension(runtimeFormat))}`;
}
function collectExternalDependencyNames(packageJson) {
@@ -115,11 +123,41 @@ export function listPublishablePluginPackageDirs(params = {}) {
/** List package-local runtime output files expected from a runtime build plan. */
export function listPluginNpmRuntimeBuildOutputs(plan) {
const extension = runtimeBuildExtension(plan.runtimeFormat);
return Object.keys(plan.entry)
.map((entryKey) => `./dist/${entryKey}.js`)
.map((entryKey) => `./dist/${entryKey}${extension}`)
.toSorted((left, right) => left.localeCompare(right));
}
function rewriteCommonJsRuntimeSpecifiers(plan) {
if (plan.runtimeFormat !== "cjs") {
return;
}
const specifierRewrites = new Map(
plan.runtimeBuildOutputs.map((output) => {
const cjsSpecifier = output.replace(/^\.\/dist\//u, "./");
return [cjsSpecifier.replace(/\.cjs$/u, ".js"), cjsSpecifier];
}),
);
for (const output of plan.runtimeBuildOutputs) {
const outputPath = path.join(plan.packageDir, output.replace(/^\.\//u, ""));
let text = fs.readFileSync(outputPath, "utf8");
const original = text;
// Source entries stay .js for the root bundled build; package-local CJS
// artifacts must point at their generated .cjs sidecars instead.
for (const [fromSpecifier, toSpecifier] of specifierRewrites) {
text = text.replaceAll(
`specifier: ${JSON.stringify(fromSpecifier)}`,
`specifier: ${JSON.stringify(toSpecifier)}`,
);
}
if (text !== original) {
fs.writeFileSync(outputPath, text, "utf8");
}
}
}
/** Resolve package `files` entries needed for runtime build outputs and plugin metadata. */
function resolvePluginNpmRuntimePackageFiles(plan) {
const merged = new Set(
@@ -209,6 +247,7 @@ export function resolvePluginNpmRuntimeBuildPlan(params) {
return null;
}
const runtimeFormat = resolveRuntimeBuildFormat(packageJson);
const packageEntries = collectPluginSourceEntries(packageJson).map(normalizePackageEntry);
const requiresRuntimeBuild = packageEntries.some(isTypeScriptEntry);
if (!requiresRuntimeBuild) {
@@ -238,15 +277,16 @@ export function resolvePluginNpmRuntimeBuildPlan(params) {
sourceEntries,
entry,
outDir: path.join(packageDir, "dist"),
runtimeFormat,
runtimeExtensions: (Array.isArray(packageJson.openclaw?.extensions)
? packageJson.openclaw.extensions
: []
)
.map(normalizePackageEntry)
.filter(Boolean)
.map(toPackageRuntimeEntry),
.map((runtimeEntry) => toPackageRuntimeEntry(runtimeEntry, runtimeFormat)),
runtimeSetupEntry: normalizePackageEntry(packageJson.openclaw?.setupEntry)
? toPackageRuntimeEntry(packageJson.openclaw.setupEntry)
? toPackageRuntimeEntry(packageJson.openclaw.setupEntry, runtimeFormat)
: undefined,
};
return {
@@ -274,11 +314,13 @@ export async function buildPluginNpmRuntime(params) {
},
entry: plan.entry,
env,
fixedExtension: false,
fixedExtension: plan.runtimeFormat === "cjs",
format: plan.runtimeFormat,
logLevel: params.logLevel ?? "info",
outDir: plan.outDir,
platform: "node",
});
rewriteCommonJsRuntimeSpecifiers(plan);
const assetBuildCommand = runPackageAssetBuild(plan);
const missingStaticAssets = listMissingPackageStaticAssetSources(plan);
if (missingStaticAssets.length > 0) {

View File

@@ -1,7 +1,9 @@
// Plugin npm runtime build tests validate plugin runtime package builds.
import { existsSync, readFileSync } from "node:fs";
import path from "node:path";
import { describe, expect, it } from "vitest";
import {
buildPluginNpmRuntime,
listPublishablePluginPackageDirs,
resolvePluginNpmRuntimeBuildPlan,
} from "../scripts/lib/plugin-npm-runtime-build.mjs";
@@ -101,11 +103,82 @@ describe("plugin npm runtime build planning", () => {
expect(plan.entry["doctor-contract-api"]).toBe(
path.join(repoRoot, "extensions", pluginDir, "doctor-contract-api.ts"),
);
expect(plan.runtimeBuildOutputs).toContain("./dist/doctor-contract-api.js");
const extension = plan.runtimeFormat === "cjs" ? ".cjs" : ".js";
expect(plan.runtimeBuildOutputs).toContain(`./dist/doctor-contract-api${extension}`);
expect(plan.packageFiles).toContain("dist/**");
}
});
it("plans msteams startup runtime surfaces as native CommonJS entrypoints", () => {
const plan = expectPluginNpmRuntimeBuildPlan(
resolvePluginNpmRuntimeBuildPlan({
repoRoot,
packageDir: path.join(repoRoot, "extensions", "msteams"),
}),
);
expect(plan.runtimeFormat).toBe("cjs");
expect(plan.runtimeExtensions).toEqual(["./dist/index.cjs"]);
expect(plan.runtimeSetupEntry).toBe("./dist/setup-entry.cjs");
expect(plan.runtimeBuildOutputs).toEqual(
expect.arrayContaining([
"./dist/channel-plugin-api.cjs",
"./dist/doctor-contract-api.cjs",
"./dist/index.cjs",
"./dist/runtime-api.cjs",
"./dist/secret-contract-api.cjs",
"./dist/setup-entry.cjs",
"./dist/setup-plugin-api.cjs",
]),
);
});
it("builds msteams startup runtime surfaces as CommonJS files", async () => {
const result = await buildPluginNpmRuntime({
repoRoot,
packageDir: "extensions/msteams",
logLevel: "silent",
});
const plan = expectPluginNpmRuntimeBuildPlan(result);
expect(plan.runtimeFormat).toBe("cjs");
expect(plan.runtimeExtensions).toEqual(["./dist/index.cjs"]);
expect(plan.runtimeSetupEntry).toBe("./dist/setup-entry.cjs");
const entrypoints = [
"dist/index.cjs",
"dist/channel-plugin-api.cjs",
"dist/runtime-api.cjs",
"dist/setup-plugin-api.cjs",
"dist/secret-contract-api.cjs",
];
const missing = entrypoints.filter(
(relativePath) => !existsSync(path.join(repoRoot, "extensions/msteams", relativePath)),
);
expect(missing).toEqual([]);
for (const relativePath of entrypoints) {
const text = readFileSync(path.join(repoRoot, "extensions/msteams", relativePath), "utf8");
expect(text).not.toMatch(/^import\s/u);
expect(text).toMatch(/(?:require\(|exports\.)/u);
}
const indexText = readFileSync(
path.join(repoRoot, "extensions/msteams/dist/index.cjs"),
"utf8",
);
expect(indexText).toContain('specifier: "./channel-plugin-api.cjs"');
expect(indexText).toContain('specifier: "./secret-contract-api.cjs"');
expect(indexText).toContain('specifier: "./runtime-api.cjs"');
const setupEntryText = readFileSync(
path.join(repoRoot, "extensions/msteams/dist/setup-entry.cjs"),
"utf8",
);
expect(setupEntryText).toContain('specifier: "./setup-plugin-api.cjs"');
expect(setupEntryText).toContain('specifier: "./secret-contract-api.cjs"');
});
it("builds Tencent setup metadata for installed-package migrations", () => {
const plan = expectPluginNpmRuntimeBuildPlan(
resolvePluginNpmRuntimeBuildPlan({