@yasainet/eslint 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/package.json +30 -0
- package/src/base.mjs +74 -0
- package/src/cardinality.mjs +71 -0
- package/src/constants.mjs +99 -0
- package/src/directives.mjs +72 -0
- package/src/imports.mjs +58 -0
- package/src/index.mjs +33 -0
- package/src/layers.mjs +160 -0
- package/src/naming.mjs +152 -0
- package/src/plugins.mjs +26 -0
package/package.json
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@yasainet/eslint",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Shared ESLint configuration for Next.js projects with feature-based architecture",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"exports": {
|
|
7
|
+
".": "./src/index.mjs"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"src"
|
|
11
|
+
],
|
|
12
|
+
"keywords": [
|
|
13
|
+
"eslint",
|
|
14
|
+
"eslintconfig",
|
|
15
|
+
"nextjs",
|
|
16
|
+
"typescript"
|
|
17
|
+
],
|
|
18
|
+
"author": "yasainet",
|
|
19
|
+
"license": "MIT",
|
|
20
|
+
"dependencies": {
|
|
21
|
+
"@stylistic/eslint-plugin": "^5.9.0",
|
|
22
|
+
"eslint-plugin-check-file": "^3.3.1",
|
|
23
|
+
"eslint-plugin-react-you-might-not-need-an-effect": "^0.5.6",
|
|
24
|
+
"eslint-plugin-simple-import-sort": "^12.1.1"
|
|
25
|
+
},
|
|
26
|
+
"peerDependencies": {
|
|
27
|
+
"eslint": "^9",
|
|
28
|
+
"eslint-config-next": "^15 || ^16"
|
|
29
|
+
}
|
|
30
|
+
}
|
package/src/base.mjs
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Base ESLint configurations including Next.js presets and shared rules.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { globalIgnores } from "eslint/config";
|
|
6
|
+
import nextVitals from "eslint-config-next/core-web-vitals";
|
|
7
|
+
import nextTs from "eslint-config-next/typescript";
|
|
8
|
+
|
|
9
|
+
import {
|
|
10
|
+
reactYouMightNotNeedAnEffect,
|
|
11
|
+
simpleImportSortPlugin,
|
|
12
|
+
stylistic,
|
|
13
|
+
} from "./plugins.mjs";
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Next.js base configurations (core-web-vitals + typescript).
|
|
17
|
+
* @type {import("eslint").Linter.Config[]}
|
|
18
|
+
*/
|
|
19
|
+
export const baseConfigs = [...nextVitals, ...nextTs];
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Global ignore patterns for build outputs and generated files.
|
|
23
|
+
* @type {import("eslint").Linter.Config}
|
|
24
|
+
*/
|
|
25
|
+
export const ignoresConfig = globalIgnores([
|
|
26
|
+
".backup/**", // NOTE: Not a default.
|
|
27
|
+
".next/**",
|
|
28
|
+
"out/**",
|
|
29
|
+
"build/**",
|
|
30
|
+
"next-env.d.ts",
|
|
31
|
+
]);
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Shared rules applied to all files.
|
|
35
|
+
* Includes stylistic rules, import sorting, and TypeScript best practices.
|
|
36
|
+
* @type {import("eslint").Linter.Config}
|
|
37
|
+
*/
|
|
38
|
+
export const sharedRulesConfig = {
|
|
39
|
+
name: "shared-rules",
|
|
40
|
+
plugins: {
|
|
41
|
+
"@stylistic": stylistic,
|
|
42
|
+
"react-you-might-not-need-an-effect": reactYouMightNotNeedAnEffect,
|
|
43
|
+
"simple-import-sort": simpleImportSortPlugin,
|
|
44
|
+
},
|
|
45
|
+
rules: {
|
|
46
|
+
...reactYouMightNotNeedAnEffect.configs.recommended.rules,
|
|
47
|
+
"@typescript-eslint/no-unused-vars": [
|
|
48
|
+
"error",
|
|
49
|
+
{
|
|
50
|
+
argsIgnorePattern: "^_",
|
|
51
|
+
varsIgnorePattern: "^_",
|
|
52
|
+
caughtErrorsIgnorePattern: "^_",
|
|
53
|
+
},
|
|
54
|
+
],
|
|
55
|
+
"@typescript-eslint/consistent-type-imports": [
|
|
56
|
+
"error",
|
|
57
|
+
{ prefer: "type-imports" },
|
|
58
|
+
],
|
|
59
|
+
"@typescript-eslint/no-explicit-any": "warn",
|
|
60
|
+
"no-console": "warn",
|
|
61
|
+
"no-irregular-whitespace": [
|
|
62
|
+
"warn",
|
|
63
|
+
{
|
|
64
|
+
skipStrings: false,
|
|
65
|
+
skipComments: false,
|
|
66
|
+
skipRegExps: false,
|
|
67
|
+
skipTemplates: false,
|
|
68
|
+
},
|
|
69
|
+
],
|
|
70
|
+
"simple-import-sort/imports": "warn",
|
|
71
|
+
"simple-import-sort/exports": "warn",
|
|
72
|
+
"@stylistic/quotes": ["warn", "double", { avoidEscape: true }],
|
|
73
|
+
},
|
|
74
|
+
};
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Cardinality constraints for action-domain relationships.
|
|
3
|
+
*
|
|
4
|
+
* Each action file can only import its matching domain:
|
|
5
|
+
* - server.action.ts → server.domain.ts only
|
|
6
|
+
* - client.action.ts → client.domain.ts only
|
|
7
|
+
* - admin.action.ts → admin.domain.ts only
|
|
8
|
+
*
|
|
9
|
+
* This prevents mixing server-side and client-side logic.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Cardinality constraint configurations.
|
|
14
|
+
* @type {import("eslint").Linter.Config[]}
|
|
15
|
+
*/
|
|
16
|
+
export const cardinalityConfigs = [
|
|
17
|
+
{
|
|
18
|
+
name: "cardinality/server-action",
|
|
19
|
+
files: ["**/actions/server.action.ts"],
|
|
20
|
+
rules: {
|
|
21
|
+
"no-restricted-imports": [
|
|
22
|
+
"error",
|
|
23
|
+
{
|
|
24
|
+
patterns: [
|
|
25
|
+
{
|
|
26
|
+
group: ["**/domains/client.domain*", "**/domains/admin.domain*"],
|
|
27
|
+
message:
|
|
28
|
+
"server.action can only import server.domain (cardinality violation)",
|
|
29
|
+
},
|
|
30
|
+
],
|
|
31
|
+
},
|
|
32
|
+
],
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
name: "cardinality/client-action",
|
|
37
|
+
files: ["**/actions/client.action.ts"],
|
|
38
|
+
rules: {
|
|
39
|
+
"no-restricted-imports": [
|
|
40
|
+
"error",
|
|
41
|
+
{
|
|
42
|
+
patterns: [
|
|
43
|
+
{
|
|
44
|
+
group: ["**/domains/server.domain*", "**/domains/admin.domain*"],
|
|
45
|
+
message:
|
|
46
|
+
"client.action can only import client.domain (cardinality violation)",
|
|
47
|
+
},
|
|
48
|
+
],
|
|
49
|
+
},
|
|
50
|
+
],
|
|
51
|
+
},
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
name: "cardinality/admin-action",
|
|
55
|
+
files: ["**/actions/admin.action.ts"],
|
|
56
|
+
rules: {
|
|
57
|
+
"no-restricted-imports": [
|
|
58
|
+
"error",
|
|
59
|
+
{
|
|
60
|
+
patterns: [
|
|
61
|
+
{
|
|
62
|
+
group: ["**/domains/server.domain*", "**/domains/client.domain*"],
|
|
63
|
+
message:
|
|
64
|
+
"admin.action can only import admin.domain (cardinality violation)",
|
|
65
|
+
},
|
|
66
|
+
],
|
|
67
|
+
},
|
|
68
|
+
],
|
|
69
|
+
},
|
|
70
|
+
},
|
|
71
|
+
];
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Shared constants and helpers for ESLint configuration.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import fs from "fs";
|
|
6
|
+
import path from "path";
|
|
7
|
+
|
|
8
|
+
/** Project root directory (resolved from the consuming project's CWD) */
|
|
9
|
+
const PROJECT_ROOT = process.cwd();
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Root directories containing feature modules.
|
|
13
|
+
* @type {string[]}
|
|
14
|
+
*/
|
|
15
|
+
export const FEATURE_ROOTS = [
|
|
16
|
+
"src/features",
|
|
17
|
+
"scripts/features",
|
|
18
|
+
"supabase/functions/features",
|
|
19
|
+
];
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Files and directories to exclude from prefix mapping.
|
|
23
|
+
* @type {string[]}
|
|
24
|
+
*/
|
|
25
|
+
const EXCLUDE_LIST = ["proxy.ts", "types"];
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Generate PREFIX_LIB_MAPPING by scanning src/lib/ directory.
|
|
29
|
+
*
|
|
30
|
+
* @returns {Record<string, string>} Mapping of prefix to lib import path.
|
|
31
|
+
* @example
|
|
32
|
+
* - src/lib/supabase/server.ts → { server: "@/lib/supabase/server" }
|
|
33
|
+
* - src/lib/supabase/client.ts → { server: "@/lib/supabase/client" }
|
|
34
|
+
*/
|
|
35
|
+
function generatePrefixLibMapping() {
|
|
36
|
+
const libDir = path.join(PROJECT_ROOT, "src/lib");
|
|
37
|
+
const mapping = {};
|
|
38
|
+
|
|
39
|
+
if (!fs.existsSync(libDir)) {
|
|
40
|
+
return mapping;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const entries = fs.readdirSync(libDir, { withFileTypes: true });
|
|
44
|
+
|
|
45
|
+
for (const entry of entries) {
|
|
46
|
+
if (EXCLUDE_LIST.includes(entry.name)) {
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (entry.isDirectory()) {
|
|
51
|
+
// Scan subdirectory (e.g., src/lib/supabase/)
|
|
52
|
+
const subDir = path.join(libDir, entry.name);
|
|
53
|
+
const subEntries = fs.readdirSync(subDir, { withFileTypes: true });
|
|
54
|
+
|
|
55
|
+
for (const subEntry of subEntries) {
|
|
56
|
+
if (
|
|
57
|
+
subEntry.isFile() &&
|
|
58
|
+
subEntry.name.endsWith(".ts") &&
|
|
59
|
+
!EXCLUDE_LIST.includes(subEntry.name)
|
|
60
|
+
) {
|
|
61
|
+
const prefix = subEntry.name.replace(".ts", "");
|
|
62
|
+
mapping[prefix] = `@/lib/${entry.name}/${prefix}`;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
} else if (entry.isFile() && entry.name.endsWith(".ts")) {
|
|
66
|
+
const prefix = entry.name.replace(".ts", "");
|
|
67
|
+
mapping[prefix] = `@/lib/${prefix}`;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return mapping;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Mapping of file prefixes to their corresponding lib imports.
|
|
76
|
+
*
|
|
77
|
+
* Dynamically generated by scanning src/lib/ directory.
|
|
78
|
+
* Used by:
|
|
79
|
+
* - naming.mjs: Validates file naming (e.g., server.repo.ts, client.repo.ts)
|
|
80
|
+
* - imports.mjs: Enforces import restrictions
|
|
81
|
+
*
|
|
82
|
+
* @example
|
|
83
|
+
* server.repo.ts → can only import @/lib/supabase/server
|
|
84
|
+
* client.repo.ts → can only import @/lib/supabase/client
|
|
85
|
+
*
|
|
86
|
+
* @type {Record<string, string>}
|
|
87
|
+
*/
|
|
88
|
+
export const PREFIX_LIB_MAPPING = generatePrefixLibMapping();
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Creates glob patterns for all feature roots.
|
|
92
|
+
* @param {string} subpath - The subpath to append to each feature root.
|
|
93
|
+
* @returns {string[]} Array of glob patterns.
|
|
94
|
+
* @example
|
|
95
|
+
* featuresGlob("**\/actions/*.ts")
|
|
96
|
+
* // => ["src/features/**\/actions/*.ts", "scripts/features/**\/actions/*.ts", ...]
|
|
97
|
+
*/
|
|
98
|
+
export const featuresGlob = (subpath) =>
|
|
99
|
+
FEATURE_ROOTS.map((root) => `${root}/${subpath}`);
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Directive requirements for React Server Components.
|
|
3
|
+
*
|
|
4
|
+
* Ensures correct usage of "use server" and "use client" directives:
|
|
5
|
+
* - server.action.ts: Must start with "use server"
|
|
6
|
+
* - admin.action.ts: Must start with "use server"
|
|
7
|
+
* - client.action.ts: Must NOT have "use server"
|
|
8
|
+
* - hooks/*.ts: Must start with "use client"
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Directive requirement configurations.
|
|
13
|
+
* @type {import("eslint").Linter.Config[]}
|
|
14
|
+
*/
|
|
15
|
+
export const directivesConfigs = [
|
|
16
|
+
{
|
|
17
|
+
name: "directives/server-action",
|
|
18
|
+
files: ["src/features/**/actions/server.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: 'server.action.ts must start with "use server" directive.',
|
|
26
|
+
},
|
|
27
|
+
],
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
name: "directives/admin-action",
|
|
32
|
+
files: ["src/features/**/actions/admin.action.ts"],
|
|
33
|
+
rules: {
|
|
34
|
+
"no-restricted-syntax": [
|
|
35
|
+
"error",
|
|
36
|
+
{
|
|
37
|
+
selector:
|
|
38
|
+
"Program > :first-child:not(ExpressionStatement[expression.value='use server'])",
|
|
39
|
+
message: 'admin.action.ts must start with "use server" directive.',
|
|
40
|
+
},
|
|
41
|
+
],
|
|
42
|
+
},
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
name: "directives/client-action",
|
|
46
|
+
files: ["src/features/**/actions/client.action.ts"],
|
|
47
|
+
rules: {
|
|
48
|
+
"no-restricted-syntax": [
|
|
49
|
+
"error",
|
|
50
|
+
{
|
|
51
|
+
selector: "ExpressionStatement[expression.value='use server']",
|
|
52
|
+
message:
|
|
53
|
+
'client.action.ts must NOT have "use server" directive. It uses @/lib/supabase/client.',
|
|
54
|
+
},
|
|
55
|
+
],
|
|
56
|
+
},
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
name: "directives/hooks",
|
|
60
|
+
files: ["src/features/**/hooks/*.ts"],
|
|
61
|
+
rules: {
|
|
62
|
+
"no-restricted-syntax": [
|
|
63
|
+
"error",
|
|
64
|
+
{
|
|
65
|
+
selector:
|
|
66
|
+
"Program > :first-child:not(ExpressionStatement[expression.value='use client'])",
|
|
67
|
+
message: 'Hooks must start with "use client" directive.',
|
|
68
|
+
},
|
|
69
|
+
],
|
|
70
|
+
},
|
|
71
|
+
},
|
|
72
|
+
];
|
package/src/imports.mjs
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Import restrictions for repository files.
|
|
3
|
+
*
|
|
4
|
+
* Enforces that each {prefix}.repo.ts can only import its corresponding lib:
|
|
5
|
+
* - server.repo.ts → @/lib/supabase/server only
|
|
6
|
+
* - client.repo.ts → @/lib/supabase/client only
|
|
7
|
+
*
|
|
8
|
+
* This prevents mixing different data sources in a single repository file.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { PREFIX_LIB_MAPPING } from "./constants.mjs";
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Generate import restriction configs for each prefix.
|
|
15
|
+
*
|
|
16
|
+
* For each prefix (e.g., "server"), creates a rule that forbids importing
|
|
17
|
+
* any other lib from PREFIX_LIB_MAPPING.
|
|
18
|
+
*
|
|
19
|
+
* @returns {import("eslint").Linter.Config[]}
|
|
20
|
+
*/
|
|
21
|
+
function generateImportConfigs() {
|
|
22
|
+
const prefixes = Object.keys(PREFIX_LIB_MAPPING);
|
|
23
|
+
const configs = [];
|
|
24
|
+
|
|
25
|
+
for (const prefix of prefixes) {
|
|
26
|
+
const allowedLib = PREFIX_LIB_MAPPING[prefix];
|
|
27
|
+
const forbiddenLibs = prefixes
|
|
28
|
+
.filter((p) => p !== prefix)
|
|
29
|
+
.map((p) => PREFIX_LIB_MAPPING[p]);
|
|
30
|
+
|
|
31
|
+
// Skip if no forbidden libs (shouldn't happen, but safety check)
|
|
32
|
+
if (forbiddenLibs.length === 0) continue;
|
|
33
|
+
|
|
34
|
+
configs.push({
|
|
35
|
+
name: `imports/${prefix}-repo`,
|
|
36
|
+
files: [`**/repositories/${prefix}.repo.ts`],
|
|
37
|
+
rules: {
|
|
38
|
+
"no-restricted-imports": [
|
|
39
|
+
"error",
|
|
40
|
+
{
|
|
41
|
+
patterns: forbiddenLibs.map((lib) => ({
|
|
42
|
+
group: [`${lib}`, `${lib}/*`],
|
|
43
|
+
message: `${prefix}.repo.ts can only import from ${allowedLib}. Use the correct repository file for this lib.`,
|
|
44
|
+
})),
|
|
45
|
+
},
|
|
46
|
+
],
|
|
47
|
+
},
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return configs;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Import restriction configurations for repository files.
|
|
56
|
+
* @type {import("eslint").Linter.Config[]}
|
|
57
|
+
*/
|
|
58
|
+
export const importsConfigs = generateImportConfigs();
|
package/src/index.mjs
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Main ESLint configuration aggregator.
|
|
3
|
+
*
|
|
4
|
+
* Combines all configuration modules:
|
|
5
|
+
* - base: Next.js presets and shared rules
|
|
6
|
+
* - naming: File naming conventions
|
|
7
|
+
* - layers: Layer architecture constraints
|
|
8
|
+
* - cardinality: Action-domain relationships
|
|
9
|
+
* - directives: "use server" / "use client" requirements
|
|
10
|
+
* - imports: Repository import restrictions (prefix → lib mapping)
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { baseConfigs, ignoresConfig, sharedRulesConfig } from "./base.mjs";
|
|
14
|
+
import { cardinalityConfigs } from "./cardinality.mjs";
|
|
15
|
+
import { directivesConfigs } from "./directives.mjs";
|
|
16
|
+
import { importsConfigs } from "./imports.mjs";
|
|
17
|
+
import { layersConfigs } from "./layers.mjs";
|
|
18
|
+
import { namingConfigs } from "./naming.mjs";
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Complete ESLint configuration array.
|
|
22
|
+
* @type {import("eslint").Linter.Config[]}
|
|
23
|
+
*/
|
|
24
|
+
export const eslintConfig = [
|
|
25
|
+
...baseConfigs,
|
|
26
|
+
ignoresConfig,
|
|
27
|
+
sharedRulesConfig,
|
|
28
|
+
...namingConfigs,
|
|
29
|
+
...layersConfigs,
|
|
30
|
+
...cardinalityConfigs,
|
|
31
|
+
...directivesConfigs,
|
|
32
|
+
...importsConfigs,
|
|
33
|
+
];
|
package/src/layers.mjs
ADDED
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Layer architecture constraints.
|
|
3
|
+
*
|
|
4
|
+
* Enforces the following dependency rules:
|
|
5
|
+
*
|
|
6
|
+
* Layer hierarchy (top to bottom):
|
|
7
|
+
* hooks → actions → domains → repositories
|
|
8
|
+
*
|
|
9
|
+
* Rules:
|
|
10
|
+
* - Repositories: Cannot import domains, actions, or hooks.
|
|
11
|
+
* Cannot use try-catch or if statements.
|
|
12
|
+
* - Domains: Cannot import actions or hooks.
|
|
13
|
+
* Cannot use try-catch.
|
|
14
|
+
* - Actions: Cannot import hooks.
|
|
15
|
+
* Exported functions must start with "handle".
|
|
16
|
+
* - Hooks: Exported functions must start with "use".
|
|
17
|
+
*
|
|
18
|
+
* Cross-feature imports are also prohibited within the same layer.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import { featuresGlob } from "./constants.mjs";
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Layer constraint configurations.
|
|
25
|
+
* @type {import("eslint").Linter.Config[]}
|
|
26
|
+
*/
|
|
27
|
+
export const layersConfigs = [
|
|
28
|
+
{
|
|
29
|
+
name: "layers/repositories",
|
|
30
|
+
files: ["**/repositories/*.ts"],
|
|
31
|
+
rules: {
|
|
32
|
+
"no-restricted-syntax": [
|
|
33
|
+
"error",
|
|
34
|
+
{
|
|
35
|
+
selector: "TryStatement",
|
|
36
|
+
message:
|
|
37
|
+
"try-catch is not allowed in repositories. Error handling belongs in actions.",
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
selector: "IfStatement",
|
|
41
|
+
message:
|
|
42
|
+
"if statements are not allowed in repositories. Conditional logic belongs in domain.",
|
|
43
|
+
},
|
|
44
|
+
],
|
|
45
|
+
"no-restricted-imports": [
|
|
46
|
+
"error",
|
|
47
|
+
{
|
|
48
|
+
patterns: [
|
|
49
|
+
{
|
|
50
|
+
group: ["*/domains/*", "*/domains"],
|
|
51
|
+
message: "repositories cannot import domains (layer violation)",
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
group: ["*/actions/*", "*/actions"],
|
|
55
|
+
message: "repositories cannot import actions (layer violation)",
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
group: ["*/hooks/*", "*/hooks"],
|
|
59
|
+
message: "repositories cannot import hooks (layer violation)",
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
group: [
|
|
63
|
+
"@/features/*/repositories/*",
|
|
64
|
+
"@/features/*/repositories",
|
|
65
|
+
],
|
|
66
|
+
message:
|
|
67
|
+
"repositories cannot import other feature's repositories (cross-feature violation)",
|
|
68
|
+
},
|
|
69
|
+
],
|
|
70
|
+
},
|
|
71
|
+
],
|
|
72
|
+
},
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
name: "layers/domains",
|
|
76
|
+
files: ["**/domains/*.ts"],
|
|
77
|
+
rules: {
|
|
78
|
+
"no-restricted-syntax": [
|
|
79
|
+
"error",
|
|
80
|
+
{
|
|
81
|
+
selector: "TryStatement",
|
|
82
|
+
message:
|
|
83
|
+
"try-catch is not allowed in domain. Error handling belongs in actions.",
|
|
84
|
+
},
|
|
85
|
+
],
|
|
86
|
+
"no-restricted-imports": [
|
|
87
|
+
"error",
|
|
88
|
+
{
|
|
89
|
+
patterns: [
|
|
90
|
+
{
|
|
91
|
+
group: ["*/actions/*", "*/actions"],
|
|
92
|
+
message: "domains cannot import actions (layer violation)",
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
group: ["*/hooks/*", "*/hooks"],
|
|
96
|
+
message: "domains cannot import hooks (layer violation)",
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
group: ["@/features/*/domains/*", "@/features/*/domains"],
|
|
100
|
+
message:
|
|
101
|
+
"domains cannot import other feature's domains (cross-feature violation)",
|
|
102
|
+
},
|
|
103
|
+
],
|
|
104
|
+
},
|
|
105
|
+
],
|
|
106
|
+
},
|
|
107
|
+
},
|
|
108
|
+
{
|
|
109
|
+
name: "layers/actions-naming",
|
|
110
|
+
files: featuresGlob("**/actions/*.ts"),
|
|
111
|
+
rules: {
|
|
112
|
+
"no-restricted-syntax": [
|
|
113
|
+
"error",
|
|
114
|
+
{
|
|
115
|
+
selector:
|
|
116
|
+
"ExportNamedDeclaration > FunctionDeclaration[id.name!=/^handle[A-Z]/]",
|
|
117
|
+
message:
|
|
118
|
+
"Exported functions in actions must start with 'handle' (e.g., handleGetComics).",
|
|
119
|
+
},
|
|
120
|
+
],
|
|
121
|
+
},
|
|
122
|
+
},
|
|
123
|
+
{
|
|
124
|
+
name: "layers/actions-imports",
|
|
125
|
+
files: ["**/actions/*.ts"],
|
|
126
|
+
rules: {
|
|
127
|
+
"no-restricted-imports": [
|
|
128
|
+
"error",
|
|
129
|
+
{
|
|
130
|
+
patterns: [
|
|
131
|
+
{
|
|
132
|
+
group: ["*/hooks/*", "*/hooks"],
|
|
133
|
+
message: "actions cannot import hooks (layer violation)",
|
|
134
|
+
},
|
|
135
|
+
{
|
|
136
|
+
group: ["@/features/*/actions/*", "@/features/*/actions"],
|
|
137
|
+
message:
|
|
138
|
+
"actions cannot import other feature's actions (cross-feature violation)",
|
|
139
|
+
},
|
|
140
|
+
],
|
|
141
|
+
},
|
|
142
|
+
],
|
|
143
|
+
},
|
|
144
|
+
},
|
|
145
|
+
{
|
|
146
|
+
name: "layers/hooks-naming",
|
|
147
|
+
files: featuresGlob("**/hooks/*.ts"),
|
|
148
|
+
rules: {
|
|
149
|
+
"no-restricted-syntax": [
|
|
150
|
+
"error",
|
|
151
|
+
{
|
|
152
|
+
selector:
|
|
153
|
+
"ExportNamedDeclaration > FunctionDeclaration[id.name!=/^use[A-Z]/]",
|
|
154
|
+
message:
|
|
155
|
+
"Exported functions in hooks must start with 'use' (e.g., useAuth).",
|
|
156
|
+
},
|
|
157
|
+
],
|
|
158
|
+
},
|
|
159
|
+
},
|
|
160
|
+
];
|
package/src/naming.mjs
ADDED
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview File naming conventions for feature modules.
|
|
3
|
+
*
|
|
4
|
+
* Enforces consistent naming patterns:
|
|
5
|
+
* - domains: {prefix}.domain.ts (e.g., server.domain.ts, stripe.domain.ts)
|
|
6
|
+
* - repositories: {prefix}.repo.ts (e.g., server.repo.ts, stripe.repo.ts)
|
|
7
|
+
* - actions: {prefix}.action.ts (e.g., server.action.ts, stripe.action.ts)
|
|
8
|
+
* - hooks: useXxx.ts (e.g., useAuth.ts)
|
|
9
|
+
* - types: xxx.type.ts (e.g., comic.type.ts)
|
|
10
|
+
* - schemas: xxx.schema.ts (e.g., comic.schema.ts)
|
|
11
|
+
* - utils: xxx.util.ts (e.g., format.util.ts)
|
|
12
|
+
* - constants: xxx.constant.ts (e.g., api.constant.ts)
|
|
13
|
+
*
|
|
14
|
+
* Extension constraints:
|
|
15
|
+
* - features/**: .ts only (no .tsx — components belong in src/components/)
|
|
16
|
+
* - components/**: .tsx only (no .ts — logic belongs in src/features/)
|
|
17
|
+
*
|
|
18
|
+
* Valid prefixes are defined in PREFIX_LIB_MAPPING (constants.mjs).
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import { featuresGlob, PREFIX_LIB_MAPPING } from "./constants.mjs";
|
|
22
|
+
import { checkFile } from "./plugins.mjs";
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Generate glob pattern from PREFIX_LIB_MAPPING keys.
|
|
26
|
+
* @example "@(server|client|admin)"
|
|
27
|
+
*/
|
|
28
|
+
const prefixPattern = `@(${Object.keys(PREFIX_LIB_MAPPING).join("|")})`;
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* File naming convention configurations.
|
|
32
|
+
* @type {import("eslint").Linter.Config[]}
|
|
33
|
+
*/
|
|
34
|
+
export const namingConfigs = [
|
|
35
|
+
{
|
|
36
|
+
name: "naming/domains",
|
|
37
|
+
files: featuresGlob("**/domains/*.ts"),
|
|
38
|
+
plugins: { "check-file": checkFile },
|
|
39
|
+
rules: {
|
|
40
|
+
"check-file/filename-naming-convention": [
|
|
41
|
+
"error",
|
|
42
|
+
{ "**/*.ts": `${prefixPattern}.domain` },
|
|
43
|
+
],
|
|
44
|
+
},
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
name: "naming/repositories",
|
|
48
|
+
files: featuresGlob("**/repositories/*.ts"),
|
|
49
|
+
plugins: { "check-file": checkFile },
|
|
50
|
+
rules: {
|
|
51
|
+
"check-file/filename-naming-convention": [
|
|
52
|
+
"error",
|
|
53
|
+
{ "**/*.ts": `${prefixPattern}.repo` },
|
|
54
|
+
],
|
|
55
|
+
},
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
name: "naming/hooks",
|
|
59
|
+
files: featuresGlob("**/hooks/*.ts"),
|
|
60
|
+
plugins: { "check-file": checkFile },
|
|
61
|
+
rules: {
|
|
62
|
+
"check-file/filename-naming-convention": [
|
|
63
|
+
"error",
|
|
64
|
+
{ "**/*.ts": "use[A-Z]*([a-zA-Z0-9])" },
|
|
65
|
+
{ ignoreMiddleExtensions: true },
|
|
66
|
+
],
|
|
67
|
+
},
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
name: "naming/types",
|
|
71
|
+
files: featuresGlob("**/types/*.ts"),
|
|
72
|
+
plugins: { "check-file": checkFile },
|
|
73
|
+
rules: {
|
|
74
|
+
"check-file/filename-naming-convention": [
|
|
75
|
+
"error",
|
|
76
|
+
{ "**/*.ts": "+([a-z0-9-]).type" },
|
|
77
|
+
],
|
|
78
|
+
},
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
name: "naming/schemas",
|
|
82
|
+
files: featuresGlob("**/schemas/*.ts"),
|
|
83
|
+
plugins: { "check-file": checkFile },
|
|
84
|
+
rules: {
|
|
85
|
+
"check-file/filename-naming-convention": [
|
|
86
|
+
"error",
|
|
87
|
+
{ "**/*.ts": "+([a-z0-9-]).schema" },
|
|
88
|
+
],
|
|
89
|
+
},
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
name: "naming/utils",
|
|
93
|
+
files: featuresGlob("**/util/*.ts"),
|
|
94
|
+
plugins: { "check-file": checkFile },
|
|
95
|
+
rules: {
|
|
96
|
+
"check-file/filename-naming-convention": [
|
|
97
|
+
"error",
|
|
98
|
+
{ "**/*.ts": "+([a-z0-9-]).util" },
|
|
99
|
+
],
|
|
100
|
+
},
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
name: "naming/constants",
|
|
104
|
+
files: featuresGlob("**/constants/*.ts"),
|
|
105
|
+
plugins: { "check-file": checkFile },
|
|
106
|
+
rules: {
|
|
107
|
+
"check-file/filename-naming-convention": [
|
|
108
|
+
"error",
|
|
109
|
+
{ "**/*.ts": "+([a-z0-9-]).constant" },
|
|
110
|
+
],
|
|
111
|
+
},
|
|
112
|
+
},
|
|
113
|
+
{
|
|
114
|
+
name: "naming/actions",
|
|
115
|
+
files: featuresGlob("**/actions/*.ts"),
|
|
116
|
+
plugins: { "check-file": checkFile },
|
|
117
|
+
rules: {
|
|
118
|
+
"check-file/filename-naming-convention": [
|
|
119
|
+
"error",
|
|
120
|
+
{ "**/*.ts": `${prefixPattern}.action` },
|
|
121
|
+
],
|
|
122
|
+
},
|
|
123
|
+
},
|
|
124
|
+
{
|
|
125
|
+
name: "naming/features-ts-only",
|
|
126
|
+
files: featuresGlob("**/*.tsx"),
|
|
127
|
+
rules: {
|
|
128
|
+
"no-restricted-syntax": [
|
|
129
|
+
"error",
|
|
130
|
+
{
|
|
131
|
+
selector: "Program",
|
|
132
|
+
message:
|
|
133
|
+
"features/ must only contain .ts files. Components belong in src/components/.",
|
|
134
|
+
},
|
|
135
|
+
],
|
|
136
|
+
},
|
|
137
|
+
},
|
|
138
|
+
{
|
|
139
|
+
name: "naming/components-tsx-only",
|
|
140
|
+
files: ["src/components/**/*.ts"],
|
|
141
|
+
rules: {
|
|
142
|
+
"no-restricted-syntax": [
|
|
143
|
+
"error",
|
|
144
|
+
{
|
|
145
|
+
selector: "Program",
|
|
146
|
+
message:
|
|
147
|
+
"components/ must only contain .tsx files. Logic belongs in src/features/.",
|
|
148
|
+
},
|
|
149
|
+
],
|
|
150
|
+
},
|
|
151
|
+
},
|
|
152
|
+
];
|
package/src/plugins.mjs
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview ESLint plugin imports and exports.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import stylistic from "@stylistic/eslint-plugin";
|
|
6
|
+
import checkFile from "eslint-plugin-check-file";
|
|
7
|
+
import reactYouMightNotNeedAnEffect from "eslint-plugin-react-you-might-not-need-an-effect";
|
|
8
|
+
import simpleImportSortPlugin from "eslint-plugin-simple-import-sort";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Plugin namespace mapping for ESLint configuration.
|
|
12
|
+
* @type {Record<string, import("eslint").ESLint.Plugin>}
|
|
13
|
+
*/
|
|
14
|
+
export const plugins = {
|
|
15
|
+
"@stylistic": stylistic,
|
|
16
|
+
"check-file": checkFile,
|
|
17
|
+
"react-you-might-not-need-an-effect": reactYouMightNotNeedAnEffect,
|
|
18
|
+
"simple-import-sort": simpleImportSortPlugin,
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export {
|
|
22
|
+
checkFile,
|
|
23
|
+
reactYouMightNotNeedAnEffect,
|
|
24
|
+
simpleImportSortPlugin,
|
|
25
|
+
stylistic,
|
|
26
|
+
};
|