From 68dcfe633d85acdb38cdc102de806f2c7141cd6f Mon Sep 17 00:00:00 2001 From: Galin Iliev Date: Sat, 11 Jul 2026 23:48:05 -0700 Subject: [PATCH] fix(msteams): emit native cjs runtime entrypoints (#102366) Co-authored-by: Galin Iliev --- extensions/msteams/package.json | 3 +- scripts/lib/plugin-npm-runtime-build.d.mts | 40 ++++-------- scripts/lib/plugin-npm-runtime-build.mjs | 54 ++++++++++++++-- test/plugin-npm-runtime-build.test.ts | 75 +++++++++++++++++++++- 4 files changed, 137 insertions(+), 35 deletions(-) diff --git a/extensions/msteams/package.json b/extensions/msteams/package.json index d5345252ad5f..b675ec809773 100644 --- a/extensions/msteams/package.json +++ b/extensions/msteams/package.json @@ -59,7 +59,8 @@ "pluginApi": ">=2026.7.2" }, "build": { - "openclawVersion": "2026.7.2" + "openclawVersion": "2026.7.2", + "runtimeFormat": "cjs" }, "release": { "publishToClawHub": true, diff --git a/scripts/lib/plugin-npm-runtime-build.d.mts b/scripts/lib/plugin-npm-runtime-build.d.mts index ba4fe54cc87c..830de923ab92 100644 --- a/scripts/lib/plugin-npm-runtime-build.d.mts +++ b/scripts/lib/plugin-npm-runtime-build.d.mts @@ -2,8 +2,8 @@ export function listPublishablePluginPackageDirs(params?: Record): 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; diff --git a/scripts/lib/plugin-npm-runtime-build.mjs b/scripts/lib/plugin-npm-runtime-build.mjs index cc1ca347f3b7..378861085e25 100644 --- a/scripts/lib/plugin-npm-runtime-build.mjs +++ b/scripts/lib/plugin-npm-runtime-build.mjs @@ -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) { diff --git a/test/plugin-npm-runtime-build.test.ts b/test/plugin-npm-runtime-build.test.ts index e46f0b224258..5ccefa00bbd0 100644 --- a/test/plugin-npm-runtime-build.test.ts +++ b/test/plugin-npm-runtime-build.test.ts @@ -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({