Skip to content

Latest commit

 

History

History
194 lines (157 loc) · 8.28 KB

File metadata and controls

194 lines (157 loc) · 8.28 KB

Lexical Agent Guide

This file provides detailed guidance for AI agents and automated tools working with the Lexical codebase.

Build, Test, and Development Commands

Building

  • pnpm run build - Build all packages in development mode
  • pnpm run build-prod - Clean and build all packages in production mode
  • pnpm run build-release - Build production release with error codes
  • pnpm run build-types - Build TypeScript type definitions and validate them

Testing

  • pnpm run test-unit - Run all unit tests (Vitest)
  • pnpm run test-unit-watch - Run unit tests in watch mode
  • pnpm run test-e2e-chromium - Run E2E tests in Chromium (requires dev server running)
  • pnpm run test-e2e-firefox - Run E2E tests in Firefox
  • pnpm run test-e2e-webkit - Run E2E tests in WebKit
  • pnpm 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:

  1. Start the dev server: pnpm run start (or pnpm run dev if you don't need collab)
  2. In another terminal: pnpm run test-e2e-chromium

Development Servers

  • 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

Code Quality

  • pnpm run lint - Run ESLint on all files
  • pnpm run lint:fix - Auto-fix lint issues
  • pnpm run prettier - Check code formatting
  • pnpm run prettier:fix - Auto-fix formatting issues
  • pnpm run flow - Run Flow type checker
  • pnpm run tsc - Run TypeScript compiler
  • pnpm run ci-check - Run all checks (TypeScript, Flow, Prettier, ESLint)

High-Level Architecture

Core Concepts

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 mutations
  • editor.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:

  1. Current EditorState is cloned as work-in-progress
  2. Mutations modify the work-in-progress state
  3. Multiple synchronous updates are batched
  4. DOM reconciler diffs and applies changes
  5. 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.

Monorepo Structure

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/extension - Extend editor functionality
  • @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 application
  • lexical-website - Docusaurus documentation site

Key Architectural Patterns

Extensions - Extensions should be used to add features and configuration to an editor. The set of extensions in an editor must be determined when the editor is created with buildEditorFromExtensions. Extensions with functionality that can be toggled on or off typically have a disabled configuration property and output signal that defaults to false. See the lexical-extension package and the supporting code in lexical for more examples and implementation details.

export interface MyConfig {
  disabled: boolean;
}
export const MyExtension = defineExtension({
  build: (_editor, config, _state) => namedSignals(config),
  config: safeCast<MyConfig>({ disabled: false }),
  name: '@lexical/docs/My',
  nodes: () => [MyNode],
  register: (editor, _config, state) => {
    const {disabled} = state.getOutput();
    return effect(() => {
      if (!disabled.value) {
        return editor.registerUpdateListener(({editorState}) => {
          // React to updates
        });
      }
    })
  },
})

Plugin System (React) - Plugins are a legacy pattern for React components to hook into the editor lifecycle, extensions should be preferred for new code:

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.

Type System

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 flow to check Flow types
  • Run pnpm run tsc to check TypeScript types
  • Both are checked in CI via pnpm run ci-check

When adding/modifying APIs, types must be maintained for both systems.

Important Development Notes

Reconciliation and Updates

  • 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)

Node References

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.

Testing Strategy

  • 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-chromium to debug E2E tests with browser UI

Custom Nodes

When creating custom nodes:

  1. Extend a base node class (TextNode, ElementNode, DecoratorNode)
  2. Implement instance methods: $config(), createDOM(), updateDOM()
  3. Register with extension or editor config: nodes: [YourCustomNode]
  4. Export a $createYourNode() factory function (follows $ convention)

Build System

  • 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.mjs