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:
committed by
gsxdsm
parent
24ad23bf6f
commit
6bb70dc805
95
packages/dashboard/src/__tests__/view-chunk-manifest.test.ts
Normal file
95
packages/dashboard/src/__tests__/view-chunk-manifest.test.ts
Normal 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 });
|
||||
});
|
||||
});
|
||||
90
packages/dashboard/src/view-chunk-manifest.ts
Normal file
90
packages/dashboard/src/view-chunk-manifest.ts
Normal 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();
|
||||
}
|
||||
Reference in New Issue
Block a user