feat(forms-rjsf): accept @rjsf 6.x and make the theme injectable - #58
Merged
Merged
Conversation
`@rjsf/shadcn` — the theme every frontend in this ecosystem wants — is published on the 6.x line only, peering `@rjsf/core@^6`. The adapter's peer range was `^5.20.0`, so the two could not be installed together: an npm peer conflict at install time, before a line of form code got written. The adapter was unusable by exactly the consumer it was written for. Peers move to `^5.20.0 || ^6.0.0` rather than to 6-only. No adapter code was needed to span the two majors, because every API it touches is shape-identical across them: `@rjsf/core`'s default export and `FormProps`, `@rjsf/validator-ajv8`'s default export, and the `schema` / `formData` / `validator` / `liveValidate` / `onSubmit` / children props. The one signature that did change widened rather than moved — `liveValidate` went from `boolean` to `'onChange' | 'onBlur' | boolean`, so the `false` we pass is still valid. This follows the convention set by #55, which declared `ai@^5 || ^6 || ^7` while dev-testing only the newest. Reaching the shadcn theme needed a seam that did not exist. The README already told readers to "pass your own `Form` from the themed package", and there was no prop that accepted one — the core `Form` was imported and rendered directly. `<RjsfForm />` now takes an optional `form` (`ComponentType<FormProps>`, the type every RJSF theme's default export already has), defaulting to `@rjsf/core`'s. That is hard-don't #8's prescribed slot API: `@rjsf/shadcn` is a devDependency for the smoke test only, never a runtime or peer dependency, so no UI kit is bundled. Verification, since a peer range is a promise: - 6.x is what the dev tree installs and CI tests. New smoke test renders and submits through the real `@rjsf/shadcn` Form and asserts the theme actually rendered (its Tailwind classes, not `@rjsf/core`'s bare `form-control`). - 5.x was exercised by hand, once: `@rjsf/{core,utils,validator-ajv8}` pinned to `^5.24.0`, `pnpm install`, `@rjsf/core@5.24.13` resolved — `rjsf-form.test.tsx` 4/4 green and `tsc --noEmit` clean over the source. It is not re-checked per commit, and the tests say so. - It cannot be, in one dev tree: pnpm matches peers by package NAME, so an `npm:`-aliased 5.x install silently binds the 6.x `@rjsf/utils` (measured — `@rjsf+core@5.24.13_@rjsf+utils@6.8.0_...` appears in `node_modules/.pnpm`) and the "5.x matrix" would be 6.x wearing a 5.x label. `packageExtensions`, the documented fix, is not honoured by pnpm 11.1.1 from either `pnpm-workspace.yaml` or `package.json#pnpm`. - A guard pins the declared range against the major actually installed and against the major `@rjsf/shadcn` peers on, so this drift has to be deliberate next time. Mutation-tested three ways (narrow the peer range / ignore the injected form / drift the declared range) — each turns it red. `peerDependencyRules.ignoreMissing: [tailwindcss]` keeps `pnpm peers check` clean: `@rjsf/shadcn` pulls `tailwindcss-animate`, whose peer range is the malformed `">=3.0.0 || insiders"` and is therefore unmet whatever is installed. forms-rjsf 3 -> 11 tests. Workspace: 595 passed across 21 packages, build + typecheck clean. `minor` changeset. Closes #57 Claude-Session: https://claude.ai/code/session_0191cTtmnhFbC2ZKNtgxHXye
Review repair. The PR claimed a 5.x matrix was impossible; it is not, and
the gap it left was demonstrable.
`packages/forms-rjsf` publishes `@rjsf` peers at `^5.20.0 || ^6.0.0` but
nothing automated exercised the 5.x half, so a value that is legal on 6.x
and illegal on 5.x shipped green. Reproduced: change `liveValidate={false}`
to `liveValidate="onChange"` (6.x widened the type to
`'onChange' | 'onBlur' | boolean`) and typecheck + all 17 tests + CI stay
green, while the same source against real @rjsf/core@5.24.13 declarations
gives `TS2769 ... Type 'string' is not assignable to type
'boolean | undefined'`. A consumer the peer range invites gets a hard
compile error nothing here could catch.
The impossibility claim was overstated. pnpm matching peers by package name
rules out two alias trees inside ONE install; a CI matrix is two INSTALLS.
`scripts/pin-rjsf-5x.mjs` rewrites the three `@rjsf` devDependencies to the
5.x clause READ OFF the declared peer range, drops the 6.x-only
`@rjsf/shadcn` and its smoke test, and a plain `pnpm install` resolves a
plain 5.x tree — no aliases, no packageExtensions. The new `rjsf5` CI job
runs it, asserts a real 5.x tree resolved (`--verify` — a job silently
testing the wrong major is this bug again), then typechecks and tests.
Verified end to end in a throwaway worktree: @rjsf/core@5.24.13 resolved,
`tsc --noEmit` clean, `rjsf-form.test.tsx` 4/4 — and red on the
`liveValidate` mutation above. The script no-ops if the range ever narrows
to 6-only, so the matrix retires with the promise it checks.
Three guards added, one strengthened; each mutation-tested:
- Hard-don't #8 had no test. Promoting `@rjsf/shadcn` from `devDependencies`
to `dependencies` — the obvious "make the theme just work" edit — pulled a
UI kit into every consumer's install with nothing red. Now
`@rjsf/shadcn` is asserted absent from `dependencies` and
`peerDependencies`, and the package is asserted to declare no runtime
dependencies at all. Mutation: 2 failed | 15 passed.
- `peerDependencyRules.ignoreMissing: [tailwindcss]` disables missing-peer
detection workspace-wide and permanently (pnpm honours no scoped form —
`tailwindcss-animate>tailwindcss` and `@rjsf/shadcn>tailwindcss` were both
tried). Compensating check: no workspace package may declare a
`tailwindcss` peer while the suppression is in place, and the block
retires itself if the rule is removed. Both halves mutation-tested.
- "submits through the theme" passed on an adapter that ignored the injected
`form` — @rjsf/core's form submits identically, so only one of the two
theme tests guarded the feature. Both now assert theme identity through
one `expectThemedInput` helper keyed on @rjsf/core's `form-control` class
rather than on a Tailwind class that can move between releases. Mutation
(`form ?? Form` -> `Form`): 1 failed -> 2 failed.
`FormImpl` is recomputed per render, so a non-stable `form` prop remounts
the form and discards in-progress input. Memoizing cannot fix it — the
changed identity arrives as the prop, and React reconciles by element type —
so the requirement is documented on the prop, in the README and in the
palette-design skill instead.
README, test docstrings and the skill no longer state the impossibility;
they say both majors run in CI, which is now true.
forms-rjsf 11 -> 17 tests. Workspace: 601 passed across 21 packages,
`pnpm build` / `pnpm typecheck` / `pnpm test` all exit 0, `pnpm peers check`
clean.
Claude-Session: https://claude.ai/code/session_0191cTtmnhFbC2ZKNtgxHXye
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #57.
The problem
@rjsf/shadcnis published on the 6.x line only (6.8.0, peering@rjsf/core@^6.8.0).acture-forms-rjsf@1.0.0peered@rjsf/core@^5.20.0. The two could not be installed together — an npm peer conflict at install time, i.e. before anyone wrote a line of form code. The adapter was unusable by exactly the consumer it was written for.There was a second half to that, not named in the issue: even with the range fixed, there was no way to hand the theme to the component.
rjsf-form.tsximportedFormfrom@rjsf/coreand rendered it directly. The README's "Theming" section told readers to "pass your ownFormfrom the themed package" and no prop accepted one.What changed
1. Peer range →
^5.20.0 || ^6.0.0for@rjsf/core,@rjsf/utils,@rjsf/validator-ajv8. Widened, not moved to 6-only — see Why dual below.2.
<RjsfForm />takes an optionalformprop —ComponentType<FormProps>, which is the type every RJSF theme's default export already has — defaulting to@rjsf/core's unstyled Form. Also exports theRjsfFormComponenttype.This is hard-don't #8's prescribed fix ("Adapter packages expose unstyled components OR a config slot"), not a violation of it:
@rjsf/shadcnis a devDependency for the smoke test only — never a runtime dependency, never a peer. No UI kit is bundled. That claim now has a test (below); it did not before.3. Both halves of the declared peer range run in CI — a new
rjsf5job. See The 5.x matrix below.4. Docs updated in the same pass — package README (install + a rewritten Theming section with the palette-adapter binding and the
form-stability note), theindex.tsmodule docstring, the npmdescription, and theacture-palette-designskill's Form adapters section.Adapter code needed no change to span the two majors
Checked at source, from the published
.d.tsof@rjsf/core@5.24.13and@rjsf/core@6.8.0. Everything this adapter touches is shape-identical:@rjsf/coredefault exportFormFormFormPropsexported@rjsf/validator-ajv8default exportschema/validator/formData/childrenonSubmit(data: IChangeEvent, event) => voidliveValidateboolean'onChange' | 'onBlur' | booleanThe only signature that moved widened, so the
falsewe pass is still valid.@rjsf/utilsis a declared peer but is never imported by this package's source.Why dual rather than 6-only
inputSchema#55 movedacture-ai-vercel'saipeer to^5 || ^6 || ^7while dev-testing only the newest major.minor; narrowing to^6.0.0would be a breakingmajorthat the issue did not ask for as its primary option, and it would strand any 5.x consumer for no gain.inputSchema#55's precedent.The 5.x matrix (review repair — the first draft of this PR got this wrong)
The first draft shipped a hand-run-once 5.x check plus an argument that CI could not do better. The argument was overstated and the gap it left was real. Both are now fixed.
The gap, demonstrated. On the previous commit, change
rjsf-form.tsx'sliveValidate={false}toliveValidate="onChange"— a legal, documented 6.x value (the widening in the table above).pnpm typecheckexits 0, all tests pass, CI is fully green, and the package publishes with"@rjsf/core": "^5.20.0 || ^6.0.0". The same source against real@rjsf/core@5.24.13declarations:A consumer who installs
acture-forms-rjsfalongside@rjsf/core@5— which the peer range invites — gets a hard compile error, and nothing in this repo could have caught it. Acceptance criterion 2 of #57 was unmet.Where the impossibility argument went wrong. The premise is true: pnpm matches peer dependencies by package name, so an
npm:-aliased 5.x@rjsf/coresilently binds the 6.x@rjsf/utils, andpackageExtensionsis not honoured by pnpm 11.1.1 from eitherpnpm-workspace.yamlorpackage.json#pnpm. But that rules out two alias trees inside one install. A CI matrix is two installs, and the second one needs no aliases at all.What was built.
scripts/pin-rjsf-5x.mjsrewrites the three@rjsfdevDependencies to the 5.x clause read off the declared peer range, drops the 6.x-only@rjsf/shadcnand its smoke test, and then a plainpnpm install --no-frozen-lockfileresolves a plain 5.x tree. Therjsf5CI job runs it, then--verify(a job that silently tests the wrong major is this same bug again), then buildsacture, typechecks and tests. Verified end to end in a throwaway worktree:…and red on the mutation that motivated it:
The script reads the 5.x clause rather than hardcoding it, so narrowing the published range to
^6.0.0makes the job no-op instead of fail — the matrix retires together with the promise it exists to check (verified: bothpinand--verifyexit 0 with "no 5.x clause. Nothing to check — skipping.").The one thing that genuinely stays 6.x-only is the
@rjsf/shadcnsmoke test, because the theme has no 5.x release. That is why it lives in its own file.Guards and their mutation tests
Every number below re-measured on the current branch state (17 tests in
packages/forms-rjsf).peerDependenciesto^6.0.03 failed | 14 passed^5.20.0 || ^7.0.0)3 failed | 14 passedform(form ?? Form→Form)2 failed | 15 passed@rjsf/shadcnpromoted todependencies2 failed | 15 passedtailwindcsspeer1 failed | 16 passedignoreMissing: [tailwindcss]deleted frompnpm-workspace.yaml1 failed | 15 passed | 1 skipped17 passedThree of those guards are new in the repair commit, and each closes a claim this PR made with nothing behind it:
"@rjsf/shadcn"fromdevDependenciestodependencies— the obvious "make the theme just work for consumers" edit — pulled radix + lucide-react + tailwind-merge + tailwindcss-animate into every consumer's install while all 11 tests stayed green. Now asserted absent fromdependenciesandpeerDependencies, plus the stronger form the sibling adapters already satisfy: this package declares no runtime dependencies at all.tailwindcsssuppression had no compensating check.peerDependencyRules.ignoreMissing: [tailwindcss]is workspace-wide and permanent, and pnpm honours no scoped form (tailwindcss-animate>tailwindcssand@rjsf/shadcn>tailwindcsswere both tried; both still report the unmet peer). So the day a workspace package genuinely peers ontailwindcss, its missing peer would go unreported. The check asserts no workspace package declares that peer while the rule is in place — and asserts the rule is still there, so the block retires with it.formentirely, because@rjsf/core's own form submits identically. Both tests now assert theme identity through oneexpectThemedInputhelper, keyed on@rjsf/core'sform-controlclass (a discriminator that survives Tailwind class churn) rather than onrounded-mdalone. Theform ?? Form→Formmutation went from1 failedto2 failed.The
formprop must be referentially stableFormImplis recomputed on every render, so an inline theme —form={withTheme(myTheme)}in JSX, or an un-hoisted adapter arrow — is a new component type each render. React reconciles by element type, so the form unmounts, remounts, and whatever the user had typed is silently discarded and reset todefaults.Memoizing cannot fix this, which is worth stating because it is the obvious remedy: the changed identity arrives as the prop, so
useMemo(() => form ?? Form, [form])invalidates on exactly the renders that matter. The requirement is documented instead — on the prop's JSDoc, in the README's Theming section (both snippets there are safe precisely because they bind at module level), and in theacture-palette-designskill.Test delta
acture-forms-rjsfpnpm build→ 0,pnpm typecheck→ 0,pnpm test→ 0 (exit code checked, not inferred).pnpm install --frozen-lockfileclean.pnpm peers check→No peer dependency issues found.One incidental
pnpm peers checkwas clean before this PR and is clean after. Keeping it that way neededpeerDependencyRules.ignoreMissing: [tailwindcss]inpnpm-workspace.yaml:@rjsf/shadcnpullstailwindcss-animate@1.0.7, whose peer range is the malformed">=3.0.0 || insiders".insidersis not a semver range, so pnpm treats the whole range as unsatisfiable and reports the peer unmet no matter whichtailwindcssis installed — verified by installingtailwindcss@4.3.3, which did not silence it. Nothing here compiles CSS. Commented in place, and now covered by the compensating test above.Deliberately not done
rjsf5job is onci.ymlonly, not onrelease.yml's gate.ci.ymlalso runs on pushes tomain, so the 5.x check does run alongside every release; it is just not aneeds:edge on the publish step. Worth revisiting if a release ever lands without a greencirun.inputSchema#55) did not touchdocs/roadmap.mdeither.uiSchema/validatorprops. Neither is needed by Widen acture-forms-rjsf's @rjsf/* peer range to 6.x — @rjsf/shadcn ships on 6 only, so shadcn consumers cannot install it #57, andPaletteFormAdapterPropscarries nouiSchemafor the palette path to supply.createRjsfFormAdapterfactory. A one-line arrow at the call site does it.minorchangeset:.changeset/rjsf-6-peer-range-and-theme-prop.md.https://claude.ai/code/session_0191cTtmnhFbC2ZKNtgxHXye