Skip to content
Ultracite
Esc
↑↓navigate↵open⌘Jpreview
On this page

ultracite@8.2.0

Minor Changes

  • f971134: The ESLint angular preset now lints Angular templates, accessibility included, with @angular-eslint/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 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 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).
    • 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). 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 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), 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).
    • 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), 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).

    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. 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).

  • 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), 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.

Last updated on

Was this page helpful?