Embed EpiphanyGraph in Hugin

This commit is contained in:
Meta Vens 2026-05-18 03:56:43 +01:00
commit e5c4c9dd50
9 changed files with 661 additions and 242 deletions

View file

@ -57,8 +57,9 @@ npm run dist:inspector
```
Hugin is read-only. Drop a `.cc`, `.msgpack`, or `.mpack` file onto the window to
inspect the snapshot header, schema catalog, records, and decoded MessagePack
payload previews without registering application schemas.
inspect the snapshot header, schema catalog, records, decoded MessagePack
payload previews, and an EpiphanyGraph view of the file's structured data tree
without registering application schemas.
## Example

View file

@ -7,6 +7,6 @@
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

View file

@ -1,239 +0,0 @@
import { inspectCultCacheBytes, type CultCacheInspection, type InspectedCatalogEntry, type InspectedRecord } from "../../src/cult-cache-inspector";
import "./styles.css";
const app = document.querySelector<HTMLDivElement>("#app");
if (!app) {
throw new Error("Missing inspector app root.");
}
const appRoot = app;
type Selection = {
record: number;
catalog: number;
};
let inspection: CultCacheInspection | undefined;
let selection: Selection = { record: 0, catalog: 0 };
let errorMessage = "";
let dragging = false;
appRoot.addEventListener("dragover", (event) => {
event.preventDefault();
dragging = true;
render();
});
appRoot.addEventListener("dragleave", (event) => {
if (event.target === appRoot) {
dragging = false;
render();
}
});
appRoot.addEventListener("drop", (event) => {
event.preventDefault();
dragging = false;
const file = event.dataTransfer?.files.item(0);
if (file) {
void inspectFile(file);
}
});
render();
function render(): void {
appRoot.innerHTML = `
<main class="workspace ${dragging ? "is-dragging" : ""}">
<section class="sidebar">
<div class="brand">
<img src="/hugin-64.png" alt="" width="64" height="64" />
<div>
<h1>Hugin</h1>
<p>CultCache state inspection for <code>.cc</code> files.</p>
</div>
</div>
<label class="dropzone">
<input id="file-input" type="file" accept=".cc,.msgpack,.mpack,application/octet-stream" />
<span class="drop-title">Drop CultCache file</span>
<span class="drop-copy">or choose one from disk</span>
</label>
${errorMessage ? `<div class="error">${escapeHtml(errorMessage)}</div>` : ""}
${inspection ? renderSummary(inspection) : renderEmptyState()}
</section>
<section class="content">
${inspection ? renderInspection(inspection, selection) : renderNoFile()}
</section>
</main>
`;
appRoot.querySelector<HTMLInputElement>("#file-input")?.addEventListener("change", (event) => {
const file = (event.currentTarget as HTMLInputElement).files?.item(0);
if (file) {
void inspectFile(file);
}
});
appRoot.querySelectorAll<HTMLButtonElement>("[data-record]").forEach((button) => {
button.addEventListener("click", () => {
selection = { ...selection, record: Number(button.dataset.record) };
render();
});
});
appRoot.querySelectorAll<HTMLButtonElement>("[data-catalog]").forEach((button) => {
button.addEventListener("click", () => {
selection = { ...selection, catalog: Number(button.dataset.catalog) };
render();
});
});
}
async function inspectFile(file: File): Promise<void> {
try {
const bytes = new Uint8Array(await file.arrayBuffer());
inspection = inspectCultCacheBytes(file.name, bytes, file.size);
selection = { record: 0, catalog: 0 };
errorMessage = "";
} catch (error) {
inspection = undefined;
errorMessage = error instanceof Error ? error.message : String(error);
}
render();
}
function renderSummary(model: CultCacheInspection): string {
return `
<div class="summary">
${metric("Format", model.format)}
${metric("Records", model.records.length.toString())}
${metric("Schemas", model.catalog.length.toString())}
${metric("Bytes", model.fileSizeBytes.toString())}
</div>
`;
}
function renderEmptyState(): string {
return `
<div class="summary">
${metric("Format", ".cc")}
${metric("Mode", "Read-only")}
${metric("Payload", "MessagePack")}
${metric("Runtime", "Vite")}
</div>
`;
}
function renderNoFile(): string {
return `
<div class="hero">
<img src="/hugin.png" alt="" />
<h2>No state loaded</h2>
<p>Drag a CultCache <code>.cc</code> store into the window to inspect the snapshot header, schema catalog, records, and decoded MessagePack payloads.</p>
</div>
`;
}
function renderInspection(model: CultCacheInspection, current: Selection): string {
const record = model.records[current.record];
const catalogEntry = model.catalog[current.catalog];
return `
<div class="columns">
<section class="panel">
<header><h2>Records</h2><span>${model.records.length}</span></header>
<div class="list">
${model.records.map((entry, index) => renderRecordButton(entry, index, index === current.record)).join("") || `<div class="empty">No records</div>`}
</div>
</section>
<section class="panel detail">
<header><h2>Record Detail</h2><span>${escapeHtml(model.filePath)}</span></header>
${record ? renderRecordDetail(record) : `<div class="empty">No record selected</div>`}
</section>
</div>
<div class="columns bottom">
<section class="panel">
<header><h2>Schema Catalog</h2><span>${model.catalog.length}</span></header>
<div class="list">
${model.catalog.map((entry, index) => renderCatalogButton(entry, index, index === current.catalog)).join("") || `<div class="empty">No schema catalog</div>`}
</div>
</section>
<section class="panel detail">
<header><h2>Catalog Entry</h2><span>${catalogEntry ? escapeHtml(catalogEntry.schemaVersion) : ""}</span></header>
${catalogEntry ? renderCatalogDetail(catalogEntry) : `<div class="empty">No catalog entry selected</div>`}
</section>
</div>
`;
}
function renderRecordButton(record: InspectedRecord, index: number, selected: boolean): string {
return `
<button class="row" type="button" data-record="${index}" aria-selected="${selected}">
<strong>${escapeHtml(record.key)}</strong>
<span>${escapeHtml(record.schemaName)} · ${record.payloadBytes} bytes</span>
</button>
`;
}
function renderCatalogButton(entry: InspectedCatalogEntry, index: number, selected: boolean): string {
return `
<button class="row" type="button" data-catalog="${index}" aria-selected="${selected}">
<strong>${escapeHtml(entry.schemaName)}</strong>
<span>${escapeHtml(entry.schemaId)}</span>
</button>
`;
}
function renderRecordDetail(record: InspectedRecord): string {
return `
${facts([
["Key", record.key],
["Schema", record.schemaName],
["Schema ID", record.schemaId],
["Stored", record.storedAt],
["Payload", `${record.payloadBytes} bytes`],
])}
${record.payloadDecodeError ? `<div class="error">${escapeHtml(record.payloadDecodeError)}</div>` : ""}
<pre>${escapeHtml(JSON.stringify(record.payloadPreview, null, 2))}</pre>
`;
}
function renderCatalogDetail(entry: InspectedCatalogEntry): string {
return `
${facts([
["Schema", entry.schemaName],
["Version", entry.schemaVersion],
["Schema ID", entry.schemaId],
["Hash", entry.contentHash],
["Compatible", entry.compatibleSchemaIds.join(", ")],
])}
<pre>${escapeHtml(JSON.stringify({
members: entry.members,
canonicalSchemaJson: parseJson(entry.canonicalSchemaJson),
}, null, 2))}</pre>
`;
}
function facts(rows: Array<[string, string]>): string {
return `<dl>${rows.map(([name, value]) => `<dt>${escapeHtml(name)}</dt><dd>${escapeHtml(value)}</dd>`).join("")}</dl>`;
}
function metric(label: string, value: string): string {
return `<div class="metric"><span>${escapeHtml(label)}</span><strong>${escapeHtml(value)}</strong></div>`;
}
function parseJson(value: string): unknown {
try {
return JSON.parse(value);
} catch {
return value;
}
}
function escapeHtml(value: string): string {
return value
.replaceAll("&", "&amp;")
.replaceAll("<", "&lt;")
.replaceAll(">", "&gt;")
.replaceAll('"', "&quot;")
.replaceAll("'", "&#39;");
}

519
inspector/src/main.tsx Normal file
View file

@ -0,0 +1,519 @@
import { EpiphanyGraphViewer, type EpiphanyGraphEdge, type EpiphanyGraphNode, type EpiphanyGraphsState } from "@epiphanygraph/epiphany-graph-viewer";
import { createRoot } from "react-dom/client";
import { useMemo, useState, type DragEvent } from "react";
import { inspectCultCacheBytes, type CultCacheInspection, type InspectedCatalogEntry, type InspectedRecord } from "../../src/cult-cache-inspector";
import "./styles.css";
type Selection = {
record: number;
catalog: number;
};
const app = document.querySelector<HTMLDivElement>("#app");
if (!app) {
throw new Error("Missing inspector app root.");
}
createRoot(app).render(<HuginApp />);
function HuginApp() {
const [inspection, setInspection] = useState<CultCacheInspection | undefined>();
const [selection, setSelection] = useState<Selection>({ record: 0, catalog: 0 });
const [errorMessage, setErrorMessage] = useState("");
const [dragging, setDragging] = useState(false);
const graphState = useMemo(
() => inspection ? buildGraphState(inspection) : emptyGraphState(),
[inspection],
);
async function inspectFile(file: File): Promise<void> {
try {
const bytes = new Uint8Array(await file.arrayBuffer());
setInspection(inspectCultCacheBytes(file.name, bytes, file.size));
setSelection({ record: 0, catalog: 0 });
setErrorMessage("");
} catch (error) {
setInspection(undefined);
setErrorMessage(error instanceof Error ? error.message : String(error));
}
}
function handleDrop(event: DragEvent<HTMLElement>): void {
event.preventDefault();
setDragging(false);
const file = event.dataTransfer.files.item(0);
if (file) {
void inspectFile(file);
}
}
return (
<main
className={`workspace ${dragging ? "is-dragging" : ""}`}
onDragOver={(event) => {
event.preventDefault();
setDragging(true);
}}
onDragLeave={(event) => {
if (event.target === event.currentTarget) {
setDragging(false);
}
}}
onDrop={handleDrop}
>
<section className="sidebar">
<div className="brand">
<img src="/hugin-64.png" alt="" width="64" height="64" />
<div>
<h1>Hugin</h1>
<p>CultCache state inspection for <code>.cc</code> files.</p>
</div>
</div>
<label className="dropzone">
<input
type="file"
accept=".cc,.msgpack,.mpack,application/octet-stream"
onChange={(event) => {
const file = event.currentTarget.files?.item(0);
if (file) {
void inspectFile(file);
}
}}
/>
<span className="drop-title">Drop CultCache file</span>
<span className="drop-copy">or choose one from disk</span>
</label>
{errorMessage ? <div className="error">{errorMessage}</div> : null}
{inspection ? <Summary inspection={inspection} /> : <EmptySummary />}
</section>
<section className="content">
{inspection
? <InspectionView inspection={inspection} graphState={graphState} selection={selection} setSelection={setSelection} />
: <NoFile />}
</section>
</main>
);
}
function Summary({ inspection }: { inspection: CultCacheInspection }) {
return (
<div className="summary">
<Metric label="Format" value={inspection.format} />
<Metric label="Records" value={inspection.records.length.toString()} />
<Metric label="Schemas" value={inspection.catalog.length.toString()} />
<Metric label="Bytes" value={inspection.fileSizeBytes.toString()} />
</div>
);
}
function EmptySummary() {
return (
<div className="summary">
<Metric label="Format" value=".cc" />
<Metric label="Mode" value="Read-only" />
<Metric label="Payload" value="MessagePack" />
<Metric label="Runtime" value="Vite" />
</div>
);
}
function Metric({ label, value }: { label: string; value: string }) {
return (
<div className="metric">
<span>{label}</span>
<strong>{value}</strong>
</div>
);
}
function NoFile() {
return (
<div className="hero">
<img src="/hugin.png" alt="" />
<h2>No state loaded</h2>
<p>Drag a CultCache <code>.cc</code> store into the window to inspect the snapshot header, schema catalog, records, and decoded MessagePack payloads.</p>
</div>
);
}
function InspectionView({
inspection,
graphState,
selection,
setSelection,
}: {
inspection: CultCacheInspection;
graphState: EpiphanyGraphsState;
selection: Selection;
setSelection: (selection: Selection) => void;
}) {
const record = inspection.records[selection.record];
const catalogEntry = inspection.catalog[selection.catalog];
return (
<div className="inspector-stack">
<section className="graph-panel">
<EpiphanyGraphViewer
state={graphState}
initialGraph="dataflow"
title="CultCache Structure"
style={{ minHeight: 620 }}
/>
</section>
<div className="columns">
<section className="panel">
<header><h2>Records</h2><span>{inspection.records.length}</span></header>
<div className="list">
{inspection.records.length
? inspection.records.map((entry, index) => (
<RecordButton
key={`${entry.schemaId}:${entry.key}`}
record={entry}
index={index}
selected={index === selection.record}
onSelect={() => setSelection({ ...selection, record: index })}
/>
))
: <div className="empty">No records</div>}
</div>
</section>
<section className="panel detail">
<header><h2>Record Detail</h2><span>{inspection.filePath}</span></header>
{record ? <RecordDetail record={record} /> : <div className="empty">No record selected</div>}
</section>
</div>
<div className="columns bottom">
<section className="panel">
<header><h2>Schema Catalog</h2><span>{inspection.catalog.length}</span></header>
<div className="list">
{inspection.catalog.length
? inspection.catalog.map((entry, index) => (
<CatalogButton
key={entry.schemaId}
entry={entry}
index={index}
selected={index === selection.catalog}
onSelect={() => setSelection({ ...selection, catalog: index })}
/>
))
: <div className="empty">No schema catalog</div>}
</div>
</section>
<section className="panel detail">
<header><h2>Catalog Entry</h2><span>{catalogEntry?.schemaVersion ?? ""}</span></header>
{catalogEntry ? <CatalogDetail entry={catalogEntry} /> : <div className="empty">No catalog entry selected</div>}
</section>
</div>
</div>
);
}
function RecordButton({
record,
index,
selected,
onSelect,
}: {
record: InspectedRecord;
index: number;
selected: boolean;
onSelect: () => void;
}) {
return (
<button className="row" type="button" data-record={index} aria-selected={selected} onClick={onSelect}>
<strong>{record.key}</strong>
<span>{record.schemaName} · {record.payloadBytes} bytes</span>
</button>
);
}
function CatalogButton({
entry,
index,
selected,
onSelect,
}: {
entry: InspectedCatalogEntry;
index: number;
selected: boolean;
onSelect: () => void;
}) {
return (
<button className="row" type="button" data-catalog={index} aria-selected={selected} onClick={onSelect}>
<strong>{entry.schemaName}</strong>
<span>{entry.schemaId}</span>
</button>
);
}
function RecordDetail({ record }: { record: InspectedRecord }) {
return (
<>
<Facts rows={[
["Key", record.key],
["Schema", record.schemaName],
["Schema ID", record.schemaId],
["Stored", record.storedAt],
["Payload", `${record.payloadBytes} bytes`],
]} />
{record.payloadDecodeError ? <div className="error">{record.payloadDecodeError}</div> : null}
<pre>{JSON.stringify(record.payloadPreview, null, 2)}</pre>
</>
);
}
function CatalogDetail({ entry }: { entry: InspectedCatalogEntry }) {
return (
<>
<Facts rows={[
["Schema", entry.schemaName],
["Version", entry.schemaVersion],
["Schema ID", entry.schemaId],
["Hash", entry.contentHash],
["Compatible", entry.compatibleSchemaIds.join(", ")],
]} />
<pre>{JSON.stringify({
members: entry.members,
canonicalSchemaJson: parseJson(entry.canonicalSchemaJson),
}, null, 2)}</pre>
</>
);
}
function Facts({ rows }: { rows: Array<[string, string]> }) {
return (
<dl>
{rows.map(([name, value]) => (
<div className="fact-row" key={name}>
<dt>{name}</dt>
<dd>{value}</dd>
</div>
))}
</dl>
);
}
function buildGraphState(inspection: CultCacheInspection): EpiphanyGraphsState {
const dataflowNodes: EpiphanyGraphNode[] = [];
const dataflowEdges: EpiphanyGraphEdge[] = [];
const architectureNodes: EpiphanyGraphNode[] = [
{
id: "store",
title: ".cc Store",
purpose: `${inspection.format} file with ${inspection.records.length} record(s) and ${inspection.catalog.length} schema catalog entr${inspection.catalog.length === 1 ? "y" : "ies"}.`,
mechanism: inspection.filePath,
status: "source",
},
{
id: "catalog",
title: "Schema Catalog",
purpose: "Maps persisted schema ids to schema names, versions, compatible ids, and slot metadata.",
status: `${inspection.catalog.length} schemas`,
},
{
id: "records",
title: "Records",
purpose: "Persisted document entries carrying key, schema id, timestamp, and raw MessagePack payload bytes.",
status: `${inspection.records.length} records`,
},
];
const architectureEdges: EpiphanyGraphEdge[] = [
{
id: "store-catalog",
source_id: "store",
target_id: "catalog",
kind: "contains",
label: "schema catalog",
},
{
id: "store-records",
source_id: "store",
target_id: "records",
kind: "contains",
label: "record set",
},
];
dataflowNodes.push({
id: "store",
title: ".cc Store",
purpose: `Root ${inspection.format} snapshot.`,
mechanism: inspection.filePath,
status: `${inspection.fileSizeBytes} bytes`,
});
for (const entry of inspection.catalog) {
const schemaNodeId = nodeId("schema", entry.schemaId);
dataflowNodes.push({
id: schemaNodeId,
title: entry.schemaName,
purpose: `Schema ${entry.schemaVersion} with ${entry.members.length} declared slot(s).`,
mechanism: entry.contentHash,
status: "schema",
});
dataflowEdges.push({
id: edgeId("store", schemaNodeId, "catalogs"),
source_id: "store",
target_id: schemaNodeId,
kind: "catalogs",
label: "catalog",
});
}
inspection.records.forEach((record, recordIndex) => {
const recordNodeId = nodeId("record", `${record.schemaId}:${record.key}:${recordIndex}`);
const schemaNodeId = nodeId("schema", record.schemaId);
dataflowNodes.push({
id: recordNodeId,
title: record.key,
purpose: `Document record for ${record.schemaName}.`,
mechanism: `${record.payloadBytes} payload bytes stored at ${record.storedAt}`,
status: "record",
});
dataflowEdges.push({
id: edgeId("store", recordNodeId, "contains"),
source_id: "store",
target_id: recordNodeId,
kind: "contains",
label: "record",
});
if (dataflowNodes.some((node) => node.id === schemaNodeId)) {
dataflowEdges.push({
id: edgeId(recordNodeId, schemaNodeId, "uses-schema"),
source_id: recordNodeId,
target_id: schemaNodeId,
kind: "uses-schema",
label: "schema",
});
}
appendValueTree({
nodes: dataflowNodes,
edges: dataflowEdges,
parentId: recordNodeId,
value: record.payloadPreview,
path: "payload",
depth: 0,
});
});
return {
architecture: { nodes: architectureNodes, edges: architectureEdges },
dataflow: { nodes: dataflowNodes, edges: dataflowEdges },
links: [],
};
}
function appendValueTree({
nodes,
edges,
parentId,
value,
path,
depth,
}: {
nodes: EpiphanyGraphNode[];
edges: EpiphanyGraphEdge[];
parentId: string;
value: unknown;
path: string;
depth: number;
}): void {
if (depth > 5) {
return;
}
if (Array.isArray(value)) {
value.forEach((item, index) => {
const childPath = `${path}[${index}]`;
const childId = nodeId("value", `${parentId}:${childPath}`);
nodes.push(valueNode(childId, `[${index}]`, item, childPath));
edges.push(valueEdge(parentId, childId, "slot", index.toString()));
appendValueTree({ nodes, edges, parentId: childId, value: item, path: childPath, depth: depth + 1 });
});
return;
}
if (isPlainObject(value)) {
for (const [key, item] of Object.entries(value)) {
const childPath = `${path}.${key}`;
const childId = nodeId("value", `${parentId}:${childPath}`);
nodes.push(valueNode(childId, key, item, childPath));
edges.push(valueEdge(parentId, childId, "field", key));
appendValueTree({ nodes, edges, parentId: childId, value: item, path: childPath, depth: depth + 1 });
}
}
}
function valueNode(id: string, title: string, value: unknown, path: string): EpiphanyGraphNode {
return {
id,
title,
purpose: valuePurpose(value),
mechanism: path,
status: valueStatus(value),
};
}
function valueEdge(sourceId: string, targetId: string, kind: string, label: string): EpiphanyGraphEdge {
return {
id: edgeId(sourceId, targetId, kind),
source_id: sourceId,
target_id: targetId,
kind,
label,
};
}
function valuePurpose(value: unknown): string {
if (Array.isArray(value)) {
return `Array with ${value.length} item(s).`;
}
if (isPlainObject(value)) {
return `Object with ${Object.keys(value).length} field(s).`;
}
if (value === null) {
return "Null payload value.";
}
return JSON.stringify(value) ?? String(value);
}
function valueStatus(value: unknown): string {
if (Array.isArray(value)) {
return "array";
}
if (isPlainObject(value)) {
return "object";
}
return typeof value;
}
function emptyGraphState(): EpiphanyGraphsState {
return {
architecture: { nodes: [], edges: [] },
dataflow: { nodes: [], edges: [] },
links: [],
};
}
function parseJson(value: string): unknown {
try {
return JSON.parse(value);
} catch {
return value;
}
}
function nodeId(prefix: string, value: string): string {
return `${prefix}:${value.replace(/[^a-zA-Z0-9:_-]/g, "_")}`;
}
function edgeId(sourceId: string, targetId: string, kind: string): string {
return `${sourceId}->${targetId}:${kind}`;
}
function isPlainObject(value: unknown): value is Record<string, unknown> {
return typeof value === "object" && value !== null && !Array.isArray(value);
}

View file

@ -150,6 +150,20 @@ code {
overflow: auto;
}
.inspector-stack {
display: grid;
gap: 14px;
}
.graph-panel {
min-width: 0;
border: 1px solid var(--line);
border-radius: 8px;
overflow: hidden;
background: rgba(5, 9, 13, 0.94);
box-shadow: 0 22px 46px rgba(0, 0, 0, 0.28);
}
.hero {
display: grid;
align-content: center;

115
package-lock.json generated
View file

@ -13,16 +13,44 @@
"zod": "^3.25.76"
},
"devDependencies": {
"@epiphanygraph/epiphany-graph-viewer": "file:../EpiphanyGraph/web/epiphany-graph-viewer",
"@types/node": "^24.0.0",
"@types/react": "^19.2.14",
"@types/react-dom": "^19.2.3",
"@vitejs/plugin-react": "^6.0.2",
"concurrently": "^9.2.1",
"cross-env": "^10.1.0",
"electron": "^42.1.0",
"electron-builder": "^26.8.1",
"react": "^19.2.6",
"react-dom": "^19.2.6",
"typescript": "^5.9.0",
"vite": "^8.0.13",
"wait-on": "^9.0.10"
}
},
"../EpiphanyGraph/web/epiphany-graph-viewer": {
"name": "@epiphanygraph/epiphany-graph-viewer",
"version": "0.1.0",
"dev": true,
"dependencies": {
"elkjs": "^0.11.0"
},
"devDependencies": {
"@types/react": "^19.2.2",
"@types/react-dom": "^19.2.2",
"@vitejs/plugin-react": "^5.1.1",
"react": "^19.2.0",
"react-dom": "^19.2.0",
"tsup": "^8.5.0",
"typescript": "^5.9.3",
"vite": "^7.2.4"
},
"peerDependencies": {
"react": "^19.0.0",
"react-dom": "^19.0.0"
}
},
"node_modules/@develar/schema-utils": {
"version": "2.6.5",
"resolved": "https://registry.npmjs.org/@develar/schema-utils/-/schema-utils-2.6.5.tgz",
@ -376,6 +404,10 @@
"dev": true,
"license": "MIT"
},
"node_modules/@epiphanygraph/epiphany-graph-viewer": {
"resolved": "../EpiphanyGraph/web/epiphany-graph-viewer",
"link": true
},
"node_modules/@hapi/address": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/@hapi/address/-/address-5.1.1.tgz",
@ -941,6 +973,26 @@
"xmlbuilder": ">=11.0.1"
}
},
"node_modules/@types/react": {
"version": "19.2.14",
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz",
"integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==",
"dev": true,
"license": "MIT",
"dependencies": {
"csstype": "^3.2.2"
}
},
"node_modules/@types/react-dom": {
"version": "19.2.3",
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz",
"integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==",
"dev": true,
"license": "MIT",
"peerDependencies": {
"@types/react": "^19.2.0"
}
},
"node_modules/@types/responselike": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/@types/responselike/-/responselike-1.0.3.tgz",
@ -970,6 +1022,32 @@
"@types/node": "*"
}
},
"node_modules/@vitejs/plugin-react": {
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-6.0.2.tgz",
"integrity": "sha512-DlSMqo4WhThw4vB8Mpn0Woe9J+Jfq1geJ61AKW0QEgLzGMNwtIMdxbDUzLxcun8W7NbJO0e2Jg/Nxm3cCSVzzg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@rolldown/pluginutils": "^1.0.0"
},
"engines": {
"node": "^20.19.0 || >=22.12.0"
},
"peerDependencies": {
"@rolldown/plugin-babel": "^0.1.7 || ^0.2.0",
"babel-plugin-react-compiler": "^1.0.0",
"vite": "^8.0.0"
},
"peerDependenciesMeta": {
"@rolldown/plugin-babel": {
"optional": true
},
"babel-plugin-react-compiler": {
"optional": true
}
}
},
"node_modules/@xmldom/xmldom": {
"version": "0.8.13",
"resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.13.tgz",
@ -1784,6 +1862,13 @@
"node": ">= 8"
}
},
"node_modules/csstype": {
"version": "3.2.3",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
"integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
"dev": true,
"license": "MIT"
},
"node_modules/debug": {
"version": "4.4.3",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
@ -3937,6 +4022,29 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/react": {
"version": "19.2.6",
"resolved": "https://registry.npmjs.org/react/-/react-19.2.6.tgz",
"integrity": "sha512-sfWGGfavi0xr8Pg0sVsyHMAOziVYKgPLNrS7ig+ivMNb3wbCBw3KxtflsGBAwD3gYQlE/AEZsTLgToRrSCjb0Q==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/react-dom": {
"version": "19.2.6",
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.6.tgz",
"integrity": "sha512-0prMI+hvBbPjsWnxDLxlCGyM8PN6UuWjEUCYmZhO67xIV9Xasa/r/vDnq+Xyq4Lo27g8QSbO5YzARu0D1Sps3g==",
"dev": true,
"license": "MIT",
"dependencies": {
"scheduler": "^0.27.0"
},
"peerDependencies": {
"react": "^19.2.6"
}
},
"node_modules/read-binary-file-arch": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/read-binary-file-arch/-/read-binary-file-arch-1.0.6.tgz",
@ -4113,6 +4221,13 @@
"node": ">=11.0.0"
}
},
"node_modules/scheduler": {
"version": "0.27.0",
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz",
"integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==",
"dev": true,
"license": "MIT"
},
"node_modules/semver": {
"version": "7.8.0",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.8.0.tgz",

View file

@ -31,11 +31,17 @@
"zod": "^3.25.76"
},
"devDependencies": {
"@epiphanygraph/epiphany-graph-viewer": "file:../EpiphanyGraph/web/epiphany-graph-viewer",
"@types/node": "^24.0.0",
"@types/react": "^19.2.14",
"@types/react-dom": "^19.2.3",
"@vitejs/plugin-react": "^6.0.2",
"concurrently": "^9.2.1",
"cross-env": "^10.1.0",
"electron": "^42.1.0",
"electron-builder": "^26.8.1",
"react": "^19.2.6",
"react-dom": "^19.2.6",
"typescript": "^5.9.0",
"vite": "^8.0.13",
"wait-on": "^9.0.10"

View file

@ -2,6 +2,7 @@
"extends": "./tsconfig.json",
"compilerOptions": {
"lib": ["ES2022", "DOM", "DOM.Iterable"],
"jsx": "react-jsx",
"noEmit": true,
"rootDir": "."
},

View file

@ -1,7 +1,9 @@
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
export default defineConfig({
root: "inspector",
plugins: [react()],
build: {
outDir: "../dist-inspector",
emptyOutDir: true