From e7fa87b301e3cf2cf0ac0bfa50130be2595b97f7 Mon Sep 17 00:00:00 2001 From: xingyu4j Date: Fri, 13 Mar 2026 20:25:25 +0800 Subject: [PATCH] chore: finalize oxlint migration config --- .../lint-configs/eslint-config/package.json | 10 +- .../eslint-config/src/configs/comments.ts | 6 +- .../eslint-config/src/configs/disableds.ts | 15 - .../eslint-config/src/configs/import.ts | 25 - .../eslint-config/src/configs/index.ts | 6 +- .../eslint-config/src/configs/javascript.ts | 141 --- .../eslint-config/src/configs/jsdoc.ts | 34 - .../eslint-config/src/configs/node.ts | 30 +- .../eslint-config/src/configs/oxlint.ts | 9 + .../eslint-config/src/configs/prettier.ts | 4 +- .../eslint-config/src/configs/regexp.ts | 20 - .../eslint-config/src/configs/tailwindcss.ts | 49 -- .../eslint-config/src/configs/test.ts | 45 - .../eslint-config/src/configs/turbo.ts | 4 +- .../eslint-config/src/configs/typescript.ts | 26 - .../eslint-config/src/configs/unicorn.ts | 4 +- .../eslint-config/src/custom-config.ts | 5 - .../lint-configs/eslint-config/src/index.ts | 12 +- .../oxlint-config/build.config.ts | 7 + .../lint-configs/oxlint-config/package.json | 33 + .../oxlint-config/src/configs/ignores.ts | 17 + .../oxlint-config/src/configs/import.ts | 18 + .../oxlint-config/src/configs/index.ts | 90 ++ .../oxlint-config/src/configs/javascript.ts | 156 ++++ .../oxlint-config/src/configs/node.ts | 11 + .../oxlint-config/src/configs/overrides.ts | 98 +++ .../oxlint-config/src/configs/plugins.ts | 7 + .../oxlint-config/src/configs/tailwindcss.ts | 50 ++ .../oxlint-config/src/configs/test.ts | 23 + .../oxlint-config/src/configs/typescript.ts | 12 + .../oxlint-config/src/configs/unicorn.ts | 14 + .../oxlint-config/src/configs/vue.ts | 9 + .../lint-configs/oxlint-config/src/index.ts | 21 + .../lint-configs/oxlint-config/tsconfig.json | 6 + lefthook.yml | 4 +- oxlint.config.ts | 3 + package.json | 6 +- .../@core/ui-kit/menu-ui/src/sub-menu.vue | 1 - .../components/full-screen/full-screen.vue | 6 +- .../src/components/json-viewer/index.vue | 3 +- .../src/widgets/theme-toggle/theme-button.vue | 3 +- .../plugins/src/vxe-table/use-vxe-grid.vue | 2 +- .../src/views/examples/vxe-table/basic.vue | 1 - pnpm-workspace.yaml | 7 +- scripts/vsh/src/check-dep/index.ts | 1 + scripts/vsh/src/lint/index.ts | 3 + up-oxc.md | 823 ++++++++++++++++++ 47 files changed, 1466 insertions(+), 414 deletions(-) delete mode 100644 internal/lint-configs/eslint-config/src/configs/import.ts delete mode 100644 internal/lint-configs/eslint-config/src/configs/jsdoc.ts create mode 100644 internal/lint-configs/eslint-config/src/configs/oxlint.ts delete mode 100644 internal/lint-configs/eslint-config/src/configs/regexp.ts delete mode 100644 internal/lint-configs/eslint-config/src/configs/tailwindcss.ts delete mode 100644 internal/lint-configs/eslint-config/src/configs/test.ts create mode 100644 internal/lint-configs/oxlint-config/build.config.ts create mode 100644 internal/lint-configs/oxlint-config/package.json create mode 100644 internal/lint-configs/oxlint-config/src/configs/ignores.ts create mode 100644 internal/lint-configs/oxlint-config/src/configs/import.ts create mode 100644 internal/lint-configs/oxlint-config/src/configs/index.ts create mode 100644 internal/lint-configs/oxlint-config/src/configs/javascript.ts create mode 100644 internal/lint-configs/oxlint-config/src/configs/node.ts create mode 100644 internal/lint-configs/oxlint-config/src/configs/overrides.ts create mode 100644 internal/lint-configs/oxlint-config/src/configs/plugins.ts create mode 100644 internal/lint-configs/oxlint-config/src/configs/tailwindcss.ts create mode 100644 internal/lint-configs/oxlint-config/src/configs/test.ts create mode 100644 internal/lint-configs/oxlint-config/src/configs/typescript.ts create mode 100644 internal/lint-configs/oxlint-config/src/configs/unicorn.ts create mode 100644 internal/lint-configs/oxlint-config/src/configs/vue.ts create mode 100644 internal/lint-configs/oxlint-config/src/index.ts create mode 100644 internal/lint-configs/oxlint-config/tsconfig.json create mode 100644 oxlint.config.ts create mode 100644 up-oxc.md diff --git a/internal/lint-configs/eslint-config/package.json b/internal/lint-configs/eslint-config/package.json index 6ae85557b..8f29254fd 100644 --- a/internal/lint-configs/eslint-config/package.json +++ b/internal/lint-configs/eslint-config/package.json @@ -27,26 +27,22 @@ } }, "dependencies": { + "@vben/oxlint-config": "workspace:*", "eslint-config-turbo": "catalog:", - "eslint-plugin-command": "catalog:", - "eslint-plugin-import-x": "catalog:" + "eslint-plugin-command": "catalog:" }, "devDependencies": { "@eslint-community/eslint-plugin-eslint-comments": "catalog:", "@eslint/js": "catalog:", "@typescript-eslint/eslint-plugin": "catalog:", "@typescript-eslint/parser": "catalog:", - "@vitest/eslint-plugin": "catalog:", "eslint": "catalog:", - "eslint-plugin-better-tailwindcss": "catalog:", - "eslint-plugin-jsdoc": "catalog:", "eslint-plugin-jsonc": "catalog:", "eslint-plugin-n": "catalog:", - "eslint-plugin-no-only-tests": "catalog:", + "eslint-plugin-oxlint": "catalog:", "eslint-plugin-perfectionist": "catalog:", "eslint-plugin-pnpm": "catalog:", "eslint-plugin-prettier": "catalog:", - "eslint-plugin-regexp": "catalog:", "eslint-plugin-unicorn": "catalog:", "eslint-plugin-unused-imports": "catalog:", "eslint-plugin-vue": "catalog:", diff --git a/internal/lint-configs/eslint-config/src/configs/comments.ts b/internal/lint-configs/eslint-config/src/configs/comments.ts index d638c045f..80765dfb8 100644 --- a/internal/lint-configs/eslint-config/src/configs/comments.ts +++ b/internal/lint-configs/eslint-config/src/configs/comments.ts @@ -3,9 +3,9 @@ import type { Linter } from 'eslint'; import { interopDefault } from '../util'; export async function comments(): Promise { - const [pluginComments] = await Promise.all([ - interopDefault(import('@eslint-community/eslint-plugin-eslint-comments')), - ] as const); + const pluginComments = await interopDefault( + import('@eslint-community/eslint-plugin-eslint-comments'), + ); return [ { diff --git a/internal/lint-configs/eslint-config/src/configs/disableds.ts b/internal/lint-configs/eslint-config/src/configs/disableds.ts index 152b84cac..5a0ca7d36 100644 --- a/internal/lint-configs/eslint-config/src/configs/disableds.ts +++ b/internal/lint-configs/eslint-config/src/configs/disableds.ts @@ -2,21 +2,6 @@ import type { Linter } from 'eslint'; export async function disableds(): Promise { return [ - { - files: ['**/__tests__/**/*.?([cm])[jt]s?(x)'], - name: 'disables/test', - rules: { - '@typescript-eslint/ban-ts-comment': 'off', - 'no-console': 'off', - }, - }, - { - files: ['**/*.d.ts'], - name: 'disables/dts', - rules: { - '@typescript-eslint/triple-slash-reference': 'off', - }, - }, { files: ['**/*.js', '**/*.mjs', '**/*.cjs'], name: 'disables/js', diff --git a/internal/lint-configs/eslint-config/src/configs/import.ts b/internal/lint-configs/eslint-config/src/configs/import.ts deleted file mode 100644 index ce6cf65d9..000000000 --- a/internal/lint-configs/eslint-config/src/configs/import.ts +++ /dev/null @@ -1,25 +0,0 @@ -import type { Linter } from 'eslint'; - -import * as pluginImport from 'eslint-plugin-import-x'; - -export async function importPluginConfig(): Promise { - return [ - { - plugins: { - // @ts-expect-error - This is a dynamic import - import: pluginImport, - }, - rules: { - 'import/consistent-type-specifier-style': ['error', 'prefer-top-level'], - 'import/first': 'error', - 'import/newline-after-import': 'error', - 'import/no-duplicates': 'error', - 'import/no-mutable-exports': 'error', - 'import/no-named-default': 'error', - 'import/no-self-import': 'error', - 'import/no-unresolved': 'off', - 'import/no-webpack-loader-syntax': 'error', - }, - }, - ]; -} diff --git a/internal/lint-configs/eslint-config/src/configs/index.ts b/internal/lint-configs/eslint-config/src/configs/index.ts index 94dca701a..cd13343ac 100644 --- a/internal/lint-configs/eslint-config/src/configs/index.ts +++ b/internal/lint-configs/eslint-config/src/configs/index.ts @@ -2,17 +2,13 @@ export * from './command'; export * from './comments'; export * from './disableds'; export * from './ignores'; -export * from './import'; export * from './javascript'; -export * from './jsdoc'; export * from './jsonc'; export * from './node'; +export * from './oxlint'; export * from './perfectionist'; export * from './pnpm'; export * from './prettier'; -export * from './regexp'; -export * from './tailwindcss'; -export * from './test'; export * from './turbo'; export * from './typescript'; export * from './unicorn'; diff --git a/internal/lint-configs/eslint-config/src/configs/javascript.ts b/internal/lint-configs/eslint-config/src/configs/javascript.ts index 44cf5b6e6..a083d78c0 100644 --- a/internal/lint-configs/eslint-config/src/configs/javascript.ts +++ b/internal/lint-configs/eslint-config/src/configs/javascript.ts @@ -34,78 +34,11 @@ export async function javascript(): Promise { }, rules: { ...js.configs.recommended.rules, - 'accessor-pairs': [ - 'error', - { enforceForClassMembers: true, setWithoutGet: true }, - ], - 'array-callback-return': 'error', - 'block-scoped-var': 'error', - 'constructor-super': 'error', - 'default-case-last': 'error', 'dot-notation': ['error', { allowKeywords: true }], - eqeqeq: ['error', 'always'], 'keyword-spacing': 'off', - - 'new-cap': [ - 'error', - { capIsNew: false, newIsCap: true, properties: true }, - ], - 'no-alert': 'error', - 'no-array-constructor': 'error', - 'no-async-promise-executor': 'error', - 'no-caller': 'error', - 'no-case-declarations': 'error', - 'no-class-assign': 'error', - 'no-compare-neg-zero': 'error', - 'no-cond-assign': ['error', 'always'], - 'no-console': ['error', { allow: ['warn', 'error'] }], - 'no-const-assign': 'error', 'no-control-regex': 'error', - 'no-debugger': 'error', - 'no-delete-var': 'error', 'no-dupe-args': 'error', - 'no-dupe-class-members': 'error', - 'no-dupe-keys': 'error', - 'no-duplicate-case': 'error', - 'no-empty': ['error', { allowEmptyCatch: true }], - 'no-empty-character-class': 'error', 'no-empty-function': 'off', - 'no-empty-pattern': 'error', - 'no-eval': 'error', - 'no-ex-assign': 'error', - 'no-extend-native': 'error', - 'no-extra-bind': 'error', - 'no-extra-boolean-cast': 'error', - 'no-fallthrough': 'error', - 'no-func-assign': 'error', - 'no-global-assign': 'error', - 'no-implied-eval': 'error', - 'no-import-assign': 'error', - 'no-invalid-regexp': 'error', - 'no-irregular-whitespace': 'error', - 'no-iterator': 'error', - 'no-labels': ['error', { allowLoop: false, allowSwitch: false }], - 'no-lone-blocks': 'error', - 'no-loss-of-precision': 'error', - 'no-misleading-character-class': 'error', - 'no-multi-str': 'error', - 'no-new': 'error', - 'no-new-func': 'error', - 'no-new-object': 'error', - 'no-new-symbol': 'error', - 'no-new-wrappers': 'error', - 'no-obj-calls': 'error', - 'no-octal': 'error', - 'no-octal-escape': 'error', - 'no-proto': 'error', - 'no-prototype-builtins': 'error', - 'no-redeclare': ['error', { builtinGlobals: false }], - 'no-regex-spaces': 'error', - 'no-restricted-globals': [ - 'error', - { message: 'Use `globalThis` instead.', name: 'global' }, - { message: 'Use `globalThis` instead.', name: 'self' }, - ], 'no-restricted-properties': [ 'error', { @@ -138,31 +71,8 @@ export async function javascript(): Promise { 'TSEnumDeclaration[const=true]', 'TSExportAssignment', ], - 'no-self-assign': ['error', { props: true }], - 'no-self-compare': 'error', - 'no-sequences': 'error', - 'no-shadow-restricted-names': 'error', - 'no-sparse-arrays': 'error', - 'no-template-curly-in-string': 'error', - 'no-this-before-super': 'error', - 'no-throw-literal': 'error', 'no-undef': 'off', - 'no-undef-init': 'error', - 'no-unexpected-multiline': 'error', - 'no-unmodified-loop-condition': 'error', - 'no-unneeded-ternary': ['error', { defaultAssignment: false }], - 'no-unreachable': 'error', 'no-unreachable-loop': 'error', - 'no-unsafe-finally': 'error', - 'no-unsafe-negation': 'error', - 'no-unused-expressions': [ - 'error', - { - allowShortCircuit: true, - allowTaggedTemplates: true, - allowTernary: true, - }, - ], 'no-unused-vars': [ 'error', { @@ -172,50 +82,7 @@ export async function javascript(): Promise { vars: 'all', }, ], - 'no-use-before-define': [ - 'error', - { classes: false, functions: false, variables: false }, - ], - 'no-useless-backreference': 'error', - 'no-useless-call': 'error', - 'no-useless-catch': 'error', - 'no-useless-computed-key': 'error', - 'no-useless-constructor': 'error', - 'no-useless-rename': 'error', - 'no-useless-return': 'error', - 'no-var': 'error', - 'no-with': 'error', - 'object-shorthand': [ - 'error', - 'always', - { avoidQuotes: true, ignoreConstructors: false }, - ], - 'one-var': ['error', { initialized: 'never' }], - 'prefer-arrow-callback': [ - 'error', - { - allowNamedFunctions: false, - allowUnboundThis: true, - }, - ], - 'prefer-const': [ - 'error', - { - destructuring: 'all', - ignoreReadBeforeAssign: true, - }, - ], - 'prefer-exponentiation-operator': 'error', - - 'prefer-promise-reject-errors': 'error', - 'prefer-regex-literals': ['error', { disallowRedundantWrapping: true }], - 'prefer-rest-params': 'error', - 'prefer-spread': 'error', - 'prefer-template': 'error', 'space-before-function-paren': 'off', - 'spaced-comment': 'error', - 'symbol-description': 'error', - 'unicode-bom': ['error', 'never'], 'unused-imports/no-unused-imports': 'error', 'unused-imports/no-unused-vars': [ @@ -227,14 +94,6 @@ export async function javascript(): Promise { varsIgnorePattern: '^_', }, ], - 'use-isnan': [ - 'error', - { enforceForIndexOf: true, enforceForSwitchCase: true }, - ], - 'valid-typeof': ['error', { requireStringLiterals: true }], - - 'vars-on-top': 'error', - yoda: ['error', 'never'], }, }, ]; diff --git a/internal/lint-configs/eslint-config/src/configs/jsdoc.ts b/internal/lint-configs/eslint-config/src/configs/jsdoc.ts deleted file mode 100644 index 136819769..000000000 --- a/internal/lint-configs/eslint-config/src/configs/jsdoc.ts +++ /dev/null @@ -1,34 +0,0 @@ -import type { Linter } from 'eslint'; - -import { interopDefault } from '../util'; - -export async function jsdoc(): Promise { - const [pluginJsdoc] = await Promise.all([ - interopDefault(import('eslint-plugin-jsdoc')), - ] as const); - - return [ - { - plugins: { - jsdoc: pluginJsdoc, - }, - rules: { - 'jsdoc/check-access': 'warn', - 'jsdoc/check-param-names': 'warn', - 'jsdoc/check-property-names': 'warn', - 'jsdoc/check-types': 'warn', - 'jsdoc/empty-tags': 'warn', - 'jsdoc/implements-on-classes': 'warn', - 'jsdoc/no-defaults': 'warn', - 'jsdoc/no-multi-asterisks': 'warn', - 'jsdoc/require-param-name': 'warn', - 'jsdoc/require-property': 'warn', - 'jsdoc/require-property-description': 'warn', - 'jsdoc/require-property-name': 'warn', - 'jsdoc/require-returns-check': 'warn', - 'jsdoc/require-returns-description': 'warn', - 'jsdoc/require-yields-check': 'warn', - }, - }, - ]; -} diff --git a/internal/lint-configs/eslint-config/src/configs/node.ts b/internal/lint-configs/eslint-config/src/configs/node.ts index adc9c2c72..65f81fd53 100644 --- a/internal/lint-configs/eslint-config/src/configs/node.ts +++ b/internal/lint-configs/eslint-config/src/configs/node.ts @@ -13,7 +13,6 @@ export async function node(): Promise { rules: { 'n/handle-callback-err': ['error', '^(err|error)$'], 'n/no-deprecated-api': 'error', - 'n/no-exports-assign': 'error', 'n/no-extraneous-import': [ 'error', { @@ -27,8 +26,6 @@ export async function node(): Promise { ], }, ], - 'n/no-new-require': 'error', - 'n/no-path-concat': 'error', // 'n/no-unpublished-import': 'off', 'n/no-unsupported-features/es-syntax': [ 'error', @@ -43,6 +40,33 @@ export async function node(): Promise { 'n/process-exit-as-throw': 'error', }, }, + { + files: [ + '**/__tests__/**/*.?([cm])[jt]s?(x)', + '**/*.spec.?([cm])[jt]s?(x)', + '**/*.test.?([cm])[jt]s?(x)', + '**/*.bench.?([cm])[jt]s?(x)', + '**/*.benchmark.?([cm])[jt]s?(x)', + ], + rules: { + 'n/prefer-global/process': 'off', + }, + }, + { + files: ['apps/backend-mock/**/**', 'docs/**/**'], + rules: { + 'n/no-extraneous-import': 'off', + 'n/prefer-global/buffer': 'off', + 'n/prefer-global/process': 'off', + }, + }, + { + files: ['**/**/playwright.config.ts'], + rules: { + 'n/prefer-global/buffer': 'off', + 'n/prefer-global/process': 'off', + }, + }, { files: [ 'scripts/**/*.?([cm])[jt]s?(x)', diff --git a/internal/lint-configs/eslint-config/src/configs/oxlint.ts b/internal/lint-configs/eslint-config/src/configs/oxlint.ts new file mode 100644 index 000000000..28b5d1dd0 --- /dev/null +++ b/internal/lint-configs/eslint-config/src/configs/oxlint.ts @@ -0,0 +1,9 @@ +import type { Linter } from 'eslint'; + +import { mergeOxlintConfigs, oxlintConfig } from '@vben/oxlint-config'; + +import oxlint from 'eslint-plugin-oxlint'; + +export async function oxcCompat(): Promise { + return oxlint.buildFromOxlintConfig(mergeOxlintConfigs(oxlintConfig)); +} diff --git a/internal/lint-configs/eslint-config/src/configs/prettier.ts b/internal/lint-configs/eslint-config/src/configs/prettier.ts index 3cd7af40e..e0dc92fc9 100644 --- a/internal/lint-configs/eslint-config/src/configs/prettier.ts +++ b/internal/lint-configs/eslint-config/src/configs/prettier.ts @@ -3,9 +3,7 @@ import type { Linter } from 'eslint'; import { interopDefault } from '../util'; export async function prettier(): Promise { - const [pluginPrettier] = await Promise.all([ - interopDefault(import('eslint-plugin-prettier')), - ] as const); + const pluginPrettier = await interopDefault(import('eslint-plugin-prettier')); return [ { plugins: { diff --git a/internal/lint-configs/eslint-config/src/configs/regexp.ts b/internal/lint-configs/eslint-config/src/configs/regexp.ts deleted file mode 100644 index c0f4c9f43..000000000 --- a/internal/lint-configs/eslint-config/src/configs/regexp.ts +++ /dev/null @@ -1,20 +0,0 @@ -import type { Linter } from 'eslint'; - -import { interopDefault } from '../util'; - -export async function regexp(): Promise { - const [pluginRegexp] = await Promise.all([ - interopDefault(import('eslint-plugin-regexp')), - ] as const); - - return [ - { - plugins: { - regexp: pluginRegexp, - }, - rules: { - ...pluginRegexp.configs.recommended.rules, - }, - }, - ]; -} diff --git a/internal/lint-configs/eslint-config/src/configs/tailwindcss.ts b/internal/lint-configs/eslint-config/src/configs/tailwindcss.ts deleted file mode 100644 index 7a7f82304..000000000 --- a/internal/lint-configs/eslint-config/src/configs/tailwindcss.ts +++ /dev/null @@ -1,49 +0,0 @@ -import type { Linter } from 'eslint'; - -import { getDefaultSelectors } from 'eslint-plugin-better-tailwindcss/defaults'; -import { SelectorKind } from 'eslint-plugin-better-tailwindcss/types'; - -import { interopDefault } from '../util'; - -export async function tailwindcss(): Promise { - const [pluginBetterTailwindcss] = await Promise.all([ - interopDefault(import('eslint-plugin-better-tailwindcss')), - ] as const); - - return [ - { - plugins: { - 'better-tailwindcss': pluginBetterTailwindcss, - }, - // shadcn-ui 内部组件是自动生成的,不做太多限制 - ignores: ['packages/@core/ui-kit/shadcn-ui/**/**'], - settings: { - 'better-tailwindcss': { - entryPoint: 'packages/@core/base/design/src/css/global.css', - selectors: [ - ...getDefaultSelectors(), // preserve default selectors - { - kind: SelectorKind.Attribute, - match: [{ type: 'objectValues' }], - name: '^classNames$', - }, - ], - }, - }, - rules: { - ...pluginBetterTailwindcss.configs.recommended.rules, - 'better-tailwindcss/enforce-consistent-class-order': [ - 'error', - { - detectComponentClasses: true, - unknownClassOrder: 'asc', - unknownClassPosition: 'start', - }, - ], - // Let Prettier own wrapping decisions to avoid ping-pong formatting. - 'better-tailwindcss/enforce-consistent-line-wrapping': 'off', - 'better-tailwindcss/no-unknown-classes': 'off', - }, - }, - ]; -} diff --git a/internal/lint-configs/eslint-config/src/configs/test.ts b/internal/lint-configs/eslint-config/src/configs/test.ts deleted file mode 100644 index 029ffd0f1..000000000 --- a/internal/lint-configs/eslint-config/src/configs/test.ts +++ /dev/null @@ -1,45 +0,0 @@ -import type { Linter } from 'eslint'; - -import { interopDefault } from '../util'; - -export async function test(): Promise { - const [pluginTest, pluginNoOnlyTests] = await Promise.all([ - interopDefault(import('@vitest/eslint-plugin')), - // @ts-expect-error - no types - interopDefault(import('eslint-plugin-no-only-tests')), - ] as const); - - return [ - { - files: [ - `**/__tests__/**/*.?([cm])[jt]s?(x)`, - `**/*.spec.?([cm])[jt]s?(x)`, - `**/*.test.?([cm])[jt]s?(x)`, - `**/*.bench.?([cm])[jt]s?(x)`, - `**/*.benchmark.?([cm])[jt]s?(x)`, - ], - plugins: { - test: { - ...pluginTest, - rules: { - ...pluginTest.rules, - ...pluginNoOnlyTests.rules, - }, - }, - }, - rules: { - 'no-console': 'off', - 'node/prefer-global/process': 'off', - 'test/consistent-test-it': [ - 'error', - { fn: 'it', withinDescribe: 'it' }, - ], - 'test/no-identical-title': 'error', - 'test/no-import-node-test': 'error', - 'test/no-only-tests': 'error', - 'test/prefer-hooks-in-order': 'error', - 'test/prefer-lowercase-title': 'error', - }, - }, - ]; -} diff --git a/internal/lint-configs/eslint-config/src/configs/turbo.ts b/internal/lint-configs/eslint-config/src/configs/turbo.ts index bcc27eb69..1dc155046 100644 --- a/internal/lint-configs/eslint-config/src/configs/turbo.ts +++ b/internal/lint-configs/eslint-config/src/configs/turbo.ts @@ -3,9 +3,7 @@ import type { Linter } from 'eslint'; import { interopDefault } from '../util'; export async function turbo(): Promise { - const [pluginTurbo] = await Promise.all([ - interopDefault(import('eslint-config-turbo')), - ] as const); + const pluginTurbo = await interopDefault(import('eslint-config-turbo')); return [ { diff --git a/internal/lint-configs/eslint-config/src/configs/typescript.ts b/internal/lint-configs/eslint-config/src/configs/typescript.ts index 2f6f97650..fca509ac7 100644 --- a/internal/lint-configs/eslint-config/src/configs/typescript.ts +++ b/internal/lint-configs/eslint-config/src/configs/typescript.ts @@ -31,39 +31,13 @@ export async function typescript(): Promise { rules: { ...pluginTs.configs['eslint-recommended']?.overrides?.[0]?.rules, ...pluginTs.configs.strict?.rules, - '@typescript-eslint/ban-ts-comment': [ - 'error', - { - 'ts-check': false, - 'ts-expect-error': 'allow-with-description', - 'ts-ignore': 'allow-with-description', - 'ts-nocheck': 'allow-with-description', - }, - ], - // '@typescript-eslint/consistent-type-definitions': ['warn', 'interface'], '@typescript-eslint/consistent-type-definitions': 'off', '@typescript-eslint/explicit-function-return-type': 'off', '@typescript-eslint/explicit-module-boundary-types': 'off', - '@typescript-eslint/no-empty-function': [ - 'error', - { - allow: ['arrowFunctions', 'functions', 'methods'], - }, - ], '@typescript-eslint/no-explicit-any': 'off', '@typescript-eslint/no-namespace': 'off', - '@typescript-eslint/no-non-null-assertion': 'error', - '@typescript-eslint/no-unused-expressions': 'off', - '@typescript-eslint/no-unused-vars': [ - 'error', - { - argsIgnorePattern: '^_', - varsIgnorePattern: '^_', - }, - ], '@typescript-eslint/no-use-before-define': 'off', - '@typescript-eslint/no-var-requires': 'error', 'unused-imports/no-unused-vars': 'off', }, }, diff --git a/internal/lint-configs/eslint-config/src/configs/unicorn.ts b/internal/lint-configs/eslint-config/src/configs/unicorn.ts index 21b19025d..56c9ddf40 100644 --- a/internal/lint-configs/eslint-config/src/configs/unicorn.ts +++ b/internal/lint-configs/eslint-config/src/configs/unicorn.ts @@ -3,9 +3,7 @@ import type { Linter } from 'eslint'; import { interopDefault } from '../util'; export async function unicorn(): Promise { - const [pluginUnicorn] = await Promise.all([ - interopDefault(import('eslint-plugin-unicorn')), - ] as const); + const pluginUnicorn = await interopDefault(import('eslint-plugin-unicorn')); return [ { diff --git a/internal/lint-configs/eslint-config/src/custom-config.ts b/internal/lint-configs/eslint-config/src/custom-config.ts index 9185f2c4e..57413f9ae 100644 --- a/internal/lint-configs/eslint-config/src/custom-config.ts +++ b/internal/lint-configs/eslint-config/src/custom-config.ts @@ -138,9 +138,6 @@ const customConfig: Linter.Config[] = [ files: ['apps/backend-mock/**/**', 'docs/**/**'], rules: { '@typescript-eslint/no-extraneous-class': 'off', - 'n/no-extraneous-import': 'off', - 'n/prefer-global/buffer': 'off', - 'n/prefer-global/process': 'off', 'no-console': 'off', 'unicorn/prefer-module': 'off', }, @@ -148,8 +145,6 @@ const customConfig: Linter.Config[] = [ { files: ['**/**/playwright.config.ts'], rules: { - 'n/prefer-global/buffer': 'off', - 'n/prefer-global/process': 'off', 'no-console': 'off', }, }, diff --git a/internal/lint-configs/eslint-config/src/index.ts b/internal/lint-configs/eslint-config/src/index.ts index 58f978ddb..ea3d62604 100644 --- a/internal/lint-configs/eslint-config/src/index.ts +++ b/internal/lint-configs/eslint-config/src/index.ts @@ -5,17 +5,13 @@ import { comments, disableds, ignores, - importPluginConfig, javascript, - jsdoc, jsonc, node, + oxcCompat, perfectionist, pnpm, prettier, - regexp, - tailwindcss, - test, turbo, typescript, unicorn, @@ -41,20 +37,16 @@ async function defineConfig(config: FlatConfig[] = []) { typescript(), jsonc(), disableds(), - importPluginConfig(), node(), perfectionist(), comments(), - jsdoc(), - tailwindcss(), unicorn(), - test(), - regexp(), command(), turbo(), yaml(), pnpm(), ...customConfig, + oxcCompat(), ...config, ]; diff --git a/internal/lint-configs/oxlint-config/build.config.ts b/internal/lint-configs/oxlint-config/build.config.ts new file mode 100644 index 000000000..97e572c56 --- /dev/null +++ b/internal/lint-configs/oxlint-config/build.config.ts @@ -0,0 +1,7 @@ +import { defineBuildConfig } from 'unbuild'; + +export default defineBuildConfig({ + clean: true, + declaration: true, + entries: ['src/index'], +}); diff --git a/internal/lint-configs/oxlint-config/package.json b/internal/lint-configs/oxlint-config/package.json new file mode 100644 index 000000000..97d1f0992 --- /dev/null +++ b/internal/lint-configs/oxlint-config/package.json @@ -0,0 +1,33 @@ +{ + "name": "@vben/oxlint-config", + "version": "5.6.0", + "private": true, + "homepage": "https://github.com/vbenjs/vue-vben-admin", + "bugs": "https://github.com/vbenjs/vue-vben-admin/issues", + "repository": { + "type": "git", + "url": "git+https://github.com/vbenjs/vue-vben-admin.git", + "directory": "internal/lint-configs/oxlint-config" + }, + "license": "MIT", + "type": "module", + "scripts": { + "stub": "pnpm unbuild --stub" + }, + "files": [ + "dist" + ], + "main": "./dist/index.mjs", + "module": "./dist/index.mjs", + "types": "./dist/index.d.ts", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.mjs" + } + }, + "dependencies": { + "eslint-plugin-better-tailwindcss": "catalog:", + "oxlint": "catalog:" + } +} diff --git a/internal/lint-configs/oxlint-config/src/configs/ignores.ts b/internal/lint-configs/oxlint-config/src/configs/ignores.ts new file mode 100644 index 000000000..858a3cd73 --- /dev/null +++ b/internal/lint-configs/oxlint-config/src/configs/ignores.ts @@ -0,0 +1,17 @@ +import type { OxlintConfig } from 'oxlint'; + +const ignores: OxlintConfig = { + ignorePatterns: [ + '**/dist/**', + '**/node_modules/**', + 'docs/**', + 'playground/public/**', + '**/*.json', + '**/*.md', + '**/*.svg', + '**/*.yaml', + '**/*.yml', + ], +}; + +export { ignores }; diff --git a/internal/lint-configs/oxlint-config/src/configs/import.ts b/internal/lint-configs/oxlint-config/src/configs/import.ts new file mode 100644 index 000000000..8e719ad48 --- /dev/null +++ b/internal/lint-configs/oxlint-config/src/configs/import.ts @@ -0,0 +1,18 @@ +import type { OxlintConfig } from 'oxlint'; + +const importPluginConfig: OxlintConfig = { + rules: { + 'import/consistent-type-specifier-style': ['error', 'prefer-top-level'], + 'import/first': 'error', + 'import/no-duplicates': 'error', + 'import/no-mutable-exports': 'error', + 'import/no-named-as-default': 'off', + 'import/no-named-as-default-member': 'off', + 'import/no-named-default': 'error', + 'import/no-self-import': 'error', + 'import/no-unassigned-import': 'off', + 'import/no-webpack-loader-syntax': 'error', + }, +}; + +export { importPluginConfig }; diff --git a/internal/lint-configs/oxlint-config/src/configs/index.ts b/internal/lint-configs/oxlint-config/src/configs/index.ts new file mode 100644 index 000000000..6ba29de79 --- /dev/null +++ b/internal/lint-configs/oxlint-config/src/configs/index.ts @@ -0,0 +1,90 @@ +import type { OxlintConfig } from 'oxlint'; + +import { defineConfig as defineOxlintConfig } from 'oxlint'; + +import { ignores } from './ignores'; +import { importPluginConfig } from './import'; +import { javascript } from './javascript'; +import { node } from './node'; +import { overrides } from './overrides'; +import { plugins } from './plugins'; +import { tailwindcss } from './tailwindcss'; +import { test } from './test'; +import { typescript } from './typescript'; +import { unicorn } from './unicorn'; +import { vue } from './vue'; + +function mergeOxlintConfigs(...configs: OxlintConfig[]): OxlintConfig { + const merged: OxlintConfig = {}; + + for (const config of configs) { + merged.categories = + merged.categories && config.categories + ? { ...merged.categories, ...config.categories } + : (config.categories ?? merged.categories); + merged.env = + merged.env && config.env + ? { ...merged.env, ...config.env } + : (config.env ?? merged.env); + merged.globals = + merged.globals && config.globals + ? { ...merged.globals, ...config.globals } + : (config.globals ?? merged.globals); + merged.ignorePatterns = [ + ...(merged.ignorePatterns ?? []), + ...(config.ignorePatterns ?? []), + ]; + merged.jsPlugins = [ + ...new Set([...(merged.jsPlugins ?? []), ...(config.jsPlugins ?? [])]), + ]; + merged.overrides = [ + ...(merged.overrides ?? []), + ...(config.overrides ?? []), + ]; + merged.plugins = [ + ...new Set([...(merged.plugins ?? []), ...(config.plugins ?? [])]), + ]; + merged.rules = + merged.rules && config.rules + ? { ...merged.rules, ...config.rules } + : (config.rules ?? merged.rules); + merged.settings = + merged.settings && config.settings + ? { ...merged.settings, ...config.settings } + : (config.settings ?? merged.settings); + } + + return merged; +} + +const oxlintConfig = defineOxlintConfig( + mergeOxlintConfigs( + javascript, + ignores, + plugins, + importPluginConfig, + node, + overrides, + tailwindcss, + test, + typescript, + unicorn, + vue, + ), +); + +export { + ignores, + importPluginConfig, + javascript, + mergeOxlintConfigs, + node, + overrides, + oxlintConfig, + plugins, + tailwindcss, + test, + typescript, + unicorn, + vue, +}; diff --git a/internal/lint-configs/oxlint-config/src/configs/javascript.ts b/internal/lint-configs/oxlint-config/src/configs/javascript.ts new file mode 100644 index 000000000..4ea37891c --- /dev/null +++ b/internal/lint-configs/oxlint-config/src/configs/javascript.ts @@ -0,0 +1,156 @@ +import type { OxlintConfig } from 'oxlint'; + +const javascript: OxlintConfig = { + categories: { + correctness: 'error', + suspicious: 'warn', + }, + env: { + browser: true, + es2021: true, + node: true, + }, + globals: { + document: 'readonly', + navigator: 'readonly', + window: 'readonly', + }, + rules: { + 'accessor-pairs': [ + 'error', + { + enforceForClassMembers: true, + setWithoutGet: true, + }, + ], + 'array-callback-return': 'error', + 'block-scoped-var': 'error', + 'default-case-last': 'error', + eqeqeq: ['error', 'always'], + 'eslint/no-unreachable': 'error', + 'new-cap': [ + 'error', + { + capIsNew: false, + newIsCap: true, + properties: true, + }, + ], + 'no-alert': 'error', + 'no-array-constructor': 'error', + 'no-caller': 'error', + 'no-case-declarations': 'error', + 'no-console': ['error', { allow: ['warn', 'error'] }], + 'no-control-regex': 'off', + 'no-debugger': 'error', + 'no-empty': ['error', { allowEmptyCatch: true }], + 'no-fallthrough': 'error', + 'no-new-func': 'error', + 'no-new-object': 'error', + 'no-new-symbol': 'error', + 'no-labels': ['error', { allowLoop: false, allowSwitch: false }], + 'no-lone-blocks': 'error', + 'no-multi-str': 'error', + 'no-octal': 'error', + 'no-octal-escape': 'error', + 'no-proto': 'error', + 'no-prototype-builtins': 'error', + 'no-redeclare': ['error', { builtinGlobals: false }], + 'no-regex-spaces': 'error', + 'no-self-compare': 'error', + 'no-sequences': 'error', + 'no-shadow': 'off', + 'no-shadow-restricted-names': 'error', + 'eslint/no-empty-function': [ + 'error', + { + allow: ['arrowFunctions', 'functions', 'methods'], + }, + ], + 'no-template-curly-in-string': 'error', + 'no-throw-literal': 'error', + 'no-undef-init': 'error', + 'no-unused-expressions': [ + 'error', + { + allowShortCircuit: true, + allowTaggedTemplates: true, + allowTernary: true, + }, + ], + 'eslint/no-unused-vars': [ + 'error', + { + argsIgnorePattern: '^_', + varsIgnorePattern: '^_', + }, + ], + 'no-var': 'error', + 'no-eval': 'error', + 'no-iterator': 'error', + 'no-new-wrappers': 'error', + 'no-restricted-globals': [ + 'error', + { message: 'Use `globalThis` instead.', name: 'global' }, + { message: 'Use `globalThis` instead.', name: 'self' }, + ], + 'no-useless-call': 'error', + 'no-useless-computed-key': 'error', + 'no-useless-constructor': 'error', + 'no-useless-return': 'error', + 'object-shorthand': [ + 'error', + 'always', + { + avoidQuotes: true, + ignoreConstructors: false, + }, + ], + 'one-var': ['error', { initialized: 'never' }], + 'prefer-const': [ + 'error', + { + destructuring: 'all', + ignoreReadBeforeAssign: true, + }, + ], + 'eslint/prefer-arrow-callback': [ + 'error', + { + allowNamedFunctions: false, + allowUnboundThis: true, + }, + ], + 'prefer-exponentiation-operator': 'error', + 'prefer-promise-reject-errors': 'error', + 'eslint/prefer-regex-literals': [ + 'error', + { + disallowRedundantWrapping: true, + }, + ], + 'prefer-rest-params': 'error', + 'prefer-spread': 'error', + 'prefer-template': 'error', + 'spaced-comment': 'error', + 'symbol-description': 'error', + 'unicode-bom': ['error', 'never'], + 'use-isnan': [ + 'error', + { + enforceForIndexOf: true, + enforceForSwitchCase: true, + }, + ], + 'valid-typeof': [ + 'error', + { + requireStringLiterals: true, + }, + ], + 'vars-on-top': 'error', + yoda: ['error', 'never'], + }, +}; + +export { javascript }; diff --git a/internal/lint-configs/oxlint-config/src/configs/node.ts b/internal/lint-configs/oxlint-config/src/configs/node.ts new file mode 100644 index 000000000..24aa08dfa --- /dev/null +++ b/internal/lint-configs/oxlint-config/src/configs/node.ts @@ -0,0 +1,11 @@ +import type { OxlintConfig } from 'oxlint'; + +const node: OxlintConfig = { + rules: { + 'node/no-exports-assign': 'error', + 'node/no-new-require': 'error', + 'node/no-path-concat': 'error', + }, +}; + +export { node }; diff --git a/internal/lint-configs/oxlint-config/src/configs/overrides.ts b/internal/lint-configs/oxlint-config/src/configs/overrides.ts new file mode 100644 index 000000000..c8838feb1 --- /dev/null +++ b/internal/lint-configs/oxlint-config/src/configs/overrides.ts @@ -0,0 +1,98 @@ +import type { OxlintConfig } from 'oxlint'; + +const overrides: OxlintConfig = { + overrides: [ + { + files: ['*.d.ts', '**/*.d.ts'], + rules: { + 'import/no-unassigned-import': 'off', + 'typescript/triple-slash-reference': 'off', + }, + }, + { + files: [ + '**/__tests__/**/*.js', + '**/__tests__/**/*.cjs', + '**/__tests__/**/*.mjs', + '**/__tests__/**/*.jsx', + '**/__tests__/**/*.ts', + '**/__tests__/**/*.cts', + '**/__tests__/**/*.mts', + '**/__tests__/**/*.tsx', + '**/*.spec.js', + '**/*.spec.cjs', + '**/*.spec.mjs', + '**/*.spec.jsx', + '**/*.spec.ts', + '**/*.spec.cts', + '**/*.spec.mts', + '**/*.spec.tsx', + '**/*.test.js', + '**/*.test.cjs', + '**/*.test.mjs', + '**/*.test.jsx', + '**/*.test.ts', + '**/*.test.cts', + '**/*.test.mts', + '**/*.test.tsx', + '**/*.bench.js', + '**/*.bench.cjs', + '**/*.bench.mjs', + '**/*.bench.jsx', + '**/*.bench.ts', + '**/*.bench.cts', + '**/*.bench.mts', + '**/*.bench.tsx', + '**/*.benchmark.js', + '**/*.benchmark.cjs', + '**/*.benchmark.mjs', + '**/*.benchmark.jsx', + '**/*.benchmark.ts', + '**/*.benchmark.cts', + '**/*.benchmark.mts', + '**/*.benchmark.tsx', + ], + rules: { + 'no-console': 'off', + }, + }, + { + files: ['packages/@core/base/shared/src/utils/inference.ts'], + rules: { + 'vue/prefer-import-from-vue': 'off', + }, + }, + { + files: ['packages/@core/ui-kit/menu-ui/src/sub-menu.vue'], + rules: { + 'import/no-self-import': 'off', + }, + }, + { + files: [ + 'scripts/**/*.js', + 'scripts/**/*.cjs', + 'scripts/**/*.mjs', + 'scripts/**/*.jsx', + 'scripts/**/*.ts', + 'scripts/**/*.cts', + 'scripts/**/*.mts', + 'scripts/**/*.tsx', + 'internal/**/*.js', + 'internal/**/*.cjs', + 'internal/**/*.mjs', + 'internal/**/*.jsx', + 'internal/**/*.ts', + 'internal/**/*.cts', + 'internal/**/*.mts', + 'internal/**/*.tsx', + ], + rules: { + 'no-console': 'off', + 'unicorn/no-process-exit': 'off', + }, + }, + ], +}; + +export { overrides }; diff --git a/internal/lint-configs/oxlint-config/src/configs/plugins.ts b/internal/lint-configs/oxlint-config/src/configs/plugins.ts new file mode 100644 index 000000000..8b6f3dd17 --- /dev/null +++ b/internal/lint-configs/oxlint-config/src/configs/plugins.ts @@ -0,0 +1,7 @@ +import type { OxlintConfig } from 'oxlint'; + +const plugins: OxlintConfig = { + plugins: ['import', 'node', 'oxc', 'typescript', 'unicorn', 'vitest', 'vue'], +}; + +export { plugins }; diff --git a/internal/lint-configs/oxlint-config/src/configs/tailwindcss.ts b/internal/lint-configs/oxlint-config/src/configs/tailwindcss.ts new file mode 100644 index 000000000..c24e5c865 --- /dev/null +++ b/internal/lint-configs/oxlint-config/src/configs/tailwindcss.ts @@ -0,0 +1,50 @@ +import type { OxlintConfig } from 'oxlint'; + +import eslintPluginBetterTailwindcss from 'eslint-plugin-better-tailwindcss'; +import { getDefaultSelectors } from 'eslint-plugin-better-tailwindcss/defaults'; +import { SelectorKind } from 'eslint-plugin-better-tailwindcss/types'; + +const selectors = [ + ...getDefaultSelectors(), + { + kind: SelectorKind.Attribute, + match: [{ type: 'objectValues' }], + name: '^classNames$', + }, +]; + +const settings = { + entryPoint: 'packages/@core/base/design/src/css/global.css', + selectors, +}; + +const tailwindcss: OxlintConfig = { + // Generated shadcn-ui internals are intentionally left unmanaged. + ignorePatterns: ['packages/@core/ui-kit/shadcn-ui/**/*'], + jsPlugins: [ + { + name: 'better-tailwindcss', + specifier: 'eslint-plugin-better-tailwindcss', + }, + ], + rules: { + ...eslintPluginBetterTailwindcss.configs.recommended.rules, + 'better-tailwindcss/enforce-consistent-class-order': [ + 'error', + { + detectComponentClasses: true, + unknownClassOrder: 'asc', + unknownClassPosition: 'start', + }, + ], + // Let Prettier own wrapping decisions to avoid ping-pong formatting. + 'better-tailwindcss/enforce-consistent-line-wrapping': 'off', + 'better-tailwindcss/no-unknown-classes': 'off', + }, + settings: { + 'better-tailwindcss': settings, + 'eslint-plugin-better-tailwindcss': settings, + }, +}; + +export { tailwindcss }; diff --git a/internal/lint-configs/oxlint-config/src/configs/test.ts b/internal/lint-configs/oxlint-config/src/configs/test.ts new file mode 100644 index 000000000..a7470eb6a --- /dev/null +++ b/internal/lint-configs/oxlint-config/src/configs/test.ts @@ -0,0 +1,23 @@ +import type { OxlintConfig } from 'oxlint'; + +const test: OxlintConfig = { + rules: { + 'jest/no-conditional-expect': 'off', + 'jest/require-to-throw-message': 'off', + 'vitest/consistent-test-it': [ + 'error', + { + fn: 'it', + withinDescribe: 'it', + }, + ], + 'vitest/hoisted-apis-on-top': 'off', + 'vitest/no-focused-tests': 'error', + 'vitest/no-identical-title': 'error', + 'vitest/no-import-node-test': 'error', + 'vitest/prefer-hooks-in-order': 'error', + 'vitest/prefer-lowercase-title': 'error', + }, +}; + +export { test }; diff --git a/internal/lint-configs/oxlint-config/src/configs/typescript.ts b/internal/lint-configs/oxlint-config/src/configs/typescript.ts new file mode 100644 index 000000000..e25b8fbd0 --- /dev/null +++ b/internal/lint-configs/oxlint-config/src/configs/typescript.ts @@ -0,0 +1,12 @@ +import type { OxlintConfig } from 'oxlint'; + +const typescript: OxlintConfig = { + rules: { + 'typescript/ban-ts-comment': 'error', + 'typescript/no-non-null-assertion': 'error', + 'typescript/no-var-requires': 'error', + 'typescript/triple-slash-reference': 'error', + }, +}; + +export { typescript }; diff --git a/internal/lint-configs/oxlint-config/src/configs/unicorn.ts b/internal/lint-configs/oxlint-config/src/configs/unicorn.ts new file mode 100644 index 000000000..b72af15a0 --- /dev/null +++ b/internal/lint-configs/oxlint-config/src/configs/unicorn.ts @@ -0,0 +1,14 @@ +import type { OxlintConfig } from 'oxlint'; + +const unicorn: OxlintConfig = { + rules: { + 'unicorn/consistent-function-scoping': 'off', + 'unicorn/no-process-exit': 'error', + 'unicorn/no-single-promise-in-promise-methods': 'off', + 'unicorn/no-useless-spread': 'off', + 'unicorn/prefer-global-this': 'off', + 'unicorn/prefer-module': 'error', + }, +}; + +export { unicorn }; diff --git a/internal/lint-configs/oxlint-config/src/configs/vue.ts b/internal/lint-configs/oxlint-config/src/configs/vue.ts new file mode 100644 index 000000000..172507d69 --- /dev/null +++ b/internal/lint-configs/oxlint-config/src/configs/vue.ts @@ -0,0 +1,9 @@ +import type { OxlintConfig } from 'oxlint'; + +const vue: OxlintConfig = { + rules: { + 'vue/prefer-import-from-vue': 'error', + }, +}; + +export { vue }; diff --git a/internal/lint-configs/oxlint-config/src/index.ts b/internal/lint-configs/oxlint-config/src/index.ts new file mode 100644 index 000000000..b890cb0d3 --- /dev/null +++ b/internal/lint-configs/oxlint-config/src/index.ts @@ -0,0 +1,21 @@ +import type { OxlintConfig } from 'oxlint'; + +import { defineConfig as defineOxlintConfig } from 'oxlint'; + +import { mergeOxlintConfigs, oxlintConfig } from './configs'; + +type VbenOxlintConfig = Omit & { + extends?: OxlintConfig[]; +}; + +function defineConfig(config: VbenOxlintConfig = {}) { + const { extends: extendedConfigs = [], ...restConfig } = config; + + return defineOxlintConfig( + mergeOxlintConfigs(oxlintConfig, ...extendedConfigs, restConfig), + ); +} + +export { defineConfig, oxlintConfig }; +export * from './configs'; +export type { OxlintConfig, VbenOxlintConfig }; diff --git a/internal/lint-configs/oxlint-config/tsconfig.json b/internal/lint-configs/oxlint-config/tsconfig.json new file mode 100644 index 000000000..b2ec3b61e --- /dev/null +++ b/internal/lint-configs/oxlint-config/tsconfig.json @@ -0,0 +1,6 @@ +{ + "$schema": "https://json.schemastore.org/tsconfig", + "extends": "@vben/tsconfig/node.json", + "include": ["src"], + "exclude": ["node_modules"] +} diff --git a/lefthook.yml b/lefthook.yml index 728017416..554fb2467 100644 --- a/lefthook.yml +++ b/lefthook.yml @@ -50,10 +50,10 @@ pre-commit: run: pnpm prettier --cache --ignore-unknown --write {staged_files} glob: '*.md' lint-vue: - run: pnpm prettier --write {staged_files} && pnpm eslint --cache --fix {staged_files} && pnpm stylelint --fix --allow-empty-input {staged_files} + run: pnpm prettier --write {staged_files} && pnpm oxlint --fix {staged_files} && pnpm eslint --cache --fix {staged_files} && pnpm stylelint --fix --allow-empty-input {staged_files} glob: '*.vue' lint-js: - run: pnpm prettier --cache --ignore-unknown --write {staged_files} && pnpm eslint --cache --fix {staged_files} + run: pnpm prettier --cache --ignore-unknown --write {staged_files} && pnpm oxlint --fix {staged_files} && pnpm eslint --cache --fix {staged_files} glob: '*.{js,jsx,ts,tsx}' lint-style: run: pnpm prettier --cache --ignore-unknown --write {staged_files} && pnpm stylelint --fix --allow-empty-input {staged_files} diff --git a/oxlint.config.ts b/oxlint.config.ts new file mode 100644 index 000000000..0b64163bc --- /dev/null +++ b/oxlint.config.ts @@ -0,0 +1,3 @@ +import { defineConfig } from '@vben/oxlint-config'; + +export default defineConfig(); diff --git a/package.json b/package.json index 4de5d7cb1..c4437b0dc 100644 --- a/package.json +++ b/package.json @@ -51,7 +51,9 @@ "dev:tdesign": "pnpm -F @vben/web-tdesign run dev", "dev:play": "pnpm -F @vben/playground run dev", "format": "vsh lint --format", - "lint": "vsh lint", + "lint": "pnpm run lint:oxc && pnpm run lint:eslint", + "lint:eslint": "vsh lint", + "lint:oxc": "oxlint .", "postinstall": "pnpm -r run stub --if-present", "preinstall": "npx only-allow pnpm", "preview": "turbo-run preview", @@ -70,6 +72,7 @@ "@types/node": "catalog:", "@vben/commitlint-config": "workspace:*", "@vben/eslint-config": "workspace:*", + "@vben/oxlint-config": "workspace:*", "@vben/prettier-config": "workspace:*", "@vben/stylelint-config": "workspace:*", "@vben/tsconfig": "workspace:*", @@ -84,6 +87,7 @@ "happy-dom": "catalog:", "is-ci": "catalog:", "lefthook": "catalog:", + "oxlint": "catalog:", "playwright": "catalog:", "rimraf": "catalog:", "tailwindcss": "catalog:", diff --git a/packages/@core/ui-kit/menu-ui/src/sub-menu.vue b/packages/@core/ui-kit/menu-ui/src/sub-menu.vue index e4d471fb0..57e17dee6 100644 --- a/packages/@core/ui-kit/menu-ui/src/sub-menu.vue +++ b/packages/@core/ui-kit/menu-ui/src/sub-menu.vue @@ -4,7 +4,6 @@ import type { MenuRecordRaw } from '@vben-core/typings'; import { computed } from 'vue'; import { MenuBadge, MenuItem, SubMenu as SubMenuComp } from './components'; -// eslint-disable-next-line import/no-self-import import SubMenu from './sub-menu.vue'; interface Props { diff --git a/packages/@core/ui-kit/shadcn-ui/src/components/full-screen/full-screen.vue b/packages/@core/ui-kit/shadcn-ui/src/components/full-screen/full-screen.vue index 80e469d33..970e713c9 100644 --- a/packages/@core/ui-kit/shadcn-ui/src/components/full-screen/full-screen.vue +++ b/packages/@core/ui-kit/shadcn-ui/src/components/full-screen/full-screen.vue @@ -12,11 +12,11 @@ const { isFullscreen, toggle } = useFullscreen(); // 重新检查全屏状态 isFullscreen.value = !!( document.fullscreenElement || - // @ts-ignore + // @ts-expect-error: vendor fullscreen APIs are not included in the standard DOM typings document.webkitFullscreenElement || - // @ts-ignore + // @ts-expect-error: vendor fullscreen APIs are not included in the standard DOM typings document.mozFullScreenElement || - // @ts-ignore + // @ts-expect-error: vendor fullscreen APIs are not included in the standard DOM typings document.msFullscreenElement ); diff --git a/packages/effects/common-ui/src/components/json-viewer/index.vue b/packages/effects/common-ui/src/components/json-viewer/index.vue index f66c31828..a8121de03 100644 --- a/packages/effects/common-ui/src/components/json-viewer/index.vue +++ b/packages/effects/common-ui/src/components/json-viewer/index.vue @@ -11,14 +11,13 @@ import type { } from './types'; import { computed, useAttrs } from 'vue'; -// @ts-ignore +// @ts-expect-error: vue-json-viewer does not expose compatible typings for this import path import VueJsonViewer from 'vue-json-viewer'; import { $t } from '@vben/locales'; import { isBoolean } from '@vben-core/shared/utils'; -// @ts-ignore import JsonBigint from 'json-bigint'; defineOptions({ name: 'JsonViewer' }); diff --git a/packages/effects/layouts/src/widgets/theme-toggle/theme-button.vue b/packages/effects/layouts/src/widgets/theme-toggle/theme-button.vue index d54009901..ce5809502 100644 --- a/packages/effects/layouts/src/widgets/theme-toggle/theme-button.vue +++ b/packages/effects/layouts/src/widgets/theme-toggle/theme-button.vue @@ -41,7 +41,7 @@ const bindProps = computed(() => { function toggleTheme(event: MouseEvent) { const isAppearanceTransition = - // @ts-expect-error + // @ts-expect-error: startViewTransition is not available in the current DOM lib target document.startViewTransition && !window.matchMedia('(prefers-reduced-motion: reduce)').matches; if (!isAppearanceTransition || !event) { @@ -54,7 +54,6 @@ function toggleTheme(event: MouseEvent) { Math.max(x, innerWidth - x), Math.max(y, innerHeight - y), ); - // @ts-ignore startViewTransition const transition = document.startViewTransition(async () => { isDark.value = !isDark.value; await nextTick(); diff --git a/packages/effects/plugins/src/vxe-table/use-vxe-grid.vue b/packages/effects/plugins/src/vxe-table/use-vxe-grid.vue index eafe567fc..83b5cb6cc 100644 --- a/packages/effects/plugins/src/vxe-table/use-vxe-grid.vue +++ b/packages/effects/plugins/src/vxe-table/use-vxe-grid.vue @@ -316,7 +316,7 @@ async function init() { '[Vben Vxe Table]: The formConfig in the grid is not supported, please use the `formOptions` props', ); } - // @ts-ignore + // @ts-expect-error: setState is injected by the extended grid api wrapper at runtime props.api?.setState?.({ gridOptions: defaultGridOptions }); // form 由 vben-form 代替,所以需要保证query相关事件可以拿到参数 extendProxyOptions(props.api, defaultGridOptions, () => diff --git a/playground/src/views/examples/vxe-table/basic.vue b/playground/src/views/examples/vxe-table/basic.vue index a4e9aa862..d20472461 100644 --- a/playground/src/views/examples/vxe-table/basic.vue +++ b/playground/src/views/examples/vxe-table/basic.vue @@ -43,7 +43,6 @@ const gridEvents: VxeGridListeners = { }, }; -// @ts-ignore const [Grid, gridApi] = useVbenVxeGrid({ // 放开注释查看表单组件的类型 // formOptions: { diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 18e5f85e5..8a4546bb7 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -99,14 +99,12 @@ catalog: eslint-plugin-better-tailwindcss: ^4.3.2 eslint-plugin-command: ^3.5.2 eslint-plugin-import-x: ^4.16.2 - eslint-plugin-jsdoc: ^62.8.0 eslint-plugin-jsonc: ^3.1.2 eslint-plugin-n: ^17.24.0 - eslint-plugin-no-only-tests: ^3.3.0 + eslint-plugin-oxlint: ^1.55.0 eslint-plugin-perfectionist: ^5.6.0 eslint-plugin-pnpm: ^1.6.0 eslint-plugin-prettier: ^5.5.5 - eslint-plugin-regexp: ^3.1.0 eslint-plugin-unicorn: ^63.0.0 eslint-plugin-unused-imports: ^4.4.1 eslint-plugin-vue: ^10.8.0 @@ -129,6 +127,7 @@ catalog: nitropack: ^2.13.1 nprogress: ^0.2.0 ora: ^9.3.0 + oxlint: ^1.55.0 pinia: ^3.0.4 pinia-plugin-persistedstate: ^4.7.1 pkg-types: ^2.3.0 @@ -140,7 +139,7 @@ catalog: publint: ^0.3.18 qrcode: ^1.5.4 qs: ^6.15.0 - reka-ui: ^2.9.1 + reka-ui: ^2.9.2 resolve.exports: ^2.0.3 rimraf: ^6.1.3 rollup: ^4.59.0 diff --git a/scripts/vsh/src/check-dep/index.ts b/scripts/vsh/src/check-dep/index.ts index 3870b1d8b..8f6a171e2 100644 --- a/scripts/vsh/src/check-dep/index.ts +++ b/scripts/vsh/src/check-dep/index.ts @@ -22,6 +22,7 @@ const DEFAULT_CONFIG = { '@vben/commitlint-config', '@vben/eslint-config', '@vben/node-utils', + '@vben/oxlint-config', '@vben/prettier-config', '@vben/stylelint-config', '@vben/tsconfig', diff --git a/scripts/vsh/src/lint/index.ts b/scripts/vsh/src/lint/index.ts index f0a05ec86..0ce74752d 100644 --- a/scripts/vsh/src/lint/index.ts +++ b/scripts/vsh/src/lint/index.ts @@ -19,6 +19,9 @@ async function runLint({ format }: LintCommandOptions) { await execaCommand(`prettier . --write --cache --log-level warn`, { stdio: 'inherit', }); + await execaCommand(`oxlint . --fix`, { + stdio: 'inherit', + }); await execaCommand(`eslint . --cache --fix`, { stdio: 'inherit', }); diff --git a/up-oxc.md b/up-oxc.md new file mode 100644 index 000000000..cd90ccb2f --- /dev/null +++ b/up-oxc.md @@ -0,0 +1,823 @@ +# OXC 迁移计划 + +> 本文档记录将项目中可替代的工具链逐步迁移到 [oxc](https://oxc.rs/) 生态的计划。 + +## 当前现状 + +项目 ESLint 体系依赖约 20+ 个包,包括: + +- `eslint` ^10.0.3 +- `@typescript-eslint/parser` ^8.57.0 +- `@typescript-eslint/eslint-plugin` ^8.57.0 +- `eslint-plugin-unicorn` ^63.0.0 +- `eslint-plugin-import-x` ^4.16.2 +- `eslint-plugin-unused-imports` ^4.4.1 +- `eslint-plugin-regexp` ^3.1.0 +- `eslint-plugin-jsdoc` ^62.8.0 +- `eslint-plugin-n` ^17.24.0 +- `eslint-plugin-no-only-tests` ^3.3.0 +- `eslint-plugin-perfectionist` ^5.6.0 +- `eslint-plugin-prettier` ^5.5.5 +- `eslint-plugin-vue` ^10.8.0 +- `eslint-plugin-better-tailwindcss` ^4.3.2 +- `eslint-plugin-jsonc` ^3.1.2 +- `eslint-plugin-yml` ^3.3.1 +- `eslint-plugin-command` ^3.5.2 +- `eslint-plugin-pnpm` ^1.6.0 +- `@eslint-community/eslint-plugin-eslint-comments` ^4.7.1 +- `@vitest/eslint-plugin` ^1.6.11 +- `vue-eslint-parser` ^10.4.0 +- `yaml-eslint-parser` ^2.0.0 +- `globals` ^17.4.0 +- `prettier` ^3.8.1 +- `esbuild` ^0.27.4 +- `html-minifier-terser` ^7.2.0 + +--- + +## 第一阶段:引入 oxlint,与 ESLint 共存 + +**目标**:将通用 JS/TS lint 规则交给 oxlint,ESLint 仅保留 oxlint 无法覆盖的特殊规则,大幅减少依赖数量并提升 lint 速度。 + +### 1.1 安装 oxlint + +```bash +pnpm add -Dw oxlint +``` + +### 1.2 可移除的 ESLint 插件 + +以下插件与规则在 oxlint 中已有对应能力,但“有能力承接”不等于“现在就能删依赖”。这里区分为“已移除 / 已迁出 ESLint”和“仍需保留”两类: + +| 插件 | 当前状态 | 说明 | +| --- | --- | --- | +| `eslint-plugin-regexp` | 已移除 | 由 oxlint 内建能力承接 | +| `eslint-plugin-jsdoc` | 已移除 | 由 oxlint 内建能力承接 | +| `eslint-plugin-no-only-tests` | 已移除 | 测试规则已迁到 oxlint / vitest | +| `eslint-plugin-better-tailwindcss` | 已迁出 ESLint | 仍然保留依赖,但已通过 oxlint `jsPlugins` 运行 | +| `@typescript-eslint/eslint-plugin` | 仍需保留 | 还有一批 TS 规则未迁移 | +| `@typescript-eslint/parser` | 仍需保留 | ESLint 侧 TS / Vue 规则仍依赖它 | +| `eslint-plugin-unicorn` | 仍需保留 | ESLint 侧仍使用其 recommended 规则补充 | +| `eslint-plugin-import-x` | 仍需保留 | `import/newline-after-import`、`import/no-webpack-loader-syntax` 尚未迁移 | +| `eslint-plugin-unused-imports` | 仍需保留 | `no-unused-imports` 仍由 ESLint 负责 | +| `eslint-plugin-n` | 仍需保留 | 多条 Node 规则在 oxlint 中暂无等价承接 | +| `eslint-plugin-perfectionist` | 仍需保留 | 排序规则策略不同,不建议强迁 | +| `@eslint-community/eslint-plugin-eslint-comments` | 仍需保留 | 当前尚未迁移 | + +### 1.3 必须保留的 ESLint 插件 + +以下插件 oxlint 目前不支持或不完全支持,需继续由 ESLint 处理: + +| 插件 | 原因 | +| --- | --- | +| `eslint-plugin-vue` + `vue-eslint-parser` | Vue SFC 模板 lint,oxlint Vue 支持尚未完善 | +| `eslint-plugin-jsonc` + `eslint-plugin-yml` | JSON/YAML 文件 lint,oxlint 不支持 | +| `eslint-plugin-pnpm` | pnpm workspace 规则,oxlint 无对应 | +| `eslint-plugin-command` | 魔法注释命令,oxlint 无对应 | +| `@vitest/eslint-plugin` | Vitest 特定规则(oxlint vitest 分类可能部分覆盖,需验证) | +| `yaml-eslint-parser` | YAML 解析,配合 eslint-plugin-yml 使用 | +| `eslint-plugin-prettier` | Prettier 集成(第二阶段替换 Prettier 后可移除) | + +### 1.4 配置 oxlint + +当前实现已调整为与 ESLint 一致的 workspace 包组织方式: + +- 根目录使用 [oxlint.config.ts](./oxlint.config.ts) 作为薄入口 +- 实际规则配置位于 `internal/lint-configs/oxlint-config` +- ESLint 兼容层通过 `eslint-plugin-oxlint` 直接消费 `@vben/oxlint-config` 导出的配置对象,确保 oxlint 与 ESLint 共用同一份规则源 + +示例结构: + +```ts +// oxlint.config.ts +import { defineConfig } from '@vben/oxlint-config'; + +export default defineConfig(); +``` + +```ts +// internal/lint-configs/oxlint-config/src/index.ts +import { defineConfig as defineOxlintConfig } from 'oxlint'; +import { mergeOxlintConfigs, oxlintConfig } from './configs'; + +export function defineConfig(config = {}) { + const { extends: extendedConfigs = [], ...restConfig } = config; + return defineOxlintConfig( + mergeOxlintConfigs(oxlintConfig, ...extendedConfigs, restConfig), + ); +} +``` + +不再使用根目录 `.oxlintrc.json`。 + +### 1.5 当前已完成项 + +截至目前,第一阶段已完成以下工作: + +1. 根 `lint` 已调整为先跑 `oxlint` 再跑 `vsh lint` +2. `lefthook` 已接入 `oxlint --fix` +3. 已新增 `@vben/oxlint-config`,并将 oxlint 规则集中到 `internal/lint-configs/oxlint-config` +4. `eslint-plugin-oxlint` 已接入 `@vben/eslint-config` +5. 已移除 `eslint-plugin-jsdoc`、`eslint-plugin-regexp`、`eslint-plugin-no-only-tests` +6. `better-tailwindcss` 已迁移到 `@vben/oxlint-config` 的 `jsPlugins` 方案 +7. `unicorn` 插件已在 oxlint 侧显式启用,并补齐仓库内所需的 override +8. 当前 `pnpm run lint:oxc`、`pnpm run lint:eslint`、`pnpm run lint` 均可通过 + +### 1.6 当前 oxlint 基线策略 + +当前 `@vben/oxlint-config` 采用“先收敛噪音,再逐步接管规则”的策略: + +- 先启用 `correctness` 和 `suspicious` +- 保留 `import`、`node`、`typescript`、`unicorn`、`vitest`、`vue` 插件 +- 通过 `jsPlugins` 在 oxlint 侧承接 `better-tailwindcss` +- 对当前仓库内高噪音且不准备立即批量整改的规则先显式关闭 +- 通过 `overrides` 为 `.d.ts` 等特殊文件保留例外配置 + +### 1.7 已确认可迁移到 oxlint 的规则 + +以下规则已确认可由当前版本 `oxlint` 接管,后续应从 `@vben/eslint-config` 逐步迁移到 `@vben/oxlint-config`: + +#### JavaScript / core + +- `eqeqeq` +- `no-alert` +- `no-array-constructor` +- `no-caller` +- `no-case-declarations` +- `no-control-regex` +- `no-debugger` +- `no-empty-function` +- `no-eval` +- `no-iterator` +- `no-new-wrappers` +- `no-shadow` +- `no-shadow-restricted-names` +- `no-unused-expressions` +- `no-unused-vars` +- `prefer-const` + +#### import + +- `import/consistent-type-specifier-style` +- `import/first` +- `import/no-duplicates` +- `import/no-mutable-exports` +- `import/no-named-default` +- `import/no-self-import` + +#### TypeScript + +- `@typescript-eslint/no-non-null-assertion` +- `@typescript-eslint/no-empty-function` +- `@typescript-eslint/no-shadow` +- `@typescript-eslint/no-unused-expressions` +- `@typescript-eslint/no-unused-vars` +- `@typescript-eslint/no-var-requires` + +> `@typescript-eslint/ban-ts-comment` 虽然 oxlint 已支持,但当前仓库中存在多处 `@ts-ignore`,直接迁移会新增报错,暂不建议在这一阶段接管。 + +#### Vitest + +- `vitest/no-focused-tests` +- `vitest/no-identical-title` +- `vitest/prefer-hooks-in-order` +- `vitest/prefer-lowercase-title` + +#### Node + +- `n/no-exports-assign` +- `n/no-new-require` +- `n/no-path-concat` + +#### Unicorn + +- `unicorn/consistent-function-scoping` +- `unicorn/no-process-exit` +- `unicorn/prefer-global-this` +- `unicorn/prefer-module` + +#### Tailwind CSS + +- `better-tailwindcss/enforce-consistent-class-order` +- `better-tailwindcss/enforce-consistent-line-wrapping` +- `better-tailwindcss/no-unknown-classes` + +#### Vue + +- `vue/prefer-import-from-vue` + +### 1.8 暂时仍需保留在 ESLint 的部分 + +以下内容当前不建议从 ESLint 侧移除: + +1. Vue SFC/template 主体规则 +2. `perfectionist` +3. `jsonc` / `yml` +4. `pnpm` +5. `command` +6. `prettier` +7. `eslint-comments` +8. `unused-imports` + +其中 Vue 相关规则尤其需要保留,至少包括: + +- `vue/block-order` +- `vue/component-name-in-template-casing` +- `vue/component-options-name-casing` +- `vue/custom-event-name-casing` +- `vue/define-macros-order` +- `vue/no-unused-refs` +- `vue/no-useless-v-bind` +- `vue/require-default-prop` +- `vue/require-explicit-emits` +- `vue/v-on-event-hyphenation` +- `vue/prefer-separate-static-class` +- `vue/prefer-template` + +### 1.9 推荐的下一步执行顺序 + +为降低回归风险,后续规则迁移建议按以下顺序推进: + +1. `import.ts` +2. `javascript.ts` 中剩余的低风险 1:1 规则 +3. `typescript.ts` +4. `node.ts` +5. `test.ts` +6. 最后再评估 `import.ts` 中剩余规则是否值得继续下沉 +7. 最后处理 `vue.ts` 中的个别确认项 + +每迁移一组后都执行: + +```bash +pnpm run lint:oxc +pnpm run lint:eslint +pnpm run lint +``` + +### 1.10 更新 scripts + +```jsonc +// package.json +{ + "scripts": { + "lint": "pnpm run lint:oxc && pnpm run lint:eslint", + "lint:oxc": "oxlint .", + "lint:eslint": "vsh lint", + }, +} +``` + +### 1.11 更新 lefthook + +在 pre-commit 中将 oxlint 加入暂存文件检查: + +```yaml +pre-commit: + commands: + lint-vue: + glob: '*.vue' + run: pnpm prettier --write {staged_files} && pnpm oxlint --fix {staged_files} && pnpm eslint --cache --fix {staged_files} && pnpm stylelint --fix --allow-empty-input {staged_files} + lint-js: + glob: '*.{js,jsx,ts,tsx}' + run: pnpm prettier --cache --ignore-unknown --write {staged_files} && pnpm oxlint --fix {staged_files} && pnpm eslint --cache --fix {staged_files} +``` + +### 1.12 验证步骤 + +1. 安装 oxlint 后全量运行 `pnpm lint:oxc`,记录所有报告 +2. 对比当前 ESLint 规则覆盖情况,调整 oxlint 配置使两者对齐 +3. 逐个移除可替代的 ESLint 插件,每次移除后运行完整 lint 验证无遗漏 +4. CI 中同时运行 oxlint 和 ESLint,确保双重覆盖期间无回归 + +### 1.13 待修复问题与改进项 + +> 以下问题通过代码审查发现,按优先级排列。 + +#### [高] `unicorn` 插件启用后要同时收敛默认规则与 override + +这个问题已经修复,但需要记录经验: + +- `plugins` 一旦显式声明,必须手动包含 `unicorn` +- 启用后不只会打开我们手写的 `unicorn/no-process-exit`、`unicorn/prefer-module` +- 还会带出 oxlint 默认启用的一部分 `unicorn` 规则,需要按仓库现状显式关闭噪音项 +- OXC `overrides.files` 不要继续使用 `?([cm])[jt]s?(x)` 这类 ESLint 风格的 extglob,当前仓库已改为显式文件后缀列表 + +#### [高] 使用 `@oxlint/migrate` 仅做审计,不直接落地配置 + +官方提供了 [`@oxlint/migrate`](https://github.com/oxc-project/oxlint-migrate) 工具,可自动读取 ESLint flat config 生成对应 oxlint 配置: + +也可以参考 [`oxlint-migration-inspector`](https://github.com/joris-gallot/oxlint-migration-inspector) `npx oxlint-migration-inspector` 命令,查看哪些规则无法迁移。 + +```bash +# 查看哪些规则无法迁移 +npx @oxlint/migrate --details + +# 与已有配置合并(而非覆盖) +npx @oxlint/migrate --merge +``` + +对于当前仓库,`@oxlint/migrate` 更适合做“审计工具”,不适合直接生成或合并为最终配置,原因是: + +1. 当前仓库已经不是简单的 `.oxlintrc.json` 形态,而是 workspace 包 + 模块化 `configs/*` +2. `@vben/oxlint-config` 依赖自定义 `mergeOxlintConfigs(...)` 来保证 `settings`、`ignorePatterns`、`jsPlugins` 正确合并 +3. Tailwind 规则依赖手写 `jsPlugins` 和 `settings` +4. 现有迁移流程还包含 ESLint 兼容层 `eslint-plugin-oxlint` + +因此更合理的用法是: + +```bash +# 只做对账,不直接采纳输出 +npx @oxlint/migrate --details +``` + +不建议直接使用 `--merge` 覆盖当前仓库的真实配置结构。 + +#### [中] `mergeOxlintConfigs` 函数重复定义 + +`mergeOxlintConfigs` 在两处有几乎相同的实现: + +- `oxlint-config/src/configs/index.ts` — 包含 `jsPlugins` 合并逻辑 +- `eslint-config/src/configs/oxlint.ts` — 缺少 `jsPlugins` 合并逻辑 + +这个问题已经修复。ESLint 侧的 `oxlint.ts` 已直接导入 `@vben/oxlint-config` 的 `mergeOxlintConfigs`,避免实现不一致: + +```ts +// eslint-config/src/configs/oxlint.ts +import { mergeOxlintConfigs, oxlintConfig } from '@vben/oxlint-config'; +import oxlint from 'eslint-plugin-oxlint'; + +export async function oxcCompat(): Promise { + return oxlint.buildFromOxlintConfig(mergeOxlintConfigs(oxlintConfig)); +} +``` + +#### [中] 验证 `oxcCompat()` 是否正确关闭 ESLint 重复规则 + +ESLint 侧 `javascript.ts` 约有 **70+ 条规则**,很多已被 oxlint 的 `correctness`/`suspicious` category 覆盖(如 `no-debugger`、`no-dupe-keys`、`no-unreachable` 等),但 ESLint 侧并未手动关闭。 + +`eslint-plugin-oxlint` 的 `buildFromOxlintConfig()` 理论上会自动关闭 ESLint 中被 oxlint 覆盖的规则。需要验证 `oxcCompat()` 确实生效——如果正常工作,则 ESLint 侧无需手动清理这些规则。 + +**验证方法**: + +```bash +# 临时在 oxlint 中 warn 一个 ESLint 也有的规则(如 no-debugger), +# 确认 ESLint 不再报告该规则 +``` + +#### [中] `eslint-plugin-import-x` 迁移不完全 + +ESLint 侧 `import.ts` 仍保留 3 条规则: + +| 规则 | 状态 | +| --------------------------------- | --------------------------------- | +| `import/newline-after-import` | 未迁移到 oxlint,需检查是否已支持 | +| `import/no-unresolved` | 已 off,无需处理 | +| `import/no-webpack-loader-syntax` | 未迁移到 oxlint,需检查是否已支持 | + +如果这两条规则被 oxlint 支持,则 `eslint-plugin-import-x` 可从 ESLint 依赖中完全移除。 + +#### [中] `eslint-plugin-n` 实际不能完全移除 + +1.2 表格中标记 `eslint-plugin-n` 为"可移除",但 ESLint 侧 `node.ts` 仍有以下规则未被 oxlint 接管: + +可参考 https://github.com/oxc-project/oxc/issues/481 查看 Tasks 列表,oxlint 正在逐步接管 ESLint 的规则。 + +| 规则 | 备注 | +| --- | --- | +| `n/handle-callback-err` | oxlint 未支持 | +| `n/no-deprecated-api` | oxlint 未支持 | +| `n/no-extraneous-import` | 有自定义 `allowModules`,oxlint 未支持 | +| `n/no-unsupported-features/es-syntax` | oxlint 未支持 | +| `n/prefer-global/buffer` | oxlint 未支持 | +| `n/prefer-global/process` | oxlint 未支持 | +| `n/process-exit-as-throw` | oxlint 未支持 | + +**结论**:`eslint-plugin-n` 当前仍需保留在 ESLint 侧,1.2 表格需修正。 + +#### [低] 考虑启用 `--type-aware` 规则 + +ESLint 侧 `typescript.ts` 已配置 `project: './tsconfig.*.json'`,即已使用 type-aware linting。oxlint 也支持 `--type-aware` 选项,启用后可迁移更多依赖类型信息的规则(如 `@typescript-eslint/no-floating-promises` 等),进一步减少对 ESLint TypeScript 插件的依赖。 + +```bash +# 测试 type-aware 模式 +oxlint --type-aware --tsconfig tsconfig.json +``` + +#### [低] `--replace-eslint-comments` 批量转换注释 + +代码中存在的 `// eslint-disable` 注释虽然 oxlint 兼容,但在第四阶段移除 ESLint 后会变得语义不清。官方迁移工具提供了批量转换选项: + +```bash +npx @oxlint/migrate --replace-eslint-comments +``` + +建议在第一阶段完成后或第四阶段开始前执行。 + +#### [低] `jsPlugins` 特性可减少 ESLint 依赖 + +oxlint 支持通过 `jsPlugins` 字段加载外部 ESLint 插件。对于 `eslint-plugin-pnpm`、`eslint-plugin-command` 等 oxlint 不内置的插件,未来可评估通过 `jsPlugins` 在 oxlint 中直接加载,从而减少对 ESLint 的依赖: + +```json +{ + "jsPlugins": ["eslint-plugin-pnpm"], + "rules": { + "pnpm/some-rule": "warn" + } +} +``` + +--- + +## 第二阶段:oxfmt 替换 Prettier + +**前提**:oxfmt 已发布,支持 JS/TS/Vue SFC 格式化,CSS/HTML/YAML/Markdown 等通过内部委托 Prettier 处理。 + +> 参考文档:[oxfmt CLI](https://oxc.rs/docs/guide/usage/formatter/cli.html) | [Config Reference](https://oxc.rs/docs/guide/usage/formatter/config-file-reference.html) + +### 2.1 当前 Prettier 引用全景 + +迁移前需清楚 Prettier 在仓库中的所有触点: + +| 位置 | 用途 | 迁移动作 | +| --- | --- | --- | +| `internal/lint-configs/prettier-config/` | `@vben/prettier-config` 包,导出共享配置 | **改写为 `@vben/oxfmt-config`** | +| `.prettierrc.mjs` | 根配置入口,`export { default } from '@vben/prettier-config'` | **替换为 `.oxfmtrc.json`** | +| `.prettierignore` | 格式化忽略文件 | **迁移到 `.oxfmtrc.json` 的 `ignorePatterns`** | +| `eslint-config/src/configs/prettier.ts` | ESLint 中 `eslint-plugin-prettier` 集成 | **移除整个配置文件** | +| `eslint-config/src/index.ts` | 引用 `prettier()` 配置 | **移除 `prettier()` 调用** | +| `eslint-config/package.json` | 依赖 `eslint-plugin-prettier` | **移除该依赖** | +| `stylelint-config/index.mjs` | 插件 `stylelint-prettier`,规则 `prettier/prettier: true` | **移除 `stylelint-prettier` 插件和规则** | +| `stylelint-config/package.json` | 依赖 `prettier`、`stylelint-prettier` | **移除这两个依赖** | +| `internal/node-utils/src/prettier.ts` | `prettierFormat()` 函数,供构建工具调用 | **改写为调用 oxfmt** | +| `internal/node-utils/package.json` | 依赖 `prettier` | **替换为 `oxfmt`** | +| `scripts/vsh/src/lint/index.ts` | `prettier . --write --cache` / `prettier . --check --cache` | **替换为 `oxfmt` / `oxfmt --check`** | +| `lefthook.yml` | 多处 `pnpm prettier --write/--cache` | **替换为 `pnpm oxfmt`** | +| `pnpm-workspace.yaml` catalog | `prettier: ^3.8.1`、`eslint-plugin-prettier`、`stylelint-prettier` | **移除这三项** | +| `package.json` 根 | 依赖 `@vben/prettier-config` | **替换为 `@vben/oxfmt-config`** | +| `vben-admin.code-workspace` | workspace 引用 `prettier-config` | **更新** | + +### 2.2 自动迁移配置 + +oxfmt 内置了从 Prettier 迁移的能力: + +```bash +npx oxfmt@latest --migrate prettier +``` + +该命令会: + +- 读取当前 `.prettierrc.mjs`(通过 `@vben/prettier-config` 解析) +- 生成 `.oxfmtrc.json` +- 将 `.prettierignore` 的模式迁移到 `ignorePatterns` +- 如果检测到 `prettier-plugin-tailwindcss` 会自动启用 `sortTailwindcss` +- 如果检测到 `prettier-plugin-packagejson` 会启用 `sortPackageJson` + +> **注意**:如果 `.oxfmtrc.json` 已存在会报错,需先删除再重新运行。 + +### 2.3 配置对照与预期输出 + +当前 Prettier 配置(`@vben/prettier-config`): + +```js +{ + endOfLine: 'auto', + printWidth: 80, + proseWrap: 'never', + semi: true, + singleQuote: true, + trailingComma: 'all', + overrides: [ + { files: ['*.json5'], options: { quoteProps: 'preserve', singleQuote: false } } + ] +} +``` + +预期 oxfmt 配置(`.oxfmtrc.json`): + +```json +{ + "$schema": "./node_modules/oxfmt/configuration_schema.json", + "printWidth": 80, + "semi": true, + "singleQuote": true, + "trailingComma": "all", + "proseWrap": "never", + "endOfLine": "lf", + "ignorePatterns": [ + "dist", + "dev-dist", + ".local", + ".claude", + ".agent", + ".agents", + ".codex", + ".output.js", + "node_modules", + ".nvmrc", + "coverage", + "CODEOWNERS", + ".nitro", + ".output", + "**/*.svg", + "**/*.sh", + "public", + ".npmrc", + "*-lock.yaml", + "skills-lock.json" + ], + "overrides": [ + { + "files": ["*.json5"], + "options": { "quoteProps": "preserve", "singleQuote": false } + } + ] +} +``` + +关键差异说明: + +| Prettier 选项 | oxfmt 处理 | +| --- | --- | +| `endOfLine: 'auto'` | **不支持 `auto`**,需显式设为 `"lf"`(与 `.editorconfig` 的 `end_of_line=lf` 一致) | +| `printWidth: 80` | 直接映射(oxfmt 默认 100,需显式设 80 保持一致) | +| `overrides` | **自动迁移不支持 `overrides`**,需手动编写 | + +### 2.4 `@vben/oxfmt-config` workspace 包 + +保持与 `@vben/prettier-config` 一致的 workspace 包组织模式: + +```text +internal/lint-configs/oxfmt-config/ + package.json + index.mjs # 导出 oxfmt 配置对象(供程序化调用) +``` + +```json +// internal/lint-configs/oxfmt-config/package.json +{ + "name": "@vben/oxfmt-config", + "version": "5.6.0", + "private": true, + "type": "module", + "main": "./index.mjs", + "module": "./index.mjs", + "exports": { + ".": { "default": "./index.mjs" } + }, + "dependencies": { + "oxfmt": "catalog:" + } +} +``` + +> **注意**:oxfmt 的配置文件 `.oxfmtrc.json` 不像 Prettier 那样支持 JS 配置的 `export default`。 `@vben/oxfmt-config` 包的作用是: +> +> 1. 集中管理 `oxfmt` 版本依赖 +> 2. 导出配置对象供 `internal/node-utils` 等程序化场景使用 +> 3. 根目录直接使用 `.oxfmtrc.json` 文件(oxfmt 会自动读取) + +### 2.5 替换范围详表 + +| 当前依赖 | 替代方案 | +| --- | --- | +| `prettier` ^3.8.1 | `oxfmt`(JS/TS 原生格式化,CSS/HTML/YAML/MD 内部委托 Prettier) | +| `eslint-plugin-prettier` | 移除(不再需要 ESLint-Prettier 集成) | +| `stylelint-prettier` | 移除(Stylelint 不再需要 Prettier 集成) | +| `@vben/prettier-config` | 改写为 `@vben/oxfmt-config` | + +### 2.6 操作步骤 + +#### 第一步:生成 oxfmt 配置 + +```bash +# 自动迁移(生成 .oxfmtrc.json) +npx oxfmt@latest --migrate prettier + +# 手动补充 overrides(自动迁移不支持) +# 手动将 endOfLine: 'auto' 改为 "lf" +# 手动添加 ignorePatterns(从 .prettierignore 迁移) +``` + +#### 第二步:创建 `@vben/oxfmt-config` 包 + +```bash +mkdir -p internal/lint-configs/oxfmt-config +# 创建 package.json 和 index.mjs +``` + +#### 第三步:全量格式化并对比 + +```bash +# 先用 prettier 格式化一遍作为基线 +pnpm prettier . --write + +# 再用 oxfmt 格式化 +npx oxfmt@latest + +# 查看差异 +git diff --stat +``` + +检查差异是否可接受。已知的可能差异: + +- oxfmt 的 `sortPackageJson` 排序算法可能与 Prettier 不同 +- 嵌入语言格式化(CSS-in-JS)可能有细微差异 + +#### 第四步:更新 `scripts/vsh/src/lint/index.ts` + +```ts +// Before +await execaCommand(`prettier . --write --cache --log-level warn`, { + stdio: 'inherit', +}); +await execaCommand(`prettier . --ignore-unknown --check --cache`, { + stdio: 'inherit', +}); + +// After +await execaCommand(`oxfmt`, { stdio: 'inherit' }); +await execaCommand(`oxfmt --check`, { stdio: 'inherit' }); +``` + +#### 第五步:更新 `lefthook.yml` + +```yaml +pre-commit: + parallel: true + commands: + lint-md: + run: pnpm oxfmt {staged_files} + glob: '*.md' + lint-vue: + run: pnpm oxfmt {staged_files} && pnpm oxlint --fix {staged_files} && pnpm eslint --cache --fix {staged_files} && pnpm stylelint --fix --allow-empty-input {staged_files} + glob: '*.vue' + lint-js: + run: pnpm oxfmt {staged_files} && pnpm oxlint --fix {staged_files} && pnpm eslint --cache --fix {staged_files} + glob: '*.{js,jsx,ts,tsx}' + lint-style: + run: pnpm oxfmt {staged_files} && pnpm stylelint --fix --allow-empty-input {staged_files} + glob: '*.{scss,less,styl,html,vue,css}' + lint-package: + run: pnpm oxfmt {staged_files} + glob: 'package.json' + lint-json: + run: pnpm oxfmt {staged_files} + glob: '{!(package)*.json,*.code-snippets,.!(browserslist)*rc}' +``` + +#### 第六步:清理 ESLint 中的 Prettier 集成 + +1. 删除 `eslint-config/src/configs/prettier.ts` +2. 从 `eslint-config/src/configs/index.ts` 移除 `export * from './prettier'` +3. 从 `eslint-config/src/index.ts` 移除 `prettier()` 调用 +4. 从 `eslint-config/package.json` 移除 `eslint-plugin-prettier` 依赖 + +#### 第七步:清理 Stylelint 中的 Prettier 集成 + +1. 从 `stylelint-config/index.mjs` 的 `plugins` 数组移除 `'stylelint-prettier'` +2. 从 `stylelint-config/index.mjs` 的 `rules` 移除 `'prettier/prettier': true` +3. 从 `stylelint-config/package.json` 移除 `prettier` 和 `stylelint-prettier` 依赖 + +#### 第八步:更新 `internal/node-utils/src/prettier.ts` + +```ts +// 重命名为 formatter.ts 或保留文件名 +import { execaCommand } from 'execa'; + +async function formatFile(filepath: string) { + await execaCommand(`oxfmt ${filepath}`, { stdio: 'inherit' }); +} + +export { formatFile }; +``` + +或者如果 oxfmt 提供了 Node API,则使用程序化调用。 + +#### 第九步:移除旧依赖 + +```bash +# 移除根目录文件 +rm .prettierrc.mjs +rm .prettierignore + +# 从 pnpm-workspace.yaml catalog 移除 +# - prettier: ^3.8.1 +# - eslint-plugin-prettier: ^5.5.5 +# - stylelint-prettier: ^5.0.3 + +# 移除 @vben/prettier-config 包 +rm -rf internal/lint-configs/prettier-config + +# 更新 package.json 根,移除 @vben/prettier-config 依赖 +# 更新 vben-admin.code-workspace,移除 prettier-config workspace 引用 +``` + +#### 第十步:全量验证 + +```bash +pnpm install +pnpm run lint +pnpm run lint:oxc +pnpm run lint:eslint +``` + +### 2.7 oxfmt 额外能力评估 + +oxfmt 提供了 Prettier 没有的扩展特性,可选启用: + +| 特性 | 说明 | 建议 | +| --- | --- | --- | +| `sortImports` | import 语句排序,灵感来自 `eslint-plugin-perfectionist/sort-imports` | 评估是否可替代 ESLint 侧 perfectionist 的 import 排序 | +| `sortTailwindcss` | 替代 `prettier-plugin-tailwindcss` | 当前项目已通过 oxlint `jsPlugins` 处理,需评估是否切换 | +| `sortPackageJson` | 自动排序 `package.json` 字段 | 默认启用,排序算法与 prettier-plugin-packagejson 不同 | +| `insertFinalNewline` | 文件末尾添加换行 | 默认启用,与 `.editorconfig` 的 `insert_final_newline=true` 一致 | + +### 2.8 文件类型覆盖说明 + +oxfmt 对不同文件类型的处理方式: + +| 文件类型 | 格式化方式 | +| ------------------------- | --------------------------------------------- | +| JS / TS / JSX / TSX | oxfmt 原生格式化 | +| Vue SFC | oxfmt 原生格式化(`