Skip to content

feat(editor): migrate to Tiptap 3 - #18

Merged
christianhuening merged 1 commit into
mainfrom
feat/tiptap-3
Sep 5, 2026
Merged

feat(editor): migrate to Tiptap 3#18
christianhuening merged 1 commit into
mainfrom
feat/tiptap-3

Conversation

@christianhuening

Copy link
Copy Markdown
Contributor

Packages 2–7 of the migration plan, landed together because they have to be: every @tiptap/* package peer-depends on the exact string 3.31.3, and the binding swap, caret replacement, StarterKit reconfiguration and anchor repoint are one dependency graph.

The real content of this upgrade is not Tiptap. It's a Yjs binding swap — Collaboration now installs @tiptap/y-tiptap's ySyncPlugin instead of y-prosemirror's — plus a collaboration-cursor package that has no v3 release.

anchor.ts is repointed accordingly. Importing ySyncPluginKey from the old package would compile, run, and silently return undefined for every anchor: both packages call new PluginKey("y-sync") and ProseMirror uniquifies the second.

Dependencies

20 packages to ^3.31.3. extension-collaboration-cursorextension-collaboration-caret. The three list and three table sub-packages collapse into @tiptap/extension-list and @tiptap/extension-table — whose aggregate has no default export. Adds @tiptap/y-tiptap (a peer of both collaboration packages that nothing declares transitively) and @floating-ui/dom (a new runtime peer of suggestion). Drops y-prosemirror.

The prosemirror-* pins are carried unchanged — one variable at a time — and pnpm still resolves a single copy of each. Lifting them is the follow-up (WP-8).

StarterKit v3 turns on four things v2 did not

option why disabled
link, underline both registered explicitly here; the duplicate loses the argument fight over openOnClick
trailingNode appending a paragraph is, in this app, a write to a shared CRDT that every peer receives and that reaches the markdown export — triggered by opening a document, not editing it
listKeymap inert against snake_case nodes

history is now undoRedo.

Three new extensions arrive that cannot be turned off: textDirection and delete from core, and a branching-delete keymap ListItem registers for itself. All three were read before being accepted — textDirection adds no attribute unless configured, delete only emits events and skips y-sync transactions — and are pinned in schema.test.ts with that reasoning.

v3's Image adds width/height. Nothing here sets them, and declaring attributes tools/schema.json doesn't know puts content in the CRDT that to_markdown can't emit, so they're dropped — keeping the stored schema identical to what every existing document was written against.

One genuine regression, found and fixed

Switching documents via the command palette took the whole route down with "Unexpected Application Error!".

Tiptap 3 empties extensionStorage on destroy(), and the comment-highlight effect runs twice against the outgoing editor before its replacement exists — verified by instrumenting it (destroyed: true, keys: 0). In v2 that write landed on a dead object and did nothing.

Worth noting: the Storage interface augmentation this migration required told tsc the property was always defined, so the type system actively concealed it. search.spec.ts is the regression gate — a sidebar link click does not reproduce it, and there's a comment saying so.

Caret classes

Follow upstream (collaboration-carets__*) rather than pinning the old names via a custom render. prose.css, docText and the converge spec move with them.


tsc, eslint --max-warnings 0 and 153 unit tests green. e2e green on a clean stack apart from tree-reorder's "nest" — the known macOS-only dnd-kit failure that passes in CI.

Unrelated issue found along the way, worth filing separately: the collab room's doc_updates read leaves backends idle in transaction (aborted). Enough of them and the collab WebSocket stops reaching connected while plain HTTP still answers — which is what made local e2e runs flaky here, and is very likely the "leaked transaction" I misattributed earlier.

🤖 Generated with Claude Code

Packages 2-7 of the migration plan, landed together because they have to
be: every @tiptap/* package peer-depends on the exact string 3.31.3, and
the binding swap, caret replacement, StarterKit reconfiguration and anchor
repoint are one dependency graph.

The real content of this upgrade is not Tiptap. It is a Yjs binding swap
-- Collaboration now installs @tiptap/y-tiptap's ySyncPlugin rather than
y-prosemirror's -- and a collaboration-cursor package that has no v3
release. anchor.ts is repointed at @tiptap/y-tiptap accordingly; importing
ySyncPluginKey from the other package would compile, run, and silently
return undefined for every anchor, because both call new
PluginKey("y-sync") and ProseMirror uniquifies the second.

Dependencies: 20 packages to ^3.31.3. extension-collaboration-cursor ->
extension-collaboration-caret. The three list and three table
sub-packages collapse into @tiptap/extension-list and
@tiptap/extension-table, whose aggregate has no default export. Adds
@tiptap/y-tiptap (a peer of both collaboration packages that nothing
declares) and @floating-ui/dom (a new runtime peer of suggestion). Drops
y-prosemirror. The prosemirror-* pins are carried unchanged -- one
variable at a time -- and pnpm still resolves a single copy of each.

StarterKit v3 turns on four things v2 did not. link and underline are
disabled because both are registered explicitly here and the duplicate
loses the argument fight over openOnClick. trailingNode is disabled
because appending a paragraph to a document that does not end in one is,
in this app, a write to a shared CRDT that every peer receives and that
reaches the markdown export -- triggered by opening a document rather
than editing it. listKeymap is disabled as inert against snake_case
nodes. history is now undoRedo.

Three new extensions arrive that cannot be turned off: textDirection and
delete from core, and a branching-delete keymap ListItem registers for
itself. All three were read before being accepted -- textDirection adds
no attribute unless configured, delete only emits events and skips
y-sync transactions -- and are now pinned in schema.test.ts with that
reasoning.

v3's Image adds width and height. Nothing here sets them, and declaring
attributes tools/schema.json does not know puts content in the CRDT that
to_markdown cannot emit, so they are dropped to keep the stored schema
identical to what every existing document was written against.

One genuine regression, found by the e2e suite and fixed here: switching
documents via the command palette took the whole route down with
"Unexpected Application Error!". Tiptap 3 empties extensionStorage on
destroy, and the comment-highlight effect runs twice against the
outgoing editor before its replacement exists -- verified by
instrumenting it: destroyed true, storage keys 0. In v2 that write landed
on a dead object and did nothing. Worth noting that the Storage interface
augmentation this migration required told tsc the property was always
defined, so the type system actively concealed it.

Caret classes follow upstream (collaboration-carets__*) rather than being
pinned to the old names with a custom render; prose.css, docText and the
converge spec move with them.

tsc, eslint --max-warnings 0 and 153 unit tests green. e2e green on a
clean stack apart from tree-reorder's "nest", the known macOS-only
failure that passes in CI.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@christianhuening
christianhuening merged commit 6d631d2 into main Sep 5, 2026
5 checks passed
@christianhuening
christianhuening deleted the feat/tiptap-3 branch September 5, 2026 22:04
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