@yasainet/eslint 0.0.62 → 0.0.64
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
|
@@ -137,6 +137,19 @@ const ROUTE_BOUNDARY_PATTERNS = [
|
|
|
137
137
|
},
|
|
138
138
|
];
|
|
139
139
|
|
|
140
|
+
const SITEMAP_BOUNDARY_PATTERNS = [
|
|
141
|
+
{
|
|
142
|
+
group: ["**/queries/*", "**/queries"],
|
|
143
|
+
message:
|
|
144
|
+
"sitemap.ts can only import entries, not queries (sitemap-boundary violation)",
|
|
145
|
+
},
|
|
146
|
+
{
|
|
147
|
+
group: ["**/services/*", "**/services"],
|
|
148
|
+
message:
|
|
149
|
+
"sitemap.ts can only import entries, not services (sitemap-boundary violation)",
|
|
150
|
+
},
|
|
151
|
+
];
|
|
152
|
+
|
|
140
153
|
const HOOKS_BOUNDARY_PATTERNS = [
|
|
141
154
|
{
|
|
142
155
|
group: ["**/queries/*", "**/queries"],
|
|
@@ -220,6 +233,26 @@ export const routeBoundaryConfigs = [
|
|
|
220
233
|
},
|
|
221
234
|
];
|
|
222
235
|
|
|
236
|
+
/** Next.js-only: restrict sitemap.ts to only import entries. */
|
|
237
|
+
export const sitemapBoundaryConfigs = [
|
|
238
|
+
{
|
|
239
|
+
name: "imports/sitemap-boundary",
|
|
240
|
+
files: ["src/app/sitemap.ts", "src/app/**/sitemap.ts"],
|
|
241
|
+
rules: {
|
|
242
|
+
"no-restricted-imports": [
|
|
243
|
+
"error",
|
|
244
|
+
{
|
|
245
|
+
patterns: [
|
|
246
|
+
...SITEMAP_BOUNDARY_PATTERNS,
|
|
247
|
+
...LIB_BOUNDARY_PATTERNS,
|
|
248
|
+
...MAPPING_PATTERNS,
|
|
249
|
+
],
|
|
250
|
+
},
|
|
251
|
+
],
|
|
252
|
+
},
|
|
253
|
+
},
|
|
254
|
+
];
|
|
255
|
+
|
|
223
256
|
/** Next.js-only: restrict hooks to only import entries (not queries or services). */
|
|
224
257
|
export const hooksBoundaryConfigs = [
|
|
225
258
|
{
|
|
@@ -271,7 +304,6 @@ export const libBoundaryConfigs = [
|
|
|
271
304
|
ignores: [
|
|
272
305
|
"src/lib/**",
|
|
273
306
|
"src/proxy.ts",
|
|
274
|
-
"src/app/sitemap.ts",
|
|
275
307
|
"src/app/**/route.ts",
|
|
276
308
|
"src/features/**",
|
|
277
309
|
],
|
|
@@ -0,0 +1,470 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Enforce the canonical entry template for `**\/entries/*.ts` exports:
|
|
3
|
+
*
|
|
4
|
+
* - body must be a single try/catch
|
|
5
|
+
* - try first statement: `logger.info(<obj>, "Start <funcName>")`
|
|
6
|
+
* - try success return preceded by: `logger.info(<obj>, "Success <funcName>")`
|
|
7
|
+
* - try failed branch (when present): `logger.error(<obj>, "Failed <funcName>")`
|
|
8
|
+
* followed by a return with the proper error shape
|
|
9
|
+
* - catch param: `error: unknown`
|
|
10
|
+
* - catch first statement: `logger.error(<obj>, "Unexpected error in <funcName>")`
|
|
11
|
+
* - catch return error.message must be the literal "An unexpected error occurred"
|
|
12
|
+
* - every log object must include the `err` key first (when applicable) and
|
|
13
|
+
* propagate all function input parameters as values
|
|
14
|
+
*
|
|
15
|
+
* Why one rule with many messageIds: each invariant is a small rule
|
|
16
|
+
* conceptually, but they share the same structural traversal and access to
|
|
17
|
+
* funcName / inputArgs. Splitting would duplicate the AST walk.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
const CATCH_RETURN_MESSAGE = "An unexpected error occurred";
|
|
21
|
+
|
|
22
|
+
function getInputArgNames(params) {
|
|
23
|
+
const names = [];
|
|
24
|
+
for (const p of params) {
|
|
25
|
+
if (p.type === "Identifier") {
|
|
26
|
+
names.push(p.name);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
return names;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function isLoggerCall(node, level) {
|
|
33
|
+
if (node?.type !== "CallExpression") return false;
|
|
34
|
+
const callee = node.callee;
|
|
35
|
+
return (
|
|
36
|
+
callee.type === "MemberExpression" &&
|
|
37
|
+
callee.object.type === "Identifier" &&
|
|
38
|
+
callee.object.name === "logger" &&
|
|
39
|
+
callee.property.type === "Identifier" &&
|
|
40
|
+
callee.property.name === level
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function getStringLiteralArg(callExpr, index) {
|
|
45
|
+
const arg = callExpr.arguments[index];
|
|
46
|
+
if (arg?.type === "Literal" && typeof arg.value === "string") return arg.value;
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function getObjectLiteralArg(callExpr, index) {
|
|
51
|
+
const arg = callExpr.arguments[index];
|
|
52
|
+
if (arg?.type === "ObjectExpression") return arg;
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function objectContainsValue(objExpr, identifierName) {
|
|
57
|
+
if (!objExpr) return false;
|
|
58
|
+
for (const prop of objExpr.properties) {
|
|
59
|
+
if (prop.type !== "Property") continue;
|
|
60
|
+
if (
|
|
61
|
+
prop.shorthand &&
|
|
62
|
+
prop.key.type === "Identifier" &&
|
|
63
|
+
prop.key.name === identifierName
|
|
64
|
+
) {
|
|
65
|
+
return true;
|
|
66
|
+
}
|
|
67
|
+
if (prop.value.type === "Identifier" && prop.value.name === identifierName) {
|
|
68
|
+
return true;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return false;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function firstPropertyIsErrKey(objExpr, errorIdentifierName) {
|
|
75
|
+
if (!objExpr || objExpr.properties.length === 0) return false;
|
|
76
|
+
const first = objExpr.properties[0];
|
|
77
|
+
if (first.type !== "Property") return false;
|
|
78
|
+
if (first.key.type !== "Identifier" || first.key.name !== "err") return false;
|
|
79
|
+
// value must be the catch error identifier (or any Identifier — we accept both
|
|
80
|
+
// `err: error` and `err: result.error` since Failed Pattern C uses MemberExpression)
|
|
81
|
+
if (first.value.type === "Identifier") {
|
|
82
|
+
return errorIdentifierName ? first.value.name === errorIdentifierName : true;
|
|
83
|
+
}
|
|
84
|
+
if (first.value.type === "MemberExpression") return true;
|
|
85
|
+
return false;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function isExpressionStatementWithLoggerCall(stmt, level) {
|
|
89
|
+
if (stmt?.type !== "ExpressionStatement") return false;
|
|
90
|
+
return isLoggerCall(stmt.expression, level);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function reportMissingInputArgs(context, objExpr, inputArgNames, ctx) {
|
|
94
|
+
for (const name of inputArgNames) {
|
|
95
|
+
if (!objectContainsValue(objExpr, name)) {
|
|
96
|
+
context.report({
|
|
97
|
+
node: objExpr ?? ctx.fallbackNode,
|
|
98
|
+
messageId: "logMissingInputArg",
|
|
99
|
+
data: { argName: name, where: ctx.where, funcName: ctx.funcName },
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function checkLogCall({
|
|
106
|
+
context,
|
|
107
|
+
callExpr,
|
|
108
|
+
expectedLevel,
|
|
109
|
+
expectedMessage,
|
|
110
|
+
funcName,
|
|
111
|
+
inputArgNames,
|
|
112
|
+
requireErrFirst,
|
|
113
|
+
errorIdentifierName,
|
|
114
|
+
where,
|
|
115
|
+
}) {
|
|
116
|
+
if (!isLoggerCall(callExpr, expectedLevel)) {
|
|
117
|
+
context.report({
|
|
118
|
+
node: callExpr,
|
|
119
|
+
messageId: "logWrongCallShape",
|
|
120
|
+
data: { where, expectedLevel, funcName, expectedMessage },
|
|
121
|
+
});
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
const msg = getStringLiteralArg(callExpr, 1);
|
|
125
|
+
if (msg !== expectedMessage) {
|
|
126
|
+
context.report({
|
|
127
|
+
node: callExpr,
|
|
128
|
+
messageId: "logWrongMessage",
|
|
129
|
+
data: { where, expectedMessage, actual: msg ?? "<not-a-literal>" },
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
const obj = getObjectLiteralArg(callExpr, 0);
|
|
133
|
+
if (!obj) {
|
|
134
|
+
context.report({
|
|
135
|
+
node: callExpr,
|
|
136
|
+
messageId: "logFirstArgNotObject",
|
|
137
|
+
data: { where, funcName },
|
|
138
|
+
});
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
if (requireErrFirst && !firstPropertyIsErrKey(obj, errorIdentifierName)) {
|
|
142
|
+
context.report({
|
|
143
|
+
node: obj,
|
|
144
|
+
messageId: "logErrKeyNotFirst",
|
|
145
|
+
data: { where, funcName },
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
reportMissingInputArgs(context, obj, inputArgNames, {
|
|
149
|
+
where,
|
|
150
|
+
funcName,
|
|
151
|
+
fallbackNode: callExpr,
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function isReturnDataErrorNull(ret) {
|
|
156
|
+
// `return { data: ..., error: null }` (data shorthand or explicit)
|
|
157
|
+
const arg = ret.argument;
|
|
158
|
+
if (arg?.type !== "ObjectExpression") return false;
|
|
159
|
+
let hasData = false;
|
|
160
|
+
let hasErrorNull = false;
|
|
161
|
+
for (const prop of arg.properties) {
|
|
162
|
+
if (prop.type !== "Property") continue;
|
|
163
|
+
if (prop.key.type !== "Identifier") continue;
|
|
164
|
+
if (prop.key.name === "data") hasData = true;
|
|
165
|
+
if (
|
|
166
|
+
prop.key.name === "error" &&
|
|
167
|
+
prop.value.type === "Literal" &&
|
|
168
|
+
prop.value.value === null
|
|
169
|
+
) {
|
|
170
|
+
hasErrorNull = true;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
return hasData && hasErrorNull;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function getReturnErrorMessageLiteral(ret) {
|
|
177
|
+
const arg = ret.argument;
|
|
178
|
+
if (arg?.type !== "ObjectExpression") return null;
|
|
179
|
+
for (const prop of arg.properties) {
|
|
180
|
+
if (prop.type !== "Property") continue;
|
|
181
|
+
if (prop.key.type !== "Identifier" || prop.key.name !== "error") continue;
|
|
182
|
+
if (prop.value.type !== "ObjectExpression") return null;
|
|
183
|
+
for (const inner of prop.value.properties) {
|
|
184
|
+
if (inner.type !== "Property") continue;
|
|
185
|
+
if (inner.key.type !== "Identifier" || inner.key.name !== "message") continue;
|
|
186
|
+
if (inner.value.type === "Literal" && typeof inner.value.value === "string") {
|
|
187
|
+
return inner.value.value;
|
|
188
|
+
}
|
|
189
|
+
return "<non-literal>";
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
return null;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function checkTryBlock(context, tryBlock, funcName, inputArgNames) {
|
|
196
|
+
if (tryBlock.body.length === 0) {
|
|
197
|
+
context.report({ node: tryBlock, messageId: "tryEmpty", data: { funcName } });
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// Start log: first statement
|
|
202
|
+
const first = tryBlock.body[0];
|
|
203
|
+
if (!isExpressionStatementWithLoggerCall(first, "info")) {
|
|
204
|
+
context.report({
|
|
205
|
+
node: first,
|
|
206
|
+
messageId: "tryMissingStartLog",
|
|
207
|
+
data: { funcName },
|
|
208
|
+
});
|
|
209
|
+
} else {
|
|
210
|
+
checkLogCall({
|
|
211
|
+
context,
|
|
212
|
+
callExpr: first.expression,
|
|
213
|
+
expectedLevel: "info",
|
|
214
|
+
expectedMessage: `Start ${funcName}`,
|
|
215
|
+
funcName,
|
|
216
|
+
inputArgNames,
|
|
217
|
+
requireErrFirst: false,
|
|
218
|
+
errorIdentifierName: null,
|
|
219
|
+
where: "Start",
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// Walk body to find Success returns and Failed branches
|
|
224
|
+
let successFound = false;
|
|
225
|
+
for (let i = 0; i < tryBlock.body.length; i++) {
|
|
226
|
+
const stmt = tryBlock.body[i];
|
|
227
|
+
|
|
228
|
+
// Success log + return
|
|
229
|
+
if (stmt.type === "ReturnStatement" && isReturnDataErrorNull(stmt)) {
|
|
230
|
+
successFound = true;
|
|
231
|
+
// The previous non-ExpressionStatement non-IfStatement statement should be
|
|
232
|
+
// the Success log. Walk back to find it.
|
|
233
|
+
const prev = findPrecedingLoggerCall(tryBlock.body, i);
|
|
234
|
+
if (
|
|
235
|
+
!prev ||
|
|
236
|
+
!isLoggerCall(prev, "info") ||
|
|
237
|
+
getStringLiteralArg(prev, 1) !== `Success ${funcName}`
|
|
238
|
+
) {
|
|
239
|
+
context.report({
|
|
240
|
+
node: stmt,
|
|
241
|
+
messageId: "trySuccessLogMissing",
|
|
242
|
+
data: { funcName },
|
|
243
|
+
});
|
|
244
|
+
} else {
|
|
245
|
+
checkLogCall({
|
|
246
|
+
context,
|
|
247
|
+
callExpr: prev,
|
|
248
|
+
expectedLevel: "info",
|
|
249
|
+
expectedMessage: `Success ${funcName}`,
|
|
250
|
+
funcName,
|
|
251
|
+
inputArgNames,
|
|
252
|
+
requireErrFirst: false,
|
|
253
|
+
errorIdentifierName: null,
|
|
254
|
+
where: "Success",
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// Failed branches inside if statements
|
|
260
|
+
if (stmt.type === "IfStatement") {
|
|
261
|
+
checkFailedBranch(context, stmt, funcName, inputArgNames);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
if (!successFound) {
|
|
266
|
+
context.report({
|
|
267
|
+
node: tryBlock,
|
|
268
|
+
messageId: "trySuccessReturnMissing",
|
|
269
|
+
data: { funcName },
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
function findPrecedingLoggerCall(body, returnIndex) {
|
|
275
|
+
for (let j = returnIndex - 1; j >= 0; j--) {
|
|
276
|
+
const s = body[j];
|
|
277
|
+
if (s.type === "IfStatement") continue;
|
|
278
|
+
if (
|
|
279
|
+
s.type === "ExpressionStatement" &&
|
|
280
|
+
s.expression.type === "AwaitExpression"
|
|
281
|
+
) {
|
|
282
|
+
// `await revalidatePath(...)` etc — keep walking
|
|
283
|
+
continue;
|
|
284
|
+
}
|
|
285
|
+
if (s.type === "ExpressionStatement") {
|
|
286
|
+
return s.expression;
|
|
287
|
+
}
|
|
288
|
+
if (s.type === "VariableDeclaration") continue;
|
|
289
|
+
return null;
|
|
290
|
+
}
|
|
291
|
+
return null;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
function checkFailedBranch(context, ifStmt, funcName, inputArgNames) {
|
|
295
|
+
// We only validate IFs that look like Failed branches: contain a return whose
|
|
296
|
+
// error.message is a string literal (not the catch's "An unexpected ..." literal).
|
|
297
|
+
const consequent = ifStmt.consequent;
|
|
298
|
+
if (consequent.type !== "BlockStatement") return;
|
|
299
|
+
const ret = consequent.body.find((s) => s.type === "ReturnStatement");
|
|
300
|
+
if (!ret) return;
|
|
301
|
+
const errMsg = getReturnErrorMessageLiteral(ret);
|
|
302
|
+
if (errMsg === null) return; // not a Failed-shaped return; skip
|
|
303
|
+
|
|
304
|
+
// Must have logger.error("Failed <funcName>") preceding the return
|
|
305
|
+
const idx = consequent.body.indexOf(ret);
|
|
306
|
+
let loggerCall = null;
|
|
307
|
+
for (let j = 0; j < idx; j++) {
|
|
308
|
+
const s = consequent.body[j];
|
|
309
|
+
if (
|
|
310
|
+
s.type === "ExpressionStatement" &&
|
|
311
|
+
isLoggerCall(s.expression, "error")
|
|
312
|
+
) {
|
|
313
|
+
loggerCall = s.expression;
|
|
314
|
+
break;
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
if (!loggerCall) {
|
|
318
|
+
context.report({
|
|
319
|
+
node: ret,
|
|
320
|
+
messageId: "failedLogMissing",
|
|
321
|
+
data: { funcName },
|
|
322
|
+
});
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
const isPatternC = errMsg === "<non-literal>"; // .message access
|
|
327
|
+
checkLogCall({
|
|
328
|
+
context,
|
|
329
|
+
callExpr: loggerCall,
|
|
330
|
+
expectedLevel: "error",
|
|
331
|
+
expectedMessage: `Failed ${funcName}`,
|
|
332
|
+
funcName,
|
|
333
|
+
inputArgNames,
|
|
334
|
+
requireErrFirst: isPatternC,
|
|
335
|
+
errorIdentifierName: null,
|
|
336
|
+
where: "Failed",
|
|
337
|
+
});
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
function checkCatchClause(context, handler, funcName, inputArgNames) {
|
|
341
|
+
const param = handler.param;
|
|
342
|
+
if (
|
|
343
|
+
!param ||
|
|
344
|
+
param.type !== "Identifier" ||
|
|
345
|
+
param.name !== "error" ||
|
|
346
|
+
!param.typeAnnotation ||
|
|
347
|
+
param.typeAnnotation.typeAnnotation?.type !== "TSUnknownKeyword"
|
|
348
|
+
) {
|
|
349
|
+
context.report({
|
|
350
|
+
node: param ?? handler,
|
|
351
|
+
messageId: "catchParamWrongType",
|
|
352
|
+
data: { funcName },
|
|
353
|
+
});
|
|
354
|
+
}
|
|
355
|
+
const block = handler.body;
|
|
356
|
+
if (block.body.length === 0) {
|
|
357
|
+
context.report({ node: block, messageId: "catchEmpty", data: { funcName } });
|
|
358
|
+
return;
|
|
359
|
+
}
|
|
360
|
+
const first = block.body[0];
|
|
361
|
+
if (!isExpressionStatementWithLoggerCall(first, "error")) {
|
|
362
|
+
context.report({
|
|
363
|
+
node: first,
|
|
364
|
+
messageId: "catchMissingErrorLog",
|
|
365
|
+
data: { funcName },
|
|
366
|
+
});
|
|
367
|
+
} else {
|
|
368
|
+
checkLogCall({
|
|
369
|
+
context,
|
|
370
|
+
callExpr: first.expression,
|
|
371
|
+
expectedLevel: "error",
|
|
372
|
+
expectedMessage: `Unexpected error in ${funcName}`,
|
|
373
|
+
funcName,
|
|
374
|
+
inputArgNames,
|
|
375
|
+
requireErrFirst: true,
|
|
376
|
+
errorIdentifierName: "error",
|
|
377
|
+
where: "catch",
|
|
378
|
+
});
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
// Last statement must be a return whose error.message is the catch literal
|
|
382
|
+
const last = block.body[block.body.length - 1];
|
|
383
|
+
if (last?.type !== "ReturnStatement") {
|
|
384
|
+
context.report({
|
|
385
|
+
node: last ?? block,
|
|
386
|
+
messageId: "catchLastNotReturn",
|
|
387
|
+
data: { funcName },
|
|
388
|
+
});
|
|
389
|
+
return;
|
|
390
|
+
}
|
|
391
|
+
const msg = getReturnErrorMessageLiteral(last);
|
|
392
|
+
if (msg !== CATCH_RETURN_MESSAGE) {
|
|
393
|
+
context.report({
|
|
394
|
+
node: last,
|
|
395
|
+
messageId: "catchWrongReturnMessage",
|
|
396
|
+
data: { funcName, expected: CATCH_RETURN_MESSAGE, actual: msg ?? "<missing>" },
|
|
397
|
+
});
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
export const entryTemplateRule = {
|
|
402
|
+
meta: {
|
|
403
|
+
type: "problem",
|
|
404
|
+
docs: {
|
|
405
|
+
description:
|
|
406
|
+
"Enforce the canonical try/catch + Start/Failed/Success/Unexpected logger structure for entries.",
|
|
407
|
+
},
|
|
408
|
+
messages: {
|
|
409
|
+
bodyNotTryCatch:
|
|
410
|
+
"entry '{{ funcName }}' must have a single try/catch as its body.",
|
|
411
|
+
tryEmpty: "entry '{{ funcName }}' try block is empty.",
|
|
412
|
+
tryMissingStartLog:
|
|
413
|
+
"entry '{{ funcName }}' try block must start with `logger.info(<obj>, \"Start {{ funcName }}\")`.",
|
|
414
|
+
trySuccessLogMissing:
|
|
415
|
+
"entry '{{ funcName }}' success return must be preceded by `logger.info(<obj>, \"Success {{ funcName }}\")`.",
|
|
416
|
+
trySuccessReturnMissing:
|
|
417
|
+
"entry '{{ funcName }}' must contain a success return `return { data, error: null }`.",
|
|
418
|
+
failedLogMissing:
|
|
419
|
+
"entry '{{ funcName }}' Failed branch must call `logger.error(<obj>, \"Failed {{ funcName }}\")` before return.",
|
|
420
|
+
catchParamWrongType:
|
|
421
|
+
"entry '{{ funcName }}' catch param must be `error: unknown`.",
|
|
422
|
+
catchEmpty: "entry '{{ funcName }}' catch block is empty.",
|
|
423
|
+
catchMissingErrorLog:
|
|
424
|
+
"entry '{{ funcName }}' catch block must start with `logger.error(<obj>, \"Unexpected error in {{ funcName }}\")`.",
|
|
425
|
+
catchLastNotReturn:
|
|
426
|
+
"entry '{{ funcName }}' catch block must end with a return statement.",
|
|
427
|
+
catchWrongReturnMessage:
|
|
428
|
+
"entry '{{ funcName }}' catch return error.message must be the literal '{{ expected }}'. Got: '{{ actual }}'.",
|
|
429
|
+
logWrongCallShape:
|
|
430
|
+
"{{ where }} log in '{{ funcName }}' must be `logger.{{ expectedLevel }}(<obj>, \"{{ expectedMessage }}\")`.",
|
|
431
|
+
logWrongMessage:
|
|
432
|
+
"{{ where }} log message must be '{{ expectedMessage }}'. Got: '{{ actual }}'.",
|
|
433
|
+
logFirstArgNotObject:
|
|
434
|
+
"{{ where }} log in '{{ funcName }}' first argument must be an object literal.",
|
|
435
|
+
logErrKeyNotFirst:
|
|
436
|
+
"{{ where }} log in '{{ funcName }}' object must start with `err:` key.",
|
|
437
|
+
logMissingInputArg:
|
|
438
|
+
"{{ where }} log in '{{ funcName }}' is missing input arg '{{ argName }}'. All function inputs must propagate to log objects.",
|
|
439
|
+
},
|
|
440
|
+
schema: [],
|
|
441
|
+
},
|
|
442
|
+
create(context) {
|
|
443
|
+
return {
|
|
444
|
+
ExportNamedDeclaration(node) {
|
|
445
|
+
if (!node.declaration) return;
|
|
446
|
+
const decl = node.declaration;
|
|
447
|
+
if (decl.type !== "FunctionDeclaration") return;
|
|
448
|
+
if (!decl.async) return;
|
|
449
|
+
if (!decl.id) return;
|
|
450
|
+
const funcName = decl.id.name;
|
|
451
|
+
const inputArgNames = getInputArgNames(decl.params);
|
|
452
|
+
|
|
453
|
+
const body = decl.body.body;
|
|
454
|
+
if (body.length !== 1 || body[0].type !== "TryStatement") {
|
|
455
|
+
context.report({
|
|
456
|
+
node: decl.id,
|
|
457
|
+
messageId: "bodyNotTryCatch",
|
|
458
|
+
data: { funcName },
|
|
459
|
+
});
|
|
460
|
+
return;
|
|
461
|
+
}
|
|
462
|
+
const tryStmt = body[0];
|
|
463
|
+
checkTryBlock(context, tryStmt.block, funcName, inputArgNames);
|
|
464
|
+
if (tryStmt.handler) {
|
|
465
|
+
checkCatchClause(context, tryStmt.handler, funcName, inputArgNames);
|
|
466
|
+
}
|
|
467
|
+
},
|
|
468
|
+
};
|
|
469
|
+
},
|
|
470
|
+
};
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { entryTemplateRule } from "./entry-template.mjs";
|
|
1
2
|
import { featureNameRule } from "./feature-name.mjs";
|
|
2
3
|
import { formStateNamingRule } from "./form-state-naming.mjs";
|
|
3
4
|
import { importPathStyleRule } from "./import-path-style.mjs";
|
|
@@ -13,6 +14,7 @@ import { supabaseSelectTypedColumnsRule } from "./supabase-select-typed-columns.
|
|
|
13
14
|
/** Single plugin object to avoid ESLint "Cannot redefine plugin" errors. */
|
|
14
15
|
export const localPlugin = {
|
|
15
16
|
rules: {
|
|
17
|
+
"entry-template": entryTemplateRule,
|
|
16
18
|
"feature-name": featureNameRule,
|
|
17
19
|
"form-state-naming": formStateNamingRule,
|
|
18
20
|
"import-path-style": importPathStyleRule,
|
package/src/common/naming.mjs
CHANGED
|
@@ -296,6 +296,15 @@ export function createNamingConfigs(featureRoot, prefixLibMapping) {
|
|
|
296
296
|
},
|
|
297
297
|
});
|
|
298
298
|
|
|
299
|
+
configs.push({
|
|
300
|
+
name: "naming/entry-template",
|
|
301
|
+
files: featuresGlob(featureRoot, "**/entries/*.ts"),
|
|
302
|
+
plugins: { local: localPlugin },
|
|
303
|
+
rules: {
|
|
304
|
+
"local/entry-template": "error",
|
|
305
|
+
},
|
|
306
|
+
});
|
|
307
|
+
|
|
299
308
|
configs.push(
|
|
300
309
|
{
|
|
301
310
|
name: "naming/entries-shared",
|
package/src/next/index.mjs
CHANGED
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
libBoundaryConfigs,
|
|
7
7
|
pageBoundaryConfigs,
|
|
8
8
|
routeBoundaryConfigs,
|
|
9
|
+
sitemapBoundaryConfigs,
|
|
9
10
|
} from "../common/imports.mjs";
|
|
10
11
|
|
|
11
12
|
import { directivesConfigs } from "./directives.mjs";
|
|
@@ -29,6 +30,7 @@ export const eslintConfig = [
|
|
|
29
30
|
...libBoundaryConfigs,
|
|
30
31
|
...pageBoundaryConfigs,
|
|
31
32
|
...routeBoundaryConfigs,
|
|
33
|
+
...sitemapBoundaryConfigs,
|
|
32
34
|
...hooksBoundaryConfigs,
|
|
33
35
|
...componentsBoundaryConfigs,
|
|
34
36
|
...namingConfigs,
|