Skip to content

Commit 4ebe15f

Browse files
committed
feat: support get special component displayName
support format Suspense、Profiler、StrictMode and Context.
1 parent dbbd9e5 commit 4ebe15f

File tree

4 files changed

+242
-7
lines changed

4 files changed

+242
-7
lines changed

src/index.spec.js

Lines changed: 55 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,14 @@
22

33
/* eslint-disable react/no-string-refs */
44

5-
import React, { Fragment, Component } from 'react';
5+
import React, {
6+
Fragment,
7+
Component,
8+
Suspense,
9+
createContext,
10+
Profiler,
11+
StrictMode,
12+
} from 'react';
613
import { createRenderer } from 'react-test-renderer/shallow';
714
import { mount } from 'enzyme';
815
import reactElementToJSXString, { preserveFunctionLineBreak } from './index';
@@ -1113,6 +1120,53 @@ describe('reactElementToJSXString(ReactElement)', () => {
11131120
).toEqual(`<div render={<><div /><div /></>} />`);
11141121
});
11151122

1123+
it('reactElementToJSXString(<Suspense fallback="loading" />)', () => {
1124+
expect(reactElementToJSXString(<Suspense fallback="loading" />)).toEqual(
1125+
`<Suspense fallback="loading" />`
1126+
);
1127+
});
1128+
1129+
it('reactElementToJSXString(<Profiler id="Main" />)', () => {
1130+
expect(reactElementToJSXString(<Profiler id="Main" />)).toEqual(
1131+
`<Profiler id="Main" />`
1132+
);
1133+
});
1134+
1135+
it('reactElementToJSXString(<StrictMode />)', () => {
1136+
expect(reactElementToJSXString(<StrictMode />)).toEqual(`<StrictMode />`);
1137+
});
1138+
1139+
it('reactElementToJSXString(<Context.Provider><Context.Consumer/></Context.Provider>)', () => {
1140+
const Context = createContext('Custom Context');
1141+
expect(
1142+
reactElementToJSXString(
1143+
<Context.Provider>
1144+
<Context.Consumer />
1145+
</Context.Provider>
1146+
)
1147+
).toEqual(
1148+
`<Context.Provider>
1149+
<Context.Consumer />
1150+
</Context.Provider>`
1151+
);
1152+
});
1153+
1154+
it('reactElementToJSXString: context with displayName', () => {
1155+
const Context = createContext('Custom Context');
1156+
Context.displayName = 'CustomContext';
1157+
expect(
1158+
reactElementToJSXString(
1159+
<Context.Provider>
1160+
<Context.Consumer />
1161+
</Context.Provider>
1162+
)
1163+
).toEqual(
1164+
`<CustomContext.Provider>
1165+
<CustomContext.Consumer />
1166+
</CustomContext.Provider>`
1167+
);
1168+
});
1169+
11161170
it('should not cause recursive loop when prop object contains an element', () => {
11171171
const Test = () => <div>Test</div>;
11181172

src/libs/ReactSymbols.js

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
/**
2+
* Copy from https://github.com/facebook/react/blob/28625c6f45423e6edc5ca0e2932281769c0d431e/packages/shared/ReactSymbols.js
3+
*
4+
* @flow
5+
*/
6+
export let REACT_ELEMENT_TYPE = 0xeac7;
7+
export let REACT_PORTAL_TYPE = 0xeaca;
8+
export let REACT_FRAGMENT_TYPE = 0xeacb;
9+
export let REACT_STRICT_MODE_TYPE = 0xeacc;
10+
export let REACT_PROFILER_TYPE = 0xead2;
11+
export let REACT_PROVIDER_TYPE = 0xeacd;
12+
export let REACT_CONTEXT_TYPE = 0xeace;
13+
export let REACT_FORWARD_REF_TYPE = 0xead0;
14+
export let REACT_SUSPENSE_TYPE = 0xead1;
15+
export let REACT_SUSPENSE_LIST_TYPE = 0xead8;
16+
export let REACT_MEMO_TYPE = 0xead3;
17+
export let REACT_LAZY_TYPE = 0xead4;
18+
export let REACT_SCOPE_TYPE = 0xead7;
19+
export let REACT_OPAQUE_ID_TYPE = 0xeae0;
20+
export let REACT_DEBUG_TRACING_MODE_TYPE = 0xeae1;
21+
export let REACT_OFFSCREEN_TYPE = 0xeae2;
22+
export let REACT_LEGACY_HIDDEN_TYPE = 0xeae3;
23+
export let REACT_CACHE_TYPE = 0xeae4;
24+
25+
if (typeof Symbol === 'function' && Symbol.for) {
26+
const symbolFor = Symbol.for;
27+
REACT_ELEMENT_TYPE = symbolFor('react.element');
28+
REACT_PORTAL_TYPE = symbolFor('react.portal');
29+
REACT_FRAGMENT_TYPE = symbolFor('react.fragment');
30+
REACT_STRICT_MODE_TYPE = symbolFor('react.strict_mode');
31+
REACT_PROFILER_TYPE = symbolFor('react.profiler');
32+
REACT_PROVIDER_TYPE = symbolFor('react.provider');
33+
REACT_CONTEXT_TYPE = symbolFor('react.context');
34+
REACT_FORWARD_REF_TYPE = symbolFor('react.forward_ref');
35+
REACT_SUSPENSE_TYPE = symbolFor('react.suspense');
36+
REACT_SUSPENSE_LIST_TYPE = symbolFor('react.suspense_list');
37+
REACT_MEMO_TYPE = symbolFor('react.memo');
38+
REACT_LAZY_TYPE = symbolFor('react.lazy');
39+
REACT_SCOPE_TYPE = symbolFor('react.scope');
40+
REACT_OPAQUE_ID_TYPE = symbolFor('react.opaque.id');
41+
REACT_DEBUG_TRACING_MODE_TYPE = symbolFor('react.debug_trace_mode');
42+
REACT_OFFSCREEN_TYPE = symbolFor('react.offscreen');
43+
REACT_LEGACY_HIDDEN_TYPE = symbolFor('react.legacy_hidden');
44+
REACT_CACHE_TYPE = symbolFor('react.cache');
45+
}
46+
47+
const MAYBE_ITERATOR_SYMBOL = typeof Symbol === 'function' && Symbol.iterator;
48+
const FAUX_ITERATOR_SYMBOL = '@@iterator';
49+
50+
export function getIteratorFn(maybeIterable: ?any): ?() => ?Iterator<*> {
51+
if (maybeIterable === null || typeof maybeIterable !== 'object') {
52+
return null;
53+
}
54+
const maybeIterator =
55+
(MAYBE_ITERATOR_SYMBOL && maybeIterable[MAYBE_ITERATOR_SYMBOL]) ||
56+
maybeIterable[FAUX_ITERATOR_SYMBOL];
57+
if (typeof maybeIterator === 'function') {
58+
return maybeIterator;
59+
}
60+
return null;
61+
}

src/libs/getComponentNameFromType.js

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
/**
2+
* Core logic copy from https://github.com/facebook/react/blob/28625c6f45423e6edc5ca0e2932281769c0d431e/packages/shared/getComponentNameFromType.js
3+
*
4+
* @flow
5+
*/
6+
7+
import type { LazyComponent, ReactContext, ReactProviderType } from 'react';
8+
9+
import {
10+
REACT_CONTEXT_TYPE,
11+
REACT_FORWARD_REF_TYPE,
12+
REACT_FRAGMENT_TYPE,
13+
REACT_PORTAL_TYPE,
14+
REACT_MEMO_TYPE,
15+
REACT_PROFILER_TYPE,
16+
REACT_PROVIDER_TYPE,
17+
REACT_STRICT_MODE_TYPE,
18+
REACT_SUSPENSE_TYPE,
19+
REACT_SUSPENSE_LIST_TYPE,
20+
REACT_LAZY_TYPE,
21+
REACT_CACHE_TYPE,
22+
} from './ReactSymbols';
23+
24+
// Keep in sync with react-reconciler/getComponentNameFromFiber
25+
function getWrappedName(
26+
outerType: mixed,
27+
innerType: any,
28+
wrapperName: string
29+
): string {
30+
const displayName = (outerType: any).displayName;
31+
if (displayName) {
32+
return displayName;
33+
}
34+
const functionName = innerType.displayName || innerType.name || '';
35+
return functionName !== '' ? `${wrapperName}(${functionName})` : wrapperName;
36+
}
37+
38+
// Keep in sync with react-reconciler/getComponentNameFromFiber
39+
function getContextName(type: ReactContext<any>) {
40+
return type.displayName || 'Context';
41+
}
42+
43+
// Note that the reconciler package should generally prefer to use getComponentNameFromFiber() instead.
44+
// eslint-disable-next-line complexity
45+
function getComponentNameFromType(type: mixed): string | null {
46+
if (type === null || type === undefined) {
47+
// Host root, text node or just invalid type.
48+
return null;
49+
}
50+
if (typeof type === 'function') {
51+
return (type: any).displayName || type.name || null;
52+
}
53+
if (typeof type === 'string') {
54+
return type;
55+
}
56+
// eslint-disable-next-line default-case
57+
switch (type) {
58+
case REACT_FRAGMENT_TYPE:
59+
return 'Fragment';
60+
case REACT_PORTAL_TYPE:
61+
return 'Portal';
62+
case REACT_PROFILER_TYPE:
63+
return 'Profiler';
64+
case REACT_STRICT_MODE_TYPE:
65+
return 'StrictMode';
66+
case REACT_SUSPENSE_TYPE:
67+
return 'Suspense';
68+
case REACT_SUSPENSE_LIST_TYPE:
69+
return 'SuspenseList';
70+
case REACT_CACHE_TYPE:
71+
return 'Cache';
72+
}
73+
if (typeof type === 'object') {
74+
// eslint-disable-next-line default-case
75+
switch (type.$$typeof) {
76+
case REACT_CONTEXT_TYPE:
77+
// eslint-disable-next-line no-case-declarations
78+
const context: ReactContext<any> = (type: any);
79+
/**
80+
* in DEV, should get context from `_context`.
81+
* https://github.com/facebook/react/blob/e16d61c3000e2de6217d06b9afad162e883f73c4/packages/react/src/ReactContext.js#L44-L125
82+
*/
83+
return `${getContextName(context._context ?? context)}.Consumer`;
84+
case REACT_PROVIDER_TYPE:
85+
// eslint-disable-next-line no-case-declarations
86+
const provider: ReactProviderType<any> = (type: any);
87+
return `${getContextName(provider._context)}.Provider`;
88+
case REACT_FORWARD_REF_TYPE:
89+
// eslint-disable-next-line no-case-declarations
90+
return getWrappedName(type, type.render, 'ForwardRef');
91+
case REACT_MEMO_TYPE:
92+
// eslint-disable-next-line no-case-declarations
93+
const outerName = (type: any).displayName || null;
94+
if (outerName !== null) {
95+
return outerName;
96+
}
97+
return getComponentNameFromType(type.type) || 'Memo';
98+
case REACT_LAZY_TYPE: {
99+
const lazyComponent: LazyComponent<any, any> = (type: any);
100+
const payload = lazyComponent._payload;
101+
const init = lazyComponent._init;
102+
try {
103+
return getComponentNameFromType(init(payload));
104+
} catch (x) {
105+
return null;
106+
}
107+
}
108+
}
109+
}
110+
return null;
111+
}
112+
113+
export default getComponentNameFromType;

src/parser/parseReactElement.js

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,15 +9,22 @@ import {
99
createReactFragmentTreeNode,
1010
} from './../tree';
1111
import type { TreeNode } from './../tree';
12+
import getComponentNameFromType from '../libs/getComponentNameFromType';
1213

1314
const supportFragment = Boolean(Fragment);
1415

15-
const getReactElementDisplayName = (element: ReactElement<*>): string =>
16-
element.type.displayName ||
17-
(element.type.name !== '_default' ? element.type.name : null) || // function name
18-
(typeof element.type === 'function' // function without a name, you should provide one
19-
? 'No Display Name'
20-
: element.type);
16+
const getReactElementDisplayName = (element: ReactElement<*>): string => {
17+
const displayName = getComponentNameFromType(element.type);
18+
if (
19+
displayName === '_default' ||
20+
displayName === null ||
21+
displayName === undefined
22+
) {
23+
return 'No Display Name';
24+
}
25+
26+
return displayName;
27+
};
2128

2229
const noChildren = (propsValue, propName) => propName !== 'children';
2330

0 commit comments

Comments
 (0)