fix(FN-3888): resolve dependency-graph plugin from src in dashboard build

The dashboard dynamically imports `@fusion-plugin-examples/dependency-graph/dashboard-view`, which resolved through the plugin's package.json exports to `dist/`. When plugin source was edited without rebuilding, stale `dist/` (extensionless ESM imports) made the import throw and the UI surfaced "Bundled plugin view unavailable". Add vite/vitest aliases mapping the plugin (and its `/dashboard-view` subpath) to `src/` so the dashboard never depends on `dist/`. Mirrors the existing pattern for hermes/openclaw/paperclip runtimes. Also extends the runtime-plugin alias regression test, and emits `.js` extensions from the plugin source for the CLI-bundled path.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
gsxdsm
2026-05-09 16:44:44 -07:00
parent 98b9e25ca4
commit 958af16ded
15 changed files with 90 additions and 25 deletions

View File

@@ -1,15 +1,15 @@
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import type { Task } from "@fusion/core";
import { GraphTaskNode } from "./GraphTaskNode";
import { GraphToolbar } from "./GraphToolbar";
import { GraphEdges } from "./edges";
import { filterGraphTasks } from "./filters";
import { computeAutoLayout } from "./layout";
import { useGraphData } from "./useGraphData";
import { useGraphInteraction } from "./useGraphInteraction";
import { useDependencyChain } from "./hooks/useDependencyChain";
import { useGraphPositions } from "./hooks/useGraphPositions";
import { mergePositions, type NodePositions } from "./utils/graphPositionStorage";
import { GraphTaskNode } from "./GraphTaskNode.js";
import { GraphToolbar } from "./GraphToolbar.js";
import { GraphEdges } from "./edges.js";
import { filterGraphTasks } from "./filters.js";
import { computeAutoLayout } from "./layout.js";
import { useGraphData } from "./useGraphData.js";
import { useGraphInteraction } from "./useGraphInteraction.js";
import { useDependencyChain } from "./hooks/useDependencyChain.js";
import { useGraphPositions } from "./hooks/useGraphPositions.js";
import { mergePositions, type NodePositions } from "./utils/graphPositionStorage.js";
import "./DependencyGraph.css";
const NODE_WIDTH = 280;

View File

@@ -1,6 +1,6 @@
import type { CSSProperties, ComponentProps, HTMLAttributes } from "react";
import type { GraphPosition } from "./types";
import { useNodeDrag } from "./hooks/useNodeDrag";
import type { GraphPosition } from "./types.js";
import { useNodeDrag } from "./hooks/useNodeDrag.js";
import { TaskCard } from "@fusion/dashboard/app/components/TaskCard";
import { isTaskStuck } from "@fusion/dashboard/app/utils/taskStuck";
import "./GraphTaskNode.css";

View File

