This didn’t start as a tooling migration—it started with my own curiosity.
Oxlint and Oxfmt were suddenly everywhere. People were claiming massive speed improvements and near-instant TypeScript linting. That sounded great, but most benchmarks were tiny demo projects. I wanted to see what happens in a real Angular codebase.
There was also a real problem underneath the curiosity. ESLint + Prettier had slowly become friction in the developer workflow. Linting was slow enough that people stopped running it locally and relied more on pre-commit and CI checks instead. Over time, this created slower feedback loops, which is usually the point where tooling starts getting in the way instead of helping developers move faster.
So, I tried replacing Prettier and most TypeScript ESLint checks with Oxfmt + Oxlint using Vite+, while keeping ESLint for Angular templates. Here’s what actually happened.
First Important Clarification
I did not replace Angular CLI. Angular CLI still handles:
ng serveng buildng test
What I changed was only the quality tooling layer:
- Prettier → replaced by Oxfmt
- Most TypeScript ESLint rules → replaced by Oxlint
- ESLint → kept only for Angular templates
- Stylelint → kept for CSS rules
- Vite+ → used as the orchestrator for staged linting and formatting
Angular CLI still builds and serves the app. Vite+ is only used for linting, formatting, and staged file checks.
Why Not Biome?
Biome came up immediately. Biome is great, especially for greenfield JS or React projects.
But Angular has one big requirement: @angular-eslint rules. Biome doesn’t support Angular ESLint plugins. Oxlint doesn’t fully support Angular either, but it allows Angular TypeScript rules via plugins, which made it usable for Angular projects.
The Angular Limitation (This Is Important)
Oxlint cannot lint Angular templates yet. Templates require a custom parser, so ESLint is still required for .html files. However, Angular TypeScript rules can now run inside Oxlint, meaning ESLint is only needed for templates.
Final Tool Responsibility Split
| File Type | Formatting | Linting |
|---|---|---|
| .ts | Oxfmt | Oxlint + Angular TS rules |
| .html | Oxfmt | ESLint (Angular template rules) |
| .css / .scss | Oxfmt | Stylelint (rules only) |
- Oxfmt handles formatting everywhere.
- Oxlint handles TypeScript linting.
- ESLint handles Angular templates.
- Stylelint handles CSS rules.
- Vite+ orchestrates everything.
Once this responsibility split was clear, the setup actually felt simpler than the traditional ESLint + Prettier + Stylelint pipeline.
Core Idea of the Setup
What this really means is... Vite+ acts as a high-speed traffic controller. Instead of running one heavy ESLint process on every file, it routes each file type to the fastest tool that can handle it correctly.
The orchestration lives in the Vite+ config:
// vite.config.js
import { defineConfig } from 'vite-plus';
export default defineConfig({
ignorePatterns: ['dist/**', '.angular/**', 'node_modules/**'],
staged: {
'*.ts': 'vp lint --fix',
'*.{json,html}': 'vp fmt',
'*.html': 'eslint --fix',
'*.{css,scss}': ['vp fmt', 'stylelint --fix'],
},
});
Formatting rules live separately in .oxfmtrc.json:
{
"$schema": "./node_modules/oxfmt/configuration_schema.json",
"singleQuote": true,
"semi": true,
"printWidth": 100,
"tabWidth": 2,
"trailingComma": "all"
}
Keeping Oxfmt rules in .oxfmtrc.json turned out to be more reliable than nesting them inside vite.config.js. That split ended up being the more maintainable option: runner config in vite.config.js, tool rules in tool-specific config files.
Step-by-Step Setup for Your Angular Project
1. Install dependencies
npm install --save-dev vite-plus stylelint stylelint-config-standard stylelint-order stylelint-config-recess-order @angular-eslint/eslint-plugin @angular-eslint/template-parser @angular-eslint/eslint-plugin-template
2. Configure Oxlint (.oxlintrc.json)
{
"jsPlugins": ["@angular-eslint/eslint-plugin"],
"ignorePatterns": ["dist/**", ".angular/**", "node_modules/**"],
"rules": {
"no-debugger": "error",
"no-var": "error",
"prefer-const": "error",
"@typescript-eslint/no-unused-vars": "warn",
"@angular-eslint/no-output-native": "error",
"@angular-eslint/use-lifecycle-interface": "warn",
"@angular-eslint/component-selector": [
"error",
{ "type": "element", "prefix": "app", "style": "kebab-case" }
]
}
}
3. Configure Oxfmt (.oxfmtrc.json)
{
"$schema": "./node_modules/oxfmt/configuration_schema.json",
"singleQuote": true,
"semi": true,
"printWidth": 100,
"tabWidth": 2,
"trailingComma": "all"
}
4. Configure ESLint for Templates (eslint.config.js)
import templateParser from '@angular-eslint/template-parser';
import templatePlugin from '@angular-eslint/eslint-plugin-template';
export default [
{
files: ['**/*.html'],
languageOptions: {
parser: templateParser,
},
plugins: {
'@angular-eslint/template': templatePlugin,
},
rules: {
'@angular-eslint/template/button-has-type': 'error',
'@angular-eslint/template/no-negated-async': 'warn',
'@angular-eslint/template/alt-text': 'error',
},
},
];
5. Configure Stylelint (.stylelintrc.json)
{
"extends": ["stylelint-config-standard", "stylelint-config-recess-order"],
"ignoreFiles": ["dist/**", "node_modules/**", ".angular/**"],
"rules": {
"color-no-invalid-hex": true,
"unit-no-unknown": true,
"property-no-unknown": true,
"declaration-block-no-duplicate-properties": true,
"color-named": "never",
"no-empty-source": null
}
}
6. Update package.json scripts
"scripts": {
"ng": "ng",
"start": "ng serve",
"build": "ng build",
"test": "ng test",
"lint": "vp lint",
"lint:fix": "vp lint --fix",
"lint:html": "eslint \"src/**/*.html\"",
"lint:styles": "stylelint \"src/**/*.{css,scss}\"",
"lint:styles:fix": "stylelint \"src/**/*.{css,scss}\" --fix",
"format": "vp fmt --check",
"format:fix": "vp fmt"
}
7. Add a Pre-Commit Hook (Important)
Vite+ handles staged file processing, but you still need a Git hook to trigger it during commits.
Create the file: .git/hooks/pre-commit and add:
#!/bin/sh
npx vp check
Make it executable: chmod +x .git/hooks/pre-commit
Now every commit will automatically format staged files and run the relevant linters. This replaces the need for Husky + lint-staged.
Before vs After Tooling
| Before | After |
|---|---|
| ESLint (TS + HTML) | Oxlint (TypeScript) |
| Prettier | ESLint (HTML templates only) |
| Stylelint | Stylelint (CSS rules only) |
| Manual formatting drift | Oxfmt (formatting everywhere) |
| One big lint pipeline | Vite+ orchestrating specialized tools |
Biggest Improvement
Yes, Oxlint is faster than ESLint. But the biggest win was the workflow:
- Before: Linting mostly happened in CI → After: Linting happens locally again.
- Before: Pre-commit was slow → After: Pre-commit fast again.
- Result: Faster feedback loops and a smoother development workflow.
Things to Watch Out For
- Oxlint auto-fix is more limited than ESLint.
- ESLint is still required for Angular templates.
- Multiple config files: You still have to manage them, but it’s a fair trade for speed.
-
Keep rules separate from orchestration: For larger apps, don't force everything into
vite.config.js.
Final Thoughts
For Angular projects today, completely replacing ESLint is still not realistic because templates require specialized rules. But splitting responsibilities between specialized tools—Oxlint for TS, ESLint for templates, Stylelint for styles, and Oxfmt for formatting—with Vite+ as the orchestrator is a game-changer.
The biggest improvement was not just lint speed, but developer behavior. When the tools are fast, developers actually use them.
🚀 Full boilerplate and working config: angular-viteplus

Top comments (0)