@saasmakers/eslint 0.1.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/index.cjs +138 -0
- package/dist/index.d.cts +9 -0
- package/dist/index.d.mts +9 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.mjs +136 -0
- package/package.json +41 -0
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const rule = {
|
|
4
|
+
meta: {
|
|
5
|
+
docs: {
|
|
6
|
+
category: "Best Practices",
|
|
7
|
+
description: "Enforce sorted test functions grouped by method with sorted exceptions and middlewares",
|
|
8
|
+
recommended: true
|
|
9
|
+
},
|
|
10
|
+
fixable: "code",
|
|
11
|
+
messages: { sortError: "Test functions should be grouped by method with sorted exceptions and middlewares." },
|
|
12
|
+
schema: [],
|
|
13
|
+
type: "suggestion"
|
|
14
|
+
},
|
|
15
|
+
create(context) {
|
|
16
|
+
if (!context.filename.endsWith(".spec.ts")) {
|
|
17
|
+
return {};
|
|
18
|
+
}
|
|
19
|
+
function getTestName(node) {
|
|
20
|
+
if (node.arguments && node.arguments[0] && node.arguments[0].type === "Literal") {
|
|
21
|
+
return node.arguments[0].value;
|
|
22
|
+
}
|
|
23
|
+
return "";
|
|
24
|
+
}
|
|
25
|
+
function getTestPriority(testName) {
|
|
26
|
+
if (testName.includes(".middlewares.")) {
|
|
27
|
+
return 3;
|
|
28
|
+
}
|
|
29
|
+
if (testName.includes(".exceptions.")) {
|
|
30
|
+
return 2;
|
|
31
|
+
}
|
|
32
|
+
return 1;
|
|
33
|
+
}
|
|
34
|
+
function getFunctionName(testName) {
|
|
35
|
+
const parts = testName.split(".");
|
|
36
|
+
return parts.slice(0, 2).join(".");
|
|
37
|
+
}
|
|
38
|
+
function getSpecificName(testName) {
|
|
39
|
+
const parts = testName.split(".");
|
|
40
|
+
const specialIndex = parts.findIndex(
|
|
41
|
+
(part) => part === "exceptions" || part === "middlewares"
|
|
42
|
+
);
|
|
43
|
+
if (specialIndex === -1) {
|
|
44
|
+
return parts.slice(2).join(".");
|
|
45
|
+
}
|
|
46
|
+
return parts.slice(specialIndex + 1).join(".");
|
|
47
|
+
}
|
|
48
|
+
function compareTests(testA, testB) {
|
|
49
|
+
const functionA = getFunctionName(testA);
|
|
50
|
+
const functionB = getFunctionName(testB);
|
|
51
|
+
if (functionA !== functionB) {
|
|
52
|
+
return functionA.localeCompare(functionB);
|
|
53
|
+
}
|
|
54
|
+
const priorityA = getTestPriority(testA);
|
|
55
|
+
const priorityB = getTestPriority(testB);
|
|
56
|
+
if (priorityA !== priorityB) {
|
|
57
|
+
return priorityA - priorityB;
|
|
58
|
+
}
|
|
59
|
+
const specificNameA = getSpecificName(testA);
|
|
60
|
+
const specificNameB = getSpecificName(testB);
|
|
61
|
+
return specificNameA.localeCompare(specificNameB);
|
|
62
|
+
}
|
|
63
|
+
function handleTestGroup(node) {
|
|
64
|
+
const testGroup = node.arguments[0];
|
|
65
|
+
if (!testGroup || !node.arguments[1]) {
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
const groupCallback = node.arguments[1];
|
|
69
|
+
if (groupCallback.type !== "FunctionExpression" && groupCallback.type !== "ArrowFunctionExpression") {
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
const callbackBody = groupCallback.body;
|
|
73
|
+
if (callbackBody.type !== "BlockStatement" || !callbackBody.body) {
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
const tests = callbackBody.body.filter((statement) => {
|
|
77
|
+
return statement.type === "ExpressionStatement" && statement.expression.type === "CallExpression" && statement.expression.callee.type === "Identifier" && (statement.expression.callee.name === "test" || statement.expression.callee.name === "it");
|
|
78
|
+
});
|
|
79
|
+
const testNames = tests.map((test) => getTestName(test.expression));
|
|
80
|
+
const sortedTestNames = [...testNames].sort(compareTests);
|
|
81
|
+
const isSorted = testNames.every(
|
|
82
|
+
(name, index) => name === sortedTestNames[index]
|
|
83
|
+
);
|
|
84
|
+
if (!isSorted) {
|
|
85
|
+
context.report({
|
|
86
|
+
fix(fixer) {
|
|
87
|
+
const testMap = new Map(
|
|
88
|
+
tests.map((test) => [getTestName(test.expression), test])
|
|
89
|
+
);
|
|
90
|
+
const sortedTests = sortedTestNames.map((name) => testMap.get(name));
|
|
91
|
+
const fixes = [];
|
|
92
|
+
for (let index = 0; index < tests.length; index++) {
|
|
93
|
+
const originalTest = tests[index];
|
|
94
|
+
const sortedTest = sortedTests[index];
|
|
95
|
+
if (originalTest !== sortedTest) {
|
|
96
|
+
fixes.push(fixer.replaceText(
|
|
97
|
+
originalTest,
|
|
98
|
+
context.sourceCode.getText(sortedTest)
|
|
99
|
+
));
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
return fixes;
|
|
103
|
+
},
|
|
104
|
+
loc: node.loc ?? void 0,
|
|
105
|
+
messageId: "sortError",
|
|
106
|
+
node
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
return {
|
|
111
|
+
'CallExpression[callee.name="describe"]': handleTestGroup,
|
|
112
|
+
'CallExpression[callee.object.name="describe"][callee.property.name="concurrent"]': handleTestGroup,
|
|
113
|
+
'CallExpression[callee.object.name="test"][callee.property.name="group"]': handleTestGroup
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
const index = {
|
|
119
|
+
rules: {
|
|
120
|
+
// 'ts-multiline-ternary': tsMultilineTernary,
|
|
121
|
+
// 'ts-multiline-union': tsMultilineUnion,
|
|
122
|
+
"ts-sort-tests": rule
|
|
123
|
+
// 'vue-i18n-consistent-locales': vueI18nConsistentLocales,
|
|
124
|
+
// 'vue-i18n-consistent-t': vueI18nConsistentT,
|
|
125
|
+
// 'vue-i18n-sort-keys': vueI18nSortKeys,
|
|
126
|
+
// 'vue-i18n-unused-strings': vueI18nUnusedStrings,
|
|
127
|
+
// 'vue-script-format-computed': vueScriptFormatComputed,
|
|
128
|
+
// 'vue-script-format-emits': vueScriptFormatEmits,
|
|
129
|
+
// 'vue-script-format-props': vueScriptFormatProps,
|
|
130
|
+
// 'vue-script-order': vueScriptOrder,
|
|
131
|
+
// 'vue-template-format-classes': vueTemplateFormatClasses,
|
|
132
|
+
// 'vue-template-format-props': vueTemplateFormatProps,
|
|
133
|
+
// 'vue-template-remove-comments': vueTemplateRemoveComments,
|
|
134
|
+
// 'vue-template-remove-true-attributes': vueTemplateRemoveTrueAttributes,
|
|
135
|
+
}
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
module.exports = index;
|
package/dist/index.d.cts
ADDED
package/dist/index.d.mts
ADDED
package/dist/index.d.ts
ADDED
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
const rule = {
|
|
2
|
+
meta: {
|
|
3
|
+
docs: {
|
|
4
|
+
category: "Best Practices",
|
|
5
|
+
description: "Enforce sorted test functions grouped by method with sorted exceptions and middlewares",
|
|
6
|
+
recommended: true
|
|
7
|
+
},
|
|
8
|
+
fixable: "code",
|
|
9
|
+
messages: { sortError: "Test functions should be grouped by method with sorted exceptions and middlewares." },
|
|
10
|
+
schema: [],
|
|
11
|
+
type: "suggestion"
|
|
12
|
+
},
|
|
13
|
+
create(context) {
|
|
14
|
+
if (!context.filename.endsWith(".spec.ts")) {
|
|
15
|
+
return {};
|
|
16
|
+
}
|
|
17
|
+
function getTestName(node) {
|
|
18
|
+
if (node.arguments && node.arguments[0] && node.arguments[0].type === "Literal") {
|
|
19
|
+
return node.arguments[0].value;
|
|
20
|
+
}
|
|
21
|
+
return "";
|
|
22
|
+
}
|
|
23
|
+
function getTestPriority(testName) {
|
|
24
|
+
if (testName.includes(".middlewares.")) {
|
|
25
|
+
return 3;
|
|
26
|
+
}
|
|
27
|
+
if (testName.includes(".exceptions.")) {
|
|
28
|
+
return 2;
|
|
29
|
+
}
|
|
30
|
+
return 1;
|
|
31
|
+
}
|
|
32
|
+
function getFunctionName(testName) {
|
|
33
|
+
const parts = testName.split(".");
|
|
34
|
+
return parts.slice(0, 2).join(".");
|
|
35
|
+
}
|
|
36
|
+
function getSpecificName(testName) {
|
|
37
|
+
const parts = testName.split(".");
|
|
38
|
+
const specialIndex = parts.findIndex(
|
|
39
|
+
(part) => part === "exceptions" || part === "middlewares"
|
|
40
|
+
);
|
|
41
|
+
if (specialIndex === -1) {
|
|
42
|
+
return parts.slice(2).join(".");
|
|
43
|
+
}
|
|
44
|
+
return parts.slice(specialIndex + 1).join(".");
|
|
45
|
+
}
|
|
46
|
+
function compareTests(testA, testB) {
|
|
47
|
+
const functionA = getFunctionName(testA);
|
|
48
|
+
const functionB = getFunctionName(testB);
|
|
49
|
+
if (functionA !== functionB) {
|
|
50
|
+
return functionA.localeCompare(functionB);
|
|
51
|
+
}
|
|
52
|
+
const priorityA = getTestPriority(testA);
|
|
53
|
+
const priorityB = getTestPriority(testB);
|
|
54
|
+
if (priorityA !== priorityB) {
|
|
55
|
+
return priorityA - priorityB;
|
|
56
|
+
}
|
|
57
|
+
const specificNameA = getSpecificName(testA);
|
|
58
|
+
const specificNameB = getSpecificName(testB);
|
|
59
|
+
return specificNameA.localeCompare(specificNameB);
|
|
60
|
+
}
|
|
61
|
+
function handleTestGroup(node) {
|
|
62
|
+
const testGroup = node.arguments[0];
|
|
63
|
+
if (!testGroup || !node.arguments[1]) {
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
const groupCallback = node.arguments[1];
|
|
67
|
+
if (groupCallback.type !== "FunctionExpression" && groupCallback.type !== "ArrowFunctionExpression") {
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
const callbackBody = groupCallback.body;
|
|
71
|
+
if (callbackBody.type !== "BlockStatement" || !callbackBody.body) {
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
const tests = callbackBody.body.filter((statement) => {
|
|
75
|
+
return statement.type === "ExpressionStatement" && statement.expression.type === "CallExpression" && statement.expression.callee.type === "Identifier" && (statement.expression.callee.name === "test" || statement.expression.callee.name === "it");
|
|
76
|
+
});
|
|
77
|
+
const testNames = tests.map((test) => getTestName(test.expression));
|
|
78
|
+
const sortedTestNames = [...testNames].sort(compareTests);
|
|
79
|
+
const isSorted = testNames.every(
|
|
80
|
+
(name, index) => name === sortedTestNames[index]
|
|
81
|
+
);
|
|
82
|
+
if (!isSorted) {
|
|
83
|
+
context.report({
|
|
84
|
+
fix(fixer) {
|
|
85
|
+
const testMap = new Map(
|
|
86
|
+
tests.map((test) => [getTestName(test.expression), test])
|
|
87
|
+
);
|
|
88
|
+
const sortedTests = sortedTestNames.map((name) => testMap.get(name));
|
|
89
|
+
const fixes = [];
|
|
90
|
+
for (let index = 0; index < tests.length; index++) {
|
|
91
|
+
const originalTest = tests[index];
|
|
92
|
+
const sortedTest = sortedTests[index];
|
|
93
|
+
if (originalTest !== sortedTest) {
|
|
94
|
+
fixes.push(fixer.replaceText(
|
|
95
|
+
originalTest,
|
|
96
|
+
context.sourceCode.getText(sortedTest)
|
|
97
|
+
));
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
return fixes;
|
|
101
|
+
},
|
|
102
|
+
loc: node.loc ?? void 0,
|
|
103
|
+
messageId: "sortError",
|
|
104
|
+
node
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
return {
|
|
109
|
+
'CallExpression[callee.name="describe"]': handleTestGroup,
|
|
110
|
+
'CallExpression[callee.object.name="describe"][callee.property.name="concurrent"]': handleTestGroup,
|
|
111
|
+
'CallExpression[callee.object.name="test"][callee.property.name="group"]': handleTestGroup
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
const index = {
|
|
117
|
+
rules: {
|
|
118
|
+
// 'ts-multiline-ternary': tsMultilineTernary,
|
|
119
|
+
// 'ts-multiline-union': tsMultilineUnion,
|
|
120
|
+
"ts-sort-tests": rule
|
|
121
|
+
// 'vue-i18n-consistent-locales': vueI18nConsistentLocales,
|
|
122
|
+
// 'vue-i18n-consistent-t': vueI18nConsistentT,
|
|
123
|
+
// 'vue-i18n-sort-keys': vueI18nSortKeys,
|
|
124
|
+
// 'vue-i18n-unused-strings': vueI18nUnusedStrings,
|
|
125
|
+
// 'vue-script-format-computed': vueScriptFormatComputed,
|
|
126
|
+
// 'vue-script-format-emits': vueScriptFormatEmits,
|
|
127
|
+
// 'vue-script-format-props': vueScriptFormatProps,
|
|
128
|
+
// 'vue-script-order': vueScriptOrder,
|
|
129
|
+
// 'vue-template-format-classes': vueTemplateFormatClasses,
|
|
130
|
+
// 'vue-template-format-props': vueTemplateFormatProps,
|
|
131
|
+
// 'vue-template-remove-comments': vueTemplateRemoveComments,
|
|
132
|
+
// 'vue-template-remove-true-attributes': vueTemplateRemoveTrueAttributes,
|
|
133
|
+
}
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
export { index as default };
|
package/package.json
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@saasmakers/eslint",
|
|
3
|
+
"type": "module",
|
|
4
|
+
"version": "0.1.1",
|
|
5
|
+
"private": false,
|
|
6
|
+
"description": "Shared ESLint config for SaaS Makers projects",
|
|
7
|
+
"license": "MIT",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "git+https://github.com/saasmakers/saasmakers-turborepo.git",
|
|
11
|
+
"directory": "packages/eslint"
|
|
12
|
+
},
|
|
13
|
+
"exports": {
|
|
14
|
+
".": {
|
|
15
|
+
"types": "./dist/index.d.ts",
|
|
16
|
+
"import": "./dist/index.mjs",
|
|
17
|
+
"require": "./dist/index.cjs"
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
"main": "dist/index.cjs",
|
|
21
|
+
"types": "dist/index.d.ts",
|
|
22
|
+
"publishConfig": {
|
|
23
|
+
"access": "public"
|
|
24
|
+
},
|
|
25
|
+
"files": [
|
|
26
|
+
"dist/*"
|
|
27
|
+
],
|
|
28
|
+
"peerDependencies": {
|
|
29
|
+
"eslint": "^9.0.0"
|
|
30
|
+
},
|
|
31
|
+
"devDependencies": {
|
|
32
|
+
"@types/estree": "1.0.8",
|
|
33
|
+
"typescript": "5.9.3",
|
|
34
|
+
"@saasmakers/config": "0.1.19"
|
|
35
|
+
},
|
|
36
|
+
"scripts": {
|
|
37
|
+
"build": "unbuild",
|
|
38
|
+
"lint": "eslint . --max-warnings=0",
|
|
39
|
+
"typecheck": "tsc --noEmit"
|
|
40
|
+
}
|
|
41
|
+
}
|