Imported from Ovyerus/prismaliser (
AGENTS.md). Install upstream withnpx skills add Ovyerus/prismaliser. Copyright stays with the author.
Repository Guidelines
Project Overview
Prismaliser is a fully client-side webapp (Vite SPA) that visualises
Prisma schemas as ER diagrams. Users paste a schema into a
Monaco editor; the app parses it entirely in the browser (via Prisma's
schema WASM module) and renders models, enums, and relations (1-1, 1-n, m-n) as
an interactive React Flow graph. The build output is plain static files
(dist/) — no server component at all. It is self-hostable (Docker image
published to GHCR); a hosted version lives at
prismaliser.app.
Architecture & Data Flow
The app is a single page (src/App.tsx) — editor on one side, graph on the
other.
Monaco editor (EditorView)
→ schema text, debounced 1s (react-use useDebounce)
→ util/prisma.ts (getDMMF / formatSchema — client-side WASM wrappers)
→ @prisma/prisma-schema-wasm (patched; instantiated from
/prisma_schema_build_bg.wasm fetched out of public/)
→ DMMF.Datamodel (or PrismaSchemaError → Monaco markers)
→ components/FlowView.tsx
→ util/prismaToFlow.ts (DMMF → React Flow nodes/edges)
→ util/layout.ts (elkjs layered layout, DOWN)
→ ReactFlow canvas (ModelNode / EnumNode / RelationEdge)
Key points:
- Parsing/formatting is client-side WASM.
@prisma/prisma-schema-wasmis the Rustprisma-fmtengine compiled to WASM — pure JS+WASM, no native code. The published package is a Node-only build (loads the binary viafs/__dirname), so the repo carries a Yarn patch (.yarn/patches/, wired via thepatch:protocol inpackage.json) that replaces the self-instantiating fs tail with an exported__init(wasmBytes). - The wasm binary is vendored at
public/prisma_schema_build_bg.wasm(2.9 MB) and fetched once, eagerly, bysrc/util/prisma.ts. ⚠️ It is a copy ofnode_modules/@prisma/prisma-schema-wasm/src/prisma_schema_build_bg.wasm— re-copy it when updating the pinned package version, and regenerate the Yarn patch. - Error contract: the wasm throws
Errors whose message is a JSON{ error_code, message }blob.util/prisma.tsunwraps it and throwsPrismaSchemaError(carryingSchemaError[]for Monaco markers) when the message containserror:diagnostics, otherwise a plainError.parseDMMFError(util/index.ts) does the line-number extraction and works unchanged. - Monaco is bundled, not CDN-loaded (
src/monaco.ts):loader.config({ monaco })with a Vite?workerimport, so the app works fully offline. Consequentlywindow.monacodoes not exist — drive the editor in tests/tools via share links (?code=) or DOM, not the monaco global. - No global state store (no zustand/redux/context). State is local
useStateinsrc/App.tsxandcomponents/FlowView.tsx; the schema persists viauseLocalStorage("prismaliser.text")fromreact-use. - m-n relations create implicit virtual tables in
prismaToFlow.ts(IDs like_${relationName}, columnsA/B) — the graph intentionally differs from the raw schema. - Layout is not automatic. On
dmmfchange, nodes regenerate at{0,0}(or previous positions) until the user clicks "Disperse nodes", which runs elkjs. - Handle ID coupling:
ModelNode/RelationEdgemust agree with the handle ID strings generated inprismaToFlow.ts(relationEdgeSourceHandleId,relationEdgeTargetHandleId,enumEdgeTargetHandleId). Change both sides together. - Share links: schema is URL-safe-base64-encoded into
?code=(toUrlSafeB64/fromUrlSafeB64inutil/index.ts). There is no client-side router; query params only, so static hosting needs no rewrite rules.
Key Directories
| Path | Purpose |
|---|---|
src/ |
All application code. main.tsx (entry), App.tsx (the whole page), monaco.ts (editor setup). |
src/components/ |
React components: FlowView, ModelNode, EnumNode, RelationEdge, EditorView, Layout, … |
src/util/ |
Core logic: prisma.ts (client-side parse/format), prismaToFlow.ts (DMMF→graph), layout.ts (elkjs), prisma-language.ts (Monarch grammar), types.ts (shared contracts), index.ts (helpers). Tests colocated as *.test.ts. |
src/assets/ |
style/global.css — Tailwind entrypoint + custom .button/.focusable utilities. |
public/ |
Static files copied verbatim into dist/: images + the vendored prisma_schema_build_bg.wasm. |
.yarn/patches/ |
Yarn patch making @prisma/prisma-schema-wasm browser-loadable. |
Development Commands
yarn dev # Vite dev server
yarn build # tsc --noEmit && vite build → dist/
yarn start # vite preview (any static file server works too)
yarn test # vitest run
yarn lint # eslint --ext ts,tsx .
yarn lint:fix # eslint --ext ts,tsx --fix .
Note: yarn install may refuse lockfile changes when a CI env var is set
(immutable installs) — override with
YARN_ENABLE_IMMUTABLE_INSTALLS=false yarn install when changing dependencies
locally.
Code Conventions & Common Patterns
- TypeScript strict:
strict: true,noUncheckedIndexedAccess: true,isolatedModules: true,moduleResolution: "bundler",jsx: "react-jsx". - Imports: internal modules use the
~/*path alias →./src/*(tsconfigpaths+ Viteresolve.alias), e.g.import FlowView from "~/components/FlowView". Do not use relative../imports for internal modules.import/orderis enforced with newlines between groups;import typegroups go last, then~icons/*imports in their own trailing group. - Components: arrow-function components only
(
react/function-component-definition), PascalCase files, default export, separate exported*Propsinterface. JSX props must be sorted (react/jsx-sort-props). - Types: prefer
interfaceovertype(@typescript-eslint/consistent-type-definitions).switchstatements must be exhaustive.readonlyparameter properties without an explicitpublicmodifier. - Styling: Tailwind v4 (CSS-first, via
@tailwindcss/vite— no PostCSS, no config file). Utility classes inline + plain CSS modules per component (Node.module.css,Layout.module.css,FlowView.module.css). Global styles insrc/assets/style/global.css:@import "tailwindcss"+@themetokens +@utilitycustom utilities. Content is auto-detected (no globs). Sass was removed — do not reintroduce it; v4 is the only preprocessor. v4 gotchas already handled: borders need explicit colors (no gray-200 default),outline-hiddennotoutline-none,rounded-smnotrounded, important in@applyis a per-utility!suffix. CSS modules using@apply/theme vars need@reference "tailwindcss";at the top. - Icons:
unplugin-icons— import as components from virtual~icons/<set>/<kebab-name>modules (e.g.import GithubIcon from "~icons/simple-icons/github"), backed by@iconify-json/simple-icons+@iconify-json/ggdata packages. Fully offline/build-time; never the runtime Iconify API, never the deprecated@iconify/icons-*per-icon packages. Always pass explicitwidthANDheight— the generated components default the other dimension to1emand will squash the icon otherwise. - Error handling: schema errors surface as thrown
PrismaSchemaErrorfromutil/prisma.ts→ caught inApp.tsx→ Monaco markers. Unexpected errors areconsole.errored. No React error boundaries. - Async:
async/awaitthroughout; wasm calls themselves are synchronous after the one-time async init.@typescript-eslint/no-misused-promisesis disabled in.eslintrc.js. - Formatting: Prettier defaults (80 col, 2 spaces, semicolons, double
quotes, trailing commas) with only
proseWrap: "always"andhtmlWhitespaceSensitivity: "ignore"overridden in.prettierrc. Formatting violations fail lint (prettier/prettier: error). EditorConfig: 2-space indent, LF, final newline.
Important Files
| File | Role |
|---|---|
index.html |
Vite entry HTML; all meta/OG/Twitter tags live here. |
src/main.tsx |
App entrypoint: font/style imports, Umami injection, createRoot. |
src/monaco.ts |
Bundles Monaco locally (loader.config + ?worker); full monaco-editor import on 0.56. ⚠️ 0.56 removed the edcore.main aggregate, and cherry-picking contrib modules by hand silently breaks features (the context menu stops working) — do not attempt the trim without thorough browser verification. |
src/App.tsx |
Main page; owns schema state, debounced parse, error markers, share links. |
src/util/prisma.ts |
Client-side getDMMF/formatSchema wrappers; wasm init + error unwrapping. |
src/util/prismaToFlow.ts |
DMMF → React Flow nodes/edges; relation typing; implicit m-n virtual tables. |
src/util/layout.ts |
elkjs layout; node sizes are text-length heuristics (CHAR_WIDTH = 10, etc.). elkjs (~1.6MB) is a dynamic import — only fetched on first "Disperse nodes" click. |
src/util/types.ts |
Shared contracts: ModelNodeData, EnumNodeData, RelationType, SchemaError. |
src/util/prisma-language.ts |
Monaco Monarch grammar + language config for Prisma. |
public/prisma_schema_build_bg.wasm |
Vendored Prisma parser/formatter wasm binary (keep in sync with pinned package). |
.yarn/patches/@prisma-prisma-schema-wasm-*.patch |
Replaces the glue's fs-based self-instantiation with exported __init(bytes). |
vite.config.ts |
React plugin, Tailwind v4 plugin, unplugin-icons, ~ → /src alias, Vitest config. |
Dockerfile |
Multi-stage: node:24-alpine builds dist/, caddy:alpine serves it from /srv via Caddyfile. |
Caddyfile |
Static file server on :80. |
Runtime/Tooling Preferences
- Node 24 is the target (Vite 7+ requires ≥20.19; the repo standardises on
24):
flake.nixdev shell (nodejs_24),.tool-versions(24.18.0), Dockerfile builder, and@types/node@^24all agree. - Yarn 4.12.0 (
packageManagerfield, vendored at.yarn/releases/yarn-4.12.0.cjs) withnodeLinker: node-modules(classicnode_modules, not PnP despite.gitignoreentries). Useyarn install --immutablein CI contexts. Bun is not used. - Yarn patches:
@prisma/prisma-schema-wasmis consumed through a committedpatch:resolution. To modify the patch:yarn patch <pkg>, edit,yarn patch-commit -s <dir>. The Dockerfile copies.yarn/patchesbeforeyarn install --immutable— this ordering is load-bearing. - Nix:
flake.nixprovides the dev shell;.envrcrunsuse flake. CI builds insidenix develop. If your shell node is older than 20.19, prefix commands withnix develop --command. - Key dependency constraints: React 19 with matching
@types/react*(v19 types: useReact.JSX.Element, the globalJSXnamespace is gone),reactflow@11(legacy packages — not@xyflow/react),monaco-editor@0.56full bundle (seesrc/monaco.tsnote), rambda 11 (curried-only API + weak object typing — onlycount/groupBy/pickare used; prefer plain JS for the rest),elkjslazy-loaded, Tailwind 4 via@tailwindcss/vite,@prisma/prisma-schema-wasmpinned exact. - Env vars (both optional, analytics only; baked at build time):
VITE_UMAMI_SITE,VITE_UMAMI_HOST— Umami script loads directly from the analytics host (the old Next.js same-origin proxy + IP-munging was dropped with the SPA migration). Dockerfile accepts them asUMAMI_SITE/UMAMI_HOSTbuild args. No.envfiles are committed. - Version control: repo uses Jujutsu (
.jj/) colocated with git.
Testing & QA
- Vitest (
yarn test), Node environment, tests colocated assrc/**/*.test.ts. The suite coversutil/prisma.ts(real-wasm integration: parse valid/invalid, format),util/prismaToFlow.ts(relation typing, m-n virtual tables, enum edges, handle IDs — fixtures are authentic DMMF produced by the wasm),util/layout.ts(sizing heuristics), andutil/index.ts(error parsing, base64). - Wasm in tests:
typeof window === "undefined"in Vitest, soutil/prisma.ts's auto-init is inert — test files instantiate manually: import~/util/prismafirst (installs the panic-registry polyfill), then call__initon@prisma/prisma-schema-wasmwith the binary read fromnode_modules. Followsrc/util/prisma.test.ts. - Prisma 7 schema gotcha:
urlinsidedatasourceblocks is rejected (P1012) — don't put it in fixtures. - Lint:
yarn lint— ESLint viaeslint-config-clarity(clarity/react-typescript):import,prettier,@typescript-eslint,react,react-hooks,jsx-a11y. - Build:
yarn buildtype-checks (tsc --noEmit) before bundling.
CI (.github/workflows/push.yaml, "Test and build", on push/PR) runs via Nix:
nix flake check → yarn install --immutable → yarn lint → yarn test →
yarn build, then builds and pushes Docker images (linux/amd64,
linux/arm64) on master/dev. Verify changes locally with
yarn lint && yarn test && yarn build before pushing — that is the full QA bar.
For behaviour changes, smoke-test the production build
(yarn build && yarn start) in a browser.
