はじめに
Oxlintは、JavaScript/TypeScriptコードの品質チェックを行うためにRustで開発された次世代リンターです。
Oxlintは2025年12月現在、eslint-plugin-svelteに対応していないため、OxlintとESLintを併用して使用することになります。
環境構築
今回はsv createコマンドを使用して、Svelteの環境を構築します。セットアップ時にeslintをアドオンとして選択してください。
pnpm dlx sv create my-app
┌ Welcome to the Svelte CLI! (v0.10.8)
│
◇ Which template would you like?
│ SvelteKit demo
│
◇ Add type checking with TypeScript?
│ Yes, using TypeScript syntax
│
◇ What would you like to add to your project? (use arrow keys / space bar)
│ eslint
│
◆ Project created
│
◆ Successfully setup add-ons: eslint
│
◇ Which package manager do you want to install dependencies with?
│ pnpm
│
│ pnpm dlx sv create --template demo --types ts --add eslint --install pnpm my-app
│
│
◆ Successfully installed dependencies with pnpm
│
◇ What's next? ───────────────────────────────╮
│ │
│ 📁 Project steps │
│ │
│ 1: cd my-app │
│ 2: pnpm run dev --open │
│ │
│ To close the dev server, hit Ctrl-C │
│ │
│ Stuck? Visit us at https://svelte.dev/chat │
│ │
├──────────────────────────────────────────────╯
│
└ You're all set!
Oxlintのインストール
次に、Oxlint本体とESLintとの競合を避けるためのプラグインをインストールします。
pnpm add -D oxlint eslint-plugin-oxlint
設定ファイルの移行
公式が提供している移行ツール@oxlint/migrateを使用すると、既存のESLint Flat ConfigからOxlint用の設定ファイルを生成できます。
ただし、実行時のログを見るとわかる通り、現時点ではeslint-plugin-svelteのルールはunsupported ruleとして無視されます。
pnpm dlx @oxlint/migrate eslint.config.js
unsupported rule, but available as a nursery rule: getter-return
unsupported rule: no-dupe-args
unsupported rule, but available as a nursery rule: no-misleading-character-class
unsupported rule: no-octal
unsupported rule, but available as a nursery rule: no-undef
unsupported rule, but available as a nursery rule: no-unreachable
unsupported rule: prefer-const
unsupported rule: svelte/comment-directive
unsupported rule: svelte/system
special parser detected: svelte-eslint-parser
unsupported rule: svelte/infinite-reactive-loop
unsupported rule: svelte/no-at-debug-tags
unsupported rule: svelte/no-at-html-tags
unsupported rule: svelte/no-dom-manipulating
unsupported rule: svelte/no-dupe-else-if-blocks
unsupported rule: svelte/no-dupe-on-directives
unsupported rule: svelte/no-dupe-style-properties
unsupported rule: svelte/no-dupe-use-directives
unsupported rule: svelte/no-export-load-in-svelte-module-in-kit-pages
unsupported rule: svelte/no-immutable-reactive-statements
unsupported rule: svelte/no-inner-declarations
unsupported rule: svelte/no-inspect
unsupported rule: svelte/no-navigation-without-resolve
unsupported rule: svelte/no-not-function-handler
unsupported rule: svelte/no-object-in-text-mustaches
unsupported rule: svelte/no-raw-special-elements
unsupported rule: svelte/no-reactive-functions
unsupported rule: svelte/no-reactive-literals
unsupported rule: svelte/no-reactive-reassign
unsupported rule: svelte/no-shorthand-style-property-overrides
unsupported rule: svelte/no-store-async
unsupported rule: svelte/no-svelte-internal
unsupported rule: svelte/no-unknown-style-directive-property
unsupported rule: svelte/no-unnecessary-state-wrap
unsupported rule: svelte/no-unused-props
unsupported rule: svelte/no-unused-svelte-ignore
unsupported rule: svelte/no-useless-children-snippet
unsupported rule: svelte/no-useless-mustaches
unsupported rule: svelte/prefer-svelte-reactivity
unsupported rule: svelte/prefer-writable-derived
unsupported rule: svelte/require-each-key
unsupported rule: svelte/require-event-dispatcher-types
unsupported rule: svelte/require-store-reactive-access
unsupported rule: svelte/valid-each-key
unsupported rule: svelte/valid-prop-names-in-kit-pages
以下のようなファイルが生成されます。
{
"$schema": "./node_modules/oxlint/configuration_schema.json",
"plugins": [
"typescript"
],
"categories": {
"correctness": "off"
},
"env": {
"builtin": true,
"browser": true,
"node": true
},
"ignorePatterns": [
"**/node_modules",
"**/.output",
"**/.vercel",
"**/.netlify",
"**/.wrangler",
".svelte-kit",
"build",
"**/.DS_Store",
"**/Thumbs.db",
"**/.env",
"**/.env.*",
"!**/.env.example",
"!**/.env.test",
"**/vite.config.js.timestamp-*",
"**/vite.config.ts.timestamp-*"
],
"rules": {
"constructor-super": "error",
"for-direction": "error",
"no-async-promise-executor": "error",
"no-case-declarations": "error",
"no-class-assign": "error",
"no-compare-neg-zero": "error",
"no-cond-assign": "error",
"no-const-assign": "error",
"no-constant-binary-expression": "error",
"no-constant-condition": "error",
"no-control-regex": "error",
"no-debugger": "error",
"no-delete-var": "error",
"no-dupe-class-members": "error",
"no-dupe-else-if": "error",
"no-dupe-keys": "error",
"no-duplicate-case": "error",
"no-empty": "error",
"no-empty-character-class": "error",
"no-empty-pattern": "error",
"no-empty-static-block": "error",
"no-ex-assign": "error",
"no-extra-boolean-cast": "error",
"no-fallthrough": "error",
"no-func-assign": "error",
"no-global-assign": "error",
"no-import-assign": "error",
"no-invalid-regexp": "error",
"no-irregular-whitespace": "error",
"no-loss-of-precision": "error",
"no-new-native-nonconstructor": "error",
"no-nonoctal-decimal-escape": "error",
"no-obj-calls": "error",
"no-prototype-builtins": "error",
"no-redeclare": "error",
"no-regex-spaces": "error",
"no-self-assign": "error",
"no-setter-return": "error",
"no-shadow-restricted-names": "error",
"no-sparse-arrays": "error",
"no-this-before-super": "error",
"no-unexpected-multiline": "error",
"no-unsafe-finally": "error",
"no-unsafe-negation": "error",
"no-unsafe-optional-chaining": "error",
"no-unused-labels": "error",
"no-unused-private-class-members": "error",
"no-unused-vars": "error",
"no-useless-backreference": "error",
"no-useless-catch": "error",
"no-useless-escape": "error",
"no-with": "error",
"require-yield": "error",
"use-isnan": "error",
"valid-typeof": "error",
"@typescript-eslint/ban-ts-comment": "error",
"no-array-constructor": "error",
"@typescript-eslint/no-duplicate-enum-values": "error",
"@typescript-eslint/no-empty-object-type": "error",
"@typescript-eslint/no-explicit-any": "error",
"@typescript-eslint/no-extra-non-null-assertion": "error",
"@typescript-eslint/no-misused-new": "error",
"@typescript-eslint/no-namespace": "error",
"@typescript-eslint/no-non-null-asserted-optional-chain": "error",
"@typescript-eslint/no-require-imports": "error",
"@typescript-eslint/no-this-alias": "error",
"@typescript-eslint/no-unnecessary-type-constraint": "error",
"@typescript-eslint/no-unsafe-declaration-merging": "error",
"@typescript-eslint/no-unsafe-function-type": "error",
"no-unused-expressions": "error",
"@typescript-eslint/no-wrapper-object-types": "error",
"@typescript-eslint/prefer-as-const": "error",
"@typescript-eslint/prefer-namespace-keyword": "error",
"@typescript-eslint/triple-slash-reference": "error"
},
"overrides": [
{
"files": [
"**/*.ts",
"**/*.tsx",
"**/*.mts",
"**/*.cts"
],
"rules": {
"constructor-super": "off",
"no-class-assign": "off",
"no-const-assign": "off",
"no-dupe-class-members": "off",
"no-dupe-keys": "off",
"no-func-assign": "off",
"no-import-assign": "off",
"no-new-native-nonconstructor": "off",
"no-obj-calls": "off",
"no-redeclare": "off",
"no-setter-return": "off",
"no-this-before-super": "off",
"no-unsafe-negation": "off",
"no-var": "error",
"no-with": "off",
"prefer-rest-params": "error",
"prefer-spread": "error"
}
},
{
"files": [
"*.svelte",
"**/*.svelte"
],
"rules": {
"no-inner-declarations": "off",
"no-self-assign": "off"
}
}
]
}
OxlintとESLintを併用する
eslint-plugin-svelteが対応していないのでtsファイルなどはOxlintを使いつつ、svelteファイルはESLintを使うような併用をしないといけません。
その際に、eslint-plugin-oxlintを使用し、OxlintがすでにサポートしているルールをESLintで無効化することにより重複するルールチェックを避けることができます。
import { fileURLToPath } from 'node:url';
import { includeIgnoreFile } from '@eslint/compat';
import js from '@eslint/js';
import svelte from 'eslint-plugin-svelte';
import { defineConfig } from 'eslint/config';
import globals from 'globals';
import ts from 'typescript-eslint';
import svelteConfig from './svelte.config.js';
+ import oxlint from 'eslint-plugin-oxlint';
const gitignorePath = fileURLToPath(new URL('./.gitignore', import.meta.url));
export default defineConfig(
includeIgnoreFile(gitignorePath),
js.configs.recommended,
...ts.configs.recommended,
...svelte.configs.recommended,
{
languageOptions: { globals: { ...globals.browser, ...globals.node } },
rules: {
// typescript-eslint strongly recommend that you do not use the no-undef lint rule on TypeScript projects.
// see: https://typescript-eslint.io/troubleshooting/faqs/eslint/#i-get-errors-from-the-no-undef-rule-about-global-variables-not-being-defined-even-though-there-are-no-typescript-errors
"no-undef": 'off'
}
},
{
files: ['**/*.svelte', '**/*.svelte.ts', '**/*.svelte.js'],
languageOptions: {
parserOptions: {
projectService: true,
extraFileExtensions: ['.svelte'],
parser: ts.parser,
svelteConfig
}
}
},
+ ...oxlint.buildFromOxlintConfigFile("./.oxlintrc.json"),
);
実行
ESLintとOxlintを併用する場合は、Oxlint → ESLint の順番で実行する必要があります。
- "lint": "eslint ."
+ "lint": "oxlint && eslint ."
計測
hyperfineを使用し、約600ファイルに対してそれぞれ実行しました。
| 項目 | ESLint | Oxlint+ESLint | 比較 |
|---|---|---|---|
| 平均実行時間 (mean) | 3.851 s | 3.830 s | 0.021 s 短縮 (1.01倍速) |
| 標準偏差 ($\sigma$) | ± 0.025 s | ± 0.072 s | - |
| 実行範囲 (min...max) | 3.809 s … 3.909 s | 3.772 s … 4.018 s | - |
| ユーザー時間 (User) | 5.745 s | 5.623 s | 0.122 s 減少 |
| システム時間 (System) | 0.520 s | 0.875 s | 0.355 s 増加 |
結論
Svelteファイルが多くを占めるSvelteの開発環境ではOxlintとESLintを併用したとしても現時点では劇的な速度向上は見込めないという結果になりました。
これは、ボトルネックの多くがESLintによるSvelteファイルのパースやチェックによるものだと思われます。
現時点ではSvelte開発環境においてはOxlintとESLintを併用して使うメリットが少ないのでSvelteが対応されるのを期待して待ちたいと思います。