---
changelog:
  category: Release
  version: ultracite@8.2.0
date: '2026-11-12T04:18:13Z'
seo:
  description: >-
    The ESLint angular preset now lints Angular templates, accessibility
    included, with @angular-eslint/eslint-plugin-template. Until now the preset
    only linted…
title: ultracite@8.2.0
type: changelog
---
## Minor Changes

- f971134: The ESLint `angular` preset now lints Angular templates, accessibility included, with [`@angular-eslint/eslint-plugin-template`](https://github.com/angular-eslint/angular-eslint/tree/main/packages/eslint-plugin-template). Until now the preset only linted component classes, so a template got no checks on ESLint.
  
  Angular projects on ESLint will see new errors in templates. The preset covers every `.html` file and the inline `template` of each component in a `.ts` file, where errors are reported at their place in the TypeScript file and `ultracite fix` fixes them there. Thirty-nine rules are on at `error`. The accessibility rules cover missing `alt` text, labels with no control, click and mouse handlers with no keyboard equivalent, handlers on elements that can't take focus, empty headings, links and buttons, and invalid ARIA roles and attributes. The rest are Angular's general template rules, such as `@angular-eslint/template/prefer-control-flow`, `@angular-eslint/template/eqeqeq`, `@angular-eslint/template/button-has-type` and `@angular-eslint/template/no-duplicate-attributes`. Measured on three open-source Angular apps, the two rules that report most are `@angular-eslint/template/attributes-order` and `@angular-eslint/template/prefer-self-closing-tags`, and `ultracite fix` fixes both.
  
  - `@angular-eslint/template/no-autofocus` is off, as `jsx-a11y/no-autofocus` is in the React presets. Autofocus is a deliberate UX choice (a waitlist or search field that's ready to type into), and the rule can't tell that from a harmful use.
  - `@angular-eslint/template/i18n` is off. It only applies to apps translated with Angular's own i18n, and wants an `i18n` attribute on every element with text and beside attributes such as `rel` and `data-testid`.
  - `@angular-eslint/template/no-call-expression` is off. It reports every signal read (`count()`), which is how a template reads a signal.
  - `@angular-eslint/template/cyclomatic-complexity` allows 20 control-flow blocks in a template, as core's `complexity` allows a function, in place of the plugin's 5.
  - `@angular-eslint/template/no-duplicate-attributes` accepts a static `class` or `style` beside a binding to the same attribute, which Angular merges.
  - `@angular-eslint/template/no-inline-styles` accepts a style bound to a computed value (`[style.width.%]="progress()"`), which can't be a class. A static `style` attribute is still reported.
  - The rules can't see what a directive or component adds. A button whose label comes from a directive (`<button appIconButton></button>`) is reported as empty by `@angular-eslint/template/elements-content`. List those attributes in the rule's `allowList` option in your own config.
  
  Every `.html` file is linted as a template, because Angular no longer names templates `*.component.html`. That includes whole pages: files that start with a doctype, an XML declaration, or an `<html>`, `<head>` or `<body>` element, such as `src/index.html`, a static page in `public/` or an email. A page gets the accessibility rules and the other template rules, apart from three that only fit a template Angular compiles: `@angular-eslint/template/prefer-ngsrc`, `@angular-eslint/template/no-inline-styles` and `@angular-eslint/template/prefer-self-closing-tags`. The last one's fix would turn `<my-widget></my-widget>` into `<my-widget />`, which a browser reads as an open tag. A `{` or `}` in a page's text, such as in a code sample, is read as text. In any other HTML file it's a parse error, as it is when Angular compiles the template. The inline scripts of an HTML file are linted by core's rules, and `github/filenames-match-regex` checks each HTML file's name, as core does.
  
  To change a template rule, set it in a config object with `files: ["**/*.html"]`, which also matches inline templates. That pattern no longer reaches the inline scripts of an HTML file, which are linted as a block of their own: settings for them, such as extra browser globals, need `files: ["**/*.html/*.htm"]`. Core's JavaScript rules are off for templates, since they can't read a template's syntax tree. A rule your config turns on for every file runs on templates too. Some report there: `jsdoc/require-file-overview`, for example, reports every template as missing `@file`. Others throw: `github/js-class-name`, `github/unescaped-html-literal` and `sonarjs/code-eval` throw on a template literal in a template (``{{ `Hi ${name}` }}``). Give rules meant for your TypeScript `files` that leave out `**/*.html`.
  
  `ultracite init` installs `@angular-eslint/eslint-plugin-template` and `@angular-eslint/template-parser` for Angular projects that choose ESLint. Existing projects get both from `ultracite upgrade`, which installs every package the presets in your ESLint config load, and `ultracite doctor` reports the plugin as missing until then. Oxlint doesn't parse Angular templates, and Biome's accessibility rules already run on `.html` files from its core preset, so neither changes.
- 37ead86: Ultracite no longer sorts object keys by default. `sort-keys` is off in the Oxlint and ESLint core presets, and `useSortedKeys` is off in the Biome core preset, so `ultracite fix` stops reordering your objects and `ultracite check` stops reporting unsorted ones.
  
  Both fixes are classed as safe, so they ran on every `ultracite fix`, and key order can change what code does: the bytes `JSON.stringify` writes, the order a loop visits keys, which entry of an `exports` map wins, and what TypeScript infers for an options object. [The research post](/research/sort-keys) has the cases we measured on real codebases.
  
  To keep sorting, turn the rule back on in your own config:
  
  - Oxlint and ESLint: `"sort-keys": "error"` in `rules`.
  - Biome: `"useSortedKeys": "on"` under `assist.actions.source`.
  
  The TanStack presets no longer need their own exemption from key sorting and have dropped it. If you turn sorting back on in a TanStack project, turn it off again for your query, mutation and route option objects, where sorting breaks type inference.
  
  Biome's `useSortedSelectionSet` is off too: the order of a GraphQL selection set is the key order of the response. Biome's other sorting actions (imports, JSX attributes, interface members, CSS properties, `package.json` fields and Tailwind classes) are unchanged.
- 37ead86: Bring the three linters' presets in line with each other on the rules the [research posts](/research) measured. Oxlint is the benchmark, so most changes move ESLint and Biome to what Oxlint already did.
  
  **Stricter. These can report new problems when you upgrade.**
  
  - Biome: `noExplicitAny` now applies to test files too, as `no-explicit-any` does in Oxlint and ESLint.
  - Biome: `noImportCycles` moves from the `type-aware` preset into core, where Oxlint and ESLint already check import cycles. It adds no measurable time, because core skips `node_modules` ([#843](https://github.com/haydenbleasel/ultracite/issues/843)).
  - Biome: `noExcessiveClassesPerFile` is on, matching `max-classes-per-file` in Oxlint and ESLint.
  - ESLint: the `react` preset now applies to every JavaScript and TypeScript file, not only `.jsx` and `.tsx`, so custom hooks in `.ts` files get the `react-hooks` rules and JSX in `.js` files gets the accessibility rules. Oxlint and Biome already checked both. Linting a non-React TypeScript project with the `react` preset loaded took about 30% longer in our measurement.
  - Oxlint: the `js-plugins`, `next/js-plugins` and `tanstack/js-plugins` presets gain the 267 React Doctor rules the ESLint presets already ran, so both linters now run the same list. Most are for one library (React Router, three.js, Ink, Motion, Remotion) and stay silent in projects that don't use it. The JS plugin pass took about 1.6 times as long on our test project.
  - ESLint and Oxlint `js-plugins`: `sonarjs/shorthand-property-grouping` is on. It was off only because it conflicted with `sort-keys`, which is now off by default.
  - Oxlint and ESLint: `no-warning-comments` also reports JSDoc's `@todo` tag and a marker behind the asterisks of a block comment (`/** @todo */`, `/* ** TODO */`).
  
  **Looser, or off.**
  
  - `no-unused-vars` follows one convention in all three linters: a leading underscore marks a parameter, variable, caught error or destructured array element as unused on purpose, and a property left out with a rest sibling (`const { secret, ...rest } = obj`) and a `using` declaration aren't reported. ESLint used to report all of these, and Oxlint reported a bare `_` and every unused `catch` binding. This also stops `ultracite fix --unsafe` on Oxlint from deleting the omitted property, which put it back into `rest`. `sonarjs/no-unused-vars` is off in ESLint and Oxlint `js-plugins`: it duplicates the rule and has no option for the convention.
  - `no-use-before-define` sets `variables: false` in Oxlint and ESLint: a function that mentions a variable declared further down in an outer scope is no longer reported, since the body runs later. Functions that call each other had no order that passed. A use in the same scope, such as a call at load time above the definition, still reports. The TanStack presets now keep the rule on in route files instead of turning it off, so a route that reads its component before the component is declared is reported.
  - `react/function-component-definition` is off in Oxlint and ESLint. Its fix turned a hoisted `function` component into a `const` that could be read before it was defined, and its fix for an unnamed component produced code the rule reported again. `func-style` still asks for function expressions, and leaves `export default function Page()` alone.
  - Namespace imports are allowed everywhere: `sonarjs/no-wildcard-import` is off in ESLint and Oxlint `js-plugins`, and `noNamespaceImport` is off in Biome, matching `import/no-namespace` in Oxlint core. Libraries such as Zod and Radix document `import * as` as their API.
  - Barrel files: Biome's `noBarrelFile`, ESLint's `unicorn/no-barrel-files` and React Doctor's `no-barrel-import` are off. The first two report a package's entry point, and React Doctor made its rule opt-in. Oxlint's `oxc/no-barrel-file`, which reports only very large barrels, stays on.
  - Biome: `noVoid` is off, so `void promise;` can mark a promise that's deliberately not awaited, as Oxlint and ESLint allow.
  - Oxlint: `unicorn/prefer-export-from` is off until its fix is corrected ([oxc-project/oxc#29318](https://github.com/oxc-project/oxc/issues/29318)). With `--unsafe` it could drop an export when one `export { a, b }` re-exported from two modules. ESLint keeps the rule.
  - ESLint: `jsx-a11y/prefer-tag-over-role` is off until [jsx-eslint/eslint-plugin-jsx-a11y#1102](https://github.com/jsx-eslint/eslint-plugin-jsx-a11y/issues/1102) is fixed. It threw on an object-valued `role` prop, which aborted the whole run.
  - ESLint: `unicorn/name-replacements` is off. Its autofix renamed identifiers across a file (`req` to `request`, `env` to `environment`).
  - ESLint: these rules, which reported nothing or didn't apply, are off: `import-x/dynamic-import-chunkname` (webpack magic comments), `unicorn/expiring-todo-comments`, `vue/max-lines-per-block`, `vue/max-template-depth` and `svelte/max-lines-per-block`.
  - ESLint and Oxlint `js-plugins`: `sonarjs/todo-tag` and `sonarjs/fixme-tag` are off. They reported every TODO a second time, with a message that contradicted `no-warning-comments`.
  - ESLint: `jsx-a11y/control-has-associated-label` uses the options the plugin ships in its own configs, so an input its label wraps is no longer reported, and the Astro preset's `label-has-associated-control` takes the same label and control components as the React preset. `astro/jsx-a11y/no-autofocus` is off, as `no-autofocus` already was for React.
  - ESLint: five React Doctor rules are dropped: the four `jsx-no-*-as-prop` rules, which the Oxlint React preset turns off as idiomatic React, and `no-ref-callback-cleanup-before-react-19`, which reported valid React 19 code.
- f971134: The Oxlint and ESLint `qwik` presets now lint Qwik's JSX for accessibility, with the same `jsx-a11y` rules, severities and options as the `react` presets. Until now a Qwik project got no accessibility checks on either linter, because the rules lived in the `react` presets only.
  
  Qwik projects on Oxlint and ESLint will see new errors. Thirty-four rules are on at `error` in each, covering missing `alt` text, labels and controls that aren't associated, anchors with no `href`, invalid ARIA roles and attributes, media without captions, and more. Measured on two open-source Qwik codebases, the rules that report most are `jsx-a11y/control-has-associated-label`, `jsx-a11y/label-has-associated-control` (ESLint), `jsx-a11y/alt-text` and `jsx-a11y/role-supports-aria-props`, and on Oxlint `jsx-a11y/prefer-tag-over-role`.
  
  - The rules that look at event handlers don't work on Qwik's handlers yet. `jsx-a11y/click-events-have-key-events`, `jsx-a11y/mouse-events-have-key-events`, `jsx-a11y/interactive-supports-focus`, `jsx-a11y/no-static-element-interactions` and `jsx-a11y/no-noninteractive-element-interactions` only know React's names, and Qwik's end in `$`, so a `<div onClick$={…}>` with no keyboard handler isn't reported by either linter. They're on anyway: they report nothing wrong on `$` handlers, and they'll start reporting when Oxlint and `eslint-plugin-jsx-a11y` learn the names. On the codebases measured, that would be 46 more reports on ESLint and 37 on Oxlint.
  - Qwik writes `class` and `for` where React writes `className` and `htmlFor`. The ESLint preset tells the label rules that a label's target is `for`, so `<label for="email">` beside `<input id="email" />` passes, and a label with no control is still reported. No rule reads `className`.
  - On Oxlint, `jsx-a11y/label-has-associated-control` is off. Oxlint only accepts `for` through `settings["jsx-a11y"].attributes`, and it doesn't read `settings` from a preset ([oxc-project/oxc#28047](https://github.com/oxc-project/oxc/issues/28047)), so the rule would report every `<label for="…">`. To turn it on, set `"jsx-a11y/label-has-associated-control": "error"` in `rules` and `"jsx-a11y": { attributes: { for: ["htmlFor", "for"] } }` in `settings` in your own `oxlint.config.ts`.
  - On ESLint, a `<Slot />` isn't counted as a label's control the way `{children}` is in React, so a label component that renders `<label {...props}><Slot /></label>` is reported by `jsx-a11y/label-has-associated-control`. Give the label a `for`, or disable the rule on that line.
  - `jsx-a11y/no-autofocus` is off, as it is in the React presets, and on ESLint so is `jsx-a11y/prefer-tag-over-role`, which crashes on an object-valued `role` ([jsx-eslint/eslint-plugin-jsx-a11y#1102](https://github.com/jsx-eslint/eslint-plugin-jsx-a11y/issues/1102)).
  - `jsx-a11y/anchor-is-valid` and `qwik/jsx-a` both report an `<a>` with no `href` on ESLint.
  
  `ultracite init` installs `eslint-plugin-jsx-a11y` for Qwik projects that choose ESLint. Existing projects get it from `ultracite upgrade`, which installs every plugin the presets in your ESLint config load, and `ultracite doctor` reports it as missing until then. Oxlint's rules are built in, so there's nothing to install. Biome's accessibility rules already run from its core preset, so it doesn't change.
- f971134: The Oxlint and ESLint `solid` presets now lint Solid's JSX for accessibility, with the same `jsx-a11y` rules, severities and options as the `react` presets. Until now a Solid project got no accessibility checks on either linter, because the rules lived in the `react` presets only.
  
  Solid projects on Oxlint and ESLint will see new errors. Thirty-four rules are on at `error` in each, covering missing `alt` text, labels and controls that aren't associated, click and mouse handlers with no keyboard equivalent, handlers on non-interactive elements, invalid ARIA roles and attributes, media without captions, and more. Measured on three open-source Solid codebases, the rules that report most are `jsx-a11y/no-static-element-interactions`, `jsx-a11y/control-has-associated-label`, `jsx-a11y/click-events-have-key-events` and `jsx-a11y/alt-text`, and on Oxlint `jsx-a11y/prefer-tag-over-role`.
  
  - Solid writes `class` and `for` where React writes `className` and `htmlFor`. The ESLint preset tells the label rules that a label's target is `for`, so `<label for="email">` beside `<input id="email" />` passes, and a label with no control is still reported. No rule reads `className`.
  - On Oxlint, `jsx-a11y/label-has-associated-control` is off. Oxlint only accepts `for` through `settings["jsx-a11y"].attributes`, and it doesn't read `settings` from a preset ([oxc-project/oxc#28047](https://github.com/oxc-project/oxc/issues/28047)), so the rule would report every `<label for="…">`. To turn it on, set `"jsx-a11y/label-has-associated-control": "error"` in `rules` and `"jsx-a11y": { attributes: { for: ["htmlFor", "for"] } }` in `settings` in your own `oxlint.config.ts`.
  - The rules that look at event handlers know `onClick`, and on ESLint `onclick`. They don't see Solid's `on:click`, or on Oxlint lowercase `onclick`, so an element that only has a handler in those forms isn't checked for a keyboard equivalent. An element that mixes the forms, such as `onClick` with `on:keydown`, is reported as having no key handler.
  - The rules can't see content set through `innerHTML` or `textContent`, so `<h1 innerHTML={title} />` is reported as an empty heading, as an element React fills with `dangerouslySetInnerHTML` is not.
  - `jsx-a11y/no-autofocus` is off, as it is in the React presets, and on ESLint so is `jsx-a11y/prefer-tag-over-role`, which crashes on an object-valued `role` ([jsx-eslint/eslint-plugin-jsx-a11y#1102](https://github.com/jsx-eslint/eslint-plugin-jsx-a11y/issues/1102)).
  
  `ultracite init` installs `eslint-plugin-jsx-a11y` for Solid projects that choose ESLint. Existing projects get it from `ultracite upgrade`, which installs every plugin the presets in your ESLint config load, and `ultracite doctor` reports it as missing until then. Oxlint's rules are built in, so there's nothing to install. Biome's accessibility rules already run from its core preset, so it doesn't change.
- f971134: The ESLint `vue` preset now lints Vue templates for accessibility with [`eslint-plugin-vuejs-accessibility`](https://github.com/vue-a11y/eslint-plugin-vuejs-accessibility). Until now a `.vue` template got no accessibility checks on ESLint, because `eslint-plugin-vue` has none.
  
  Vue projects on ESLint will see new errors. Twenty-one rules are on at `error`, covering missing `alt` text, form controls and labels that aren't associated, click and mouse handlers with no keyboard equivalent, handlers on non-interactive elements, invalid ARIA roles and attributes, media without captions, and more. Measured on three open-source Vue 3 apps, the rules that report most are `vuejs-accessibility/no-static-element-interactions`, `vuejs-accessibility/label-has-for`, `vuejs-accessibility/click-events-have-key-events` and `vuejs-accessibility/form-control-has-label`.
  
  - `vuejs-accessibility/no-autofocus` is off, as `jsx-a11y/no-autofocus` is in the React presets. Autofocus is a deliberate UX choice (a waitlist or search field that's ready to type into), and the rule can't tell that from a harmful use.
  - `vuejs-accessibility/label-has-for` accepts a label that either wraps its control or points at it with `for`. The plugin's default asks for both, which reports correct markup. It also counts `<Checkbox>` and `<Radio>` components as controls, and a `<slot />` inside a label as one.
  - The rules can't see inside components. A heading or link whose only content is a component (`<h1><PageTitle /></h1>`) is reported as empty by `vuejs-accessibility/heading-has-content` and `vuejs-accessibility/anchor-has-content`. List those components in the rules' `accessibleChildren` option in your own config.
  
  `ultracite init` installs `eslint-plugin-vuejs-accessibility` for Vue projects that choose ESLint. Existing projects get it from `ultracite upgrade`, which installs every plugin the presets in your ESLint config load, and `ultracite doctor` reports it as missing until then. Oxlint doesn't lint Vue templates, and Biome's accessibility rules already run from its core preset, so neither changes.
  
  `aria-role` now checks DOM elements only, in the React presets of all three linters as well as the new Vue rules: a `role` prop on a component, such as `<Message role="assistant">`, is no longer reported as an invalid ARIA role.

## Patch Changes

- 8be2267: `ultracite fix --claude` and `--codex` now recognise an issue that's still reported after the agent's edit when a limit rule's message changed only by its count, when a message changed only by the line it points at, or when the same rule reports several times with the same message.
  
  - A limit rule such as `max-lines-per-function`, `max-statements`, `max-params` or `complexity` puts the count in its message ("too many lines (88)"). A function the agent cut from 88 lines to 55 was counted as one issue fixed and one new issue, and the retry told the agent its edit had "resolved the issues it was given". It's now the same issue, still reported. Only the limit rules (the `max-*` and complexity rules, and Biome's `noExcessive*` and `useMax*`) are matched without their numbers, so a `no-magic-numbers` report for `100` isn't taken for the `42` the agent fixed. Other rules ignore only a line or column a message points at, or a count of repeats (`no-shadow`'s "declared in the upper scope on line 3", `sonarjs/no-duplicate-string`'s "duplicating this literal 3 times"), since an edit anywhere in the file changes those. A report whose message changed only by some other number, such as `import/no-cycle`'s "Dependency cycle via ./b:3", is still the same issue when it's on the original's line and the edit left that line as it was.
  - Rules such as `typescript/no-explicit-any` give every report the same message. When an edit added new reports above the original ones, the retry called the agent's new ones "still reported" and pointed at the untouched originals as "new since the previous edit". Reports are now matched by the line of code they're on first, and then by where the edit left an original's line, between the nearest unchanged lines around it. A report there is the original, rewritten. An original whose surroundings are still there with no report among them counts as fixed, so a report the edit added elsewhere is new. Reports whose place can't be told, such as code the edit moved, or a limit rule like `max-lines`, which reports where its count runs out, are matched in file order as before.
  - Each agent is told to edit only its own file, but one can change another anyway (by running a formatter, say). A later file's issues are now found again where that edit left them, so the ones it cleared count as fixed, and if re-linting the file fails, the agent is pointed at the lines its issues are on now.
- 1341a85: `ultracite fix --claude` and `--codex` now show what the agent said, and give it better instructions for the cases where the obvious edit is the wrong one.
  
  - The end of the agent's final message is printed under each file. It used to be shown only when the agent left an issue for you, so a note such as "this looks like a real bug, I left it alone" never reached you.
  - When the agent leaves issues for you, the summary says so: "Fixed 3 of 5 issues with Claude Code. 2 left for you to decide." The command still exits non-zero for them, as `ultracite check` would.
  - The prompt no longer tells the agent to preserve runtime behavior for every rule. Style rules keep that instruction, but when a rule reports a bug, such as a promise nobody awaits, fixing the behavior is the point. If a fix would change behavior that may be intended, the agent leaves the issue for you and says why.
  - The prompt names edits that only hide code from a rule (rewording a comment, re-spelling an identifier, an equivalent construct) as not a fix, and says that leaving an issue is a valid result when the rule's help text doesn't fit the code or the proper fix is in another file. It also tells the agent not to run the linter itself, since the file is re-linted after every attempt.
  - Each attempt gets 10 minutes instead of 5. The slowest correct fixes in our research runs took between five and six.
  - With Oxlint's `js-plugins` preset, rules from `eslint-plugin-sonarjs`, `eslint-plugin-github` and `oxlint-plugin-react-doctor` now reach the agent with the rule's description and documentation URL, read from the plugin installed where the Oxlint config that lints the file loads it from. Oxlint reports neither for JS-plugin rules.
- ece41c0: The rules `ultracite init` writes for agents (`AGENTS.md`, `CLAUDE.md` and the other rules files) and the skill's code standards now match what the presets enforce, and replace advice that led agents to the cheapest edit instead of the right one:
  
  - Accessibility has its own section instead of sitting under "React & JSX", since it applies to every framework. It now leads with "Use semantic HTML first; add ARIA only when no native element fits", and says to make anything clickable a `<button>` or a link rather than adding key handlers to a `<div>`.
  - Hooks: "When a hook dependency rule fires, first check whether the effect is needed, and whether adding the dependency changes when it runs" replaces "Specify all dependencies in hook dependency arrays correctly".
  - Functions: "Write functions as arrow functions assigned to `const`, and define them above where they're used", which is what `func-style` and `no-use-before-define` ask for.
  - TODOs: a new line says not to leave TODO or FIXME comments in new code, and never to reword an existing one to get past the linter.
  - Imports: "Prefer specific imports over namespace imports" is gone, and the barrel file line now says a package's entry point is fine.
  
  Existing projects get the new rules by re-running `ultracite init`.
- 7a2d831: Turn off `unicorn/consistent-arrow-return-style` in the ESLint core preset. It conflicts with `arrow-body-style`: a multi-line arrow function that only returns a value was reported by the Unicorn rule when written with a concise body and by `arrow-body-style` when written with a block, so no form passed, and `ultracite fix` switched between the two without settling. `arrow-body-style` stays on, as in the Oxlint preset.
  
  `func-style` drops its `allowArrowFunctions: true` option in the Oxlint and ESLint core presets. The option does nothing with the `"expression"` style, so nothing is reported differently, and ESLint plans to reject the combination in its next major ([eslint/eslint#20689](https://github.com/eslint/eslint/issues/20689)).
- 15fdb0c: `ultracite doctor` now checks the versions of the lint plugins your config loads, not only the linter and formatter. Each rule plugin of the Ultracite presets your `eslint.config.*` imports, and each Oxlint JS plugin your `oxlint.config.ts` loads, has to satisfy the range this release installs it at. A plugin that's older than that range, or that your config loads but isn't installed, fails the check and points at `ultracite upgrade`, and one that's newer warns. Plugins your config doesn't load are left alone, even when another package (such as the `react-doctor` CLI) installed them.
  
  This catches a crash that `doctor` used to miss. The ESLint React preset and `ultracite/oxlint/js-plugins` turn on `react-doctor/forwarded-set-state-in-effect`, which React Doctor added in 0.9.21, so a project that bumped only `ultracite` and kept an older React Doctor plugin saw ESLint stop with `Could not find "forwarded-set-state-in-effect" in plugin "react-doctor"` and Oxlint with `Rule 'forwarded-set-state-in-effect' not found in plugin 'react-doctor'`, while `doctor` passed. For `eslint-plugin-react-doctor`, `doctor` also checks the `oxlint-plugin-react-doctor` it takes its rules from, which only differs when an override or resolution pins it.
  
  `ultracite upgrade` now also reinstalls the Oxlint JS plugins your config loads when `package.json` doesn't list them, so it can fix every plugin `doctor` flags.
- ece41c0: `ultracite doctor` now warns when an `oxlint.config.ts` extends the core preset without setting `env`, and `ultracite upgrade` adds it. Oxlint doesn't inherit `env` through `extends` ([oxc-project/oxc#20087](https://github.com/oxc-project/oxc/issues/20087)), so a config `init` wrote before 8.0.1 lints without the browser and Node globals, and `no-implied-eval` misses `setTimeout("code")` and `no-eval` misses `window.eval`, with nothing to say so. `upgrade` makes the change through the same update `init` runs, so the rest of the config is kept as written. A project that still has an `.oxlintrc.json`, or a config `init` can't update, is left for `ultracite init`, and `upgrade` only says it added `env` when it did. When the config takes core through a name of its own (`const base = { ...core }`), imports a preset with `import * as`, or declares a name the Ultracite config also declares (its own `jsPluginSettings`, say), `doctor` says to add `env` by hand and why `upgrade` can't. When the config is in a parent directory, it says where to run `upgrade`. A config that spreads in another object (`...shared`) may set `env` through it, so `doctor` also says it can't tell. A config `doctor` can't read, or one the Oxlint configuration check fails on (next to another Oxlint config, or a `.ts` config in a CommonJS package), gets no `env` check rather than a pass.
  
  `ultracite init` and `upgrade` no longer move a spread in an Oxlint config. `init` writes `env`, `extends`, `ignorePatterns`, `jsPlugins` and `settings` ahead of everything it carries over, so a spread that came before one of them ended up after it and overrode it. Such a config is now left unchanged, with a warning that says what to move. A spread after those properties stays where it is, and a property `init` adds goes ahead of it, so whatever the spread sets still wins. `init` also no longer says it updated the Oxlint config when it left it unchanged.
- 42f5b93: `ultracite init` now extends the Oxlint presets in a fixed order when it updates an existing `oxlint.config.ts`: `core` first, then the framework presets, and `anti-slop` last. It used to follow the order of the preset imports, so once `ultracite fix` had sorted them, re-running `init` put `antiSlop` (and presets such as `angular` and `astro`) ahead of `core`, which turned back on the rules Anti-Slop turns off. Configs that were reordered this way are put back in order, and `init` now says when it moves a preset. A js-plugins selection now sits where the full js-plugins preset would, ahead of the react-doctor add-ons, `shadcn` and `anti-slop`, so Anti-Slop is always last.
  
  Updating an existing `oxlint.config.ts` also keeps more of what you wrote:
  
  - Which presets are extended now comes from `extends` rather than the imports, so a preset you import without extending (to reuse its `rules`, say) keeps its import and is no longer added to `extends`. A full js-plugins preset that a new selection replaces in `extends`, or that an older release's generated selection was built from, keeps its import only when your own code still uses it; a property key, comment, string, parameter or local const with its name doesn't count. Workspace configs and `ultracite doctor` go by `extends` as well, so a preset that's only imported doesn't count as loaded, while one your own `extends` entry takes whole (a const that is or spreads it, or `jp.default` for `import * as jp`) does. Reading a property off it, as `{ rules: jsPlugins.rules }` does, doesn't count.
  - A preset imported under another name, such as `import ultraciteCore from "ultracite/oxlint/core"`, keeps that name, including in the settings `init` moves over from `.oxlintrc.json`, and a second import of the same preset under another name is kept as written. A preset `init` adds takes an `ultracite`-prefixed name, such as `ultraciteReact`, when the config already uses the usual one.
  - Your own `extends` entries stay where they were among the presets: an entry before `core` stays first, one right after a preset stays after it, and one after the last preset stays last. An entry is no longer dropped because its name matches a constant older Ultracite releases generated, such as `selectedJsPlugins`, or because it's a `jsPlugins` const declared with a `selectJsPlugins` of your own, and a `selectJsPlugins` of your own is no longer read as an Ultracite selection.
  - A config that imports a preset with `import * as`, or declares its own `defineConfig`, `selectJsPlugins` or `jsPluginSettings` where the Ultracite config needs that name, is left unchanged with a warning that says how to fix it, instead of being rewritten into one that doesn't load.
