From 4c5e4988401315273917b6857e89c01f747350ba Mon Sep 17 00:00:00 2001 From: Nick the Sick Date: Mon, 20 Apr 2026 11:32:35 +0200 Subject: [PATCH 1/6] ci(fresh-install): widen prod-dep update to examples and playground MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Without this, pnpm update --prod only bumped the publishable @blocknote/* packages, leaving examples/* and playground pinned to older versions. For peer-dep'd libs like @tiptap/core + @tiptap/pm that produced two virtual- store copies keyed on different peer versions, which TypeScript treated as unrelated types — breaking example-editor:build with TS2322. Extending the filter keeps every app that consumes @blocknote/* in lockstep with the published packages. docs/shared/tests/fumadocs stay excluded. Also add this branch to the push triggers so the PR exercises the workflow. Co-Authored-By: Claude Opus 4.7 (1M context) --- .github/workflows/fresh-install-tests.yml | 38 ++++++++++++----------- 1 file changed, 20 insertions(+), 18 deletions(-) diff --git a/.github/workflows/fresh-install-tests.yml b/.github/workflows/fresh-install-tests.yml index 6596724f76..faa66c8302 100644 --- a/.github/workflows/fresh-install-tests.yml +++ b/.github/workflows/fresh-install-tests.yml @@ -14,6 +14,7 @@ on: push: branches: - package-upgrades + - feat/fresh-install-debug schedule: - cron: "0 2 * * *" # Daily at 02:00 UTC workflow_dispatch: # Allow manual runs @@ -47,25 +48,26 @@ jobs: run: pnpm install - id: update_prod_deps - name: Update prod deps of published packages - # Resolves production dependencies of every published (non-private) - # workspace package to the latest version within their declared semver - # ranges. This simulates what a user gets when running - # `npm install @blocknote/react` in a fresh project. + name: Update prod deps of packages, examples, and playground + # Resolves production dependencies to the latest versions within declared + # semver ranges for: + # - packages/* – the published @blocknote/* packages + # - examples/*/* – the individual example apps + # - playground – the example-editor app that bundles the examples + # Examples and playground are included so shared peer-dep'd libs + # (e.g. @tiptap/core + @tiptap/pm) resolve to the same version across + # publishable packages and the apps consuming them; otherwise pnpm ends + # up with two virtual-store copies keyed on different peer versions and + # TypeScript treats their exports as unrelated types. + # docs/, shared/, tests/, fumadocs/ are deliberately excluded — they're + # not part of the "fresh npm install @blocknote/react" user experience + # this workflow simulates. # DevDependencies are left at their lockfile versions. - run: | - FILTERS=$(node -e " - const fs = require('fs'); - const path = require('path'); - fs.readdirSync('packages').forEach(dir => { - try { - const pkg = JSON.parse(fs.readFileSync(path.join('packages', dir, 'package.json'), 'utf8')); - if (!pkg.private && pkg.name) process.stdout.write('--filter ' + pkg.name + ' '); - } catch {} - }); - ") - echo "Updating prod deps for: $FILTERS" - eval pnpm update --prod $FILTERS + run: >- + pnpm update --prod + --filter "./packages/*" + --filter "./examples/*/*" + --filter "./playground" - id: build_packages name: Build packages From aefa9d2be49b52c22f0aaa6fa66c0dad8349e153 Mon Sep 17 00:00:00 2001 From: Nick the Sick Date: Mon, 20 Apr 2026 12:05:44 +0200 Subject: [PATCH 2/6] ci(fresh-install): use pnpm dedupe instead of widening the update filter MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The previous commit widened the update filter to include examples/* and playground so @tiptap/core + @tiptap/pm would resolve uniformly across the workspace. But pnpm update on examples/* rewrites "@blocknote/*": "latest" to "workspace:^", which means CI stops testing what CodeSandbox users actually see (they pull the published @blocknote/* from npm via "latest"). Switch to a two-step approach: keep the original publishable-only filter so examples' package.json stays untouched, then run pnpm dedupe afterward. Dedupe only rewrites the lockfile and collapses duplicate transitive resolutions — zero package.json churn, and both the TS2322 build failure and the CodeSandbox parity are preserved. Co-Authored-By: Claude Opus 4.7 (1M context) --- .github/workflows/fresh-install-tests.yml | 52 ++++++++++++++--------- 1 file changed, 33 insertions(+), 19 deletions(-) diff --git a/.github/workflows/fresh-install-tests.yml b/.github/workflows/fresh-install-tests.yml index faa66c8302..6e6dfcb585 100644 --- a/.github/workflows/fresh-install-tests.yml +++ b/.github/workflows/fresh-install-tests.yml @@ -48,26 +48,38 @@ jobs: run: pnpm install - id: update_prod_deps - name: Update prod deps of packages, examples, and playground - # Resolves production dependencies to the latest versions within declared - # semver ranges for: - # - packages/* – the published @blocknote/* packages - # - examples/*/* – the individual example apps - # - playground – the example-editor app that bundles the examples - # Examples and playground are included so shared peer-dep'd libs - # (e.g. @tiptap/core + @tiptap/pm) resolve to the same version across - # publishable packages and the apps consuming them; otherwise pnpm ends - # up with two virtual-store copies keyed on different peer versions and - # TypeScript treats their exports as unrelated types. - # docs/, shared/, tests/, fumadocs/ are deliberately excluded — they're - # not part of the "fresh npm install @blocknote/react" user experience - # this workflow simulates. + name: Update prod deps of published packages + # Resolves production dependencies of every published (non-private) + # workspace package to the latest version within their declared semver + # ranges. This simulates what a user gets when running + # `npm install @blocknote/react` in a fresh project. # DevDependencies are left at their lockfile versions. - run: >- - pnpm update --prod - --filter "./packages/*" - --filter "./examples/*/*" - --filter "./playground" + run: | + FILTERS=$(node -e " + const fs = require('fs'); + const path = require('path'); + fs.readdirSync('packages').forEach(dir => { + try { + const pkg = JSON.parse(fs.readFileSync(path.join('packages', dir, 'package.json'), 'utf8')); + if (!pkg.private && pkg.name) process.stdout.write('--filter ' + pkg.name + ' '); + } catch {} + }); + ") + echo "Updating prod deps for: $FILTERS" + eval pnpm update --prod $FILTERS + + - id: dedupe_deps + name: Dedupe transitive dependencies + # After bumping the publishable packages' prod deps, collapse any + # duplicate transitive resolutions (e.g. @tiptap/core + @tiptap/pm) + # that would otherwise differ between the updated publishable packages + # and the un-updated examples/playground. Without this, TypeScript + # treats the two copies' exports as unrelated types and example-editor + # fails to build (TS2322 on Extension vs AnyExtension). + # Dedupe only rewrites the lockfile — it does NOT modify package.json, + # so the examples' "@blocknote/*": "latest" specs (which is what + # CodeSandbox users see) stay intact. + run: pnpm dedupe - id: build_packages name: Build packages @@ -108,6 +120,8 @@ jobs: failed_step="Install dependencies" elif [ "${{ steps.update_prod_deps.outcome }}" = "failure" ]; then failed_step="Update prod deps of published packages" + elif [ "${{ steps.dedupe_deps.outcome }}" = "failure" ]; then + failed_step="Dedupe transitive dependencies" elif [ "${{ steps.build_packages.outcome }}" = "failure" ]; then failed_step="Build packages" elif [ "${{ steps.run_unit_tests.outcome }}" = "failure" ]; then From 25c26042d8d812b565f0690ad7a56607d9e08978 Mon Sep 17 00:00:00 2001 From: Nick the Sick Date: Mon, 20 Apr 2026 12:41:00 +0200 Subject: [PATCH 3/6] test(core): destroy editors in IndexingPlugin.test afterEach The tests created BlockNoteEditor instances via createEditor() but never destroyed them. prosemirror-view's DOMObserver keeps a setTimeout alive waiting to flush pending mutations; when vitest tears down jsdom between test files the timer fires against a torn-down document and throws `ReferenceError: document is not defined`. Vitest catches it as an unhandled error and fails the whole run even when all 16 assertions pass. Local runs usually hit the lucky teardown ordering and pass; CI surfaces the race intermittently. Tracking created editors and calling _tiptapEditor.destroy() in afterEach ensures the DOMObserver timers are canceled before jsdom goes away. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../NumberedListItem/IndexingPlugin.test.ts | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/packages/core/src/blocks/ListItem/NumberedListItem/IndexingPlugin.test.ts b/packages/core/src/blocks/ListItem/NumberedListItem/IndexingPlugin.test.ts index d10d4b08f1..63bce99a95 100644 --- a/packages/core/src/blocks/ListItem/NumberedListItem/IndexingPlugin.test.ts +++ b/packages/core/src/blocks/ListItem/NumberedListItem/IndexingPlugin.test.ts @@ -1,5 +1,5 @@ import { Selection } from "prosemirror-state"; -import { describe, expect, it } from "vitest"; +import { afterEach, describe, expect, it } from "vitest"; import { BlockNoteEditor } from "../../../editor/BlockNoteEditor.js"; @@ -9,9 +9,22 @@ import { BlockNoteEditor } from "../../../editor/BlockNoteEditor.js"; const PLUGIN_KEY = "numbered-list-indexing-decorations$"; +// Track editors created in each test so we can destroy them in afterEach — +// otherwise prosemirror-view's DOMObserver leaves a setTimeout alive that +// fires after vitest tears down jsdom, throwing +// `ReferenceError: document is not defined` and failing the run. +const activeEditors: BlockNoteEditor[] = []; + +afterEach(() => { + while (activeEditors.length) { + activeEditors.pop()!._tiptapEditor.destroy(); + } +}); + function createEditor() { const editor = BlockNoteEditor.create(); editor.mount(document.createElement("div")); + activeEditors.push(editor); return editor; } From b0eb56923f32dccbfdc5408447faf0240c694ef9 Mon Sep 17 00:00:00 2001 From: Nick the Sick Date: Mon, 20 Apr 2026 12:56:30 +0200 Subject: [PATCH 4/6] test(core): use editor.unmount() instead of _tiptapEditor.destroy() unmount() is the public API for tearing down a BlockNoteEditor and handles portalElement cleanup in addition to the underlying prosemirror teardown. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../blocks/ListItem/NumberedListItem/IndexingPlugin.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/core/src/blocks/ListItem/NumberedListItem/IndexingPlugin.test.ts b/packages/core/src/blocks/ListItem/NumberedListItem/IndexingPlugin.test.ts index 63bce99a95..433fa47806 100644 --- a/packages/core/src/blocks/ListItem/NumberedListItem/IndexingPlugin.test.ts +++ b/packages/core/src/blocks/ListItem/NumberedListItem/IndexingPlugin.test.ts @@ -9,7 +9,7 @@ import { BlockNoteEditor } from "../../../editor/BlockNoteEditor.js"; const PLUGIN_KEY = "numbered-list-indexing-decorations$"; -// Track editors created in each test so we can destroy them in afterEach — +// Track editors created in each test so we can unmount them in afterEach — // otherwise prosemirror-view's DOMObserver leaves a setTimeout alive that // fires after vitest tears down jsdom, throwing // `ReferenceError: document is not defined` and failing the run. @@ -17,7 +17,7 @@ const activeEditors: BlockNoteEditor[] = []; afterEach(() => { while (activeEditors.length) { - activeEditors.pop()!._tiptapEditor.destroy(); + activeEditors.pop()!.unmount(); } }); From d1178c4df6e799ee75680c8c777168168b4b5f51 Mon Sep 17 00:00:00 2001 From: Nick the Sick Date: Mon, 20 Apr 2026 13:39:46 +0200 Subject: [PATCH 5/6] ci(fresh-install): run on pushes to main only Co-Authored-By: Claude Opus 4.7 (1M context) --- .github/workflows/fresh-install-tests.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/fresh-install-tests.yml b/.github/workflows/fresh-install-tests.yml index 6e6dfcb585..91a6a8c07e 100644 --- a/.github/workflows/fresh-install-tests.yml +++ b/.github/workflows/fresh-install-tests.yml @@ -13,8 +13,7 @@ name: Fresh Install Tests on: push: branches: - - package-upgrades - - feat/fresh-install-debug + - main schedule: - cron: "0 2 * * *" # Daily at 02:00 UTC workflow_dispatch: # Allow manual runs From 47314bf447f1561e92c57598bbd0c04b73cf75e9 Mon Sep 17 00:00:00 2001 From: Nick the Sick Date: Mon, 20 Apr 2026 13:41:55 +0200 Subject: [PATCH 6/6] ci(fresh-install): drop push trigger, cron + manual only Co-Authored-By: Claude Opus 4.7 (1M context) --- .github/workflows/fresh-install-tests.yml | 3 --- 1 file changed, 3 deletions(-) diff --git a/.github/workflows/fresh-install-tests.yml b/.github/workflows/fresh-install-tests.yml index 91a6a8c07e..6d6ed4a452 100644 --- a/.github/workflows/fresh-install-tests.yml +++ b/.github/workflows/fresh-install-tests.yml @@ -11,9 +11,6 @@ name: Fresh Install Tests # so test tooling churn doesn't cause false positives. on: - push: - branches: - - main schedule: - cron: "0 2 * * *" # Daily at 02:00 UTC workflow_dispatch: # Allow manual runs