@sarj/eslint-plugin 1.0.1 → 2.0.1
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 +7 -128
- package/dist/index.cjs +1070 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +115 -0
- package/dist/index.d.ts +115 -12
- package/dist/index.js +1049 -95
- package/dist/index.js.map +1 -0
- package/package.json +43 -26
- package/dist/index.d.ts.map +0 -1
- package/dist/rules/enforce-file-structure.d.ts +0 -4
- package/dist/rules/enforce-file-structure.d.ts.map +0 -1
- package/dist/rules/enforce-file-structure.js +0 -90
- package/dist/rules/no-enum.d.ts +0 -8
- package/dist/rules/no-enum.d.ts.map +0 -1
- package/dist/rules/no-enum.js +0 -29
- package/dist/rules/no-raw-env.d.ts +0 -8
- package/dist/rules/no-raw-env.d.ts.map +0 -1
- package/dist/rules/no-raw-env.js +0 -33
- package/dist/rules/prefer-shadcn.d.ts +0 -4
- package/dist/rules/prefer-shadcn.d.ts.map +0 -1
- package/dist/rules/prefer-shadcn.js +0 -50
- package/dist/rules/require-assert-never.d.ts +0 -8
- package/dist/rules/require-assert-never.d.ts.map +0 -1
- package/dist/rules/require-assert-never.js +0 -53
- package/dist/rules/require-zod-form-validation.d.ts +0 -8
- package/dist/rules/require-zod-form-validation.d.ts.map +0 -1
- package/dist/rules/require-zod-form-validation.js +0 -59
- package/dist/rules/zod-naming-convention.d.ts +0 -8
- package/dist/rules/zod-naming-convention.d.ts.map +0 -1
- package/dist/rules/zod-naming-convention.js +0 -53
package/dist/index.js
CHANGED
|
@@ -1,98 +1,1052 @@
|
|
|
1
|
-
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
const
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
1
|
+
// src/rules/enforce-file-structure.ts
|
|
2
|
+
import { ESLintUtils, AST_NODE_TYPES } from "@typescript-eslint/utils";
|
|
3
|
+
var SECTION = {
|
|
4
|
+
imports: 0,
|
|
5
|
+
types: 1,
|
|
6
|
+
constants: 2,
|
|
7
|
+
functions: 3,
|
|
8
|
+
exports: 4
|
|
9
|
+
};
|
|
10
|
+
var SECTION_NAMES = [
|
|
11
|
+
"imports",
|
|
12
|
+
"types",
|
|
13
|
+
"constants",
|
|
14
|
+
"functions",
|
|
15
|
+
"exports"
|
|
16
|
+
];
|
|
17
|
+
var sectionName = (ordinal) => {
|
|
18
|
+
const name = SECTION_NAMES[ordinal];
|
|
19
|
+
return name ?? "unknown";
|
|
20
|
+
};
|
|
21
|
+
var isConstantNamed = (declarator) => {
|
|
22
|
+
if (declarator.id.type !== AST_NODE_TYPES.Identifier) return false;
|
|
23
|
+
const name = declarator.id.name;
|
|
24
|
+
return name.length > 0 && name === name.toUpperCase();
|
|
25
|
+
};
|
|
26
|
+
var getStatementSection = (statement) => {
|
|
27
|
+
switch (statement.type) {
|
|
28
|
+
case AST_NODE_TYPES.ImportDeclaration:
|
|
29
|
+
return SECTION.imports;
|
|
30
|
+
case AST_NODE_TYPES.TSTypeAliasDeclaration:
|
|
31
|
+
case AST_NODE_TYPES.TSInterfaceDeclaration:
|
|
32
|
+
case AST_NODE_TYPES.TSEnumDeclaration:
|
|
33
|
+
return SECTION.types;
|
|
34
|
+
case AST_NODE_TYPES.VariableDeclaration: {
|
|
35
|
+
if (statement.kind === "const") {
|
|
36
|
+
const firstDeclarator = statement.declarations[0];
|
|
37
|
+
if (firstDeclarator !== void 0 && isConstantNamed(firstDeclarator)) {
|
|
38
|
+
return SECTION.constants;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return SECTION.functions;
|
|
42
|
+
}
|
|
43
|
+
case AST_NODE_TYPES.FunctionDeclaration:
|
|
44
|
+
return SECTION.functions;
|
|
45
|
+
case AST_NODE_TYPES.ExportNamedDeclaration:
|
|
46
|
+
case AST_NODE_TYPES.ExportDefaultDeclaration:
|
|
47
|
+
case AST_NODE_TYPES.ExportAllDeclaration:
|
|
48
|
+
return SECTION.exports;
|
|
49
|
+
default:
|
|
50
|
+
return SECTION.functions;
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
var isUseServerDirective = (statement) => {
|
|
54
|
+
if (statement === void 0) return false;
|
|
55
|
+
if (statement.type !== AST_NODE_TYPES.ExpressionStatement) return false;
|
|
56
|
+
const expr = statement.expression;
|
|
57
|
+
if (expr.type !== AST_NODE_TYPES.Literal) return false;
|
|
58
|
+
return expr.value === "use server";
|
|
59
|
+
};
|
|
60
|
+
var enforce_file_structure_default = ESLintUtils.RuleCreator(
|
|
61
|
+
(name) => `https://github.com/sarj-ai/linting/blob/main/packages/typescript/src/rules/${name}.ts`
|
|
62
|
+
)({
|
|
63
|
+
name: "enforce-file-structure",
|
|
64
|
+
meta: {
|
|
65
|
+
type: "suggestion",
|
|
66
|
+
docs: {
|
|
67
|
+
description: "Enforce a canonical top-of-file ordering: imports -> types -> constants -> functions -> exports. Server-action files (under `/actions/` or with `action` in the path) must also begin with a `use server` directive."
|
|
27
68
|
},
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
69
|
+
schema: [],
|
|
70
|
+
messages: {
|
|
71
|
+
incorrectOrder: "File structure violation: {{current}} should come before {{expected}}",
|
|
72
|
+
useServerDirective: "Server action files must start with 'use server' directive"
|
|
73
|
+
}
|
|
74
|
+
},
|
|
75
|
+
defaultOptions: [],
|
|
76
|
+
create(context) {
|
|
77
|
+
const filename = context.filename;
|
|
78
|
+
const isServerAction = filename.includes("/actions/") || filename.includes("action");
|
|
79
|
+
return {
|
|
80
|
+
Program(node) {
|
|
81
|
+
const body = node.body;
|
|
82
|
+
if (isServerAction) {
|
|
83
|
+
const firstNode = body[0];
|
|
84
|
+
if (!isUseServerDirective(firstNode)) {
|
|
85
|
+
context.report({
|
|
86
|
+
node,
|
|
87
|
+
messageId: "useServerDirective"
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
let currentSection = SECTION.imports;
|
|
92
|
+
for (const statement of body) {
|
|
93
|
+
if (isUseServerDirective(statement)) continue;
|
|
94
|
+
if (statement.type === AST_NODE_TYPES.ExpressionStatement && statement.expression.type === AST_NODE_TYPES.Literal && typeof statement.expression.value === "string" && statement.expression.value.startsWith("use ")) {
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
const statementSection = getStatementSection(statement);
|
|
98
|
+
if (statementSection < currentSection) {
|
|
99
|
+
context.report({
|
|
100
|
+
node: statement,
|
|
101
|
+
messageId: "incorrectOrder",
|
|
102
|
+
data: {
|
|
103
|
+
current: sectionName(statementSection),
|
|
104
|
+
expected: sectionName(currentSection)
|
|
105
|
+
}
|
|
106
|
+
});
|
|
107
|
+
} else if (statementSection > currentSection) {
|
|
108
|
+
currentSection = statementSection;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
// src/rules/no-client-side-data-fetching.ts
|
|
117
|
+
import {
|
|
118
|
+
AST_NODE_TYPES as AST_NODE_TYPES2,
|
|
119
|
+
ESLintUtils as ESLintUtils2
|
|
120
|
+
} from "@typescript-eslint/utils";
|
|
121
|
+
var FETCH_LIBS = /* @__PURE__ */ new Set(["axios", "ky", "superagent"]);
|
|
122
|
+
var HTTP_METHOD_NAMES = /* @__PURE__ */ new Set([
|
|
123
|
+
"get",
|
|
124
|
+
"post",
|
|
125
|
+
"put",
|
|
126
|
+
"delete",
|
|
127
|
+
"patch",
|
|
128
|
+
"request",
|
|
129
|
+
"head",
|
|
130
|
+
"options"
|
|
131
|
+
]);
|
|
132
|
+
var ANALYTICS_KEYWORDS = [
|
|
133
|
+
"analytics",
|
|
134
|
+
"telemetry",
|
|
135
|
+
"track",
|
|
136
|
+
"log",
|
|
137
|
+
"ping",
|
|
138
|
+
"beacon",
|
|
139
|
+
"metrics",
|
|
140
|
+
"event"
|
|
141
|
+
];
|
|
142
|
+
function isEffectHookCall(node) {
|
|
143
|
+
const callee = node.callee;
|
|
144
|
+
if (callee.type === AST_NODE_TYPES2.Identifier) {
|
|
145
|
+
return callee.name === "useEffect" || callee.name === "useLayoutEffect";
|
|
146
|
+
}
|
|
147
|
+
if (callee.type === AST_NODE_TYPES2.MemberExpression && !callee.computed && callee.object.type === AST_NODE_TYPES2.Identifier && callee.object.name === "React" && callee.property.type === AST_NODE_TYPES2.Identifier) {
|
|
148
|
+
return callee.property.name === "useEffect" || callee.property.name === "useLayoutEffect";
|
|
149
|
+
}
|
|
150
|
+
return false;
|
|
151
|
+
}
|
|
152
|
+
function readMethodProperty(optionsArg) {
|
|
153
|
+
if (!optionsArg || optionsArg.type !== AST_NODE_TYPES2.ObjectExpression) {
|
|
154
|
+
return null;
|
|
155
|
+
}
|
|
156
|
+
for (const prop of optionsArg.properties) {
|
|
157
|
+
if (prop.type !== AST_NODE_TYPES2.Property) continue;
|
|
158
|
+
if (prop.computed) continue;
|
|
159
|
+
const key = prop.key;
|
|
160
|
+
const matchesMethodKey = key.type === AST_NODE_TYPES2.Identifier && key.name === "method" || key.type === AST_NODE_TYPES2.Literal && key.value === "method";
|
|
161
|
+
if (!matchesMethodKey) continue;
|
|
162
|
+
if (prop.value.type === AST_NODE_TYPES2.Literal && typeof prop.value.value === "string") {
|
|
163
|
+
return prop.value.value.toUpperCase();
|
|
164
|
+
}
|
|
165
|
+
return null;
|
|
166
|
+
}
|
|
167
|
+
return null;
|
|
168
|
+
}
|
|
169
|
+
function isFetchCall(node) {
|
|
170
|
+
const callee = node.callee;
|
|
171
|
+
if (callee.type === AST_NODE_TYPES2.Identifier && callee.name === "fetch") {
|
|
172
|
+
const method = readMethodProperty(node.arguments[1]);
|
|
173
|
+
if (method !== null && method !== "GET") {
|
|
174
|
+
return false;
|
|
175
|
+
}
|
|
176
|
+
return true;
|
|
177
|
+
}
|
|
178
|
+
if (callee.type === AST_NODE_TYPES2.MemberExpression && !callee.computed && callee.object.type === AST_NODE_TYPES2.Identifier && FETCH_LIBS.has(callee.object.name) && callee.property.type === AST_NODE_TYPES2.Identifier) {
|
|
179
|
+
return HTTP_METHOD_NAMES.has(callee.property.name);
|
|
180
|
+
}
|
|
181
|
+
if (callee.type === AST_NODE_TYPES2.Identifier && (callee.name === "axios" || callee.name === "ky")) {
|
|
182
|
+
const firstArg = node.arguments[0];
|
|
183
|
+
const secondArg = node.arguments[1];
|
|
184
|
+
let configArg;
|
|
185
|
+
if (firstArg?.type === AST_NODE_TYPES2.ObjectExpression) {
|
|
186
|
+
configArg = firstArg;
|
|
187
|
+
} else if (secondArg?.type === AST_NODE_TYPES2.ObjectExpression) {
|
|
188
|
+
configArg = secondArg;
|
|
189
|
+
}
|
|
190
|
+
const method = readMethodProperty(configArg);
|
|
191
|
+
if (method !== null && method !== "GET") {
|
|
192
|
+
return false;
|
|
193
|
+
}
|
|
194
|
+
return true;
|
|
195
|
+
}
|
|
196
|
+
return false;
|
|
197
|
+
}
|
|
198
|
+
function extractUrlString(node) {
|
|
199
|
+
const firstArg = node.arguments[0];
|
|
200
|
+
if (!firstArg) return "";
|
|
201
|
+
if (firstArg.type === AST_NODE_TYPES2.Literal && typeof firstArg.value === "string") {
|
|
202
|
+
return firstArg.value;
|
|
203
|
+
}
|
|
204
|
+
if (firstArg.type === AST_NODE_TYPES2.TemplateLiteral) {
|
|
205
|
+
return firstArg.quasis.map((q) => q.value.cooked).join("");
|
|
206
|
+
}
|
|
207
|
+
if (firstArg.type === AST_NODE_TYPES2.Identifier) {
|
|
208
|
+
return firstArg.name;
|
|
209
|
+
}
|
|
210
|
+
return "";
|
|
211
|
+
}
|
|
212
|
+
function isAnalyticsCall(node) {
|
|
213
|
+
const url = extractUrlString(node).toLowerCase();
|
|
214
|
+
if (url === "") return false;
|
|
215
|
+
return ANALYTICS_KEYWORDS.some((keyword) => url.includes(keyword));
|
|
216
|
+
}
|
|
217
|
+
var no_client_side_data_fetching_default = ESLintUtils2.RuleCreator(
|
|
218
|
+
(name) => `https://github.com/sarj-ai/linting/blob/main/packages/typescript/src/rules/${name}.ts`
|
|
219
|
+
)({
|
|
220
|
+
name: "no-client-side-data-fetching",
|
|
221
|
+
meta: {
|
|
222
|
+
type: "problem",
|
|
223
|
+
docs: {
|
|
224
|
+
description: "Disallow data fetching inside `useEffect` / `useLayoutEffect`; prefer React Server Components, Server Actions, or a client-side cache (SWR / React Query)."
|
|
225
|
+
},
|
|
226
|
+
schema: [],
|
|
227
|
+
messages: {
|
|
228
|
+
noClientFetch: "Avoid direct data fetching inside useEffect / useLayoutEffect. This causes waterfalls and layout shifts. Prefer React Server Components, Server Actions, or client-side caching libraries like SWR or React Query."
|
|
229
|
+
}
|
|
230
|
+
},
|
|
231
|
+
defaultOptions: [],
|
|
232
|
+
create(context) {
|
|
233
|
+
let effectDepth = 0;
|
|
234
|
+
return {
|
|
235
|
+
CallExpression(node) {
|
|
236
|
+
if (isEffectHookCall(node)) {
|
|
237
|
+
effectDepth += 1;
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
if (effectDepth === 0) return;
|
|
241
|
+
if (!isFetchCall(node)) return;
|
|
242
|
+
if (isAnalyticsCall(node)) return;
|
|
243
|
+
context.report({ node, messageId: "noClientFetch" });
|
|
244
|
+
},
|
|
245
|
+
"CallExpression:exit"(node) {
|
|
246
|
+
if (isEffectHookCall(node)) {
|
|
247
|
+
effectDepth -= 1;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
// src/rules/no-enum.ts
|
|
255
|
+
import { ESLintUtils as ESLintUtils3 } from "@typescript-eslint/utils";
|
|
256
|
+
var DEFAULT_IGNORE_PATTERNS = [
|
|
257
|
+
/[\\/]generated[\\/]/,
|
|
258
|
+
/\.gen\.tsx?$/,
|
|
259
|
+
/\.generated\.tsx?$/
|
|
260
|
+
];
|
|
261
|
+
function matchesAnyPattern(filename, patterns) {
|
|
262
|
+
for (const pattern of patterns) {
|
|
263
|
+
const regexSource = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*\*/g, "::DOUBLESTAR::").replace(/\*/g, "[^/\\\\]*").replace(/::DOUBLESTAR::/g, ".*");
|
|
264
|
+
if (new RegExp(`^${regexSource}$`).test(filename)) {
|
|
265
|
+
return true;
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
return false;
|
|
269
|
+
}
|
|
270
|
+
function hasGeneratedMarker(sourceText) {
|
|
271
|
+
const head = sourceText.slice(0, 1024);
|
|
272
|
+
return /@generated\b/.test(head);
|
|
273
|
+
}
|
|
274
|
+
var no_enum_default = ESLintUtils3.RuleCreator(
|
|
275
|
+
(name) => `https://github.com/sarj-ai/linting/blob/main/packages/typescript/src/rules/${name}.ts`
|
|
276
|
+
)({
|
|
277
|
+
name: "no-enum",
|
|
278
|
+
meta: {
|
|
279
|
+
type: "suggestion",
|
|
280
|
+
docs: {
|
|
281
|
+
description: "Disallow TypeScript `enum`; use string-literal unions or `as const` objects instead."
|
|
282
|
+
},
|
|
283
|
+
schema: [
|
|
284
|
+
{
|
|
285
|
+
type: "object",
|
|
286
|
+
additionalProperties: false,
|
|
287
|
+
properties: {
|
|
288
|
+
ignoreFiles: {
|
|
289
|
+
type: "array",
|
|
290
|
+
items: { type: "string" }
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
}
|
|
96
294
|
],
|
|
295
|
+
messages: {
|
|
296
|
+
noEnum: 'Enums are discouraged. Use a string-literal union (e.g. `type Status = "active" | "inactive"`) or an `as const` object instead.'
|
|
297
|
+
}
|
|
298
|
+
},
|
|
299
|
+
defaultOptions: [{}],
|
|
300
|
+
create(context, [optionsArg]) {
|
|
301
|
+
const options = optionsArg ?? {};
|
|
302
|
+
const ignoreFiles = options.ignoreFiles ?? [];
|
|
303
|
+
const filename = context.filename;
|
|
304
|
+
const sourceText = context.sourceCode.getText();
|
|
305
|
+
const isIgnoredByDefault = DEFAULT_IGNORE_PATTERNS.some(
|
|
306
|
+
(re) => re.test(filename)
|
|
307
|
+
);
|
|
308
|
+
const isIgnoredByOption = ignoreFiles.length > 0 && matchesAnyPattern(filename, ignoreFiles);
|
|
309
|
+
const isGenerated = hasGeneratedMarker(sourceText);
|
|
310
|
+
if (isIgnoredByDefault || isIgnoredByOption || isGenerated) {
|
|
311
|
+
return {};
|
|
312
|
+
}
|
|
313
|
+
return {
|
|
314
|
+
TSEnumDeclaration(node) {
|
|
315
|
+
context.report({
|
|
316
|
+
node,
|
|
317
|
+
messageId: "noEnum"
|
|
318
|
+
});
|
|
319
|
+
}
|
|
320
|
+
};
|
|
321
|
+
}
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
// src/rules/no-raw-env.ts
|
|
325
|
+
import { ESLintUtils as ESLintUtils4 } from "@typescript-eslint/utils";
|
|
326
|
+
var no_raw_env_default = ESLintUtils4.RuleCreator(
|
|
327
|
+
(name) => `https://github.com/sarj-ai/linting/blob/main/packages/typescript/src/rules/${name}.ts`
|
|
328
|
+
)({
|
|
329
|
+
name: "no-raw-env",
|
|
330
|
+
meta: {
|
|
331
|
+
type: "problem",
|
|
332
|
+
docs: {
|
|
333
|
+
description: "Disallow direct `process.env` access; use a Zod-validated env module instead."
|
|
334
|
+
},
|
|
335
|
+
schema: [],
|
|
336
|
+
messages: {
|
|
337
|
+
noRawEnv: "Do not read from `process.env` directly. Import the Zod-validated env module instead so values are typed and validated at startup."
|
|
338
|
+
}
|
|
339
|
+
},
|
|
340
|
+
defaultOptions: [],
|
|
341
|
+
create(context) {
|
|
342
|
+
return {
|
|
343
|
+
MemberExpression(node) {
|
|
344
|
+
if (node.computed) {
|
|
345
|
+
return;
|
|
346
|
+
}
|
|
347
|
+
if (node.object.type === "Identifier" && node.object.name === "process" && node.property.type === "Identifier" && node.property.name === "env") {
|
|
348
|
+
context.report({
|
|
349
|
+
node,
|
|
350
|
+
messageId: "noRawEnv"
|
|
351
|
+
});
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
};
|
|
355
|
+
}
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
// src/rules/no-unnecessary-use-client.ts
|
|
359
|
+
import {
|
|
360
|
+
AST_NODE_TYPES as AST_NODE_TYPES3,
|
|
361
|
+
ESLintUtils as ESLintUtils5
|
|
362
|
+
} from "@typescript-eslint/utils";
|
|
363
|
+
var HOOK_REGEX = /^use([A-Z]|$)/;
|
|
364
|
+
var EVENT_PROP_REGEX = /^on[A-Z]/;
|
|
365
|
+
var ERROR_FILE_REGEX = /\b(?:global-)?error\.[jt]sx?$/;
|
|
366
|
+
var BROWSER_GLOBALS = /* @__PURE__ */ new Set([
|
|
367
|
+
"window",
|
|
368
|
+
"document",
|
|
369
|
+
"navigator",
|
|
370
|
+
"localStorage",
|
|
371
|
+
"sessionStorage",
|
|
372
|
+
"location",
|
|
373
|
+
"history",
|
|
374
|
+
"screen",
|
|
375
|
+
"requestAnimationFrame",
|
|
376
|
+
"cancelAnimationFrame",
|
|
377
|
+
"CustomEvent",
|
|
378
|
+
"Event",
|
|
379
|
+
"MouseEvent",
|
|
380
|
+
"KeyboardEvent",
|
|
381
|
+
"TouchEvent"
|
|
382
|
+
]);
|
|
383
|
+
var CLIENT_ONLY_PACKAGES_REGEX = /^(?:@radix-ui\/|framer-motion|react-dom|react-day-picker|@floating-ui\/|react-select|react-toastify|react-hook-form|recharts|react-dropzone|react-slick|react-swipeable|react-resizable|react-draggable|react-beautiful-dnd|@hello-pangea\/dnd|react-virtualized|react-window|@tanstack\/react-table|@tanstack\/react-query|react-redux|recoil|jotai|zustand|@tippyjs\/react|react-color|react-datepicker|next-themes|react-helmet|react-helmet-async|styled-components|@emotion\/)/;
|
|
384
|
+
var isUseClientDirective = (node) => {
|
|
385
|
+
return node.type === AST_NODE_TYPES3.ExpressionStatement && node.expression.type === AST_NODE_TYPES3.Literal && node.expression.value === "use client";
|
|
386
|
+
};
|
|
387
|
+
var isGlobalReference = (node, context) => {
|
|
388
|
+
if (!BROWSER_GLOBALS.has(node.name)) return false;
|
|
389
|
+
const parent = node.parent;
|
|
390
|
+
if (parent !== void 0) {
|
|
391
|
+
if (parent.type === AST_NODE_TYPES3.MemberExpression && parent.property === node && !parent.computed) {
|
|
392
|
+
return false;
|
|
393
|
+
}
|
|
394
|
+
if (parent.type === AST_NODE_TYPES3.Property && parent.key === node && !parent.computed) {
|
|
395
|
+
return false;
|
|
396
|
+
}
|
|
397
|
+
if (parent.type.startsWith("TS")) {
|
|
398
|
+
return false;
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
let scope = context.sourceCode.getScope(node);
|
|
402
|
+
while (scope !== null) {
|
|
403
|
+
const variable = scope.set.get(node.name);
|
|
404
|
+
if (variable !== void 0 && variable.defs.length > 0) {
|
|
405
|
+
return false;
|
|
406
|
+
}
|
|
407
|
+
scope = scope.upper;
|
|
408
|
+
}
|
|
409
|
+
return true;
|
|
410
|
+
};
|
|
411
|
+
var no_unnecessary_use_client_default = ESLintUtils5.RuleCreator(
|
|
412
|
+
(name) => `https://github.com/sarj-ai/linting/blob/main/packages/typescript/src/rules/${name}.ts`
|
|
413
|
+
)({
|
|
414
|
+
name: "no-unnecessary-use-client",
|
|
415
|
+
meta: {
|
|
416
|
+
type: "suggestion",
|
|
417
|
+
docs: {
|
|
418
|
+
description: "Flag `'use client'` files with no hooks or event handlers \u2014 they could be RSC."
|
|
419
|
+
},
|
|
420
|
+
schema: [],
|
|
421
|
+
messages: {
|
|
422
|
+
unnecessaryUseClient: "'use client' directive but no hooks (use*), JSX event handlers (on*), browser globals, or client-only imports found. Consider removing the directive and serving as a React Server Component."
|
|
423
|
+
}
|
|
424
|
+
},
|
|
425
|
+
defaultOptions: [],
|
|
426
|
+
create(context) {
|
|
427
|
+
const filename = context.filename;
|
|
428
|
+
if (ERROR_FILE_REGEX.test(filename)) {
|
|
429
|
+
return {};
|
|
430
|
+
}
|
|
431
|
+
let directiveNode = null;
|
|
432
|
+
let hasClientIndicator = false;
|
|
433
|
+
const markIfHookOrContext = (callee) => {
|
|
434
|
+
if (callee.type === AST_NODE_TYPES3.Identifier) {
|
|
435
|
+
if (HOOK_REGEX.test(callee.name) || callee.name === "createContext") {
|
|
436
|
+
hasClientIndicator = true;
|
|
437
|
+
}
|
|
438
|
+
return;
|
|
439
|
+
}
|
|
440
|
+
if (callee.type === AST_NODE_TYPES3.MemberExpression && callee.property.type === AST_NODE_TYPES3.Identifier) {
|
|
441
|
+
const name = callee.property.name;
|
|
442
|
+
if (HOOK_REGEX.test(name) || name === "createContext") {
|
|
443
|
+
hasClientIndicator = true;
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
};
|
|
447
|
+
return {
|
|
448
|
+
Program(node) {
|
|
449
|
+
for (const stmt of node.body) {
|
|
450
|
+
if (stmt.type !== AST_NODE_TYPES3.ExpressionStatement) break;
|
|
451
|
+
if (isUseClientDirective(stmt)) {
|
|
452
|
+
directiveNode = stmt;
|
|
453
|
+
break;
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
},
|
|
457
|
+
CallExpression(node) {
|
|
458
|
+
markIfHookOrContext(node.callee);
|
|
459
|
+
},
|
|
460
|
+
JSXAttribute(node) {
|
|
461
|
+
if (node.name.type === AST_NODE_TYPES3.JSXIdentifier && EVENT_PROP_REGEX.test(node.name.name)) {
|
|
462
|
+
hasClientIndicator = true;
|
|
463
|
+
}
|
|
464
|
+
},
|
|
465
|
+
ImportDeclaration(node) {
|
|
466
|
+
if (typeof node.source.value === "string" && CLIENT_ONLY_PACKAGES_REGEX.test(node.source.value)) {
|
|
467
|
+
hasClientIndicator = true;
|
|
468
|
+
}
|
|
469
|
+
},
|
|
470
|
+
ExportNamedDeclaration(node) {
|
|
471
|
+
if (node.source !== null) {
|
|
472
|
+
hasClientIndicator = true;
|
|
473
|
+
}
|
|
474
|
+
},
|
|
475
|
+
ExportAllDeclaration(node) {
|
|
476
|
+
if (node.source !== null) {
|
|
477
|
+
hasClientIndicator = true;
|
|
478
|
+
}
|
|
479
|
+
},
|
|
480
|
+
ClassDeclaration() {
|
|
481
|
+
hasClientIndicator = true;
|
|
482
|
+
},
|
|
483
|
+
ClassExpression() {
|
|
484
|
+
hasClientIndicator = true;
|
|
485
|
+
},
|
|
486
|
+
Identifier(node) {
|
|
487
|
+
if (isGlobalReference(node, context)) {
|
|
488
|
+
hasClientIndicator = true;
|
|
489
|
+
}
|
|
490
|
+
},
|
|
491
|
+
"Program:exit"() {
|
|
492
|
+
if (directiveNode !== null && !hasClientIndicator) {
|
|
493
|
+
context.report({
|
|
494
|
+
node: directiveNode,
|
|
495
|
+
messageId: "unnecessaryUseClient"
|
|
496
|
+
});
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
};
|
|
500
|
+
}
|
|
501
|
+
});
|
|
502
|
+
|
|
503
|
+
// src/rules/prefer-schema-for-api-payload.ts
|
|
504
|
+
import {
|
|
505
|
+
AST_NODE_TYPES as AST_NODE_TYPES4,
|
|
506
|
+
ESLintUtils as ESLintUtils6
|
|
507
|
+
} from "@typescript-eslint/utils";
|
|
508
|
+
var unwrap = (node) => {
|
|
509
|
+
let current = node;
|
|
510
|
+
while (current !== null && current !== void 0) {
|
|
511
|
+
if (current.type === AST_NODE_TYPES4.TSAsExpression || current.type === AST_NODE_TYPES4.TSTypeAssertion || current.type === AST_NODE_TYPES4.TSNonNullExpression || current.type === AST_NODE_TYPES4.TSSatisfiesExpression) {
|
|
512
|
+
current = current.expression;
|
|
513
|
+
} else if (current.type === AST_NODE_TYPES4.ChainExpression) {
|
|
514
|
+
current = current.expression;
|
|
515
|
+
} else {
|
|
516
|
+
break;
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
return current ?? null;
|
|
520
|
+
};
|
|
521
|
+
var isJsonCall = (node) => {
|
|
522
|
+
let current = unwrap(node);
|
|
523
|
+
if (current === null) return false;
|
|
524
|
+
if (current.type === AST_NODE_TYPES4.AwaitExpression) {
|
|
525
|
+
current = unwrap(current.argument);
|
|
526
|
+
}
|
|
527
|
+
if (current === null || current.type !== AST_NODE_TYPES4.CallExpression) {
|
|
528
|
+
return false;
|
|
529
|
+
}
|
|
530
|
+
const callee = unwrap(current.callee);
|
|
531
|
+
if (callee === null || callee.type !== AST_NODE_TYPES4.MemberExpression) {
|
|
532
|
+
return false;
|
|
533
|
+
}
|
|
534
|
+
const property = unwrap(callee.property);
|
|
535
|
+
return property !== null && property.type === AST_NODE_TYPES4.Identifier && property.name === "json";
|
|
536
|
+
};
|
|
537
|
+
var findVariable = (scope, name) => {
|
|
538
|
+
let current = scope;
|
|
539
|
+
while (current !== null) {
|
|
540
|
+
const variable = current.set.get(name);
|
|
541
|
+
if (variable !== void 0) return variable;
|
|
542
|
+
current = current.upper;
|
|
543
|
+
}
|
|
544
|
+
return null;
|
|
545
|
+
};
|
|
546
|
+
var isUnvalidatedVariableRef = (node, scope, tracked) => {
|
|
547
|
+
const unwrapped = unwrap(node);
|
|
548
|
+
if (unwrapped === null || unwrapped.type !== AST_NODE_TYPES4.Identifier) {
|
|
549
|
+
return false;
|
|
550
|
+
}
|
|
551
|
+
const variable = findVariable(scope, unwrapped.name);
|
|
552
|
+
return variable !== null && tracked.has(variable);
|
|
553
|
+
};
|
|
554
|
+
var prefer_schema_for_api_payload_default = ESLintUtils6.RuleCreator(
|
|
555
|
+
(name) => `https://github.com/sarj-ai/linting/blob/main/packages/typescript/src/rules/${name}.ts`
|
|
556
|
+
)({
|
|
557
|
+
name: "prefer-schema-for-api-payload",
|
|
558
|
+
meta: {
|
|
559
|
+
type: "problem",
|
|
560
|
+
docs: {
|
|
561
|
+
description: "Require Zod (or similar) schema validation on `response.json()` before property access."
|
|
562
|
+
},
|
|
563
|
+
schema: [],
|
|
564
|
+
messages: {
|
|
565
|
+
unparsedJsonAccess: "Property access on the result of `response.json()` without a schema parse. Pipe through `XSchema.parse(...)` (Zod) before reading fields."
|
|
566
|
+
}
|
|
567
|
+
},
|
|
568
|
+
defaultOptions: [],
|
|
569
|
+
create(context) {
|
|
570
|
+
const unvalidatedVariables = /* @__PURE__ */ new Set();
|
|
571
|
+
const trackInitializer = (declarator) => {
|
|
572
|
+
if (!isJsonCall(declarator.init)) return;
|
|
573
|
+
const declaredVars = context.sourceCode.getDeclaredVariables(declarator);
|
|
574
|
+
const variable = declaredVars[0];
|
|
575
|
+
if (variable !== void 0) {
|
|
576
|
+
unvalidatedVariables.add(variable);
|
|
577
|
+
}
|
|
578
|
+
};
|
|
579
|
+
return {
|
|
580
|
+
VariableDeclarator(node) {
|
|
581
|
+
const scope = context.sourceCode.getScope(node);
|
|
582
|
+
if (node.id.type === AST_NODE_TYPES4.Identifier) {
|
|
583
|
+
trackInitializer(node);
|
|
584
|
+
return;
|
|
585
|
+
}
|
|
586
|
+
if (node.id.type === AST_NODE_TYPES4.ObjectPattern || node.id.type === AST_NODE_TYPES4.ArrayPattern) {
|
|
587
|
+
if (isJsonCall(node.init)) {
|
|
588
|
+
context.report({ node: node.id, messageId: "unparsedJsonAccess" });
|
|
589
|
+
return;
|
|
590
|
+
}
|
|
591
|
+
if (isUnvalidatedVariableRef(node.init, scope, unvalidatedVariables)) {
|
|
592
|
+
context.report({ node: node.id, messageId: "unparsedJsonAccess" });
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
},
|
|
596
|
+
AssignmentExpression(node) {
|
|
597
|
+
const scope = context.sourceCode.getScope(node);
|
|
598
|
+
if (node.left.type === AST_NODE_TYPES4.Identifier) {
|
|
599
|
+
const variable = findVariable(scope, node.left.name);
|
|
600
|
+
if (variable === null) return;
|
|
601
|
+
if (isJsonCall(node.right)) {
|
|
602
|
+
unvalidatedVariables.add(variable);
|
|
603
|
+
} else {
|
|
604
|
+
unvalidatedVariables.delete(variable);
|
|
605
|
+
}
|
|
606
|
+
return;
|
|
607
|
+
}
|
|
608
|
+
if (node.left.type === AST_NODE_TYPES4.ObjectPattern || node.left.type === AST_NODE_TYPES4.ArrayPattern) {
|
|
609
|
+
if (isJsonCall(node.right)) {
|
|
610
|
+
context.report({
|
|
611
|
+
node: node.left,
|
|
612
|
+
messageId: "unparsedJsonAccess"
|
|
613
|
+
});
|
|
614
|
+
return;
|
|
615
|
+
}
|
|
616
|
+
if (isUnvalidatedVariableRef(node.right, scope, unvalidatedVariables)) {
|
|
617
|
+
context.report({
|
|
618
|
+
node: node.left,
|
|
619
|
+
messageId: "unparsedJsonAccess"
|
|
620
|
+
});
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
},
|
|
624
|
+
MemberExpression(node) {
|
|
625
|
+
const scope = context.sourceCode.getScope(node);
|
|
626
|
+
const obj = unwrap(node.object);
|
|
627
|
+
if (isJsonCall(obj)) {
|
|
628
|
+
const parent = node.parent;
|
|
629
|
+
if (parent.type === AST_NODE_TYPES4.CallExpression && parent.callee === node && node.property.type === AST_NODE_TYPES4.Identifier && (node.property.name === "parse" || node.property.name === "safeParse")) {
|
|
630
|
+
return;
|
|
631
|
+
}
|
|
632
|
+
context.report({ node, messageId: "unparsedJsonAccess" });
|
|
633
|
+
return;
|
|
634
|
+
}
|
|
635
|
+
if (obj !== null && obj.type === AST_NODE_TYPES4.Identifier && isUnvalidatedVariableRef(obj, scope, unvalidatedVariables)) {
|
|
636
|
+
context.report({ node, messageId: "unparsedJsonAccess" });
|
|
637
|
+
const variable = findVariable(scope, obj.name);
|
|
638
|
+
if (variable !== null) {
|
|
639
|
+
unvalidatedVariables.delete(variable);
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
};
|
|
644
|
+
}
|
|
645
|
+
});
|
|
646
|
+
|
|
647
|
+
// src/rules/prefer-server-actions.ts
|
|
648
|
+
import { ESLintUtils as ESLintUtils7 } from "@typescript-eslint/utils";
|
|
649
|
+
var MUTATION_METHODS = /* @__PURE__ */ new Set(["POST", "PUT", "DELETE", "PATCH"]);
|
|
650
|
+
var AXIOS_MUTATION_METHODS = /* @__PURE__ */ new Set(["post", "put", "delete", "patch"]);
|
|
651
|
+
var SKIP_FILE_REGEX = /(?:\.test\.[jt]sx?$|\.spec\.[jt]sx?$|\/tests?\/|\/__tests__\/|\/scripts?\/|\/app\/api\/.*\/route\.[jt]sx?$|\/pages\/api\/)/;
|
|
652
|
+
function getScope(context, node) {
|
|
653
|
+
return context.sourceCode.getScope(node);
|
|
654
|
+
}
|
|
655
|
+
function resolveNode(node, context) {
|
|
656
|
+
if (!node) return null;
|
|
657
|
+
if (node.type !== "Identifier") return node;
|
|
658
|
+
let scope = getScope(context, node);
|
|
659
|
+
while (scope) {
|
|
660
|
+
const variable = scope.set.get(node.name);
|
|
661
|
+
if (variable && variable.defs.length === 1) {
|
|
662
|
+
const def = variable.defs[0];
|
|
663
|
+
if (def && def.type === "Variable") {
|
|
664
|
+
const declarator = def.node;
|
|
665
|
+
if (declarator.type === "VariableDeclarator" && declarator.init) {
|
|
666
|
+
return declarator.init;
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
scope = scope.upper;
|
|
671
|
+
}
|
|
672
|
+
return node;
|
|
673
|
+
}
|
|
674
|
+
function isApiUrl(node, context) {
|
|
675
|
+
const resolved = resolveNode(node, context);
|
|
676
|
+
if (!resolved) return false;
|
|
677
|
+
if (resolved.type === "Literal" && typeof resolved.value === "string") {
|
|
678
|
+
return resolved.value.startsWith("/api/");
|
|
679
|
+
}
|
|
680
|
+
if (resolved.type === "TemplateLiteral") {
|
|
681
|
+
const firstQuasi = resolved.quasis[0];
|
|
682
|
+
const cooked = firstQuasi?.value.cooked;
|
|
683
|
+
return typeof cooked === "string" && cooked.startsWith("/api/");
|
|
684
|
+
}
|
|
685
|
+
if (resolved.type === "BinaryExpression" && resolved.operator === "+") {
|
|
686
|
+
return isApiUrl(resolved.left, context);
|
|
687
|
+
}
|
|
688
|
+
return false;
|
|
689
|
+
}
|
|
690
|
+
function isMutationMethod(node, context) {
|
|
691
|
+
const resolved = resolveNode(node, context);
|
|
692
|
+
if (!resolved) return false;
|
|
693
|
+
if (resolved.type === "Literal" && typeof resolved.value === "string") {
|
|
694
|
+
return MUTATION_METHODS.has(resolved.value.toUpperCase());
|
|
695
|
+
}
|
|
696
|
+
if (resolved.type === "TemplateLiteral" && resolved.expressions.length === 0) {
|
|
697
|
+
const val = resolved.quasis.map((q) => q.value.cooked).join("");
|
|
698
|
+
return MUTATION_METHODS.has(val.toUpperCase());
|
|
699
|
+
}
|
|
700
|
+
if (resolved.type === "ConditionalExpression") {
|
|
701
|
+
return isMutationMethod(resolved.consequent, context) || isMutationMethod(resolved.alternate, context);
|
|
702
|
+
}
|
|
703
|
+
if (resolved.type === "LogicalExpression" && resolved.operator === "||") {
|
|
704
|
+
return isMutationMethod(resolved.left, context) || isMutationMethod(resolved.right, context);
|
|
705
|
+
}
|
|
706
|
+
return false;
|
|
707
|
+
}
|
|
708
|
+
function getPropertyNode(objNode, propName) {
|
|
709
|
+
if (!objNode || objNode.type !== "ObjectExpression") return null;
|
|
710
|
+
for (const prop of objNode.properties) {
|
|
711
|
+
if (prop.type !== "Property") continue;
|
|
712
|
+
let keyName = null;
|
|
713
|
+
if (prop.key.type === "Identifier" && !prop.computed) {
|
|
714
|
+
keyName = prop.key.name;
|
|
715
|
+
} else if (prop.key.type === "Literal" && typeof prop.key.value === "string") {
|
|
716
|
+
keyName = prop.key.value;
|
|
717
|
+
}
|
|
718
|
+
if (keyName === propName) {
|
|
719
|
+
if (prop.value.type === "AssignmentPattern" || prop.value.type === "ArrayPattern" || prop.value.type === "ObjectPattern") {
|
|
720
|
+
return null;
|
|
721
|
+
}
|
|
722
|
+
return prop.value;
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
return null;
|
|
726
|
+
}
|
|
727
|
+
var prefer_server_actions_default = ESLintUtils7.RuleCreator(
|
|
728
|
+
(name) => `https://github.com/sarj-ai/linting/blob/main/packages/typescript/src/rules/${name}.ts`
|
|
729
|
+
)({
|
|
730
|
+
name: "prefer-server-actions",
|
|
731
|
+
meta: {
|
|
732
|
+
type: "suggestion",
|
|
733
|
+
docs: {
|
|
734
|
+
description: "Prefer Next.js Server Actions over /api/* mutations."
|
|
735
|
+
},
|
|
736
|
+
schema: [],
|
|
737
|
+
messages: {
|
|
738
|
+
preferServerAction: "Mutation against /api/* \u2014 prefer a Next.js Server Action for type-safety and to avoid the JSON round-trip."
|
|
739
|
+
}
|
|
740
|
+
},
|
|
741
|
+
defaultOptions: [],
|
|
742
|
+
create(context) {
|
|
743
|
+
const filename = context.filename;
|
|
744
|
+
if (SKIP_FILE_REGEX.test(filename)) {
|
|
745
|
+
return {};
|
|
746
|
+
}
|
|
747
|
+
return {
|
|
748
|
+
CallExpression(node) {
|
|
749
|
+
let isMutation = false;
|
|
750
|
+
if (node.callee.type === "Identifier" && node.callee.name === "fetch") {
|
|
751
|
+
const urlArg = node.arguments[0];
|
|
752
|
+
if (urlArg && urlArg.type !== "SpreadElement" && isApiUrl(urlArg, context)) {
|
|
753
|
+
const initArg = node.arguments[1];
|
|
754
|
+
if (initArg && initArg.type !== "SpreadElement") {
|
|
755
|
+
const resolvedInit = resolveNode(initArg, context);
|
|
756
|
+
const methodNode = getPropertyNode(resolvedInit, "method");
|
|
757
|
+
if (methodNode && isMutationMethod(methodNode, context)) {
|
|
758
|
+
isMutation = true;
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
} else if (node.callee.type === "MemberExpression" && node.callee.property.type === "Identifier" && !node.callee.computed) {
|
|
763
|
+
const methodName = node.callee.property.name.toLowerCase();
|
|
764
|
+
if (AXIOS_MUTATION_METHODS.has(methodName)) {
|
|
765
|
+
const urlArg = node.arguments[0];
|
|
766
|
+
if (urlArg && urlArg.type !== "SpreadElement" && isApiUrl(urlArg, context)) {
|
|
767
|
+
isMutation = true;
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
} else if (node.callee.type === "Identifier" && (node.callee.name === "axios" || node.callee.name === "request")) {
|
|
771
|
+
const firstArg = node.arguments[0];
|
|
772
|
+
if (firstArg && firstArg.type !== "SpreadElement") {
|
|
773
|
+
const configArg = resolveNode(firstArg, context);
|
|
774
|
+
if (configArg && configArg.type === "ObjectExpression") {
|
|
775
|
+
const urlNode = getPropertyNode(configArg, "url");
|
|
776
|
+
const methodNode = getPropertyNode(configArg, "method");
|
|
777
|
+
if (urlNode && isApiUrl(urlNode, context) && methodNode && isMutationMethod(methodNode, context)) {
|
|
778
|
+
isMutation = true;
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
if (isMutation) {
|
|
784
|
+
context.report({ node, messageId: "preferServerAction" });
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
};
|
|
788
|
+
}
|
|
789
|
+
});
|
|
790
|
+
|
|
791
|
+
// src/rules/prefer-shadcn.ts
|
|
792
|
+
import { ESLintUtils as ESLintUtils8 } from "@typescript-eslint/utils";
|
|
793
|
+
var REPLACEMENTS = {
|
|
794
|
+
input: "Input",
|
|
795
|
+
select: "Select",
|
|
796
|
+
textarea: "Textarea",
|
|
797
|
+
dialog: "Dialog"
|
|
798
|
+
};
|
|
799
|
+
var prefer_shadcn_default = ESLintUtils8.RuleCreator(
|
|
800
|
+
(name) => `https://github.com/sarj-ai/linting/blob/main/packages/typescript/src/rules/${name}.ts`
|
|
801
|
+
)({
|
|
802
|
+
name: "prefer-shadcn",
|
|
803
|
+
meta: {
|
|
804
|
+
type: "suggestion",
|
|
805
|
+
docs: {
|
|
806
|
+
description: "Prefer shadcn/ui form primitives over native `<input>`, `<select>`, `<textarea>`, and `<dialog>` elements."
|
|
807
|
+
},
|
|
808
|
+
schema: [],
|
|
809
|
+
messages: {
|
|
810
|
+
preferShadcn: "Use the shadcn <{{replacement}}> component from @/components/ui/{{lowercase}} instead of native <{{element}}>."
|
|
811
|
+
}
|
|
812
|
+
},
|
|
813
|
+
defaultOptions: [],
|
|
814
|
+
create(context) {
|
|
815
|
+
return {
|
|
816
|
+
JSXOpeningElement(node) {
|
|
817
|
+
if (node.name.type !== "JSXIdentifier") {
|
|
818
|
+
return;
|
|
819
|
+
}
|
|
820
|
+
const elementName = node.name.name;
|
|
821
|
+
const replacement = REPLACEMENTS[elementName];
|
|
822
|
+
if (replacement === void 0) {
|
|
823
|
+
return;
|
|
824
|
+
}
|
|
825
|
+
context.report({
|
|
826
|
+
node,
|
|
827
|
+
messageId: "preferShadcn",
|
|
828
|
+
data: {
|
|
829
|
+
element: elementName,
|
|
830
|
+
replacement,
|
|
831
|
+
lowercase: elementName
|
|
832
|
+
}
|
|
833
|
+
});
|
|
834
|
+
}
|
|
835
|
+
};
|
|
836
|
+
}
|
|
837
|
+
});
|
|
838
|
+
|
|
839
|
+
// src/rules/require-assert-never.ts
|
|
840
|
+
import { ESLintUtils as ESLintUtils9, AST_NODE_TYPES as AST_NODE_TYPES5 } from "@typescript-eslint/utils";
|
|
841
|
+
var isAssertNeverCall = (expression) => {
|
|
842
|
+
if (expression.type !== AST_NODE_TYPES5.CallExpression) return false;
|
|
843
|
+
const callee = expression.callee;
|
|
844
|
+
return callee.type === AST_NODE_TYPES5.Identifier && callee.name === "assertNever";
|
|
845
|
+
};
|
|
846
|
+
var statementContainsAssertNever = (statement) => {
|
|
847
|
+
if (statement.type === AST_NODE_TYPES5.ExpressionStatement) {
|
|
848
|
+
return isAssertNeverCall(statement.expression);
|
|
849
|
+
}
|
|
850
|
+
if (statement.type === AST_NODE_TYPES5.ThrowStatement) {
|
|
851
|
+
return isAssertNeverCall(statement.argument);
|
|
852
|
+
}
|
|
853
|
+
if (statement.type === AST_NODE_TYPES5.BlockStatement) {
|
|
854
|
+
return statement.body.some(statementContainsAssertNever);
|
|
855
|
+
}
|
|
856
|
+
return false;
|
|
857
|
+
};
|
|
858
|
+
var require_assert_never_default = ESLintUtils9.RuleCreator(
|
|
859
|
+
(name) => `https://github.com/sarj-ai/linting/blob/main/packages/typescript/src/rules/${name}.ts`
|
|
860
|
+
)({
|
|
861
|
+
name: "require-assert-never",
|
|
862
|
+
meta: {
|
|
863
|
+
type: "problem",
|
|
864
|
+
docs: {
|
|
865
|
+
description: "Require switch statements to end with `assertNever(_)` in their default case so that discriminated unions are exhaustively checked at compile time."
|
|
866
|
+
},
|
|
867
|
+
schema: [],
|
|
868
|
+
messages: {
|
|
869
|
+
missingAssertNever: "Switch statement default case must call assertNever() for exhaustive type checking"
|
|
870
|
+
}
|
|
871
|
+
},
|
|
872
|
+
defaultOptions: [],
|
|
873
|
+
create(context) {
|
|
874
|
+
return {
|
|
875
|
+
SwitchStatement(node) {
|
|
876
|
+
const defaultCase = node.cases.find(
|
|
877
|
+
(caseNode) => caseNode.test === null
|
|
878
|
+
);
|
|
879
|
+
if (!defaultCase) return;
|
|
880
|
+
const hasAssertNever = defaultCase.consequent.some(
|
|
881
|
+
statementContainsAssertNever
|
|
882
|
+
);
|
|
883
|
+
if (hasAssertNever) return;
|
|
884
|
+
context.report({
|
|
885
|
+
node: defaultCase,
|
|
886
|
+
messageId: "missingAssertNever"
|
|
887
|
+
});
|
|
888
|
+
}
|
|
889
|
+
};
|
|
890
|
+
}
|
|
891
|
+
});
|
|
892
|
+
|
|
893
|
+
// src/rules/require-zod-form-validation.ts
|
|
894
|
+
import { ESLintUtils as ESLintUtils10, AST_NODE_TYPES as AST_NODE_TYPES6 } from "@typescript-eslint/utils";
|
|
895
|
+
var isFormDataGetCall = (node) => {
|
|
896
|
+
const callee = node.callee;
|
|
897
|
+
if (callee.type !== AST_NODE_TYPES6.MemberExpression) return false;
|
|
898
|
+
if (callee.property.type !== AST_NODE_TYPES6.Identifier || callee.property.name !== "get") {
|
|
899
|
+
return false;
|
|
900
|
+
}
|
|
901
|
+
return callee.object.type === AST_NODE_TYPES6.Identifier && callee.object.name === "formData";
|
|
902
|
+
};
|
|
903
|
+
var isParseCallExpression = (node) => {
|
|
904
|
+
if (node.type !== AST_NODE_TYPES6.CallExpression) return false;
|
|
905
|
+
const callee = node.callee;
|
|
906
|
+
if (callee.type !== AST_NODE_TYPES6.MemberExpression) return false;
|
|
907
|
+
return callee.property.type === AST_NODE_TYPES6.Identifier && callee.property.name === "parse";
|
|
908
|
+
};
|
|
909
|
+
var require_zod_form_validation_default = ESLintUtils10.RuleCreator(
|
|
910
|
+
(name) => `https://github.com/sarj-ai/linting/blob/main/packages/typescript/src/rules/${name}.ts`
|
|
911
|
+
)({
|
|
912
|
+
name: "require-zod-form-validation",
|
|
913
|
+
meta: {
|
|
914
|
+
type: "problem",
|
|
915
|
+
docs: {
|
|
916
|
+
description: "Require Zod validation (`Schema.parse(...)`) when reading values out of a `FormData` object."
|
|
917
|
+
},
|
|
918
|
+
schema: [],
|
|
919
|
+
messages: {
|
|
920
|
+
missingZodValidation: "FormData parsing must use Zod schema validation (e.g., Schema.parse())"
|
|
921
|
+
}
|
|
922
|
+
},
|
|
923
|
+
defaultOptions: [],
|
|
924
|
+
create(context) {
|
|
925
|
+
return {
|
|
926
|
+
CallExpression(node) {
|
|
927
|
+
if (!isFormDataGetCall(node)) return;
|
|
928
|
+
let parent = node.parent;
|
|
929
|
+
while (parent !== null && parent !== void 0) {
|
|
930
|
+
if (isParseCallExpression(parent)) return;
|
|
931
|
+
parent = parent.parent;
|
|
932
|
+
}
|
|
933
|
+
context.report({
|
|
934
|
+
node,
|
|
935
|
+
messageId: "missingZodValidation"
|
|
936
|
+
});
|
|
937
|
+
}
|
|
938
|
+
};
|
|
939
|
+
}
|
|
940
|
+
});
|
|
941
|
+
|
|
942
|
+
// src/rules/zod-naming-convention.ts
|
|
943
|
+
import { ESLintUtils as ESLintUtils11, AST_NODE_TYPES as AST_NODE_TYPES7 } from "@typescript-eslint/utils";
|
|
944
|
+
var calleeChainStartsWithZ = (node) => {
|
|
945
|
+
let current = node;
|
|
946
|
+
while (current.type === AST_NODE_TYPES7.MemberExpression) {
|
|
947
|
+
const receiver = current.object;
|
|
948
|
+
if (receiver.type === AST_NODE_TYPES7.Identifier && receiver.name === "z") {
|
|
949
|
+
return true;
|
|
950
|
+
}
|
|
951
|
+
if (receiver.type === AST_NODE_TYPES7.CallExpression) {
|
|
952
|
+
current = receiver.callee;
|
|
953
|
+
continue;
|
|
954
|
+
}
|
|
955
|
+
return false;
|
|
956
|
+
}
|
|
957
|
+
return false;
|
|
958
|
+
};
|
|
959
|
+
var zod_naming_convention_default = ESLintUtils11.RuleCreator(
|
|
960
|
+
(name) => `https://github.com/sarj-ai/linting/blob/main/packages/typescript/src/rules/${name}.ts`
|
|
961
|
+
)({
|
|
962
|
+
name: "zod-naming-convention",
|
|
963
|
+
meta: {
|
|
964
|
+
type: "suggestion",
|
|
965
|
+
docs: {
|
|
966
|
+
description: "Enforce Zod schemas to be named with a Z prefix (e.g. `ZUser = z.object({...})`)."
|
|
967
|
+
},
|
|
968
|
+
schema: [],
|
|
969
|
+
messages: {
|
|
970
|
+
zPrefix: "Zod schema names should start with Z"
|
|
971
|
+
}
|
|
972
|
+
},
|
|
973
|
+
defaultOptions: [],
|
|
974
|
+
create(context) {
|
|
975
|
+
return {
|
|
976
|
+
VariableDeclarator(node) {
|
|
977
|
+
const init = node.init;
|
|
978
|
+
if (init === null || init === void 0) return;
|
|
979
|
+
if (init.type !== AST_NODE_TYPES7.CallExpression) return;
|
|
980
|
+
const callee = init.callee;
|
|
981
|
+
if (callee.type !== AST_NODE_TYPES7.MemberExpression) return;
|
|
982
|
+
if (!calleeChainStartsWithZ(callee)) return;
|
|
983
|
+
if (node.id.type !== AST_NODE_TYPES7.Identifier) return;
|
|
984
|
+
const variableName = node.id.name;
|
|
985
|
+
if (variableName.startsWith("Z")) return;
|
|
986
|
+
context.report({
|
|
987
|
+
node: node.id,
|
|
988
|
+
messageId: "zPrefix"
|
|
989
|
+
});
|
|
990
|
+
}
|
|
991
|
+
};
|
|
992
|
+
}
|
|
993
|
+
});
|
|
994
|
+
|
|
995
|
+
// src/index.ts
|
|
996
|
+
var rules = {
|
|
997
|
+
"enforce-file-structure": enforce_file_structure_default,
|
|
998
|
+
"no-client-side-data-fetching": no_client_side_data_fetching_default,
|
|
999
|
+
"no-enum": no_enum_default,
|
|
1000
|
+
"no-raw-env": no_raw_env_default,
|
|
1001
|
+
"no-unnecessary-use-client": no_unnecessary_use_client_default,
|
|
1002
|
+
"prefer-schema-for-api-payload": prefer_schema_for_api_payload_default,
|
|
1003
|
+
"prefer-server-actions": prefer_server_actions_default,
|
|
1004
|
+
"prefer-shadcn": prefer_shadcn_default,
|
|
1005
|
+
"require-assert-never": require_assert_never_default,
|
|
1006
|
+
"require-zod-form-validation": require_zod_form_validation_default,
|
|
1007
|
+
"zod-naming-convention": zod_naming_convention_default
|
|
1008
|
+
};
|
|
1009
|
+
var plugin = {
|
|
1010
|
+
meta: {
|
|
1011
|
+
name: "@sarj/eslint-plugin",
|
|
1012
|
+
version: "2.0.0"
|
|
1013
|
+
},
|
|
1014
|
+
rules,
|
|
1015
|
+
configs: {
|
|
1016
|
+
recommended: {
|
|
1017
|
+
plugins: ["@sarj"],
|
|
1018
|
+
rules: {
|
|
1019
|
+
"@sarj/zod-naming-convention": "warn",
|
|
1020
|
+
"@sarj/require-assert-never": "error",
|
|
1021
|
+
"@sarj/require-zod-form-validation": "error",
|
|
1022
|
+
"@sarj/enforce-file-structure": "warn",
|
|
1023
|
+
"@sarj/no-client-side-data-fetching": "warn",
|
|
1024
|
+
"@sarj/prefer-server-actions": "warn",
|
|
1025
|
+
"@sarj/no-unnecessary-use-client": "warn",
|
|
1026
|
+
"@sarj/prefer-schema-for-api-payload": "warn"
|
|
1027
|
+
}
|
|
1028
|
+
},
|
|
1029
|
+
strict: {
|
|
1030
|
+
plugins: ["@sarj"],
|
|
1031
|
+
rules: {
|
|
1032
|
+
"@sarj/zod-naming-convention": "error",
|
|
1033
|
+
"@sarj/require-assert-never": "error",
|
|
1034
|
+
"@sarj/require-zod-form-validation": "error",
|
|
1035
|
+
"@sarj/enforce-file-structure": "error",
|
|
1036
|
+
"@sarj/no-raw-env": "error",
|
|
1037
|
+
"@sarj/prefer-shadcn": "error",
|
|
1038
|
+
"@sarj/no-enum": "error",
|
|
1039
|
+
"@sarj/no-client-side-data-fetching": "error",
|
|
1040
|
+
"@sarj/prefer-server-actions": "error",
|
|
1041
|
+
"@sarj/no-unnecessary-use-client": "error",
|
|
1042
|
+
"@sarj/prefer-schema-for-api-payload": "error"
|
|
1043
|
+
}
|
|
1044
|
+
}
|
|
1045
|
+
}
|
|
1046
|
+
};
|
|
1047
|
+
var index_default = plugin;
|
|
1048
|
+
export {
|
|
1049
|
+
index_default as default,
|
|
1050
|
+
rules
|
|
97
1051
|
};
|
|
98
|
-
|
|
1052
|
+
//# sourceMappingURL=index.js.map
|