Uses pnpm (install via corepack enable pnpm). Node >= 24 required.
pnpm dev— dev server on http://localhost:3000pnpm build— production build (static site, output todocs/)pnpm check— svelte-check type checkingpnpm lint/pnpm lint:fix— prettier + eslintpnpm test:unit— vitest in watch mode;pnpm vitest runfor a single pass- Single unit test:
pnpm vitest run src/lib/util/serde.test.ts(or-t "test name") pnpm test:e2e— Playwright tests intests/; auto-starts the dev server on port 3000 (reuses one already running).pnpm test:e2e:ui/:debugfor interactive runspnpm test— unit + e2e
Gotchas:
- If
.claude/worktrees/contains repo copies, vitest picks up their test files too — scope withpnpm vitest run --dir src. - Local Playwright runs can be flaky under parallel workers; retry or use
--workers=1before assuming a real failure. - CI runs lint, check, unit tests, and Playwright (against a production build) on PRs to master.
SvelteKit 2 + Svelte 5 (runes) SPA using @sveltejs/adapter-static. docs/ is build output (GitHub Pages convention) — never edit it. Routes: / (redirects), /edit (main editor), /view (read-only viewer).
src/lib/util/state.svelte.ts is the single source of truth:
inputState— a$staterune holdingState(code, mermaid config JSON, pan/zoom, etc.), initialized from localStorage keycodeStore. All writes must go through the exported update functions (updateCode,updateCodeStore,updateConfig,replaceInputState, ...), which callpersistAndProcess(): persist to localStorage → async re-validate viamermaid.parse→ publish tovalidatedState. Reads inside update functions are wrapped inuntrackso calling effects don't depend on the whole input state.validatedState— read-only validated snapshot (addserror,errorMarkers,serialized,diagramType). Internal reads should use this, but it is never persisted or shared externally.- The serialized state is mirrored into the URL hash (debounced) — the URL is the sharing mechanism.
src/lib/util/serde.tsencodes state aspako:<base64 deflate>(legacy plain-base64 supported).mermaid.inkPNG/SVG links, Kroki, and Mermaid Chart URLs are derived from it inurls. - App startup goes through
initHandler()insrc/lib/util/util.ts: migrations → load state from URL hash → optional gist/URL loading (fileLoaders/) → start URL-hash subscription → analytics.
src/lib/util/persist.svelte.ts provides readJSON/writeJSON and a persisted() localStorage-backed rune, shared by state, migrations (migrations.svelte.ts), promos, and History. History (src/lib/components/History/historyState.svelte.ts) auto-saves a timeline of diagrams for the whole session (startAutoSave() in the edit page).
src/lib/util/mermaid.ts wraps mermaid and registers the ELK and tidy-tree layout engines plus ZenUML at module load. View.svelte renders the diagram; panZoom.ts wraps svg-pan-zoom; "rough" hand-drawn mode uses svg2roughjs. Sample diagrams come from @mermaid-js/examples.
Two editor implementations behind Editor.svelte: Monaco for desktop (DesktopEditor.svelte; custom mermaid language, completions, and error markers in monacoExtra.ts) and CodeMirror for mobile (MobileEditor.svelte). Both edit either the diagram code or the mermaid config JSON depending on editorMode.
States loaded from URLs pass through sanitizeConfig (state.svelte.ts), which detects unsafe mermaid config (XSS vectors, prototype pollution, secure keys) and asks the user before stripping it. Keep this in mind when touching anything that deserializes external state.
- shadcn-svelte components live in
src/lib/components/ui/(bits-ui based, registry config incomponents.json) — treat as vendored; several lint rules are disabled there. - Tailwind CSS 4 (via
@tailwindcss/vite). - Icons via unplugin-icons:
import X from '~icons/material-symbols/...'; custom icons load fromstatic/iconsunder~icons/custom/*. - Path alias:
$/*→src/lib/*(in addition to SvelteKit's$lib).
- Env vars use the
MERMAID_prefix (import.meta.env), read insrc/lib/util/env.ts. Defaults in.env; copy to.env.localfor local overrides. These control renderer URL (mermaid.ink), Kroki, analytics, and Mermaid Chart integration. - HMR is deliberately disabled — every change triggers a full page reload (see
alwaysFullReloadinvite.config.js) because HMR corrupts app state. vite.config.jspins monaco/mermaid/codemirror into named vendor chunks for long-term caching — keep that intact when touching build config.
- ESLint enforces alphabetically sorted object keys in
src/for objects with 5+ keys (sort-keys/sort-keys-fix), plus typescript-eslint strict and unicorn rules. - Unit tests are colocated with source (
*.test.tsinsrc/); vitest runs with jsdom and also supports in-source tests (import.meta.vitest). Playwright e2e specs live intests/and usedata-testidconstants fromsrc/lib/constants.ts(TID).