Skip to content

feat: decouple yjs from blocknote/core#2741

Open
nperez0111 wants to merge 2 commits into
mainfrom
decouple-yjs
Open

feat: decouple yjs from blocknote/core#2741
nperez0111 wants to merge 2 commits into
mainfrom
decouple-yjs

Conversation

@nperez0111
Copy link
Copy Markdown
Contributor

@nperez0111 nperez0111 commented May 13, 2026

Summary

In preparation of the upcoming Y.js 14. Which will introduce breaking changes on the API, type, package, and possibly document level.
I'm doing what we probably should have done from the start & de-coupling BlockNote from Y.js completely.
This means that when importing from @blocknote/core, we will no longer depend on anything in yjs or y-prosemirror.
Before this, certain functionality would import yjs or y-prosemirror at runtime even if the editor didn't actually need it's methods.

Now, to make your editor collaborative, you essentially just need to wrap your existing editor options with withCollaboration, for the editor to become collaborative.
This will automatically configure your editor to be collaborative, installing all of the necessary extensions to make the feature work.

Rationale

I'm fairly certain that we are going to need to support both Y.js 13 & Y.js 14 for some period of time. Since it will require document migrations at the discretion of the integrator.
So, it is better to get ahead of this & completely de-couple the editor from yjs & y-prosemirror.

I'm hoping for this to land pretty soon, before we are done with the other y/prosemirror changes. Since it will reduce drift in the PR as well as reduce bundle size for existing users

Changes

This PR removes the tight coupling between Yjs and @blocknote/core by moving all Yjs-related code into a dedicated @blocknote/core/yjs subpath export.

Breaking Changes

collaboration is no longer a direct editor option. Instead, use the new withCollaboration() helper to wrap your editor options:

// Before
const editor = useCreateBlockNote({
  collaboration: { provider, fragment, user },
});

// After
import { withCollaboration } from "@blocknote/core/yjs";

const editor = useCreateBlockNote(
  withCollaboration({
    collaboration: { provider, fragment, user },
  }),
);

Yjs-related imports have moved:

Export Old path New path
YjsThreadStore @blocknote/core/comments @blocknote/core/yjs
RESTYjsThreadStore @blocknote/core/comments @blocknote/core/yjs
YjsThreadStoreBase @blocknote/core/comments @blocknote/core/yjs
ForkYDocExtension @blocknote/core/extensions @blocknote/core/yjs
YSyncExtension, YUndoExtension, YCursorExtension, SchemaMigration @blocknote/core/extensions @blocknote/core/yjs

Non-Yjs exports (CommentsExtension, DefaultThreadStoreAuth, TiptapThreadStore, etc.) remain in @blocknote/core/comments.

Impact

Now, @blocknote/core does not depend on yjs or y-prosemirror unless using the new @blocknote/core/yjs. Reducing bundle size & separating concerns better.

Additional Notes

We should follow this up with some changes to the @blocknote/xl-ai & @blocknote/server-editor packages to remove dependency on the yjs package

Closes #2759

Summary by CodeRabbit

  • New Features

    • Added withCollaboration helper for streamlined editor configuration with real-time collaboration.
    • New dedicated subpackage for Yjs integration at @blocknote/core/yjs.
  • Bug Fixes & Improvements

    • Simplified comments API by flattening selection parameter structure.
    • Yjs packages are now optional peer dependencies, reducing bundle size for non-collaboration use cases.
    • Improved position mapping for accurate cursor/selection tracking across edits.
  • Documentation

    • Updated collaboration examples and guides to use new withCollaboration helper pattern.

Review Change Stack

@vercel
Copy link
Copy Markdown

vercel Bot commented May 13, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
blocknote Ready Ready Preview May 20, 2026 4:36pm
blocknote-website Error Error May 20, 2026 4:36pm

Request Review

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 13, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: e9cd0d48-e126-40ad-9cbe-ed94ba7188f6

📥 Commits

Reviewing files that changed from the base of the PR and between b1afce2 and a437d1c.

