From 7cfc2168a22dea29099872a0d82d57bb74d45cf7 Mon Sep 17 00:00:00 2001 From: gsxdsm Date: Tue, 9 Jun 2026 12:00:45 -0700 Subject: [PATCH] FN-6111: alias bundled plugin views to source entries Keep bundled plugin dashboard views loading from source so Compound Engineering no longer falls back to the unavailable placeholder. - switch bundled dependency graph, roadmap, and compound engineering dashboard view loaders to static literal imports - add vitest aliases for dependency graph and compound engineering dashboard view source entrypoints and cover the alias contract with regression tests - document the missing-alias failure mode and the static-import requirement for bundled dashboard views Files changed: .../bundled-plugin-vite-alias-missing.md | 20 +++++++++++--------- .../__tests__/registerBundledPluginViews.test.tsx | 10 +++++++-- .../app/plugins/registerBundledPluginViews.ts | 19 +++++------------- .../runtime-plugin-alias-regression.test.ts | 21 ++++++++++++++------ packages/dashboard/vitest.config.ts | 8 ++++++++ 5 files changed, 49 insertions(+), 29 deletions(-) Fusion-Task-Id: FN-6111 Fusion-Task-Lineage: a41b893c-4c14-4a2d-b0c9-47e1dbd0e1ef --- .../bundled-plugin-vite-alias-missing.md | 20 ++++++++++-------- .../registerBundledPluginViews.test.tsx | 10 ++++++++- .../app/plugins/registerBundledPluginViews.ts | 19 +++++------------ .../runtime-plugin-alias-regression.test.ts | 21 ++++++++++++++----- packages/dashboard/vitest.config.ts | 8 +++++++ 5 files changed, 49 insertions(+), 29 deletions(-) diff --git a/docs/solutions/integration-issues/bundled-plugin-vite-alias-missing.md b/docs/solutions/integration-issues/bundled-plugin-vite-alias-missing.md index 22b41a4ce6..7480cabd63 100644 --- a/docs/solutions/integration-issues/bundled-plugin-vite-alias-missing.md +++ b/docs/solutions/integration-issues/bundled-plugin-vite-alias-missing.md @@ -8,6 +8,7 @@ component: tooling symptoms: - "Plugin fails to enable with: Unknown file extension \".css\" for /path/to/PluginView.css" - "Dynamic import of bundled plugin view returns 404 or module not found" + - "Bundled plugin view unavailable: @fusion-plugin-examples/compound-engineering/dashboard-view#CompoundEngineeringDashboardView" - "Plugin works in its own package build but fails when loaded by the dashboard" root_cause: incomplete_setup resolution_type: config_change @@ -19,16 +20,16 @@ tags: [plugins, bundled-plugins, vite, alias, dashboard-view, registration-drift ## Problem -When a bundled plugin exports a dashboard view, the dashboard dynamically imports it at runtime via `registerBundledPluginViews.ts`. For this to work, the dashboard's Vite configuration must include a `resolve.alias` entry that maps the plugin's package name to its source directory. Without this alias, Vite cannot resolve the dynamic import, and the plugin view fails to load. +When a bundled plugin exports a dashboard view, the dashboard lazy-loads it via `registerBundledPluginViews.ts`. For this to work, the dashboard's Vite and Vitest configurations must include `resolve.alias` entries that map the plugin's package name to its source directory, and the loader's `import()` call must use a static string literal so Vite can emit a bundled lazy chunk. Without the alias or static import, Vite cannot resolve or bundle the view module, and the plugin view fails to load. -The error message is misleading: Vite reports `Unknown file extension ".css"` because the module resolution fails entirely and the error bubbles up through an unrelated loader path. +The error message can be misleading: Vite may report `Unknown file extension ".css"` because module resolution fails and the error bubbles up through an unrelated loader path. In production, a `/* @vite-ignore */` dynamic import can instead surface as the dashboard placeholder: `Bundled plugin view unavailable: #`. ## Symptoms - The Compound Engineering plugin (or any bundled plugin with a dashboard view) fails to enable - Console shows: `Failed to enable Compound Engineering: Unknown file extension ".css" for /path/to/PluginView.css` - The plugin's own package builds successfully — the issue only manifests when the dashboard tries to load it -- Other bundled plugins (e.g., dependency-graph) load correctly — they have aliases +- Other bundled plugins (e.g., dependency-graph) load correctly — they have aliases and static literal `import()` loaders ## What Didn't Work @@ -38,7 +39,7 @@ The error message is misleading: Vite reports `Unknown file extension ".css"` be ## Solution -Add the missing `resolve.alias` entries to `packages/dashboard/vite.config.ts`: +Add the missing `resolve.alias` entries to both `packages/dashboard/vite.config.ts` and `packages/dashboard/vitest.config.ts`: ```ts // packages/dashboard/vite.config.ts @@ -67,14 +68,14 @@ Both aliases are needed: ## Why This Works -The dashboard uses dynamic imports with `@vite-ignore` to load plugin views at runtime: +The dashboard should use statically analyzable literal imports for bundled plugin views: ```ts // packages/dashboard/app/plugins/registerBundledPluginViews.ts -const mod = await import(/* @vite-ignore */ moduleId); +const mod = await import("@fusion-plugin-examples/compound-engineering/dashboard-view"); ``` -Vite's static analysis cannot trace these imports, so it relies on `resolve.alias` to map the module ID to a filesystem path. Without the alias, Vite falls through to default resolution, which fails because the plugin package is in a sibling `plugins/` directory outside the dashboard's root. The error surfaces through the CSS loader because Vite's fallback resolution path misattributes the failure. +Vite can trace a literal `import()` through `resolve.alias`, include the aliased source file in the build graph, and emit a code-split chunk for the lazy view. A `/* @vite-ignore */` import using a dynamic module ID prevents this analysis; production browsers then try to resolve the bare package specifier at runtime and the dashboard falls back to the "Bundled plugin view unavailable" placeholder. Without the alias, Vite falls through to default resolution, which fails because the plugin package is in a sibling `plugins/` directory outside the dashboard's root. Some failures surface through the CSS loader because Vite's fallback resolution path misattributes the failure. ## Related root cause: CSS import via index re-export @@ -88,9 +89,10 @@ Server-side plugin loading uses Node.js `import()` against the plugin entry. If ## Prevention -- **When adding a bundled plugin with a dashboard view, grep for an existing plugin alias** in `packages/dashboard/vite.config.ts` and mirror the pattern for the new plugin +- **When adding a bundled plugin with a dashboard view, grep for an existing plugin alias** in `packages/dashboard/vite.config.ts` and `packages/dashboard/vitest.config.ts` and mirror the pattern for the new plugin - **Do not re-export dashboard view components from the plugin's server-side `index.ts`** — the server entry must stay free of React/CSS view imports; `dashboardViews` metadata is enough for registration -- **Verify the alias in both dev and production builds** — the alias must resolve correctly for Vite's dev server and its production bundler +- **Use a static string literal `import()` in `registerBundledPluginViews.ts`** for bundled dashboard views that should be code-split by Vite; avoid `/* @vite-ignore */` unless the task explicitly requires runtime-only resolution +- **Verify the alias in dev, production builds, and Vitest** — the alias must resolve correctly for Vite's dev server, production bundler, and test runner - **Consider a consistency test** that asserts every plugin registered in `registerBundledPluginViews.ts` has a corresponding Vite alias (similar to the existing `lazy-loaded-views-docs.test.ts` that keeps the AGENTS.md view inventory in sync) - **Watch for the misleading `.css` error** — when Vite reports an unknown file extension for a file that clearly exists, suspect module resolution failure before investigating loaders; when Node reports it while enabling a plugin, suspect a server-entry re-export of the dashboard view diff --git a/packages/dashboard/app/plugins/__tests__/registerBundledPluginViews.test.tsx b/packages/dashboard/app/plugins/__tests__/registerBundledPluginViews.test.tsx index b4f46de187..c3b09654d6 100644 --- a/packages/dashboard/app/plugins/__tests__/registerBundledPluginViews.test.tsx +++ b/packages/dashboard/app/plugins/__tests__/registerBundledPluginViews.test.tsx @@ -7,6 +7,7 @@ import { } from "../registerBundledPluginViews"; const MockDependencyGraphDashboardView = () => createElement("div", { "data-testid": "dep-graph-view" }); +const MockCompoundEngineeringDashboardView = () => createElement("div", { "data-testid": "ce-view" }); const MockRoadmapDashboardView = () => createElement("div", { "data-testid": "roadmap-view" }); const MockCliPrintingPressWizardView = () => createElement("div", { "data-testid": "cli-printing-press-view" }); const MockCliPrintingPressManageView = () => createElement("div", { "data-testid": "cli-printing-press-manage-view" }); @@ -15,6 +16,10 @@ vi.mock("@fusion-plugin-examples/dependency-graph/dashboard-view", () => ({ DependencyGraphDashboardView: (...args: unknown[]) => MockDependencyGraphDashboardView(...args), })); +vi.mock("@fusion-plugin-examples/compound-engineering/dashboard-view", () => ({ + CompoundEngineeringDashboardView: (...args: unknown[]) => MockCompoundEngineeringDashboardView(...args), +})); + vi.mock("@fusion-plugin-examples/fusion-plugin-roadmap/dashboard-view", () => ({ RoadmapDashboardView: (...args: unknown[]) => MockRoadmapDashboardView(...args), })); @@ -36,13 +41,15 @@ describe("registerBundledPluginViews", () => { __test_resetBundledPluginViewRegistration(); }); - it("registers dependency graph, roadmap, and cli printing press bundled views", () => { + it("registers dependency graph, compound engineering, roadmap, and cli printing press bundled views", () => { registerBundledPluginViews(); // This registration is independent of engine-side plugin load success; the // dashboard can still render the Graph view while the plugin install row is errored. expect(isPluginViewRegistered("fusion-plugin-dependency-graph", "graph")).toBe(true); expect(getPluginViewComponent("fusion-plugin-dependency-graph", "graph")).toBeTruthy(); + expect(isPluginViewRegistered("fusion-plugin-compound-engineering", "compound-engineering")).toBe(true); + expect(getPluginViewComponent("fusion-plugin-compound-engineering", "compound-engineering")).toBeTruthy(); expect(getPluginViewComponent("fusion-plugin-roadmap", "roadmaps")).toBeTruthy(); expect(getPluginViewComponent("fusion-plugin-cli-printing-press", "wizard")).toBeTruthy(); expect(getPluginViewComponent("fusion-plugin-cli-printing-press", "manage")).toBeTruthy(); @@ -62,6 +69,7 @@ describe("registerBundledPluginViews", () => { registerBundledPluginViews(); expect(isPluginViewRegistered("fusion-plugin-dependency-graph", "graph")).toBe(true); + expect(isPluginViewRegistered("fusion-plugin-compound-engineering", "compound-engineering")).toBe(true); expect(isPluginViewRegistered("fusion-plugin-roadmap", "roadmaps")).toBe(true); expect(isPluginViewRegistered("fusion-plugin-cli-printing-press", "wizard")).toBe(true); expect(isPluginViewRegistered("fusion-plugin-cli-printing-press", "manage")).toBe(true); diff --git a/packages/dashboard/app/plugins/registerBundledPluginViews.ts b/packages/dashboard/app/plugins/registerBundledPluginViews.ts index c859d8b98e..a78f27b225 100644 --- a/packages/dashboard/app/plugins/registerBundledPluginViews.ts +++ b/packages/dashboard/app/plugins/registerBundledPluginViews.ts @@ -38,24 +38,15 @@ async function loadRoadmapView(): Promise<{ default: PluginViewComponent }> { } async function loadCompoundEngineeringView(): Promise<{ default: PluginViewComponent }> { - // @vite-ignore + moduleId variable so tsc does NOT statically resolve/compile - // the plugin's source here. The plugin must not depend on @fusion/dashboard - // (workspace-acyclicity invariant), so its type-only dashboard import only - // resolves in the plugin's own build; a literal import would make the - // dashboard typecheck the plugin file and fail to resolve that import. const moduleId = "@fusion-plugin-examples/compound-engineering/dashboard-view"; const exportName = "CompoundEngineeringDashboardView"; - try { - const mod = await import(/* @vite-ignore */ moduleId) as unknown as Record>; - const component = mod[exportName]; - if (!component) { - console.warn(`[plugin-views] Missing export ${exportName} from ${moduleId}`); - return { default: createMissingPluginView(moduleId, exportName) }; - } - return { default: component as PluginViewComponent }; - } catch { + const mod = await import("@fusion-plugin-examples/compound-engineering/dashboard-view") as unknown as Record>; + const component = mod[exportName]; + if (!component) { + console.warn(`[plugin-views] Missing export ${exportName} from ${moduleId}`); return { default: createMissingPluginView(moduleId, exportName) }; } + return { default: component as PluginViewComponent }; } async function loadCliPrintingPressWizardView(): Promise<{ default: PluginViewComponent }> { diff --git a/packages/dashboard/src/__tests__/runtime-plugin-alias-regression.test.ts b/packages/dashboard/src/__tests__/runtime-plugin-alias-regression.test.ts index 9371edc563..bfd7aa2d61 100644 --- a/packages/dashboard/src/__tests__/runtime-plugin-alias-regression.test.ts +++ b/packages/dashboard/src/__tests__/runtime-plugin-alias-regression.test.ts @@ -23,11 +23,10 @@ describe("FN-3298 regression: dashboard vitest runtime plugins resolve from sour expect(config).not.toContain('fusion-plugin-paperclip-runtime/dist/index.js'); }); - // FN-3888 regression: bundled dependency-graph dashboard view must not depend - // on plugins/fusion-plugin-dependency-graph/dist/, which goes stale when the - // plugin source is edited without a manual rebuild and surfaces in the UI as - // "Bundled plugin view unavailable: @fusion-plugin-examples/dependency-graph/dashboard-view". - it("aliases dependency-graph plugin imports to src in vite and vitest configs", () => { + // FN-3888/FN-6111 regression: bundled dashboard views must not depend on + // plugins/*/dist/, which goes stale when plugin source is edited without a + // manual rebuild and surfaces in the UI as "Bundled plugin view unavailable". + it("aliases bundled dashboard view plugin imports to src in vite and vitest configs", () => { const testDir = dirname(fileURLToPath(import.meta.url)); const dashboardDir = join(testDir, "..", ".."); @@ -41,6 +40,18 @@ describe("FN-3298 regression: dashboard vitest runtime plugins resolve from sour '"../../plugins/fusion-plugin-dependency-graph/src/dashboard-view.tsx"', ); expect(config).not.toContain("fusion-plugin-dependency-graph/dist/"); + expect( + config, + `${configFile} must alias the compound-engineering dashboard-view to src`, + ).toContain('"@fusion-plugin-examples/compound-engineering/dashboard-view": resolve('); + expect(config).toContain( + '"../../plugins/fusion-plugin-compound-engineering/src/dashboard-view.tsx"', + ); + expect(config).toContain('"@fusion-plugin-examples/compound-engineering": resolve('); + expect(config).toContain( + '"../../plugins/fusion-plugin-compound-engineering/src/index.ts"', + ); + expect(config).not.toContain("fusion-plugin-compound-engineering/dist/"); } }); }); diff --git a/packages/dashboard/vitest.config.ts b/packages/dashboard/vitest.config.ts index 13c9b263a5..590de66c4c 100644 --- a/packages/dashboard/vitest.config.ts +++ b/packages/dashboard/vitest.config.ts @@ -302,6 +302,14 @@ export default defineConfig({ __dirname, "../../plugins/fusion-plugin-dependency-graph/src/index.ts", ), + "@fusion-plugin-examples/compound-engineering/dashboard-view": resolve( + __dirname, + "../../plugins/fusion-plugin-compound-engineering/src/dashboard-view.tsx", + ), + "@fusion-plugin-examples/compound-engineering": resolve( + __dirname, + "../../plugins/fusion-plugin-compound-engineering/src/index.ts", + ), }, }, test: {