mirror of
https://github.com/jxxghp/MoviePilot-Frontend.git
synced 2026-08-12 00:54:42 +08:00
ci: add observational ESLint check (#550)
This commit is contained in:
20
.github/workflows/test.yml
vendored
20
.github/workflows/test.yml
vendored
@@ -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
|
||||
|
||||
@@ -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 变更文件。已有问题通过规则选择或受控基线管理,保证新问题不能借由“只检查改动行”绕过项目约束。
|
||||
|
||||
|
||||
@@ -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 或格式问题。
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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: '<script setup lang="ts">const bytes = Buffer.from("x")</script><template>{{ bytes }}</template>',
|
||||
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: '<template><VDataTable><template #item.name /></VDataTable></template>',
|
||||
@@ -195,3 +236,32 @@ it.each(cases)('$name', async ({ code, filePath, ruleId, shouldReport }) => {
|
||||
|
||||
expect(ruleIds.includes(ruleId)).toBe(shouldReport)
|
||||
})
|
||||
|
||||
it.each(nodeOnlyGlobalNames)('拒绝浏览器 TypeScript 使用 Node 全局变量 %s', async globalName => {
|
||||
const ruleIds = await lintRuleIds(`console.log(${globalName})`, `src/components/invalid-${globalName}.ts`)
|
||||
|
||||
expect(ruleIds).toContain(restrictedGlobalsRule)
|
||||
})
|
||||
|
||||
it.each([
|
||||
'example.config.mts',
|
||||
'example.config.cts',
|
||||
'scripts/example.mts',
|
||||
'scripts/example.cts',
|
||||
])('使用 TypeScript parser 解析 %s', async filePath => {
|
||||
const ruleIds = await lintRuleIds('const value: string = "x"; console.log(value)', filePath)
|
||||
|
||||
expect(ruleIds).not.toContain(null)
|
||||
})
|
||||
|
||||
it('忽略 Vite 生成的临时配置模块', async () => {
|
||||
await expect(eslint.isPathIgnored('vite.config.ts.timestamp-1784340502076-0129cef6d3bbd8.mjs')).resolves.toBe(true)
|
||||
})
|
||||
|
||||
it('不忽略普通 MJS 源码', async () => {
|
||||
await expect(eslint.isPathIgnored('scripts/example.mjs')).resolves.toBe(false)
|
||||
})
|
||||
|
||||
it('不忽略名称包含 timestamp 的普通 MJS 源码', async () => {
|
||||
await expect(eslint.isPathIgnored('scripts/feature.timestamp-parser.mjs')).resolves.toBe(false)
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user