feat(FN-4782): complete Step 2 — add view chunk manifest helper

Fusion-Task-Id: FN-4782
Fusion-Task-Lineage: 5d6b5632-a135-4d25-9382-57fa23e22484
This commit is contained in:
Fusion (runfusion.ai)
2026-05-17 01:41:55 -07:00
committed by gsxdsm
parent 24ad23bf6f
commit 6bb70dc805
2 changed files with 185 additions and 0 deletions

View File

@@ -0,0 +1,95 @@
import { mkdirSync, rmSync, writeFileSync } from "node:fs";
import { join } from "node:path";
import { tmpdir } from "node:os";
import { afterEach, describe, expect, it } from "vitest";
import {
VIEW_SOURCE_MAP,
loadViewChunkManifest,
resetViewChunkManifestCache,
} from "../view-chunk-manifest";
function makeClientDir(name: string): string {
return join(tmpdir(), `fn-4782-${name}-${Date.now()}-${Math.random().toString(36).slice(2)}`);
}
afterEach(() => {
resetViewChunkManifestCache();
});
describe("view chunk manifest", () => {
it("resolves hashed chunk paths", () => {
const clientDir = makeClientDir("resolve");
mkdirSync(join(clientDir, ".vite"), { recursive: true });
writeFileSync(
join(clientDir, ".vite", "manifest.json"),
JSON.stringify({
[VIEW_SOURCE_MAP.agents]: { file: "assets/AgentsView-abc123.js" },
[VIEW_SOURCE_MAP.chat]: { file: "assets/ChatView-def456.js" },
}),
);
const map = loadViewChunkManifest(clientDir);
expect(map.agents).toBe("/assets/AgentsView-abc123.js");
expect(map.chat).toBe("/assets/ChatView-def456.js");
rmSync(clientDir, { recursive: true, force: true });
});
it("returns empty map when manifest file is missing", () => {
const clientDir = makeClientDir("missing");
mkdirSync(clientDir, { recursive: true });
const map = loadViewChunkManifest(clientDir);
expect(map).toEqual({});
rmSync(clientDir, { recursive: true, force: true });
});
it("returns partial map when source entry is absent", () => {
const clientDir = makeClientDir("partial");
mkdirSync(join(clientDir, ".vite"), { recursive: true });
writeFileSync(
join(clientDir, ".vite", "manifest.json"),
JSON.stringify({
[VIEW_SOURCE_MAP.agents]: { file: "assets/AgentsView-abc123.js" },
}),
);
const map = loadViewChunkManifest(clientDir);
expect(map.agents).toBe("/assets/AgentsView-abc123.js");
expect(map.chat).toBeUndefined();
rmSync(clientDir, { recursive: true, force: true });
});
it("cache is invalidated by reset", () => {
const clientDir = makeClientDir("cache");
mkdirSync(join(clientDir, ".vite"), { recursive: true });
const manifestPath = join(clientDir, ".vite", "manifest.json");
writeFileSync(
manifestPath,
JSON.stringify({
[VIEW_SOURCE_MAP.agents]: { file: "assets/AgentsView-old.js" },
}),
);
const first = loadViewChunkManifest(clientDir);
expect(first.agents).toBe("/assets/AgentsView-old.js");
writeFileSync(
manifestPath,
JSON.stringify({
[VIEW_SOURCE_MAP.agents]: { file: "assets/AgentsView-new.js" },
}),
);
const cached = loadViewChunkManifest(clientDir);
expect(cached.agents).toBe("/assets/AgentsView-old.js");
resetViewChunkManifestCache();
const refreshed = loadViewChunkManifest(clientDir);
expect(refreshed.agents).toBe("/assets/AgentsView-new.js");
rmSync(clientDir, { recursive: true, force: true });
});
});

View File

@@ -0,0 +1,90 @@
import { existsSync, readFileSync } from "node:fs";
import { join, resolve } from "node:path";
type TaskViewId = string;
type ManifestEntry = {
file?: string;
};
type ViteManifest = Record<string, ManifestEntry>;
// Canonical taskView ids that map to lazy React views in App.tsx.
// Intentionally excluded:
// - nodes: opened by overlay state, not taskView routing
// - todo: lazy import exists but not a valid BuiltInTaskView route
// - board/list/graph/missions/mailbox: non-lazy views
export const VIEW_SOURCE_MAP: Record<TaskViewId, string> = {
agents: "components/AgentsView.tsx",
chat: "components/ChatView.tsx",
documents: "components/DocumentsView.tsx",
research: "components/ResearchView.tsx",
evals: "components/EvalsView.tsx",
skills: "components/SkillsView.tsx",
memory: "components/MemoryView.tsx",
insights: "components/InsightsView.tsx",
reliability: "components/ReliabilityView.tsx",
"dev-server": "components/DevServerView.tsx",
goalsView: "components/GoalsView.tsx",
"stash-recovery": "components/StashRecoveryView.tsx",
};
const manifestCache = new Map<string, Record<TaskViewId, string>>();
const warnedMissingManifest = new Set<string>();
const warnedMissingEntries = new Set<string>();
function warnOnce(set: Set<string>, key: string, message: string): void {
if (set.has(key)) {
return;
}
set.add(key);
console.warn(message);
}
export function loadViewChunkManifest(clientDir: string): Record<TaskViewId, string> {
const cacheKey = resolve(clientDir);
const cached = manifestCache.get(cacheKey);
if (cached) {
return cached;
}
const manifestPath = join(cacheKey, ".vite", "manifest.json");
if (!existsSync(manifestPath)) {
warnOnce(warnedMissingManifest, cacheKey, `[dashboard] View chunk manifest missing: ${manifestPath}`);
const empty: Record<TaskViewId, string> = {};
manifestCache.set(cacheKey, empty);
return empty;
}
try {
const manifest = JSON.parse(readFileSync(manifestPath, "utf8")) as ViteManifest;
const resolvedEntries: Record<TaskViewId, string> = {};
for (const [viewId, sourcePath] of Object.entries(VIEW_SOURCE_MAP)) {
const entry = manifest[sourcePath];
if (!entry?.file) {
warnOnce(
warnedMissingEntries,
`${cacheKey}:${viewId}`,
`[dashboard] View chunk manifest entry missing: ${sourcePath} (${viewId})`,
);
continue;
}
resolvedEntries[viewId] = `/${entry.file}`;
}
manifestCache.set(cacheKey, resolvedEntries);
return resolvedEntries;
} catch {
warnOnce(warnedMissingManifest, `${cacheKey}:parse`, `[dashboard] Failed to parse view chunk manifest: ${manifestPath}`);
const empty: Record<TaskViewId, string> = {};
manifestCache.set(cacheKey, empty);
return empty;
}
}
export function resetViewChunkManifestCache(): void {
manifestCache.clear();
warnedMissingManifest.clear();
warnedMissingEntries.clear();
}