Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions .changeset/react-runtime-peer-upper-bound-3741.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
---
'@object-ui/react-runtime': patch
---

Give `@object-ui/react-runtime`'s React peer range an upper bound: `peerDependencies.react` narrows from `>=18` to `^18.0.0 || ^19.0.0`, the spelling the other 30 react peers in the fixed version group already declare (objectui#3741).

An unbounded range is a promise that grows on its own. `>=18` satisfies React 20 the day React 20 is published — a major this package has never been built against, let alone tested — and it makes that claim from an already-published manifest, with no commit and no review to point at. This package is the least appropriate place in the workspace for such a promise: it vendors react-runner and evaluates author-supplied JSX against the *host's* React, so a host on an untested major does not fail at this package's boundary, it fails somewhere inside the page it rendered.

Nothing in the package wanted the wider range. Its entire React surface is `Component`, `createElement`, `isValidElement`, `ReactElement` and `ReactNode`, all unchanged in React 19 and none of them touching anything React 19 removed. The range was never a stated constraint either: `>=18` was written when the package was created on 2026-06-30 (`d23d6ebfa`, PR #2105) and no commit since revisited the line. The workspace pins React to `19.2.8` through the root `pnpm.overrides`, so 19 is the only major this package's tests have ever exercised — the unbounded upper end was untested by construction.

The README sentence restating the range moves with it, and the doc gate that compares the two (`doc-version-claims.test.ts`, objectui#3717) keeps them in step from here.

A new pin, `scripts/__tests__/react-peer-range-norm-3741.test.ts`, now asserts the norm across every workspace manifest, because this was the third package born off-norm and the first two were each corrected in isolation: `plugin-dashboard` was born narrow and fixed on 2026-05-08 (`d2b6ecec6`), `plugin-report` was born narrow and fixed in objectui#3690 (PR #3727) after a React 19 consumer hit `ERESOLVE` on install, and this one was born unbounded. The existing doc gate could not have caught any of them — it checks a README against its own manifest, and react-runtime's two sides agreed with each other while both said `>=18`.
2 changes: 1 addition & 1 deletion packages/react-runtime/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ lazy-load it behind a capability flag.
npm install @object-ui/react-runtime
```

`react >= 18` is a peer dependency.
`react ^18.0.0 || ^19.0.0` is a peer dependency.

## Usage

Expand Down
2 changes: 1 addition & 1 deletion packages/react-runtime/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
"sucrase": "^3.35.0"
},
"peerDependencies": {
"react": ">=18"
"react": "^18.0.0 || ^19.0.0"
},
"devDependencies": {}
}
65 changes: 45 additions & 20 deletions scripts/__tests__/doc-version-claims.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,9 +56,14 @@ import { fileURLToPath } from 'node:url';
* untouched, and both directions of the ratchet report green over a README that now
* misstates the range. objectui#3690 was exactly that shape, found by a human census
* (there the README was right and the MANIFEST lagged it — the assertion is symmetric, it
* names the disagreement, not the guilty side). objectui#3741, still open, proposes
* narrowing react-runtime's manifest range; the day someone does, this test goes red until
* that README follows.
* names the disagreement, not the guilty side). objectui#3741 was the prediction this
* paragraph used to carry as future tense: it narrowed react-runtime's manifest from the
* unbounded `>=18` to the group's `^18.0.0 || ^19.0.0`, and this test did exactly what was
* written here — it went red until that README followed, in the same change. Worth keeping
* as the worked example, because it is also the case that shows what this assertion does
* NOT do: both sides said `>=18` and AGREED, so nothing here objected for the five weeks
* the unbounded range sat in a published manifest. Agreement is not correctness. The norm
* itself is asserted separately, in `react-peer-range-norm-3741.test.ts`.
*
* Two boundaries, both deliberate, both pinned below:
*
Expand Down Expand Up @@ -554,9 +559,9 @@ const KNOWN_CLAIMS: KnownClaim[] = [
{ file: 'packages/react/README.md', claim: PEER_18_19, kind: 'restatement', why: PEER_RESTATEMENT_OK },
{
file: 'packages/react-runtime/README.md',
claim: 'react >= 18',
claim: 'react ^18.0.0',
kind: 'restatement',
why: 'Restates this package peerDependencies.react, which is literally ">=18"the one README whose looser spelling is the manifest spelling.',
why: 'Restates this package peerDependencies.react verbatim, in the prose spelling rather than a peer block. Until objectui#3741 both sides read ">=18": the manifest was the last react peer in the workspace with no upper bound, so the README faithfully restated a range that would have claimed React 20 the day it shipped. Narrowing the manifest to the group norm brought this line with it — the restatement was never the defect, the range it restated was.',
},
{
file: 'packages/plugin-chatbot/README.md',
Expand Down Expand Up @@ -594,7 +599,7 @@ const RANGE_OPENS = /^[\^~><=\d]/;
* - a bullet in a peer-dependency list — 11 of the 12 entries, one spelling between them:
* - `react` ^18.0.0 || ^19.0.0
* - one code span read as prose, `packages/react-runtime/README.md` alone:
* `react >= 18` is a peer dependency.
* `react ^18.0.0 || ^19.0.0` is a peer dependency.
*
* Note what the range is here and is not in the inventory key: the key stops at the first
* version token (`^18.0.0`), this reads the range to end of line (`^18.0.0 || ^19.0.0`).
Expand Down Expand Up @@ -689,19 +694,28 @@ function peerBlockBullets(rel: string): PeerBullet[] | null {
/**
* Range equality: VERBATIM, up to whitespace INSIDE the range.
*
* That one-space normalisation is not a softening, and it exists for a measured reason.
* `packages/react-runtime/README.md` writes `react >= 18` while its manifest writes
* `>=18`. Nothing is drifted there — the two spell the same single comparator and npm
* parses them identically — so byte-strict equality would paint that entry red on a tree
* where nothing is wrong. The only two answers to such a red are to rewrite one side for
* the gate's benefit, or to declare the entry uncovered: a cosmetic edit, or lost
* coverage, in exchange for nothing.
* HISTORY — why the normalisation was written. `packages/react-runtime/README.md` wrote
* `react >= 18` while its manifest wrote `>=18`. Nothing was drifted there — the two
* spell the same single comparator and npm parses them identically — so byte-strict
* equality would have painted that entry red on a tree where nothing was wrong. The only
* two answers to such a red are to rewrite one side for the gate's benefit, or to declare
* the entry uncovered: a cosmetic edit, or lost coverage, in exchange for nothing.
*
* Everything this assertion exists to catch survives, because whitespace is the only
* thing dropped: a bumped major, a `||` arm added or removed, `^` turning into `~`, a
* vanished upper bound all still compare unequal. Pinned by its own test below — a
* normaliser that quietly grew to strip operators would make the whole assertion vacuous
* while every other test in this file stayed green.
* CURRENT STATE — that specimen is gone. objectui#3741 narrowed react-runtime's manifest
* to the `^18.0.0 || ^19.0.0` group norm (it was the last react peer in the workspace with
* no upper bound) and brought the README sentence with it, so both sides now spell the
* range identically. Measured on the tree at that change: 21 peer statements resolve to a
* manifest counterpart and ZERO of them differ by whitespace alone. The normaliser is
* therefore exercised only by its own unit test below, not by the corpus.
*
* It stays anyway, and deliberately: the two spellings it equates are equally correct npm
* ranges, so the day a README writes `>= 19` beside a `>=19` manifest the gate should stay
* green rather than demand a cosmetic edit. Tightening it to bytes would buy no new defect
* class — everything this assertion exists to catch survives the normalisation, because
* whitespace is the only thing dropped: a bumped major, a `||` arm added or removed, `^`
* turning into `~`, a vanished upper bound all still compare unequal. Pinned by its own
* test below — a normaliser that quietly grew to strip operators would make the whole
* assertion vacuous while every other test in this file stayed green.
*/
const sameRange = (a: string, b: string): boolean =>
a.replace(/\s+/g, '') === b.replace(/\s+/g, '');
Expand Down Expand Up @@ -1152,6 +1166,15 @@ describe('doc version claims - the peer-line assertion', () => {
dep: 'react-router-dom',
range: '^6.0.0 || ^7.0.0',
});
// react-runtime's prose sentence, in the spelling it carries since objectui#3741.
expect(parsePeerStatement(ticked('react ^18.0.0 || ^19.0.0') + ' is a peer dependency.')).toEqual({
dep: 'react',
range: '^18.0.0 || ^19.0.0',
});
// The same shape carrying a single comparator. No longer in the corpus — it WAS
// react-runtime's spelling until objectui#3741 narrowed it — but kept because
// RANGE_OPENS admits `>` and a prose line is the shape most likely to be written
// that way again.
expect(parsePeerStatement(ticked('react >= 18') + ' is a peer dependency.')).toEqual({
dep: 'react',
range: '>= 18',
Expand All @@ -1175,8 +1198,10 @@ describe('doc version claims - the peer-line assertion', () => {
});

it('treats whitespace inside a range as insignificant, and nothing else', () => {
// The one pair the normalisation exists for, and the only place in the corpus where
// README and manifest differ by anything at all.
// The pair the normalisation was written for. It is no longer a corpus pair — since
// objectui#3741 every one of the 21 resolved statements matches its manifest byte for
// byte — so these two cases are now the ONLY thing keeping the normalisation honest.
// See `sameRange` for why it is kept rather than tightened to bytes.
expect(sameRange('>= 18', '>=18')).toBe(true);
expect(sameRange('^18.0.0 || ^19.0.0', '^18.0.0||^19.0.0')).toBe(true);

Expand Down
Loading
Loading