mirror of
https://github.com/openclaw/openclaw.git
synced 2026-07-21 10:16:44 +00:00
fix: prevent generated asset watch loops
This commit is contained in:
@@ -10,15 +10,46 @@ const repoRoot = path.resolve(path.dirname(modulePath), "..");
|
||||
const discordDir = path.join(repoRoot, "extensions/discord");
|
||||
const outputPath = path.join(repoRoot, "extensions/discord/assets/embedded-app-sdk.mjs");
|
||||
|
||||
await fs.mkdir(path.dirname(outputPath), { recursive: true });
|
||||
await build({
|
||||
entryPoints: ["@discord/embedded-app-sdk"],
|
||||
absWorkingDir: discordDir,
|
||||
bundle: true,
|
||||
platform: "browser",
|
||||
target: "es2020",
|
||||
format: "esm",
|
||||
minify: true,
|
||||
legalComments: "none",
|
||||
outfile: outputPath,
|
||||
});
|
||||
/** Builds the browser SDK bundle without rewriting an identical tracked asset. */
|
||||
export async function buildDiscordActivitySdk(params = {}) {
|
||||
const buildImpl = params.build ?? build;
|
||||
const targetPath = params.outputPath ?? outputPath;
|
||||
await fs.mkdir(path.dirname(targetPath), { recursive: true });
|
||||
const result = await buildImpl({
|
||||
entryPoints: ["@discord/embedded-app-sdk"],
|
||||
absWorkingDir: discordDir,
|
||||
bundle: true,
|
||||
platform: "browser",
|
||||
target: "es2020",
|
||||
format: "esm",
|
||||
minify: true,
|
||||
legalComments: "none",
|
||||
outfile: targetPath,
|
||||
write: false,
|
||||
});
|
||||
|
||||
const outputFile = result.outputFiles?.[0];
|
||||
if (!outputFile) {
|
||||
throw new Error("esbuild did not produce the Discord Embedded App SDK bundle");
|
||||
}
|
||||
|
||||
const nextBundle = outputFile.text;
|
||||
let currentBundle = null;
|
||||
try {
|
||||
currentBundle = await fs.readFile(targetPath, "utf8");
|
||||
} catch (error) {
|
||||
if (error?.code !== "ENOENT") {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
if (currentBundle === nextBundle) {
|
||||
return false;
|
||||
}
|
||||
await fs.writeFile(targetPath, nextBundle);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (process.argv[1] === modulePath) {
|
||||
await buildDiscordActivitySdk();
|
||||
}
|
||||
|
||||
@@ -40,6 +40,13 @@ const ignoredRunNodeRepoPathPatterns = [
|
||||
/^extensions\/[^/]+\/src\/host\/.+\/\.bundle\.hash$/u,
|
||||
/^extensions\/[^/]+\/src\/host\/.+\/[^/]+\.bundle\.js$/u,
|
||||
];
|
||||
// Asset build hooks write these tracked runtime bundles. They are outputs, not
|
||||
// source inputs; watching them makes the dev build react to its own writes.
|
||||
const generatedPluginAssetPaths = new Set([
|
||||
"extensions/diffs-language-pack/assets/viewer-runtime.js",
|
||||
"extensions/diffs/assets/viewer-runtime.js",
|
||||
"extensions/discord/assets/embedded-app-sdk.mjs",
|
||||
]);
|
||||
const extensionSourceFilePattern = /\.(?:[cm]?[jt]sx?)$/;
|
||||
|
||||
/** Normalizes watch paths to repository-style POSIX separators. */
|
||||
@@ -69,7 +76,10 @@ const isRestartRelevantExtensionPath = (relativePath) => {
|
||||
|
||||
const isRelevantRunNodePath = (repoPath, isRelevantBundledPluginPath) => {
|
||||
const normalizedPath = normalizeRunNodePath(repoPath).replace(/^\.\/+/, "");
|
||||
if (ignoredRunNodeRepoPathPatterns.some((pattern) => pattern.test(normalizedPath))) {
|
||||
if (
|
||||
generatedPluginAssetPaths.has(normalizedPath) ||
|
||||
ignoredRunNodeRepoPathPatterns.some((pattern) => pattern.test(normalizedPath))
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
if (runNodeConfigFiles.includes(normalizedPath)) {
|
||||
|
||||
@@ -2092,7 +2092,9 @@ const TOOLING_SOURCE_TEST_TARGETS = new Map([
|
||||
["scripts/e2e/cron-mcp-cleanup-seed.ts", ["test/scripts/docker-e2e-seeds.test.ts"]],
|
||||
["scripts/bundled-plugin-assets.mjs", ["test/scripts/bundled-plugin-assets.test.ts"]],
|
||||
["scripts/bundle-a2ui.mjs", ["test/scripts/bundled-plugin-assets.test.ts"]],
|
||||
["scripts/build-discord-activity-sdk.mjs", ["test/scripts/bundled-plugin-assets.test.ts"]],
|
||||
["scripts/build-diffs-viewer-runtime.mjs", ["test/scripts/build-diffs-viewer-runtime.test.ts"]],
|
||||
["scripts/run-node-watch-paths.mjs", ["test/scripts/bundled-plugin-assets.test.ts"]],
|
||||
["extensions/canvas/scripts/bundle-a2ui.mjs", ["extensions/canvas/scripts/bundle-a2ui.test.ts"]],
|
||||
["extensions/canvas/scripts/copy-a2ui.mjs", ["extensions/canvas/scripts/copy-a2ui.test.ts"]],
|
||||
]);
|
||||
|
||||
@@ -2,11 +2,16 @@
|
||||
import fs from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { buildDiscordActivitySdk } from "../../scripts/build-discord-activity-sdk.mjs";
|
||||
import {
|
||||
parseBundledPluginAssetArgs,
|
||||
readBundledPluginAssetHooks,
|
||||
} from "../../scripts/bundled-plugin-assets.mjs";
|
||||
import {
|
||||
isBuildRelevantRunNodePath,
|
||||
isRestartRelevantRunNodePath,
|
||||
} from "../../scripts/run-node-watch-paths.mjs";
|
||||
|
||||
async function withPluginAssetFixture(run: (rootDir: string) => Promise<void>) {
|
||||
const rootDir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-plugin-assets-"));
|
||||
@@ -39,14 +44,30 @@ async function withPluginAssetFixture(run: (rootDir: string) => Promise<void>) {
|
||||
}
|
||||
|
||||
describe("bundled plugin assets", () => {
|
||||
it("resolves the Discord SDK entry from the package that declares it", () => {
|
||||
const script = fs.readFileSync(
|
||||
path.join(process.cwd(), "scripts/build-discord-activity-sdk.mjs"),
|
||||
"utf8",
|
||||
);
|
||||
it("does not rewrite an unchanged Discord SDK bundle", async () => {
|
||||
const rootDir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-discord-sdk-"));
|
||||
const outputPath = path.join(rootDir, "embedded-app-sdk.mjs");
|
||||
const build = vi.fn(async () => ({
|
||||
outputFiles: [{ text: "export const sdk = true;\n" }],
|
||||
}));
|
||||
|
||||
expect(script).toContain('const discordDir = path.join(repoRoot, "extensions/discord")');
|
||||
expect(script).toContain("absWorkingDir: discordDir");
|
||||
try {
|
||||
fs.writeFileSync(outputPath, "export const sdk = true;\n");
|
||||
const initialTime = new Date("2026-07-16T12:00:00.000Z");
|
||||
fs.utimesSync(outputPath, initialTime, initialTime);
|
||||
|
||||
await expect(buildDiscordActivitySdk({ build, outputPath })).resolves.toBe(false);
|
||||
expect(fs.statSync(outputPath).mtimeMs).toBe(initialTime.getTime());
|
||||
expect(build).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
absWorkingDir: path.join(process.cwd(), "extensions/discord"),
|
||||
outfile: outputPath,
|
||||
write: false,
|
||||
}),
|
||||
);
|
||||
} finally {
|
||||
fs.rmSync(rootDir, { force: true, recursive: true });
|
||||
}
|
||||
});
|
||||
|
||||
it("discovers the Discord Embedded App SDK build hook", async () => {
|
||||
@@ -66,6 +87,30 @@ describe("bundled plugin assets", () => {
|
||||
]);
|
||||
});
|
||||
|
||||
it("keeps build-generated static assets out of the source watcher", async () => {
|
||||
const rootDir = process.cwd();
|
||||
const hooks = await readBundledPluginAssetHooks({ phase: "build", rootDir });
|
||||
const generatedAssetSources = hooks.flatMap((hook) => {
|
||||
const packageJson = JSON.parse(
|
||||
fs.readFileSync(path.join(hook.pluginDir, "package.json"), "utf8"),
|
||||
) as {
|
||||
openclaw?: { build?: { staticAssets?: Array<{ source?: string }> } };
|
||||
};
|
||||
const pluginPath = path.relative(rootDir, hook.pluginDir).replaceAll(path.sep, "/");
|
||||
return (packageJson.openclaw?.build?.staticAssets ?? []).flatMap((asset) => {
|
||||
const source = asset.source?.replace(/^\.\/+/u, "");
|
||||
return source ? [path.posix.join(pluginPath, source)] : [];
|
||||
});
|
||||
});
|
||||
|
||||
expect(generatedAssetSources).toContain("extensions/discord/assets/embedded-app-sdk.mjs");
|
||||
for (const source of generatedAssetSources) {
|
||||
expect(isBuildRelevantRunNodePath(source), source).toBe(false);
|
||||
expect(isRestartRelevantRunNodePath(source), source).toBe(false);
|
||||
}
|
||||
expect(isRestartRelevantRunNodePath("extensions/discord/src/activities/http.ts")).toBe(true);
|
||||
});
|
||||
|
||||
it("discovers plugin-owned asset scripts by manifest id", async () => {
|
||||
await withPluginAssetFixture(async (rootDir) => {
|
||||
const hooks = await readBundledPluginAssetHooks({
|
||||
|
||||
Reference in New Issue
Block a user