@yasainet/eslint 0.0.56 → 0.0.58

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yasainet/eslint",
3
- "version": "0.0.56",
3
+ "version": "0.0.58",
4
4
  "description": "ESLint",
5
5
  "type": "module",
6
6
  "exports": {
@@ -1,35 +1,35 @@
1
1
  const LAYER_PATTERNS = {
2
2
  queries: [
3
3
  {
4
- group: ["*/services/*", "*/services"],
4
+ group: ["**/services/*", "**/services"],
5
5
  message: "queries cannot import services (layer violation)",
6
6
  },
7
7
  {
8
- group: ["*/interactors/*", "*/interactors"],
8
+ group: ["**/interactors/*", "**/interactors"],
9
9
  message: "queries cannot import interactors (layer violation)",
10
10
  },
11
11
  {
12
- group: ["*/hooks/*", "*/hooks"],
12
+ group: ["**/hooks/*", "**/hooks"],
13
13
  message: "queries cannot import hooks (layer violation)",
14
14
  },
15
15
  ],
16
16
  services: [
17
17
  {
18
- group: ["*/interactors/*", "*/interactors"],
18
+ group: ["**/interactors/*", "**/interactors"],
19
19
  message: "services cannot import interactors (layer violation)",
20
20
  },
21
21
  {
22
- group: ["*/hooks/*", "*/hooks"],
22
+ group: ["**/hooks/*", "**/hooks"],
23
23
  message: "services cannot import hooks (layer violation)",
24
24
  },
25
25
  ],
26
26
  interactors: [
27
27
  {
28
- group: ["*/queries/*", "*/queries"],
28
+ group: ["**/queries/*", "**/queries"],
29
29
  message: "interactors cannot import queries (layer violation)",
30
30
  },
31
31
  {
32
- group: ["*/hooks/*", "*/hooks"],
32
+ group: ["**/hooks/*", "**/hooks"],
33
33
  message: "interactors cannot import hooks (layer violation)",
34
34
  },
35
35
  ],
@@ -113,25 +113,38 @@ const MAPPING_PATTERNS = [
113
113
 
114
114
  const PAGE_BOUNDARY_PATTERNS = [
115
115
  {
116
- group: ["*/queries/*", "*/queries"],
116
+ group: ["**/queries/*", "**/queries"],
117
117
  message:
118
118
  "page.tsx can only import interactors, not queries (page-boundary violation)",
119
119
  },
120
120
  {
121
- group: ["*/services/*", "*/services"],
121
+ group: ["**/services/*", "**/services"],
122
122
  message:
123
123
  "page.tsx can only import interactors, not services (page-boundary violation)",
124
124
  },
125
125
  ];
126
126
 
127
+ const ROUTE_BOUNDARY_PATTERNS = [
128
+ {
129
+ group: ["**/queries/*", "**/queries"],
130
+ message:
131
+ "route.ts can only import interactors, not queries (route-boundary violation)",
132
+ },
133
+ {
134
+ group: ["**/services/*", "**/services"],
135
+ message:
136
+ "route.ts can only import interactors, not services (route-boundary violation)",
137
+ },
138
+ ];
139
+
127
140
  const HOOKS_BOUNDARY_PATTERNS = [
128
141
  {
129
- group: ["*/queries/*", "*/queries"],
142
+ group: ["**/queries/*", "**/queries"],
130
143
  message:
131
144
  "hooks can only import interactors, not queries (hooks-boundary violation)",
132
145
  },
133
146
  {
134
- group: ["*/services/*", "*/services"],
147
+ group: ["**/services/*", "**/services"],
135
148
  message:
136
149
  "hooks can only import interactors, not services (hooks-boundary violation)",
137
150
  },
@@ -139,12 +152,12 @@ const HOOKS_BOUNDARY_PATTERNS = [
139
152
 
140
153
  const COMPONENTS_BOUNDARY_PATTERNS = [
141
154
  {
142
- group: ["*/queries/*", "*/queries"],
155
+ group: ["**/queries/*", "**/queries"],
143
156
  message:
144
157
  "components can only import interactors or hooks, not queries (components-boundary violation)",
145
158
  },
146
159
  {
147
- group: ["*/services/*", "*/services"],
160
+ group: ["**/services/*", "**/services"],
148
161
  message:
149
162
  "components can only import interactors or hooks, not services (components-boundary violation)",
150
163
  },
@@ -187,6 +200,26 @@ export const pageBoundaryConfigs = [
187
200
  },
188
201
  ];
189
202
 
203
+ /** Next.js-only: restrict route.ts to only import interactors. */
204
+ export const routeBoundaryConfigs = [
205
+ {
206
+ name: "imports/route-boundary",
207
+ files: ["src/app/**/route.ts"],
208
+ rules: {
209
+ "no-restricted-imports": [
210
+ "error",
211
+ {
212
+ patterns: [
213
+ ...ROUTE_BOUNDARY_PATTERNS,
214
+ ...LIB_BOUNDARY_PATTERNS,
215
+ ...MAPPING_PATTERNS,
216
+ ],
217
+ },
218
+ ],
219
+ },
220
+ },
221
+ ];
222
+
190
223
  /** Next.js-only: restrict hooks to only import interactors (not queries or services). */
191
224
  export const hooksBoundaryConfigs = [
192
225
  {
@@ -1,50 +1,47 @@
1
1
  /**
2
- * Enforce `[...] as const satisfies readonly (keyof Tables<"...">)[]` for
3
- * `*_COLUMNS` constant declarations.
2
+ * Enforce `<string literal> as const` for `*_COLUMNS` constant declarations.
4
3
  *
5
- * Apply to `**\/queries/*.query.ts`. Without `as const satisfies`, typos
6
- * in column names slip past the lint phase — Supabase only complains at
7
- * runtime when the query executes. The `satisfies` annotation forces
8
- * TypeScript to validate every entry against the schema before the code
9
- * ever runs. The actual `keyof Tables<"...">` content is left to the type
10
- * checker; the lint rule only verifies the syntactic shape.
4
+ * Apply to `**\/queries/*.query.ts` and `**\/constants/*.constant.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` 等)は不要。
11
23
  */
12
24
 
13
25
  const COLUMNS_NAME = /^[A-Z][A-Z0-9_]*_COLUMNS$/;
14
26
 
15
- /** Unwrap nested type assertions and find the underlying expression. */
16
- function unwrapTypeAssertions(node) {
17
- let current = node;
18
- while (
19
- current &&
20
- (current.type === "TSAsExpression" ||
21
- current.type === "TSSatisfiesExpression")
22
- ) {
23
- current = current.expression;
24
- }
25
- return current;
26
- }
27
-
28
- function isAsConstSatisfies(initNode) {
27
+ function isStringAsConst(initNode) {
29
28
  if (!initNode) return false;
30
- if (initNode.type !== "TSSatisfiesExpression") return false;
31
- const inner = initNode.expression;
32
- if (inner.type !== "TSAsExpression") return false;
33
- const ann = inner.typeAnnotation;
34
- if (!ann) return false;
35
- if (ann.type !== "TSTypeReference") return false;
29
+ if (initNode.type !== "TSAsExpression") return false;
30
+ const ann = initNode.typeAnnotation;
31
+ if (!ann || ann.type !== "TSTypeReference") return false;
36
32
  if (ann.typeName.type !== "Identifier") return false;
37
33
  if (ann.typeName.name !== "const") return false;
38
- if (inner.expression.type !== "ArrayExpression") return false;
39
- return true;
34
+ const inner = initNode.expression;
35
+ if (inner.type !== "Literal") return false;
36
+ return typeof inner.value === "string";
40
37
  }
41
38
 
42
39
  export const supabaseColumnsSatisfiesRule = {
43
40
  meta: {
44
41
  type: "problem",
45
42
  messages: {
46
- missing:
47
- 'Column constant `{{ name }}` must use `[...] as const satisfies readonly (keyof Tables<"table">)[]` so column names are validated against the schema at compile time.',
43
+ shape:
44
+ 'Column constant `{{ name }}` must be `"<comma-separated columns>" as const`. `as const` を外すと Supabase `.select()` 型推論が壊れる。配列 / template literal も不可。',
48
45
  },
49
46
  schema: [],
50
47
  },
@@ -53,15 +50,11 @@ export const supabaseColumnsSatisfiesRule = {
53
50
  VariableDeclarator(node) {
54
51
  if (node.id.type !== "Identifier") return;
55
52
  if (!COLUMNS_NAME.test(node.id.name)) return;
56
- // Only enforce on array initializers. String literals like
57
- // POST_UPSERT_CONFLICT_COLUMNS are PostgREST conflict-target specs,
58
- // not column lists, and are out of scope.
59
- const inner = unwrapTypeAssertions(node.init);
60
- if (!inner || inner.type !== "ArrayExpression") return;
61
- if (isAsConstSatisfies(node.init)) return;
53
+ if (!node.init) return;
54
+ if (isStringAsConst(node.init)) return;
62
55
  context.report({
63
56
  node: node.id,
64
- messageId: "missing",
57
+ messageId: "shape",
65
58
  data: { name: node.id.name },
66
59
  });
67
60
  },
@@ -1,59 +1,51 @@
1
1
  /**
2
- * Enforce typed column constants for Supabase `.select()` calls.
2
+ * Enforce explicit column lists for Supabase `.select()` calls.
3
3
  *
4
- * Apply to `**\/queries/*.query.ts`. Forces `.select()` to take the form
5
- * `joinColumns(<X_COLUMNS>)` where `X_COLUMNS` is an UPPER_SNAKE identifier
6
- * ending with `_COLUMNS`. The identifier is expected to be declared with
7
- * `as const satisfies readonly (keyof Tables<"table">)[]` so the column
8
- * names are validated against the schema at compile time. The `satisfies`
9
- * shape itself is enforced by the companion `supabase-columns-satisfies`
10
- * rule.
4
+ * Apply to `**\/queries/*.query.ts`. `.select()` の引数は次のいずれかでなければならない:
11
5
  *
12
- * `joinColumns()` is a project-supplied helper that comma-joins a const
13
- * string tuple while preserving the literal string type so Supabase's
14
- * `.select()` type parser can infer the row shape (a plain `.join(",")`
15
- * widens to `string` and breaks inference).
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`)は不要
16
15
  *
17
16
  * Banned:
18
- * .select() implicit "all columns"
19
- * .select("*") silent exposure of new schema columns
20
- * .select("id, name") inline literal, invisible to grep
21
- * .select(`${x}, y`) dynamic concatenation
22
- * .select(POST_LIST_COLUMNS.join(",")) plain .join widens to `string`, breaks inference
23
- * .select(someVar) non-conforming variable
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
24
22
  *
25
23
  * Allowed:
26
- * .select(joinColumns(POST_LIST_COLUMNS)) typed constant via project helper
24
+ * .select("id,url,platform") inline literal
25
+ * .select(POST_DETAIL_COLUMNS) *_COLUMNS named constant
27
26
  *
28
- * Why: column lists must be (1) named for grep / review, (2) checked
29
- * against the schema, (3) never silently grow on schema additions.
30
- * For column-level access control, use Postgres views (`from("posts_public")`).
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
31
  */
32
32
 
33
33
  const COLUMNS_NAME = /^[A-Z][A-Z0-9_]*_COLUMNS$/;
34
34
 
35
- function asJoinColumnsCall(arg) {
36
- if (!arg) return null;
37
- if (arg.type !== "CallExpression") return null;
38
- if (arg.callee.type !== "Identifier") return null;
39
- if (arg.callee.name !== "joinColumns") return null;
40
- if (arg.arguments.length !== 1) return null;
41
- if (arg.arguments[0].type !== "Identifier") return null;
42
- return arg.arguments[0];
43
- }
44
-
45
35
  export const supabaseSelectTypedColumnsRule = {
46
36
  meta: {
47
37
  type: "problem",
48
38
  messages: {
49
39
  noArgs:
50
- "Empty `.select()` returns all columns implicitly. Pass `joinColumns(<X_COLUMNS>)` where X_COLUMNS is a typed constant.",
51
- literalArg:
52
- 'Inline `.select()` argument is forbidden. Define `const X_COLUMNS = [...] as const satisfies readonly (keyof Tables<"table">)[];` and call `.select(joinColumns(X_COLUMNS))`. Use Postgres views for column-level access control.',
40
+ "Empty `.select()` returns all columns implicitly. Pass a string literal or a `*_COLUMNS` constant.",
41
+ wildcard:
42
+ '`.select("*")` exposes new schema columns silently. Enumerate columns explicitly.',
43
+ template:
44
+ "Template literal in `.select()` defeats type inference. Use a string literal or a `*_COLUMNS` constant.",
53
45
  shapeArg:
54
- '`.select()` argument must be `joinColumns(<X_COLUMNS>)`. Other expressions defeat type inference and column-level review.',
46
+ "`.select()` argument must be a string literal or a `*_COLUMNS` identifier.",
55
47
  naming:
56
- "Column constant `{{ name }}` must be UPPER_SNAKE_CASE ending with `_COLUMNS` (e.g. POST_LIST_COLUMNS, POST_DETAIL_COLUMNS).",
48
+ "Column constant `{{ name }}` must be UPPER_SNAKE_CASE ending with `_COLUMNS` (e.g. POST_DETAIL_COLUMNS).",
57
49
  },
58
50
  schema: [],
59
51
  },
@@ -71,24 +63,34 @@ export const supabaseSelectTypedColumnsRule = {
71
63
 
72
64
  const arg = node.arguments[0];
73
65
 
74
- if (arg.type === "Literal" || arg.type === "TemplateLiteral") {
75
- context.report({ node: arg, messageId: "literalArg" });
66
+ if (arg.type === "Literal") {
67
+ if (typeof arg.value !== "string") {
68
+ context.report({ node: arg, messageId: "shapeArg" });
69
+ return;
70
+ }
71
+ if (arg.value.trim() === "*") {
72
+ context.report({ node: arg, messageId: "wildcard" });
73
+ }
76
74
  return;
77
75
  }
78
76
 
79
- const id = asJoinColumnsCall(arg);
80
- if (!id) {
81
- context.report({ node: arg, messageId: "shapeArg" });
77
+ if (arg.type === "TemplateLiteral") {
78
+ context.report({ node: arg, messageId: "template" });
82
79
  return;
83
80
  }
84
81
 
85
- if (!COLUMNS_NAME.test(id.name)) {
86
- context.report({
87
- node: id,
88
- messageId: "naming",
89
- data: { name: id.name },
90
- });
82
+ if (arg.type === "Identifier") {
83
+ if (!COLUMNS_NAME.test(arg.name)) {
84
+ context.report({
85
+ node: arg,
86
+ messageId: "naming",
87
+ data: { name: arg.name },
88
+ });
89
+ }
90
+ return;
91
91
  }
92
+
93
+ context.report({ node: arg, messageId: "shapeArg" });
92
94
  },
93
95
  };
94
96
  },
@@ -5,6 +5,7 @@ import {
5
5
  hooksBoundaryConfigs,
6
6
  libBoundaryConfigs,
7
7
  pageBoundaryConfigs,
8
+ routeBoundaryConfigs,
8
9
  } from "../common/imports.mjs";
9
10
 
10
11
  import { directivesConfigs } from "./directives.mjs";
@@ -26,6 +27,7 @@ export const eslintConfig = [
26
27
  ...createCommonConfigs("src/features"),
27
28
  ...libBoundaryConfigs,
28
29
  ...pageBoundaryConfigs,
30
+ ...routeBoundaryConfigs,
29
31
  ...hooksBoundaryConfigs,
30
32
  ...componentsBoundaryConfigs,
31
33
  ...namingConfigs,