fix(regression): ship diffs viewer runtime asset

This commit is contained in:
Gustavo Madeira Santana
2026-04-01 09:53:59 -04:00
parent 4de1606f4c
commit 2dab0c518a
6 changed files with 117 additions and 6 deletions

View File

@@ -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);

View File

@@ -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;