@yasainet/eslint 0.0.57 → 0.0.59

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 CHANGED
@@ -20,24 +20,45 @@ src/
20
20
  └── deno/ # Deno entry point (entry-point boundary, _utils boundary, _lib boundary)
21
21
  ```
22
22
 
23
- Each entry point enforces a feature-based architecture with the following convention in consuming projects:
23
+ Each entry point enforces a feature-based architecture. **Files do not carry role suffixes — the directory declares the role**:
24
24
 
25
25
  ```text
26
26
  {featureRoot}/
27
27
  ├── {feature}/
28
- │ ├── interactors/ # *.interactor.ts entry points
29
- │ ├── services/ # *.service.ts — business logic
30
- │ ├── queries/ # *.query.ts data access
31
- │ ├── types/ # *.type.ts
32
- │ ├── schemas/ # *.schema.ts
33
- │ ├── utils/ # *.util.ts
34
- │ └── constants/ # *.constant.ts
28
+ │ ├── interactors/ # entry points (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
35
  ├── shared/ # Cross-feature shared modules
36
- ├── ...
37
- {libRoot}/ # *.lib.ts library wrappers (e.g., supabase.lib.ts)
38
- {utilsRoot}/ # *.util.ts top-level utilities (e.g., font.util.ts)
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 ...)
39
43
  ```
40
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`, `.interactor` 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).
61
+
41
62
  ## Setup
42
63
 
43
64
  ### Next.js + Node.js + Deno
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yasainet/eslint",
3
- "version": "0.0.57",
3
+ "version": "0.0.59",
4
4
  "description": "ESLint",
5
5
  "type": "module",
