mirror of
https://github.com/millionco/react-doctor.git
synced 2026-09-14 20:00:24 +08:00
codex/fix-latest-issues
4 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
115fccf266 |
fix(cli): fail incomplete plugin scans (#1185)
Co-authored-by: Claude Fable 5 <noreply@anthropic.com> Co-authored-by: Rayhan Noufal Arayilakath <me@rayhanadev.com> |
||
|
|
cbdff6203d |
feat(cli): "Add to CI" handoff option + default CI setup in install (#658)
Co-authored-by: Aiden Bai <aiden.bai05@gmail.com> Co-authored-by: Nisarg Patel <awesomenisarg@gmail.com> |
||
|
|
9777f1ac45 |
feat(cli): add rules management commands and doctor-explain skill (#617)
Co-authored-by: Cursor <cursoragent@cursor.com> |
||
|
|
0a406389d4 |
feat(core): user-plugin extension — config.plugins: [...] for custom rules (#438)
* feat(core): user-plugin extension — \`config.plugins: [...]\` for custom rules
Users can now drop their own oxlint-shaped plugin into
\`react-doctor.config.json\`'s new \`plugins: [...]\` field and
react-doctor runs the rules alongside the built-in ones. Closes
the long-standing gap where the only way to ship a custom rule
was to fork the repo.
## Config shape
\`\`\`json
{
"plugins": [
"./lint/team-conventions.cjs",
"react-doctor-plugin-shopify-conventions"
],
"rules": {
"team-conventions/no-bare-fetch": "error",
"shopify-conventions/use-polaris-tokens": "warn"
}
}
\`\`\`
Each \`plugins\` entry is either:
- A **relative path** (\`./\`, \`../\`, or absolute) resolved relative
to the directory of the config file that declared it — same
resolution shape as \`rootDir\`. NOT relative to CWD.
- An **npm package name** resolved via Node module resolution
from the config source dir's \`node_modules\`.
The module must default-export an oxlint-plugin-shaped object:
\`{ meta: { name }, rules: Record<ruleName, HostRule> }\`. The
existing \`RulePlugin\` types from \`oxlint-plugin-react-doctor\`
are the contract — third-party plugins can import \`defineRule\`
+ \`Rule\` / \`RulePlugin\` types for a typed authoring surface.
## Behavior
- **Opt-in by default**: a user-plugin rule never runs unless
\`rules: { "<plugin-name>/<rule>": "warn" | "error" }\` explicitly
enables it. Mirrors \`defaultEnabled: false\` for built-in rules
so installing a third-party plugin doesn't surprise the user
with a flood of new diagnostics.
- **Namespace**: rule keys are \`<plugin.meta.name>/<rule-name>\`.
If the plugin omits \`meta.name\`, react-doctor slugifies the
filename (\`./lint/team-conventions.cjs\` → \`team-conventions\`).
- **Failure tolerance**: unresolvable entries / non-plugin-shaped
modules / duplicate plugin names log a warning to stderr and
are skipped. The scan continues — a typo in one entry doesn't
block the rest.
- **Full surface integration**: user-plugin rules flow through
every existing severity / ignore / surface knob (\`rules\`,
\`categories\`, \`ignore.{paths, rules, tags}\`,
\`surfaces.{cli, prComment, score, ciFailure}\`) the same as
built-in rules. Demote out of CI without touching the source.
## Implementation
- \`config.plugins?: string[]\` added to \`ReactDoctorConfig\` schema
(\`packages/types/src/config.ts\`) with full inline docs.
- \`validateConfigTypes\` now coerces / strips the new field via
the existing \`validateStringArrayField\` helper — typos / wrong
types warn instead of crashing.
- \`resolveUserPlugin(spec, configSourceDir)\` + \`resolveUserPlugins\`
in \`packages/core/src/runners/oxlint/plugin-resolution.ts\` —
Node-module-resolution + introspection that returns
\`{ entry: { name, specifier }, availableRuleNames }\` ready for
inclusion in oxlint's \`jsPlugins\`. Slug fallback for plugins
without \`meta.name\`.
- \`createOxlintConfig\` accepts \`userPlugins\` and folds each
plugin's enabled rules + its specifier into the generated
oxlintrc.json. Rules are opt-in via the existing
\`severityControls\` lookup path so the existing severity /
category / surface controls all keep working.
- \`runOxlint\` calls \`resolveUserPlugins(userConfig?.plugins, rootDirectory)\`
and passes the result through. Zero call-site changes for
consumers that don't opt in.
## Tests
\`packages/react-doctor/tests/user-plugins.test.ts\` (5 cases):
1. Relative-path plugin loads + surfaces diagnostics when enabled
2. User-plugin rules are opt-in by default (no severity → no fire)
3. \`rules: { "<plugin>/<rule>": "warn" }\` honored on output severity
4. Unresolvable plugin entry warns + scan continues
5. \`meta.name\` omission falls back to slugified filename
## Docs
New \`Custom rules (user plugins)\` section in
\`packages/react-doctor/README.md\` with the full author → register →
run flow + the behavioral notes above.
Co-authored-by: Cursor <cursoragent@cursor.com>
* refactor(user-plugins): thermo-nuclear cleanup — canonical helpers + required meta.name
Self-review against the thermo-nuclear-code-quality-review skill
caught four real code-quality issues. Fixed each.
### 1. Canonical helper duplication — `warnConfigIssue`
The codebase already exports `warnConfigIssue` from
`core/src/utils/warn-config-issue.ts`. The PR introduced four new
`process.stderr.write("[react-doctor] ...")` call sites in
`plugin-resolution.ts`, AND `validate-config-types.ts` had a
pre-existing duplicate `warnConfigField` doing the same thing.
Three copies of the same one-liner.
Replaced everything with `warnConfigIssue` imports. Net: removed
the `warnConfigField` constant + 4 inline writes, single canonical
warning helper across the config-validation surface.
### 2. Slug-from-filename fallback for `meta.name` — magical, fragile
Original PR let users omit `meta.name` and slugged the filename
(`./lint/team-rules.cjs` → namespace `team-rules`). This is
exactly the "ad-hoc magic that hides simple data-shape assumptions"
the skill flags: rename the file and your rule keys in
`config.rules` silently stop matching.
Switched to a hard contract: `meta.name` is required. A plugin
without it logs an actionable warning ("add
`module.exports = { meta: { name: '...' }, rules: {...} }`")
and is skipped. Deletes `slugifyUserPluginSpec` (the whole
function), one branch in `readPluginShape`, one bullet from the
README, and one test case (replaced with a regression test that
asserts the skip behavior).
### 3. Plugin-shape introspection duplicated
The existing `readPluginRuleNames` + `resolveReactHooksJsPlugin`
and the new `readPluginModule` + `readPluginShape` +
`resolveUserPlugin` were doing the same introspection twice.
Unified into a single `readPluginShape(specifier, loadModule)`
helper that returns `{ name, ruleNames }`. The `loadModule`
parameter lets each caller use its own `require` instance —
`resolveReactHooksJsPlugin` uses the file-local `esmRequire`,
`resolveUserPlugin` uses a `candidateRequire` rooted at the
config source directory so user-installed npm packages resolve
through the user's `node_modules`. `readPluginRuleNames` is
gone; both call sites now go through `readPluginShape`.
### 4. `MaybePluginModule` defined twice in same file
The pre-existing module-shape type (used by `readPluginRuleNames`)
and my new one (with `meta?`) were near-duplicates in
`plugin-resolution.ts`. Unified into a single permissive shape
with `meta?: { name?: unknown }` + `default?: MaybePluginModule`
that both code paths use.
### File sizes (skill's 1k rule)
No file crosses the 1000-line boundary. Touched files after this
cleanup:
packages/core/src/runners/oxlint/plugin-resolution.ts 200
packages/core/src/runners/oxlint/config.ts 169
packages/core/src/validate-config-types.ts 208
packages/core/src/run-oxlint.ts 651
packages/types/src/config.ts 287
packages/react-doctor/tests/user-plugins.test.ts 168
### Net diff
The cleanup removes more than it adds:
- `slugifyUserPluginSpec` (entire function)
- `warnConfigField` (entire constant + 11 call-site renames)
- `readPluginRuleNames` (subsumed by `readPluginShape`)
- 4 inline `process.stderr.write(...)` calls
- Duplicate `MaybePluginModule` type
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(user-plugins): resolve from config source dir, not scan root (bugbot)
Bugbot found a real bug introduced by the original PR. \`config.plugins\`
docs and intent say "relative paths resolve against the directory of
the config file that declared them, same shape as \`rootDir\`," but
the implementation was passing \`rootDirectory\` (the post-\`rootDir\`
SCAN root) into \`resolveUserPlugins\`. The two diverge whenever
\`userConfig.rootDir\` redirects the scan to a subtree — the
canonical monorepo case.
Layout that broke:
workspace/
react-doctor.config.json ← { rootDir: "apps/web", plugins: ["./lint/team.cjs"] }
lint/team.cjs ← plugin sits next to CONFIG
apps/web/ ← scan root after rootDir redirect
Before this fix: \`./lint/team.cjs\` resolved from \`apps/web/\` and
silently 404'd — every team setting up monorepo scoping would lose
their plugin without an actionable error.
### Fix: thread \`configSourceDirectory\` end-to-end
\`ResolvedConfig\` (from \`Config.resolve\`) now carries
\`configSourceDirectory: string | null\` alongside \`config\` +
\`resolvedDirectory\`. The Config layer populates it from
\`loadConfigWithSource\` (which already returns the source dir).
\`LintInput\` + \`RunOxlintOptions\` thread it through; the runner
defaults to \`rootDirectory\` for direct callers that don't load
a config file. Plugin resolution uses \`configSourceDirectory\`
so relative paths resolve against the config's location no
matter how aggressively \`rootDir\` redirects the scan.
### Regression test
New case in \`tests/user-plugins.test.ts\`:
"resolves a plugin spec from the config's source directory, not
the scan root (rootDir redirect)" — constructs the exact
monorepo layout above (plugin at workspace root, scan target at
\`apps/web\`) and asserts the plugin's diagnostic fires.
### Test layer call-site updates
\`ResolvedConfig\` is now \`{ config, resolvedDirectory, configSourceDirectory }\`
— callers that constructed it explicitly (the 5 \`Config.layerOf({...})\`
test fixtures + \`inspect.ts\`'s \`configOverride\` path) now pass
\`configSourceDirectory: null\` to indicate "no canonical config
file source." When null, plugin resolution falls back to
\`rootDirectory\`, matching the prior behavior for the no-config-
file path.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(user-plugins): keep userPlugins on extends-retry fallback (bugbot)
Bugbot regression on #438: when oxlint crashes on the user's
adopted \`.oxlintrc.json\`, react-doctor retries once without
\`extends\`. The fallback \`createOxlintConfig({...})\` call was
omitting \`userPlugins\`, so custom rules from \`config.plugins\`
ran on the first attempt and silently disappeared from the
successful retry. No warning, just missing diagnostics.
Pass \`userPlugins\` through the fallback alongside every other
field. Added a regression test that points
\`adoptExistingLintConfig: true\` at a project with a deliberately
broken \`.oxlintrc.json\` (one oxlint can't parse), forcing the
retry to fire, then asserts the user-plugin diagnostic still
surfaces.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(user-plugins): thread configSourceDirectory through inspect() (bugbot HIGH)
Bugbot HIGH on #438: even after threading \`configSourceDirectory\`
into the runtime/Linter/runOxlint layers, \`inspect.ts\` was
hard-coding \`configSourceDirectory: null\` on its
\`Config.layerOf(...)\` call — and the CLI ALWAYS goes through the
\`configOverride\` path (it loads the config itself then passes it
in via the option). So the CLI path lost the source-directory
information on every invocation, and \`config.plugins\` relative
paths silently failed under \`rootDir\` redirect, ancestor-loaded
configs, and \`--staged\` (which scans a temp tree).
The fix: \`inspect()\` already calls \`loadConfigWithSource(directory)\`
on the non-override path; capture its \`sourceDirectory\` and thread
it through \`runInspectWithRuntime\` → \`Config.layerOf({ ..., configSourceDirectory })\`
→ the runner. For the override-only path (programmatic API caller
that synthesizes a config), \`configSourceDirectory\` stays \`null\`
and the runner falls back to the scan root — the expected behavior
when no canonical config file exists.
Co-authored-by: Cursor <cursoragent@cursor.com>
---------
Co-authored-by: Cursor <cursoragent@cursor.com>
|