diff --git a/package.json b/package.json index 7811865fb..ed54da445 100644 --- a/package.json +++ b/package.json @@ -24,10 +24,7 @@ "smoke": "node tests/smoke/run" }, "lint-staged": { - "*.js": [ - "prettier --write \"**/*.{js,json}\"", - "git add" - ] + "*.js": ["prettier --write \"**/*.{js,json}\"", "git add"] }, "author": { "name": "Algolia, Inc.", @@ -80,15 +77,14 @@ }, "peerDependencies": { "react": "^0.14.8 || ^15.0.1 || ^16.0.0 || ^17.0.1", - "react-dom": "^0.14.8 || ^15.0.1 || ^16.0.0 || ^17.0.1" + "react-dom": "^0.14.8 || ^15.0.1 || ^16.0.0 || ^17.0.1", + "react-is": "^16.0.0" }, "dependencies": { "@base2/pretty-print-object": "1.0.0", "is-plain-object": "3.0.1" }, "jest": { - "setupFilesAfterEnv": [ - "<rootDir>tests/setupTests.js" - ] + "setupFilesAfterEnv": ["<rootDir>tests/setupTests.js"] } } diff --git a/src/index.spec.js b/src/index.spec.js index c2b4c288c..985ae8065 100644 --- a/src/index.spec.js +++ b/src/index.spec.js @@ -2,7 +2,14 @@ /* eslint-disable react/no-string-refs */ -import React, { Fragment, Component } from 'react'; +import React, { + Fragment, + Component, + Suspense, + createContext, + Profiler, + StrictMode, +} from 'react'; import { createRenderer } from 'react-test-renderer/shallow'; import { mount } from 'enzyme'; import reactElementToJSXString, { preserveFunctionLineBreak } from './index'; @@ -1113,6 +1120,53 @@ describe('reactElementToJSXString(ReactElement)', () => { ).toEqual(`<div render={<><div /><div /></>} />`); }); + it('reactElementToJSXString(<Suspense fallback="loading" />)', () => { + expect(reactElementToJSXString(<Suspense fallback="loading" />)).toEqual( + `<Suspense fallback="loading" />` + ); + }); + + it('reactElementToJSXString(<Profiler id="Main" />)', () => { + expect(reactElementToJSXString(<Profiler id="Main" />)).toEqual( + `<Profiler id="Main" />` + ); + }); + + it('reactElementToJSXString(<StrictMode />)', () => { + expect(reactElementToJSXString(<StrictMode />)).toEqual(`<StrictMode />`); + }); + + it('reactElementToJSXString(<Context.Provider><Context.Consumer/></Context.Provider>)', () => { + const Context = createContext('Custom Context'); + expect( + reactElementToJSXString( + <Context.Provider> + <Context.Consumer /> + </Context.Provider> + ) + ).toEqual( + `<Context.Provider> + <Context.Consumer /> +</Context.Provider>` + ); + }); + + it('reactElementToJSXString: context with displayName', () => { + const Context = createContext('Custom Context'); + Context.displayName = 'CustomContext'; + expect( + reactElementToJSXString( + <Context.Provider> + <Context.Consumer /> + </Context.Provider> + ) + ).toEqual( + `<CustomContext.Provider> + <CustomContext.Consumer /> +</CustomContext.Provider>` + ); + }); + it('should not cause recursive loop when prop object contains an element', () => { const Test = () => <div>Test</div>; diff --git a/src/libs/getComponentNameFromType.js b/src/libs/getComponentNameFromType.js new file mode 100644 index 000000000..a7c50b835 --- /dev/null +++ b/src/libs/getComponentNameFromType.js @@ -0,0 +1,121 @@ +/** + * Reference from https://github.com/facebook/react/blob/28625c6f45423e6edc5ca0e2932281769c0d431e/packages/shared/getComponentNameFromType.js + * + * @flow + */ + +import type { Context } from 'react'; +import { Fragment } from 'react'; +import { + ContextConsumer, + ContextProvider, + ForwardRef, + Portal, + Memo, + Profiler, + StrictMode, + Suspense, + SuspenseList, + Lazy, +} from 'react-is'; + +/** + * didn't export the type in React + * same as https://github.com/facebook/react/blob/310187264d01a31bc3079358f13662d31a079d9e/packages/react/index.js + */ +type LazyComponent<T, P> = { + $$typeof: Symbol | number, + _payload: P, + _init: (payload: P) => T, +}; + +// Keep in sync with react-reconciler/getComponentNameFromFiber +function getWrappedName( + outerType: mixed, + innerType: any, + wrapperName: string +): string { + const displayName = (outerType: any).displayName; + if (displayName) { + return displayName; + } + const functionName = innerType.displayName || innerType.name || ''; + return functionName !== '' ? `${wrapperName}(${functionName})` : wrapperName; +} + +// Keep in sync with react-reconciler/getComponentNameFromFiber +function getContextName(type: Context<any>) { + return type.displayName || 'Context'; +} + +// Note that the reconciler package should generally prefer to use getComponentNameFromFiber() instead. +// eslint-disable-next-line complexity +function getComponentNameFromType(type: mixed): string | null { + if (type === null || type === undefined) { + // Host root, text node or just invalid type. + return null; + } + if (typeof type === 'function') { + return (type: any).displayName || type.name || null; + } + if (typeof type === 'string') { + return type; + } + // eslint-disable-next-line default-case + switch (type) { + case Fragment: + return 'Fragment'; + case Portal: + return 'Portal'; + case Profiler: + return 'Profiler'; + case StrictMode: + return 'StrictMode'; + case Suspense: + return 'Suspense'; + case SuspenseList: + return 'SuspenseList'; + // case REACT_CACHE_TYPE: + // return 'Cache'; + } + if (typeof type === 'object') { + // eslint-disable-next-line default-case + switch (type.$$typeof) { + case ContextConsumer: { + /** + * in DEV, should get context from `_context`. + * https://github.com/facebook/react/blob/e16d61c3000e2de6217d06b9afad162e883f73c4/packages/react/src/ReactContext.js#L44-L125 + */ + const context: any = type._context ?? type; + return `${getContextName(context)}.Consumer`; + } + case ContextProvider: { + const context: any = type._context; + return `${getContextName(context)}.Provider`; + } + case ForwardRef: + // eslint-disable-next-line no-case-declarations + return getWrappedName(type, type.render, 'ForwardRef'); + case Memo: { + const outerName = (type: any).displayName || null; + if (outerName !== null) { + return outerName; + } + return getComponentNameFromType(type.type) || 'Memo'; + } + case Lazy: { + const lazyComponent: LazyComponent<any, any> = (type: any); + const payload = lazyComponent._payload; + const init = lazyComponent._init; + try { + return getComponentNameFromType(init(payload)); + } catch (x) { + return null; + } + } + } + } + return null; +} + +export default getComponentNameFromType; diff --git a/src/parser/parseReactElement.js b/src/parser/parseReactElement.js index 773b1a98d..f681a9b0f 100644 --- a/src/parser/parseReactElement.js +++ b/src/parser/parseReactElement.js @@ -9,15 +9,22 @@ import { createReactFragmentTreeNode, } from './../tree'; import type { TreeNode } from './../tree'; +import getComponentNameFromType from '../libs/getComponentNameFromType'; const supportFragment = Boolean(Fragment); -const getReactElementDisplayName = (element: ReactElement<*>): string => - element.type.displayName || - (element.type.name !== '_default' ? element.type.name : null) || // function name - (typeof element.type === 'function' // function without a name, you should provide one - ? 'No Display Name' - : element.type); +const getReactElementDisplayName = (element: ReactElement<*>): string => { + const displayName = getComponentNameFromType(element.type); + if ( + displayName === '_default' || + displayName === null || + displayName === undefined + ) { + return 'No Display Name'; + } + + return displayName; +}; const noChildren = (propsValue, propName) => propName !== 'children';