improve(plugins): compile externalized plugins in source builds (#124639)

* build(plugins): compile externalized plugins into local dist

* test(plugins): assert native external plugin loading

* chore(plugins): keep source runner asset scan unchanged

* refactor(plugins): isolate external local dist builds

* test(plugins): codify external artifact precedence

* test(plugins): preserve contract path boundary
This commit is contained in:
Peter Steinberger
2026-08-16 08:21:17 -07:00
committed by GitHub
parent 146350f417
commit eeffa53b20
13 changed files with 382 additions and 85 deletions
+5 -4
View File
@@ -230,10 +230,11 @@ OPENCLAW_DOCKER_BUILD_NODE_OPTIONS=--max-old-space-size=4096 OPENCLAW_DOCKER_BUI
`OPENCLAW_EXTENSIONS` selects plugin manifest ids from the source checkout;
existing source-directory names are also accepted when they differ. The Docker
build resolves the selection to source directories once, installs production
dependencies, and, when a selected plugin is published separately with
`openclaw.build.bundledDist: false`, compiles its runtime into the root bundled
dist. This Docker-only packaging does not change the plugin's npm or ClawHub
artifact contract. Unknown, invalid, or ambiguous ids fail the image build.
dependencies, and includes the selected plugin runtime in the image. Source
checkouts also compile first-party plugins published separately with
`openclaw.build.bundledDist: false`; that marker still preserves the plugin's
external npm or ClawHub ownership and does not change either artifact contract.
Unknown, invalid, or ambiguous ids fail the image build.
Known dependency/source-only ids keep their existing source and dependency
staging without gaining a compiled root dist entry. A selected plugin with
unified build entries must compile successfully; unselected external plugin
+7
View File
@@ -235,6 +235,7 @@ export const BUILD_ALL_STEPS: BuildAllStep[] = [
},
},
},
tsxStep("external-plugins:local-dist", "scripts/build-external-plugin-local-dist.mts"),
tsxStep("check-cli-bootstrap-imports", "scripts/check-cli-bootstrap-imports.mts"),
{
label: "plugins:assets:copy",
@@ -289,6 +290,7 @@ export const BUILD_ALL_PROFILES: Record<string, string[]> = {
"tsdown-ai",
"tsdown-packages",
"tsdown-unified",
"external-plugins:local-dist",
"check-cli-bootstrap-imports",
"plugins:assets:copy",
"runtime-postbuild",
@@ -304,6 +306,7 @@ export const BUILD_ALL_PROFILES: Record<string, string[]> = {
ciArtifacts: [
"plugins:assets:build",
"tsdown",
"external-plugins:local-dist",
"check-cli-bootstrap-imports",
"plugins:assets:copy",
"runtime-postbuild",
@@ -318,6 +321,7 @@ export const BUILD_ALL_PROFILES: Record<string, string[]> = {
],
gatewayWatch: [
"tsdown",
"external-plugins:local-dist",
"check-cli-bootstrap-imports",
"runtime-postbuild",
"build-stamp",
@@ -326,6 +330,7 @@ export const BUILD_ALL_PROFILES: Record<string, string[]> = {
qaRuntime: [
"plugins:assets:build",
"tsdown",
"external-plugins:local-dist",
"check-cli-bootstrap-imports",
"plugins:assets:copy",
"runtime-postbuild",
@@ -335,6 +340,7 @@ export const BUILD_ALL_PROFILES: Record<string, string[]> = {
sourcePerformance: [
"plugins:assets:build",
"tsdown",
"external-plugins:local-dist",
"check-cli-bootstrap-imports",
"plugins:assets:copy",
"runtime-postbuild",
@@ -345,6 +351,7 @@ export const BUILD_ALL_PROFILES: Record<string, string[]> = {
],
cliStartup: [
"tsdown",
"external-plugins:local-dist",
"check-cli-bootstrap-imports",
"runtime-postbuild",
"build-stamp",
@@ -0,0 +1,92 @@
#!/usr/bin/env node
// Builds source-checkout runtime output for externally published first-party plugins.
import fs from "node:fs";
import path from "node:path";
import { performance } from "node:perf_hooks";
import { pathToFileURL } from "node:url";
import { DOCKER_SELECTED_PLUGIN_BUILD_IDS_ENV } from "./lib/bundled-plugin-build-entries.mjs";
import { shouldBuildBundledCluster } from "./lib/optional-bundled-clusters.mjs";
import { assertRealOutputRoot } from "./lib/output-root-guard.mjs";
import {
buildPluginNpmRuntime,
listPublishablePluginPackageDirs,
type PluginPackageJson,
} from "./lib/plugin-npm-runtime-build.mts";
type ExternalPluginLocalDistParams = {
repoRoot?: string;
env?: NodeJS.ProcessEnv;
logLevel?: "silent" | "warn";
};
function readPluginPackageJson(repoRoot: string, packageDir: string): PluginPackageJson {
return JSON.parse(fs.readFileSync(path.join(repoRoot, packageDir, "package.json"), "utf8"));
}
/** Lists external first-party packages that need source-checkout dist output. */
export function listExternalPluginLocalDistPackageDirs(
params: Pick<ExternalPluginLocalDistParams, "repoRoot" | "env"> = {},
): string[] {
const repoRoot = path.resolve(params.repoRoot ?? ".");
const env = params.env ?? process.env;
if (env[DOCKER_SELECTED_PLUGIN_BUILD_IDS_ENV]?.trim()) {
return [];
}
return listPublishablePluginPackageDirs({ repoRoot }).filter((packageDir) => {
const packageJson = readPluginPackageJson(repoRoot, packageDir);
return (
packageJson.openclaw?.build?.bundledDist === false &&
shouldBuildBundledCluster(path.basename(packageDir), env, { packageJson })
);
});
}
/** Builds isolated plugin graphs, then stages every output below its excluded root dist path. */
export async function buildExternalPluginLocalDist(
params: ExternalPluginLocalDistParams = {},
): Promise<{ durationMs: number; pluginDirs: string[] }> {
const repoRoot = path.resolve(params.repoRoot ?? ".");
const packageDirs = listExternalPluginLocalDistPackageDirs({
repoRoot,
env: params.env,
});
const startedAt = performance.now();
const pluginDirs: string[] = [];
for (const packageDir of packageDirs) {
const result = await buildPluginNpmRuntime({
repoRoot,
packageDir,
// Standalone package validation owns its existing bundler warnings; this
// root build still surfaces errors and validates every emitted host import.
logLevel: params.logLevel ?? "error",
});
if (!result) {
throw new Error(`${packageDir} did not produce source-checkout runtime output`);
}
const targetDir = path.join(repoRoot, "dist", "extensions", result.pluginDir);
assertRealOutputRoot(targetDir);
fs.rmSync(targetDir, { recursive: true, force: true });
fs.mkdirSync(path.dirname(targetDir), { recursive: true });
fs.cpSync(result.outDir, targetDir, { recursive: true });
fs.rmSync(result.outDir, { recursive: true, force: true });
pluginDirs.push(result.pluginDir);
}
return {
durationMs: performance.now() - startedAt,
pluginDirs,
};
}
if (import.meta.url === pathToFileURL(process.argv[1] ?? "").href) {
try {
const result = await buildExternalPluginLocalDist();
console.log(
`[external-plugin-local-dist] built ${result.pluginDirs.length} plugins in ${Math.round(result.durationMs)}ms`,
);
} catch (error) {
console.error(error instanceof Error ? error.message : String(error));
process.exitCode = 1;
}
}
+9 -1
View File
@@ -290,7 +290,15 @@ export function copyBundledPluginMetadata(params: CopyMetadataParams = {}): void
: undefined;
const packageJson = isRecord(parsedPackageJson) ? parsedPackageJson : undefined;
const topLevelPublicSurfaceEntries = collectTopLevelPublicSurfaceEntries(pluginDir);
if (!shouldCopyBundledPluginMetadata(dirent.name, env, buildablePluginDirs)) {
const hasExternalLocalDist =
isRecord(packageJson?.openclaw) &&
isRecord(packageJson.openclaw.build) &&
packageJson.openclaw.build.bundledDist === false &&
fs.existsSync(distPluginDir);
if (
!hasExternalLocalDist &&
!shouldCopyBundledPluginMetadata(dirent.name, env, buildablePluginDirs)
) {
removePathIfExists(distPluginDir);
continue;
}
+1 -1
View File
@@ -24,7 +24,7 @@ export type PluginPackageJson = JsonRecord & {
dependencies?: JsonRecord;
openclaw?: {
assetScripts?: { build?: unknown };
build?: { openclawVersion?: unknown; runtimeFormat?: unknown };
build?: { bundledDist?: unknown; openclawVersion?: unknown; runtimeFormat?: unknown };
compat?: { pluginApi?: unknown };
release?: {
bundleRuntimeDependencies?: unknown;
+62 -2
View File
@@ -14,12 +14,18 @@ const repoRoot = resolveRepoRoot(import.meta.url);
const smokeEntryPath = path.join(repoRoot, "dist", "plugins", "build-smoke-entry.js");
assert.ok(fs.existsSync(smokeEntryPath), `missing build output: ${smokeEntryPath}`);
const { clearPluginCommands, getPluginCommandSpecs, loadOpenClawPlugins, matchPluginCommand } =
await import(pathToFileURL(smokeEntryPath).href);
const {
clearPluginCommands,
getPluginCommandSpecs,
getPluginModuleLoaderStats,
loadOpenClawPlugins,
matchPluginCommand,
} = await import(pathToFileURL(smokeEntryPath).href);
assert.equal(typeof loadOpenClawPlugins, "function", "built loader export missing");
assert.equal(typeof clearPluginCommands, "function", "clearPluginCommands missing");
assert.equal(typeof getPluginCommandSpecs, "function", "getPluginCommandSpecs missing");
assert.equal(typeof getPluginModuleLoaderStats, "function", "plugin loader stats missing");
assert.equal(typeof matchPluginCommand, "function", "matchPluginCommand missing");
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-build-smoke-"));
@@ -105,6 +111,12 @@ stageBundledPluginRuntime({ repoRoot });
const runtimeEntryPath = path.join(runtimePluginDir, "index.js");
assert.ok(fs.existsSync(runtimeEntryPath), "runtime overlay entry missing");
const smsRuntimeEntryPath = path.join(repoRoot, "dist-runtime", "extensions", "sms", "index.js");
assert.ok(fs.existsSync(smsRuntimeEntryPath), "compiled SMS runtime entry missing");
assert.ok(
fs.existsSync(path.join(repoRoot, "dist-runtime", "extensions", "mxc", "mxc-spawn-launcher.mjs")),
"compiled MXC runtime asset missing",
);
assert.equal(
fs.existsSync(path.join(repoRoot, "dist-runtime", "plugins", "commands.js")),
false,
@@ -113,6 +125,54 @@ assert.equal(
clearPluginCommands();
const smsStatsBefore = getPluginModuleLoaderStats();
const smsRegistry = loadOpenClawPlugins({
cache: false,
preferBuiltPluginArtifacts: true,
workspaceDir: tempRoot,
env: {
...process.env,
OPENCLAW_BUNDLED_PLUGINS_DIR: path.join(repoRoot, "dist-runtime", "extensions"),
},
config: {
plugins: {
enabled: true,
allow: ["sms"],
entries: {
sms: { enabled: true },
},
},
},
});
const smsRecord = smsRegistry.plugins.find((entry: { id: string }) => entry.id === "sms");
assert.ok(smsRecord, "SMS plugin missing from registry");
assert.equal(smsRecord.status, "loaded", smsRecord.error ?? "SMS plugin failed to load");
const smsStatsAfter = getPluginModuleLoaderStats();
assert.ok(
smsStatsAfter.nativeHits > smsStatsBefore.nativeHits,
"compiled SMS runtime did not use native loading",
);
for (const counter of [
"nativeMisses",
"sourceTransformForced",
"sourceTransformFallbacks",
] as const) {
assert.equal(
smsStatsAfter[counter],
smsStatsBefore[counter],
`compiled SMS runtime changed ${counter}`,
);
}
assert.equal(
smsStatsAfter.topSourceTransformTargets.some(({ target }: { target: string }) =>
target.replaceAll("\\", "/").includes("/extensions/sms/"),
),
false,
"compiled SMS runtime reached the source transformer",
);
clearPluginCommands();
const registry = loadOpenClawPlugins({
cache: false,
workspaceDir: tempRoot,
+1
View File
@@ -2,3 +2,4 @@
export { clearPluginCommands, executePluginCommand, matchPluginCommand } from "./commands.js";
export { getPluginCommandSpecs } from "./command-specs.js";
export { loadOpenClawPlugins, loadPluginRegistryHandle } from "./loader.js";
export { getPluginModuleLoaderStats } from "./plugin-module-loader-cache.js";
@@ -445,6 +445,28 @@ describe("copyBundledPluginMetadata", () => {
expect(fs.existsSync(staleDistDir)).toBe(false);
});
it("preserves isolated source-checkout output for an external plugin", () => {
const repoRoot = makeRepoRoot("openclaw-external-plugin-local-dist-meta-");
createPlugin(repoRoot, {
id: "sms",
packageName: "@openclaw/sms",
packageOpenClaw: {
extensions: ["./index.ts"],
build: { bundledDist: false },
release: { publishToNpm: true },
},
});
const distPluginDir = path.join(repoRoot, "dist", "extensions", "sms");
fs.mkdirSync(distPluginDir, { recursive: true });
fs.writeFileSync(path.join(distPluginDir, "index.js"), "export default {};\n", "utf8");
copyBundledPluginMetadata({ repoRoot });
expect(fs.existsSync(path.join(distPluginDir, "index.js"))).toBe(true);
expect(fs.existsSync(path.join(distPluginDir, "openclaw.plugin.json"))).toBe(true);
expect(readBundledPackageJson(repoRoot, "sms").openclaw?.extensions).toEqual(["./index.js"]);
});
it("preserves manifest-less runtime support package outputs and copies package metadata", () => {
const repoRoot = makeRepoRoot("openclaw-bundled-runtime-support-");
const pluginDir = path.join(repoRoot, "extensions", "image-generation-core");
@@ -200,6 +200,75 @@ function loadBuiltArtifactScenario(scenario: BuiltArtifactScenario) {
return registry.plugins.find((entry) => entry.id === scenario.id)?.status;
}
function loadSourceExternalArtifactScenario(params: {
sourceBody: string;
packageLocalBody: string;
rootBuildBody?: string;
runtimeOverlayBody?: string;
}) {
const id = "source-external-artifact-test";
const repoRoot = makePluginLoaderTempDir();
const sourceDir = path.join(repoRoot, "extensions", id);
const rootBuildDir = path.join(repoRoot, "dist", "extensions", id);
mkdirSafe(path.join(repoRoot, ".git"));
mkdirSafe(path.join(repoRoot, "src"));
writeFixtureText(repoRoot, "pnpm-workspace.yaml", "packages: []\n");
writeFixtureJson(sourceDir, "openclaw.plugin.json", pluginManifest(id));
writeFixtureJson(sourceDir, "package.json", {
openclaw: {
extensions: ["./index.ts"],
build: { bundledDist: false },
},
});
writeFixtureText(sourceDir, "index.ts", params.sourceBody);
writeFixtureText(sourceDir, "dist/index.js", params.packageLocalBody);
if (params.rootBuildBody) {
mkdirSafe(rootBuildDir);
fs.copyFileSync(
path.join(sourceDir, "openclaw.plugin.json"),
path.join(rootBuildDir, "openclaw.plugin.json"),
);
writeFixtureJson(rootBuildDir, "package.json", {
openclaw: { extensions: ["./index.js"] },
});
writeFixtureText(rootBuildDir, "index.js", params.rootBuildBody);
}
if (params.runtimeOverlayBody) {
writeFixtureText(
repoRoot,
path.join("dist-runtime", "extensions", id, "index.js"),
params.runtimeOverlayBody,
);
}
const config = {
plugins: {
allow: [id],
entries: { [id]: { enabled: true } },
},
};
const registry = withEnv(
{
OPENCLAW_BUNDLED_PLUGINS_DIR: params.rootBuildBody
? path.join(repoRoot, "dist", "extensions")
: path.join(repoRoot, "extensions"),
OPENCLAW_TEST_TRUST_BUNDLED_PLUGINS_DIR: "1",
OPENCLAW_DISABLE_BUNDLED_PLUGINS: undefined,
},
() => {
const manifestRegistry = loadPluginManifestRegistryCore({ config });
return loadOpenClawPlugins({
cache: false,
preferBuiltPluginArtifacts: true,
onlyPluginIds: [id],
config,
manifestRegistry,
});
},
);
return registry.plugins.find((entry) => entry.id === id)?.status;
}
describe("loadOpenClawPlugins", () => {
it("setup-loads a trusted global channel plugin when the caller scopes to it", () => {
useNoBundledPlugins();
@@ -863,80 +932,25 @@ ${channelPluginSource({
).toBe("loaded");
});
it("ignores built artifacts when the bundled source plugin opts out of core dist", () => {
const repoRoot = makePluginLoaderTempDir();
const sourceDir = path.join(repoRoot, "extensions", "source-only-artifact-test");
const builtPluginDir = path.join(repoRoot, "dist", "extensions", "source-only-artifact-test");
mkdirSafe(path.join(repoRoot, ".git"));
mkdirSafe(path.join(repoRoot, "src"));
writeFixtureText(repoRoot, "pnpm-workspace.yaml", "packages: []\n");
writeFixtureJson(
sourceDir,
"openclaw.plugin.json",
pluginManifest("source-only-artifact-test"),
);
writeFixtureJson(sourceDir, "package.json", {
openclaw: {
extensions: ["./index.ts"],
build: { bundledDist: false },
},
});
writeFixtureText(
sourceDir,
"index.ts",
'export default { id: "source-only-artifact-test", register() {} };\n',
);
writeFixtureText(
sourceDir,
"dist/index.js",
'throw new Error("stale package-local dist should not load");\n',
);
mkdirSafe(builtPluginDir);
fs.copyFileSync(
path.join(sourceDir, "openclaw.plugin.json"),
path.join(builtPluginDir, "openclaw.plugin.json"),
);
writeFixtureJson(builtPluginDir, "package.json", {
openclaw: { extensions: ["./index.js"] },
});
writeFixtureText(
builtPluginDir,
"index.js",
'throw new Error("stale discovered core dist should not load");\n',
);
writeFixtureText(
repoRoot,
"dist-runtime/extensions/source-only-artifact-test/index.js",
'throw new Error("stale core dist should not load");\n',
);
it("ignores package-local dist when a bundled source plugin opts out of core dist", () => {
expect(
loadSourceExternalArtifactScenario({
sourceBody: 'export default { id: "source-external-artifact-test", register() {} };\n',
packageLocalBody: 'throw new Error("stale package-local dist should not load");\n',
}),
).toBe("loaded");
});
const config = {
plugins: {
allow: ["source-only-artifact-test"],
entries: { "source-only-artifact-test": { enabled: true } },
},
};
const registry = withEnv(
{
OPENCLAW_BUNDLED_PLUGINS_DIR: path.join(repoRoot, "dist", "extensions"),
OPENCLAW_TEST_TRUST_BUNDLED_PLUGINS_DIR: "1",
OPENCLAW_DISABLE_BUNDLED_PLUGINS: undefined,
},
() => {
const manifestRegistry = loadPluginManifestRegistryCore({ config });
return loadOpenClawPlugins({
cache: false,
preferBuiltPluginArtifacts: true,
onlyPluginIds: ["source-only-artifact-test"],
config,
manifestRegistry,
});
},
);
expect(registry.plugins.find((entry) => entry.id === "source-only-artifact-test")?.status).toBe(
"loaded",
);
it("prefers the root build when a bundled source plugin opts out of core dist", () => {
expect(
loadSourceExternalArtifactScenario({
sourceBody: 'throw new Error("source should not load when root build exists");\n',
packageLocalBody: 'throw new Error("stale package-local dist should not load");\n',
rootBuildBody: 'module.exports = { id: "source-external-artifact-test", register() {} };\n',
runtimeOverlayBody:
'throw new Error("staged runtime should canonicalize to the root build");\n',
}),
).toBe("loaded");
});
it("prefers package-local dist artifacts over workspace source TS when requested", () => {
@@ -87,6 +87,26 @@ describe("resolvePluginRuntimeArtifact", () => {
},
);
it("prefers the root build for source-external plugins without using package-local output", () => {
const fixture = createBundledPluginFixture();
const packageLocalSource = path.join(fixture.rootDir, "dist", "index.js");
fs.mkdirSync(path.dirname(packageLocalSource), { recursive: true });
fs.writeFileSync(packageLocalSource, 'module.exports = { id: "stale" };\n');
const resolved = resolvePluginRuntimeArtifact({
pluginId: "fixture",
entryKind: "runtime",
rootDir: fixture.rootDir,
source: fixture.source,
origin: "bundled",
preferBuiltPluginArtifacts: true,
packageManifest: { build: { bundledDist: false } },
});
expect(resolved.source).toBe(fixture.builtSource);
expect(resolved.source).not.toBe(fs.realpathSync(packageLocalSource));
});
it("aliases different physical inputs for the same logical runtime entry", () => {
const fixture = createBundledPluginFixture();
const first = resolveFixture({
@@ -117,10 +117,13 @@ function resolvePreferredBuiltRuntimeArtifact(params: {
}
return { source, rootDir };
}
if (params.packageManifest?.build?.bundledDist === false) {
return { source, rootDir };
}
const packageLocalArtifactSource = resolvePackageLocalDistRuntimeArtifact({ source, rootDir });
// Source-external plugins can leave package-local npm build output behind.
// Keep source authoritative over that output, but allow the lifecycle-owned root
// build to provide the fresh JavaScript artifact used by source checkouts.
const packageLocalArtifactSource =
params.packageManifest?.build?.bundledDist === false
? null
: resolvePackageLocalDistRuntimeArtifact({ source, rootDir });
if (packageLocalArtifactSource) {
return { source: packageLocalArtifactSource, rootDir };
}
@@ -137,6 +140,8 @@ function resolvePreferredBuiltRuntimeArtifact(params: {
return { source, rootDir };
}
const artifactRelativePath = rewriteBundledRuntimeArtifactRelativePath(relativeSource);
// The runtime overlay is a staging fallback. Final canonicalization maps it
// to the matching root build when both exist, so one build owns execution.
for (const artifactRootName of ["dist-runtime", "dist"] as const) {
const artifactRoot = path.join(
packageRoot,
+19
View File
@@ -362,6 +362,7 @@ describe("resolveBuildAllSteps", () => {
"tsdown-ai",
"tsdown-packages",
"tsdown-unified",
"external-plugins:local-dist",
"check-cli-bootstrap-imports",
"plugins:assets:copy",
"runtime-postbuild",
@@ -436,6 +437,7 @@ describe("resolveBuildAllSteps", () => {
expect(resolveBuildAllSteps("ciArtifacts").map((step) => step.label)).toEqual([
"plugins:assets:build",
"tsdown",
"external-plugins:local-dist",
"check-cli-bootstrap-imports",
"plugins:assets:copy",
"runtime-postbuild",
@@ -568,6 +570,7 @@ describe("resolveBuildAllSteps", () => {
it("uses a minimal built runtime profile for gateway watch regression", () => {
expect(resolveBuildAllSteps("gatewayWatch").map((step) => step.label)).toEqual([
"tsdown",
"external-plugins:local-dist",
"check-cli-bootstrap-imports",
"runtime-postbuild",
"build-stamp",
@@ -579,6 +582,7 @@ describe("resolveBuildAllSteps", () => {
expect(resolveBuildAllSteps("qaRuntime").map((step) => step.label)).toEqual([
"plugins:assets:build",
"tsdown",
"external-plugins:local-dist",
"check-cli-bootstrap-imports",
"plugins:assets:copy",
"runtime-postbuild",
@@ -591,6 +595,7 @@ describe("resolveBuildAllSteps", () => {
expect(resolveBuildAllSteps("sourcePerformance").map((step) => step.label)).toEqual([
"plugins:assets:build",
"tsdown",
"external-plugins:local-dist",
"check-cli-bootstrap-imports",
"plugins:assets:copy",
"runtime-postbuild",
@@ -604,6 +609,7 @@ describe("resolveBuildAllSteps", () => {
it("uses a CLI startup profile without generated plugin assets", () => {
expect(resolveBuildAllSteps("cliStartup").map((step) => step.label)).toEqual([
"tsdown",
"external-plugins:local-dist",
"check-cli-bootstrap-imports",
"runtime-postbuild",
"build-stamp",
@@ -676,6 +682,19 @@ describe("resolveBuildAllSteps", () => {
}
});
it("builds isolated external plugin output after tsdown and before runtime postbuild", () => {
for (const profile of Object.keys(BUILD_ALL_PROFILES)) {
const labels = resolveBuildAllSteps(profile).map((step) => step.label);
const lastTsdown = profile === "full" ? "tsdown-unified" : "tsdown";
expect(labels.indexOf("external-plugins:local-dist")).toBeGreaterThan(
labels.indexOf(lastTsdown),
);
expect(labels.indexOf("external-plugins:local-dist")).toBeLessThan(
labels.indexOf("runtime-postbuild"),
);
}
});
it("writes the runtime postbuild stamp after the build stamp", () => {
const labels = resolveBuildAllSteps("full").map((step) => step.label);
expect(labels).toContain("runtime-postbuild");
@@ -0,0 +1,48 @@
import { describe, expect, it } from "vitest";
import {
buildExternalPluginLocalDist,
listExternalPluginLocalDistPackageDirs,
} from "../../scripts/build-external-plugin-local-dist.mts";
import {
collectRootPackageExcludedExtensionDirs,
DOCKER_SELECTED_PLUGIN_BUILD_IDS_ENV,
} from "../../scripts/lib/bundled-plugin-build-entries.mjs";
describe("external plugin local dist build", () => {
it("selects every externalized first-party plugin behind a package exclusion", () => {
const packageDirs = listExternalPluginLocalDistPackageDirs();
const excludedPluginIds = collectRootPackageExcludedExtensionDirs();
expect(packageDirs).toHaveLength(61);
expect(packageDirs).toEqual(
expect.arrayContaining(["extensions/slack", "extensions/sms", "extensions/mxc"]),
);
expect(packageDirs).not.toContain("extensions/whatsapp");
expect(
packageDirs.every((packageDir) => excludedPluginIds.has(packageDir.split("/").at(-1) ?? "")),
).toBe(true);
});
it("leaves Docker-selected external plugin compilation on the unified build path", () => {
expect(
listExternalPluginLocalDistPackageDirs({
env: {
...process.env,
[DOCKER_SELECTED_PLUGIN_BUILD_IDS_ENV]: "slack",
},
}),
).toEqual([]);
});
it("performs no writes when Docker owns the selected build", async () => {
await expect(
buildExternalPluginLocalDist({
env: {
...process.env,
[DOCKER_SELECTED_PLUGIN_BUILD_IDS_ENV]: "slack",
},
logLevel: "silent",
}),
).resolves.toMatchObject({ pluginDirs: [] });
});
});