@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 +32 -11
- package/package.json +1 -1
- package/src/common/constants.mjs +35 -22
- package/src/common/imports.mjs +9 -9
- package/src/common/local-plugins/feature-name.mjs +2 -2
- package/src/common/local-plugins/namespace-import-name.mjs +22 -22
- package/src/common/local-plugins/queries-export.mjs +4 -4
- package/src/common/local-plugins/queries-namespace-import.mjs +7 -6
- package/src/common/local-plugins/supabase-columns-satisfies.mjs +32 -39
- package/src/common/local-plugins/supabase-select-typed-columns.mjs +50 -48
- package/src/common/naming.mjs +43 -39
- package/src/next/directives.mjs +6 -6
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
|
|
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/ #
|
|
29
|
-
│ ├── services/ #
|
|
30
|
-
│ ├── queries/ #
|
|
31
|
-
│ ├── types/ #
|
|
32
|
-
│ ├── schemas/ #
|
|
33
|
-
│ ├── utils/ #
|
|
34
|
-
│ └── constants/ #
|
|
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
|
-
{
|
|
38
|
-
{
|
|
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
package/src/common/constants.mjs
CHANGED
|
@@ -20,14 +20,22 @@ function findProjectRoot() {
|
|
|
20
20
|
|
|
21
21
|
const PROJECT_ROOT = findProjectRoot();
|
|
22
22
|
|
|
23
|
-
|
|
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
|
-
/**
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
)
|
|
57
|
-
|
|
58
|
-
|
|
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
|
|
62
|
-
const prefix =
|
|
63
|
-
mapping[prefix] =
|
|
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
|
|
package/src/common/imports.mjs
CHANGED
|
@@ -62,23 +62,23 @@ const LATERAL_PATTERNS = {
|
|
|
62
62
|
const CARDINALITY_PATTERNS = {
|
|
63
63
|
server: [
|
|
64
64
|
{
|
|
65
|
-
group: ["**/services/client
|
|
65
|
+
group: ["**/services/client", "**/services/admin"],
|
|
66
66
|
message:
|
|
67
|
-
"server
|
|
67
|
+
"server interactor can only import server service (cardinality violation)",
|
|
68
68
|
},
|
|
69
69
|
],
|
|
70
70
|
client: [
|
|
71
71
|
{
|
|
72
|
-
group: ["**/services/server
|
|
72
|
+
group: ["**/services/server", "**/services/admin"],
|
|
73
73
|
message:
|
|
74
|
-
"client
|
|
74
|
+
"client interactor can only import client service (cardinality violation)",
|
|
75
75
|
},
|
|
76
76
|
],
|
|
77
77
|
admin: [
|
|
78
78
|
{
|
|
79
|
-
group: ["**/services/server
|
|
79
|
+
group: ["**/services/server", "**/services/client"],
|
|
80
80
|
message:
|
|
81
|
-
"admin
|
|
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:
|
|
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}.
|
|
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}.
|
|
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/
|
|
116
|
+
featureRoot.replace(/features$/, "lib/supabase/types.ts"),
|
|
117
117
|
);
|
|
118
118
|
const fallbackTypePath = path.join(
|
|
119
119
|
projectRoot,
|
|
120
|
-
"src/lib/supabase/
|
|
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}/{
|
|
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
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
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
|
-
*
|
|
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}
|
|
54
|
+
// Expected: {feature}/{layerDir}/{scope}
|
|
50
55
|
const segments = afterRoot.split("/");
|
|
51
|
-
if (segments.length <
|
|
56
|
+
if (segments.length < 3) return null;
|
|
52
57
|
|
|
53
58
|
const featureDir = segments[0];
|
|
54
|
-
const
|
|
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 =
|
|
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/*.
|
|
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
|
-
*
|
|
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
|
|
2
|
+
* Enforce namespace imports for files under `queries/`:
|
|
3
3
|
*
|
|
4
|
-
* Value imports must use `import * as xxxQuery from "..."` so that the
|
|
5
|
-
*
|
|
6
|
-
*
|
|
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\/[^/]
|
|
11
|
+
const QUERIES_PATH = /\/queries\/[^/]+$/;
|
|
11
12
|
|
|
12
13
|
export const queriesNamespaceImportRule = {
|
|
13
14
|
meta: {
|
|
@@ -1,50 +1,47 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Enforce
|
|
3
|
-
* `*_COLUMNS` constant declarations.
|
|
2
|
+
* Enforce `<string literal> as const` for `*_COLUMNS` constant declarations.
|
|
4
3
|
*
|
|
5
|
-
* Apply to `**\/queries/*.
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* TypeScript
|
|
9
|
-
*
|
|
10
|
-
*
|
|
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
|
-
|
|
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 !== "
|
|
31
|
-
const
|
|
32
|
-
if (
|
|
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
|
-
|
|
39
|
-
return
|
|
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
|
-
|
|
47
|
-
'Column constant `{{ name }}` must
|
|
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
|
-
|
|
57
|
-
|
|
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: "
|
|
57
|
+
messageId: "shape",
|
|
65
58
|
data: { name: node.id.name },
|
|
66
59
|
});
|
|
67
60
|
},
|
|
@@ -1,59 +1,51 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Enforce
|
|
2
|
+
* Enforce explicit column lists for Supabase `.select()` calls.
|
|
3
3
|
*
|
|
4
|
-
* Apply to `**\/queries/*.
|
|
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
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
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()
|
|
19
|
-
* .select("*")
|
|
20
|
-
* .select(
|
|
21
|
-
* .select(
|
|
22
|
-
* .select(
|
|
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(
|
|
24
|
+
* .select("id,url,platform") inline literal
|
|
25
|
+
* .select(POST_DETAIL_COLUMNS) *_COLUMNS named constant
|
|
27
26
|
*
|
|
28
|
-
* Why: column lists must be (1)
|
|
29
|
-
*
|
|
30
|
-
* For column-level access control, use Postgres views
|
|
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
|
|
51
|
-
|
|
52
|
-
'
|
|
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
|
-
|
|
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.
|
|
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"
|
|
75
|
-
|
|
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
|
-
|
|
80
|
-
|
|
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 (
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
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
|
},
|
package/src/common/naming.mjs
CHANGED
|
@@ -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
|
-
/**
|
|
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": "
|
|
22
|
+
{ "**/*.ts": "*" },
|
|
18
23
|
],
|
|
19
24
|
},
|
|
20
25
|
},
|
|
21
26
|
];
|
|
22
27
|
}
|
|
23
28
|
|
|
24
|
-
/**
|
|
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": "
|
|
45
|
+
{ "**/*.ts": "*" },
|
|
37
46
|
],
|
|
38
47
|
},
|
|
39
48
|
},
|
|
40
49
|
];
|
|
41
50
|
}
|
|
42
51
|
|
|
43
|
-
/**
|
|
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("|")})` :
|
|
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":
|
|
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":
|
|
109
|
+
{ "**/*.ts": prefixPattern },
|
|
102
110
|
],
|
|
103
111
|
},
|
|
104
112
|
},
|
|
105
113
|
{
|
|
106
114
|
name: "naming/queries-export",
|
|
107
|
-
files: featuresGlob(featureRoot, "**/queries/*.
|
|
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/*.
|
|
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/*.
|
|
133
|
-
...featuresGlob(featureRoot, "**/constants/*.
|
|
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":
|
|
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":
|
|
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/*.
|
|
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":
|
|
203
|
+
{ "**/*.ts": sharedPrefixPattern },
|
|
197
204
|
],
|
|
198
205
|
},
|
|
199
206
|
},
|
|
200
207
|
{
|
|
201
208
|
name: "naming/schemas",
|
|
202
|
-
files: featuresGlob(featureRoot, "*/schemas/*.
|
|
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/*.
|
|
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":
|
|
234
|
+
{ "**/*.ts": sharedPrefixPattern },
|
|
229
235
|
],
|
|
230
236
|
},
|
|
231
237
|
},
|
|
232
238
|
{
|
|
233
239
|
name: "naming/utils",
|
|
234
|
-
files: featuresGlob(featureRoot, "*/utils/*.
|
|
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":
|
|
257
|
+
{ "**/*.ts": sharedPrefixPattern },
|
|
253
258
|
],
|
|
254
259
|
},
|
|
255
260
|
},
|
|
256
261
|
{
|
|
257
262
|
name: "naming/constants",
|
|
258
|
-
files: featuresGlob(featureRoot, "*/constants/*.
|
|
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":
|
|
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":
|
|
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":
|
|
307
|
+
{ "**/*.ts": sharedPrefixPattern },
|
|
304
308
|
],
|
|
305
309
|
},
|
|
306
310
|
},
|
package/src/next/directives.mjs
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
export const directivesConfigs = [
|
|
3
3
|
{
|
|
4
4
|
name: "directives/server-interactor",
|
|
5
|
-
files: ["src/features/**/interactors/server.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
42
|
+
'interactors/client.ts must NOT have "use server" directive. It uses @/lib/supabase/client.',
|
|
43
43
|
},
|
|
44
44
|
],
|
|
45
45
|
},
|