@yasainet/eslint 0.0.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/README.md +50 -0
- package/package.json +34 -0
- package/src/common/constants.mjs +72 -0
- package/src/common/imports.mjs +186 -0
- package/src/common/index.mjs +13 -0
- package/src/common/jsdoc.mjs +39 -0
- package/src/common/layers.mjs +35 -0
- package/src/common/local-plugins/action-handle-service.mjs +60 -0
- package/src/common/naming.mjs +150 -0
- package/src/common/plugins.mjs +13 -0
- package/src/common/rules.mjs +39 -0
- package/src/next/directives.mjs +58 -0
- package/src/next/index.mjs +10 -0
- package/src/next/naming.mjs +58 -0
- package/src/node/index.mjs +3 -0
package/README.md
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# @yasainet/eslint
|
|
2
|
+
|
|
3
|
+
Shared ESLint configuration for feature-based architecture.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```sh
|
|
8
|
+
npm install -D @yasainet/eslint eslint
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Usage
|
|
12
|
+
|
|
13
|
+
### Next.js
|
|
14
|
+
|
|
15
|
+
```js
|
|
16
|
+
// eslint.config.mjs
|
|
17
|
+
import { defineConfig, globalIgnores } from "eslint/config";
|
|
18
|
+
import nextVitals from "eslint-config-next/core-web-vitals";
|
|
19
|
+
import nextTs from "eslint-config-next/typescript";
|
|
20
|
+
import { eslintConfig } from "@yasainet/eslint/next";
|
|
21
|
+
|
|
22
|
+
export default defineConfig([
|
|
23
|
+
...nextVitals,
|
|
24
|
+
...nextTs,
|
|
25
|
+
globalIgnores([".next/**", "out/**", "build/**", "next-env.d.ts"]),
|
|
26
|
+
...eslintConfig,
|
|
27
|
+
]);
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
### Node.js
|
|
31
|
+
|
|
32
|
+
```js
|
|
33
|
+
// eslint.config.mjs
|
|
34
|
+
import { eslintConfig } from "@yasainet/eslint/node";
|
|
35
|
+
|
|
36
|
+
export default eslintConfig;
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Release
|
|
40
|
+
|
|
41
|
+
1. Update `version` in `package.json`
|
|
42
|
+
2. Commit and push to `main`
|
|
43
|
+
3. Create and push a tag:
|
|
44
|
+
|
|
45
|
+
```sh
|
|
46
|
+
git tag v1.0.0
|
|
47
|
+
git push --tags
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
4. GitHub Actions will automatically publish to npm
|
package/package.json
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@yasainet/eslint",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "ESLint",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"exports": {
|
|
7
|
+
"./next": "./src/next/index.mjs",
|
|
8
|
+
"./node": "./src/node/index.mjs"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"src"
|
|
12
|
+
],
|
|
13
|
+
"keywords": [
|
|
14
|
+
"eslint",
|
|
15
|
+
"eslintconfig",
|
|
16
|
+
"nextjs",
|
|
17
|
+
"nodejs",
|
|
18
|
+
"typescript"
|
|
19
|
+
],
|
|
20
|
+
"author": "yasainet",
|
|
21
|
+
"license": "MIT",
|
|
22
|
+
"publishConfig": {
|
|
23
|
+
"access": "public"
|
|
24
|
+
},
|
|
25
|
+
"dependencies": {
|
|
26
|
+
"@stylistic/eslint-plugin": "^5.9.0",
|
|
27
|
+
"eslint-plugin-check-file": "^3.3.1",
|
|
28
|
+
"eslint-plugin-jsdoc": "^62.7.1",
|
|
29
|
+
"eslint-plugin-simple-import-sort": "^12.1.1"
|
|
30
|
+
},
|
|
31
|
+
"peerDependencies": {
|
|
32
|
+
"eslint": "^9"
|
|
33
|
+
}
|
|
34
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import { fileURLToPath } from "url";
|
|
4
|
+
|
|
5
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
6
|
+
|
|
7
|
+
function findProjectRoot() {
|
|
8
|
+
let dir = __dirname;
|
|
9
|
+
while (dir !== path.dirname(dir)) {
|
|
10
|
+
if (
|
|
11
|
+
fs.existsSync(path.join(dir, "package.json")) &&
|
|
12
|
+
!dir.includes("/node_modules/")
|
|
13
|
+
) {
|
|
14
|
+
return dir;
|
|
15
|
+
}
|
|
16
|
+
dir = path.dirname(dir);
|
|
17
|
+
}
|
|
18
|
+
return process.cwd();
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const PROJECT_ROOT = findProjectRoot();
|
|
22
|
+
|
|
23
|
+
export const FEATURE_ROOTS = [
|
|
24
|
+
"src/features",
|
|
25
|
+
"scripts/features",
|
|
26
|
+
"supabase/functions/features",
|
|
27
|
+
];
|
|
28
|
+
|
|
29
|
+
const EXCLUDE_LIST = ["proxy.ts", "types"];
|
|
30
|
+
|
|
31
|
+
function generatePrefixLibMapping() {
|
|
32
|
+
const libDir = path.join(PROJECT_ROOT, "src/lib");
|
|
33
|
+
const mapping = {};
|
|
34
|
+
|
|
35
|
+
if (!fs.existsSync(libDir)) {
|
|
36
|
+
return mapping;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const entries = fs.readdirSync(libDir, { withFileTypes: true });
|
|
40
|
+
|
|
41
|
+
for (const entry of entries) {
|
|
42
|
+
if (EXCLUDE_LIST.includes(entry.name)) {
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (entry.isDirectory()) {
|
|
47
|
+
const subDir = path.join(libDir, entry.name);
|
|
48
|
+
const subEntries = fs.readdirSync(subDir, { withFileTypes: true });
|
|
49
|
+
|
|
50
|
+
for (const subEntry of subEntries) {
|
|
51
|
+
if (
|
|
52
|
+
subEntry.isFile() &&
|
|
53
|
+
subEntry.name.endsWith(".ts") &&
|
|
54
|
+
!EXCLUDE_LIST.includes(subEntry.name)
|
|
55
|
+
) {
|
|
56
|
+
const prefix = subEntry.name.replace(".ts", "");
|
|
57
|
+
mapping[prefix] = `@/lib/${entry.name}/${prefix}`;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
} else if (entry.isFile() && entry.name.endsWith(".ts")) {
|
|
61
|
+
const prefix = entry.name.replace(".ts", "");
|
|
62
|
+
mapping[prefix] = `@/lib/${prefix}`;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return mapping;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export const PREFIX_LIB_MAPPING = generatePrefixLibMapping();
|
|
70
|
+
|
|
71
|
+
export const featuresGlob = (subpath) =>
|
|
72
|
+
FEATURE_ROOTS.map((root) => `${root}/${subpath}`);
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import { PREFIX_LIB_MAPPING } from "./constants.mjs";
|
|
2
|
+
|
|
3
|
+
const LAYER_PATTERNS = {
|
|
4
|
+
repositories: [
|
|
5
|
+
{
|
|
6
|
+
group: ["*/services/*", "*/services"],
|
|
7
|
+
message: "repositories cannot import services (layer violation)",
|
|
8
|
+
},
|
|
9
|
+
{
|
|
10
|
+
group: ["*/actions/*", "*/actions"],
|
|
11
|
+
message: "repositories cannot import actions (layer violation)",
|
|
12
|
+
},
|
|
13
|
+
{
|
|
14
|
+
group: ["*/hooks/*", "*/hooks"],
|
|
15
|
+
message: "repositories cannot import hooks (layer violation)",
|
|
16
|
+
},
|
|
17
|
+
],
|
|
18
|
+
services: [
|
|
19
|
+
{
|
|
20
|
+
group: ["*/actions/*", "*/actions"],
|
|
21
|
+
message: "services cannot import actions (layer violation)",
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
group: ["*/hooks/*", "*/hooks"],
|
|
25
|
+
message: "services cannot import hooks (layer violation)",
|
|
26
|
+
},
|
|
27
|
+
],
|
|
28
|
+
actions: [
|
|
29
|
+
{
|
|
30
|
+
group: ["*/hooks/*", "*/hooks"],
|
|
31
|
+
message: "actions cannot import hooks (layer violation)",
|
|
32
|
+
},
|
|
33
|
+
],
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const LATERAL_PATTERNS = {
|
|
37
|
+
repositories: [
|
|
38
|
+
{
|
|
39
|
+
group: ["@/features/*/repositories/*", "@/features/*/repositories"],
|
|
40
|
+
message:
|
|
41
|
+
"repositories cannot import other feature's repositories (lateral violation)",
|
|
42
|
+
},
|
|
43
|
+
],
|
|
44
|
+
services: [
|
|
45
|
+
{
|
|
46
|
+
group: ["@/features/*/services/*", "@/features/*/services"],
|
|
47
|
+
message:
|
|
48
|
+
"services cannot import other feature's services (lateral violation)",
|
|
49
|
+
},
|
|
50
|
+
],
|
|
51
|
+
actions: [
|
|
52
|
+
{
|
|
53
|
+
group: ["@/features/*/actions/*", "@/features/*/actions"],
|
|
54
|
+
message:
|
|
55
|
+
"actions cannot import other feature's actions (lateral violation)",
|
|
56
|
+
},
|
|
57
|
+
],
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
const CARDINALITY_PATTERNS = {
|
|
61
|
+
server: [
|
|
62
|
+
{
|
|
63
|
+
group: ["**/services/client.service*", "**/services/admin.service*"],
|
|
64
|
+
message:
|
|
65
|
+
"server.action can only import server.service (cardinality violation)",
|
|
66
|
+
},
|
|
67
|
+
],
|
|
68
|
+
client: [
|
|
69
|
+
{
|
|
70
|
+
group: ["**/services/server.service*", "**/services/admin.service*"],
|
|
71
|
+
message:
|
|
72
|
+
"client.action can only import client.service (cardinality violation)",
|
|
73
|
+
},
|
|
74
|
+
],
|
|
75
|
+
admin: [
|
|
76
|
+
{
|
|
77
|
+
group: ["**/services/server.service*", "**/services/client.service*"],
|
|
78
|
+
message:
|
|
79
|
+
"admin.action can only import admin.service (cardinality violation)",
|
|
80
|
+
},
|
|
81
|
+
],
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
function prefixLibPatterns(prefix) {
|
|
85
|
+
const prefixes = Object.keys(PREFIX_LIB_MAPPING);
|
|
86
|
+
const allowedLib = PREFIX_LIB_MAPPING[prefix];
|
|
87
|
+
return prefixes
|
|
88
|
+
.filter((p) => p !== prefix)
|
|
89
|
+
.map((p) => ({
|
|
90
|
+
group: [PREFIX_LIB_MAPPING[p], `${PREFIX_LIB_MAPPING[p]}/*`],
|
|
91
|
+
message: `${prefix}.repo.ts can only import from ${allowedLib}. Use the correct repository file for this lib.`,
|
|
92
|
+
}));
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const LIB_BOUNDARY_PATTERNS = [
|
|
96
|
+
{
|
|
97
|
+
group: ["@/lib/*", "@/lib/**"],
|
|
98
|
+
message:
|
|
99
|
+
"@/lib/* can only be imported from repositories (lib-boundary violation)",
|
|
100
|
+
},
|
|
101
|
+
];
|
|
102
|
+
|
|
103
|
+
function makeConfig(name, files, ...patternArrays) {
|
|
104
|
+
const patterns = patternArrays.flat();
|
|
105
|
+
if (patterns.length === 0) return null;
|
|
106
|
+
return {
|
|
107
|
+
name: `imports/${name}`,
|
|
108
|
+
files,
|
|
109
|
+
rules: {
|
|
110
|
+
"no-restricted-imports": ["error", { patterns }],
|
|
111
|
+
},
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function generateImportConfigs() {
|
|
116
|
+
const configs = [];
|
|
117
|
+
|
|
118
|
+
configs.push({
|
|
119
|
+
name: "imports/lib-boundary",
|
|
120
|
+
files: ["src/**/*.{ts,tsx}"],
|
|
121
|
+
ignores: ["src/lib/**", "src/proxy.ts", "src/app/sitemap.ts"],
|
|
122
|
+
rules: {
|
|
123
|
+
"no-restricted-imports": ["error", { patterns: LIB_BOUNDARY_PATTERNS }],
|
|
124
|
+
},
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
configs.push(
|
|
128
|
+
makeConfig(
|
|
129
|
+
"repositories",
|
|
130
|
+
["**/repositories/*.ts"],
|
|
131
|
+
LAYER_PATTERNS.repositories,
|
|
132
|
+
LATERAL_PATTERNS.repositories,
|
|
133
|
+
),
|
|
134
|
+
);
|
|
135
|
+
|
|
136
|
+
for (const prefix of Object.keys(PREFIX_LIB_MAPPING)) {
|
|
137
|
+
const patterns = prefixLibPatterns(prefix);
|
|
138
|
+
if (patterns.length === 0) continue;
|
|
139
|
+
configs.push(
|
|
140
|
+
makeConfig(
|
|
141
|
+
`repositories/${prefix}`,
|
|
142
|
+
[`**/repositories/${prefix}.repo.ts`],
|
|
143
|
+
LAYER_PATTERNS.repositories,
|
|
144
|
+
LATERAL_PATTERNS.repositories,
|
|
145
|
+
patterns,
|
|
146
|
+
),
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
configs.push(
|
|
151
|
+
makeConfig(
|
|
152
|
+
"services",
|
|
153
|
+
["**/services/*.ts"],
|
|
154
|
+
LAYER_PATTERNS.services,
|
|
155
|
+
LATERAL_PATTERNS.services,
|
|
156
|
+
LIB_BOUNDARY_PATTERNS,
|
|
157
|
+
),
|
|
158
|
+
);
|
|
159
|
+
|
|
160
|
+
configs.push(
|
|
161
|
+
makeConfig(
|
|
162
|
+
"actions",
|
|
163
|
+
["**/actions/*.ts"],
|
|
164
|
+
LAYER_PATTERNS.actions,
|
|
165
|
+
LATERAL_PATTERNS.actions,
|
|
166
|
+
LIB_BOUNDARY_PATTERNS,
|
|
167
|
+
),
|
|
168
|
+
);
|
|
169
|
+
|
|
170
|
+
for (const prefix of ["server", "client", "admin"]) {
|
|
171
|
+
configs.push(
|
|
172
|
+
makeConfig(
|
|
173
|
+
`actions/${prefix}`,
|
|
174
|
+
[`**/actions/${prefix}.action.ts`],
|
|
175
|
+
LAYER_PATTERNS.actions,
|
|
176
|
+
LATERAL_PATTERNS.actions,
|
|
177
|
+
CARDINALITY_PATTERNS[prefix],
|
|
178
|
+
LIB_BOUNDARY_PATTERNS,
|
|
179
|
+
),
|
|
180
|
+
);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
return configs.filter(Boolean);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
export const importsConfigs = generateImportConfigs();
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { importsConfigs } from "./imports.mjs";
|
|
2
|
+
import { jsdocConfigs } from "./jsdoc.mjs";
|
|
3
|
+
import { layersConfigs } from "./layers.mjs";
|
|
4
|
+
import { namingConfigs } from "./naming.mjs";
|
|
5
|
+
import { rulesConfigs } from "./rules.mjs";
|
|
6
|
+
|
|
7
|
+
export const commonConfigs = [
|
|
8
|
+
...rulesConfigs,
|
|
9
|
+
...namingConfigs,
|
|
10
|
+
...layersConfigs,
|
|
11
|
+
...importsConfigs,
|
|
12
|
+
...jsdocConfigs,
|
|
13
|
+
];
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import jsdocPlugin from "eslint-plugin-jsdoc";
|
|
2
|
+
|
|
3
|
+
import { featuresGlob } from "./constants.mjs";
|
|
4
|
+
|
|
5
|
+
export const jsdocConfigs = [
|
|
6
|
+
{
|
|
7
|
+
name: "jsdoc",
|
|
8
|
+
files: [
|
|
9
|
+
...featuresGlob("**/repositories/*.ts"),
|
|
10
|
+
...featuresGlob("**/services*/*.ts"),
|
|
11
|
+
...featuresGlob("**/utils*/*.ts"),
|
|
12
|
+
],
|
|
13
|
+
plugins: {
|
|
14
|
+
jsdoc: jsdocPlugin,
|
|
15
|
+
},
|
|
16
|
+
rules: {
|
|
17
|
+
"jsdoc/require-jsdoc": [
|
|
18
|
+
"warn",
|
|
19
|
+
{
|
|
20
|
+
publicOnly: true,
|
|
21
|
+
require: {
|
|
22
|
+
FunctionDeclaration: true,
|
|
23
|
+
ArrowFunctionExpression: true,
|
|
24
|
+
FunctionExpression: true,
|
|
25
|
+
},
|
|
26
|
+
checkGetters: false,
|
|
27
|
+
checkSetters: false,
|
|
28
|
+
checkConstructors: false,
|
|
29
|
+
},
|
|
30
|
+
],
|
|
31
|
+
"jsdoc/require-description": [
|
|
32
|
+
"warn",
|
|
33
|
+
{
|
|
34
|
+
contexts: ["any"],
|
|
35
|
+
},
|
|
36
|
+
],
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
];
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
export const layersConfigs = [
|
|
2
|
+
{
|
|
3
|
+
name: "layers/repositories",
|
|
4
|
+
files: ["**/repositories/*.ts"],
|
|
5
|
+
rules: {
|
|
6
|
+
"no-restricted-syntax": [
|
|
7
|
+
"error",
|
|
8
|
+
{
|
|
9
|
+
selector: "TryStatement",
|
|
10
|
+
message:
|
|
11
|
+
"try-catch is not allowed in repositories. Error handling belongs in actions.",
|
|
12
|
+
},
|
|
13
|
+
{
|
|
14
|
+
selector: "IfStatement",
|
|
15
|
+
message:
|
|
16
|
+
"if statements are not allowed in repositories. Conditional logic belongs in services.",
|
|
17
|
+
},
|
|
18
|
+
],
|
|
19
|
+
},
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
name: "layers/services",
|
|
23
|
+
files: ["**/services/*.ts"],
|
|
24
|
+
rules: {
|
|
25
|
+
"no-restricted-syntax": [
|
|
26
|
+
"error",
|
|
27
|
+
{
|
|
28
|
+
selector: "TryStatement",
|
|
29
|
+
message:
|
|
30
|
+
"try-catch is not allowed in services. Error handling belongs in actions.",
|
|
31
|
+
},
|
|
32
|
+
],
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
];
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @description In *.action.ts, `handleXxx` must call the service method `*.xxx()`.
|
|
3
|
+
*/
|
|
4
|
+
export const actionHandleServiceRule = {
|
|
5
|
+
meta: {
|
|
6
|
+
type: "problem",
|
|
7
|
+
messages: {
|
|
8
|
+
missingCall:
|
|
9
|
+
"handleXxx must call the corresponding service method '*.{{ expected }}()'.",
|
|
10
|
+
},
|
|
11
|
+
schema: [],
|
|
12
|
+
},
|
|
13
|
+
create(context) {
|
|
14
|
+
return {
|
|
15
|
+
"ExportNamedDeclaration > FunctionDeclaration[id.name=/^handle[A-Z]/]"(
|
|
16
|
+
node,
|
|
17
|
+
) {
|
|
18
|
+
const name = node.id.name;
|
|
19
|
+
const suffix = name.slice("handle".length);
|
|
20
|
+
const expected = suffix[0].toLowerCase() + suffix.slice(1);
|
|
21
|
+
|
|
22
|
+
let found = false;
|
|
23
|
+
|
|
24
|
+
function walk(n) {
|
|
25
|
+
if (found || !n || typeof n !== "object") return;
|
|
26
|
+
|
|
27
|
+
if (
|
|
28
|
+
n.type === "CallExpression" &&
|
|
29
|
+
n.callee.type === "MemberExpression" &&
|
|
30
|
+
!n.callee.computed &&
|
|
31
|
+
n.callee.property.name === expected
|
|
32
|
+
) {
|
|
33
|
+
found = true;
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
for (const key of Object.keys(n)) {
|
|
38
|
+
if (key === "parent") continue;
|
|
39
|
+
const child = n[key];
|
|
40
|
+
if (Array.isArray(child)) {
|
|
41
|
+
for (const item of child) walk(item);
|
|
42
|
+
} else if (child && typeof child.type === "string") {
|
|
43
|
+
walk(child);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
walk(node.body);
|
|
49
|
+
|
|
50
|
+
if (!found) {
|
|
51
|
+
context.report({
|
|
52
|
+
node: node.id,
|
|
53
|
+
messageId: "missingCall",
|
|
54
|
+
data: { expected },
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
},
|
|
58
|
+
};
|
|
59
|
+
},
|
|
60
|
+
};
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import { featuresGlob, PREFIX_LIB_MAPPING } from "./constants.mjs";
|
|
2
|
+
import { checkFile } from "./plugins.mjs";
|
|
3
|
+
import { actionHandleServiceRule } from "./local-plugins/action-handle-service.mjs";
|
|
4
|
+
|
|
5
|
+
const prefixPattern = `@(${Object.keys(PREFIX_LIB_MAPPING).join("|")})`;
|
|
6
|
+
|
|
7
|
+
export const namingConfigs = [
|
|
8
|
+
{
|
|
9
|
+
name: "naming/services",
|
|
10
|
+
files: featuresGlob("**/services/*.ts"),
|
|
11
|
+
plugins: { "check-file": checkFile },
|
|
12
|
+
rules: {
|
|
13
|
+
"check-file/filename-naming-convention": [
|
|
14
|
+
"error",
|
|
15
|
+
{ "**/*.ts": `${prefixPattern}.service` },
|
|
16
|
+
],
|
|
17
|
+
},
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
name: "naming/repositories",
|
|
21
|
+
files: featuresGlob("**/repositories/*.ts"),
|
|
22
|
+
plugins: { "check-file": checkFile },
|
|
23
|
+
rules: {
|
|
24
|
+
"check-file/filename-naming-convention": [
|
|
25
|
+
"error",
|
|
26
|
+
{ "**/*.ts": `${prefixPattern}.repo` },
|
|
27
|
+
],
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
name: "naming/types",
|
|
32
|
+
files: featuresGlob("*/types/*.type.ts"),
|
|
33
|
+
ignores: featuresGlob("shared/types/*.ts"),
|
|
34
|
+
plugins: { "check-file": checkFile },
|
|
35
|
+
rules: {
|
|
36
|
+
"check-file/filename-naming-convention": [
|
|
37
|
+
"error",
|
|
38
|
+
{ "**/*/types/*.ts": "<1>" },
|
|
39
|
+
{ ignoreMiddleExtensions: true },
|
|
40
|
+
],
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
name: "naming/types-shared",
|
|
45
|
+
files: featuresGlob("shared/types/*.ts"),
|
|
46
|
+
plugins: { "check-file": checkFile },
|
|
47
|
+
rules: {
|
|
48
|
+
"check-file/filename-naming-convention": [
|
|
49
|
+
"error",
|
|
50
|
+
{ "**/*.ts": "+([a-z0-9_-]).type" },
|
|
51
|
+
],
|
|
52
|
+
},
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
name: "naming/schemas",
|
|
56
|
+
files: featuresGlob("**/schemas/*.ts"),
|
|
57
|
+
plugins: { "check-file": checkFile },
|
|
58
|
+
rules: {
|
|
59
|
+
"check-file/filename-naming-convention": [
|
|
60
|
+
"error",
|
|
61
|
+
{ "**/*.ts": "+([a-z0-9-]).schema" },
|
|
62
|
+
],
|
|
63
|
+
},
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
name: "naming/utils",
|
|
67
|
+
files: featuresGlob("*/utils/*.utils.ts"),
|
|
68
|
+
ignores: featuresGlob("shared/utils/*.ts"),
|
|
69
|
+
plugins: { "check-file": checkFile },
|
|
70
|
+
rules: {
|
|
71
|
+
"check-file/filename-naming-convention": [
|
|
72
|
+
"error",
|
|
73
|
+
{ "**/*/utils/*.ts": "<1>" },
|
|
74
|
+
{ ignoreMiddleExtensions: true },
|
|
75
|
+
],
|
|
76
|
+
},
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
name: "naming/utils-shared",
|
|
80
|
+
files: featuresGlob("shared/utils/*.ts"),
|
|
81
|
+
plugins: { "check-file": checkFile },
|
|
82
|
+
rules: {
|
|
83
|
+
"check-file/filename-naming-convention": [
|
|
84
|
+
"error",
|
|
85
|
+
{ "**/*.ts": "+([a-z0-9_-]).utils" },
|
|
86
|
+
],
|
|
87
|
+
},
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
name: "naming/constants",
|
|
91
|
+
files: featuresGlob("**/constants/*.ts"),
|
|
92
|
+
plugins: { "check-file": checkFile },
|
|
93
|
+
rules: {
|
|
94
|
+
"check-file/filename-naming-convention": [
|
|
95
|
+
"error",
|
|
96
|
+
{ "**/*.ts": "+([a-z0-9-]).constant" },
|
|
97
|
+
],
|
|
98
|
+
},
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
name: "naming/actions",
|
|
102
|
+
files: featuresGlob("**/actions/*.ts"),
|
|
103
|
+
plugins: { "check-file": checkFile },
|
|
104
|
+
rules: {
|
|
105
|
+
"check-file/filename-naming-convention": [
|
|
106
|
+
"error",
|
|
107
|
+
{ "**/*.ts": `${prefixPattern}.action` },
|
|
108
|
+
],
|
|
109
|
+
},
|
|
110
|
+
},
|
|
111
|
+
{
|
|
112
|
+
name: "naming/actions-export",
|
|
113
|
+
files: featuresGlob("**/actions/*.ts"),
|
|
114
|
+
rules: {
|
|
115
|
+
"no-restricted-syntax": [
|
|
116
|
+
"error",
|
|
117
|
+
{
|
|
118
|
+
selector:
|
|
119
|
+
"ExportNamedDeclaration > FunctionDeclaration[id.name!=/^handle[A-Z]/]",
|
|
120
|
+
message:
|
|
121
|
+
"Exported functions in actions must start with 'handle' (e.g., handleGetComics).",
|
|
122
|
+
},
|
|
123
|
+
],
|
|
124
|
+
},
|
|
125
|
+
},
|
|
126
|
+
{
|
|
127
|
+
name: "naming/actions-handle-service",
|
|
128
|
+
files: featuresGlob("**/actions/*.ts"),
|
|
129
|
+
plugins: {
|
|
130
|
+
local: { rules: { "action-handle-service": actionHandleServiceRule } },
|
|
131
|
+
},
|
|
132
|
+
rules: {
|
|
133
|
+
"local/action-handle-service": "error",
|
|
134
|
+
},
|
|
135
|
+
},
|
|
136
|
+
{
|
|
137
|
+
name: "naming/features-ts-only",
|
|
138
|
+
files: featuresGlob("**/*.tsx"),
|
|
139
|
+
rules: {
|
|
140
|
+
"no-restricted-syntax": [
|
|
141
|
+
"error",
|
|
142
|
+
{
|
|
143
|
+
selector: "Program",
|
|
144
|
+
message:
|
|
145
|
+
"features/ must only contain .ts files. Components belong in src/components/.",
|
|
146
|
+
},
|
|
147
|
+
],
|
|
148
|
+
},
|
|
149
|
+
},
|
|
150
|
+
];
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import stylistic from "@stylistic/eslint-plugin";
|
|
2
|
+
import checkFile from "eslint-plugin-check-file";
|
|
3
|
+
import jsdocPlugin from "eslint-plugin-jsdoc";
|
|
4
|
+
import simpleImportSortPlugin from "eslint-plugin-simple-import-sort";
|
|
5
|
+
|
|
6
|
+
export const plugins = {
|
|
7
|
+
"@stylistic": stylistic,
|
|
8
|
+
"check-file": checkFile,
|
|
9
|
+
jsdoc: jsdocPlugin,
|
|
10
|
+
"simple-import-sort": simpleImportSortPlugin,
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export { checkFile, jsdocPlugin, simpleImportSortPlugin, stylistic };
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { simpleImportSortPlugin, stylistic } from "./plugins.mjs";
|
|
2
|
+
|
|
3
|
+
export const rulesConfigs = [
|
|
4
|
+
{
|
|
5
|
+
name: "rules/shared",
|
|
6
|
+
plugins: {
|
|
7
|
+
"@stylistic": stylistic,
|
|
8
|
+
"simple-import-sort": simpleImportSortPlugin,
|
|
9
|
+
},
|
|
10
|
+
rules: {
|
|
11
|
+
"@typescript-eslint/no-unused-vars": [
|
|
12
|
+
"error",
|
|
13
|
+
{
|
|
14
|
+
argsIgnorePattern: "^_",
|
|
15
|
+
varsIgnorePattern: "^_",
|
|
16
|
+
caughtErrorsIgnorePattern: "^_",
|
|
17
|
+
},
|
|
18
|
+
],
|
|
19
|
+
"@typescript-eslint/consistent-type-imports": [
|
|
20
|
+
"error",
|
|
21
|
+
{ prefer: "type-imports" },
|
|
22
|
+
],
|
|
23
|
+
"@typescript-eslint/no-explicit-any": "warn",
|
|
24
|
+
"no-console": "warn",
|
|
25
|
+
"no-irregular-whitespace": [
|
|
26
|
+
"warn",
|
|
27
|
+
{
|
|
28
|
+
skipStrings: false,
|
|
29
|
+
skipComments: false,
|
|
30
|
+
skipRegExps: false,
|
|
31
|
+
skipTemplates: false,
|
|
32
|
+
},
|
|
33
|
+
],
|
|
34
|
+
"simple-import-sort/imports": "warn",
|
|
35
|
+
"simple-import-sort/exports": "warn",
|
|
36
|
+
"@stylistic/quotes": ["warn", "double", { avoidEscape: true }],
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
];
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
export const directivesConfigs = [
|
|
2
|
+
{
|
|
3
|
+
name: "directives/server-action",
|
|
4
|
+
files: ["src/features/**/actions/server.action.ts"],
|
|
5
|
+
rules: {
|
|
6
|
+
"no-restricted-syntax": [
|
|
7
|
+
"error",
|
|
8
|
+
{
|
|
9
|
+
selector:
|
|
10
|
+
"Program > :first-child:not(ExpressionStatement[expression.value='use server'])",
|
|
11
|
+
message: 'server.action.ts must start with "use server" directive.',
|
|
12
|
+
},
|
|
13
|
+
],
|
|
14
|
+
},
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
name: "directives/admin-action",
|
|
18
|
+
files: ["src/features/**/actions/admin.action.ts"],
|
|
19
|
+
rules: {
|
|
20
|
+
"no-restricted-syntax": [
|
|
21
|
+
"error",
|
|
22
|
+
{
|
|
23
|
+
selector:
|
|
24
|
+
"Program > :first-child:not(ExpressionStatement[expression.value='use server'])",
|
|
25
|
+
message: 'admin.action.ts must start with "use server" directive.',
|
|
26
|
+
},
|
|
27
|
+
],
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
name: "directives/client-action",
|
|
32
|
+
files: ["src/features/**/actions/client.action.ts"],
|
|
33
|
+
rules: {
|
|
34
|
+
"no-restricted-syntax": [
|
|
35
|
+
"error",
|
|
36
|
+
{
|
|
37
|
+
selector: "ExpressionStatement[expression.value='use server']",
|
|
38
|
+
message:
|
|
39
|
+
'client.action.ts must NOT have "use server" directive. It uses @/lib/supabase/client.',
|
|
40
|
+
},
|
|
41
|
+
],
|
|
42
|
+
},
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
name: "directives/hooks",
|
|
46
|
+
files: ["src/features/**/hooks/*.ts"],
|
|
47
|
+
rules: {
|
|
48
|
+
"no-restricted-syntax": [
|
|
49
|
+
"error",
|
|
50
|
+
{
|
|
51
|
+
selector:
|
|
52
|
+
"Program > :first-child:not(ExpressionStatement[expression.value='use client'])",
|
|
53
|
+
message: 'Hooks must start with "use client" directive.',
|
|
54
|
+
},
|
|
55
|
+
],
|
|
56
|
+
},
|
|
57
|
+
},
|
|
58
|
+
];
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { commonConfigs } from "../common/index.mjs";
|
|
2
|
+
|
|
3
|
+
import { directivesConfigs } from "./directives.mjs";
|
|
4
|
+
import { namingConfigs } from "./naming.mjs";
|
|
5
|
+
|
|
6
|
+
export const eslintConfig = [
|
|
7
|
+
...commonConfigs,
|
|
8
|
+
...namingConfigs,
|
|
9
|
+
...directivesConfigs,
|
|
10
|
+
];
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { featuresGlob } from "../common/constants.mjs";
|
|
2
|
+
import { checkFile } from "../common/plugins.mjs";
|
|
3
|
+
|
|
4
|
+
export const namingConfigs = [
|
|
5
|
+
{
|
|
6
|
+
name: "naming/hooks",
|
|
7
|
+
files: featuresGlob("**/hooks/*.ts"),
|
|
8
|
+
plugins: { "check-file": checkFile },
|
|
9
|
+
rules: {
|
|
10
|
+
"check-file/filename-naming-convention": [
|
|
11
|
+
"error",
|
|
12
|
+
{ "**/*.ts": "use[A-Z]*([a-zA-Z0-9])" },
|
|
13
|
+
{ ignoreMiddleExtensions: true },
|
|
14
|
+
],
|
|
15
|
+
},
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
name: "naming/hooks-export",
|
|
19
|
+
files: featuresGlob("**/hooks/*.ts"),
|
|
20
|
+
rules: {
|
|
21
|
+
"no-restricted-syntax": [
|
|
22
|
+
"error",
|
|
23
|
+
{
|
|
24
|
+
selector:
|
|
25
|
+
"ExportNamedDeclaration > FunctionDeclaration[id.name!=/^use[A-Z]/]",
|
|
26
|
+
message:
|
|
27
|
+
"Exported functions in hooks must start with 'use' (e.g., useAuth).",
|
|
28
|
+
},
|
|
29
|
+
],
|
|
30
|
+
},
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
name: "naming/components-tsx-only",
|
|
34
|
+
files: ["src/components/**/*.ts"],
|
|
35
|
+
rules: {
|
|
36
|
+
"no-restricted-syntax": [
|
|
37
|
+
"error",
|
|
38
|
+
{
|
|
39
|
+
selector: "Program",
|
|
40
|
+
message:
|
|
41
|
+
"components/ must only contain .tsx files. Logic belongs in src/features/.",
|
|
42
|
+
},
|
|
43
|
+
],
|
|
44
|
+
},
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
name: "naming/components-pascal-case",
|
|
48
|
+
files: ["src/components/**/*.tsx"],
|
|
49
|
+
ignores: ["src/components/shared/ui/**"],
|
|
50
|
+
plugins: { "check-file": checkFile },
|
|
51
|
+
rules: {
|
|
52
|
+
"check-file/filename-naming-convention": [
|
|
53
|
+
"error",
|
|
54
|
+
{ "**/*.tsx": "PASCAL_CASE" },
|
|
55
|
+
],
|
|
56
|
+
},
|
|
57
|
+
},
|
|
58
|
+
];
|