mirror of
https://github.com/jxxghp/MoviePilot-Frontend.git
synced 2026-09-07 08:46:40 +08:00
ci: add observational ESLint check (#550)
This commit is contained in:
@@ -13,6 +13,26 @@ concurrency:
|
|||||||
cancel-in-progress: true
|
cancel-in-progress: true
|
||||||
|
|
||||||
jobs:
|
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:
|
unit-tests:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
timeout-minutes: 20
|
timeout-minutes: 20
|
||||||
|
|||||||
@@ -77,11 +77,13 @@ yarn build
|
|||||||
|
|
||||||
## ESLint CI:先观察,再强制
|
## ESLint CI:先观察,再强制
|
||||||
|
|
||||||
ESLint 本地检查稳定后,Pull Request workflow 增加全仓 `yarn lint`:
|
第二阶段在 Pull Request workflow 中增加独立的全仓 `yarn lint` job:
|
||||||
|
|
||||||
1. 初始阶段作为普通 check 运行,不立即配置 required check。
|
1. lint 与既有 `unit-tests` 使用不同 job,避免改变现有测试 check 的名称和职责。
|
||||||
2. 观察 fork PR、依赖缓存、执行时间、误报和路径范围。
|
2. 初始阶段作为普通 check 运行,不立即配置 required check。
|
||||||
3. 连续多个 PR 稳定通过后,再由维护者决定是否设为 required。
|
3. workflow 使用 Node 24、frozen lockfile 和只读 `yarn lint`,不执行自动修复或更新 baseline。
|
||||||
|
4. 观察 fork PR、依赖缓存、执行时间、误报和路径范围。
|
||||||
|
5. 连续多个 PR 稳定通过后,再由维护者决定是否设为 required。
|
||||||
|
|
||||||
ESLint CI 必须检查全仓受管源码,而不是只检查 PR 变更文件。已有问题通过规则选择或受控基线管理,保证新问题不能借由“只检查改动行”绕过项目约束。
|
ESLint CI 必须检查全仓受管源码,而不是只检查 PR 变更文件。已有问题通过规则选择或受控基线管理,保证新问题不能借由“只检查改动行”绕过项目约束。
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -92,4 +92,4 @@ yarn lint
|
|||||||
yarn build
|
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 或格式问题。
|
||||||
|
|||||||
+25
-5
@@ -10,18 +10,20 @@ const javascriptFiles = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
const typescriptFiles = [
|
const typescriptFiles = [
|
||||||
'**/*.{ts,tsx}',
|
'**/*.{ts,tsx,mts,cts}',
|
||||||
'**/*.vue',
|
'**/*.vue',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
const managedScriptExtensions = 'js,mjs,cjs,jsx,ts,tsx,mts,cts'
|
||||||
|
|
||||||
const managedFiles = [
|
const managedFiles = [
|
||||||
...javascriptFiles,
|
...javascriptFiles,
|
||||||
...typescriptFiles,
|
...typescriptFiles,
|
||||||
]
|
]
|
||||||
|
|
||||||
const browserFiles = [
|
const browserFiles = [
|
||||||
'src/**/*.{js,jsx,ts,tsx,vue}',
|
`src/**/*.{${managedScriptExtensions},vue}`,
|
||||||
'examples/**/src/**/*.{js,jsx,ts,tsx,vue}',
|
`examples/**/src/**/*.{${managedScriptExtensions},vue}`,
|
||||||
]
|
]
|
||||||
|
|
||||||
const nodeFiles = [
|
const nodeFiles = [
|
||||||
@@ -30,6 +32,16 @@ const nodeFiles = [
|
|||||||
'scripts/**/*.{js,mjs,cjs,ts,mts,cts}',
|
'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(?:\\/|$)/'
|
const restrictedCoreSourcePattern = '/(?:^|\\/)@core(?:\\/|$)/'
|
||||||
|
|
||||||
// @layouts 是 @core 的下游依赖;禁止反向引用,避免两层形成直接循环。
|
// @layouts 是 @core 的下游依赖;禁止反向引用,避免两层形成直接循环。
|
||||||
@@ -95,6 +107,7 @@ export default defineConfig([
|
|||||||
'**/dist/**',
|
'**/dist/**',
|
||||||
'**/coverage/**',
|
'**/coverage/**',
|
||||||
'**/.worktrees/**',
|
'**/.worktrees/**',
|
||||||
|
'**/vite.config.*.timestamp-*.mjs',
|
||||||
'public/plugin_icon/**',
|
'public/plugin_icon/**',
|
||||||
'src/@iconify/**',
|
'src/@iconify/**',
|
||||||
'**/*.d.ts',
|
'**/*.d.ts',
|
||||||
@@ -136,6 +149,9 @@ export default defineConfig([
|
|||||||
languageOptions: {
|
languageOptions: {
|
||||||
globals: globals.browser,
|
globals: globals.browser,
|
||||||
},
|
},
|
||||||
|
rules: {
|
||||||
|
'no-restricted-globals': ['error', ...nodeOnlyGlobalRestrictions],
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'moviepilot/node-globals',
|
name: 'moviepilot/node-globals',
|
||||||
@@ -143,6 +159,10 @@ export default defineConfig([
|
|||||||
languageOptions: {
|
languageOptions: {
|
||||||
globals: globals.node,
|
globals: globals.node,
|
||||||
},
|
},
|
||||||
|
// Node 配置可能位于浏览器源码目录;显式覆盖浏览器专用限制,避免 flat config 合并后误报。
|
||||||
|
rules: {
|
||||||
|
'no-restricted-globals': 'off',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'moviepilot/sonarjs',
|
name: 'moviepilot/sonarjs',
|
||||||
@@ -154,7 +174,7 @@ export default defineConfig([
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'moviepilot/sonarjs-locales',
|
name: 'moviepilot/sonarjs-locales',
|
||||||
files: ['src/locales/**/*.{js,jsx,ts,tsx}'],
|
files: [`src/locales/**/*.{${managedScriptExtensions}}`],
|
||||||
rules: {
|
rules: {
|
||||||
'sonarjs/no-hardcoded-passwords': 'off',
|
'sonarjs/no-hardcoded-passwords': 'off',
|
||||||
'sonarjs/no-hardcoded-secrets': 'off',
|
'sonarjs/no-hardcoded-secrets': 'off',
|
||||||
@@ -169,7 +189,7 @@ export default defineConfig([
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'moviepilot/layout-boundary',
|
name: 'moviepilot/layout-boundary',
|
||||||
files: ['src/@layouts/**/*.{js,jsx,ts,tsx,vue}'],
|
files: [`src/@layouts/**/*.{${managedScriptExtensions},vue}`],
|
||||||
rules: {
|
rules: {
|
||||||
'no-restricted-syntax': [
|
'no-restricted-syntax': [
|
||||||
'error',
|
'error',
|
||||||
|
|||||||
@@ -1,9 +1,15 @@
|
|||||||
import { ESLint } from 'eslint'
|
import { ESLint } from 'eslint'
|
||||||
|
import globals from 'globals'
|
||||||
import { expect, it } from 'vitest'
|
import { expect, it } from 'vitest'
|
||||||
|
|
||||||
const eslint = new ESLint()
|
const eslint = new ESLint()
|
||||||
const restrictedSyntaxRule = 'no-restricted-syntax'
|
const restrictedSyntaxRule = 'no-restricted-syntax'
|
||||||
|
const restrictedGlobalsRule = 'no-restricted-globals'
|
||||||
const unusedVariablesRule = '@typescript-eslint/no-unused-vars'
|
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 检查虚拟源码,避免项目边界和新增问题门禁在依赖升级时静默丢失。 */
|
/** 使用真实 flat config 检查虚拟源码,避免项目边界和新增问题门禁在依赖升级时静默丢失。 */
|
||||||
async function lintRuleIds(code: string, filePath: string) {
|
async function lintRuleIds(code: string, filePath: string) {
|
||||||
@@ -160,6 +166,13 @@ const cases = [
|
|||||||
ruleId: 'no-undef',
|
ruleId: 'no-undef',
|
||||||
shouldReport: true,
|
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 全局变量',
|
name: '允许构建配置使用 Node 全局变量',
|
||||||
code: 'process.env.NODE_ENV',
|
code: 'process.env.NODE_ENV',
|
||||||
@@ -167,6 +180,34 @@ const cases = [
|
|||||||
ruleId: 'no-undef',
|
ruleId: 'no-undef',
|
||||||
shouldReport: false,
|
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 名称',
|
name: '允许 Vuetify 点号 slot 名称',
|
||||||
code: '<template><VDataTable><template #item.name /></VDataTable></template>',
|
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)
|
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