Skip to content

Remove ESLint package support#50

Merged
kentcdodds merged 5 commits intomainfrom
cursor/eslint-removal-c888
Mar 26, 2026
Merged

Remove ESLint package support#50
kentcdodds merged 5 commits intomainfrom
cursor/eslint-removal-c888

Conversation

@kentcdodds
Copy link
Copy Markdown
Member

@kentcdodds kentcdodds commented Mar 26, 2026

Summary

  • remove the published ESLint configs, ESLint plugin entry points, and ESLint package dependencies
  • switch the repo's own lint workflow to Oxlint and preserve custom rule coverage with Oxlint-native Vitest tests
  • add an ADR for the move to Oxlint, restore an Oxlint suggestion discussion path, and clarify fixture ownership in the Oxlint test helper

Testing

  • npm run lint
  • npm test
  • npm run typecheck
Open in Web Open in Cursor 

cursoragent and others added 4 commits March 26, 2026 19:52
Co-authored-by: Kent C. Dodds <me+github@kentcdodds.com>
Co-authored-by: Kent C. Dodds <me+github@kentcdodds.com>
Co-authored-by: Kent C. Dodds <me+github@kentcdodds.com>
Co-authored-by: Kent C. Dodds <me+github@kentcdodds.com>
@kentcdodds kentcdodds marked this pull request as ready for review March 26, 2026 20:48
Copy link
Copy Markdown

@cursor cursor bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 2 potential issues.

Fix All in Cursor

Bugbot Autofix prepared fixes for both issues found in the latest run.

  • ✅ Fixed: Unused readLockfile export is dead code
    • Removed the unused export and its unused dependency to eliminate dead code.
  • ✅ Fixed: Test helper throws on error-severity lint diagnostics
    • Adjusted the exit code handling to allow code 1 outputs to be parsed instead of throwing.
Preview (3f47644334)
diff --git a/.github/DISCUSSION_TEMPLATE/eslint-suggestion.yml b/.github/DISCUSSION_TEMPLATE/eslint-suggestion.yml
deleted file mode 100644
--- a/.github/DISCUSSION_TEMPLATE/eslint-suggestion.yml
+++ /dev/null
@@ -1,64 +1,0 @@
-body:
-  - type: markdown
-    attributes:
-      value: |-
-        Thank you for helping to improve `@epic-web/config`!
-
-        Please fill out the details below. Keep in mind that every rule added to the linting config:
-
-        1. Helps catch potential issues
-        2. Slows down running ESLint
-        3. Increases the number of annoying false positives
-
-        With that in mind, please consider the following guiding principles which will be used to evaluate your suggestion:
-
-        1. The rule must catch issues that are likely to happen
-        2. The rule must prevent actual issues (not subjective choices)
-
-        You can always extend the built-in configuration by adding your own rules in your project. The ones in the built-in config need to balance the benefit and the cost of adding a new rule.
-  - type: textarea
-    attributes:
-      label: Search Terms Used
-      description: >-
-        Please list the search terms you used to search the discussions, issues,
-        and PRs before submitting this issue.
-    validations:
-      required: true
-  - type: input
-    attributes:
-      label: Rule Docs
-      description: >-
-        Link to the documentation for the rule you're suggesting changing
-      placeholder: https://eslint.org/docs/latest/rules/no-var
-    validations:
-      required: true
-  - type: textarea
-    attributes:
-      label: Suggested Change
-      description: >-
-        Write out the suggested change in configuration. If the rule is already
-        enabled, explain why it should be disabled or changed. If it's not
-        enabled, explain why it should be enabled. And provide the final
-        configuration for the rule (if it has any additional options).
-    validations:
-      required: true
-  - type: textarea
-    attributes:
-      label: Issue Likelihood
-      description: >-
-        Convince me that the rule catches issues that engineers will likely
-        encounter.
-    validations:
-      required: true
-  - type: textarea
-    attributes:
-      label: Issue Severity
-      description: Convince me that the rule prevents actual issues.
-    validations:
-      required: true
-  - type: textarea
-    attributes:
-      label: Additional Context
-      description: Any other context that could be helpful.
-    validations:
-      required: false
\ No newline at end of file

