mirror of
https://github.com/openclaw/openclaw.git
synced 2026-03-17 21:10:54 +00:00
Gateway: sync runtime post-build artifacts
This commit is contained in:
@@ -66,6 +66,7 @@ Docs: https://docs.openclaw.ai
|
||||
- CLI/completion: reduce recursive completion-script string churn and fix nested PowerShell command-path matching so generated nested completions resolve on PowerShell too. (#45537) Thanks @yiShanXin and @vincentkoc.
|
||||
- Gateway/startup: load bundled channel plugins from compiled `dist/extensions` entries in built installs, so gateway boot no longer recompiles bundled extension TypeScript on every startup and WhatsApp-class cold starts drop back to seconds instead of tens of seconds or worse.
|
||||
- Gateway/watch mode: restart on bundled-plugin package and manifest metadata changes, rebuild `dist` for extension source and `tsdown.config.ts` changes, and still ignore extension docs. (#47571) thanks @gumadeiras.
|
||||
- Gateway/watch mode: recreate bundled plugin runtime metadata after clean or stale `dist` states, so `pnpm gateway:watch` no longer fails on missing `dist/extensions/*/openclaw.plugin.json` manifests after a rebuild. Thanks @gumadeiras.
|
||||
|
||||
## 2026.3.13
|
||||
|
||||
|
||||
@@ -225,10 +225,10 @@
|
||||
"android:run": "cd apps/android && ./gradlew :app:installDebug && adb shell am start -n ai.openclaw.app/.MainActivity",
|
||||
"android:test": "cd apps/android && ./gradlew :app:testDebugUnitTest",
|
||||
"android:test:integration": "OPENCLAW_LIVE_TEST=1 OPENCLAW_LIVE_ANDROID_NODE=1 vitest run --config vitest.live.config.ts src/gateway/android-node.capabilities.live.test.ts",
|
||||
"build": "pnpm canvas:a2ui:bundle && node scripts/tsdown-build.mjs && node scripts/copy-plugin-sdk-root-alias.mjs && node scripts/copy-bundled-plugin-metadata.mjs && pnpm build:plugin-sdk:dts && node --import tsx scripts/write-plugin-sdk-entry-dts.ts && node --import tsx scripts/canvas-a2ui-copy.ts && node --import tsx scripts/copy-hook-metadata.ts && node --import tsx scripts/copy-export-html-templates.ts && node --import tsx scripts/write-build-info.ts && node --import tsx scripts/write-cli-startup-metadata.ts && node --import tsx scripts/write-cli-compat.ts",
|
||||
"build:docker": "node scripts/tsdown-build.mjs && node scripts/copy-plugin-sdk-root-alias.mjs && node scripts/copy-bundled-plugin-metadata.mjs && pnpm build:plugin-sdk:dts && node --import tsx scripts/write-plugin-sdk-entry-dts.ts && node --import tsx scripts/canvas-a2ui-copy.ts && node --import tsx scripts/copy-hook-metadata.ts && node --import tsx scripts/copy-export-html-templates.ts && node --import tsx scripts/write-build-info.ts && node --import tsx scripts/write-cli-startup-metadata.ts && node --import tsx scripts/write-cli-compat.ts",
|
||||
"build": "pnpm canvas:a2ui:bundle && node scripts/tsdown-build.mjs && node scripts/runtime-postbuild.mjs && pnpm build:plugin-sdk:dts && node --import tsx scripts/write-plugin-sdk-entry-dts.ts && node --import tsx scripts/canvas-a2ui-copy.ts && node --import tsx scripts/copy-hook-metadata.ts && node --import tsx scripts/copy-export-html-templates.ts && node --import tsx scripts/write-build-info.ts && node --import tsx scripts/write-cli-startup-metadata.ts && node --import tsx scripts/write-cli-compat.ts",
|
||||
"build:docker": "node scripts/tsdown-build.mjs && node scripts/runtime-postbuild.mjs && pnpm build:plugin-sdk:dts && node --import tsx scripts/write-plugin-sdk-entry-dts.ts && node --import tsx scripts/canvas-a2ui-copy.ts && node --import tsx scripts/copy-hook-metadata.ts && node --import tsx scripts/copy-export-html-templates.ts && node --import tsx scripts/write-build-info.ts && node --import tsx scripts/write-cli-startup-metadata.ts && node --import tsx scripts/write-cli-compat.ts",
|
||||
"build:plugin-sdk:dts": "tsc -p tsconfig.plugin-sdk.dts.json || true",
|
||||
"build:strict-smoke": "pnpm canvas:a2ui:bundle && node scripts/tsdown-build.mjs && node scripts/copy-plugin-sdk-root-alias.mjs && pnpm build:plugin-sdk:dts",
|
||||
"build:strict-smoke": "pnpm canvas:a2ui:bundle && node scripts/tsdown-build.mjs && node scripts/runtime-postbuild.mjs && pnpm build:plugin-sdk:dts",
|
||||
"canvas:a2ui:bundle": "bash scripts/bundle-a2ui.sh",
|
||||
"check": "pnpm check:host-env-policy:swift && pnpm format:check && pnpm tsgo && pnpm lint && pnpm lint:tmp:no-random-messaging && pnpm lint:tmp:channel-agnostic-boundaries && pnpm lint:tmp:no-raw-channel-fetch && pnpm lint:agent:ingress-owner && pnpm lint:plugins:no-register-http-handler && pnpm lint:plugins:no-monolithic-plugin-sdk-entry-imports && pnpm lint:webhook:no-low-level-body-read && pnpm lint:auth:no-pairing-store-group && pnpm lint:auth:pairing-account-scope",
|
||||
"check:docs": "pnpm format:docs:check && pnpm lint:docs && pnpm docs:check-i18n-glossary && pnpm docs:check-links",
|
||||
|
||||
@@ -1,11 +1,7 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
|
||||
const repoRoot = process.cwd();
|
||||
const extensionsRoot = path.join(repoRoot, "extensions");
|
||||
const distExtensionsRoot = path.join(repoRoot, "dist", "extensions");
|
||||
import { pathToFileURL } from "node:url";
|
||||
import { removeFileIfExists, writeTextFileIfChanged } from "./runtime-postbuild-shared.mjs";
|
||||
|
||||
function rewritePackageExtensions(entries) {
|
||||
if (!Array.isArray(entries)) {
|
||||
@@ -21,37 +17,66 @@ function rewritePackageExtensions(entries) {
|
||||
});
|
||||
}
|
||||
|
||||
for (const dirent of fs.readdirSync(extensionsRoot, { withFileTypes: true })) {
|
||||
if (!dirent.isDirectory()) {
|
||||
continue;
|
||||
export function copyBundledPluginMetadata(params = {}) {
|
||||
const repoRoot = params.cwd ?? process.cwd();
|
||||
const extensionsRoot = path.join(repoRoot, "extensions");
|
||||
const distExtensionsRoot = path.join(repoRoot, "dist", "extensions");
|
||||
if (!fs.existsSync(extensionsRoot)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const pluginDir = path.join(extensionsRoot, dirent.name);
|
||||
const manifestPath = path.join(pluginDir, "openclaw.plugin.json");
|
||||
if (!fs.existsSync(manifestPath)) {
|
||||
continue;
|
||||
const sourcePluginDirs = new Set();
|
||||
|
||||
for (const dirent of fs.readdirSync(extensionsRoot, { withFileTypes: true })) {
|
||||
if (!dirent.isDirectory()) {
|
||||
continue;
|
||||
}
|
||||
sourcePluginDirs.add(dirent.name);
|
||||
|
||||
const pluginDir = path.join(extensionsRoot, dirent.name);
|
||||
const manifestPath = path.join(pluginDir, "openclaw.plugin.json");
|
||||
const distPluginDir = path.join(distExtensionsRoot, dirent.name);
|
||||
const distManifestPath = path.join(distPluginDir, "openclaw.plugin.json");
|
||||
const distPackageJsonPath = path.join(distPluginDir, "package.json");
|
||||
if (!fs.existsSync(manifestPath)) {
|
||||
removeFileIfExists(distManifestPath);
|
||||
removeFileIfExists(distPackageJsonPath);
|
||||
continue;
|
||||
}
|
||||
|
||||
writeTextFileIfChanged(distManifestPath, fs.readFileSync(manifestPath, "utf8"));
|
||||
|
||||
const packageJsonPath = path.join(pluginDir, "package.json");
|
||||
if (!fs.existsSync(packageJsonPath)) {
|
||||
removeFileIfExists(distPackageJsonPath);
|
||||
continue;
|
||||
}
|
||||
|
||||
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf8"));
|
||||
if (packageJson.openclaw && "extensions" in packageJson.openclaw) {
|
||||
packageJson.openclaw = {
|
||||
...packageJson.openclaw,
|
||||
extensions: rewritePackageExtensions(packageJson.openclaw.extensions),
|
||||
};
|
||||
}
|
||||
|
||||
writeTextFileIfChanged(distPackageJsonPath, `${JSON.stringify(packageJson, null, 2)}\n`);
|
||||
}
|
||||
|
||||
const distPluginDir = path.join(distExtensionsRoot, dirent.name);
|
||||
fs.mkdirSync(distPluginDir, { recursive: true });
|
||||
fs.copyFileSync(manifestPath, path.join(distPluginDir, "openclaw.plugin.json"));
|
||||
|
||||
const packageJsonPath = path.join(pluginDir, "package.json");
|
||||
if (!fs.existsSync(packageJsonPath)) {
|
||||
continue;
|
||||
if (!fs.existsSync(distExtensionsRoot)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf8"));
|
||||
if (packageJson.openclaw && "extensions" in packageJson.openclaw) {
|
||||
packageJson.openclaw = {
|
||||
...packageJson.openclaw,
|
||||
extensions: rewritePackageExtensions(packageJson.openclaw.extensions),
|
||||
};
|
||||
for (const dirent of fs.readdirSync(distExtensionsRoot, { withFileTypes: true })) {
|
||||
if (!dirent.isDirectory() || sourcePluginDirs.has(dirent.name)) {
|
||||
continue;
|
||||
}
|
||||
const distPluginDir = path.join(distExtensionsRoot, dirent.name);
|
||||
removeFileIfExists(path.join(distPluginDir, "openclaw.plugin.json"));
|
||||
removeFileIfExists(path.join(distPluginDir, "package.json"));
|
||||
}
|
||||
|
||||
fs.writeFileSync(
|
||||
path.join(distPluginDir, "package.json"),
|
||||
`${JSON.stringify(packageJson, null, 2)}\n`,
|
||||
"utf8",
|
||||
);
|
||||
}
|
||||
|
||||
if (import.meta.url === pathToFileURL(process.argv[1] ?? "").href) {
|
||||
copyBundledPluginMetadata();
|
||||
}
|
||||
|
||||
@@ -1,10 +1,16 @@
|
||||
#!/usr/bin/env node
|
||||
import { readFileSync } from "node:fs";
|
||||
import { resolve } from "node:path";
|
||||
import { pathToFileURL } from "node:url";
|
||||
import { writeTextFileIfChanged } from "./runtime-postbuild-shared.mjs";
|
||||
|
||||
import { copyFileSync, mkdirSync } from "node:fs";
|
||||
import { dirname, resolve } from "node:path";
|
||||
export function copyPluginSdkRootAlias(params = {}) {
|
||||
const cwd = params.cwd ?? process.cwd();
|
||||
const source = resolve(cwd, "src/plugin-sdk/root-alias.cjs");
|
||||
const target = resolve(cwd, "dist/plugin-sdk/root-alias.cjs");
|
||||
|
||||
const source = resolve("src/plugin-sdk/root-alias.cjs");
|
||||
const target = resolve("dist/plugin-sdk/root-alias.cjs");
|
||||
writeTextFileIfChanged(target, readFileSync(source, "utf8"));
|
||||
}
|
||||
|
||||
mkdirSync(dirname(target), { recursive: true });
|
||||
copyFileSync(source, target);
|
||||
if (import.meta.url === pathToFileURL(process.argv[1] ?? "").href) {
|
||||
copyPluginSdkRootAlias();
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import process from "node:process";
|
||||
import { pathToFileURL } from "node:url";
|
||||
import { runRuntimePostBuild } from "./runtime-postbuild.mjs";
|
||||
|
||||
const compiler = "tsdown";
|
||||
const compilerArgs = ["exec", compiler, "--no-clean"];
|
||||
@@ -275,6 +276,19 @@ const runOpenClaw = async (deps) => {
|
||||
return res.exitCode ?? 1;
|
||||
};
|
||||
|
||||
const syncRuntimeArtifacts = (deps) => {
|
||||
try {
|
||||
runRuntimePostBuild({ cwd: deps.cwd });
|
||||
} catch (error) {
|
||||
logRunner(
|
||||
`Failed to write runtime build artifacts: ${error?.message ?? "unknown error"}`,
|
||||
deps,
|
||||
);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
const writeBuildStamp = (deps) => {
|
||||
try {
|
||||
deps.fs.mkdirSync(deps.distRoot, { recursive: true });
|
||||
@@ -312,6 +326,9 @@ export async function runNodeMain(params = {}) {
|
||||
deps.configFiles = runNodeConfigFiles.map((filePath) => path.join(deps.cwd, filePath));
|
||||
|
||||
if (!shouldBuild(deps)) {
|
||||
if (!syncRuntimeArtifacts(deps)) {
|
||||
return 1;
|
||||
}
|
||||
return await runOpenClaw(deps);
|
||||
}
|
||||
|
||||
@@ -334,6 +351,9 @@ export async function runNodeMain(params = {}) {
|
||||
if (buildRes.exitCode !== 0 && buildRes.exitCode !== null) {
|
||||
return buildRes.exitCode;
|
||||
}
|
||||
if (!syncRuntimeArtifacts(deps)) {
|
||||
return 1;
|
||||
}
|
||||
writeBuildStamp(deps);
|
||||
return await runOpenClaw(deps);
|
||||
}
|
||||
|
||||
26
scripts/runtime-postbuild-shared.mjs
Normal file
26
scripts/runtime-postbuild-shared.mjs
Normal file
@@ -0,0 +1,26 @@
|
||||
import fs from "node:fs";
|
||||
import { dirname } from "node:path";
|
||||
|
||||
export function writeTextFileIfChanged(filePath, contents) {
|
||||
const next = String(contents);
|
||||
try {
|
||||
const current = fs.readFileSync(filePath, "utf8");
|
||||
if (current === next) {
|
||||
return false;
|
||||
}
|
||||
} catch {
|
||||
// Write the file when it does not exist or cannot be read.
|
||||
}
|
||||
fs.mkdirSync(dirname(filePath), { recursive: true });
|
||||
fs.writeFileSync(filePath, next, "utf8");
|
||||
return true;
|
||||
}
|
||||
|
||||
export function removeFileIfExists(filePath) {
|
||||
try {
|
||||
fs.rmSync(filePath, { force: true });
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
12
scripts/runtime-postbuild.mjs
Normal file
12
scripts/runtime-postbuild.mjs
Normal file
@@ -0,0 +1,12 @@
|
||||
import { pathToFileURL } from "node:url";
|
||||
import { copyBundledPluginMetadata } from "./copy-bundled-plugin-metadata.mjs";
|
||||
import { copyPluginSdkRootAlias } from "./copy-plugin-sdk-root-alias.mjs";
|
||||
|
||||
export function runRuntimePostBuild(params = {}) {
|
||||
copyPluginSdkRootAlias(params);
|
||||
copyBundledPluginMetadata(params);
|
||||
}
|
||||
|
||||
if (import.meta.url === pathToFileURL(process.argv[1] ?? "").href) {
|
||||
runRuntimePostBuild();
|
||||
}
|
||||
@@ -24,6 +24,15 @@ function createExitedProcess(code: number | null, signal: string | null = null)
|
||||
};
|
||||
}
|
||||
|
||||
async function writeRuntimePostBuildScaffold(tmp: string): Promise<void> {
|
||||
const pluginSdkAliasPath = path.join(tmp, "src", "plugin-sdk", "root-alias.cjs");
|
||||
await fs.mkdir(path.dirname(pluginSdkAliasPath), { recursive: true });
|
||||
await fs.mkdir(path.join(tmp, "extensions"), { recursive: true });
|
||||
await fs.writeFile(pluginSdkAliasPath, "module.exports = {};\n", "utf-8");
|
||||
const baselineTime = new Date("2026-03-13T09:00:00.000Z");
|
||||
await fs.utimes(pluginSdkAliasPath, baselineTime, baselineTime);
|
||||
}
|
||||
|
||||
function expectedBuildSpawn(platform: NodeJS.Platform = process.platform) {
|
||||
return platform === "win32"
|
||||
? ["cmd.exe", "/d", "/s", "/c", "pnpm", "exec", "tsdown", "--no-clean"]
|
||||
@@ -38,6 +47,7 @@ describe("run-node script", () => {
|
||||
const argsPath = path.join(tmp, ".pnpm-args.txt");
|
||||
const indexPath = path.join(tmp, "dist", "control-ui", "index.html");
|
||||
|
||||
await writeRuntimePostBuildScaffold(tmp);
|
||||
await fs.mkdir(path.dirname(indexPath), { recursive: true });
|
||||
await fs.writeFile(indexPath, "<html>sentinel</html>\n", "utf-8");
|
||||
|
||||
@@ -84,6 +94,73 @@ describe("run-node script", () => {
|
||||
},
|
||||
);
|
||||
|
||||
it("copies bundled plugin metadata after rebuilding from a clean dist", async () => {
|
||||
await withTempDir(async (tmp) => {
|
||||
const extensionManifestPath = path.join(tmp, "extensions", "demo", "openclaw.plugin.json");
|
||||
const extensionPackagePath = path.join(tmp, "extensions", "demo", "package.json");
|
||||
|
||||
await writeRuntimePostBuildScaffold(tmp);
|
||||
await fs.mkdir(path.dirname(extensionManifestPath), { recursive: true });
|
||||
await fs.writeFile(
|
||||
extensionManifestPath,
|
||||
'{"id":"demo","configSchema":{"type":"object"}}\n',
|
||||
"utf-8",
|
||||
);
|
||||
await fs.writeFile(
|
||||
extensionPackagePath,
|
||||
JSON.stringify(
|
||||
{
|
||||
name: "demo",
|
||||
openclaw: {
|
||||
extensions: ["./src/index.ts", "./nested/entry.mts"],
|
||||
},
|
||||
},
|
||||
null,
|
||||
2,
|
||||
) + "\n",
|
||||
"utf-8",
|
||||
);
|
||||
|
||||
const spawnCalls: string[][] = [];
|
||||
const spawn = (cmd: string, args: string[]) => {
|
||||
spawnCalls.push([cmd, ...args]);
|
||||
return createExitedProcess(0);
|
||||
};
|
||||
|
||||
const { runNodeMain } = await import("../../scripts/run-node.mjs");
|
||||
const exitCode = await runNodeMain({
|
||||
cwd: tmp,
|
||||
args: ["status"],
|
||||
env: {
|
||||
...process.env,
|
||||
OPENCLAW_FORCE_BUILD: "1",
|
||||
OPENCLAW_RUNNER_LOG: "0",
|
||||
},
|
||||
spawn,
|
||||
execPath: process.execPath,
|
||||
platform: process.platform,
|
||||
});
|
||||
|
||||
expect(exitCode).toBe(0);
|
||||
expect(spawnCalls).toEqual([
|
||||
expectedBuildSpawn(),
|
||||
[process.execPath, "openclaw.mjs", "status"],
|
||||
]);
|
||||
|
||||
await expect(
|
||||
fs.readFile(path.join(tmp, "dist", "plugin-sdk", "root-alias.cjs"), "utf-8"),
|
||||
).resolves.toContain("module.exports = {};");
|
||||
await expect(
|
||||
fs.readFile(path.join(tmp, "dist", "extensions", "demo", "openclaw.plugin.json"), "utf-8"),
|
||||
).resolves.toContain('"id":"demo"');
|
||||
await expect(
|
||||
fs.readFile(path.join(tmp, "dist", "extensions", "demo", "package.json"), "utf-8"),
|
||||
).resolves.toContain(
|
||||
'"extensions": [\n "./src/index.js",\n "./nested/entry.js"\n ]',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it("skips rebuilding when dist is current and the source tree is clean", async () => {
|
||||
await withTempDir(async (tmp) => {
|
||||
const srcPath = path.join(tmp, "src", "index.ts");
|
||||
@@ -91,6 +168,7 @@ describe("run-node script", () => {
|
||||
const buildStampPath = path.join(tmp, "dist", ".buildstamp");
|
||||
const tsconfigPath = path.join(tmp, "tsconfig.json");
|
||||
const packageJsonPath = path.join(tmp, "package.json");
|
||||
await writeRuntimePostBuildScaffold(tmp);
|
||||
await fs.mkdir(path.dirname(srcPath), { recursive: true });
|
||||
await fs.mkdir(path.dirname(distEntryPath), { recursive: true });
|
||||
await fs.writeFile(srcPath, "export const value = 1;\n", "utf-8");
|
||||
@@ -175,6 +253,7 @@ describe("run-node script", () => {
|
||||
const buildStampPath = path.join(tmp, "dist", ".buildstamp");
|
||||
const tsconfigPath = path.join(tmp, "tsconfig.json");
|
||||
const packageJsonPath = path.join(tmp, "package.json");
|
||||
await writeRuntimePostBuildScaffold(tmp);
|
||||
await fs.mkdir(path.dirname(extensionPath), { recursive: true });
|
||||
await fs.mkdir(path.dirname(distEntryPath), { recursive: true });
|
||||
await fs.writeFile(extensionPath, "export const extensionValue = 1;\n", "utf-8");
|
||||
@@ -222,14 +301,20 @@ describe("run-node script", () => {
|
||||
|
||||
it("skips rebuilding when extension package metadata is newer than the build stamp", async () => {
|
||||
await withTempDir(async (tmp) => {
|
||||
const manifestPath = path.join(tmp, "extensions", "demo", "openclaw.plugin.json");
|
||||
const packagePath = path.join(tmp, "extensions", "demo", "package.json");
|
||||
const distPackagePath = path.join(tmp, "dist", "extensions", "demo", "package.json");
|
||||
const distEntryPath = path.join(tmp, "dist", "entry.js");
|
||||
const buildStampPath = path.join(tmp, "dist", ".buildstamp");
|
||||
const tsconfigPath = path.join(tmp, "tsconfig.json");
|
||||
const packageJsonPath = path.join(tmp, "package.json");
|
||||
const tsdownConfigPath = path.join(tmp, "tsdown.config.ts");
|
||||
await writeRuntimePostBuildScaffold(tmp);
|
||||
await fs.mkdir(path.dirname(manifestPath), { recursive: true });
|
||||
await fs.mkdir(path.dirname(packagePath), { recursive: true });
|
||||
await fs.mkdir(path.dirname(distEntryPath), { recursive: true });
|
||||
await fs.mkdir(path.dirname(distPackagePath), { recursive: true });
|
||||
await fs.writeFile(manifestPath, '{"id":"demo","configSchema":{"type":"object"}}\n', "utf-8");
|
||||
await fs.writeFile(
|
||||
packagePath,
|
||||
'{"name":"demo","openclaw":{"extensions":["./index.ts"]}}\n',
|
||||
@@ -239,11 +324,17 @@ describe("run-node script", () => {
|
||||
await fs.writeFile(packageJsonPath, '{"name":"openclaw-test"}\n', "utf-8");
|
||||
await fs.writeFile(tsdownConfigPath, "export default {};\n", "utf-8");
|
||||
await fs.writeFile(distEntryPath, "console.log('built');\n", "utf-8");
|
||||
await fs.writeFile(
|
||||
distPackagePath,
|
||||
'{"name":"demo","openclaw":{"extensions":["./stale.js"]}}\n',
|
||||
"utf-8",
|
||||
);
|
||||
await fs.writeFile(buildStampPath, '{"head":"abc123"}\n', "utf-8");
|
||||
|
||||
const oldTime = new Date("2026-03-13T10:00:00.000Z");
|
||||
const stampTime = new Date("2026-03-13T12:00:00.000Z");
|
||||
const newTime = new Date("2026-03-13T12:00:01.000Z");
|
||||
await fs.utimes(manifestPath, oldTime, oldTime);
|
||||
await fs.utimes(tsconfigPath, oldTime, oldTime);
|
||||
await fs.utimes(packageJsonPath, oldTime, oldTime);
|
||||
await fs.utimes(tsdownConfigPath, oldTime, oldTime);
|
||||
@@ -274,6 +365,7 @@ describe("run-node script", () => {
|
||||
|
||||
expect(exitCode).toBe(0);
|
||||
expect(spawnCalls).toEqual([[process.execPath, "openclaw.mjs", "status"]]);
|
||||
await expect(fs.readFile(distPackagePath, "utf-8")).resolves.toContain('"./index.js"');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -286,6 +378,7 @@ describe("run-node script", () => {
|
||||
const tsconfigPath = path.join(tmp, "tsconfig.json");
|
||||
const packageJsonPath = path.join(tmp, "package.json");
|
||||
const tsdownConfigPath = path.join(tmp, "tsdown.config.ts");
|
||||
await writeRuntimePostBuildScaffold(tmp);
|
||||
await fs.mkdir(path.dirname(srcPath), { recursive: true });
|
||||
await fs.mkdir(path.dirname(readmePath), { recursive: true });
|
||||
await fs.mkdir(path.dirname(distEntryPath), { recursive: true });
|
||||
@@ -344,20 +437,28 @@ describe("run-node script", () => {
|
||||
await withTempDir(async (tmp) => {
|
||||
const srcPath = path.join(tmp, "src", "index.ts");
|
||||
const manifestPath = path.join(tmp, "extensions", "demo", "openclaw.plugin.json");
|
||||
const distManifestPath = path.join(tmp, "dist", "extensions", "demo", "openclaw.plugin.json");
|
||||
const distEntryPath = path.join(tmp, "dist", "entry.js");
|
||||
const buildStampPath = path.join(tmp, "dist", ".buildstamp");
|
||||
const tsconfigPath = path.join(tmp, "tsconfig.json");
|
||||
const packageJsonPath = path.join(tmp, "package.json");
|
||||
const tsdownConfigPath = path.join(tmp, "tsdown.config.ts");
|
||||
await writeRuntimePostBuildScaffold(tmp);
|
||||
await fs.mkdir(path.dirname(srcPath), { recursive: true });
|
||||
await fs.mkdir(path.dirname(manifestPath), { recursive: true });
|
||||
await fs.mkdir(path.dirname(distEntryPath), { recursive: true });
|
||||
await fs.mkdir(path.dirname(distManifestPath), { recursive: true });
|
||||
await fs.writeFile(srcPath, "export const value = 1;\n", "utf-8");
|
||||
await fs.writeFile(manifestPath, '{"id":"demo"}\n', "utf-8");
|
||||
await fs.writeFile(manifestPath, '{"id":"demo","configSchema":{"type":"object"}}\n', "utf-8");
|
||||
await fs.writeFile(tsconfigPath, "{}\n", "utf-8");
|
||||
await fs.writeFile(packageJsonPath, '{"name":"openclaw-test"}\n', "utf-8");
|
||||
await fs.writeFile(tsdownConfigPath, "export default {};\n", "utf-8");
|
||||
await fs.writeFile(distEntryPath, "console.log('built');\n", "utf-8");
|
||||
await fs.writeFile(
|
||||
distManifestPath,
|
||||
'{"id":"stale","configSchema":{"type":"object"}}\n',
|
||||
"utf-8",
|
||||
);
|
||||
await fs.writeFile(buildStampPath, '{"head":"abc123"}\n', "utf-8");
|
||||
|
||||
const stampTime = new Date("2026-03-13T12:00:00.000Z");
|
||||
@@ -400,6 +501,146 @@ describe("run-node script", () => {
|
||||
|
||||
expect(exitCode).toBe(0);
|
||||
expect(spawnCalls).toEqual([[process.execPath, "openclaw.mjs", "status"]]);
|
||||
await expect(fs.readFile(distManifestPath, "utf-8")).resolves.toContain('"id":"demo"');
|
||||
});
|
||||
});
|
||||
|
||||
it("repairs missing bundled plugin metadata without rerunning tsdown", async () => {
|
||||
await withTempDir(async (tmp) => {
|
||||
const srcPath = path.join(tmp, "src", "index.ts");
|
||||
const manifestPath = path.join(tmp, "extensions", "demo", "openclaw.plugin.json");
|
||||
const distManifestPath = path.join(tmp, "dist", "extensions", "demo", "openclaw.plugin.json");
|
||||
const distEntryPath = path.join(tmp, "dist", "entry.js");
|
||||
const buildStampPath = path.join(tmp, "dist", ".buildstamp");
|
||||
const tsconfigPath = path.join(tmp, "tsconfig.json");
|
||||
const packageJsonPath = path.join(tmp, "package.json");
|
||||
const tsdownConfigPath = path.join(tmp, "tsdown.config.ts");
|
||||
await writeRuntimePostBuildScaffold(tmp);
|
||||
await fs.mkdir(path.dirname(srcPath), { recursive: true });
|
||||
await fs.mkdir(path.dirname(manifestPath), { recursive: true });
|
||||
await fs.mkdir(path.dirname(distEntryPath), { recursive: true });
|
||||
await fs.writeFile(srcPath, "export const value = 1;\n", "utf-8");
|
||||
await fs.writeFile(manifestPath, '{"id":"demo","configSchema":{"type":"object"}}\n', "utf-8");
|
||||
await fs.writeFile(tsconfigPath, "{}\n", "utf-8");
|
||||
await fs.writeFile(packageJsonPath, '{"name":"openclaw-test"}\n', "utf-8");
|
||||
await fs.writeFile(tsdownConfigPath, "export default {};\n", "utf-8");
|
||||
await fs.writeFile(distEntryPath, "console.log('built');\n", "utf-8");
|
||||
await fs.writeFile(buildStampPath, '{"head":"abc123"}\n', "utf-8");
|
||||
|
||||
const stampTime = new Date("2026-03-13T12:00:00.000Z");
|
||||
await fs.utimes(srcPath, stampTime, stampTime);
|
||||
await fs.utimes(manifestPath, stampTime, stampTime);
|
||||
await fs.utimes(tsconfigPath, stampTime, stampTime);
|
||||
await fs.utimes(packageJsonPath, stampTime, stampTime);
|
||||
await fs.utimes(tsdownConfigPath, stampTime, stampTime);
|
||||
await fs.utimes(distEntryPath, stampTime, stampTime);
|
||||
await fs.utimes(buildStampPath, stampTime, stampTime);
|
||||
|
||||
const spawnCalls: string[][] = [];
|
||||
const spawn = (cmd: string, args: string[]) => {
|
||||
spawnCalls.push([cmd, ...args]);
|
||||
return createExitedProcess(0);
|
||||
};
|
||||
const spawnSync = (cmd: string, args: string[]) => {
|
||||
if (cmd === "git" && args[0] === "rev-parse") {
|
||||
return { status: 0, stdout: "abc123\n" };
|
||||
}
|
||||
if (cmd === "git" && args[0] === "status") {
|
||||
return { status: 0, stdout: "" };
|
||||
}
|
||||
return { status: 1, stdout: "" };
|
||||
};
|
||||
|
||||
const { runNodeMain } = await import("../../scripts/run-node.mjs");
|
||||
const exitCode = await runNodeMain({
|
||||
cwd: tmp,
|
||||
args: ["status"],
|
||||
env: {
|
||||
...process.env,
|
||||
OPENCLAW_RUNNER_LOG: "0",
|
||||
},
|
||||
spawn,
|
||||
spawnSync,
|
||||
execPath: process.execPath,
|
||||
platform: process.platform,
|
||||
});
|
||||
|
||||
expect(exitCode).toBe(0);
|
||||
expect(spawnCalls).toEqual([[process.execPath, "openclaw.mjs", "status"]]);
|
||||
await expect(fs.readFile(distManifestPath, "utf-8")).resolves.toContain('"id":"demo"');
|
||||
});
|
||||
});
|
||||
|
||||
it("removes stale bundled plugin metadata when the source manifest is gone", async () => {
|
||||
await withTempDir(async (tmp) => {
|
||||
const srcPath = path.join(tmp, "src", "index.ts");
|
||||
const extensionDir = path.join(tmp, "extensions", "demo");
|
||||
const distManifestPath = path.join(tmp, "dist", "extensions", "demo", "openclaw.plugin.json");
|
||||
const distPackagePath = path.join(tmp, "dist", "extensions", "demo", "package.json");
|
||||
const distEntryPath = path.join(tmp, "dist", "entry.js");
|
||||
const buildStampPath = path.join(tmp, "dist", ".buildstamp");
|
||||
const tsconfigPath = path.join(tmp, "tsconfig.json");
|
||||
const packageJsonPath = path.join(tmp, "package.json");
|
||||
const tsdownConfigPath = path.join(tmp, "tsdown.config.ts");
|
||||
await writeRuntimePostBuildScaffold(tmp);
|
||||
await fs.mkdir(path.dirname(srcPath), { recursive: true });
|
||||
await fs.mkdir(extensionDir, { recursive: true });
|
||||
await fs.mkdir(path.dirname(distManifestPath), { recursive: true });
|
||||
await fs.mkdir(path.dirname(distEntryPath), { recursive: true });
|
||||
await fs.writeFile(srcPath, "export const value = 1;\n", "utf-8");
|
||||
await fs.writeFile(tsconfigPath, "{}\n", "utf-8");
|
||||
await fs.writeFile(packageJsonPath, '{"name":"openclaw-test"}\n', "utf-8");
|
||||
await fs.writeFile(tsdownConfigPath, "export default {};\n", "utf-8");
|
||||
await fs.writeFile(distEntryPath, "console.log('built');\n", "utf-8");
|
||||
await fs.writeFile(buildStampPath, '{"head":"abc123"}\n', "utf-8");
|
||||
await fs.writeFile(
|
||||
distManifestPath,
|
||||
'{"id":"stale","configSchema":{"type":"object"}}\n',
|
||||
"utf-8",
|
||||
);
|
||||
await fs.writeFile(distPackagePath, '{"name":"stale"}\n', "utf-8");
|
||||
|
||||
const stampTime = new Date("2026-03-13T12:00:00.000Z");
|
||||
await fs.utimes(srcPath, stampTime, stampTime);
|
||||
await fs.utimes(tsconfigPath, stampTime, stampTime);
|
||||
await fs.utimes(packageJsonPath, stampTime, stampTime);
|
||||
await fs.utimes(tsdownConfigPath, stampTime, stampTime);
|
||||
await fs.utimes(distEntryPath, stampTime, stampTime);
|
||||
await fs.utimes(buildStampPath, stampTime, stampTime);
|
||||
|
||||
const spawnCalls: string[][] = [];
|
||||
const spawn = (cmd: string, args: string[]) => {
|
||||
spawnCalls.push([cmd, ...args]);
|
||||
return createExitedProcess(0);
|
||||
};
|
||||
const spawnSync = (cmd: string, args: string[]) => {
|
||||
if (cmd === "git" && args[0] === "rev-parse") {
|
||||
return { status: 0, stdout: "abc123\n" };
|
||||
}
|
||||
if (cmd === "git" && args[0] === "status") {
|
||||
return { status: 0, stdout: "" };
|
||||
}
|
||||
return { status: 1, stdout: "" };
|
||||
};
|
||||
|
||||
const { runNodeMain } = await import("../../scripts/run-node.mjs");
|
||||
const exitCode = await runNodeMain({
|
||||
cwd: tmp,
|
||||
args: ["status"],
|
||||
env: {
|
||||
...process.env,
|
||||
OPENCLAW_RUNNER_LOG: "0",
|
||||
},
|
||||
spawn,
|
||||
spawnSync,
|
||||
execPath: process.execPath,
|
||||
platform: process.platform,
|
||||
});
|
||||
|
||||
expect(exitCode).toBe(0);
|
||||
expect(spawnCalls).toEqual([[process.execPath, "openclaw.mjs", "status"]]);
|
||||
await expect(fs.access(distManifestPath)).rejects.toThrow();
|
||||
await expect(fs.access(distPackagePath)).rejects.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -412,6 +653,7 @@ describe("run-node script", () => {
|
||||
const tsconfigPath = path.join(tmp, "tsconfig.json");
|
||||
const packageJsonPath = path.join(tmp, "package.json");
|
||||
const tsdownConfigPath = path.join(tmp, "tsdown.config.ts");
|
||||
await writeRuntimePostBuildScaffold(tmp);
|
||||
await fs.mkdir(path.dirname(srcPath), { recursive: true });
|
||||
await fs.mkdir(path.dirname(readmePath), { recursive: true });
|
||||
await fs.mkdir(path.dirname(distEntryPath), { recursive: true });
|
||||
@@ -468,6 +710,7 @@ describe("run-node script", () => {
|
||||
const tsconfigPath = path.join(tmp, "tsconfig.json");
|
||||
const packageJsonPath = path.join(tmp, "package.json");
|
||||
const tsdownConfigPath = path.join(tmp, "tsdown.config.ts");
|
||||
await writeRuntimePostBuildScaffold(tmp);
|
||||
await fs.mkdir(path.dirname(srcPath), { recursive: true });
|
||||
await fs.mkdir(path.dirname(distEntryPath), { recursive: true });
|
||||
await fs.writeFile(srcPath, "export const value = 1;\n", "utf-8");
|
||||
|
||||
Reference in New Issue
Block a user