react-doctor 0.9.6 → 0.9.7-dev.13138a4
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/dist/build-section-divider-QykRnrlR.js +312 -0
- package/dist/ci-BvGqcDKc.js +7501 -0
- package/dist/cli.js +96 -10780
- package/dist/design-SGX69YQq.js +14 -0
- package/dist/{devtools-BQATi33M.js → devtools-DPRVRu3S.js} +3 -3
- package/dist/exit-gracefully-CddufLS0.js +47 -0
- package/dist/index.d.ts +4703 -4081
- package/dist/index.js +6070 -4387
- package/dist/inspect-DBJtHure.js +1443 -0
- package/dist/install-doctor-script-DxrteuEw.js +3251 -0
- package/dist/install-ffJ108LC.js +32 -0
- package/dist/install-github-workflow-BP6t3YBA.js +267 -0
- package/dist/install-react-doctor-BhfvyNZB.js +1154 -0
- package/dist/lsp.js +6183 -4512
- package/dist/print-branded-header-Cw5AlicW.js +22 -0
- package/dist/read-object-file-ITGVjFKp.js +24 -0
- package/dist/{exit-gracefully-DcDvSbzg.js → record-metric-CtlQma6P.js} +6252 -11955
- package/dist/report-error-DUoFKR7d.js +158 -0
- package/dist/report-workflow-result-D3GCowCN.js +279 -0
- package/dist/resolve-scope-gy72JH3h.js +339 -0
- package/dist/rules-6-d19pN1.js +581 -0
- package/dist/{run-scan-app-CeiP4spo.js → run-scan-app-Drlqxtzl.js} +36 -29
- package/dist/scan-C3Io41Dq.js +53 -0
- package/dist/should-fail-scan-gate-DImKUuu-.js +3075 -0
- package/dist/should-skip-prompts-CbIIaU8b.js +10 -0
- package/dist/unref-stdin-B5RnVeLa.js +120 -0
- package/dist/version-BFKBxkKV.js +21 -0
- package/dist/why-DO4CTn-R.js +21 -0
- package/package.json +6 -7
|
@@ -0,0 +1,581 @@
|
|
|
1
|
+
|
|
2
|
+
!function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="3482bd59-e632-5753-a94a-167622d04810")}catch(e){}}();
|
|
3
|
+
import { At as clearConfigCache, Et as buildRuleDocsUrl, G as COMPILER_CLEANUP_BUCKET, K as COMPILER_CLEANUP_RULE_KEYS, Nn as validateConfigTypes, Yt as getEquivalentRuleKeys, en as highlighter, ln as isSameRuleKey, mn as loadConfigWithSource, on as isPlainObject, t as recordCount, y as METRIC } from "./record-metric-CtlQma6P.js";
|
|
4
|
+
import { L as cliLogger, n as findNearestPackageDirectory } from "./install-doctor-script-DxrteuEw.js";
|
|
5
|
+
import { t as readObjectFile } from "./read-object-file-ITGVjFKp.js";
|
|
6
|
+
import * as path$1 from "node:path";
|
|
7
|
+
import * as fs$1 from "node:fs";
|
|
8
|
+
import { REACT_DOCTOR_RULES } from "oxlint-plugin-react-doctor/core";
|
|
9
|
+
import { generateCode, loadFile, writeFile } from "magicast";
|
|
10
|
+
import { getConfigFromVariableDeclaration, getDefaultExportOptions } from "magicast/helpers";
|
|
11
|
+
//#region src/cli/utils/rule-catalog.ts
|
|
12
|
+
const buildRuleCatalog = () => REACT_DOCTOR_RULES.map((entry) => ({
|
|
13
|
+
key: entry.key,
|
|
14
|
+
id: entry.id,
|
|
15
|
+
category: entry.rule.category ?? "Other",
|
|
16
|
+
defaultSeverity: entry.rule.severity,
|
|
17
|
+
framework: entry.rule.framework ?? "global",
|
|
18
|
+
tags: entry.rule.tags ?? [],
|
|
19
|
+
recommendation: entry.rule.recommendation,
|
|
20
|
+
defaultEnabled: entry.rule.defaultEnabled !== false
|
|
21
|
+
}));
|
|
22
|
+
/**
|
|
23
|
+
* Resolves a user-supplied rule reference to a catalog entry. Accepts the
|
|
24
|
+
* fully-qualified key (`react-doctor/no-danger`), the bare id (`no-danger`),
|
|
25
|
+
* and legacy plugin keys (`react/no-danger`) via the shared alias map.
|
|
26
|
+
*/
|
|
27
|
+
const findRuleInCatalog = (catalog, ruleQuery) => {
|
|
28
|
+
const normalizedQuery = ruleQuery.trim();
|
|
29
|
+
if (normalizedQuery.length === 0) return void 0;
|
|
30
|
+
const directMatch = catalog.find((entry) => entry.key === normalizedQuery || entry.id === normalizedQuery);
|
|
31
|
+
if (directMatch) return directMatch;
|
|
32
|
+
return catalog.find((entry) => isSameRuleKey(entry.key, normalizedQuery));
|
|
33
|
+
};
|
|
34
|
+
const listRuleCategories = (catalog) => [...new Set(catalog.map((entry) => entry.category))].sort();
|
|
35
|
+
const listRuleTags = (catalog) => [...new Set(catalog.flatMap((entry) => [...entry.tags]))].sort();
|
|
36
|
+
//#endregion
|
|
37
|
+
//#region src/cli/utils/render-rule-catalog.ts
|
|
38
|
+
const SEVERITY_COLUMN_WIDTH_CHARS = 6;
|
|
39
|
+
const colorizeSeverity = (severity, text) => {
|
|
40
|
+
if (severity === "error") return highlighter.error(text);
|
|
41
|
+
if (severity === "warn") return highlighter.warn(text);
|
|
42
|
+
return highlighter.gray(text);
|
|
43
|
+
};
|
|
44
|
+
const formatSourceNote = (effective) => effective.source === "default" ? highlighter.dim("(default)") : highlighter.dim(`(${effective.source})`);
|
|
45
|
+
const renderRuleCatalog = (rows) => {
|
|
46
|
+
if (rows.length === 0) return highlighter.dim("No rules match the given filters.");
|
|
47
|
+
const rowsByCategory = /* @__PURE__ */ new Map();
|
|
48
|
+
for (const row of rows) {
|
|
49
|
+
const bucket = rowsByCategory.get(row.entry.category) ?? [];
|
|
50
|
+
bucket.push(row);
|
|
51
|
+
rowsByCategory.set(row.entry.category, bucket);
|
|
52
|
+
}
|
|
53
|
+
const lines = [];
|
|
54
|
+
for (const category of [...rowsByCategory.keys()].sort()) {
|
|
55
|
+
const categoryRows = (rowsByCategory.get(category) ?? []).sort((leftRow, rightRow) => leftRow.entry.key.localeCompare(rightRow.entry.key));
|
|
56
|
+
lines.push(highlighter.bold(`${category} ${highlighter.dim(`(${categoryRows.length})`)}`));
|
|
57
|
+
for (const row of categoryRows) {
|
|
58
|
+
const severityBadge = colorizeSeverity(row.effective.value, row.effective.value.padEnd(SEVERITY_COLUMN_WIDTH_CHARS));
|
|
59
|
+
const tagSuffix = row.entry.tags.length > 0 ? highlighter.dim(` [${row.entry.tags.join(", ")}]`) : "";
|
|
60
|
+
lines.push(` ${severityBadge} ${row.entry.key} ${formatSourceNote(row.effective)}${tagSuffix}`);
|
|
61
|
+
}
|
|
62
|
+
lines.push("");
|
|
63
|
+
}
|
|
64
|
+
lines.push(highlighter.dim(`${rows.length} rule${rows.length === 1 ? "" : "s"} shown.`));
|
|
65
|
+
return lines.join("\n");
|
|
66
|
+
};
|
|
67
|
+
const DETAIL_LABEL_COLUMN_WIDTH_CHARS = 18;
|
|
68
|
+
const formatDetailRow = (label, value) => ` ${highlighter.dim(label.padEnd(DETAIL_LABEL_COLUMN_WIDTH_CHARS))}${value}`;
|
|
69
|
+
const renderRuleExplanation = (row) => {
|
|
70
|
+
const { entry, effective } = row;
|
|
71
|
+
const lines = [highlighter.bold(entry.key), ""];
|
|
72
|
+
lines.push(formatDetailRow("Category", entry.category));
|
|
73
|
+
lines.push(formatDetailRow("Default severity", entry.defaultSeverity));
|
|
74
|
+
lines.push(formatDetailRow("Current severity", `${colorizeSeverity(effective.value, effective.value)} ${formatSourceNote(effective)}`));
|
|
75
|
+
lines.push(formatDetailRow("Framework", entry.framework));
|
|
76
|
+
lines.push(formatDetailRow("Tags", entry.tags.length > 0 ? entry.tags.join(", ") : "none"));
|
|
77
|
+
lines.push(formatDetailRow("Default enabled", entry.defaultEnabled ? "yes" : "no (opt-in)"));
|
|
78
|
+
lines.push("");
|
|
79
|
+
lines.push(highlighter.bold("Why it matters"));
|
|
80
|
+
lines.push(` ${entry.recommendation ?? "No additional guidance recorded for this rule yet."}`);
|
|
81
|
+
lines.push("");
|
|
82
|
+
lines.push(highlighter.bold("Configure"));
|
|
83
|
+
lines.push(highlighter.dim(` react-doctor rules disable ${entry.key}`));
|
|
84
|
+
lines.push(highlighter.dim(` react-doctor rules enable ${entry.key} --severity error`));
|
|
85
|
+
lines.push(highlighter.dim(` react-doctor rules set ${entry.key} warn`));
|
|
86
|
+
lines.push("");
|
|
87
|
+
lines.push(highlighter.bold("Learn more"));
|
|
88
|
+
lines.push(highlighter.dim(` ${buildRuleDocsUrl("react-doctor", entry.id)}`));
|
|
89
|
+
return lines.join("\n");
|
|
90
|
+
};
|
|
91
|
+
//#endregion
|
|
92
|
+
//#region src/cli/utils/rule-config-file.ts
|
|
93
|
+
const NEW_CONFIG_FILENAME = "doctor.config.json";
|
|
94
|
+
const PACKAGE_JSON_CONFIG_KEY = "reactDoctor";
|
|
95
|
+
const JSON_INDENT_SPACES = 2;
|
|
96
|
+
const MANAGED_KEYS = [
|
|
97
|
+
"rules",
|
|
98
|
+
"categories",
|
|
99
|
+
"ignore"
|
|
100
|
+
];
|
|
101
|
+
/**
|
|
102
|
+
* Decides where a rule-config mutation should be written. Discovery
|
|
103
|
+
* reuses `loadConfigWithSource` (the loader the scan uses) so edits land
|
|
104
|
+
* in the file the scan reads — `doctor.config.{ts,js,…}` is preferred,
|
|
105
|
+
* then `package.json#reactDoctor`. When nothing exists, a fresh
|
|
106
|
+
* `doctor.config.json` is targeted at `projectRoot`. Data configs are
|
|
107
|
+
* re-read raw so unrelated fields round-trip untouched.
|
|
108
|
+
*/
|
|
109
|
+
const resolveRuleConfigTarget = async (projectRoot) => {
|
|
110
|
+
clearConfigCache();
|
|
111
|
+
const loaded = await loadConfigWithSource(projectRoot);
|
|
112
|
+
if (loaded) {
|
|
113
|
+
if (loaded.format === "package-json") {
|
|
114
|
+
const embedded = (readObjectFile(loaded.configFilePath) ?? {})[PACKAGE_JSON_CONFIG_KEY];
|
|
115
|
+
return {
|
|
116
|
+
format: "package-json",
|
|
117
|
+
filePath: loaded.configFilePath,
|
|
118
|
+
directory: loaded.sourceDirectory,
|
|
119
|
+
exists: true,
|
|
120
|
+
config: isPlainObject(embedded) ? embedded : {}
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
if (loaded.format === "json") {
|
|
124
|
+
if (path$1.basename(loaded.configFilePath) === "react-doctor.config.json") {
|
|
125
|
+
const legacyConfig = readObjectFile(loaded.configFilePath) ?? {};
|
|
126
|
+
delete legacyConfig.$schema;
|
|
127
|
+
return {
|
|
128
|
+
format: "json",
|
|
129
|
+
filePath: path$1.join(loaded.sourceDirectory, NEW_CONFIG_FILENAME),
|
|
130
|
+
directory: loaded.sourceDirectory,
|
|
131
|
+
exists: false,
|
|
132
|
+
config: legacyConfig,
|
|
133
|
+
migratedFromFilePath: loaded.configFilePath
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
return {
|
|
137
|
+
format: "json",
|
|
138
|
+
filePath: loaded.configFilePath,
|
|
139
|
+
directory: loaded.sourceDirectory,
|
|
140
|
+
exists: true,
|
|
141
|
+
config: readObjectFile(loaded.configFilePath) ?? {}
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
return {
|
|
145
|
+
format: "module",
|
|
146
|
+
filePath: loaded.configFilePath,
|
|
147
|
+
directory: loaded.sourceDirectory,
|
|
148
|
+
exists: true,
|
|
149
|
+
config: loaded.config
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
return {
|
|
153
|
+
format: "json",
|
|
154
|
+
filePath: path$1.join(projectRoot, NEW_CONFIG_FILENAME),
|
|
155
|
+
directory: projectRoot,
|
|
156
|
+
exists: false,
|
|
157
|
+
config: {}
|
|
158
|
+
};
|
|
159
|
+
};
|
|
160
|
+
const writeJsonConfig = (filePath, nextConfig) => {
|
|
161
|
+
const { $schema, ...rest } = nextConfig;
|
|
162
|
+
const serialized = JSON.stringify({
|
|
163
|
+
$schema: $schema ?? "https://react.doctor/schema/config.json",
|
|
164
|
+
...rest
|
|
165
|
+
}, null, JSON_INDENT_SPACES);
|
|
166
|
+
fs$1.writeFileSync(filePath, `${serialized}\n`);
|
|
167
|
+
};
|
|
168
|
+
const writePackageJsonConfig = (filePath, nextConfig) => {
|
|
169
|
+
const packageJson = readObjectFile(filePath) ?? {};
|
|
170
|
+
const serialized = JSON.stringify({
|
|
171
|
+
...packageJson,
|
|
172
|
+
[PACKAGE_JSON_CONFIG_KEY]: nextConfig
|
|
173
|
+
}, null, JSON_INDENT_SPACES);
|
|
174
|
+
fs$1.writeFileSync(filePath, `${serialized}\n`);
|
|
175
|
+
};
|
|
176
|
+
const syncManagedKeys = (target, nextConfig) => {
|
|
177
|
+
for (const key of MANAGED_KEYS) {
|
|
178
|
+
const value = nextConfig[key];
|
|
179
|
+
if (value === void 0) {
|
|
180
|
+
if (target[key] !== void 0) delete target[key];
|
|
181
|
+
} else target[key] = value;
|
|
182
|
+
}
|
|
183
|
+
};
|
|
184
|
+
const assignNodeSource = (owner, key, code) => {
|
|
185
|
+
owner[key] = code;
|
|
186
|
+
};
|
|
187
|
+
const editVariableDeclarationConfig = (declaration, config, nextConfig) => {
|
|
188
|
+
syncManagedKeys(config, nextConfig);
|
|
189
|
+
const initializer = declaration.init;
|
|
190
|
+
if (!initializer) return false;
|
|
191
|
+
const generatedSource = generateCode(config).code;
|
|
192
|
+
if (initializer.type === "ObjectExpression") {
|
|
193
|
+
assignNodeSource(declaration, "init", generatedSource);
|
|
194
|
+
return true;
|
|
195
|
+
}
|
|
196
|
+
if (initializer.type === "TSSatisfiesExpression" && initializer.expression.type === "ObjectExpression") {
|
|
197
|
+
assignNodeSource(initializer, "expression", generatedSource);
|
|
198
|
+
return true;
|
|
199
|
+
}
|
|
200
|
+
return false;
|
|
201
|
+
};
|
|
202
|
+
const writeModuleConfig = async (filePath, nextConfig) => {
|
|
203
|
+
try {
|
|
204
|
+
const module = await loadFile(filePath);
|
|
205
|
+
if (module.exports.default?.$type === "identifier") {
|
|
206
|
+
const { declaration, config } = getConfigFromVariableDeclaration(module);
|
|
207
|
+
if (!config || !editVariableDeclarationConfig(declaration, config, nextConfig)) return false;
|
|
208
|
+
} else syncManagedKeys(getDefaultExportOptions(module), nextConfig);
|
|
209
|
+
await writeFile(module, filePath);
|
|
210
|
+
return true;
|
|
211
|
+
} catch {
|
|
212
|
+
return false;
|
|
213
|
+
}
|
|
214
|
+
};
|
|
215
|
+
const writeRuleConfig = async (target, nextConfig) => {
|
|
216
|
+
if (target.format === "module") {
|
|
217
|
+
const written = await writeModuleConfig(target.filePath, nextConfig);
|
|
218
|
+
if (written) clearConfigCache();
|
|
219
|
+
return { written };
|
|
220
|
+
}
|
|
221
|
+
if (target.format === "package-json") writePackageJsonConfig(target.filePath, nextConfig);
|
|
222
|
+
else {
|
|
223
|
+
writeJsonConfig(target.filePath, nextConfig);
|
|
224
|
+
if (target.migratedFromFilePath) fs$1.rmSync(target.migratedFromFilePath, { force: true });
|
|
225
|
+
}
|
|
226
|
+
clearConfigCache();
|
|
227
|
+
return { written: true };
|
|
228
|
+
};
|
|
229
|
+
//#endregion
|
|
230
|
+
//#region src/cli/utils/resolve-effective-rule-severity.ts
|
|
231
|
+
/**
|
|
232
|
+
* Resolves what a rule will actually do under the current config without
|
|
233
|
+
* running a scan. `ignore.tags` is a pre-lint gate: a rule carrying an
|
|
234
|
+
* ignored tag is dropped (via `shouldEnableRule`) before any severity is
|
|
235
|
+
* read, so it wins over every override. Among rules that survive the gate,
|
|
236
|
+
* the scanner's order is `rules` > `categories` > `buckets` > the registry
|
|
237
|
+
* default.
|
|
238
|
+
*/
|
|
239
|
+
const resolveEffectiveRuleSeverity = (config, entry) => {
|
|
240
|
+
const ignoredTags = config?.ignore?.tags ?? [];
|
|
241
|
+
if (entry.tags.some((tag) => ignoredTags.includes(tag))) return {
|
|
242
|
+
value: "off",
|
|
243
|
+
source: "tag"
|
|
244
|
+
};
|
|
245
|
+
const ruleOverrides = config?.rules ?? {};
|
|
246
|
+
for (const equivalentKey of getEquivalentRuleKeys(entry.key)) {
|
|
247
|
+
const override = ruleOverrides[equivalentKey];
|
|
248
|
+
if (override !== void 0) return {
|
|
249
|
+
value: override,
|
|
250
|
+
source: "rule"
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
if (!entry.defaultEnabled) return {
|
|
254
|
+
value: "off",
|
|
255
|
+
source: "default"
|
|
256
|
+
};
|
|
257
|
+
const categoryOverride = config?.categories?.[entry.category];
|
|
258
|
+
if (categoryOverride !== void 0) return {
|
|
259
|
+
value: categoryOverride,
|
|
260
|
+
source: "category"
|
|
261
|
+
};
|
|
262
|
+
if (COMPILER_CLEANUP_RULE_KEYS.has(entry.key)) {
|
|
263
|
+
const bucketOverride = config?.buckets?.[COMPILER_CLEANUP_BUCKET];
|
|
264
|
+
if (bucketOverride !== void 0) return {
|
|
265
|
+
value: bucketOverride,
|
|
266
|
+
source: "bucket"
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
return {
|
|
270
|
+
value: entry.defaultEnabled ? entry.defaultSeverity : "off",
|
|
271
|
+
source: "default"
|
|
272
|
+
};
|
|
273
|
+
};
|
|
274
|
+
//#endregion
|
|
275
|
+
//#region src/cli/utils/update-rule-config.ts
|
|
276
|
+
/**
|
|
277
|
+
* Sets a per-rule severity, replacing any existing entry for the same
|
|
278
|
+
* rule (including legacy-aliased keys, so a config still targeting
|
|
279
|
+
* `react/no-danger` is rewritten to the canonical key instead of
|
|
280
|
+
* leaving a dead duplicate).
|
|
281
|
+
*/
|
|
282
|
+
const setRuleSeverity = (config, ruleKey, severity) => {
|
|
283
|
+
const equivalentKeys = new Set(getEquivalentRuleKeys(ruleKey));
|
|
284
|
+
const nextRules = {};
|
|
285
|
+
for (const [existingKey, existingSeverity] of Object.entries(config.rules ?? {})) if (!equivalentKeys.has(existingKey)) nextRules[existingKey] = existingSeverity;
|
|
286
|
+
nextRules[ruleKey] = severity;
|
|
287
|
+
return {
|
|
288
|
+
...config,
|
|
289
|
+
rules: nextRules
|
|
290
|
+
};
|
|
291
|
+
};
|
|
292
|
+
const setCategorySeverity = (config, category, severity) => ({
|
|
293
|
+
...config,
|
|
294
|
+
categories: {
|
|
295
|
+
...config.categories,
|
|
296
|
+
[category]: severity
|
|
297
|
+
}
|
|
298
|
+
});
|
|
299
|
+
const addIgnoredTag = (config, tag) => {
|
|
300
|
+
const currentTags = config.ignore?.tags ?? [];
|
|
301
|
+
if (currentTags.includes(tag)) return config;
|
|
302
|
+
return {
|
|
303
|
+
...config,
|
|
304
|
+
ignore: {
|
|
305
|
+
...config.ignore,
|
|
306
|
+
tags: [...new Set([...currentTags, tag])].sort()
|
|
307
|
+
}
|
|
308
|
+
};
|
|
309
|
+
};
|
|
310
|
+
const removeIgnoredTag = (config, tag) => {
|
|
311
|
+
const currentTags = config.ignore?.tags ?? [];
|
|
312
|
+
if (!currentTags.includes(tag)) return config;
|
|
313
|
+
const remainingTags = currentTags.filter((existingTag) => existingTag !== tag);
|
|
314
|
+
const { tags: _removed, ...remainingIgnore } = config.ignore ?? {};
|
|
315
|
+
if (remainingTags.length === 0) {
|
|
316
|
+
if (Object.keys(remainingIgnore).length === 0) {
|
|
317
|
+
const { ignore: _ignore, ...configWithoutIgnore } = config;
|
|
318
|
+
return configWithoutIgnore;
|
|
319
|
+
}
|
|
320
|
+
return {
|
|
321
|
+
...config,
|
|
322
|
+
ignore: remainingIgnore
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
return {
|
|
326
|
+
...config,
|
|
327
|
+
ignore: {
|
|
328
|
+
...remainingIgnore,
|
|
329
|
+
tags: remainingTags
|
|
330
|
+
}
|
|
331
|
+
};
|
|
332
|
+
};
|
|
333
|
+
//#endregion
|
|
334
|
+
//#region src/cli/commands/rules.ts
|
|
335
|
+
const SEVERITY_VALUES = [
|
|
336
|
+
"off",
|
|
337
|
+
"warn",
|
|
338
|
+
"error"
|
|
339
|
+
];
|
|
340
|
+
const recordRulesInvocation = () => recordCount(METRIC.cliInvoked, 1, { command: "rules" });
|
|
341
|
+
const resolveProjectRoot = (options) => {
|
|
342
|
+
const requestedDirectory = path$1.resolve(options.cwd ?? process.cwd());
|
|
343
|
+
return findNearestPackageDirectory(requestedDirectory) ?? requestedDirectory;
|
|
344
|
+
};
|
|
345
|
+
const parseSeverity = (value) => SEVERITY_VALUES.includes(value) ? value : null;
|
|
346
|
+
const reportInvalidSeverity = (value) => {
|
|
347
|
+
cliLogger.error(`Invalid severity "${value}". Expected one of: ${SEVERITY_VALUES.join(", ")}.`);
|
|
348
|
+
process.exitCode = 1;
|
|
349
|
+
};
|
|
350
|
+
const reportRuleNotFound = (ruleQuery) => {
|
|
351
|
+
cliLogger.error(`Unknown rule "${ruleQuery}".`);
|
|
352
|
+
cliLogger.dim(" Run `react-doctor rules list` to see every available rule.");
|
|
353
|
+
process.exitCode = 1;
|
|
354
|
+
};
|
|
355
|
+
const describeTargetPath = (target) => {
|
|
356
|
+
const relativePath = path$1.relative(process.cwd(), target.filePath);
|
|
357
|
+
const displayPath = relativePath.length > 0 && !relativePath.startsWith("..") ? relativePath : target.filePath;
|
|
358
|
+
return target.exists ? displayPath : `${displayPath} ${highlighter.dim("(created)")}`;
|
|
359
|
+
};
|
|
360
|
+
const applyConfigChange = async (options, change) => {
|
|
361
|
+
const target = await resolveRuleConfigTarget(resolveProjectRoot(options));
|
|
362
|
+
const nextConfig = change(target.config);
|
|
363
|
+
const { written } = await writeRuleConfig(target, nextConfig);
|
|
364
|
+
return {
|
|
365
|
+
target,
|
|
366
|
+
nextConfig,
|
|
367
|
+
written
|
|
368
|
+
};
|
|
369
|
+
};
|
|
370
|
+
const reportManualEdit = (target, nextConfig) => {
|
|
371
|
+
const managed = {};
|
|
372
|
+
for (const key of [
|
|
373
|
+
"rules",
|
|
374
|
+
"categories",
|
|
375
|
+
"ignore"
|
|
376
|
+
]) if (nextConfig[key] !== void 0) managed[key] = nextConfig[key];
|
|
377
|
+
cliLogger.error(`Couldn't automatically edit ${describeTargetPath(target)} (dynamic config).`);
|
|
378
|
+
cliLogger.dim(" Apply this to your config's default export, then re-run:");
|
|
379
|
+
for (const line of JSON.stringify(managed, null, 2).split("\n")) cliLogger.dim(` ${line}`);
|
|
380
|
+
process.exitCode = 1;
|
|
381
|
+
};
|
|
382
|
+
const rulesListAction = async (options) => {
|
|
383
|
+
recordRulesInvocation();
|
|
384
|
+
recordCount(METRIC.rulesQueried, 1, {
|
|
385
|
+
subcommand: "list",
|
|
386
|
+
hadFilter: Boolean(options.category || options.tag || options.framework || options.configured)
|
|
387
|
+
});
|
|
388
|
+
const catalog = buildRuleCatalog();
|
|
389
|
+
const config = validateConfigTypes((await resolveRuleConfigTarget(resolveProjectRoot(options))).config);
|
|
390
|
+
const categoryFilter = typeof options.category === "string" ? options.category.toLowerCase() : void 0;
|
|
391
|
+
const frameworkFilter = options.framework?.toLowerCase();
|
|
392
|
+
const rows = catalog.filter((entry) => {
|
|
393
|
+
if (categoryFilter && entry.category.toLowerCase() !== categoryFilter) return false;
|
|
394
|
+
if (frameworkFilter && entry.framework.toLowerCase() !== frameworkFilter) return false;
|
|
395
|
+
if (options.tag && !entry.tags.includes(options.tag)) return false;
|
|
396
|
+
return true;
|
|
397
|
+
}).map((entry) => ({
|
|
398
|
+
entry,
|
|
399
|
+
effective: resolveEffectiveRuleSeverity(config, entry)
|
|
400
|
+
})).filter((row) => options.configured ? row.effective.source !== "default" : true);
|
|
401
|
+
if (options.json) {
|
|
402
|
+
const payload = rows.map((row) => ({
|
|
403
|
+
key: row.entry.key,
|
|
404
|
+
id: row.entry.id,
|
|
405
|
+
category: row.entry.category,
|
|
406
|
+
framework: row.entry.framework,
|
|
407
|
+
tags: row.entry.tags,
|
|
408
|
+
defaultSeverity: row.entry.defaultSeverity,
|
|
409
|
+
defaultEnabled: row.entry.defaultEnabled,
|
|
410
|
+
severity: row.effective.value,
|
|
411
|
+
source: row.effective.source
|
|
412
|
+
}));
|
|
413
|
+
cliLogger.log(JSON.stringify(payload, null, 2));
|
|
414
|
+
return;
|
|
415
|
+
}
|
|
416
|
+
cliLogger.log(renderRuleCatalog(rows));
|
|
417
|
+
};
|
|
418
|
+
const rulesExplainAction = async (ruleQuery, options) => {
|
|
419
|
+
recordRulesInvocation();
|
|
420
|
+
recordCount(METRIC.rulesQueried, 1, { subcommand: "explain" });
|
|
421
|
+
const entry = findRuleInCatalog(buildRuleCatalog(), ruleQuery);
|
|
422
|
+
if (!entry) {
|
|
423
|
+
reportRuleNotFound(ruleQuery);
|
|
424
|
+
return;
|
|
425
|
+
}
|
|
426
|
+
const effective = resolveEffectiveRuleSeverity(validateConfigTypes((await resolveRuleConfigTarget(resolveProjectRoot(options))).config), entry);
|
|
427
|
+
if (options.json) {
|
|
428
|
+
cliLogger.log(JSON.stringify({
|
|
429
|
+
key: entry.key,
|
|
430
|
+
id: entry.id,
|
|
431
|
+
category: entry.category,
|
|
432
|
+
framework: entry.framework,
|
|
433
|
+
tags: entry.tags,
|
|
434
|
+
defaultSeverity: entry.defaultSeverity,
|
|
435
|
+
defaultEnabled: entry.defaultEnabled,
|
|
436
|
+
severity: effective.value,
|
|
437
|
+
source: effective.source,
|
|
438
|
+
recommendation: entry.recommendation ?? null,
|
|
439
|
+
learnMoreUrl: buildRuleDocsUrl("react-doctor", entry.id)
|
|
440
|
+
}, null, 2));
|
|
441
|
+
return;
|
|
442
|
+
}
|
|
443
|
+
cliLogger.log(renderRuleExplanation({
|
|
444
|
+
entry,
|
|
445
|
+
effective
|
|
446
|
+
}));
|
|
447
|
+
};
|
|
448
|
+
const setRuleSeverityAndReport = async (entry, severity, options, action) => {
|
|
449
|
+
const { target, nextConfig, written } = await applyConfigChange(options, (config) => setRuleSeverity(config, entry.key, severity));
|
|
450
|
+
if (!written) {
|
|
451
|
+
reportManualEdit(target, nextConfig);
|
|
452
|
+
return;
|
|
453
|
+
}
|
|
454
|
+
cliLogger.success(`Set ${entry.key} → ${severity}`);
|
|
455
|
+
cliLogger.dim(` Updated ${describeTargetPath(target)}`);
|
|
456
|
+
recordCount(METRIC.rulesChanged, 1, {
|
|
457
|
+
action,
|
|
458
|
+
severity,
|
|
459
|
+
target: entry.key
|
|
460
|
+
});
|
|
461
|
+
};
|
|
462
|
+
const rulesSetAction = async (ruleQuery, severityValue, options) => {
|
|
463
|
+
recordRulesInvocation();
|
|
464
|
+
const severity = parseSeverity(severityValue);
|
|
465
|
+
if (!severity) {
|
|
466
|
+
reportInvalidSeverity(severityValue);
|
|
467
|
+
return;
|
|
468
|
+
}
|
|
469
|
+
const entry = findRuleInCatalog(buildRuleCatalog(), ruleQuery);
|
|
470
|
+
if (!entry) {
|
|
471
|
+
reportRuleNotFound(ruleQuery);
|
|
472
|
+
return;
|
|
473
|
+
}
|
|
474
|
+
await setRuleSeverityAndReport(entry, severity, options, "set");
|
|
475
|
+
};
|
|
476
|
+
const rulesEnableAction = async (ruleQuery, options) => {
|
|
477
|
+
recordRulesInvocation();
|
|
478
|
+
const entry = findRuleInCatalog(buildRuleCatalog(), ruleQuery);
|
|
479
|
+
if (!entry) {
|
|
480
|
+
reportRuleNotFound(ruleQuery);
|
|
481
|
+
return;
|
|
482
|
+
}
|
|
483
|
+
if (options.severity === void 0) {
|
|
484
|
+
await setRuleSeverityAndReport(entry, entry.defaultSeverity, options, "enable");
|
|
485
|
+
return;
|
|
486
|
+
}
|
|
487
|
+
const severity = parseSeverity(options.severity);
|
|
488
|
+
if (!severity) {
|
|
489
|
+
reportInvalidSeverity(options.severity);
|
|
490
|
+
return;
|
|
491
|
+
}
|
|
492
|
+
if (severity === "off") {
|
|
493
|
+
cliLogger.error("`enable` cannot set a rule to off. Use `react-doctor rules disable` instead.");
|
|
494
|
+
process.exitCode = 1;
|
|
495
|
+
return;
|
|
496
|
+
}
|
|
497
|
+
await setRuleSeverityAndReport(entry, severity, options, "enable");
|
|
498
|
+
};
|
|
499
|
+
const rulesDisableAction = async (ruleQuery, options) => {
|
|
500
|
+
recordRulesInvocation();
|
|
501
|
+
const entry = findRuleInCatalog(buildRuleCatalog(), ruleQuery);
|
|
502
|
+
if (!entry) {
|
|
503
|
+
reportRuleNotFound(ruleQuery);
|
|
504
|
+
return;
|
|
505
|
+
}
|
|
506
|
+
await setRuleSeverityAndReport(entry, "off", options, "disable");
|
|
507
|
+
};
|
|
508
|
+
const rulesCategoryAction = async (categoryQuery, severityValue, options) => {
|
|
509
|
+
recordRulesInvocation();
|
|
510
|
+
const severity = parseSeverity(severityValue);
|
|
511
|
+
if (!severity) {
|
|
512
|
+
reportInvalidSeverity(severityValue);
|
|
513
|
+
return;
|
|
514
|
+
}
|
|
515
|
+
const knownCategories = listRuleCategories(buildRuleCatalog());
|
|
516
|
+
const matchedCategory = knownCategories.find((category) => category.toLowerCase() === categoryQuery.toLowerCase());
|
|
517
|
+
if (!matchedCategory) {
|
|
518
|
+
cliLogger.error(`Unknown category "${categoryQuery}".`);
|
|
519
|
+
cliLogger.dim(` Known categories: ${knownCategories.join(", ")}`);
|
|
520
|
+
process.exitCode = 1;
|
|
521
|
+
return;
|
|
522
|
+
}
|
|
523
|
+
const { target, nextConfig, written } = await applyConfigChange(options, (config) => setCategorySeverity(config, matchedCategory, severity));
|
|
524
|
+
if (!written) {
|
|
525
|
+
reportManualEdit(target, nextConfig);
|
|
526
|
+
return;
|
|
527
|
+
}
|
|
528
|
+
cliLogger.success(`Set category "${matchedCategory}" → ${severity}`);
|
|
529
|
+
cliLogger.dim(` Updated ${describeTargetPath(target)}`);
|
|
530
|
+
recordCount(METRIC.rulesChanged, 1, {
|
|
531
|
+
action: "category",
|
|
532
|
+
severity,
|
|
533
|
+
target: matchedCategory
|
|
534
|
+
});
|
|
535
|
+
};
|
|
536
|
+
const rulesIgnoreTagAction = async (tag, options) => {
|
|
537
|
+
recordRulesInvocation();
|
|
538
|
+
const knownTags = listRuleTags(buildRuleCatalog());
|
|
539
|
+
if (!knownTags.includes(tag)) {
|
|
540
|
+
cliLogger.error(`Unknown tag "${tag}".`);
|
|
541
|
+
cliLogger.dim(` Known tags: ${knownTags.join(", ")}`);
|
|
542
|
+
process.exitCode = 1;
|
|
543
|
+
return;
|
|
544
|
+
}
|
|
545
|
+
const { target, nextConfig, written } = await applyConfigChange(options, (config) => addIgnoredTag(config, tag));
|
|
546
|
+
if (!written) {
|
|
547
|
+
reportManualEdit(target, nextConfig);
|
|
548
|
+
return;
|
|
549
|
+
}
|
|
550
|
+
cliLogger.success(`Ignoring tag "${tag}" (rules with this tag are skipped before linting)`);
|
|
551
|
+
cliLogger.dim(` Updated ${describeTargetPath(target)}`);
|
|
552
|
+
recordCount(METRIC.rulesChanged, 1, {
|
|
553
|
+
action: "ignoreTag",
|
|
554
|
+
target: tag
|
|
555
|
+
});
|
|
556
|
+
};
|
|
557
|
+
const rulesUnignoreTagAction = async (tag, options) => {
|
|
558
|
+
recordRulesInvocation();
|
|
559
|
+
const target = await resolveRuleConfigTarget(resolveProjectRoot(options));
|
|
560
|
+
if (!(target.config.ignore?.tags ?? []).includes(tag)) {
|
|
561
|
+
cliLogger.dim(`Tag "${tag}" was not being ignored; nothing to change.`);
|
|
562
|
+
return;
|
|
563
|
+
}
|
|
564
|
+
const nextConfig = removeIgnoredTag(target.config, tag);
|
|
565
|
+
const { written } = await writeRuleConfig(target, nextConfig);
|
|
566
|
+
if (!written) {
|
|
567
|
+
reportManualEdit(target, nextConfig);
|
|
568
|
+
return;
|
|
569
|
+
}
|
|
570
|
+
cliLogger.success(`Tag "${tag}" is no longer ignored`);
|
|
571
|
+
cliLogger.dim(` Updated ${describeTargetPath(target)}`);
|
|
572
|
+
recordCount(METRIC.rulesChanged, 1, {
|
|
573
|
+
action: "unignoreTag",
|
|
574
|
+
target: tag
|
|
575
|
+
});
|
|
576
|
+
};
|
|
577
|
+
//#endregion
|
|
578
|
+
export { rulesCategoryAction, rulesDisableAction, rulesEnableAction, rulesExplainAction, rulesIgnoreTagAction, rulesListAction, rulesSetAction, rulesUnignoreTagAction };
|
|
579
|
+
|
|
580
|
+
//# sourceMappingURL=rules-6-d19pN1.js.map
|
|
581
|
+
//# debugId=3482bd59-e632-5753-a94a-167622d04810
|