32
return typeof name === 'string' && name[0] >= 'A' && name[0] <= 'Z';
33
}
34
35
- function isComponentish(node) {
35
+ function findInnerComponents(inferredName, path, callback) {
36
+ const node = path.node;
37
switch (node.type) {
37
- case 'FunctionDeclaration':
38
- return node.id !== null && isComponentishName(node.id.name);
39
- case 'VariableDeclarator':
40
- return (
41
- isComponentishName(node.id.name) &&
42
- node.init !== null &&
43
- (node.init.type === 'FunctionExpression' ||
44
- (node.init.type === 'ArrowFunctionExpression' &&
45
- node.init.body.type !== 'ArrowFunctionExpression'))
38
+ case 'FunctionDeclaration': {
39
+ // function Foo() {}
40
+ // export function Foo() {}
41
+ // export default function Foo() {}
42
+ callback(inferredName, node.id, null);
43
+ return true;
44
+ }
45
+ case 'ArrowFunctionExpression': {
46
+ if (node.body.type === 'ArrowFunctionExpression') {
47
+ return false;
48
+ }
49
+ // let Foo = () => {}
50
+ // export default hoc1(hoc2(() => {}))
51
+ callback(inferredName, node, path);
52
+ return true;
53
+ }
54
+ case 'FunctionExpression': {
55
+ // let Foo = function() {}
56
+ // const Foo = hoc1(forwardRef(function renderFoo() {}))
57
+ // export default memo(function() {})
58
+ callback(inferredName, node, path);
59
+ return true;
60
+ }
61
+ case 'CallExpression': {
62
+ const argsPath = path.get('arguments');
63
+ if (argsPath === undefined || argsPath.length === 0) {
64
+ return false;
65
+ }
66
+ const calleePath = path.get('callee');
67
+ switch (calleePath.node.type) {
68
+ case 'MemberExpression':
69
+ case 'Identifier': {
70
+ const calleeSource = calleePath.getSource();
71
+ const firstArgPath = argsPath[0];
72
+ const innerName = inferredName + '$' + calleeSource;
73
+ const foundInside = findInnerComponents(
74
+ innerName,
75
+ firstArgPath,
76
+ callback,
77
+ );
78
+ if (!foundInside) {
79
+ return false;
80
+ }
81
+ // const Foo = hoc1(hoc2(() => {}))
82
+ // export default memo(React.forwardRef(function() {}))
83
+ callback(inferredName, node, path);
84
+ return true;
85
+ }
86
+ default: {
87
+ return false;
88
+ }
89
+ }
90
+ }
91
+ case 'VariableDeclarator': {
92
+ const init = node.init;
93
+ if (init === null) {
94
+ return false;
95
+ }
96
+ const name = node.id.name;
97
+ if (!isComponentishName(name)) {
98
+ return false;
99
+ }
100
+ if (init.type === 'Identifier' || init.type === 'MemberExpression') {
101
+ return false;
102
+ }
103
+ const initPath = path.get('init');
104
+ const foundInside = findInnerComponents(
105
+ inferredName,
106
+ initPath,
107
+ callback,
108
);
47
- default:
48
- return false;
109
+ if (foundInside) {
110
+ return true;
111
+ }
112
+ // See if this identifier is used in JSX. Then it's a component.
113
+ const binding = path.scope.getBinding(name);
114
+ if (binding === undefined) {
115
+ return;
116
+ }
117
+ let isLikelyUsedAsType = false;
118
+ const referencePaths = binding.referencePaths;
119
+ for (let i = 0; i < referencePaths.length; i++) {
120
+ const ref = referencePaths[i];
121
+ if (
122
+ ref.node.type !== 'JSXIdentifier' &&
123
+ ref.node.type !== 'Identifier'
124
+ ) {
125
+ continue;
126
+ }
127
+ const refParent = ref.parent;
128
+ if (refParent.type === 'JSXOpeningElement') {
129
+ isLikelyUsedAsType = true;
130
+ } else if (refParent.type === 'CallExpression') {
131
+ const callee = refParent.callee;
132
+ let fnName;
133
+ switch (callee.type) {
134
+ case 'Identifier':
135
+ fnName = callee.name;
136
+ break;
137
+ case 'MemberExpression':
138
+ fnName = callee.property.name;
139
+ break;
140
+ }
141
+ switch (fnName) {
142
+ case 'createElement':
143
+ case 'jsx':
144
+ case 'jsxDEV':
145
+ case 'jsxs':
146
+ isLikelyUsedAsType = true;
147
+ break;
148
+ }
149
+ }
150
+ if (isLikelyUsedAsType) {
151
+ // const X = ... + later <X />
152
+ callback(inferredName, init, initPath);
153
+ return true;
154
+ }
155
+ }
156
+ }
157
}
158
+ return false;
159
}
160
161
return {
162
visitor: {
163
+ ExportDefaultDeclaration(path) {
164
+ const node = path.node;
165
+ const decl = node.declaration;
166
+ const declPath = path.get('declaration');
167
+ if (decl.type !== 'CallExpression') {
168
+ // For now, we only support possible HOC calls here.
169
+ // Named function declarations are handled in FunctionDeclaration.
170
+ // Anonymous direct exports like export default function() {}
171
+ // are currently ignored.
172
+ return;
173
+ }
174
+ // This code path handles nested cases like:
175
+ // export default memo(() => {})
176
+ // In those cases it is more plausible people will omit names
177
+ // so they're worth handling despite possible false positives.
178
+ // More importantly, it handles the named case:
179
+ // export default memo(function Named() {})
180
+ const inferredName = '%default%';
181
+ const programPath = path.parentPath;
182
+ findInnerComponents(
183
+ inferredName,
184
+ declPath,
185
+ (persistentID, targetExpr, targetPath) => {
186
+ const handle = createRegistration(programPath, persistentID);
187
+ targetPath.replaceWith(
188
+ t.assignmentExpression('=', handle, targetExpr),
189
+ );
190
+ },
191
+ );
192
+ },
193
FunctionDeclaration(path) {
194
let programPath;
195
let insertAfterPath;
199
programPath = path.parentPath;
200
break;
201
case 'ExportNamedDeclaration':
202
+ insertAfterPath = path.parentPath;
203
+ programPath = insertAfterPath.parentPath;
204
+ break;
205
case 'ExportDefaultDeclaration':
206
insertAfterPath = path.parentPath;
207
programPath = insertAfterPath.parentPath;
209
default:
210
return;
211
}
70
- const maybeComponent = path.node;
71
- if (!isComponentish(maybeComponent)) {
212
+ const id = path.node.id;
213
+ if (id === null) {
214
+ // We don't currently handle anonymous default exports.
215
return;
216
}
74
- const functionName = path.node.id.name;
75
- const handle = createRegistration(programPath, functionName);
76
- insertAfterPath.insertAfter(
77
- t.expressionStatement(
78
- t.assignmentExpression('=', handle, path.node.id),
79
- ),
80
- );
217
+ const inferredName = id.name;
218
+ if (!isComponentishName(inferredName)) {
219
+ return;
220
+ }
221
+ // export function Named() {}
222
+ // function Named() {}
223
+ findInnerComponents(inferredName, path, (persistentID, targetExpr) => {
224
+ const handle = createRegistration(programPath, persistentID);
225
+ insertAfterPath.insertAfter(
226
+ t.expressionStatement(
227
+ t.assignmentExpression('=', handle, targetExpr),
228
+ ),
229
+ );
230
+ });
231
},
232
VariableDeclaration(path) {
233
let programPath;
242
default:
243
return;
244
}
95
- const declPath = path.get('declarations');
96
- if (declPath.length !== 1) {
245
+ const declPaths = path.get('declarations');
246
+ if (declPaths.length !== 1) {
247
return;
248
}
99
- const firstDeclPath = declPath[0];
100
- const maybeComponent = firstDeclPath.node;
101
- if (!isComponentish(maybeComponent)) {
102
- return;
103
- }
104
- const functionName = maybeComponent.id.name;
105
- const initPath = firstDeclPath.get('init');
106
- const handle = createRegistration(programPath, functionName);
107
- initPath.replaceWith(
108
- t.assignmentExpression('=', handle, initPath.node),
249
+ const declPath = declPaths[0];
250
+ const inferredName = declPath.node.id.name;
251
+ findInnerComponents(
252
+ inferredName,
253
+ declPath,
254
+ (persistentID, targetExpr, targetPath) => {
255
+ const handle = createRegistration(programPath, persistentID);
256
+ targetPath.replaceWith(
257
+ t.assignmentExpression('=', handle, targetExpr),
258
+ );
259
+ },
260
);
261
},
262
Program: {