Skip to main content

Design Doc

Guiding principles

Minimize boilerplate

Wherever possible, sensible defaults should be provided and it must be possible to override them. This is why a Plan has the config property which specifies the initial configuration.

Full backwards compatibility

Almost anything that you do with the editor or a legacy React plug-in should just work. Upgrading to use a Plan should be a simple change.

Minimize change required to add a Plan

Ideally, adding a plan should be two or three lines of code:

  • Import it
  • Add it as a dependency (with config if necessary)
  • Add UI related code (only if needed, e.g. has to be rendered in a specific place in the DOM)

Avoid invalid states

TypeScript goes a long way here, but it's only as good as the API design. For example, splitting up the workflow for a Plan to have separate Config, Init and Output types was something that evolved over time. In earlier prototypes, Config was overloaded to maintain all of that state, but it required a lot more runtime support and type assertions to check that the state was properly initialized for that phase.

Try to avoid undecipherable type errors

There's only so much that can be done to make readable error messages out of complex type expressions, so the API is designed to avoid them when possible. See also microsoft/TypeScript#23689.

Inspiration

ESLint Flat Plugins

These have many of the same constraints as this project, although of course are used for a very different purpose

MDXEditor

This editor is based on Lexical, but provides its own mechanism for plugins based on a third party state management library. These plugins don't really expose any type-safe configuration.

@payloadcms/richtext-lexical

Another Lexical based editor framework for use inside the Payload CMS. It also doesn't really expose type-safe configuration, but it does seem to have quite a lot of features and seems well-designed for laziness and SSR. Features are conceptually similar to Plans.

Effect

This library is pretty much state of the art for type safety with a focus on composition and usability.

Tighter integration with LexicalEditor

This package was originally prototyped as an entirely optional feature, as adoption increases it's expected that more of Lexical will directly depend on it.

Intentional trade-offs

No compile-time support for dependency resolution

The current theory is that it would require too much TypeScript in order to carry around the list of all dependency names that exist in the graph, and would likely add another type argument to LexicalPlan.

The features that this blocks are:

  • Compile-time support for detecting Plan conflicts. Detecting that two plans defined with the same name (but are not identical object references, because a dependency can be shared!) would also be quite the undertaking in TypeScript. This is automatically detected at runtime by the builder.
  • Compile-time support for required configuration without defaults. A plan can implement this at runtime in init or register.
  • Compile-time support for required peer dependencies. A use case for this would be the requirement of a RectProviderPlan provided by either LexicalPlanComposer or ReactPluginHost. A plan can implement this at runtime in init or register.

Generally speaking, all of these are already surfaced as runtime errors while building the editor with sufficient information to quickly track down the root cause.

Known Missing Features

Direct support for devtools

This is a TODO, the infrastructure was designed with this in mind.

Helpers for working with nested editors

It's not quite clear what all of the use cases for nested Plan editors are, this is a TODO.

Documented patterns for RSC/SSR/Headless

Having a known peer dependency that is used to declare SSR may help guide future Plan development. In many cases there are decorator nodes that have React dependencies that you do not want to (or can not) render in an SSR context. For example, RSC can not be supported anywhere that React.Context is used which is everywhere that React is currently used in Lexical.

Another option would be to build separate entrypoints for the SSR use case, which is more of an RSC specific strategy, but we could encourage people to use those import conditions even in a Vanilla JS or some other bespoke SSR use case. That strategy may make it hard to support including both "headless" and "non-headless" in the same module.