diff --git a/.github/DISCUSSION_TEMPLATE/oxlint-suggestion.yml b/.github/DISCUSSION_TEMPLATE/oxlint-suggestion.yml
new file mode 100644
--- /dev/null
+++ b/.github/DISCUSSION_TEMPLATE/oxlint-suggestion.yml
@@ -1,0 +1,55 @@
+body:
+  - type: markdown
+    attributes:
+      value: |-
+        Thank you for helping to improve `@epic-web/config`!
+
+        Please fill out the details below.
+
+        Keep in mind you can always extend the built-in configuration with your own config.
+  - type: textarea
+    attributes:
+      label: Search Terms Used
+      description: >-
+        Please list the search terms you used to search the discussions, issues,
+        and PRs before submitting this issue.
+    validations:
+      required: true
+  - type: input
+    attributes:
+      label: Rule or Config Docs
+      description: >-
+        Link to the Oxlint rule or configuration docs you're suggesting
+        changing
+      placeholder: https://oxc.rs/docs/guide/usage/linter/rules/
+    validations:
+      required: true
+  - type: textarea
+    attributes:
+      label: Suggested Change
+      description: >-
+        Write out the suggested change in configuration. If the rule or config is
+        already present, explain why it should be removed or changed. If it's not
+        present, explain why it should be added. And provide the final
+        configuration for the option.
+    validations:
+      required: true
+  - type: textarea
+    attributes:
+      label: Issues without the change
+      description: >-
+        Convince me why life is better with this change.
+    validations:
+      required: true
+  - type: textarea
+    attributes:
+      label: Severity of inaction
+      description: Convince me why not doing anything is bad.
+    validations:
+      required: true
+  - type: textarea
+    attributes:
+      label: Additional Context
+      description: Any other context that could be helpful.
+    validations:
+      required: false

diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml
--- a/.github/ISSUE_TEMPLATE/config.yml
+++ b/.github/ISSUE_TEMPLATE/config.yml
@@ -5,11 +5,12 @@
     about:
       If you can't get something to work the way you expect, open a question in
       the discussions.
-  - name: 👮 ESLint Rule Suggestion
-    url: https://github.com/epicweb-dev/config/discussions/new?category=eslint-suggestion
+  - name: 🐂 Oxlint Rule Suggestion
+    url: https://github.com/epicweb-dev/config/discussions/new?category=oxlint-suggestion
     about:
       We appreciate you taking the time to improve `@epic-web/config` with your
-      ideas, but we use the Discussions for this instead of the issues tab 🙂.
+      ideas, but we use Discussions for config suggestions instead of the issues
+      tab.
   - name: 💬 Epic Web Discord Server
     url: https://kcd.im/discord
     about: Interact with other Epic Web developers

diff --git a/README.md b/README.md
--- a/README.md
+++ b/README.md
@@ -1,7 +1,7 @@
 <div>
   <h1 align="center"><a href="https://npm.im/@epic-web/config">👮 @epic-web/config</a></h1>
   <strong>
-    Reasonable ESLint, Oxlint, Prettier, and TypeScript configs for epic web devs
+    Reasonable Oxlint, Prettier, and TypeScript configs for epic web devs
   </strong>
   <p>
     This makes assumptions about the way you prefer to develop software and gives you configurations that will actually help you in your development.
@@ -40,9 +40,12 @@
 
 ## This solution
 
-This is a set of configurations you can use in your web projects to avoid
-wasting time.
+This package provides shared defaults for the tools this repo currently ships:
 
+- Oxlint
+- Prettier
+- TypeScript
+
 ## Decisions
 
 You can learn about the different decisions made for this project in
@@ -114,33 +117,6 @@
 
 </details>
 
