From 9d07e85232a09b98574972f254a238b0686a5265 Mon Sep 17 00:00:00 2001 From: gsxdsm Date: Fri, 19 Jun 2026 03:49:56 -0700 Subject: [PATCH] FN-6690: preload lazy-view CSS chunks Ensure persisted lazy dashboard views request their extracted CSS before first paint. - Carry CSS asset paths through the Vite view chunk manifest. - Inject stylesheet links alongside modulepreload links for persisted lazy views. - Cover Command Center CSS preloading and served CSS availability in dashboard tests. - Quarantine the unrelated session cross-tab cleanup flake observed during verification. Files changed: .changeset/fn-6690-lazy-view-css.md | 5 + .../__tests__/board-mobile-initial-render.test.tsx | 16 ++- .../src/__tests__/server-view-preload.test.ts | 152 ++++++++++++++++++++- .../src/__tests__/view-chunk-manifest.test.ts | 57 ++++++-- packages/dashboard/src/server.ts | 16 ++- packages/dashboard/src/view-chunk-manifest.ts | 26 +++- packages/dashboard/vitest.config.ts | 6 +- scripts/lib/test-quarantine.json | 5 + 8 files changed, 253 insertions(+), 30 deletions(-) Fusion-Task-Id: FN-6690 Fusion-Task-Lineage: 6ac0d06f-14db-4302-98f8-0e742172ed7f --- .changeset/fn-6690-lazy-view-css.md | 5 + .../board-mobile-initial-render.test.tsx | 16 +- .../src/__tests__/server-view-preload.test.ts | 152 +++++++++++++++++- .../src/__tests__/view-chunk-manifest.test.ts | 57 +++++-- packages/dashboard/src/server.ts | 16 +- packages/dashboard/src/view-chunk-manifest.ts | 26 ++- packages/dashboard/vitest.config.ts | 6 +- scripts/lib/test-quarantine.json | 5 + 8 files changed, 253 insertions(+), 30 deletions(-) create mode 100644 .changeset/fn-6690-lazy-view-css.md diff --git a/.changeset/fn-6690-lazy-view-css.md b/.changeset/fn-6690-lazy-view-css.md new file mode 100644 index 0000000000..8e4d3f8a44 --- /dev/null +++ b/.changeset/fn-6690-lazy-view-css.md @@ -0,0 +1,5 @@ +--- +"@runfusion/fusion": patch +--- + +Fix served dashboard lazy-view preloads so persisted Command Center and other lazy views include their extracted CSS chunks. diff --git a/packages/dashboard/app/components/__tests__/board-mobile-initial-render.test.tsx b/packages/dashboard/app/components/__tests__/board-mobile-initial-render.test.tsx index e79f87b9c4..68979ac333 100644 --- a/packages/dashboard/app/components/__tests__/board-mobile-initial-render.test.tsx +++ b/packages/dashboard/app/components/__tests__/board-mobile-initial-render.test.tsx @@ -74,6 +74,12 @@ function extractRule(content: string, selector: string): string { return content.match(new RegExp(`${escapedSelector}\\s*\\{[^}]*\\}`))?.[0] ?? ""; } +function expectLogicalOrPhysicalMinSize(rule: string, axis: "block" | "inline"): void { + const logicalProp = axis === "block" ? "min-block-size" : "min-inline-size"; + const physicalProp = axis === "block" ? "min-height" : "min-width"; + expect(rule).toSatisfy((value: string) => value.includes(`${logicalProp}: 0`) || value.includes(`${physicalProp}: 0`)); +} + const workflowPayload = { flagEnabled: true, defaultWorkflowId: "builtin:coding", @@ -272,8 +278,12 @@ describe("Board mobile initial render stabilization (FN-4574)", () => { const projectContentRule = extractRule(cssContent, ".project-content"); expect(projectContentRule).toContain("display: flex"); - expect(projectContentRule).toContain("min-height: 0"); - expect(projectContentRule).toContain("min-width: 0"); + /* + * FNXC:BoardMobileCss 2026-06-19-03:16: + * The fill-height invariant accepts logical min-size properties because styles.css canonicalizes .project-content to writing-mode-safe min-block-size/min-inline-size declarations. + */ + expectLogicalOrPhysicalMinSize(projectContentRule, "block"); + expectLogicalOrPhysicalMinSize(projectContentRule, "inline"); expect(baseBoardRule).toContain("box-sizing: border-box"); expect(baseBoardRule).toContain("flex: 1 1 auto"); @@ -324,7 +334,7 @@ describe("Board mobile initial render stabilization (FN-4574)", () => { expect(mobileProjectContentRule).toContain("display: flex"); expect(mobileProjectContentRule).toContain("align-items: stretch"); expect(mobileProjectContentRule).toContain("width: 100%"); - expect(mobileProjectContentRule).toContain("min-height: 0"); + expectLogicalOrPhysicalMinSize(mobileProjectContentRule, "block"); expect(mobileProjectContentRule).toContain("overflow: hidden"); expect(mobileWorkflowViewRule).toContain("display: flex"); diff --git a/packages/dashboard/src/__tests__/server-view-preload.test.ts b/packages/dashboard/src/__tests__/server-view-preload.test.ts index 091828f618..3aabc4a432 100644 --- a/packages/dashboard/src/__tests__/server-view-preload.test.ts +++ b/packages/dashboard/src/__tests__/server-view-preload.test.ts @@ -1,11 +1,13 @@ // @vitest-environment node -import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from "node:fs"; +import { mkdtempSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; -import { afterEach, describe, expect } from "vitest"; +import vm from "node:vm"; +import { JSDOM } from "jsdom"; +import { afterEach, describe, expect, it } from "vitest"; import { TaskStore } from "@fusion/core"; -import { createServer } from "../server.js"; +import { buildViewPreloadInjection, createServer } from "../server.js"; import { createLoopbackIntegrationTest } from "./loopback-integration-test.js"; const serverViewPreloadIntegrationTest = await createLoopbackIntegrationTest("server-view-preload integration"); @@ -18,6 +20,39 @@ function makeTempDir(prefix: string): string { return dir; } +type AppendedLink = { + rel?: string; + href?: string; + crossOrigin?: string; +}; + +function runPreloadBootstrap(injection: string, taskView: string, projectId?: string): AppendedLink[] { + const script = injection.match(/^`; +} + function parseVersion(version: string): number[] { return version .split(".") @@ -792,11 +801,6 @@ export function createServer(store: TaskStore, options?: ServerOptions): ReturnT let cachedIndexMtimeMs: number | null = null; let cachedTemplatedIndexHtml: string | null = null; - const buildViewPreloadInjection = (chunkMap: Record): string => { - const serializedChunkMap = JSON.stringify(chunkMap).replace(/<\//g, "<\\/"); - return ``; - }; - const renderIndexHtml = (): string => { const resolvedClientDir = process.env.FUSION_CLIENT_DIR ? process.env.FUSION_CLIENT_DIR diff --git a/packages/dashboard/src/view-chunk-manifest.ts b/packages/dashboard/src/view-chunk-manifest.ts index a79b2a08bb..7be00c32fe 100644 --- a/packages/dashboard/src/view-chunk-manifest.ts +++ b/packages/dashboard/src/view-chunk-manifest.ts @@ -5,10 +5,20 @@ type TaskViewId = string; type ManifestEntry = { file?: string; + css?: string[]; +}; + +export type ViewChunkManifestEntry = { + file: string; + css: string[]; }; type ViteManifest = Record; +/* +FNXC:CommandCenterStyling 2026-06-19-09:42: +Persisted lazy views need the served index bootstrap to know both the JavaScript chunk and any Vite-emitted CSS chunks. Command Center's co-located styles are split into a dynamic CSS asset, so omitting either the view id or the css array from this manifest map can render the served first load unstyled even though in-app navigation still works through Vite's runtime preload helper. +*/ // Canonical taskView ids that map to lazy React views in App.tsx. // Intentionally excluded: // - nodes: opened by overlay state, not taskView routing @@ -24,13 +34,14 @@ export const VIEW_SOURCE_MAP: Record = { memory: "components/MemoryView.tsx", insights: "components/InsightsView.tsx", reliability: "components/ReliabilityView.tsx", + "command-center": "components/command-center/CommandCenter.tsx", "dev-server": "components/DevServerView.tsx", goalsView: "components/GoalsView.tsx", "stash-recovery": "components/StashRecoveryView.tsx", }; type ManifestCacheEntry = { - entries: Record; + entries: Record; mtimeMs: number | null; }; @@ -46,7 +57,7 @@ function warnOnce(set: Set, key: string, message: string): void { console.warn(message); } -export function loadViewChunkManifest(clientDir: string): Record { +export function loadViewChunkManifest(clientDir: string): Record { const cacheKey = resolve(clientDir); const manifestPath = join(cacheKey, ".vite", "manifest.json"); @@ -67,14 +78,14 @@ export function loadViewChunkManifest(clientDir: string): Record = {}; + const empty: Record = {}; manifestCache.set(cacheKey, { entries: empty, mtimeMs }); return empty; } try { const manifest = JSON.parse(readFileSync(manifestPath, "utf8")) as ViteManifest; - const resolvedEntries: Record = {}; + const resolvedEntries: Record = {}; for (const [viewId, sourcePath] of Object.entries(VIEW_SOURCE_MAP)) { const entry = manifest[sourcePath]; @@ -86,14 +97,17 @@ export function loadViewChunkManifest(clientDir: string): Record `/${asset}`), + }; } manifestCache.set(cacheKey, { entries: resolvedEntries, mtimeMs }); return resolvedEntries; } catch { warnOnce(warnedMissingManifest, `${cacheKey}:parse`, `[dashboard] Failed to parse view chunk manifest: ${manifestPath}`); - const empty: Record = {}; + const empty: Record = {}; manifestCache.set(cacheKey, { entries: empty, mtimeMs }); return empty; } diff --git a/packages/dashboard/vitest.config.ts b/packages/dashboard/vitest.config.ts index f3f8c7ec73..f6654551bf 100644 --- a/packages/dashboard/vitest.config.ts +++ b/packages/dashboard/vitest.config.ts @@ -265,8 +265,12 @@ Keep QuickEntryBox out of this list so focus-restoration coverage remains active FNXC:DashboardTestQuarantine 2026-06-18-09:07: FN-6642 rescued chat-routes by fixing the shared engine mock to return an iterable chat-task-document tool list during broad API lanes. Keep chat-routes out of this list so SSE lifecycle coverage remains active and the ledger/config stay in lockstep. + +FNXC:DashboardTestQuarantine 2026-06-19-03:22: +FN-6690 workspace verification observed session-cross-tab fail only during the broad dashboard API backfill shard with temp-directory cleanup ENOTEMPTY, then pass on isolated rerun. +Quarantine the cleanup-flaky file under the deletion ratchet rather than changing timing or session-locking behavior outside the lazy-view CSS chunk scope. */ -const quarantinedDashboardTests: string[] = []; +const quarantinedDashboardTests: string[] = ["src/__tests__/session-cross-tab.test.ts"]; const qualityApiTests = [ // Critical HTTP/server behavior: auth, task/project/settings mutation, diff --git a/scripts/lib/test-quarantine.json b/scripts/lib/test-quarantine.json index d8744f3523..fa7167159a 100644 --- a/scripts/lib/test-quarantine.json +++ b/scripts/lib/test-quarantine.json @@ -1,5 +1,10 @@ { "$comment": "Flaky-test quarantine ledger (deletion ratchet — see AGENTS.md 'Flaky tests: quarantine on sight' and docs/testing.md 'Quarantine ledger and the deletion ratchet'). A test observed failing without a corresponding real bug is quarantined ON SIGHT: add an entry here AND a matching one-line `exclude` entry in that package's vitest config, in the same commit. Every entry needs a non-empty `reason` (link the failing run) and a `quarantinedAt` date — the entry expires 14 days later, at which point the test file is DELETED unless someone rescues it with evidence it catches real regressions plus a root-cause fix (never appeasement). There is deliberately no loader module and no automation around this file: it is a dated record, the vitest config exclude is the mechanism, and the sweep is policy executed by whoever touches the suite.", "entries": [ + { + "file": "packages/dashboard/src/__tests__/session-cross-tab.test.ts", + "reason": "FN-6690 local workspace `pnpm test` observed ENOTEMPTY while removing the test's temp .fusion directory in dashboard-api-quality-backfill shard; isolated rerun passed, indicating cleanup flake rather than a lazy-view CSS regression.", + "quarantinedAt": "2026-06-19" + } ] }