diff --git a/internal/lint-configs/eslint-config/src/configs/disableds.ts b/internal/lint-configs/eslint-config/src/configs/disableds.ts deleted file mode 100644 index 5a0ca7d36..000000000 --- a/internal/lint-configs/eslint-config/src/configs/disableds.ts +++ /dev/null @@ -1,13 +0,0 @@ -import type { Linter } from 'eslint'; - -export async function disableds(): Promise { - return [ - { - files: ['**/*.js', '**/*.mjs', '**/*.cjs'], - name: 'disables/js', - rules: { - '@typescript-eslint/explicit-module-boundary-types': 'off', - }, - }, - ]; -} diff --git a/internal/lint-configs/eslint-config/src/configs/index.ts b/internal/lint-configs/eslint-config/src/configs/index.ts index 00ca56690..04de7a011 100644 --- a/internal/lint-configs/eslint-config/src/configs/index.ts +++ b/internal/lint-configs/eslint-config/src/configs/index.ts @@ -1,6 +1,5 @@ export * from './command'; export * from './comments'; -export * from './disableds'; export * from './ignores'; export * from './javascript'; export * from './jsonc'; diff --git a/internal/lint-configs/eslint-config/src/configs/javascript.ts b/internal/lint-configs/eslint-config/src/configs/javascript.ts index a083d78c0..2019ecdd2 100644 --- a/internal/lint-configs/eslint-config/src/configs/javascript.ts +++ b/internal/lint-configs/eslint-config/src/configs/javascript.ts @@ -4,7 +4,73 @@ import js from '@eslint/js'; import pluginUnusedImports from 'eslint-plugin-unused-imports'; import globals from 'globals'; +const rulesCoveredByOxlint = new Set([ + 'constructor-super', + 'for-direction', + 'getter-return', + 'no-async-promise-executor', + 'no-case-declarations', + 'no-class-assign', + 'no-compare-neg-zero', + 'no-cond-assign', + 'no-const-assign', + 'no-constant-binary-expression', + 'no-constant-condition', + 'no-debugger', + 'no-delete-var', + 'no-dupe-args', + 'no-dupe-class-members', + 'no-dupe-else-if', + 'no-dupe-keys', + 'no-duplicate-case', + 'no-empty', + 'no-empty-character-class', + 'no-empty-pattern', + 'no-empty-static-block', + 'no-ex-assign', + 'no-extra-boolean-cast', + 'no-fallthrough', + 'no-func-assign', + 'no-global-assign', + 'no-import-assign', + 'no-invalid-regexp', + 'no-irregular-whitespace', + 'no-loss-of-precision', + 'no-misleading-character-class', + 'no-new-native-nonconstructor', + 'no-nonoctal-decimal-escape', + 'no-obj-calls', + 'no-prototype-builtins', + 'no-redeclare', + 'no-regex-spaces', + 'no-self-assign', + 'no-setter-return', + 'no-shadow-restricted-names', + 'no-sparse-arrays', + 'no-this-before-super', + 'no-unreachable', + 'no-unsafe-finally', + 'no-unsafe-negation', + 'no-unsafe-optional-chaining', + 'no-unused-labels', + 'no-unused-private-class-members', + 'no-unused-vars', + 'no-useless-backreference', + 'no-useless-catch', + 'no-useless-escape', + 'no-with', + 'require-yield', + 'use-isnan', + 'valid-typeof', +]); + export async function javascript(): Promise { + const recommendedRules = Object.fromEntries( + Object.entries(js.configs.recommended.rules).filter( + ([ruleName]) => !rulesCoveredByOxlint.has(ruleName), + ), + ); + return [ { languageOptions: { @@ -33,11 +99,10 @@ export async function javascript(): Promise { 'unused-imports': pluginUnusedImports, }, rules: { - ...js.configs.recommended.rules, + ...recommendedRules, 'dot-notation': ['error', { allowKeywords: true }], 'keyword-spacing': 'off', 'no-control-regex': 'error', - 'no-dupe-args': 'error', 'no-empty-function': 'off', 'no-restricted-properties': [ 'error', @@ -73,15 +138,6 @@ export async function javascript(): Promise { ], 'no-undef': 'off', 'no-unreachable-loop': 'error', - 'no-unused-vars': [ - 'error', - { - args: 'none', - caughtErrors: 'none', - ignoreRestSiblings: true, - vars: 'all', - }, - ], 'space-before-function-paren': 'off', 'unused-imports/no-unused-imports': 'error', diff --git a/internal/lint-configs/eslint-config/src/configs/typescript.ts b/internal/lint-configs/eslint-config/src/configs/typescript.ts index fca509ac7..5e4157cf2 100644 --- a/internal/lint-configs/eslint-config/src/configs/typescript.ts +++ b/internal/lint-configs/eslint-config/src/configs/typescript.ts @@ -2,11 +2,24 @@ import type { Linter } from 'eslint'; import { interopDefault } from '../util'; +const rulesCoveredByOxlint = new Set([ + '@typescript-eslint/ban-ts-comment', + '@typescript-eslint/no-non-null-assertion', + '@typescript-eslint/no-unused-expressions', + '@typescript-eslint/no-unused-vars', + '@typescript-eslint/triple-slash-reference', +]); + export async function typescript(): Promise { const [pluginTs, parserTs] = await Promise.all([ interopDefault(import('@typescript-eslint/eslint-plugin')), interopDefault(import('@typescript-eslint/parser')), ] as const); + const strictRules = Object.fromEntries( + Object.entries(pluginTs.configs.strict?.rules ?? {}).filter( + ([ruleName]) => !rulesCoveredByOxlint.has(ruleName), + ), + ); return [ { @@ -30,7 +43,7 @@ export async function typescript(): Promise { }, rules: { ...pluginTs.configs['eslint-recommended']?.overrides?.[0]?.rules, - ...pluginTs.configs.strict?.rules, + ...strictRules, // '@typescript-eslint/consistent-type-definitions': ['warn', 'interface'], '@typescript-eslint/consistent-type-definitions': 'off', '@typescript-eslint/explicit-function-return-type': 'off', diff --git a/internal/lint-configs/eslint-config/src/configs/unicorn.ts b/internal/lint-configs/eslint-config/src/configs/unicorn.ts index 56c9ddf40..6e779e3bf 100644 --- a/internal/lint-configs/eslint-config/src/configs/unicorn.ts +++ b/internal/lint-configs/eslint-config/src/configs/unicorn.ts @@ -2,8 +2,20 @@ import type { Linter } from 'eslint'; import { interopDefault } from '../util'; +const rulesCoveredByOxlint = new Set([ + 'unicorn/consistent-function-scoping', + 'unicorn/no-process-exit', + 'unicorn/prefer-global-this', + 'unicorn/prefer-module', +]); + export async function unicorn(): Promise { const pluginUnicorn = await interopDefault(import('eslint-plugin-unicorn')); + const recommendedRules = Object.fromEntries( + Object.entries(pluginUnicorn.configs.recommended.rules ?? {}).filter( + ([ruleName]) => !rulesCoveredByOxlint.has(ruleName), + ), + ); return [ { @@ -11,11 +23,10 @@ export async function unicorn(): Promise { unicorn: pluginUnicorn, }, rules: { - ...pluginUnicorn.configs.recommended.rules, + ...recommendedRules, 'unicorn/better-regex': 'off', 'unicorn/consistent-destructuring': 'off', - 'unicorn/consistent-function-scoping': 'off', 'unicorn/expiring-todo-comments': 'off', 'unicorn/filename-case': 'off', 'unicorn/import-style': 'off', @@ -25,19 +36,9 @@ export async function unicorn(): Promise { 'unicorn/prefer-at': 'off', 'unicorn/prefer-dom-node-text-content': 'off', 'unicorn/prefer-export-from': ['error', { ignoreUsedVariables: true }], - 'unicorn/prefer-global-this': 'off', 'unicorn/prefer-top-level-await': 'off', 'unicorn/prevent-abbreviations': 'off', }, }, - { - files: [ - 'scripts/**/*.?([cm])[jt]s?(x)', - 'internal/**/*.?([cm])[jt]s?(x)', - ], - rules: { - 'unicorn/no-process-exit': 'off', - }, - }, ]; } diff --git a/internal/lint-configs/eslint-config/src/configs/vue.ts b/internal/lint-configs/eslint-config/src/configs/vue.ts index 5db72992d..edd576888 100644 --- a/internal/lint-configs/eslint-config/src/configs/vue.ts +++ b/internal/lint-configs/eslint-config/src/configs/vue.ts @@ -128,7 +128,6 @@ export async function vue(): Promise { }, ], 'vue/one-component-per-file': 'error', - 'vue/prefer-import-from-vue': 'error', 'vue/prefer-separate-static-class': 'error', 'vue/prefer-template': 'error', 'vue/prop-name-casing': ['error', 'camelCase'], diff --git a/internal/lint-configs/eslint-config/src/custom-config.ts b/internal/lint-configs/eslint-config/src/custom-config.ts index 57413f9ae..864e74916 100644 --- a/internal/lint-configs/eslint-config/src/custom-config.ts +++ b/internal/lint-configs/eslint-config/src/custom-config.ts @@ -21,14 +21,6 @@ const customConfig: Linter.Config[] = [ ignores: restrictedImportIgnores, rules: { 'perfectionist/sort-interfaces': 'off', - 'perfectionist/sort-objects': 'off', - }, - }, - { - files: ['**/**.vue'], - ignores: restrictedImportIgnores, - rules: { - 'perfectionist/sort-objects': 'off', }, }, { @@ -63,7 +55,6 @@ const customConfig: Linter.Config[] = [ ], }, ], - 'perfectionist/sort-interfaces': 'off', }, }, { @@ -137,9 +128,7 @@ const customConfig: Linter.Config[] = [ { files: ['apps/backend-mock/**/**', 'docs/**/**'], rules: { - '@typescript-eslint/no-extraneous-class': 'off', 'no-console': 'off', - 'unicorn/prefer-module': 'off', }, }, { diff --git a/internal/lint-configs/eslint-config/src/index.ts b/internal/lint-configs/eslint-config/src/index.ts index e0658df1f..dfda5f26b 100644 --- a/internal/lint-configs/eslint-config/src/index.ts +++ b/internal/lint-configs/eslint-config/src/index.ts @@ -3,7 +3,6 @@ import type { Linter } from 'eslint'; import { command, comments, - disableds, ignores, javascript, jsonc, @@ -34,7 +33,6 @@ async function defineConfig(config: FlatConfig[] = []) { ignores(), typescript(), jsonc(), - disableds(), node(), perfectionist(), comments(), diff --git a/packages/effects/common-ui/src/components/captcha/slider-translate-captcha/index.vue b/packages/effects/common-ui/src/components/captcha/slider-translate-captcha/index.vue index 5fcacd743..02f81ae05 100644 --- a/packages/effects/common-ui/src/components/captcha/slider-translate-captcha/index.vue +++ b/packages/effects/common-ui/src/components/captcha/slider-translate-captcha/index.vue @@ -35,12 +35,12 @@ const emit = defineEmits<{ }>(); const PI: number = Math.PI; -enum CanvasOpr { - // eslint-disable-next-line no-unused-vars - Clip = 'clip', - // eslint-disable-next-line no-unused-vars - Fill = 'fill', -} +const canvasOpr = { + clip: 'clip', + fill: 'fill', +} as const; + +type CanvasOpr = (typeof canvasOpr)[keyof typeof canvasOpr]; const modalValue = defineModel({ default: false }); @@ -189,8 +189,8 @@ function draw(ctx1: CanvasRenderingContext2D, ctx2: CanvasRenderingContext2D) { 3 * circleRadius, canvasHeight - (squareLength + 2 * circleRadius), ); - drawPiece(ctx1, state.pieceX, state.pieceY, CanvasOpr.Fill); - drawPiece(ctx2, state.pieceX, state.pieceY, CanvasOpr.Clip); + drawPiece(ctx1, state.pieceX, state.pieceY, canvasOpr.fill); + drawPiece(ctx2, state.pieceX, state.pieceY, canvasOpr.clip); } // 绘制拼图切块 @@ -233,7 +233,8 @@ function drawPiece( ctx.fillStyle = 'rgba(255, 255, 255, 0.7)'; ctx.strokeStyle = 'rgba(255, 255, 255, 0.7)'; ctx.stroke(); - opr === CanvasOpr.Clip ? ctx.clip() : ctx.fill(); + // oxlint-disable-next-line no-unused-expressions + opr === canvasOpr.clip ? ctx.clip() : ctx.fill(); ctx.globalCompositeOperation = 'destination-over'; } diff --git a/packages/effects/common-ui/src/components/resize/resize.vue b/packages/effects/common-ui/src/components/resize/resize.vue index a62ff042c..18b1cf635 100644 --- a/packages/effects/common-ui/src/components/resize/resize.vue +++ b/packages/effects/common-ui/src/components/resize/resize.vue @@ -997,16 +997,16 @@ const bodyDown = (ev: MouseEvent & TouchEvent) => { ev.preventDefault(); } - if (isDraggable.value) { - bodyDrag.value = true; - } - const pointerPosition = getPointerPosition(ev); if (!pointerPosition) { return; } + if (isDraggable.value) { + bodyDrag.value = true; + } + saveDimensionsBeforeMove(pointerPosition); if (parentLimitation.value) { diff --git a/up-oxc.md b/up-oxc.md index cd90ccb2f..7a26f222d 100644 --- a/up-oxc.md +++ b/up-oxc.md @@ -1,23 +1,25 @@ # OXC 迁移计划 > 本文档记录将项目中可替代的工具链逐步迁移到 [oxc](https://oxc.rs/) 生态的计划。 +> +> 说明:本文档已按当前仓库状态校准。第一阶段已完成大部分工作,第二阶段的 formatter 迁移也已大部分落地。 ## 当前现状 -项目 ESLint 体系依赖约 20+ 个包,包括: +当前仓库的 lint / format 体系已经从“纯 ESLint + Prettier”演进为 “oxlint + ESLint 共存,oxfmt 替换 Prettier”。 + +当前与 lint / format 直接相关的核心依赖包括: - `eslint` ^10.0.3 +- `oxlint` ^1.55.0 +- `oxfmt` ^0.40.0 +- `eslint-plugin-oxlint` ^1.55.0 - `@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 @@ -25,19 +27,28 @@ - `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 +以下依赖已不再存在于当前仓库的 lint / format 链路中: + +- `eslint-plugin-import-x` +- `eslint-plugin-regexp` +- `eslint-plugin-jsdoc` +- `eslint-plugin-no-only-tests` +- `@vitest/eslint-plugin` +- `eslint-plugin-prettier` +- `stylelint-prettier` +- `prettier` + --- ## 第一阶段:引入 oxlint,与 ESLint 共存 -**目标**:将通用 JS/TS lint 规则交给 oxlint,ESLint 仅保留 oxlint 无法覆盖的特殊规则,大幅减少依赖数量并提升 lint 速度。 +**目标**:将通用 JS/TS lint 规则交给 oxlint,ESLint 仅保留 oxlint 无法覆盖的特殊规则,减少依赖数量并提升 lint 速度。 ### 1.1 安装 oxlint @@ -45,42 +56,46 @@ pnpm add -Dw oxlint ``` -### 1.2 可移除的 ESLint 插件 +### 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` 运行 | +| `eslint-plugin-no-only-tests` | 已移除 | 测试规则已迁到 oxlint / vitest 分类 | +| `eslint-plugin-import-x` | 已移除 | `import` 规则已集中迁入 `@vben/oxlint-config` | +| `@vitest/eslint-plugin` | 已移除 | Vitest 规则已迁入 `@vben/oxlint-config` | +| `eslint-plugin-prettier` | 已移除 | formatter 已切换到 oxfmt | +| `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 插件 +### 1.3 当前仍需保留在 ESLint 侧的插件 / 能力 -以下插件 oxlint 目前不支持或不完全支持,需继续由 ESLint 处理: +以下插件或能力当前仍需继续由 ESLint 处理: -| 插件 | 原因 | +| 插件 / 能力 | 原因 | | --- | --- | -| `eslint-plugin-vue` + `vue-eslint-parser` | Vue SFC 模板 lint,oxlint Vue 支持尚未完善 | -| `eslint-plugin-jsonc` + `eslint-plugin-yml` | JSON/YAML 文件 lint,oxlint 不支持 | +| `eslint-plugin-vue` + `vue-eslint-parser` | Vue SFC template lint,oxlint Vue 支持尚未完善 | +| `@typescript-eslint/eslint-plugin` + `@typescript-eslint/parser` | 仍有一批 TS strict / type-aware 规则保留在 ESLint 侧 | +| `eslint-plugin-unused-imports` | `no-unused-imports` 仍由 ESLint 负责 | +| `eslint-plugin-n` | 多条 Node 规则尚无等价承接 | +| `eslint-plugin-perfectionist` | 当前仍负责排序策略 | +| `eslint-plugin-jsonc` + `eslint-plugin-yml` + `yaml-eslint-parser` | JSON / YAML 文件 lint 仍在 ESLint 侧 | | `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 后可移除) | +| `@eslint-community/eslint-plugin-eslint-comments` | 当前尚未迁移 | ### 1.4 配置 oxlint -当前实现已调整为与 ESLint 一致的 workspace 包组织方式: +当前实现已经调整为与 ESLint 一致的 workspace 包组织方式: - 根目录使用 [oxlint.config.ts](./oxlint.config.ts) 作为薄入口 - 实际规则配置位于 `internal/lint-configs/oxlint-config` @@ -112,30 +127,35 @@ export function defineConfig(config = {}) { ### 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` 均可通过 +截至当前仓库状态,第一阶段与其配套的 formatter 迁移已完成以下工作: + +1. 根 `lint` 已统一委托给 `vsh lint` +2. 保留 `lint:oxc`、`lint:oxc:type-aware`、`lint:eslint`、`lint:style` 作为独立入口 +3. `lefthook` 已接入 `oxfmt`、`oxlint --fix`、`eslint --cache --fix`、`stylelint --fix` +4. 已新增 `@vben/oxlint-config`,并将 oxlint 规则集中到 `internal/lint-configs/oxlint-config` +5. 已新增 `@vben/oxfmt-config`,并使用根级 `oxfmt.config.ts` 作为格式化配置入口 +6. `eslint-plugin-oxlint` 已接入 `@vben/eslint-config` +7. 已移除 `eslint-plugin-jsdoc`、`eslint-plugin-regexp`、`eslint-plugin-no-only-tests` +8. `import` 与 `vitest` 规则已迁入 oxlint,相关 ESLint 插件已移除 +9. `better-tailwindcss` 已迁移到 `@vben/oxlint-config` 的 `jsPlugins` 方案 +10. `unicorn` 插件已在 oxlint 侧显式启用,并补齐仓库内所需的 override +11. Prettier 相关根配置与 ESLint / Stylelint 集成已移除 +12. `internal/node-utils` 已改为调用 `oxfmt` ### 1.6 当前 oxlint 基线策略 当前 `@vben/oxlint-config` 采用“先收敛噪音,再逐步接管规则”的策略: -- 先启用 `correctness` 和 `suspicious` -- 保留 `import`、`node`、`typescript`、`unicorn`、`vitest`、`vue` 插件 +- 启用 `correctness` 和 `suspicious` +- 显式启用 `import`、`node`、`oxc`、`typescript`、`unicorn`、`vitest`、`vue` 插件 - 通过 `jsPlugins` 在 oxlint 侧承接 `better-tailwindcss` - 对当前仓库内高噪音且不准备立即批量整改的规则先显式关闭 -- 通过 `overrides` 为 `.d.ts` 等特殊文件保留例外配置 +- 通过 `overrides` 为 `.d.ts`、测试文件、脚本目录等特殊场景保留例外配置 +- `overrides.files` 已避免继续使用 ESLint 风格的 extglob,改为显式文件后缀列表 -### 1.7 已确认可迁移到 oxlint 的规则 +### 1.7 当前已迁入 oxlint 的规则 -以下规则已确认可由当前版本 `oxlint` 接管,后续应从 `@vben/eslint-config` 逐步迁移到 `@vben/oxlint-config`: +以下规则已在当前仓库的 `@vben/oxlint-config` 中落地: #### JavaScript / core @@ -144,17 +164,14 @@ export function defineConfig(config = {}) { - `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` +- 以及一批额外的 core 规则,例如 `no-console`、`no-template-curly-in-string`、`prefer-template`、`object-shorthand` #### import @@ -164,43 +181,42 @@ export function defineConfig(config = {}) { - `import/no-mutable-exports` - `import/no-named-default` - `import/no-self-import` +- `import/no-webpack-loader-syntax` #### 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/ban-ts-comment` +- `typescript/no-non-null-assertion` +- `typescript/no-var-requires` +- `typescript/triple-slash-reference` -> `@typescript-eslint/ban-ts-comment` 虽然 oxlint 已支持,但当前仓库中存在多处 `@ts-ignore`,直接迁移会新增报错,暂不建议在这一阶段接管。 +> 当前仓库已开始为 type-aware 规则预留位置,但 `no-floating-promises`、`await-thenable` 等高噪音规则仍显式关闭,等待后续逐步接管。 #### Vitest +- `vitest/consistent-test-it` - `vitest/no-focused-tests` - `vitest/no-identical-title` +- `vitest/no-import-node-test` - `vitest/prefer-hooks-in-order` - `vitest/prefer-lowercase-title` #### Node -- `n/no-exports-assign` -- `n/no-new-require` -- `n/no-path-concat` +- `node/no-exports-assign` +- `node/no-new-require` +- `node/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` + +> `better-tailwindcss/enforce-consistent-line-wrapping` 与 `better-tailwindcss/no-unknown-classes` 当前仍因噪音 / 与 formatter 冲突而关闭。 #### Vue @@ -210,14 +226,15 @@ export function defineConfig(config = {}) { 以下内容当前不建议从 ESLint 侧移除: -1. Vue SFC/template 主体规则 +1. Vue SFC / template 主体规则 2. `perfectionist` 3. `jsonc` / `yml` 4. `pnpm` 5. `command` -6. `prettier` -7. `eslint-comments` -8. `unused-imports` +6. `eslint-comments` +7. `unused-imports` +8. `node.ts` 中尚未被 oxlint 覆盖的规则 +9. `typescript.ts` 中暂不适合直接迁移的 strict / type-aware 规则 其中 Vue 相关规则尤其需要保留,至少包括: @@ -236,15 +253,14 @@ export function defineConfig(config = {}) { ### 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` 中的个别确认项 +1. 验证 `oxcCompat()` 是否正确关闭 ESLint 重复规则 +2. 继续清理 `eslint-config/src/configs/javascript.ts` 中已被 oxlint 明确覆盖的低风险规则 +3. 继续从 `eslint-config/src/configs/typescript.ts` 向 `@vben/oxlint-config` 下沉规则,并结合 `lint:oxc:type-aware` 逐步启用 type-aware 规则 +4. 评估 `eslint-config/src/configs/unicorn.ts` 中剩余规则是否值得继续保留 +5. 保留并持续观察 `eslint-config/src/configs/node.ts`、`vue.ts`、`perfectionist.ts`、`jsonc.ts`、`yaml.ts`、`pnpm.ts`、`command.ts` +6. 最后再评估是否要重新启用 tailwind 的 wrapping / unknown classes 规则 每迁移一组后都执行: @@ -254,67 +270,75 @@ pnpm run lint:eslint pnpm run lint ``` -### 1.10 更新 scripts +### 1.10 当前 scripts ```jsonc // package.json { "scripts": { - "lint": "pnpm run lint:oxc && pnpm run lint:eslint", + "format": "vsh lint --format", + "lint": "vsh lint", "lint:oxc": "oxlint .", - "lint:eslint": "vsh lint", + "lint:oxc:type-aware": "oxlint . --type-aware", + "lint:eslint": "eslint . --cache", + "lint:style": "stylelint \"**/*.{vue,css,less,scss}\" --cache", }, } ``` -### 1.11 更新 lefthook +### 1.11 当前 lefthook -在 pre-commit 中将 oxlint 加入暂存文件检查: +当前 `pre-commit` 已包含如下链路: ```yaml pre-commit: + parallel: true commands: + code-workspace: + run: pnpm vsh code-workspace --auto-commit + 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' - 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: + run: pnpm oxfmt {staged_files} && pnpm oxlint --fix {staged_files} && pnpm eslint --cache --fix {staged_files} 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} + 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}' ``` ### 1.12 验证步骤 -1. 安装 oxlint 后全量运行 `pnpm lint:oxc`,记录所有报告 +1. 全量运行 `pnpm run lint:oxc`,记录 oxlint 报告 2. 对比当前 ESLint 规则覆盖情况,调整 oxlint 配置使两者对齐 -3. 逐个移除可替代的 ESLint 插件,每次移除后运行完整 lint 验证无遗漏 +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,当前仓库已改为显式文件后缀列表 +- 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`](https://github.com/oxc-project/oxlint-migrate) 工具,可自动读取 ESLint flat config 生成对应 oxlint 配置。 对于当前仓库,`@oxlint/migrate` 更适合做“审计工具”,不适合直接生成或合并为最终配置,原因是: @@ -323,10 +347,9 @@ npx @oxlint/migrate --merge 3. Tailwind 规则依赖手写 `jsPlugins` 和 `settings` 4. 现有迁移流程还包含 ESLint 兼容层 `eslint-plugin-oxlint` -因此更合理的用法是: +更合理的用法是: ```bash -# 只做对账,不直接采纳输出 npx @oxlint/migrate --details ``` @@ -334,53 +357,43 @@ npx @oxlint/migrate --details #### [中] `mergeOxlintConfigs` 函数重复定义 -`mergeOxlintConfigs` 在两处有几乎相同的实现: - -- `oxlint-config/src/configs/index.ts` — 包含 `jsPlugins` 合并逻辑 -- `eslint-config/src/configs/oxlint.ts` — 缺少 `jsPlugins` 合并逻辑 - -这个问题已经修复。ESLint 侧的 `oxlint.ts` 已直接导入 `@vben/oxlint-config` 的 `mergeOxlintConfigs`,避免实现不一致: +这个问题已经修复。ESLint 侧的 `oxlint.ts` 已直接导入 `@vben/oxlint-config` 的 `mergeOxlintConfigs`,并在传入 `buildFromOxlintConfig()` 前剥离 `extends`: ```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)); + const { extends: _extends, ...config } = mergeOxlintConfigs(oxlintConfig); + + return oxlint.buildFromOxlintConfig( + config as Parameters[0], + ); } ``` #### [中] 验证 `oxcCompat()` 是否正确关闭 ESLint 重复规则 -ESLint 侧 `javascript.ts` 约有 **70+ 条规则**,很多已被 oxlint 的 `correctness`/`suspicious` category 覆盖(如 `no-debugger`、`no-dupe-keys`、`no-unreachable` 等),但 ESLint 侧并未手动关闭。 +ESLint 侧 `javascript.ts` 仍有较多规则,而其中很多已被 oxlint 的 `correctness` / `suspicious` category 覆盖。 -`eslint-plugin-oxlint` 的 `buildFromOxlintConfig()` 理论上会自动关闭 ESLint 中被 oxlint 覆盖的规则。需要验证 `oxcCompat()` 确实生效——如果正常工作,则 ESLint 侧无需手动清理这些规则。 +`eslint-plugin-oxlint` 的 `buildFromOxlintConfig()` 理论上会自动关闭 ESLint 中被 oxlint 覆盖的规则。仍需验证 `oxcCompat()` 确实生效。如果正常工作,则 ESLint 侧无需手动清理这批重复规则。 -**验证方法**: +建议验证方法: ```bash -# 临时在 oxlint 中 warn 一个 ESLint 也有的规则(如 no-debugger), -# 确认 ESLint 不再报告该规则 +# 临时在 oxlint 中启用一个 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,需检查是否已支持 | +#### [中] `eslint-plugin-import-x` 已从当前仓库移除 -如果这两条规则被 oxlint 支持,则 `eslint-plugin-import-x` 可从 ESLint 依赖中完全移除。 +`eslint-plugin-import-x` 相关迁移已经落地,当前仓库不再保留该依赖,`import` 规则集中定义在 `internal/lint-configs/oxlint-config/src/configs/import.ts`。 -#### [中] `eslint-plugin-n` 实际不能完全移除 +当前未再单独保留 `import/newline-after-import`。如未来仍需这类纯风格规则,应单独评估是否值得重新引入。 -1.2 表格中标记 `eslint-plugin-n` 为"可移除",但 ESLint 侧 `node.ts` 仍有以下规则未被 oxlint 接管: +#### [中] `eslint-plugin-n` 仍不能完全移除 -可参考 https://github.com/oxc-project/oxc/issues/481 查看 Tasks 列表,oxlint 正在逐步接管 ESLint 的规则。 +当前 ESLint 侧 `node.ts` 仍保留以下规则,尚未被 oxlint 接管: | 规则 | 备注 | | --- | --- | @@ -392,17 +405,25 @@ ESLint 侧 `import.ts` 仍保留 3 条规则: | `n/prefer-global/process` | oxlint 未支持 | | `n/process-exit-as-throw` | oxlint 未支持 | -**结论**:`eslint-plugin-n` 当前仍需保留在 ESLint 侧,1.2 表格需修正。 +**结论**:`eslint-plugin-n` 当前仍需保留在 ESLint 侧。 -#### [低] 考虑启用 `--type-aware` 规则 +#### [低] 已增加 `lint:oxc:type-aware` 入口,后续逐步启用规则 -ESLint 侧 `typescript.ts` 已配置 `project: './tsconfig.*.json'`,即已使用 type-aware linting。oxlint 也支持 `--type-aware` 选项,启用后可迁移更多依赖类型信息的规则(如 `@typescript-eslint/no-floating-promises` 等),进一步减少对 ESLint TypeScript 插件的依赖。 +当前根 `package.json` 已提供: ```bash -# 测试 type-aware 模式 -oxlint --type-aware --tsconfig tsconfig.json +pnpm run lint:oxc:type-aware ``` +当前 `@vben/oxlint-config` 已显式保留一批关闭状态的 type-aware 规则,例如: + +- `typescript/no-floating-promises` +- `typescript/await-thenable` +- `typescript/no-base-to-string` +- `typescript/no-unnecessary-type-assertion` + +后续建议基于实际报告量逐条开启,而不是一次性打开全部 type-aware 规则。 + #### [低] `--replace-eslint-comments` 批量转换注释 代码中存在的 `// eslint-disable` 注释虽然 oxlint 兼容,但在第四阶段移除 ESLint 后会变得语义不清。官方迁移工具提供了批量转换选项: @@ -413,9 +434,9 @@ npx @oxlint/migrate --replace-eslint-comments 建议在第一阶段完成后或第四阶段开始前执行。 -#### [低] `jsPlugins` 特性可减少 ESLint 依赖 +#### [低] `jsPlugins` 已在仓库中使用,后续可继续减少 ESLint 依赖 -oxlint 支持通过 `jsPlugins` 字段加载外部 ESLint 插件。对于 `eslint-plugin-pnpm`、`eslint-plugin-command` 等 oxlint 不内置的插件,未来可评估通过 `jsPlugins` 在 oxlint 中直接加载,从而减少对 ESLint 的依赖: +当前仓库已经通过 `jsPlugins` 在 oxlint 侧接入 `eslint-plugin-better-tailwindcss`。对于 `eslint-plugin-pnpm`、`eslint-plugin-command` 等 oxlint 不内置的插件,未来仍可评估采用同样方式进一步减少对 ESLint 的依赖: ```json { @@ -428,141 +449,114 @@ oxlint 支持通过 `jsPlugins` 字段加载外部 ESLint 插件。对于 `eslin --- -## 第二阶段:oxfmt 替换 Prettier +## 第二阶段: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 在仓库中的所有触点: +### 2.1 当前 formatter 触点全景 -| 位置 | 用途 | 迁移动作 | +| 位置 | 当前状态 | 说明 | | --- | --- | --- | -| `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 -``` - -该命令会: +| `internal/lint-configs/oxfmt-config/` | 已完成 | 新增 `@vben/oxfmt-config` workspace 包 | +| `oxfmt.config.ts` | 已完成 | 根级 formatter 配置入口,负责追加 `ignorePatterns` | +| `.prettierrc.mjs` | 已移除 | 不再作为根入口 | +| `.prettierignore` | 已移除 | 忽略模式已迁入 `oxfmt.config.ts` | +| `eslint-config/src/configs/prettier.ts` | 已移除 | ESLint 不再集成 Prettier | +| `eslint-config/src/index.ts` | 已完成 | 已无 `prettier()` 调用 | +| `eslint-config/package.json` | 已完成 | 已移除 `eslint-plugin-prettier` 依赖 | +| `stylelint-config/index.mjs` | 已完成 | 已移除 `stylelint-prettier` 插件和规则 | +| `stylelint-config/package.json` | 已完成 | 已移除 `prettier` / `stylelint-prettier` 依赖 | +| `internal/node-utils/src/formatter.ts` | 已完成 | 格式化工具已改为调用 `oxfmt` | +| `scripts/vsh/src/lint/index.ts` | 已完成 | 已改为调用 `oxfmt` | +| `lefthook.yml` | 已完成 | 已统一切换为 `pnpm oxfmt` | +| `pnpm-workspace.yaml` catalog | 已完成 | 已移除 `prettier`、`eslint-plugin-prettier`、`stylelint-prettier`,并新增 `oxfmt` | +| `package.json` 根 | 已完成 | 已依赖 `@vben/oxfmt-config` 与 `oxfmt` | +| `vben-admin.code-workspace` | 已完成 | 已更新 workspace 引用 | + +### 2.2 当前实际配置 + +当前仓库并未使用 `.oxfmtrc.json`,而是采用 “共享包 + 根入口” 的组织方式。 + +共享配置位于 `internal/lint-configs/oxfmt-config/src/index.ts`: -- 读取当前 `.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`): +```ts +import { defineConfig as defineOxfmtConfig } from 'oxfmt'; -```js -{ - endOfLine: 'auto', +const oxfmtConfig = defineOxfmtConfig({ printWidth: 80, proseWrap: 'never', semi: true, singleQuote: true, + sortPackageJson: false, trailingComma: 'all', - overrides: [ - { files: ['*.json5'], options: { quoteProps: 'preserve', singleQuote: false } } - ] -} +}); ``` -预期 oxfmt 配置(`.oxfmtrc.json`): +根级入口位于 `oxfmt.config.ts`: -```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" +```ts +import { defineConfig } from '@vben/oxfmt-config'; + +export default defineConfig({ + 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`**,需手动编写 | +1. 当前 formatter 的 `printWidth` 明确设为 `80` +2. `.editorconfig` 的 `max_line_length` 仍为 `100` -### 2.4 `@vben/oxfmt-config` workspace 包 +也就是说,当前仓库实际上采用的是“oxfmt 显式配置优先于 `.editorconfig`”的策略,这一点需要在后续验证中保持一致。 -保持与 `@vben/prettier-config` 一致的 workspace 包组织模式: +### 2.3 `@vben/oxfmt-config` workspace 包 + +当前包结构与其他 lint-config 包保持一致: ```text internal/lint-configs/oxfmt-config/ package.json - index.mjs # 导出 oxfmt 配置对象(供程序化调用) + src/index.ts ``` ```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", + "files": ["dist"], + "main": "./dist/index.mjs", + "module": "./dist/index.mjs", + "types": "./dist/index.d.ts", "exports": { - ".": { "default": "./index.mjs" } + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.mjs" + } }, "dependencies": { "oxfmt": "catalog:" @@ -570,167 +564,54 @@ internal/lint-configs/oxfmt-config/ } ``` -> **注意**: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` | +1. 集中管理 `oxfmt` 版本依赖 +2. 导出共享格式化配置对象 +3. 供根级 `oxfmt.config.ts` 和其他程序化场景复用 -### 2.6 操作步骤 +### 2.4 已完成的替换范围 -#### 第一步:生成 oxfmt 配置 +| 原方案 | 当前状态 | +| ------------------------ | ----------------------------- | +| `prettier` | 已移除,当前统一使用 `oxfmt` | +| `eslint-plugin-prettier` | 已移除 | +| `stylelint-prettier` | 已移除 | +| `@vben/prettier-config` | 已替换为 `@vben/oxfmt-config` | -```bash -# 自动迁移(生成 .oxfmtrc.json) -npx oxfmt@latest --migrate prettier +### 2.5 已完成的操作 -# 手动补充 overrides(自动迁移不支持) -# 手动将 endOfLine: 'auto' 改为 "lf" -# 手动添加 ignorePatterns(从 .prettierignore 迁移) -``` +以下动作已在当前仓库完成: -#### 第二步:创建 `@vben/oxfmt-config` 包 +1. 创建 `@vben/oxfmt-config` workspace 包 +2. 新增根级 `oxfmt.config.ts` +3. 将 `scripts/vsh/src/lint/index.ts` 切换到 `oxfmt` +4. 将 `lefthook.yml` 切换到 `pnpm oxfmt` +5. 删除 ESLint 中的 Prettier 集成 +6. 删除 Stylelint 中的 Prettier 集成 +7. 将 `internal/node-utils` 中的格式化能力切换到 `oxfmt` +8. 删除 `.prettierrc.mjs`、`.prettierignore` 和旧的 `@vben/prettier-config` +9. 更新根 `package.json`、`pnpm-workspace.yaml`、`vben-admin.code-workspace` -```bash -mkdir -p internal/lint-configs/oxfmt-config -# 创建 package.json 和 index.mjs -``` +### 2.6 下一步只保留验证项 -#### 第三步:全量格式化并对比 +第二阶段当前不再需要按“迁移步骤”执行,剩余工作主要是验证和收口: -```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 -``` +1. 重新全量运行 `pnpm run lint`,确认 `vsh lint` 当前默认行为与预期一致 +2. 明确 `oxfmt.config.ts` 是否作为 CLI 与编辑器共同入口;如团队工具链存在兼容性差异,再决定是否需要补 `.oxfmtrc.json` +3. 决定 `printWidth: 80` 与 `.editorconfig` 的 `max_line_length = 100` 是否继续并存 +4. 评估是否启用 oxfmt 的额外能力,例如 `sortImports`、`sortTailwindcss` ### 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` 一致 | +| `sortImports` | import 语句排序,灵感来自 `eslint-plugin-perfectionist/sort-imports` | 可评估是否替代 ESLint 侧部分排序职责 | +| `sortTailwindcss` | 替代 `prettier-plugin-tailwindcss` | 当前仓库仍优先使用 oxlint `jsPlugins` 方案 | +| `sortPackageJson` | 自动排序 `package.json` 字段 | 当前共享配置显式设为 `false` | +| `insertFinalNewline` | 文件末尾添加换行 | 与 `.editorconfig` 的 `insert_final_newline=true` 一致 | ### 2.8 文件类型覆盖说明 @@ -745,15 +626,15 @@ oxfmt 对不同文件类型的处理方式: | HTML | 内部委托 Prettier | | YAML / Markdown / GraphQL | 内部委托 Prettier | -> **重要**:CSS 等非 JS/TS 文件仍然通过 oxfmt 内部委托 Prettier 处理,因此 `npx oxfmt` 一个命令即可覆盖所有文件类型。不需要单独安装 Prettier——oxfmt 自带内嵌版本。 +> **重要**:CSS 等非 JS/TS 文件仍然通过 oxfmt 内部委托 Prettier 处理,因此 `oxfmt` 一个命令即可覆盖主要文件类型;仓库中不需要再单独安装 `prettier`。 -### 2.9 迁移风险 +### 2.9 当前风险与注意事项 -1. **格式化差异**:oxfmt 与 Prettier 的输出可能存在细微差异,首次全量格式化会产生大量 diff,建议单独一个 commit -2. **嵌套配置不支持**:oxfmt 不支持子目录级别的配置文件,如有需要需用 `overrides` 替代 -3. **`endOfLine: 'auto'` 不支持**:必须改为显式值,建议统一为 `"lf"` -4. **`node-utils/prettier.ts`**:如果有其他包或脚本通过 `prettierFormat()` 调用格式化,需逐一排查 -5. **IDE 配置**:团队成员需安装 oxfmt VS Code 扩展,或配置 LSP(`oxfmt --lsp`) +1. **配置入口一致性**:当前实际入口是 `oxfmt.config.ts`,需确认 CLI、编辑器、CI 是否都按这份配置执行 +2. **行宽来源不一致**:`oxfmt` 显式 `printWidth: 80`,`.editorconfig` 为 `100`,需要明确这是有意保留还是待统一 +3. **格式化差异**:首次重新全量格式化时,仍可能出现与历史 Prettier 输出不同的 diff +4. **扩展能力取舍**:`sortImports`、`sortTailwindcss` 是否启用会直接影响 ESLint / oxlint 的职责边界 +5. **IDE 配置**:团队成员仍需统一使用 oxfmt 对应扩展或 LSP(`oxfmt --lsp`) --- @@ -777,7 +658,7 @@ oxfmt 对不同文件类型的处理方式: ## 第四阶段:完全移除 ESLint(远期) -**前提**:oxlint 完全支持 Vue SFC 模板 lint 及所有保留插件的功能。 +**前提**:oxlint 完全支持 Vue SFC 模板 lint 及所有当前保留插件的功能。 ### 4.1 替换范围 @@ -790,7 +671,6 @@ oxfmt 对不同文件类型的处理方式: | `eslint-config-turbo` | 移除 | | `eslint-plugin-pnpm` | oxlint 或独立检查 | | `eslint-plugin-command` | oxlint 或移除 | -| `@vitest/eslint-plugin` | oxlint vitest 分类 | | `@vben/eslint-config` | 改写为 `@vben/oxlint-config` 或移除 | ### 4.2 操作步骤 @@ -798,26 +678,26 @@ oxfmt 对不同文件类型的处理方式: 1. 持续跟踪 oxlint 的 Vue / JSON / YAML 支持进展 2. 当 oxlint 规则覆盖度满足要求时,全面切换 3. 移除 `internal/lint-configs/eslint-config/` 及所有 ESLint 相关依赖 -4. 更新 CI 和 lefthook 配置 +4. 更新 CI 和 `lefthook` 配置 --- ## 迁移风险与注意事项 1. **规则对齐**:oxlint 的规则命名和行为可能与 ESLint 插件不完全一致,需逐条验证 -2. **Vue SFC 支持**:oxlint 对 `