-### ESLint
-
-Create a `eslint.config.js` file in your project root with the following
-content:
-
-```js
-import { config as defaultConfig } from '@epic-web/config/eslint'
-
-/** @type {import("eslint").Linter.Config[]} */
-export default [...defaultConfig]
-```
-
-<details>
-  <summary>Customizing ESLint</summary>
-
-Learn more from
-[the Eslint docs here](https://eslint.org/docs/latest/extend/shareable-configs#overriding-settings-from-shareable-configs).
-
-</details>
-
-There are endless rules we could enable. However, we want to keep our
-configurations minimal and only enable rules that catch real problems (the kind
-that are likely to happen). This keeps our linting faster and reduces the number
-of false positives.
-
-Custom rule documentation lives in [`lint-rules/index.md`](./lint-rules/index.md).
-
 ### Oxlint
 
 Create a `.oxlintrc.json` file in your project root with the following content:
@@ -151,22 +127,28 @@
 }

+This config includes the custom epic-web/* rules documented in
+lint-rules/index.md.
+
Note: typescript/no-misused-promises and typescript/no-floating-promises are
type-aware in Oxlint and require the type-aware setup described in the Oxlint
docs.

-#### Unsupported rules
+Some Oxlint rule IDs still use the eslint/ namespace because that is how
+Oxlint exposes those compatibility rules. You do not need to install ESLint to
+use them.

-The following ESLint rules/plugins from this config are not yet available in
-Oxlint, so they are intentionally omitted:
+#### Not yet covered

+The following rule families are intentionally omitted because they are not yet
+part of the Oxlint config this package ships:
+

  • import/order
  • react-hooks/rules-of-hooks
  • react-hooks/exhaustive-deps
    -- @typescript-eslint/no-unused-vars (falls back to eslint/no-unused-vars)
  • testing-library/*
  • jest-dom/*
    -- vitest/* (except vitest/no-import-node-test)
    +- most vitest/* rules
  • playwright/*

License

diff --git a/docs/decisions/010-move-to-oxlint.md b/docs/decisions/010-move-to-oxlint.md
new file mode 100644
--- /dev/null
+++ b/docs/decisions/010-move-to-oxlint.md
@@ -1,0 +1,41 @@
+# Move to Oxlint
+
+Date: 2026-03-26
+
+Status: accepted
+
+## Context
+
+This package originally shipped both ESLint and Oxlint configuration. Over time,
+the Oxlint side of the package became the simpler path to maintain and consume.
+
+There are a few reasons for that:
+
+1. Oxlint is dramatically faster, which makes local feedback and CI validation

  • cheaper.
    +2. The configuration story is simpler when we only ship one linting toolchain.
    +3. The Oxlint project has an active and clearly invested team behind it, which
  • makes it a good foundation to build on.
    +4. Oxlint's compatibility with many established ESLint rule IDs makes migration
  • straightforward, so moving away from ESLint does not require a wholesale
  • rethink of the existing rule set.

+Because Oxlint can consume many familiar eslint/... rule identifiers and we
+already author our custom rules as Oxlint JS plugins, the migration path is
+mostly about removing duplicate package surface rather than redesigning the
+linting rules themselves.
+
+## Decision
+
+Ship Oxlint as the package's linting solution and remove the published ESLint
+configs, ESLint plugin entry points, and ESLint package dependencies.
+
+## Consequences
+
+- Consumers must migrate from the removed ESLint entry points to the Oxlint

  • config.
    +- Some rule IDs may continue to use the eslint/ namespace inside Oxlint
  • config, because that is how Oxlint exposes compatibility rules.
    +- The package becomes smaller and easier to maintain because it only supports a
  • single linting toolchain.
    +- Linting should be faster for both maintainers and consumers.

diff --git a/docs/style-guide.md b/docs/style-guide.md
--- a/docs/style-guide.md
+++ b/docs/style-guide.md
@@ -1073,10 +1073,9 @@

Don't use semicolons. The rules for when you should use semicolons are more
complicated than the rules for when you must use semicolons. With the right
-eslint rule
-(no-unexpected-multiline)
-and a formatter that will format your code funny for you if you mess up, you can
-avoid the pitfalls. Read more about this in
+Oxlint rule (eslint/no-unexpected-multiline) and a formatter that will format
+your code funny for you if you mess up, you can avoid the pitfalls. Read more
+about this in
Semicolons in JavaScript: A preference.

@@ -1826,7 +1825,7 @@

Prioritize your tests according to the Testing Trophy:

-1. Static Analysis (TypeScript, ESLint)
+1. Static Analysis (TypeScript, Oxlint)
2. Unit Tests (Pure Functions)
3. Integration Tests (Component Integration)
4. E2E Tests (Critical User Flows)

diff --git a/eslint-plugin-epic-web.js b/eslint-plugin-epic-web.js
deleted file mode 100644
--- a/eslint-plugin-epic-web.js
+++ /dev/null
@@ -1,3 +1,0 @@
-export { default } from './lint-rules/epic-web-plugin.js'
-export { default as noManualDisposeRule } from './lint-rules/no-manual-dispose.js'
-export { default as preferDisposeInTestsRule } from './lint-rules/prefer-dispose-in-tests.js'
\ No newline at end of file

diff --git a/eslint.config.js b/eslint.config.js
deleted file mode 100644
--- a/eslint.config.js
+++ /dev/null
@@ -1 +1,0 @@
-export { default } from './eslint.js'
\ No newline at end of file

diff --git a/eslint.js b/eslint.js
deleted file mode 100644
--- a/eslint.js
+++ /dev/null
@@ -1,353 +1,0 @@
-import globals from 'globals'
-import epicWebPlugin from './lint-rules/epic-web-plugin.js'
-import { has } from './utils.js'

-const ERROR = 'error'
-const WARN = 'warn'

-const hasTypeScript = has('typescript')
-const hasReact = has('react')
-const hasTestingLibrary = has('@testing-library/dom')
-const hasJestDom = has('@testing-library/jest-dom')
-const hasVitest = has('vitest')
-const hasPlaywright = has('playwright')

-const vitestFiles = ['/tests//', '**/.test.', '**/.spec.*']
-const testFiles = ['/tests/', '/#tests/', ...vitestFiles]
-const playwrightFiles = ['/tests/e2e/']

