diff --git a/extensions/diffs-language-pack/package.json b/extensions/diffs-language-pack/package.json index 13ebed0bce5c..9c0e8e8780b0 100644 --- a/extensions/diffs-language-pack/package.json +++ b/extensions/diffs-language-pack/package.json @@ -28,6 +28,7 @@ "build": "node --import tsx ../../scripts/build-diffs-viewer-runtime.mts full" }, "build": { + "bundledDist": false, "openclawVersion": "2026.8.1", "staticAssets": [ { diff --git a/extensions/diffs/package.json b/extensions/diffs/package.json index 5b1784756dc7..0d81272e36ec 100644 --- a/extensions/diffs/package.json +++ b/extensions/diffs/package.json @@ -34,6 +34,7 @@ "build": "node --import tsx ../../scripts/build-diffs-viewer-runtime.mts curated" }, "build": { + "bundledDist": false, "openclawVersion": "2026.8.1", "staticAssets": [ { diff --git a/package.json b/package.json index c9a2c64fb339..918ca5f1041f 100644 --- a/package.json +++ b/package.json @@ -1517,7 +1517,7 @@ "audit:seams": "node --import tsx scripts/audit-seams.mts", "build": "node --import tsx scripts/build-all.mts", "build:ci-artifacts": "node --import tsx scripts/build-all.mts ciArtifacts", - "build:docker": "node --import tsx scripts/tsdown-build.mts && node --import tsx scripts/check-cli-bootstrap-imports.mts && node scripts/runtime-postbuild.mjs && node --import tsx scripts/build-stamp.mts && node --import tsx scripts/runtime-postbuild-stamp.mts && pnpm plugins:assets:build && pnpm plugins:assets:copy && node --import tsx scripts/write-build-info.ts && node --import tsx scripts/write-cli-startup-metadata.ts", + "build:docker": "pnpm plugins:assets:build && node --import tsx scripts/tsdown-build.mts && node --import tsx scripts/check-cli-bootstrap-imports.mts && node scripts/runtime-postbuild.mjs && node --import tsx scripts/build-stamp.mts && node --import tsx scripts/runtime-postbuild-stamp.mts && pnpm plugins:assets:copy && node --import tsx scripts/write-build-info.ts && node --import tsx scripts/write-cli-startup-metadata.ts", "build:package": "node --import tsx scripts/build-all.mts package", "build:plugin-sdk:dts": "node scripts/run-tsgo.mjs -p tsconfig.plugin-sdk.dts.json --declaration true", "build:plugin-sdk:strict-smoke": "node --import tsx scripts/tsdown-build.mts && node scripts/runtime-postbuild.mjs && node --import tsx scripts/run-with-env.mts OPENCLAW_PLUGIN_SDK_CANONICAL_DTS=1 -- node --import tsx scripts/write-plugin-sdk-entry-dts.ts && node --import tsx scripts/check-plugin-sdk-exports.mts", diff --git a/scripts/lib/bundled-plugin-build-entries-types.d.ts b/scripts/lib/bundled-plugin-build-entries-types.d.ts index efee305008ce..818b2b1782bf 100644 --- a/scripts/lib/bundled-plugin-build-entries-types.d.ts +++ b/scripts/lib/bundled-plugin-build-entries-types.d.ts @@ -14,6 +14,7 @@ export type BundledPluginBuildEntryParams = { export const NON_PACKAGED_BUNDLED_PLUGIN_DIRS: Set; export const DOCKER_SELECTED_PLUGIN_BUILD_IDS_ENV: string; +export function parseDockerSelectedPluginBuildIdFilter(env?: NodeJS.ProcessEnv): Set | null; export function collectPluginSourceEntries(packageJson: unknown): string[]; export function collectTopLevelPublicSurfaceEntries(pluginDir: string): string[]; export function collectRootPackageExcludedExtensionDirs( diff --git a/scripts/lib/bundled-plugin-build-entries.mjs b/scripts/lib/bundled-plugin-build-entries.mjs index a03c696f0fe8..66928904fbc6 100644 --- a/scripts/lib/bundled-plugin-build-entries.mjs +++ b/scripts/lib/bundled-plugin-build-entries.mjs @@ -37,7 +37,7 @@ function parseBundledPluginBuildIdFilter(env = process.env) { ); } -function parseDockerSelectedPluginBuildIdFilter(env = process.env) { +export function parseDockerSelectedPluginBuildIdFilter(env = process.env) { const raw = env[DOCKER_SELECTED_PLUGIN_BUILD_IDS_ENV]; if (typeof raw !== "string" || raw.trim() === "") { return null; diff --git a/scripts/lib/static-extension-assets.mts b/scripts/lib/static-extension-assets.mts index fc141e714881..b35868941524 100644 --- a/scripts/lib/static-extension-assets.mts +++ b/scripts/lib/static-extension-assets.mts @@ -2,6 +2,7 @@ import { spawnSync } from "node:child_process"; import fs from "node:fs"; import path from "node:path"; +import { parseDockerSelectedPluginBuildIdFilter } from "./bundled-plugin-build-entries.mjs"; import { isRecord } from "./record-shared.mjs"; type StaticExtensionAsset = { @@ -13,6 +14,7 @@ type StaticExtensionAsset = { type StaticExtensionAssetParams = { rootDir?: string; fs?: typeof fs; + env?: NodeJS.ProcessEnv; includeExternalPlugins?: boolean; assets?: StaticExtensionAsset[]; warn?: (message: string) => void; @@ -164,6 +166,7 @@ export function discoverStaticExtensionAssets(params: StaticExtensionAssetParams const rootDir = params.rootDir ?? process.cwd(); const fsImpl = params.fs ?? fs; const includeExternalPlugins = params.includeExternalPlugins ?? false; + const dockerSelectedPluginIds = parseDockerSelectedPluginBuildIdFilter(params.env ?? process.env); const assets: StaticExtensionAsset[] = []; for (const { dirName, hasPackageJson, packageJsonPath } of listExtensionPackageDirs( rootDir, @@ -173,7 +176,11 @@ export function discoverStaticExtensionAssets(params: StaticExtensionAssetParams continue; } const packageJson = readJsonFile(packageJsonPath, fsImpl); - if (!includeExternalPlugins && isExternalDistPackage(packageJson)) { + if ( + !includeExternalPlugins && + isExternalDistPackage(packageJson) && + !dockerSelectedPluginIds?.has(dirName) + ) { continue; } for (const entry of readPackageStaticAssetEntries(packageJson)) { @@ -196,7 +203,8 @@ function discoverStaticExtensionRuntimeOverlayAssets(params: StaticExtensionAsse const rootDir = params.rootDir ?? process.cwd(); const fsImpl = params.fs ?? fs; const assetsByDest = new Map(); - for (const asset of params.assets ?? discoverStaticExtensionAssets({ rootDir, fs: fsImpl })) { + for (const asset of params.assets ?? + discoverStaticExtensionAssets({ rootDir, fs: fsImpl, env: params.env })) { assetsByDest.set(asset.dest, asset); } for (const { dirName, packageDir } of listDistExtensionPackageDirs(rootDir, fsImpl)) { @@ -278,7 +286,8 @@ export function listGeneratedExtensionAssetSources(params: StaticExtensionAssetP export function copyStaticExtensionAssets(params: StaticExtensionAssetParams = {}) { const rootDir = params.rootDir ?? process.cwd(); const fsImpl = params.fs ?? fs; - const assets = params.assets ?? discoverStaticExtensionAssets({ rootDir, fs: fsImpl }); + const assets = + params.assets ?? discoverStaticExtensionAssets({ rootDir, fs: fsImpl, env: params.env }); const warn = params.warn ?? console.warn; for (const { src, dest } of assets) { const srcPath = path.join(rootDir, src); @@ -332,7 +341,12 @@ export function copyStaticExtensionAssetsForPackage( const fsImpl = params.fs ?? fs; const assets = params.assets ?? - discoverStaticExtensionAssets({ rootDir, fs: fsImpl, includeExternalPlugins: true }); + discoverStaticExtensionAssets({ + rootDir, + fs: fsImpl, + env: params.env, + includeExternalPlugins: true, + }); const packagePrefix = `extensions/${params.pluginDir}/`; const rootDistPrefix = `dist/extensions/${params.pluginDir}/`; const copied: string[] = []; diff --git a/test/package-scripts.test.ts b/test/package-scripts.test.ts index 3651a234649e..326c05338334 100644 --- a/test/package-scripts.test.ts +++ b/test/package-scripts.test.ts @@ -182,6 +182,17 @@ describe("package scripts", () => { ); }); + it("builds generated plugin assets before Docker runtime postbuild", () => { + const commands = expectDefined( + readPackageJson().scripts["build:docker"], + "package script build:docker", + ).split(" && "); + + expect(commands.indexOf("pnpm plugins:assets:build")).toBeLessThan( + commands.indexOf("node scripts/runtime-postbuild.mjs"), + ); + }); + it("cleans package builds before validating release contents", () => { const scripts = readPackageJson().scripts; diff --git a/test/scripts/build-external-plugin-local-dist.test.ts b/test/scripts/build-external-plugin-local-dist.test.ts index 4f0bfc2aa575..82eabbc24bb8 100644 --- a/test/scripts/build-external-plugin-local-dist.test.ts +++ b/test/scripts/build-external-plugin-local-dist.test.ts @@ -13,9 +13,15 @@ describe("external plugin local dist build", () => { const packageDirs = listExternalPluginLocalDistPackageDirs(); const excludedPluginIds = collectRootPackageExcludedExtensionDirs(); - expect(packageDirs).toHaveLength(61); + expect(packageDirs).toHaveLength(63); expect(packageDirs).toEqual( - expect.arrayContaining(["extensions/slack", "extensions/sms", "extensions/mxc"]), + expect.arrayContaining([ + "extensions/diffs", + "extensions/diffs-language-pack", + "extensions/slack", + "extensions/sms", + "extensions/mxc", + ]), ); expect(packageDirs).not.toContain("extensions/whatsapp"); expect( diff --git a/test/scripts/bundled-plugin-build-entries.test.ts b/test/scripts/bundled-plugin-build-entries.test.ts index fdcf9ea45add..9a7922a6a315 100644 --- a/test/scripts/bundled-plugin-build-entries.test.ts +++ b/test/scripts/bundled-plugin-build-entries.test.ts @@ -196,7 +196,14 @@ describe("bundled plugin build entries", () => { const entries = listBundledPluginBuildEntries(); const artifacts = listBundledPluginPackArtifacts(); - for (const pluginId of ["copilot", "openshell", "slack", "tokenjuice"]) { + for (const pluginId of [ + "copilot", + "diffs", + "diffs-language-pack", + "openshell", + "slack", + "tokenjuice", + ]) { expectNoPrefixMatches(Object.keys(entries), `extensions/${pluginId}/`); expectNoPrefixMatches(artifacts, `dist/extensions/${pluginId}/`); } diff --git a/test/scripts/runtime-postbuild.test.ts b/test/scripts/runtime-postbuild.test.ts index 55152b0eab63..1e6f4722cb37 100644 --- a/test/scripts/runtime-postbuild.test.ts +++ b/test/scripts/runtime-postbuild.test.ts @@ -108,8 +108,6 @@ describe("runtime postbuild static assets", () => { "dist/extensions/acpx/mcp-command-line.mjs", "dist/extensions/acpx/mcp-proxy.mjs", "dist/extensions/crabbox/assets/openclaw-worker-wallpaper.png", - "dist/extensions/diffs-language-pack/assets/viewer-runtime.js", - "dist/extensions/diffs/assets/viewer-runtime.js", "dist/extensions/discord/assets/embedded-app-sdk.mjs", "dist/extensions/onepassword/onepassword-op-path.js", "dist/extensions/onepassword/onepassword-secret-id.js", @@ -117,8 +115,10 @@ describe("runtime postbuild static assets", () => { "dist/extensions/vault/vault-secret-id.js", "dist/extensions/vault/vault-secret-ref-resolver.js", ]); - expect(payload.sources).toContain("extensions/diffs-language-pack/assets/viewer-runtime.js"); - expect(payload.sources).toContain("extensions/diffs/assets/viewer-runtime.js"); + expect(payload.sources).not.toContain( + "extensions/diffs-language-pack/assets/viewer-runtime.js", + ); + expect(payload.sources).not.toContain("extensions/diffs/assets/viewer-runtime.js"); expect(payload.sources).toContain("extensions/discord/assets/embedded-app-sdk.mjs"); expect(payload.sources).toContain("extensions/crabbox/assets/openclaw-worker-wallpaper.png"); }); @@ -177,7 +177,15 @@ describe("runtime postbuild static assets", () => { expect(discoverStaticExtensionAssets({ rootDir })).toEqual([]); }); - it("excludes external plugin (bundledDist: false) static assets by default", async () => { + it.each([ + { name: "normal root build", params: {}, included: false }, + { name: "isolated external build", params: { includeExternalPlugins: true }, included: true }, + { + name: "Docker-selected build", + params: { env: { OPENCLAW_INTERNAL_DOCKER_BUILD_PLUGIN_IDS: "external-demo" } }, + included: true, + }, + ])("$name handles external plugin assets", async ({ params, included }) => { const rootDir = createTempDir("openclaw-runtime-postbuild-"); const packageDir = path.join(rootDir, "extensions", "external-demo"); await fs.mkdir(packageDir, { recursive: true }); @@ -200,39 +208,17 @@ describe("runtime postbuild static assets", () => { "utf8", ); - expect(discoverStaticExtensionAssets({ rootDir })).toEqual([]); - }); - - it("includes external plugin (bundledDist: false) static assets when includeExternalPlugins is true", async () => { - const rootDir = createTempDir("openclaw-runtime-postbuild-"); - const packageDir = path.join(rootDir, "extensions", "external-demo"); - await fs.mkdir(packageDir, { recursive: true }); - await fs.writeFile( - path.join(packageDir, "package.json"), - JSON.stringify({ - name: "@openclaw/external-demo", - openclaw: { - build: { - bundledDist: false, - staticAssets: [ - { - source: "./assets/runtime.js", - output: "assets/runtime.js", - }, - ], - }, - }, - }), - "utf8", + expect(discoverStaticExtensionAssets({ rootDir, ...params })).toEqual( + included + ? [ + { + pluginDir: "external-demo", + src: "extensions/external-demo/assets/runtime.js", + dest: "dist/extensions/external-demo/assets/runtime.js", + }, + ] + : [], ); - - expect(discoverStaticExtensionAssets({ rootDir, includeExternalPlugins: true })).toEqual([ - { - pluginDir: "external-demo", - src: "extensions/external-demo/assets/runtime.js", - dest: "dist/extensions/external-demo/assets/runtime.js", - }, - ]); }); it("copies declared static assets into root and package dist", async () => {