power-linter 0.1.0
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 +71 -0
- package/package.json +38 -0
- package/scripts/addDataTestId/add-data-testid.js +186 -0
- package/scripts/addDataTestId/run-codemod.js +78 -0
- package/scripts/classToFC/class-to-functional.js +519 -0
- package/scripts/classToFC/run-codemod.js +58 -0
- package/src/eslint/config.js +73 -0
- package/src/index.js +7 -0
- package/src/prettier/config.js +8 -0
package/README.md
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# power-linter
|
|
2
|
+
|
|
3
|
+
power-linter — линтер (ESLint) и форматтер (Prettier) с расширяемой архитектурой для произвольных правил и скриптов.
|
|
4
|
+
|
|
5
|
+
- конфиги eslint и prettier из коробки
|
|
6
|
+
- кастомные правила-линтера (see `eslint-plugin-power-esrules`)
|
|
7
|
+
|
|
8
|
+
## Использование
|
|
9
|
+
|
|
10
|
+
1. Установка:
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
npm install power-linter --save-dev
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
2. Добавьте экспорт конфигурации eslint
|
|
17
|
+
|
|
18
|
+
```js
|
|
19
|
+
// .eslintrc.js
|
|
20
|
+
module.exports = require('power-linter/src/eslint/config');
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
3. Добавьте экспорт конфигурации Prettier
|
|
24
|
+
|
|
25
|
+
```js
|
|
26
|
+
// .prettierrc.js
|
|
27
|
+
module.exports = require('power-linter/src/prettier/config');
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
4. Для кастомных правил добавляйте файлы в `eslint-plugin-power-esrules`
|
|
31
|
+
|
|
32
|
+
5. Добавьте в корневой каталог проекта настройки vscode в каталоге .vscode создайте файл settings.json и добавьте в него
|
|
33
|
+
|
|
34
|
+
`{
|
|
35
|
+
"eslint.enable": true,
|
|
36
|
+
"eslint.validate": [
|
|
37
|
+
"javascript",
|
|
38
|
+
"javascriptreact",
|
|
39
|
+
"typescript",
|
|
40
|
+
"typescriptreact"
|
|
41
|
+
],
|
|
42
|
+
"eslint.run": "onType",
|
|
43
|
+
"editor.codeActionsOnSave": {
|
|
44
|
+
"source.fixAll.eslint": "explicit"
|
|
45
|
+
},
|
|
46
|
+
"editor.formatOnSave": true,
|
|
47
|
+
"[javascript]": {
|
|
48
|
+
"editor.defaultFormatter": "dbaeumer.vscode-eslint",
|
|
49
|
+
"editor.formatOnSave": true
|
|
50
|
+
},
|
|
51
|
+
"[javascriptreact]": {
|
|
52
|
+
"editor.defaultFormatter": "dbaeumer.vscode-eslint",
|
|
53
|
+
"editor.formatOnSave": true
|
|
54
|
+
},
|
|
55
|
+
"[typescript]": {
|
|
56
|
+
"editor.defaultFormatter": "dbaeumer.vscode-eslint",
|
|
57
|
+
"editor.formatOnSave": true
|
|
58
|
+
},
|
|
59
|
+
"[typescriptreact]": {
|
|
60
|
+
"editor.defaultFormatter": "dbaeumer.vscode-eslint",
|
|
61
|
+
"editor.formatOnSave": true
|
|
62
|
+
},
|
|
63
|
+
"eslint.lintTask.enable": true,
|
|
64
|
+
"eslint.workingDirectories": [
|
|
65
|
+
{
|
|
66
|
+
"mode": "auto"
|
|
67
|
+
}
|
|
68
|
+
]
|
|
69
|
+
}`
|
|
70
|
+
|
|
71
|
+
---
|
package/package.json
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "power-linter",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "power-linter — линтер (ESLint) и форматтер (Prettier) с расширяемой архитектурой для произвольных правил и скриптов.",
|
|
5
|
+
"main": "src/index.js",
|
|
6
|
+
"author": "vollmond148",
|
|
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
|
+
"@babel/eslint-parser": "^7.19.1",
|
|
21
|
+
"eslint": "^8.56.0",
|
|
22
|
+
"eslint-config-airbnb": "^19.0.4",
|
|
23
|
+
"eslint-config-prettier": "^8.5.0",
|
|
24
|
+
"eslint-plugin-import": "^2.26.0",
|
|
25
|
+
"eslint-plugin-jsx-a11y": "^6.6.0",
|
|
26
|
+
"eslint-plugin-power-esrules": "^0.1.4",
|
|
27
|
+
"eslint-plugin-react": "^7.30.1",
|
|
28
|
+
"eslint-plugin-react-hooks": "^4.6.0",
|
|
29
|
+
"prettier": "^3.2.5"
|
|
30
|
+
},
|
|
31
|
+
"peerDependencies": {
|
|
32
|
+
"eslint": "^8.0.0"
|
|
33
|
+
},
|
|
34
|
+
"files": [
|
|
35
|
+
"src/",
|
|
36
|
+
"scripts/"
|
|
37
|
+
]
|
|
38
|
+
}
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
/* eslint-disable max-len */
|
|
2
|
+
/**
|
|
3
|
+
* Codemod: Добавляет уникальный data-testid только к внешнему визуальному JSX элементу.
|
|
4
|
+
* Если верхний узел — Fragment, спускается к первому реальному JSXElement.
|
|
5
|
+
*
|
|
6
|
+
* Использование через power-linter:
|
|
7
|
+
* node node_modules/power-linter/scripts/addDataTestId/run-codemod.js <path>
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
// const fs = require('fs');
|
|
11
|
+
const path = require("path");
|
|
12
|
+
const crypto = require("crypto");
|
|
13
|
+
|
|
14
|
+
module.exports.parser = "tsx";
|
|
15
|
+
|
|
16
|
+
// const DATA_FILE_PATH = path.resolve('./scripts/addDataId/data-testids.json');
|
|
17
|
+
const IGNORED_IMPORTED_COMPONENTS = [
|
|
18
|
+
"div",
|
|
19
|
+
"span",
|
|
20
|
+
"SvgRoot",
|
|
21
|
+
"TooltipContentRoot",
|
|
22
|
+
"Svg",
|
|
23
|
+
"Field",
|
|
24
|
+
];
|
|
25
|
+
const COMPONENTS_FOR_TEST_ID_PROP = ["Field"];
|
|
26
|
+
|
|
27
|
+
function md5short(value) {
|
|
28
|
+
return crypto.createHash("md5").update(value).digest("hex").slice(0, 6);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Имя JSX: Identifier / MemberExpression / NamespacedName
|
|
32
|
+
function getJSXName(jsxName) {
|
|
33
|
+
if (!jsxName) return null;
|
|
34
|
+
switch (jsxName.type) {
|
|
35
|
+
case "JSXIdentifier":
|
|
36
|
+
return jsxName.name || null;
|
|
37
|
+
case "JSXMemberExpression":
|
|
38
|
+
// Берем правую часть: UI.Button -> Button
|
|
39
|
+
return getJSXName(jsxName.property);
|
|
40
|
+
case "JSXNamespacedName":
|
|
41
|
+
return `${jsxName.namespace?.name || "ns"}:${jsxName.name?.name || "Name"}`;
|
|
42
|
+
default:
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Проверка наличия родительского JSX узла (Element или Fragment)
|
|
48
|
+
function hasJSXAncestor(pathNode) {
|
|
49
|
+
let p = pathNode.parentPath;
|
|
50
|
+
while (p) {
|
|
51
|
+
const t = p.node && p.node.type;
|
|
52
|
+
if (t === "JSXElement" || t === "JSXFragment") return true;
|
|
53
|
+
p = p.parentPath;
|
|
54
|
+
}
|
|
55
|
+
return false;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Спуститься от Fragment к первому рендеримому JSXElement
|
|
59
|
+
function descendToFirstRenderable(j, node, root) {
|
|
60
|
+
// node: JSXElement (в т.ч. React.Fragment) или JSXFragment
|
|
61
|
+
if (!node) return null;
|
|
62
|
+
|
|
63
|
+
// 1) Если это JSXFragment — обходим детей
|
|
64
|
+
if (node.type === "JSXFragment") {
|
|
65
|
+
for (const ch of node.children || []) {
|
|
66
|
+
if (ch.type === "JSXElement") {
|
|
67
|
+
const resolved = descendToFirstRenderable(j, ch, root);
|
|
68
|
+
if (resolved) return resolved;
|
|
69
|
+
}
|
|
70
|
+
// Пропускаем JSXText/JSXExpressionContainer и т.п.
|
|
71
|
+
}
|
|
72
|
+
return null; // нет рендеримого элемента
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// 2) Если это JSXElement с именем Fragment (включая React.Fragment)
|
|
76
|
+
if (node.type === "JSXElement") {
|
|
77
|
+
const nm = getJSXName(node.openingElement?.name);
|
|
78
|
+
if (nm === "Fragment" || checkIsImported(root, j, nm)) {
|
|
79
|
+
// Это <Fragment>...</Fragment>
|
|
80
|
+
for (const ch of node.children || []) {
|
|
81
|
+
if (ch.type === "JSXElement") {
|
|
82
|
+
const resolved = descendToFirstRenderable(j, ch, root);
|
|
83
|
+
if (resolved) return resolved;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
// Иначе это реальный рендеримый элемент
|
|
89
|
+
return node;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function hasLocatorAttr(openingEl) {
|
|
96
|
+
return (openingEl.attributes || []).some((attr) => {
|
|
97
|
+
if (!attr || attr.type !== "JSXAttribute" || !attr.name) return false;
|
|
98
|
+
const n = attr.name.name;
|
|
99
|
+
return n === "data-testid" || n === "dataTestID";
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function checkIsImported(root, j, name) {
|
|
104
|
+
// Для обычных html-тегов или 'Root' — не проверяем импорт (считаем всегда "true")
|
|
105
|
+
if (!name || IGNORED_IMPORTED_COMPONENTS.includes(name)) {
|
|
106
|
+
return false;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Найдём все import и require декларации
|
|
110
|
+
const imports = root.find(j.ImportDeclaration);
|
|
111
|
+
// Проверим, импортируется ли нужное имя (или как дефолт, или как имя)
|
|
112
|
+
const isImported = imports.some((path) =>
|
|
113
|
+
path.node.specifiers.some(
|
|
114
|
+
(spec) =>
|
|
115
|
+
(spec.type === "ImportDefaultSpecifier" ||
|
|
116
|
+
spec.type === "ImportSpecifier") &&
|
|
117
|
+
spec.local &&
|
|
118
|
+
spec.local.name === name
|
|
119
|
+
)
|
|
120
|
+
);
|
|
121
|
+
|
|
122
|
+
return isImported;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
module.exports = function transformer(fileInfo, api) {
|
|
126
|
+
const j = api.jscodeshift;
|
|
127
|
+
const root = j(fileInfo.source);
|
|
128
|
+
|
|
129
|
+
const absPath = fileInfo.path;
|
|
130
|
+
const relPath = path.relative(process.cwd(), absPath);
|
|
131
|
+
const fileBaseName = path.basename(relPath, path.extname(relPath));
|
|
132
|
+
|
|
133
|
+
// const registry = {};
|
|
134
|
+
// if (fs.existsSync(DATA_FILE_PATH)) {
|
|
135
|
+
// try {
|
|
136
|
+
// registry = JSON.parse(fs.readFileSync(DATA_FILE_PATH, 'utf8'));
|
|
137
|
+
// } catch {
|
|
138
|
+
// registry = {};
|
|
139
|
+
// }
|
|
140
|
+
// }
|
|
141
|
+
// Ищем «верхние» JSX узлы (тут можно не искать верхние узлы тогда будет сильно больше элементов)
|
|
142
|
+
const topLevelJSX = root
|
|
143
|
+
.find(j.JSXElement)
|
|
144
|
+
.filter((p) => !hasJSXAncestor(p))
|
|
145
|
+
.nodes();
|
|
146
|
+
|
|
147
|
+
// Переберм массив элементов
|
|
148
|
+
topLevelJSX.forEach((candidate) => {
|
|
149
|
+
// Если фрагмент или Fragment — спустимся до первого реального элемента
|
|
150
|
+
const realRoot = descendToFirstRenderable(j, candidate, root);
|
|
151
|
+
if (!realRoot || !realRoot.openingElement) {
|
|
152
|
+
// Нечего помечать
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const opening = realRoot.openingElement;
|
|
157
|
+
// Имя компонента/тега (div / Button / UI.Button -> Button)
|
|
158
|
+
const name = getJSXName(opening.name);
|
|
159
|
+
// Проверяем, импортируется ли этот компонент в файле
|
|
160
|
+
const isImported = checkIsImported(root, j, name);
|
|
161
|
+
const isInsertProp = COMPONENTS_FOR_TEST_ID_PROP.includes(name);
|
|
162
|
+
|
|
163
|
+
if (!isImported && !hasLocatorAttr(opening)) {
|
|
164
|
+
// Собираем детерминированный короткий хеш: путь + имя
|
|
165
|
+
const uniqueId = `${fileBaseName}_${name}_${md5short(`${relPath}::${name}`)}`;
|
|
166
|
+
|
|
167
|
+
// Вешаем data-testid
|
|
168
|
+
opening.attributes = opening.attributes || [];
|
|
169
|
+
const testIDName = isInsertProp ? "dataTestID" : "data-testid";
|
|
170
|
+
opening.attributes.push(
|
|
171
|
+
j.jsxAttribute(j.jsxIdentifier(testIDName), j.literal(uniqueId))
|
|
172
|
+
);
|
|
173
|
+
|
|
174
|
+
// Обновляем реестр: храним уникальный список ID на файл
|
|
175
|
+
// const prev = Array.isArray(registry[relPath]) ? registry[relPath] : [];
|
|
176
|
+
// const nextSet = new Set(prev);
|
|
177
|
+
// nextSet.add(uniqueId);
|
|
178
|
+
// registry[relPath] = Array.from(nextSet);
|
|
179
|
+
}
|
|
180
|
+
});
|
|
181
|
+
// Пишем реестр один раз на файл
|
|
182
|
+
// для записи в json нужно раскоментировать fs , DATA_FILE_PATH, логику заполенения и обновления registry
|
|
183
|
+
// fs.writeFileSync(DATA_FILE_PATH, JSON.stringify(registry, null, 2), 'utf8');
|
|
184
|
+
|
|
185
|
+
return root.toSource({ quote: "single" });
|
|
186
|
+
};
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Обертка для запуска codemod скрипта add-Data-testid.js
|
|
5
|
+
*
|
|
6
|
+
* Использование:
|
|
7
|
+
* node node_modules/power-linter/scripts/addDataTestId/run-codemod.js <path-or-directory>
|
|
8
|
+
*
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const { execSync } = require("child_process");
|
|
12
|
+
const path = require("path");
|
|
13
|
+
const fs = require("fs");
|
|
14
|
+
|
|
15
|
+
const targetPath = process.argv[2];
|
|
16
|
+
if (!targetPath) {
|
|
17
|
+
console.error(
|
|
18
|
+
"Ошибка: Укажите путь к файлу или директории для преобразования"
|
|
19
|
+
);
|
|
20
|
+
console.error(
|
|
21
|
+
"Использование: node node_modules/power-linter/scripts/addDataTestId/run-codemod.js <path-or-directory>"
|
|
22
|
+
);
|
|
23
|
+
process.exit(1);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Определяем корень проекта
|
|
27
|
+
const currentDir = process.cwd();
|
|
28
|
+
const isPowerLinterDir =
|
|
29
|
+
currentDir.endsWith("power-linter") ||
|
|
30
|
+
path.basename(currentDir) === "power-linter";
|
|
31
|
+
const projectRoot = isPowerLinterDir ? path.join(currentDir, "..") : currentDir;
|
|
32
|
+
const absolutePath = path.isAbsolute(targetPath)
|
|
33
|
+
? targetPath
|
|
34
|
+
: path.join(projectRoot, targetPath);
|
|
35
|
+
|
|
36
|
+
if (!fs.existsSync(absolutePath)) {
|
|
37
|
+
console.error(`Ошибка: Путь не найден: ${absolutePath}`);
|
|
38
|
+
process.exit(1);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const codemodPath = path.join(__dirname, "add-data-testid.js");
|
|
42
|
+
const stats = fs.statSync(absolutePath);
|
|
43
|
+
const isDirectory = stats.isDirectory();
|
|
44
|
+
|
|
45
|
+
// eslint-disable-next-line no-console
|
|
46
|
+
console.log(`Преобразование: ${targetPath}`);
|
|
47
|
+
// eslint-disable-next-line no-console
|
|
48
|
+
console.log(`Использование codemod: ${codemodPath}`);
|
|
49
|
+
|
|
50
|
+
try {
|
|
51
|
+
let command;
|
|
52
|
+
if (isDirectory) {
|
|
53
|
+
// Для директории используем те же параметры, что и в оригинальном скрипте
|
|
54
|
+
command = `npx jscodeshift -t "${codemodPath}" "${absolutePath}" --extensions=js,jsx,ts,tsx --parser=tsx`;
|
|
55
|
+
} else {
|
|
56
|
+
// Для одного файла
|
|
57
|
+
const ext = path.extname(absolutePath);
|
|
58
|
+
if (![".js", ".jsx", ".ts", ".tsx"].includes(ext)) {
|
|
59
|
+
console.error(
|
|
60
|
+
`Ошибка: Файл должен иметь расширение .js, .jsx, .ts или .tsx`
|
|
61
|
+
);
|
|
62
|
+
process.exit(1);
|
|
63
|
+
}
|
|
64
|
+
command = `npx jscodeshift -t "${codemodPath}" "${absolutePath}" --parser=tsx`;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// eslint-disable-next-line no-console
|
|
68
|
+
console.log(`Выполнение: ${command}`);
|
|
69
|
+
execSync(command, {
|
|
70
|
+
stdio: "inherit",
|
|
71
|
+
cwd: projectRoot,
|
|
72
|
+
});
|
|
73
|
+
// eslint-disable-next-line no-console
|
|
74
|
+
console.log("\n✓ Преобразование завершено успешно!");
|
|
75
|
+
} catch (error) {
|
|
76
|
+
console.error("\n✗ Ошибка при выполнении преобразования:", error.message);
|
|
77
|
+
process.exit(1);
|
|
78
|
+
}
|
|
@@ -0,0 +1,519 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Codemod для преобразования классовых React компонентов в функциональные
|
|
3
|
+
*
|
|
4
|
+
* Условия применения:
|
|
5
|
+
* - Компонент не использует this.state
|
|
6
|
+
* - Компонент имеет от 1 до 4 методов класса
|
|
7
|
+
* - Компонент не имеет сложных lifecycle методов
|
|
8
|
+
*
|
|
9
|
+
* Использование:
|
|
10
|
+
* node node_modules/power-linter/scripts/classToFC/run-codemod.js <file-path>
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Проверяет, является ли класс React компонентом
|
|
15
|
+
*/
|
|
16
|
+
function isReactComponentClass(node) {
|
|
17
|
+
if (
|
|
18
|
+
!node ||
|
|
19
|
+
(node.type !== "ClassDeclaration" && node.type !== "ClassExpression")
|
|
20
|
+
) {
|
|
21
|
+
return false;
|
|
22
|
+
}
|
|
23
|
+
if (!node.superClass) {
|
|
24
|
+
return false;
|
|
25
|
+
}
|
|
26
|
+
const { superClass } = node;
|
|
27
|
+
if (superClass.type === "Identifier") {
|
|
28
|
+
return (
|
|
29
|
+
superClass.name === "Component" || superClass.name === "PureComponent"
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
if (
|
|
33
|
+
superClass.type === "MemberExpression" &&
|
|
34
|
+
superClass.object &&
|
|
35
|
+
superClass.object.type === "Identifier" &&
|
|
36
|
+
superClass.object.name === "React" &&
|
|
37
|
+
superClass.property &&
|
|
38
|
+
superClass.property.type === "Identifier" &&
|
|
39
|
+
(superClass.property.name === "Component" ||
|
|
40
|
+
superClass.property.name === "PureComponent")
|
|
41
|
+
) {
|
|
42
|
+
return true;
|
|
43
|
+
}
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Проверяет, содержит ли узел использование this.state или this.setState
|
|
49
|
+
*/
|
|
50
|
+
function hasStateUsage(node, j) {
|
|
51
|
+
let hasState = false;
|
|
52
|
+
j(node)
|
|
53
|
+
.find(j.MemberExpression, {
|
|
54
|
+
object: { type: "ThisExpression" },
|
|
55
|
+
property: { name: "state" },
|
|
56
|
+
})
|
|
57
|
+
.forEach(() => {
|
|
58
|
+
hasState = true;
|
|
59
|
+
});
|
|
60
|
+
j(node)
|
|
61
|
+
.find(j.CallExpression, {
|
|
62
|
+
callee: {
|
|
63
|
+
type: "MemberExpression",
|
|
64
|
+
object: { type: "ThisExpression" },
|
|
65
|
+
property: { name: "setState" },
|
|
66
|
+
},
|
|
67
|
+
})
|
|
68
|
+
.forEach(() => {
|
|
69
|
+
hasState = true;
|
|
70
|
+
});
|
|
71
|
+
return hasState;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Получает имя компонента
|
|
76
|
+
*/
|
|
77
|
+
function getComponentName(node, j, path) {
|
|
78
|
+
if (node.type === "ClassDeclaration" && node.id) {
|
|
79
|
+
return node.id.name;
|
|
80
|
+
}
|
|
81
|
+
if (node.type === "ClassExpression") {
|
|
82
|
+
let currentPath = path;
|
|
83
|
+
while (currentPath) {
|
|
84
|
+
const { parent } = currentPath;
|
|
85
|
+
if (parent && parent.value) {
|
|
86
|
+
if (parent.value.type === "VariableDeclarator" && parent.value.id) {
|
|
87
|
+
return parent.value.id.name;
|
|
88
|
+
}
|
|
89
|
+
if (parent.value.type === "ExportDefaultDeclaration") {
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
currentPath = parent;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Проверяет, является ли метод методом жизненного цикла
|
|
101
|
+
*/
|
|
102
|
+
function isLifecycleMethod(methodName) {
|
|
103
|
+
const lifecycleMethods = [
|
|
104
|
+
"componentDidMount",
|
|
105
|
+
"componentDidUpdate",
|
|
106
|
+
"componentWillUnmount",
|
|
107
|
+
"componentWillMount",
|
|
108
|
+
"componentWillReceiveProps",
|
|
109
|
+
"UNSAFE_componentWillMount",
|
|
110
|
+
"UNSAFE_componentWillReceiveProps",
|
|
111
|
+
"UNSAFE_componentWillUpdate",
|
|
112
|
+
"shouldComponentUpdate",
|
|
113
|
+
"getSnapshotBeforeUpdate",
|
|
114
|
+
];
|
|
115
|
+
return (
|
|
116
|
+
lifecycleMethods.includes(methodName) || methodName.startsWith("component")
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Преобразует методы класса в функции внутри компонента
|
|
122
|
+
*/
|
|
123
|
+
function convertMethodsToFunctions(classBody) {
|
|
124
|
+
const methods = [];
|
|
125
|
+
const lifecycleMethods = [];
|
|
126
|
+
const staticProperties = [];
|
|
127
|
+
classBody.body.forEach((member) => {
|
|
128
|
+
if (member.type === "MethodDefinition") {
|
|
129
|
+
const methodName = member.key.name;
|
|
130
|
+
if (methodName === "render" || methodName === "constructor") {
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
if (isLifecycleMethod(methodName)) {
|
|
134
|
+
lifecycleMethods.push(member);
|
|
135
|
+
} else {
|
|
136
|
+
methods.push(member);
|
|
137
|
+
}
|
|
138
|
+
} else if (member.type === "Property" || member.type === "ClassProperty") {
|
|
139
|
+
if (member.static) {
|
|
140
|
+
staticProperties.push(member);
|
|
141
|
+
} else if (member.key && member.key.name !== "render") {
|
|
142
|
+
const methodName = member.key.name;
|
|
143
|
+
if (isLifecycleMethod(methodName)) {
|
|
144
|
+
lifecycleMethods.push(member);
|
|
145
|
+
} else {
|
|
146
|
+
methods.push(member);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
});
|
|
151
|
+
return { methods, lifecycleMethods, staticProperties };
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Заменяет this.props на props в узле
|
|
156
|
+
*/
|
|
157
|
+
function replaceThisProps(node, j) {
|
|
158
|
+
j(node)
|
|
159
|
+
.find(j.MemberExpression, {
|
|
160
|
+
object: { type: "ThisExpression" },
|
|
161
|
+
property: { name: "props" },
|
|
162
|
+
})
|
|
163
|
+
.replaceWith(() => j.identifier("props"));
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Заменяет this.methodName на methodName
|
|
168
|
+
*/
|
|
169
|
+
function replaceThisMethods(node, methodNames, j) {
|
|
170
|
+
methodNames.forEach((methodName) => {
|
|
171
|
+
j(node)
|
|
172
|
+
.find(j.MemberExpression, {
|
|
173
|
+
object: { type: "ThisExpression" },
|
|
174
|
+
property: { name: methodName },
|
|
175
|
+
})
|
|
176
|
+
.replaceWith(() => j.identifier(methodName));
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Преобразует методы жизненного цикла в хуки React
|
|
182
|
+
*/
|
|
183
|
+
function convertLifecycleMethodsToHooks(lifecycleMethods, j) {
|
|
184
|
+
const hooks = [];
|
|
185
|
+
let needsUseEffect = false;
|
|
186
|
+
lifecycleMethods.forEach((method) => {
|
|
187
|
+
const methodName = method.key.name;
|
|
188
|
+
let methodBody;
|
|
189
|
+
if (method.type === "MethodDefinition") {
|
|
190
|
+
methodBody = method.value.body;
|
|
191
|
+
} else if (method.type === "Property" || method.type === "ClassProperty") {
|
|
192
|
+
methodBody = method.value.body;
|
|
193
|
+
}
|
|
194
|
+
if (!methodBody) {
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
replaceThisProps(methodBody, j);
|
|
198
|
+
if (methodName === "componentDidMount") {
|
|
199
|
+
needsUseEffect = true;
|
|
200
|
+
hooks.push(
|
|
201
|
+
j.expressionStatement(
|
|
202
|
+
j.callExpression(j.identifier("useEffect"), [
|
|
203
|
+
j.arrowFunctionExpression([], methodBody),
|
|
204
|
+
j.arrayExpression([]),
|
|
205
|
+
])
|
|
206
|
+
)
|
|
207
|
+
);
|
|
208
|
+
} else if (methodName === "componentWillUnmount") {
|
|
209
|
+
needsUseEffect = true;
|
|
210
|
+
let cleanupFunction;
|
|
211
|
+
if (methodBody.type === "BlockStatement") {
|
|
212
|
+
cleanupFunction = j.arrowFunctionExpression([], methodBody);
|
|
213
|
+
} else {
|
|
214
|
+
cleanupFunction = j.arrowFunctionExpression(
|
|
215
|
+
[],
|
|
216
|
+
j.blockStatement([j.returnStatement(methodBody)])
|
|
217
|
+
);
|
|
218
|
+
}
|
|
219
|
+
const effectBody = j.blockStatement([j.returnStatement(cleanupFunction)]);
|
|
220
|
+
hooks.push(
|
|
221
|
+
j.expressionStatement(
|
|
222
|
+
j.callExpression(j.identifier("useEffect"), [
|
|
223
|
+
j.arrowFunctionExpression([], effectBody),
|
|
224
|
+
j.arrayExpression([]),
|
|
225
|
+
])
|
|
226
|
+
)
|
|
227
|
+
);
|
|
228
|
+
} else if (methodName === "componentDidUpdate") {
|
|
229
|
+
needsUseEffect = true;
|
|
230
|
+
const params =
|
|
231
|
+
method.type === "MethodDefinition"
|
|
232
|
+
? method.value.params
|
|
233
|
+
: method.value.params || [];
|
|
234
|
+
if (params.length > 0) {
|
|
235
|
+
hooks.push(
|
|
236
|
+
j.expressionStatement(
|
|
237
|
+
j.callExpression(j.identifier("useEffect"), [
|
|
238
|
+
j.arrowFunctionExpression([], methodBody),
|
|
239
|
+
j.arrayExpression([j.identifier("props")]),
|
|
240
|
+
])
|
|
241
|
+
)
|
|
242
|
+
);
|
|
243
|
+
} else {
|
|
244
|
+
hooks.push(
|
|
245
|
+
j.expressionStatement(
|
|
246
|
+
j.callExpression(j.identifier("useEffect"), [
|
|
247
|
+
j.arrowFunctionExpression([], methodBody),
|
|
248
|
+
])
|
|
249
|
+
)
|
|
250
|
+
);
|
|
251
|
+
}
|
|
252
|
+
} else if (
|
|
253
|
+
methodName === "UNSAFE_componentWillReceiveProps" ||
|
|
254
|
+
methodName === "componentWillReceiveProps"
|
|
255
|
+
) {
|
|
256
|
+
needsUseEffect = true;
|
|
257
|
+
const params =
|
|
258
|
+
method.type === "MethodDefinition"
|
|
259
|
+
? method.value.params
|
|
260
|
+
: method.value.params || [];
|
|
261
|
+
if (params.length > 0 && params[0].name) {
|
|
262
|
+
j(methodBody)
|
|
263
|
+
.find(j.Identifier, { name: params[0].name })
|
|
264
|
+
.replaceWith(() => j.identifier("props"));
|
|
265
|
+
}
|
|
266
|
+
hooks.push(
|
|
267
|
+
j.expressionStatement(
|
|
268
|
+
j.callExpression(j.identifier("useEffect"), [
|
|
269
|
+
j.arrowFunctionExpression([], methodBody),
|
|
270
|
+
j.arrayExpression([j.identifier("props")]),
|
|
271
|
+
])
|
|
272
|
+
)
|
|
273
|
+
);
|
|
274
|
+
}
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
return { hooks, needsUseEffect };
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* Создает функциональный компонент из классового
|
|
282
|
+
*/
|
|
283
|
+
function createFunctionalComponent(classNode, j, path) {
|
|
284
|
+
const className = getComponentName(classNode, j, path);
|
|
285
|
+
const classBody = classNode.body;
|
|
286
|
+
if (hasStateUsage(classNode, j)) {
|
|
287
|
+
return null;
|
|
288
|
+
}
|
|
289
|
+
const { methods, lifecycleMethods, staticProperties } =
|
|
290
|
+
convertMethodsToFunctions(classBody);
|
|
291
|
+
const renderMethod = classBody.body.find((member) => {
|
|
292
|
+
if (member.type === "MethodDefinition" && member.key.name === "render") {
|
|
293
|
+
return true;
|
|
294
|
+
}
|
|
295
|
+
return (
|
|
296
|
+
(member.type === "Property" || member.type === "ClassProperty") &&
|
|
297
|
+
member.key &&
|
|
298
|
+
member.key.name === "render"
|
|
299
|
+
);
|
|
300
|
+
});
|
|
301
|
+
if (!renderMethod) {
|
|
302
|
+
return null;
|
|
303
|
+
}
|
|
304
|
+
let renderBody;
|
|
305
|
+
if (renderMethod.type === "MethodDefinition") {
|
|
306
|
+
renderBody = renderMethod.value.body;
|
|
307
|
+
} else {
|
|
308
|
+
renderBody = renderMethod.value.body;
|
|
309
|
+
}
|
|
310
|
+
if (!renderBody) {
|
|
311
|
+
return null;
|
|
312
|
+
}
|
|
313
|
+
const { hooks, needsUseEffect } = convertLifecycleMethodsToHooks(
|
|
314
|
+
lifecycleMethods,
|
|
315
|
+
j
|
|
316
|
+
);
|
|
317
|
+
const componentStatements = [];
|
|
318
|
+
const methodNames = [];
|
|
319
|
+
componentStatements.push(...hooks);
|
|
320
|
+
methods.forEach((method) => {
|
|
321
|
+
const methodName = method.key.name;
|
|
322
|
+
methodNames.push(methodName);
|
|
323
|
+
let methodBody;
|
|
324
|
+
let isArrow = false;
|
|
325
|
+
let params = [];
|
|
326
|
+
if (method.type === "MethodDefinition") {
|
|
327
|
+
methodBody = method.value.body;
|
|
328
|
+
params = method.value.params || [];
|
|
329
|
+
} else if (method.type === "Property" || method.type === "ClassProperty") {
|
|
330
|
+
if (method.value.type === "ArrowFunctionExpression") {
|
|
331
|
+
methodBody = method.value.body;
|
|
332
|
+
params = method.value.params || [];
|
|
333
|
+
isArrow = true;
|
|
334
|
+
} else {
|
|
335
|
+
methodBody = method.value.body;
|
|
336
|
+
params = method.value.params || [];
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
if (methodBody) {
|
|
340
|
+
replaceThisProps(methodBody, j);
|
|
341
|
+
replaceThisMethods(methodBody, methodNames, j);
|
|
342
|
+
if (
|
|
343
|
+
isArrow ||
|
|
344
|
+
method.type === "Property" ||
|
|
345
|
+
method.type === "ClassProperty"
|
|
346
|
+
) {
|
|
347
|
+
componentStatements.push(
|
|
348
|
+
j.variableDeclaration("const", [
|
|
349
|
+
j.variableDeclarator(
|
|
350
|
+
j.identifier(methodName),
|
|
351
|
+
j.arrowFunctionExpression(params, methodBody)
|
|
352
|
+
),
|
|
353
|
+
])
|
|
354
|
+
);
|
|
355
|
+
} else {
|
|
356
|
+
componentStatements.push(
|
|
357
|
+
j.variableDeclaration("const", [
|
|
358
|
+
j.variableDeclarator(
|
|
359
|
+
j.identifier(methodName),
|
|
360
|
+
j.functionExpression(null, params, methodBody)
|
|
361
|
+
),
|
|
362
|
+
])
|
|
363
|
+
);
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
});
|
|
367
|
+
replaceThisProps(renderBody, j);
|
|
368
|
+
replaceThisMethods(renderBody, methodNames, j);
|
|
369
|
+
let componentBody;
|
|
370
|
+
if (renderBody.type === "BlockStatement") {
|
|
371
|
+
componentBody = j.blockStatement([
|
|
372
|
+
...componentStatements,
|
|
373
|
+
...renderBody.body,
|
|
374
|
+
]);
|
|
375
|
+
} else {
|
|
376
|
+
componentBody = j.blockStatement([
|
|
377
|
+
...componentStatements,
|
|
378
|
+
j.returnStatement(renderBody),
|
|
379
|
+
]);
|
|
380
|
+
}
|
|
381
|
+
const componentParams = [j.identifier("props")];
|
|
382
|
+
const arrowFunction = j.arrowFunctionExpression(
|
|
383
|
+
componentParams,
|
|
384
|
+
componentBody
|
|
385
|
+
);
|
|
386
|
+
let componentDeclaration;
|
|
387
|
+
if (className) {
|
|
388
|
+
componentDeclaration = j.variableDeclaration("const", [
|
|
389
|
+
j.variableDeclarator(j.identifier(className), arrowFunction),
|
|
390
|
+
]);
|
|
391
|
+
} else {
|
|
392
|
+
componentDeclaration = j.variableDeclaration("const", [
|
|
393
|
+
j.variableDeclarator(j.identifier("Component"), arrowFunction),
|
|
394
|
+
]);
|
|
395
|
+
}
|
|
396
|
+
const result = [componentDeclaration];
|
|
397
|
+
if (staticProperties.length > 0 && className) {
|
|
398
|
+
staticProperties.forEach((staticProp) => {
|
|
399
|
+
const propName = staticProp.key.name;
|
|
400
|
+
result.push(
|
|
401
|
+
j.expressionStatement(
|
|
402
|
+
j.assignmentExpression(
|
|
403
|
+
"=",
|
|
404
|
+
j.memberExpression(j.identifier(className), j.identifier(propName)),
|
|
405
|
+
staticProp.value
|
|
406
|
+
)
|
|
407
|
+
)
|
|
408
|
+
);
|
|
409
|
+
});
|
|
410
|
+
}
|
|
411
|
+
return { result, needsUseEffect };
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
module.exports = function transformer(file, api) {
|
|
415
|
+
const j = api.jscodeshift;
|
|
416
|
+
const root = j(file.source);
|
|
417
|
+
let hasTransformations = false;
|
|
418
|
+
let needsUseEffect = false;
|
|
419
|
+
root.find(j.ClassDeclaration).forEach((path) => {
|
|
420
|
+
if (isReactComponentClass(path.node)) {
|
|
421
|
+
const transformed = createFunctionalComponent(path.node, j, path);
|
|
422
|
+
if (transformed) {
|
|
423
|
+
j(path).replaceWith(...transformed.result);
|
|
424
|
+
if (transformed.needsUseEffect) {
|
|
425
|
+
needsUseEffect = true;
|
|
426
|
+
}
|
|
427
|
+
hasTransformations = true;
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
});
|
|
431
|
+
root.find(j.ClassExpression).forEach((path) => {
|
|
432
|
+
if (isReactComponentClass(path.node)) {
|
|
433
|
+
const transformed = createFunctionalComponent(path.node, j, path);
|
|
434
|
+
if (transformed) {
|
|
435
|
+
const parent = path.parent.value;
|
|
436
|
+
if (parent && parent.type === "ReturnStatement") {
|
|
437
|
+
const arrowFunction = transformed.result[0].declarations[0].init;
|
|
438
|
+
j(path.parent).replaceWith(j.returnStatement(arrowFunction));
|
|
439
|
+
} else if (parent && parent.type === "VariableDeclarator") {
|
|
440
|
+
j(path.parent.parent).replaceWith(...transformed.result);
|
|
441
|
+
} else {
|
|
442
|
+
j(path).replaceWith(...transformed.result);
|
|
443
|
+
}
|
|
444
|
+
if (transformed.needsUseEffect) {
|
|
445
|
+
needsUseEffect = true;
|
|
446
|
+
}
|
|
447
|
+
hasTransformations = true;
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
});
|
|
451
|
+
root
|
|
452
|
+
.find(j.ImportDeclaration, {
|
|
453
|
+
source: { value: "react" },
|
|
454
|
+
})
|
|
455
|
+
.forEach((path) => {
|
|
456
|
+
const specifiers = path.value.specifiers || [];
|
|
457
|
+
const componentSpecifier = specifiers.find(
|
|
458
|
+
(s) => s.type === "ImportSpecifier" && s.imported.name === "Component"
|
|
459
|
+
);
|
|
460
|
+
const useEffectSpecifier = specifiers.find(
|
|
461
|
+
(s) => s.type === "ImportSpecifier" && s.imported.name === "useEffect"
|
|
462
|
+
);
|
|
463
|
+
if (needsUseEffect && !useEffectSpecifier) {
|
|
464
|
+
const newSpecifier = j.importSpecifier(j.identifier("useEffect"));
|
|
465
|
+
if (specifiers.length === 0) {
|
|
466
|
+
path.value.specifiers = [newSpecifier];
|
|
467
|
+
} else {
|
|
468
|
+
path.value.specifiers.push(newSpecifier);
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
if (componentSpecifier) {
|
|
472
|
+
const componentName = componentSpecifier.local.name;
|
|
473
|
+
const hasOtherUsage = root
|
|
474
|
+
.find(j.Identifier, { name: componentName })
|
|
475
|
+
.some((p) => {
|
|
476
|
+
const parent = p.parent.value;
|
|
477
|
+
return (
|
|
478
|
+
parent.type !== "ImportSpecifier" ||
|
|
479
|
+
(parent.type === "ImportSpecifier" &&
|
|
480
|
+
parent.imported.name !== "Component")
|
|
481
|
+
);
|
|
482
|
+
});
|
|
483
|
+
if (!hasOtherUsage) {
|
|
484
|
+
const newSpecifiers = specifiers.filter(
|
|
485
|
+
(s) => s !== componentSpecifier
|
|
486
|
+
);
|
|
487
|
+
if (newSpecifiers.length === 0) {
|
|
488
|
+
const hasDefault = specifiers.some(
|
|
489
|
+
(s) =>
|
|
490
|
+
s.type === "ImportDefaultSpecifier" ||
|
|
491
|
+
s.type === "ImportNamespaceSpecifier"
|
|
492
|
+
);
|
|
493
|
+
if (!hasDefault) {
|
|
494
|
+
j(path).remove();
|
|
495
|
+
} else {
|
|
496
|
+
path.value.specifiers = newSpecifiers;
|
|
497
|
+
}
|
|
498
|
+
} else {
|
|
499
|
+
path.value.specifiers = newSpecifiers;
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
});
|
|
504
|
+
if (needsUseEffect) {
|
|
505
|
+
const reactImports = root.find(j.ImportDeclaration, {
|
|
506
|
+
source: { value: "react" },
|
|
507
|
+
});
|
|
508
|
+
if (reactImports.length === 0) {
|
|
509
|
+
const useEffectImport = j.importDeclaration(
|
|
510
|
+
[j.importSpecifier(j.identifier("useEffect"))],
|
|
511
|
+
j.literal("react")
|
|
512
|
+
);
|
|
513
|
+
root.find(j.Program).get("body", 0).insertBefore(useEffectImport);
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
return hasTransformations
|
|
517
|
+
? root.toSource({ quote: "single", trailingComma: true })
|
|
518
|
+
: null;
|
|
519
|
+
};
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Обертка для запуска codemod скрипта
|
|
5
|
+
*
|
|
6
|
+
* Использование:
|
|
7
|
+
* node node_modules/power-linter/scripts/classToFC/run-codemod.js <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 =
|
|
23
|
+
currentDir.endsWith("power-linter") ||
|
|
24
|
+
path.basename(currentDir) === "power-linter";
|
|
25
|
+
const projectRoot = isPowerLinterDir ? path.join(currentDir, "..") : currentDir;
|
|
26
|
+
const absolutePath = path.isAbsolute(filePath)
|
|
27
|
+
? filePath
|
|
28
|
+
: path.join(projectRoot, filePath);
|
|
29
|
+
if (!fs.existsSync(absolutePath)) {
|
|
30
|
+
console.error(`Ошибка: Файл не найден: ${absolutePath}`);
|
|
31
|
+
process.exit(1);
|
|
32
|
+
}
|
|
33
|
+
const ext = path.extname(absolutePath);
|
|
34
|
+
if (![".js", ".jsx", ".ts", ".tsx"].includes(ext)) {
|
|
35
|
+
console.error(`Ошибка: Файл должен иметь расширение .js, .jsx, .ts или .tsx`);
|
|
36
|
+
process.exit(1);
|
|
37
|
+
}
|
|
38
|
+
const codemodPath = path.join(__dirname, "class-to-functional.js");
|
|
39
|
+
// eslint-disable-next-line no-console
|
|
40
|
+
console.log(`Преобразование файла: ${filePath}`);
|
|
41
|
+
// eslint-disable-next-line no-console
|
|
42
|
+
console.log(`Использование codemod: ${codemodPath}`);
|
|
43
|
+
try {
|
|
44
|
+
const command = `npx jscodeshift -t "${codemodPath}" "${absolutePath}"`;
|
|
45
|
+
// eslint-disable-next-line no-console
|
|
46
|
+
console.log(`Выполнение: ${command}`);
|
|
47
|
+
execSync(command, {
|
|
48
|
+
stdio: "inherit",
|
|
49
|
+
cwd: projectRoot,
|
|
50
|
+
});
|
|
51
|
+
// eslint-disable-next-line no-console
|
|
52
|
+
console.log("\n✓ Преобразование завершено успешно!");
|
|
53
|
+
// eslint-disable-next-line no-console
|
|
54
|
+
console.log("Проверьте файл и при необходимости внесите ручные исправления.");
|
|
55
|
+
} catch (error) {
|
|
56
|
+
console.error("\n✗ Ошибка при выполнении преобразования:", error.message);
|
|
57
|
+
process.exit(1);
|
|
58
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
module.exports = {
|
|
2
|
+
root: true,
|
|
3
|
+
env: {
|
|
4
|
+
browser: true,
|
|
5
|
+
node: true,
|
|
6
|
+
es2021: true,
|
|
7
|
+
},
|
|
8
|
+
extends: [
|
|
9
|
+
"airbnb",
|
|
10
|
+
"airbnb/hooks",
|
|
11
|
+
"eslint:recommended",
|
|
12
|
+
"prettier",
|
|
13
|
+
"plugin:power-esrules/recommended",
|
|
14
|
+
],
|
|
15
|
+
plugins: ["power-esrules"],
|
|
16
|
+
parser: "@babel/eslint-parser",
|
|
17
|
+
parserOptions: {
|
|
18
|
+
requireConfigFile: false,
|
|
19
|
+
},
|
|
20
|
+
globals: {
|
|
21
|
+
document: true,
|
|
22
|
+
localStorage: true,
|
|
23
|
+
Headers: true,
|
|
24
|
+
fetch: true,
|
|
25
|
+
},
|
|
26
|
+
rules: {
|
|
27
|
+
"no-continue": 0,
|
|
28
|
+
"consistent-return": 0,
|
|
29
|
+
"no-constant-condition": 0,
|
|
30
|
+
"arrow-parens": 0,
|
|
31
|
+
"no-else-return": 0,
|
|
32
|
+
"global-require": 0,
|
|
33
|
+
"max-len": ["error", { code: 120, ignorePattern: "^import\\W.*" }],
|
|
34
|
+
"linebreak-style": 0,
|
|
35
|
+
indent: 0,
|
|
36
|
+
"prefer-template": ["warn"],
|
|
37
|
+
"no-restricted-syntax": 0,
|
|
38
|
+
"no-prototype-builtins": 0,
|
|
39
|
+
"no-param-reassign": ["error", { props: false }],
|
|
40
|
+
"comma-dangle": ["error", "only-multiline"],
|
|
41
|
+
"no-undef": 0,
|
|
42
|
+
"no-shadow": 0,
|
|
43
|
+
"no-plusplus": 0,
|
|
44
|
+
"arrow-body-style": ["error", "as-needed"],
|
|
45
|
+
"default-param-last": 0,
|
|
46
|
+
"class-methods-use-this": ["error", { enforceForClassFields: false }],
|
|
47
|
+
"no-use-before-define": [
|
|
48
|
+
"error",
|
|
49
|
+
{ functions: false, classes: true, variables: true },
|
|
50
|
+
],
|
|
51
|
+
"no-useless-escape": "warn",
|
|
52
|
+
"no-console": ["error", { allow: ["warn", "error"] }],
|
|
53
|
+
"func-names": 0,
|
|
54
|
+
"jsx-a11y/anchor-is-valid": ["warn"],
|
|
55
|
+
"jsx-a11y/no-static-element-interactions": 0,
|
|
56
|
+
"jsx-a11y/click-events-have-key-events": ["warn"],
|
|
57
|
+
"import/prefer-default-export": 0,
|
|
58
|
+
"import/extensions": ["warn", { jsx: "never" }],
|
|
59
|
+
"import/no-unresolved": 0,
|
|
60
|
+
"import/no-extraneous-dependencies": 0,
|
|
61
|
+
"react/jsx-curly-brace-presence": 0,
|
|
62
|
+
"react/jsx-filename-extension": ["warn", { extensions: [".js", ".jsx"] }],
|
|
63
|
+
"react/require-default-props": 0,
|
|
64
|
+
"react/jsx-curly-spacing": 0,
|
|
65
|
+
"react/jsx-indent": ["error", 4],
|
|
66
|
+
"react/jsx-indent-props": 0,
|
|
67
|
+
"react/forbid-prop-types": 0,
|
|
68
|
+
"react/sort-comp": 0,
|
|
69
|
+
"react/prop-types": 0,
|
|
70
|
+
"react/jsx-props-no-spreading": 0,
|
|
71
|
+
"react/jsx-no-bind": 0,
|
|
72
|
+
},
|
|
73
|
+
};
|
package/src/index.js
ADDED