@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
|
@@ -1,13 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Enforce {Verb}{Subject}FormState pattern for FormState type names.
|
|
3
|
-
*
|
|
4
|
-
* Targets TSInterfaceDeclaration and TSTypeAliasDeclaration whose name ends
|
|
5
|
-
* with "FormState". Requires at least two PascalCase words before FormState
|
|
6
|
-
* (e.g. SignInFormState, CreateCommentFormState) so the verb prefix is never
|
|
7
|
-
* omitted. Single-noun names like "ContactFormState" are forbidden — rename
|
|
8
|
-
* to "CreateContactFormState" to make intent explicit.
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
1
|
const FORM_STATE_ALLOW = /^[A-Z][a-z]+[A-Z]\w*FormState$/;
|
|
12
2
|
|
|
13
3
|
function reportIfInvalid(context, idNode) {
|
|
@@ -26,7 +16,7 @@ export const formStateNamingRule = {
|
|
|
26
16
|
type: "problem",
|
|
27
17
|
messages: {
|
|
28
18
|
invalidName:
|
|
29
|
-
"FormState
|
|
19
|
+
"FormState 型 '{{ name }}' は {Verb}{Subject}FormState 形式にする (例 CreateCommentFormState, SignInFormState)。PascalCase 2 語以上が必須。",
|
|
30
20
|
},
|
|
31
21
|
schema: [],
|
|
32
22
|
},
|
|
@@ -1,37 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Enforce the canonical `{ data, error: { message } }` shape for FormState types.
|
|
3
|
-
*
|
|
4
|
-
* Targets `TSInterfaceDeclaration` and `TSTypeAliasDeclaration` whose name ends
|
|
5
|
-
* with `FormState`. Required shape:
|
|
6
|
-
*
|
|
7
|
-
* ```ts
|
|
8
|
-
* interface XxxFormState {
|
|
9
|
-
* data: T | null; // T may be any type; `data: null` literal also OK
|
|
10
|
-
* error: { message: string } | null;
|
|
11
|
-
* }
|
|
12
|
-
* ```
|
|
13
|
-
*
|
|
14
|
-
* Why this shape:
|
|
15
|
-
*
|
|
16
|
-
* - Single uniform shape lets entry / hook / UI be templated and lint-checked.
|
|
17
|
-
* - Forbids `code` / `status` / similar discriminator fields that tend to grow
|
|
18
|
-
* defensive branches (e.g. `error.code === "unauthenticated"`) which usually
|
|
19
|
-
* indicate a missing upstream guard (route gate, button conditional render),
|
|
20
|
-
* not a legitimate need for in-action discrimination.
|
|
21
|
-
* - Discriminated unions (`{ data: T; error: null } | { data: null; error: ... }`)
|
|
22
|
-
* are rejected for the same reason — they leak shape complexity into hooks
|
|
23
|
-
* (`useActionState<T | null>`), components (`state?.error`), and entries.
|
|
24
|
-
*
|
|
25
|
-
* Future extension when Stripe / payment libraries actually need `code` / `status`:
|
|
26
|
-
*
|
|
27
|
-
* - a) Add a `// allow-error-code: <reason>` comment opt-out to this rule, so
|
|
28
|
-
* individual FormStates can justify their extra field on the previous line.
|
|
29
|
-
* - b) Change the rule to whitelist a fixed set of allowed extra fields (enum
|
|
30
|
-
* of standard error codes shared across the codebase).
|
|
31
|
-
*
|
|
32
|
-
* Until that need materializes, this rule is strict (no opt-out) per YAGNI.
|
|
33
|
-
*/
|
|
34
|
-
|
|
35
1
|
const FORM_STATE_SUFFIX = "FormState";
|
|
36
2
|
|
|
37
3
|
function isNullLiteralType(node) {
|
|
@@ -168,21 +134,21 @@ export const formStateShapeRule = {
|
|
|
168
134
|
},
|
|
169
135
|
messages: {
|
|
170
136
|
dataMissing:
|
|
171
|
-
"FormState '{{ name }}'
|
|
137
|
+
"FormState '{{ name }}' に `data` プロパティが必須 (payload が無ければ `data: null`)。",
|
|
172
138
|
dataNotNullable:
|
|
173
|
-
"FormState '{{ name }}' `data`
|
|
139
|
+
"FormState '{{ name }}' の `data` は null 許容にする (例: `data: T | null` / `data: null`)。",
|
|
174
140
|
errorMissing:
|
|
175
|
-
"FormState '{{ name }}'
|
|
141
|
+
"FormState '{{ name }}' に `error: { message: string } | null` プロパティが必須。",
|
|
176
142
|
errorNotNullable:
|
|
177
|
-
"FormState '{{ name }}' `error`
|
|
143
|
+
"FormState '{{ name }}' の `error` は nullable にする (`{ message: string } | null`)。",
|
|
178
144
|
errorWrongShape:
|
|
179
|
-
"FormState '{{ name }}' `error`
|
|
145
|
+
"FormState '{{ name }}' の `error` は厳密に `{ message: string } | null`。",
|
|
180
146
|
errorExtraField:
|
|
181
|
-
"FormState '{{ name }}' `error`
|
|
147
|
+
"FormState '{{ name }}' の `error` は `message: string` のみ許可。禁止フィールド: '{{ field }}'。",
|
|
182
148
|
extraProperty:
|
|
183
|
-
"FormState '{{ name }}'
|
|
149
|
+
"FormState '{{ name }}' は `data` と `error` のみ。禁止プロパティ: '{{ field }}'。",
|
|
184
150
|
discriminatedUnion:
|
|
185
|
-
"FormState '{{ name }}'
|
|
151
|
+
"FormState '{{ name }}' は単一の interface か type literal にする。discriminated union 不可。",
|
|
186
152
|
},
|
|
187
153
|
schema: [],
|
|
188
154
|
},
|
|
@@ -1,18 +1,13 @@
|
|
|
1
1
|
import path from "path";
|
|
2
2
|
|
|
3
|
-
/**
|
|
4
|
-
* Enforce import path style within features:
|
|
5
|
-
* - Same-feature imports must use relative paths
|
|
6
|
-
* - Cross-feature imports must use @/ alias
|
|
7
|
-
*/
|
|
8
3
|
export const importPathStyleRule = {
|
|
9
4
|
meta: {
|
|
10
5
|
type: "problem",
|
|
11
6
|
messages: {
|
|
12
7
|
useRelative:
|
|
13
|
-
"
|
|
8
|
+
"同一 feature の import は相対パスにする ('{{ importPath }}' を使わない)。",
|
|
14
9
|
useAlias:
|
|
15
|
-
"
|
|
10
|
+
"feature を跨ぐ import は '@/' にする (相対パス '{{ importPath }}' を使わない)。",
|
|
16
11
|
},
|
|
17
12
|
schema: [
|
|
18
13
|
{
|
|
@@ -48,7 +43,6 @@ export const importPathStyleRule = {
|
|
|
48
43
|
if (!source || typeof source.value !== "string") return;
|
|
49
44
|
const importPath = source.value;
|
|
50
45
|
|
|
51
|
-
// Same-feature alias should be a relative path for consistency.
|
|
52
46
|
if (
|
|
53
47
|
importPath.startsWith(sameFeaturePrefix) ||
|
|
54
48
|
importPath === sameFeatureExact
|
|
@@ -61,7 +55,6 @@ export const importPathStyleRule = {
|
|
|
61
55
|
return;
|
|
62
56
|
}
|
|
63
57
|
|
|
64
|
-
// Cross-feature relative path should use @/ alias for clarity.
|
|
65
58
|
if (importPath.startsWith(".")) {
|
|
66
59
|
const resolved = path.resolve(path.dirname(filename), importPath);
|
|
67
60
|
if (
|
|
@@ -7,13 +7,13 @@ import { importPathStyleRule } from "./import-path-style.mjs";
|
|
|
7
7
|
import { layoutMainStructuralOnlyRule } from "./layout-main-structural-only.mjs";
|
|
8
8
|
import { namespaceImportNameRule } from "./namespace-import-name.mjs";
|
|
9
9
|
import { noAnyReturnRule } from "./no-any-return.mjs";
|
|
10
|
+
import { noColocatedTestRule } from "./no-colocated-test.mjs";
|
|
10
11
|
import { queriesExportRule } from "./queries-export.mjs";
|
|
11
12
|
import { queriesNamespaceImportRule } from "./queries-namespace-import.mjs";
|
|
12
13
|
import { schemaNamingRule } from "./schema-naming.mjs";
|
|
13
14
|
import { supabaseColumnsSatisfiesRule } from "./supabase-columns-satisfies.mjs";
|
|
14
15
|
import { supabaseSelectTypedColumnsRule } from "./supabase-select-typed-columns.mjs";
|
|
15
16
|
|
|
16
|
-
/** Single plugin object to avoid ESLint "Cannot redefine plugin" errors. */
|
|
17
17
|
export const localPlugin = {
|
|
18
18
|
rules: {
|
|
19
19
|
"entry-single-service-call": entrySingleServiceCallRule,
|
|
@@ -25,6 +25,7 @@ export const localPlugin = {
|
|
|
25
25
|
"layout-main-structural-only": layoutMainStructuralOnlyRule,
|
|
26
26
|
"namespace-import-name": namespaceImportNameRule,
|
|
27
27
|
"no-any-return": noAnyReturnRule,
|
|
28
|
+
"no-colocated-test": noColocatedTestRule,
|
|
28
29
|
"queries-export": queriesExportRule,
|
|
29
30
|
"queries-namespace-import": queriesNamespaceImportRule,
|
|
30
31
|
"schema-naming": schemaNamingRule,
|
|
@@ -1,24 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Forbid decoration utility classes on the `<main>` element.
|
|
3
|
-
*
|
|
4
|
-
* Design philosophy: `app/**\/layout.tsx` の `<main>` は Header / Footer の
|
|
5
|
-
* 縦積みを受け持つ構造スロット。装飾(余白・間隔)は page.tsx 側の Container 等が
|
|
6
|
-
* 持つべき。main に padding / margin / gap を直接当てると、page 全体に暗黙の
|
|
7
|
-
* オフセットが生まれ、page ごとの調整余地が失われる。
|
|
8
|
-
*
|
|
9
|
-
* Disallowed Tailwind token prefixes (case-sensitive):
|
|
10
|
-
*
|
|
11
|
-
* - `p`, `m`, `py`, `px`, `pt`, `pb`, `pl`, `pr`, `my`, `mx`, `mt`, `mb`, `ml`, `mr` — padding / margin
|
|
12
|
-
* - `space-x`, `space-y` — sibling spacing
|
|
13
|
-
* - `gap` — flex / grid gap
|
|
14
|
-
*
|
|
15
|
-
* Allowed: structural utilities (`flex`, `flex-1`, `flex-col`, `min-h-*`,
|
|
16
|
-
* `block`, `relative`, etc.)
|
|
17
|
-
*
|
|
18
|
-
* className value forms supported: string Literal, TemplateLiteral,
|
|
19
|
-
* `cn(...)` / `clsx(...)` style CallExpression args (string args only).
|
|
20
|
-
*/
|
|
21
|
-
|
|
22
1
|
const DISALLOWED_TOKEN = /^(?:[pm][xytrbl]?-|space-[xy]-|gap-)/;
|
|
23
2
|
|
|
24
3
|
function collectStringLiterals(node, out) {
|
|
@@ -68,7 +47,7 @@ export const layoutMainStructuralOnlyRule = {
|
|
|
68
47
|
type: "problem",
|
|
69
48
|
messages: {
|
|
70
49
|
invalidToken:
|
|
71
|
-
"
|
|
50
|
+
"layout.tsx の <main> は構造のみ。spacing/装飾 ({{ tokens }}) は page.tsx へ移す (例 <Container className=\"py-8\">)。",
|
|
72
51
|
},
|
|
73
52
|
schema: [],
|
|
74
53
|
},
|
|
@@ -1,13 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Enforce consistent naming for `import * as` namespace imports
|
|
3
|
-
* within feature-based architecture.
|
|
4
|
-
*
|
|
5
|
-
* Convention: import * as {featureName}{Scope}{Layer} from "{path}/{layerDir}/{scope}"
|
|
6
|
-
*
|
|
7
|
-
* Layer はファイル名 suffix ではなくディレクトリ名 (`queries/` / `services/` 等) から識別する。
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
|
-
/** @type {Record<string, string>} */
|
|
11
1
|
const LAYER_DIR_MAP = {
|
|
12
2
|
queries: "Query",
|
|
13
3
|
services: "Service",
|
|
@@ -19,39 +9,27 @@ const LAYER_DIR_MAP = {
|
|
|
19
9
|
constants: "Constant",
|
|
20
10
|
};
|
|
21
11
|
|
|
22
|
-
/** Convert a snake_case or kebab-case string to camelCase. */
|
|
23
12
|
function toCamelCase(str) {
|
|
24
13
|
return str.replace(/[-_]+(.)/g, (_, c) => c.toUpperCase());
|
|
25
14
|
}
|
|
26
15
|
|
|
27
|
-
/** Convert a snake_case or kebab-case string to PascalCase. */
|
|
28
16
|
function toPascalCase(str) {
|
|
29
17
|
const camel = toCamelCase(str);
|
|
30
18
|
return camel.charAt(0).toUpperCase() + camel.slice(1);
|
|
31
19
|
}
|
|
32
20
|
|
|
33
|
-
/**
|
|
34
|
-
* Parse import source to extract featureName, scope, and layer:
|
|
35
|
-
*
|
|
36
|
-
* - feature root 内の path のみを対象とする (外部 lib / shared / 相対インポートで feature 外に行くものは無視)
|
|
37
|
-
* - segments は `[feature, layerDir, ..., file]` の形式を想定し、末尾要素を scope として取り出す
|
|
38
|
-
* - layerDir が LAYER_DIR_MAP に無い場合は対象外
|
|
39
|
-
*/
|
|
40
21
|
function parseImportSource(importPath, featureRoot) {
|
|
41
|
-
// Normalize alias: @/features/... → features/...
|
|
42
22
|
let normalized = importPath;
|
|
43
23
|
const aliasBase = featureRoot.replace(/^src\//, "@/");
|
|
44
24
|
if (normalized.startsWith(aliasBase + "/")) {
|
|
45
25
|
normalized = featureRoot + normalized.slice(aliasBase.length);
|
|
46
26
|
}
|
|
47
27
|
|
|
48
|
-
// Only process paths within the feature root
|
|
49
28
|
const rootPrefix = featureRoot + "/";
|
|
50
29
|
const rootIdx = normalized.indexOf(rootPrefix);
|
|
51
30
|
if (rootIdx === -1) return null;
|
|
52
31
|
|
|
53
32
|
const afterRoot = normalized.slice(rootIdx + rootPrefix.length);
|
|
54
|
-
// Expected: {feature}/{layerDir}/{scope}
|
|
55
33
|
const segments = afterRoot.split("/");
|
|
56
34
|
if (segments.length < 3) return null;
|
|
57
35
|
|
|
@@ -65,7 +43,6 @@ function parseImportSource(importPath, featureRoot) {
|
|
|
65
43
|
return { featureDir, scope, layer };
|
|
66
44
|
}
|
|
67
45
|
|
|
68
|
-
/** Build the expected namespace import name. */
|
|
69
46
|
function buildExpectedName(featureDir, scope, layer) {
|
|
70
47
|
const featureCamel = toCamelCase(featureDir);
|
|
71
48
|
const scopePascal = toPascalCase(scope);
|
|
@@ -78,7 +55,7 @@ export const namespaceImportNameRule = {
|
|
|
78
55
|
type: "suggestion",
|
|
79
56
|
messages: {
|
|
80
57
|
mismatch:
|
|
81
|
-
"
|
|
58
|
+
"namespace import は '{{ expected }}' と命名する ('{{ actual }}' ではなく)。",
|
|
82
59
|
},
|
|
83
60
|
schema: [
|
|
84
61
|
{
|
|
@@ -96,7 +73,6 @@ export const namespaceImportNameRule = {
|
|
|
96
73
|
|
|
97
74
|
return {
|
|
98
75
|
ImportDeclaration(node) {
|
|
99
|
-
// Only check namespace imports: import * as Name
|
|
100
76
|
const nsSpecifier = node.specifiers.find(
|
|
101
77
|
(s) => s.type === "ImportNamespaceSpecifier",
|
|
102
78
|
);
|
|
@@ -104,7 +80,6 @@ export const namespaceImportNameRule = {
|
|
|
104
80
|
|
|
105
81
|
const importPath = node.source.value;
|
|
106
82
|
|
|
107
|
-
// Skip external/built-in imports (only check relative and alias)
|
|
108
83
|
if (
|
|
109
84
|
!importPath.startsWith("./") &&
|
|
110
85
|
!importPath.startsWith("../") &&
|
|
@@ -113,7 +88,6 @@ export const namespaceImportNameRule = {
|
|
|
113
88
|
return;
|
|
114
89
|
}
|
|
115
90
|
|
|
116
|
-
// For relative imports, resolve to an absolute-like path for parsing
|
|
117
91
|
let resolvedPath = importPath;
|
|
118
92
|
if (importPath.startsWith(".")) {
|
|
119
93
|
const fileDir = context.filename.replace(/\/[^/]+$/, "");
|
|
@@ -1,12 +1,5 @@
|
|
|
1
1
|
import ts from "typescript";
|
|
2
2
|
|
|
3
|
-
/**
|
|
4
|
-
* Exported function の return type が any を含んでいる場合に error:
|
|
5
|
-
*
|
|
6
|
-
* - typescript-eslint の type checker を使って inferred type まで見る
|
|
7
|
-
* - queries / services の API 境界を any 汚染から守ることで、domain shape が型で保証される
|
|
8
|
-
* - Promise<any>, Promise<{ data: any }>, Array<any> など nested も展開して検査する
|
|
9
|
-
*/
|
|
10
3
|
export const noAnyReturnRule = {
|
|
11
4
|
meta: {
|
|
12
5
|
type: "problem",
|
|
@@ -16,7 +9,7 @@ export const noAnyReturnRule = {
|
|
|
16
9
|
},
|
|
17
10
|
messages: {
|
|
18
11
|
anyInReturn:
|
|
19
|
-
"
|
|
12
|
+
"export 関数の推論された返り値型に `any` が含まれる: {{ typeText }}。既知の型を注釈するか絞り込む (public な層の API は型を確定させる)。",
|
|
20
13
|
},
|
|
21
14
|
schema: [],
|
|
22
15
|
},
|
|
@@ -41,7 +34,6 @@ export const noAnyReturnRule = {
|
|
|
41
34
|
return checker.getReturnTypeOfSignature(signature);
|
|
42
35
|
};
|
|
43
36
|
|
|
44
|
-
// `any` を type tree 全体で検出する (Promise<any>, { a: any }, any[] 等を展開)
|
|
45
37
|
const containsAny = (type, seen = new Set()) => {
|
|
46
38
|
if (!type) return false;
|
|
47
39
|
if (seen.has(type)) return false;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* orchestration layer (services / queries / entries) での co-located test を禁止する:
|
|
3
|
+
*
|
|
4
|
+
* - これらは Supabase 等への配線で、unit 化すると mock の echo になる
|
|
5
|
+
* - 検証は e2e に委ね、純粋ロジックは utils へ抽出してそちらを unit する
|
|
6
|
+
*/
|
|
7
|
+
export const noColocatedTestRule = {
|
|
8
|
+
meta: {
|
|
9
|
+
type: "problem",
|
|
10
|
+
messages: {
|
|
11
|
+
forbidden:
|
|
12
|
+
"orchestration layer (services/queries/entries) に test を置かない。" +
|
|
13
|
+
"mock の echo になる:\n" +
|
|
14
|
+
"- 配線の検証は e2e に委ねる\n" +
|
|
15
|
+
"- 純粋ロジックは utils へ抽出し、そちらを unit する",
|
|
16
|
+
},
|
|
17
|
+
schema: [],
|
|
18
|
+
},
|
|
19
|
+
create(context) {
|
|
20
|
+
return {
|
|
21
|
+
Program(node) {
|
|
22
|
+
context.report({ node, messageId: "forbidden" });
|
|
23
|
+
},
|
|
24
|
+
};
|
|
25
|
+
},
|
|
26
|
+
};
|
|
@@ -1,11 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Enforce verb allow list for `queries/*.ts` exports:
|
|
3
|
-
*
|
|
4
|
-
* - The queries layer is the TS-idiomatic translation of Rails 5 actions
|
|
5
|
-
* (index/show -> get, create, update, destroy -> delete)
|
|
6
|
-
* - Auth ceremonies (signUp / signIn / signOut) are admitted as industry-standard exceptions
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
1
|
const QUERIES_ALLOW = /^(get|create|update|delete|signUp|signIn|signOut)([A-Z]|$)/;
|
|
10
2
|
|
|
11
3
|
function isFunctionLike(initNode) {
|
|
@@ -37,7 +29,7 @@ export const queriesExportRule = {
|
|
|
37
29
|
type: "problem",
|
|
38
30
|
messages: {
|
|
39
31
|
invalidName:
|
|
40
|
-
"queries export '{{ name }}'
|
|
32
|
+
"queries の export '{{ name }}' は get, create, update, delete, signUp, signIn, signOut のいずれかで始める。",
|
|
41
33
|
},
|
|
42
34
|
schema: [],
|
|
43
35
|
},
|
|
@@ -1,13 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Enforce namespace imports for files under `queries/`:
|
|
3
|
-
*
|
|
4
|
-
* - Value imports must use `import * as xxxQuery from "..."` so that the
|
|
5
|
-
* `naming/namespace-import-name` rule can guarantee a single canonical
|
|
6
|
-
* binding (e.g. `comicsServerQuery.getComics`)
|
|
7
|
-
* - Type-only imports are exempted because they have no runtime presence
|
|
8
|
-
* - 検出は path に `/queries/<name>` が含まれることのみで行い、suffix には依存しない
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
1
|
const QUERIES_PATH = /\/queries\/[^/]+$/;
|
|
12
2
|
|
|
13
3
|
export const queriesNamespaceImportRule = {
|
|
@@ -15,7 +5,7 @@ export const queriesNamespaceImportRule = {
|
|
|
15
5
|
type: "problem",
|
|
16
6
|
messages: {
|
|
17
7
|
useNamespace:
|
|
18
|
-
'
|
|
8
|
+
'queries 層は named import でなく `import * as xxxQuery from "{{ source }}"` を使う (`import type {}` は可)。',
|
|
19
9
|
},
|
|
20
10
|
schema: [],
|
|
21
11
|
},
|
|
@@ -1,17 +1,11 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Enforce that exported variables in schema files use camelCase with a "Schema" suffix.
|
|
3
|
-
* e.g., `export const userSchema = z.object(...)` is valid.
|
|
4
|
-
* `export const UserSchema = ...` or `export const user = ...` are invalid.
|
|
5
|
-
* `export type` declarations are ignored (used for `z.infer` types).
|
|
6
|
-
*/
|
|
7
1
|
export const schemaNamingRule = {
|
|
8
2
|
meta: {
|
|
9
3
|
type: "problem",
|
|
10
4
|
messages: {
|
|
11
5
|
missingSuffix:
|
|
12
|
-
"
|
|
6
|
+
"schema file の export 変数 '{{ name }}' は 'Schema' で終える。",
|
|
13
7
|
invalidCasing:
|
|
14
|
-
"
|
|
8
|
+
"export 変数 '{{ name }}' は camelCase にする (小文字始まり)。",
|
|
15
9
|
},
|
|
16
10
|
schema: [],
|
|
17
11
|
},
|
|
@@ -1,27 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Enforce `<string literal> as const` for `*_COLUMNS` constant declarations.
|
|
3
|
-
*
|
|
4
|
-
* Apply to `**\/queries/*.ts` and `**\/constants/*.ts`.
|
|
5
|
-
*
|
|
6
|
-
* `*_COLUMNS` 定数は Supabase の `.select()` に直接渡される。`as const` を
|
|
7
|
-
* 外すと TypeScript が `string` に widen し、Supabase の `.select<Query>()`
|
|
8
|
-
* が literal を parse できなくなって row 型推論が壊れる(戻り値が
|
|
9
|
-
* `GenericStringError` になる)。
|
|
10
|
-
*
|
|
11
|
-
* Allowed:
|
|
12
|
-
* const POST_DETAIL_COLUMNS = "id,url,platform" as const;
|
|
13
|
-
*
|
|
14
|
-
* Banned:
|
|
15
|
-
* const POST_DETAIL_COLUMNS = "id,url,platform"; // string に widen
|
|
16
|
-
* const POST_DETAIL_COLUMNS = ["id", "url"] as const; // 配列
|
|
17
|
-
* const POST_DETAIL_COLUMNS = [...] as const satisfies ...; // 配列 + satisfies
|
|
18
|
-
* const POST_DETAIL_COLUMNS = `id,${col}`; // template literal
|
|
19
|
-
*
|
|
20
|
-
* Why: シンプルな string literal を `as const` で保つだけで、Supabase の
|
|
21
|
-
* 型推論(row 型 / column 名タイポ検出)はすべて自動で効く。runtime helper
|
|
22
|
-
* (`joinColumns` 等)は不要。
|
|
23
|
-
*/
|
|
24
|
-
|
|
25
1
|
const COLUMNS_NAME = /^[A-Z][A-Z0-9_]*_COLUMNS$/;
|
|
26
2
|
|
|
27
3
|
function isStringAsConst(initNode) {
|
|
@@ -41,7 +17,7 @@ export const supabaseColumnsSatisfiesRule = {
|
|
|
41
17
|
type: "problem",
|
|
42
18
|
messages: {
|
|
43
19
|
shape:
|
|
44
|
-
'
|
|
20
|
+
'column 定数 `{{ name }}` は `"<comma-separated columns>" as const` にする。`as const` を外すと Supabase の `.select()` 型推論が壊れる。配列 / template literal も不可。',
|
|
45
21
|
},
|
|
46
22
|
schema: [],
|
|
47
23
|
},
|
|
@@ -1,35 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Enforce explicit column lists for Supabase `.select()` calls.
|
|
3
|
-
*
|
|
4
|
-
* Apply to `**\/queries/*.ts`. `.select()` の引数は次のいずれかでなければならない:
|
|
5
|
-
*
|
|
6
|
-
* - inline string literal(例: `.select("id,url,platform")`)
|
|
7
|
-
* - `*_COLUMNS` という UPPER_SNAKE 命名の identifier(例: `.select(POST_DETAIL_COLUMNS)`)
|
|
8
|
-
*
|
|
9
|
-
* `*_COLUMNS` 定数は companion rule `supabase-columns-satisfies` で
|
|
10
|
-
* `<string literal> as const` の形が強制される。これにより:
|
|
11
|
-
*
|
|
12
|
-
* - Supabase の `.select()` は literal string を parse して row 型を推論できる
|
|
13
|
-
* - 存在しない column 名は Supabase の型推論が `SelectQueryError` として弾く(compile time)
|
|
14
|
-
* - runtime helper(`joinColumns`)は不要
|
|
15
|
-
*
|
|
16
|
-
* Banned:
|
|
17
|
-
* .select() implicit "all columns"
|
|
18
|
-
* .select("*") silent exposure of new schema columns
|
|
19
|
-
* .select(`${x},y`) dynamic concatenation
|
|
20
|
-
* .select(cols.join(",")) runtime expression
|
|
21
|
-
* .select(someVar) non-conforming variable
|
|
22
|
-
*
|
|
23
|
-
* Allowed:
|
|
24
|
-
* .select("id,url,platform") inline literal
|
|
25
|
-
* .select(POST_DETAIL_COLUMNS) *_COLUMNS named constant
|
|
26
|
-
*
|
|
27
|
-
* Why: column lists must be (1) statically analyzable for grep / review,
|
|
28
|
-
* (2) literal so Supabase can infer the row shape, (3) never silently grow
|
|
29
|
-
* on schema additions. For column-level access control, use Postgres views
|
|
30
|
-
* (`from("posts_public")`).
|
|
31
|
-
*/
|
|
32
|
-
|
|
33
1
|
const COLUMNS_NAME = /^[A-Z][A-Z0-9_]*_COLUMNS$/;
|
|
34
2
|
|
|
35
3
|
export const supabaseSelectTypedColumnsRule = {
|
|
@@ -37,15 +5,15 @@ export const supabaseSelectTypedColumnsRule = {
|
|
|
37
5
|
type: "problem",
|
|
38
6
|
messages: {
|
|
39
7
|
noArgs:
|
|
40
|
-
"
|
|
8
|
+
"空の `.select()` は全列を暗黙取得する。文字列リテラルか `*_COLUMNS` 定数を渡す。",
|
|
41
9
|
wildcard:
|
|
42
|
-
'`.select("*")`
|
|
10
|
+
'`.select("*")` はスキーマ拡張時に列を暗黙露出する。列を明示列挙する。',
|
|
43
11
|
template:
|
|
44
|
-
"
|
|
12
|
+
"`.select()` の template literal は型推論を壊す。文字列リテラルか `*_COLUMNS` 定数を使う。",
|
|
45
13
|
shapeArg:
|
|
46
|
-
"`.select()`
|
|
14
|
+
"`.select()` の引数は文字列リテラルか `*_COLUMNS` 識別子にする。",
|
|
47
15
|
naming:
|
|
48
|
-
"
|
|
16
|
+
"column 定数 `{{ name }}` は `_COLUMNS` で終わる UPPER_SNAKE_CASE にする (例 POST_DETAIL_COLUMNS)。",
|
|
49
17
|
},
|
|
50
18
|
schema: [],
|
|
51
19
|
},
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# src/deno/CLAUDE.md
|
|
2
|
+
|
|
3
|
+
Supabase Edge Functions (Deno) 固有 rule の置き場所判断:
|
|
4
|
+
|
|
5
|
+
- `boundaries/<surface>.mjs` — Deno 固有の boundary (外界 → features の入口)
|
|
6
|
+
- 種類: lib / utils / entry-point
|
|
7
|
+
- `local-plugins/` — Deno 固有の local plugin
|
|
8
|
+
- `index.mjs` — Deno entry。common の rule + 上記 file を合成
|
|
9
|
+
|
|
10
|
+
Deno entry 横断の共通 rule は `src/common/` に追加する。
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { denoLocalPlugin } from "../local-plugins/index.mjs";
|
|
2
|
+
|
|
3
|
+
const FUNCTIONS_ROOT = "supabase/functions";
|
|
4
|
+
|
|
5
|
+
export const denoEntryPointConfigs = [
|
|
6
|
+
{
|
|
7
|
+
name: "deno/entry-point",
|
|
8
|
+
files: [`${FUNCTIONS_ROOT}/**/*.ts`],
|
|
9
|
+
ignores: [`${FUNCTIONS_ROOT}/_*/**`],
|
|
10
|
+
rules: {
|
|
11
|
+
"no-restricted-imports": [
|
|
12
|
+
"error",
|
|
13
|
+
{
|
|
14
|
+
patterns: [
|
|
15
|
+
{
|
|
16
|
+
group: ["**/services/*", "**/services"],
|
|
17
|
+
message:
|
|
18
|
+
"top-level file は services を直接 import 不可。entries 経由で使う。",
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
group: ["**/queries/*", "**/queries"],
|
|
22
|
+
message:
|
|
23
|
+
"top-level file は queries を直接 import 不可。entries 経由で使う。",
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
group: ["*/_lib/*", "*/_lib/**"],
|
|
27
|
+
message:
|
|
28
|
+
"top-level file は _lib/ を直接 import 不可。entries 経由で使う。",
|
|
29
|
+
},
|
|
30
|
+
],
|
|
31
|
+
},
|
|
32
|
+
],
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
name: "deno/flat-entry-point",
|
|
37
|
+
files: [`${FUNCTIONS_ROOT}/**/*.ts`],
|
|
38
|
+
ignores: [`${FUNCTIONS_ROOT}/_*/**`],
|
|
39
|
+
plugins: { "deno-local": denoLocalPlugin },
|
|
40
|
+
rules: {
|
|
41
|
+
"deno-local/flat-entry-point": "error",
|
|
42
|
+
},
|
|
43
|
+
},
|
|
44
|
+
];
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
const FUNCTIONS_ROOT = "supabase/functions";
|
|
2
|
+
const FEATURE_ROOT = "supabase/functions/_features";
|
|
3
|
+
|
|
4
|
+
export const denoLibBoundaryConfigs = [
|
|
5
|
+
{
|
|
6
|
+
name: "deno/lib-boundary",
|
|
7
|
+
files: [`${FUNCTIONS_ROOT}/**/*.ts`],
|
|
8
|
+
ignores: [
|
|
9
|
+
`${FUNCTIONS_ROOT}/_lib/**`,
|
|
10
|
+
`${FEATURE_ROOT}/**/queries/**`,
|
|
11
|
+
`${FEATURE_ROOT}/**/types/**`,
|
|
12
|
+
],
|
|
13
|
+
rules: {
|
|
14
|
+
"no-restricted-imports": [
|
|
15
|
+
"error",
|
|
16
|
+
{
|
|
17
|
+
patterns: [
|
|
18
|
+
{
|
|
19
|
+
group: ["*/_lib/*", "*/_lib/**"],
|
|
20
|
+
message:
|
|
21
|
+
"_lib/ は queries からのみ import 可。他層は queries 経由で使う。",
|
|
22
|
+
},
|
|
23
|
+
],
|
|
24
|
+
},
|
|
25
|
+
],
|
|
26
|
+
},
|
|
27
|
+
},
|
|
28
|
+
];
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
const FUNCTIONS_ROOT = "supabase/functions";
|
|
2
|
+
|
|
3
|
+
export const denoUtilsBoundaryConfigs = [
|
|
4
|
+
{
|
|
5
|
+
name: "deno/utils-boundary",
|
|
6
|
+
files: [`${FUNCTIONS_ROOT}/_utils/**/*.ts`],
|
|
7
|
+
rules: {
|
|
8
|
+
"no-restricted-imports": [
|
|
9
|
+
"error",
|
|
10
|
+
{
|
|
11
|
+
patterns: [
|
|
12
|
+
{
|
|
13
|
+
group: ["*/_features/*", "*/_features/**"],
|
|
14
|
+
message: "_utils/ は _features/ を import 不可。依存方向を守る。",
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
group: ["*/_lib/*", "*/_lib/**"],
|
|
18
|
+
message: "_utils/ は _lib/ を import 不可。依存方向を守る。",
|
|
19
|
+
},
|
|
20
|
+
],
|
|
21
|
+
},
|
|
22
|
+
],
|
|
23
|
+
},
|
|
24
|
+
},
|
|
25
|
+
];
|
package/src/deno/index.mjs
CHANGED
|
@@ -1,29 +1,25 @@
|
|
|
1
|
-
import { createEntryPointConfigs } from "../common/entry-
|
|
1
|
+
import { createEntryPointConfigs } from "../common/boundaries/entry-point.mjs";
|
|
2
2
|
import { createCommonConfigs } from "../common/index.mjs";
|
|
3
|
-
|
|
3
|
+
|
|
4
|
+
import { denoEntryPointConfigs } from "./boundaries/entry-point.mjs";
|
|
5
|
+
import { denoLibBoundaryConfigs } from "./boundaries/lib.mjs";
|
|
6
|
+
import { denoUtilsBoundaryConfigs } from "./boundaries/utils.mjs";
|
|
4
7
|
|
|
5
8
|
const FEATURE_ROOT = "supabase/functions/_features";
|
|
6
9
|
|
|
7
|
-
const
|
|
10
|
+
const denoNamespaceImportConfigs = createEntryPointConfigs(
|
|
8
11
|
["supabase/functions/**/*.ts"],
|
|
9
12
|
["supabase/functions/_*/**"],
|
|
10
13
|
);
|
|
11
14
|
|
|
12
|
-
// Deno files are not covered by the consumer's project tsconfig
|
|
13
|
-
// (Supabase functions live outside Next.js's tsconfig), so type-aware
|
|
14
|
-
// rules cannot consult the type checker. Disable them here and rely on
|
|
15
|
-
// `deno check` / `deno lint` for type-level guarantees.
|
|
16
|
-
//
|
|
17
|
-
// `rulesFiles` is narrowed so the override only affects Deno files. When this
|
|
18
|
-
// entry is combined with `next`/`node`, those entries keep their type-aware
|
|
19
|
-
// settings on their own files.
|
|
20
|
-
/** Deno ESLint flat config entry point. */
|
|
21
15
|
export const eslintConfig = [
|
|
22
16
|
...createCommonConfigs(FEATURE_ROOT, {
|
|
23
17
|
banAliasImports: true,
|
|
24
18
|
typeAware: false,
|
|
25
19
|
rulesFiles: ["supabase/functions/**/*.ts"],
|
|
26
20
|
}),
|
|
27
|
-
...
|
|
21
|
+
...denoLibBoundaryConfigs,
|
|
28
22
|
...denoEntryPointConfigs,
|
|
23
|
+
...denoUtilsBoundaryConfigs,
|
|
24
|
+
...denoNamespaceImportConfigs,
|
|
29
25
|
];
|