From 251498047b5e08d507152a8a65a0b47c4313f7ed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20H=C3=BCning?= Date: Sun, 6 Sep 2026 00:29:13 +0200 Subject: [PATCH] chore(deps): drop the prosemirror pins, gate dedupe in CI MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The three exact `prosemirror-*` overrides in web/pnpm-workspace.yaml existed to deduplicate the editor core that @tiptap/pm and y-prosemirror both pulled in. y-prosemirror is gone — @tiptap/y-tiptap replaced it in the Tiptap 3 migration, and it declares prosemirror-* as PEER dependencies, so it resolves to the consumer's copy and cannot introduce a second one. The pins had outlived their reason. They had also started doing harm. @tiptap/pm@3.31.3 declares prosemirror-model ^1.25.11 and prosemirror-view ^1.42.3; the overrides held the tree at 1.25.7 and 1.41.8, BELOW both. pnpm `overrides` bypass range checks silently, so this installed without a warning. Dropping them moves prosemirror-model 1.25.7 -> 1.25.11 and prosemirror-view 1.41.8 -> 1.42.3. prosemirror-view 1.42.3 also carries an XSS fix in clipboard handling — attribute validators were not run on attributes arriving via slice context. No advisory was ever filed, so no vulnerability scanner would have surfaced it. knot was not reachable through it: Tiptap only sets `spec.validate` when an extension declares one, and knot declares none. Being on the patched version closes the exposure the moment that stops being true. a5c7042 introduced these pins blaming prosemirror-view 1.42 for a history-restore failure, and ccf5dbd retracted that diagnosis — the real defect was ReplaceWithMarkdown broadcasting only its insertion. This commit deletes the last in-tree record of that exchange, so: e2e/flows/history.spec.ts is the test that motivated the pin, and it passes on 1.42.3. CI gains `pnpm dedupe --check`. This is not belt-and-braces. While preparing this change the tree was genuinely split for one run — @tiptap/pm linked prosemirror-view@1.41.8 while @tiptap/y-tiptap linked 1.42.3 — and tsc, eslint and all 153 unit tests passed anyway. The old override comment claimed tsc would catch duplication with a wall of "not assignable" errors. It does not; TypeScript is structural and the two Node classes match. Nothing else in CI looks at resolution, and lockFileMaintenance runs `pnpm update` and automerges itself once green, so an automated lockfile refresh could re-split the editor core unobserved. `--check` fails only when duplication is avoidable, which is exactly the condition worth failing on. Renovate: the prosemirror packageRule is dead config now that the overrides are gone (Renovate does not raise PRs for transitive npm deps outside lockFileMaintenance). Replaced with a rule grouping @tiptap/y-tiptap into the `tiptap` group — it is published from ueberdosis/y-tiptap, a different repo, so the monorepo preset does not group it, and it would otherwise be bumped in its own PR without the tiptap version it has to work with. Verified: tsc, eslint --max-warnings 0, 153 unit tests, and 18/18 of the editor-critical Playwright specs. The full suite on a freshly restarted stack is 60/63, with the three failures accounted for: tree-reorder "nest" is the known macOS-only dnd-kit failure that passes in CI, ws-reconnect cannot reach the toxiproxy admin API on this machine, and comment-anchors:100 timed out waiting for the add-comment float — upstream of any anchor logic, while the two sibling tests that drive the identical helper and assert on the resolved highlight both passed in the same run. Co-Authored-By: Claude Opus 5 --- .github/renovate.json | 11 ++----- .github/workflows/ci.yml | 13 ++++++++ CONTRIBUTING.md | 12 ++++--- web/pnpm-lock.yaml | 69 +++++++++++++++++++--------------------- web/pnpm-workspace.yaml | 20 ------------ 5 files changed, 57 insertions(+), 68 deletions(-) diff --git a/.github/renovate.json b/.github/renovate.json index 950aa40..bdb1763 100644 --- a/.github/renovate.json +++ b/.github/renovate.json @@ -104,14 +104,9 @@ "dependencyDashboardApproval": true }, { - "description": "web/pnpm-workspace.yaml pins these EXACTLY, to deduplicate the ProseMirror core that @tiptap/pm and y-prosemirror both depend on: when pnpm resolves them to different patches, tsc fails with a wall of 'not assignable' errors between two structurally identical Node types. That is the whole reason. Surfaced on the dashboard rather than automerged because a bump has to be checked for re-duplication, not because any version is suspect.", - "matchPackageNames": [ - "prosemirror-model", - "prosemirror-state", - "prosemirror-view" - ], - "automerge": false, - "dependencyDashboardApproval": true + "description": "@tiptap/y-tiptap is published from ueberdosis/y-tiptap, a DIFFERENT repo from ueberdosis/tiptap, so Renovate's monorepo preset does not group it with @tiptap/* and it would otherwise get its own PR. It has to move in the same PR: it is the Yjs binding that @tiptap/extension-collaboration and -collaboration-caret peer-depend on, and the module src/features/comments/anchor.ts imports ySyncPluginKey from. Resolve two copies and the key becomes 'y-sync$' in one and 'y-sync$1' in the other, at which point getState() silently returns undefined and comment anchors stop resolving. Nothing throws.", + "groupName": "tiptap", + "matchPackageNames": ["@tiptap/y-tiptap"] }, { "description": "Security floors in web/pnpm-workspace.yaml overrides. Raising them is welcome, but they exist because a parent hard-pins a vulnerable version, so a bump here needs a human to confirm the parent still resolves.", diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 89e8780..dbb92c5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -31,6 +31,19 @@ jobs: cache-dependency-path: web/pnpm-lock.yaml - run: pnpm install --frozen-lockfile working-directory: web + # The editor breaks quietly when pnpm resolves two copies of the + # ProseMirror core: @tiptap/pm and @tiptap/y-tiptap get structurally + # identical but distinct Node classes, and @tiptap/y-tiptap gets two + # ySyncPluginKey objects — 'y-sync$' and 'y-sync$1' — at which point + # getState() returns undefined and every comment anchor resolves to null. + # web/pnpm-workspace.yaml used to prevent this by pinning prosemirror-* + # exactly, which also held them BELOW the ranges @tiptap/pm declares. + # This is the replacement, and it is not redundant with the jobs below: + # a duplicated tree passes tsc, eslint and the full vitest suite. + # It matters most for lockFileMaintenance, which runs pnpm update and + # automerges itself once green. + - run: pnpm dedupe --check + working-directory: web - run: pnpm tsc --noEmit working-directory: web - run: pnpm lint diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index a532a76..e19f4b4 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -107,10 +107,14 @@ Renovate groups each into a single PR; do the same by hand: | pnpm | `packageManager` in `web/` and `e2e/package.json`, `pnpm/action-setup` `version:` | | Node | `node:…-alpine` in `Dockerfile`, `node-version:` in `ci.yml` | -Left out of automation on purpose: the exact `prosemirror-*` pins in -`web/pnpm-workspace.yaml` (they deduplicate the editor core — see the comment -there for what breaks), the security floors in the same file's `overrides`, and -`@playwright/test`, which ships the browser the e2e suite runs against. +Left out of automation on purpose: the security floors in +`web/pnpm-workspace.yaml`'s `overrides`, and `@playwright/test`, which ships the +browser the e2e suite runs against. + +`@tiptap/y-tiptap` is grouped with the `@tiptap/*` packages in `renovate.json` +even though it is published from a different upstream repo. It is the Yjs +binding the collaboration extensions peer-depend on and the one `ySyncPluginKey` +comes from, so it has to be tested against the tiptap version it will ship with. `cargo deny check` gates every PR. When an advisory has no reachable fix, add it to `deny.toml` with the dependency path and the upstream event that would let us diff --git a/web/pnpm-lock.yaml b/web/pnpm-lock.yaml index 6844e12..7a8d831 100644 --- a/web/pnpm-lock.yaml +++ b/web/pnpm-lock.yaml @@ -9,9 +9,6 @@ overrides: nanoid@4: ^5.1.16 lodash-es@<4.18.1: ^4.18.1 dompurify@<3.4.13: ^3.4.13 - prosemirror-model: 1.25.7 - prosemirror-state: 1.4.4 - prosemirror-view: 1.41.8 importers: @@ -46,10 +43,10 @@ importers: version: 3.31.3(@tiptap/core@3.31.3(@tiptap/pm@3.31.3))(@tiptap/pm@3.31.3) '@tiptap/extension-collaboration': specifier: ^3.31.3 - version: 3.31.3(@tiptap/core@3.31.3(@tiptap/pm@3.31.3))(@tiptap/pm@3.31.3)(@tiptap/y-tiptap@3.0.9(prosemirror-model@1.25.7)(prosemirror-state@1.4.4)(prosemirror-view@1.41.8)(y-protocols@1.0.7(yjs@13.6.32))(yjs@13.6.32))(yjs@13.6.32) + version: 3.31.3(@tiptap/core@3.31.3(@tiptap/pm@3.31.3))(@tiptap/pm@3.31.3)(@tiptap/y-tiptap@3.0.9(prosemirror-model@1.25.11)(prosemirror-state@1.4.4)(prosemirror-view@1.42.3)(y-protocols@1.0.7(yjs@13.6.32))(yjs@13.6.32))(yjs@13.6.32) '@tiptap/extension-collaboration-caret': specifier: ^3.31.3 - version: 3.31.3(@tiptap/core@3.31.3(@tiptap/pm@3.31.3))(@tiptap/pm@3.31.3)(@tiptap/y-tiptap@3.0.9(prosemirror-model@1.25.7)(prosemirror-state@1.4.4)(prosemirror-view@1.41.8)(y-protocols@1.0.7(yjs@13.6.32))(yjs@13.6.32)) + version: 3.31.3(@tiptap/core@3.31.3(@tiptap/pm@3.31.3))(@tiptap/pm@3.31.3)(@tiptap/y-tiptap@3.0.9(prosemirror-model@1.25.11)(prosemirror-state@1.4.4)(prosemirror-view@1.42.3)(y-protocols@1.0.7(yjs@13.6.32))(yjs@13.6.32)) '@tiptap/extension-hard-break': specifier: ^3.31.3 version: 3.31.3(@tiptap/core@3.31.3(@tiptap/pm@3.31.3)) @@ -85,7 +82,7 @@ importers: version: 3.31.3(@floating-ui/dom@1.8.0)(@tiptap/core@3.31.3(@tiptap/pm@3.31.3))(@tiptap/pm@3.31.3) '@tiptap/y-tiptap': specifier: ^3.0.9 - version: 3.0.9(prosemirror-model@1.25.7)(prosemirror-state@1.4.4)(prosemirror-view@1.41.8)(y-protocols@1.0.7(yjs@13.6.32))(yjs@13.6.32) + version: 3.0.9(prosemirror-model@1.25.11)(prosemirror-state@1.4.4)(prosemirror-view@1.42.3)(y-protocols@1.0.7(yjs@13.6.32))(yjs@13.6.32) autoprefixer: specifier: ^10.5.4 version: 10.5.4(postcss@8.5.26) @@ -1349,9 +1346,9 @@ packages: resolution: {integrity: sha512-7/El8NQ8R5V5MkdrOUdfj9IgZacpt0H071xNimX7B0AnYiWiKefQnMKd41neQYzo2MOXbWdN3iZ+7Z7BruzOSA==} engines: {node: '>=16.0.0', npm: '>=8.0.0'} peerDependencies: - prosemirror-model: 1.25.7 - prosemirror-state: 1.4.4 - prosemirror-view: 1.41.8 + prosemirror-model: ^1.7.1 + prosemirror-state: ^1.2.3 + prosemirror-view: ^1.9.10 y-protocols: ^1.0.1 yjs: ^13.5.38 @@ -3022,8 +3019,8 @@ packages: prosemirror-keymap@1.2.3: resolution: {integrity: sha512-4HucRlpiLd1IPQQXNqeo81BGtkY8Ai5smHhKW9jjPKRc2wQIxksg7Hl1tTI2IfT2B/LgX6bfYvXxEpJl7aKYKw==} - prosemirror-model@1.25.7: - resolution: {integrity: sha512-A79aN8QEFUwI6cax8Yq4Rpcx1TJZ3Kagn+ii7qLo4/V8H3mMiHrhFyhTyHHvpSnOgMPpWiDGSwM3etwrxE50ug==} + prosemirror-model@1.25.11: + resolution: {integrity: sha512-QWg9RhnpLlogAmp3p96uEFrE5txQpFynd4vhBAELkwgOCWQs/X0yCzB3/hrHqiPwf91RG5KyWq6553zs9JqIOQ==} prosemirror-schema-list@1.5.1: resolution: {integrity: sha512-927lFx/uwyQaGwJxLWCZRkjXG0p48KpMj6ueoYiu4JX05GGuGcgzAy62dfiV8eFZftgyBUvLx76RsMe20fJl+Q==} @@ -3037,8 +3034,8 @@ packages: prosemirror-transform@1.12.0: resolution: {integrity: sha512-GxboyN4AMIsoHNtz5uf2r2Ru551i5hWeCMD6E2Ib4Eogqoub0NflniaBPVQ4MrGE5yZ8JV9tUHg9qcZTTrcN4w==} - prosemirror-view@1.41.8: - resolution: {integrity: sha512-TnKDdohEatgyZNGCDWIdccOHXhYloJwbwU+phw/a23KBvJIR9lWQWW7WHHK3vBdOLDNuF7TaX98GObUZOWkOnA==} + prosemirror-view@1.42.3: + resolution: {integrity: sha512-oTN7EtH+CpwxU9NrwEYWd0UZ4JUx7l048l5A2Xppm4p/60isZYLnth9QVQmC3VRIvdrIWCxwZSd+Uz791G31/w==} punycode@2.3.1: resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} @@ -4626,17 +4623,17 @@ snapshots: dependencies: '@tiptap/core': 3.31.3(@tiptap/pm@3.31.3) - '@tiptap/extension-collaboration-caret@3.31.3(@tiptap/core@3.31.3(@tiptap/pm@3.31.3))(@tiptap/pm@3.31.3)(@tiptap/y-tiptap@3.0.9(prosemirror-model@1.25.7)(prosemirror-state@1.4.4)(prosemirror-view@1.41.8)(y-protocols@1.0.7(yjs@13.6.32))(yjs@13.6.32))': + '@tiptap/extension-collaboration-caret@3.31.3(@tiptap/core@3.31.3(@tiptap/pm@3.31.3))(@tiptap/pm@3.31.3)(@tiptap/y-tiptap@3.0.9(prosemirror-model@1.25.11)(prosemirror-state@1.4.4)(prosemirror-view@1.42.3)(y-protocols@1.0.7(yjs@13.6.32))(yjs@13.6.32))': dependencies: '@tiptap/core': 3.31.3(@tiptap/pm@3.31.3) '@tiptap/pm': 3.31.3 - '@tiptap/y-tiptap': 3.0.9(prosemirror-model@1.25.7)(prosemirror-state@1.4.4)(prosemirror-view@1.41.8)(y-protocols@1.0.7(yjs@13.6.32))(yjs@13.6.32) + '@tiptap/y-tiptap': 3.0.9(prosemirror-model@1.25.11)(prosemirror-state@1.4.4)(prosemirror-view@1.42.3)(y-protocols@1.0.7(yjs@13.6.32))(yjs@13.6.32) - '@tiptap/extension-collaboration@3.31.3(@tiptap/core@3.31.3(@tiptap/pm@3.31.3))(@tiptap/pm@3.31.3)(@tiptap/y-tiptap@3.0.9(prosemirror-model@1.25.7)(prosemirror-state@1.4.4)(prosemirror-view@1.41.8)(y-protocols@1.0.7(yjs@13.6.32))(yjs@13.6.32))(yjs@13.6.32)': + '@tiptap/extension-collaboration@3.31.3(@tiptap/core@3.31.3(@tiptap/pm@3.31.3))(@tiptap/pm@3.31.3)(@tiptap/y-tiptap@3.0.9(prosemirror-model@1.25.11)(prosemirror-state@1.4.4)(prosemirror-view@1.42.3)(y-protocols@1.0.7(yjs@13.6.32))(yjs@13.6.32))(yjs@13.6.32)': dependencies: '@tiptap/core': 3.31.3(@tiptap/pm@3.31.3) '@tiptap/pm': 3.31.3 - '@tiptap/y-tiptap': 3.0.9(prosemirror-model@1.25.7)(prosemirror-state@1.4.4)(prosemirror-view@1.41.8)(y-protocols@1.0.7(yjs@13.6.32))(yjs@13.6.32) + '@tiptap/y-tiptap': 3.0.9(prosemirror-model@1.25.11)(prosemirror-state@1.4.4)(prosemirror-view@1.42.3)(y-protocols@1.0.7(yjs@13.6.32))(yjs@13.6.32) yjs: 13.6.32 '@tiptap/extension-document@3.31.3(@tiptap/core@3.31.3(@tiptap/pm@3.31.3))': @@ -4737,12 +4734,12 @@ snapshots: prosemirror-history: 1.5.0 prosemirror-inputrules: 1.5.1 prosemirror-keymap: 1.2.3 - prosemirror-model: 1.25.7 + prosemirror-model: 1.25.11 prosemirror-schema-list: 1.5.1 prosemirror-state: 1.4.4 prosemirror-tables: 1.8.5 prosemirror-transform: 1.12.0 - prosemirror-view: 1.41.8 + prosemirror-view: 1.42.3 '@tiptap/react@3.31.3(@floating-ui/dom@1.8.0)(@tiptap/core@3.31.3(@tiptap/pm@3.31.3))(@tiptap/pm@3.31.3)(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': dependencies: @@ -4794,12 +4791,12 @@ snapshots: '@tiptap/core': 3.31.3(@tiptap/pm@3.31.3) '@tiptap/pm': 3.31.3 - '@tiptap/y-tiptap@3.0.9(prosemirror-model@1.25.7)(prosemirror-state@1.4.4)(prosemirror-view@1.41.8)(y-protocols@1.0.7(yjs@13.6.32))(yjs@13.6.32)': + '@tiptap/y-tiptap@3.0.9(prosemirror-model@1.25.11)(prosemirror-state@1.4.4)(prosemirror-view@1.42.3)(y-protocols@1.0.7(yjs@13.6.32))(yjs@13.6.32)': dependencies: lib0: 0.2.117 - prosemirror-model: 1.25.7 + prosemirror-model: 1.25.11 prosemirror-state: 1.4.4 - prosemirror-view: 1.41.8 + prosemirror-view: 1.42.3 y-protocols: 1.0.7(yjs@13.6.32) yjs: 13.6.32 @@ -6665,7 +6662,7 @@ snapshots: prosemirror-commands@1.7.2: dependencies: - prosemirror-model: 1.25.7 + prosemirror-model: 1.25.11 prosemirror-state: 1.4.4 prosemirror-transform: 1.12.0 @@ -6673,20 +6670,20 @@ snapshots: dependencies: prosemirror-state: 1.4.4 prosemirror-transform: 1.12.0 - prosemirror-view: 1.41.8 + prosemirror-view: 1.42.3 prosemirror-gapcursor@1.4.1: dependencies: prosemirror-keymap: 1.2.3 - prosemirror-model: 1.25.7 + prosemirror-model: 1.25.11 prosemirror-state: 1.4.4 - prosemirror-view: 1.41.8 + prosemirror-view: 1.42.3 prosemirror-history@1.5.0: dependencies: prosemirror-state: 1.4.4 prosemirror-transform: 1.12.0 - prosemirror-view: 1.41.8 + prosemirror-view: 1.42.3 rope-sequence: 1.3.4 prosemirror-inputrules@1.5.1: @@ -6699,37 +6696,37 @@ snapshots: prosemirror-state: 1.4.4 w3c-keyname: 2.2.8 - prosemirror-model@1.25.7: + prosemirror-model@1.25.11: dependencies: orderedmap: 2.1.1 prosemirror-schema-list@1.5.1: dependencies: - prosemirror-model: 1.25.7 + prosemirror-model: 1.25.11 prosemirror-state: 1.4.4 prosemirror-transform: 1.12.0 prosemirror-state@1.4.4: dependencies: - prosemirror-model: 1.25.7 + prosemirror-model: 1.25.11 prosemirror-transform: 1.12.0 - prosemirror-view: 1.41.8 + prosemirror-view: 1.42.3 prosemirror-tables@1.8.5: dependencies: prosemirror-keymap: 1.2.3 - prosemirror-model: 1.25.7 + prosemirror-model: 1.25.11 prosemirror-state: 1.4.4 prosemirror-transform: 1.12.0 - prosemirror-view: 1.41.8 + prosemirror-view: 1.42.3 prosemirror-transform@1.12.0: dependencies: - prosemirror-model: 1.25.7 + prosemirror-model: 1.25.11 - prosemirror-view@1.41.8: + prosemirror-view@1.42.3: dependencies: - prosemirror-model: 1.25.7 + prosemirror-model: 1.25.11 prosemirror-state: 1.4.4 prosemirror-transform: 1.12.0 diff --git a/web/pnpm-workspace.yaml b/web/pnpm-workspace.yaml index b0b819b..6650e7f 100644 --- a/web/pnpm-workspace.yaml +++ b/web/pnpm-workspace.yaml @@ -27,26 +27,6 @@ overrides: # patched release regardless of what mermaid or excalidraw ask for. dompurify@<3.4.13: "^3.4.13" - # Deduplication, not security. @tiptap/pm and y-prosemirror both declare wide - # `^1.x` ranges on the ProseMirror core, and pnpm is free to resolve them to - # different patches. When it does, the editor is handed two structurally - # identical but nominally distinct `Node` types and tsc fails with a wall of - # "not assignable" errors. - # - # Pinned EXACTLY to the versions main resolved, not to the newest release. - # Dedupe is the goal; upgrading this stack is a separate, deliberate change. - # - # These pins were introduced in a5c7042, which blamed prosemirror-view 1.42 - # for a history-restore failure. That diagnosis was wrong and ccf5dbd - # retracts it: the actual defect was ReplaceWithMarkdown broadcasting only - # its insertion, so peers appended the restored text instead of replacing. - # Nothing is known to be wrong with prosemirror-view 1.42 — do not treat - # these versions as a quarantine. Lifting them is an ordinary upgrade that - # wants a CI run, not a re-investigation. - prosemirror-model: "1.25.7" - prosemirror-state: "1.4.4" - prosemirror-view: "1.41.8" - # pnpm >= 10 blocks dependency lifecycle scripts unless they are approved here. # esbuild is the only package in this tree whose install script is required. allowBuilds: