From 985874506e82625838b2346ef49a8325a55e10f5 Mon Sep 17 00:00:00 2001 From: InfinityPacer <160988576+InfinityPacer@users.noreply.github.com> Date: Sat, 18 Jul 2026 10:24:00 +0800 Subject: [PATCH] ci: add observational ESLint check (#550) --- .github/workflows/test.yml | 20 +++++++++ docs/code-quality.md | 10 +++-- docs/testing.md | 2 +- eslint.config.js | 30 ++++++++++--- tests/config/eslint-config.spec.ts | 70 ++++++++++++++++++++++++++++++ 5 files changed, 122 insertions(+), 10 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 3ca0b354..1403d842 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -13,6 +13,26 @@ concurrency: cancel-in-progress: true jobs: + lint: + runs-on: ubuntu-latest + timeout-minutes: 20 + steps: + - name: Checkout + uses: actions/checkout@v7 + + - name: Setup Node + uses: actions/setup-node@v7 + with: + node-version: '24' + cache: yarn + cache-dependency-path: yarn.lock + + - name: Install dependencies + run: yarn --frozen-lockfile + + - name: Lint + run: yarn lint + unit-tests: runs-on: ubuntu-latest timeout-minutes: 20 diff --git a/docs/code-quality.md b/docs/code-quality.md index 238e3526..e002216b 100644 --- a/docs/code-quality.md +++ b/docs/code-quality.md @@ -77,11 +77,13 @@ yarn build ## ESLint CI:先观察,再强制 -ESLint 本地检查稳定后,Pull Request workflow 增加全仓 `yarn lint`: +第二阶段在 Pull Request workflow 中增加独立的全仓 `yarn lint` job: -1. 初始阶段作为普通 check 运行,不立即配置 required check。 -2. 观察 fork PR、依赖缓存、执行时间、误报和路径范围。 -3. 连续多个 PR 稳定通过后,再由维护者决定是否设为 required。 +1. lint 与既有 `unit-tests` 使用不同 job,避免改变现有测试 check 的名称和职责。 +2. 初始阶段作为普通 check 运行,不立即配置 required check。 +3. workflow 使用 Node 24、frozen lockfile 和只读 `yarn lint`,不执行自动修复或更新 baseline。 +4. 观察 fork PR、依赖缓存、执行时间、误报和路径范围。 +5. 连续多个 PR 稳定通过后,再由维护者决定是否设为 required。 ESLint CI 必须检查全仓受管源码,而不是只检查 PR 变更文件。已有问题通过规则选择或受控基线管理,保证新问题不能借由“只检查改动行”绕过项目约束。 diff --git a/docs/testing.md b/docs/testing.md index 5dbff101..bc6e12ad 100644 --- a/docs/testing.md +++ b/docs/testing.md @@ -92,4 +92,4 @@ yarn lint yarn build ``` -Pull Request 测试工作流使用 Node 24 LTS 和 frozen lockfile,依次执行类型检查和覆盖率门禁。ESLint、Prettier 和 Node 兼容范围按[前端代码质量工具链演进](code-quality.md)渐进接入,新增测试代码不得引入新的 lint 或格式问题。 +Pull Request 工作流使用 Node 24 LTS 和 frozen lockfile。`unit-tests` job 依次执行类型检查和覆盖率门禁;独立的 `lint` job 执行全仓只读 ESLint 检查,当前处于普通 check 观察阶段,不改变既有 required checks。Prettier 和 Node 兼容范围按[前端代码质量工具链演进](code-quality.md)继续渐进接入,新增测试代码不得引入新的 lint 或格式问题。 diff --git a/eslint.config.js b/eslint.config.js index 0d7a0415..f197665a 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -10,18 +10,20 @@ const javascriptFiles = [ ] const typescriptFiles = [ - '**/*.{ts,tsx}', + '**/*.{ts,tsx,mts,cts}', '**/*.vue', ] +const managedScriptExtensions = 'js,mjs,cjs,jsx,ts,tsx,mts,cts' + const managedFiles = [ ...javascriptFiles, ...typescriptFiles, ] const browserFiles = [ - 'src/**/*.{js,jsx,ts,tsx,vue}', - 'examples/**/src/**/*.{js,jsx,ts,tsx,vue}', + `src/**/*.{${managedScriptExtensions},vue}`, + `examples/**/src/**/*.{${managedScriptExtensions},vue}`, ] const nodeFiles = [ @@ -30,6 +32,16 @@ const nodeFiles = [ 'scripts/**/*.{js,mjs,cjs,ts,mts,cts}', ] +// TypeScript 关闭核心 no-undef;显式禁止浏览器域中的 Node-only globals,保证 JS、TS 与 Vue 使用同一运行时边界。 +const browserGlobalNames = new Set(Object.keys(globals.browser)) +const nodeOnlyGlobalRestrictions = Object.keys(globals.node) + .filter(name => !browserGlobalNames.has(name)) + .sort() + .map(name => ({ + name, + message: `'${name}' is only available in Node.js code.`, + })) + const restrictedCoreSourcePattern = '/(?:^|\\/)@core(?:\\/|$)/' // @layouts 是 @core 的下游依赖;禁止反向引用,避免两层形成直接循环。 @@ -95,6 +107,7 @@ export default defineConfig([ '**/dist/**', '**/coverage/**', '**/.worktrees/**', + '**/vite.config.*.timestamp-*.mjs', 'public/plugin_icon/**', 'src/@iconify/**', '**/*.d.ts', @@ -136,6 +149,9 @@ export default defineConfig([ languageOptions: { globals: globals.browser, }, + rules: { + 'no-restricted-globals': ['error', ...nodeOnlyGlobalRestrictions], + }, }, { name: 'moviepilot/node-globals', @@ -143,6 +159,10 @@ export default defineConfig([ languageOptions: { globals: globals.node, }, + // Node 配置可能位于浏览器源码目录;显式覆盖浏览器专用限制,避免 flat config 合并后误报。 + rules: { + 'no-restricted-globals': 'off', + }, }, { name: 'moviepilot/sonarjs', @@ -154,7 +174,7 @@ export default defineConfig([ }, { name: 'moviepilot/sonarjs-locales', - files: ['src/locales/**/*.{js,jsx,ts,tsx}'], + files: [`src/locales/**/*.{${managedScriptExtensions}}`], rules: { 'sonarjs/no-hardcoded-passwords': 'off', 'sonarjs/no-hardcoded-secrets': 'off', @@ -169,7 +189,7 @@ export default defineConfig([ }, { name: 'moviepilot/layout-boundary', - files: ['src/@layouts/**/*.{js,jsx,ts,tsx,vue}'], + files: [`src/@layouts/**/*.{${managedScriptExtensions},vue}`], rules: { 'no-restricted-syntax': [ 'error', diff --git a/tests/config/eslint-config.spec.ts b/tests/config/eslint-config.spec.ts index 97a467a9..31fbf010 100644 --- a/tests/config/eslint-config.spec.ts +++ b/tests/config/eslint-config.spec.ts @@ -1,9 +1,15 @@ import { ESLint } from 'eslint' +import globals from 'globals' import { expect, it } from 'vitest' const eslint = new ESLint() const restrictedSyntaxRule = 'no-restricted-syntax' +const restrictedGlobalsRule = 'no-restricted-globals' const unusedVariablesRule = '@typescript-eslint/no-unused-vars' +const browserGlobalNames = new Set(Object.keys(globals.browser)) +const nodeOnlyGlobalNames = Object.keys(globals.node) + .filter(name => !browserGlobalNames.has(name)) + .sort() /** 使用真实 flat config 检查虚拟源码,避免项目边界和新增问题门禁在依赖升级时静默丢失。 */ async function lintRuleIds(code: string, filePath: string) { @@ -160,6 +166,13 @@ const cases = [ ruleId: 'no-undef', shouldReport: true, }, + { + name: '拒绝浏览器 Vue 脚本使用 Node 全局变量', + code: '', + filePath: 'src/components/invalid-node-global.vue', + ruleId: restrictedGlobalsRule, + shouldReport: true, + }, { name: '允许构建配置使用 Node 全局变量', code: 'process.env.NODE_ENV', @@ -167,6 +180,34 @@ const cases = [ ruleId: 'no-undef', shouldReport: false, }, + { + name: '允许 TypeScript 构建配置使用 Node 全局变量', + code: 'const runtime: string | undefined = process.env.NODE_ENV; console.log(runtime)', + filePath: 'example.config.mts', + ruleId: restrictedGlobalsRule, + shouldReport: false, + }, + { + name: '允许浏览器目录中的 Node 配置使用 Node 全局变量', + code: 'const runtime: string | undefined = process.env.NODE_ENV; console.log(runtime)', + filePath: 'src/tool.config.ts', + ruleId: restrictedGlobalsRule, + shouldReport: false, + }, + { + name: '拒绝 MTS 脚本保留 debugger', + code: 'const value: string = "x"; console.log(value); debugger', + filePath: 'scripts/invalid-debugger.mts', + ruleId: 'no-debugger', + shouldReport: true, + }, + { + name: '拒绝 CTS 脚本保留 debugger', + code: 'const value: string = "x"; console.log(value); debugger', + filePath: 'scripts/invalid-debugger.cts', + ruleId: 'no-debugger', + shouldReport: true, + }, { name: '允许 Vuetify 点号 slot 名称', code: '