refactor(react, docs): derive component props from the API schema, drop react-docgen - #10621
refactor(react, docs): derive component props from the API schema, drop react-docgen#10621luvkapur wants to merge 7 commits into
Conversation
…op react-docgen The properties tables in the docs UI (the Compositions "properties" tab and the Overview properties table) were fed by react-docgen 5.3.1 via the legacy `ConsumerComponent.docs` doclets. Point `ReactMain.getDocs` at the TypeScript schema extractor instead and remove react-docgen entirely. react-docgen ran for every non-test file on every cold-cache component load — 3,224 parses in this workspace, of which 2,942 produced nothing — and it read props without type information. The schema extractor already computes this data for the API reference. Measured against real schema artifacts, prop counts match react-docgen exactly (avatar 8, component-preview 15, lane-selector 14, version-dropdown 7, tooltip 3, time-ago 2) with more precise types and default values react-docgen missed. `bit show --legacy` now reports jsdoc-only docs for React components. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
CI noteThe
The earlier version of this note described the file-count guard failure in |
PR Summary by QodoDerive React property docs from API schemas
AI Description
Diagram
High-Level Assessment
Files changed (6)
|
Code Review by Qodo
1.
|
…own generics `react-docs-from-schema` knew the internals of seven node types — it matched `__schema` strings for Export/Module/TypeRef/Type/TypeLiteral/Interface/ TypeIntersection and cast through `as unknown as` to reach their fields. That coupled the React aspect to the shape of every schema node it might meet and sidestepped the polymorphic design of `SchemaNode`. Each node now owns the answer to "which members do you contribute as an object-like type", the same way it owns `getNodes()`/`toString()`/`diff()`: - `SchemaNode.getMembers(context)` — default: none. Interface and type literal return their members (interfaces append what they inherit via `extends`); intersection combines its parts; alias, parentheses and export wrappers delegate; a type reference resolves through `context.resolveRef`. A visited set makes self-referencing types terminate. - `ModuleSchema.listExports()` / `listDeclarations()` — non-mutating views with export wrappers and namespaces unwrapped (`flatExportsRecursively` mutates). - `APISchema.findDeclaration()` / `resolveRef()` / `getMembersOf()` — resolve references by name within the component; references to other components or packages resolve to nothing, since their declarations aren't in this schema. - `ParameterSchema.getBindingDefaults()` — defaults from destructured bindings. - `VariableLikeSchema.isVariableLikeSchema()` / `ReactSchema.isReactSchema()` guards, in the package's existing `isTypeRefSchema` idiom. The React mapper is now ~20 lines that only know `ReactSchema` and the generic API. Inherited props (`interface ButtonProps extends BaseProps`) are described too, which neither react-docgen nor the previous mapper did. 18 new specs for the entity, 10 for the mapper (one new, for inheritance). Note: `bit test` can't run `components/*` specs in this workspace locally (pre-existing — same failure on `doc-parser`); they run in CI's capsules. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Code review by qodo was updated up to the latest commit 89bbe61 |
…nions contribute members - `APISchema.resolveRef()`: a reference that carries `internalFilePath` only resolves to a declaration in that file, so same-named declarations in other files of the component are never mistaken for it. Refs to other components or packages already resolved to nothing. - `TypeUnionSchema.getMembers()`: a union of object types contributes the members of every alternative, in order; a member declared by only some alternatives is listed as the first alternative declares it. Previously a union props type rendered an empty table. Specs: entity 20 (+2), mapper 11 (+1). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Code review by qodo was updated up to the latest commit ab51bb7 |
… from other package copies
- `ParameterTransformer.getObjectBindingNodes()`: for `({ text = 'click' }: { text?: string })`
the binding reuses the matching member of the inline props type, which knows
nothing of the initializer — the default was lost. The member is now cloned
with the binding's initializer as `defaultValue` (a field `VariableLikeSchema`
has always serialized), so no wire-format change.
- `reactDocsFromSchema()`: a schema built by another copy of the semantic-schema
package (an env's extractor graph, an artifact hydrated elsewhere) has the
serialized fields but not this package's members API. Such a schema is
re-hydrated through `APISchema.fromObject(api.toObject())`, with the schema
classes and `ReactSchema` registered in this copy's registry. The serialized
form is the contract between versions; nothing walks node internals.
Specs: extractor 3 (new), mapper 12 (+1), entity 20.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Code review by qodo was updated up to the latest commit c51c71c |
…rences, union requiredness
- `ReactAPITransformer` also recognises classes extending React's `Component`
/ `PureComponent` (namespaced or not) and takes their props from the base
class's type argument; `.js` files count as React files alongside `.tsx`/`.jsx`.
- `ModuleSchema.findExport()` + `APISchema.findDeclaration()` resolve the name
a declaration is exported under (`export { Props as ButtonProps }`);
`APISchema.listExportedDeclarations()` follows an exported reference to its
local declaration (`export default Button`). The mapper uses the latter.
- `TypeUnionSchema.getMembers()` computes requiredness across alternatives: a
member some alternative lacks or leaves optional is contributed as optional.
- `SchemaNode.membersOf()` tracks the current path rather than the whole
traversal, so a base type shared by two branches contributes to each.
- `getObjectBindingNodes()` keys `{ text: label = 'x' }` by the prop `text`.
Specs: entity 24 (+4), mapper 13 (+1), extractor 4 (+1), transformer 3 (new).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Code review by qodo was updated up to the latest commit 8b96fce |
…ase must be React's
- `TypeUnionSchema.getMembers()` contributes one member per name: the types
the alternatives give it are unioned (`{ value: string } | { value: number }`
→ `value: string | number`), requiredness stays "required everywhere".
- `ReactAPITransformer` accepts a `Component`/`PureComponent` base only when
it resolved to the `react` package or did not resolve at all — a base from
another package, another component or a file of this one is not React's.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Code review by qodo was updated up to the latest commit 7157077 |
Summary
The docs UI shows a properties table for each React component. Before this change,
react-docgen5.3.1 produced that table through the legacyConsumerComponent.docsdoclets. After this change,ReactMain.getDocsreads the TypeScript API schema. Thereact-docgenpackage is removed.Two UI surfaces use
getDocs:compositions.tsx→useDocs).docs-app→PropertiesTable→useFetchDocs).The UI components do not change. Only the data source of the resolver changes.
Why
react-docgenran on each non-test file on each cold component load. In this workspace that was 3,224 parses. 2,942 of them produced nothing and fell through to the jsdoc parser.react-docgenalso had no type information. It could only describe what it could match in the source text.The schema extractor already computes this data for the API Reference tab. It describes props with their types. The
react-docgenpackage was pinned at 5.3.1. The 7.x upstream has a different API, so an update was not possible.How the props are resolved
The React aspect does not inspect other schema node types. The schema resolves its own members. This is a polymorphic capability of
SchemaNode, likegetNodes(),toString()anddiff().SchemaNode.getMembers(context). The default returns no members. An interface and a type literal return their members. An interface also appends the members it inherits withextends. An intersection combines the members of its parts. An alias, a parenthesized type and an export wrapper delegate to the node they wrap. A type reference resolves throughcontext.resolveRef. A visited set stops self-referencing types.ModuleSchema.listExports()andlistDeclarations(). These return the declarations with export wrappers and namespaces unwrapped. They do not change the module.flatExportsRecursively()does.APISchema.findDeclaration(),resolveRef()andgetMembersOf(). These resolve a reference by name inside the component. A reference to another component or to a package resolves to nothing, because that declaration is not part of this schema.ParameterSchema.getBindingDefaults(). This returns the default values of destructured bindings, for example{ size = 32 }.APISchema.listExportedDeclarations(). This follows an exported reference to the local declaration, for exampleexport default Button.findDeclaration()also accepts the name a declaration is exported under, for exampleexport { Props as ButtonProps }.TypeUnionSchema.getMembers(). A union contributes one member per name. If the alternatives give a member different types, the member gets the union of those types. A member is required only when every alternative requires it.react-docs-from-schema.tsis now about 20 lines. It knows onlyReactSchemaand this API. Inherited props, for exampleinterface ButtonProps extends BaseProps, are included.react-docgendid not include them.ReactAPITransformerdecides what is a React component. It now also accepts a class that extendsReact.Component,ComponentorPureComponent. The props of a class component are the first type argument of the base class..jsfiles count as React files, like.tsxand.jsx.Compatibility
More than one version of the schema can be in use at the same time. This change keeps them compatible:
defaultValueon aVariableLikeSchemabinding. That class has always serialized this field. An old artifact has no default for this case, which is the current behavior.semantic-schemapackage has the serialized fields but not the new methods. The mapper detects this and hydrates the schema again throughAPISchema.fromObject(api.toObject()), with the schema classes andReactSchemaregistered in its own copy. The serialized form is the contract between versions. No code walks node internals.ReactSchemaat extraction time. A component that was built before this change shows the class-component props after it is built again.Verification
npm run lint(tsc and oxlint) passes.bit compilepasses.The mapper ran against real schema artifacts on a running
bit start. The prop counts are equal to thereact-docgencounts, before and after the generics refactor:design/ui/avatarpreview/ui/component-previewlanes/ui/inputs/lane-selectorcomponent/ui/version-dropdowndesign/ui/tooltipdesign/ui/time-agoThe schema output has more detail. For example, it gives
isTag?: (version?: string): boolean = (version) => semver.valid(version) !== null.react-docgengave only a baresignatureand no default value.Measured on a workspace component: a cold
getDocscall takes 0.4 s to 1.1 s. A warm call takes about 190 ms. Six concurrent calls for one component run one extraction. A non-React component returns an empty table and no error.The aspect graph has no new edge.
SchemaAspectwas already a dependency ofReactAspect, andschemaMainwas already injected into the provider. This change only passes the instance to the constructor. There is noschema → reactedge.bit statusloads all components without a circular-dependency error.Review notes
getDocschanged from a field read to a possible schema extraction. For a built component it reads the build artifact. For a workspace component it runs the extractor. The extractor is warm after tsserver is up. An in-flight map removes duplicate concurrent requests for one component. Nothing is cached after a request settles, so a workspace component never shows a stale schema. Check this on a large workspace.bit show --legacychanges. Docs are now jsdoc only, so React components do not show a prop table in that command. A component comment without an explicit@nameshows a blank name, as it does for all other files, because the jsdoc parser does not infer declaration names. That command is the only remaining consumer ofConsumerComponent.docs. The jsdoc parser still fills that field.Version.id()does not includedocs, so no version hash changes.pnpm-lock.yamlis not regenerated. CI runs plainbit installwithout--frozen-lockfile. A local regeneration produced about 33k lines of unrelated changes.Known limitations
GenericProps<string>shows member types asT. The API Reference tab shows the same. This is a schema-wide capability for a follow-up.propTypesandComponent.defaultPropsare not read. The schema extractor does not model them, and the API Reference tab does not show them. The destructured default form,{ size = 32 }, is captured.Pre-existing issues seen during this work
bit schema <pattern> --jsonfails withUnknown argument: jsonand prints the help text.bit testoncomponents/*specs fails in this workspace withUnexpected token. The same failure occurs onmasterfordoc-parser. These specs run in the CI capsules.