Skip to content

feat(packs): react 18 → 19 verified pack - #53

Merged
lucasbraum merged 7 commits into
mainfrom
feat/react-pack
Oct 9, 2026
Merged

lucasbraum merged 7 commits into
mainfrom
feat/react-pack

Conversation

@lucasbraum

Copy link
Copy Markdown
Collaborator

Pack

react, >=18 <19 → >=19 <20. Companions move with it: react-dom, @types/react and @types/react-dom, as the engine's companion rules decide (not listed by the pack). Supersedes #45: same pack and ground truth, brought onto current main (merge of main at 0de525c, no conflict), with the changeset, the README table and a test fixed up.

Sources

Rules and behavior notes

Rewrites by rule:

  • use-ref-argument: useRef<T>() → useRef<T>(undefined).
  • ref-callback-return: ref={(el) => (x = el)} → a block body (sites from detect).
  • mutable-ref-object: React.MutableRefObject → React.RefObject.

To the agent, with a guide:

  • ref-object-nullable: a parameter or prop typed RefObject<T> once useRef returns RefObject<T | null>.
  • global-jsx-namespace: the removed global JSX namespace (one finding at the tsconfig's jsx line, the elements as evidence).
  • element-props-unknown: untyped element.props.
  • removed-react-dom: ReactDOM.render, hydrate, unmountComponentAtNode, findDOMNode.
  • props-with-ref.

Behavior notes, listed for review (the compiler cannot see them): errors-not-rethrown, removed-legacy-apis, act-moved-to-react, element-ref-deprecated, strict-mode-memoization, suspense-fallback-first, umd-removed, javascript-urls-blocked, use-reducer-types.

Ground-truth repositories

Repository Pinned commit (before the upgrade) Upgrade With Expected findings
excalidraw/excalidraw 9ee0b8ffcbd3664a47748a93262860321a203821 react 18.2.0 → 19.0.0 react-dom 19.0.0, @types/react 19.0.10, @types/react-dom 19.0.4 73
tldraw/tldraw 5957867a9c5c205a0b0c3665be443138b8014a79 react 18.3.1 → 19.2.1 react-dom 19.2.1, @types/react 19.2.7, @types/react-dom 19.2.3 60

Expected findings were drafted with pnpm packs:truth from each repository's own TypeScript and its upgrade commit, and read by hand; tldraw's 1,745 JSX element diagnostics are one site at the tsconfig that causes them (#45). Rules, expectations, ground truth and companions are unchanged from #45's last state.

uptide pack test output

verification.json on this branch records the run on main after #51 (commit 8573cc0); pack test react was not rerun here, since the merge touched neither packages/core/src/check nor the pack. That run:

uptide pack test react · verified
fixtures      3 cases, 6 sites · pass
ground truth
  excalidraw/excalidraw@9ee0b8ffcbd3           react 18.2.0 → 19.0.0  predicted 60, expected 73, false positives 1, false negatives 14
  tldraw/tldraw@5957867a9c5c                   react 18.3.1 → 19.2.1  predicted 39, expected 60, false positives 6, false negatives 26

  rule                         precision  recall    tp   fp   fn
  element-props-unknown             100%     60%     6    0    4
  generic                             4%     33%     1   26    2
  global-jsx-namespace              100%     51%    20    0   19
  mutable-ref-object                100%    100%     2    0    0
  props-with-ref                    100%    100%     1    0    0
  ref-callback-return               100%    100%     1    0    0
  ref-object-nullable               100%     37%    20    0   34
  removed-react-dom                 100%    100%    18    0    0
  use-ref-argument                  100%     80%     4    0    1
  overall (sites)                    93%     70%    92    7   40
  breaking (sites)                  100%     69%    89    0   40

Breaking findings: precision 100% (89 sites, none false), recall 69%, on 2 public repositories. The 7 non-breaking false positives are deprecated or unverified findings and do not count toward the verdict.

What is not covered

  • Recall on breaking sites is 69%: 40 expected sites are missed, most of them ref-object-nullable (34) and global-jsx-namespace (19 in the per-rule table, counted at the tsconfig anchor) sites the compiler does not reach, plus 4 element-props-unknown and 1 use-ref-argument. They are listed by pack test react as false negatives.
  • Behavior changes the compiler cannot see are reported as notes for review, never rewritten.
  • The engine links one copy of the target types; a nested second @types/react copy, as a real install can keep, is not modeled.

This PR on top of #45

  • .changeset/react-pack.md is minor (a new verified pack) and opens with the two repositories; numbers unchanged.
  • README "Verified packs" regenerated with pnpm docs:packs; pnpm docs:packs --check and pnpm docs:links pass.
  • packages/core/src/fix/isolate.test.ts used react as its example of a package without a pack; it is vitest now, like the two cli.test.ts cases. No other test did.
  • pnpm lint, pnpm typecheck, core (919 tests) and CLI (350 tests) pass.

Checklist

  • Every rule and note comes from a source listed in meta.sources
  • Every rule that rewrites or detects has a fixture case, with a site it must leave alone (keep)
  • Ground truth: public repositories at the commit before their upgrade; expected findings read from their own upgrade commit and the compiler, never copied from Uptide's output
  • At least two public repositories for verified; otherwise the pack ships as a candidate
  • pnpm uptide pack test <package> passes and its output is pasted above; verification.json written with --write
  • pnpm lint && pnpm typecheck && pnpm test pass
  • Changeset added (pnpm changeset): a new pack is something users notice
  • No private code, names or paths anywhere: fixtures are synthetic, ground truth is public

Supersedes #45.

🤖 Generated with Claude Code

https://claude.ai/code/session_018u9b3Pp5H68DdYZGp5Qg1U


Generated by Claude Code

@lucasbraum lucasbraum added the pack-candidate Migration pack proposed by the daily pack task label Oct 9, 2026 — with Claude
claude and others added 6 commits October 9, 2026 19:12
…ding)

Scaffold only: the example rule is still in place and ground-truth.json is empty.
The tldraw ground truth could not be drafted because Yarn 4 cannot be downloaded
by corepack behind the sandbox proxy.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PiuU3en7qKGdWHBGbAZirY
Signed-off-by: Claude <noreply@anthropic.com>
…ldraw

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PiuU3en7qKGdWHBGbAZirY
Signed-off-by: Claude <noreply@anthropic.com>
… are one site, at the tsconfig that causes them

apps/dotcom/client reads JSX element types through the global JSX namespace
("jsx": "preserve", no "jsxImportSource"); after the upgrade its lockfile
keeps a second copy of @types/react (18.3.23, for a dependency pinned to
16 || 17 || 18) beside @types/react 19, and every element in every file
errors with one fix, the "jsxImportSource": "react" the upgrade added.
Those 1745 expected sites are one, at that line of the tsconfig, with the
cause in its note. verification.json records the run of uptide-dev/uptide
with the check engine fixes (breaking precision 88%, recall 64%).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018u9b3Pp5H68DdYZGp5Qg1U
Signed-off-by: Lucas Braum <lucas.braum@deskfy.io>
…reaking precision 96%, 4 false positives)

