@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.
@@ -0,0 +1,59 @@
1
+ import { ESLint } from "eslint";
2
+
3
+ //#region src/next/fix-auth-protection.d.ts
4
+ interface FixAuthProtectionOptions {
5
+ /** File, directory, or glob patterns to scan. Defaults to `['.']`. */
6
+ patterns?: string[];
7
+ /** Working directory ESLint resolves config and files against. Defaults to `process.cwd()`. */
8
+ cwd?: string;
9
+ /** Compute the changes without writing them to disk. */
10
+ dryRun?: boolean;
11
+ /**
12
+ * Advanced/escape hatch: a pre-configured ESLint instance to lint with. When
13
+ * omitted, a default `new ESLint({ cwd })` is used, which discovers the
14
+ * consumer's flat config. Mainly useful for tests.
15
+ */
16
+ eslint?: ESLint;
17
+ /**
18
+ * Called before scanning with the path to the ESLint config file that will be
19
+ * used (or `undefined` when none is found / an instance was injected).
20
+ */
21
+ onConfigResolved?: (configFilePath: string | undefined) => void;
22
+ /**
23
+ * Called once linting finishes and per-file fixing begins, with the number of
24
+ * files that have flagged resources. Useful for reporting progress, since the
25
+ * initial lint can be slow on large projects.
26
+ */
27
+ onScanComplete?: (fileCount: number) => void;
28
+ /** Called after each file is fixed (or, in `dryRun`, would be fixed). */
29
+ onFileFixed?: (file: FixedFile) => void;
30
+ }
31
+ interface FixedFile {
32
+ filePath: string;
33
+ /** Number of resources that had `await auth.protect()` applied. */
34
+ protections: number;
35
+ }
36
+ interface UnresolvedIssue {
37
+ line: number;
38
+ column: number;
39
+ message: string;
40
+ }
41
+ interface UnresolvedFile {
42
+ filePath: string;
43
+ issues: UnresolvedIssue[];
44
+ }
45
+ interface FixAuthProtectionResult {
46
+ /** Files that were (or, in `dryRun`, would be) modified. */
47
+ fixed: FixedFile[];
48
+ /** Files with flagged resources that have no safe automatic fix. */
49
+ unresolved: UnresolvedFile[];
50
+ }
51
+ /**
52
+ * Lint the given patterns with the consumer's ESLint config and apply the
53
+ * `require-auth-protection` rule's `await auth.protect()` suggestions to every
54
+ * resource it can safely fix.
55
+ */
56
+ declare function fixAuthProtection(options?: FixAuthProtectionOptions): Promise<FixAuthProtectionResult>;
57
+ //#endregion
58
+ export { FixAuthProtectionOptions, FixAuthProtectionResult, FixedFile, UnresolvedFile, UnresolvedIssue, fixAuthProtection };
59
+ //# sourceMappingURL=fix-auth-protection.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fix-auth-protection.d.ts","names":[],"sources":["../../src/next/fix-auth-protection.ts"],"mappings":";;;UAyBiB,wBAAA;EAyBA;EAvBf,QAAA;EAuB8B;EArB9B,GAAA;EAwBwB;EAtBxB,MAAA;EAuBA;AAEW;AAGb;;;EAtBE,MAAA,GAAS,MAAA;EAuBT;;;;EAlBA,gBAAA,IAAoB,cAAA;EAuBL;;;;;EAjBf,cAAA,IAAkB,SAAA;EAmBV;EAjBR,WAAA,IAAe,IAAA,EAAM,SAAS;AAAA;AAAA,UAGf,SAAA;EACf,QAAA;;EAEA,WAAW;AAAA;AAAA,UAGI,eAAA;EACf,IAAA;EACA,MAAA;EACA,OAAA;AAAA;AAAA,UAGe,cAAA;EACf,QAAA;EACA,MAAA,EAAQ,eAAe;AAAA;AAAA,UAGR,uBAAA;EAmGgE;EAjG/E,KAAA,EAAO,SAAA;EAiG+E;EA/FtF,UAAA,EAAY,cAAc;AAAA;;;;AA+FoF;;iBAA1F,iBAAA,CAAkB,OAAA,GAAS,wBAAA,GAAgC,OAAA,CAAQ,uBAAA"}
@@ -0,0 +1,149 @@
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
2
+ let node_fs_promises = require("node:fs/promises");
3
+ let eslint = require("eslint");
4
+
5
+ //#region src/next/fix-auth-protection.ts
6
+ /**
7
+ * Programmatic auto-fixer for the `require-auth-protection` rule.
8
+ *
9
+ * The rule exposes its `await auth.protect()` insertion as an opt-in
10
+ * *suggestion* rather than an autofix, so `eslint --fix` deliberately leaves it
11
+ * alone (adding a protection check changes runtime behavior). This runner lets
12
+ * you apply those suggestions in bulk on demand — from a script via
13
+ * `fixAuthProtection()` or from the terminal via the
14
+ * `clerk-next-fix-auth-protection` command.
15
+ *
16
+ * It works by linting with the consumer's own ESLint config (so the
17
+ * protected/public folder globs are honored) and applying the rule's
18
+ * `addAuthProtect` suggestion to each flagged resource. Resources the rule
19
+ * cannot safely fix (imported/wrapped exports, unacknowledged mixed-scope
20
+ * layouts) are reported back as `unresolved` for manual follow-up.
21
+ */
22
+ const RULE_NAME = "require-auth-protection";
23
+ const SUGGESTION_MESSAGE_ID = "addAuthProtect";
24
+ const UNFIXABLE_MESSAGE_IDS = new Set([
25
+ "exportImported",
26
+ "unverifiableExport",
27
+ "unlistedMixedScopeLayout"
28
+ ]);
29
+ function isAuthProtectionRule(ruleId) {
30
+ return ruleId === RULE_NAME || (ruleId?.endsWith(`/${RULE_NAME}`) ?? false);
31
+ }
32
+ function collectSuggestionFixes(messages) {
33
+ const fixes = [];
34
+ for (const message of messages) {
35
+ if (!isAuthProtectionRule(message.ruleId)) continue;
36
+ const suggestion = message.suggestions?.find((s) => s.messageId === SUGGESTION_MESSAGE_ID);
37
+ if (suggestion?.fix) fixes.push({
38
+ range: [suggestion.fix.range[0], suggestion.fix.range[1]],
39
+ text: suggestion.fix.text
40
+ });
41
+ }
42
+ return fixes;
43
+ }
44
+ /**
45
+ * Apply as many non-overlapping fixes as possible in a single pass, mirroring
46
+ * ESLint's own `SourceCodeFixer`: sort by position and skip any fix that starts
47
+ * before the previous one ended. Overlapping fixes are left for a later pass.
48
+ */
49
+ function applyFixes(source, fixes) {
50
+ const sorted = [...fixes].sort((a, b) => a.range[0] - b.range[0] || a.range[1] - b.range[1]);
51
+ let output = "";
52
+ let lastPos = 0;
53
+ let applied = 0;
54
+ for (const fix of sorted) {
55
+ const [start, end] = fix.range;
56
+ if (start < lastPos) continue;
57
+ output += source.slice(lastPos, start) + fix.text;
58
+ lastPos = end;
59
+ applied++;
60
+ }
61
+ output += source.slice(lastPos);
62
+ return {
63
+ output,
64
+ applied
65
+ };
66
+ }
67
+ async function applyFileFixes(eslint$1, filePath, source) {
68
+ const MAX_FIX_PASSES = 10;
69
+ let current = source;
70
+ let total = 0;
71
+ let passes = 0;
72
+ for (let i = 0; i < 11; i += 1) {
73
+ const [result] = await eslint$1.lintText(current, { filePath });
74
+ if (!result) break;
75
+ const fixes = collectSuggestionFixes(result.messages);
76
+ if (fixes.length === 0) break;
77
+ if (passes >= MAX_FIX_PASSES) throw new Error(`Auth-protect fixes for ${filePath} did not converge after ${MAX_FIX_PASSES} passes. This is unexpected; please report it at https://github.com/clerk/javascript/issues.`);
78
+ const { output, applied } = applyFixes(current, fixes);
79
+ if (applied === 0) break;
80
+ current = output;
81
+ total += applied;
82
+ passes += 1;
83
+ }
84
+ return {
85
+ output: current,
86
+ applied: total
87
+ };
88
+ }
89
+ /**
90
+ * Lint the given patterns with the consumer's ESLint config and apply the
91
+ * `require-auth-protection` rule's `await auth.protect()` suggestions to every
92
+ * resource it can safely fix.
93
+ */
94
+ async function fixAuthProtection(options = {}) {
95
+ const cwd = options.cwd ?? process.cwd();
96
+ const patterns = options.patterns && options.patterns.length > 0 ? options.patterns : ["."];
97
+ const dryRun = options.dryRun ?? false;
98
+ const eslint$2 = options.eslint ?? new eslint.ESLint({
99
+ cwd,
100
+ ruleFilter: ({ ruleId }) => isAuthProtectionRule(ruleId)
101
+ });
102
+ if (options.onConfigResolved) {
103
+ let configFile;
104
+ try {
105
+ configFile = await eslint$2.findConfigFile();
106
+ } catch {
107
+ configFile = void 0;
108
+ }
109
+ options.onConfigResolved(configFile);
110
+ }
111
+ const results = await eslint$2.lintFiles(patterns);
112
+ const fixed = [];
113
+ const unresolved = [];
114
+ const flaggedResults = results.filter((result) => result.messages.some((message) => isAuthProtectionRule(message.ruleId)));
115
+ options.onScanComplete?.(flaggedResults.length);
116
+ for (const result of flaggedResults) {
117
+ const ruleMessages = result.messages.filter((message) => isAuthProtectionRule(message.ruleId));
118
+ if (ruleMessages.some((message) => message.suggestions?.some((s) => s.messageId === SUGGESTION_MESSAGE_ID) ?? false)) {
119
+ const source = await (0, node_fs_promises.readFile)(result.filePath, "utf8");
120
+ const { output, applied } = await applyFileFixes(eslint$2, result.filePath, source);
121
+ if (applied > 0) {
122
+ if (!dryRun) await (0, node_fs_promises.writeFile)(result.filePath, output, "utf8");
123
+ const fixedFile = {
124
+ filePath: result.filePath,
125
+ protections: applied
126
+ };
127
+ fixed.push(fixedFile);
128
+ options.onFileFixed?.(fixedFile);
129
+ }
130
+ }
131
+ const issues = ruleMessages.filter((message) => UNFIXABLE_MESSAGE_IDS.has(message.messageId ?? "")).map((message) => ({
132
+ line: message.line,
133
+ column: message.column,
134
+ message: message.message
135
+ }));
136
+ if (issues.length > 0) unresolved.push({
137
+ filePath: result.filePath,
138
+ issues
139
+ });
140
+ }
141
+ return {
142
+ fixed,
143
+ unresolved
144
+ };
145
+ }
146
+
147
+ //#endregion
148
+ exports.fixAuthProtection = fixAuthProtection;
149
+ //# sourceMappingURL=fix-auth-protection.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fix-auth-protection.js","names":["eslint","ESLint"],"sources":["../../src/next/fix-auth-protection.ts"],"sourcesContent":["/**\n * Programmatic auto-fixer for the `require-auth-protection` rule.\n *\n * The rule exposes its `await auth.protect()` insertion as an opt-in\n * *suggestion* rather than an autofix, so `eslint --fix` deliberately leaves it\n * alone (adding a protection check changes runtime behavior). This runner lets\n * you apply those suggestions in bulk on demand — from a script via\n * `fixAuthProtection()` or from the terminal via the\n * `clerk-next-fix-auth-protection` command.\n *\n * It works by linting with the consumer's own ESLint config (so the\n * protected/public folder globs are honored) and applying the rule's\n * `addAuthProtect` suggestion to each flagged resource. Resources the rule\n * cannot safely fix (imported/wrapped exports, unacknowledged mixed-scope\n * layouts) are reported back as `unresolved` for manual follow-up.\n */\n\nimport { readFile, writeFile } from 'node:fs/promises';\n\nimport { ESLint, type Linter } from 'eslint';\n\nconst RULE_NAME = 'require-auth-protection';\nconst SUGGESTION_MESSAGE_ID = 'addAuthProtect';\nconst UNFIXABLE_MESSAGE_IDS = new Set(['exportImported', 'unverifiableExport', 'unlistedMixedScopeLayout']);\n\nexport interface FixAuthProtectionOptions {\n /** File, directory, or glob patterns to scan. Defaults to `['.']`. */\n patterns?: string[];\n /** Working directory ESLint resolves config and files against. Defaults to `process.cwd()`. */\n cwd?: string;\n /** Compute the changes without writing them to disk. */\n dryRun?: boolean;\n /**\n * Advanced/escape hatch: a pre-configured ESLint instance to lint with. When\n * omitted, a default `new ESLint({ cwd })` is used, which discovers the\n * consumer's flat config. Mainly useful for tests.\n */\n eslint?: ESLint;\n /**\n * Called before scanning with the path to the ESLint config file that will be\n * used (or `undefined` when none is found / an instance was injected).\n */\n onConfigResolved?: (configFilePath: string | undefined) => void;\n /**\n * Called once linting finishes and per-file fixing begins, with the number of\n * files that have flagged resources. Useful for reporting progress, since the\n * initial lint can be slow on large projects.\n */\n onScanComplete?: (fileCount: number) => void;\n /** Called after each file is fixed (or, in `dryRun`, would be fixed). */\n onFileFixed?: (file: FixedFile) => void;\n}\n\nexport interface FixedFile {\n filePath: string;\n /** Number of resources that had `await auth.protect()` applied. */\n protections: number;\n}\n\nexport interface UnresolvedIssue {\n line: number;\n column: number;\n message: string;\n}\n\nexport interface UnresolvedFile {\n filePath: string;\n issues: UnresolvedIssue[];\n}\n\nexport interface FixAuthProtectionResult {\n /** Files that were (or, in `dryRun`, would be) modified. */\n fixed: FixedFile[];\n /** Files with flagged resources that have no safe automatic fix. */\n unresolved: UnresolvedFile[];\n}\n\ntype Fix = { range: [number, number]; text: string };\n\nfunction isAuthProtectionRule(ruleId: string | null): boolean {\n // The plugin can be registered under any namespace (e.g. `@clerk/next/...`),\n // so match on the rule name rather than a fixed, fully-qualified id.\n return ruleId === RULE_NAME || (ruleId?.endsWith(`/${RULE_NAME}`) ?? false);\n}\n\nfunction collectSuggestionFixes(messages: Linter.LintMessage[]): Fix[] {\n const fixes: Fix[] = [];\n for (const message of messages) {\n if (!isAuthProtectionRule(message.ruleId)) {\n continue;\n }\n const suggestion = message.suggestions?.find(s => s.messageId === SUGGESTION_MESSAGE_ID);\n if (suggestion?.fix) {\n fixes.push({ range: [suggestion.fix.range[0], suggestion.fix.range[1]], text: suggestion.fix.text });\n }\n }\n return fixes;\n}\n\n/**\n * Apply as many non-overlapping fixes as possible in a single pass, mirroring\n * ESLint's own `SourceCodeFixer`: sort by position and skip any fix that starts\n * before the previous one ended. Overlapping fixes are left for a later pass.\n */\nfunction applyFixes(source: string, fixes: Fix[]): { output: string; applied: number } {\n const sorted = [...fixes].sort((a, b) => a.range[0] - b.range[0] || a.range[1] - b.range[1]);\n let output = '';\n let lastPos = 0;\n let applied = 0;\n for (const fix of sorted) {\n const [start, end] = fix.range;\n if (start < lastPos) {\n continue;\n }\n output += source.slice(lastPos, start) + fix.text;\n lastPos = end;\n applied++;\n }\n output += source.slice(lastPos);\n return { output, applied };\n}\n\nasync function applyFileFixes(\n eslint: ESLint,\n filePath: string,\n source: string,\n): Promise<{ output: string; applied: number }> {\n // Applying a file's suggestions should converge in at most two passes: the first pass\n // fixes one resource and adds the shared top-of-file `auth` import, after which\n // every remaining resource is independent and applied in the second pass.\n // We allow up to 10 passes to allow for unaccounted for edge cases, or future\n // changes to the fixer, but throw an error if it fails to converge.\n const MAX_FIX_PASSES = 10;\n\n let current = source;\n let total = 0;\n let passes = 0;\n // Run one extra time so we can throw an error if the fixes don't converge.\n for (let i = 0; i < MAX_FIX_PASSES + 1; i += 1) {\n const [result] = await eslint.lintText(current, { filePath });\n if (!result) {\n break;\n }\n const fixes = collectSuggestionFixes(result.messages);\n if (fixes.length === 0) {\n break;\n }\n if (passes >= MAX_FIX_PASSES) {\n throw new Error(\n `Auth-protect fixes for ${filePath} did not converge after ${MAX_FIX_PASSES} passes. ` +\n 'This is unexpected; please report it at https://github.com/clerk/javascript/issues.',\n );\n }\n const { output, applied } = applyFixes(current, fixes);\n if (applied === 0) {\n break;\n }\n current = output;\n total += applied;\n passes += 1;\n }\n return { output: current, applied: total };\n}\n\n/**\n * Lint the given patterns with the consumer's ESLint config and apply the\n * `require-auth-protection` rule's `await auth.protect()` suggestions to every\n * resource it can safely fix.\n */\nexport async function fixAuthProtection(options: FixAuthProtectionOptions = {}): Promise<FixAuthProtectionResult> {\n const cwd = options.cwd ?? process.cwd();\n const patterns = options.patterns && options.patterns.length > 0 ? options.patterns : ['.'];\n const dryRun = options.dryRun ?? false;\n\n // Only run our rule. The consumer's config (and its protected/public globs)\n // still applies, but skipping every other rule avoids the cost of linting the\n // whole project with the full ruleset on each pass.\n const eslint = options.eslint ?? new ESLint({ cwd, ruleFilter: ({ ruleId }) => isAuthProtectionRule(ruleId) });\n\n if (options.onConfigResolved) {\n let configFile: string | undefined;\n try {\n configFile = await eslint.findConfigFile();\n } catch {\n configFile = undefined;\n }\n options.onConfigResolved(configFile);\n }\n\n const results = await eslint.lintFiles(patterns);\n\n const fixed: FixedFile[] = [];\n const unresolved: UnresolvedFile[] = [];\n\n const flaggedResults = results.filter(result =>\n result.messages.some(message => isAuthProtectionRule(message.ruleId)),\n );\n options.onScanComplete?.(flaggedResults.length);\n\n for (const result of flaggedResults) {\n const ruleMessages = result.messages.filter(message => isAuthProtectionRule(message.ruleId));\n\n const hasFixable = ruleMessages.some(\n message => message.suggestions?.some(s => s.messageId === SUGGESTION_MESSAGE_ID) ?? false,\n );\n if (hasFixable) {\n const source = await readFile(result.filePath, 'utf8');\n const { output, applied } = await applyFileFixes(eslint, result.filePath, source);\n if (applied > 0) {\n if (!dryRun) {\n await writeFile(result.filePath, output, 'utf8');\n }\n const fixedFile = { filePath: result.filePath, protections: applied };\n fixed.push(fixedFile);\n options.onFileFixed?.(fixedFile);\n }\n }\n\n // Messages without a suggestion (imported/wrapped exports, mixed-scope\n // layouts) need a human; surface them so they aren't silently skipped.\n const issues = ruleMessages\n .filter(message => UNFIXABLE_MESSAGE_IDS.has(message.messageId ?? ''))\n .map(message => ({ line: message.line, column: message.column, message: message.message }));\n if (issues.length > 0) {\n unresolved.push({ filePath: result.filePath, issues });\n }\n }\n\n return { fixed, unresolved };\n}\n"],"mappings":"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"}
@@ -0,0 +1,148 @@
1
+ import { readFile, writeFile } from "node:fs/promises";
2
+ import { ESLint } from "eslint";
3
+
4
+ //#region src/next/fix-auth-protection.ts
5
+ /**
6
+ * Programmatic auto-fixer for the `require-auth-protection` rule.
7
+ *
8
+ * The rule exposes its `await auth.protect()` insertion as an opt-in
9
+ * *suggestion* rather than an autofix, so `eslint --fix` deliberately leaves it
10
+ * alone (adding a protection check changes runtime behavior). This runner lets
11
+ * you apply those suggestions in bulk on demand — from a script via
12
+ * `fixAuthProtection()` or from the terminal via the
13
+ * `clerk-next-fix-auth-protection` command.
14
+ *
15
+ * It works by linting with the consumer's own ESLint config (so the
16
+ * protected/public folder globs are honored) and applying the rule's
17
+ * `addAuthProtect` suggestion to each flagged resource. Resources the rule
18
+ * cannot safely fix (imported/wrapped exports, unacknowledged mixed-scope
19
+ * layouts) are reported back as `unresolved` for manual follow-up.
20
+ */
21
+ const RULE_NAME = "require-auth-protection";
22
+ const SUGGESTION_MESSAGE_ID = "addAuthProtect";
23
+ const UNFIXABLE_MESSAGE_IDS = new Set([
24
+ "exportImported",
25
+ "unverifiableExport",
26
+ "unlistedMixedScopeLayout"
27
+ ]);
28
+ function isAuthProtectionRule(ruleId) {
29
+ return ruleId === RULE_NAME || (ruleId?.endsWith(`/${RULE_NAME}`) ?? false);
30
+ }
31
+ function collectSuggestionFixes(messages) {
32
+ const fixes = [];
33
+ for (const message of messages) {
34
+ if (!isAuthProtectionRule(message.ruleId)) continue;
35
+ const suggestion = message.suggestions?.find((s) => s.messageId === SUGGESTION_MESSAGE_ID);
36
+ if (suggestion?.fix) fixes.push({
37
+ range: [suggestion.fix.range[0], suggestion.fix.range[1]],
38
+ text: suggestion.fix.text
39
+ });
40
+ }
41
+ return fixes;
42
+ }
43
+ /**
44
+ * Apply as many non-overlapping fixes as possible in a single pass, mirroring
45
+ * ESLint's own `SourceCodeFixer`: sort by position and skip any fix that starts
46
+ * before the previous one ended. Overlapping fixes are left for a later pass.
47
+ */
48
+ function applyFixes(source, fixes) {
49
+ const sorted = [...fixes].sort((a, b) => a.range[0] - b.range[0] || a.range[1] - b.range[1]);
50
+ let output = "";
51
+ let lastPos = 0;
52
+ let applied = 0;
53
+ for (const fix of sorted) {
54
+ const [start, end] = fix.range;
55
+ if (start < lastPos) continue;
56
+ output += source.slice(lastPos, start) + fix.text;
57
+ lastPos = end;
58
+ applied++;
59
+ }
60
+ output += source.slice(lastPos);
61
+ return {
62
+ output,
63
+ applied
64
+ };
65
+ }
66
+ async function applyFileFixes(eslint, filePath, source) {
67
+ const MAX_FIX_PASSES = 10;
68
+ let current = source;
69
+ let total = 0;
70
+ let passes = 0;
71
+ for (let i = 0; i < 11; i += 1) {
72
+ const [result] = await eslint.lintText(current, { filePath });
73
+ if (!result) break;
74
+ const fixes = collectSuggestionFixes(result.messages);
75
+ if (fixes.length === 0) break;
76
+ if (passes >= MAX_FIX_PASSES) throw new Error(`Auth-protect fixes for ${filePath} did not converge after ${MAX_FIX_PASSES} passes. This is unexpected; please report it at https://github.com/clerk/javascript/issues.`);
77
+ const { output, applied } = applyFixes(current, fixes);
78
+ if (applied === 0) break;
79
+ current = output;
80
+ total += applied;
81
+ passes += 1;
82
+ }
83
+ return {
84
+ output: current,
85
+ applied: total
86
+ };
87
+ }
88
+ /**
89
+ * Lint the given patterns with the consumer's ESLint config and apply the
90
+ * `require-auth-protection` rule's `await auth.protect()` suggestions to every
91
+ * resource it can safely fix.
92
+ */
93
+ async function fixAuthProtection(options = {}) {
94
+ const cwd = options.cwd ?? process.cwd();
95
+ const patterns = options.patterns && options.patterns.length > 0 ? options.patterns : ["."];
96
+ const dryRun = options.dryRun ?? false;
97
+ const eslint = options.eslint ?? new ESLint({
98
+ cwd,
99
+ ruleFilter: ({ ruleId }) => isAuthProtectionRule(ruleId)
100
+ });
101
+ if (options.onConfigResolved) {
102
+ let configFile;
103
+ try {
104
+ configFile = await eslint.findConfigFile();
105
+ } catch {
106
+ configFile = void 0;
107
+ }
108
+ options.onConfigResolved(configFile);
109
+ }
110
+ const results = await eslint.lintFiles(patterns);
111
+ const fixed = [];
112
+ const unresolved = [];
113
+ const flaggedResults = results.filter((result) => result.messages.some((message) => isAuthProtectionRule(message.ruleId)));
114
+ options.onScanComplete?.(flaggedResults.length);
115
+ for (const result of flaggedResults) {
116
+ const ruleMessages = result.messages.filter((message) => isAuthProtectionRule(message.ruleId));
117
+ if (ruleMessages.some((message) => message.suggestions?.some((s) => s.messageId === SUGGESTION_MESSAGE_ID) ?? false)) {
118
+ const source = await readFile(result.filePath, "utf8");
119
+ const { output, applied } = await applyFileFixes(eslint, result.filePath, source);
120
+ if (applied > 0) {
121
+ if (!dryRun) await writeFile(result.filePath, output, "utf8");
122
+ const fixedFile = {
123
+ filePath: result.filePath,
124
+ protections: applied
125
+ };
126
+ fixed.push(fixedFile);
127
+ options.onFileFixed?.(fixedFile);
128
+ }
129
+ }
130
+ const issues = ruleMessages.filter((message) => UNFIXABLE_MESSAGE_IDS.has(message.messageId ?? "")).map((message) => ({
131
+ line: message.line,
132
+ column: message.column,
133
+ message: message.message
134
+ }));
135
+ if (issues.length > 0) unresolved.push({
136
+ filePath: result.filePath,
137
+ issues
138
+ });
139
+ }
140
+ return {
141
+ fixed,
142
+ unresolved
143
+ };
144
+ }
145
+
146
+ //#endregion
147
+ export { fixAuthProtection };
148
+ //# sourceMappingURL=fix-auth-protection.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fix-auth-protection.mjs","names":[],"sources":["../../src/next/fix-auth-protection.ts"],"sourcesContent":["/**\n * Programmatic auto-fixer for the `require-auth-protection` rule.\n *\n * The rule exposes its `await auth.protect()` insertion as an opt-in\n * *suggestion* rather than an autofix, so `eslint --fix` deliberately leaves it\n * alone (adding a protection check changes runtime behavior). This runner lets\n * you apply those suggestions in bulk on demand — from a script via\n * `fixAuthProtection()` or from the terminal via the\n * `clerk-next-fix-auth-protection` command.\n *\n * It works by linting with the consumer's own ESLint config (so the\n * protected/public folder globs are honored) and applying the rule's\n * `addAuthProtect` suggestion to each flagged resource. Resources the rule\n * cannot safely fix (imported/wrapped exports, unacknowledged mixed-scope\n * layouts) are reported back as `unresolved` for manual follow-up.\n */\n\nimport { readFile, writeFile } from 'node:fs/promises';\n\nimport { ESLint, type Linter } from 'eslint';\n\nconst RULE_NAME = 'require-auth-protection';\nconst SUGGESTION_MESSAGE_ID = 'addAuthProtect';\nconst UNFIXABLE_MESSAGE_IDS = new Set(['exportImported', 'unverifiableExport', 'unlistedMixedScopeLayout']);\n\nexport interface FixAuthProtectionOptions {\n /** File, directory, or glob patterns to scan. Defaults to `['.']`. */\n patterns?: string[];\n /** Working directory ESLint resolves config and files against. Defaults to `process.cwd()`. */\n cwd?: string;\n /** Compute the changes without writing them to disk. */\n dryRun?: boolean;\n /**\n * Advanced/escape hatch: a pre-configured ESLint instance to lint with. When\n * omitted, a default `new ESLint({ cwd })` is used, which discovers the\n * consumer's flat config. Mainly useful for tests.\n */\n eslint?: ESLint;\n /**\n * Called before scanning with the path to the ESLint config file that will be\n * used (or `undefined` when none is found / an instance was injected).\n */\n onConfigResolved?: (configFilePath: string | undefined) => void;\n /**\n * Called once linting finishes and per-file fixing begins, with the number of\n * files that have flagged resources. Useful for reporting progress, since the\n * initial lint can be slow on large projects.\n */\n onScanComplete?: (fileCount: number) => void;\n /** Called after each file is fixed (or, in `dryRun`, would be fixed). */\n onFileFixed?: (file: FixedFile) => void;\n}\n\nexport interface FixedFile {\n filePath: string;\n /** Number of resources that had `await auth.protect()` applied. */\n protections: number;\n}\n\nexport interface UnresolvedIssue {\n line: number;\n column: number;\n message: string;\n}\n\nexport interface UnresolvedFile {\n filePath: string;\n issues: UnresolvedIssue[];\n}\n\nexport interface FixAuthProtectionResult {\n /** Files that were (or, in `dryRun`, would be) modified. */\n fixed: FixedFile[];\n /** Files with flagged resources that have no safe automatic fix. */\n unresolved: UnresolvedFile[];\n}\n\ntype Fix = { range: [number, number]; text: string };\n\nfunction isAuthProtectionRule(ruleId: string | null): boolean {\n // The plugin can be registered under any namespace (e.g. `@clerk/next/...`),\n // so match on the rule name rather than a fixed, fully-qualified id.\n return ruleId === RULE_NAME || (ruleId?.endsWith(`/${RULE_NAME}`) ?? false);\n}\n\nfunction collectSuggestionFixes(messages: Linter.LintMessage[]): Fix[] {\n const fixes: Fix[] = [];\n for (const message of messages) {\n if (!isAuthProtectionRule(message.ruleId)) {\n continue;\n }\n const suggestion = message.suggestions?.find(s => s.messageId === SUGGESTION_MESSAGE_ID);\n if (suggestion?.fix) {\n fixes.push({ range: [suggestion.fix.range[0], suggestion.fix.range[1]], text: suggestion.fix.text });\n }\n }\n return fixes;\n}\n\n/**\n * Apply as many non-overlapping fixes as possible in a single pass, mirroring\n * ESLint's own `SourceCodeFixer`: sort by position and skip any fix that starts\n * before the previous one ended. Overlapping fixes are left for a later pass.\n */\nfunction applyFixes(source: string, fixes: Fix[]): { output: string; applied: number } {\n const sorted = [...fixes].sort((a, b) => a.range[0] - b.range[0] || a.range[1] - b.range[1]);\n let output = '';\n let lastPos = 0;\n let applied = 0;\n for (const fix of sorted) {\n const [start, end] = fix.range;\n if (start < lastPos) {\n continue;\n }\n output += source.slice(lastPos, start) + fix.text;\n lastPos = end;\n applied++;\n }\n output += source.slice(lastPos);\n return { output, applied };\n}\n\nasync function applyFileFixes(\n eslint: ESLint,\n filePath: string,\n source: string,\n): Promise<{ output: string; applied: number }> {\n // Applying a file's suggestions should converge in at most two passes: the first pass\n // fixes one resource and adds the shared top-of-file `auth` import, after which\n // every remaining resource is independent and applied in the second pass.\n // We allow up to 10 passes to allow for unaccounted for edge cases, or future\n // changes to the fixer, but throw an error if it fails to converge.\n const MAX_FIX_PASSES = 10;\n\n let current = source;\n let total = 0;\n let passes = 0;\n // Run one extra time so we can throw an error if the fixes don't converge.\n for (let i = 0; i < MAX_FIX_PASSES + 1; i += 1) {\n const [result] = await eslint.lintText(current, { filePath });\n if (!result) {\n break;\n }\n const fixes = collectSuggestionFixes(result.messages);\n if (fixes.length === 0) {\n break;\n }\n if (passes >= MAX_FIX_PASSES) {\n throw new Error(\n `Auth-protect fixes for ${filePath} did not converge after ${MAX_FIX_PASSES} passes. ` +\n 'This is unexpected; please report it at https://github.com/clerk/javascript/issues.',\n );\n }\n const { output, applied } = applyFixes(current, fixes);\n if (applied === 0) {\n break;\n }\n current = output;\n total += applied;\n passes += 1;\n }\n return { output: current, applied: total };\n}\n\n/**\n * Lint the given patterns with the consumer's ESLint config and apply the\n * `require-auth-protection` rule's `await auth.protect()` suggestions to every\n * resource it can safely fix.\n */\nexport async function fixAuthProtection(options: FixAuthProtectionOptions = {}): Promise<FixAuthProtectionResult> {\n const cwd = options.cwd ?? process.cwd();\n const patterns = options.patterns && options.patterns.length > 0 ? options.patterns : ['.'];\n const dryRun = options.dryRun ?? false;\n\n // Only run our rule. The consumer's config (and its protected/public globs)\n // still applies, but skipping every other rule avoids the cost of linting the\n // whole project with the full ruleset on each pass.\n const eslint = options.eslint ?? new ESLint({ cwd, ruleFilter: ({ ruleId }) => isAuthProtectionRule(ruleId) });\n\n if (options.onConfigResolved) {\n let configFile: string | undefined;\n try {\n configFile = await eslint.findConfigFile();\n } catch {\n configFile = undefined;\n }\n options.onConfigResolved(configFile);\n }\n\n const results = await eslint.lintFiles(patterns);\n\n const fixed: FixedFile[] = [];\n const unresolved: UnresolvedFile[] = [];\n\n const flaggedResults = results.filter(result =>\n result.messages.some(message => isAuthProtectionRule(message.ruleId)),\n );\n options.onScanComplete?.(flaggedResults.length);\n\n for (const result of flaggedResults) {\n const ruleMessages = result.messages.filter(message => isAuthProtectionRule(message.ruleId));\n\n const hasFixable = ruleMessages.some(\n message => message.suggestions?.some(s => s.messageId === SUGGESTION_MESSAGE_ID) ?? false,\n );\n if (hasFixable) {\n const source = await readFile(result.filePath, 'utf8');\n const { output, applied } = await applyFileFixes(eslint, result.filePath, source);\n if (applied > 0) {\n if (!dryRun) {\n await writeFile(result.filePath, output, 'utf8');\n }\n const fixedFile = { filePath: result.filePath, protections: applied };\n fixed.push(fixedFile);\n options.onFileFixed?.(fixedFile);\n }\n }\n\n // Messages without a suggestion (imported/wrapped exports, mixed-scope\n // layouts) need a human; surface them so they aren't silently skipped.\n const issues = ruleMessages\n .filter(message => UNFIXABLE_MESSAGE_IDS.has(message.messageId ?? ''))\n .map(message => ({ line: message.line, column: message.column, message: message.message }));\n if (issues.length > 0) {\n unresolved.push({ filePath: result.filePath, issues });\n }\n }\n\n return { fixed, unresolved };\n}\n"],"mappings":"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"}
@@ -0,0 +1,7 @@
1
+ import { ESLint } from "eslint";
2
+
3
+ //#region src/next/index.d.ts
4
+ declare const plugin: ESLint.Plugin;
5
+ //#endregion
6
+ export { plugin as default };
7
+ //# sourceMappingURL=next.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"next.d.mts","names":[],"sources":["../src/next/index.ts"],"mappings":";;;cAIM,MAAA,EAAQ,MAAA,CAAO,MAQpB"}
package/dist/next.d.ts ADDED
@@ -0,0 +1,6 @@
1
+ import { ESLint } from "eslint";
2
+
3
+ //#region src/next/index.d.ts
4
+ declare const plugin: ESLint.Plugin;
5
+ export = plugin;
6
+ //# sourceMappingURL=next.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"next.d.ts","names":[],"sources":["../src/next/index.ts"],"mappings":";;;cAIM,MAAA,EAAQ,MAAA,CAAO,MAQpB;AAAA"}