-export const config = [

  • {
  •   ignores: [
    
  •   	'**/.cache/**',
    
  •   	'**/node_modules/**',
    
  •   	'**/build/**',
    
  •   	'**/public/**',
    
  •   	'**/*.json',
    
  •   	'**/playwright-report/**',
    
  •   	'**/server-build/**',
    
  •   	'**/dist/**',
    
  •   	'**/coverage/**',
    
  •   	'**/*.tsbuildinfo',
    
  •   	'**/.react-router/**',
    
  •   	'**/.wrangler/**',
    
  •   	'**/worker-configuration.d.ts',
    
  •   ],
    
  • },
  • // all files
  • {
  •   plugins: {
    
  •   	import: (await import('eslint-plugin-import-x')).default,
    
  •   	'epic-web': epicWebPlugin,
    
  •   },
    
  •   languageOptions: {
    
  •   	globals: {
    
  •   		...globals.browser,
    
  •   		...globals.node,
    
  •   	},
    
  •   },
    
  •   rules: {
    
  •   	'no-unexpected-multiline': ERROR,
    
  •   	'no-warning-comments': [
    
  •   		ERROR,
    
  •   		{ terms: ['FIXME'], location: 'anywhere' },
    
  •   	],
    
  •   	'epic-web/no-manual-dispose': WARN,
    
  •   	'import/no-duplicates': [WARN, { 'prefer-inline': true }],
    
  •   	'import/order': [
    
  •   		WARN,
    
  •   		{
    
  •   			alphabetize: { order: 'asc', caseInsensitive: true },
    
  •   			pathGroups: [{ pattern: '#*/**', group: 'internal' }],
    
  •   			groups: [
    
  •   				'builtin',
    
  •   				'external',
    
  •   				'internal',
    
  •   				'parent',
    
  •   				'sibling',
    
  •   				'index',
    
  •   			],
    
  •   		},
    
  •   	],
    
  •   },
    
  • },
  • // JSX/TSX files
  • hasReact
  •   ? {
    
  •   		files: ['**/*.tsx', '**/*.jsx'],
    
  •   		plugins: {
    
  •   			react: (await import('eslint-plugin-react')).default,
    
  •   		},
    
  •   		languageOptions: {
    
  •   			parser: hasTypeScript
    
  •   				? (await import('typescript-eslint')).parser
    
  •   				: undefined,
    
  •   			parserOptions: {
    
  •   				jsx: true,
    
  •   			},
    
  •   		},
    
  •   		rules: {
    
  •   			'react/jsx-key': WARN,
    
  •   		},
    
  •   	}
    
  •   : null,
    
  • // react-hook rules are applicable in ts/js/tsx/jsx, but only with React as a
  • // dep
  • hasReact
  •   ? {
    
  •   		files: ['**/*.ts?(x)', '**/*.js?(x)'],
    
  •   		plugins: {
    
  •   			'react-hooks': (await import('eslint-plugin-react-hooks')).default,
    
  •   		},
    
  •   		rules: {
    
  •   			'react-hooks/rules-of-hooks': ERROR,
    
  •   			'react-hooks/exhaustive-deps': WARN,
    
  •   		},
    
  •   	}
    
  •   : null,
    
  • // JS and JSX files
  • {
  •   files: ['**/*.js?(x)'],
    
  •   rules: {
    
  •   	'no-undef': ERROR,
    
  •   	// most of these rules are useful for JS but not TS because TS handles these better
    
  •   	// if it weren't for https://github.com/import-js/eslint-plugin-import/issues/2132
    
  •   	// we could enable this :(
    
  •   	// 'import/no-unresolved': ERROR,
    
  •   	'no-unused-vars': [
    
  •   		WARN,
    
  •   		{
    
  •   			args: 'after-used',
    
  •   			argsIgnorePattern: '^(_|ignored)',
    
  •   			ignoreRestSiblings: true,
    
  •   			varsIgnorePattern: '^(_|ignored)',
    
  •   		},
    
  •   	],
    
  •   },
    
  • },
  • // TS and TSX files
  • hasTypeScript
  •   ? {
    
  •   		files: ['**/*.ts?(x)'],
    
  •   		languageOptions: {
    
  •   			parser: (await import('typescript-eslint')).parser,
    
  •   			parserOptions: {
    
  •   				projectService: true,
    
  •   			},
    
  •   		},
    
  •   		plugins: {
    
  •   			'@typescript-eslint': (await import('typescript-eslint')).plugin,
    
  •   		},
    
  •   		rules: {
    
  •   			'@typescript-eslint/no-unused-vars': [
    
  •   				WARN,
    
  •   				{
    
  •   					args: 'after-used',
    
  •   					argsIgnorePattern: '^(_|ignored)',
    
  •   					ignoreRestSiblings: true,
    
  •   					varsIgnorePattern: '^(_|ignored)',
    
  •   				},
    
  •   			],
    
  •   			'import/consistent-type-specifier-style': [WARN, 'prefer-inline'],
    
  •   			'@typescript-eslint/consistent-type-imports': [
    
  •   				WARN,
    
  •   				{
    
  •   					prefer: 'type-imports',
    
  •   					disallowTypeAnnotations: true,
    
  •   					fixStyle: 'inline-type-imports',
    
  •   				},
    
  •   			],
    
  •   			'@typescript-eslint/no-misused-promises': [
    
  •   				'error',
    
  •   				{ checksVoidReturn: false },
    
  •   			],
    
  •   			'@typescript-eslint/no-floating-promises': 'error',
    
  •   			// here are rules we've decided to not enable. Commented out rather
    
  •   			// than setting them to disabled to avoid them being referenced at all
    
  •   			// when config resolution happens.
    
  •   			// @typescript-eslint/require-await - sometimes you really do want
    
  •   			// async without await to make a function async. TypeScript will ensure
    
  •   			// it's treated as an async function by consumers and that's enough for me.
    
  •   			// @typescript-eslint/prefer-promise-reject-errors - sometimes you
    
  •   			// aren't the one creating the error, and you just want to propagate an
    
  •   			// error object with an unknown type.
    
  •   			// @typescript-eslint/only-throw-error - same reason as above.
    
  •   			// However, this rule supports options to allow you to throw `any` and
    
  •   			// `unknown`. Unfortunately, in Remix you can throw Response objects,
    
  •   			// and we don't want to enable this rule for those cases.
    
  •   			// @typescript-eslint/no-unsafe-declaration-merging - this is a rare
    
  •   			// enough problem (especially if you focus on types over interfaces)
    
  •   			// that it's not worth enabling.
    
  •   			// @typescript-eslint/no-unsafe-enum-comparison - enums are not
    
  •   			// recommended or used in epic projects, so it's not worth enabling.
    
  •   			// @typescript-eslint/no-unsafe-unary-minus - this is a rare enough
    
  •   			// problem that it's not worth enabling.
    
  •   			// @typescript-eslint/no-base-to-string - this doesn't handle when
    
  •   			// your object actually does implement toString unless you do so with
    
  •   			// a class which is not 100% of the time. For example, the timings
    
  •   			// object in the epic stack uses defineProperty to implement toString.
    
  •   			// It's not high enough risk/impact to enable.
    
  •   			// @typescript-eslint/no-non-null-assertion - normally you should not
    
  •   			// use ! to tell TS to ignore the null case, but you're a responsible
    
  •   			// adult and if you're going to do that, the linter shouldn't yell at
    
  •   			// you about it.
    
  •   			// @typescript-eslint/restrict-template-expressions - toString is a
    
  •   			// feature of many built-in objects and custom ones. It's not worth
    
  •   			// enabling.
    
  •   			// @typescript-eslint/no-confusing-void-expression - what's confusing
    
  •   			// to one person isn't necessarily confusing to others. Arrow
    
  •   			// functions that call something that returns void is not confusing
    
  •   			// and the types will make sure you don't mess something up.
    
  •   			// these each protect you from `any` and while it's best to avoid
    
  •   			// using `any`, it's not worth having a lint rule yell at you when you
    
  •   			// do:
    
  •   			// - @typescript-eslint/no-unsafe-argument
    
  •   			// - @typescript-eslint/no-unsafe-call
    
  •   			// - @typescript-eslint/no-unsafe-member-access
    
  •   			// - @typescript-eslint/no-unsafe-return
    
  •   			// - @typescript-eslint/no-unsafe-assignment
    
  •   		},
    
  •   	}
    
  •   : null,
    
  • // This assumes test files are those which are in the test directory or have
  • // .test. or .spec. in the filename. If a file doesn't match this assumption,
  • // then it will not be allowed to import test files.
  • {
  •   files: ['**/*.ts?(x)', '**/*.js?(x)'],
    
  •   ignores: testFiles,
    
  •   rules: {
    
  •   	'no-restricted-imports': [
    
  •   		ERROR,
    
  •   		{
    
  •   			patterns: [
    
  •   				{
    
  •   					group: testFiles,
    
  •   					message: 'Do not import test files in source files',
    
  •   				},
    
  •   			],
    
  •   		},
    
  •   	],
    
  •   },
    
  • },
  • {
  •   files: testFiles,
    
  •   rules: {
    
  •   	'epic-web/prefer-dispose-in-tests': WARN,
    
  •   },
    
  • },
  • hasTestingLibrary
  •   ? {
    
  •   		files: testFiles,
    
  •   		ignores: [...playwrightFiles],
    
  •   		plugins: {
    
  •   			'testing-library': (await import('eslint-plugin-testing-library'))
    
  •   				.default,
    
  •   		},
    
  •   		rules: {
    
  •   			'testing-library/no-unnecessary-act': [ERROR, { isStrict: false }],
    
  •   			'testing-library/no-wait-for-side-effects': ERROR,
    
  •   			'testing-library/prefer-find-by': ERROR,
    
  •   		},
    
  •   	}
    
  •   : null,
    
  • hasJestDom
  •   ? {
    
  •   		files: testFiles,
    
  •   		ignores: [...playwrightFiles],
    
  •   		plugins: {
    
  •   			'jest-dom': (await import('eslint-plugin-jest-dom')).default,
    
  •   		},
    
  •   		rules: {
    
  •   			'jest-dom/prefer-checked': ERROR,
    
  •   			'jest-dom/prefer-enabled-disabled': ERROR,
    
  •   			'jest-dom/prefer-focus': ERROR,
    
  •   			'jest-dom/prefer-required': ERROR,
    
  •   		},
    
  •   	}
    
  •   : null,
    
  • hasVitest
  •   ? {
    
  •   		files: testFiles,
    
  •   		ignores: [...playwrightFiles],
    
  •   		plugins: {
    
  •   			vitest: (await import('@vitest/eslint-plugin')).default,
    
  •   		},
    
  •   		rules: {
    
  •   			// you don't want the editor to autofix this, but we do want to be
    
  •   			// made aware of it
    
  •   			'vitest/no-focused-tests': [WARN, { fixable: false }],
    
  •   			'vitest/no-import-node-test': ERROR,
    
  •   			'vitest/prefer-comparison-matcher': ERROR,
    
  •   			'vitest/prefer-equality-matcher': ERROR,
    
  •   			'vitest/prefer-to-be': ERROR,
    
  •   			'vitest/prefer-to-contain': ERROR,
    
  •   			'vitest/prefer-to-have-length': ERROR,
    
  •   			'vitest/valid-expect-in-promise': ERROR,
    
  •   			'vitest/valid-expect': ERROR,
    
  •   			// vitest/expect-expect - we don't enable this because it's fine to
    
  •   			// rely on testing-library to throw errors if elements aren't found.
    
  •   		},
    
  •   	}
    
  •   : null,
    
  • hasPlaywright
  •   ? {
    
  •   		files: [...playwrightFiles],
    
  •   		plugins: {
    
  •   			playwright: (await import('eslint-plugin-playwright')).default,
    
  •   		},
    
  •   		rules: {
    
  •   			'playwright/max-nested-describe': ERROR,
    
  •   			'playwright/missing-playwright-await': ERROR,
    
  •   			'playwright/no-focused-test': WARN,
    
  •   			'playwright/no-page-pause': ERROR,
    
  •   			'playwright/no-raw-locators': [WARN, { allowed: ['iframe'] }],
    
  •   			'playwright/no-slowed-test': ERROR,
    
  •   			'playwright/no-standalone-expect': ERROR,
    
  •   			'playwright/no-unsafe-references': ERROR,
    
  •   			'playwright/prefer-comparison-matcher': ERROR,
    
  •   			'playwright/prefer-equality-matcher': ERROR,
    
  •   			'playwright/prefer-native-locators': ERROR,
    
  •   			'playwright/prefer-to-be': ERROR,
    
  •   			'playwright/prefer-to-contain': ERROR,
    
  •   			'playwright/prefer-to-have-count': ERROR,
    
  •   			'playwright/prefer-to-have-length': ERROR,
    
  •   			'playwright/prefer-web-first-assertions': ERROR,
    
  •   			'playwright/valid-expect-in-promise': ERROR,
    
  •   			'playwright/valid-expect': ERROR,
    
  •   			// playwright/expect-expect - we don't enable this because it's fine to
    
  •   			// rely on thrown errors if elements aren't found.
    
  •   		},
    
  •   	}
    
  •   : null,
    

-].filter(Boolean)