The four breaking false positives left are engine or ground-truth-environment
issues, not rules: dev-docs/Homepage (a workspace whose tsconfig extends a
package that is not installed), two useTransform(ref) call sites in other
workspaces than the declaration that is the one cause, and
next-mdx-remote-client/rsc after a companion move the real upgrade never made.
The pack stays a candidate.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AKRx4kuV6oi25MP5WguxW9
Signed-off-by: Claude <noreply@anthropic.com>
…g precision 100%)

pack test react --write: excalidraw and tldraw, 0 false positives among breaking
findings. queue.json: react verified. The two cli fix tests that used react as their
example of a package without a pack now use vitest.

Known: packages/core/src/fix/isolate.test.ts:600 still uses react as the no-pack package and fails.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CECy7agtzRrX4SJWSAUHfS
Signed-off-by: Claude <noreply@anthropic.com>
…-pack example

- The changeset is minor: a new verified pack. It opens with the two public repositories the
  pack is scored against and keeps the numbers (2 repositories, 100% precision on breaking
  findings, 89 sites, recall 69%).
- README "Verified packs" regenerated with pnpm docs:packs: react joins ai, stripe and zod.
- packages/core/src/fix/isolate.test.ts used react as its example of a package without a
  pack; it is vitest now, as the two cli tests already are.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018u9b3Pp5H68DdYZGp5Qg1U
