LinkML Visual Schema Editor — Developer Guide
Prerequisites
| Tool | Version | Install |
|---|---|---|
| Node.js | >= 20.0.0 | https://nodejs.org |
| pnpm | >= 9.0.0 | npm install -g pnpm |
| Git | any | https://git-scm.com |
Setup
git clone https://github.com/adamlabadorf/linkml-modeler-app.git
cd linkml-modeler-app
pnpm installRunning in Development
Web app
pnpm devStarts the Vite dev server at http://localhost:5173 with hot module replacement.
Experimental: desktop (Electron) app
Note: The Electron desktop build is experimental and is not part of the v1.0 supported surface. The web app is the primary development target.
To run the desktop build in development, you need both the web dev server and the compiled main process running simultaneously.
Terminal 1 — web renderer:
pnpm devTerminal 2 — Electron main process:
pnpm --filter @linkml-editor/core build
pnpm --filter @linkml-editor/electron build
NODE_ENV=development npx electron packages/electron/dist/main.jsBuilding for Production
pnpm buildBuilds core and web packages only — Electron is excluded:
@linkml-editor/core— TypeScript compilation + Vite library build →packages/core/dist/@linkml-editor/web— TypeScript check + Vite static build →packages/web/dist/
To include the Electron main process in the build, use pnpm build:all instead.
Serving the web build
# Preview the production web build locally
pnpm --filter @linkml-editor/web previewThe packages/web/dist/ directory is a self-contained static site — deploy it to any web server or CDN.
Deploying the web build with Docker
A Docker Compose stack (nginx + CORS proxy) is provided under deploy/web/.
# Build the web package (root path, default)
VITE_GIT_CORS_PROXY=https://your-domain.com/cors-proxy \
pnpm --filter @linkml-editor/web build
# Start the stack
docker compose -f deploy/web/docker-compose.yml up --buildThe app is served on port 80. The CORS proxy is reachable at /cors-proxy/ on the same origin.
Serving behind a reverse proxy at a subpath
When your reverse proxy routes the app under a URL prefix (e.g. https://your-domain.com/linkml-editor/), two values must agree: the Vite asset base and the nginx location prefix. Both are controlled at build time.
Step 1 — build the web package with VITE_BASE_URL set:
VITE_BASE_URL=/linkml-editor/ \
VITE_GIT_CORS_PROXY=https://your-domain.com/linkml-editor/cors-proxy \
pnpm --filter @linkml-editor/web buildVITE_BASE_URL must end with /. It controls the <script> and <link> src attributes emitted in index.html so browsers fetch assets from the right path.
Step 2 — build and start Docker with BASE_PATH set to the same value:
BASE_PATH=/linkml-editor/ \
docker compose -f deploy/web/docker-compose.yml up --buildBASE_PATH is passed as a Docker build arg and written into the nginx config. It must match VITE_BASE_URL exactly.
Configuration summary:
| Variable | Where used | Example |
|---|---|---|
VITE_BASE_URL | pnpm build env (baked into index.html) | /linkml-editor/ |
VITE_GIT_CORS_PROXY | pnpm build env (baked into JS bundle) | https://your-domain.com/linkml-editor/cors-proxy |
BASE_PATH | docker compose env → Docker build arg → nginx config | /linkml-editor/ |
Reverse proxy config (example nginx upstream):
location /linkml-editor/ {
proxy_pass http://your-docker-host:80/linkml-editor/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}Experimental: Electron production build
Note: Not part of v1.0 supported surface.
# Build core, web, and the Electron main process together
pnpm build:all
npx electron packages/electron/dist/main.jsIn production mode, Electron loads packages/web/dist/index.html from disk.
Testing
# Run all tests across all packages
pnpm test
# Run tests for a specific package
pnpm --filter @linkml-editor/core test
pnpm --filter @linkml-editor/web test
# Watch mode (core only)
pnpm --filter @linkml-editor/core test:watchTests use Vitest with jsdom. Test files live in packages/core/src/__tests__/.
Linting and Formatting
# Lint TypeScript/TSX across all packages
pnpm lint
# Auto-format all source files
pnpm formatESLint is configured with @typescript-eslint + eslint-plugin-react. Prettier handles formatting.
Repository Structure
linkml-modeler-app/
├── packages/
│ ├── core/ # Shared React renderer (platform-agnostic)
│ │ └── src/
│ │ ├── canvas/ # ReactFlow nodes, edges, auto-layout
│ │ ├── editor/ # Property panel forms
│ │ ├── store/ # Zustand state slices
│ │ ├── io/ # YAML parse/emit, Git operations
│ │ ├── model/ # TypeScript types for the LinkML metamodel
│ │ ├── ui/ # shadcn/ui components, layout chrome
│ │ ├── validation/# Schema validation engine
│ │ └── platform/ # PlatformContext interface + provider types
│ ├── web/ # Web harness
│ │ └── src/
│ │ ├── main.tsx # React entry point, App component
│ │ ├── platform/ # WebPlatform adapter (File System Access API, OPFS git)
│ │ └── editor/ # Web-specific panels (GitPanel)
│ └── electron/ # Electron harness
│ └── src/
│ ├── main.ts # Electron main process (BrowserWindow, IPC handlers)
│ └── preload.ts # Context bridge (exposes electronAPI to renderer)
├── docs/
│ ├── design-spec.md # High-level design specification
│ ├── user-guide.md # End-user guide
│ └── development.md # This file
├── tsconfig.base.json # Shared TypeScript base config
├── pnpm-workspace.yaml # pnpm monorepo workspace definition
└── package.json # Root scripts and shared dev dependenciesArchitecture
Platform abstraction
The core package has zero Electron or Node.js imports. All platform differences (file dialogs, filesystem access, Git backend) are hidden behind a PlatformAPI interface defined in packages/core/src/platform/.
A PlatformContext React context provides the active implementation to the whole component tree. The two concrete implementations are:
| Implementation | Location | Mechanism |
|---|---|---|
WebPlatform | packages/web/src/platform/WebPlatform.ts | File System Access API, OPFS for Git via @isomorphic-git/lightning-fs |
ElectronPlatform | packages/web/src/platform/ElectronPlatform.ts | Thin wrapper over window.electronAPI (IPC, defined in preload) |
The Electron main process (packages/electron/src/main.ts) registers ipcMain handlers that implement the same operations using Node.js fs and isomorphic-git.
State management
All application state lives in a single Zustand store (packages/core/src/store/), split into slices:
| Slice | Responsibility |
|---|---|
projectSlice | Active project, schema file list, active schema selection |
canvasSlice | ReactFlow node/edge positions, layout, focus mode |
editorSlice | Selected element, properties panel open state |
gitSlice | Git status, staged files, commit/push state |
uiSlice | Toast notifications, panel open/close flags, dialogs |
validationSlice | Validation issues, last-validated schema hash |
Undo/redo is provided by zundo, wrapping the Zustand store.
Data flow
User action
→ Zustand store mutation (via slice action)
→ React re-render (component subscriptions)
→ Canvas / Properties Panel update
→ YAML serialization (via serializeYaml in io/)
→ YAML Preview / file saveAdding a new feature
- Define or extend types in
packages/core/src/model/. - Add store state and actions to the relevant slice in
packages/core/src/store/. - Build the UI in
packages/core/src/canvas/orpackages/core/src/editor/. - If the feature needs platform I/O, add a method to the
PlatformAPIinterface and implement it in bothWebPlatformandElectronPlatform. - Export any new public API from
packages/core/src/index.ts. - Write tests in
packages/core/src/__tests__/.
TypeScript Configuration
| File | Purpose |
|---|---|
tsconfig.base.json | Shared compiler options (ES2022, strict mode, bundler resolution) |
packages/*/tsconfig.json | Package-level config extending the base |
packages/core/tsconfig.test.json | Test-specific config (relaxed for Vitest/jsdom) |
The project targets ES2022 with "moduleResolution": "bundler" — do not use CommonJS require() in core or web packages.
Common Issues
pnpm install fails with EACCES
Run npm install -g pnpm as your normal user (not root). See pnpm installation docs.
Electron window shows a blank page
Make sure the web dev server is running on port 5173 before launching Electron in dev mode. Check NODE_ENV=development is set.
TypeScript errors after pulling changes
Rebuild the core package first — other packages depend on its compiled output:
pnpm --filter @linkml-editor/core buildTests fail with "Cannot find module"
Run pnpm install to restore any missing workspace symlinks, then rebuild core.