diff --git a/components/semantics/doc-parser/fixtures/jsdoc/react/elevation.tsxx b/components/semantics/doc-parser/fixtures/jsdoc/react/elevation.tsxx deleted file mode 100644 index c82fd934196a..000000000000 --- a/components/semantics/doc-parser/fixtures/jsdoc/react/elevation.tsxx +++ /dev/null @@ -1,22 +0,0 @@ -// @bit-no-check - -import React from 'react'; - -export type CardProps = { - /** - * Controls the shadow cast by the card, to generate a "stacking" effects. - * For example, a modal floating over elements may have a 'high' elevation - */ - elevation: 'none' | 'low' | 'medium' | 'high'; -} & React.HTMLAttributes; - -/** - * A wrapper resembling a physical card, grouping elements and improve readability. - */ -export function Card({ className, elevation }: CardProps) { - return
; -} - -Card.defaultProps = { - elevation: 'low' -}; diff --git a/components/semantics/doc-parser/fixtures/jsdoc/react/react-docs.js b/components/semantics/doc-parser/fixtures/jsdoc/react/react-docs.js deleted file mode 100644 index bb22e6e74fab..000000000000 --- a/components/semantics/doc-parser/fixtures/jsdoc/react/react-docs.js +++ /dev/null @@ -1,76 +0,0 @@ -// @bit-no-check -import React, { Component } from 'react'; -import PropTypes from 'prop-types'; - -/** - * @description Styled button component for the rich and famous! - * - * @example - * - ); - } -} - -Button.propTypes = { - /** - * @property {propTypes.string} text - Button text. - */ - text: PropTypes.string.isRequired, - /** - * @property {propTypes.string} buttonHoverColor - Button color to be shown on hover. - */ - buttonHoverColor: PropTypes.string, - /** - * @property {propTypes.string} buttonColor- Button default background color. - */ - buttonColor: PropTypes.string -}; - -Button.defaultProps = { - text: 'Example Button', - buttonColor: 'blue', - buttonHoverColor: 'green' -}; - -export default Button; diff --git a/components/semantics/doc-parser/parser.ts b/components/semantics/doc-parser/parser.ts index 7019f8502c93..57ad46cba28d 100644 --- a/components/semantics/doc-parser/parser.ts +++ b/components/semantics/doc-parser/parser.ts @@ -1,9 +1,7 @@ import fs from 'fs-extra'; import type { FsCache } from '@teambit/workspace.modules.fs-cache'; import type { SourceFile } from '@teambit/component.sources'; -import type { PathOsBased } from '@teambit/toolbox.path.path'; import jsDocParse from './jsdoc'; -import reactParse from './react'; import type { Doclet } from './types'; export default async function parse(file: SourceFile, componentFsCache: FsCache): Promise { @@ -16,15 +14,7 @@ export default async function parse(file: SourceFile, componentFsCache: FsCache) } } - const results = await parseFile(file.contents.toString(), file.relative); + const results = await jsDocParse(file.contents.toString(), file.relative); await componentFsCache.saveDocsInCache(file.path, results); return results; } - -async function parseFile(data: string, filePath: PathOsBased): Promise { - const reactDocs = await reactParse(data, filePath); - if (reactDocs && Object.keys(reactDocs).length > 0) { - return reactDocs; - } - return jsDocParse(data, filePath); -} diff --git a/components/semantics/doc-parser/react/index.ts b/components/semantics/doc-parser/react/index.ts deleted file mode 100644 index 08eab2c8d75f..000000000000 --- a/components/semantics/doc-parser/react/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -import parse from './react-parser'; - -export default parse; diff --git a/components/semantics/doc-parser/react/react-parser.spec.ts b/components/semantics/doc-parser/react/react-parser.spec.ts deleted file mode 100644 index e4ebc6406a4d..000000000000 --- a/components/semantics/doc-parser/react/react-parser.spec.ts +++ /dev/null @@ -1,79 +0,0 @@ -import { expect } from 'chai'; -import fs from 'fs-extra'; -import * as path from 'path'; - -import parser from './'; - -const fixtures = path.join(__dirname, '..', 'fixtures', 'jsdoc'); - -function parseFile(filePath: string) { - return parser(fs.readFileSync(filePath).toString(), 'my-file.js'); -} - -describe('React docs Parser', () => { - describe('parse()', () => { - describe('Invalid code', () => { - it('should returns an empty array', async () => { - const doclets = await parser('this is an invalid code', 'some-file'); - expect(doclets).to.be.undefined; - }); - }); - - describe('React Docs', () => { - let doclet; - before(async () => { - const file = path.join(fixtures, 'react/react-docs.js'); - const doclets = await parseFile(file); - // @ts-ignore - doclet = doclets[0]; - }); - it('should have properties parsed', () => { - expect(doclet).to.have.property('properties'); - expect(doclet.properties).to.be.an('array').with.lengthOf(3); - }); - it('should have methods parsed', () => { - expect(doclet).to.have.property('methods'); - expect(doclet.methods).to.be.an('array').with.lengthOf(2); - }); - it('should parse the description correctly', () => { - expect(doclet) - .to.have.property('description') - .that.is.equal('Styled button component for the rich and famous!'); - }); - it('should parse the examples correctly', () => { - expect(doclet).to.have.property('examples').that.is.an('array').with.lengthOf(1); - }); - it('should preserve the spaces in the example', () => { - const example = doclet.examples[0].raw; - expect(example).to.string(' text'); - }); - it('should parse the properties description correctly', () => { - expect(doclet).to.have.property('properties').that.is.an('array'); - expect(doclet.properties[0].description).to.equal('Button text.'); - }); - }); - describe('elevation', () => { - let doclet; - before(async () => { - const file = path.join(fixtures, 'react/elevation.tsxx'); - const doclets = await parseFile(file); - // @ts-ignore - doclet = doclets[0]; - expect(doclet).to.be.an('object'); - }); - it('should have properties parsed', () => { - expect(doclet).to.have.property('properties'); - expect(doclet.properties).to.be.an('array').with.lengthOf(1); - }); - it('should parse the description correctly', () => { - expect(doclet) - .to.have.property('description') - .that.is.equal('A wrapper resembling a physical card, grouping elements and improve readability.'); - }); - it('should parse the properties type correctly', () => { - expect(doclet).to.have.property('properties').that.is.an('array'); - expect(doclet.properties[0].type).to.equal("'none' | 'low' | 'medium' | 'high'"); - }); - }); - }); -}); diff --git a/components/semantics/doc-parser/react/react-parser.ts b/components/semantics/doc-parser/react/react-parser.ts deleted file mode 100644 index b3df546072f3..000000000000 --- a/components/semantics/doc-parser/react/react-parser.ts +++ /dev/null @@ -1,124 +0,0 @@ -import doctrine from 'doctrine'; -import * as reactDocs from 'react-docgen'; - -import { logger } from '@teambit/legacy.logger'; -import type { PathOsBased } from '@teambit/legacy.utils'; -import { pathNormalizeToLinux } from '@teambit/legacy.utils'; -import extractDataRegex from '../extract-data-regex'; -import type { Doclet } from '../types'; - -function formatProperties(props) { - const parseDescription = (description) => { - // an extra step is needed to parse the properties description correctly. without this step - // it'd show the entire tag, e.g. `@property {propTypes.string} text - Button text.` - // instead of just `text - Button text.`. - try { - const descriptionAST = doctrine.parse(description, { unwrap: true, recoverable: true, sloppy: true }); - if (descriptionAST && descriptionAST.tags[0]) return descriptionAST.tags[0].description; - } catch { - // failed to parse the react property, that's fine, it'll return the original description - } - return description; - }; - return Object.keys(props).map((name) => { - const { type, description, required, defaultValue, flowType, tsType } = props[name]; - - return { - name, - description: parseDescription(description), - required, - type: stringifyType(type || flowType || tsType), - defaultValue, - }; - }); -} - -function formatMethods(methods) { - return Object.keys(methods).map((key) => { - const { returns, modifiers, params, docblock, name } = methods[key]; - return { - name, - description: docblock, - returns, - modifiers, - params, - }; - }); -} - -function fromReactDocs({ description, displayName, props, methods }, filePath): Doclet { - return { - filePath: pathNormalizeToLinux(filePath), - name: displayName, - description, - properties: formatProperties(props), - access: 'public', - // @ts-ignore AUTO-ADDED-AFTER-MIGRATION-PLEASE-FIX! - methods: formatMethods(methods), - }; -} - -function stringifyType(prop: { name: string; value?: any; raw?: string }): string { - if (!prop) return '?'; // TODO! - - const { name } = prop; - let transformed; - - switch (name) { - default: - transformed = name; - break; - case 'func': - transformed = 'function'; - break; - case 'shape': - transformed = JSON.stringify( - Object.keys(prop.value).reduce((acc = {}, current) => { - acc[current] = stringifyType(prop.value[current]); - return acc; - }, {}) - ); - break; - case 'enum': - transformed = prop.value.map((enumProp) => enumProp.value).join(' | '); - break; - case 'instanceOf': - transformed = prop.value; - break; - case 'union': - transformed = prop.value ? prop.value.map((p) => stringifyType(p)).join(' | ') : prop.raw; - break; - case 'arrayOf': - transformed = `${stringifyType(prop.value)}[]`; - break; - } - - return transformed; -} - -export default async function parse(data: string, filePath: PathOsBased): Promise { - const doclets: Array = []; - try { - const componentsInfo = reactDocs.parse(data, reactDocs.resolver.findAllExportedComponentDefinitions, undefined, { - configFile: false, - filename: filePath, // should we use pathNormalizeToLinux(filePath) ? - }); - - if (componentsInfo) { - return componentsInfo.map((componentInfo) => { - const formatted = fromReactDocs(componentInfo, filePath); - formatted.args = []; - // this is a workaround to get the 'example' tag parsed when using react-docs - // because as of now Docgen doesn't parse @example tag, instead, it shows it inside - // the @description tag. - extractDataRegex(formatted.description, doclets, filePath, false); - formatted.description = doclets[0].description; - formatted.examples = doclets[0].examples; - return formatted; - }); - } - } catch (err: any) { - logger.trace(`failed parsing docs using docgen on path ${filePath} with error`, err); - } - return undefined; -} diff --git a/scopes/react/react/react-docs-from-schema.spec.ts b/scopes/react/react/react-docs-from-schema.spec.ts new file mode 100644 index 000000000000..10011e9d162d --- /dev/null +++ b/scopes/react/react/react-docs-from-schema.spec.ts @@ -0,0 +1,176 @@ +import { expect } from 'chai'; +import type { Location as SchemaLocation, SchemaNode } from '@teambit/semantics.entities.semantic-schema'; +import { + APISchema, + DocSchema, + ExportSchema, + InterfaceSchema, + KeywordTypeSchema, + ModuleSchema, + ParameterSchema, + TypeIntersectionSchema, + TypeLiteralSchema, + TypeRefSchema, + TypeSchema, + VariableLikeSchema, +} from '@teambit/semantics.entities.semantic-schema'; +import { ComponentID } from '@teambit/component-id'; +import { ReactSchema } from './react.schema'; +import { reactDocsFromSchema } from './react-docs-from-schema'; + +const loc: SchemaLocation = { filePath: 'index.ts', line: 0, character: 0 }; +const compId = ComponentID.fromString('org.scope/button'); + +function member(name: string, type: string, isOptional: boolean, comment?: string): VariableLikeSchema { + return new VariableLikeSchema( + loc, + name, + `${name}: ${type}`, + new KeywordTypeSchema(loc, type), + isOptional, + comment ? new DocSchema(loc, `/** ${comment} */`, comment) : undefined + ); +} + +function reactNode(name: string, propsTypeName: string, bindings?: SchemaNode[]): ReactSchema { + const props = new ParameterSchema( + loc, + 'props', + new TypeRefSchema(loc, propsTypeName), + false, + undefined, + undefined, + bindings + ); + return new ReactSchema(loc, name, new TypeRefSchema(loc, 'JSX.Element'), props); +} + +function apiSchema(exports: SchemaNode[], internals: SchemaNode[] = []): APISchema { + return new APISchema(loc, new ModuleSchema(loc, exports, internals), [], compId); +} + +describe('reactDocsFromSchema()', () => { + it('returns undefined when the component exports no react component', () => { + const api = apiSchema([new TypeSchema(loc, 'ButtonProps', new TypeLiteralSchema(loc, []), 'type ButtonProps')]); + expect(reactDocsFromSchema(api)).to.be.undefined; + }); + + it('resolves props from a type alias to an object type', () => { + const propsType = new TypeSchema( + loc, + 'ButtonProps', + new TypeLiteralSchema(loc, [member('text', 'string', true, 'the button label'), member('onClick', 'function', false)]), + 'type ButtonProps' + ); + const docs = reactDocsFromSchema(apiSchema([reactNode('Button', 'ButtonProps'), propsType])); + + expect(docs?.properties).to.have.lengthOf(2); + expect(docs?.properties[0]).to.deep.include({ + name: 'text', + type: 'string', + description: 'the button label', + required: false, + }); + expect(docs?.properties[1]).to.deep.include({ name: 'onClick', required: true }); + }); + + it('resolves props from an interface', () => { + const propsType = new InterfaceSchema(loc, 'ButtonProps', 'interface ButtonProps', [], [ + member('text', 'string', true), + ]); + const docs = reactDocsFromSchema(apiSchema([reactNode('Button', 'ButtonProps'), propsType])); + + expect(docs?.properties.map((prop) => prop.name)).to.deep.equal(['text']); + }); + + it('resolves a props type that is internal rather than exported', () => { + const propsType = new TypeSchema( + loc, + 'ButtonProps', + new TypeLiteralSchema(loc, [member('text', 'string', true)]), + 'type ButtonProps' + ); + const docs = reactDocsFromSchema(apiSchema([reactNode('Button', 'ButtonProps')], [propsType])); + + expect(docs?.properties.map((prop) => prop.name)).to.deep.equal(['text']); + }); + + it('merges the members of an intersection and ignores references it cannot resolve', () => { + // `type ButtonProps = { text?: string } & HTMLAttributes` — the second member + // belongs to an external package, so this schema says nothing about it. + const propsType = new TypeSchema( + loc, + 'ButtonProps', + new TypeIntersectionSchema(loc, [ + new TypeLiteralSchema(loc, [member('text', 'string', true)]), + new TypeRefSchema(loc, 'HTMLAttributes', undefined, 'react'), + ]), + 'type ButtonProps' + ); + const docs = reactDocsFromSchema(apiSchema([reactNode('Button', 'ButtonProps'), propsType])); + + expect(docs?.properties.map((prop) => prop.name)).to.deep.equal(['text']); + }); + + it('takes default values from the destructured parameter', () => { + const propsType = new TypeSchema( + loc, + 'ButtonProps', + new TypeLiteralSchema(loc, [member('text', 'string', true)]), + 'type ButtonProps' + ); + const binding = new VariableLikeSchema( + loc, + 'text', + 'text: string', + new KeywordTypeSchema(loc, 'string'), + true, + undefined, + "'click me'" + ); + const docs = reactDocsFromSchema(apiSchema([reactNode('Button', 'ButtonProps', [binding]), propsType])); + + expect(docs?.properties[0].defaultValue).to.deep.equal({ value: "'click me'", computed: false }); + }); + + it('unwraps export wrappers and describes the first component that has resolvable props', () => { + const propsType = new TypeSchema( + loc, + 'ButtonProps', + new TypeLiteralSchema(loc, [member('text', 'string', true)]), + 'type ButtonProps' + ); + const withoutProps = reactNode('Spacer', 'UnknownProps'); + const withProps = reactNode('Button', 'ButtonProps'); + const api = apiSchema([ + new ExportSchema(loc, 'Spacer', withoutProps), + new ExportSchema(loc, 'Button', withProps), + propsType, + ]); + + const docs = reactDocsFromSchema(api); + expect(docs?.properties.map((prop) => prop.name)).to.deep.equal(['text']); + }); + + it('describes the component even when none of them have resolvable props', () => { + const docs = reactDocsFromSchema(apiSchema([reactNode('Spacer', 'UnknownProps')])); + + expect(docs).to.not.be.undefined; + expect(docs?.properties).to.deep.equal([]); + expect(docs?.filePath).to.equal('index.ts'); + }); + + it('exposes the component doc comment as the abstract', () => { + const node = new ReactSchema( + loc, + 'Button', + new TypeRefSchema(loc, 'JSX.Element'), + undefined, + undefined, + [], + new DocSchema(loc, '/** a button */', 'a button') + ); + + expect(reactDocsFromSchema(apiSchema([node]))?.abstract).to.equal('a button'); + }); +}); diff --git a/scopes/react/react/react-docs-from-schema.ts b/scopes/react/react/react-docs-from-schema.ts new file mode 100644 index 000000000000..3ca2e23d5f6f --- /dev/null +++ b/scopes/react/react/react-docs-from-schema.ts @@ -0,0 +1,148 @@ +import type { APISchema, SchemaNode } from '@teambit/semantics.entities.semantic-schema'; +import { compact, uniqBy } from 'lodash'; + +export type ReactDocsProperty = { + name: string; + description: string; + required: boolean; + type: string; + defaultValue?: { value: string; computed: boolean }; +}; + +export type ReactDocsFromSchema = { + abstract: string; + filePath: string; + properties: ReactDocsProperty[]; +}; + +/** + * schema nodes are matched on `__schema` rather than `instanceof`, so that a duplicated copy of + * the semantic-schema module (a real possibility across the aspect graph) doesn't silently stop + * every prop from resolving. + */ +function isSchema(node: SchemaNode | undefined, schemaName: string): boolean { + return node?.__schema === schemaName; +} + +function unwrapExports(module: { exports: SchemaNode[] }): SchemaNode[] { + return module.exports.flatMap((node) => { + if (isSchema(node, 'ExportSchema')) { + const exportNode = (node as unknown as { exportNode?: SchemaNode }).exportNode; + return exportNode ? [exportNode] : []; + } + if (isSchema(node, 'ModuleSchema')) return unwrapExports(node as unknown as { exports: SchemaNode[] }); + return [node]; + }); +} + +/** + * a props type may be exported alongside the component, or declared privately in one of its files, + * so both are indexed to resolve a type reference by name. + */ +function indexByName(api: APISchema): Map { + const index = new Map(); + const add = (node: SchemaNode) => { + if (node.name && !index.has(node.name)) index.set(node.name, node); + }; + unwrapExports(api.module).forEach(add); + api.module.internals.forEach(add); + api.internals.forEach((internal) => { + unwrapExports(internal).forEach(add); + internal.internals.forEach(add); + }); + return index; +} + +/** + * resolves a props type down to the members it contributes: an inline object type, an interface, an + * alias to either, or an intersection of them. a reference that resolves to nothing contributes no + * members — the schema of one component doesn't describe types owned by another component or by an + * external package. + */ +function membersOf( + node: SchemaNode | undefined, + index: Map, + seen = new Set() +): SchemaNode[] { + if (!node || seen.has(node)) return []; + seen.add(node); + + if (isSchema(node, 'TypeRefSchema')) { + return node.name ? membersOf(index.get(node.name), index, seen) : []; + } + if (isSchema(node, 'TypeSchema')) { + return membersOf((node as unknown as { type?: SchemaNode }).type, index, seen); + } + if (isSchema(node, 'TypeLiteralSchema') || isSchema(node, 'InterfaceSchema')) { + return (node as unknown as { members: SchemaNode[] }).members; + } + if (isSchema(node, 'TypeIntersectionSchema')) { + return (node as unknown as { types: SchemaNode[] }).types.flatMap((type) => membersOf(type, index, seen)); + } + return []; +} + +/** + * default values live on the destructured parameter (`{ isTag = () => true }`) rather than on the + * props type, so they are collected separately and merged in by name. + */ +function defaultsByName(props: SchemaNode | undefined): Map { + const bindingNodes = (props as unknown as { objectBindingNodes?: SchemaNode[] } | undefined)?.objectBindingNodes; + const defaults = new Map(); + bindingNodes?.forEach((node) => { + const { name, defaultValue } = node as unknown as { name?: string; defaultValue?: string }; + if (name && defaultValue !== undefined && !defaults.has(name)) defaults.set(name, defaultValue); + }); + return defaults; +} + +function toProperty(member: SchemaNode, defaults: Map): ReactDocsProperty | undefined { + if (!member.name) return undefined; + const { type, isOptional, doc } = member as unknown as { + type?: SchemaNode; + isOptional?: boolean; + doc?: { comment?: string; raw?: string }; + }; + const defaultValue = defaults.get(member.name); + + return { + name: member.name, + description: doc?.comment || '', + required: isOptional === undefined ? false : !isOptional, + type: type ? type.toString() : member.toString(), + defaultValue: defaultValue === undefined ? undefined : { value: defaultValue, computed: false }, + }; +} + +/** + * derives the docs shown in the properties table from the component's API schema. + * + * only the first React component that resolves any props is described, which is what the docs UI + * has always rendered — it reads a single entry, not one per export. + */ +export function reactDocsFromSchema(api: APISchema): ReactDocsFromSchema | undefined { + const reactNodes = unwrapExports(api.module).filter((node) => isSchema(node, 'ReactSchema')); + if (!reactNodes.length) return undefined; + + const index = indexByName(api); + + const docsFor = (node: SchemaNode): ReactDocsFromSchema => { + const props = (node as unknown as { props?: SchemaNode }).props; + const propsType = (props as unknown as { type?: SchemaNode } | undefined)?.type; + const defaults = defaultsByName(props); + const properties = uniqBy( + compact(membersOf(propsType, index).map((member) => toProperty(member, defaults))), + 'name' + ); + const doc = (node as unknown as { doc?: { comment?: string } }).doc; + + return { + abstract: doc?.comment || '', + filePath: node.location.filePath, + properties, + }; + }; + + const allDocs = reactNodes.map(docsFor); + return allDocs.find((docs) => docs.properties.length > 0) || allDocs[0]; +} diff --git a/scopes/react/react/react.graphql.ts b/scopes/react/react/react.graphql.ts index 0ba71bd19bb9..2526cddd9415 100644 --- a/scopes/react/react/react.graphql.ts +++ b/scopes/react/react/react.graphql.ts @@ -41,7 +41,7 @@ export function reactSchema(react: ReactMain) { }; if (!component) return empty; - const docs = react.getDocs(component); + const docs = await react.getDocs(component); if (!docs) return empty; return docs; diff --git a/scopes/react/react/react.main.runtime.ts b/scopes/react/react/react.main.runtime.ts index 260db48738ef..7a097932a7e5 100644 --- a/scopes/react/react/react.main.runtime.ts +++ b/scopes/react/react/react.main.runtime.ts @@ -50,6 +50,8 @@ import { getTemplates } from './react.templates'; import { getStarters } from './react.starters'; import type { ReactAppOptions } from './apps/web/react-app-options'; import { ReactSchema } from './react.schema'; +import type { ReactDocsFromSchema } from './react-docs-from-schema'; +import { reactDocsFromSchema } from './react-docs-from-schema'; import { ReactAPITransformer } from './react.api.transformer'; import type { PrettierConfigTransformer } from '@teambit/defender.prettier.config-mutator'; @@ -124,7 +126,9 @@ export class ReactMain { private dependencyResolver: DependencyResolverMain, - private logger: Logger + private logger: Logger, + + private schema?: SchemaMain ) {} readonly env = this.reactEnv; @@ -395,21 +399,34 @@ export class ReactMain { } /** - * returns doc adjusted specifically for react components. + * dedupes concurrent extractions of the same component. nothing is retained once an extraction + * settles, so a workspace component is never described from a stale schema. */ - getDocs(component: Component) { - const docsArray = component.state._consumer.docs; - if (!docsArray || !docsArray[0]) { - return null; - } - - const docs = docsArray[0]; + private docsInflight = new Map>(); - return { - abstract: docs.description, - filePath: docs.filePath, - properties: docs.properties, - }; + /** + * returns doc adjusted specifically for react components, derived from the component's API schema. + */ + async getDocs(component: Component): Promise { + if (!this.schema) return null; + + const key = component.id.toString(); + const inflight = this.docsInflight.get(key); + if (inflight) return inflight; + + const promise = this.schema + .getSchema(component) + .then((api) => reactDocsFromSchema(api) || null) + .catch((err) => { + this.logger.debug(`react.getDocs, failed extracting the schema of ${key}`, err); + return null; + }) + .finally(() => { + this.docsInflight.delete(key); + }); + + this.docsInflight.set(key, promise); + return promise; } static runtime = MainRuntime; @@ -469,7 +486,7 @@ export class ReactMain { CompilerAspect.id ); const appType = new ReactAppType('react-app', reactEnv, logger, dependencyResolver); - const react = new ReactMain(reactEnv, envs, application, appType, dependencyResolver, logger); + const react = new ReactMain(reactEnv, envs, application, appType, dependencyResolver, logger, schemaMain); graphql.register(() => reactSchema(react)); envs.registerEnv(reactEnv); if (generator) { diff --git a/workspace.jsonc b/workspace.jsonc index 57029a208c79..b5537b9e84b8 100644 --- a/workspace.jsonc +++ b/workspace.jsonc @@ -604,7 +604,6 @@ "query-string": "7.0.0", "react-animate-height": "3.2.3", "react-dev-utils": "12.0.1", - "react-docgen": "5.3.1", "react-error-boundary": "^3.0.0", "react-error-overlay": "6.0.9", "react-syntax-highlighter": "^15.6.1",