diff --git a/README.md b/README.md index 30ea75d..a0e63b5 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/inspector/index.html b/inspector/index.html index f13d971..488dd3c 100644 --- a/inspector/index.html +++ b/inspector/index.html @@ -7,6 +7,6 @@
- + diff --git a/inspector/src/main.ts b/inspector/src/main.ts deleted file mode 100644 index 1e805c5..0000000 --- a/inspector/src/main.ts +++ /dev/null @@ -1,239 +0,0 @@ -import { inspectCultCacheBytes, type CultCacheInspection, type InspectedCatalogEntry, type InspectedRecord } from "../../src/cult-cache-inspector"; -import "./styles.css"; - -const app = document.querySelector("#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 = ` -
- -
- ${inspection ? renderInspection(inspection, selection) : renderNoFile()} -
-
- `; - - appRoot.querySelector("#file-input")?.addEventListener("change", (event) => { - const file = (event.currentTarget as HTMLInputElement).files?.item(0); - if (file) { - void inspectFile(file); - } - }); - - appRoot.querySelectorAll("[data-record]").forEach((button) => { - button.addEventListener("click", () => { - selection = { ...selection, record: Number(button.dataset.record) }; - render(); - }); - }); - - appRoot.querySelectorAll("[data-catalog]").forEach((button) => { - button.addEventListener("click", () => { - selection = { ...selection, catalog: Number(button.dataset.catalog) }; - render(); - }); - }); -} - -async function inspectFile(file: File): Promise { - 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 ` -
- ${metric("Format", model.format)} - ${metric("Records", model.records.length.toString())} - ${metric("Schemas", model.catalog.length.toString())} - ${metric("Bytes", model.fileSizeBytes.toString())} -
- `; -} - -function renderEmptyState(): string { - return ` -
- ${metric("Format", ".cc")} - ${metric("Mode", "Read-only")} - ${metric("Payload", "MessagePack")} - ${metric("Runtime", "Vite")} -
- `; -} - -function renderNoFile(): string { - return ` -
- -

No state loaded

-

Drag a CultCache .cc store into the window to inspect the snapshot header, schema catalog, records, and decoded MessagePack payloads.

-
- `; -} - -function renderInspection(model: CultCacheInspection, current: Selection): string { - const record = model.records[current.record]; - const catalogEntry = model.catalog[current.catalog]; - return ` -
-
-

Records

${model.records.length}
-
- ${model.records.map((entry, index) => renderRecordButton(entry, index, index === current.record)).join("") || `
No records
`} -
-
-
-

Record Detail

${escapeHtml(model.filePath)}
- ${record ? renderRecordDetail(record) : `
No record selected
`} -
-
-
-
-

Schema Catalog

${model.catalog.length}
-
- ${model.catalog.map((entry, index) => renderCatalogButton(entry, index, index === current.catalog)).join("") || `
No schema catalog
`} -
-
-
-

Catalog Entry

${catalogEntry ? escapeHtml(catalogEntry.schemaVersion) : ""}
- ${catalogEntry ? renderCatalogDetail(catalogEntry) : `
No catalog entry selected
`} -
-
- `; -} - -function renderRecordButton(record: InspectedRecord, index: number, selected: boolean): string { - return ` - - `; -} - -function renderCatalogButton(entry: InspectedCatalogEntry, index: number, selected: boolean): string { - return ` - - `; -} - -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 ? `
${escapeHtml(record.payloadDecodeError)}
` : ""} -
${escapeHtml(JSON.stringify(record.payloadPreview, null, 2))}
- `; -} - -function renderCatalogDetail(entry: InspectedCatalogEntry): string { - return ` - ${facts([ - ["Schema", entry.schemaName], - ["Version", entry.schemaVersion], - ["Schema ID", entry.schemaId], - ["Hash", entry.contentHash], - ["Compatible", entry.compatibleSchemaIds.join(", ")], - ])} -
${escapeHtml(JSON.stringify({
-      members: entry.members,
-      canonicalSchemaJson: parseJson(entry.canonicalSchemaJson),
-    }, null, 2))}
- `; -} - -function facts(rows: Array<[string, string]>): string { - return `
${rows.map(([name, value]) => `
${escapeHtml(name)}
${escapeHtml(value)}
`).join("")}
`; -} - -function metric(label: string, value: string): string { - return `
${escapeHtml(label)}${escapeHtml(value)}
`; -} - -function parseJson(value: string): unknown { - try { - return JSON.parse(value); - } catch { - return value; - } -} - -function escapeHtml(value: string): string { - return value - .replaceAll("&", "&") - .replaceAll("<", "<") - .replaceAll(">", ">") - .replaceAll('"', """) - .replaceAll("'", "'"); -} diff --git a/inspector/src/main.tsx b/inspector/src/main.tsx new file mode 100644 index 0000000..97801dd --- /dev/null +++ b/inspector/src/main.tsx @@ -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("#app"); +if (!app) { + throw new Error("Missing inspector app root."); +} + +createRoot(app).render(); + +function HuginApp() { + const [inspection, setInspection] = useState(); + const [selection, setSelection] = useState({ 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 { + 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): void { + event.preventDefault(); + setDragging(false); + const file = event.dataTransfer.files.item(0); + if (file) { + void inspectFile(file); + } + } + + return ( +
{ + event.preventDefault(); + setDragging(true); + }} + onDragLeave={(event) => { + if (event.target === event.currentTarget) { + setDragging(false); + } + }} + onDrop={handleDrop} + > +
+
+ +
+

Hugin

+

CultCache state inspection for .cc files.

+
+
+ + {errorMessage ?
{errorMessage}
: null} + {inspection ? : } +
+
+ {inspection + ? + : } +
+
+ ); +} + +function Summary({ inspection }: { inspection: CultCacheInspection }) { + return ( +
+ + + + +
+ ); +} + +function EmptySummary() { + return ( +
+ + + + +
+ ); +} + +function Metric({ label, value }: { label: string; value: string }) { + return ( +
+ {label} + {value} +
+ ); +} + +function NoFile() { + return ( +
+ +

No state loaded

+

Drag a CultCache .cc store into the window to inspect the snapshot header, schema catalog, records, and decoded MessagePack payloads.

+
+ ); +} + +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 ( +
+
+ +
+
+
+

Records

{inspection.records.length}
+
+ {inspection.records.length + ? inspection.records.map((entry, index) => ( + setSelection({ ...selection, record: index })} + /> + )) + :
No records
} +
+
+
+

Record Detail

{inspection.filePath}
+ {record ? :
No record selected
} +
+
+
+
+

Schema Catalog

{inspection.catalog.length}
+
+ {inspection.catalog.length + ? inspection.catalog.map((entry, index) => ( + setSelection({ ...selection, catalog: index })} + /> + )) + :
No schema catalog
} +
+
+
+

Catalog Entry

{catalogEntry?.schemaVersion ?? ""}
+ {catalogEntry ? :
No catalog entry selected
} +
+
+
+ ); +} + +function RecordButton({ + record, + index, + selected, + onSelect, +}: { + record: InspectedRecord; + index: number; + selected: boolean; + onSelect: () => void; +}) { + return ( + + ); +} + +function CatalogButton({ + entry, + index, + selected, + onSelect, +}: { + entry: InspectedCatalogEntry; + index: number; + selected: boolean; + onSelect: () => void; +}) { + return ( + + ); +} + +function RecordDetail({ record }: { record: InspectedRecord }) { + return ( + <> + + {record.payloadDecodeError ?
{record.payloadDecodeError}
: null} +
{JSON.stringify(record.payloadPreview, null, 2)}
+ + ); +} + +function CatalogDetail({ entry }: { entry: InspectedCatalogEntry }) { + return ( + <> + +
{JSON.stringify({
+        members: entry.members,
+        canonicalSchemaJson: parseJson(entry.canonicalSchemaJson),
+      }, null, 2)}
+ + ); +} + +function Facts({ rows }: { rows: Array<[string, string]> }) { + return ( +
+ {rows.map(([name, value]) => ( +
+
{name}
+
{value}
+
+ ))} +
+ ); +} + +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 { + return typeof value === "object" && value !== null && !Array.isArray(value); +} diff --git a/inspector/src/styles.css b/inspector/src/styles.css index f9884cc..49039a0 100644 --- a/inspector/src/styles.css +++ b/inspector/src/styles.css @@ -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; diff --git a/package-lock.json b/package-lock.json index 6284c31..d5477b6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/package.json b/package.json index 90947b1..d20e652 100644 --- a/package.json +++ b/package.json @@ -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" diff --git a/tsconfig.inspector.json b/tsconfig.inspector.json index 6c0c941..16332e4 100644 --- a/tsconfig.inspector.json +++ b/tsconfig.inspector.json @@ -2,6 +2,7 @@ "extends": "./tsconfig.json", "compilerOptions": { "lib": ["ES2022", "DOM", "DOM.Iterable"], + "jsx": "react-jsx", "noEmit": true, "rootDir": "." }, diff --git a/vite.inspector.config.ts b/vite.inspector.config.ts index 4e86f74..7bde39a 100644 --- a/vite.inspector.config.ts +++ b/vite.inspector.config.ts @@ -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