Skip to content

feat(rust): the extension API (buildEditorFromExtensions), with a JS-recorded scripts/extensions vector - #155

Open
tmm wants to merge 1 commit into
rust-file-devtools-textfrom
rust-extension-api
Open

tmm wants to merge 1 commit into
rust-file-devtools-textfrom
rust-extension-api

Conversation

@tmm

@tmm tmm commented Nov 14, 2026

Copy link
Copy Markdown
Member

Description

The Rust port had register_* functions for each package but no
extension API, so nothing could build an editor from extensions the way
buildEditorFromExtensions does, and the extension-shaped parts of
upstream (InitialStateExtension, ClearEditorExtension, signals) had no
counterpart.

This ports it behind the existing extension feature:

  • lexical's extension-core: defineExtension, configExtension,
    declarePeerDependency, safeCast, shallowMergeConfig and the
    LexicalExtension types, plus mergeRegister.
  • @lexical/extension's builder: buildEditorFromExtensions and
    LexicalBuilder (dependency and peer resolution, the duplicate-name and
    conflict invariants, config merging, the init/build/register phases in
    JS's order, disposal), ExtensionRep, and the dependency lookups
    (getExtensionDependencyFromEditor, getPeerDependencyFromEditor and
    their OrThrow forms).
  • A port of @preact/signals-core 1.14.4 (signal, computed, effect,
    batch, untracked), with namedSignals and watchedSignal.
  • InitialStateExtension, ClearEditorExtension, EditorStateExtension
    and DecoratorTextExtension's extension.

packages/lexical-conformance gains JSON-described test extensions
(ExtensionSpec) that log each phase with their merged config, and a
scripts/extensions vector with 26 cases: a dependency diamond, duplicate
and conflicting extensions, peers present and absent, peers read before
they are configured or built, every form of $initialEditorState, node
registration, ClearEditorExtension, EditorStateExtension as a signal,
namedSignals toggling a registration, and disposal. Builder errors are
recorded in JS's wording, and both Rust and Swift reproduce it. Swift
matched every case, so no Swift source changes.

Not ported, each listed in PARITY.md: the editor options a headless Rust
editor lacks (namespace, theme, html, editable, onError,
parentEditor, dom...), node replacements, HMRExtension,
HorizontalRuleExtension and the other DOM- or keyboard-bound extensions,
and the packages' own extensions (RichTextExtension, HistoryExtension,
...), whose register_* functions are already ported.

Test plan

Before

$ cargo run -q --locked --release -p lexical-conformance
| **Total** | 631 | 631 | 0 | 0 | 0 | |

(No scripts/extensions row.)

After

$ cargo run -q --locked --release -p lexical-conformance
| `property/sequences` | 81 | 81 | 0 | 0 | 0 | pass |
| `scripts/extensions` | 26 | 26 | 0 | 0 | 0 | pass |
| `scripts/text` | 13 | 13 | 0 | 0 | 0 | pass |
| **Total** | 657 | 657 | 0 | 0 | 0 | |

Mutation checks (each reverted): merge_register cleaning up in forward
order fails 4 cases; root configExtension overrides not merged fails 4;
batch snapshots never reconciled fails 1; an extension recorded before its
dependencies fails 2; in Swift, no array concatenation in the test
mergeConfig fails 7.

cargo fmt --all --check, cargo clippy --workspace --all-targets --all-features --locked -- -D warnings, cargo test --workspace --all-features --locked, cargo doc with -D warnings, cargo +1.88 check -p lexical-editor --all-features, --no-default-features alone and
with extension, and the Rune app's clippy all pass.
UPDATE_VECTORS=1 pnpm exec vitest run src/scripts.test.ts: 20 passed,
and no vector changed (md5). Swift: swift build --build-tests and
swift test --skip-build --filter ConformanceTests, 33 tests in 13 suites
passed.

…recorded scripts/extensions vector

## Description

