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

ultracite@7.12.2

Patch Changes

  • 0f192a7: ultracite init no longer wipes an existing Biome config it can’t read. Previously a biome.json or biome.jsonc with a syntax error, or a nested monorepo config with "extends": "//", was treated as empty and replaced with just the Ultracite extends, losing every other setting. Now:

    • A config with a syntax error is left unchanged, with a warning asking you to fix it and re-run init.
    • A nested config that extends the root config ("extends": "//") is left unchanged, since the Ultracite presets belong in the root config.
    • A string extends is turned into a list that also includes the Ultracite presets.
    • Updates edit the file in place, so comments and formatting in biome.jsonc are preserved.

    ultracite doctor and the check/fix resolution check now follow a nested config that extends "//" to the root config, instead of warning that the nested config doesn’t extend ultracite/biome/core.

  • c68ba48: ultracite check and ultracite fix handle their arguments and missing tools more reliably:

    • check now treats explicit files the way fix does. Oxlint only gets files it can lint, Prettier runs with --ignore-unknown, and oxfmt with --no-error-on-unmatched-pattern. Before, ultracite check README.md or ultracite check Dockerfile src/index.ts failed even though nothing was wrong.
    • Linter flags that take a value keep it, even when the value looks like a file: --tsconfig tsconfig.json, -c .oxlintrc.json, --config-path biome.json, --only lint/suspicious/noDebugger, --since origin/main and similar. Before, the value was treated as a lint target, so ultracite fix --tsconfig tsconfig.json skipped Oxlint entirely and only formatted tsconfig.json. Ultracite’s own --claude, --codex, --hook and --unsafe never take a value, so the next argument is always a target.
    • ultracite fix --unsafe with the ESLint toolchain no longer fails with ESLint’s “Invalid option ‘–unsafe’”. ESLint has no unsafe fixes, so the flag is dropped with a warning.
    • A linter that isn’t installed is reported with a plain message instead of a stack trace. The other tools still run first. Stylelint is optional in the ESLint toolchain, as ultracite doctor already said, so a project without it now skips CSS linting with a warning instead of failing. “No linter configuration found” is also printed without a stack trace.
    • Linters installed in the project’s node_modules/.bin are found even when Ultracite isn’t run through a package manager script, npx or bunx, for example ./node_modules/.bin/ultracite check.
  • f61f393: ultracite init now looks for existing ESLint, Prettier and Stylelint configs in the same order the tools do, so when a project has more than one, init updates the one the tool actually loads. For example, Prettier reads .prettierrc.json before prettier.config.mjs, and ESLint reads eslint.config.js before eslint.config.mjs. Before, init could update a config the tool ignored and leave the active one in place. Stylelint’s .stylelintrc.ts and stylelint.config.ts are now recognised too.

  • 242cd2a: ultracite init now updates an existing .vscode/settings.json or .zed/settings.json in place, so your comments and formatting are kept. Before, the file was re-serialised as plain JSON, which stripped every comment. A settings file with a syntax error is now left unchanged with a warning. Before, it was rewritten with whatever part the parser could recover, which dropped the rest.

    For the ESLint toolchain, init now also installs the Prettier VS Code extension (esbenp.prettier-vscode), since the settings it writes make Prettier the default formatter. Before, only the ESLint extension was installed, so format-on-save did nothing until you added Prettier yourself.

  • c273393: ultracite init now checks every flag value before it changes anything in the project. An unknown value for --linter, --pm, --frameworks, --editors, --agents, --hooks, --integrations or --js-plugins stops init with a message listing the valid values. Previously a misspelled --linter (for example --linter Biome) deleted every existing Biome, ESLint, Prettier, Stylelint, Oxlint and oxfmt config file and then crashed.

    When --linter is not passed and init runs without prompts (because of --quiet, CI, or flags such as --agents or --pm), it now keeps the linter the project is already set up with and only falls back to Oxlint when there is none. Running ultracite init --agents universal on a Biome project no longer migrates it to Oxlint. The interactive linter prompt also preselects the detected linter.

  • 3cb2e71: Clearer wording in the CLI:

    • ultracite init --help now lists the valid values for --pm, --linter, --frameworks, --hooks and --integrations, and explains what --type-aware does for Biome and for Oxlint.
    • The agent rules file says “Oxlint + Oxfmt will catch most mechanical issues automatically” instead of “Oxlint + Oxfmt’s linter will catch…”.
    • init and upgrade say “Using pnpm (detected from the project)” instead of “Detected lockfile”, since the package manager can also come from packageManager.
    • ultracite doctor spells “unrecognized” consistently, formats commands as code, and describes warnings as “Some checks have warnings” instead of “optional improvements”.
  • 6ae8be8: The ESLint nestjs preset imports @darraghor/eslint-plugin-nestjs-typed, but ultracite init --linter eslint --frameworks nestjs never installed it, so ESLint failed to load the config with “Cannot find package”. init now installs the plugin with the preset, and ultracite upgrade installs the plugins of every framework preset your eslint.config.* imports, so existing NestJS projects pick it up on their next upgrade.

  • a8e83bb: ultracite init now migrates an existing .oxlintrc.json, .oxfmtrc.json or .oxfmtrc.jsonc when it sets up Oxlint. Oxlint and oxfmt refuse to load any config when a JSON config sits next to oxlint.config.ts or oxfmt.config.ts, and init used to write the TS configs beside the JSON ones, so ultracite check and ultracite fix stopped working. Init now moves the JSON config’s settings into the TS config and deletes the JSON file:

    • rules, overrides, env, plugins and other options become properties of the generated config.
    • ignorePatterns, settings and jsPlugins are added to the ones Ultracite generates instead of replacing them.
    • Ultracite extends entries become presets. Other extends paths can’t be referenced from a TS config, so init names them in a warning.

    A JSON config that doesn’t parse is left in place, and neither file is written.

    Re-running ultracite init also keeps what you added to oxlint.config.ts and oxfmt.config.ts: custom rules, overrides, ignorePatterns, settings and other properties, extra extends entries, your own imports and statements, and comments are carried over while the Ultracite parts are regenerated. Before, both files were regenerated from scratch. A config that doesn’t parse is now left unchanged with a warning instead of being overwritten.

    ultracite doctor now fails when a JSON config and a TS config for Oxlint or oxfmt sit side by side, and suggests running init to migrate a lone .oxfmtrc.json.

  • 981ef2f: ultracite init --linter oxlint no longer adds "type": "module" to package.json. That field changes how Node loads every .js file in the package, so CommonJS files such as a next.config.js, postcss.config.js or jest.config.js using module.exports stopped working after init.

    Instead, init writes the Oxlint and oxfmt configs as oxlint.config.mts and oxfmt.config.mts when the package isn’t an ES module package (no "type" or "type": "commonjs"). A .mts file always loads as an ES module, with no MODULE_TYPELESS_PACKAGE_JSON warning on every run, and it works under "type": "commonjs". ES module packages ("type": "module") still get oxlint.config.ts and oxfmt.config.ts.

    Re-running init updates an existing config under the name it already has. The one exception is a .ts config in a "type": "commonjs" package, which Node can’t load: init renames it to .mts and says so. ultracite doctor, linter detection and the stale-config cleanup all recognise the .mts names. doctor fails a .ts config in a CommonJS package, and fails when a .ts and an .mts config sit side by side.

    Requires oxfmt >= 0.59.0, the first release that finds oxfmt.config.mts on its own.

  • 9a9beb0: ultracite init is gentler with package.json:

    • It keeps the file’s key order, indentation and line endings. Before, every write moved devDependencies to the top and pushed keys like private, main and exports to the end.
    • It no longer overwrites a check or fix script the project already has (for example "check": "tsc --noEmit"). It prints a warning instead, and it only adds the scripts that are missing. Existing scripts that already run Ultracite with extra flags are left as they are.
    • When switching linters, it only removes the previous linter’s packages from devDependencies. Packages in dependencies and peerDependencies stay, such as prettier used at runtime or eslint as the peer of a published plugin.
    • With --skip-install, the final message now says the dependencies were added to package.json instead of claiming they were installed.
  • c3027a5: Re-running ultracite init no longer silently discards changes to an ESLint, Prettier or Stylelint config that builds on Ultracite’s:

    • eslint.config.*: the framework presets it already spreads are kept alongside any newly selected ones, together with your own config objects, imports and a defineConfig(...) wrapper. Before, the file was regenerated from the selected frameworks only, dropping earlier presets and every custom rule.
    • prettier.config.*: your options, imports and extra plugins are kept. Framework plugins are added, and prettier-plugin-tailwindcss stays last.
    • Stylelint: a config that already uses ultracite/stylelint, including one in package.json, is left as it is.

    A config that doesn’t use Ultracite’s presets is still replaced, but init now prints a warning naming the file (or the package.json key) it replaced. A config that doesn’t parse is left unchanged with a warning. With the ESLint toolchain, a "prettier" or "stylelint" key in package.json is now handled by these updates (and reported when replaced) instead of being deleted silently during migration.

  • 44d6997: ultracite init no longer creates directories for agent rule files or agent hook configs (such as .claude/ or .cursor/) before checking that the path stays inside the project. When one of those directories was a symlink to somewhere outside the project, init created the missing directories there before it refused to write the file.

  • 33a1ccd: ultracite init --integrations husky --skip-install no longer replaces an existing prepare script in package.json. It adds husky to the script, as the installing path already did (for example "prepare": "svelte-kit sync && husky"). The Lefthook setup messages now name the config file init actually updates or creates, such as .lefthook.yaml, instead of always saying lefthook.yml.

  • 923667b: ultracite init no longer overrides a tsconfig that turns strictNullChecks off. It leaves an explicit "strictNullChecks": false, whether set in the file or in a config it extends, as it is and prints a warning. It also follows extends, whether a relative path or a package such as @tsconfig/strictest, so a tsconfig that already inherits strict or strictNullChecks is no longer given a redundant "strictNullChecks": true.

  • 199974c: ultracite init and ultracite upgrade now work in Yarn 2+ monorepos. The root install used to pass Yarn 1’s -W flag, which Yarn 2+ rejects with “Unsupported option name”, whenever the Yarn version wasn’t named in package.json’s packageManager field. That happened with --pm yarn, with lockfile-only detection, and always in the second half of upgrade, which re-ran the new CLI with a bare --pm yarn. Ultracite now tells Yarn 2+ apart from Yarn 1 by .yarnrc.yml or the lockfile format. upgrade only forwards --pm to the newly installed CLI when you passed it.

    ultracite doctor (and the check at the end of upgrade) no longer fails in Yarn Plug’n’Play projects just because it can’t find Ultracite or the tools in node_modules. It warns that it can’t verify them there instead.

  • c33158d: Agent hooks set up by ultracite init --hooks work in more places and no longer double up.

    • GitHub Copilot: .github/hooks/ultracite.json is now written in the format the Copilot CLI and Copilot cloud agent read: "version": 1 and a camelCase postToolUse event. VS Code reads this format too. The previous file, with a PascalCase PostToolUse and no version, was only picked up by VS Code. Re-running init moves an existing Ultracite hook to the new format instead of keeping both.
    • fix --hook with Copilot: the file path is now read from Copilot payloads: toolArgs.path from the Copilot CLI (including when toolArgs is a JSON string), tool_input.filePath from VS Code’s edit tools, every file of a multi_replace_string_in_file edit, and the file headers of an apply_patch. So only the edited files are fixed, not the whole project. These hosts run the hook after every tool, so tools that edit nothing (such as read_file, view or bash) now skip the fix instead of fixing the whole project.
    • Re-running init no longer adds a second hook next to one generated by Ultracite 7.8.2 or earlier: pnpm fix …, bun fix …, yarn fix …, or npm run fix --skip=…. That older hook is updated in place, so it stops running a whole-project fix after every edit.
    • A hook is now added even when the settings file mentions ultracite somewhere else, such as a Claude Code permission rule like Bash(npx ultracite check). Only existing hook commands are checked.
    • fix --hook now fixes a file at the project root whose name starts with .., instead of treating it as outside the project.
    • Zed: the includePackageJsonAutoImports preference is now set for vtsls, Zed’s default TypeScript and JavaScript language server (lsp.vtsls.settings), for both TypeScript and JavaScript. It was previously set under typescript-language-server, which Zed doesn’t use by default.
  • dc5e18e: Re-running ultracite init now updates the Ultracite rules in AGENTS.md, .claude/CLAUDE.md, GEMINI.md and the other shared rule files in place, instead of appending another full copy.

    • Previously a copy was appended whenever the linter, the package manager or the rules text changed. For example, switching from Biome to Oxlint left instructions for both engines side by side. Duplicate copies written by earlier versions are now removed, and your own content before and after the block is kept.
    • Files with CRLF line endings keep them, and are no longer duplicated on every run.
    • The commands in the rules now run the project’s installed CLI (npx ultracite fix, yarn ultracite fix, pnpm exec ultracite fix, bunx ultracite fix) instead of a dlx runner. For Deno they read deno run -A npm:ultracite fix; previously the broken deno run -A npm: ultracite fix was written.
    • Firebender: rules are now written to .firebender/rules/ultracite.mdc with alwaysApply: true, where Firebender reads project rules. They were previously written as Markdown into firebender.json, which made that file invalid JSON and overwrote an existing Firebender config. A firebender.json that still holds only those Markdown rules is reset to {}.
  • 2ad7704: Fixes for ultracite fix --claude and ultracite fix --codex:

    • On a terminal, the ✓/✗ result for every issue is now kept once a file is done. Previously a file with more issues than the terminal had rows kept only the last screenful of results.
    • In CI logs and other piped output, lint messages are no longer cut off at 80 columns.
    • The prompt is now sent to the agent CLI on stdin (claude -p, codex exec -) instead of as a command-line argument. On Windows, an npm-installed CLI runs through a .cmd shim, which can cut an argument off at its first newline, so the agent received only the prompt’s first line and never saw the file or its issues.
    • --codex now passes --skip-git-repo-check, so it also works in a project that isn’t a git repository. Previously codex exec refused to run there and every file failed.
  • 0b0775e: The Husky pre-commit hook written by ultracite init --integrations husky is more robust.

    • The hook now runs the project’s installed tools (npx, yarn, pnpm exec, bunx) instead of yarn dlx / pnpm dlx. yarn dlx does not exist in Yarn 1, so every commit failed with “Ultracite found issues that could not be auto-fixed”, and pnpm dlx downloaded the latest release instead of the version the project pins. Husky itself is also initialised with the installed binary.
    • Installing Husky now chains husky onto an existing prepare script (for example SvelteKit’s svelte-kit sync || echo '') instead of replacing it. Choosing Husky and lefthook together no longer leaves only one of them in prepare.
    • When nothing is staged, the hook no longer exits early, so commands you keep after the Ultracite section still run.
    • Re-running init replaces the Ultracite section of a hook saved with CRLF line endings instead of appending a second one. It also no longer drops the last line of a hook that only mentions “# ultracite” in a comment. Commands after a lint-staged section written by an older version are kept.
  • 3862221: ultracite init --integrations lint-staged no longer damages or shadows an existing lint-staged config.

    • An extension-less .lintstagedrc written in YAML (which is how lint-staged reads it) is now edited as YAML. Previously it was overwritten with a JSON file holding only the Ultracite task, deleting the user’s own tasks, and single-quoted YAML was silently skipped.
    • YAML configs are edited through the yaml Document API, so comments and the rest of the file are kept.
    • TypeScript configs (.lintstagedrc.ts, .mts, .cts and lint-staged.config.ts, .mts, .cts) and the lint-staged key of package.yaml are now recognised. Previously init created a .lintstagedrc.json next to them, and because lint-staged uses only the first config in a directory, the user’s config silently stopped running.
    • When a config can’t be edited automatically (a function-based entry, a CommonJS module that can’t be loaded, a syntax error), init now leaves it untouched and prints the command to add, instead of writing a second config file that would replace it.
    • When both exist, the dedicated config file is updated rather than the package.json key, matching the file lint-staged actually uses.
    • A single command already mapped to the same glob is kept alongside ultracite fix instead of being replaced.
    • The task now runs the project’s installed Ultracite (npx, yarn, pnpm exec, bunx) instead of yarn dlx / pnpm dlx, which don’t exist in Yarn 1 or download the latest release instead of the pinned one. Re-running init upgrades a dlx command written by an earlier version.
  • bffb8dd: Corrected the bundled Ultracite agent skill (skills/ultracite/SKILL.md):

    • It said Oxlint has github and sonarjs presets that init includes by default. Neither exists. The GitHub, SonarJS and React Doctor rules live in the ultracite/oxlint/js-plugins preset (plus next/js-plugins and tanstack/js-plugins), next to the anti-slop and shadcn presets, and all of them are opt-in.
    • The --js-plugins init flag is now documented.
    • The skill now tells agents to run the project’s installed CLI (npx, pnpm exec, yarn, bunx) rather than pnpx or yarn dlx.
  • da2934a: Installing the Ultracite skill from ultracite init now works when init runs in a normal terminal.

    Init runs skills add haydenbleasel/ultracite without a terminal attached. When the skills CLI needed to ask which agents to install to, or whether to install into the project or globally, it could not show the prompt:

    • In some cases it cancelled the prompt and exited successfully with nothing installed, so init said “Ultracite skill installed.” when the skill wasn’t there.
    • In others it failed, so the install never happened.

    Init now passes --yes and names the agents to install for, so skills installs without asking. The skill always goes to the shared .agents/skills directory, which Codex, Cursor, GitHub Copilot, Gemini CLI, Amp, Cline, OpenCode and other agents read. It also goes to the skills directory of any agent whose project folder exists (for example .claude, .windsurf, .codebuddy or .roo, which init creates when you choose those agents).

    Init names the agents because otherwise, in a project where skills detects no agent, --yes would install for every agent it knows. That also created a .claude/skills link and a second copy in a top-level agent/ directory (for the Eve framework). The “install it later” hint still shows the interactive skills add haydenbleasel/ultracite command.

  • 4941883: ultracite init now edits .pre-commit-config.yaml and lefthook configs through the yaml Document API instead of splicing text, so the result is always valid YAML and comments are kept.

    • pre-commit: a repos: list written flush with its key (- repo:, the style pre-commit sample-config generates) or with four-space indentation no longer turns into invalid YAML that breaks every commit. The file’s sequence style is kept.
    • lefthook: a four-space indented pre-commit: block, a compact jobs: list, or a column-0 comment inside the block no longer produce invalid YAML or a duplicate jobs: key. A hook that uses commands: gets a jobs: list next to it.
    • lefthook: init now finds an existing .lefthook.yml, lefthook.yaml, .config/lefthook.yml (and the other names lefthook reads) and edits it. Previously it created lefthook.yml, which lefthook picks first, so the user’s own config silently stopped running. A JSON or TOML config is left alone with instructions.
    • lefthook: the job’s globs are now *.js, *.ts, and so on. With lefthook’s default matcher, the previous **/*.js form skipped root-level files such as package.json or index.ts. Projects that set glob_matcher: doublestar get **/*.js.
    • lefthook: re-running init with a different package manager updates the existing job instead of adding a second one.
    • Both now run the project’s installed Ultracite (npx, yarn, pnpm exec, bunx) instead of yarn dlx / pnpm dlx. Re-running init upgrades a hook or job written by an earlier version.
    • lefthook install runs the project’s installed lefthook, and the prepare script is chained onto an existing one (e.g. svelte-kit sync || echo '' && lefthook install) instead of replacing it.
  • 9c30c15: Raise the oxlint peer dependency to ^1.82.0. The core Oxlint preset configures no-unmodified-loop-condition with checkConditionalExpressions, an option Oxlint 1.79 to 1.81 reject, so those releases failed with “Failed to parse oxlint configuration file” even though the old ^1.79.0 range allowed them. ultracite doctor and your package manager now flag the too-old version instead.

    Requires oxlint >= 1.82.0

  • 47303f4: The Biome jest preset now declares Jest’s globals (describe, it, test, expect, jest, beforeEach and the rest of globals.jest) for test files. Jest injects them without imports, so noUndeclaredVariables from the core preset reported every one of them in idiomatic Jest tests. The ESLint jest preset already declared them.

    The route-file exemptions in the Biome remix and tanstack presets (useFilenamingConvention off in routes/) now cover .js and .jsx route files as well as .ts and .tsx.

  • 8f236ee: The ESLint core preset now lints .jsx, .tsx, .mts and .cts files. Its main block previously matched only .js, .mjs, .cjs, .ts, .json and .html, so ESLint skipped .mts and .cts files entirely (“File ignored because no matching configuration was supplied”), and .jsx and .tsx files received only the framework preset’s rules. .tsx files were also parsed by espree, so any TypeScript syntax in them was a parse error. Every TypeScript extension now uses @typescript-eslint/parser with type information, and the core rules apply to all of them.

    Type information now comes from the TypeScript project service (parserOptions.projectService: true) instead of a fixed project: "./tsconfig.json". Solution-style tsconfigs ("files": [] plus references, as in the Vite templates) and monorepo packages now resolve their own tsconfig, where previously every file failed to parse. A file that no tsconfig includes still has to be added to one; typescript-eslint reports it as “not found by the project service” (see https://typescript-eslint.io/troubleshooting/typed-linting/#i-get-errors-telling-me-was-not-found-by-the-project-service-consider-either-including-it-in-the-tsconfigjson-or-including-it-in-allowdefaultproject).

    The JavaScript block no longer matches **/*.json. espree cannot parse JSON, so every package.json and tsconfig.json failed with Parsing error: Unexpected token :. The ESLint preset does not lint JSON, matching the Oxlint preset.

  • 94722fa: The eslint.config.mjs that ultracite init writes no longer fails its own lint. Every ESLint preset exported a default named config, so import core from "ultracite/eslint/core" (and each framework import) tripped import-x/no-rename-default twice per preset. Each preset’s default export is now named after the identifier the generated config imports it as (core, react, tanstack, …).

  • d9803a6: Fix ESLint presets that crashed the whole run or never reached the files they target.

    • TanStack: @tanstack/start/no-async-client-component and @tanstack/start/no-client-code-in-server-component need type information and aborted ESLint (“You have used a rule which requires type information”) on the first .jsx or .tsx file. They now run only on TypeScript files. The Query, Router and Start rules also apply to plain .ts/.js modules, where query and mutation options are usually declared, instead of only to .jsx and .tsx. sort-keys is off, and no-use-before-define and unicorn/filename-case are off in routes/ directories, matching the Oxlint and Biome TanStack presets (alphabetical option keys break TanStack’s type inference, and file routes are mutually recursive and encode the URL in the filename).
    • Qwik: qwik/valid-lexical-scope needs type information and crashed ESLint on .jsx files. It now runs only on .tsx files.
    • Jest: jest/no-deprecated-functions threw “Unable to detect Jest version” and crashed ESLint when the jest package was not resolvable from the plugin, for example in bun:test projects. The preset now reads the Jest version from the project and falls back to the latest major.
    • Cypress: the block used globals.cypress, which does not exist in the globals package, so cy, Cypress, describe and it were reported by no-undef. It now uses eslint-plugin-cypress’s own globals config. It also matched only *.cy.js, and now covers *.cy.* for every JS and TS extension plus files under cypress/.
    • Storybook: the block matched only *.stories.js and *.stories.ts. It now covers *.stories.* and *.story.* for every JS and TS extension, including .tsx and .jsx.
  • d6ee272: The ESLint vue, svelte and astro presets now parse their files. They registered only the plugin and its rules, never a parser, so ESLint read every .vue, .svelte and .astro file with espree and reported a parse error (Unexpected token <) instead of linting it.

    Each preset now starts from its plugin’s own base config (flat/base in eslint-plugin-vue, eslint-plugin-svelte and eslint-plugin-astro), which registers vue-eslint-parser, svelte-eslint-parser or astro-eslint-parser from the plugin’s dependencies, and parses <script lang="ts"> blocks with @typescript-eslint/parser. The Svelte preset also covers .svelte.js and .svelte.ts rune modules. In Astro files, client-side <script> tags are linted without type information, because those in-memory virtual files cannot be type-checked.

  • f7599ef: The ESLint core preset no longer re-enables formatting rules that eslint-config-prettier turns off. The unicorn rule set was spread after eslint-config-prettier, which switched unicorn/number-literal-case, unicorn/template-indent and unicorn/empty-brace-spaces back on. unicorn/number-literal-case demands 0xABCD while Prettier prints 0xabcd, so prettier/prettier and the unicorn rule reported every hex literal and eslint --fix could not satisfy both. eslint-config-prettier now applies after every plugin, so unicorn/number-literal-case and unicorn/template-indent are off. unicorn/empty-brace-spaces stays on (Prettier prints empty braces as {} too), matching the Oxlint preset.

  • efa80e8: The ESLint core preset no longer reports the same problem two or three times on TypeScript files, and no longer flags ambient type-only globals.

    • no-undef and sonarjs/no-reference-error are off for .ts, .tsx, .mts and .cts files. TypeScript already reports unresolved identifiers, and both rules flagged valid ambient types such as NodeJS.Timeout (see the typescript-eslint FAQ).
    • The base class-methods-use-this, consistent-return, no-unused-private-class-members, prefer-destructuring and prefer-promise-reject-errors rules are off for TypeScript files, because their @typescript-eslint extension rules are enabled there. The base rules duplicated every report and ignored the extensions’ TypeScript-aware options.
    • unused-imports/no-unused-vars is off. It is a copy of no-unused-vars / @typescript-eslint/no-unused-vars, which stay on as in the Oxlint preset, so every unused variable was reported twice. unused-imports/no-unused-imports stays on for its autofix.
    • @typescript-eslint/max-params is off, matching max-params being off in the core presets. It was on with its default limit of 3 parameters for TypeScript files only.
  • 9ae95d1: Fix false positives in the ESLint vue, svelte and astro presets that appeared once their files were actually parsed.

    • vue/block-lang and svelte/block-lang rejected every <script lang="ts">, because with no options they only allow blocks without a lang attribute. They now allow TypeScript scripts, and less and scss styles, as well as blocks without lang.
    • Astro’s client-side <script> tags are linted as virtual files named 1_1.ts, 2_1.js and so on, which github/filenames-match-regex reported on every page with a script tag. Filename rules are now off for those virtual files.
    • The Astro preset passes @typescript-eslint/parser for the frontmatter explicitly. ESLint’s config merge dropped the copy in eslint-plugin-astro’s base config, so frontmatter TypeScript parsed only when @typescript-eslint/parser happened to resolve from the working directory, and failed with The keyword 'interface' is reserved otherwise.
    • ultracite init now installs eslint-plugin-jsx-a11y for Astro projects using ESLint. The preset enables eslint-plugin-astro’s astro/jsx-a11y/* rules, which load that package and reported “you need to install eslint-plugin-jsx-a11y” on every .astro file when it was missing.
  • e88e76d: The Prettier preset now sets tailwindFunctions to clsx, cva, tw, twMerge, cn, twJoin and tv, so prettier-plugin-tailwindcss sorts Tailwind classes passed to those helpers anywhere in a file, for example const button = cva("p-4 flex"). Previously only class strings inside class/className attributes were sorted with Prettier, while the Oxfmt (sortTailwindcss.functions) and Biome (useSortedClasses) presets already sorted these helpers. See https://github.com/tailwindlabs/prettier-plugin-tailwindcss#sorting-classes-in-function-calls.

  • 3a5885e: github/filenames-match-regex now accepts file-based routing filenames in the ESLint preset too, and Next.js’s special pages in both presets.

    • The page-route regex (Oxlint js-plugins and now ESLint core) allows a leading underscore, so the Next.js pages-router files _app, _document and _error, which cannot be renamed, are no longer reported. Other names in pages/ (BadPage.ts) and camelCase route params ([postId], which unicorn/filename-case rejects too) are still reported.
    • The ESLint core preset now carries the same route exemptions as the Oxlint js-plugins preset (#799, #804). Previously it applied the plain kebab-case regex everywhere and reported SvelteKit’s +page.ts and +server.ts, TanStack Router and React Router files such as posts.$postId.ts, and page routes such as [slug].json.ts and [...auth].ts.
    • The route-directory exemptions (github/filenames-match-regex in Oxlint js-plugins and ESLint core, plus unicorn/filename-case and no-use-before-define in the Oxlint and ESLint tanstack presets) now cover .js and .jsx route files as well as .ts and .tsx.
  • ad2adb0: The Stylelint preset now handles Tailwind CSS v4, SCSS and Less, and skips build output.

    • Tailwind CSS v4: @theme, @utility, @variant, @custom-variant, @slot, @plugin and @config are allowed by at-rule-no-unknown, alongside the Tailwind v3 at-rules and @source and @reference. at-rule-prelude-no-invalid no longer rejects every @apply (or the other Tailwind at-rules’ preludes). custom-property-pattern accepts theme namespace resets (--color-*: initial) and sub-properties (--text-xl--line-height). declaration-property-value-no-unknown and function-no-unknown accept --spacing(), --alpha(), --value(), --modifier() and theme(). media-query-no-invalid accepts theme() breakpoints, and nesting-selector-no-missing-scoping-root allows & inside @custom-variant, @variant and @utility. See https://tailwindcss.com/docs/functions-and-directives.
    • import-notation is now "string". stylelint-config-standard’s "url" autofix rewrote @import "tailwindcss" to @import url("tailwindcss").
    • SCSS and Less: .scss files are parsed with postcss-scss and .less files with postcss-less. Previously every SCSS and Less file failed with CssSyntaxError (for example on // comments or mixin calls). The CSS-only rules that cannot understand preprocessor syntax (unknown at-rules such as @use, @include and @mixin, $variable and @variable values, Sass and Less built-in functions, // comments) are relaxed for those files. ultracite init now installs postcss-scss and postcss-less with the ESLint toolchain.
    • .sass: the indented Sass syntax has no maintained PostCSS parser, so ultracite check and ultracite fix no longer pass .sass files to Stylelint, where every one failed to parse.
    • Ignores: ignoreFiles now uses the shared ignore list (dist, build, .next, coverage, storybook-static and so on). Stylelint does not read .gitignore, so it previously linted generated CSS.
    • Prettier: prettier/prettier is now enabled. The preset loaded stylelint-prettier but never turned its rule on.
    • ultracite check and ultracite fix now escape glob characters in directory arguments before passing them to Stylelint. A directory such as app/(marketing) previously matched no files.
  • 89989df: Test-file overrides in every preset now match .mts, .cts, .mjs and .cjs test files as well as .ts, .tsx, .js and .jsx. The globs were **/*.{test,spec,test-d,spec-d}.{ts,tsx,js,jsx} and **/__tests__/**/*.{ts,tsx,js,jsx}, so a sum.test.mts file got none of the test-file relaxations and none of the Jest or Vitest rules. For example, test.only in a .test.mts file was not reported by vitest/no-focused-tests or jest/no-focused-tests. This covers the Oxlint core, js-plugins, jest and vitest presets, the Biome core preset, and the ESLint core, jest and vitest presets.

  • 4d28cf9: The Oxlint vue preset now lists every non-nursery Vue rule Oxlint implements, like the other framework presets. It previously enabled 18 of them. Among the 27 newly enabled rules are vue/valid-define-props, vue/valid-define-emits, vue/no-export-in-script-setup, vue/no-lifecycle-after-await, vue/no-arrow-functions-in-watch, vue/no-side-effects-in-computed-properties, vue/require-typed-ref, vue/define-props-destructuring and the vue/no-deprecated-* rules. vue/max-props is off in both the Oxlint and ESLint presets, because its default allows a single prop per component.

    The Oxlint vitest preset now enables vitest/consistent-test-it, the one Vitest rule it was missing (the ESLint preset already had it).

    Vue files whose names Nuxt and file-based Vue Router require (pages/**, layouts/**, error.vue and app.vue) are exempt from useVueMultiWordComponentNames in the Biome preset and from vue/multi-word-component-names in the ESLint preset. Previously pages/index.vue, layouts/default.vue and error.vue were always reported.

Last updated on September 29, 2026

Was this page helpful?