@@ -1,7 +1,8 @@
import { mkdtempSync } from "node:fs";
import { spawnSync } from "node:child_process";
import { existsSync, mkdtempSync } from "node:fs";
import { rm } from "node:fs/promises";
import { tmpdir } from "node:os";
import { dirname, join } from "node:path";
import { dirname, join, resolve } from "node:path";
import { PluginLoader, PluginStore } from "@fusion/core";
import { afterEach, describe, expect, it } from "vitest";
import plugin from "../index";
@@ -24,6 +25,7 @@ describe("dependency graph plugin index", () => {
);
});
// Vitest's resolver can mask extensionless-import issues in emitted dist files.
it("is loadable through package exports", async () => {
const entryModule = await import("@fusion-plugin-examples/dependency-graph");
expect(entryModule.default?.manifest?.id).toBe("fusion-plugin-dependency-graph");
@@ -35,6 +37,29 @@ describe("dependency graph plugin index", () => {
expect(typeof viewModule.default).toBe("function");
});
const hasNodeImportPrereqs =
existsSync(join(process.cwd(), "dist/dashboard-view.js")) &&
existsSync(join(process.cwd(), "node_modules/@fusion/plugin-sdk/dist/index.js"));
const nodeImportTest = hasNodeImportPrereqs ? it : it.skip;
nodeImportTest("keeps built entrypoint imports Node-ESM-safe for relative specifiers", () => {
const script =
"Promise.all([" +
"import('./plugins/fusion-plugin-dependency-graph/dist/index.js')," +
"import('node:fs/promises').then((fs) => fs.readFile('./plugins/fusion-plugin-dependency-graph/dist/dashboard-view.js', 'utf8'))" +
"]).then(([root, dashboardViewSource]) => {" +
"if (root.default?.manifest?.id !== 'fusion-plugin-dependency-graph') process.exit(2);" +
"if (!dashboardViewSource.includes('from \\\"./DependencyGraph.js\\\"')) process.exit(3);" +
"}).catch((e) => { console.error(e?.code, e?.message); process.exit(1); });";
const repoRoot = resolve(process.cwd(), "../..");
const result = spawnSync(process.execPath, ["-e", script], {
cwd: repoRoot,
encoding: "utf8",
});
expect(result.status, `Node import check failed: ${result.stderr || result.stdout}`).toBe(0);
});
it("is loadable by PluginLoader without throwing", async () => {
const rootDir = mkdtempSync(join(tmpdir(), "fn-3737-plugin-loader-"));
testDirs.push(rootDir);

View File

@@ -1,7 +1,7 @@
import type { Task, TaskDetail, WorkflowStep } from "@fusion/core";
import type { PluginDashboardViewContext } from "@fusion/dashboard/app/plugins/types";
import { createElement } from "react";
import { DependencyGraph } from "./DependencyGraph";
import { DependencyGraph } from "./DependencyGraph.js";
function createWorkflowStepNameLookup(workflowSteps: WorkflowStep[] | undefined): ReadonlyMap<string, string> {
return new Map((workflowSteps ?? []).map((step) => [step.id, step.name] as const));

View File

@@ -1,4 +1,4 @@
import type { GraphEdge, GraphPosition } from "./types";
import type { GraphEdge, GraphPosition } from "./types.js";
import "./GraphHighlight.css";
interface GraphEdgesProps {

View File

@@ -1,5 +1,5 @@
import { useCallback, useEffect, useState } from "react";
import { clearPositions, loadPositions, savePositions, type NodePositions } from "../utils/graphPositionStorage";
import { clearPositions, loadPositions, savePositions, type NodePositions } from "../utils/graphPositionStorage.js";
function filterVisiblePositions(positions: NodePositions, visibleTaskIds: Set<string>): NodePositions {
const filtered: NodePositions = {};

View File

@@ -1,6 +1,6 @@
import { useCallback, useMemo, useRef, useState } from "react";
import type { MouseEvent as ReactMouseEvent, PointerEvent as ReactPointerEvent } from "react";
import type { GraphPosition } from "../types";
import type { GraphPosition } from "../types.js";
const DRAG_THRESHOLD_PX = 4;

View File

@@ -1,4 +1,4 @@
import type { GraphData, GraphPosition } from "./types";
import type { GraphData, GraphPosition } from "./types.js";
export interface LayoutOptions {
nodeWidth?: number;

View File

@@ -1,6 +1,6 @@
import { useMemo } from "react";
import type { Task } from "@fusion/core";
import type { GraphData, GraphNode } from "./types";
import type { GraphData, GraphNode } from "./types.js";
export function useGraphData(tasks: Task[]): GraphData {
return useMemo(() => {

View File

@@ -1,7 +1,7 @@
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import type { KeyboardEvent as ReactKeyboardEvent } from "react";
import type { LayoutOptions } from "./layout";
import type { GraphPosition } from "./types";
import type { LayoutOptions } from "./layout.js";
import type { GraphPosition } from "./types.js";
const MIN_ZOOM = 0.1;
const MAX_ZOOM = 3;

View File

@@ -4,8 +4,6 @@
"outDir": "dist",
"rootDir": "./src",
"jsx": "react-jsx",
"module": "esnext",
"moduleResolution": "bundler",
"types": ["react"],
"paths": {
"@fusion/dashboard/app/components/TaskCard": ["./src/dashboard-interop.d.ts"],
@@ -14,5 +12,5 @@
}
},
"include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.d.ts"],
"exclude": ["src/__tests__/**"]
"exclude": ["src/**/__tests__/**"]
}