react-doctor 0.8.0 → 0.8.1-dev.1839566
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/cli.js +602 -75
- package/dist/index.d.ts +92 -94
- package/dist/index.js +107 -6
- package/dist/lsp.js +108 -7
- package/dist/skills/react-doctor/references/explain.md +1 -0
- package/package.json +6 -6
package/dist/lsp.js
CHANGED
|
@@ -33092,7 +33092,7 @@ const SOCKET_FREE_USER_AGENT = "react-doctor-supply-chain";
|
|
|
33092
33092
|
const FILE_LINT_CACHE_FILENAME = "file-lint-cache.json";
|
|
33093
33093
|
const FILE_LINT_CACHE_MAX_FILE_COUNT = 5e4;
|
|
33094
33094
|
const SIDECAR_LINT_CACHE_FILENAME = "sidecar-lint-cache.json";
|
|
33095
|
-
const CORE_PACKAGE_VERSION = "0.8.
|
|
33095
|
+
const CORE_PACKAGE_VERSION = "0.8.1";
|
|
33096
33096
|
const DEAD_CODE_CACHE_FILENAME = "dead-code-cache.json";
|
|
33097
33097
|
const DEAD_CODE_SUMMARY_CACHE_FILENAME = "dead-code-summaries.json";
|
|
33098
33098
|
const SUPPLY_CHAIN_PLUGIN = "socket";
|
|
@@ -35207,6 +35207,9 @@ const SCRIPT_EXTENSION_FRAGMENT = "[cm]?[jt]sx?";
|
|
|
35207
35207
|
const STORY_FILE_SUFFIX_PATTERN = new RegExp(`\\.(?:stories|story)\\.(?:${SCRIPT_EXTENSION_FRAGMENT})$`);
|
|
35208
35208
|
const TEST_FILE_SUFFIX_PATTERN = new RegExp(`\\.(?:test|spec|fixture|fixtures)\\.(?:${SCRIPT_EXTENSION_FRAGMENT})$`);
|
|
35209
35209
|
const TEST_FILE_DIRECTORY_PATTERN = /(?:^|\/)(?:__tests__|__test__|tests|test|__mocks__|cypress|e2e|playwright)\//;
|
|
35210
|
+
const AMBIGUOUS_TEST_ROUTE_DIRECTORY_PATTERN = /(?:^|\/)(?:tests|test)\//;
|
|
35211
|
+
const NEXT_APP_ROUTE_FILE_SUFFIX_PATTERN = /\/(?:default|error|forbidden|global-error|layout|loading|not-found|page|route|template|unauthorized)\.[cm]?[jt]sx?$/;
|
|
35212
|
+
const SCRIPT_FILE_SUFFIX_PATTERN = /\.[cm]?[jt]sx?$/;
|
|
35210
35213
|
const FIXTURE_PROJECT_PATTERN = /\/(?:fixtures|__fixtures__)\//;
|
|
35211
35214
|
const SOURCE_ROOT_PATTERN = /\/(?:src|app|lib|components|pages|features|modules|packages|apps|frontend|client)\//g;
|
|
35212
35215
|
const stripAboveSourceRoot = (relativePath) => {
|
|
@@ -35217,6 +35220,14 @@ const stripAboveSourceRoot = (relativePath) => {
|
|
|
35217
35220
|
if (lastIdx >= 0) return relativePath.slice(lastIdx);
|
|
35218
35221
|
return relativePath.slice(fixtureMatch.index + fixtureMatch[0].length - 1);
|
|
35219
35222
|
};
|
|
35223
|
+
const isFrameworkRouteBeforeAmbiguousTestDirectory = (relativePath) => {
|
|
35224
|
+
const testDirectoryMatch = AMBIGUOUS_TEST_ROUTE_DIRECTORY_PATTERN.exec(relativePath);
|
|
35225
|
+
if (testDirectoryMatch === null) return false;
|
|
35226
|
+
let nearestRouteRoot;
|
|
35227
|
+
for (const pathSegment of relativePath.slice(0, testDirectoryMatch.index).split("/")) if (pathSegment === "app" || pathSegment === "pages") nearestRouteRoot = pathSegment;
|
|
35228
|
+
if (nearestRouteRoot === void 0) return false;
|
|
35229
|
+
return nearestRouteRoot === "pages" ? SCRIPT_FILE_SUFFIX_PATTERN.test(relativePath) : NEXT_APP_ROUTE_FILE_SUFFIX_PATTERN.test(relativePath);
|
|
35230
|
+
};
|
|
35220
35231
|
/**
|
|
35221
35232
|
* Classifies where a file sits relative to shipped code. A finding in a
|
|
35222
35233
|
* `.stories.tsx` or `.spec.ts` file never runs in front of users, so
|
|
@@ -35233,6 +35244,7 @@ const classifyFileContext = (relativePath) => {
|
|
|
35233
35244
|
if (STORY_FILE_SUFFIX_PATTERN.test(forwardSlashed)) return "story";
|
|
35234
35245
|
if (TEST_FILE_SUFFIX_PATTERN.test(forwardSlashed)) return "test";
|
|
35235
35246
|
const scoped = stripAboveSourceRoot(forwardSlashed);
|
|
35247
|
+
if (isFrameworkRouteBeforeAmbiguousTestDirectory(scoped)) return "production";
|
|
35236
35248
|
return TEST_FILE_DIRECTORY_PATTERN.test(scoped) ? "test" : "production";
|
|
35237
35249
|
};
|
|
35238
35250
|
/**
|
|
@@ -36070,6 +36082,7 @@ const VALID_RULE_SEVERITIES = [
|
|
|
36070
36082
|
"warn",
|
|
36071
36083
|
"off"
|
|
36072
36084
|
];
|
|
36085
|
+
const VALID_INCLUDED_FILE_CONTEXTS = ["test", "story"];
|
|
36073
36086
|
const KNOWN_CATEGORY_LABEL = DIAGNOSTIC_CATEGORY_BUCKETS.join(", ");
|
|
36074
36087
|
const isDiagnosticCategoryBucket = (value) => DIAGNOSTIC_CATEGORY_BUCKETS.includes(value);
|
|
36075
36088
|
const filterKnownCategories = (fieldName, categories) => categories.filter((category) => {
|
|
@@ -36104,6 +36117,7 @@ const SURFACE_CONTROL_FIELD_NAMES = [
|
|
|
36104
36117
|
"includeRules",
|
|
36105
36118
|
"excludeRules"
|
|
36106
36119
|
];
|
|
36120
|
+
const isIncludedFileContext = (value) => VALID_INCLUDED_FILE_CONTEXTS.some((fileContext) => fileContext === value);
|
|
36107
36121
|
const SEVERITY_FIELD_NAMES = ["rules", "categories"];
|
|
36108
36122
|
const isPlainObject$1 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
36109
36123
|
const formatType = (value) => typeof value === "string" ? `"${value}"` : typeof value;
|
|
@@ -36138,6 +36152,15 @@ const validateSurfaceControls = (surface, rawControls) => {
|
|
|
36138
36152
|
return;
|
|
36139
36153
|
}
|
|
36140
36154
|
const validatedSurfaceControls = {};
|
|
36155
|
+
if (rawControls.includeFileContexts !== void 0) {
|
|
36156
|
+
const qualifiedName = `surfaces.${surface}.includeFileContexts`;
|
|
36157
|
+
const result = validateStringArrayField(qualifiedName, rawControls.includeFileContexts);
|
|
36158
|
+
if (result !== void 0) validatedSurfaceControls.includeFileContexts = result.filter((fileContext) => {
|
|
36159
|
+
if (isIncludedFileContext(fileContext)) return true;
|
|
36160
|
+
warnConfigIssue(`config field "${qualifiedName}" lists "${fileContext}", which is not a non-production file context (expected one of: ${VALID_INCLUDED_FILE_CONTEXTS.join(", ")}); ignoring the entry.`);
|
|
36161
|
+
return false;
|
|
36162
|
+
});
|
|
36163
|
+
}
|
|
36141
36164
|
for (const fieldName of SURFACE_CONTROL_FIELD_NAMES) {
|
|
36142
36165
|
if (rawControls[fieldName] === void 0) continue;
|
|
36143
36166
|
const qualifiedName = `surfaces.${surface}.${fieldName}`;
|
|
@@ -38781,6 +38804,7 @@ const buildResolvedControls = (surface, userControls) => {
|
|
|
38781
38804
|
for (const tag of includeTags) excludeTags.delete(tag);
|
|
38782
38805
|
for (const tag of toStringSet(userControls?.excludeTags)) excludeTags.add(tag);
|
|
38783
38806
|
return {
|
|
38807
|
+
includeFileContexts: toStringSet(userControls?.includeFileContexts),
|
|
38784
38808
|
includeTags,
|
|
38785
38809
|
excludeTags,
|
|
38786
38810
|
includeCategories: toStringSet(userControls?.includeCategories),
|
|
@@ -38796,7 +38820,7 @@ const isDiagnosticOnSurface = (diagnostic, surface, config) => {
|
|
|
38796
38820
|
if (resolved.includeRuleKeys.has(ruleKey)) return true;
|
|
38797
38821
|
if (resolved.includeCategories.has(category)) return true;
|
|
38798
38822
|
if (intersects(tags, resolved.includeTags)) return true;
|
|
38799
|
-
if (diagnostic.fileContext !== void 0 && (surface === "score" || surface === "ciFailure")) return false;
|
|
38823
|
+
if (diagnostic.fileContext !== void 0 && (surface === "score" || surface === "ciFailure") && !resolved.includeFileContexts.has(diagnostic.fileContext)) return false;
|
|
38800
38824
|
if (resolved.excludeRuleKeys.has(ruleKey)) return false;
|
|
38801
38825
|
if (resolved.excludeCategories.has(category)) return false;
|
|
38802
38826
|
if (intersects(tags, resolved.excludeTags)) return false;
|
|
@@ -39044,6 +39068,35 @@ const parseGithubViewerPermission = (stdout) => {
|
|
|
39044
39068
|
return /^[A-Z_]+$/.test(value) ? value.toLowerCase() : null;
|
|
39045
39069
|
};
|
|
39046
39070
|
const splitNullSeparated = (value) => value.split("\0").filter((entry) => entry.length > 0);
|
|
39071
|
+
const parseBaselineDiffPlan = (value) => {
|
|
39072
|
+
const entries = splitNullSeparated(value);
|
|
39073
|
+
const baseFiles = /* @__PURE__ */ new Set();
|
|
39074
|
+
const headFiles = /* @__PURE__ */ new Set();
|
|
39075
|
+
for (let entryIndex = 0; entryIndex < entries.length; entryIndex += 2) {
|
|
39076
|
+
const status = entries[entryIndex];
|
|
39077
|
+
const filePath = entries[entryIndex + 1];
|
|
39078
|
+
if (status === void 0 || filePath === void 0 || status.length !== 1) return null;
|
|
39079
|
+
if (status === "A") {
|
|
39080
|
+
headFiles.add(filePath);
|
|
39081
|
+
continue;
|
|
39082
|
+
}
|
|
39083
|
+
if (status === "D") {
|
|
39084
|
+
baseFiles.add(filePath);
|
|
39085
|
+
continue;
|
|
39086
|
+
}
|
|
39087
|
+
if (status === "M" || status === "T") {
|
|
39088
|
+
baseFiles.add(filePath);
|
|
39089
|
+
headFiles.add(filePath);
|
|
39090
|
+
continue;
|
|
39091
|
+
}
|
|
39092
|
+
return null;
|
|
39093
|
+
}
|
|
39094
|
+
return {
|
|
39095
|
+
baseFiles: [...baseFiles],
|
|
39096
|
+
headFiles: [...headFiles],
|
|
39097
|
+
untrackedFiles: []
|
|
39098
|
+
};
|
|
39099
|
+
};
|
|
39047
39100
|
const UNTRACKED_FILE_LAST_LINE = Number.MAX_SAFE_INTEGER;
|
|
39048
39101
|
/**
|
|
39049
39102
|
* `Git` wraps every `git`-via-subprocess call react-doctor makes
|
|
@@ -39246,6 +39299,45 @@ var Git = class Git extends Service()("react-doctor/Git") {
|
|
|
39246
39299
|
githubViewerPermission,
|
|
39247
39300
|
branchExists,
|
|
39248
39301
|
mergeBase,
|
|
39302
|
+
baselineDiffPlan: (input) => {
|
|
39303
|
+
if (!isSafeGitRevision(input.ref)) return succeed$2(null);
|
|
39304
|
+
return gen(function* () {
|
|
39305
|
+
const unmerged = yield* runGit(input.directory, [
|
|
39306
|
+
"diff",
|
|
39307
|
+
"--no-ext-diff",
|
|
39308
|
+
"-z",
|
|
39309
|
+
"--name-only",
|
|
39310
|
+
"--diff-filter=U",
|
|
39311
|
+
"--relative"
|
|
39312
|
+
]);
|
|
39313
|
+
if (unmerged.status !== 0 || unmerged.stdout.length > 0) return null;
|
|
39314
|
+
const result = yield* runGit(input.directory, [
|
|
39315
|
+
"diff",
|
|
39316
|
+
"--no-ext-diff",
|
|
39317
|
+
"--no-textconv",
|
|
39318
|
+
"--no-renames",
|
|
39319
|
+
"-z",
|
|
39320
|
+
"--name-status",
|
|
39321
|
+
"--relative",
|
|
39322
|
+
input.ref
|
|
39323
|
+
]);
|
|
39324
|
+
if (result.status !== 0) return null;
|
|
39325
|
+
const plan = parseBaselineDiffPlan(result.stdout);
|
|
39326
|
+
if (plan === null) return null;
|
|
39327
|
+
const untracked = yield* runGit(input.directory, [
|
|
39328
|
+
"ls-files",
|
|
39329
|
+
"--others",
|
|
39330
|
+
"--exclude-standard",
|
|
39331
|
+
"-z"
|
|
39332
|
+
]);
|
|
39333
|
+
if (untracked.status !== 0) return null;
|
|
39334
|
+
return {
|
|
39335
|
+
baseFiles: plan.baseFiles,
|
|
39336
|
+
headFiles: plan.headFiles,
|
|
39337
|
+
untrackedFiles: splitNullSeparated(untracked.stdout)
|
|
39338
|
+
};
|
|
39339
|
+
}).pipe(catch_$1(() => succeed$2(null)), withSpan("Git.baselineDiffPlan"));
|
|
39340
|
+
},
|
|
39249
39341
|
diffSelection: ({ directory, explicitBaseBranch, includeUntracked = false }) => gen(function* () {
|
|
39250
39342
|
if (explicitBaseBranch !== void 0 && explicitBaseBranch.trim().length === 0) return yield* fail$4(new ReactDoctorError({ reason: new GitBaseBranchInvalid({ detail: "Diff base branch cannot be empty." }) }));
|
|
39251
39343
|
if (explicitBaseBranch !== void 0) {
|
|
@@ -39395,6 +39487,7 @@ var Git = class Git extends Service()("react-doctor/Git") {
|
|
|
39395
39487
|
githubViewerPermission: () => succeed$2(snapshot.githubViewerPermission ?? null),
|
|
39396
39488
|
branchExists: (_directory, branch) => succeed$2(snapshot.branchExists?.get(branch) ?? false),
|
|
39397
39489
|
mergeBase: ({ ref }) => succeed$2(snapshot.mergeBase?.get(ref) ?? null),
|
|
39490
|
+
baselineDiffPlan: () => succeed$2(snapshot.baselineDiffPlan ?? null),
|
|
39398
39491
|
diffSelection: () => succeed$2(snapshot.diffSelection ?? null),
|
|
39399
39492
|
stagedFilePaths: () => succeed$2(snapshot.stagedFiles ?? []),
|
|
39400
39493
|
showStagedContent: (_directory, relativePath) => succeed$2(snapshot.stagedContent?.get(relativePath) ?? null),
|
|
@@ -43120,12 +43213,19 @@ const isPathInsideDirectory = (childAbsolutePath, parentAbsolutePath) => {
|
|
|
43120
43213
|
*/
|
|
43121
43214
|
const materializeSourceTree = (input) => gen(function* () {
|
|
43122
43215
|
const materializedFiles = [];
|
|
43216
|
+
const unmaterializedFiles = [];
|
|
43123
43217
|
const resolvedTempDirectory = path.resolve(input.tempDirectory);
|
|
43124
43218
|
for (const relativePath of input.files) {
|
|
43125
43219
|
const content = yield* input.readContent(relativePath).pipe(orElseSucceed(() => null));
|
|
43126
|
-
if (content === null)
|
|
43220
|
+
if (content === null) {
|
|
43221
|
+
unmaterializedFiles.push(relativePath);
|
|
43222
|
+
continue;
|
|
43223
|
+
}
|
|
43127
43224
|
const candidateTargetPath = path.resolve(resolvedTempDirectory, relativePath);
|
|
43128
|
-
if (!isPathInsideDirectory(candidateTargetPath, resolvedTempDirectory))
|
|
43225
|
+
if (!isPathInsideDirectory(candidateTargetPath, resolvedTempDirectory)) {
|
|
43226
|
+
unmaterializedFiles.push(relativePath);
|
|
43227
|
+
continue;
|
|
43228
|
+
}
|
|
43129
43229
|
yield* sync(() => {
|
|
43130
43230
|
fs.mkdirSync(path.dirname(candidateTargetPath), { recursive: true });
|
|
43131
43231
|
fs.writeFileSync(candidateTargetPath, content);
|
|
@@ -43142,6 +43242,7 @@ const materializeSourceTree = (input) => gen(function* () {
|
|
|
43142
43242
|
return {
|
|
43143
43243
|
tempDirectory: input.tempDirectory,
|
|
43144
43244
|
materializedFiles,
|
|
43245
|
+
unmaterializedFiles,
|
|
43145
43246
|
cleanup: () => {
|
|
43146
43247
|
try {
|
|
43147
43248
|
fs.rmSync(input.tempDirectory, {
|
|
@@ -43317,7 +43418,7 @@ const computeConfigFingerprint = (projectDirectory, version) => {
|
|
|
43317
43418
|
/** Display name used in client-facing messages and progress titles. */
|
|
43318
43419
|
const SERVER_DISPLAY_NAME = "React Doctor";
|
|
43319
43420
|
/** Server version reported in `serverInfo`; injected at build, `dev` from source. */
|
|
43320
|
-
const SERVER_VERSION = "0.8.
|
|
43421
|
+
const SERVER_VERSION = "0.8.1";
|
|
43321
43422
|
/** `Diagnostic.source` shown next to every published diagnostic. */
|
|
43322
43423
|
const DIAGNOSTIC_SOURCE = "react-doctor";
|
|
43323
43424
|
/**
|
|
@@ -45347,5 +45448,5 @@ const startLanguageServer = () => {
|
|
|
45347
45448
|
};
|
|
45348
45449
|
//#endregion
|
|
45349
45450
|
export { startLanguageServer };
|
|
45350
|
-
!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]="
|
|
45351
|
-
//# debugId=
|
|
45451
|
+
!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]="4fa679df-c187-5989-bc8a-a96791a6f02c")}catch(e){}}();
|
|
45452
|
+
//# debugId=4fa679df-c187-5989-bc8a-a96791a6f02c
|
|
@@ -51,6 +51,7 @@ Match the control to the intent — prefer the narrowest one:
|
|
|
51
51
|
- **A whole area is unwanted** (e.g. all React Native rules) → `rules category "<Category>" off`.
|
|
52
52
|
- **A behavioral family is noisy** (`design`, `test-noise`, `migration-hint`) → `rules ignore-tag <tag>`.
|
|
53
53
|
- **Keep it locally but hide from PR comment / score / CI gate only** → do NOT disable. Edit `surfaces` in your config (`surfaces.prComment.excludeRules`, `surfaces.score.excludeTags`, `surfaces.ciFailure.excludeCategories`). The rule still shows in local `cli` output.
|
|
54
|
+
- **Restore test or story findings to production health** → set `surfaces.score.includeFileContexts` or `surfaces.ciFailure.includeFileContexts` to `["test"]`, `["story"]`, or both. Other surface exclusions still apply.
|
|
54
55
|
|
|
55
56
|
How the layers combine: `ignore.tags` disables every rule carrying that tag **before** linting, so a tagged rule stays off even if `rules`/`categories` set it to `warn`/`error` (a rule-level override cannot re-enable a tag-ignored rule). For rules that aren't tag-disabled, `rules` overrides `categories` overrides the rule's default. `surfaces` is visibility-only and never changes whether a rule runs.
|
|
56
57
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "react-doctor",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.1-dev.1839566",
|
|
4
4
|
"description": "Your agent writes bad React. This catches it",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"accessibility",
|
|
@@ -65,8 +65,8 @@
|
|
|
65
65
|
"vscode-languageserver-textdocument": "^1.0.12",
|
|
66
66
|
"vscode-uri": "^3.1.0",
|
|
67
67
|
"yaml": "^2.9.0",
|
|
68
|
-
"
|
|
69
|
-
"
|
|
68
|
+
"oxlint-plugin-react-doctor": "0.8.1-dev.1839566",
|
|
69
|
+
"deslop-js": "0.8.1"
|
|
70
70
|
},
|
|
71
71
|
"devDependencies": {
|
|
72
72
|
"@types/babel__code-frame": "^7.27.0",
|
|
@@ -74,9 +74,9 @@
|
|
|
74
74
|
"@xterm/headless": "^6.0.0",
|
|
75
75
|
"commander": "^14.0.3",
|
|
76
76
|
"ora": "^9.4.0",
|
|
77
|
-
"@react-doctor/
|
|
78
|
-
"@react-doctor/
|
|
79
|
-
"@react-doctor/language-server": "0.8.
|
|
77
|
+
"@react-doctor/core": "0.8.1",
|
|
78
|
+
"@react-doctor/api": "0.8.1",
|
|
79
|
+
"@react-doctor/language-server": "0.8.1"
|
|
80
80
|
},
|
|
81
81
|
"engines": {
|
|
82
82
|
"node": "^20.19.0 || >=22.13.0"
|