Skip to content

LinkML Visual Schema Editor — Developer Guide


Prerequisites

ToolVersionInstall
Node.js>= 20.0.0https://nodejs.org
pnpm>= 9.0.0npm install -g pnpm
Gitanyhttps://git-scm.com

Setup

bash
git clone <repo-url>
cd linkml-visual-editor
pnpm install

Running in Development

Web app

bash
pnpm dev

Starts the Vite dev server at http://localhost:5173 with hot module replacement.

Electron desktop app

The Electron main process loads the renderer from http://localhost:5173 in dev mode. You need both the web dev server and the compiled main process running simultaneously.

Terminal 1 — web renderer:

bash
pnpm dev

Terminal 2 — Electron main process:

bash
# Build core first (needed by the web renderer)
pnpm --filter @linkml-editor/core build

# Compile the Electron main process (TypeScript → CommonJS)
pnpm --filter @linkml-editor/electron build

# Launch Electron pointing at the compiled entry point
NODE_ENV=development npx electron packages/electron/dist/main.js

After making changes to the Electron main process source (packages/electron/src/), re-run the build + electron commands in Terminal 2. The renderer hot-reloads automatically via the dev server.


Building for Production

bash
pnpm build

Runs build in all packages in dependency order:

  1. @linkml-editor/core — TypeScript compilation + Vite library build → packages/core/dist/
  2. @linkml-editor/web — TypeScript check + Vite static build → packages/web/dist/
  3. @linkml-editor/electron — TypeScript compilation → packages/electron/dist/

Serving the web build

bash
# Preview the production web build locally
pnpm --filter @linkml-editor/web preview

The 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/.

bash
# 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 --build

The 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:

bash
VITE_BASE_URL=/linkml-editor/ \
VITE_GIT_CORS_PROXY=https://your-domain.com/linkml-editor/cors-proxy \
  pnpm --filter @linkml-editor/web build

VITE_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:

bash
BASE_PATH=/linkml-editor/ \
  docker compose -f deploy/web/docker-compose.yml up --build

BASE_PATH is passed as a Docker build arg and written into the nginx config. It must match VITE_BASE_URL exactly.

Configuration summary:

VariableWhere usedExample
VITE_BASE_URLpnpm build env (baked into index.html)/linkml-editor/
VITE_GIT_CORS_PROXYpnpm build env (baked into JS bundle)https://your-domain.com/linkml-editor/cors-proxy
BASE_PATHdocker compose env → Docker build arg → nginx config/linkml-editor/

Reverse proxy config (example nginx upstream):

nginx
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;
}

Running the Electron production build

bash
# Ensure the web build is present first
pnpm build

# Launch the production Electron app
npx electron packages/electron/dist/main.js

In production mode (NODE_ENV=production), Electron loads packages/web/dist/index.html directly from disk instead of hitting the dev server.


Testing

bash
# 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:watch

Tests use Vitest with jsdom. Test files live in packages/core/src/__tests__/.


Linting and Formatting

bash
# Lint TypeScript/TSX across all packages
pnpm lint

# Auto-format all source files
pnpm format

ESLint is configured with @typescript-eslint + eslint-plugin-react. Prettier handles formatting.


Repository Structure

linkml-visual-editor/
├── 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 dependencies

Architecture

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:

ImplementationLocationMechanism
WebPlatformpackages/web/src/platform/WebPlatform.tsFile System Access API, OPFS for Git via @isomorphic-git/lightning-fs
ElectronPlatformpackages/web/src/platform/ElectronPlatform.tsThin 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:

SliceResponsibility
projectSliceActive project, schema file list, active schema selection
canvasSliceReactFlow node/edge positions, layout, focus mode
editorSliceSelected element, properties panel open state
gitSliceGit status, staged files, commit/push state
uiSliceToast notifications, panel open/close flags, dialogs
validationSliceValidation 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 save

Adding a new feature

  1. Define or extend types in packages/core/src/model/.
  2. Add store state and actions to the relevant slice in packages/core/src/store/.
  3. Build the UI in packages/core/src/canvas/ or packages/core/src/editor/.
  4. If the feature needs platform I/O, add a method to the PlatformAPI interface and implement it in both WebPlatform and ElectronPlatform.
  5. Export any new public API from packages/core/src/index.ts.
  6. Write tests in packages/core/src/__tests__/.

TypeScript Configuration

FilePurpose
tsconfig.base.jsonShared compiler options (ES2022, strict mode, bundler resolution)
packages/*/tsconfig.jsonPackage-level config extending the base
packages/core/tsconfig.test.jsonTest-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:

bash
pnpm --filter @linkml-editor/core build

Tests fail with "Cannot find module"

Run pnpm install to restore any missing workspace symlinks, then rebuild core.