@clerk/eslint-plugin 0.0.1 → 0.1.0
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/LICENSE +21 -0
- package/README.md +263 -0
- package/dist/chunk-C_NdSu1c.js +34 -0
- package/dist/next/fix-auth-protection-cli.d.mts +1 -0
- package/dist/next/fix-auth-protection-cli.d.ts +1 -0
- package/dist/next/fix-auth-protection-cli.js +101 -0
- package/dist/next/fix-auth-protection-cli.js.map +1 -0
- package/dist/next/fix-auth-protection-cli.mjs +100 -0
- package/dist/next/fix-auth-protection-cli.mjs.map +1 -0
- package/dist/next/fix-auth-protection.d.mts +59 -0
- package/dist/next/fix-auth-protection.d.mts.map +1 -0
- package/dist/next/fix-auth-protection.d.ts +59 -0
- package/dist/next/fix-auth-protection.d.ts.map +1 -0
- package/dist/next/fix-auth-protection.js +149 -0
- package/dist/next/fix-auth-protection.js.map +1 -0
- package/dist/next/fix-auth-protection.mjs +148 -0
- package/dist/next/fix-auth-protection.mjs.map +1 -0
- package/dist/next.d.mts +7 -0
- package/dist/next.d.mts.map +1 -0
- package/dist/next.d.ts +6 -0
- package/dist/next.d.ts.map +1 -0
- package/dist/next.js +833 -0
- package/dist/next.js.map +1 -0
- package/dist/next.mjs +831 -0
- package/dist/next.mjs.map +1 -0
- package/package.json +81 -3
package/dist/next.js
ADDED
|
@@ -0,0 +1,833 @@
|
|
|
1
|
+
const require_chunk = require('./chunk-C_NdSu1c.js');
|
|
2
|
+
let node_path = require("node:path");
|
|
3
|
+
node_path = require_chunk.__toESM(node_path);
|
|
4
|
+
let node_fs = require("node:fs");
|
|
5
|
+
|
|
6
|
+
//#region src/next/lib/exports.ts
|
|
7
|
+
function unwrapFunction(node) {
|
|
8
|
+
if (!node) return null;
|
|
9
|
+
if (node.type === "FunctionDeclaration" || node.type === "FunctionExpression" || node.type === "ArrowFunctionExpression") return node;
|
|
10
|
+
return null;
|
|
11
|
+
}
|
|
12
|
+
function resolveLocalIdentifierTarget(programNode, name) {
|
|
13
|
+
for (const stmt of programNode.body) {
|
|
14
|
+
if (stmt.type === "FunctionDeclaration" && stmt.id && stmt.id.name === name) return {
|
|
15
|
+
kind: "function",
|
|
16
|
+
node: stmt
|
|
17
|
+
};
|
|
18
|
+
if (stmt.type === "VariableDeclaration") for (const declarator of stmt.declarations) {
|
|
19
|
+
if (declarator.id.type !== "Identifier" || declarator.id.name !== name) continue;
|
|
20
|
+
const initFn = unwrapFunction(declarator.init ?? void 0);
|
|
21
|
+
if (initFn) return {
|
|
22
|
+
kind: "function",
|
|
23
|
+
node: initFn
|
|
24
|
+
};
|
|
25
|
+
return { kind: "unknown" };
|
|
26
|
+
}
|
|
27
|
+
if (stmt.type === "ImportDeclaration") {
|
|
28
|
+
for (const spec of stmt.specifiers) if (spec.local && spec.local.name === name) return {
|
|
29
|
+
kind: "imported",
|
|
30
|
+
source: stmt.source.value
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return { kind: "unknown" };
|
|
35
|
+
}
|
|
36
|
+
function resolveDefaultExportTarget(programNode, declaration) {
|
|
37
|
+
const direct = unwrapFunction(declaration);
|
|
38
|
+
if (direct) return {
|
|
39
|
+
kind: "function",
|
|
40
|
+
node: direct
|
|
41
|
+
};
|
|
42
|
+
if (declaration.type !== "Identifier") return { kind: "unknown" };
|
|
43
|
+
return resolveLocalIdentifierTarget(programNode, declaration.name);
|
|
44
|
+
}
|
|
45
|
+
function getExportedName(spec) {
|
|
46
|
+
const node = spec.exported;
|
|
47
|
+
if (node.type === "Identifier") return node.name;
|
|
48
|
+
if (node.type === "Literal" && typeof node.value === "string") return node.value;
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Resolve the module's default export, regardless of whether it's declared with
|
|
53
|
+
* `export default ...` or via a specifier such as `export { Page as default }`
|
|
54
|
+
* or `export { default } from './Page'`.
|
|
55
|
+
*/
|
|
56
|
+
function resolveDefaultExport(programNode) {
|
|
57
|
+
for (const stmt of programNode.body) {
|
|
58
|
+
if (stmt.type === "ExportDefaultDeclaration") return {
|
|
59
|
+
target: resolveDefaultExportTarget(programNode, stmt.declaration),
|
|
60
|
+
reportNode: stmt
|
|
61
|
+
};
|
|
62
|
+
if (stmt.type !== "ExportNamedDeclaration") continue;
|
|
63
|
+
if (stmt.exportKind === "type") continue;
|
|
64
|
+
for (const spec of stmt.specifiers) {
|
|
65
|
+
if (spec.type !== "ExportSpecifier") continue;
|
|
66
|
+
if (spec.exportKind === "type") continue;
|
|
67
|
+
if (getExportedName(spec) !== "default") continue;
|
|
68
|
+
if (stmt.source) return {
|
|
69
|
+
target: {
|
|
70
|
+
kind: "imported",
|
|
71
|
+
source: stmt.source.value
|
|
72
|
+
},
|
|
73
|
+
reportNode: stmt
|
|
74
|
+
};
|
|
75
|
+
if (spec.local.type !== "Identifier") return {
|
|
76
|
+
target: { kind: "unknown" },
|
|
77
|
+
reportNode: stmt
|
|
78
|
+
};
|
|
79
|
+
return {
|
|
80
|
+
target: resolveLocalIdentifierTarget(programNode, spec.local.name),
|
|
81
|
+
reportNode: stmt
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Yield value-level `export * from '...'` declarations. The rule cannot follow
|
|
89
|
+
* these across files, so callers treat them conservatively as unverifiable.
|
|
90
|
+
*/
|
|
91
|
+
function* iterateExportAllDeclarations(programNode) {
|
|
92
|
+
for (const stmt of programNode.body) {
|
|
93
|
+
if (stmt.type !== "ExportAllDeclaration") continue;
|
|
94
|
+
if (stmt.exportKind === "type") continue;
|
|
95
|
+
if (stmt.exported) continue;
|
|
96
|
+
yield {
|
|
97
|
+
source: stmt.source.value,
|
|
98
|
+
reportNode: stmt
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
function* iterateNamedExports(programNode) {
|
|
103
|
+
for (const stmt of programNode.body) {
|
|
104
|
+
if (stmt.type !== "ExportNamedDeclaration") continue;
|
|
105
|
+
if (stmt.exportKind === "type") continue;
|
|
106
|
+
if (stmt.declaration) {
|
|
107
|
+
const decl = stmt.declaration;
|
|
108
|
+
if (decl.type === "FunctionDeclaration" && decl.id) yield {
|
|
109
|
+
name: decl.id.name,
|
|
110
|
+
target: {
|
|
111
|
+
kind: "function",
|
|
112
|
+
node: decl
|
|
113
|
+
},
|
|
114
|
+
reportNode: stmt
|
|
115
|
+
};
|
|
116
|
+
else if (decl.type === "VariableDeclaration") for (const declarator of decl.declarations) {
|
|
117
|
+
if (declarator.id.type !== "Identifier") continue;
|
|
118
|
+
const fn = unwrapFunction(declarator.init ?? void 0);
|
|
119
|
+
yield {
|
|
120
|
+
name: declarator.id.name,
|
|
121
|
+
target: fn ? {
|
|
122
|
+
kind: "function",
|
|
123
|
+
node: fn
|
|
124
|
+
} : { kind: "unknown" },
|
|
125
|
+
reportNode: stmt
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
continue;
|
|
129
|
+
}
|
|
130
|
+
for (const spec of stmt.specifiers) {
|
|
131
|
+
if (spec.type !== "ExportSpecifier") continue;
|
|
132
|
+
if (spec.exportKind === "type") continue;
|
|
133
|
+
const exportedName = getExportedName(spec);
|
|
134
|
+
if (!exportedName) continue;
|
|
135
|
+
if (stmt.source) {
|
|
136
|
+
yield {
|
|
137
|
+
name: exportedName,
|
|
138
|
+
target: {
|
|
139
|
+
kind: "imported",
|
|
140
|
+
source: stmt.source.value
|
|
141
|
+
},
|
|
142
|
+
reportNode: stmt
|
|
143
|
+
};
|
|
144
|
+
continue;
|
|
145
|
+
}
|
|
146
|
+
if (spec.local.type !== "Identifier") continue;
|
|
147
|
+
yield {
|
|
148
|
+
name: exportedName,
|
|
149
|
+
target: resolveLocalIdentifierTarget(programNode, spec.local.name),
|
|
150
|
+
reportNode: stmt
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
//#endregion
|
|
157
|
+
//#region src/next/lib/file-info.ts
|
|
158
|
+
/**
|
|
159
|
+
* Utilities for classifying a file by path, kind, and module-level directives.
|
|
160
|
+
*/
|
|
161
|
+
const RESOURCE_FILES = new Set([
|
|
162
|
+
"page",
|
|
163
|
+
"layout",
|
|
164
|
+
"template",
|
|
165
|
+
"default",
|
|
166
|
+
"route"
|
|
167
|
+
]);
|
|
168
|
+
const RESOURCE_EXTENSIONS = /\.(ts|tsx|js|jsx|mjs|cjs)$/;
|
|
169
|
+
/**
|
|
170
|
+
* @param rootDir Project root to relativize against — typically from
|
|
171
|
+
* `resolveProjectRoot()` (explicit option, nearest `eslint.config.*`, or ESLint
|
|
172
|
+
* `cwd`). When omitted or when the file lies outside `rootDir`, the absolute
|
|
173
|
+
* path is scanned for an `app` segment instead.
|
|
174
|
+
*/
|
|
175
|
+
function getRelativeFolder(filename, rootDir) {
|
|
176
|
+
if (!filename) return null;
|
|
177
|
+
const normalizedFile = filename.replaceAll("\\", "/");
|
|
178
|
+
let candidate = normalizedFile;
|
|
179
|
+
if (rootDir) {
|
|
180
|
+
const normalizedRoot = rootDir.replaceAll("\\", "/");
|
|
181
|
+
const rel = node_path.default.posix.relative(normalizedRoot, normalizedFile);
|
|
182
|
+
if (rel && !rel.startsWith("..")) candidate = rel;
|
|
183
|
+
}
|
|
184
|
+
const segments = candidate.split("/");
|
|
185
|
+
const appIdx = segments.findIndex((seg) => seg === "app");
|
|
186
|
+
if (appIdx !== -1) return node_path.default.posix.dirname(segments.slice(appIdx).join("/"));
|
|
187
|
+
if (candidate !== normalizedFile) return node_path.default.posix.dirname(candidate);
|
|
188
|
+
return null;
|
|
189
|
+
}
|
|
190
|
+
function getFileKind(filename) {
|
|
191
|
+
if (!filename) return null;
|
|
192
|
+
const base = node_path.default.basename(filename).replace(RESOURCE_EXTENSIONS, "");
|
|
193
|
+
return RESOURCE_FILES.has(base) ? base : null;
|
|
194
|
+
}
|
|
195
|
+
function hasTopLevelDirective(programNode, name) {
|
|
196
|
+
for (const stmt of programNode.body) {
|
|
197
|
+
if (stmt.type !== "ExpressionStatement") break;
|
|
198
|
+
if (!("directive" in stmt)) break;
|
|
199
|
+
if (stmt.directive === name) return true;
|
|
200
|
+
}
|
|
201
|
+
return false;
|
|
202
|
+
}
|
|
203
|
+
function isServerFunctionModule(programNode) {
|
|
204
|
+
return hasTopLevelDirective(programNode, "use server");
|
|
205
|
+
}
|
|
206
|
+
function isClientModule(programNode) {
|
|
207
|
+
return hasTopLevelDirective(programNode, "use client");
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
//#endregion
|
|
211
|
+
//#region src/next/lib/fixers.ts
|
|
212
|
+
const CLERK_AUTH_SOURCE$1 = "@clerk/nextjs/server";
|
|
213
|
+
/**
|
|
214
|
+
* The local name to call `.protect()` on. Reuses an existing alias (e.g.
|
|
215
|
+
* `import { auth as clerkAuth }`) when present, otherwise defaults to `auth`.
|
|
216
|
+
*/
|
|
217
|
+
function resolveAuthName(authNames) {
|
|
218
|
+
for (const name of authNames) return name;
|
|
219
|
+
return "auth";
|
|
220
|
+
}
|
|
221
|
+
/**
|
|
222
|
+
* Build the ordered, non-overlapping set of edits that protect `fn`. All edits
|
|
223
|
+
* belong to a single suggestion and are applied atomically.
|
|
224
|
+
*/
|
|
225
|
+
function buildAuthProtectFixes({ fixer, sourceCode, fn, authNames }) {
|
|
226
|
+
const program = sourceCode.ast;
|
|
227
|
+
const authName = resolveAuthName(authNames);
|
|
228
|
+
const fixes = [];
|
|
229
|
+
const importFix = ensureAuthImportFix(fixer, sourceCode, program, authNames);
|
|
230
|
+
if (importFix) fixes.push(importFix);
|
|
231
|
+
const asyncFix = ensureAsyncFix(fixer, sourceCode, fn);
|
|
232
|
+
if (asyncFix) fixes.push(asyncFix);
|
|
233
|
+
fixes.push(insertProtectCallFix(fixer, sourceCode, fn, authName, authNames));
|
|
234
|
+
return fixes;
|
|
235
|
+
}
|
|
236
|
+
/**
|
|
237
|
+
* If `node` is `await <auth>()` (a zero-argument call to one of the imported
|
|
238
|
+
* `auth` names), return the callee identifier so it can be rewritten to
|
|
239
|
+
* `<auth>.protect`. Returns `null` otherwise.
|
|
240
|
+
*
|
|
241
|
+
* Used to merge protection into an existing call (`const { userId } = await
|
|
242
|
+
* auth()` -> `const { userId } = await auth.protect()`) instead of prepending a
|
|
243
|
+
* duplicate `await auth.protect();`.
|
|
244
|
+
*/
|
|
245
|
+
function mergeableAuthCallee(node, authNames) {
|
|
246
|
+
if (!node || node.type !== "AwaitExpression") return null;
|
|
247
|
+
const arg = node.argument;
|
|
248
|
+
if (arg.type !== "CallExpression" || arg.arguments.length > 0) return null;
|
|
249
|
+
if (arg.callee.type !== "Identifier" || !authNames.has(arg.callee.name)) return null;
|
|
250
|
+
return arg.callee;
|
|
251
|
+
}
|
|
252
|
+
/**
|
|
253
|
+
* Look for a mergeable `await <auth>()` in the first executable statement of a
|
|
254
|
+
* block body — either a bare `await auth();` or the initializer of the first
|
|
255
|
+
* declarator (`const { userId } = await auth();`).
|
|
256
|
+
*/
|
|
257
|
+
function firstStatementAuthCallee(stmt, authNames) {
|
|
258
|
+
if (stmt.type === "ExpressionStatement") return mergeableAuthCallee(stmt.expression, authNames);
|
|
259
|
+
if (stmt.type === "VariableDeclaration") {
|
|
260
|
+
const first = stmt.declarations[0];
|
|
261
|
+
return first ? mergeableAuthCallee(first.init, authNames) : null;
|
|
262
|
+
}
|
|
263
|
+
return null;
|
|
264
|
+
}
|
|
265
|
+
function getLineIndent(sourceCode, node) {
|
|
266
|
+
const line = sourceCode.lines[node.loc.start.line - 1] ?? "";
|
|
267
|
+
const match = /^\s*/.exec(line);
|
|
268
|
+
return match ? match[0] : "";
|
|
269
|
+
}
|
|
270
|
+
function ensureAsyncFix(fixer, sourceCode, fn) {
|
|
271
|
+
if (fn.async) return null;
|
|
272
|
+
const firstToken = sourceCode.getFirstToken(fn);
|
|
273
|
+
if (!firstToken) return null;
|
|
274
|
+
return fixer.insertTextBefore(firstToken, "async ");
|
|
275
|
+
}
|
|
276
|
+
function insertProtectCallFix(fixer, sourceCode, fn, authName, authNames) {
|
|
277
|
+
const call = `await ${authName}.protect();`;
|
|
278
|
+
const body = fn.body;
|
|
279
|
+
if (body.type !== "BlockStatement") {
|
|
280
|
+
const conciseCallee = mergeableAuthCallee(body, authNames);
|
|
281
|
+
if (conciseCallee) return fixer.replaceText(conciseCallee, `${conciseCallee.name}.protect`);
|
|
282
|
+
const fnIndent = getLineIndent(sourceCode, fn);
|
|
283
|
+
const inner = `${fnIndent} `;
|
|
284
|
+
const blockBody = `{\n${inner}${call}\n${inner}return ${sourceCode.getText(body)};\n${fnIndent}}`;
|
|
285
|
+
const arrowToken = sourceCode.getTokenBefore(body, { filter: (token) => token.type === "Punctuator" && token.value === "=>" });
|
|
286
|
+
if (arrowToken) return fixer.replaceTextRange([arrowToken.range[1], fn.range[1]], ` ${blockBody}`);
|
|
287
|
+
return fixer.replaceText(body, blockBody);
|
|
288
|
+
}
|
|
289
|
+
const stmts = body.body;
|
|
290
|
+
let lastDirective = null;
|
|
291
|
+
let firstExecIdx = 0;
|
|
292
|
+
for (const stmt of stmts) if (stmt.type === "ExpressionStatement" && stmt.directive) {
|
|
293
|
+
lastDirective = stmt;
|
|
294
|
+
firstExecIdx++;
|
|
295
|
+
} else break;
|
|
296
|
+
const firstExec = stmts[firstExecIdx];
|
|
297
|
+
if (firstExec) {
|
|
298
|
+
const mergeCallee = firstStatementAuthCallee(firstExec, authNames);
|
|
299
|
+
if (mergeCallee) return fixer.replaceText(mergeCallee, `${mergeCallee.name}.protect`);
|
|
300
|
+
}
|
|
301
|
+
if (lastDirective) {
|
|
302
|
+
const indent = getLineIndent(sourceCode, lastDirective);
|
|
303
|
+
return fixer.insertTextAfter(lastDirective, `\n${indent}${call}`);
|
|
304
|
+
}
|
|
305
|
+
const firstStmt = stmts[0];
|
|
306
|
+
if (firstStmt) {
|
|
307
|
+
const indent = getLineIndent(sourceCode, firstStmt);
|
|
308
|
+
return fixer.insertTextBefore(firstStmt, `${call}\n${indent}`);
|
|
309
|
+
}
|
|
310
|
+
const openBrace = sourceCode.getFirstToken(body);
|
|
311
|
+
const indent = `${getLineIndent(sourceCode, fn)} `;
|
|
312
|
+
if (openBrace) return fixer.insertTextAfter(openBrace, `\n${indent}${call}`);
|
|
313
|
+
return fixer.insertTextBefore(body, `${call}\n`);
|
|
314
|
+
}
|
|
315
|
+
function ensureAuthImportFix(fixer, sourceCode, program, authNames) {
|
|
316
|
+
if (authNames.size > 0) return null;
|
|
317
|
+
for (const stmt of program.body) {
|
|
318
|
+
if (stmt.type !== "ImportDeclaration") continue;
|
|
319
|
+
if (stmt.source.value !== CLERK_AUTH_SOURCE$1 || stmt.importKind === "type") continue;
|
|
320
|
+
const named = stmt.specifiers.filter((spec) => spec.type === "ImportSpecifier");
|
|
321
|
+
const last = named[named.length - 1];
|
|
322
|
+
if (last) return fixer.insertTextAfter(last, ", auth");
|
|
323
|
+
}
|
|
324
|
+
const importText = `import { auth } from '${CLERK_AUTH_SOURCE$1}';`;
|
|
325
|
+
const stmts = program.body;
|
|
326
|
+
let lastDirective = null;
|
|
327
|
+
for (const stmt of stmts) if (stmt.type === "ExpressionStatement" && stmt.directive) lastDirective = stmt;
|
|
328
|
+
else break;
|
|
329
|
+
if (lastDirective) return fixer.insertTextAfter(lastDirective, `\n${importText}`);
|
|
330
|
+
const firstStmt = stmts[0];
|
|
331
|
+
if (firstStmt) return fixer.insertTextBefore(firstStmt, `${importText}\n`);
|
|
332
|
+
return fixer.insertTextAfterRange([0, 0], `${importText}\n`);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
//#endregion
|
|
336
|
+
//#region src/next/lib/match-folders.ts
|
|
337
|
+
const REGEX_SPECIAL = /[.+^${}()|[\]\\]/g;
|
|
338
|
+
function segmentToRegex(seg) {
|
|
339
|
+
let out = "";
|
|
340
|
+
for (let i = 0; i < seg.length; i++) {
|
|
341
|
+
const ch = seg[i];
|
|
342
|
+
if (ch === "*") out += "[^/]*";
|
|
343
|
+
else if (REGEX_SPECIAL.test(ch)) out += "\\" + ch;
|
|
344
|
+
else out += ch;
|
|
345
|
+
REGEX_SPECIAL.lastIndex = 0;
|
|
346
|
+
}
|
|
347
|
+
return out;
|
|
348
|
+
}
|
|
349
|
+
function segmentMatches(patternSeg, pathSeg) {
|
|
350
|
+
if (patternSeg === pathSeg) return true;
|
|
351
|
+
if (!patternSeg.includes("*")) return false;
|
|
352
|
+
return new RegExp("^" + segmentToRegex(patternSeg) + "$").test(pathSeg);
|
|
353
|
+
}
|
|
354
|
+
function matchSegments(patternSegs, pi, pathSegs, si) {
|
|
355
|
+
while (pi < patternSegs.length) {
|
|
356
|
+
const seg = patternSegs[pi];
|
|
357
|
+
if (seg === "**") {
|
|
358
|
+
if (pi === patternSegs.length - 1) return true;
|
|
359
|
+
for (let k = si; k <= pathSegs.length; k++) if (matchSegments(patternSegs, pi + 1, pathSegs, k)) return true;
|
|
360
|
+
return false;
|
|
361
|
+
}
|
|
362
|
+
if (si >= pathSegs.length) return false;
|
|
363
|
+
if (!segmentMatches(seg, pathSegs[si])) return false;
|
|
364
|
+
pi++;
|
|
365
|
+
si++;
|
|
366
|
+
}
|
|
367
|
+
return si === pathSegs.length;
|
|
368
|
+
}
|
|
369
|
+
function matchPath(pattern, path) {
|
|
370
|
+
return matchSegments(pattern.split("/"), 0, path.split("/"), 0);
|
|
371
|
+
}
|
|
372
|
+
function specificity(pattern) {
|
|
373
|
+
return pattern.split("/").filter((seg) => seg.length > 0 && seg !== "**" && !seg.includes("*")).length;
|
|
374
|
+
}
|
|
375
|
+
function literalPrefix(pattern) {
|
|
376
|
+
const segs = pattern.split("/");
|
|
377
|
+
const result = [];
|
|
378
|
+
for (const seg of segs) {
|
|
379
|
+
if (seg === "**" || seg.includes("*")) break;
|
|
380
|
+
result.push(seg);
|
|
381
|
+
}
|
|
382
|
+
return result.join("/");
|
|
383
|
+
}
|
|
384
|
+
function hasDescendantsMatching(folder, patterns) {
|
|
385
|
+
if (!patterns || patterns.length === 0) return false;
|
|
386
|
+
for (const pattern of patterns) {
|
|
387
|
+
const prefix = literalPrefix(pattern);
|
|
388
|
+
if (!prefix) continue;
|
|
389
|
+
if (prefix === folder) return true;
|
|
390
|
+
if (prefix.startsWith(folder + "/")) return true;
|
|
391
|
+
}
|
|
392
|
+
return false;
|
|
393
|
+
}
|
|
394
|
+
function classifyFolder(folderPath, options) {
|
|
395
|
+
const protectPatterns = options.protected ?? [];
|
|
396
|
+
const publicPatterns = options.public ?? [];
|
|
397
|
+
const protectMatches = protectPatterns.filter((p) => matchPath(p, folderPath));
|
|
398
|
+
const publicMatches = publicPatterns.filter((p) => matchPath(p, folderPath));
|
|
399
|
+
if (protectMatches.length === 0 && publicMatches.length === 0) return "unmatched";
|
|
400
|
+
if (publicMatches.length === 0) return "protected";
|
|
401
|
+
if (protectMatches.length === 0) return "public";
|
|
402
|
+
return Math.max(...protectMatches.map(specificity)) >= Math.max(...publicMatches.map(specificity)) ? "protected" : "public";
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
//#endregion
|
|
406
|
+
//#region src/next/lib/project-root.ts
|
|
407
|
+
/**
|
|
408
|
+
* Resolve the directory paths should be relativized against when classifying
|
|
409
|
+
* App Router folders.
|
|
410
|
+
*/
|
|
411
|
+
/** Filenames ESLint searches for when resolving flat config (precedence order). */
|
|
412
|
+
const ESLINT_CONFIG_FILENAMES = [
|
|
413
|
+
"eslint.config.js",
|
|
414
|
+
"eslint.config.mjs",
|
|
415
|
+
"eslint.config.cjs",
|
|
416
|
+
"eslint.config.ts",
|
|
417
|
+
"eslint.config.mts",
|
|
418
|
+
"eslint.config.cts"
|
|
419
|
+
];
|
|
420
|
+
function normalizeDir(dir) {
|
|
421
|
+
return dir.replaceAll("\\", "/");
|
|
422
|
+
}
|
|
423
|
+
/**
|
|
424
|
+
* Walk up from `filePath` and return the directory of the nearest
|
|
425
|
+
* `eslint.config.*`, mirroring ESLint's per-file config lookup.
|
|
426
|
+
*/
|
|
427
|
+
function findEslintProjectRoot(filePath) {
|
|
428
|
+
let dir = node_path.default.resolve(node_path.default.dirname(filePath));
|
|
429
|
+
const { root } = node_path.default.parse(dir);
|
|
430
|
+
while (true) {
|
|
431
|
+
for (const name of ESLINT_CONFIG_FILENAMES) if ((0, node_fs.existsSync)(node_path.default.join(dir, name))) return normalizeDir(dir);
|
|
432
|
+
if (dir === root) return null;
|
|
433
|
+
dir = node_path.default.dirname(dir);
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
/**
|
|
437
|
+
* Pick the directory to relativize linted file paths against.
|
|
438
|
+
*
|
|
439
|
+
* Precedence: explicit `rootDir` → nearest `eslint.config.*` ancestor → `cwd`.
|
|
440
|
+
*/
|
|
441
|
+
function resolveProjectRoot(filename, { rootDir, cwd } = {}) {
|
|
442
|
+
if (rootDir) return normalizeDir(rootDir);
|
|
443
|
+
if (filename) {
|
|
444
|
+
const fromConfig = findEslintProjectRoot(filename);
|
|
445
|
+
if (fromConfig) return fromConfig;
|
|
446
|
+
}
|
|
447
|
+
return cwd ? normalizeDir(cwd) : void 0;
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
//#endregion
|
|
451
|
+
//#region src/next/lib/protection-checks.ts
|
|
452
|
+
const CLERK_AUTH_SOURCE = "@clerk/nextjs/server";
|
|
453
|
+
/**
|
|
454
|
+
* Collect the local names that `auth` is imported as from `@clerk/nextjs/server`
|
|
455
|
+
* in the given module. Usually returns `{'auth'}`, but handles aliased imports
|
|
456
|
+
* like `import { auth as clerkAuth } from '@clerk/nextjs/server'` correctly.
|
|
457
|
+
* Type-only imports (`import type { auth }` / `import { type auth }`) are
|
|
458
|
+
* excluded — they are erased at compile time and do not provide a runtime binding.
|
|
459
|
+
*/
|
|
460
|
+
function findAuthLocalNames(programNode) {
|
|
461
|
+
const names = /* @__PURE__ */ new Set();
|
|
462
|
+
for (const stmt of programNode.body) {
|
|
463
|
+
if (stmt.type !== "ImportDeclaration") continue;
|
|
464
|
+
if (stmt.source.value !== CLERK_AUTH_SOURCE) continue;
|
|
465
|
+
if (stmt.importKind === "type") continue;
|
|
466
|
+
for (const spec of stmt.specifiers) {
|
|
467
|
+
if (spec.type !== "ImportSpecifier") continue;
|
|
468
|
+
if (spec.importKind === "type") continue;
|
|
469
|
+
if ((spec.imported.type === "Identifier" ? spec.imported.name : spec.imported.value) === "auth") names.add(spec.local.name);
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
return names;
|
|
473
|
+
}
|
|
474
|
+
const AUTH_FIELDS = new Set([
|
|
475
|
+
"userId",
|
|
476
|
+
"sessionId",
|
|
477
|
+
"isAuthenticated"
|
|
478
|
+
]);
|
|
479
|
+
const EXIT_FUNCTIONS = new Set([
|
|
480
|
+
"redirect",
|
|
481
|
+
"permanentRedirect",
|
|
482
|
+
"notFound",
|
|
483
|
+
"unauthorized",
|
|
484
|
+
"forbidden",
|
|
485
|
+
"redirectToSignIn",
|
|
486
|
+
"redirectToSignUp"
|
|
487
|
+
]);
|
|
488
|
+
function isProtectCall(node, authNames) {
|
|
489
|
+
if (!node || node.type !== "CallExpression") return false;
|
|
490
|
+
const callee = node.callee;
|
|
491
|
+
if (callee.type !== "MemberExpression") return false;
|
|
492
|
+
if (callee.property.type !== "Identifier" || callee.property.name !== "protect") return false;
|
|
493
|
+
if (callee.object.type === "Identifier" && authNames.has(callee.object.name)) return true;
|
|
494
|
+
if (callee.object.type === "AwaitExpression" && callee.object.argument.type === "CallExpression" && callee.object.argument.callee.type === "Identifier" && authNames.has(callee.object.argument.callee.name)) return true;
|
|
495
|
+
return false;
|
|
496
|
+
}
|
|
497
|
+
function isProtectAwait(node, authNames) {
|
|
498
|
+
return !!node && node.type === "AwaitExpression" && isProtectCall(node.argument, authNames);
|
|
499
|
+
}
|
|
500
|
+
function isProtectAwaitStatement(stmt, authNames) {
|
|
501
|
+
if (stmt.type === "ExpressionStatement") return isProtectAwait(stmt.expression, authNames);
|
|
502
|
+
if (stmt.type === "VariableDeclaration") {
|
|
503
|
+
const first = stmt.declarations[0];
|
|
504
|
+
return first != null && isProtectAwait(first.init ?? void 0, authNames);
|
|
505
|
+
}
|
|
506
|
+
return false;
|
|
507
|
+
}
|
|
508
|
+
function capturedAuthBindings(stmt, authNames) {
|
|
509
|
+
if (stmt.type !== "VariableDeclaration") return null;
|
|
510
|
+
if (stmt.declarations.length !== 1) return null;
|
|
511
|
+
const decl = stmt.declarations[0];
|
|
512
|
+
if (!decl) return null;
|
|
513
|
+
if (decl.id.type !== "ObjectPattern") return null;
|
|
514
|
+
if (!decl.init || decl.init.type !== "AwaitExpression") return null;
|
|
515
|
+
const arg = decl.init.argument;
|
|
516
|
+
if (arg.type !== "CallExpression") return null;
|
|
517
|
+
if (arg.callee.type !== "Identifier" || !authNames.has(arg.callee.name)) return null;
|
|
518
|
+
const bindings = /* @__PURE__ */ new Set();
|
|
519
|
+
for (const prop of decl.id.properties) {
|
|
520
|
+
if (prop.type !== "Property") continue;
|
|
521
|
+
if (prop.key.type !== "Identifier") continue;
|
|
522
|
+
const fieldName = prop.key.name;
|
|
523
|
+
if (!AUTH_FIELDS.has(fieldName)) continue;
|
|
524
|
+
if (prop.value.type !== "Identifier" || prop.value.name !== fieldName) continue;
|
|
525
|
+
bindings.add(fieldName);
|
|
526
|
+
}
|
|
527
|
+
return bindings.size > 0 ? bindings : null;
|
|
528
|
+
}
|
|
529
|
+
function isRecognizedAuthCheck(test, bindings) {
|
|
530
|
+
if (test.type === "UnaryExpression" && test.operator === "!") return test.argument.type === "Identifier" && AUTH_FIELDS.has(test.argument.name) && bindings.has(test.argument.name);
|
|
531
|
+
if (test.type !== "BinaryExpression") return false;
|
|
532
|
+
if (test.operator !== "===" && test.operator !== "==") return false;
|
|
533
|
+
let id;
|
|
534
|
+
let other;
|
|
535
|
+
if (test.left.type === "Identifier" && bindings.has(test.left.name)) {
|
|
536
|
+
id = test.left;
|
|
537
|
+
other = test.right;
|
|
538
|
+
} else if (test.right.type === "Identifier" && bindings.has(test.right.name)) {
|
|
539
|
+
id = test.right;
|
|
540
|
+
other = test.left;
|
|
541
|
+
} else return false;
|
|
542
|
+
const name = id.name;
|
|
543
|
+
if (name === "userId" || name === "sessionId") return other.type === "Literal" && other.value === null;
|
|
544
|
+
if (name === "isAuthenticated") return test.operator === "===" && other.type === "Literal" && other.value === false;
|
|
545
|
+
return false;
|
|
546
|
+
}
|
|
547
|
+
function isExitCall(expr) {
|
|
548
|
+
if (!expr || expr.type !== "CallExpression") return false;
|
|
549
|
+
if (expr.callee.type !== "Identifier") return false;
|
|
550
|
+
return EXIT_FUNCTIONS.has(expr.callee.name);
|
|
551
|
+
}
|
|
552
|
+
function statementExits(stmt) {
|
|
553
|
+
if (!stmt) return false;
|
|
554
|
+
if (stmt.type === "ReturnStatement") return true;
|
|
555
|
+
if (stmt.type === "ThrowStatement") return true;
|
|
556
|
+
if (stmt.type === "ExpressionStatement") return isExitCall(stmt.expression);
|
|
557
|
+
return false;
|
|
558
|
+
}
|
|
559
|
+
function consequentExits(consequent) {
|
|
560
|
+
if (!consequent) return false;
|
|
561
|
+
if (statementExits(consequent)) return true;
|
|
562
|
+
if (consequent.type === "BlockStatement") return consequent.body.some(statementExits);
|
|
563
|
+
return false;
|
|
564
|
+
}
|
|
565
|
+
function isAuthGuardWithExit(stmt, bindings) {
|
|
566
|
+
if (stmt.type !== "IfStatement") return false;
|
|
567
|
+
if (!isRecognizedAuthCheck(stmt.test, bindings)) return false;
|
|
568
|
+
return consequentExits(stmt.consequent);
|
|
569
|
+
}
|
|
570
|
+
function isNonRuntimeStatement(stmt) {
|
|
571
|
+
if (stmt.type === "ExpressionStatement" && stmt.directive) return true;
|
|
572
|
+
if (stmt.type === "TSTypeAliasDeclaration") return true;
|
|
573
|
+
if (stmt.type === "TSInterfaceDeclaration") return true;
|
|
574
|
+
return false;
|
|
575
|
+
}
|
|
576
|
+
function nextExecutable(stmts, from) {
|
|
577
|
+
let i = from;
|
|
578
|
+
while (i < stmts.length && isNonRuntimeStatement(stmts[i])) i++;
|
|
579
|
+
return i;
|
|
580
|
+
}
|
|
581
|
+
function hasProtectAtTop(fn, authNames) {
|
|
582
|
+
if (!fn || !fn.async) return false;
|
|
583
|
+
const body = fn.body;
|
|
584
|
+
if (body && body.type !== "BlockStatement") return body.type === "AwaitExpression" && isProtectCall(body.argument, authNames);
|
|
585
|
+
if (!body || body.type !== "BlockStatement") return false;
|
|
586
|
+
const stmts = body.body;
|
|
587
|
+
const first = nextExecutable(stmts, 0);
|
|
588
|
+
if (first >= stmts.length) return false;
|
|
589
|
+
if (isProtectAwaitStatement(stmts[first], authNames)) return true;
|
|
590
|
+
const captured = capturedAuthBindings(stmts[first], authNames);
|
|
591
|
+
if (captured) {
|
|
592
|
+
const second = nextExecutable(stmts, first + 1);
|
|
593
|
+
if (second < stmts.length && isAuthGuardWithExit(stmts[second], captured)) return true;
|
|
594
|
+
}
|
|
595
|
+
return false;
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
//#endregion
|
|
599
|
+
//#region src/next/require-auth-protection.ts
|
|
600
|
+
const HTTP_METHODS = new Set([
|
|
601
|
+
"GET",
|
|
602
|
+
"POST",
|
|
603
|
+
"PUT",
|
|
604
|
+
"PATCH",
|
|
605
|
+
"DELETE",
|
|
606
|
+
"OPTIONS",
|
|
607
|
+
"HEAD"
|
|
608
|
+
]);
|
|
609
|
+
const DEFAULT_RESOURCES = {
|
|
610
|
+
routeHandlers: true,
|
|
611
|
+
serverFunctions: true,
|
|
612
|
+
serverComponentEntrypoints: true
|
|
613
|
+
};
|
|
614
|
+
const rule = {
|
|
615
|
+
meta: {
|
|
616
|
+
type: "problem",
|
|
617
|
+
hasSuggestions: true,
|
|
618
|
+
docs: { description: "Require `await auth.protect()` in App Router resources under protected folders" },
|
|
619
|
+
schema: [{
|
|
620
|
+
type: "object",
|
|
621
|
+
properties: {
|
|
622
|
+
protected: {
|
|
623
|
+
type: "array",
|
|
624
|
+
items: { type: "string" },
|
|
625
|
+
minItems: 1
|
|
626
|
+
},
|
|
627
|
+
public: {
|
|
628
|
+
type: "array",
|
|
629
|
+
items: { type: "string" }
|
|
630
|
+
},
|
|
631
|
+
resources: {
|
|
632
|
+
type: "object",
|
|
633
|
+
properties: {
|
|
634
|
+
routeHandlers: { type: "boolean" },
|
|
635
|
+
serverFunctions: { type: "boolean" },
|
|
636
|
+
serverComponentEntrypoints: { type: "boolean" }
|
|
637
|
+
},
|
|
638
|
+
additionalProperties: false
|
|
639
|
+
},
|
|
640
|
+
mixedScopeLayouts: { oneOf: [{
|
|
641
|
+
type: "string",
|
|
642
|
+
enum: ["auto"]
|
|
643
|
+
}, {
|
|
644
|
+
type: "array",
|
|
645
|
+
items: { type: "string" }
|
|
646
|
+
}] },
|
|
647
|
+
rootDir: { type: "string" }
|
|
648
|
+
},
|
|
649
|
+
required: ["protected"],
|
|
650
|
+
additionalProperties: false
|
|
651
|
+
}],
|
|
652
|
+
messages: {
|
|
653
|
+
missingProtect: "Expected `await auth.protect()` at the top of {{subject}} in a folder configured as protected. Add the call to the top of the function, move the file into a public folder, or configure this folder as public.",
|
|
654
|
+
addAuthProtect: "Add `await auth.protect()` to the top of this {{subject}}.",
|
|
655
|
+
exportImported: "This {{subject}} is exported from '{{source}}'. The rule cannot follow imports across files. Add a wrapper with `await auth.protect()`, or ensure the imported function calls it and add an eslint-disable comment with a reason.",
|
|
656
|
+
unverifiableExport: "This {{subject}} could not be verified as being protected, likely because it is assigned from a call expression (e.g. `const handler = withAuth(impl)`). Inline a function literal that calls `await auth.protect()`, or add an eslint-disable comment with a reason.",
|
|
657
|
+
unlistedMixedScopeLayout: "This {{fileKind}} at '{{folder}}/' wraps both protected and public descendants but is not listed in `mixedScopeLayouts`. Either add '{{folder}}' to the list to acknowledge the mixed scope, or restructure so the {{fileKind}} wraps only public or protected descendants."
|
|
658
|
+
}
|
|
659
|
+
},
|
|
660
|
+
create(context) {
|
|
661
|
+
const filename = context.physicalFilename ?? context.filename ?? context.getFilename?.();
|
|
662
|
+
const cwd = context.cwd || context.getCwd?.();
|
|
663
|
+
const options = context.options[0] ?? {};
|
|
664
|
+
const config = {
|
|
665
|
+
protected: options.protected,
|
|
666
|
+
public: options.public ?? []
|
|
667
|
+
};
|
|
668
|
+
const resources = normalizeResources(options.resources);
|
|
669
|
+
const mixedScopeLayoutsOption = options.mixedScopeLayouts === void 0 ? "auto" : options.mixedScopeLayouts;
|
|
670
|
+
const folder = getRelativeFolder(filename, resolveProjectRoot(filename, {
|
|
671
|
+
rootDir: options.rootDir,
|
|
672
|
+
cwd
|
|
673
|
+
}));
|
|
674
|
+
if (!folder) return {};
|
|
675
|
+
const fileKind = getFileKind(filename);
|
|
676
|
+
let authNames = /* @__PURE__ */ new Set();
|
|
677
|
+
let shouldCheckInlineServerFunctions = false;
|
|
678
|
+
const checkedFunctions = /* @__PURE__ */ new WeakSet();
|
|
679
|
+
return {
|
|
680
|
+
Program(programNode) {
|
|
681
|
+
const ast = programNode;
|
|
682
|
+
const isServerFunction = isServerFunctionModule(ast);
|
|
683
|
+
const isClient = isClientModule(ast);
|
|
684
|
+
if (classifyFolder(folder, config) !== "protected") return;
|
|
685
|
+
authNames = findAuthLocalNames(ast);
|
|
686
|
+
shouldCheckInlineServerFunctions = resources.serverFunctions && !isClient;
|
|
687
|
+
if (!fileKind && !isServerFunction) return;
|
|
688
|
+
if (isClient && (fileKind === "page" || fileKind === "layout" || fileKind === "template" || fileKind === "default")) return;
|
|
689
|
+
if (resources.serverComponentEntrypoints && (fileKind === "layout" || fileKind === "template") && hasDescendantsMatching(folder, config.public)) {
|
|
690
|
+
checkUnacknowledgedMixedScope(context, ast, fileKind, folder, mixedScopeLayoutsOption);
|
|
691
|
+
return;
|
|
692
|
+
}
|
|
693
|
+
if (resources.serverComponentEntrypoints && (fileKind === "page" || fileKind === "layout" || fileKind === "template" || fileKind === "default")) checkDefaultExport(context, ast, fileKind, authNames, checkedFunctions);
|
|
694
|
+
else if (resources.routeHandlers && fileKind === "route") checkRouteHandlers(context, ast, authNames, checkedFunctions);
|
|
695
|
+
else if (resources.serverFunctions && isServerFunction) checkServerFunctions(context, ast, authNames, checkedFunctions);
|
|
696
|
+
},
|
|
697
|
+
FunctionDeclaration(node) {
|
|
698
|
+
checkInlineServerFunction(context, node, authNames, shouldCheckInlineServerFunctions, checkedFunctions);
|
|
699
|
+
},
|
|
700
|
+
FunctionExpression(node) {
|
|
701
|
+
checkInlineServerFunction(context, node, authNames, shouldCheckInlineServerFunctions, checkedFunctions);
|
|
702
|
+
},
|
|
703
|
+
ArrowFunctionExpression(node) {
|
|
704
|
+
checkInlineServerFunction(context, node, authNames, shouldCheckInlineServerFunctions, checkedFunctions);
|
|
705
|
+
}
|
|
706
|
+
};
|
|
707
|
+
}
|
|
708
|
+
};
|
|
709
|
+
function normalizeResources(resources) {
|
|
710
|
+
return {
|
|
711
|
+
...DEFAULT_RESOURCES,
|
|
712
|
+
...resources
|
|
713
|
+
};
|
|
714
|
+
}
|
|
715
|
+
function checkUnacknowledgedMixedScope(context, programNode, fileKind, folder, mixedScopeLayoutsOption) {
|
|
716
|
+
if (mixedScopeLayoutsOption === "auto") return;
|
|
717
|
+
if (mixedScopeLayoutsOption.includes(folder)) return;
|
|
718
|
+
const defaultExport = programNode.body.find((n) => n.type === "ExportDefaultDeclaration");
|
|
719
|
+
context.report({
|
|
720
|
+
node: defaultExport ?? programNode,
|
|
721
|
+
messageId: "unlistedMixedScopeLayout",
|
|
722
|
+
data: {
|
|
723
|
+
folder,
|
|
724
|
+
fileKind
|
|
725
|
+
}
|
|
726
|
+
});
|
|
727
|
+
}
|
|
728
|
+
function getMissingProtectReportNode(fn, fallback) {
|
|
729
|
+
return fn.id ?? fn.body ?? fallback;
|
|
730
|
+
}
|
|
731
|
+
function checkMissingProtect(context, reportNode, target, subject, authNames, checkedFunctions) {
|
|
732
|
+
if (target.kind === "imported") {
|
|
733
|
+
context.report({
|
|
734
|
+
node: reportNode,
|
|
735
|
+
messageId: "exportImported",
|
|
736
|
+
data: {
|
|
737
|
+
subject,
|
|
738
|
+
source: target.source
|
|
739
|
+
}
|
|
740
|
+
});
|
|
741
|
+
return;
|
|
742
|
+
}
|
|
743
|
+
if (target.kind === "function") {
|
|
744
|
+
checkedFunctions?.add(target.node);
|
|
745
|
+
if (!hasProtectAtTop(target.node, authNames)) context.report({
|
|
746
|
+
node: getMissingProtectReportNode(target.node, reportNode),
|
|
747
|
+
messageId: "missingProtect",
|
|
748
|
+
data: { subject },
|
|
749
|
+
suggest: buildAddAuthProtectSuggestion(context, target.node, subject, authNames)
|
|
750
|
+
});
|
|
751
|
+
return;
|
|
752
|
+
}
|
|
753
|
+
context.report({
|
|
754
|
+
node: reportNode,
|
|
755
|
+
messageId: "unverifiableExport",
|
|
756
|
+
data: { subject }
|
|
757
|
+
});
|
|
758
|
+
}
|
|
759
|
+
function checkRouteHandlers(context, programNode, authNames, checkedFunctions) {
|
|
760
|
+
for (const { name, target, reportNode } of iterateNamedExports(programNode)) {
|
|
761
|
+
if (!HTTP_METHODS.has(name)) continue;
|
|
762
|
+
checkMissingProtect(context, reportNode, target, `${name} handler`, authNames, checkedFunctions);
|
|
763
|
+
}
|
|
764
|
+
for (const { source, reportNode } of iterateExportAllDeclarations(programNode)) checkMissingProtect(context, reportNode, {
|
|
765
|
+
kind: "imported",
|
|
766
|
+
source
|
|
767
|
+
}, "route handlers", authNames);
|
|
768
|
+
}
|
|
769
|
+
function checkServerFunctions(context, programNode, authNames, checkedFunctions) {
|
|
770
|
+
for (const { name, target, reportNode } of iterateNamedExports(programNode)) {
|
|
771
|
+
if (name === "default") continue;
|
|
772
|
+
checkMissingProtect(context, reportNode, target, `Server Function '${name}'`, authNames, checkedFunctions);
|
|
773
|
+
}
|
|
774
|
+
const defaultExport = resolveDefaultExport(programNode);
|
|
775
|
+
if (defaultExport) checkMissingProtect(context, defaultExport.reportNode, defaultExport.target, "Server Function", authNames, checkedFunctions);
|
|
776
|
+
for (const { source, reportNode } of iterateExportAllDeclarations(programNode)) checkMissingProtect(context, reportNode, {
|
|
777
|
+
kind: "imported",
|
|
778
|
+
source
|
|
779
|
+
}, "Server Functions", authNames);
|
|
780
|
+
}
|
|
781
|
+
function checkDefaultExport(context, programNode, fileKind, authNames, checkedFunctions) {
|
|
782
|
+
const defaultExport = resolveDefaultExport(programNode);
|
|
783
|
+
if (!defaultExport) return;
|
|
784
|
+
checkMissingProtect(context, defaultExport.reportNode, defaultExport.target, fileKind, authNames, checkedFunctions);
|
|
785
|
+
}
|
|
786
|
+
function hasUseServerDirective(fn) {
|
|
787
|
+
const body = fn.body;
|
|
788
|
+
if (!body || body.type !== "BlockStatement") return false;
|
|
789
|
+
for (const stmt of body.body) {
|
|
790
|
+
if (stmt.type !== "ExpressionStatement") return false;
|
|
791
|
+
if (typeof stmt.directive !== "string") return false;
|
|
792
|
+
if (stmt.directive === "use server") return true;
|
|
793
|
+
}
|
|
794
|
+
return false;
|
|
795
|
+
}
|
|
796
|
+
function checkInlineServerFunction(context, fn, authNames, shouldCheckInlineServerFunctions, checkedFunctions) {
|
|
797
|
+
if (!shouldCheckInlineServerFunctions || checkedFunctions.has(fn) || !hasUseServerDirective(fn)) return;
|
|
798
|
+
if (!hasProtectAtTop(fn, authNames)) context.report({
|
|
799
|
+
node: getMissingProtectReportNode(fn, fn),
|
|
800
|
+
messageId: "missingProtect",
|
|
801
|
+
data: { subject: "Inline Server Function" },
|
|
802
|
+
suggest: buildAddAuthProtectSuggestion(context, fn, "Inline Server Function", authNames)
|
|
803
|
+
});
|
|
804
|
+
}
|
|
805
|
+
function buildAddAuthProtectSuggestion(context, fn, subject, authNames) {
|
|
806
|
+
const sourceCode = context.sourceCode;
|
|
807
|
+
return [{
|
|
808
|
+
messageId: "addAuthProtect",
|
|
809
|
+
data: { subject },
|
|
810
|
+
fix(fixer) {
|
|
811
|
+
return buildAuthProtectFixes({
|
|
812
|
+
fixer,
|
|
813
|
+
sourceCode,
|
|
814
|
+
fn,
|
|
815
|
+
authNames
|
|
816
|
+
});
|
|
817
|
+
}
|
|
818
|
+
}];
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
//#endregion
|
|
822
|
+
//#region src/next/index.ts
|
|
823
|
+
const plugin = {
|
|
824
|
+
meta: {
|
|
825
|
+
name: "@clerk/eslint-plugin/next",
|
|
826
|
+
version: "0.1.0"
|
|
827
|
+
},
|
|
828
|
+
rules: { "require-auth-protection": rule }
|
|
829
|
+
};
|
|
830
|
+
|
|
831
|
+
//#endregion
|
|
832
|
+
module.exports = plugin;
|
|
833
|
+
//# sourceMappingURL=next.js.map
|