Skip to content

2026 Uplift #140

Closed
Closed

Description

@ryan-roemer

Living tracking issue, modelled on FormidableLabs/victory#3093 (and nearform/llm-splitter for the single-package, JSDoc-typed setup), but much smaller. We'll keep editing it as we go, and record what changed in the Decision log at the bottom.

Status (2026-11-12): merged. #156 merged chore/2026-uplift into master with a merge commit (1e16aab); release.yml's first push run on master published nothing, as expected (run). Post-merge cleanup is done apart from #70 (waiting on Dependabot's rescan). Follow-up work is tracked in the issues it names. History: Phase 1 is done: #141 (pnpm 12, Node 26, vendored test cases, Codecov and AppVeyor removed, mocha 11), changesets v3 + OIDC release.yml with canary (65c9439), and a canary 3.2.3-canary-20261105232110 published to next through OIDC with provenance (run). Phase 2 so far: oxlint + oxfmt (#142 config, #143 format, #144 lint); #145 reverted #130's unreleased type guard so index.d.ts matches 3.2.2 again; #146 added pack:check, which compares the packed tarball with a snapshot of the published 3.2.2; #147 added TypeScript 7 + JSDoc types and types:check. #148 added the type probe: test/types/ compiled against the packed tarball inside pack:check. The size report (pnpm size, CI job summary, compared with the parent commit) replaced the compress/size-min-gz scripts and the README badge. Phase 3: Vitest 5 (node project + browser mode in Chromium, Firefox and WebKit) replaced mocha, nyc, Karma, webpack, Babel and the rest of the Phase 2 cruft; React 19 + Preact 11 in the lockfile, a CI job on React 18.0.0 + Preact 10, v8 coverage in the job summary. zizmor runs in a CI job (lint-gha) and in pnpm check via pnpm lint:gha (skipped when zizmor isn't installed); .github/pinact.yml drives pinact run --update. pnpm check (format, lint, lint:gha, types:check, test, pack:check) passes. Phase 3 is done. Phase 4: pnpm bench (Vitest 5 benchmarking, bench/compare.bench.mjs) compares the working tree, 3.2.2 (aliased devDep) and fast-deep-equal per fixture group, with a report-only bench CI job writing the tables to the job summary; benchmark/, the benchmark/lodash/nano-equal/shallow-equal-fuzzy devDeps and the README chart are gone. Phase 2 leftovers: pnpm dep:check (npm-check-updates, .ncurc.cjs with a 7-day cooldown) and the update routine in CONTRIBUTING; a knip audit (pnpm dlx knip, knip.jsonc) found no unused devDependencies; first dep:check run bumped Vitest to 5.1 and pnpm to 12.17. AGENTS.md (+ CLAUDE.md pointer, @AGENTS.md) closes Phase 2. Phase 5: README/CONTRIBUTING/PR template docs pass (#155, Nearform naming); Dependabot PRs and obsolete issues closed, stale branches and the NPM_TOKEN secret deleted. Phases 1–5 are done. Next: #139 (Temporal) + #137 as the first 3.x release from the new pipeline.

Why

