power-linter 1.0.0 → 1.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/package.json +41 -48
- package/scripts/classToFC/class-to-functional.js +463 -0
- package/scripts/classToFC/run-codemod.js +54 -0
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# power-linter
|
|
2
2
|
|
|
3
|
-
power-linter —
|
|
3
|
+
power-linter — линтер (ESLint) и форматтер (Prettier) с расширяемой архитектурой для произвольных правил.
|
|
4
4
|
|
|
5
5
|
- конфиги eslint и prettier из коробки
|
|
6
6
|
- кастомные правила-линтера (see `eslint-plugin-rules`)
|
package/package.json
CHANGED
|
@@ -1,50 +1,43 @@
|
|
|
1
1
|
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
"
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
"type": "git",
|
|
44
|
-
"url": "git+https://github.com/Vollmond148259/power-linter.git"
|
|
45
|
-
},
|
|
46
|
-
"bugs": {
|
|
47
|
-
"url": "https://github.com/Vollmond148259/power-linter/issues"
|
|
48
|
-
},
|
|
49
|
-
"homepage": "https://github.com/Vollmond148259/power-linter#readme"
|
|
2
|
+
"name": "power-linter",
|
|
3
|
+
"version": "1.0.2",
|
|
4
|
+
"description": "power-linter — линтер (ESLint) и форматтер (Prettier) с расширяемой архитектурой для произвольных правил.",
|
|
5
|
+
"main": "src/index.js",
|
|
6
|
+
"author": "",
|
|
7
|
+
"license": "MIT",
|
|
8
|
+
"keywords": [
|
|
9
|
+
"eslint",
|
|
10
|
+
"prettier",
|
|
11
|
+
"linter",
|
|
12
|
+
"config",
|
|
13
|
+
"eslint-plugin",
|
|
14
|
+
"eslint-config",
|
|
15
|
+
"eslint-rules",
|
|
16
|
+
"custom-rules",
|
|
17
|
+
"code-style"
|
|
18
|
+
],
|
|
19
|
+
"dependencies": {
|
|
20
|
+
"eslint": "^8.56.0",
|
|
21
|
+
"prettier": "^3.2.5",
|
|
22
|
+
"@babel/eslint-parser": "^7.19.1",
|
|
23
|
+
"eslint-config-airbnb": "^19.0.4",
|
|
24
|
+
"eslint-config-prettier": "^8.5.0",
|
|
25
|
+
"eslint-plugin-rules": "file:./eslint-plugin-rules-1.0.0.tgz",
|
|
26
|
+
"eslint-plugin-import": "^2.26.0",
|
|
27
|
+
"eslint-plugin-jsx-a11y": "^6.6.0",
|
|
28
|
+
"eslint-plugin-react": "^7.30.1",
|
|
29
|
+
"eslint-plugin-react-hooks": "^4.6.0"
|
|
30
|
+
},
|
|
31
|
+
"peerDependencies": {
|
|
32
|
+
"eslint": "^8.0.0"
|
|
33
|
+
},
|
|
34
|
+
"files": [
|
|
35
|
+
"src/",
|
|
36
|
+
"scripts/"
|
|
37
|
+
],
|
|
38
|
+
"scripts": {
|
|
39
|
+
"lint": "eslint . --ext .js,.jsx,.ts,.tsx --config src/eslint/config.js",
|
|
40
|
+
"format": "prettier --write .",
|
|
41
|
+
"codemod:classToFC": "node scripts/classToFC/run-codemod.js"
|
|
42
|
+
}
|
|
50
43
|
}
|
|
@@ -0,0 +1,463 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Codemod для преобразования классовых React компонентов в функциональные
|
|
3
|
+
*
|
|
4
|
+
* Условия применения:
|
|
5
|
+
* - Компонент не использует this.state
|
|
6
|
+
* - Компонент имеет от 1 до 4 методов класса
|
|
7
|
+
* - Компонент не имеет сложных lifecycle методов
|
|
8
|
+
*
|
|
9
|
+
* Использование:
|
|
10
|
+
* npm run codemod:classToFC -- <file-path>
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Проверяет, является ли класс React компонентом
|
|
15
|
+
*/
|
|
16
|
+
function isReactComponentClass(node) {
|
|
17
|
+
if (!node || (node.type !== 'ClassDeclaration' && node.type !== 'ClassExpression')) {
|
|
18
|
+
return false;
|
|
19
|
+
}
|
|
20
|
+
if (!node.superClass) {
|
|
21
|
+
return false;
|
|
22
|
+
}
|
|
23
|
+
const { superClass } = node;
|
|
24
|
+
if (superClass.type === 'Identifier') {
|
|
25
|
+
return superClass.name === 'Component' || superClass.name === 'PureComponent';
|
|
26
|
+
}
|
|
27
|
+
if (
|
|
28
|
+
superClass.type === 'MemberExpression' &&
|
|
29
|
+
superClass.object &&
|
|
30
|
+
superClass.object.type === 'Identifier' &&
|
|
31
|
+
superClass.object.name === 'React' &&
|
|
32
|
+
superClass.property &&
|
|
33
|
+
superClass.property.type === 'Identifier' &&
|
|
34
|
+
(superClass.property.name === 'Component' || superClass.property.name === 'PureComponent')
|
|
35
|
+
) {
|
|
36
|
+
return true;
|
|
37
|
+
}
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Проверяет, содержит ли узел использование this.state или this.setState
|
|
43
|
+
*/
|
|
44
|
+
function hasStateUsage(node, j) {
|
|
45
|
+
let hasState = false;
|
|
46
|
+
j(node)
|
|
47
|
+
.find(j.MemberExpression, {
|
|
48
|
+
object: { type: 'ThisExpression' },
|
|
49
|
+
property: { name: 'state' },
|
|
50
|
+
})
|
|
51
|
+
.forEach(() => {
|
|
52
|
+
hasState = true;
|
|
53
|
+
});
|
|
54
|
+
j(node)
|
|
55
|
+
.find(j.CallExpression, {
|
|
56
|
+
callee: {
|
|
57
|
+
type: 'MemberExpression',
|
|
58
|
+
object: { type: 'ThisExpression' },
|
|
59
|
+
property: { name: 'setState' },
|
|
60
|
+
},
|
|
61
|
+
})
|
|
62
|
+
.forEach(() => {
|
|
63
|
+
hasState = true;
|
|
64
|
+
});
|
|
65
|
+
return hasState;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Получает имя компонента
|
|
70
|
+
*/
|
|
71
|
+
function getComponentName(node, j, path) {
|
|
72
|
+
if (node.type === 'ClassDeclaration' && node.id) {
|
|
73
|
+
return node.id.name;
|
|
74
|
+
}
|
|
75
|
+
if (node.type === 'ClassExpression') {
|
|
76
|
+
let currentPath = path;
|
|
77
|
+
while (currentPath) {
|
|
78
|
+
const { parent } = currentPath;
|
|
79
|
+
if (parent && parent.value) {
|
|
80
|
+
if (parent.value.type === 'VariableDeclarator' && parent.value.id) {
|
|
81
|
+
return parent.value.id.name;
|
|
82
|
+
}
|
|
83
|
+
if (parent.value.type === 'ExportDefaultDeclaration') {
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
currentPath = parent;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Проверяет, является ли метод методом жизненного цикла
|
|
95
|
+
*/
|
|
96
|
+
function isLifecycleMethod(methodName) {
|
|
97
|
+
const lifecycleMethods = [
|
|
98
|
+
'componentDidMount',
|
|
99
|
+
'componentDidUpdate',
|
|
100
|
+
'componentWillUnmount',
|
|
101
|
+
'componentWillMount',
|
|
102
|
+
'componentWillReceiveProps',
|
|
103
|
+
'UNSAFE_componentWillMount',
|
|
104
|
+
'UNSAFE_componentWillReceiveProps',
|
|
105
|
+
'UNSAFE_componentWillUpdate',
|
|
106
|
+
'shouldComponentUpdate',
|
|
107
|
+
'getSnapshotBeforeUpdate',
|
|
108
|
+
];
|
|
109
|
+
return lifecycleMethods.includes(methodName) || methodName.startsWith('component');
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Преобразует методы класса в функции внутри компонента
|
|
114
|
+
*/
|
|
115
|
+
function convertMethodsToFunctions(classBody) {
|
|
116
|
+
const methods = [];
|
|
117
|
+
const lifecycleMethods = [];
|
|
118
|
+
const staticProperties = [];
|
|
119
|
+
classBody.body.forEach((member) => {
|
|
120
|
+
if (member.type === 'MethodDefinition') {
|
|
121
|
+
const methodName = member.key.name;
|
|
122
|
+
if (methodName === 'render' || methodName === 'constructor') {
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
if (isLifecycleMethod(methodName)) {
|
|
126
|
+
lifecycleMethods.push(member);
|
|
127
|
+
} else {
|
|
128
|
+
methods.push(member);
|
|
129
|
+
}
|
|
130
|
+
} else if (member.type === 'Property' || member.type === 'ClassProperty') {
|
|
131
|
+
if (member.static) {
|
|
132
|
+
staticProperties.push(member);
|
|
133
|
+
} else if (member.key && member.key.name !== 'render') {
|
|
134
|
+
const methodName = member.key.name;
|
|
135
|
+
if (isLifecycleMethod(methodName)) {
|
|
136
|
+
lifecycleMethods.push(member);
|
|
137
|
+
} else {
|
|
138
|
+
methods.push(member);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
});
|
|
143
|
+
return { methods, lifecycleMethods, staticProperties };
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Заменяет this.props на props в узле
|
|
148
|
+
*/
|
|
149
|
+
function replaceThisProps(node, j) {
|
|
150
|
+
j(node)
|
|
151
|
+
.find(j.MemberExpression, {
|
|
152
|
+
object: { type: 'ThisExpression' },
|
|
153
|
+
property: { name: 'props' },
|
|
154
|
+
})
|
|
155
|
+
.replaceWith(() => j.identifier('props'));
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Заменяет this.methodName на methodName
|
|
160
|
+
*/
|
|
161
|
+
function replaceThisMethods(node, methodNames, j) {
|
|
162
|
+
methodNames.forEach((methodName) => {
|
|
163
|
+
j(node)
|
|
164
|
+
.find(j.MemberExpression, {
|
|
165
|
+
object: { type: 'ThisExpression' },
|
|
166
|
+
property: { name: methodName },
|
|
167
|
+
})
|
|
168
|
+
.replaceWith(() => j.identifier(methodName));
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Преобразует методы жизненного цикла в хуки React
|
|
174
|
+
*/
|
|
175
|
+
function convertLifecycleMethodsToHooks(lifecycleMethods, j) {
|
|
176
|
+
const hooks = [];
|
|
177
|
+
let needsUseEffect = false;
|
|
178
|
+
lifecycleMethods.forEach((method) => {
|
|
179
|
+
const methodName = method.key.name;
|
|
180
|
+
let methodBody;
|
|
181
|
+
if (method.type === 'MethodDefinition') {
|
|
182
|
+
methodBody = method.value.body;
|
|
183
|
+
} else if (method.type === 'Property' || method.type === 'ClassProperty') {
|
|
184
|
+
methodBody = method.value.body;
|
|
185
|
+
}
|
|
186
|
+
if (!methodBody) {
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
replaceThisProps(methodBody, j);
|
|
190
|
+
if (methodName === 'componentDidMount') {
|
|
191
|
+
needsUseEffect = true;
|
|
192
|
+
hooks.push(
|
|
193
|
+
j.expressionStatement(
|
|
194
|
+
j.callExpression(j.identifier('useEffect'), [
|
|
195
|
+
j.arrowFunctionExpression([], methodBody),
|
|
196
|
+
j.arrayExpression([]),
|
|
197
|
+
])
|
|
198
|
+
)
|
|
199
|
+
);
|
|
200
|
+
} else if (methodName === 'componentWillUnmount') {
|
|
201
|
+
needsUseEffect = true;
|
|
202
|
+
let cleanupFunction;
|
|
203
|
+
if (methodBody.type === 'BlockStatement') {
|
|
204
|
+
cleanupFunction = j.arrowFunctionExpression([], methodBody);
|
|
205
|
+
} else {
|
|
206
|
+
cleanupFunction = j.arrowFunctionExpression([], j.blockStatement([j.returnStatement(methodBody)]));
|
|
207
|
+
}
|
|
208
|
+
const effectBody = j.blockStatement([j.returnStatement(cleanupFunction)]);
|
|
209
|
+
hooks.push(
|
|
210
|
+
j.expressionStatement(
|
|
211
|
+
j.callExpression(j.identifier('useEffect'), [
|
|
212
|
+
j.arrowFunctionExpression([], effectBody),
|
|
213
|
+
j.arrayExpression([]),
|
|
214
|
+
])
|
|
215
|
+
)
|
|
216
|
+
);
|
|
217
|
+
} else if (methodName === 'componentDidUpdate') {
|
|
218
|
+
needsUseEffect = true;
|
|
219
|
+
const params = method.type === 'MethodDefinition' ? method.value.params : method.value.params || [];
|
|
220
|
+
if (params.length > 0) {
|
|
221
|
+
hooks.push(
|
|
222
|
+
j.expressionStatement(
|
|
223
|
+
j.callExpression(j.identifier('useEffect'), [
|
|
224
|
+
j.arrowFunctionExpression([], methodBody),
|
|
225
|
+
j.arrayExpression([j.identifier('props')]),
|
|
226
|
+
])
|
|
227
|
+
)
|
|
228
|
+
);
|
|
229
|
+
} else {
|
|
230
|
+
hooks.push(
|
|
231
|
+
j.expressionStatement(
|
|
232
|
+
j.callExpression(j.identifier('useEffect'), [j.arrowFunctionExpression([], methodBody)])
|
|
233
|
+
)
|
|
234
|
+
);
|
|
235
|
+
}
|
|
236
|
+
} else if (methodName === 'UNSAFE_componentWillReceiveProps' || methodName === 'componentWillReceiveProps') {
|
|
237
|
+
needsUseEffect = true;
|
|
238
|
+
const params = method.type === 'MethodDefinition' ? method.value.params : method.value.params || [];
|
|
239
|
+
if (params.length > 0 && params[0].name) {
|
|
240
|
+
j(methodBody)
|
|
241
|
+
.find(j.Identifier, { name: params[0].name })
|
|
242
|
+
.replaceWith(() => j.identifier('props'));
|
|
243
|
+
}
|
|
244
|
+
hooks.push(
|
|
245
|
+
j.expressionStatement(
|
|
246
|
+
j.callExpression(j.identifier('useEffect'), [
|
|
247
|
+
j.arrowFunctionExpression([], methodBody),
|
|
248
|
+
j.arrayExpression([j.identifier('props')]),
|
|
249
|
+
])
|
|
250
|
+
)
|
|
251
|
+
);
|
|
252
|
+
}
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
return { hooks, needsUseEffect };
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Создает функциональный компонент из классового
|
|
260
|
+
*/
|
|
261
|
+
function createFunctionalComponent(classNode, j, path) {
|
|
262
|
+
const className = getComponentName(classNode, j, path);
|
|
263
|
+
const classBody = classNode.body;
|
|
264
|
+
if (hasStateUsage(classNode, j)) {
|
|
265
|
+
return null;
|
|
266
|
+
}
|
|
267
|
+
const { methods, lifecycleMethods, staticProperties } = convertMethodsToFunctions(classBody);
|
|
268
|
+
const renderMethod = classBody.body.find((member) => {
|
|
269
|
+
if (member.type === 'MethodDefinition' && member.key.name === 'render') {
|
|
270
|
+
return true;
|
|
271
|
+
}
|
|
272
|
+
return (
|
|
273
|
+
(member.type === 'Property' || member.type === 'ClassProperty') &&
|
|
274
|
+
member.key &&
|
|
275
|
+
member.key.name === 'render'
|
|
276
|
+
);
|
|
277
|
+
});
|
|
278
|
+
if (!renderMethod) {
|
|
279
|
+
return null;
|
|
280
|
+
}
|
|
281
|
+
let renderBody;
|
|
282
|
+
if (renderMethod.type === 'MethodDefinition') {
|
|
283
|
+
renderBody = renderMethod.value.body;
|
|
284
|
+
} else {
|
|
285
|
+
renderBody = renderMethod.value.body;
|
|
286
|
+
}
|
|
287
|
+
if (!renderBody) {
|
|
288
|
+
return null;
|
|
289
|
+
}
|
|
290
|
+
const { hooks, needsUseEffect } = convertLifecycleMethodsToHooks(lifecycleMethods, j);
|
|
291
|
+
const componentStatements = [];
|
|
292
|
+
const methodNames = [];
|
|
293
|
+
componentStatements.push(...hooks);
|
|
294
|
+
methods.forEach((method) => {
|
|
295
|
+
const methodName = method.key.name;
|
|
296
|
+
methodNames.push(methodName);
|
|
297
|
+
let methodBody;
|
|
298
|
+
let isArrow = false;
|
|
299
|
+
let params = [];
|
|
300
|
+
if (method.type === 'MethodDefinition') {
|
|
301
|
+
methodBody = method.value.body;
|
|
302
|
+
params = method.value.params || [];
|
|
303
|
+
} else if (method.type === 'Property' || method.type === 'ClassProperty') {
|
|
304
|
+
if (method.value.type === 'ArrowFunctionExpression') {
|
|
305
|
+
methodBody = method.value.body;
|
|
306
|
+
params = method.value.params || [];
|
|
307
|
+
isArrow = true;
|
|
308
|
+
} else {
|
|
309
|
+
methodBody = method.value.body;
|
|
310
|
+
params = method.value.params || [];
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
if (methodBody) {
|
|
314
|
+
replaceThisProps(methodBody, j);
|
|
315
|
+
replaceThisMethods(methodBody, methodNames, j);
|
|
316
|
+
if (isArrow || method.type === 'Property' || method.type === 'ClassProperty') {
|
|
317
|
+
componentStatements.push(
|
|
318
|
+
j.variableDeclaration('const', [
|
|
319
|
+
j.variableDeclarator(j.identifier(methodName), j.arrowFunctionExpression(params, methodBody)),
|
|
320
|
+
])
|
|
321
|
+
);
|
|
322
|
+
} else {
|
|
323
|
+
componentStatements.push(
|
|
324
|
+
j.variableDeclaration('const', [
|
|
325
|
+
j.variableDeclarator(j.identifier(methodName), j.functionExpression(null, params, methodBody)),
|
|
326
|
+
])
|
|
327
|
+
);
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
});
|
|
331
|
+
replaceThisProps(renderBody, j);
|
|
332
|
+
replaceThisMethods(renderBody, methodNames, j);
|
|
333
|
+
let componentBody;
|
|
334
|
+
if (renderBody.type === 'BlockStatement') {
|
|
335
|
+
componentBody = j.blockStatement([...componentStatements, ...renderBody.body]);
|
|
336
|
+
} else {
|
|
337
|
+
componentBody = j.blockStatement([...componentStatements, j.returnStatement(renderBody)]);
|
|
338
|
+
}
|
|
339
|
+
const componentParams = [j.identifier('props')];
|
|
340
|
+
const arrowFunction = j.arrowFunctionExpression(componentParams, componentBody);
|
|
341
|
+
let componentDeclaration;
|
|
342
|
+
if (className) {
|
|
343
|
+
componentDeclaration = j.variableDeclaration('const', [
|
|
344
|
+
j.variableDeclarator(j.identifier(className), arrowFunction),
|
|
345
|
+
]);
|
|
346
|
+
} else {
|
|
347
|
+
componentDeclaration = j.variableDeclaration('const', [
|
|
348
|
+
j.variableDeclarator(j.identifier('Component'), arrowFunction),
|
|
349
|
+
]);
|
|
350
|
+
}
|
|
351
|
+
const result = [componentDeclaration];
|
|
352
|
+
if (staticProperties.length > 0 && className) {
|
|
353
|
+
staticProperties.forEach((staticProp) => {
|
|
354
|
+
const propName = staticProp.key.name;
|
|
355
|
+
result.push(
|
|
356
|
+
j.expressionStatement(
|
|
357
|
+
j.assignmentExpression(
|
|
358
|
+
'=',
|
|
359
|
+
j.memberExpression(j.identifier(className), j.identifier(propName)),
|
|
360
|
+
staticProp.value
|
|
361
|
+
)
|
|
362
|
+
)
|
|
363
|
+
);
|
|
364
|
+
});
|
|
365
|
+
}
|
|
366
|
+
return { result, needsUseEffect };
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
module.exports = function transformer(file, api) {
|
|
370
|
+
const j = api.jscodeshift;
|
|
371
|
+
const root = j(file.source);
|
|
372
|
+
let hasTransformations = false;
|
|
373
|
+
let needsUseEffect = false;
|
|
374
|
+
root.find(j.ClassDeclaration).forEach((path) => {
|
|
375
|
+
if (isReactComponentClass(path.node)) {
|
|
376
|
+
const transformed = createFunctionalComponent(path.node, j, path);
|
|
377
|
+
if (transformed) {
|
|
378
|
+
j(path).replaceWith(...transformed.result);
|
|
379
|
+
if (transformed.needsUseEffect) {
|
|
380
|
+
needsUseEffect = true;
|
|
381
|
+
}
|
|
382
|
+
hasTransformations = true;
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
});
|
|
386
|
+
root.find(j.ClassExpression).forEach((path) => {
|
|
387
|
+
if (isReactComponentClass(path.node)) {
|
|
388
|
+
const transformed = createFunctionalComponent(path.node, j, path);
|
|
389
|
+
if (transformed) {
|
|
390
|
+
const parent = path.parent.value;
|
|
391
|
+
if (parent && parent.type === 'ReturnStatement') {
|
|
392
|
+
const arrowFunction = transformed.result[0].declarations[0].init;
|
|
393
|
+
j(path.parent).replaceWith(j.returnStatement(arrowFunction));
|
|
394
|
+
} else if (parent && parent.type === 'VariableDeclarator') {
|
|
395
|
+
j(path.parent.parent).replaceWith(...transformed.result);
|
|
396
|
+
} else {
|
|
397
|
+
j(path).replaceWith(...transformed.result);
|
|
398
|
+
}
|
|
399
|
+
if (transformed.needsUseEffect) {
|
|
400
|
+
needsUseEffect = true;
|
|
401
|
+
}
|
|
402
|
+
hasTransformations = true;
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
});
|
|
406
|
+
root.find(j.ImportDeclaration, {
|
|
407
|
+
source: { value: 'react' },
|
|
408
|
+
}).forEach((path) => {
|
|
409
|
+
const specifiers = path.value.specifiers || [];
|
|
410
|
+
const componentSpecifier = specifiers.find(
|
|
411
|
+
(s) => s.type === 'ImportSpecifier' && s.imported.name === 'Component'
|
|
412
|
+
);
|
|
413
|
+
const useEffectSpecifier = specifiers.find(
|
|
414
|
+
(s) => s.type === 'ImportSpecifier' && s.imported.name === 'useEffect'
|
|
415
|
+
);
|
|
416
|
+
if (needsUseEffect && !useEffectSpecifier) {
|
|
417
|
+
const newSpecifier = j.importSpecifier(j.identifier('useEffect'));
|
|
418
|
+
if (specifiers.length === 0) {
|
|
419
|
+
path.value.specifiers = [newSpecifier];
|
|
420
|
+
} else {
|
|
421
|
+
path.value.specifiers.push(newSpecifier);
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
if (componentSpecifier) {
|
|
425
|
+
const componentName = componentSpecifier.local.name;
|
|
426
|
+
const hasOtherUsage = root.find(j.Identifier, { name: componentName }).some((p) => {
|
|
427
|
+
const parent = p.parent.value;
|
|
428
|
+
return (
|
|
429
|
+
parent.type !== 'ImportSpecifier' ||
|
|
430
|
+
(parent.type === 'ImportSpecifier' && parent.imported.name !== 'Component')
|
|
431
|
+
);
|
|
432
|
+
});
|
|
433
|
+
if (!hasOtherUsage) {
|
|
434
|
+
const newSpecifiers = specifiers.filter((s) => s !== componentSpecifier);
|
|
435
|
+
if (newSpecifiers.length === 0) {
|
|
436
|
+
const hasDefault = specifiers.some(
|
|
437
|
+
(s) => s.type === 'ImportDefaultSpecifier' || s.type === 'ImportNamespaceSpecifier'
|
|
438
|
+
);
|
|
439
|
+
if (!hasDefault) {
|
|
440
|
+
j(path).remove();
|
|
441
|
+
} else {
|
|
442
|
+
path.value.specifiers = newSpecifiers;
|
|
443
|
+
}
|
|
444
|
+
} else {
|
|
445
|
+
path.value.specifiers = newSpecifiers;
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
});
|
|
450
|
+
if (needsUseEffect) {
|
|
451
|
+
const reactImports = root.find(j.ImportDeclaration, {
|
|
452
|
+
source: { value: 'react' },
|
|
453
|
+
});
|
|
454
|
+
if (reactImports.length === 0) {
|
|
455
|
+
const useEffectImport = j.importDeclaration(
|
|
456
|
+
[j.importSpecifier(j.identifier('useEffect'))],
|
|
457
|
+
j.literal('react')
|
|
458
|
+
);
|
|
459
|
+
root.find(j.Program).get('body', 0).insertBefore(useEffectImport);
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
return hasTransformations ? root.toSource({ quote: 'single', trailingComma: true }) : null;
|
|
463
|
+
};
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Обертка для запуска codemod скрипта
|
|
5
|
+
*
|
|
6
|
+
* Использование:
|
|
7
|
+
* npm run codemod:classToFC -- <file-path>
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const { execSync } = require('child_process');
|
|
11
|
+
const path = require('path');
|
|
12
|
+
const fs = require('fs');
|
|
13
|
+
|
|
14
|
+
const filePath = process.argv[2];
|
|
15
|
+
if (!filePath) {
|
|
16
|
+
console.error('Ошибка: Укажите путь к файлу для преобразования');
|
|
17
|
+
console.error('Использование: npm run codemod:classToFC -- <file-path>');
|
|
18
|
+
process.exit(1);
|
|
19
|
+
}
|
|
20
|
+
// Определяем корень проекта: если запускаемся из power-linter, используем родительскую директорию
|
|
21
|
+
const currentDir = process.cwd();
|
|
22
|
+
const isPowerLinterDir = currentDir.endsWith('power-linter') || path.basename(currentDir) === 'power-linter';
|
|
23
|
+
const projectRoot = isPowerLinterDir ? path.join(currentDir, '..') : currentDir;
|
|
24
|
+
const absolutePath = path.isAbsolute(filePath) ? filePath : path.join(projectRoot, filePath);
|
|
25
|
+
if (!fs.existsSync(absolutePath)) {
|
|
26
|
+
console.error(`Ошибка: Файл не найден: ${absolutePath}`);
|
|
27
|
+
process.exit(1);
|
|
28
|
+
}
|
|
29
|
+
const ext = path.extname(absolutePath);
|
|
30
|
+
if (!['.js', '.jsx', '.ts', '.tsx'].includes(ext)) {
|
|
31
|
+
console.error(`Ошибка: Файл должен иметь расширение .js, .jsx, .ts или .tsx`);
|
|
32
|
+
process.exit(1);
|
|
33
|
+
}
|
|
34
|
+
const codemodPath = path.join(__dirname, 'class-to-functional.js');
|
|
35
|
+
// eslint-disable-next-line no-console
|
|
36
|
+
console.log(`Преобразование файла: ${filePath}`);
|
|
37
|
+
// eslint-disable-next-line no-console
|
|
38
|
+
console.log(`Использование codemod: ${codemodPath}`);
|
|
39
|
+
try {
|
|
40
|
+
const command = `npx jscodeshift -t "${codemodPath}" "${absolutePath}"`;
|
|
41
|
+
// eslint-disable-next-line no-console
|
|
42
|
+
console.log(`Выполнение: ${command}`);
|
|
43
|
+
execSync(command, {
|
|
44
|
+
stdio: 'inherit',
|
|
45
|
+
cwd: projectRoot,
|
|
46
|
+
});
|
|
47
|
+
// eslint-disable-next-line no-console
|
|
48
|
+
console.log('\n✓ Преобразование завершено успешно!');
|
|
49
|
+
// eslint-disable-next-line no-console
|
|
50
|
+
console.log('Проверьте файл и при необходимости внесите ручные исправления.');
|
|
51
|
+
} catch (error) {
|
|
52
|
+
console.error('\n✗ Ошибка при выполнении преобразования:', error.message);
|
|
53
|
+
process.exit(1);
|
|
54
|
+
}
|