Skip to content

Commit cbd9557

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

File tree

4 files changed

+205
-15
lines changed

4 files changed

+205
-15
lines changed

Diff for: package.json

+4-8
Original file line numberDiff line numberDiff line change
@@ -24,10 +24,7 @@
2424
"smoke": "node tests/smoke/run"
2525
},
2626
"lint-staged": {
27-
"*.js": [
28-
"prettier --write \"**/*.{js,json}\"",
29-
"git add"
30-
]
27+
"*.js": ["prettier --write \"**/*.{js,json}\"", "git add"]
3128
},
3229
"author": {
3330
"name": "Algolia, Inc.",
@@ -80,15 +77,14 @@
8077
},
8178
"peerDependencies": {
8279
"react": "^0.14.8 || ^15.0.1 || ^16.0.0 || ^17.0.1",
83-
"react-dom": "^0.14.8 || ^15.0.1 || ^16.0.0 || ^17.0.1"
80+
"react-dom": "^0.14.8 || ^15.0.1 || ^16.0.0 || ^17.0.1",
81+
"react-is": "^16.0.0"
8482
},
8583
"dependencies": {
8684
"@base2/pretty-print-object": "1.0.0",
8785
"is-plain-object": "3.0.1"
8886
},
8987
"jest": {
90-
"setupFilesAfterEnv": [
91-
"<rootDir>tests/setupTests.js"
92-
]
88+
"setupFilesAfterEnv": ["<rootDir>tests/setupTests.js"]
9389
}
9490
}

Diff for: src/index.spec.js

+55-1
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

Diff for: src/libs/getComponentNameFromType.js

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

Diff for: src/parser/parseReactElement.js

+25-6
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,18 @@
11
/* @flow */
22

33
import React, { type Element as ReactElement, Fragment } from 'react';
4+
import {
5+
isElement,
6+
isForwardRef,
7+
isFragment,
8+
isLazy,
9+
isMemo,
10+
isPortal,
11+
isProfiler,
12+
isStrictMode,
13+
isSuspense,
14+
isSuspenseList,
15+
} from 'react-is';
416
import type { Options } from './../options';
517
import {
618
createStringTreeNode,
@@ -9,15 +21,22 @@ import {
921
createReactFragmentTreeNode,
1022
} from './../tree';
1123
import type { TreeNode } from './../tree';
24+
import getComponentNameFromType from '../libs/getComponentNameFromType';
1225

1326
const supportFragment = Boolean(Fragment);
1427

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);
28+
const getReactElementDisplayName = (element: ReactElement<*>): string => {
29+
const displayName = getComponentNameFromType(element.type);
30+
if (
31+
displayName === '_default' ||
32+
displayName === null ||
33+
displayName === undefined
34+
) {
35+
return 'No Display Name';
36+
}
37+
38+
return displayName;
39+
};
2140

2241
const noChildren = (propsValue, propName) => propName !== 'children';
2342

0 commit comments

Comments
 (0)