@siberiacancode/eslint 2.15.3 → 2.16.1
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/dist/cjs/index.cjs +404 -22
- package/dist/cjs/index.d.cts +2 -0
- package/dist/esm/index.d.mts +2 -0
- package/dist/esm/index.mjs +400 -21
- package/package.json +4 -3
package/dist/cjs/index.cjs
CHANGED
|
@@ -28,15 +28,385 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
28
28
|
//#endregion
|
|
29
29
|
let _antfu_eslint_config = require("@antfu/eslint-config");
|
|
30
30
|
_antfu_eslint_config = __toESM(_antfu_eslint_config);
|
|
31
|
+
let _eslint_css = require("@eslint/css");
|
|
32
|
+
_eslint_css = __toESM(_eslint_css);
|
|
31
33
|
let eslint_plugin_jsx_a11y = require("eslint-plugin-jsx-a11y");
|
|
32
34
|
eslint_plugin_jsx_a11y = __toESM(eslint_plugin_jsx_a11y);
|
|
33
|
-
let
|
|
34
|
-
|
|
35
|
+
let eslint_plugin_playwright = require("eslint-plugin-playwright");
|
|
36
|
+
eslint_plugin_playwright = __toESM(eslint_plugin_playwright);
|
|
37
|
+
let node_fs = require("node:fs");
|
|
38
|
+
node_fs = __toESM(node_fs);
|
|
39
|
+
let node_path = require("node:path");
|
|
40
|
+
node_path = __toESM(node_path);
|
|
35
41
|
|
|
42
|
+
//#region src/plugin/rules/function-component-definition.ts
|
|
43
|
+
const NAMED_TEMPLATES = {
|
|
44
|
+
"function-declaration": "function {name}{typeParams}({params}){returnType} {body}",
|
|
45
|
+
"arrow-function": "{varType} {name}{typeAnnotation} = {typeParams}({params}){returnType} => {body}",
|
|
46
|
+
"function-expression": "{varType} {name}{typeAnnotation} = function{typeParams}({params}){returnType} {body}"
|
|
47
|
+
};
|
|
48
|
+
const buildFunction = (template, parts) => Object.keys(parts).reduce((acc, key) => acc.replace(new RegExp(`\\{${key}\\}`, "g"), () => parts[key] ?? ""), template);
|
|
49
|
+
const hasName = (node) => {
|
|
50
|
+
if (node.type === "FunctionDeclaration") return true;
|
|
51
|
+
return node.parent?.type === "VariableDeclarator";
|
|
52
|
+
};
|
|
53
|
+
const getName = (node) => {
|
|
54
|
+
if (node.type === "FunctionDeclaration" && node.id?.type === "Identifier") return node.id.name;
|
|
55
|
+
const parent = node.parent;
|
|
56
|
+
if ((node.type === "ArrowFunctionExpression" || node.type === "FunctionExpression") && parent?.type === "VariableDeclarator" && parent.id?.type === "Identifier") return parent.id.name;
|
|
57
|
+
};
|
|
58
|
+
const getTypeParams = (node) => {
|
|
59
|
+
return node.typeParameters;
|
|
60
|
+
};
|
|
61
|
+
const getNodeText = (sourceCode, node) => {
|
|
62
|
+
if (!node || !("range" in node) || !node.range) return void 0;
|
|
63
|
+
return sourceCode.getText(node);
|
|
64
|
+
};
|
|
65
|
+
const getParams = (sourceCode, node) => {
|
|
66
|
+
if (node.params.length === 0) return void 0;
|
|
67
|
+
const first = node.params[0];
|
|
68
|
+
const last = node.params[node.params.length - 1];
|
|
69
|
+
if (first == null || last == null || !("range" in first) || !("range" in last)) return void 0;
|
|
70
|
+
return sourceCode.getText({ range: [first.range[0], last.range[1]] });
|
|
71
|
+
};
|
|
72
|
+
const getBody = (sourceCode, node) => {
|
|
73
|
+
const body = node.body;
|
|
74
|
+
if (!("range" in body) || !body.range) return "{}";
|
|
75
|
+
const text = sourceCode.getText(body);
|
|
76
|
+
if (body.type === "BlockStatement") return text;
|
|
77
|
+
return `{\n return ${text}\n}`;
|
|
78
|
+
};
|
|
79
|
+
const getTypeAnnotation = (sourceCode, node) => {
|
|
80
|
+
if (!hasName(node) || node.type === "FunctionDeclaration") return void 0;
|
|
81
|
+
const parent = node.parent;
|
|
82
|
+
if (parent?.type !== "VariableDeclarator" || parent.id?.type !== "Identifier") return void 0;
|
|
83
|
+
const id = parent.id;
|
|
84
|
+
if (!("typeAnnotation" in id) || id.typeAnnotation === void 0 || id.typeAnnotation === null) return void 0;
|
|
85
|
+
return getNodeText(sourceCode, id.typeAnnotation);
|
|
86
|
+
};
|
|
87
|
+
const isUnfixableExport = (node) => node.type === "FunctionDeclaration" && node.parent?.type === "ExportDefaultDeclaration";
|
|
88
|
+
const isFunctionExpressionWithName = (node) => node.type === "FunctionExpression" && "id" in node && node.id !== null && node.id !== void 0;
|
|
89
|
+
const JSX_TYPES = ["JSXElement", "JSXFragment"];
|
|
90
|
+
/** Walk AST and return true if any descendant is JSX */
|
|
91
|
+
const containsJSX = (node) => {
|
|
92
|
+
if (JSX_TYPES.includes(node.type)) return true;
|
|
93
|
+
for (const key of Object.keys(node)) {
|
|
94
|
+
if (key === "parent") continue;
|
|
95
|
+
const value = node[key];
|
|
96
|
+
if (value !== null && value !== void 0 && typeof value === "object") {
|
|
97
|
+
if (Array.isArray(value)) {
|
|
98
|
+
if (value.some((child) => child !== null && child !== void 0 && typeof child === "object" && "type" in child && containsJSX(child))) return true;
|
|
99
|
+
} else if (value !== null && value !== void 0 && typeof value === "object" && "type" in value && containsJSX(value)) return true;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
return false;
|
|
103
|
+
};
|
|
104
|
+
const walk = (node, visit) => {
|
|
105
|
+
visit(node);
|
|
106
|
+
for (const key of Object.keys(node)) {
|
|
107
|
+
if (key === "parent") continue;
|
|
108
|
+
const value = node[key];
|
|
109
|
+
if (value !== null && value !== void 0 && typeof value === "object") {
|
|
110
|
+
if (Array.isArray(value)) value.forEach((child) => {
|
|
111
|
+
if (child !== null && child !== void 0 && typeof child === "object" && "type" in child) walk(child, visit);
|
|
112
|
+
});
|
|
113
|
+
else if (value !== null && value !== void 0 && typeof value === "object" && "type" in value) walk(value, visit);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
};
|
|
117
|
+
/** Collect component nodes: functions that contain JSX (and for expr/arrow, are in VariableDeclarator) */
|
|
118
|
+
const getComponentNodes = (program) => {
|
|
119
|
+
const components = /* @__PURE__ */ new Set();
|
|
120
|
+
let fileHasJSX = false;
|
|
121
|
+
walk(program, (n) => {
|
|
122
|
+
const type = n.type;
|
|
123
|
+
if (type === "JSXElement" || type === "JSXFragment") fileHasJSX = true;
|
|
124
|
+
});
|
|
125
|
+
if (!fileHasJSX) return components;
|
|
126
|
+
walk(program, (n) => {
|
|
127
|
+
if (n.type === "FunctionDeclaration" && containsJSX(n)) components.add(n);
|
|
128
|
+
const withParent = n;
|
|
129
|
+
if ((n.type === "ArrowFunctionExpression" || n.type === "FunctionExpression") && withParent.parent?.type === "VariableDeclarator" && containsJSX(n)) components.add(n);
|
|
130
|
+
});
|
|
131
|
+
return components;
|
|
132
|
+
};
|
|
133
|
+
const functionComponentDefinition = {
|
|
134
|
+
meta: {
|
|
135
|
+
type: "layout",
|
|
136
|
+
docs: { description: "Enforce a specific function type for function components" },
|
|
137
|
+
fixable: "code",
|
|
138
|
+
schema: [{
|
|
139
|
+
type: "object",
|
|
140
|
+
properties: { namedComponents: { anyOf: [{ enum: [
|
|
141
|
+
"function-declaration",
|
|
142
|
+
"arrow-function",
|
|
143
|
+
"function-expression"
|
|
144
|
+
] }, {
|
|
145
|
+
type: "array",
|
|
146
|
+
items: {
|
|
147
|
+
type: "string",
|
|
148
|
+
enum: [
|
|
149
|
+
"function-declaration",
|
|
150
|
+
"arrow-function",
|
|
151
|
+
"function-expression"
|
|
152
|
+
]
|
|
153
|
+
}
|
|
154
|
+
}] } },
|
|
155
|
+
additionalProperties: false
|
|
156
|
+
}],
|
|
157
|
+
messages: {
|
|
158
|
+
"function-declaration": "Function component is not a function declaration",
|
|
159
|
+
"function-expression": "Function component is not a function expression",
|
|
160
|
+
"arrow-function": "Function component is not an arrow function"
|
|
161
|
+
}
|
|
162
|
+
},
|
|
163
|
+
create(context) {
|
|
164
|
+
const sourceCode = context.sourceCode;
|
|
165
|
+
const options = context.options[0] ?? {};
|
|
166
|
+
const namedConfig = [].concat(options.namedComponents ?? "function-declaration");
|
|
167
|
+
let fileVarType = "var";
|
|
168
|
+
const validatePairs = [];
|
|
169
|
+
let componentNodes = /* @__PURE__ */ new Set();
|
|
170
|
+
const getFixer = (node, fixOptions) => {
|
|
171
|
+
const typeAnnotation = getTypeAnnotation(sourceCode, node);
|
|
172
|
+
if (fixOptions.type === "function-declaration" && typeAnnotation) return void 0;
|
|
173
|
+
if (isUnfixableExport(node)) return void 0;
|
|
174
|
+
if (isFunctionExpressionWithName(node)) return void 0;
|
|
175
|
+
let varType = fileVarType;
|
|
176
|
+
const parent = node.parent;
|
|
177
|
+
if ((node.type === "FunctionExpression" || node.type === "ArrowFunctionExpression") && parent?.type === "VariableDeclarator" && parent.parent?.type === "VariableDeclaration") varType = parent.parent.kind;
|
|
178
|
+
const typeParams = getNodeText(sourceCode, getTypeParams(node));
|
|
179
|
+
const params = getParams(sourceCode, node);
|
|
180
|
+
const returnType = getNodeText(sourceCode, "returnType" in node ? node.returnType : void 0);
|
|
181
|
+
const body = getBody(sourceCode, node);
|
|
182
|
+
const name = getName(node);
|
|
183
|
+
const text = buildFunction(fixOptions.template, {
|
|
184
|
+
typeAnnotation,
|
|
185
|
+
typeParams,
|
|
186
|
+
params,
|
|
187
|
+
returnType,
|
|
188
|
+
body,
|
|
189
|
+
name: name !== void 0 && name !== null && name !== "" ? name : "",
|
|
190
|
+
varType
|
|
191
|
+
});
|
|
192
|
+
return (fixer) => fixer.replaceTextRange(fixOptions.range, text);
|
|
193
|
+
};
|
|
194
|
+
const report = (node, fixOptions) => {
|
|
195
|
+
const fix = getFixer(node, fixOptions);
|
|
196
|
+
context.report({
|
|
197
|
+
node,
|
|
198
|
+
messageId: fixOptions.messageId,
|
|
199
|
+
fix
|
|
200
|
+
});
|
|
201
|
+
};
|
|
202
|
+
const validate = (node, functionType) => {
|
|
203
|
+
if (!componentNodes.has(node)) return;
|
|
204
|
+
if (node.parent?.type === "Property") return;
|
|
205
|
+
if (hasName(node) && !namedConfig.includes(functionType)) {
|
|
206
|
+
const parent = node.parent;
|
|
207
|
+
report(node, {
|
|
208
|
+
messageId: namedConfig[0],
|
|
209
|
+
type: namedConfig[0],
|
|
210
|
+
template: NAMED_TEMPLATES[namedConfig[0]],
|
|
211
|
+
range: node.type === "FunctionDeclaration" ? node.range : parent?.parent?.range ?? node.range
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
};
|
|
215
|
+
return {
|
|
216
|
+
Program(programNode) {
|
|
217
|
+
componentNodes = getComponentNodes(programNode);
|
|
218
|
+
},
|
|
219
|
+
FunctionDeclaration(node) {
|
|
220
|
+
validatePairs.push([node, "function-declaration"]);
|
|
221
|
+
},
|
|
222
|
+
ArrowFunctionExpression(node) {
|
|
223
|
+
validatePairs.push([node, "arrow-function"]);
|
|
224
|
+
},
|
|
225
|
+
FunctionExpression(node) {
|
|
226
|
+
validatePairs.push([node, "function-expression"]);
|
|
227
|
+
},
|
|
228
|
+
VariableDeclaration(node) {
|
|
229
|
+
if (node.kind === "const" || node.kind === "let") fileVarType = "const";
|
|
230
|
+
},
|
|
231
|
+
"Program:exit": function() {
|
|
232
|
+
if (fileVarType === "var") {
|
|
233
|
+
if (validatePairs.some(([n]) => n.parent?.type === "VariableDeclarator") || componentNodes.size > 0 && validatePairs.length > 0) fileVarType = "const";
|
|
234
|
+
}
|
|
235
|
+
validatePairs.forEach(([node, type]) => validate(node, type));
|
|
236
|
+
},
|
|
237
|
+
ImportDeclaration: () => {
|
|
238
|
+
fileVarType = "const";
|
|
239
|
+
},
|
|
240
|
+
ExportNamedDeclaration: () => {
|
|
241
|
+
fileVarType = "const";
|
|
242
|
+
},
|
|
243
|
+
ExportDefaultDeclaration: () => {
|
|
244
|
+
fileVarType = "const";
|
|
245
|
+
},
|
|
246
|
+
JSXElement: () => {
|
|
247
|
+
fileVarType = "const";
|
|
248
|
+
}
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
};
|
|
252
|
+
|
|
253
|
+
//#endregion
|
|
254
|
+
//#region src/plugin/rules/no-unused-class.ts
|
|
255
|
+
const STYLE_IMPORT_REGEXP = /\.(?:css|scss|less)$/u;
|
|
256
|
+
const toCamelCase = (value) => value.replace(/-([a-z])/gu, (_substring, letter) => letter.toUpperCase());
|
|
257
|
+
const extractClassNames = (source) => {
|
|
258
|
+
const classNames = /* @__PURE__ */ new Set();
|
|
259
|
+
const classNameRegExp = /\.([_a-zA-Z][\w-]*)/gu;
|
|
260
|
+
let match = classNameRegExp.exec(source);
|
|
261
|
+
while (match !== null) {
|
|
262
|
+
const className = match[1];
|
|
263
|
+
if (className !== void 0 && className !== "") classNames.add(className);
|
|
264
|
+
match = classNameRegExp.exec(source);
|
|
265
|
+
}
|
|
266
|
+
return [...classNames];
|
|
267
|
+
};
|
|
268
|
+
const buildClassesMap = (classNames, camelCaseOption) => {
|
|
269
|
+
const onlyCamelCase = camelCaseOption === "only" || camelCaseOption === "dashes-only";
|
|
270
|
+
const withCamelCase = camelCaseOption === true || camelCaseOption === "dashes" || onlyCamelCase;
|
|
271
|
+
const classesMap = {};
|
|
272
|
+
classNames.forEach((className) => {
|
|
273
|
+
if (!onlyCamelCase) classesMap[className] = className;
|
|
274
|
+
if (withCamelCase) classesMap[toCamelCase(className)] = className;
|
|
275
|
+
});
|
|
276
|
+
return classesMap;
|
|
277
|
+
};
|
|
278
|
+
const getStyleImportData = (node) => {
|
|
279
|
+
if (node.source.type !== "Literal" || typeof node.source.value !== "string") return void 0;
|
|
280
|
+
const source = node.source.value;
|
|
281
|
+
if (!STYLE_IMPORT_REGEXP.test(source)) return void 0;
|
|
282
|
+
const importSpecifier = node.specifiers.find((specifier) => specifier.type === "ImportDefaultSpecifier" || specifier.type === "ImportNamespaceSpecifier");
|
|
283
|
+
if (importSpecifier === void 0) return void 0;
|
|
284
|
+
return {
|
|
285
|
+
importName: importSpecifier.local.name,
|
|
286
|
+
importNode: importSpecifier,
|
|
287
|
+
styleFilePath: source
|
|
288
|
+
};
|
|
289
|
+
};
|
|
290
|
+
const getPropertyName = (node, camelCaseOption) => {
|
|
291
|
+
if (node.computed === false && node.property.type === "Identifier") return node.property.name;
|
|
292
|
+
if (node.computed === true && node.property.type === "Literal") {
|
|
293
|
+
if (typeof node.property.value !== "string" || node.property.value === "") return void 0;
|
|
294
|
+
return camelCaseOption === "only" ? toCamelCase(node.property.value) : node.property.value;
|
|
295
|
+
}
|
|
296
|
+
if (node.computed === true && node.property.type === "TemplateLiteral" && node.property.expressions.length === 0) {
|
|
297
|
+
const value = node.property.quasis[0]?.value.cooked;
|
|
298
|
+
if (typeof value !== "string" || value === "") return void 0;
|
|
299
|
+
return camelCaseOption === "only" ? toCamelCase(value) : value;
|
|
300
|
+
}
|
|
301
|
+
};
|
|
302
|
+
const getAbsoluteStylePath = (context, styleFilePath) => {
|
|
303
|
+
const filename = context.filename;
|
|
304
|
+
if (filename === "<input>") return void 0;
|
|
305
|
+
return node_path.default.resolve(node_path.default.dirname(filename), styleFilePath);
|
|
306
|
+
};
|
|
307
|
+
const noUnusedClass = {
|
|
308
|
+
meta: {
|
|
309
|
+
type: "problem",
|
|
310
|
+
docs: { description: "Checks that all CSS/SCSS/LESS classes imported as modules are used" },
|
|
311
|
+
schema: [{
|
|
312
|
+
type: "object",
|
|
313
|
+
properties: {
|
|
314
|
+
camelCase: { enum: [
|
|
315
|
+
true,
|
|
316
|
+
"dashes",
|
|
317
|
+
"only",
|
|
318
|
+
"dashes-only"
|
|
319
|
+
] },
|
|
320
|
+
markAsUsed: {
|
|
321
|
+
type: "array",
|
|
322
|
+
items: { type: "string" }
|
|
323
|
+
}
|
|
324
|
+
},
|
|
325
|
+
additionalProperties: false
|
|
326
|
+
}]
|
|
327
|
+
},
|
|
328
|
+
create(context) {
|
|
329
|
+
const options = context.options[0] ?? {};
|
|
330
|
+
const camelCaseOption = options.camelCase;
|
|
331
|
+
const markAsUsed = options.markAsUsed ?? [];
|
|
332
|
+
const importMap = {};
|
|
333
|
+
return {
|
|
334
|
+
ImportDeclaration(node) {
|
|
335
|
+
const styleImportData = getStyleImportData(node);
|
|
336
|
+
if (styleImportData === void 0) return;
|
|
337
|
+
const absoluteStylePath = getAbsoluteStylePath(context, styleImportData.styleFilePath);
|
|
338
|
+
if (absoluteStylePath === void 0 || !node_fs.default.existsSync(absoluteStylePath)) return;
|
|
339
|
+
const classNames = extractClassNames(node_fs.default.readFileSync(absoluteStylePath, "utf8"));
|
|
340
|
+
const classes = {};
|
|
341
|
+
classNames.forEach((className) => {
|
|
342
|
+
classes[className] = false;
|
|
343
|
+
});
|
|
344
|
+
importMap[styleImportData.importName] = {
|
|
345
|
+
classes,
|
|
346
|
+
classesMap: buildClassesMap(classNames, camelCaseOption),
|
|
347
|
+
filePath: styleImportData.styleFilePath,
|
|
348
|
+
node: styleImportData.importNode
|
|
349
|
+
};
|
|
350
|
+
},
|
|
351
|
+
MemberExpression(node) {
|
|
352
|
+
const typedNode = node;
|
|
353
|
+
if (typedNode.object.type !== "Identifier") return;
|
|
354
|
+
const entry = importMap[typedNode.object.name];
|
|
355
|
+
if (entry === void 0) return;
|
|
356
|
+
const propertyName = getPropertyName(typedNode, camelCaseOption);
|
|
357
|
+
if (propertyName === void 0 || propertyName === "") return;
|
|
358
|
+
const className = entry.classesMap[propertyName];
|
|
359
|
+
if (className === void 0 || className === "") return;
|
|
360
|
+
entry.classes[className] = true;
|
|
361
|
+
},
|
|
362
|
+
"Program:exit": () => {
|
|
363
|
+
Object.values(importMap).forEach((entry) => {
|
|
364
|
+
markAsUsed.forEach((usedClass) => {
|
|
365
|
+
if (usedClass !== "") entry.classes[usedClass] = true;
|
|
366
|
+
});
|
|
367
|
+
const unusedClasses = Object.entries(entry.classes).filter(([, used]) => used === false).map(([className]) => className);
|
|
368
|
+
if (unusedClasses.length > 0) context.report({
|
|
369
|
+
node: entry.node,
|
|
370
|
+
message: `Unused classes found in ${node_path.default.basename(entry.filePath)}: ${unusedClasses.join(", ")}`
|
|
371
|
+
});
|
|
372
|
+
});
|
|
373
|
+
}
|
|
374
|
+
};
|
|
375
|
+
}
|
|
376
|
+
};
|
|
377
|
+
|
|
378
|
+
//#endregion
|
|
379
|
+
//#region src/plugin/index.ts
|
|
380
|
+
const version = "1.0.0";
|
|
381
|
+
const siberiacancodePlugin = {
|
|
382
|
+
meta: {
|
|
383
|
+
name: "@siberiacancode/eslint-plugin",
|
|
384
|
+
version
|
|
385
|
+
},
|
|
386
|
+
rules: {
|
|
387
|
+
"function-component-definition": functionComponentDefinition,
|
|
388
|
+
"no-unused-class": noUnusedClass
|
|
389
|
+
}
|
|
390
|
+
};
|
|
391
|
+
|
|
392
|
+
//#endregion
|
|
36
393
|
//#region src/index.ts
|
|
37
394
|
const eslint = (inputOptions = {}, ...configs) => {
|
|
38
|
-
const { jsxA11y = false, ...options } = inputOptions;
|
|
39
|
-
const stylistic = options
|
|
395
|
+
const { jsxA11y = false, playwright = false, typescript = false, ...options } = inputOptions;
|
|
396
|
+
const stylistic = options.stylistic ?? false;
|
|
397
|
+
if (typescript === "engine") configs.unshift({
|
|
398
|
+
name: "siberiacancode/typescript",
|
|
399
|
+
files: ["**/*.?([cm])ts", "**/*.?([cm])tsx"],
|
|
400
|
+
rules: {
|
|
401
|
+
"ts/promise-function-async": "off",
|
|
402
|
+
"ts/strict-boolean-expressions": "off",
|
|
403
|
+
"ts/no-unnecessary-condition": "error",
|
|
404
|
+
"ts/no-namespace": "off",
|
|
405
|
+
"ts/no-floating-promises": "off",
|
|
406
|
+
"ts/no-misused-promises": "off",
|
|
407
|
+
"ts/no-empty-object-type": "warn"
|
|
408
|
+
}
|
|
409
|
+
});
|
|
40
410
|
if (jsxA11y) {
|
|
41
411
|
const jsxA11yRules = eslint_plugin_jsx_a11y.default.flatConfigs.recommended.rules;
|
|
42
412
|
configs.unshift({
|
|
@@ -48,23 +418,17 @@ const eslint = (inputOptions = {}, ...configs) => {
|
|
|
48
418
|
}, {}) }
|
|
49
419
|
});
|
|
50
420
|
}
|
|
51
|
-
if (
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
421
|
+
if (playwright) {
|
|
422
|
+
const playwrightRules = eslint_plugin_playwright.default.configs["flat/recommended"].rules;
|
|
423
|
+
configs.unshift({
|
|
424
|
+
name: "siberiacancode/playwright",
|
|
425
|
+
plugins: { "siberiacancode-playwright": eslint_plugin_playwright.default },
|
|
426
|
+
rules: { ...Object.entries(playwrightRules).reduce((acc, [key, value]) => {
|
|
427
|
+
acc[key.replace("playwright", "siberiacancode-playwright")] = value;
|
|
57
428
|
return acc;
|
|
58
|
-
}, {})
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
unnamedComponents: "arrow-function"
|
|
62
|
-
}],
|
|
63
|
-
"siberiacancode-react/prop-types": "off",
|
|
64
|
-
"siberiacancode-react/react-in-jsx-scope": "off"
|
|
65
|
-
},
|
|
66
|
-
settings: { react: { version: "detect" } }
|
|
67
|
-
});
|
|
429
|
+
}, {}) }
|
|
430
|
+
});
|
|
431
|
+
}
|
|
68
432
|
if (stylistic) configs.unshift({
|
|
69
433
|
name: "siberiacancode/formatter",
|
|
70
434
|
rules: {
|
|
@@ -103,9 +467,26 @@ const eslint = (inputOptions = {}, ...configs) => {
|
|
|
103
467
|
"style/semi": ["error", "always"]
|
|
104
468
|
}
|
|
105
469
|
});
|
|
470
|
+
configs.unshift({
|
|
471
|
+
name: "siberiacancode",
|
|
472
|
+
plugins: { siberiacancode: siberiacancodePlugin },
|
|
473
|
+
rules: {
|
|
474
|
+
"siberiacancode/function-component-definition": ["error", { namedComponents: ["arrow-function"] }],
|
|
475
|
+
"siberiacancode/no-unused-class": "error"
|
|
476
|
+
}
|
|
477
|
+
});
|
|
478
|
+
configs.unshift({
|
|
479
|
+
name: "siberiacancode/css",
|
|
480
|
+
plugins: { "siberiacancode-css": _eslint_css.default },
|
|
481
|
+
rules: { ...Object.entries(_eslint_css.default.configs.recommended.rules).reduce((acc, [key, value]) => {
|
|
482
|
+
acc[key.replace("css", "siberiacancode-css")] = value;
|
|
483
|
+
return acc;
|
|
484
|
+
}, {}) }
|
|
485
|
+
});
|
|
106
486
|
return (0, _antfu_eslint_config.default)({
|
|
107
487
|
...options,
|
|
108
|
-
stylistic
|
|
488
|
+
stylistic,
|
|
489
|
+
...typescript === "engine" ? { typescript: { tsconfigPath: "./tsconfig.json" } } : typescript
|
|
109
490
|
}, {
|
|
110
491
|
name: "siberiacancode/rewrite",
|
|
111
492
|
rules: {
|
|
@@ -141,9 +522,10 @@ const eslint = (inputOptions = {}, ...configs) => {
|
|
|
141
522
|
],
|
|
142
523
|
"side-effect",
|
|
143
524
|
"side-effect-style",
|
|
525
|
+
"ts-equals-import",
|
|
144
526
|
"unknown"
|
|
145
527
|
],
|
|
146
|
-
internalPattern: ["
|
|
528
|
+
internalPattern: ["^~/.+", "^@/.+"],
|
|
147
529
|
newlinesBetween: 1,
|
|
148
530
|
order: "asc",
|
|
149
531
|
type: "natural"
|
package/dist/cjs/index.d.cts
CHANGED
|
@@ -5,6 +5,8 @@ import { FlatConfigComposer } from "eslint-flat-config-utils";
|
|
|
5
5
|
//#region src/index.d.ts
|
|
6
6
|
type EslintOptions = OptionsConfig & TypedFlatConfigItem & {
|
|
7
7
|
jsxA11y?: boolean;
|
|
8
|
+
playwright?: boolean;
|
|
9
|
+
typescript?: 'engine';
|
|
8
10
|
};
|
|
9
11
|
type Eslint = (options?: EslintOptions, ...userConfigs: Awaitable<FlatConfigComposer<any, any> | Linter.Config[] | TypedFlatConfigItem | TypedFlatConfigItem[]>[]) => FlatConfigComposer<TypedFlatConfigItem, ConfigNames>;
|
|
10
12
|
declare const eslint: Eslint;
|
package/dist/esm/index.d.mts
CHANGED
|
@@ -5,6 +5,8 @@ import { FlatConfigComposer } from "eslint-flat-config-utils";
|
|
|
5
5
|
//#region src/index.d.ts
|
|
6
6
|
type EslintOptions = OptionsConfig & TypedFlatConfigItem & {
|
|
7
7
|
jsxA11y?: boolean;
|
|
8
|
+
playwright?: boolean;
|
|
9
|
+
typescript?: 'engine';
|
|
8
10
|
};
|
|
9
11
|
type Eslint = (options?: EslintOptions, ...userConfigs: Awaitable<FlatConfigComposer<any, any> | Linter.Config[] | TypedFlatConfigItem | TypedFlatConfigItem[]>[]) => FlatConfigComposer<TypedFlatConfigItem, ConfigNames>;
|
|
10
12
|
declare const eslint: Eslint;
|
package/dist/esm/index.mjs
CHANGED
|
@@ -1,11 +1,378 @@
|
|
|
1
1
|
import antfu from "@antfu/eslint-config";
|
|
2
|
+
import pluginCss from "@eslint/css";
|
|
2
3
|
import pluginJsxA11y from "eslint-plugin-jsx-a11y";
|
|
3
|
-
import
|
|
4
|
+
import pluginPlaywright from "eslint-plugin-playwright";
|
|
5
|
+
import fs from "node:fs";
|
|
6
|
+
import path from "node:path";
|
|
4
7
|
|
|
8
|
+
//#region src/plugin/rules/function-component-definition.ts
|
|
9
|
+
const NAMED_TEMPLATES = {
|
|
10
|
+
"function-declaration": "function {name}{typeParams}({params}){returnType} {body}",
|
|
11
|
+
"arrow-function": "{varType} {name}{typeAnnotation} = {typeParams}({params}){returnType} => {body}",
|
|
12
|
+
"function-expression": "{varType} {name}{typeAnnotation} = function{typeParams}({params}){returnType} {body}"
|
|
13
|
+
};
|
|
14
|
+
const buildFunction = (template, parts) => Object.keys(parts).reduce((acc, key) => acc.replace(new RegExp(`\\{${key}\\}`, "g"), () => parts[key] ?? ""), template);
|
|
15
|
+
const hasName = (node) => {
|
|
16
|
+
if (node.type === "FunctionDeclaration") return true;
|
|
17
|
+
return node.parent?.type === "VariableDeclarator";
|
|
18
|
+
};
|
|
19
|
+
const getName = (node) => {
|
|
20
|
+
if (node.type === "FunctionDeclaration" && node.id?.type === "Identifier") return node.id.name;
|
|
21
|
+
const parent = node.parent;
|
|
22
|
+
if ((node.type === "ArrowFunctionExpression" || node.type === "FunctionExpression") && parent?.type === "VariableDeclarator" && parent.id?.type === "Identifier") return parent.id.name;
|
|
23
|
+
};
|
|
24
|
+
const getTypeParams = (node) => {
|
|
25
|
+
return node.typeParameters;
|
|
26
|
+
};
|
|
27
|
+
const getNodeText = (sourceCode, node) => {
|
|
28
|
+
if (!node || !("range" in node) || !node.range) return void 0;
|
|
29
|
+
return sourceCode.getText(node);
|
|
30
|
+
};
|
|
31
|
+
const getParams = (sourceCode, node) => {
|
|
32
|
+
if (node.params.length === 0) return void 0;
|
|
33
|
+
const first = node.params[0];
|
|
34
|
+
const last = node.params[node.params.length - 1];
|
|
35
|
+
if (first == null || last == null || !("range" in first) || !("range" in last)) return void 0;
|
|
36
|
+
return sourceCode.getText({ range: [first.range[0], last.range[1]] });
|
|
37
|
+
};
|
|
38
|
+
const getBody = (sourceCode, node) => {
|
|
39
|
+
const body = node.body;
|
|
40
|
+
if (!("range" in body) || !body.range) return "{}";
|
|
41
|
+
const text = sourceCode.getText(body);
|
|
42
|
+
if (body.type === "BlockStatement") return text;
|
|
43
|
+
return `{\n return ${text}\n}`;
|
|
44
|
+
};
|
|
45
|
+
const getTypeAnnotation = (sourceCode, node) => {
|
|
46
|
+
if (!hasName(node) || node.type === "FunctionDeclaration") return void 0;
|
|
47
|
+
const parent = node.parent;
|
|
48
|
+
if (parent?.type !== "VariableDeclarator" || parent.id?.type !== "Identifier") return void 0;
|
|
49
|
+
const id = parent.id;
|
|
50
|
+
if (!("typeAnnotation" in id) || id.typeAnnotation === void 0 || id.typeAnnotation === null) return void 0;
|
|
51
|
+
return getNodeText(sourceCode, id.typeAnnotation);
|
|
52
|
+
};
|
|
53
|
+
const isUnfixableExport = (node) => node.type === "FunctionDeclaration" && node.parent?.type === "ExportDefaultDeclaration";
|
|
54
|
+
const isFunctionExpressionWithName = (node) => node.type === "FunctionExpression" && "id" in node && node.id !== null && node.id !== void 0;
|
|
55
|
+
const JSX_TYPES = ["JSXElement", "JSXFragment"];
|
|
56
|
+
/** Walk AST and return true if any descendant is JSX */
|
|
57
|
+
const containsJSX = (node) => {
|
|
58
|
+
if (JSX_TYPES.includes(node.type)) return true;
|
|
59
|
+
for (const key of Object.keys(node)) {
|
|
60
|
+
if (key === "parent") continue;
|
|
61
|
+
const value = node[key];
|
|
62
|
+
if (value !== null && value !== void 0 && typeof value === "object") {
|
|
63
|
+
if (Array.isArray(value)) {
|
|
64
|
+
if (value.some((child) => child !== null && child !== void 0 && typeof child === "object" && "type" in child && containsJSX(child))) return true;
|
|
65
|
+
} else if (value !== null && value !== void 0 && typeof value === "object" && "type" in value && containsJSX(value)) return true;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return false;
|
|
69
|
+
};
|
|
70
|
+
const walk = (node, visit) => {
|
|
71
|
+
visit(node);
|
|
72
|
+
for (const key of Object.keys(node)) {
|
|
73
|
+
if (key === "parent") continue;
|
|
74
|
+
const value = node[key];
|
|
75
|
+
if (value !== null && value !== void 0 && typeof value === "object") {
|
|
76
|
+
if (Array.isArray(value)) value.forEach((child) => {
|
|
77
|
+
if (child !== null && child !== void 0 && typeof child === "object" && "type" in child) walk(child, visit);
|
|
78
|
+
});
|
|
79
|
+
else if (value !== null && value !== void 0 && typeof value === "object" && "type" in value) walk(value, visit);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
/** Collect component nodes: functions that contain JSX (and for expr/arrow, are in VariableDeclarator) */
|
|
84
|
+
const getComponentNodes = (program) => {
|
|
85
|
+
const components = /* @__PURE__ */ new Set();
|
|
86
|
+
let fileHasJSX = false;
|
|
87
|
+
walk(program, (n) => {
|
|
88
|
+
const type = n.type;
|
|
89
|
+
if (type === "JSXElement" || type === "JSXFragment") fileHasJSX = true;
|
|
90
|
+
});
|
|
91
|
+
if (!fileHasJSX) return components;
|
|
92
|
+
walk(program, (n) => {
|
|
93
|
+
if (n.type === "FunctionDeclaration" && containsJSX(n)) components.add(n);
|
|
94
|
+
const withParent = n;
|
|
95
|
+
if ((n.type === "ArrowFunctionExpression" || n.type === "FunctionExpression") && withParent.parent?.type === "VariableDeclarator" && containsJSX(n)) components.add(n);
|
|
96
|
+
});
|
|
97
|
+
return components;
|
|
98
|
+
};
|
|
99
|
+
const functionComponentDefinition = {
|
|
100
|
+
meta: {
|
|
101
|
+
type: "layout",
|
|
102
|
+
docs: { description: "Enforce a specific function type for function components" },
|
|
103
|
+
fixable: "code",
|
|
104
|
+
schema: [{
|
|
105
|
+
type: "object",
|
|
106
|
+
properties: { namedComponents: { anyOf: [{ enum: [
|
|
107
|
+
"function-declaration",
|
|
108
|
+
"arrow-function",
|
|
109
|
+
"function-expression"
|
|
110
|
+
] }, {
|
|
111
|
+
type: "array",
|
|
112
|
+
items: {
|
|
113
|
+
type: "string",
|
|
114
|
+
enum: [
|
|
115
|
+
"function-declaration",
|
|
116
|
+
"arrow-function",
|
|
117
|
+
"function-expression"
|
|
118
|
+
]
|
|
119
|
+
}
|
|
120
|
+
}] } },
|
|
121
|
+
additionalProperties: false
|
|
122
|
+
}],
|
|
123
|
+
messages: {
|
|
124
|
+
"function-declaration": "Function component is not a function declaration",
|
|
125
|
+
"function-expression": "Function component is not a function expression",
|
|
126
|
+
"arrow-function": "Function component is not an arrow function"
|
|
127
|
+
}
|
|
128
|
+
},
|
|
129
|
+
create(context) {
|
|
130
|
+
const sourceCode = context.sourceCode;
|
|
131
|
+
const options = context.options[0] ?? {};
|
|
132
|
+
const namedConfig = [].concat(options.namedComponents ?? "function-declaration");
|
|
133
|
+
let fileVarType = "var";
|
|
134
|
+
const validatePairs = [];
|
|
135
|
+
let componentNodes = /* @__PURE__ */ new Set();
|
|
136
|
+
const getFixer = (node, fixOptions) => {
|
|
137
|
+
const typeAnnotation = getTypeAnnotation(sourceCode, node);
|
|
138
|
+
if (fixOptions.type === "function-declaration" && typeAnnotation) return void 0;
|
|
139
|
+
if (isUnfixableExport(node)) return void 0;
|
|
140
|
+
if (isFunctionExpressionWithName(node)) return void 0;
|
|
141
|
+
let varType = fileVarType;
|
|
142
|
+
const parent = node.parent;
|
|
143
|
+
if ((node.type === "FunctionExpression" || node.type === "ArrowFunctionExpression") && parent?.type === "VariableDeclarator" && parent.parent?.type === "VariableDeclaration") varType = parent.parent.kind;
|
|
144
|
+
const typeParams = getNodeText(sourceCode, getTypeParams(node));
|
|
145
|
+
const params = getParams(sourceCode, node);
|
|
146
|
+
const returnType = getNodeText(sourceCode, "returnType" in node ? node.returnType : void 0);
|
|
147
|
+
const body = getBody(sourceCode, node);
|
|
148
|
+
const name = getName(node);
|
|
149
|
+
const text = buildFunction(fixOptions.template, {
|
|
150
|
+
typeAnnotation,
|
|
151
|
+
typeParams,
|
|
152
|
+
params,
|
|
153
|
+
returnType,
|
|
154
|
+
body,
|
|
155
|
+
name: name !== void 0 && name !== null && name !== "" ? name : "",
|
|
156
|
+
varType
|
|
157
|
+
});
|
|
158
|
+
return (fixer) => fixer.replaceTextRange(fixOptions.range, text);
|
|
159
|
+
};
|
|
160
|
+
const report = (node, fixOptions) => {
|
|
161
|
+
const fix = getFixer(node, fixOptions);
|
|
162
|
+
context.report({
|
|
163
|
+
node,
|
|
164
|
+
messageId: fixOptions.messageId,
|
|
165
|
+
fix
|
|
166
|
+
});
|
|
167
|
+
};
|
|
168
|
+
const validate = (node, functionType) => {
|
|
169
|
+
if (!componentNodes.has(node)) return;
|
|
170
|
+
if (node.parent?.type === "Property") return;
|
|
171
|
+
if (hasName(node) && !namedConfig.includes(functionType)) {
|
|
172
|
+
const parent = node.parent;
|
|
173
|
+
report(node, {
|
|
174
|
+
messageId: namedConfig[0],
|
|
175
|
+
type: namedConfig[0],
|
|
176
|
+
template: NAMED_TEMPLATES[namedConfig[0]],
|
|
177
|
+
range: node.type === "FunctionDeclaration" ? node.range : parent?.parent?.range ?? node.range
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
};
|
|
181
|
+
return {
|
|
182
|
+
Program(programNode) {
|
|
183
|
+
componentNodes = getComponentNodes(programNode);
|
|
184
|
+
},
|
|
185
|
+
FunctionDeclaration(node) {
|
|
186
|
+
validatePairs.push([node, "function-declaration"]);
|
|
187
|
+
},
|
|
188
|
+
ArrowFunctionExpression(node) {
|
|
189
|
+
validatePairs.push([node, "arrow-function"]);
|
|
190
|
+
},
|
|
191
|
+
FunctionExpression(node) {
|
|
192
|
+
validatePairs.push([node, "function-expression"]);
|
|
193
|
+
},
|
|
194
|
+
VariableDeclaration(node) {
|
|
195
|
+
if (node.kind === "const" || node.kind === "let") fileVarType = "const";
|
|
196
|
+
},
|
|
197
|
+
"Program:exit": function() {
|
|
198
|
+
if (fileVarType === "var") {
|
|
199
|
+
if (validatePairs.some(([n]) => n.parent?.type === "VariableDeclarator") || componentNodes.size > 0 && validatePairs.length > 0) fileVarType = "const";
|
|
200
|
+
}
|
|
201
|
+
validatePairs.forEach(([node, type]) => validate(node, type));
|
|
202
|
+
},
|
|
203
|
+
ImportDeclaration: () => {
|
|
204
|
+
fileVarType = "const";
|
|
205
|
+
},
|
|
206
|
+
ExportNamedDeclaration: () => {
|
|
207
|
+
fileVarType = "const";
|
|
208
|
+
},
|
|
209
|
+
ExportDefaultDeclaration: () => {
|
|
210
|
+
fileVarType = "const";
|
|
211
|
+
},
|
|
212
|
+
JSXElement: () => {
|
|
213
|
+
fileVarType = "const";
|
|
214
|
+
}
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
};
|
|
218
|
+
|
|
219
|
+
//#endregion
|
|
220
|
+
//#region src/plugin/rules/no-unused-class.ts
|
|
221
|
+
const STYLE_IMPORT_REGEXP = /\.(?:css|scss|less)$/u;
|
|
222
|
+
const toCamelCase = (value) => value.replace(/-([a-z])/gu, (_substring, letter) => letter.toUpperCase());
|
|
223
|
+
const extractClassNames = (source) => {
|
|
224
|
+
const classNames = /* @__PURE__ */ new Set();
|
|
225
|
+
const classNameRegExp = /\.([_a-zA-Z][\w-]*)/gu;
|
|
226
|
+
let match = classNameRegExp.exec(source);
|
|
227
|
+
while (match !== null) {
|
|
228
|
+
const className = match[1];
|
|
229
|
+
if (className !== void 0 && className !== "") classNames.add(className);
|
|
230
|
+
match = classNameRegExp.exec(source);
|
|
231
|
+
}
|
|
232
|
+
return [...classNames];
|
|
233
|
+
};
|
|
234
|
+
const buildClassesMap = (classNames, camelCaseOption) => {
|
|
235
|
+
const onlyCamelCase = camelCaseOption === "only" || camelCaseOption === "dashes-only";
|
|
236
|
+
const withCamelCase = camelCaseOption === true || camelCaseOption === "dashes" || onlyCamelCase;
|
|
237
|
+
const classesMap = {};
|
|
238
|
+
classNames.forEach((className) => {
|
|
239
|
+
if (!onlyCamelCase) classesMap[className] = className;
|
|
240
|
+
if (withCamelCase) classesMap[toCamelCase(className)] = className;
|
|
241
|
+
});
|
|
242
|
+
return classesMap;
|
|
243
|
+
};
|
|
244
|
+
const getStyleImportData = (node) => {
|
|
245
|
+
if (node.source.type !== "Literal" || typeof node.source.value !== "string") return void 0;
|
|
246
|
+
const source = node.source.value;
|
|
247
|
+
if (!STYLE_IMPORT_REGEXP.test(source)) return void 0;
|
|
248
|
+
const importSpecifier = node.specifiers.find((specifier) => specifier.type === "ImportDefaultSpecifier" || specifier.type === "ImportNamespaceSpecifier");
|
|
249
|
+
if (importSpecifier === void 0) return void 0;
|
|
250
|
+
return {
|
|
251
|
+
importName: importSpecifier.local.name,
|
|
252
|
+
importNode: importSpecifier,
|
|
253
|
+
styleFilePath: source
|
|
254
|
+
};
|
|
255
|
+
};
|
|
256
|
+
const getPropertyName = (node, camelCaseOption) => {
|
|
257
|
+
if (node.computed === false && node.property.type === "Identifier") return node.property.name;
|
|
258
|
+
if (node.computed === true && node.property.type === "Literal") {
|
|
259
|
+
if (typeof node.property.value !== "string" || node.property.value === "") return void 0;
|
|
260
|
+
return camelCaseOption === "only" ? toCamelCase(node.property.value) : node.property.value;
|
|
261
|
+
}
|
|
262
|
+
if (node.computed === true && node.property.type === "TemplateLiteral" && node.property.expressions.length === 0) {
|
|
263
|
+
const value = node.property.quasis[0]?.value.cooked;
|
|
264
|
+
if (typeof value !== "string" || value === "") return void 0;
|
|
265
|
+
return camelCaseOption === "only" ? toCamelCase(value) : value;
|
|
266
|
+
}
|
|
267
|
+
};
|
|
268
|
+
const getAbsoluteStylePath = (context, styleFilePath) => {
|
|
269
|
+
const filename = context.filename;
|
|
270
|
+
if (filename === "<input>") return void 0;
|
|
271
|
+
return path.resolve(path.dirname(filename), styleFilePath);
|
|
272
|
+
};
|
|
273
|
+
const noUnusedClass = {
|
|
274
|
+
meta: {
|
|
275
|
+
type: "problem",
|
|
276
|
+
docs: { description: "Checks that all CSS/SCSS/LESS classes imported as modules are used" },
|
|
277
|
+
schema: [{
|
|
278
|
+
type: "object",
|
|
279
|
+
properties: {
|
|
280
|
+
camelCase: { enum: [
|
|
281
|
+
true,
|
|
282
|
+
"dashes",
|
|
283
|
+
"only",
|
|
284
|
+
"dashes-only"
|
|
285
|
+
] },
|
|
286
|
+
markAsUsed: {
|
|
287
|
+
type: "array",
|
|
288
|
+
items: { type: "string" }
|
|
289
|
+
}
|
|
290
|
+
},
|
|
291
|
+
additionalProperties: false
|
|
292
|
+
}]
|
|
293
|
+
},
|
|
294
|
+
create(context) {
|
|
295
|
+
const options = context.options[0] ?? {};
|
|
296
|
+
const camelCaseOption = options.camelCase;
|
|
297
|
+
const markAsUsed = options.markAsUsed ?? [];
|
|
298
|
+
const importMap = {};
|
|
299
|
+
return {
|
|
300
|
+
ImportDeclaration(node) {
|
|
301
|
+
const styleImportData = getStyleImportData(node);
|
|
302
|
+
if (styleImportData === void 0) return;
|
|
303
|
+
const absoluteStylePath = getAbsoluteStylePath(context, styleImportData.styleFilePath);
|
|
304
|
+
if (absoluteStylePath === void 0 || !fs.existsSync(absoluteStylePath)) return;
|
|
305
|
+
const classNames = extractClassNames(fs.readFileSync(absoluteStylePath, "utf8"));
|
|
306
|
+
const classes = {};
|
|
307
|
+
classNames.forEach((className) => {
|
|
308
|
+
classes[className] = false;
|
|
309
|
+
});
|
|
310
|
+
importMap[styleImportData.importName] = {
|
|
311
|
+
classes,
|
|
312
|
+
classesMap: buildClassesMap(classNames, camelCaseOption),
|
|
313
|
+
filePath: styleImportData.styleFilePath,
|
|
314
|
+
node: styleImportData.importNode
|
|
315
|
+
};
|
|
316
|
+
},
|
|
317
|
+
MemberExpression(node) {
|
|
318
|
+
const typedNode = node;
|
|
319
|
+
if (typedNode.object.type !== "Identifier") return;
|
|
320
|
+
const entry = importMap[typedNode.object.name];
|
|
321
|
+
if (entry === void 0) return;
|
|
322
|
+
const propertyName = getPropertyName(typedNode, camelCaseOption);
|
|
323
|
+
if (propertyName === void 0 || propertyName === "") return;
|
|
324
|
+
const className = entry.classesMap[propertyName];
|
|
325
|
+
if (className === void 0 || className === "") return;
|
|
326
|
+
entry.classes[className] = true;
|
|
327
|
+
},
|
|
328
|
+
"Program:exit": () => {
|
|
329
|
+
Object.values(importMap).forEach((entry) => {
|
|
330
|
+
markAsUsed.forEach((usedClass) => {
|
|
331
|
+
if (usedClass !== "") entry.classes[usedClass] = true;
|
|
332
|
+
});
|
|
333
|
+
const unusedClasses = Object.entries(entry.classes).filter(([, used]) => used === false).map(([className]) => className);
|
|
334
|
+
if (unusedClasses.length > 0) context.report({
|
|
335
|
+
node: entry.node,
|
|
336
|
+
message: `Unused classes found in ${path.basename(entry.filePath)}: ${unusedClasses.join(", ")}`
|
|
337
|
+
});
|
|
338
|
+
});
|
|
339
|
+
}
|
|
340
|
+
};
|
|
341
|
+
}
|
|
342
|
+
};
|
|
343
|
+
|
|
344
|
+
//#endregion
|
|
345
|
+
//#region src/plugin/index.ts
|
|
346
|
+
const version = "1.0.0";
|
|
347
|
+
const siberiacancodePlugin = {
|
|
348
|
+
meta: {
|
|
349
|
+
name: "@siberiacancode/eslint-plugin",
|
|
350
|
+
version
|
|
351
|
+
},
|
|
352
|
+
rules: {
|
|
353
|
+
"function-component-definition": functionComponentDefinition,
|
|
354
|
+
"no-unused-class": noUnusedClass
|
|
355
|
+
}
|
|
356
|
+
};
|
|
357
|
+
|
|
358
|
+
//#endregion
|
|
5
359
|
//#region src/index.ts
|
|
6
360
|
const eslint = (inputOptions = {}, ...configs) => {
|
|
7
|
-
const { jsxA11y = false, ...options } = inputOptions;
|
|
8
|
-
const stylistic = options
|
|
361
|
+
const { jsxA11y = false, playwright = false, typescript = false, ...options } = inputOptions;
|
|
362
|
+
const stylistic = options.stylistic ?? false;
|
|
363
|
+
if (typescript === "engine") configs.unshift({
|
|
364
|
+
name: "siberiacancode/typescript",
|
|
365
|
+
files: ["**/*.?([cm])ts", "**/*.?([cm])tsx"],
|
|
366
|
+
rules: {
|
|
367
|
+
"ts/promise-function-async": "off",
|
|
368
|
+
"ts/strict-boolean-expressions": "off",
|
|
369
|
+
"ts/no-unnecessary-condition": "error",
|
|
370
|
+
"ts/no-namespace": "off",
|
|
371
|
+
"ts/no-floating-promises": "off",
|
|
372
|
+
"ts/no-misused-promises": "off",
|
|
373
|
+
"ts/no-empty-object-type": "warn"
|
|
374
|
+
}
|
|
375
|
+
});
|
|
9
376
|
if (jsxA11y) {
|
|
10
377
|
const jsxA11yRules = pluginJsxA11y.flatConfigs.recommended.rules;
|
|
11
378
|
configs.unshift({
|
|
@@ -17,23 +384,17 @@ const eslint = (inputOptions = {}, ...configs) => {
|
|
|
17
384
|
}, {}) }
|
|
18
385
|
});
|
|
19
386
|
}
|
|
20
|
-
if (
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
387
|
+
if (playwright) {
|
|
388
|
+
const playwrightRules = pluginPlaywright.configs["flat/recommended"].rules;
|
|
389
|
+
configs.unshift({
|
|
390
|
+
name: "siberiacancode/playwright",
|
|
391
|
+
plugins: { "siberiacancode-playwright": pluginPlaywright },
|
|
392
|
+
rules: { ...Object.entries(playwrightRules).reduce((acc, [key, value]) => {
|
|
393
|
+
acc[key.replace("playwright", "siberiacancode-playwright")] = value;
|
|
26
394
|
return acc;
|
|
27
|
-
}, {})
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
unnamedComponents: "arrow-function"
|
|
31
|
-
}],
|
|
32
|
-
"siberiacancode-react/prop-types": "off",
|
|
33
|
-
"siberiacancode-react/react-in-jsx-scope": "off"
|
|
34
|
-
},
|
|
35
|
-
settings: { react: { version: "detect" } }
|
|
36
|
-
});
|
|
395
|
+
}, {}) }
|
|
396
|
+
});
|
|
397
|
+
}
|
|
37
398
|
if (stylistic) configs.unshift({
|
|
38
399
|
name: "siberiacancode/formatter",
|
|
39
400
|
rules: {
|
|
@@ -72,9 +433,26 @@ const eslint = (inputOptions = {}, ...configs) => {
|
|
|
72
433
|
"style/semi": ["error", "always"]
|
|
73
434
|
}
|
|
74
435
|
});
|
|
436
|
+
configs.unshift({
|
|
437
|
+
name: "siberiacancode",
|
|
438
|
+
plugins: { siberiacancode: siberiacancodePlugin },
|
|
439
|
+
rules: {
|
|
440
|
+
"siberiacancode/function-component-definition": ["error", { namedComponents: ["arrow-function"] }],
|
|
441
|
+
"siberiacancode/no-unused-class": "error"
|
|
442
|
+
}
|
|
443
|
+
});
|
|
444
|
+
configs.unshift({
|
|
445
|
+
name: "siberiacancode/css",
|
|
446
|
+
plugins: { "siberiacancode-css": pluginCss },
|
|
447
|
+
rules: { ...Object.entries(pluginCss.configs.recommended.rules).reduce((acc, [key, value]) => {
|
|
448
|
+
acc[key.replace("css", "siberiacancode-css")] = value;
|
|
449
|
+
return acc;
|
|
450
|
+
}, {}) }
|
|
451
|
+
});
|
|
75
452
|
return antfu({
|
|
76
453
|
...options,
|
|
77
|
-
stylistic
|
|
454
|
+
stylistic,
|
|
455
|
+
...typescript === "engine" ? { typescript: { tsconfigPath: "./tsconfig.json" } } : typescript
|
|
78
456
|
}, {
|
|
79
457
|
name: "siberiacancode/rewrite",
|
|
80
458
|
rules: {
|
|
@@ -110,9 +488,10 @@ const eslint = (inputOptions = {}, ...configs) => {
|
|
|
110
488
|
],
|
|
111
489
|
"side-effect",
|
|
112
490
|
"side-effect-style",
|
|
491
|
+
"ts-equals-import",
|
|
113
492
|
"unknown"
|
|
114
493
|
],
|
|
115
|
-
internalPattern: ["
|
|
494
|
+
internalPattern: ["^~/.+", "^@/.+"],
|
|
116
495
|
newlinesBetween: 1,
|
|
117
496
|
order: "asc",
|
|
118
497
|
type: "natural"
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@siberiacancode/eslint",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "2.
|
|
4
|
+
"version": "2.16.1",
|
|
5
5
|
"description": "eslint configs",
|
|
6
6
|
"author": {
|
|
7
7
|
"name": "SIBERIA CAN CODE 🧊",
|
|
@@ -54,15 +54,16 @@
|
|
|
54
54
|
"pretty": "yarn type && yarn lint && yarn format"
|
|
55
55
|
},
|
|
56
56
|
"dependencies": {
|
|
57
|
-
"@antfu/eslint-config": "7.
|
|
57
|
+
"@antfu/eslint-config": "7.4.3",
|
|
58
58
|
"@eslint-react/eslint-plugin": "2.13.0",
|
|
59
|
+
"@eslint/css": "^0.14.1",
|
|
59
60
|
"@next/eslint-plugin-next": "16.1.6",
|
|
60
61
|
"@types/eslint-plugin-jsx-a11y": "^6.10.1",
|
|
61
62
|
"@vue/compiler-sfc": "3.5.29",
|
|
62
63
|
"eslint": "10.0.2",
|
|
63
64
|
"eslint-flat-config-utils": "^3.0.1",
|
|
64
65
|
"eslint-plugin-jsx-a11y": "6.10.2",
|
|
65
|
-
"eslint-plugin-
|
|
66
|
+
"eslint-plugin-playwright": "^2.9.0",
|
|
66
67
|
"eslint-plugin-react-hooks": "7.0.1",
|
|
67
68
|
"eslint-plugin-react-refresh": "0.5.2"
|
|
68
69
|
},
|