Post Revisions: Upgrade diff from v4 to v8#77992
Conversation
|
The following accounts have interacted with this PR and/or linked issues. I will continue to update these lists as activity occurs. You can also manually ask me to refresh this list by adding the If you're merging code through a pull request on GitHub, copy and paste the following into the bottom of the merge commit message. To understand the WordPress project's expectations around crediting contributors, please review the Contributor Attribution page in the Core Handbook. |
|
Size Change: +866 B (+0.01%) Total Size: 7.93 MB 📦 View Changed
ℹ️ View Unchanged
|
|
Flaky tests detected in deddbd0. 🔍 Workflow run URL: https://github.com/WordPress/gutenberg/actions/runs/25426573501
|
|
This feels like something that @ellatrix should take a look at — I don't personally have much context to understand the full implications of changing / keeping the previous diff behavior. In the meantime, I'm sharing some notes from an initial round of AI-assisted reviewPR Review: #77992 — Post Revisions: Upgrade
|
Addresses review feedback on #77992 (findings 1, 5, 6, 7, 8, 9). The previous commit inlined ~150 LoC of v4's Myers algorithm to keep `diffArrays`'s LCS pivot stable across the v4 -> v8 bump. That preserved all existing test assertions but came at a real maintenance cost. The cleaner approach (the issue's original "Class 1" fix): just drop freeform/whitespace pseudo-blocks from both arrays before LCS. Without the `\n\n` blocks competing as a match anchor, v8's "deletions before insertions" tie-breaker picks the same content-block pivot v4 did for every input that was previously failing, and the inlined algorithm becomes unnecessary. Two follow-ups to make that approach work end-to-end: 1. Adjust `pairSimilarBlocks`'s placement heuristic. The original heuristic looked for *added* blocks between the removed and added positions to decide where to anchor a paired modification. With whitespace pseudo-blocks no longer in the result list, an unchanged content block between the two positions is now the only "the modified block crosses current-revision content" signal -- so the heuristic now also fires on unchanged blocks (but still ignores removed blocks, which don't exist in the current revision and so don't count as crossing). 2. Relax the `'handles two blocks that swapped positions'` assertion to the user-facing invariant (one unmarked + one removed/added pair with same content) rather than which side of the swap gets matched. For a pure swap the two LCS choices are equally valid -- both v4 and v8 produce semantically-correct output, they just disagree on which block reads as "unchanged" -- so asserting one is testing the implementation, not the behaviour. Net: -191 +56 LoC in `block-diff.js`. All 33 block-diff unit tests pass and the broader revision-related suites stay green.
Addresses review feedback on #77992 (findings 2, 3, 4, 10). - `preserve-client-ids.js` and `block-compare/index.js` now import `diffArrays` / `diffChars` from the top-level `'diff'` package instead of the deep `'diff/lib/diff/<name>'` paths. v8's `package.json` `exports` map only wildcards `./lib/*.js` (with extension); the bare-folder `./lib/` mapping requires a trailing slash. The deep paths only resolve here because the bundler/Jest resolver fills in `.js` -- a future tooling change could break them. v8 also marks the package `sideEffects: false`, so the historical tree-shaking reason for the deep imports no longer applies. The "diff doesn't tree-shake correctly" comment in `block-compare/index.js` is now stale and gets removed. - Add `### Internal` entries to `packages/editor/CHANGELOG.md` and `packages/block-editor/CHANGELOG.md` recording the major dependency bump.
Addresses review feedback on #77992 (findings 1, 5, 6, 7, 8, 9). The previous commit inlined ~150 LoC of v4's Myers algorithm to keep `diffArrays`'s LCS pivot stable across the v4 -> v8 bump. That preserved all existing test assertions but came at a real maintenance cost. The cleaner approach (the issue's original "Class 1" fix): just drop freeform/whitespace pseudo-blocks from both arrays before LCS. Without the `\n\n` blocks competing as a match anchor, v8's "deletions before insertions" tie-breaker picks the same content-block pivot v4 did for every input that was previously failing, and the inlined algorithm becomes unnecessary. Two follow-ups to make that approach work end-to-end: 1. Adjust `pairSimilarBlocks`'s placement heuristic. The original heuristic looked for *added* blocks between the removed and added positions to decide where to anchor a paired modification. With whitespace pseudo-blocks no longer in the result list, an unchanged content block between the two positions is now the only "the modified block crosses current-revision content" signal -- so the heuristic now also fires on unchanged blocks (but still ignores removed blocks, which don't exist in the current revision and so don't count as crossing). 2. Relax the `'handles two blocks that swapped positions'` assertion to the user-facing invariant (one unmarked + one removed/added pair with same content) rather than which side of the swap gets matched. For a pure swap the two LCS choices are equally valid -- both v4 and v8 produce semantically-correct output, they just disagree on which block reads as "unchanged" -- so asserting one is testing the implementation, not the behaviour. Net: -191 +56 LoC in `block-diff.js`. All 33 block-diff unit tests pass and the broader revision-related suites stay green.
Addresses review feedback on #77992 (findings 2, 3, 4, 10). - `preserve-client-ids.js` and `block-compare/index.js` now import `diffArrays` / `diffChars` from the top-level `'diff'` package instead of the deep `'diff/lib/diff/<name>'` paths. v8's `package.json` `exports` map only wildcards `./lib/*.js` (with extension); the bare-folder `./lib/` mapping requires a trailing slash. The deep paths only resolve here because the bundler/Jest resolver fills in `.js` -- a future tooling change could break them. v8 also marks the package `sideEffects: false`, so the historical tree-shaking reason for the deep imports no longer applies. The "diff doesn't tree-shake correctly" comment in `block-compare/index.js` is now stale and gets removed. - Add `### Internal` entries to `packages/editor/CHANGELOG.md` and `packages/block-editor/CHANGELOG.md` recording the major dependency bump.
37057b0 to
deddbd0
Compare
Aligns `packages/editor` and `packages/block-editor` with `packages/sync` on `diff@^8.0.3` (needed for the Syncpack alignment work in #77950 / #77954). The bump exposes two unrelated upstream changes that would regress the post-revisions UI: 1. v6+ adds a "deletions before insertions" tie-breaker, so for inputs with multiple equal-length LCSes (whitespace-block pivots, paragraph swaps), `diffArrays` selects a different match than v4 did. The downstream `pairSimilarBlocks` step then mis-pairs blocks and shows two confusing inline diffs instead of a clean modified+unchanged pair. 2. v6+ stops treating whitespace as a token in `diffWords`, coalescing adjacent word changes into one removed/added pair and losing per-word precision in inline rich-text diffs. Fix on the consumer side so existing tests pass without touching any assertion: - Replace the imported `diffArrays` in `block-diff.js` with a local v4-compatible port of `Diff.prototype.diff` (Myers, array+strict-eq), including v4's `(added, removed)` -> `(removed, added)` swap in `buildValues` so condensed sections still render in the right order. - Switch `diffWords` -> `diffWordsWithSpace` for the inline rich-text diff, the `changedAttributes` panel diff, and the `Meta` field diff in `revision-fields-diff`. `preserve-client-ids.js` and `block-compare` (uses `diffChars`) need no changes -- neither hits the affected v6+ behaviours and their tests pass unmodified under v8. 41/41 revision-related unit tests pass; full `npm run test:unit` is green. Closes #77976
Addresses review feedback on #77992 (findings 1, 5, 6, 7, 8, 9). The previous commit inlined ~150 LoC of v4's Myers algorithm to keep `diffArrays`'s LCS pivot stable across the v4 -> v8 bump. That preserved all existing test assertions but came at a real maintenance cost. The cleaner approach (the issue's original "Class 1" fix): just drop freeform/whitespace pseudo-blocks from both arrays before LCS. Without the `\n\n` blocks competing as a match anchor, v8's "deletions before insertions" tie-breaker picks the same content-block pivot v4 did for every input that was previously failing, and the inlined algorithm becomes unnecessary. Two follow-ups to make that approach work end-to-end: 1. Adjust `pairSimilarBlocks`'s placement heuristic. The original heuristic looked for *added* blocks between the removed and added positions to decide where to anchor a paired modification. With whitespace pseudo-blocks no longer in the result list, an unchanged content block between the two positions is now the only "the modified block crosses current-revision content" signal -- so the heuristic now also fires on unchanged blocks (but still ignores removed blocks, which don't exist in the current revision and so don't count as crossing). 2. Relax the `'handles two blocks that swapped positions'` assertion to the user-facing invariant (one unmarked + one removed/added pair with same content) rather than which side of the swap gets matched. For a pure swap the two LCS choices are equally valid -- both v4 and v8 produce semantically-correct output, they just disagree on which block reads as "unchanged" -- so asserting one is testing the implementation, not the behaviour. Net: -191 +56 LoC in `block-diff.js`. All 33 block-diff unit tests pass and the broader revision-related suites stay green.
Addresses review feedback on #77992 (findings 2, 3, 4, 10). - `preserve-client-ids.js` and `block-compare/index.js` now import `diffArrays` / `diffChars` from the top-level `'diff'` package instead of the deep `'diff/lib/diff/<name>'` paths. v8's `package.json` `exports` map only wildcards `./lib/*.js` (with extension); the bare-folder `./lib/` mapping requires a trailing slash. The deep paths only resolve here because the bundler/Jest resolver fills in `.js` -- a future tooling change could break them. v8 also marks the package `sideEffects: false`, so the historical tree-shaking reason for the deep imports no longer applies. The "diff doesn't tree-shake correctly" comment in `block-compare/index.js` is now stale and gets removed. - Add `### Internal` entries to `packages/editor/CHANGELOG.md` and `packages/block-editor/CHANGELOG.md` recording the major dependency bump.
deddbd0 to
98f3d2c
Compare
There was a problem hiding this comment.
Pull request overview
This PR upgrades the diff dependency used by the editor’s revisions/compare features from v4 to v8, and updates the revisions diffing logic to preserve the same user-facing pairing and word-level inline diff behavior despite upstream behavioral changes in diff.
Changes:
- Upgrade
diffto^8.0.3in@wordpress/editorand@wordpress/block-editor, and migrate remaining imports to the top-leveldiffentrypoint. - Stabilize block pairing under
diffArraysby filtering whitespace-only freeform pseudo-blocks before LCS matching, and adjust the placement heuristic inpairSimilarBlocks. - Restore per-word inline diffs by switching from
diffWordstodiffWordsWithSpacefor rich-text, sidebar changed-attribute diffs, and meta-field diffs; relax one swap test assertion to assert the UX invariant rather than a specific LCS tie-break.
Reviewed changes
Copilot reviewed 9 out of 10 changed files in this pull request and generated no comments.
Show a summary per file
| File | Description |
|---|---|
| packages/editor/src/components/revision-fields-diff/index.js | Switch meta diffs to diffWordsWithSpace via top-level diff import to preserve per-word output. |
| packages/editor/src/components/post-revisions-preview/test/block-diff.js | Update swap test to assert invariant output instead of a specific unchanged-anchor choice. |
| packages/editor/src/components/post-revisions-preview/preserve-client-ids.js | Migrate diffArrays import to top-level diff. |
| packages/editor/src/components/post-revisions-preview/block-diff.js | Filter whitespace pseudo-blocks pre-LCS; adjust pairing placement heuristic; use diffWordsWithSpace for inline and attribute diffs. |
| packages/editor/package.json | Bump diff dependency to ^8.0.3. |
| packages/editor/CHANGELOG.md | Record the diff major-version bump under Internal changes. |
| packages/block-editor/src/components/block-compare/index.js | Migrate diffChars import to top-level diff and remove now-stale tree-shaking comment. |
| packages/block-editor/package.json | Bump diff dependency to ^8.0.3. |
| packages/block-editor/CHANGELOG.md | Record the diff major-version bump under Internal changes. |
| package-lock.json | Lockfile updates reflecting diff v8 installs for the updated workspaces. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
|
@manzoorwanijk FWIW, sharing another round of AI-assisted review. Click to expandPR Review: #77992 — Post Revisions: Upgrade
|
Addresses round-2 review feedback on #77992 (human #1, #3, #5, #6 + Codex test gaps a, b). - Add a `'filters whitespace-only freeform pseudo-blocks before LCS'` test that's a direct canary for the whitespace filter — without the filter, `pairSimilarBlocks` would mis-match two paragraphs across the whitespace pseudo-block as the LCS anchor and produce two confused modified blocks instead of one modified + one unchanged. - Add a `'places paired modification at current-revision position when only unchanged blocks sit between'` test exercising the new `crossesCurrentContent` "unchanged between removed and added" branch in isolation. Previously only hit transitively through the `'handles block move with a tiny change'` test, which mixes that branch with the whitespace-filter path and other heuristics. - Tighten the `crossesCurrentContent` comment so it matches what the code actually checks (unpaired-added + unchanged) and adds a one-line note that 'removed' / `pairedAdded` blocks aren't checked because they aren't in the current revision. - Match the `diffWordsWithSpace` rationale comment between `block-diff.js` and `revision-fields-diff/index.js` for grep-ability. - Document on `diffRawBlocks` that the whitespace filter is intentionally re-applied at every recursive level so the function stays self-contained when called directly with raw grammar output. No logic changes. All 35 block-diff tests + the broader unit suite (32598 tests) stay green.
Thank you. I have addressed the feedback.
Yes, I agree. Let us see if @ellatrix finds some time to have a look at this. |
|
cc'ing @aduth on this PR too, since he was partially involved in some npm / package.json / syncpack conversations |
|
Thank you. I agree that we should update, but I'm not sure I agree that we should try to replicate old behavior. I'll have a look into this after 7.0 etc. I don't think we should backport this to 7.0. |
Thank you. We don’t need to backport this, it’s just a cleanup of inconsistent dependency versions in the monorepo. |
What?
Closes #77976
Upgrades the
difflibrary from^4.0.2to^8.0.3inpackages/editorandpackages/block-editor, and updates the post-revisions consumer code so the four-major bump is invisible to users — the in-editor revisions UI keeps the same block pairing and inline word-level diff output it produced under v4.Why?
The
difflibrary was declared at^4.0.2ineditor/block-editorand^8.0.3insync, blocking the cross-workspace Syncpack alignment work (#77950, #77954). Bumpingeditorto^8.0.3is a four-major jump that, on its own, breaks four unit tests inblock-diff.jsand surfaces real UX regressions in the post-revisions feature. The failures fall into two unrelated upstream behaviour changes:prev=[paragraph, whitespace, paragraph]versuscurr=[paragraph_modified, whitespace, paragraph]: v8 picks the whitespace pseudo-block as the LCS anchor, sopairSimilarBlocksthen sees two removed and two added paragraphs and similarity-matches dissimilar pairs — producing two confusing inline diffs instead of a clean modified+unchanged pair.diffWordssemantics change (v6+). v6 stopped treating whitespace as a token, so adjacent word changes coalesce into one removed/added pair. Inline rich-text diffs lose precision:Visit <a><del>our</del><ins>the</ins> <del>site</del><ins>website</ins></a> today(per-word) becomesVisit <a><del>our site</del><ins>the website</ins></a> today(one coarser diff).Both are real user-visible regressions, not snapshot drift.
How?
1. Bump the dependency
packages/editor/package.jsonandpackages/block-editor/package.json:diff→^8.0.3. (packages/syncwas already on v8.)2. Filter whitespace pseudo-blocks before LCS
packages/editor/src/components/post-revisions-preview/block-diff.js—diffRawBlocksnow strips freeform/whitespace pseudo-blocks (the\n\nbetween block markers) from both arrays before passing them todiffArrays. With those out of the picture, v8's tie-breaker selects the same content-block LCS anchor v4 did for every previously-failing case. Whitespace pseudo-blocks aren't rendered (parseRawBlockreturns undefined for them), so dropping them before the diff has no user-visible effect.pairSimilarBlocks's placement heuristic was extended to match: it previously decided where to anchor a paired modification by checking whether any added block sat between the removed and added positions. Now that whitespace pseudo-blocks are no longer in the result list, an unchanged content block between the two positions is the only "the modified block crosses current-revision content" signal — so the heuristic also fires on unchanged blocks (but still ignores removed blocks, which don't exist in the current revision and so don't count as crossing).3. Switch rich-text and field diffs to
diffWordsWithSpaceblock-diff.js:applyRichTextDiffandapplyDiffToBlock(sidebarchangedAttributes) now calldiffWordsWithSpacefrom the top-leveldiffimport. v8'sdiffWordsWithSpacekeeps whitespace as its own token, matching v4'sdiffWordssemantics.revision-fields-diff/index.js: meta field diffs likewise usediffWordsWithSpace.4. Migrate remaining
diffimports to top-levelpreserve-client-ids.jsandblock-compare/index.jsused deep paths like'diff/lib/diff/array'. v8'spackage.jsonexportsmap only wildcards./lib/*.js(with extension); the bare-folder./lib/mapping requires a trailing slash. The deep paths only resolve here because the bundler/Jest resolver fills in.js— a future tooling change could break them. v8 also declaressideEffects: falsein its per-output sub-manifests (libcjs/package.jsonandlibesm/package.json), which webpack 4+ honours, so the historical tree-shaking reason for the deep imports no longer applies. The "diff doesn't tree-shake correctly" comment inblock-compare/index.jswas true for v4 but is now stale and gets removed.5. Relax one test assertion
The
'handles two blocks that swapped positions'test was asserting which block stayed unmarked vs. which became removed/added. For a pure swap, both LCS choices are equally valid — both v4 and v8 produce semantically-correct output, they just disagree on which block reads as "unchanged." The test now asserts the user-facing invariant (one unmarked + one removed/added pair with same content) instead of the implementation choice.6. CHANGELOG entries
Added
### Internalentries topackages/editor/CHANGELOG.mdandpackages/block-editor/CHANGELOG.mdrecording the major dependency bump.Testing Instructions
Automated:
npm run test:unit -- -- packages/editor/src/components/post-revisions-preview/ \ packages/editor/src/components/revision-fields-diff/ \ packages/editor/src/components/revision-diff-panel/ \ packages/block-editor/src/components/block-compare/Expected: 42/42 pass. Full
npm run test:unitis also green (32538/32545, 7 unrelated skips).Manual (golden path for the revisions UI):
wp-env(npm run wp-env start) and create a post with several paragraphs.our site→the website).<strong>/<em>formatting to part of a paragraph.trunkbefore the bump:<del>…</del><ins>…</ins>diff, not two confusing pair-ups.our → theandsite → websiteas two separate diffs), not coalesced into one.revision-diff-format-*spans on just the affected range.Testing Instructions for Keyboard
No UI changes; existing keyboard navigation through the revisions panel is unaffected.
Screenshots or screencast
<del>First</del><ins>Second</ins> block content <ins>modified</ins>+ a second confusing inline diff for the unchanged siblingSecond block content<ins> modified</ins>; the unchanged sibling stays unmarkedVisit <a><del>our site</del><ins>the website</ins></a> today(one coarse diff)Visit <a><del>our</del><ins>the</ins> <del>site</del><ins>website</ins></a> today(per-word)Use of AI Tools
This PR was authored with the assistance of Claude Code (Opus 4.7). All edits, the test results, and the manual testing steps were reviewed and verified by me.