⛔ Files ignored due to path filters (5)
  • packages/core/src/yjs/extensions/__snapshots__/fork-yjs-snap-editor-forked.json is excluded by !**/__snapshots__/**
  • packages/core/src/yjs/extensions/__snapshots__/fork-yjs-snap-editor.json is excluded by !**/__snapshots__/**
  • packages/core/src/yjs/extensions/__snapshots__/fork-yjs-snap-forked.html is excluded by !**/__snapshots__/**
  • packages/core/src/yjs/extensions/__snapshots__/fork-yjs-snap.html is excluded by !**/__snapshots__/**
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (48)
  • docs/content/docs/features/collaboration/comments.mdx
  • docs/content/docs/features/collaboration/index.mdx
  • examples/07-collaboration/01-partykit/src/App.tsx
  • examples/07-collaboration/03-y-sweet/src/App.tsx
  • examples/07-collaboration/05-comments/src/App.tsx
  • examples/07-collaboration/06-comments-with-sidebar/src/App.tsx
  • examples/07-collaboration/07-ghost-writer/src/App.tsx
  • examples/07-collaboration/08-forking/src/App.tsx
  • examples/07-collaboration/09-comments-testing/src/App.tsx
  • packages/core/package.json
  • packages/core/src/api/positionMapping.test.ts
  • packages/core/src/api/positionMapping.ts
  • packages/core/src/comments/extension.ts
  • packages/core/src/comments/index.ts
  • packages/core/src/comments/threadstore/ThreadStore.ts
  • packages/core/src/editor/BlockNoteEditor.test.ts
  • packages/core/src/editor/BlockNoteEditor.ts
  • packages/core/src/editor/managers/ExtensionManager/extensions.ts
  • packages/core/src/editor/managers/StateManager.ts
  • packages/core/src/extensions/PositionMapping/PositionMapping.ts
  • packages/core/src/extensions/index.ts
  • packages/core/src/extensions/tiptap-extensions/UniqueID/UniqueID.ts
  • packages/core/src/extensions/tiptap-extensions/index.ts
  • packages/core/src/yjs/README.md
  • packages/core/src/yjs/comments/RESTYjsThreadStore.ts
  • packages/core/src/yjs/comments/YjsThreadStore.test.ts
  • packages/core/src/yjs/comments/YjsThreadStore.ts
  • packages/core/src/yjs/comments/YjsThreadStoreBase.ts
  • packages/core/src/yjs/comments/index.ts
  • packages/core/src/yjs/comments/yjsHelpers.ts
  • packages/core/src/yjs/extensions/FixupCreateAndFill.ts
  • packages/core/src/yjs/extensions/ForkYDoc.test.ts
  • packages/core/src/yjs/extensions/ForkYDoc.ts
  • packages/core/src/yjs/extensions/RelativePositionMapping.test.ts
  • packages/core/src/yjs/extensions/RelativePositionMapping.ts
  • packages/core/src/yjs/extensions/YCursorPlugin.ts
  • packages/core/src/yjs/extensions/YSync.ts
  • packages/core/src/yjs/extensions/YUndo.ts
  • packages/core/src/yjs/extensions/index.ts
  • packages/core/src/yjs/extensions/schemaMigration/SchemaMigration.ts
  • packages/core/src/yjs/extensions/schemaMigration/migrationRules/index.ts
  • packages/core/src/yjs/extensions/schemaMigration/migrationRules/migrationRule.ts
  • packages/core/src/yjs/extensions/schemaMigration/migrationRules/moveColorAttributes.test.ts
  • packages/core/src/yjs/extensions/schemaMigration/migrationRules/moveColorAttributes.ts
  • packages/core/src/yjs/index.ts
  • packages/xl-ai/package.json
  • packages/xl-ai/src/AIExtension.ts
  • packages/xl-ai/src/plugins/AgentCursorPlugin.ts
💤 Files with no reviewable changes (1)
  • packages/core/src/comments/index.ts
✅ Files skipped from review due to trivial changes (10)
  • packages/core/src/yjs/extensions/YCursorPlugin.ts
  • packages/core/src/yjs/extensions/YSync.ts
  • examples/07-collaboration/09-comments-testing/src/App.tsx
  • packages/core/src/yjs/comments/YjsThreadStore.test.ts
  • packages/core/src/yjs/extensions/ForkYDoc.ts
  • packages/core/src/yjs/comments/YjsThreadStoreBase.ts
  • packages/core/src/yjs/README.md
  • docs/content/docs/features/collaboration/comments.mdx
  • docs/content/docs/features/collaboration/index.mdx
  • packages/xl-ai/package.json

📝 Walkthrough

Walkthrough

This PR refactors BlockNote's Yjs collaboration architecture by introducing a withCollaboration() helper function, removing collaboration from direct editor options, implementing dedicated position-mapping extensions for both local and Yjs-synced scenarios, relocating thread store exports to the yjs package, and updating all examples and tests to use the new patterns.

Changes

Collaboration Architecture Refactoring

Layer / File(s) Summary
Core editor API: remove collaboration option, add helper factory
packages/core/src/editor/BlockNoteEditor.ts, packages/core/src/editor/managers/ExtensionManager/extensions.ts, packages/core/src/extensions/index.ts
Removes collaboration from BlockNoteEditorOptions, eliminates collaboration-specific initialization logic and schema workarounds, consolidates default extension setup to always include HistoryExtension and PositionMappingExtension, and prepares for new withCollaboration entry point.
withCollaboration helper and Yjs extensions orchestration
packages/core/src/yjs/extensions/index.ts
Implements withCollaboration() factory that merges options with CollaborationExtension, disables ProseMirror history, sets deterministic initial block ID, and updates extension composition to include FixupCreateAndFillExtension and RelativePositionMappingExtension.
Position mapping extensions: local and Yjs-synced implementations
packages/core/src/extensions/PositionMapping/PositionMapping.ts, packages/core/src/yjs/extensions/RelativePositionMapping.ts, packages/core/src/api/positionMapping.ts
Implements PositionMappingExtension for non-Yjs editors to share transaction mapping history across position-tracking closures, RelativePositionMappingExtension for synced documents using relative position conversion with fallback to local mapping, and refactors trackPosition API to delegate to extensions.
Thread store relocation and selection API simplification
packages/core/src/comments/*, packages/core/src/yjs/comments/*
Moves Yjs thread store exports from @blocknote/core/comments to @blocknote/core/yjs, simplifies addThreadToDocument signature from nested selection to flat { head; anchor }, removes Yjs selection computation from CommentsExtension, and updates all imports.
FixupCreateAndFillExtension and Yjs extension updates
packages/core/src/yjs/extensions/FixupCreateAndFill.ts, packages/core/src/yjs/extensions/ForkYDoc.ts, packages/core/src/yjs/extensions/YCursorPlugin.ts, packages/core/src/yjs/extensions/YSync.ts
Adds FixupCreateAndFillExtension to normalize initial document creation with deterministic block ID; updates Yjs extensions to import CollaborationOptions as type-only from new location.
Extension manager and tiptap-extensions refactoring
packages/core/src/extensions/tiptap-extensions/index.ts, packages/core/src/editor/managers/StateManager.ts, packages/core/src/extensions/tiptap-extensions/UniqueID/UniqueID.ts
Consolidates tiptap-extensions into pure barrel exports with new Link export, updates StateManager to treat undo/redo as HistoryExtension, and refactors UniqueID to remove filterTransaction and add type clarity.
Position mapping test suite: local and Yjs collaboration scenarios
packages/core/src/api/positionMapping.test.ts, packages/core/src/yjs/extensions/RelativePositionMapping.test.ts
Simplifies mount/unmount tests to verify trackPosition returns correct closures, updates teardowns to use editor.unmount(), and adds comprehensive Yjs position mapping tests for local edits, remote edits, sequential transactions, and bidirectional syncing.
BlockNoteEditor and ForkYDoc tests
packages/core/src/editor/BlockNoteEditor.test.ts, packages/core/src/yjs/extensions/ForkYDoc.test.ts
Updates Y.js collaboration tests to use withCollaboration pattern; changes block ID assertions from fixed snapshots to regex matching; updates all forking test cases to wrap collaboration via helper.
Documentation and collaboration examples: withCollaboration adoption
docs/content/docs/features/collaboration/*, examples/07-collaboration/*/src/App.tsx
Updates collaboration docs and all seven example files (PartyKit, Y-Sweet, comments, comments-with-sidebar, ghost-writer, forking, comments-testing) to demonstrate and use withCollaboration wrapper; adjusts thread store imports to yjs package.
Dependency restructuring and public API surface
packages/core/package.json, packages/core/src/yjs/README.md, packages/core/src/yjs/index.ts, packages/xl-ai/package.json
Moves Yjs dependencies to devDependencies and optional peerDependencies; adds yjs README documentation; expands yjs re-exports to include extensions and comments modules; removes Yjs dependency from xl-ai.
AI extension and agent cursor plugin: generic extension lookup
packages/xl-ai/src/AIExtension.ts, packages/xl-ai/src/plugins/AgentCursorPlugin.ts
Updates AIExtension to use generic type syntax for extension lookup and imports type from yjs package; inlines defaultSelectionBuilder function in agent cursor plugin instead of importing from y-prosemirror.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Suggested reviewers

  • matthewlipski

