From af8047effb6c12ab8f1c42e6a55f854ee04d7479 Mon Sep 17 00:00:00 2001 From: Daniel Tschinder <231804+danez@users.noreply.github.com> Date: Thu, 19 Oct 2023 16:45:10 +0200 Subject: [PATCH] Support TypeScript builtin prop types --- .changeset/perfect-turtles-reply.md | 8 ++ .../src/handlers/codeTypeHandler.ts | 2 +- .../getTypeFromReactComponent-test.ts.snap | 12 +++ .../unwrapBuiltinTSPropTypes-test.ts.snap | 71 +++++++++++++ .../getTypeFromReactComponent-test.ts | 12 +++ .../unwrapBuiltinTSPropTypes-test.ts | 99 +++++++++++++++++++ .../src/utils/getTypeFromReactComponent.ts | 37 ++++--- .../src/utils/unwrapBuiltinTSPropTypes.ts | 31 ++++++ 8 files changed, 251 insertions(+), 21 deletions(-) create mode 100644 .changeset/perfect-turtles-reply.md create mode 100644 packages/react-docgen/src/utils/__tests__/__snapshots__/unwrapBuiltinTSPropTypes-test.ts.snap create mode 100644 packages/react-docgen/src/utils/__tests__/unwrapBuiltinTSPropTypes-test.ts create mode 100644 packages/react-docgen/src/utils/unwrapBuiltinTSPropTypes.ts diff --git a/.changeset/perfect-turtles-reply.md b/.changeset/perfect-turtles-reply.md new file mode 100644 index 00000000000..e169a0e94dc --- /dev/null +++ b/.changeset/perfect-turtles-reply.md @@ -0,0 +1,8 @@ +--- +'react-docgen': minor +--- + +Support `PropsWithoutRef`, `PropsWithRef` and `PropsWithChildren` in TypeScript. + +Component props are now detected correctly when these builtin types are used, +but they do currently not add any props to the documentation. diff --git a/packages/react-docgen/src/handlers/codeTypeHandler.ts b/packages/react-docgen/src/handlers/codeTypeHandler.ts index 0d38e1de9f4..75310b4b3d8 100644 --- a/packages/react-docgen/src/handlers/codeTypeHandler.ts +++ b/packages/react-docgen/src/handlers/codeTypeHandler.ts @@ -98,7 +98,7 @@ function setPropDescriptor( } /** - * This handler tries to find flow Type annotated react components and extract + * This handler tries to find flow and TS Type annotated react components and extract * its types to the documentation. It also extracts docblock comments which are * inlined in the type definition. */ diff --git a/packages/react-docgen/src/utils/__tests__/__snapshots__/getTypeFromReactComponent-test.ts.snap b/packages/react-docgen/src/utils/__tests__/__snapshots__/getTypeFromReactComponent-test.ts.snap index cf555189505..ce2f988c66b 100644 --- a/packages/react-docgen/src/utils/__tests__/__snapshots__/getTypeFromReactComponent-test.ts.snap +++ b/packages/react-docgen/src/utils/__tests__/__snapshots__/getTypeFromReactComponent-test.ts.snap @@ -258,6 +258,18 @@ exports[`getTypeFromReactComponent > TypeScript > stateless > finds variable typ ] `; +exports[`getTypeFromReactComponent > TypeScript > stateless > finds wrapped param type annotation 1`] = ` +[ + Node { + "type": "TSTypeReference", + "typeName": Node { + "name": "Props", + "type": "Identifier", + }, + }, +] +`; + exports[`getTypeFromReactComponent > handles no class props 1`] = `[]`; exports[`getTypeFromReactComponent > handles no stateless props 1`] = `[]`; diff --git a/packages/react-docgen/src/utils/__tests__/__snapshots__/unwrapBuiltinTSPropTypes-test.ts.snap b/packages/react-docgen/src/utils/__tests__/__snapshots__/unwrapBuiltinTSPropTypes-test.ts.snap new file mode 100644 index 00000000000..ac938cafe2b --- /dev/null +++ b/packages/react-docgen/src/utils/__tests__/__snapshots__/unwrapBuiltinTSPropTypes-test.ts.snap @@ -0,0 +1,71 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`unwrapBuiltinTSPropTypes > React.PropsWithChildren 1`] = ` +Node { + "type": "TSTypeReference", + "typeName": Node { + "name": "Props", + "type": "Identifier", + }, +} +`; + +exports[`unwrapBuiltinTSPropTypes > React.PropsWithRef 1`] = ` +Node { + "type": "TSTypeReference", + "typeName": Node { + "name": "Props", + "type": "Identifier", + }, +} +`; + +exports[`unwrapBuiltinTSPropTypes > React.PropsWithoutRef 1`] = ` +Node { + "type": "TSTypeReference", + "typeName": Node { + "name": "Props", + "type": "Identifier", + }, +} +`; + +exports[`unwrapBuiltinTSPropTypes > does not follow reassignment 1`] = ` +Node { + "type": "TSTypeReference", + "typeName": Node { + "name": "bar", + "type": "Identifier", + }, +} +`; + +exports[`unwrapBuiltinTSPropTypes > multiple 1`] = ` +Node { + "type": "TSTypeReference", + "typeName": Node { + "name": "Props", + "type": "Identifier", + }, +} +`; + +exports[`unwrapBuiltinTSPropTypes > with named import 1`] = ` +Node { + "type": "TSTypeReference", + "typeName": Node { + "name": "Props", + "type": "Identifier", + }, +} +`; + +exports[`unwrapBuiltinTSPropTypes > with require 1`] = ` +Node { + "type": "TSTypeReference", + "typeName": Node { + "name": "Props", + "type": "Identifier", + }, +} +`; diff --git a/packages/react-docgen/src/utils/__tests__/getTypeFromReactComponent-test.ts b/packages/react-docgen/src/utils/__tests__/getTypeFromReactComponent-test.ts index b149cfad882..3e89e65fc11 100644 --- a/packages/react-docgen/src/utils/__tests__/getTypeFromReactComponent-test.ts +++ b/packages/react-docgen/src/utils/__tests__/getTypeFromReactComponent-test.ts @@ -40,6 +40,18 @@ describe('getTypeFromReactComponent', () => { expect(getTypeFromReactComponent(path)).toMatchSnapshot(); }); + test('finds wrapped param type annotation', () => { + const path = parseTypescript + .statementLast( + `import React from 'react'; + const x = (props: React.PropsWithChildren) => {}`, + ) + .get('declarations')[0] + .get('init') as NodePath; + + expect(getTypeFromReactComponent(path)).toMatchSnapshot(); + }); + test('finds param inline type', () => { const path = parseTypescript .statementLast( diff --git a/packages/react-docgen/src/utils/__tests__/unwrapBuiltinTSPropTypes-test.ts b/packages/react-docgen/src/utils/__tests__/unwrapBuiltinTSPropTypes-test.ts new file mode 100644 index 00000000000..cf7e7f0aa32 --- /dev/null +++ b/packages/react-docgen/src/utils/__tests__/unwrapBuiltinTSPropTypes-test.ts @@ -0,0 +1,99 @@ +import type { TSTypeReference, VariableDeclaration } from '@babel/types'; +import { parseTypescript } from '../../../tests/utils'; +import unwrapBuiltinTSPropTypes from '../unwrapBuiltinTSPropTypes.js'; +import { describe, expect, test } from 'vitest'; +import type { NodePath } from '@babel/traverse'; + +describe('unwrapBuiltinTSPropTypes', () => { + test('React.PropsWithChildren', () => { + const path = parseTypescript + .statementLast( + `import React from 'react'; + var foo: React.PropsWithChildren`, + ) + .get( + 'declarations.0.id.typeAnnotation.typeAnnotation', + ) as NodePath; + + expect(unwrapBuiltinTSPropTypes(path)).toMatchSnapshot(); + }); + + test('React.PropsWithoutRef', () => { + const path = parseTypescript + .statementLast( + `import React from 'react'; + var foo: React.PropsWithoutRef`, + ) + .get( + 'declarations.0.id.typeAnnotation.typeAnnotation', + ) as NodePath; + + expect(unwrapBuiltinTSPropTypes(path)).toMatchSnapshot(); + }); + + test('React.PropsWithRef', () => { + const path = parseTypescript + .statementLast( + `import React from 'react'; + var foo: React.PropsWithRef`, + ) + .get( + 'declarations.0.id.typeAnnotation.typeAnnotation', + ) as NodePath; + + expect(unwrapBuiltinTSPropTypes(path)).toMatchSnapshot(); + }); + + test('multiple', () => { + const path = parseTypescript + .statementLast( + `import React from 'react'; + var foo: React.PropsWithChildren>`, + ) + .get( + 'declarations.0.id.typeAnnotation.typeAnnotation', + ) as NodePath; + + expect(unwrapBuiltinTSPropTypes(path)).toMatchSnapshot(); + }); + + test('does not follow reassignment', () => { + const path = parseTypescript + .statementLast( + `import React from 'react'; + type bar = React.PropsWithRef + var foo: React.PropsWithChildren`, + ) + .get( + 'declarations.0.id.typeAnnotation.typeAnnotation', + ) as NodePath; + + expect(unwrapBuiltinTSPropTypes(path)).toMatchSnapshot(); + }); + + test('with require', () => { + const path = parseTypescript + .statementLast( + `const React = require('react'); + var foo: React.PropsWithRef`, + ) + .get( + 'declarations.0.id.typeAnnotation.typeAnnotation', + ) as NodePath; + + expect(unwrapBuiltinTSPropTypes(path)).toMatchSnapshot(); + }); + + test('with named import', () => { + const path = parseTypescript + .statementLast( + `import { PropsWithRef } from 'react'; + var foo: PropsWithRef`, + ) + .get( + 'declarations.0.id.typeAnnotation.typeAnnotation', + ) as NodePath; + + expect(unwrapBuiltinTSPropTypes(path)).toMatchSnapshot(); + }); +}); diff --git a/packages/react-docgen/src/utils/getTypeFromReactComponent.ts b/packages/react-docgen/src/utils/getTypeFromReactComponent.ts index 7f03c316ea4..b0241c121a0 100644 --- a/packages/react-docgen/src/utils/getTypeFromReactComponent.ts +++ b/packages/react-docgen/src/utils/getTypeFromReactComponent.ts @@ -23,6 +23,7 @@ import type { } from '@babel/types'; import getTypeIdentifier from './getTypeIdentifier.js'; import isReactBuiltinReference from './isReactBuiltinReference.js'; +import unwrapBuiltinTSPropTypes from './unwrapBuiltinTSPropTypes.js'; // TODO TESTME @@ -62,13 +63,11 @@ function findAssignedVariableType( isReactBuiltinReference(typeName, 'VoidFunctionComponent') || isReactBuiltinReference(typeName, 'VFC') ) { - const typeParameters = typeAnnotation.get( - 'typeParameters', - ) as NodePath; + const typeParameters = typeAnnotation.get('typeParameters'); - if (!typeParameters.hasNode()) return null; - - return typeParameters.get('params')[0] ?? null; + if (typeParameters.hasNode()) { + return typeParameters.get('params')[0] ?? null; + } } } @@ -106,27 +105,25 @@ export default (componentDefinition: NodePath): NodePath[] => { typePaths.push(typeAnnotation); } } + } else { + const propsParam = getStatelessPropsPath(componentDefinition); - return typePaths; - } - - const propsParam = getStatelessPropsPath(componentDefinition); - - if (propsParam) { - const typeAnnotation = getTypeAnnotation(propsParam); + if (propsParam) { + const typeAnnotation = getTypeAnnotation(propsParam); - if (typeAnnotation) { - typePaths.push(typeAnnotation); + if (typeAnnotation) { + typePaths.push(typeAnnotation); + } } - } - const assignedVariableType = findAssignedVariableType(componentDefinition); + const assignedVariableType = findAssignedVariableType(componentDefinition); - if (assignedVariableType) { - typePaths.push(assignedVariableType); + if (assignedVariableType) { + typePaths.push(assignedVariableType); + } } - return typePaths; + return typePaths.map((typePath) => unwrapBuiltinTSPropTypes(typePath)); }; export function applyToTypeProperties( diff --git a/packages/react-docgen/src/utils/unwrapBuiltinTSPropTypes.ts b/packages/react-docgen/src/utils/unwrapBuiltinTSPropTypes.ts new file mode 100644 index 00000000000..06f3cee24d4 --- /dev/null +++ b/packages/react-docgen/src/utils/unwrapBuiltinTSPropTypes.ts @@ -0,0 +1,31 @@ +import type { NodePath } from '@babel/traverse'; +import isReactBuiltinReference from './isReactBuiltinReference.js'; + +/** + * Unwraps NodePaths from the builtin TS types `PropsWithoutRef`, + * `PropsWithRef` and `PropsWithChildren` and returns the inner type param. + * If none of the builtin types is detected the path is returned as-is + */ +export default function unwrapBuiltinTSPropTypes(typePath: NodePath): NodePath { + if (typePath.isTSTypeReference()) { + const typeName = typePath.get('typeName'); + + if ( + isReactBuiltinReference(typeName, 'PropsWithoutRef') || + isReactBuiltinReference(typeName, 'PropsWithRef') || + isReactBuiltinReference(typeName, 'PropsWithChildren') + ) { + const typeParameters = typePath.get('typeParameters'); + + if (typeParameters.hasNode()) { + const innerType = typeParameters.get('params')[0]; + + if (innerType) { + return unwrapBuiltinTSPropTypes(innerType); + } + } + } + } + + return typePath; +}