mirror of https://github.com/openclaw/openclaw.git
fix(regression): ship diffs viewer runtime asset
This commit is contained in:
parent
4de1606f4c
commit
2dab0c518a
|
|
@ -6,7 +6,7 @@ import {
|
|||
ResolvingThemes,
|
||||
} from "@pierre/diffs";
|
||||
import AjvPkg from "ajv";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import {
|
||||
DEFAULT_DIFFS_PLUGIN_SECURITY,
|
||||
DEFAULT_DIFFS_TOOL_DEFAULTS,
|
||||
|
|
@ -17,7 +17,12 @@ import {
|
|||
} from "./config.js";
|
||||
import { renderDiffDocument } from "./render.js";
|
||||
import { buildViewerUrl, normalizeViewerBaseUrl } from "./url.js";
|
||||
import { getServedViewerAsset, VIEWER_LOADER_PATH, VIEWER_RUNTIME_PATH } from "./viewer-assets.js";
|
||||
import {
|
||||
getServedViewerAsset,
|
||||
resolveViewerRuntimeFileUrl,
|
||||
VIEWER_LOADER_PATH,
|
||||
VIEWER_RUNTIME_PATH,
|
||||
} from "./viewer-assets.js";
|
||||
import { parseViewerPayloadJson } from "./viewer-payload.js";
|
||||
|
||||
const FULL_DEFAULTS = {
|
||||
|
|
@ -540,6 +545,47 @@ describe("renderDiffDocument", () => {
|
|||
});
|
||||
|
||||
describe("viewer assets", () => {
|
||||
it("prefers the built plugin asset layout when present", async () => {
|
||||
const stat = vi.fn(async (path: string) => {
|
||||
if (path === "/repo/dist/extensions/diffs/assets/viewer-runtime.js") {
|
||||
return { mtimeMs: 1 };
|
||||
}
|
||||
const error = Object.assign(new Error(`missing: ${path}`), { code: "ENOENT" });
|
||||
throw error;
|
||||
});
|
||||
|
||||
await expect(
|
||||
resolveViewerRuntimeFileUrl({
|
||||
baseUrl: "file:///repo/dist/extensions/diffs/index.js",
|
||||
stat,
|
||||
}),
|
||||
).resolves.toMatchObject({
|
||||
pathname: "/repo/dist/extensions/diffs/assets/viewer-runtime.js",
|
||||
});
|
||||
expect(stat).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("falls back to the source asset layout when the built artifact is absent", async () => {
|
||||
const stat = vi.fn(async (path: string) => {
|
||||
if (path === "/repo/extensions/diffs/assets/viewer-runtime.js") {
|
||||
return { mtimeMs: 1 };
|
||||
}
|
||||
const error = Object.assign(new Error(`missing: ${path}`), { code: "ENOENT" });
|
||||
throw error;
|
||||
});
|
||||
|
||||
await expect(
|
||||
resolveViewerRuntimeFileUrl({
|
||||
baseUrl: "file:///repo/extensions/diffs/src/viewer-assets.js",
|
||||
stat,
|
||||
}),
|
||||
).resolves.toMatchObject({
|
||||
pathname: "/repo/extensions/diffs/assets/viewer-runtime.js",
|
||||
});
|
||||
expect(stat).toHaveBeenNthCalledWith(1, "/repo/extensions/diffs/src/assets/viewer-runtime.js");
|
||||
expect(stat).toHaveBeenNthCalledWith(2, "/repo/extensions/diffs/assets/viewer-runtime.js");
|
||||
});
|
||||
|
||||
it("serves a stable loader that points at the current runtime bundle", async () => {
|
||||
const loader = await getServedViewerAsset(VIEWER_LOADER_PATH);
|
||||
|
||||
|
|
|
|||
|
|
@ -6,8 +6,10 @@ export const VIEWER_ASSET_PREFIX = "/plugins/diffs/assets/";
|
|||
export const VIEWER_LOADER_PATH = `${VIEWER_ASSET_PREFIX}viewer.js`;
|
||||
export const VIEWER_RUNTIME_PATH = `${VIEWER_ASSET_PREFIX}viewer-runtime.js`;
|
||||
const VIEWER_RUNTIME_RELATIVE_IMPORT_PATH = "./viewer-runtime.js";
|
||||
|
||||
const VIEWER_RUNTIME_FILE_URL = new URL("../assets/viewer-runtime.js", import.meta.url);
|
||||
const VIEWER_RUNTIME_CANDIDATE_RELATIVE_PATHS = [
|
||||
"./assets/viewer-runtime.js",
|
||||
"../assets/viewer-runtime.js",
|
||||
] as const;
|
||||
|
||||
export type ServedViewerAsset = {
|
||||
body: string | Buffer;
|
||||
|
|
@ -22,6 +24,43 @@ type RuntimeAssetCache = {
|
|||
|
||||
let runtimeAssetCache: RuntimeAssetCache | null = null;
|
||||
|
||||
type ViewerRuntimeFileUrlParams = {
|
||||
baseUrl?: string | URL;
|
||||
stat?: (path: string) => Promise<unknown>;
|
||||
};
|
||||
|
||||
function isMissingFileError(error: unknown): error is NodeJS.ErrnoException {
|
||||
return error instanceof Error && "code" in error && error.code === "ENOENT";
|
||||
}
|
||||
|
||||
export async function resolveViewerRuntimeFileUrl(
|
||||
params: ViewerRuntimeFileUrlParams = {},
|
||||
): Promise<URL> {
|
||||
const baseUrl = params.baseUrl ?? import.meta.url;
|
||||
const stat = params.stat ?? ((path: string) => fs.stat(path));
|
||||
let missingFileError: NodeJS.ErrnoException | null = null;
|
||||
|
||||
for (const relativePath of VIEWER_RUNTIME_CANDIDATE_RELATIVE_PATHS) {
|
||||
const candidateUrl = new URL(relativePath, baseUrl);
|
||||
try {
|
||||
await stat(fileURLToPath(candidateUrl));
|
||||
return candidateUrl;
|
||||
} catch (error) {
|
||||
if (isMissingFileError(error)) {
|
||||
missingFileError = error;
|
||||
continue;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
if (missingFileError) {
|
||||
throw missingFileError;
|
||||
}
|
||||
|
||||
throw new Error("viewer runtime asset candidates were not checked");
|
||||
}
|
||||
|
||||
export async function getServedViewerAsset(pathname: string): Promise<ServedViewerAsset | null> {
|
||||
if (pathname !== VIEWER_LOADER_PATH && pathname !== VIEWER_RUNTIME_PATH) {
|
||||
return null;
|
||||
|
|
@ -46,7 +85,8 @@ export async function getServedViewerAsset(pathname: string): Promise<ServedView
|
|||
}
|
||||
|
||||
async function loadViewerAssets(): Promise<RuntimeAssetCache> {
|
||||
const runtimePath = fileURLToPath(VIEWER_RUNTIME_FILE_URL);
|
||||
const runtimeUrl = await resolveViewerRuntimeFileUrl();
|
||||
const runtimePath = fileURLToPath(runtimeUrl);
|
||||
const runtimeStat = await fs.stat(runtimePath);
|
||||
if (runtimeAssetCache && runtimeAssetCache.mtimeMs === runtimeStat.mtimeMs) {
|
||||
return runtimeAssetCache;
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import {
|
|||
} from "./lib/bundled-extension-manifest.ts";
|
||||
import { listBundledPluginPackArtifacts } from "./lib/bundled-plugin-build-entries.mjs";
|
||||
import { listPluginSdkDistArtifacts } from "./lib/plugin-sdk-entries.mjs";
|
||||
import { listStaticExtensionAssetOutputs } from "./runtime-postbuild.mjs";
|
||||
import { sparkleBuildFloorsFromShortVersion, type SparkleBuildFloors } from "./sparkle-build.ts";
|
||||
|
||||
export { collectBundledExtensionManifestErrors } from "./lib/bundled-extension-manifest.ts";
|
||||
|
|
@ -23,6 +24,7 @@ const requiredPathGroups = [
|
|||
["dist/entry.js", "dist/entry.mjs"],
|
||||
...listPluginSdkDistArtifacts(),
|
||||
...listBundledPluginPackArtifacts(),
|
||||
...listStaticExtensionAssetOutputs(),
|
||||
"scripts/npm-runner.mjs",
|
||||
"scripts/postinstall-bundled-plugins.mjs",
|
||||
"dist/plugin-sdk/compat.js",
|
||||
|
|
@ -169,7 +171,7 @@ export function collectMissingPackPaths(paths: Iterable<string>): string[] {
|
|||
}
|
||||
return available.has(group) ? [] : [group];
|
||||
})
|
||||
.toSorted();
|
||||
.toSorted((left, right) => left.localeCompare(right));
|
||||
}
|
||||
|
||||
export function collectForbiddenPackPaths(paths: Iterable<string>): string[] {
|
||||
|
|
|
|||
|
|
@ -25,8 +25,21 @@ export const STATIC_EXTENSION_ASSETS = [
|
|||
src: "extensions/acpx/src/runtime-internals/mcp-proxy.mjs",
|
||||
dest: "dist/extensions/acpx/mcp-proxy.mjs",
|
||||
},
|
||||
// diffs viewer runtime bundle — co-deployed inside the plugin package so the
|
||||
// built bundle can resolve `./assets/viewer-runtime.js` from dist.
|
||||
{
|
||||
src: "extensions/diffs/assets/viewer-runtime.js",
|
||||
dest: "dist/extensions/diffs/assets/viewer-runtime.js",
|
||||
},
|
||||
];
|
||||
|
||||
export function listStaticExtensionAssetOutputs(params = {}) {
|
||||
const assets = params.assets ?? STATIC_EXTENSION_ASSETS;
|
||||
return assets
|
||||
.map(({ dest }) => dest.replace(/\\/g, "/"))
|
||||
.toSorted((left, right) => left.localeCompare(right));
|
||||
}
|
||||
|
||||
export function copyStaticExtensionAssets(params = {}) {
|
||||
const rootDir = params.rootDir ?? ROOT;
|
||||
const assets = params.assets ?? STATIC_EXTENSION_ASSETS;
|
||||
|
|
|
|||
|
|
@ -263,6 +263,7 @@ describe("collectMissingPackPaths", () => {
|
|||
"dist/control-ui/index.html",
|
||||
"scripts/npm-runner.mjs",
|
||||
"scripts/postinstall-bundled-plugins.mjs",
|
||||
bundledDistPluginFile("diffs", "assets/viewer-runtime.js"),
|
||||
bundledDistPluginFile("matrix", "helper-api.js"),
|
||||
bundledDistPluginFile("matrix", "runtime-api.js"),
|
||||
bundledDistPluginFile("matrix", "thread-bindings-runtime.js"),
|
||||
|
|
@ -282,6 +283,8 @@ describe("collectMissingPackPaths", () => {
|
|||
"dist/index.js",
|
||||
"dist/entry.js",
|
||||
"dist/control-ui/index.html",
|
||||
"dist/extensions/acpx/mcp-proxy.mjs",
|
||||
bundledDistPluginFile("diffs", "assets/viewer-runtime.js"),
|
||||
...requiredBundledPluginPackPaths,
|
||||
...requiredPluginSdkPackPaths,
|
||||
"scripts/npm-runner.mjs",
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import path from "node:path";
|
|||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import {
|
||||
copyStaticExtensionAssets,
|
||||
listStaticExtensionAssetOutputs,
|
||||
writeStableRootRuntimeAliases,
|
||||
} from "../../scripts/runtime-postbuild.mjs";
|
||||
|
||||
|
|
@ -22,6 +23,12 @@ async function createTempRoot() {
|
|||
}
|
||||
|
||||
describe("runtime postbuild static assets", () => {
|
||||
it("tracks plugin-owned static assets that release packaging must ship", () => {
|
||||
expect(listStaticExtensionAssetOutputs()).toContain(
|
||||
"dist/extensions/diffs/assets/viewer-runtime.js",
|
||||
);
|
||||
});
|
||||
|
||||
it("copies declared static assets into dist", async () => {
|
||||
const rootDir = await createTempRoot();
|
||||
const src = "extensions/acpx/src/runtime-internals/mcp-proxy.mjs";
|
||||
|
|
|
|||
Loading…
Reference in New Issue