6
6
  "exports": {
@@ -20,14 +20,22 @@ function findProjectRoot() {
20
20
 
21
21
  const PROJECT_ROOT = findProjectRoot();
22
22
 
23
- const EXCLUDE_LIST = ["proxy.lib.ts"];
23
+ /**
24
+ * Files / basenames that should never become a prefix:
25
+ *
26
+ * - `types.ts`: 型定義のみで lib の役割を持たない
27
+ * - `proxy.ts`: middleware adapter (Next.js の proxy.ts と意味が衝突するため queries から呼ばせない)
28
+ */
29
+ const EXCLUDE_LIST = ["types.ts", "proxy.ts"];
24
30
 
25
- /** Extract the base name from a .ts filename by stripping all extensions. */
26
- function baseName(filename) {
27
- return filename.replace(/\..*$/, "");
28
- }
29
-
30
- /** Scan lib directory derived from featureRoot and build prefix-to-lib-relative-path mapping. */
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 / proxy.ts のような lib として queries から呼ばせたくないものは EXCLUDE_LIST で除外
38
+ */
31
39
  export function generatePrefixLibMapping(featureRoot) {
32
40
  const libRoot = featureRoot.replace(/features$/, "lib");
33
41
  const libDir = path.join(PROJECT_ROOT, libRoot);
@@ -37,30 +45,35 @@ export function generatePrefixLibMapping(featureRoot) {
37
45
  return mapping;
38
46
  }
39
47
 
48
+ const isPlainTsFile = (name) =>
49
+ name.endsWith(".ts") &&
50
+ name.split(".").length === 2 &&
51
+ !EXCLUDE_LIST.includes(name);
52
+
40
53
  const entries = fs.readdirSync(libDir, { withFileTypes: true });
41
54
 
42
55
  for (const entry of entries) {
43
- if (EXCLUDE_LIST.includes(entry.name)) {
44
- continue;
45
- }
46
-
47
56
  if (entry.isDirectory()) {
48
57
  const subDir = path.join(libDir, entry.name);
49
58
  const subEntries = fs.readdirSync(subDir, { withFileTypes: true });
59
+ const plainTsFiles = subEntries
60
+ .filter((e) => e.isFile() && isPlainTsFile(e.name))
61
+ .map((e) => e.name);
50
62
 
51
- for (const subEntry of subEntries) {
52
- if (
53
- subEntry.isFile() &&
54
- subEntry.name.endsWith(".lib.ts") &&
55
- !EXCLUDE_LIST.includes(subEntry.name)
56
- ) {
57
- const prefix = baseName(subEntry.name);
58
- mapping[prefix] = `${entry.name}/${subEntry.name.replace(".lib.ts", "")}`;
63
+ if (plainTsFiles.includes("index.ts")) {
64
+ // single-client lib: index.ts を entry とみなし、prefix = dir 名で登録
65
+ // 同 dir 内の他ファイル (parser 等) は sub-module として自動除外
66
+ mapping[entry.name] = `${entry.name}/index`;
67
+ } else {
68
+ // multi-client lib: 全 role file を登録 (e.g., supabase の admin / server / client)
69
+ for (const fileName of plainTsFiles) {
70
+ const prefix = fileName.replace(/\.ts$/, "");
71
+ mapping[prefix] = `${entry.name}/${prefix}`;
59
72
  }
60
73
  }
61
- } else if (entry.isFile() && entry.name.endsWith(".lib.ts")) {
62
- const prefix = baseName(entry.name);
63
- mapping[prefix] = entry.name.replace(".lib.ts", "");
74
+ } else if (entry.isFile() && isPlainTsFile(entry.name)) {
75
+ const prefix = entry.name.replace(/\.ts$/, "");
76
+ mapping[prefix] = prefix;
64
77
  }
65
78
  }
66
79
 
@@ -62,23 +62,23 @@ const LATERAL_PATTERNS = {
62
62
  const CARDINALITY_PATTERNS = {
63
63
  server: [
64
64
  {
65
- group: ["**/services/client.service*", "**/services/admin.service*"],
65
+ group: ["**/services/client", "**/services/admin"],
66
66
  message:
67
- "server.interactor can only import server.service (cardinality violation)",
67
+ "server interactor can only import server service (cardinality violation)",
68
68
  },
69
69
  ],
70
70
  client: [
71
71
  {
72
- group: ["**/services/server.service*", "**/services/admin.service*"],
72
+ group: ["**/services/server", "**/services/admin"],
73
73
  message:
74
- "client.interactor can only import client.service (cardinality violation)",
74
+ "client interactor can only import client service (cardinality violation)",
75
75
  },
76
76
  ],
77
77
  admin: [
78
78
  {
79
- group: ["**/services/server.service*", "**/services/client.service*"],
79
+ group: ["**/services/server", "**/services/client"],
80
80
  message:
81
- "admin.interactor can only import admin.service (cardinality violation)",
81
+ "admin interactor can only import admin service (cardinality violation)",
82
82
  },
83
83
  ],
84
84
  };
@@ -90,7 +90,7 @@ function prefixLibPatterns(prefix, mapping) {
90
90
  .filter((p) => p !== prefix)
91
91
  .map((p) => ({
92
92
  group: [`**/lib/${mapping[p]}`, `**/lib/${mapping[p]}/*`],
93
- message: `${prefix}.query.ts can only import from lib/${allowedLib}. Use the correct query file for this lib.`,
93
+ message: `queries/${prefix}.ts can only import from lib/${allowedLib}. Use the correct query file for this lib.`,
94
94
  }));
95
95
  }
96
96
 
@@ -329,7 +329,7 @@ export function createImportsConfigs(
329
329
  configs.push(
330
330
  makeConfig(
331
331
  `queries/${prefix}`,
332
- [`${featureRoot}/**/queries/${prefix}.query.ts`],
332
+ [`${featureRoot}/**/queries/${prefix}.ts`],
333
333
  LAYER_PATTERNS.queries,
334
334
  LATERAL_PATTERNS.queries,
335
335
  patterns,
@@ -401,7 +401,7 @@ export function createImportsConfigs(
401
401
  configs.push(
402
402
  makeConfig(
403
403
  `interactors/${prefix}`,
404
- [`${featureRoot}/**/interactors/${prefix}.interactor.ts`],
404
+ [`${featureRoot}/**/interactors/${prefix}.ts`],
405
405
  LAYER_PATTERNS.interactors,
406
406
  LATERAL_PATTERNS.interactors,
407
407
  CARDINALITY_PATTERNS[prefix],
@@ -113,11 +113,11 @@ export const featureNameRule = {
113
113
  // generated types without duplicating the file.
114
114
  const computedTypePath = path.join(
115
115
  projectRoot,
116
- featureRoot.replace(/features$/, "lib/supabase/supabase.type.ts"),
116
+ featureRoot.replace(/features$/, "lib/supabase/types.ts"),
117
117
  );
118
118
  const fallbackTypePath = path.join(
119
119
  projectRoot,
120
- "src/lib/supabase/supabase.type.ts",
120
+ "src/lib/supabase/types.ts",
121
121
  );
122
122
  const supabaseTypePath = fs.existsSync(computedTypePath)
123
123
  ? computedTypePath
@@ -2,19 +2,21 @@
2
2
  * Enforce consistent naming for `import * as` namespace imports
3
3
  * within feature-based architecture.
4
4
  *
5
- * Convention: import * as {featureName}{Scope}{Layer} from "{path}/{scope}.{layerExt}"
5
+ * Convention: import * as {featureName}{Scope}{Layer} from "{path}/{layerDir}/{scope}"
6
+ *
7
+ * Layer はファイル名 suffix ではなくディレクトリ名 (`queries/` / `services/` 等) から識別する。
6
8
  */
7
9
 
8
10
  /** @type {Record<string, string>} */
9
- const LAYER_MAP = {
10
- query: "Query",
11
- service: "Service",
12
- domain: "Domain",
13
- interactor: "Interactor",
14
- util: "Util",
15
- type: "Type",
16
- schema: "Schema",
17
- constant: "Constant",
11
+ const LAYER_DIR_MAP = {
12
+ queries: "Query",
13
+ services: "Service",
14
+ domains: "Domain",
15
+ interactors: "Interactor",
16
+ utils: "Util",
17
+ types: "Type",
18
+ schemas: "Schema",
19
+ constants: "Constant",
18
20
  };
19
21
 
20
22
  /** Convert a snake_case or kebab-case string to camelCase. */
@@ -29,8 +31,11 @@ function toPascalCase(str) {
29
31
  }
30
32
 
31
33
  /**
32
- * Parse import source to extract featureName, scope, and layer.
33
- * Returns null if the source doesn't match the expected pattern.
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 に無い場合は対象外
34
39
  */
35
40
  function parseImportSource(importPath, featureRoot) {
36
41
  // Normalize alias: @/features/... → features/...
@@ -46,20 +51,15 @@ function parseImportSource(importPath, featureRoot) {
46
51
  if (rootIdx === -1) return null;
47
52
 
48
53
  const afterRoot = normalized.slice(rootIdx + rootPrefix.length);
49
- // Expected: {feature}/{layerDir}/{scope}.{layerExt}
54
+ // Expected: {feature}/{layerDir}/{scope}
50
55
  const segments = afterRoot.split("/");
51
- if (segments.length < 2) return null;
56
+ if (segments.length < 3) return null;
52
57
 
53
58
  const featureDir = segments[0];
54
- const fileName = segments[segments.length - 1].replace(/\.[jt]sx?$/, "");
55
-
56
- const dotIdx = fileName.indexOf(".");
57
- if (dotIdx === -1) return null;
58
-
59
- const scope = fileName.slice(0, dotIdx);
60
- const ext = fileName.slice(dotIdx + 1);
59
+ const layerDir = segments[1];
60
+ const scope = segments[segments.length - 1].replace(/\.[jt]sx?$/, "");
61
61
 
62
- const layer = LAYER_MAP[ext];
62
+ const layer = LAYER_DIR_MAP[layerDir];
63
63
  if (!layer) return null;
64
64
 
65
65
  return { featureDir, scope, layer };
@@ -1,9 +1,9 @@
1
1
  /**
2
- * Enforce verb allow list for `queries/*.query.ts` exports.
2
+ * Enforce verb allow list for `queries/*.ts` exports:
3
3
  *
4
- * The queries layer is the TS-idiomatic translation of Rails 5 actions
5
- * (index/show -> get, create, update, destroy -> delete). Auth ceremonies
6
- * (signUp / signIn / signOut) are admitted as industry-standard exceptions.
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
7
  */
8
8
 
9
9
  const QUERIES_ALLOW = /^(get|create|update|delete|signUp|signIn|signOut)([A-Z]|$)/;
@@ -1,13 +1,14 @@
1
1
  /**
2
- * Enforce namespace imports for `queries/*.query.ts` files.
2
+ * Enforce namespace imports for files under `queries/`:
3
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`). Type-only imports are
7
- * exempted because they have no runtime presence.
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 には依存しない
8
9
  */
9
10
 
10
- const QUERIES_PATH = /\/queries\/[^/]+\.query$/;
11
+ const QUERIES_PATH = /\/queries\/[^/]+$/;
11
12
 
12
13
  export const queriesNamespaceImportRule = {
13
14
  meta: {
@@ -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/*.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` 等)は不要。
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/*.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
  },
@@ -2,61 +2,69 @@ import { featuresGlob } from "./constants.mjs";
2
2
  import { localPlugin } from "./local-plugins/index.mjs";
3
3
  import { checkFile } from "./plugins.mjs";
4
4
 
5
- /** Scope lib naming rules to the lib root derived from the given feature root. */
5
+ /**
6
+ * Scope lib naming rules to the lib root derived from the given feature root:
7
+ *
8
+ * - basename は suffix なしの単一トークン (`*` パターン) を強制する
9
+ * - 多重拡張子 (`<name>.lib.ts` / `<name>.parser.ts` 等) は禁止 → 役割はディレクトリで宣言する
10
+ * - types.ts は対象外 (型のみで lib の役割を持たないため check 不要)
11
+ */
6
12
  export function createLibNamingConfigs(featureRoot) {
7
13
  const libRoot = featureRoot.replace(/features$/, "lib");
8
14
  return [
9
15
  {
10
16
  name: "naming/lib",
11
17
  files: [`${libRoot}/**/*.ts`],
12
- ignores: [`${libRoot}/**/*.type.ts`],
13
18
  plugins: { "check-file": checkFile },
14
19
  rules: {
15
20
  "check-file/filename-naming-convention": [
16
21
  "error",
17
- { "**/*.ts": "*.lib" },
22
+ { "**/*.ts": "*" },
18
23
  ],
19
24
  },
20
25
  },
21
26
  ];
22
27
  }
23
28
 
24
- /** Scope utils naming rules to the utils root derived from the given feature root. */
29
+ /**
30
+ * Scope utils naming rules to the utils root derived from the given feature root:
31
+ *
32
+ * - basename は suffix なしの単一トークン (`*` パターン) を強制する
33
+ * - 多重拡張子は禁止
34
+ */
25
35
  export function createUtilsNamingConfigs(featureRoot) {
26
36
  const utilsRoot = featureRoot.replace(/features$/, "utils");
27
37
  return [
28
38
  {
29
39
  name: "naming/top-level-utils",
30
40
  files: [`${utilsRoot}/**/*.ts`],
31
- ignores: [`${utilsRoot}/**/*.type.ts`],
32
41
  plugins: { "check-file": checkFile },
33
42
  rules: {
34
43
  "check-file/filename-naming-convention": [
35
44
  "error",
36
- { "**/*.ts": "*.util" },
45
+ { "**/*.ts": "*" },
37
46
  ],
38
47
  },
39
48
  },
40
49
  ];
41
50
  }
42
51
 
43
- /** Scope naming rules to the given feature root. */
52
+ /**
53
+ * Scope naming rules to the given feature root:
54
+ *
55
+ * - 全 layer (services / queries / interactors / utils / types / schemas / constants) で suffix を廃止
56
+ * - ファイル名 (basename) は単一トークン (`*` パターン) を強制し、role はディレクトリで宣言する
57
+ * - queries / services / interactors のファイル名は prefixLibMapping のキー (lib name) と一致させ、どの lib を呼ぶか明示する
58
+ * - shared/ 配下では feature 名でなく `shared` または lib name を allowed prefix として許可する
59
+ */
44
60
  export function createNamingConfigs(featureRoot, prefixLibMapping) {
45
61
  const prefixes = Object.keys(prefixLibMapping);
46
62
  const hasPrefixes = prefixes.length > 0;
47
- const prefixPattern = hasPrefixes ? `@(${prefixes.join("|")})` : null;
63
+ const prefixPattern = hasPrefixes ? `@(${prefixes.join("|")})` : "*";
48
64
  const sharedPrefixPattern = hasPrefixes
49
65
  ? `@(shared|${prefixes.join("|")})`
50
66
  : "shared";
51
67
 
52
- const servicePattern = prefixPattern
53
- ? `${prefixPattern}.service`
54
- : "*.service";
55
- const queryPattern = prefixPattern ? `${prefixPattern}.query` : "*.query";
56
- const interactorPattern = prefixPattern
57
- ? `${prefixPattern}.interactor`
58
- : "*.interactor";
59
-
60
68
  const configs = [];
61
69
 
62
70
  configs.push({
@@ -86,7 +94,7 @@ export function createNamingConfigs(featureRoot, prefixLibMapping) {
86
94
  rules: {
87
95
  "check-file/filename-naming-convention": [
88
96
  "error",
89
- { "**/*.ts": servicePattern },
97
+ { "**/*.ts": prefixPattern },
90
98
  ],
91
99
  },
92
100
  },
@@ -98,13 +106,13 @@ export function createNamingConfigs(featureRoot, prefixLibMapping) {
98
106
  rules: {
99
107
  "check-file/filename-naming-convention": [
100
108
  "error",
101
- { "**/*.ts": queryPattern },
109
+ { "**/*.ts": prefixPattern },
102
110
  ],
103
111
  },
104
112
  },
105
113
  {
106
114
  name: "naming/queries-export",
107
- files: featuresGlob(featureRoot, "**/queries/*.query.ts"),
115
+ files: featuresGlob(featureRoot, "**/queries/*.ts"),
108
116
  plugins: { local: localPlugin },
109
117
  rules: {
110
118
  "local/queries-export": "error",
@@ -120,7 +128,7 @@ export function createNamingConfigs(featureRoot, prefixLibMapping) {
120
128
  },
121
129
  {
122
130
  name: "naming/supabase-select",
123
- files: featuresGlob(featureRoot, "**/queries/*.query.ts"),
131
+ files: featuresGlob(featureRoot, "**/queries/*.ts"),
124
132
  plugins: { local: localPlugin },
125
133
  rules: {
126
134
  "local/supabase-select-typed-columns": "error",
@@ -129,8 +137,8 @@ export function createNamingConfigs(featureRoot, prefixLibMapping) {
129
137
  {
130
138
  name: "naming/supabase-columns-satisfies",
131
139
  files: [
132
- ...featuresGlob(featureRoot, "**/queries/*.query.ts"),
133
- ...featuresGlob(featureRoot, "**/constants/*.constant.ts"),
140
+ ...featuresGlob(featureRoot, "**/queries/*.ts"),
141
+ ...featuresGlob(featureRoot, "**/constants/*.ts"),
134
142
  ],
135
143
  plugins: { local: localPlugin },
136
144
  rules: {
@@ -155,7 +163,7 @@ export function createNamingConfigs(featureRoot, prefixLibMapping) {
155
163
  rules: {
156
164
  "check-file/filename-naming-convention": [
157
165
  "error",
158
- { "**/*.ts": `${sharedPrefixPattern}.service` },
166
+ { "**/*.ts": sharedPrefixPattern },
159
167
  ],
160
168
  },
161
169
  },
@@ -166,7 +174,7 @@ export function createNamingConfigs(featureRoot, prefixLibMapping) {
166
174
  rules: {
167
175
  "check-file/filename-naming-convention": [
168
176
  "error",
169
- { "**/*.ts": `${sharedPrefixPattern}.query` },
177
+ { "**/*.ts": sharedPrefixPattern },
170
178
  ],
171
179
  },
172
180
  },
@@ -175,14 +183,13 @@ export function createNamingConfigs(featureRoot, prefixLibMapping) {
175
183
  configs.push(
176
184
  {
177
185
  name: "naming/types",
178
- files: featuresGlob(featureRoot, "*/types/*.type.ts"),
186
+ files: featuresGlob(featureRoot, "*/types/*.ts"),
179
187
  ignores: featuresGlob(featureRoot, "shared/types/*.ts"),
180
188
  plugins: { "check-file": checkFile },
181
189
  rules: {
182
190
  "check-file/filename-naming-convention": [
183
191
  "error",
184
192
  { "**/*/types/*.ts": "<1>" },
185
- { ignoreMiddleExtensions: true },
186
193
  ],
187
194
  },
188
195
  },
@@ -193,26 +200,25 @@ export function createNamingConfigs(featureRoot, prefixLibMapping) {
193
200
  rules: {
194
201
  "check-file/filename-naming-convention": [
195
202
  "error",
196
- { "**/*.ts": `${sharedPrefixPattern}.type` },
203
+ { "**/*.ts": sharedPrefixPattern },
197
204
  ],
198
205
  },
199
206
  },
200
207
  {
201
208
  name: "naming/schemas",
202
- files: featuresGlob(featureRoot, "*/schemas/*.schema.ts"),
209
+ files: featuresGlob(featureRoot, "*/schemas/*.ts"),
203
210
  ignores: featuresGlob(featureRoot, "shared/schemas/*.ts"),
204
211
  plugins: { "check-file": checkFile },
205
212
  rules: {
206
213
  "check-file/filename-naming-convention": [
207
214
  "error",
208
215
  { "**/*/schemas/*.ts": "<1>" },
209
- { ignoreMiddleExtensions: true },
210
216
  ],
211
217
  },
212
218
  },
213
219
  {
214
220
  name: "naming/schema-naming",
215
- files: featuresGlob(featureRoot, "**/schemas/*.schema.ts"),
221
+ files: featuresGlob(featureRoot, "**/schemas/*.ts"),
216
222
  plugins: { local: localPlugin },
217
223
  rules: {
218
224
  "local/schema-naming": "error",
@@ -225,20 +231,19 @@ export function createNamingConfigs(featureRoot, prefixLibMapping) {
225
231
  rules: {
226
232
  "check-file/filename-naming-convention": [
227
233
  "error",
228
- { "**/*.ts": `${sharedPrefixPattern}.schema` },
234
+ { "**/*.ts": sharedPrefixPattern },
229
235
  ],
230
236
  },
231
237
  },
232
238
  {
233
239
  name: "naming/utils",
234
- files: featuresGlob(featureRoot, "*/utils/*.util.ts"),
240
+ files: featuresGlob(featureRoot, "*/utils/*.ts"),
235
241
  ignores: featuresGlob(featureRoot, "shared/utils/*.ts"),
236
242
  plugins: { "check-file": checkFile },
237
243
  rules: {
238
244
  "check-file/filename-naming-convention": [
239
245
  "error",
240
246
  { "**/*/utils/*.ts": "<1>" },
241
- { ignoreMiddleExtensions: true },
242
247
  ],
243
248
  },
244
249
  },
@@ -249,20 +254,19 @@ export function createNamingConfigs(featureRoot, prefixLibMapping) {
249
254
  rules: {
250
255
  "check-file/filename-naming-convention": [
251
256
  "error",
252
- { "**/*.ts": `${sharedPrefixPattern}.util` },
257
+ { "**/*.ts": sharedPrefixPattern },
253
258
  ],
254
259
  },
255
260
  },
256
261
  {
257
262
  name: "naming/constants",
258
- files: featuresGlob(featureRoot, "*/constants/*.constant.ts"),
263
+ files: featuresGlob(featureRoot, "*/constants/*.ts"),
259
264
  ignores: featuresGlob(featureRoot, "shared/constants/*.ts"),
260
265
  plugins: { "check-file": checkFile },
261
266
  rules: {
262
267
  "check-file/filename-naming-convention": [
263
268
  "error",
264
269
  { "**/*/constants/*.ts": "<1>" },
265
- { ignoreMiddleExtensions: true },
266
270
  ],
267
271
  },
268
272
  },
@@ -273,7 +277,7 @@ export function createNamingConfigs(featureRoot, prefixLibMapping) {
273
277
  rules: {
274
278
  "check-file/filename-naming-convention": [
275
279
  "error",
276
- { "**/*.ts": `${sharedPrefixPattern}.constant` },
280
+ { "**/*.ts": sharedPrefixPattern },
277
281
  ],
278
282
  },
279
283
  },
@@ -287,7 +291,7 @@ export function createNamingConfigs(featureRoot, prefixLibMapping) {
287
291
  rules: {
288
292
  "check-file/filename-naming-convention": [
289
293
  "error",
290
- { "**/*.ts": interactorPattern },
294
+ { "**/*.ts": prefixPattern },
291
295
  ],
292
296
  },
293
297
  });
@@ -300,7 +304,7 @@ export function createNamingConfigs(featureRoot, prefixLibMapping) {
300
304
  rules: {
301
305
  "check-file/filename-naming-convention": [
302
306
  "error",
303
- { "**/*.ts": `${sharedPrefixPattern}.interactor` },
307
+ { "**/*.ts": sharedPrefixPattern },
304
308
  ],
305
309
  },
306
310
  },
@@ -2,7 +2,7 @@
2
2
  export const directivesConfigs = [
3
3
  {
4
4
  name: "directives/server-interactor",
5
- files: ["src/features/**/interactors/server.interactor.ts"],
5
+ files: ["src/features/**/interactors/server.ts"],
6
6
  rules: {
7
7
  "no-restricted-syntax": [
8
8
  "error",
@@ -10,14 +10,14 @@ export const directivesConfigs = [
10
10
  selector:
11
11
  "Program > :first-child:not(ExpressionStatement[expression.value='use server'])",
12
12
  message:
13
- 'server.interactor.ts must start with "use server" directive.',
13
+ 'interactors/server.ts must start with "use server" directive.',
14
14
  },
15
15
  ],
16
16
  },
17
17
  },
18
18
  {
19
19
  name: "directives/admin-interactor",
20
- files: ["src/features/**/interactors/admin.interactor.ts"],
20
+ files: ["src/features/**/interactors/admin.ts"],
21
21
  rules: {
22
22
  "no-restricted-syntax": [
23
23
  "error",
@@ -25,21 +25,21 @@ export const directivesConfigs = [
25
25
  selector:
26
26
  "Program > :first-child:not(ExpressionStatement[expression.value='use server'])",
27
27
  message:
28
- 'admin.interactor.ts must start with "use server" directive.',
28
+ 'interactors/admin.ts must start with "use server" directive.',
29
29
  },
30
30
  ],
31
31
  },
32
32
  },
33
33
  {
34
34
  name: "directives/client-interactor",
35
- files: ["src/features/**/interactors/client.interactor.ts"],
35
+ files: ["src/features/**/interactors/client.ts"],
36
36
  rules: {
37
37
  "no-restricted-syntax": [
38
38
  "error",
39
39
  {
40
40
  selector: "ExpressionStatement[expression.value='use server']",
41
41
  message:
42
- 'client.interactor.ts must NOT have "use server" directive. It uses @/lib/supabase/client.',
42
+ 'interactors/client.ts must NOT have "use server" directive. It uses @/lib/supabase/client.',
43
43
  },
44
44
  ],
45
45
  },