@yasainet/eslint-next 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,106 @@
1
+ # @yasainet/eslint-next
2
+
3
+ Shared ESLint configuration for Next.js projects with feature-based architecture.
4
+
5
+ ## Install
6
+
7
+ ```sh
8
+ npm install -D @yasainet/eslint-next eslint eslint-config-next
9
+ ```
10
+
11
+ ## Usage
12
+
13
+ ```js
14
+ // eslint.config.mjs
15
+ import { eslintConfig } from "@yasainet/eslint-next";
16
+
17
+ export default eslintConfig;
18
+ ```
19
+
20
+ ## Rules
21
+
22
+ ### Base
23
+
24
+ Next.js recommended presets (core-web-vitals + typescript) with additional rules:
25
+
26
+ - `@typescript-eslint/consistent-type-imports` — enforce `type` imports
27
+ - `@typescript-eslint/no-unused-vars` — disallow unused variables (allows `_` prefix)
28
+ - `no-console` — warn on `console.*`
29
+ - `simple-import-sort` — auto-sort imports/exports
30
+ - `@stylistic/quotes` — enforce double quotes
31
+ - `react-you-might-not-need-an-effect` — detect unnecessary `useEffect`
32
+
33
+ ### Naming
34
+
35
+ Enforces file naming conventions inside `features/`:
36
+
37
+ | Directory | Pattern | Example |
38
+ | --------------- | -------------------- | ------------------ |
39
+ | `domain/` | `{prefix}.domain.ts` | `server.domain.ts` |
40
+ | `repositories/` | `{prefix}.repo.ts` | `server.repo.ts` |
41
+ | `actions/` | `{prefix}.action.ts` | `server.action.ts` |
42
+ | `hooks/` | `use{Name}.ts` | `useAuth.ts` |
43
+ | `types/` | `{name}.type.ts` | `comic.type.ts` |
44
+ | `schemas/` | `{name}.schema.ts` | `comic.schema.ts` |
45
+ | `util/` | `{name}.util.ts` | `format.util.ts` |
46
+ | `constants/` | `{name}.constant.ts` | `api.constant.ts` |
47
+
48
+ Additionally:
49
+
50
+ - `features/**` — `.ts` only (components belong in `src/components/`)
51
+ - `components/**` — `.tsx` only (logic belongs in `src/features/`)
52
+
53
+ ### Layers
54
+
55
+ Enforces dependency direction between layers:
56
+
57
+ ```
58
+ hooks → actions → domain → repositories
59
+ ```
60
+
61
+ - **Repositories** — cannot import domain/actions/hooks, no `try-catch` or `if`
62
+ - **Domain** — cannot import actions/hooks, no `try-catch`
63
+ - **Actions** — cannot import hooks, exports must start with `handle`
64
+ - **Hooks** — exports must start with `use`
65
+
66
+ Cross-feature imports within the same layer are prohibited.
67
+
68
+ ### Cardinality
69
+
70
+ Each action can only import its matching domain:
71
+
72
+ - `server.action.ts` → `server.domain.ts`
73
+ - `client.action.ts` → `client.domain.ts`
74
+ - `admin.action.ts` → `admin.domain.ts`
75
+
76
+ ### Directives
77
+
78
+ Enforces `"use server"` / `"use client"` directives:
79
+
80
+ - `server.action.ts` / `admin.action.ts` — must start with `"use server"`
81
+ - `client.action.ts` — must NOT have `"use server"`
82
+ - `hooks/*.ts` — must start with `"use client"`
83
+
84
+ ### Imports
85
+
86
+ Each `{prefix}.repo.ts` can only import its corresponding lib (auto-generated from `src/lib/`):
87
+
88
+ - `server.repo.ts` → `@/lib/supabase/server`
89
+ - `client.repo.ts` → `@/lib/supabase/client`
90
+
91
+ ## Release
92
+
93
+ 1. Update `version` in `package.json`
94
+ 2. Commit and push to `main`
95
+ 3. Create and push a tag:
96
+
97
+ ```sh
98
+ git tag v0.1.0
99
+ git push --tags
100
+ ```
101
+
102
+ 4. GitHub Actions will automatically publish to npm
103
+
104
+ ## License
105
+
106
+ MIT
package/package.json ADDED
@@ -0,0 +1,34 @@
1
+ {
2
+ "name": "@yasainet/eslint-next",
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
+ "publishConfig": {
21
+ "access": "public"
22
+ },
23
+ "dependencies": {
24
+ "@stylistic/eslint-plugin": "^5.9.0",
25
+ "eslint-plugin-check-file": "^3.3.1",
26
+ "eslint-plugin-jsdoc": "^62.7.1",
27
+ "eslint-plugin-react-you-might-not-need-an-effect": "^0.5.6",
28
+ "eslint-plugin-simple-import-sort": "^12.1.1"
29
+ },
30
+ "peerDependencies": {
31
+ "eslint": "^9",
32
+ "eslint-config-next": "^15 || ^16"
33
+ }
34
+ }
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: ["**/domain/client.domain*", "**/domain/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: ["**/domain/server.domain*", "**/domain/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: ["**/domain/server.domain*", "**/domain/client.domain*"],
63
+ message:
64
+ "admin.action can only import admin.domain (cardinality violation)",
65
+ },
66
+ ],
67
+ },
68
+ ],
69
+ },
70
+ },
71
+ ];
@@ -0,0 +1,116 @@
1
+ /**
2
+ * @fileoverview Shared constants and helpers for ESLint configuration.
3
+ */
4
+
5
+ import fs from "fs";
6
+ import path from "path";
7
+ import { fileURLToPath } from "url";
8
+
9
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
10
+
11
+ /** Find the project root by walking up from this package's location in node_modules */
12
+ function findProjectRoot() {
13
+ let dir = __dirname;
14
+ while (dir !== path.dirname(dir)) {
15
+ if (
16
+ fs.existsSync(path.join(dir, "package.json")) &&
17
+ !dir.includes("/node_modules/")
18
+ ) {
19
+ return dir;
20
+ }
21
+ dir = path.dirname(dir);
22
+ }
23
+ return process.cwd();
24
+ }
25
+
26
+ const PROJECT_ROOT = findProjectRoot();
27
+
28
+ /**
29
+ * Root directories containing feature modules.
30
+ * @type {string[]}
31
+ */
32
+ export const FEATURE_ROOTS = [
33
+ "src/features",
34
+ "scripts/features",
35
+ "supabase/functions/features",
36
+ ];
37
+
38
+ /**
39
+ * Files and directories to exclude from prefix mapping.
40
+ * @type {string[]}
41
+ */
42
+ const EXCLUDE_LIST = ["proxy.ts", "types"];
43
+
44
+ /**
45
+ * Generate PREFIX_LIB_MAPPING by scanning src/lib/ directory.
46
+ *
47
+ * @returns {Record<string, string>} Mapping of prefix to lib import path.
48
+ * @example
49
+ * - src/lib/supabase/server.ts → { server: "@/lib/supabase/server" }
50
+ * - src/lib/supabase/client.ts → { server: "@/lib/supabase/client" }
51
+ */
52
+ function generatePrefixLibMapping() {
53
+ const libDir = path.join(PROJECT_ROOT, "src/lib");
54
+ const mapping = {};
55
+
56
+ if (!fs.existsSync(libDir)) {
57
+ return mapping;
58
+ }
59
+
60
+ const entries = fs.readdirSync(libDir, { withFileTypes: true });
61
+
62
+ for (const entry of entries) {
63
+ if (EXCLUDE_LIST.includes(entry.name)) {
64
+ continue;
65
+ }
66
+
67
+ if (entry.isDirectory()) {
68
+ // Scan subdirectory (e.g., src/lib/supabase/)
69
+ const subDir = path.join(libDir, entry.name);
70
+ const subEntries = fs.readdirSync(subDir, { withFileTypes: true });
71
+
72
+ for (const subEntry of subEntries) {
73
+ if (
74
+ subEntry.isFile() &&
75
+ subEntry.name.endsWith(".ts") &&
76
+ !EXCLUDE_LIST.includes(subEntry.name)
77
+ ) {
78
+ const prefix = subEntry.name.replace(".ts", "");
79
+ mapping[prefix] = `@/lib/${entry.name}/${prefix}`;
80
+ }
81
+ }
82
+ } else if (entry.isFile() && entry.name.endsWith(".ts")) {
83
+ const prefix = entry.name.replace(".ts", "");
84
+ mapping[prefix] = `@/lib/${prefix}`;
85
+ }
86
+ }
87
+
88
+ return mapping;
89
+ }
90
+
91
+ /**
92
+ * Mapping of file prefixes to their corresponding lib imports.
93
+ *
94
+ * Dynamically generated by scanning src/lib/ directory.
95
+ * Used by:
96
+ * - naming.mjs: Validates file naming (e.g., server.repo.ts, client.repo.ts)
97
+ * - imports.mjs: Enforces import restrictions
98
+ *
99
+ * @example
100
+ * server.repo.ts → can only import @/lib/supabase/server
101
+ * client.repo.ts → can only import @/lib/supabase/client
102
+ *
103
+ * @type {Record<string, string>}
104
+ */
105
+ export const PREFIX_LIB_MAPPING = generatePrefixLibMapping();
106
+
107
+ /**
108
+ * Creates glob patterns for all feature roots.
109
+ * @param {string} subpath - The subpath to append to each feature root.
110
+ * @returns {string[]} Array of glob patterns.
111
+ * @example
112
+ * featuresGlob("**\/actions/*.ts")
113
+ * // => ["src/features/**\/actions/*.ts", "scripts/features/**\/actions/*.ts", ...]
114
+ */
115
+ export const featuresGlob = (subpath) =>
116
+ 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
+ ];
@@ -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,36 @@
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
+ * - jsdoc: JSDoc description requirements for exported functions
12
+ */
13
+
14
+ import { baseConfigs, ignoresConfig, sharedRulesConfig } from "./base.mjs";
15
+ import { cardinalityConfigs } from "./cardinality.mjs";
16
+ import { directivesConfigs } from "./directives.mjs";
17
+ import { importsConfigs } from "./imports.mjs";
18
+ import { jsdocConfigs } from "./jsdoc.mjs";
19
+ import { layersConfigs } from "./layers.mjs";
20
+ import { namingConfigs } from "./naming.mjs";
21
+
22
+ /**
23
+ * Complete ESLint configuration array.
24
+ * @type {import("eslint").Linter.Config[]}
25
+ */
26
+ export const eslintConfig = [
27
+ ...baseConfigs,
28
+ ignoresConfig,
29
+ sharedRulesConfig,
30
+ ...namingConfigs,
31
+ ...layersConfigs,
32
+ ...cardinalityConfigs,
33
+ ...directivesConfigs,
34
+ ...importsConfigs,
35
+ ...jsdocConfigs,
36
+ ];
package/src/jsdoc.mjs ADDED
@@ -0,0 +1,53 @@
1
+ /**
2
+ * @fileoverview JSDoc configuration for the abstraction layer.
3
+ *
4
+ * Enforces a single rule: every exported function must have a description.
5
+ * Types are handled by TypeScript (machine layer), not JSDoc.
6
+ *
7
+ * Targets: repositories, domain, util.
8
+ * Excludes: actions, hooks, components, schemas, constants.
9
+ */
10
+
11
+ import jsdocPlugin from "eslint-plugin-jsdoc";
12
+
13
+ import { featuresGlob } from "./constants.mjs";
14
+
15
+ /**
16
+ * JSDoc configurations requiring descriptions on exported functions.
17
+ * @type {import("eslint").Linter.Config[]}
18
+ */
19
+ export const jsdocConfigs = [
20
+ {
21
+ name: "jsdoc",
22
+ files: [
23
+ ...featuresGlob("**/repositories/*.ts"),
24
+ ...featuresGlob("**/domain*/*.ts"),
25
+ ...featuresGlob("**/util*/*.ts"),
26
+ ],
27
+ plugins: {
28
+ jsdoc: jsdocPlugin,
29
+ },
30
+ rules: {
31
+ "jsdoc/require-jsdoc": [
32
+ "warn",
33
+ {
34
+ publicOnly: true,
35
+ require: {
36
+ FunctionDeclaration: true,
37
+ ArrowFunctionExpression: true,
38
+ FunctionExpression: true,
39
+ },
40
+ checkGetters: false,
41
+ checkSetters: false,
42
+ checkConstructors: false,
43
+ },
44
+ ],
45
+ "jsdoc/require-description": [
46
+ "warn",
47
+ {
48
+ contexts: ["any"],
49
+ },
50
+ ],
51
+ },
52
+ },
53
+ ];
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 → domain → repositories
8
+ *
9
+ * Rules:
10
+ * - Repositories: Cannot import domain, actions, or hooks.
11
+ * Cannot use try-catch or if statements.
12
+ * - Domain: 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: ["*/domain/*", "*/domain"],
51
+ message: "repositories cannot import domain (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/domain",
76
+ files: ["**/domain/*.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: "domain cannot import actions (layer violation)",
93
+ },
94
+ {
95
+ group: ["*/hooks/*", "*/hooks"],
96
+ message: "domain cannot import hooks (layer violation)",
97
+ },
98
+ {
99
+ group: ["@/features/*/domain/*", "@/features/*/domain"],
100
+ message:
101
+ "domain cannot import other feature's domain (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,194 @@
1
+ /**
2
+ * @fileoverview File naming conventions for feature modules.
3
+ *
4
+ * Enforces consistent naming patterns:
5
+ * - domain: {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: {feature}.type.ts (e.g., threads.type.ts) — shared: free naming
10
+ * - schemas: xxx.schema.ts (e.g., comic.schema.ts)
11
+ * - util: {feature}.util.ts (e.g., threads.util.ts) — shared: free naming
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
+ * Component naming:
19
+ * - components/ *.tsx: PascalCase (e.g., Button.tsx, AlertDialog.tsx)
20
+ * - components/shared/ui/ : excluded (shadcn/ui uses kebab-case)
21
+ *
22
+ * Valid prefixes are defined in PREFIX_LIB_MAPPING (constants.mjs).
23
+ */
24
+
25
+ import { featuresGlob, PREFIX_LIB_MAPPING } from "./constants.mjs";
26
+ import { checkFile } from "./plugins.mjs";
27
+
28
+ /**
29
+ * Generate glob pattern from PREFIX_LIB_MAPPING keys.
30
+ * @example "@(server|client|admin)"
31
+ */
32
+ const prefixPattern = `@(${Object.keys(PREFIX_LIB_MAPPING).join("|")})`;
33
+
34
+ /**
35
+ * File naming convention configurations.
36
+ * @type {import("eslint").Linter.Config[]}
37
+ */
38
+ export const namingConfigs = [
39
+ {
40
+ name: "naming/domain",
41
+ files: featuresGlob("**/domain/*.ts"),
42
+ plugins: { "check-file": checkFile },
43
+ rules: {
44
+ "check-file/filename-naming-convention": [
45
+ "error",
46
+ { "**/*.ts": `${prefixPattern}.domain` },
47
+ ],
48
+ },
49
+ },
50
+ {
51
+ name: "naming/repositories",
52
+ files: featuresGlob("**/repositories/*.ts"),
53
+ plugins: { "check-file": checkFile },
54
+ rules: {
55
+ "check-file/filename-naming-convention": [
56
+ "error",
57
+ { "**/*.ts": `${prefixPattern}.repo` },
58
+ ],
59
+ },
60
+ },
61
+ {
62
+ name: "naming/hooks",
63
+ files: featuresGlob("**/hooks/*.ts"),
64
+ plugins: { "check-file": checkFile },
65
+ rules: {
66
+ "check-file/filename-naming-convention": [
67
+ "error",
68
+ { "**/*.ts": "use[A-Z]*([a-zA-Z0-9])" },
69
+ { ignoreMiddleExtensions: true },
70
+ ],
71
+ },
72
+ },
73
+ {
74
+ name: "naming/types",
75
+ files: featuresGlob("*/types/*.type.ts"),
76
+ ignores: featuresGlob("shared/types/*.ts"),
77
+ plugins: { "check-file": checkFile },
78
+ rules: {
79
+ "check-file/filename-naming-convention": [
80
+ "error",
81
+ { "**/*/types/*.ts": "<1>" },
82
+ { ignoreMiddleExtensions: true },
83
+ ],
84
+ },
85
+ },
86
+ {
87
+ name: "naming/types-shared",
88
+ files: featuresGlob("shared/types/*.ts"),
89
+ plugins: { "check-file": checkFile },
90
+ rules: {
91
+ "check-file/filename-naming-convention": [
92
+ "error",
93
+ { "**/*.ts": "+([a-z0-9_-]).type" },
94
+ ],
95
+ },
96
+ },
97
+ {
98
+ name: "naming/schemas",
99
+ files: featuresGlob("**/schemas/*.ts"),
100
+ plugins: { "check-file": checkFile },
101
+ rules: {
102
+ "check-file/filename-naming-convention": [
103
+ "error",
104
+ { "**/*.ts": "+([a-z0-9-]).schema" },
105
+ ],
106
+ },
107
+ },
108
+ {
109
+ name: "naming/util",
110
+ files: featuresGlob("*/util/*.util.ts"),
111
+ ignores: featuresGlob("shared/util/*.ts"),
112
+ plugins: { "check-file": checkFile },
113
+ rules: {
114
+ "check-file/filename-naming-convention": [
115
+ "error",
116
+ { "**/*/util/*.ts": "<1>" },
117
+ { ignoreMiddleExtensions: true },
118
+ ],
119
+ },
120
+ },
121
+ {
122
+ name: "naming/util-shared",
123
+ files: featuresGlob("shared/util/*.ts"),
124
+ plugins: { "check-file": checkFile },
125
+ rules: {
126
+ "check-file/filename-naming-convention": [
127
+ "error",
128
+ { "**/*.ts": "+([a-z0-9_-]).util" },
129
+ ],
130
+ },
131
+ },
132
+ {
133
+ name: "naming/constants",
134
+ files: featuresGlob("**/constants/*.ts"),
135
+ plugins: { "check-file": checkFile },
136
+ rules: {
137
+ "check-file/filename-naming-convention": [
138
+ "error",
139
+ { "**/*.ts": "+([a-z0-9-]).constant" },
140
+ ],
141
+ },
142
+ },
143
+ {
144
+ name: "naming/actions",
145
+ files: featuresGlob("**/actions/*.ts"),
146
+ plugins: { "check-file": checkFile },
147
+ rules: {
148
+ "check-file/filename-naming-convention": [
149
+ "error",
150
+ { "**/*.ts": `${prefixPattern}.action` },
151
+ ],
152
+ },
153
+ },
154
+ {
155
+ name: "naming/features-ts-only",
156
+ files: featuresGlob("**/*.tsx"),
157
+ rules: {
158
+ "no-restricted-syntax": [
159
+ "error",
160
+ {
161
+ selector: "Program",
162
+ message:
163
+ "features/ must only contain .ts files. Components belong in src/components/.",
164
+ },
165
+ ],
166
+ },
167
+ },
168
+ {
169
+ name: "naming/components-tsx-only",
170
+ files: ["src/components/**/*.ts"],
171
+ rules: {
172
+ "no-restricted-syntax": [
173
+ "error",
174
+ {
175
+ selector: "Program",
176
+ message:
177
+ "components/ must only contain .tsx files. Logic belongs in src/features/.",
178
+ },
179
+ ],
180
+ },
181
+ },
182
+ {
183
+ name: "naming/components-pascal-case",
184
+ files: ["src/components/**/*.tsx"],
185
+ ignores: ["src/components/shared/ui/**"],
186
+ plugins: { "check-file": checkFile },
187
+ rules: {
188
+ "check-file/filename-naming-convention": [
189
+ "error",
190
+ { "**/*.tsx": "PASCAL_CASE" },
191
+ ],
192
+ },
193
+ },
194
+ ];
@@ -0,0 +1,29 @@
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 jsdocPlugin from "eslint-plugin-jsdoc";
8
+ import reactYouMightNotNeedAnEffect from "eslint-plugin-react-you-might-not-need-an-effect";
9
+ import simpleImportSortPlugin from "eslint-plugin-simple-import-sort";
10
+
11
+ /**
12
+ * Plugin namespace mapping for ESLint configuration.
13
+ * @type {Record<string, import("eslint").ESLint.Plugin>}
14
+ */
15
+ export const plugins = {
16
+ "@stylistic": stylistic,
17
+ "check-file": checkFile,
18
+ jsdoc: jsdocPlugin,
19
+ "react-you-might-not-need-an-effect": reactYouMightNotNeedAnEffect,
20
+ "simple-import-sort": simpleImportSortPlugin,
21
+ };
22
+
23
+ export {
24
+ checkFile,
25
+ jsdocPlugin,
26
+ reactYouMightNotNeedAnEffect,
27
+ simpleImportSortPlugin,
28
+ stylistic,
29
+ };