This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
MarkText is a WYSIWYG markdown editor built on Electron + Vue 3. It supports CommonMark, GitHub Flavored Markdown, math (KaTeX), Mermaid diagrams, PlantUML, and multiple editing modes (focus, typewriter, source-code).
- Version: see
package.json - License: MIT
- Repository: https://github.com/marktext/marktext
| Layer | Technology |
|---|---|
| Language | TypeScript 5.9 (strict mode) — packages/muyajs/ retained as JS via ambient shim |
| Desktop shell | Electron 42 |
| Build system | electron-vite 5 |
| Packaging | electron-builder 26 |
| Frontend framework | Vue 3 |
| State management | Pinia 3 |
| Routing | Vue Router 4 |
| UI library | Element Plus |
| Unit tests | Vitest 4 |
| E2E tests | Playwright |
| Package manager | pnpm >=10 workspace (packageManager: pnpm@10.33.4) |
| Repo layout | pnpm monorepo — see Directory Structure |
| Node.js minimum | >=20.19.0 (PR CI: Node 22.21.1 · release CI: Node 24.14.1) |
This is a pnpm workspace. Three packages live under packages/, and the
root holds only shared tooling and CI-facing scripts.
<repo-root>/
package.json Workspace orchestrator — every CI-facing script
proxies to packages/desktop via `pnpm --filter
marktext ...`. CI invocations are unchanged.
pnpm-workspace.yaml `packages: ['packages/*']` plus allowBuilds.
pnpm-lock.yaml Single lockfile, shared across all packages.
eslint.config.js Root ESLint v9 flat config (covers desktop +
muyajs; website has its own ESLint v8 config
and is ignored here).
scripts/ Workspace-level scripts. postinstall.ts,
minify-locales.ts, generateThirdPartyLicense.ts,
validateLicenses.ts, thirdPartyChecker.ts all
target packages/desktop internally.
docs/ Long-form developer docs.
dist/ Packaged installers from electron-builder
(git-ignored; electron-builder writes here via
`directories.output: ../../dist` so CI artifact
globs `dist/*` still apply).
packages/
desktop/ The Electron app (name: "marktext").
package.json Holds all Electron / Vue / build-time deps and
the dev/build/test/typecheck scripts. Depends on
@marktext/muyajs via workspace:*.
electron.vite.config.ts
electron-builder.yml directories.output points at ../../dist.
tsconfig.json / tsconfig.base.json
vitest.config.ts
patches/ pnpm patches consumed by patch-package.
build/ electron-builder resources (icons, entitlements,
NSIS scripts).
static/ Static assets bundled into the app
(icons, themes, locales).
out/ electron-vite output (git-ignored).
test/
unit/ Vitest specs → pnpm test / pnpm test:unit
e2e/ Playwright specs + playwright.config.ts
→ pnpm test:e2e
src/
common/ Pure Node.js utilities usable from main, preload,
and renderer.
main/ Electron main process (IO, native dialogs, window
management, auto-updater).
preload/ Electron preload scripts. The renderer runs
sandboxed (contextIsolation: true,
nodeIntegration: false, sandbox: true since
#4244) — all Node access flows through the typed
contextBridge surface in
packages/desktop/src/preload/index.ts.
renderer/ Vue 3 application (editor UI, Pinia stores).
src/
components/ Vue single-file components.
store/ Pinia stores (editor.ts, preferences.ts,
layout.ts, …).
pages/ Top-level Vue pages / routes.
router/ Vue Router configuration.
shared/ Cross-process types (`shared/types/`) and the
IPC contract (`shared/types/ipc.ts`).
types/ Ambient .d.ts declarations.
muyajs/ Legacy markdown editor engine
(name: "@marktext/muyajs"). Primarily JS + DOM,
avoids Electron APIs. Exception:
packages/muyajs/lib/parser/render/plantuml.js
imports Node's `zlib`. Being retired: the
desktop renderer now consumes @muyajs/core
(packages/muya) as its editor engine; only a
handful of legacy `muya/` alias call sites
remain (see #4244 era sandbox work for the
boundary tightening).
lib/
contentState/ Block structure and document transformations.
parser/ Markdown parser.
renderers/ WYSIWYG renderer.
ui/ Inline toolbar, emoji picker, etc.
utils/ Internal utilities.
themes/ Editor themes (Prism + fonts).
muya/ TypeScript rewrite of muya
(name: "@muyajs/core"; upstream:
https://github.com/marktext/muya). Built on
ot-json1 + ot-text-unicode + snabbdom + marked@16
+ rxjs. Self-contained: own eslint config
(antfu), own stylelint, own madge, own vitest
spec suites (CommonMark + GFM). Now the editor
engine the desktop renderer consumes; legacy
packages/muyajs is being retired. See
packages/muya/CLAUDE.md for layout and commands.
src/ TS source. Public entrypoint src/index.ts.
test/spec/ CommonMark 0.31 + GFM 0.29-gfm conformance.
examples/ muya-examples — vite vanilla-TS dev demo
(listed in pnpm-workspace.yaml).
e2e/ muya-e2e — Playwright suite. CI runs Chromium
only via muya-e2e.yml; Firefox + WebKit are
wired in playwright.config.ts but deferred
until BACKLOG Phase 3 lands engine-independent
specs.
website/ marktext-website (Vite + React 18). Standalone
toolchain; depends on @muyajs/core from npm,
not on the local muyajs package. Not part of
desktop CI today.
src/ / public/ / build/ / vite.config.ts / tsconfig.json
The root has no src/, test/, static/, or build/ of its own anymore — they all live in packages/desktop/.
All commands run from the repo root. The root package.json proxies every
desktop-specific script to packages/desktop via pnpm --filter marktext,
so the names and behavior are unchanged from the pre-monorepo layout.
# Install dependencies (runs scripts/postinstall.ts automatically — patches
# native-keymap, downloads Electron, rebuilds native modules, minifies locales)
pnpm install
# Run in development mode
# Renderer hot-reloads automatically. Pressing Ctrl+R in the dev window reloads
# the renderer (which re-runs the preload script); changes to the main process
# require restarting `pnpm run dev`.
pnpm run dev
# Preview the last electron-vite build (no rebuild). PERF_TESTING=true is set automatically.
pnpm run start
# Build without packaging — fast path for verifying the renderer/main compile
pnpm run build:unpack
# Auto-format the repo with Prettier (separate from `lint`, which only checks)
pnpm run format
# Minify locale files (required for production builds, skip during dev)
pnpm run minify-locales
# Performance debugging — exposes a Node inspector on :5858 against the previewed build
pnpm run perf:inspect # attach when ready
pnpm run perf:inspect-brk # break on first line
# Website (not yet wired into CI)
pnpm --filter marktext-website dev # Vite dev server
pnpm --filter marktext-website build # static build → packages/website/build/If you need to invoke a script directly inside a package, use
pnpm --filter <name> <script> or pnpm -C packages/<name> <script>.
pnpm run build:win # Windows x64 — NSIS installer + zip
pnpm run build:mac # macOS x64 + arm64 — DMG + zip
pnpm run build:linux # Linux — AppImage, snap, deb, rpm, tar.gzAll platform build scripts automatically run minify-locales and electron-rebuild before packaging.
pnpm run test # All unit tests (Vitest)
pnpm run test:unit # Unit tests only
pnpm run test:e2e # End-to-end tests (Playwright)
pnpm run lint # ESLint (run before committing; CI enforces)
pnpm run typecheck # vue-tsc --noEmit (CI enforces)
# Run a single spec — paths are relative to packages/desktop. Use `-C` so
# pnpm resolves the spec path inside the desktop package's vitest config.
pnpm -C packages/desktop exec vitest run test/unit/specs/markdown-basic.spec.ts
pnpm -C packages/desktop exec vitest run -t 'partial test name'
# Single Playwright spec (playwright.config.ts lives in test/e2e/)
pnpm -C packages/desktop exec playwright test test/e2e/launch.spec.ts
pnpm -C packages/desktop exec playwright test -g 'partial test name'Enforced by ESLint + Prettier. Run pnpm run lint and pnpm run typecheck before committing.
- 2-space indentation
- No semicolons
- Single quotes
- TypeScript with
strict: true; seepackages/website/content/docs/dev/TYPESCRIPT.md - Cross-process types live in
packages/desktop/src/shared/types/; ambient declarations inpackages/desktop/src/types/ - IPC channels are typed via the contract in
packages/desktop/src/shared/types/ipc.ts - The renderer is fully sandboxed — every IPC and Node access goes through
window.electron.*/window.fileUtils.*etc. (typed inpackages/desktop/src/types/global.d.ts)
Follow .github/COMMENTING-GUIDELINES.md for every comment you write. The core rule: a comment must describe what isn't obvious from the code — rationale, units, invariants, ownership, the abstraction a caller needs — never restate the code or echo the words already in the name. Before finishing any change, review the comments you added or touched against that document, and delete any that only repeat the code. Prefer self-explanatory names over comments; when a comment is genuinely needed, keep it short and complete and place it next to the code it describes.
All Electron processes live in packages/desktop/. Muya is a separate
workspace package that the renderer (and tests) consume via the muya
alias / @marktext/muyajs workspace dep.
main process (packages/desktop/src/main/)
├── Full Node.js + Electron API access
├── IO, file system, native dialogs, auto-updater, spell checker
├── One instance per application launch
└── Controls editor windows via IPC
preload (packages/desktop/src/preload/)
├── Bridge between main and renderer
├── Note: editor and preferences windows use contextIsolation: false +
│ nodeIntegration: true (see packages/desktop/src/main/config.js)
└── Compiled to CommonJS
renderer (packages/desktop/src/renderer/)
├── One process per editor window (spawned by main)
├── Vue 3 + Pinia — all UI state and editor interaction
├── Hosts both Muya (WYSIWYG) and CodeMirror (source-code mode)
└── Compiled to ES Modules only
Muya (packages/muyajs/) ← workspace package @marktext/muyajs
├── Self-contained editor backend
├── Primarily avoids Electron APIs; uses Node's zlib for PlantUML encoding
├── Handles markdown parsing, block data structure, document export, rendering
└── packages/muya/ (@muyajs/core, the TS rewrite from
https://github.com/marktext/muya) has landed and is now the engine
the desktop renderer consumes; muyajs is being retired.
Most IPC channels between main and renderer use the mt:: prefix (e.g. mt::open-new-tab, mt::file-saved). Some internal channels do not follow this convention (e.g. language-changed).
See packages/website/content/docs/dev/IPC.md for conventions and examples.
packages/website/content/docs/dev/ contains the deeper developer documentation referenced by this guide. Same files are published as the developer docs section on https://marktext.me/docs/dev/overview:
ARCHITECTURE.md— process/module layering beyond the summary aboveBUILD.md— full platform build prerequisites (including the Arch Linux deps added recently)DEBUGGING.md— attaching debuggers to main/renderer processesINTERFACE.md— Muya and renderer public interfacesIPC.md— full IPC channel catalog andmt::conventionsLINUX_DEV.md— Linux-specific dev environment setupPERFORMANCE.md— perf measurement workflow (pairs withpnpm run perf:inspect)RELEASE.md/RELEASE_HOTFIX.md— release process
- CommonJS vs ESM:
mainandpreloadcompile to CommonJS;rendereris ESM-only. Do not userequire()in renderer code. - Minify locales:
pnpm run minify-localesmust run before production builds. It is included inbuild:win/mac/linuxbut not indev. - Native modules: After changing Electron version, run
pnpm run rebuild-native(electron-rebuild -f). - Hot reload: The renderer hot-reloads via Vite HMR.
Ctrl+Rin the dev window reloads the renderer and re-runs the preload script. Changes tomain/source are NOT picked up by a window reload — restartpnpm run devto pick them up. - electron-builder output:
directories.outputinpackages/desktop/electron-builder.ymlis set to../../distso installers land in the repo-rootdist/(where CI artifact globs look for them).out/from electron-vite stays insidepackages/desktop/. - Path aliases (defined in
packages/desktop/electron.vite.config.ts, mirrored invitest.config.tsandtsconfig.base.json):@→packages/desktop/src/renderer/srccommon→packages/desktop/src/common@shared→packages/desktop/src/sharedmuya→../muyajs(i.e.packages/muyajs). Renderer-side imports therefore look likemuya/lib/...(the alias) — the workspace dep@marktext/muyajsis declared inpackages/desktop/package.jsonso module resolution stays inside the workspace.
- Workspace deps: muya's own npm runtime deps (
github-markdown-css,katex,dompurify,snabbdom, …) are declared inpackages/muyajs/package.jsonso Node module resolution frompackages/muyajs/lib/*.jsfinds them inside the workspace rather than walking out to a parent directory. - Patches:
patch-packagepatches live atpackages/desktop/patches/. The rootpostinstallcalls patch-package withcwd=packages/desktopso the path resolves correctly.
- Submit PRs to the
developbranch (notmain). - Reference the related issue in the PR description.
- Run
pnpm run lintbefore submitting. - All PRs must pass CI before merge.
- See
.github/CONTRIBUTING.mdfor the full contributing guide.