@yasainet/eslint 0.0.73 → 0.0.75
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 +9 -152
- package/package.json +7 -1
- package/src/cli/test-audit.mjs +97 -0
- package/src/common/CLAUDE.md +17 -0
- package/src/common/{constants.mjs → _internal/constants.mjs} +1 -19
- package/src/common/_internal/import-patterns.mjs +16 -0
- package/src/common/{plugins.mjs → _internal/plugins.mjs} +0 -1
- package/src/common/_internal/selectors.mjs +12 -0
- package/src/common/{rules.mjs → base/typescript.mjs} +15 -36
- package/src/common/{entry-points.mjs → boundaries/entry-point.mjs} +1 -2
- package/src/common/cross-cutting/ban-alias.mjs +22 -0
- package/src/common/cross-cutting/feature-default-imports.mjs +26 -0
- package/src/common/cross-cutting/feature-name.mjs +15 -0
- package/src/common/cross-cutting/features-ts-only.mjs +20 -0
- package/src/common/cross-cutting/form-state.mjs +16 -0
- package/src/common/{jsdoc.mjs → cross-cutting/jsdoc.mjs} +2 -3
- package/src/common/cross-cutting/logger.mjs +21 -0
- package/src/common/cross-cutting/namespace-import.mjs +23 -0
- package/src/common/cross-cutting/no-any-return.mjs +18 -0
- package/src/common/cross-cutting/no-colocated-test.mjs +18 -0
- package/src/common/cross-cutting/supabase-columns-satisfies.mjs +18 -0
- package/src/common/index.mjs +44 -24
- package/src/common/layers/constants.mjs +36 -0
- package/src/common/layers/entries.mjs +174 -0
- package/src/common/layers/lib.mjs +18 -0
- package/src/common/layers/queries.mjs +187 -0
- package/src/common/layers/schemas.mjs +50 -0
- package/src/common/layers/services.mjs +121 -0
- package/src/common/layers/top-level-utils.mjs +18 -0
- package/src/common/layers/types.mjs +44 -0
- package/src/common/layers/utils.mjs +54 -0
- package/src/common/local-plugins/entry-single-service-call.mjs +3 -31
- package/src/common/local-plugins/entry-template.mjs +51 -88
- package/src/common/local-plugins/feature-name.mjs +5 -24
- package/src/common/local-plugins/form-state-naming.mjs +1 -11
- package/src/common/local-plugins/form-state-shape.mjs +8 -42
- package/src/common/local-plugins/import-path-style.mjs +2 -9
- package/src/common/local-plugins/index.mjs +2 -1
- package/src/common/local-plugins/layout-main-structural-only.mjs +1 -22
- package/src/common/local-plugins/namespace-import-name.mjs +1 -27
- package/src/common/local-plugins/no-any-return.mjs +1 -9
- package/src/common/local-plugins/no-colocated-test.mjs +26 -0
- package/src/common/local-plugins/queries-export.mjs +1 -9
- package/src/common/local-plugins/queries-namespace-import.mjs +1 -11
- package/src/common/local-plugins/schema-naming.mjs +2 -8
- package/src/common/local-plugins/supabase-columns-satisfies.mjs +1 -25
- package/src/common/local-plugins/supabase-select-typed-columns.mjs +5 -37
- package/src/deno/CLAUDE.md +10 -0
- package/src/deno/boundaries/entry-point.mjs +44 -0
- package/src/deno/boundaries/lib.mjs +28 -0
- package/src/deno/boundaries/utils.mjs +25 -0
- package/src/deno/index.mjs +9 -13
- package/src/deno/local-plugins/flat-entry-point.mjs +1 -6
- package/src/deno/local-plugins/index.mjs +0 -1
- package/src/next/CLAUDE.md +14 -0
- package/src/next/boundaries/components.mjs +36 -0
- package/src/next/boundaries/hooks.mjs +36 -0
- package/src/next/boundaries/lib.mjs +23 -0
- package/src/next/boundaries/page.mjs +36 -0
- package/src/next/boundaries/route.mjs +36 -0
- package/src/next/boundaries/sitemap.mjs +36 -0
- package/src/next/directives.mjs +4 -5
- package/src/next/imports.mjs +0 -1
- package/src/next/index.mjs +12 -15
- package/src/next/layers/components.mjs +30 -0
- package/src/next/layers/hooks.mjs +31 -0
- package/src/next/layers/layouts.mjs +12 -0
- package/src/next/tailwindcss.mjs +2 -23
- package/src/node/CLAUDE.md +7 -0
- package/src/node/index.mjs +1 -2
- package/src/common/imports.mjs +0 -457
- package/src/common/layers.mjs +0 -158
- package/src/common/naming.mjs +0 -347
- package/src/deno/imports.mjs +0 -90
- package/src/next/layouts.mjs +0 -18
- package/src/next/naming.mjs +0 -60
package/README.md
CHANGED
|
@@ -4,161 +4,18 @@ Shared ESLint configuration for Next.js, Node.js and Deno.
|
|
|
4
4
|
|
|
5
5
|
## Entry Points
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
```text
|
|
16
|
-
src/
|
|
17
|
-
├── common/ # Shared rules for all environments
|
|
18
|
-
├── next/ # Next.js-specific rules (hooks, components, directives)
|
|
19
|
-
├── node/ # Node.js CLI scripts (scripts/features, scripts/commands)
|
|
20
|
-
└── deno/ # Deno entry point (entry-point boundary, _utils boundary, _lib boundary)
|
|
21
|
-
```
|
|
22
|
-
|
|
23
|
-
Each entry point enforces a feature-based architecture. **Files do not carry role suffixes — the directory declares the role**:
|
|
24
|
-
|
|
25
|
-
```text
|
|
26
|
-
{featureRoot}/
|
|
27
|
-
├── {feature}/
|
|
28
|
-
│ ├── entries/ # entry points called from page.tsx / route.ts / hooks (server.ts / admin.ts / client.ts)
|
|
29
|
-
│ ├── services/ # business logic (server.ts ...)
|
|
30
|
-
│ ├── queries/ # data access (one file per upstream lib: <lib-name>.ts)
|
|
31
|
-
│ ├── types/ # type defs (one file per feature: <feature>.ts)
|
|
32
|
-
│ ├── schemas/ # zod schemas (<feature>.ts)
|
|
33
|
-
│ ├── utils/ # pure helpers (<feature>.ts)
|
|
34
|
-
│ └── constants/ # constants (<feature>.ts)
|
|
35
|
-
├── shared/ # Cross-feature shared modules
|
|
36
|
-
{libRoot}/
|
|
37
|
-
├── {single-client-lib}/index.ts # SDK wrapper entry (e.g., gallery-dl, fxembed, r2)
|
|
38
|
-
├── {single-client-lib}/types.ts # raw SDK types
|
|
39
|
-
├── {single-client-lib}/<sub>.ts # internal sub-modules (parser, etc.) — auto-hidden from queries
|
|
40
|
-
├── {multi-client-lib}/<role>.ts # one role per file (e.g., supabase: admin / server / client / proxy)
|
|
41
|
-
└── {multi-client-lib}/types.ts
|
|
42
|
-
{utilsRoot}/ # top-level pure utilities (cn.ts / logger.ts ...)
|
|
43
|
-
```
|
|
44
|
-
|
|
45
|
-
### single-client vs multi-client lib
|
|
46
|
-
|
|
47
|
-
| Detected by | Treated as | Example |
|
|
48
|
-
| -------------------------------------------- | ---------------- | --------------------------------------------- |
|
|
49
|
-
| `lib/<dir>/index.ts` exists | single-client | `lib/gallery-dl/{index.ts, parser.ts, types.ts}` |
|
|
50
|
-
| `lib/<dir>/index.ts` absent | multi-client | `lib/supabase/{admin.ts, server.ts, client.ts, ...}` |
|
|
51
|
-
|
|
52
|
-
For single-client libs the prefix mapping registers only the directory name, automatically hiding internal sub-modules (e.g., `parser.ts`) from the queries layer. For multi-client libs every plain `<role>.ts` is registered.
|
|
53
|
-
|
|
54
|
-
### File naming rules
|
|
55
|
-
|
|
56
|
-
- **No multi-extension suffixes** (`.lib`, `.service`, `.query`, `.util`, `.type`, `.schema`, `.constant`, `.entry` are forbidden). The directory carries the role.
|
|
57
|
-
- `lib/<dir>/index.ts` for single-client lib entries (avoids `lib/<dir>/<dir>.ts` redundancy).
|
|
58
|
-
- `lib/<dir>/types.ts` and `lib/<dir>/proxy.ts` are excluded from the prefix mapping so queries cannot directly depend on them.
|
|
59
|
-
- `<feature>/{types,schemas,utils,constants}/<feature>.ts` — exactly one file per feature, named after the feature.
|
|
60
|
-
- `<feature>/queries/<lib-name>.ts` — file name must match a registered lib prefix; queries can only import from the matching lib (lib-boundary lint).
|
|
7
|
+
- `@yasainet/eslint/next` — Common rules + Next.js
|
|
8
|
+
- Feature Root: `src/features/`
|
|
9
|
+
- `@yasainet/eslint/node` — Common rules for CLI scripts
|
|
10
|
+
- Feature Root: `scripts/features/`
|
|
11
|
+
- Entry Points: `scripts/commands/*.ts`
|
|
12
|
+
- `@yasainet/eslint/deno` — Common rules for Supabase Edge Functions
|
|
13
|
+
- Feature Root: `supabase/functions/_features/`
|
|
61
14
|
|
|
62
15
|
## Setup
|
|
63
16
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
```sh
|
|
67
|
-
npm install -D @yasainet/eslint
|
|
68
|
-
```
|
|
69
|
-
|
|
70
|
-
```js
|
|
71
|
-
// eslint.config.mjs
|
|
72
|
-
import { eslintConfig as nextEslintConfig } from "@yasainet/eslint/next";
|
|
73
|
-
import { eslintConfig as nodeEslintConfig } from "@yasainet/eslint/node";
|
|
74
|
-
import { eslintConfig as denoEslintConfig } from "@yasainet/eslint/deno";
|
|
75
|
-
import { defineConfig, globalIgnores } from "eslint/config";
|
|
76
|
-
import nextVitals from "eslint-config-next/core-web-vitals";
|
|
77
|
-
import nextTs from "eslint-config-next/typescript";
|
|
78
|
-
|
|
79
|
-
export default defineConfig([
|
|
80
|
-
...nextVitals,
|
|
81
|
-
...nextTs,
|
|
82
|
-
// Override default ignores of eslint-config-next.
|
|
83
|
-
globalIgnores([
|
|
84
|
-
// Default ignores of eslint-config-next:
|
|
85
|
-
".next/**",
|
|
86
|
-
".vercel/**",
|
|
87
|
-
"out/**",
|
|
88
|
-
"build/**",
|
|
89
|
-
"next-env.d.ts",
|
|
90
|
-
]),
|
|
91
|
-
...nextEslintConfig,
|
|
92
|
-
...nodeEslintConfig,
|
|
93
|
-
...denoEslintConfig,
|
|
94
|
-
]);
|
|
95
|
-
```
|
|
96
|
-
|
|
97
|
-
### Next.js
|
|
98
|
-
|
|
99
|
-
```sh
|
|
100
|
-
npm install -D @yasainet/eslint
|
|
101
|
-
```
|
|
102
|
-
|
|
103
|
-
```js
|
|
104
|
-
// eslint.config.mjs
|
|
105
|
-
import { eslintConfig } from "@yasainet/eslint/next";
|
|
106
|
-
import { defineConfig, globalIgnores } from "eslint/config";
|
|
107
|
-
import nextVitals from "eslint-config-next/core-web-vitals";
|
|
108
|
-
import nextTs from "eslint-config-next/typescript";
|
|
109
|
-
|
|
110
|
-
export default defineConfig([
|
|
111
|
-
...nextVitals,
|
|
112
|
-
...nextTs,
|
|
113
|
-
// Override default ignores of eslint-config-next.
|
|
114
|
-
globalIgnores([
|
|
115
|
-
// Default ignores of eslint-config-next:
|
|
116
|
-
".next/**",
|
|
117
|
-
".vercel/**",
|
|
118
|
-
"out/**",
|
|
119
|
-
"build/**",
|
|
120
|
-
"next-env.d.ts",
|
|
121
|
-
]),
|
|
122
|
-
...eslintConfig,
|
|
123
|
-
]);
|
|
124
|
-
```
|
|
125
|
-
|
|
126
|
-
### Node.js
|
|
127
|
-
|
|
128
|
-
```sh
|
|
129
|
-
npm install -D @yasainet/eslint eslint typescript-eslint
|
|
130
|
-
```
|
|
131
|
-
|
|
132
|
-
```js
|
|
133
|
-
// eslint.config.mjs
|
|
134
|
-
import { eslintConfig } from "@yasainet/eslint/node";
|
|
135
|
-
|
|
136
|
-
export default [...eslintConfig];
|
|
137
|
-
```
|
|
138
|
-
|
|
139
|
-
### Deno
|
|
140
|
-
|
|
141
|
-
```sh
|
|
142
|
-
npm install -D @yasainet/eslint eslint typescript-eslint
|
|
143
|
-
```
|
|
144
|
-
|
|
145
|
-
```js
|
|
146
|
-
// eslint.config.mjs
|
|
147
|
-
import { eslintConfig } from "@yasainet/eslint/deno";
|
|
148
|
-
|
|
149
|
-
export default [...eslintConfig];
|
|
150
|
-
```
|
|
17
|
+
利用 entry 別の install / config template は [`docs/setup.md`](./docs/setup.md) を参照。
|
|
151
18
|
|
|
152
19
|
## Release
|
|
153
20
|
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
1. Commit and push to `main`
|
|
157
|
-
2. Create and push a tag:
|
|
158
|
-
|
|
159
|
-
```sh
|
|
160
|
-
git tag v1.0.0
|
|
161
|
-
git push --tags
|
|
162
|
-
```
|
|
163
|
-
|
|
164
|
-
3. GitHub Actions will automatically publish to npm
|
|
21
|
+
Git tag (`vX.Y.Z`) を push すると GitHub Actions が npm に publish する。
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yasainet/eslint",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.75",
|
|
4
4
|
"description": "ESLint",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -17,6 +17,9 @@
|
|
|
17
17
|
"default": "./src/deno/index.mjs"
|
|
18
18
|
}
|
|
19
19
|
},
|
|
20
|
+
"bin": {
|
|
21
|
+
"test-audit": "./src/cli/test-audit.mjs"
|
|
22
|
+
},
|
|
20
23
|
"files": [
|
|
21
24
|
"src"
|
|
22
25
|
],
|
|
@@ -32,6 +35,9 @@
|
|
|
32
35
|
"publishConfig": {
|
|
33
36
|
"access": "public"
|
|
34
37
|
},
|
|
38
|
+
"scripts": {
|
|
39
|
+
"docs": "node scripts/generate-rules-catalog.mjs"
|
|
40
|
+
},
|
|
35
41
|
"dependencies": {
|
|
36
42
|
"@stylistic/eslint-plugin": "^5.9.0",
|
|
37
43
|
"eslint-plugin-better-tailwindcss": "^4.1.1",
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
|
|
5
|
+
// pure layer (schemas / utils) の unit test presence を機械チェックする:
|
|
6
|
+
//
|
|
7
|
+
// - 各 source に 兄弟 *.test.ts が存在するか、`@unit-exempt:` marker を持つことを要求
|
|
8
|
+
// - schemas は定義上 pure。utils は impure 混在のため marker で opt-out できる
|
|
9
|
+
// - ESLint の per-file モデルと噛み合わない「存在強制」を全ツリー一括監査で担う
|
|
10
|
+
|
|
11
|
+
const REQUIRE_DIRS = new Set(["schemas", "utils"]);
|
|
12
|
+
const EXEMPT_RE = /@unit-exempt:/;
|
|
13
|
+
|
|
14
|
+
function getFlag(argv, name) {
|
|
15
|
+
const i = argv.indexOf(name);
|
|
16
|
+
return i !== -1 && argv[i + 1] ? argv[i + 1] : null;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function* walk(dir) {
|
|
20
|
+
if (!fs.existsSync(dir)) {
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
24
|
+
const full = path.join(dir, entry.name);
|
|
25
|
+
if (entry.isDirectory()) {
|
|
26
|
+
yield* walk(full);
|
|
27
|
+
} else if (entry.isFile()) {
|
|
28
|
+
yield full;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function isTarget(file) {
|
|
34
|
+
if (!file.endsWith(".ts")) {
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
if (file.endsWith(".test.ts") || file.endsWith(".d.ts")) {
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
return REQUIRE_DIRS.has(path.basename(path.dirname(file)));
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function isSatisfied(file) {
|
|
44
|
+
const testFile = file.replace(/\.ts$/, ".test.ts");
|
|
45
|
+
if (fs.existsSync(testFile)) {
|
|
46
|
+
return true;
|
|
47
|
+
}
|
|
48
|
+
return EXEMPT_RE.test(fs.readFileSync(file, "utf8"));
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function main() {
|
|
52
|
+
const argv = process.argv.slice(2);
|
|
53
|
+
|
|
54
|
+
if (argv.includes("--help") || argv.includes("-h")) {
|
|
55
|
+
console.log(
|
|
56
|
+
"test-audit — pure layer (schemas/utils) の unit test presence を検査\n\n" +
|
|
57
|
+
"Usage: test-audit [--feature-root <path>]\n\n" +
|
|
58
|
+
" --feature-root <path> feature root (default: src/features)\n\n" +
|
|
59
|
+
"各 schemas/*.ts と utils/*.ts に 兄弟 *.test.ts または\n" +
|
|
60
|
+
"`// @unit-exempt: <理由>` marker を要求する。",
|
|
61
|
+
);
|
|
62
|
+
process.exit(0);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const projectRoot = process.cwd();
|
|
66
|
+
const featureRoot = getFlag(argv, "--feature-root") ?? "src/features";
|
|
67
|
+
const roots = [path.join(projectRoot, featureRoot)];
|
|
68
|
+
|
|
69
|
+
const violations = [];
|
|
70
|
+
for (const root of roots) {
|
|
71
|
+
for (const file of walk(root)) {
|
|
72
|
+
if (isTarget(file) && !isSatisfied(file)) {
|
|
73
|
+
violations.push(path.relative(projectRoot, file));
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (violations.length > 0) {
|
|
79
|
+
console.error(
|
|
80
|
+
`✗ test-audit: ${violations.length} 件の pure layer に unit test も @unit-exempt marker もありません:\n`,
|
|
81
|
+
);
|
|
82
|
+
for (const v of violations.sort()) {
|
|
83
|
+
console.error(` ${v}`);
|
|
84
|
+
}
|
|
85
|
+
console.error(
|
|
86
|
+
"\n対応: 兄弟 *.test.ts を追加するか、impure な場合は\n" +
|
|
87
|
+
"`// @unit-exempt: <理由>` を記載する。",
|
|
88
|
+
);
|
|
89
|
+
process.exit(1);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
console.log(
|
|
93
|
+
"✓ test-audit: schemas / utils はすべて unit test または @unit-exempt が揃っています。",
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
main();
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# src/common/CLAUDE.md
|
|
2
|
+
|
|
3
|
+
全 entry で共有する rule の置き場所判断 (target 基準):
|
|
4
|
+
|
|
5
|
+
- `_internal/` — private 実装詳細 (constants / plugins / selectors / import-patterns)。consumer から見えない
|
|
6
|
+
- `base/` — 全ファイル対象の generic rule (TypeScript syntactic / type-aware)
|
|
7
|
+
- `boundaries/<surface>.mjs` — 外界 → features の入口で enforce する契約
|
|
8
|
+
- `cross-cutting/` — 複数 layer に跨る規約
|
|
9
|
+
- 例 (naming): feature-name / namespace-import / form-state
|
|
10
|
+
- 例 (rule): logger / jsdoc / no-any-return / supabase-columns-satisfies
|
|
11
|
+
- `layers/<layer>.mjs` — features 内部の階層単位
|
|
12
|
+
- 種類: queries / services / entries / utils / constants / schemas / types / lib / top-level-utils
|
|
13
|
+
- 1 layer の全制約 (naming + syntax + imports + local rules) を 1 file に集約
|
|
14
|
+
- `local-plugins/` — ESLint local plugin の実装本体
|
|
15
|
+
- `index.mjs` — common entry。上記 file を合成して export
|
|
16
|
+
|
|
17
|
+
新規 rule の glob が単一 layer に閉じるなら `layers/`、跨ぐなら `cross-cutting/`、外界の caller surface なら `boundaries/` に置く。
|
|
@@ -20,22 +20,8 @@ function findProjectRoot() {
|
|
|
20
20
|
|
|
21
21
|
const PROJECT_ROOT = findProjectRoot();
|
|
22
22
|
|
|
23
|
-
|
|
24
|
-
* Files / basenames that should never become a prefix:
|
|
25
|
-
*
|
|
26
|
-
* - `types.ts` / `type.ts`: 型定義のみで lib の役割を持たない (単複どちらの命名でも除外)
|
|
27
|
-
* - `proxy.ts`: middleware adapter (Next.js の proxy.ts と意味が衝突するため queries から呼ばせない)
|
|
28
|
-
*/
|
|
29
|
-
const EXCLUDE_LIST = ["types.ts", "type.ts", "proxy.ts"];
|
|
23
|
+
const EXCLUDE_LIST = ["type.ts", "proxy.ts"];
|
|
30
24
|
|
|
31
|
-
/**
|
|
32
|
-
* Scan lib directory and build prefix-to-lib-relative-path mapping:
|
|
33
|
-
*
|
|
34
|
-
* - single-client lib (`lib/<dir>/index.ts`): prefix = dir 名、entry のみ登録 — 同 dir 内の他ファイル (parser 等 sub-module) は自動除外
|
|
35
|
-
* - multi-client lib (index.ts なし): dir 内の全 `<role>.ts` を登録 (e.g., supabase の admin / server / client)
|
|
36
|
-
* - 多重拡張子 (`.test.ts` 等) を持つファイルは sub-module / 非 lib として除外
|
|
37
|
-
* - types.ts / type.ts / proxy.ts のような lib として queries から呼ばせたくないものは EXCLUDE_LIST で除外
|
|
38
|
-
*/
|
|
39
25
|
export function generatePrefixLibMapping(featureRoot) {
|
|
40
26
|
const libRoot = featureRoot.replace(/features$/, "lib");
|
|
41
27
|
const libDir = path.join(PROJECT_ROOT, libRoot);
|
|
@@ -61,11 +47,8 @@ export function generatePrefixLibMapping(featureRoot) {
|
|
|
61
47
|
.map((e) => e.name);
|
|
62
48
|
|
|
63
49
|
if (plainTsFiles.includes("index.ts")) {
|
|
64
|
-
// single-client lib: index.ts を entry とみなし、prefix = dir 名で登録
|
|
65
|
-
// 同 dir 内の他ファイル (parser 等) は sub-module として自動除外
|
|
66
50
|
mapping[entry.name] = `${entry.name}/index`;
|
|
67
51
|
} else {
|
|
68
|
-
// multi-client lib: 全 role file を登録 (e.g., supabase の admin / server / client)
|
|
69
52
|
for (const fileName of plainTsFiles) {
|
|
70
53
|
const prefix = fileName.replace(/\.ts$/, "");
|
|
71
54
|
mapping[prefix] = `${entry.name}/${prefix}`;
|
|
@@ -80,7 +63,6 @@ export function generatePrefixLibMapping(featureRoot) {
|
|
|
80
63
|
return mapping;
|
|
81
64
|
}
|
|
82
65
|
|
|
83
|
-
/** Build glob patterns scoped to the given feature root. */
|
|
84
66
|
export const featuresGlob = (featureRoot, subpath) => [
|
|
85
67
|
`${featureRoot}/${subpath}`,
|
|
86
68
|
];
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export const LIB_BOUNDARY_PATTERNS = [
|
|
2
|
+
{
|
|
3
|
+
group: ["@/lib/*", "@/lib/**"],
|
|
4
|
+
message:
|
|
5
|
+
"lib/* は queries からのみ import 可。他層は queries 経由で使う。",
|
|
6
|
+
},
|
|
7
|
+
];
|
|
8
|
+
|
|
9
|
+
export const MAPPING_PATTERNS = [
|
|
10
|
+
{
|
|
11
|
+
group: ["@/utils/mapping.util"],
|
|
12
|
+
importNames: ["mapSnakeToCamel", "mapCamelToSnake"],
|
|
13
|
+
message:
|
|
14
|
+
"mapping 関数は services のみ許可。snake/camel 変換は service 境界で行う。",
|
|
15
|
+
},
|
|
16
|
+
];
|
|
@@ -3,7 +3,6 @@ import checkFile from "eslint-plugin-check-file";
|
|
|
3
3
|
import jsdocPlugin from "eslint-plugin-jsdoc";
|
|
4
4
|
import simpleImportSortPlugin from "eslint-plugin-simple-import-sort";
|
|
5
5
|
|
|
6
|
-
/** Shared plugin instances used across ESLint configs. */
|
|
7
6
|
export const plugins = {
|
|
8
7
|
"@stylistic": stylistic,
|
|
9
8
|
"check-file": checkFile,
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export const loggerSelector = "CallExpression[callee.object.name='logger']";
|
|
2
|
+
|
|
3
|
+
export const loggerMessage =
|
|
4
|
+
"logger は entries 以外で禁止。ログ出力は entries に集約する。";
|
|
5
|
+
|
|
6
|
+
export const aliasDynamicImportSelector =
|
|
7
|
+
"ImportExpression[source.type='Literal'][source.value=/^@\\//]";
|
|
8
|
+
|
|
9
|
+
export const aliasDynamicImportMessage =
|
|
10
|
+
"features layers で `@/` パスの動的 import は禁止 (prefix-lib / lateral 制約を迂回する):\n" +
|
|
11
|
+
"- 内部依存は queries/<prefix>.ts か services/<prefix>.ts を作る\n" +
|
|
12
|
+
"- 外部 npm は cold-start 最適化の遅延 import なら可";
|
|
@@ -3,17 +3,15 @@ import { dirname, join, sep } from "node:path";
|
|
|
3
3
|
|
|
4
4
|
import tseslint from "typescript-eslint";
|
|
5
5
|
|
|
6
|
-
import { simpleImportSortPlugin, stylistic } from "
|
|
6
|
+
import { simpleImportSortPlugin, stylistic } from "../_internal/plugins.mjs";
|
|
7
7
|
|
|
8
|
-
// When evaluated under LSP servers like vscode-eslint, `process.cwd()` returns
|
|
9
|
-
// the linted file's directory rather than the consumer's project root, so it
|
|
10
|
-
// cannot be used to derive `tsconfigRootDir`. Walk up from this module until a
|
|
11
|
-
// `tsconfig.json` outside of `node_modules` is found. Falls back to
|
|
12
|
-
// `process.cwd()` for CLI parity if no such directory is reachable.
|
|
13
8
|
const findProjectRoot = (start) => {
|
|
14
9
|
let dir = start;
|
|
15
10
|
while (dir !== dirname(dir)) {
|
|
16
|
-
if (
|
|
11
|
+
if (
|
|
12
|
+
!dir.split(sep).includes("node_modules") &&
|
|
13
|
+
existsSync(join(dir, "tsconfig.json"))
|
|
14
|
+
) {
|
|
17
15
|
return dir;
|
|
18
16
|
}
|
|
19
17
|
dir = dirname(dir);
|
|
@@ -31,6 +29,7 @@ const sharedRulesConfig = {
|
|
|
31
29
|
},
|
|
32
30
|
rules: {
|
|
33
31
|
"no-console": "warn",
|
|
32
|
+
|
|
34
33
|
"no-irregular-whitespace": [
|
|
35
34
|
"warn",
|
|
36
35
|
{
|
|
@@ -40,20 +39,21 @@ const sharedRulesConfig = {
|
|
|
40
39
|
skipTemplates: false,
|
|
41
40
|
},
|
|
42
41
|
],
|
|
43
|
-
"simple-import-sort/imports": "warn",
|
|
44
|
-
"simple-import-sort/exports": "warn",
|
|
45
|
-
"@stylistic/quotes": ["warn", "double", { avoidEscape: true }],
|
|
46
|
-
// Dead code detection: rules with no legitimate use case, so always safe to error.
|
|
47
42
|
"no-unreachable": "error",
|
|
48
|
-
"no-unreachable-loop": "error",
|
|
49
|
-
"no-useless-return": "error",
|
|
50
43
|
"no-constant-condition": "error",
|
|
51
44
|
"no-constant-binary-expression": "error",
|
|
52
45
|
"no-dupe-else-if": "error",
|
|
53
46
|
"no-self-assign": "error",
|
|
54
|
-
"no-self-compare": "error",
|
|
55
47
|
"no-useless-catch": "error",
|
|
56
48
|
"no-fallthrough": "error",
|
|
49
|
+
|
|
50
|
+
"no-unreachable-loop": "error",
|
|
51
|
+
"no-useless-return": "error",
|
|
52
|
+
"no-self-compare": "error",
|
|
53
|
+
|
|
54
|
+
"simple-import-sort/imports": "warn",
|
|
55
|
+
"simple-import-sort/exports": "warn",
|
|
56
|
+
"@stylistic/quotes": ["warn", "double", { avoidEscape: true }],
|
|
57
57
|
},
|
|
58
58
|
};
|
|
59
59
|
|
|
@@ -74,18 +74,11 @@ const syntacticTypeScriptRules = {
|
|
|
74
74
|
};
|
|
75
75
|
|
|
76
76
|
const typeAwareTypeScriptRules = {
|
|
77
|
-
// Detect defensive fallbacks on non-nullable values (e.g., `?? ''`
|
|
78
|
-
// on a non-null column). Promoted to error once consuming projects
|
|
79
|
-
// (bitcomic.net, getpayme.net) reached 0 warnings.
|
|
80
77
|
"@typescript-eslint/no-unnecessary-condition": "error",
|
|
81
|
-
// Type-aware async safety: silent await omissions are a leading cause
|
|
82
|
-
// of race conditions in server actions and background tasks.
|
|
83
78
|
"@typescript-eslint/no-floating-promises": "error",
|
|
84
79
|
"@typescript-eslint/no-misused-promises": "error",
|
|
85
80
|
"@typescript-eslint/await-thenable": "error",
|
|
86
81
|
"@typescript-eslint/require-await": "error",
|
|
87
|
-
// Type-aware `any` propagation checks: any が境界を越えた瞬間に
|
|
88
|
-
// 残りのコードで型検査が無効化されるため、検出したら確実に止める。
|
|
89
82
|
"@typescript-eslint/no-unsafe-assignment": "error",
|
|
90
83
|
"@typescript-eslint/no-unsafe-call": "error",
|
|
91
84
|
"@typescript-eslint/no-unsafe-member-access": "error",
|
|
@@ -97,21 +90,7 @@ const typeAwareRulesOff = Object.fromEntries(
|
|
|
97
90
|
Object.keys(typeAwareTypeScriptRules).map((rule) => [rule, "off"]),
|
|
98
91
|
);
|
|
99
92
|
|
|
100
|
-
|
|
101
|
-
* Build base rule configs:
|
|
102
|
-
*
|
|
103
|
-
* - `typeAware: true` (default) enables `projectService` and type-aware rules
|
|
104
|
-
* (`no-unnecessary-condition`, `no-floating-promises`, `no-unsafe-*`, etc.)
|
|
105
|
-
* for the matched `files`
|
|
106
|
-
* - `typeAware: false` disables `projectService` and forces type-aware rules
|
|
107
|
-
* off for the matched `files`. Use for files outside the project tsconfig
|
|
108
|
-
* (e.g., Deno files in Supabase Edge Functions)
|
|
109
|
-
*
|
|
110
|
-
* `files` defaults to all TypeScript sources. When combining multiple entries
|
|
111
|
-
* (e.g., next + deno), pass a narrow pattern so the type-aware override only
|
|
112
|
-
* applies to its target files.
|
|
113
|
-
*/
|
|
114
|
-
export function createRulesConfigs({
|
|
93
|
+
export function createTypescriptConfigs({
|
|
115
94
|
typeAware = true,
|
|
116
95
|
files = ["**/*.ts", "**/*.tsx"],
|
|
117
96
|
} = {}) {
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
/** Ban `import * as` at entry points. */
|
|
2
1
|
export function createEntryPointConfigs(entryPointFiles, entryPointIgnores = []) {
|
|
3
2
|
return [
|
|
4
3
|
{
|
|
@@ -11,7 +10,7 @@ export function createEntryPointConfigs(entryPointFiles, entryPointIgnores = [])
|
|
|
11
10
|
{
|
|
12
11
|
selector: "ImportDeclaration:has(ImportNamespaceSpecifier)",
|
|
13
12
|
message:
|
|
14
|
-
"
|
|
13
|
+
"entry point は `import * as` 禁止。named import で依存を明示する。",
|
|
15
14
|
},
|
|
16
15
|
],
|
|
17
16
|
},
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export function createBanAliasConfigs({ featureRoot }) {
|
|
2
|
+
return [
|
|
3
|
+
{
|
|
4
|
+
name: "imports/ban-alias",
|
|
5
|
+
files: [`${featureRoot}/**/*.ts`],
|
|
6
|
+
rules: {
|
|
7
|
+
"no-restricted-imports": [
|
|
8
|
+
"error",
|
|
9
|
+
{
|
|
10
|
+
patterns: [
|
|
11
|
+
{
|
|
12
|
+
group: ["@/*", "@/**"],
|
|
13
|
+
message:
|
|
14
|
+
"この環境では alias import (@/) は使えない。相対パスを使う。",
|
|
15
|
+
},
|
|
16
|
+
],
|
|
17
|
+
},
|
|
18
|
+
],
|
|
19
|
+
},
|
|
20
|
+
},
|
|
21
|
+
];
|
|
22
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import {
|
|
2
|
+
LIB_BOUNDARY_PATTERNS,
|
|
3
|
+
MAPPING_PATTERNS,
|
|
4
|
+
} from "../_internal/import-patterns.mjs";
|
|
5
|
+
|
|
6
|
+
export function createFeatureDefaultImportsConfigs({ featureRoot }) {
|
|
7
|
+
return [
|
|
8
|
+
{
|
|
9
|
+
name: "imports/feature-other",
|
|
10
|
+
files: [`${featureRoot}/**/*.ts`],
|
|
11
|
+
ignores: [
|
|
12
|
+
`${featureRoot}/**/services/*.ts`,
|
|
13
|
+
`${featureRoot}/**/queries/*.ts`,
|
|
14
|
+
`${featureRoot}/**/entries/*.ts`,
|
|
15
|
+
`${featureRoot}/**/utils/*.ts`,
|
|
16
|
+
`${featureRoot}/**/types/*.ts`,
|
|
17
|
+
],
|
|
18
|
+
rules: {
|
|
19
|
+
"no-restricted-imports": [
|
|
20
|
+
"error",
|
|
21
|
+
{ patterns: [...LIB_BOUNDARY_PATTERNS, ...MAPPING_PATTERNS] },
|
|
22
|
+
],
|
|
23
|
+
},
|
|
24
|
+
},
|
|
25
|
+
];
|
|
26
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { featuresGlob } from "../_internal/constants.mjs";
|
|
2
|
+
import { localPlugin } from "../local-plugins/index.mjs";
|
|
3
|
+
|
|
4
|
+
export function createFeatureNameConfigs({ featureRoot }) {
|
|
5
|
+
return [
|
|
6
|
+
{
|
|
7
|
+
name: "naming/feature-name",
|
|
8
|
+
files: featuresGlob(featureRoot, "**/*.ts"),
|
|
9
|
+
plugins: { local: localPlugin },
|
|
10
|
+
rules: {
|
|
11
|
+
"local/feature-name": ["error", { featureRoot }],
|
|
12
|
+
},
|
|
13
|
+
},
|
|
14
|
+
];
|
|
15
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { featuresGlob } from "../_internal/constants.mjs";
|
|
2
|
+
|
|
3
|
+
export function createFeaturesTsOnlyConfigs({ featureRoot }) {
|
|
4
|
+
return [
|
|
5
|
+
{
|
|
6
|
+
name: "naming/features-ts-only",
|
|
7
|
+
files: featuresGlob(featureRoot, "**/*.tsx"),
|
|
8
|
+
rules: {
|
|
9
|
+
"no-restricted-syntax": [
|
|
10
|
+
"error",
|
|
11
|
+
{
|
|
12
|
+
selector: "Program",
|
|
13
|
+
message:
|
|
14
|
+
"features/ は .ts のみ。component は src/components/ に置く。",
|
|
15
|
+
},
|
|
16
|
+
],
|
|
17
|
+
},
|
|
18
|
+
},
|
|
19
|
+
];
|
|
20
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { featuresGlob } from "../_internal/constants.mjs";
|
|
2
|
+
import { localPlugin } from "../local-plugins/index.mjs";
|
|
3
|
+
|
|
4
|
+
export function createFormStateConfigs({ featureRoot }) {
|
|
5
|
+
return [
|
|
6
|
+
{
|
|
7
|
+
name: "naming/form-state",
|
|
8
|
+
files: featuresGlob(featureRoot, "**/*.ts"),
|
|
9
|
+
plugins: { local: localPlugin },
|
|
10
|
+
rules: {
|
|
11
|
+
"local/form-state-naming": "error",
|
|
12
|
+
"local/form-state-shape": "error",
|
|
13
|
+
},
|
|
14
|
+
},
|
|
15
|
+
];
|
|
16
|
+
}
|
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
import jsdocPlugin from "eslint-plugin-jsdoc";
|
|
2
2
|
|
|
3
|
-
import { featuresGlob } from "
|
|
3
|
+
import { featuresGlob } from "../_internal/constants.mjs";
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
export function createJsdocConfigs(featureRoot) {
|
|
5
|
+
export function createJsdocConfigs({ featureRoot }) {
|
|
7
6
|
return [
|
|
8
7
|
{
|
|
9
8
|
name: "jsdoc",
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import {
|
|
2
|
+
loggerMessage,
|
|
3
|
+
loggerSelector,
|
|
4
|
+
} from "../_internal/selectors.mjs";
|
|
5
|
+
|
|
6
|
+
export function createLoggerConfigs({ featureRoot }) {
|
|
7
|
+
return [
|
|
8
|
+
{
|
|
9
|
+
name: "layers/logger",
|
|
10
|
+
files: [`${featureRoot}/**/*.ts`],
|
|
11
|
+
ignores: [`${featureRoot}/**/entries/*.ts`],
|
|
12
|
+
rules: {
|
|
13
|
+
"no-console": "error",
|
|
14
|
+
"no-restricted-syntax": [
|
|
15
|
+
"error",
|
|
16
|
+
{ selector: loggerSelector, message: loggerMessage },
|
|
17
|
+
],
|
|
18
|
+
},
|
|
19
|
+
},
|
|
20
|
+
];
|
|
21
|
+
}
|