-// this is for backward compatibility
-export default config
\ No newline at end of file

diff --git a/fixture/app/components/accordion.tsx b/fixture/app/components/accordion.tsx
--- a/fixture/app/components/accordion.tsx
+++ b/fixture/app/components/accordion.tsx
@@ -1,6 +1,6 @@
-// eslint-disable-next-line
+// oxlint-disable-next-line eslint/no-restricted-imports
import { MockAccordion } from './tests/accordion.tsx'
-// eslint-disable-next-line
+// oxlint-disable-next-line eslint/no-restricted-imports
import { MockAccordion as SpecMockAccordion } from './accordion.spec.tsx'

console.log(MockAccordion)

diff --git a/lint-rules/epic-web-plugin.js b/lint-rules/epic-web-plugin.js
--- a/lint-rules/epic-web-plugin.js
+++ b/lint-rules/epic-web-plugin.js
@@ -1,8 +1,8 @@
-import { eslintCompatPlugin } from '@oxlint/plugins'
+import { definePlugin } from '@oxlint/plugins'
import noManualDispose from './no-manual-dispose.js'
import preferDisposeInTests from './prefer-dispose-in-tests.js'

-const plugin = eslintCompatPlugin({
+const plugin = definePlugin({
meta: {
name: 'epic-web',
},

diff --git a/lint-rules/index.md b/lint-rules/index.md
--- a/lint-rules/index.md
+++ b/lint-rules/index.md
@@ -8,9 +8,8 @@

  • tests: *.test.js
  • documentation: *.md

-Rules are registered through epic-web-plugin.js, which
-uses eslintCompatPlugin(...) so rules can use Oxlint's createOnce API while
-remaining ESLint-compatible.
+Rules are registered through epic-web-plugin.js as
+Oxlint JS plugin rules.

Rules

diff --git a/lint-rules/no-manual-dispose.md b/lint-rules/no-manual-dispose.md
--- a/lint-rules/no-manual-dispose.md
+++ b/lint-rules/no-manual-dispose.md
@@ -8,9 +8,8 @@
Manual cleanup with try/finally and disposal calls is easier to get wrong and
less readable than language-level disposables.

-This rule is implemented with Oxlint's createOnce API and wrapped with
-eslintCompatPlugin(...), so it is optimized for Oxlint and still works in
-ESLint.
+This rule is implemented as an Oxlint JS plugin rule using Oxlint's
+createOnce API.

What it warns on

diff --git a/lint-rules/no-manual-dispose.test.js b/lint-rules/no-manual-dispose.test.js
--- a/lint-rules/no-manual-dispose.test.js
+++ b/lint-rules/no-manual-dispose.test.js
@@ -1,115 +1,67 @@
-import { RuleTester } from 'eslint'
-import plugin from './epic-web-plugin.js'
+import { describe, expect, test } from 'vitest'

-const rule = plugin.rules['no-manual-dispose']
+import { runOxlint } from './oxlint-test-utils.js'

-const tester = new RuleTester({

  • languageOptions: {
  •   ecmaVersion: 'latest',
    
  •   sourceType: 'module',
    
  • },
    -})
    +describe('epic-web/no-manual-dispose', () => {
  • test('allows using declarations and ordinary cleanup helpers', async () => {
  •   const result = await runOxlint({
    
  •   	filename: 'sample.js',
    
  •   	code: `
    
  •   		test('reads a temp file', () => {
    
  •   			using tempFile = createTempFile()
    
  •   			return Bun.file(tempFile.path).text()
    
  •   		})
    

-tester.run('no-manual-dispose', rule, {

  • valid: [
  •   `
    
  •   test('reads a temp file', () => {
    
  •   	using tempFile = createTempFile()
    
  •   	return Bun.file(tempFile.path).text()
    
  •   })
    
  •   `,
    
  •   `
    
  •   async function setup() {
    
  •   	await using db = await createDisposableDatabase()
    
  •   	return db
    
  •   }
    

... diff truncated: showing 800 of 5518 lines


</details>
<!-- BUGBOT_AUTOFIX_REVIEW_FOOTNOTE_END -->

Comment thread lint-rules/oxlint-test-utils.js Outdated
Comment thread lint-rules/oxlint-test-utils.js
@kentcdodds kentcdodds merged commit 1180f59 into main Mar 26, 2026
5 checks passed
@kentcdodds kentcdodds deleted the cursor/eslint-removal-c888 branch March 26, 2026 21:16
@github-actions
Copy link
Copy Markdown

🎉 This PR is included in version 2.0.0 🎉

The release is available on:

Your semantic-release bot 📦🚀

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants