This file provides detailed guidance for AI agents and automated tools working with the Lexical codebase.
pnpm run build- Build all packages in development modepnpm run build-prod- Clean and build all packages in production modepnpm run build-release- Build production release with error codespnpm run build-types- Build TypeScript type definitions and validate them
pnpm run test-unit- Run all unit tests (Vitest)pnpm run test-unit-watch- Run unit tests in watch modepnpm run test-e2e-chromium- Run E2E tests in Chromium (requires dev server running)pnpm run test-e2e-firefox- Run E2E tests in Firefoxpnpm run test-e2e-webkit- Run E2E tests in WebKitpnpm run debug-test-e2e-chromium- Run E2E tests in debug mode (headed)pnpm run debug-test-unit- Debug unit tests with inspector
For E2E testing workflow:
- Start the dev server:
pnpm run start(orpnpm run devif you don't need collab) - In another terminal:
pnpm run test-e2e-chromium
pnpm run start- Start playground dev server + collab server (http://localhost:3000)pnpm run dev- Start only the playground dev server (no collab)pnpm run start:website- Start Docusaurus website (http://localhost:3001)pnpm run collab- Start collab server on localhost:1234
pnpm run lint- Run ESLint on all filespnpm run prettier- Check code formattingpnpm run prettier:fix- Auto-fix formatting issuespnpm run flow- Run Flow type checkerpnpm run tsc- Run TypeScript compilerpnpm run ci-check- Run all checks (TypeScript, Flow, Prettier, ESLint)
Lexical is built around several key architectural concepts that work together:
Editor Instance - Created via createEditor(), wires everything together. Manages the EditorState, registers listeners/commands/transforms, and handles DOM reconciliation.
EditorState - Immutable data model representing the editor content. Contains:
- A node tree (hierarchical structure of LexicalNodes)
- A selection object (current cursor/selection state)
- Fully serializable to/from JSON
$ Functions Convention - Functions prefixed with $ (e.g., $getRoot(), $getSelection()) can ONLY be called within:
editor.update(() => {...})- for mutationseditor.read(() => {...})- for read-only access- Node transforms and command handlers (which have implicit update context)
This is similar to React hooks' restrictions but enforces synchronous context instead of call order.
Double-Buffering Updates - When editor.update() is called:
- Current EditorState is cloned as work-in-progress
- Mutations modify the work-in-progress state
- Multiple synchronous updates are batched
- DOM reconciler diffs and applies changes
- New immutable EditorState becomes current
Node Immutability & Keys - All nodes are recursively frozen after reconciliation. Node methods automatically call node.getWritable() to create mutable clones. All versions of a logical node share the same runtime-only key, allowing node methods to always reference the latest version from the active EditorState.
This is a monorepo with packages in packages/:
Core Packages:
lexical- Core framework (Editor, EditorState, base nodes, selection, updates)@lexical/react- React bindings (LexicalComposer, plugins as components)@lexical/headless- Headless editor for server-side/testing
Feature Packages (extend core with nodes/commands/utilities):
@lexical/rich-text- Rich text editing (headings, quotes, etc.)@lexical/plain-text- Plain text editing@lexical/list- List nodes (ordered/unordered/checklist)@lexical/table- Table support@lexical/code- Code block with syntax highlighting@lexical/link- Link nodes and utilities@lexical/markdown- Markdown import/export@lexical/html- HTML serialization@lexical/history- Undo/redo@lexical/yjs- Real-time collaboration via Yjs- And many more...
Development Packages:
lexical-playground- Full-featured demo applicationlexical-website- Docusaurus documentation site
Plugin System (React) - Plugins are React components that hook into the editor lifecycle:
function MyPlugin() {
const [editor] = useLexicalComposerContext();
useEffect(() => {
return editor.registerUpdateListener(({editorState}) => {
// React to updates
});
}, [editor]);
return null;
}Command Pattern - Commands are the primary communication mechanism:
- Create with
createCommand() - Dispatch with
editor.dispatchCommand(command, payload) - Handle with
editor.registerCommand(command, handler, priority) - Handlers propagate by priority until one stops propagation
Node Transforms - Registered via editor.registerNodeTransform(NodeClass, transform). Called automatically during updates when nodes of that type change. Have implicit update context.
Listeners - All editor.register*() methods return cleanup functions for easy unsubscription.
This codebase uses both TypeScript and Flow:
- Source files are primarily TypeScript (
.ts,.tsx) - Flow type definitions are generated in
packages/*/flow/directories - Run
pnpm run flowto check Flow types - Run
pnpm run tscto check TypeScript types - Both are checked in CI via
pnpm run ci-check
When adding/modifying APIs, types must be maintained for both systems.
editor.read()flushes pending updates first, then provides consistent reconciled state- Inside
editor.update(), you see pending state (transforms/reconciliation not yet run) editor.getEditorState().read()always uses latest reconciled state- Updates can be nested:
editor.update(() => editor.update(...))is allowed - Do NOT nest reads in updates or vice versa (except read at end of update, which flushes)
Always access node properties/methods within read/update context. Nodes automatically resolve to their latest version via their key. Don't store node references across update boundaries.
- Unit tests - Vitest, located in
packages/**/__tests__/unit/**/*.test.{ts,tsx} - E2E tests - Playwright, located in
packages/lexical-playground/__tests__/e2e/**/*.spec.{ts,mjs} - E2E tests require the playground dev server running
- Use
pnpm run debug-test-e2e-chromiumto debug E2E tests with browser UI
When creating custom nodes:
- Extend a base node class (TextNode, ElementNode, DecoratorNode)
- Implement required static methods:
getType(),clone(),importJSON() - Implement instance methods:
createDOM(),updateDOM(),exportJSON() - Register with editor config:
nodes: [YourCustomNode] - Export a
$createYourNode()factory function (follows $ convention)
- Uses Rollup for bundling
- Build script:
scripts/build.mjs - Supports multiple build modes: development, production, www (Meta internal)
- TypeScript source → compiled to CommonJS and ESM
- Package manager logic in
scripts/shared/packagesManager.js