Poem

🐰 With withCollaboration we now dance,
Position mapping extensions prance,
Thread stores hop to yjs with glee,
Block IDs fixed deterministically,
A refactored feast for all to see! 🎉

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 35.71% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change: decoupling Yjs from @blocknote/core, which is the primary objective of this PR.
Description check ✅ Passed The description provides comprehensive coverage including summary, rationale, changes, impact, and additional notes. While some optional template sections (Screenshots/Video, Checklist) are omitted, all critical information is present.
Linked Issues check ✅ Passed The PR addresses #2759 (creation menu flickering on new docs with 'initialBlockId') by moving Yjs handling out of core and adding FixupCreateAndFillExtension to manage initial block ID setup.
Out of Scope Changes check ✅ Passed All changes are directly related to decoupling Yjs from core and organizing collaboration features into @blocknote/core/yjs. No unrelated refactoring or feature additions detected.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch decouple-yjs

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 ESLint

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

docs/content/docs/features/collaboration/comments.mdx

Oops! Something went wrong! :(

ESLint: 8.57.1

Error: ESLint configuration in --config is invalid:

  • Property "" is the wrong type (expected object but got "import nextVitals from \"eslint-config-next/core-web-vitals\"; import { defineConfig, globalIgnores } from \"eslint/config\";\nconst eslintConfig = defineConfig([ ...nextVitals, globalIgnores([ \".next/**\", \"out/**\", \"build/**\", \"next-env.d.ts\", \".source/**\", \"components/fumadocs/**\", \"components/example/generated/**\", ]), ]);\nexport default eslintConfig;").

    at ConfigValidator.validateConfigSchema (/node_modules/.pnpm/@eslint+eslintrc@2.1.4/node_modules/@eslint/eslintrc/dist/eslintrc.cjs:2177:19)
    at ConfigArrayFactory._normalizeConfigData (/node_modules/.pnpm/@eslint+eslintrc@2.1.4/node_modules/@eslint/eslintrc/dist/eslintrc.cjs:3019:19)
    at ConfigArrayFactory._loadConfigData (/node_modules/.pnpm/@eslint+eslintrc@2.1.4/node_modules/@eslint/eslintrc/dist/eslintrc.cjs:2984:21)
    at ConfigArrayFactory.loadFile (/node_modules/.pnpm/@eslint+eslintrc@2.1.4/node_modules/@eslint/eslintrc/dist/eslintrc.cjs:2850:40)
    at createCLIConfigArray (/node_modules/.pnpm/@eslint+eslintrc@2.1.4/node_modules/@eslint/eslintrc/dist/eslintrc.cjs:3660:35)
    at new CascadingConfigArrayFactory (/node_modules/.pnpm/@eslint+eslintrc@2.1.4/node_modules/@eslint/eslintrc/dist/eslintrc.cjs:3735:29)
    at new CLIEngine (/node_modules/.pnpm/eslint@8.57.1/node_modules/eslint/lib/cli-engine/cli-engine.js:617:36)
    at new ESLint (/node_modules/.pnpm/eslint@8.57.1/node_modules/eslint/lib/eslint/eslint.js:430:27)
    at Object.execute (/node_modules/.pnpm/eslint@8.57.1/node_modules/eslint/lib/cli.js:410:24)
    at async main (/node_modules/.pnpm/eslint@8.57.1/node_modules/eslint/bin/eslint.js:152:22)

docs/content/docs/features/collaboration/index.mdx

Oops! Something went wrong! :(

ESLint: 8.57.1

Error: ESLint configuration in --config is invalid:

  • Property "" is the wrong type (expected object but got "import nextVitals from \"eslint-config-next/core-web-vitals\"; import { defineConfig, globalIgnores } from \"eslint/config\";\nconst eslintConfig = defineConfig([ ...nextVitals, globalIgnores([ \".next/**\", \"out/**\", \"build/**\", \"next-env.d.ts\", \".source/**\", \"components/fumadocs/**\", \"components/example/generated/**\", ]), ]);\nexport default eslintConfig;").

    at ConfigValidator.validateConfigSchema (/node_modules/.pnpm/@eslint+eslintrc@2.1.4/node_modules/@eslint/eslintrc/dist/eslintrc.cjs:2177:19)
    at ConfigArrayFactory._normalizeConfigData (/node_modules/.pnpm/@eslint+eslintrc@2.1.4/node_modules/@eslint/eslintrc/dist/eslintrc.cjs:3019:19)
    at ConfigArrayFactory._loadConfigData (/node_modules/.pnpm/@eslint+eslintrc@2.1.4/node_modules/@eslint/eslintrc/dist/eslintrc.cjs:2984:21)
    at ConfigArrayFactory.loadFile (/node_modules/.pnpm/@eslint+eslintrc@2.1.4/node_modules/@eslint/eslintrc/dist/eslintrc.cjs:2850:40)
    at createCLIConfigArray (/node_modules/.pnpm/@eslint+eslintrc@2.1.4/node_modules/@eslint/eslintrc/dist/eslintrc.cjs:3660:35)
    at new CascadingConfigArrayFactory (/node_modules/.pnpm/@eslint+eslintrc@2.1.4/node_modules/@eslint/eslintrc/dist/eslintrc.cjs:3735:29)
    at new CLIEngine (/node_modules/.pnpm/eslint@8.57.1/node_modules/eslint/lib/cli-engine/cli-engine.js:617:36)
    at new ESLint (/node_modules/.pnpm/eslint@8.57.1/node_modules/eslint/lib/eslint/eslint.js:430:27)
    at Object.execute (/node_modules/.pnpm/eslint@8.57.1/node_modules/eslint/lib/cli.js:410:24)
    at async main (/node_modules/.pnpm/eslint@8.57.1/node_modules/eslint/bin/eslint.js:152:22)

examples/07-collaboration/01-partykit/src/App.tsx

Oops! Something went wrong! :(

ESLint: 8.57.1

ReferenceError: Cannot read config file: /examples/.eslintrc.js
Error: module is not defined in ES module scope
This file is being treated as an ES module because it has a '.js' file extension and '/package.json' contains "type": "module". To treat it as a CommonJS script, rename it to use the '.cjs' file extension.
at file:///examples/.eslintrc.js:1:1
at ModuleJobSync.runSync (node:internal/modules/esm/module_job:541:37)
at ModuleLoader.importSyncForRequire (node:internal/modules/esm/loader:366:47)
at loadESMFromCJS (node:internal/modules/cjs/loader:1649:24)
at Module._compile (node:internal/modules/cjs/loader:1812:5)
at Object..js (node:internal/modules/cjs/loader:1961:10)
at Module.load (node:internal/modules/cjs/loader:1553:32)
at Module._load (node:internal/modules/cjs/loader:1355:12)
at wrapModuleLoad (node:internal/modules/cjs/loader:255:19)
at Module.require (node:internal/modules/cjs/loader:1576:12)

  • 6 others

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@pkg-pr-new
Copy link
Copy Markdown

pkg-pr-new Bot commented May 13, 2026

Open in StackBlitz

@blocknote/ariakit

npm i https://pkg.pr.new/@blocknote/ariakit@2741

@blocknote/code-block

npm i https://pkg.pr.new/@blocknote/code-block@2741

@blocknote/core

npm i https://pkg.pr.new/@blocknote/core@2741

@blocknote/mantine

npm i https://pkg.pr.new/@blocknote/mantine@2741

@blocknote/react

npm i https://pkg.pr.new/@blocknote/react@2741

@blocknote/server-util

npm i https://pkg.pr.new/@blocknote/server-util@2741

@blocknote/shadcn

npm i https://pkg.pr.new/@blocknote/shadcn@2741

@blocknote/xl-ai

npm i https://pkg.pr.new/@blocknote/xl-ai@2741

@blocknote/xl-docx-exporter

npm i https://pkg.pr.new/@blocknote/xl-docx-exporter@2741

@blocknote/xl-email-exporter

npm i https://pkg.pr.new/@blocknote/xl-email-exporter@2741

@blocknote/xl-multi-column

npm i https://pkg.pr.new/@blocknote/xl-multi-column@2741

@blocknote/xl-odt-exporter

npm i https://pkg.pr.new/@blocknote/xl-odt-exporter@2741

@blocknote/xl-pdf-exporter

npm i https://pkg.pr.new/@blocknote/xl-pdf-exporter@2741

commit: a437d1c

selection: {
prosemirror: {
head: number;
anchor: number;
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this was changed to not depend on yjs positions in the API. There are still ways to get it, this was just exposed in the API for some reason

@nperez0111 nperez0111 marked this pull request as ready for review May 20, 2026 15:19
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 11

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/core/package.json`:
- Around line 131-135: Update package.json to mark the yjs-related peer deps as
optional by adding entries for "yjs", "y-prosemirror", and "y-protocols" under
peerDependenciesMeta with "optional": true; locate the existing peerDependencies
block (keys "yjs", "y-prosemirror", "y-protocols") and add a
peerDependenciesMeta object that sets each of those package names to optional so
consumers who don't use `@blocknote/core/yjs` won't be forced to install them.

In `@packages/core/src/comments/extension.ts`:
- Around line 353-356: The current call passes a ProseMirror Selection object
into threadStore.addThreadToDocument via editor.transact((tr) => tr.selection);
change this to extract the primitive positions and pass a plain object { head,
anchor } (e.g., const sel = editor.transact(tr => tr.selection); then call
threadStore.addThreadToDocument({ threadId: thread.id, selection: { head:
sel.head, anchor: sel.anchor } }) so the payload matches ThreadStore.ts's
expected { head: number; anchor: number } shape and avoids serializing the
Selection instance.

In `@packages/core/src/editor/performance.test.ts`:
- Line 40: The test suite is permanently disabled via describe.skip for
"Performance: transaction processing scales sub-linearly (`#2595`)"; instead, gate
it behind a CI/environment flag so it runs only in dedicated perf jobs. Replace
the describe.skip usage for the suite named "Performance: transaction processing
scales sub-linearly (`#2595`)" with a conditional invocation that uses an env/CI
variable (e.g., process.env.RUN_PERF_TESTS or CI_PERF_JOB) to choose between
describe and describe.skip (or wrap the body in if (flag) { describe(...) } ),
so CI/CD can enable the suite in perf runs while keeping it skipped by default
in regular jobs.

In `@packages/core/src/extensions/PositionMapping/PositionMapping.ts`:
- Around line 14-22: The current code relies on FinalizationRegistry and
numInstances to reset the shared Mapping, which is non-deterministic and causes
mapping.maps to grow; add an explicit lifecycle API instead: implement a
dispose() method on the PositionMapping/extension instance that decrements
numInstances and, when it reaches zero, resets mapping = new Mapping() (and
prunes mapping.maps used by map()), and ensure the constructor/initalizer
increments numInstances; remove or keep FinalizationRegistry as a fallback but
do not rely on it as the primary cleanup mechanism; optionally expose a
resetMapping() or maxHistory config to proactively trim mapping.maps during
map() calls.

In `@packages/core/src/yjs/extensions/FixupCreateAndFill.ts`:
- Around line 19-22: The code assumes jsonNode.content[0].content[0].attrs
exists before setting attrs.id and can throw; before mutating, add a guarded
path check on jsonNode (check jsonNode.content is array,
jsonNode.content[0].content is array, jsonNode.content[0].content[0].attrs is an
object) and only set jsonNode.content[0].content[0].attrs.id = "initialBlockId"
when those checks pass; if missing, either create the missing objects (content
arrays and attrs object) so Node.fromJSON(editor.pmSchema, jsonNode) receives a
consistent shape, or skip setting id but still proceed to assign cache from
Node.fromJSON using the safe jsonNode.
- Around line 13-16: The current cache used in the patched createAndFill always
returns the first created node regardless of arguments; change the caching logic
inside the wrapper around oldCreateAndFill so the cached value is only returned
when the wrapper is invoked with no arguments (e.g., args.length === 0 or all
args are undefined) and otherwise call oldCreateAndFill with the provided args
to produce a fresh node; update the code around oldCreateAndFill, the cache
variable, and the return path that currently does "if (cache) return cache" so
it first checks that args are the default/empty case before returning the cached
node.

In `@packages/core/src/yjs/extensions/index.ts`:
- Around line 72-83: The return currently unconditionally overwrites
initialContent with a deterministic paragraph; change it to preserve
caller-supplied content by only injecting the deterministic block when none was
provided (use options.initialContent if present, otherwise fallback to [{ type:
"paragraph", id: "initialBlockId" }]); update the object returned by
withCollaboration (the function building the extensions config) so
initialContent is set to options.initialContent ?? [{ type: "paragraph", id:
"initialBlockId" }], leaving everything else (extensions, disableExtensions)
unchanged.

In `@packages/core/src/yjs/extensions/RelativePositionMapping.test.ts`:
- Around line 80-82: The first test unmounts localEditor and remoteEditor but
fails to destroy the Yjs documents, leaving observers alive; update the test
teardown to call ydoc.destroy() and remoteYdoc.destroy() (or move both unmount
and destruction into the existing afterEach) so that the variables ydoc and
remoteYdoc used in RelativePositionMapping.test.ts are explicitly destroyed
after the test completes.
- Around line 337-339: The test "from a remote transaction" is incorrectly
performing the edit via localEditor, so it doesn't exercise remote-origin
updates; change the insertion to be performed by the remote side (use
remoteEditor._tiptapEditor.commands.insertContentAt or the remoteEditor API used
elsewhere in this test suite) instead of localEditor, ensuring the test inserts
"Test " at position 3 through remoteEditor so the local position mapping is
updated by a remote transaction.

In `@packages/core/src/yjs/extensions/RelativePositionMapping.ts`:
- Around line 27-35: The pre-sync fallback currently returns undefined when the
"positionMapping" extension is disabled, producing an invalid getter for callers
of trackPosition; update the fallback in RelativePositionMapping (the branch
that checks ySyncPluginState.binding.type.length === 0) to detect if
editor.getExtension("positionMapping") is missing and either throw a clear Error
(e.g. "positionMapping extension is disabled; cannot map position before sync")
or return a valid getter function (() => number) that maps to a safe position
(for example the document end via editor.state.doc.content.size); modify the
code around PositionMappingExtension.mapPosition so callers of
mapPosition/trackPosition always receive a function or an explicit thrown error.

In `@packages/xl-ai/src/AIExtension.ts`:
- Line 10: The import of ForkYDocExtension is currently a type-only import but
is later used with typeof (e.g., in the generic parameters referencing
ForkYDocExtension), so change the line importing ForkYDocExtension to a regular
value import by removing the `type` keyword; this ensures typeof
ForkYDocExtension is a valid value reference wherever it's used in
AIExtension.ts (references to ForkYDocExtension in the generics).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 4cb67314-51fc-4bd5-8003-a63ad5625713

📥 Commits

Reviewing files that changed from the base of the PR and between 1a65508 and b1afce2.

⛔ Files ignored due to path filters (5)
  • packages/core/src/yjs/extensions/__snapshots__/fork-yjs-snap-editor-forked.json is excluded by !**/__snapshots__/**
  • packages/core/src/yjs/extensions/__snapshots__/fork-yjs-snap-editor.json is excluded by !**/__snapshots__/**
  • packages/core/src/yjs/extensions/__snapshots__/fork-yjs-snap-forked.html is excluded by !**/__snapshots__/**
  • packages/core/src/yjs/extensions/__snapshots__/fork-yjs-snap.html is excluded by !**/__snapshots__/**
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (49)
  • docs/content/docs/features/collaboration/comments.mdx
  • docs/content/docs/features/collaboration/index.mdx
  • examples/07-collaboration/01-partykit/src/App.tsx
  • examples/07-collaboration/03-y-sweet/src/App.tsx
  • examples/07-collaboration/05-comments/src/App.tsx
  • examples/07-collaboration/06-comments-with-sidebar/src/App.tsx
  • examples/07-collaboration/07-ghost-writer/src/App.tsx
  • examples/07-collaboration/08-forking/src/App.tsx
  • examples/07-collaboration/09-comments-testing/src/App.tsx
  • packages/core/package.json
  • packages/core/src/api/positionMapping.test.ts
  • packages/core/src/api/positionMapping.ts
  • packages/core/src/comments/extension.ts
  • packages/core/src/comments/index.ts
  • packages/core/src/comments/threadstore/ThreadStore.ts
  • packages/core/src/editor/BlockNoteEditor.test.ts
  • packages/core/src/editor/BlockNoteEditor.ts
  • packages/core/src/editor/managers/ExtensionManager/extensions.ts
  • packages/core/src/editor/managers/StateManager.ts
  • packages/core/src/editor/performance.test.ts
  • packages/core/src/extensions/PositionMapping/PositionMapping.ts
  • packages/core/src/extensions/index.ts
  • packages/core/src/extensions/tiptap-extensions/UniqueID/UniqueID.ts
  • packages/core/src/extensions/tiptap-extensions/index.ts
  • packages/core/src/yjs/README.md
  • packages/core/src/yjs/comments/RESTYjsThreadStore.ts
  • packages/core/src/yjs/comments/YjsThreadStore.test.ts
  • packages/core/src/yjs/comments/YjsThreadStore.ts
  • packages/core/src/yjs/comments/YjsThreadStoreBase.ts
  • packages/core/src/yjs/comments/index.ts
  • packages/core/src/yjs/comments/yjsHelpers.ts
  • packages/core/src/yjs/extensions/FixupCreateAndFill.ts
  • packages/core/src/yjs/extensions/ForkYDoc.test.ts
  • packages/core/src/yjs/extensions/ForkYDoc.ts
  • packages/core/src/yjs/extensions/RelativePositionMapping.test.ts
  • packages/core/src/yjs/extensions/RelativePositionMapping.ts
  • packages/core/src/yjs/extensions/YCursorPlugin.ts
  • packages/core/src/yjs/extensions/YSync.ts
  • packages/core/src/yjs/extensions/YUndo.ts
  • packages/core/src/yjs/extensions/index.ts
  • packages/core/src/yjs/extensions/schemaMigration/SchemaMigration.ts
  • packages/core/src/yjs/extensions/schemaMigration/migrationRules/index.ts
  • packages/core/src/yjs/extensions/schemaMigration/migrationRules/migrationRule.ts
  • packages/core/src/yjs/extensions/schemaMigration/migrationRules/moveColorAttributes.test.ts
  • packages/core/src/yjs/extensions/schemaMigration/migrationRules/moveColorAttributes.ts
  • packages/core/src/yjs/index.ts
  • packages/xl-ai/package.json
  • packages/xl-ai/src/AIExtension.ts
  • packages/xl-ai/src/plugins/AgentCursorPlugin.ts
💤 Files with no reviewable changes (1)
  • packages/core/src/comments/index.ts

Comment thread packages/core/package.json
Comment thread packages/core/src/comments/extension.ts
}

describe("Performance: transaction processing scales sub-linearly (#2595)", () => {
describe.skip("Performance: transaction processing scales sub-linearly (#2595)", () => {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Avoid permanently disabling the regression suite.

Line 40 hard-disables all #2595 regression checks, removing CI guardrails for known performance regressions. Prefer conditional gating so the suite can run in dedicated perf jobs.

Proposed change
-describe.skip("Performance: transaction processing scales sub-linearly (`#2595`)", () => {
+const describePerf =
+  process.env.BN_RUN_PERF_TESTS === "1" ? describe : describe.skip;
+
+describePerf("Performance: transaction processing scales sub-linearly (`#2595`)", () => {
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
describe.skip("Performance: transaction processing scales sub-linearly (#2595)", () => {
const describePerf =
process.env.BN_RUN_PERF_TESTS === "1" ? describe : describe.skip;
describePerf("Performance: transaction processing scales sub-linearly (`#2595`)", () => {
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/core/src/editor/performance.test.ts` at line 40, The test suite is
permanently disabled via describe.skip for "Performance: transaction processing
scales sub-linearly (`#2595`)"; instead, gate it behind a CI/environment flag so
it runs only in dedicated perf jobs. Replace the describe.skip usage for the
suite named "Performance: transaction processing scales sub-linearly (`#2595`)"
with a conditional invocation that uses an env/CI variable (e.g.,
process.env.RUN_PERF_TESTS or CI_PERF_JOB) to choose between describe and
describe.skip (or wrap the body in if (flag) { describe(...) } ), so CI/CD can
enable the suite in perf runs while keeping it skipped by default in regular
jobs.

Comment thread packages/core/src/extensions/PositionMapping/PositionMapping.ts
Comment on lines +13 to +16
if (cache) {
return cache;
}
const ret = oldCreateAndFill.apply(editor.pmSchema.nodes.doc, args)!;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify whether createAndFill is called with non-empty args in this repo.
rg -nP '\bcreateAndFill\s*\(' -C2

Repository: TypeCellOS/BlockNote

Length of output: 2267


🏁 Script executed:

cat packages/core/src/yjs/extensions/FixupCreateAndFill.ts

Repository: TypeCellOS/BlockNote

Length of output: 1503


Scope the cache to prevent returning it for non-initialization calls.

The cache ignores args after the first call and always returns the same node. If createAndFill is invoked later with different arguments, it will incorrectly return the initial cached node instead of creating a new one with the expected structure.

Scope the cache to only apply when no arguments are passed (the default initialization case):

Proposed fix
-    editor.pmSchema.nodes.doc.createAndFill = ((...args: any) => {
-      if (cache) {
+    editor.pmSchema.nodes.doc.createAndFill = ((...args: any) => {
+      const isDefaultInitCall = args.length === 0;
+      if (cache && isDefaultInitCall) {
         return cache;
       }
       const ret = oldCreateAndFill.apply(editor.pmSchema.nodes.doc, args)!;
+      if (!isDefaultInitCall) {
+        return ret;
+      }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/core/src/yjs/extensions/FixupCreateAndFill.ts` around lines 13 - 16,
The current cache used in the patched createAndFill always returns the first
created node regardless of arguments; change the caching logic inside the
wrapper around oldCreateAndFill so the cached value is only returned when the
wrapper is invoked with no arguments (e.g., args.length === 0 or all args are
undefined) and otherwise call oldCreateAndFill with the provided args to produce
a fresh node; update the code around oldCreateAndFill, the cache variable, and
the return path that currently does "if (cache) return cache" so it first checks
that args are the default/empty case before returning the cached node.

Comment on lines +72 to +83
return {
...options,
extensions: [
...(options.extensions ?? []),
CollaborationExtension(options.collaboration),
],
// We disable the default prosemirror history plugin, since it's not compatible with yjs
disableExtensions: ["history", ...(options.disableExtensions ?? [])],
// We don't want the default initial content, since it will generate a random id for the initial block on each client,
// leading to conflicts when syncing happens afterwards.
initialContent: [{ type: "paragraph", id: "initialBlockId" }],
};
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Don't overwrite caller-supplied initialContent.

withCollaboration() now discards any explicit seed content and always replaces it with a single "initialBlockId" paragraph. That makes migration lossy for callers that bootstrap a brand-new shared doc with real initial content. Preserve the caller's value and only inject the deterministic block when initialContent is absent.

Proposed fix
   return {
     ...options,
     extensions: [
       ...(options.extensions ?? []),
       CollaborationExtension(options.collaboration),
     ],
     // We disable the default prosemirror history plugin, since it's not compatible with yjs
     disableExtensions: ["history", ...(options.disableExtensions ?? [])],
     // We don't want the default initial content, since it will generate a random id for the initial block on each client,
     // leading to conflicts when syncing happens afterwards.
-    initialContent: [{ type: "paragraph", id: "initialBlockId" }],
+    initialContent:
+      options.initialContent ??
+      [{ type: "paragraph", id: "initialBlockId" }],
   };
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/core/src/yjs/extensions/index.ts` around lines 72 - 83, The return
currently unconditionally overwrites initialContent with a deterministic
paragraph; change it to preserve caller-supplied content by only injecting the
deterministic block when none was provided (use options.initialContent if
present, otherwise fallback to [{ type: "paragraph", id: "initialBlockId" }]);
update the object returned by withCollaboration (the function building the
extensions config) so initialContent is set to options.initialContent ?? [{
type: "paragraph", id: "initialBlockId" }], leaving everything else (extensions,
disableExtensions) unchanged.

Comment thread packages/core/src/yjs/extensions/RelativePositionMapping.test.ts
Comment thread packages/core/src/yjs/extensions/RelativePositionMapping.test.ts Outdated
Comment thread packages/core/src/yjs/extensions/RelativePositionMapping.ts
ShowSelectionExtension,
} from "@blocknote/core/extensions";
import { ShowSelectionExtension } from "@blocknote/core/extensions";
import type { ForkYDocExtension } from "@blocknote/core/yjs";
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify declaration form of ForkYDocExtension and getExtension generic expectations
rg -n -C3 '\bForkYDocExtension\b' packages/core/src packages/xl-ai/src
echo "----"
rg -n -C3 '\bgetExtension\s*<' packages/core/src packages/xl-ai/src

Repository: TypeCellOS/BlockNote

Length of output: 14815


Change import type to a regular import; typeof cannot reference type-only imports.

ForkYDocExtension is imported with import type on line 10, but typeof ForkYDocExtension in the generic parameters on lines 222, 242, and 385 requires a value reference. Remove the type keyword so the import becomes a regular value import that can be used with typeof.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/xl-ai/src/AIExtension.ts` at line 10, The import of
ForkYDocExtension is currently a type-only import but is later used with typeof
(e.g., in the generic parameters referencing ForkYDocExtension), so change the
line importing ForkYDocExtension to a regular value import by removing the
`type` keyword; this ensures typeof ForkYDocExtension is a valid value reference
wherever it's used in AIExtension.ts (references to ForkYDocExtension in the
generics).

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.

Initial doc creation menu + not working

1 participant