# JSON Crack Admin & Developer Guide

This guide is the architectural and operational reference for **JSON Crack** — the open-source data visualizer at [jsoncrack.com](https://jsoncrack.com). It is written for engineers self-hosting the web app, developers integrating the `jsoncrack-react` component, contributors working in the monorepo, and architects evaluating how the system is built. End-user task documentation lives in the companion **User Guide**.

JSON Crack is a **Turborepo + pnpm monorepo** that produces four artifacts from one shared rendering core: a Next.js web application (statically exported and served by nginx), a VS Code extension, a Chrome extension, and a published React component. There is **no backend service** — all parsing, conversion, querying, and rendering happen client-side.

---

## 1. Executive Summary

| Attribute | Value |
|-----------|-------|
| **Project** | JSON Crack — "the open-source JSON Editor" |
| **License** | Apache-2.0 (monorepo & web app); MIT (VS Code extension) |
| **Repository** | `AykutSarac/jsoncrack.com` |
| **Architecture** | Turborepo monorepo, 3 apps + 1 published package |
| **Primary deliverable** | `apps/www` — Next.js 16 app, **static export**, served by nginx on port 8080 |
| **Rendering core** | `packages/jsoncrack-react` — React + `reaflow` (ELK layout) canvas |
| **State management** | Zustand |
| **UI toolkit** | Mantine 8 + styled-components |
| **Runtime requirements** | Node.js ≥ 24, pnpm ≥ 10 (build-time only; output is static) |
| **Backend / database** | **None** — fully client-side; no server-side data storage |
| **Analytics** | Google Analytics via `nextjs-google-analytics` (hosted site only) |

**Operational profile.** Because the web app is a static export, "operating" it means serving a directory of HTML/JS/WASM assets behind any static web server. The provided Docker image bakes the build into `nginxinc/nginx-unprivileged`. There are no migrations, no secrets, no stateful services, and no horizontal-scaling concerns beyond standard static-asset CDN practices.

---

## 2. Architecture

### 2.1 Monorepo layout

```text
jsoncrack.com/                    (Turborepo root, pnpm workspaces)
├── apps/
│   ├── www/                      Next.js web app  → jsoncrack.com (PRIMARY deliverable)
│   ├── vscode/                   VS Code extension (jsoncrack-vscode v5.1.0)
│   └── chrome-extension/         Chrome MV3 extension (v0.1.2)
├── packages/
│   └── jsoncrack-react/          Published npm package (v1.0.3) — the canvas
├── turbo.json                    Task graph (build/dev/lint/test)
├── pnpm-workspace.yaml           Workspace globs
└── package.json                  Root scripts (turbo run ...)
```

Workspace dependencies are wired with pnpm's `workspace:*` protocol: all three apps depend on `jsoncrack-react` as a local workspace package, so the canvas is built once and consumed everywhere.

### 2.2 Data flow (web app)

```text
                 ┌─────────────────────────── apps/www (browser) ───────────────────────────┐
 user input  ──▶ │  TextEditor (Monaco)                                                       │
 (type/paste/    │      │ onChange                                                            │
  import/URL)    │      ▼                                                                      │
                 │  useFile (Zustand)  ──contentToJson()──▶  useJson (Zustand)                 │
                 │      │  (jsonAdapter: js-yaml / fast-xml-parser / json-2-csv / jsonc)       │
                 │      ▼ debounced 400ms                                                      │
                 │  GraphView ──▶ <JSONCrack json={...}>  (jsoncrack-react)                    │
                 │                     │ parseGraph()  (jsonc-parser → nodes/edges)            │
                 │                     ▼                                                        │
                 │                  reaflow <Canvas> + ELK layout  ──▶  SVG graph              │
                 │                     ▲                                                        │
                 │  useGraph (Zustand) ─┘  (viewport, direction, collapse, ref API)            │
                 └────────────────────────────────────────────────────────────────────────────┘
```

Key points:

- **`useFile`** owns the raw text and its format; it converts content to a normalized JSON object via `jsonAdapter`, then pushes a stringified, pretty-printed JSON into **`useJson`** on a 400 ms debounce.
- **`useJson`** holds the canonical JSON string that the graph renders.
- **`GraphView`** subscribes to `useJson` and renders the `JSONCrack` component from the workspace package, wiring viewport/collapse callbacks into **`useGraph`**.
- The actual graph construction (`parseGraph`) and layout (ELK via `reaflow`) live entirely inside `jsoncrack-react`.

### 2.3 The four products and what they share

```text
                     packages/jsoncrack-react  (the canvas: parser + reaflow + theme)
                          ▲            ▲              ▲
        ┌─────────────────┘            │              └────────────────────┐
   apps/www                       apps/vscode                       apps/chrome-extension
   Next.js editor: full          Webview (Vite+React) driven        Content script injects a
   toolset, stores, tools,       by an esbuild extension host       Raw|Graph toggle on JSON
   conversion, export, embed     via postMessage                    pages; mounts the canvas
```

Each product wraps the same `<JSONCrack>` component but supplies its own data source and chrome:

- **www** — rich editor with Monaco, Zustand stores, conversion, query tools, export, and embedding.
- **vscode** — a Vite-built React webview that receives the document text from the extension host over `postMessage`.
- **chrome-extension** — a content script that detects JSON pages and lazy-loads the canvas into an overlay.

---

## 3. Technology Stack & Requirements

### 3.1 Build/runtime requirements

| Tool | Version | Scope |
|------|---------|-------|
| Node.js | ≥ 24 (root `engines`) | Build only |
| pnpm | ≥ 10 (pinned `pnpm@10.20.0`) | Package management |
| Turbo | `2.8.x` | Task orchestration |
| VS Code | ≥ 1.100.0 | VS Code extension host |
| React (peer) | ≥ 18 | `jsoncrack-react` consumers |

The hosted web app is a **static export** — once built there is no Node runtime in production (nginx serves files).

### 3.2 Web app (`apps/www`) key dependencies

| Dependency | Role |
|------------|------|
| `next` 16 | Framework; configured for static export (`output: "export"`) |
| `react` / `react-dom` 19 | UI runtime |
| `@mantine/core`, `@mantine/hooks`, `@mantine/dropzone`, `@mantine/code-highlight` | UI components, hooks, file dropzone, code highlighting |
| `styled-components` | Theming (light/dark token objects) |
| `@monaco-editor/react` | The text editor pane |
| `jsoncrack-react` (workspace) | The graph canvas |
| `reaflow` | Graph rendering + ELK layout (transitive via the package) |
| `react-json-tree` | The Tree view |
| `allotment` | Resizable split panes |
| `zustand` | State stores |
| `jsonc-parser` | Tolerant JSON parsing |
| `js-yaml`, `fast-xml-parser`, `json-2-csv` | Format conversion |
| `jsonpath-plus` | JSON Path tool |
| `jq-web` | In-browser jq (WASM) |
| `json_typegen_wasm`, `gofmt.js` | Type generation (WASM) + Go formatting |
| `html-to-image` | PNG/JPEG/SVG export |
| `react-hot-toast` | Toasts |
| `nextjs-google-analytics`, `next-seo`, `next-sitemap` | Analytics, SEO, sitemap |

### 3.3 Extension stacks

- **VS Code:** Vite (webview bundle) + esbuild (extension host bundle) + React 19 + Mantine + Shiki (syntax highlight).
- **Chrome:** Vite library build (IIFE) emitting a single `content-script.js`; React 19 + `jsoncrack-react`.

---

## 4. Configuration Reference

JSON Crack has very little runtime configuration — by design, since it's a static client-side app. All configuration is build-time environment variables in `apps/www`.

### 4.1 Environment variables

| Variable | Defined in | Default | Effect |
|----------|-----------|---------|--------|
| `NEXT_PUBLIC_NODE_LIMIT` | `apps/www/.env` | `1200` (prod), `2000` (dev) | Maximum renderable graph nodes; exceeding it shows the "too large" overlay. Read into `SUPPORTED_LIMIT` (`constants/graph.ts`) and passed as `maxRenderableNodes`. |
| `NEXT_PUBLIC_DISABLE_EXTERNAL_MODE` | `apps/www/.env.development` | `false` (dev) | When `true`, suppresses the ToDiagram "Upgrade to Pro" link, the editor-choice modal, and the external-mode dialog. Set this for clean self-hosting. |
| `NEXT_TELEMETRY_DISABLED` | `apps/www/.env` | `1` | Disables Next.js build telemetry. |
| `ANALYZE` | build env | unset | When `true`, wraps the build with `@next/bundle-analyzer`. |
| `NODE_ENV` | docker-compose | `production` | Standard Node environment flag. |

> [!IMPORTANT]
> All public env vars are inlined at **build time** (the `NEXT_PUBLIC_` prefix). Changing the node limit requires rebuilding the static export — it cannot be changed on an already-built deployment.

> [!NOTE]
> `NEXT_PUBLIC_NODE_LIMIT` (a node *count*) and the ~300 KB upload guard in `FullscreenDropzone`/`useFile` (a *byte* size) are two independent limits. Raising one does not raise the other.

### 4.2 `next.config.js`

```javascript
const config = {
  output: "export",                       // static HTML export — no Node server in prod
  reactStrictMode: false,
  productionBrowserSourceMaps: true,
  compiler: { styledComponents: true },   // SSR-safe styled-components
  turbopack: { resolveAlias: { fs: { browser: "./shims/empty.ts" } } },
  webpack: (config, { isServer }) => {
    config.resolve.fallback = { fs: false };
    config.output.webassemblyModuleFilename = "static/wasm/[modulehash].wasm";
    config.experiments = { asyncWebAssembly: true, layers: true };  // for jq-web / json_typegen
    if (!isServer) config.output.environment = { ...config.output.environment, asyncFunction: true };
    return config;
  },
};
```

Notable choices: **static export** (`output: "export"`) means the app ships as a folder of files; **async WebAssembly** is enabled so `jq-web` and `json_typegen_wasm` load; and the `fs` module is stubbed for the browser.

### 4.3 Turborepo task graph (`turbo.json`)

| Task | `dependsOn` | Cached outputs |
|------|-------------|----------------|
| `build` | `^build` | `.next/**`, `dist/**`, `out/**`, `build/**` |
| `dev` | `^build` | not cached, persistent |
| `lint` | `^build` | none |
| `test` | `^build` | none |
| `analyze` | `^build` | `.next/**`, `out/**`, `build/**` |

The `^build` dependency ensures the `jsoncrack-react` package is built before any app that consumes it.

---

## 5. Deployment & Self-Hosting

### 5.1 Docker (recommended)

The Docker assets live in `apps/www`. The image is a multi-stage build that prunes the monorepo to just the `www` workspace, installs, builds the static export, and serves it from an unprivileged nginx.

```dockerfile
# Dockerfile (apps/www) — abridged
FROM node:24-alpine AS base
RUN apk add --no-cache libc6-compat && npm install -g pnpm@10.20.0

FROM base AS pruner
COPY . .
RUN pnpm dlx turbo@2.8.20 prune www --docker      # isolate the www workspace

FROM base AS installer
COPY --from=pruner /app/out/json/ .
RUN pnpm install --frozen-lockfile --ignore-scripts
COPY --from=pruner /app/out/full/ .
RUN pnpm turbo run build                          # produces apps/www/out (static)

FROM nginxinc/nginx-unprivileged:stable AS production
COPY --from=installer /app/apps/www/out /app
COPY apps/www/nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 8080
```

```yaml
# docker-compose.yml (apps/www)
services:
  jsoncrack:
    image: jsoncrack
    container_name: jsoncrack
    build: { context: ., dockerfile: Dockerfile }
    ports: ["8888:8080"]
    environment: [NODE_ENV=production]
```

**Build and run:**

```bash
cd apps/www
docker compose build
docker compose up
```

```text
[+] Running 1/1
 ✔ Container jsoncrack  Started
```

The app is then served at <http://localhost:8888> (container port 8080).

> [!TIP]
> The base image is `nginxinc/nginx-unprivileged`, which runs nginx as a non-root user and listens on **8080** (not 80). Map it to whatever host port you like; the compose file uses 8888.

### 5.2 nginx routing

Because Next.js static export emits per-route HTML files, nginx maps the SPA routes explicitly:

```nginx
server {
    listen 8080;
    root  /app;
    include /etc/nginx/mime.types;

    location /editor { try_files $uri /editor.html; }
    location /widget { try_files $uri /widget.html; }
    location /docs   { try_files $uri /docs.html; }
}
```

### 5.3 Deploy checklist

- [ ] Node ≥ 24 and pnpm ≥ 10 available on the build host (or use the Docker `node:24-alpine` base).
- [ ] Set `NEXT_PUBLIC_NODE_LIMIT` for your hardware before building.
- [ ] Set `NEXT_PUBLIC_DISABLE_EXTERNAL_MODE=true` to remove ToDiagram promos in a self-hosted/branded deployment.
- [ ] Build the static export (`pnpm build:www` or the Docker `installer` stage).
- [ ] Serve `apps/www/out` from any static web server (the provided nginx config handles `/editor`, `/widget`, `/docs`).
- [ ] Front with a CDN/reverse proxy for TLS and caching of the immutable `static/` assets.

### 5.4 Build outputs

| Command | Output |
|---------|--------|
| `pnpm build:www` | `apps/www/out/` (static HTML/JS/WASM) |
| `pnpm build:vscode` | `apps/vscode/build/` (webview + `extension.js`) |
| `pnpm build:chrome` | `apps/chrome-extension/dist/` (`content-script.js` + manifest) |
| `pnpm --filter jsoncrack-react build` | `packages/jsoncrack-react/dist/` (ESM + `.d.ts` + CSS) |

---

## 6. Web App Internals (`apps/www`)

### 6.1 Pages (routes)

| Route | File | Purpose |
|-------|------|---------|
| `/` | `pages/index.tsx` | Marketing landing page (hero, features, FAQ); fetches GitHub star count at build via `getStaticProps` |
| `/editor` | `pages/editor.tsx` | The full editor (split pane, toolbar, modals) |
| `/widget` | `pages/widget.tsx` | Embeddable graph-only view driven by `postMessage` and `?json=` |
| `/docs` | `pages/docs.tsx` | Embedding documentation (iframe / postMessage / React) |
| `/legal/privacy`, `/legal/terms` | `pages/legal/*` | Legal pages rendered from `data/*.json` |
| `/404`, `/_error` | `pages/404.tsx`, `pages/_error.tsx` | Error pages |

### 6.2 State stores (Zustand)

| Store | File | Responsibility |
|-------|------|----------------|
| `useFile` | `store/useFile.ts` | Raw text, format, validation error, JSON Schema; converts content↔JSON via `jsonAdapter`; session-storage persistence (<80 KB, non-iframe); URL fetch; debounced push to `useJson` |
| `useJson` | `store/useJson.ts` | The canonical JSON string + loading flag that the graph consumes |
| `useConfig` | `store/useConfig.ts` | UI prefs — `darkmodeEnabled`, `liveTransformEnabled`, `gesturesEnabled`, `rulersEnabled`; **persisted to `localStorage` key `config`** |
| `useModal` | `store/useModal.ts` | Open/closed state for each named modal (Import, Download, JQ, JPath, Schema, Type, NodeModal) |
| `useGraph` | `features/editor/views/GraphView/stores/useGraph.ts` | Viewport, layout direction, fullscreen, selected node, collapsed count; proxies imperative calls (`zoomIn`, `centerView`, `collapseAll`, …) to the `JSONCrackRef` |

### 6.3 Format conversion (`lib/utils/jsonAdapter.ts`)

A single `contentToJson(value, format)` / `jsonToContent(json, format)` pair powers all conversion. Parsers are **dynamically imported** so each format's library only loads when used:

| Format | Parse | Serialize |
|--------|-------|-----------|
| JSON | `jsonc-parser` (tolerant) | `JSON.stringify` |
| YAML | `js-yaml` `load` | `js-yaml` `dump` |
| XML | `fast-xml-parser` `XMLParser` (`$` attr prefix) | `XMLBuilder` |
| CSV | `json-2-csv` `csv2json` | `json2csv` |

Switching the bottom-bar format selector calls `useFile.setFormat`, which round-trips the content through the previous format's parser and the new format's serializer.

### 6.4 The graph wiring (`GraphView`)

`features/editor/views/GraphView/index.tsx` renders `<JSONCrack>` from the package and binds:

- `json` ← `useJson.json`
- `theme` ← `useConfig.darkmodeEnabled`
- `layoutDirection` ← `useGraph.direction`
- `showGrid` ← `useConfig.rulersEnabled`
- `trackpadZoom` ← `useConfig.gesturesEnabled`
- `maxRenderableNodes` ← `SUPPORTED_LIMIT` (or 1500 fallback)
- `onNodeClick` → opens `NodeModal` and stores the selected node
- `renderNodeLimitExceeded` → the `NotSupported` overlay

### 6.5 Tools (the `Tools` menu)

| Tool | Implementation |
|------|----------------|
| JSON Query (jq) | `hooks/useJsonQuery.ts` → dynamic `import("jq-web")`, `jq.promised.json()` |
| JSON Path | `JPathModal` → `jsonpath-plus` `JSONPath({ path, json })` |
| JSON Schema | `SchemaModal` → sets `useFile.jsonSchema`; Monaco validates against it (`TextEditor` registers it via `jsonDefaults.setDiagnosticsOptions`) |
| Generate Type | `TypeModal` → dynamic `import("json_typegen_wasm")` for TS/Kotlin/Rust/JSON-Schema; `lib/utils/json2go.js` + `gofmt.js` for Go |

### 6.6 Search (`useFocusNode`)

`hooks/useFocusNode.ts` runs a debounced (300 ms) DOM query for `span[data-key*='<term>' i]`, highlights matches via CSS classes (`lib/utils/search.ts`), and flies the camera to the active match using the viewport's `centerFitElementIntoView`. It exposes `next`/`prev`/`clear` and match counters consumed by `SearchInput`.

---

## 7. `jsoncrack-react` — Full API Reference

The published package (`packages/jsoncrack-react`, v1.0.3) is the reusable canvas. Its public surface is its barrel export:

```ts
// packages/jsoncrack-react/src/index.ts
export { JSONCrack } from "./JSONCrackComponent";
export { parseGraph } from "./parser";
export type { JSONCrackProps, JSONCrackRef } from "./JSONCrackComponent";
export type { ParseGraphResult } from "./parser";
export type { CanvasThemeMode, EdgeData, GraphData, LayoutDirection, NodeData, NodeRow } from "./types";
```

### 7.1 `<JSONCrack>` props

| Prop | Type | Default | Description |
|------|------|---------|-------------|
| `json` | `string \| object` | — (required) | Data to visualize. Objects are stringified (memoized per reference). |
| `theme` | `"light" \| "dark"` | `"dark"` | Canvas & node color theme. |
| `layoutDirection` | `"LEFT" \| "RIGHT" \| "DOWN" \| "UP"` | `"RIGHT"` | ELK node placement direction. |
| `showControls` | `boolean` | `true` | Render the built-in zoom/focus control overlay. |
| `showGrid` | `boolean` | `true` | Draw the background grid. |
| `trackpadZoom` | `boolean` | `false` | Treat two-finger trackpad gestures like touch (pinch-zoom). |
| `centerOnLayout` | `boolean` | `true` | Auto fit-to-center after each ELK layout pass. |
| `maxRenderableNodes` | `number` | `1500` | Hard cap; exceeding it triggers the limit overlay. |
| `className` | `string` | — | Extra class on the canvas wrapper. |
| `style` | `CSSProperties` | — | Extra inline style merged onto the wrapper. |
| `onNodeClick` | `(node: NodeData) => void` | — | Fired when a node is clicked. |
| `onParse` | `(graph: GraphData) => void` | — | Fired with `nodes`/`edges` after each successful parse. |
| `onParseError` | `(error: Error) => void` | — | Fired on any parse/graph-construction error. |
| `onViewportCreate` | `(viewPort: ViewPort) => void` | — | Fired once the internal viewport is created. |
| `renderNodeLimitExceeded` | `(count, max) => ReactNode` | — | Custom overlay when the node limit is exceeded. |
| `collapsedPaths` | `string[]` | — | **Controlled** collapsed-path set (serialized keys). Requires `onToggleCollapse`. |
| `onToggleCollapse` | `(path: JSONPath) => void` | — | Mutator for controlled collapse mode. |
| `onCollapseChange` | `(paths: string[]) => void` | — | Observe collapsed-path set in **uncontrolled** mode. |

### 7.2 `JSONCrackRef` (imperative handle)

Obtain via a `ref`; methods control the viewport and collapse state:

| Method | Description |
|--------|-------------|
| `zoomIn()` / `zoomOut()` | Step zoom while keeping center fixed |
| `setZoom(factor)` | Set absolute zoom at current center |
| `centerView()` | Fit the whole graph in the viewport |
| `focusFirstNode()` | Center/zoom on the root node |
| `toggleCollapse(path)` | Toggle one JSON path (uncontrolled mode) |
| `collapseAll()` / `expandAll()` | Collapse first-level containers / clear collapse |
| `getCollapsedPaths()` | Current collapsed-paths snapshot |

### 7.3 `parseGraph(json: string): ParseGraphResult`

The pure parser (also exported standalone). Uses `jsonc-parser`'s `parseTree` and walks it into `{ nodes, edges, errors }`. Arrays and objects become container nodes; primitive rows are grouped into their parent node's `text` rows. Node sizes are computed via `calculateNodeSize`. Returns collected `ParseError[]` rather than throwing.

### 7.4 Core types

```ts
type LayoutDirection = "LEFT" | "RIGHT" | "DOWN" | "UP";
type CanvasThemeMode = "light" | "dark";

interface NodeRow { key: string | null; value: string|number|null|boolean; type: Node["type"]; childrenCount?: number; to?: string[]; }
interface NodeData { id: string; text: NodeRow[]; width: number; height: number; path?: JSONPath; parentKey?: string; parentType?: string; }
interface EdgeData { id: string; from: string; to: string; text: string | null; }
interface GraphData { nodes: NodeData[]; edges: EdgeData[]; }
```

### 7.5 Theming

`theme.ts` exports a `themes` record keyed by `"light"`/`"dark"`. `canvasHelpers.buildCanvasStyle` projects those tokens onto CSS custom properties (`--node-key`, `--node-value`, `--node-integer`, etc.) on the canvas wrapper, so theming is pure CSS variables — no re-render of nodes is needed to recolor.

### 7.6 Rendering & layout internals

- The canvas uses **`reaflow`'s `<Canvas>`** with a fixed `layoutOptions` ELK config (`NETWORK_SIMPLEX` node placement, edge-length compaction). It runs in `readonly`, non-pannable/non-zoomable mode; panning/zooming is delegated to **`react-zoomable-ui`'s `<Space>`**.
- **Collapse** is computed in JS: collapsed paths are serialized JSON arrays; nodes whose path is prefixed by a collapsed path are filtered out before being handed to reaflow.
- **Fit-to-view** (`canvasHelpers.fitGraphToViewPort`) measures the painted graph via a four-step cascade (union of leaf client rects → content-group rect → `getBBox` through CTM → ELK layout box) and centers the viewport on it.
- After a collapse toggle, a `requestAnimationFrame` loop re-pins the clicked button under the cursor once the layout stabilizes, avoiding a visual jump.

---

## 8. VS Code Extension Internals (`apps/vscode`)

The extension has two halves: an **extension host** (Node, bundled by esbuild → `build/extension.js`) and a **webview** (React app bundled by Vite → `build/webview/`).

### 8.1 Commands & contribution points

Declared in `apps/vscode/package.json`:

| Command ID | Title | Trigger |
|------------|-------|---------|
| `jsoncrack-vscode.start` | Enable JSON Crack visualization | Editor title-bar icon (shown for `.json` / `editorLangId == json`) |
| `jsoncrack-vscode.start.selected` | Open with JSON Crack | Editor context menu, when there's a selection |
| `jsoncrack-vscode.start.specific` | Enable JSON Crack visualization for specific file | Programmatic (hidden from palette) |

Activation event: `workspaceContains:**/*.{json}`.

### 8.2 Host ↔ webview messaging

`ext-src/extension.ts` registers the commands; each creates a webview panel (`ext-src/webview.ts`) beside the active editor and streams the document text in:

```text
extension host                         webview (App.tsx)
  createWebviewPanel() ───────────────▶ render <JSONCrack json={...}>
  onDidReceiveMessage("ready") ◀──────── postMessage("ready")
  postMessage({ json }) ──────────────▶ setJson(...)  (re-renders graph)
  onDidChangeTextDocument ────────────▶ postMessage({ json }) (live updates)
```

`createWebviewForSelectedText` sends only the selected range and keeps it in sync on edits. The webview reads the editor theme from `document.body[data-vscode-theme-kind]` and forces the matching Mantine color scheme.

### 8.3 Webview security (CSP)

`webview.ts` builds the panel HTML with a strict Content-Security-Policy and a per-load nonce:

```text
default-src 'self' <cspSource> blob:
connect-src <cspSource> blob:
script-src 'unsafe-eval' 'unsafe-inline' <cspSource>
style-src  <cspSource> 'unsafe-inline'
worker-src <cspSource> blob: data:
```

`enableScripts: true` and `retainContextWhenHidden: true` are set; resources are restricted to the `build/webview` and `assets` directories. `'unsafe-eval'` is required for the ELK/WASM layout path.

---

## 9. Chrome Extension Internals (`apps/chrome-extension`)

A Manifest v3 extension (`v0.1.2`) consisting of a single content script.

### 9.1 Manifest

```json
{
  "manifest_version": 3,
  "name": "JSON Crack",
  "content_scripts": [
    { "matches": ["<all_urls>"], "js": ["content-script.js"], "run_at": "document_idle" }
  ]
}
```

It declares no `host_permissions` beyond the content-script match and no background service worker.

### 9.2 Behavior

`src/content-script.tsx`:

1. Runs only in the top frame, only when `document.contentType` includes `json` and the body parses as JSON.
2. Injects a floating **Raw | Graph** segmented control plus a hidden overlay container, and inlines both the package stylesheet and a local stylesheet.
3. On **Graph**, lazily `import("jsoncrack-react")` and mounts `<JSONCrack>` into the overlay; on **Raw** (or <kbd>Esc</kbd>) it unmounts.
4. Clicking a node opens a self-contained modal (content + JSON path, each with copy).
5. Over-limit graphs render a "too large" upgrade card.

### 9.3 CSP/Worker workaround

Many JSON responses are served under a strict `default-src 'none'` CSP, which blocks ELK's attempt to spawn a Web Worker. The script **temporarily shadows `globalThis.Worker = undefined`** during the dynamic import and initial layout so ELK falls back to its synchronous path, then restores `Worker` so the host page is unaffected. The Vite build also injects a banner defining `Worker = undefined` and a minimal `process.env`.

---

## 10. Security Analysis

| Area | Assessment |
|------|------------|
| **Data residency** | All parsing/conversion/rendering is client-side. The web app is a static export with no backend; documents are never uploaded. Matches the public privacy claims. |
| **Persistence** | Web app: editor content cached in `sessionStorage` (only <80 KB, non-iframe) and UI prefs in `localStorage` (`config`). No cookies beyond a 1-day `editor_choice_shown` marker. Chrome extension: no persistent storage of page JSON. |
| **Network egress** | Web app: outbound requests only when the user supplies a URL (`?json=` / Import) or on the landing page's build-time GitHub star fetch; plus Google Analytics on the hosted site. Monaco is loaded from `unpkg.com` CDN. Extensions: VS Code is fully offline; Chrome makes no automatic transmission of page JSON. |
| **`dangerouslySetInnerHTML`** | `TreeView/TextRenderer.tsx` linkifies URL-like strings by building `<a>` markup and injecting it via `dangerouslySetInnerHTML`. Inputs are user-supplied data values. This is a deliberate feature but is the single most security-sensitive code path; downstream forks should sanitize if rendering untrusted data in a shared context. |
| **Webview CSP** | The VS Code webview uses a strict CSP with a per-load nonce and locked-down resource roots, but allows `'unsafe-eval'`/`'unsafe-inline'` (required for the WASM layout engine). |
| **Secrets** | None. There are no API keys, auth tokens, or server credentials anywhere in the deployable. |

> [!CAUTION]
> If you fork JSON Crack to visualize **untrusted, attacker-controlled** data inside a privileged page, review the `TextRenderer` linkify path — it interpolates string values into HTML. The upstream use case (a sandboxed editor showing the user's own data) makes this low-risk, but the calculus changes in an embedded/multi-tenant context.

---

## 11. Performance Tuning

| Lever | Where | Effect |
|-------|-------|--------|
| `maxRenderableNodes` / `NEXT_PUBLIC_NODE_LIMIT` | prop / env | Caps graph size before the layout engine is invoked; the primary guard against jank. |
| Live Transform off | bottom bar (`useConfig.liveTransformEnabled`) | Stops per-keystroke re-parse/re-layout for large documents. |
| Debounced JSON push | `useFile` (400 ms) | Coalesces rapid edits before re-rendering the graph. |
| Memoized parse & style | `jsoncrack-react` | `toJsonText` caches stringified objects (WeakMap); class/style memoized so reaflow doesn't see new objects each render. |
| Collapse before fit | `JSONCrackComponent` | Hidden nodes are filtered out before reaflow/ELK runs, reducing layout cost on large graphs. |

> [!TIP]
> ELK layout cost grows superlinearly with node count. For consistently large data, lower `NEXT_PUBLIC_NODE_LIMIT` rather than raising it — the overlay is a better UX than a multi-second freeze.

---

## 12. Operations

### 12.1 Common commands (from repo root)

```bash
# Web app
pnpm dev:www          # next dev on :3000
pnpm build:www        # static export → apps/www/out

# VS Code extension
pnpm dev:vscode       # watch build
pnpm build:vscode

# Chrome extension
pnpm dev:chrome       # vite build --watch → apps/chrome-extension/dist
pnpm build:chrome

# All workspaces
pnpm dev | pnpm build | pnpm lint | pnpm test
```

### 12.2 Loading the Chrome extension for testing

```text
1. pnpm build:chrome
2. chrome://extensions  →  enable Developer mode
3. Load unpacked  →  select apps/chrome-extension/dist
```

### 12.3 Debugging the VS Code extension

1. Open the **monorepo root** in VS Code.
2. Press <kbd>F5</kbd> and pick **Run VSCode Extension (apps/vscode)**.
3. In the Extension Development Host, open a `.json` file and run **JSON Crack: Enable JSON Crack visualization**. Reload the host with <kbd>Ctrl/⌘</kbd>+<kbd>R</kbd> after changes.

### 12.4 Tests

Only `jsoncrack-react` ships unit tests (Vitest): `parser.test.ts`, `canvasHelpers.test.ts`, `CollapseContext.test.ts`. Run with `pnpm --filter jsoncrack-react test`. The apps have no automated test suite — contributions are verified manually with screenshots/recordings per the contributing guide.

---

## 13. Code Patterns & Conventions

- **Workspace package as shared core.** The graph lives in one published package consumed by all apps via `workspace:*`; never duplicate rendering logic into an app.
- **Zustand slices by concern.** Separate stores for file, json, config, modal, and graph rather than one global store; `useConfig` is the only persisted one.
- **Dynamic imports for heavy/optional code.** `jq-web`, `json_typegen_wasm`, `js-yaml`, `fast-xml-parser`, `json-2-csv`, and the editor/modals are all `import()`-ed on demand to keep the initial bundle small.
- **Controlled vs. uncontrolled collapse.** `jsoncrack-react` supports both; provide `collapsedPaths` + `onToggleCollapse` to control it, or omit them to let the component manage state internally.
- **Theme via CSS custom properties.** Recoloring is a style change, not a React re-render.
- **Code style:** Google TypeScript Style Guide; ESLint + Prettier with `@trivago/prettier-plugin-sort-imports`. Lint runs `tsc` first.

---

## 14. Known Issues & Potential Improvements

Drawn from the community issue tracker and code observations:

| Theme | Detail | Source |
|-------|--------|--------|
| **Large-file handling** | Big JSON (e.g. >2,500 lines) hits the node limit; requests for partial/streamed rendering recur. | #513, #234, #337 |
| **Numeric precision** | int64+ values lose precision through JS parsing. | #462 |
| **Paste truncation** | Pasting can truncate JSON at certain positions. | #463 |
| **Blank-line handling** | Confusion over whitespace in otherwise-valid JSON. | #509 |
| **SVG→PDF export** | SVG export can require external resources, breaking PDF conversion; PNG is the reliable path. | #421 |
| **Offline editor** | Requests for a fully offline/CSP-friendly self-hosted editor and a published Docker image. | #368, #226, #459, #434 |
| **Format support** | Requests for JSON5, `.xlsx` import, encoding selection. | #331, #414, #464 |
| **Feature requests** | Copy-JSON in tools menu, jump-to-code from diagram, table view of nested JSON, per-node colors. | #576, #570, #343, #417 |
| **Monaco from CDN** | `TextEditor` loads Monaco from `unpkg.com` — a hard external dependency that breaks in air-gapped/offline deployments. | code |

> [!NOTE]
> Several "offline" requests are partially addressed by the documented Docker self-host path, but the hosted editor's Monaco CDN dependency (`unpkg.com`) and Google Analytics remain external touchpoints to remove for a truly air-gapped build.

---

## 15. Contributing

The project uses an **issue-first workflow**: open or find an issue before coding, get it approved, then submit a PR linked with `Closes #123`. PRs must include a clear description and visual evidence (screenshot or recording), pass local `pnpm dev` verification, and follow the Google TypeScript Style Guide. The performance guideline is explicit: avoid unnecessary re-renders. See `CONTRIBUTING.md` and `CODE_OF_CONDUCT.md` in the repo.

---

## 16. Quick Reference

### Ports & paths

| Item | Value |
|------|-------|
| nginx listen port (container) | `8080` |
| docker-compose host port | `8888` |
| Web app static output | `apps/www/out` |
| Dev server | `http://localhost:3000` |

### Environment variables

| Var | Default |
|-----|---------|
| `NEXT_PUBLIC_NODE_LIMIT` | 1200 (prod) / 2000 (dev) |
| `NEXT_PUBLIC_DISABLE_EXTERNAL_MODE` | false |
| `NEXT_TELEMETRY_DISABLED` | 1 |
| `ANALYZE` | unset |

### Package versions

| Package | Version |
|---------|---------|
| `jsoncrack-vscode` | 5.1.0 |
| `jsoncrack-react` | 1.0.3 |
| chrome-extension | 0.1.2 |
| next | 16 |
| react | 19 |

### Workspace map

| Path | Artifact |
|------|----------|
| `apps/www` | Next.js static site |
| `apps/vscode` | VS Code extension |
| `apps/chrome-extension` | Chrome MV3 extension |
| `packages/jsoncrack-react` | npm package (the canvas) |

---

_Generated June 2026 from AykutSarac/jsoncrack.com at f67c540. AI-generated documentation. The library is checked by sampling (three repositories so far); this guide was not individually reviewed. Verify against the project before relying on it. Unofficial; not affiliated with or reviewed by the project._