By the numbers (Nov 2026)

  • ~107M downloads/month. Last week by version: 3.2.2 ~27.5M, 2.0.4 ~6.1M, 3.2.0 ~1.45M. Most users are pinned through dependents' ranges (^3, ^2).
  • Published surface: index.js (CJS, module.exports = isEqual, ES5) and index.d.ts (export =). No exports map, no ESM, no sideEffects. There's no UMD bundle to drop, unlike victory.
  • Quick probe (2026-11-05) of current index.js:
    • React 19.3.1 (dev and prod): elements compare correctly. In dev, _owner is enumerable and still skipped; the _debug* props are non-enumerable.
    • Preact 10.29 / 11.1 with preact/compat: correct.
    • Plain preact h() vnodes with compat not loaded compare false (no $$typeof, so __v is traversed). This is pre-existing and the same in 10 and 11; only compat is documented. A later source item, not uplift work.
  • JSDoc spike (TS 7.0.2, allowJs + checkJs + strict, emitDeclarationOnly): a 5-line JSDoc block on isEqual emitted exactly the index.d.ts on master at the time (the unreleased Convert function to TypeGuard #130 type guard, since reverted). It needs redoing against 3.2.2's declare function isEqual<A = any, B = any>(a: A, b: B): boolean; (@template A, B, @returns {boolean}). strict reports 7 errors inside equal() (implicit any params, ArrayBufferView indexing, catch (error) as unknown). All can be fixed with JSDoc annotations or casts, with no runtime change.

Principles

  1. Infra first, source last. Mechanical source churn (oxfmt, lint fixes, JSDoc types) lands in its own PR with no behavior change, and goes into .git-blame-ignore-revs.
  2. The uplift doesn't change what users get. Same files, same module.exports = isEqual, same ES5 code, same declarations. Only formatting and comments may change in index.js, and pack:check proves the rest (see Phase 2). ESM, exports and a modern target wait for 4.0.0.
  3. Types come from JSDoc, as in llm-splitter. Source stays plain JS with no build step for the code. tsc type-checks it (checkJs) and, from 4.0, generates the .d.ts.
  4. Plain package.json scripts. No task runner, since this is a single package.
  5. Crib from victory and llm-splitter where they fit a one-file library.

Versioning (decided 2026-11-05)

  • Uplift → master with no published change. No changeset is needed for the infra PRs; there's nothing to release from them alone.
  • isEqual throws on Temporal values because it calls valueOf #139 (Temporal) → 3.x (patch or minor), released from the new OIDC pipeline. It reaches every ^3 user, and it's the pipeline's first real run, like victory's interim 37.4.0. Adding sideEffects: false to package.json #137 (sideEffects: false) can ride along; it's a safe manifest addition.
  • 4.0.0 later: ESM + CJS, exports map, modern target (drop ES5/IE), .d.ts generated from JSDoc. Its own design section when we get there (see "4.0.0" below).

Branching and merge strategy

  • Default branch stays master (decided 2026-11-05).
  • Integration branch: chore/2026-uplift, cut from master. Small PRs into it (squash is fine). CI runs on master + chore/2026-uplift*.
  • Final merge: one PR into master with a merge commit, which keeps the blame-ignore SHAs valid. Repo settings change first (same traps as victory); steps in "Final merge checklist".
  • Release gate for the merge: green CI, and pack:check shows the packed tarball matches 3.2.2 (see Phase 2). A canary to next from the branch proves OIDC before isEqual throws on Temporal values because it calls valueOf #139 needs it.

Phase 1: Package manager and release pipeline (OIDC)

  • pnpm 12 for development ([uplift] chore: pnpm 12, Node 26, vendored fast-deep-equal test cases #141) (packageManager, pnpm-workspace.yaml settings, minimumReleaseAge: 10080, allowBuilds). Remove yarn.lock. README install instructions say only npm install react-fast-compare; CONTRIBUTING says pnpm.
  • Repo files, as in victory (.nvmrc, .editorconfig, .gitignore done in [uplift] chore: pnpm 12, Node 26, vendored fast-deep-equal test cases #141):
    • .nvmrc → 26, used by CI via node-version-file and documented in CONTRIBUTING. Still no engines field.
    • .editorconfig (2 spaces, LF, final newline; Markdown keeps trailing whitespace).
    • Later phases add .oxlintrc.json, .oxfmtrc.json, .git-blame-ignore-revs, .ncurc.cjs (dep:check), .github/zizmor.yml and .github/pinact.yml, plus AGENTS.md / CLAUDE.md.
    • .gitignore: trim the 2017 template (bower, grunt, jspm, lib-cov, …). pnpm-lock.yaml is committed; package-lock.json and yarn.lock are ignored.
  • changesets v3 + official @changesets/changelog-github (disableThanks), replacing the deprecated @svitejs/changesets-changelog-github-compact. snapshot.useCalculatedVersion for canaries. baseBranch: master.
  • release.yml on changesets/action v2 split jobs (llm-splitter's single-package version is the closest template; victory's adds pnpm and a canary): select-mode → version | pack → publish. permissions: {} with per-job grants, id-token: write only on publish/canary, package-manager-cache: false, persist-credentials: false. One v{version} tag + gh release create --generate-notes.
    • Canary via workflow_dispatch in the same file → X.Y.Z-canary-<datetime> on next.
  • Drop publishConfig.provenance (automatic under trusted publishing).

npm and GitHub setup (manual, needs a maintainer)

One package, so this is done by hand in the npm and GitHub UIs (no npm-trust.sh like victory's).

  • GitHub: create the npm-publish environment (Settings → Environments), deployment branches master + chore/2026-uplift. Required reviewers optional. Not Production, to keep it separate from any deployment environments (victory decision log, 2026-10-26).
  • npm: on npmjs.com → react-fast-compare → Settings → Trusted Publisher → GitHub Actions: organization FormidableLabs, repository react-fast-compare, workflow release.yml, environment npm-publish. Renaming the workflow file or the environment breaks publishing until this is updated.
  • Validate: canary from the integration branch (gh workflow run release.yml --ref chore/2026-uplift) publishes X.Y.Z-canary-<datetime> to next with provenance. It needs at least one pending changeset; use a throwaway one and delete it afterwards. Done 2026-11-05: 3.2.3-canary-20261105232110, published by npm-oidc-no-reply@github.com, SLSA v1 provenance for release.yml on chore/2026-uplift.
  • npm → Publishing access → "Require two-factor authentication and disallow tokens" (done 2026-11-05, after the canary proved OIDC; earlier than planned).
  • Delete the NPM_TOKEN repo secret (unusable now that tokens are disallowed). Done 2026-11-12.
  • Before and after the final merge: see "Final merge checklist".

Phase 2: Tooling

  • oxlint + oxfmt replace ESLint 8 ([uplift] chore: replace ESLint with oxlint + oxfmt #142 config, [uplift] chore: format with oxfmt #143 format, [uplift] chore: fix oxlint errors #144 lint) (+ @typescript-eslint/parser, eslint-plugin-react). Exact-pinned (0.x). Config PR, then a format-churn PR, then a lint-churn PR, with each churn SHA in .git-blame-ignore-revs.
    • index.js stays ES5 in 3.x: no no-var/prefer-const/ES2015 autofixes on it (test and tooling files can be modern).
    • index.js's START:/END: fast-deep-equal comment blocks must survive the reformat; they're how upstream diffs are tracked (CONTRIBUTING).
  • TypeScript 7 + JSDoc (llm-splitter pattern; [uplift] chore: TypeScript 7 + JSDoc types with types:check #147):
    • tsconfig.json: allowJs, checkJs, strict, noEmit, module/moduleResolution: nodenext, lib with dom (for Element). It currently has ESLint keys (env, parserOptions) in it; those go.
    • JSDoc on isEqual (@template [A=any], @template [B=any], @param {A} a, @param {B} b, @returns {boolean}, matching 3.2.2), plus annotations in equal() for the 7 strict errors. Its own PR, no runtime change.
    • Keep the hand-written index.d.ts as the published file in 3.x. A types:check step emits declarations from JSDoc to a temp dir and fails if they differ from index.d.ts, so the two can't drift. In 4.0 the generated file replaces it.
    • Type probe (llm-splitter's type-probe idea, wired into CI; [uplift] chore: type probe against the packed tarball #148): test/types/ compiled with TS 7 (nodenext, strict) against the packed tarball, as a step in pack:check (no new script). A .cts consumer (import = require and default import) and a .mts consumer, each asserting 3.2.2's exact signature, plus React.memo(C, isEqual), shouldComponentUpdate, a useSelector-style (a, b) => boolean callback, no narrowing to never after a false result (Convert function to TypeGuard #130), and @ts-expect-error negatives. Replaces test-ts-defs/test-ts-usage; drops react-dom, react-redux, redux, @types/react-dom, @types/react-redux; @types/react 16.9 → 19. No resolution matrix: see decision log (2026-11-06).
  • Revert Convert function to TypeGuard #130's unreleased type guard ([uplift] fix: revert unreleased type guard to 3.2.2 types #145) (a is B) to 3.2.2's boolean signature, with a test-ts-usage case that fails under the guard. Lands before pack:check.
  • pack:check (simplified from victory's; snapshot pack-snapshot.json generated from the 3.2.2 tarball, --update for intended changes, --tarball to check any tarball; Linux CI only; [uplift] chore: add pack:check against the published 3.2.2 tarball #146): on pnpm pack output:
    • publint + attw.
    • Isolated require() and import(): require('react-fast-compare')(a, b) must stay callable, and import isEqual from must work in Node ESM.
    • Compatibility with 3.2.2: same file list and manifest fields (main, types/typings, files), identical index.d.ts, and index.js minified byte-identical to 3.2.2's, so formatting, comments and JSDoc are the only changes. Dropped once 4.0 work starts.
  • Size report, replacing terser | gzip | wc -c and the hand-edited README badge: scripts/size.mjs (terser, already a dep for pack:check; raw, min, min+gzip, min+brotli), printed locally and in CI's job summary. --base <git ref> adds a diff; CI passes HEAD^1 (fetch-depth: 2). Reported, not gated. The badge is gone (README and PR template point to pnpm size).
  • Remove cruft (codecov + README badge and AppVeyor/IE11 Karma config and references done in [uplift] chore: pnpm 12, Node 26, vendored fast-deep-equal test cases #141; the rest with the Vitest migration in Phase 3): builder, nyc, sinon (use vi.spyOn), jsdom-global, Babel, core-js, webpack, Karma and its launchers, karma.conf.ie.js, test/mocha.opts, .eslintrc.yml/.eslintignore.
  • Minimal dev dependencies. Keep only what a script, test or CI step strictly needs; audit with knip at the end of each phase. builder goes first. Audited after Phase 4: nothing unused. knip runs through pnpm dlx with knip.jsonc (not a devDependency); its four false positives (.ncurc.cjs, the test/types/ probe files, attw run through a helper, zizmor) are listed there.
  • Small script surface, like victory. The entry points are check, format and test:
    • check: everything CI gates on, run in sequence (format, lint, lint:gha, types, test, pack:check).
    • format / format:fix: oxfmt check / write.
    • test: Vitest.
    • Only the pieces check calls stay as scripts (lint/lint:fix, lint:gha, types:check, pack:check), plus dep:check, bench, size and changeset. The rest go (eslint, tslint, test-*, compress, size-min-gz, preversion, benchmark). dep:check added last; benchmark → bench in Phase 4.
  • AGENTS.md (+ CLAUDE.md pointer), as in victory and llm-splitter. It covers: ES5 + no published change in 3.x, JSDoc types, the upstream comment blocks, changeset rules.
  • dep:check (npm-check-updates, 7-day cooldown) and a documented manual update routine. No Renovate/Dependabot version PRs; Dependabot security alerts are already on (67 open on 2026-11-12, all from yarn.lock on master); close Audit security alerts #70 once the final merge clears them.

Phase 3: Tests

One set of spec files, run in Node and in real browsers.

  • Vitest 5 with two projects (decided 2026-11-05). Specs are test/*.spec.mjs; test/fixtures/index.mjs holds our own cases. index.js and the vendored fixtures are CommonJS: Node loads them natively, and the browser project wraps them with a 10-line inline plugin (decision log, 2026-11-07).
    • node: plain Node; jsdom only for the specs that need a DOM.
    • browser: Vitest browser mode + Playwright, Chromium, Firefox and WebKit headless. This replaces Karma and closes Browser tests: integrate sauce labs. #12 (Sauce Labs).
  • Vendor the upstream test cases ([uplift] chore: pnpm 12, Node 26, vendored fast-deep-equal test cases #141; pulled forward: pnpm packs git dependencies, which honors upstream's files list, so spec/ wasn't installed): copy fast-deep-equal@3.1.3's spec/tests.js and spec/es6tests.js into test/fixtures/ with its MIT notice, and drop the fast-deep-equal-git git dependency. They're frozen upstream anyway, and isEqual throws on Temporal values because it calls valueOf #139/Bug: Set compares by reference not value #50 will add our own cases next to them.
  • Drop comparative tests and deps (Phase 4): lodash, nano-equal, shallow-equal-fuzzy. No test uses them; only benchmark/index.js does, so they go with it in Phase 4.
  • React tests on React 19 and a CI job on React 18.0.0, plus preact/compat 10 and 11. Replace react-test-renderer (deprecated in 19) with react-dom (createRoot + flushSync; renderToString never calls shouldComponentUpdate). @testing-library/preact and @testing-library/dom dropped too: Preact's own render is synchronous. Keep React 16/17 out of CI. The library doesn't depend on React, and its React handling ($$typeof + _owner) is the same shape across versions.
  • Coverage via @vitest/coverage-v8 in the CI job summary (no Codecov). Node project only (pnpm test --project node --coverage): v8 coverage doesn't run in Firefox or WebKit.
  • CI: ubuntu with Node .nvmrc + browsers; keep windows for the Node project only (done with Vitest). zizmor in CI (lint-gha job, zizmorcore/zizmor-action, zizmor 1.31.1, online audits) and pnpm lint:gha; hybrid action pinning (actions/*/github/* on tags, the rest on SHAs, enforced by zizmor's unpinned-uses; pinact run --update with a 7-day min_age); permissions: {} was already in place. Configs as in victory. Build-free, so the CI step that matters most besides tests is pack:check.

Phase 4: Benchmarks (decided 2026-11-05: self-regression)

  • vitest bench (Vitest 5: test(name, ({ bench }) => bench.compare(...)) in bench/compare.bench.mjs, run in the derived node (bench) project) over each fixture group (generic, es6, react, custom), comparing:
    • the working tree (this tree),
    • the last published version (react-fast-compare-published@npm:react-fast-compare@3.2.2; bump after each release, once it clears minimumReleaseAge), and
    • fast-deep-equal@3.1.3 (es6/react) as the upstream baseline we forked from.
    • A candidate that throws on a group's cases is left out of that group and named in the test title (today: fast-deep-equal on custom, the Object.create(null) cases).
  • pnpm bench locally, and a report-only bench CI job: bench/summary-reporter.mjs writes one table per group (ops/s, mean, RME, samples, % vs. published) to the job summary. No threshold; the job fails only if the benchmark breaks.
  • Remove benchmark/index.js, benchmark (the npm package), lodash, nano-equal, shallow-equal-fuzzy, assets/benchmarking.png, the README chart and the Google Sheet step in CONTRIBUTING. The README keeps a short "Benchmarking" section on how to run it; CONTRIBUTING has a "Benchmarks" section.
  • First numbers (Node 26, local arm64 Mac): this tree = 3.2.2 within noise in every group; fast-deep-equal is ~3% faster on generic and ~20% faster on es6 (likely our ES5 hand-lowered Map/Set iteration; a 4.0 question).
  • Why keep it at all: speed is the library's whole pitch, and the next source changes (isEqual throws on Temporal values because it calls valueOf #139 Temporal, Bug: Set compares by reference not value #50 Sets) touch the hot path.

Phase 5: Docs and housekeeping

  • README (Nearform naming; repo URL stays FormidableLabs/react-fast-compare): npm-only install; fix "tracks fast-deep-equal@3.1.1" (it's 3.1.3); fix the broken LICENSE link (blob/readme/LICENSE); update badges; settle maintenance status/branding (Formidable → Nearform?). The "ES5 / IE9" claims stay true in 3.x and change in 4.0.
  • CONTRIBUTING rewritten for pnpm, Vitest, oxlint/oxfmt, JSDoc types, changesets, canary, dep:check, bench.
  • .github/pull_request_template.md: drop the bundle-badge and test-ts-* items.
  • Close or settle: Dependabot PRs Bump minimatch from 3.0.4 to 3.1.2 #122/Bump engine.io from 6.4.1 to 6.4.2 #124/Bump socket.io-parser from 4.2.2 to 4.2.3 #128 (Karma tree, gone), Browser tests: integrate sauce labs. #12 (Sauce Labs → Playwright), Investigate using Backstroke to track changes to fork source  #24 (Backstroke, obsolete). Audit security alerts #70 (alerts) stays open until the final merge (see "Final merge checklist").
  • Delete stale remote branches: ci-provenance, experiment/es6-circ-refs, feature/es6-extras, fix/missing-patch, task/update-dependencies, viper1104-dom, the dependabot ones, and changeset-release/master when idle.
  • Optional: move react-fast-compare-Hero.png into .github/ Deleted: it was Formidable-branded; the README uses the Nearform banner (oss.nearform.com/api/banner.svg) as victory does.

Final merge checklist

Reviewed 2026-11-12: master hasn't moved since the branch was cut; CI is green on chore/2026-uplift; master has no branch protection or rulesets (no stale required checks); npm-publish allows master; no pending changesets.

Before:

  • Last integration commit (b882172): AGENTS.md "Branch" line says PRs target master; drop chore/2026-uplift* from ci.yml's push branches (the merge PR's pull_request run still covers it).
  • Repo settings: allow merge commits (mergeCommitAllowed is false); turn off "Automatically delete head branches" (deleteBranchOnMerge is true) so the integration branch survives the merge.
  • Open the PR chore/2026-uplift → master (Chore/2026 uplift #156). Gate: CI green, including pack:check (published files match 3.2.2).

Merge:

  • Merge commit (not squash; 1e16aab, 2026-11-12), so the .git-blame-ignore-revs SHAs stay valid.
  • Watch release.yml's first push run on master (run: select-mode passed, every other job skipped; latest still 3.2.2): with no changesets and 3.2.2 already published, it should publish nothing. It's the first run of the split version/publish jobs (only the canary path has run so far).

After:

  • Remove chore/2026-uplift from the npm-publish deployment branches, then delete the branch (and the local chore/uplift-* branches).
  • Restore the repo settings (merge commits off, squash on, auto-delete head branches on).
  • Check the 67 yarn.lock Dependabot alerts closed on their own, then close Audit security alerts #70. Still open on 2026-11-12 after the merge (rescan pending).
  • Settle open PRs against master: Add coment to explain NaN verify #135 (rebase after the oxfmt churn, merge) and feat: add comparison of functions by content #115 (function comparison, declined so far; it will conflict with the uplift).

After the uplift (source, PRs straight to master)

4.0.0 (later; sketch only)

  • ESM + CJS with an exports map, sideEffects: false, modern target (drop ES5/IE; for...of instead of the hand-lowered iterators), .d.ts generated from JSDoc (.d.cts with export =, .d.mts with export default).
  • Open question for then: author ESM and generate the CJS (tsdown/rolldown, keeping module.exports = isEqual), or keep CJS source with a thin ESM wrapper (no dual-package hazard, since the function is stateless).
  • Type probe: once there's an exports map, add the resolution matrix (node10 ignores exports, so it falls back to main/types). TS 7 can't run node10 or esModuleInterop: false, so that needs an aliased TS 6 devDep (e.g. typescript-6@npm:typescript@6.0.3, ignoreDeprecations: "6.0").
  • Changeset migration note: deep imports (react-fast-compare/index.js) stop resolving under exports.

Decision log

Date Decision Why
2026-11-05 Integration branch chore/2026-uplift, PRs into it, merge commit to master Same as victory; keeps master releasable.
2026-11-05 Keep master as the default branch No churn for contributors, links and dependents' tooling.
2026-11-05 Uplift has no published change; #139 ships as 3.x from the new pipeline; ESM/exports/modern target in a later 4.0.0 Most of the ~107M monthly downloads come through ^3/^2 ranges. A 3.x fix reaches them; a 4.0 wouldn't until dependents bump. Keeps the pipeline's first release low-risk.
2026-11-05 Types via JSDoc + checkJs on plain JS (llm-splitter pattern), not a TS port No build step for the code. TS 7 emits today's index.d.ts exactly from a 5-line JSDoc block.
2026-11-05 Benchmarks: self-regression (tree vs last published vs fast-deep-equal), report-only Speed is the pitch, and upcoming fixes touch the hot path. Other libraries' numbers don't help maintain this one.
2026-11-05 No task runner; plain scripts Single package, sub-second tasks.
2026-11-05 Tokens disallowed on npm right after the canary, not after #139 The canary proved OIDC end to end; no reason to keep a token path open.
2026-11-05 Canary 3.2.3-canary-* stays on next Harmless; the next canary replaces it.
2026-11-05 Scripts: check, format, test as the entry points, plus only the sub-scripts they call Same shape as victory; fewer names to learn and keep working.
2026-11-05 Vitest (Node + browser mode/Playwright) over node:test + another browser runner One spec set, one tool, both environments; matches victory.
2026-11-05 Vendor fast-deep-equal's spec cases, drop the git dependency Upstream frozen since 2020; git deps are slow and fragile.
2026-11-05 npm trusted publishing and the GitHub environment are set up by hand, not scripted One package; victory's npm-trust.sh only pays off across 29.
2026-11-05 Dev dependencies kept to the strict minimum; builder removed Every dependency is upkeep (alerts, dep:check, release-age holds). A single-file library needs few tools.
2026-11-05 Codecov removed early (#141), not replaced until Vitest coverage The codecov@3 uploader crashes in CI; nyc still prints a summary.
2026-11-05 Dev files follow victory: .nvmrc (26), .editorconfig, oxc configs, .ncurc.cjs, zizmor/pinact configs Same habits across our repos.
2026-11-05 No .vscode/ in git; it stays gitignored Maintainer call while reviewing the oxc config PR; departs from victory.
2026-11-05 Revert #130's type guard (merged 2023, never released) to 3.2.2's boolean types; reconsider in 4.0 as a separate opt-in function A guard narrows the first argument to never after a false result when both arguments share a type, which breaks common if (isEqual(prev, next)) return; prev.x code. Too risky for a 3.x release reaching every ^3 user.
2026-11-06 types:check compares emitted vs. hand-written index.d.ts ignoring statement order TS 7 emits export = isEqual; before the declaration; 3.2.2's file has it after, and pack:check pins index.d.ts byte for byte. Order doesn't matter in a declaration file.
2026-11-06 Type probe without a resolution matrix: one .cts and one .mts consumer under TS 7 nodenext; matrix deferred to 4.0 TS 7.0.2 removed moduleResolution: node10/node/classic and esModuleInterop: false (TS5108, not overridable). And in 3.x the matrix tests nothing: one CJS file, main + types, no exports, so every mode resolves the same index.d.ts (attw's resolutions in pack-snapshot.json already pin that). Only the CJS vs. ESM import of export = differs.
2026-11-06 strict fixes in equal() go in comments only: casts on the isView() arguments (so a/b aren't narrowed), not on each use Leaves fast-deep-equal's upstream lines untouched apart from the if, and casting each use pushed the loop past 80 columns, where curly would have needed braces.
2026-11-07 Size report uses terser, compares with HEAD^1 in CI, and the README size badge is dropped rather than made dynamic terser is already a dependency and matches the old compress numbers (653 B min+gz, unchanged since 3.2.2). One file, so the base is a git show, not a second checkout and build as in victory. Bundlephobia-style badges measure unmangled code (README already explains the gap); a hand-edited one drifts.
2026-11-07 Browser project loads the CommonJS index.js and vendored fixtures through a small inline Vite plugin (wraps module.exports on the first line) index.js can't change shape in 3.x, the upstream fixtures are copied unchanged, and all three only assign module.exports. A plugin dependency (@rollup/plugin-commonjs, vite-plugin-commonjs) would be more upkeep for three files. The Node project loads them natively, so the CJS path users hit stays tested there (and in pack:check).
2026-11-07 One extra CI job for React 18.0.0 + Preact 10, Node project only, installed with pnpm add in the working tree The renderers only matter for the $$typeof/_owner/__v handling, which doesn't depend on the browser. One job instead of a matrix keeps CI small; the step prints and checks the installed versions.
2026-11-07 lodash, nano-equal, shallow-equal-fuzzy, benchmark stay until Phase 4 No test uses them, only the benchmark, which Phase 4 rewrites. benchmark/index.js now require()s the ESM fixtures (Node 26 require(esm)).
2026-11-07 zizmor in its own CI job (as victory's lint step) and in pnpm check as lint:gha; pinact is a local update tool only, not run in CI lint:gha is called by check, so it fits the small script surface; it skips without zizmor, and CI's test job doesn't install it, so the audit gates only in lint-gha. zizmor's unpinned-uses already enforces the pinning policy, and its online audits (impostor-commit, ref-version-mismatch) check each SHA and its version comment, so a pinact check in CI would add nothing.
2026-11-07 The older-renderers job's pnpm add is ignored inline for zizmor's adhoc-packages It installs outside the lockfile on purpose; it's still under minimumReleaseAge, and the job has no secrets and read-only permissions. A second lockfile for one job would be more upkeep.
2026-11-08 Benchmarks per fixture group, not the old "generic" and "generic + React" totals; a candidate that throws on a group is left out of it fast-deep-equal throws on 3 Object.create(null) cases, and timing the exceptions made it ~10× slower on the combined set, hiding everything else. Per group also shows where differences come from (es6).
2026-11-08 Benchmarks run in Node only by default (--project 'node (bench)'); browsers are opt-in (pnpm exec vitest bench --run --project 'browser (chromium) (bench)') Vitest 5 adds a (bench) project for every project, browsers included. Browser timers are coarse (0.1 ms), and the comparison code doesn't depend on the engine's DOM.
2026-11-08 Job summary from a ~50-line custom Vitest reporter, not the JSON reporter + a script One command; the reporter sees the same benchmarks() data. It's a no-op outside GitHub Actions, where the default reporter's table is enough.
2026-11-09 knip runs ad hoc (pnpm dlx knip) with a committed knip.jsonc, not as a devDependency or CI step It's an occasional audit when tools change; a dependency would be upkeep for a check that rarely finds anything in a repo this small. The config records the false positives so a re-run is clean.
2026-11-12 Hero PNG deleted; README uses the Nearform banner service (oss.nearform.com/api/banner.svg), as victory does The PNG had the Formidable logo baked in, and it was never published.
2026-11-12 Naming: "Nearform" everywhere in docs (not "Formidable" or "Nearform Commerce"); the repo stays FormidableLabs/react-fast-compare. LICENSE keeps its 2018 "Formidable Labs" copyright line Current company name; the repo isn't moving. A copyright notice records who held it then, so it isn't rebranded.
2026-11-09 .ncurc.cjs has only cooldown: 7; no parked majors Nothing is held back today (React 19, Preact 11, TS 7 are all current). Exact-pinned tools still show up, as they should: their upgrade is a separate PR.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions