diff --git a/.changeset/d9bc5793/changes.json b/.changeset/d9bc5793/changes.json new file mode 100644 index 00000000..05e7d151 --- /dev/null +++ b/.changeset/d9bc5793/changes.json @@ -0,0 +1,16 @@ +{ + "releases": [{ "name": "extract-react-types", "type": "minor" }], + "dependents": [ + { + "name": "extract-react-types-loader", + "type": "patch", + "dependencies": ["extract-react-types"] + }, + { "name": "kind2string", "type": "patch", "dependencies": ["extract-react-types"] }, + { + "name": "pretty-proptypes", + "type": "patch", + "dependencies": ["kind2string", "extract-react-types"] + } + ] +} diff --git a/.changeset/d9bc5793/changes.md b/.changeset/d9bc5793/changes.md new file mode 100644 index 00000000..43bdf44d --- /dev/null +++ b/.changeset/d9bc5793/changes.md @@ -0,0 +1 @@ +- Support memo, forwardRef and function expressions \ No newline at end of file diff --git a/packages/extract-react-types/__snapshots__/test.js.snap b/packages/extract-react-types/__snapshots__/test.js.snap index 1377f411..b8a40932 100644 --- a/packages/extract-react-types/__snapshots__/test.js.snap +++ b/packages/extract-react-types/__snapshots__/test.js.snap @@ -655,6 +655,58 @@ Object { } `; +exports[`flow React.forwardRef 1`] = ` +Object { + "component": Object { + "kind": "generic", + "value": Object { + "kind": "object", + "members": Array [ + Object { + "key": Object { + "kind": "id", + "name": "ok", + }, + "kind": "property", + "optional": false, + "value": Object { + "kind": "number", + }, + }, + ], + "referenceIdName": "Props", + }, + }, + "kind": "program", +} +`; + +exports[`flow React.memo 1`] = ` +Object { + "component": Object { + "kind": "generic", + "value": Object { + "kind": "object", + "members": Array [ + Object { + "key": Object { + "kind": "id", + "name": "ok", + }, + "kind": "property", + "optional": false, + "value": Object { + "kind": "number", + }, + }, + ], + "referenceIdName": "Props", + }, + }, + "kind": "program", +} +`; + exports[`flow any 1`] = ` Object { "component": Object { @@ -942,6 +994,32 @@ Object { } `; +exports[`flow forwardRef 1`] = ` +Object { + "component": Object { + "kind": "generic", + "value": Object { + "kind": "object", + "members": Array [ + Object { + "key": Object { + "kind": "id", + "name": "ok", + }, + "kind": "property", + "optional": false, + "value": Object { + "kind": "number", + }, + }, + ], + "referenceIdName": "Props", + }, + }, + "kind": "program", +} +`; + exports[`flow forwardRef default export 1`] = ` Object { "component": Object { @@ -968,6 +1046,39 @@ Object { } `; +exports[`flow forwardRef default export 2`] = ` +Object { + "component": Object { + "kind": "generic", + "value": Object { + "kind": "object", + "members": Array [ + Object { + "key": Object { + "kind": "id", + "name": "ok", + }, + "kind": "property", + "optional": false, + "value": Object { + "kind": "number", + }, + }, + ], + "referenceIdName": "Props", + }, + }, + "kind": "program", +} +`; + +exports[`flow func that is not valid 1`] = ` +Object { + "component": undefined, + "kind": "program", +} +`; + exports[`flow function 1`] = ` Object { "component": Object { @@ -1118,6 +1229,32 @@ Object { } `; +exports[`flow function expression 1`] = ` +Object { + "component": Object { + "kind": "generic", + "value": Object { + "kind": "object", + "members": Array [ + Object { + "key": Object { + "kind": "id", + "name": "ok", + }, + "kind": "property", + "optional": false, + "value": Object { + "kind": "number", + }, + }, + ], + "referenceIdName": "Props", + }, + }, + "kind": "program", +} +`; + exports[`flow function named params 1`] = ` Object { "component": Object { @@ -1569,6 +1706,110 @@ Object { } `; +exports[`flow memo 1`] = ` +Object { + "component": Object { + "kind": "generic", + "value": Object { + "kind": "object", + "members": Array [ + Object { + "key": Object { + "kind": "id", + "name": "ok", + }, + "kind": "property", + "optional": false, + "value": Object { + "kind": "number", + }, + }, + ], + "referenceIdName": "Props", + }, + }, + "kind": "program", +} +`; + +exports[`flow memo default export 1`] = ` +Object { + "component": Object { + "kind": "generic", + "value": Object { + "kind": "object", + "members": Array [ + Object { + "key": Object { + "kind": "id", + "name": "ok", + }, + "kind": "property", + "optional": false, + "value": Object { + "kind": "number", + }, + }, + ], + "referenceIdName": "Props", + }, + }, + "kind": "program", +} +`; + +exports[`flow memo wrapping forwardRef 1`] = ` +Object { + "component": Object { + "kind": "generic", + "value": Object { + "kind": "object", + "members": Array [ + Object { + "key": Object { + "kind": "id", + "name": "ok", + }, + "kind": "property", + "optional": false, + "value": Object { + "kind": "number", + }, + }, + ], + "referenceIdName": "Props", + }, + }, + "kind": "program", +} +`; + +exports[`flow memo wrapping forwardRef default export 1`] = ` +Object { + "component": Object { + "kind": "generic", + "value": Object { + "kind": "object", + "members": Array [ + Object { + "key": Object { + "kind": "id", + "name": "ok", + }, + "kind": "property", + "optional": false, + "value": Object { + "kind": "number", + }, + }, + ], + "referenceIdName": "Props", + }, + }, + "kind": "program", +} +`; + exports[`flow mixed 1`] = ` Object { "component": Object { diff --git a/packages/extract-react-types/index.js b/packages/extract-react-types/index.js index 01131710..e02dbdf3 100644 --- a/packages/extract-react-types/index.js +++ b/packages/extract-react-types/index.js @@ -19,18 +19,18 @@ const { sync: resolveSync } = require('resolve'); const matchExported = require('./matchExported'); const converters = {}; -const isArrowFunctionComponent = path => - path.isArrowFunctionExpression() || - (path.isVariableDeclarator() && path.get('init').isArrowFunctionExpression()); - -const isFunctionComponent = path => { - return path.isFunctionDeclaration(); +const isParentSpecialReactComponentType = (path, type /*:'memo' | 'forwardRef'*/) => { + if (path.parentPath && path.parentPath.isCallExpression()) { + const callee = path.parentPath.get('callee'); + if (callee.isIdentifier() && callee.node.name === type) { + return true; + } + if (callee.isMemberExpression() && callee.matchesPattern(`React.${type}`)) { + return true; + } + } }; -function isReactComponentFunction(path) { - return isArrowFunctionComponent(path) || isFunctionComponent(path); -} - const getPropFromObject = (props, property) => { let prop; @@ -127,19 +127,51 @@ converters.Program = (path, context) /*: K.Program*/ => { path.traverse({ ExportDefaultDeclaration(exportPath) { if (exportPath.get('declaration').isIdentifier()) { + const declarationName = exportPath.get('declaration').node.name; + const isDefaultExport = path => - path.get('id').node && - path.get('id').node.name === exportPath.get('declaration').node.name; + path.get('id').node && path.get('id').node.name === declarationName; + + const isParentVariableDeclaratorDefaultExport = path => { + const isParentVariableDeclarator = path.parentPath.isVariableDeclarator(); + if (isParentVariableDeclarator) { + return isDefaultExport(path.parentPath); + } + }; + path.traverse({ - 'FunctionDeclaration|ArrowFunctionExpression'(functionPath) { - if (isDefaultExport(functionPath) && isReactComponentFunction(functionPath)) { + FunctionDeclaration(functionPath) { + if (isDefaultExport(functionPath)) { componentPath = functionPath; } }, - VariableDeclaration(variablePath) { - const declaration = variablePath.get('declarations.0'); - if (isDefaultExport(declaration) && isReactComponentFunction(declaration)) { - componentPath = declaration.get('init'); + 'FunctionExpression|ArrowFunctionExpression'(functionPath) { + if (isParentVariableDeclaratorDefaultExport(functionPath)) { + componentPath = functionPath; + } else { + const isParentForwardRef = isParentSpecialReactComponentType( + functionPath, + 'forwardRef' + ); + const isParentForwardRefOrMemo = + isParentForwardRef || isParentSpecialReactComponentType(functionPath, 'memo'); + + // check for React.forwardRef(() => {}) and React.memo(() => {}) + if ( + isParentForwardRefOrMemo && + isParentVariableDeclaratorDefaultExport(functionPath.parentPath) + ) { + componentPath = functionPath; + return; + } + // check for React.memo(React.forwardRef(() => {})) + if ( + isParentForwardRef && + isParentSpecialReactComponentType(functionPath.parentPath, 'memo') && + isParentVariableDeclaratorDefaultExport(functionPath.parentPath.parentPath) + ) { + componentPath = functionPath; + } } }, ClassDeclaration(classPath) { @@ -155,8 +187,8 @@ converters.Program = (path, context) /*: K.Program*/ => { componentPath = classPath; } }, - 'FunctionDeclaration|ArrowFunctionExpression'(functionPath) { - if (!componentPath && isReactComponentFunction(functionPath)) { + 'FunctionDeclaration|ArrowFunctionExpression|FunctionExpression'(functionPath) { + if (!componentPath) { componentPath = functionPath; } } @@ -197,14 +229,14 @@ function convertReactComponentFunction(path, context) { }); let name = ''; - if ( - path.type === 'ArrowFunctionExpression' && - path.parent && - path.parent.type === 'VariableDeclarator' - ) { - name = path.parent.id.name; - } else if (path.type === 'FunctionDeclaration' && path.node.id && path.node.id.name) { + if (path.type === 'FunctionDeclaration' && path.node.id && path.node.id.name) { name = path.node.id.name; + } else { + const variableDeclarator = path.findParent(path => path.isVariableDeclarator()); + + if (variableDeclarator) { + name = variableDeclarator.node.id.name; + } } let defaultProps = []; diff --git a/packages/extract-react-types/test.js b/packages/extract-react-types/test.js index fc74bd59..7eb0adaf 100644 --- a/packages/extract-react-types/test.js +++ b/packages/extract-react-types/test.js @@ -1329,6 +1329,158 @@ const TESTS = [ export default Field; + ` + }, + { + name: 'flow forwardRef', + typeSystem: 'flow', + code: ` + type Props = { + ok: number + } + + const SomeComponent = forwardRef((props: Props, ref) => { + + }) + + export default SomeComponent + + + ` + }, + { + name: 'flow React.forwardRef', + typeSystem: 'flow', + code: ` + type Props = { + ok: number + } + + const SomeComponent = React.forwardRef((props: Props, ref) => { + + }) + + export default SomeComponent + + ` + }, + { + name: 'flow function expression', + typeSystem: 'flow', + code: ` + type Props = { + ok: number + } + + const SomeComponent = function(props: Props) { + + } + + export default SomeComponent + + ` + }, + { + name: 'flow React.memo', + typeSystem: 'flow', + code: ` + type Props = { + ok: number + } + + const SomeComponent = memo((props: Props, ref) => { + + }) + + export default SomeComponent + + ` + }, + { + name: 'flow memo', + typeSystem: 'flow', + code: ` + type Props = { + ok: number + } + + const SomeComponent = React.memo((props: Props, ref) => { + + }) + + export default SomeComponent + + ` + }, + { + name: 'flow func that is not valid', + typeSystem: 'flow', + code: ` + type Props = { + ok: number + } + + const SomeComponent = something((props: Props, ref) => { + + }) + + export default SomeComponent + + ` + }, + { + name: 'flow memo wrapping forwardRef', + typeSystem: 'flow', + code: ` + type Props = { + ok: number + } + + const SomeComponent = memo(forwardRef((props: Props, ref) => { + + })) + + export default SomeComponent + + ` + }, + { + name: 'flow forwardRef default export', + typeSystem: 'flow', + code: ` + type Props = { + ok: number + } + + export default forwardRef((props: Props, ref) => { + + }) + ` + }, + { + name: 'flow memo default export', + typeSystem: 'flow', + code: ` + type Props = { + ok: number + } + + export default memo((props: Props, ref) => { + + }) + ` + }, + { + name: 'flow memo wrapping forwardRef default export', + typeSystem: 'flow', + code: ` + type Props = { + ok: number + } + + export default memo(forwardRef((props: Props, ref) => { + + })) ` } ];