@yasainet/eslint 0.0.54 → 0.0.56
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
package/src/common/imports.mjs
CHANGED
|
@@ -162,13 +162,27 @@ function makeConfig(name, files, ...patternArrays) {
|
|
|
162
162
|
};
|
|
163
163
|
}
|
|
164
164
|
|
|
165
|
+
// In ESLint flat config, when multiple matching configs set the same rule
|
|
166
|
+
// (`no-restricted-imports`), the later config's options REPLACE the earlier
|
|
167
|
+
// ones — patterns are not merged. Page / hooks / components boundary configs
|
|
168
|
+
// run after libBoundaryConfigs and would silently drop lib + mapping bans
|
|
169
|
+
// unless we re-include those patterns explicitly.
|
|
165
170
|
/** Next.js-only: restrict page.tsx to only import interactors. */
|
|
166
171
|
export const pageBoundaryConfigs = [
|
|
167
172
|
{
|
|
168
173
|
name: "imports/page-boundary",
|
|
169
174
|
files: ["src/app/**/page.tsx"],
|
|
170
175
|
rules: {
|
|
171
|
-
"no-restricted-imports": [
|
|
176
|
+
"no-restricted-imports": [
|
|
177
|
+
"error",
|
|
178
|
+
{
|
|
179
|
+
patterns: [
|
|
180
|
+
...PAGE_BOUNDARY_PATTERNS,
|
|
181
|
+
...LIB_BOUNDARY_PATTERNS,
|
|
182
|
+
...MAPPING_PATTERNS,
|
|
183
|
+
],
|
|
184
|
+
},
|
|
185
|
+
],
|
|
172
186
|
},
|
|
173
187
|
},
|
|
174
188
|
];
|
|
@@ -179,7 +193,16 @@ export const hooksBoundaryConfigs = [
|
|
|
179
193
|
name: "imports/hooks-boundary",
|
|
180
194
|
files: ["src/features/**/hooks/*.ts"],
|
|
181
195
|
rules: {
|
|
182
|
-
"no-restricted-imports": [
|
|
196
|
+
"no-restricted-imports": [
|
|
197
|
+
"error",
|
|
198
|
+
{
|
|
199
|
+
patterns: [
|
|
200
|
+
...HOOKS_BOUNDARY_PATTERNS,
|
|
201
|
+
...LIB_BOUNDARY_PATTERNS,
|
|
202
|
+
...MAPPING_PATTERNS,
|
|
203
|
+
],
|
|
204
|
+
},
|
|
205
|
+
],
|
|
183
206
|
},
|
|
184
207
|
},
|
|
185
208
|
];
|
|
@@ -192,7 +215,13 @@ export const componentsBoundaryConfigs = [
|
|
|
192
215
|
rules: {
|
|
193
216
|
"no-restricted-imports": [
|
|
194
217
|
"error",
|
|
195
|
-
{
|
|
218
|
+
{
|
|
219
|
+
patterns: [
|
|
220
|
+
...COMPONENTS_BOUNDARY_PATTERNS,
|
|
221
|
+
...LIB_BOUNDARY_PATTERNS,
|
|
222
|
+
...MAPPING_PATTERNS,
|
|
223
|
+
],
|
|
224
|
+
},
|
|
196
225
|
],
|
|
197
226
|
},
|
|
198
227
|
},
|
|
@@ -6,6 +6,8 @@ import { noAnyReturnRule } from "./no-any-return.mjs";
|
|
|
6
6
|
import { queriesExportRule } from "./queries-export.mjs";
|
|
7
7
|
import { queriesNamespaceImportRule } from "./queries-namespace-import.mjs";
|
|
8
8
|
import { schemaNamingRule } from "./schema-naming.mjs";
|
|
9
|
+
import { supabaseColumnsSatisfiesRule } from "./supabase-columns-satisfies.mjs";
|
|
10
|
+
import { supabaseSelectTypedColumnsRule } from "./supabase-select-typed-columns.mjs";
|
|
9
11
|
|
|
10
12
|
/** Single plugin object to avoid ESLint "Cannot redefine plugin" errors. */
|
|
11
13
|
export const localPlugin = {
|
|
@@ -18,5 +20,7 @@ export const localPlugin = {
|
|
|
18
20
|
"queries-export": queriesExportRule,
|
|
19
21
|
"queries-namespace-import": queriesNamespaceImportRule,
|
|
20
22
|
"schema-naming": schemaNamingRule,
|
|
23
|
+
"supabase-columns-satisfies": supabaseColumnsSatisfiesRule,
|
|
24
|
+
"supabase-select-typed-columns": supabaseSelectTypedColumnsRule,
|
|
21
25
|
},
|
|
22
26
|
};
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Enforce `[...] as const satisfies readonly (keyof Tables<"...">)[]` for
|
|
3
|
+
* `*_COLUMNS` constant declarations.
|
|
4
|
+
*
|
|
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.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
const COLUMNS_NAME = /^[A-Z][A-Z0-9_]*_COLUMNS$/;
|
|
14
|
+
|
|
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) {
|
|
29
|
+
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;
|
|
36
|
+
if (ann.typeName.type !== "Identifier") return false;
|
|
37
|
+
if (ann.typeName.name !== "const") return false;
|
|
38
|
+
if (inner.expression.type !== "ArrayExpression") return false;
|
|
39
|
+
return true;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export const supabaseColumnsSatisfiesRule = {
|
|
43
|
+
meta: {
|
|
44
|
+
type: "problem",
|
|
45
|
+
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.',
|
|
48
|
+
},
|
|
49
|
+
schema: [],
|
|
50
|
+
},
|
|
51
|
+
create(context) {
|
|
52
|
+
return {
|
|
53
|
+
VariableDeclarator(node) {
|
|
54
|
+
if (node.id.type !== "Identifier") return;
|
|
55
|
+
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;
|
|
62
|
+
context.report({
|
|
63
|
+
node: node.id,
|
|
64
|
+
messageId: "missing",
|
|
65
|
+
data: { name: node.id.name },
|
|
66
|
+
});
|
|
67
|
+
},
|
|
68
|
+
};
|
|
69
|
+
},
|
|
70
|
+
};
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Enforce typed column constants for Supabase `.select()` calls.
|
|
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.
|
|
11
|
+
*
|
|
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).
|
|
16
|
+
*
|
|
17
|
+
* 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
|
|
24
|
+
*
|
|
25
|
+
* Allowed:
|
|
26
|
+
* .select(joinColumns(POST_LIST_COLUMNS)) typed constant via project helper
|
|
27
|
+
*
|
|
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")`).
|
|
31
|
+
*/
|
|
32
|
+
|
|
33
|
+
const COLUMNS_NAME = /^[A-Z][A-Z0-9_]*_COLUMNS$/;
|
|
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
|
+
export const supabaseSelectTypedColumnsRule = {
|
|
46
|
+
meta: {
|
|
47
|
+
type: "problem",
|
|
48
|
+
messages: {
|
|
49
|
+
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.',
|
|
53
|
+
shapeArg:
|
|
54
|
+
'`.select()` argument must be `joinColumns(<X_COLUMNS>)`. Other expressions defeat type inference and column-level review.',
|
|
55
|
+
naming:
|
|
56
|
+
"Column constant `{{ name }}` must be UPPER_SNAKE_CASE ending with `_COLUMNS` (e.g. POST_LIST_COLUMNS, POST_DETAIL_COLUMNS).",
|
|
57
|
+
},
|
|
58
|
+
schema: [],
|
|
59
|
+
},
|
|
60
|
+
create(context) {
|
|
61
|
+
return {
|
|
62
|
+
CallExpression(node) {
|
|
63
|
+
if (node.callee.type !== "MemberExpression") return;
|
|
64
|
+
if (node.callee.property.type !== "Identifier") return;
|
|
65
|
+
if (node.callee.property.name !== "select") return;
|
|
66
|
+
|
|
67
|
+
if (node.arguments.length === 0) {
|
|
68
|
+
context.report({ node, messageId: "noArgs" });
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const arg = node.arguments[0];
|
|
73
|
+
|
|
74
|
+
if (arg.type === "Literal" || arg.type === "TemplateLiteral") {
|
|
75
|
+
context.report({ node: arg, messageId: "literalArg" });
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const id = asJoinColumnsCall(arg);
|
|
80
|
+
if (!id) {
|
|
81
|
+
context.report({ node: arg, messageId: "shapeArg" });
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (!COLUMNS_NAME.test(id.name)) {
|
|
86
|
+
context.report({
|
|
87
|
+
node: id,
|
|
88
|
+
messageId: "naming",
|
|
89
|
+
data: { name: id.name },
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
},
|
|
93
|
+
};
|
|
94
|
+
},
|
|
95
|
+
};
|
package/src/common/naming.mjs
CHANGED
|
@@ -118,6 +118,25 @@ export function createNamingConfigs(featureRoot, prefixLibMapping) {
|
|
|
118
118
|
"local/queries-namespace-import": "error",
|
|
119
119
|
},
|
|
120
120
|
},
|
|
121
|
+
{
|
|
122
|
+
name: "naming/supabase-select",
|
|
123
|
+
files: featuresGlob(featureRoot, "**/queries/*.query.ts"),
|
|
124
|
+
plugins: { local: localPlugin },
|
|
125
|
+
rules: {
|
|
126
|
+
"local/supabase-select-typed-columns": "error",
|
|
127
|
+
},
|
|
128
|
+
},
|
|
129
|
+
{
|
|
130
|
+
name: "naming/supabase-columns-satisfies",
|
|
131
|
+
files: [
|
|
132
|
+
...featuresGlob(featureRoot, "**/queries/*.query.ts"),
|
|
133
|
+
...featuresGlob(featureRoot, "**/constants/*.constant.ts"),
|
|
134
|
+
],
|
|
135
|
+
plugins: { local: localPlugin },
|
|
136
|
+
rules: {
|
|
137
|
+
"local/supabase-columns-satisfies": "error",
|
|
138
|
+
},
|
|
139
|
+
},
|
|
121
140
|
{
|
|
122
141
|
name: "naming/form-state",
|
|
123
142
|
files: featuresGlob(featureRoot, "**/*.ts"),
|