Signed-off-by: Lucas Braum <lucas.braum@deskfy.io>
…n Berry install

tldraw pins yarn@4.7.0 through packageManager; the runner's global Yarn is 1.22.22, so the
install failed and the react pack scored one repository instead of two.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018u9b3Pp5H68DdYZGp5Qg1U
Signed-off-by: Lucas Braum <lucas.braum@deskfy.io>
@lucasbraum
lucasbraum marked this pull request as ready for review October 9, 2026 19:41
@lucasbraum
lucasbraum merged commit d33bdd0 into main Oct 9, 2026
8 checks passed
@lucasbraum
lucasbraum deleted the feat/react-pack branch October 9, 2026 19:41
@uptide-release uptide-release Bot mentioned this pull request Oct 9, 2026
lucasbraum pushed a commit that referenced this pull request Oct 9, 2026
This PR was opened by the [Changesets
release](https://github.com/changesets/action) GitHub action. When
you're ready to do a release, you can merge this and publish to npm
yourself or [setup this action to publish
automatically](https://github.com/changesets/action#with-publishing). If
you're not ready to do a release yet, that's fine, whenever you add more
changesets to main, this PR will be updated.


# Releases
## uptide@0.6.0

### Minor Changes

- [#46](#46)
[`458aa14`](458aa14)
Thanks [@lucasbraum](https://github.com/lucasbraum)! - What changed in
`check`:
  
- **Breaking means confirmed.** A type-surface change is reported as
breaking only when the
compiler rejects your code against the target at that site, the runtime
probe saw the
export go, or a migration pack found it, in the verified tier as in the
generic one. What
nothing confirmed is listed as "possible impact", apart from the
breaking count.
- **Coverage, under every package.** `compiled 355 of 356 files in 5
workspaces; skipped:
<reason (count)>` says how much of the code that uses the package the
compiler judged, and
    the verdict says "types partly verified" when not every file was.
- **TypeScript-version-aware defaults.** A repository on TypeScript 5 is
read with
TypeScript 5's defaults for what its tsconfig leaves unset (`strict`
off, automatic
`@types`), not the bundled TypeScript 6's. Monorepos compile far more of
their files:
workspace packages declared `workspace:*` under Yarn and npm resolve to
their source,
scoped programs keep the tsconfig's ambient declaration files, and a
workspace that only
    peer-depends on a package finds the hoisted copy.
- **Companions.** `check` and `fix` move `@types/<pkg>` with `<pkg>`,
and a package released
in lockstep with it (react-dom with react): `check react` 18 → 19 moves
react-dom,
    @types/react and @types/react-dom together.
- **One finding for a root cause that is one edit.** A compiler option
(the global `JSX`
namespace `@types/react` 19 removes, read by `"jsx": "preserve"`) and a
repository
parameter several call sites trip are each one finding at the place to
edit, with the
    compiler errors as evidence.

- [#51](#51)
[`a18bebd`](a18bebd)
Thanks [@lucasbraum](https://github.com/lucasbraum)! - `check` reports
one breaking finding when call sites in different workspaces trace back
to
the same local declaration (a parameter or prop whose type names
something from the upgraded
package): the finding is anchored at the declaration, where the one edit
is, and the call
sites are listed under it as evidence with an `N call sites in M
workspaces` line, in the
terminal, the HTML report and the JSON report. Before, a site alone in
its workspace stayed a
finding of its own, so a hook typed `RefObject<HTMLElement>` and called
from three
workspaces was three findings plus the one that mattered. The JSON
report gains `root` on
call-site findings and `workspace` on evidence sites; nothing is renamed
or removed. The pack
test scorer counts the anchor as the predicted site and never the
evidence under it.

- [#49](#49)
[`2e5c510`](2e5c510)
Thanks [@lucasbraum](https://github.com/lucasbraum)! - Two fixes to
`check`:
  
- **A project whose tsconfig cannot be resolved is not compiled.** A
tsconfig that `extends` a
package or file that is not installed describes options the repository
never builds with, so
what a compiler reports there is not a place to change. `check` skips
it, never turns its
diagnostics into findings, and says so in the coverage line (a file
governed by such a
nested project is dropped from the workspace that happens to reach it):
`compiled 5 of 7 files in 2 workspaces; not compiled: docs/tsconfig.json
(extends "@tsconfig/docusaurus/tsconfig.json" cannot be resolved)`.
- **A package that only peers on the package is left in place when its
peer range rejects the
target.** What always moves with the package stays as before: the
release group published at
the target's own version (`react-dom`), `@types/*`, the exact pins of
the target, and the
packages a pack names in `companions` (each with the official page that
says so), even when their installed range already accepts the
target. Any other package whose installed peer range rejects the target
is not moved and not
compiled at another version; `check` lists it under possible impact,
never as breaking:
`? possible impact, peer conflict: next-mdx-remote-client 1.1.2 declares
react >= 18.3.0 < 19.0.0`.

- [#53](#53)
[`d33bdd0`](d33bdd0)
Thanks [@lucasbraum](https://github.com/lucasbraum)! - A verified
migration pack for React 18 → 19, scored against excalidraw/excalidraw
and
tldraw/tldraw at the commit before their own upgrade. Rewrites by rule:
`useRef<T>()` →
`useRef<T>(undefined)`, `ref={(el) => (x = el)}` → a block body, and
`React.MutableRefObject`
→ `React.RefObject`. `RefObject<T | null>`, the removed global `JSX`
namespace, untyped
`element.props`, the removed react-dom APIs and `PropsWithRef` go to the
agent with the guide;
what the compiler cannot see (errors no longer re-thrown, removed legacy
APIs, `act` moved to
"react", Strict Mode and Suspense changes) is listed for review. 2
public repositories, 100%
precision on breaking findings (89 sites, none false), recall 69% on
breaking sites; the
  sites it misses are listed in the pack test output.

### Patch Changes

- [#48](#48)
[`94611a9`](94611a9)
Thanks [@lucasbraum](https://github.com/lucasbraum)! - `check` compiles
your code with the repository's own TypeScript (the
`node_modules/typescript`
its build uses, found the way `fix` has found it since verification),
and falls back to the
bundled compiler only when the repository installs none. Errors and
their positions are the
ones your `tsc` would print: a repository on TypeScript 4.9 is no longer
judged by TypeScript
6's rules. The coverage line says which compiler judged: `compiled 355
of 356 files in 5
  workspaces with the repo's TypeScript 4.9.5`.
  
A workspace without a tsconfig of its own is now configured by the
nearest one above it (a
monorepo root whose `include` covers the workspace), for its options
only; synthetic defaults
  apply only when no tsconfig exists anywhere.

Co-authored-by: uptide-release[bot] <338861287+uptide-release[bot]@users.noreply.github.com>
lucasbraum added a commit that referenced this pull request Oct 10, 2026
…ATH is classic (#54)

## What and why

On a repository whose `package.json` pins `"packageManager": "yarn@4.x"`
(any Yarn 2 or later) while the global `yarn` is classic 1.x, or there
is none, `uptide fix` and `uptide verify` failed to install: Yarn
stopped with "This project's package.json defines packageManager
yarn@4.7.0. However the current global version of Yarn is 1.22.22". The
same failure the pack-test job had on tldraw's ground truth (#53), fixed
there with corepack in CI.

The installer used by `fix` and `verify`
(`packages/core/src/fix/managers/availability.ts`) now:

- **Detects** a pinned Yarn 2+ that the `yarn` on PATH cannot run:
classic 1.x, another version than the pin, or no yarn at all
(`needsCorepack`). A Berry on PATH at the pinned version is used as
before.
- **Runs it through corepack** when corepack is available: the same
install as `corepack yarn install --immutable --mode=skip-build`, with
`COREPACK_ENABLE_DOWNLOAD_PROMPT=0` and `COREPACK_ENABLE_AUTO_PIN=0`
(`throughCorepack`). Nothing on the user's machine changes outside
corepack's own cache: no `corepack enable`, no shim, no rewrite of
`packageManager`. Every later install in the run (`updateArgs`, the
plain `install`) goes through the same route (`PackageManager.via`).
- **Exits 2 before cloning** when corepack is missing
(`PACKAGE_MANAGER_UNAVAILABLE`, raised by `run.ts` before any clone),
with the exact command:
  ```
This repository pins yarn@4.7.0 (packageManager), but the yarn on PATH
is 1.22.22 and corepack is not available to run the pinned version.
Nothing was cloned or installed.
  Next: corepack enable
  ```
- **Respects `COREPACK_NPM_REGISTRY`** from the environment (it passes
through to the corepack call as it is) and names it only when corepack
could not fetch the pinned version, together with the manual activation
command.

`docs/troubleshooting.md` gains "Yarn 4 says the global Yarn is 1.x",
and the "Package manager not available" section points to it.
`packs/tooling/truth.ts` (ground-truth installs for `pack test`) is
unchanged; CI handles that with `corepack enable` since #53.

## How it was verified

Unit tests in `availability.test.ts`, against a new fixture
`fixtures/repos/yarn-berry-pinned` (synthetic: pins `yarn@4.7.0`, Berry
lockfile, node-modules linker, nothing installed):

- detection: pinned Berry with no yarn, with classic 1.22.22, with Berry
4.6.0 → corepack; with Berry 4.7.0 → not; classic and pnpm repositories
→ never;
- command selection: `corepack yarn install --immutable
--mode=skip-build`, prompt off, `via: 'corepack'`, `updateArgs` prefixed
with `yarn`;
- the probe sequence (`yarn --version`, `corepack --version`, `corepack
yarn --version`) and that `corepack enable` is never run;
- corepack missing → exit-2 error with `Next: corepack enable`; corepack
unable to fetch → message with `COREPACK_NPM_REGISTRY` and the
activation command (and the registry hint absent when corepack is simply
missing); a corepack yarn at the wrong version →
`PACKAGE_MANAGER_VERSION`.

`pnpm lint`, `pnpm typecheck`, `pnpm docs:links`, the private-material
guard, core (928 tests) and CLI (350 tests) pass. One unrelated test
(`check/progress.test.ts`, worker-event forwarding) failed once while
the suite shared the machine with the build, and passed three times in
isolation.

## Checklist

- [x] Tests for the change (a bug fix starts with a failing test)
- [x] `pnpm lint && pnpm typecheck && pnpm test` pass
- [x] README and docs updated if behavior changed
- [x] Changeset added if a user would notice (`pnpm changeset`): patch
- [x] No new dependency, or it was agreed in an issue
- [x] No private code, names or paths in fixtures, tests or docs
- [x] The rules in CONTRIBUTING.md ("Rules that do not bend") still hold

🤖 Generated with [Claude Code](https://claude.com/claude-code)

https://claude.ai/code/session_018u9b3Pp5H68DdYZGp5Qg1U

---
_Generated by [Claude
Code](https://claude.ai/code/session_018u9b3Pp5H68DdYZGp5Qg1U)_

Signed-off-by: Lucas Braum <lucas.braum@deskfy.io>
Co-authored-by: Lucas Braum <lucas.braum@deskfy.io>
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

pack-candidate Migration pack proposed by the daily pack task

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants