From fd79bd4c68b0dcbf4aadb79e0929c8be6dec4d04 Mon Sep 17 00:00:00 2001 From: delangle Date: Mon, 11 Sep 2023 12:20:52 +0200 Subject: [PATCH] [core] Handle refType in the proptypes generation --- packages/mui-base/src/Button/Button.tsx | 10 +- packages/mui-base/src/Input/Input.tsx | 8 +- packages/mui-base/src/Tab/Tab.tsx | 11 +- packages/mui-joy/src/Button/Button.tsx | 15 +-- .../mui-joy/src/IconButton/IconButton.tsx | 15 +-- .../src/ListItemButton/ListItemButton.tsx | 15 +-- packages/mui-joy/src/Select/Select.tsx | 15 +-- packages/mui-joy/src/Tab/Tab.tsx | 15 +-- .../src/ButtonBase/ButtonBase.tsx | 10 +- .../mui-material/src/ButtonBase/ButtonBase.js | 11 +- .../typescript-to-proptypes/src/createType.ts | 8 ++ .../src/generatePropTypes.ts | 4 + .../src/getPropTypesFromFile.ts | 18 +++ .../src/injectPropTypesInFile.ts | 113 ++++++++++++------ .../typescript-to-proptypes/src/models.ts | 5 + scripts/generateProptypes.ts | 5 +- 16 files changed, 152 insertions(+), 126 deletions(-) diff --git a/packages/mui-base/src/Button/Button.tsx b/packages/mui-base/src/Button/Button.tsx index 6be0c51d98f9c3..fd12f5c90249c2 100644 --- a/packages/mui-base/src/Button/Button.tsx +++ b/packages/mui-base/src/Button/Button.tsx @@ -1,5 +1,6 @@ 'use client'; import * as React from 'react'; +import { refType } from '@mui/utils'; import PropTypes from 'prop-types'; import { PolymorphicComponent } from '../utils/PolymorphicComponent'; import { unstable_composeClasses as composeClasses } from '../composeClasses'; @@ -97,14 +98,7 @@ Button.propTypes /* remove-proptypes */ = { /** * A ref for imperative actions. It currently only supports `focusVisible()` action. */ - action: PropTypes.oneOfType([ - PropTypes.func, - PropTypes.shape({ - current: PropTypes.shape({ - focusVisible: PropTypes.func.isRequired, - }), - }), - ]), + action: refType, /** * @ignore */ diff --git a/packages/mui-base/src/Input/Input.tsx b/packages/mui-base/src/Input/Input.tsx index 5614847edd287e..16534896ac7a00 100644 --- a/packages/mui-base/src/Input/Input.tsx +++ b/packages/mui-base/src/Input/Input.tsx @@ -1,5 +1,6 @@ 'use client'; import * as React from 'react'; +import { refType } from '@mui/utils'; import PropTypes from 'prop-types'; import { PolymorphicComponent } from '../utils/PolymorphicComponent'; import { isHostComponent } from '../utils/isHostComponent'; @@ -244,12 +245,7 @@ Input.propTypes /* remove-proptypes */ = { /** * @ignore */ - inputRef: PropTypes.oneOfType([ - PropTypes.func, - PropTypes.shape({ - current: PropTypes.object, - }), - ]), + inputRef: refType, /** * Maximum number of rows to display when multiline option is set to true. */ diff --git a/packages/mui-base/src/Tab/Tab.tsx b/packages/mui-base/src/Tab/Tab.tsx index 5d728e4e9ef5c9..e3978d9dabc577 100644 --- a/packages/mui-base/src/Tab/Tab.tsx +++ b/packages/mui-base/src/Tab/Tab.tsx @@ -1,7 +1,7 @@ 'use client'; import * as React from 'react'; import PropTypes from 'prop-types'; -import { unstable_useForkRef as useForkRef } from '@mui/utils'; +import { unstable_useForkRef as useForkRef, refType } from '@mui/utils'; import { unstable_composeClasses as composeClasses } from '../composeClasses'; import { getTabUtilityClass } from './tabClasses'; import { TabProps, TabTypeMap, TabRootSlotProps, TabOwnerState } from './Tab.types'; @@ -87,14 +87,7 @@ Tab.propTypes /* remove-proptypes */ = { /** * A ref for imperative actions. It currently only supports `focusVisible()` action. */ - action: PropTypes.oneOfType([ - PropTypes.func, - PropTypes.shape({ - current: PropTypes.shape({ - focusVisible: PropTypes.func.isRequired, - }), - }), - ]), + action: refType, /** * @ignore */ diff --git a/packages/mui-joy/src/Button/Button.tsx b/packages/mui-joy/src/Button/Button.tsx index 89d5067fdb3d2a..31ffe63ab9442a 100644 --- a/packages/mui-joy/src/Button/Button.tsx +++ b/packages/mui-joy/src/Button/Button.tsx @@ -4,7 +4,11 @@ import PropTypes from 'prop-types'; import { useButton } from '@mui/base/useButton'; import { unstable_composeClasses as composeClasses } from '@mui/base/composeClasses'; import { Interpolation } from '@mui/system'; -import { unstable_capitalize as capitalize, unstable_useForkRef as useForkRef } from '@mui/utils'; +import { + unstable_capitalize as capitalize, + unstable_useForkRef as useForkRef, + refType, +} from '@mui/utils'; import { styled, Theme, useThemeProps } from '../styles'; import { useColorInversion } from '../styles/ColorInversion'; import useSlot from '../utils/useSlot'; @@ -325,14 +329,7 @@ Button.propTypes /* remove-proptypes */ = { /** * A ref for imperative actions. It currently only supports `focusVisible()` action. */ - action: PropTypes.oneOfType([ - PropTypes.func, - PropTypes.shape({ - current: PropTypes.shape({ - focusVisible: PropTypes.func.isRequired, - }), - }), - ]), + action: refType, /** * @ignore */ diff --git a/packages/mui-joy/src/IconButton/IconButton.tsx b/packages/mui-joy/src/IconButton/IconButton.tsx index 8a169ab2d456c1..bc84ac3b9ad6f5 100644 --- a/packages/mui-joy/src/IconButton/IconButton.tsx +++ b/packages/mui-joy/src/IconButton/IconButton.tsx @@ -1,7 +1,11 @@ 'use client'; import * as React from 'react'; import PropTypes from 'prop-types'; -import { unstable_capitalize as capitalize, unstable_useForkRef as useForkRef } from '@mui/utils'; +import { + unstable_capitalize as capitalize, + unstable_useForkRef as useForkRef, + refType, +} from '@mui/utils'; import { useButton } from '@mui/base/useButton'; import { unstable_composeClasses as composeClasses } from '@mui/base/composeClasses'; import { styled, useThemeProps } from '../styles'; @@ -201,14 +205,7 @@ IconButton.propTypes /* remove-proptypes */ = { /** * A ref for imperative actions. It currently only supports `focusVisible()` action. */ - action: PropTypes.oneOfType([ - PropTypes.func, - PropTypes.shape({ - current: PropTypes.shape({ - focusVisible: PropTypes.func.isRequired, - }), - }), - ]), + action: refType, /** * @ignore */ diff --git a/packages/mui-joy/src/ListItemButton/ListItemButton.tsx b/packages/mui-joy/src/ListItemButton/ListItemButton.tsx index 0463696dac13a6..5cda7fe8e20770 100644 --- a/packages/mui-joy/src/ListItemButton/ListItemButton.tsx +++ b/packages/mui-joy/src/ListItemButton/ListItemButton.tsx @@ -2,7 +2,11 @@ import * as React from 'react'; import PropTypes from 'prop-types'; import clsx from 'clsx'; -import { unstable_capitalize as capitalize, unstable_useForkRef as useForkRef } from '@mui/utils'; +import { + unstable_capitalize as capitalize, + unstable_useForkRef as useForkRef, + refType, +} from '@mui/utils'; import { unstable_composeClasses as composeClasses } from '@mui/base/composeClasses'; import { useButton } from '@mui/base/useButton'; import { styled, useThemeProps } from '../styles'; @@ -213,14 +217,7 @@ ListItemButton.propTypes /* remove-proptypes */ = { /** * A ref for imperative actions. It currently only supports `focusVisible()` action. */ - action: PropTypes.oneOfType([ - PropTypes.func, - PropTypes.shape({ - current: PropTypes.shape({ - focusVisible: PropTypes.func.isRequired, - }), - }), - ]), + action: refType, /** * If `true`, the list item is focused during the first mount. * Focus will also be triggered if the value changes from false to true. diff --git a/packages/mui-joy/src/Select/Select.tsx b/packages/mui-joy/src/Select/Select.tsx index 2b3f19a198941f..4389a017dbf3a8 100644 --- a/packages/mui-joy/src/Select/Select.tsx +++ b/packages/mui-joy/src/Select/Select.tsx @@ -3,7 +3,11 @@ import * as React from 'react'; import PropTypes from 'prop-types'; import clsx from 'clsx'; import { OverrideProps, DefaultComponentProps } from '@mui/types'; -import { unstable_capitalize as capitalize, unstable_useForkRef as useForkRef } from '@mui/utils'; +import { + unstable_capitalize as capitalize, + unstable_useForkRef as useForkRef, + refType, +} from '@mui/utils'; import { Popper, PopperProps } from '@mui/base/Popper'; import { useSelect, SelectProvider } from '@mui/base/useSelect'; import { SelectOption } from '@mui/base/useOption'; @@ -625,14 +629,7 @@ Select.propTypes /* remove-proptypes */ = { /** * A ref for imperative actions. It currently only supports `focusVisible()` action. */ - action: PropTypes.oneOfType([ - PropTypes.func, - PropTypes.shape({ - current: PropTypes.shape({ - focusVisible: PropTypes.func.isRequired, - }), - }), - ]), + action: refType, /** * If `true`, the select element is focused during the first mount * @default false diff --git a/packages/mui-joy/src/Tab/Tab.tsx b/packages/mui-joy/src/Tab/Tab.tsx index c9d8342d03b361..e369fabf05ab31 100644 --- a/packages/mui-joy/src/Tab/Tab.tsx +++ b/packages/mui-joy/src/Tab/Tab.tsx @@ -2,7 +2,11 @@ import * as React from 'react'; import PropTypes from 'prop-types'; import { OverridableComponent } from '@mui/types'; -import { unstable_capitalize as capitalize, unstable_useForkRef as useForkRef } from '@mui/utils'; +import { + unstable_capitalize as capitalize, + unstable_useForkRef as useForkRef, + refType, +} from '@mui/utils'; import { unstable_composeClasses as composeClasses } from '@mui/base'; import { useTab } from '@mui/base/useTab'; import { StyledListItemButton } from '../ListItemButton/ListItemButton'; @@ -215,14 +219,7 @@ Tab.propTypes /* remove-proptypes */ = { /** * A ref for imperative actions. It currently only supports `focusVisible()` action. */ - action: PropTypes.oneOfType([ - PropTypes.func, - PropTypes.shape({ - current: PropTypes.shape({ - focusVisible: PropTypes.func.isRequired, - }), - }), - ]), + action: refType, /** * @ignore */ diff --git a/packages/mui-material-next/src/ButtonBase/ButtonBase.tsx b/packages/mui-material-next/src/ButtonBase/ButtonBase.tsx index de46813447ad93..1f0913e15c9c53 100644 --- a/packages/mui-material-next/src/ButtonBase/ButtonBase.tsx +++ b/packages/mui-material-next/src/ButtonBase/ButtonBase.tsx @@ -299,15 +299,7 @@ ButtonBase.propTypes /* remove-proptypes */ = { /** * A ref that points to the `TouchRipple` element. */ - touchRippleRef: PropTypes.oneOfType([ - PropTypes.func, - PropTypes.shape({ - current: PropTypes.shape({ - start: PropTypes.func.isRequired, - stop: PropTypes.func.isRequired, - }), - }), - ]), + touchRippleRef: refType, /** * Type attribute applied to the root component. * @default 'button' diff --git a/packages/mui-material/src/ButtonBase/ButtonBase.js b/packages/mui-material/src/ButtonBase/ButtonBase.js index 49366647405411..e3124655f2a1fe 100644 --- a/packages/mui-material/src/ButtonBase/ButtonBase.js +++ b/packages/mui-material/src/ButtonBase/ButtonBase.js @@ -521,16 +521,7 @@ ButtonBase.propTypes /* remove-proptypes */ = { /** * A ref that points to the `TouchRipple` element. */ - touchRippleRef: PropTypes.oneOfType([ - PropTypes.func, - PropTypes.shape({ - current: PropTypes.shape({ - pulsate: PropTypes.func.isRequired, - start: PropTypes.func.isRequired, - stop: PropTypes.func.isRequired, - }), - }), - ]), + touchRippleRef: refType, /** * @ignore */ diff --git a/packages/typescript-to-proptypes/src/createType.ts b/packages/typescript-to-proptypes/src/createType.ts index 4ca05ed12f4549..cdb36e84731d42 100644 --- a/packages/typescript-to-proptypes/src/createType.ts +++ b/packages/typescript-to-proptypes/src/createType.ts @@ -15,6 +15,7 @@ import { StringType, ObjectType, NumericType, + ReactRefType, } from './models'; export function createAnyType(init: { jsDoc: string | undefined }): AnyType { @@ -64,6 +65,13 @@ export function createElementType(init: { }; } +export function createReactRefType(init: { jsDoc: string | undefined }): ReactRefType { + return { + type: 'ReactRefNode', + jsDoc: init.jsDoc, + }; +} + export function createFunctionType(init: { jsDoc: string | undefined }): FunctionType { return { type: 'FunctionNode', diff --git a/packages/typescript-to-proptypes/src/generatePropTypes.ts b/packages/typescript-to-proptypes/src/generatePropTypes.ts index 004fbb17ab1416..3993562c907170 100644 --- a/packages/typescript-to-proptypes/src/generatePropTypes.ts +++ b/packages/typescript-to-proptypes/src/generatePropTypes.ts @@ -155,6 +155,10 @@ export function generatePropTypes( return `${importedName}.bool`; } + if (propType.type === 'ReactRefNode') { + return 'refType'; + } + if (propType.type === 'NumericNode') { return `${importedName}.number`; } diff --git a/packages/typescript-to-proptypes/src/getPropTypesFromFile.ts b/packages/typescript-to-proptypes/src/getPropTypesFromFile.ts index 0da13bc41aaac8..5623ed5631a6c1 100644 --- a/packages/typescript-to-proptypes/src/getPropTypesFromFile.ts +++ b/packages/typescript-to-proptypes/src/getPropTypesFromFile.ts @@ -19,6 +19,7 @@ import { createNumericType, createObjectType, createStringType, + createReactRefType, } from './createType'; import { PropTypeDefinition, PropTypesComponent, PropType } from './models'; @@ -291,6 +292,23 @@ function checkSymbol({ ts.isTypeReferenceNode(declaration.type) ) { const name = declaration.type.typeName.getText(); + + if (name === 'React.Ref') { + return { + $$id: project.createPropTypeId(symbol), + name: symbol.getName(), + jsDoc, + filenames: symbolFilenames, + propType: createUnionType({ + jsDoc, + types: [ + createUndefinedType({ jsDoc: undefined }), + createReactRefType({ jsDoc: undefined }), + ], + }), + }; + } + if ( name === 'React.ElementType' || name === 'React.JSXElementConstructor' || diff --git a/packages/typescript-to-proptypes/src/injectPropTypesInFile.ts b/packages/typescript-to-proptypes/src/injectPropTypesInFile.ts index 33be76a163c5ad..5242bf1b025d6e 100644 --- a/packages/typescript-to-proptypes/src/injectPropTypesInFile.ts +++ b/packages/typescript-to-proptypes/src/injectPropTypesInFile.ts @@ -2,7 +2,7 @@ import * as babel from '@babel/core'; import * as babelTypes from '@babel/types'; import { v4 as uuid } from 'uuid'; import { generatePropTypes, GeneratePropTypesOptions } from './generatePropTypes'; -import { PropTypesComponent, PropTypeDefinition, LiteralType } from './models'; +import { PropTypesComponent, PropTypeDefinition, LiteralType, PropType } from './models'; export interface InjectPropTypesInFileOptions extends Pick< @@ -170,9 +170,11 @@ function createBabelPlugin({ return includeUnusedProps ? true : data.usedProps.includes(data.prop.name); }; - let importName = ''; - let needImport = false; - let alreadyImported = false; + let importNodeFromMuiUtils: babel.NodePath | null = null; + let hasRefTypeImportFromMuiUtils = false; + let needRefTypeImportFromMuiUtils = false; + let alreadyImportedPropTypesName: string | null = null; + let needImportFromPropTypePackage = false; let originalPropTypesPath: null | babel.NodePath = null; const previousPropTypesSource = new Map(); @@ -186,7 +188,7 @@ function createBabelPlugin({ const source = generatePropTypes(props, { ...otherOptions, - importedName: importName, + importedName: alreadyImportedPropTypesName ?? 'PropTypes', previousPropTypesSource, reconcilePropTypes, shouldInclude: (prop) => shouldInclude({ component: props, prop, usedProps }), @@ -194,7 +196,13 @@ function createBabelPlugin({ const emptyPropTypes = source === ''; if (!emptyPropTypes) { - needImport = true; + needImportFromPropTypePackage = true; + } + + // TODO: If we filter the injected proptypes in this file instead of in generatePropTypes. + // Then we could check correctly if some prop-type is a ref and not just rely on some string analysis. + if (source.includes(': refType')) { + needRefTypeImportFromMuiUtils = true; } const placeholder = `const a${uuid().replace(/-/g, '_')} = null;`; @@ -233,25 +241,23 @@ function createBabelPlugin({ visitor: { Program: { enter(path, state: any) { - if ( - !path.node.body.some((n) => { - if ( - babelTypes.isImportDeclaration(n) && - n.source.value === 'prop-types' && - n.specifiers.length - ) { - importName = n.specifiers[0].local.name; - alreadyImported = true; - return true; - } - return false; - }) - ) { - importName = 'PropTypes'; - } - path.get('body').forEach((nodePath) => { const { node } = nodePath; + + if (babelTypes.isImportDeclaration(node) && node.specifiers.length) { + if (node.source.value === 'prop-types') { + alreadyImportedPropTypesName = node.specifiers[0].local.name; + } else if (node.source.value === '@mui/utils') { + importNodeFromMuiUtils = nodePath; + const specifier = node.specifiers.find( + (el) => babelTypes.isImportSpecifier(el) && el.local.name === 'refType', + ); + if (specifier) { + hasRefTypeImportFromMuiUtils = true; + } + } + } + if ( babelTypes.isExpressionStatement(node) && babelTypes.isAssignmentExpression(node.expression, { operator: '=' }) && @@ -293,23 +299,54 @@ function createBabelPlugin({ }); }, exit(path) { - if (alreadyImported || !needImport) { - return; + if (alreadyImportedPropTypesName == null && needImportFromPropTypePackage) { + const propTypesImport = babel.template.ast( + `import PropTypes from 'prop-types'`, + ) as babel.types.ImportDeclaration; + + const firstImport = path + .get('body') + .find((nodePath) => babelTypes.isImportDeclaration(nodePath.node)); + + // Insert import after the first one to avoid issues with comment flags + if (firstImport) { + firstImport.insertAfter(propTypesImport); + } else { + path.node.body = [propTypesImport, ...path.node.body]; + } } - const propTypesImport = babel.template.ast( - `import ${importName} from 'prop-types'`, - ) as babel.types.ImportDeclaration; - - const firstImport = path - .get('body') - .find((nodePath) => babelTypes.isImportDeclaration(nodePath.node)); - - // Insert import after the first one to avoid issues with comment flags - if (firstImport) { - firstImport.insertAfter(propTypesImport); - } else { - path.node.body = [propTypesImport, ...path.node.body]; + if (needRefTypeImportFromMuiUtils && !hasRefTypeImportFromMuiUtils) { + if (importNodeFromMuiUtils) { + const node = importNodeFromMuiUtils.node as babel.types.ImportDeclaration; + importNodeFromMuiUtils.replaceWith( + babelTypes.importDeclaration( + [ + ...node.specifiers, + babelTypes.importSpecifier( + babelTypes.identifier('refType'), + babelTypes.identifier('refType'), + ), + ], + babelTypes.stringLiteral('@mui/utils'), + ), + ); + } else { + const refTypeImport = babel.template.ast( + `import { refType } from '@mui/utils'`, + ) as babel.types.ImportDeclaration; + + const firstImport = path + .get('body') + .find((nodePath) => babelTypes.isImportDeclaration(nodePath.node)); + + // Insert import after the first one to avoid issues with comment flags + if (firstImport) { + firstImport.insertAfter(refTypeImport); + } else { + path.node.body = [refTypeImport, ...path.node.body]; + } + } } }, }, diff --git a/packages/typescript-to-proptypes/src/models.ts b/packages/typescript-to-proptypes/src/models.ts index 626461883336f1..df7b36c1ce649f 100644 --- a/packages/typescript-to-proptypes/src/models.ts +++ b/packages/typescript-to-proptypes/src/models.ts @@ -21,6 +21,7 @@ export type PropType = | BooleanType | DOMElementType | ElementType + | ReactRefType | FunctionType | InstanceOfType | InterfaceType @@ -63,6 +64,10 @@ export interface ElementType extends BasePropType { type: 'ElementNode'; } +export interface ReactRefType extends BasePropType { + type: 'ReactRefNode'; +} + export interface FunctionType extends BasePropType { type: 'FunctionNode'; } diff --git a/scripts/generateProptypes.ts b/scripts/generateProptypes.ts index 09b923a9b33b79..e4bf0ed659c4e6 100644 --- a/scripts/generateProptypes.ts +++ b/scripts/generateProptypes.ts @@ -248,7 +248,10 @@ async function generateProptypes( ensureBabelPluginTransformReactRemovePropTypesIntegration: true, getSortLiteralUnions, reconcilePropTypes: (prop, previous, generated) => { - const usedCustomValidator = previous !== undefined && !previous.startsWith('PropTypes'); + const usedCustomValidator = + previous !== undefined && + !previous.startsWith('PropTypes') && + !previous.startsWith('refType'); const ignoreGenerated = previous !== undefined && previous.startsWith('PropTypes /* @typescript-to-proptypes-ignore */');