Skip to content

feat: headless Yjs collaboration extension (beta) + baseUrl cleanup#16

Merged
novincode merged 5 commits into
mainfrom
feat/collaboration
Jun 16, 2026
Merged

feat: headless Yjs collaboration extension (beta) + baseUrl cleanup#16
novincode merged 5 commits into
mainfrom
feat/collaboration

Conversation

@novincode

Copy link
Copy Markdown
Owner

Headless Yjs collaboration + baseUrl cleanup

Adds first-class, transport-agnostic real-time collaboration to LexKit, built on Lexical's own CollaborationPlugin + @lexical/yjs (a CRDT — offline edits merge conflict-free on reconnect). Also drops the deprecated TS baseUrl.

Targeting a beta release first: this bumps @lexkit/editor to 0.1.0-beta.0, which the release workflow now publishes under the npm beta dist-tag — so npm i @lexkit/editor stays on stable while npm i @lexkit/editor@beta gets collaboration. Promote to stable 0.1.0 on main once validated.

What's in it

  • CollaborationExtension — plugs in like every other extension. The only required option is providerFactory: (id, docMap) => Provider, the single seam where you choose a transport. LexKit bundles no transport; yjs + @lexical/yjs are optional peer deps (they already arrive via @lexical/react), so editors without the extension pull in nothing extra.
  • Headless presencecommands.collab.{connect,disconnect,toggle,setLocalUser,getUsers,onUsersChange}, an isCollaborating state query, and a useCollaborators() hook to build any presence UI.
  • Themeable cursors — reliable caret binding + theme.collab.* classes + optional cursorsContainerRef.
  • Zero-infra demo + docs/docs/extensions/CollaborationExtension with a live y-webrtc demo (two same-origin tabs sync via BroadcastChannel, no server), plus recipes for y-websocket / y-webrtc / y-indexeddb, theming, offline behaviour and the history caveat.
  • baseUrl removed from the editor/ui/web tsconfigs (TS 5+ resolves paths without it); fixes the TS 6/7 deprecation.

Tests (all green, 20/20)

Headless two-editor harness over an in-memory transport relaying two real Y.Docs through the actual extension + Lexical binding:

  • text syncs editor → editor
  • bidirectional edits converge (CRDT)
  • offline edit replays on reconnect; both docs converge (state-vector equality — no data loss)
  • presence API reflects awareness; isCollaborating tracks the connection

Compatibility

Verified the build + full test suite pass against Lexical 0.45 (latest) as well as the pinned 0.35; peer range stays >=0.34.

Notes / limitations

  • Undo: Lexical's CollaborationPlugin doesn't wire a Yjs undo manager, so don't register historyExtension while collaborating (Yjs owns history). Documented.
  • Multiple collaborative editors on one page need separate collaboration contexts (documented).

baseUrl is deprecated in TS 6.0 and removed in 7.0. Since TS 5.0 the
paths map resolves relative to the tsconfig file, so baseUrl is no
longer required. Drop it from the editor, ui and web tsconfigs and keep
the existing paths mappings.

Verified: @lexkit/editor build (incl. dts) and web typecheck both pass.
Add a first-class, transport-agnostic collaboration extension built on
Lexical's own CollaborationPlugin + @lexical/yjs (CRDT, so offline edits
merge conflict-free on reconnect).

- Single transport seam: providerFactory(id, docMap) => Provider. LexKit
  ships no transport dependency; plug in y-websocket / y-webrtc /
  y-indexeddb / Hocuspocus / Liveblocks / etc.
- yjs + @lexical/yjs declared as optional peer deps (they already arrive
  via @lexical/react, a required peer) so non-collab editors pull in
  nothing extra.
- Headless presence API: commands.collab.{connect,disconnect,toggle,
  setLocalUser,getUsers,onUsersChange} + isCollaborating state query +
  useCollaborators() hook, all via the standard extension contract.
- Themeable remote cursors via theme.collab.* and an optional
  cursorsContainerRef.
- Inert until configured with a providerFactory, so it is safe to leave
  in the extensions array unconditionally.

Note: collaborative undo/redo is not wired by Lexical's CollaborationPlugin;
do not register historyExtension while collaborating (documented).
In-memory transport (no network) relays two real Y.Docs through the
actual extension + Lexical binding to verify:
- text typed in one editor syncs to another
- bidirectional edits converge (CRDT, no lost data)
- offline edits replay on reconnect and the docs converge identically
  (state-vector equality)
- the presence API (getUsers/onUsersChange/setLocalUser) reflects awareness
- isCollaborating tracks the connection

Notes for the harness: each editor needs its own CollaborationContext
(the default shares a module-level yjsDocMap), docs need unique clientIDs
(lib0's RNG collides under jsdom), and remote updates are relayed async to
avoid re-entering the binding.
Add a CollaborationExtension docs page with a live, server-free demo:
two same-origin tabs sync peer-to-peer over y-webrtc's BroadcastChannel.
The demo loads the transport client-side only (ssr:false) so y-webrtc
never runs during prerender. Docs cover the providerFactory seam,
y-websocket/y-webrtc/y-indexeddb recipes, the useCollaborators presence
hook, cursor theming, offline/CRDT behaviour and the history/undo caveat.
Adds y-webrtc + yjs to the web app and a sidebar nav entry.
Derive the npm dist-tag from the version: a hyphenated prerelease (e.g.
0.1.0-beta.0) publishes under 'beta' and is marked as a GitHub prerelease,
so 'npm i @lexkit/editor' stays on stable 'latest' while
'npm i @lexkit/editor@beta' gets the collaboration build. A beta can be cut
from this branch via 'gh workflow run "Release @lexkit/editor" --ref
feat/collaboration'. Bump @lexkit/editor to 0.1.0-beta.0.
@novincode novincode merged commit eefa2ad into main Jun 16, 2026
2 checks passed
@novincode novincode deleted the feat/collaboration branch June 17, 2026 13:10
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.

1 participant