Repository navigation
fix: published types silently resolve React to any without @types/react - #2046
HichamELBSI merged 1 commit into
Conversation
|
@unrevised6419 is attempting to deploy a commit to the Strapi Team on Vercel. A member of the Team first needs to authorize it. |
🦋 Changeset detectedLatest commit: 16b7c6a The changes in this PR will be included in the next version bump. This PR includes changesets to release 3 packages
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
|
Important Review skippedAuto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Plus Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
|
|
Confirming this from the consumer side, and flagging one more undeclared module that this PR does not yet cover. Reproduced on In that consumer, The additional gap: It currently resolves only because For context, I opened strapi/strapi#27745 for the equivalent problem in the core monorepo packages. The two are independent, but a strict-layout consumer needs both before it type checks cleanly. |
…react` The published `.d.ts` files of `@strapi/design-system`, `@strapi/icons` and `@strapi/ui-primitives` import React types, but `@types/react` was not declared as a dependency of any kind. Consumers whose package manager does not place `@types/react` on the ambient resolution path of these packages resolve `react` to its untyped runtime entry, so `React.*` degrades to `any` and `Pick`/`Omit` derived prop types lose optionality. Declared `@types/react` as an optional peer dependency, mirroring each package's own `react` peer range. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
d75888a to
16b7c6a
Compare
|
|
Thanks for the detailed write-up and the repro — the root-cause analysis is solid and the fix (optional @types/react peer, mirroring each package's own react range) is the right pattern here. LGTM to merge as-is. Could you open a separate issue (or a follow-up PR) for the @radix-ui/react-context gap you flagged in your comment above? It's a distinct problem (a real runtime dependency, not a types-only one) and shouldn't block this fix from landing. |
|
Hey @HichamELBSI, see #2061 |
…eclared
`dist/components/Select/SelectParts.d.ts` references
`import('@radix-ui/react-context').Scope` in the public types of `SingleSelect`
and `MultiSelect`, but `@strapi/design-system` declared `@radix-ui/react-context`
in no dependency field. It resolved only by accident, through the copy
`@strapi/ui-primitives` hoists under npm/yarn.
On a strict layout — pnpm's global virtual store, `node-linker=isolated`,
`hoist=false` — the upward `node_modules` walk from the published `.d.ts` never
leaves the store, and consumers get:
SelectParts.d.ts(35,32): error TS2307: Cannot find module
'@radix-ui/react-context' or its corresponding type declarations.
Unlike `@types/react` in strapi#2046, this is a real module the package's own types
depend on, so it is a plain dependency, pinned to `1.0.1` — the version
`@strapi/ui-primitives` already declares and the one that resolves today.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…eclared (#2061) Follow-up to #2046, as requested in #2046 (comment). ### What does it do? Declares `@radix-ui/react-context` as a regular dependency of `@strapi/design-system`, pinned to `1.0.1`: ```json "dependencies": { "@radix-ui/react-checkbox": "1.0.4", + "@radix-ui/react-context": "1.0.1", "@radix-ui/react-dialog": "1.0.5", } ``` Manifest-only. No source, build or runtime change. ### Why is it needed? `@strapi/design-system` names `@radix-ui/react-context` in its published type declarations but declares it in no dependency field. `dist/components/Select/SelectParts.d.ts`: ```ts declare const Root: { (props: Select.SelectProps & { __scopeSelect?: import('@radix-ui/react-context').Scope; }): import("react/jsx-runtime").JSX.Element; displayName: string; }; ``` The `Scope` type leaks out of `@strapi/ui-primitives` through the `__scopeSelect` prop, and `tsc` writes it as an inline `import(...)` specifier against the *consuming* package's resolution path. Both occurrences (lines 35 and 43) sit on the public surface — `index.d.ts` → `./components` → `./Select` → `SingleSelect.d.ts` → `import * as SelectParts from './SelectParts'` — so any consumer that type checks `SingleSelect` or `MultiSelect` reaches them. Today it resolves only by accident. `@strapi/ui-primitives` declares `@radix-ui/react-context: 1.0.1`, and under npm/yarn hoisting that copy lands at the top level of the consumer's `node_modules`, where the upward walk from the design system's `.d.ts` happens to find it. Nothing in this package's manifest asks for that, and nothing guarantees it. On a layout without that hoist it breaks. Reproduced on published `2.2.4` with pnpm 11.26.0, `enableGlobalVirtualStore: true`, `node-linker=isolated`, `hoist=false`, TypeScript 5.6.3, `strict`, `skipLibCheck: false`: ``` <store>/@strapi/design-system/2.2.4/<hash>/node_modules/@strapi/design-system/dist/components/Select/SelectParts.d.ts(35,32): error TS2307: Cannot find module '@radix-ui/react-context' or its corresponding type declarations. <store>/@strapi/design-system/2.2.4/<hash>/node_modules/@strapi/design-system/dist/components/Select/SelectParts.d.ts(43,32): error TS2307: Cannot find module '@radix-ui/react-context' or its corresponding type declarations. ``` `--traceResolution` shows why — packages link from a store outside the consuming project, so the walk never reaches the consumer's `node_modules`: ``` ======== Resolving module '@radix-ui/react-context' from '<store>/@strapi/design-system/2.2.4/<hash>/node_modules/@strapi/design-system/dist/components/Select/SelectParts.d.ts'. ======== Loading module '@radix-ui/react-context' from 'node_modules' folder, target file types: TypeScript, JavaScript, Declaration, JSON. Searching all ancestor node_modules directories for preferred extensions: TypeScript, Declaration. Directory '.../@strapi/design-system/dist/components/Select/node_modules' does not exist, skipping all lookups in it. Directory '.../@strapi/design-system/dist/components/node_modules' does not exist, skipping all lookups in it. Directory '.../@strapi/design-system/dist/node_modules' does not exist, skipping all lookups in it. Directory '.../@strapi/design-system/node_modules' does not exist, skipping all lookups in it. File '<store>/@strapi/design-system/2.2.4/<hash>/node_modules/@radix-ui/react-context.d.ts' does not exist. Directory '<store>/@strapi/design-system/2.2.4/node_modules' does not exist, skipping all lookups in it. ... File '<consumer>/node_modules/@types/radix-ui__react-context.d.ts' does not exist. ``` This is the same class of defect as #2046, but the remedy differs. `@types/react` is a types-only package the consumer supplies, so it is an optional peer. `@radix-ui/react-context` is a real module this package's own public types depend on, so it wants a plain `dependency`. #### On the version choice `1.0.1` is what `@strapi/ui-primitives` declares and therefore what already resolves for every consumer today. Pinning exactly matches the convention of the other 22 `@radix-ui/*` entries in this manifest, all of which are exact. This is deliberately a no-op for the installed tree — it declares the edge that is already there rather than moving anyone to a new version. Worth noting for whoever lands #2033 (`chore: migrate from @radix-ui/* to unified radix-ui package`): if that migration goes through, this entry folds into the unified `radix-ui` dependency along with the rest. The two do not conflict — this is the correct manifest for the current dependency shape, and #2033 rebases over one extra line. ### How to test it? The manifest change is verifiable by inspection. To see the underlying defect and the fix: 1. In a scratch consumer, set `enableGlobalVirtualStore: true` in `pnpm-workspace.yaml` and `node-linker=isolated`, `hoist=false` in `.npmrc`. 2. Install `@strapi/design-system@2.2.4` and reference `SingleSelect` in a `.tsx` file. 3. `tsc --noEmit` with `skipLibCheck: false` → the two `TS2307` errors above. 4. Apply this manifest via a pnpm `packageExtensions` override, reinstall, repeat → `@radix-ui/react-context` appears in the package's own store directory, resolution succeeds, both errors clear. Step 4 was confirmed end to end against published `2.2.4`: 2 `TS2307` errors before, 0 after, no other diagnostic changed. Repo checks on this branch, on node 22.23.2 / yarn 3.6.4: - `yarn build` — 4/4 tasks pass - `yarn test:ts` — 6/6 tasks pass - `yarn lint` — 6/6 tasks pass (1 pre-existing warning in `docs/.storybook/main.ts`, untouched by this PR) - `yarn test:unit` — 5/5 tasks pass; 32 suites, 267 tests in `@strapi/design-system`, 0 failures A `patch` changeset is included for `@strapi/design-system`. ### Related issue(s)/PR(s) - #2046 — same family of defect (undeclared module named in published declarations), types-only variant. This was flagged in #2046 (comment) and split out at maintainer request. - #2033 — unified `radix-ui` package migration; see the note above. 🤖 Generated with [Claude Code](https://claude.com/claude-code)



What does it do?
Declares
@types/reactas an optional peer dependency of the three packages whose published type declarations import React types:@strapi/design-system,@strapi/iconsand@strapi/ui-primitives.Manifest-only. No source, build or runtime change.
Why is it needed?
This does not fail loudly at an import — it silently corrupts the inferred shape of the library's public API.
The published declarations import React types directly, e.g.
dist/primitives/Box/Box.d.ts:86 declaration files in
@strapi/design-system, 214 in@strapi/icons, and 4 in@strapi/ui-primitivesimport fromreact. But@types/reactappears in neitherdependenciesnorpeerDependenciesof any of them. Thereactpackage ships no types of its own, so a consumer's TypeScript has to locate@types/reacton its own — via ambientnode_moduleslayout that these packages neither declare nor control. (Thestyled-componentsimport on line 1 is fine; that package ships its own types.)When that ambient walk fails, resolution lands on the untyped runtime entry instead:
react/index.jshas no types, soReact.*degrades toany. That propagates through the polymorphic prop helpers — andPickoveranyloses optionality, becausePick<any, 'tag'>is{ tag: any }: required, not optional.So
dist/components/SubNav/SubNavHeader.d.ts:turns a genuinely optional
taginto a required one, and correct consumer code stops compiling:<SubNavHeader label={...} />is valid usage per the design system's own docs. The error namestag, which sends a consumer hunting throughSubNavfor a bug that isn't there — the actual cause is three layers away in an undeclared types dependency. EveryPick/Omit-derived prop type in the library is exposed to this same distortion, so the visible symptom differs per consumer, which makes it that much harder to trace back here.Declaring the peer dependency fixes it because it makes the package manager place
@types/reacton the package's own resolution path, rather than leaving it to whatever the ambient layout happens to be.This is forward-looking, not one consumer's edge case
The failure was reproduced in a pnpm 11.21.0 monorepo with
enableGlobalVirtualStore: true. Under that layout packages live in a global store outside the consuming project, so TypeScript walking up from a published.d.tsnever reaches the consumer'snode_modules. Both2.2.3and2.2.4reproduce.pnpm has stated it intends to make the global virtual store the default. At that point every TypeScript consumer of the design system hits this, not only those who opted in early.
Two details a reviewer will ask about
optional: trueis required, not cosmetic. JavaScript-only consumers have no@types/react. UnderstrictPeerDependencies(or npm's peer auto-install) a non-optional peer would break their install.reactpeer range, exactly. All three packages declarereact: "^18.0.0", so the types peer is^18.0.0— no wider, no narrower. The types version must track the React version the consumer actually runs; a wider range (e.g."*") would let a consumer on React 18 satisfy the peer with@types/react@19and get wrong types with no warning, and a narrower or different range would fight the consumer's own valid choice. When these packages widen theirreactpeer to include React 19, the@types/reactpeer should widen with it.This is the established pattern in the ecosystem for React libraries that ship types — both of these mirror their own
reactpeer as an optional peer:@testing-library/react@16.3.1→"@types/react": "^18.0.0 || ^19.0.0", optionalzustand@5.0.9→"@types/react": ">=18.0.0", optional@types/react-domis deliberately not added: no published.d.tsin any of the three packages imports fromreact-dom, so there is nothing to declare.How to test it?
The manifest change itself is verifiable by inspection, but to see the underlying defect:
enableGlobalVirtualStore: trueinpnpm-workspace.yaml.@strapi/design-system@2.2.4and render<SubNavHeader label="…" />in a.tsxfile.tsc --noEmit→TS2741: Property 'tag' is missing.tsc --traceResolution | grep "Resolving module 'react' from"→ resolution lands onreact/index.js, not@types/react.packageExtensionsoverride, reinstall, and repeat:@typesappears as a sibling in the package's store directory, resolution moves to the consumer's own@types/react, and the errors clear.Step 5 was confirmed end to end on a different package with the identical defect (
react-error-boundary) using exactly this manifest shape.Repo checks on this branch, on node 22.23.2 / yarn 3.6.4:
yarn build— 4/4 tasks passyarn test:ts— 6/6 tasks passyarn lint— 6/6 tasks pass (1 pre-existing warning indocs/.storybook/main.ts, untouched by this PR)yarn test:unit— 5/5 tasks pass; 267 tests in@strapi/design-system, 46 in@strapi/ui-primitives, 0 failuresoxfmtdoes not process.jsonor.md, so the files changed here are outside its scope; the repo's existing format drift is unrelated to this branch.A
patchchangeset is included for all three packages.Related issue(s)/PR(s)
Same defect and same fix shape, upstream in
react-error-boundary— useful as precedent: bvaughn/react-error-boundary#245Note on base branch: CONTRIBUTING.md asks for PRs against
develop, but that branch does not currently exist on this repository (git ls-remote --headsshows onlymain), and the changesets config usesmainasbaseBranch— so this targetsmain. Happy to retarget if that's wrong.Opened as a draft for maintainer review of the range choice before it lands.