The Rust port had `register_*` functions for each package but no
extension API, so nothing could build an editor from extensions the way
`buildEditorFromExtensions` does, and the extension-shaped parts of
upstream (`InitialStateExtension`, `ClearEditorExtension`, signals) had no
counterpart.

This ports it behind the existing `extension` feature:

- `lexical`'s `extension-core`: `defineExtension`, `configExtension`,
  `declarePeerDependency`, `safeCast`, `shallowMergeConfig` and the
  `LexicalExtension` types, plus `mergeRegister`.
- `@lexical/extension`'s builder: `buildEditorFromExtensions` and
  `LexicalBuilder` (dependency and peer resolution, the duplicate-name and
  conflict invariants, config merging, the init/build/register phases in
  JS's order, disposal), `ExtensionRep`, and the dependency lookups
  (`getExtensionDependencyFromEditor`, `getPeerDependencyFromEditor` and
  their `OrThrow` forms).
- A port of `@preact/signals-core` 1.14.4 (`signal`, `computed`, `effect`,
  `batch`, `untracked`), with `namedSignals` and `watchedSignal`.
- `InitialStateExtension`, `ClearEditorExtension`, `EditorStateExtension`
  and `DecoratorTextExtension`'s extension.

`packages/lexical-conformance` gains JSON-described test extensions
(`ExtensionSpec`) that log each phase with their merged config, and a
`scripts/extensions` vector with 26 cases: a dependency diamond, duplicate
and conflicting extensions, peers present and absent, peers read before
they are configured or built, every form of `$initialEditorState`, node
registration, `ClearEditorExtension`, `EditorStateExtension` as a signal,
`namedSignals` toggling a registration, and disposal. Builder errors are
recorded in JS's wording, and both Rust and Swift reproduce it. Swift
matched every case, so no Swift source changes.

Not ported, each listed in PARITY.md: the editor options a headless Rust
editor lacks (`namespace`, `theme`, `html`, `editable`, `onError`,
`parentEditor`, `dom`...), node replacements, `HMRExtension`,
`HorizontalRuleExtension` and the other DOM- or keyboard-bound extensions,
and the packages' own extensions (`RichTextExtension`, `HistoryExtension`,
...), whose `register_*` functions are already ported.

## Test plan

### Before

```
$ cargo run -q --locked --release -p lexical-conformance
| **Total** | 631 | 631 | 0 | 0 | 0 | |
```

(No `scripts/extensions` row.)

### After

```
$ cargo run -q --locked --release -p lexical-conformance
| `property/sequences` | 81 | 81 | 0 | 0 | 0 | pass |
| `scripts/extensions` | 26 | 26 | 0 | 0 | 0 | pass |
| `scripts/text` | 13 | 13 | 0 | 0 | 0 | pass |
| **Total** | 657 | 657 | 0 | 0 | 0 | |
```

Mutation checks (each reverted): `merge_register` cleaning up in forward
order fails 4 cases; root `configExtension` overrides not merged fails 4;
batch snapshots never reconciled fails 1; an extension recorded before its
dependencies fails 2; in Swift, no array concatenation in the test
`mergeConfig` fails 7.

`cargo fmt --all --check`, `cargo clippy --workspace --all-targets
--all-features --locked -- -D warnings`, `cargo test --workspace
--all-features --locked`, `cargo doc` with `-D warnings`, `cargo +1.88
check -p lexical-editor --all-features`, `--no-default-features` alone and
with `extension`, and the Rune app's clippy all pass.
`UPDATE_VECTORS=1 pnpm exec vitest run src/scripts.test.ts`: 20 passed,
and no vector changed (md5). Swift: `swift build --build-tests` and
`swift test --skip-build --filter ConformanceTests`, 33 tests in 13 suites
passed.

Amp-Thread-ID: https://ampcode.com/threads/T-16d45229-bfa8-417a-a091-d3e3888d830d

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants