chore: add incremental Prettier tooling (#555)

This commit is contained in:
InfinityPacer
2026-07-18 15:41:23 +08:00
committed by GitHub
parent 67e78d0eb1
commit f6d8ab8aeb
9 changed files with 403 additions and 53 deletions
+6
View File
@@ -1,2 +1,8 @@
dist dist
node_modules node_modules
# 自动生成文件的格式由各自生成器决定。
auto-imports.d.ts
components.d.ts
eslint-suppressions.json
/vite.config.*.timestamp-*.mjs
+1 -1
View File
@@ -3,7 +3,7 @@
"bracketSpacing": true, "bracketSpacing": true,
"htmlWhitespaceSensitivity": "css", "htmlWhitespaceSensitivity": "css",
"insertPragma": false, "insertPragma": false,
"jsxBracketSameLine": false, "bracketSameLine": false,
"jsxSingleQuote": true, "jsxSingleQuote": true,
"printWidth": 120, "printWidth": 120,
"proseWrap": "preserve", "proseWrap": "preserve",
+19 -8
View File
@@ -5,7 +5,7 @@
## 工具职责 ## 工具职责
| 工具 | 职责 | 不负责 | | 工具 | 职责 | 不负责 |
| --- | --- | --- | | ---------------------- | ------------------------------------------------------------ | -------------------------------------- |
| ESLint | JavaScript、TypeScript、Vue 代码质量,框架约束和项目模块边界 | 缩进、换行、引号、属性布局等代码格式 | | ESLint | JavaScript、TypeScript、Vue 代码质量,框架约束和项目模块边界 | 缩进、换行、引号、属性布局等代码格式 |
| Prettier | 可确定、可重复的代码格式 | 未使用变量、Vue 规则、复杂度和模块边界 | | Prettier | 可确定、可重复的代码格式 | 未使用变量、Vue 规则、复杂度和模块边界 |
| TypeScript / `vue-tsc` | TypeScript 与 Vue 模板类型检查 | 代码格式和业务行为测试 | | TypeScript / `vue-tsc` | TypeScript 与 Vue 模板类型检查 | 代码格式和业务行为测试 |
@@ -36,8 +36,9 @@ Prettier 独立运行,ESLint 中与格式重叠的规则保持关闭。任何
yarn lint # 只读 ESLint 检查,不修改文件 yarn lint # 只读 ESLint 检查,不修改文件
yarn lint:fix # 显式执行 ESLint 安全修复 yarn lint:fix # 显式执行 ESLint 安全修复
yarn lint:suppressions:prune # 修复存量问题后裁剪过期 baseline yarn lint:suppressions:prune # 修复存量问题后裁剪过期 baseline
yarn format # 使用 Prettier 格式化目标文件 yarn format # 格式化当前分支、暂存区、工作区和未跟踪文件中的受支持变更文件
yarn format:check # 检查目标文件格式,不修改文件 yarn format:check # 检查相同变更文件,不修改文件
yarn format:all:check # 只读测量全仓格式收敛情况
yarn typecheck yarn typecheck
yarn test:run yarn test:run
yarn test:coverage yarn test:coverage
@@ -104,9 +105,17 @@ Prettier 初始 CI 只检查 Pull Request 新增或修改的受支持文件,
3. 仅把 Prettier 支持且由仓库管理的文件传给 `prettier --check` 3. 仅把 Prettier 支持且由仓库管理的文件传给 `prettier --check`
4. 文件集合为空时正常通过。 4. 文件集合为空时正常通过。
本地直接运行 `yarn format``yarn format:check` 时,脚本以可用的 `upstream/v2``origin/v2` 或本地 `v2` 为基线,并合并当前工作区与未跟踪文件。CI 使用显式引用保证输入可复现:
```sh
yarn format:check --base <base-sha> --head <head-sha>
```
文件名通过 Git 的 NUL 分隔输出和 Node 参数数组传递,不经过 shell 字符串拼接;重命名只检查新路径,删除文件、ignore 文件和 Prettier 无法推断 parser 的文件不进入格式检查。
该门禁不要求未修改的存量文件通过 Prettier,但所有进入 PR diff 的受支持文件都必须通过。Prettier 是文件级格式化工具,因此修改存量文件时检查的是整个文件,而不是仅检查变更行。Agent 或开发者应在提交前运行同一项目脚本完成格式化;CI 只验证结果,不自动提交修改。 该门禁不要求未修改的存量文件通过 Prettier,但所有进入 PR diff 的受支持文件都必须通过。Prettier 是文件级格式化工具,因此修改存量文件时检查的是整个文件,而不是仅检查变更行。Agent 或开发者应在提交前运行同一项目脚本完成格式化;CI 只验证结果,不自动提交修改。
禁止在普通业务 PR 中运行全仓 `yarn format`。触及文件格式化仍可能重排整个文件;差异过大时应拆分 commit,便于 reviewer 区分机械格式与业务逻辑。 禁止在普通业务 PR 中直接运行 `prettier . --write` 或其他全仓格式写入命令。`yarn format` 只处理选择器返回的变更文件;单个存量文件被触及时仍可能整体重排,差异过大时应拆分 commit,便于 reviewer 区分机械格式与业务逻辑。
具体交付拆分为: 具体交付拆分为:
@@ -116,11 +125,13 @@ Prettier 初始 CI 只检查 Pull Request 新增或修改的受支持文件,
## 全仓格式门禁的启用条件 ## 全仓格式门禁的启用条件
不单独安排大爆炸式格式化阶段。存量通过日常“改到即格式化”逐步收敛,并定期执行全仓 `yarn format:check` 观察剩余范围。 不单独安排大爆炸式格式化阶段。存量通过日常“改到即格式化”逐步收敛,并定期执行全仓 `yarn format:all:check` 观察剩余范围。
Prettier 3.9.5 基础设施接入时,全仓只读检查在 `v2` 基线报告 203 个存量文件需要格式化。该数字是收敛起点而不是忽略白名单;变更文件仍必须完整通过检查,存量数量随日常修改逐步下降。
变更文件 required check 不需要等待全仓收敛。只有同时满足以下条件,才把 Prettier 从变更文件 required check 切换为全仓 required check 变更文件 required check 不需要等待全仓收敛。只有同时满足以下条件,才把 Prettier 从变更文件 required check 切换为全仓 required check
- 全仓 `yarn format:check` 已通过,或仅剩少量可在独立机械提交中安全处理的文件。 - 全仓 `yarn format:all:check` 已通过,或仅剩少量可在独立机械提交中安全处理的文件。
- 最近的活跃分支已合并或完成同步,避免集中格式变化制造冲突。 - 最近的活跃分支已合并或完成同步,避免集中格式变化制造冲突。
- `yarn lint``yarn typecheck``yarn test:coverage``yarn build` 在格式收敛后全部通过。 - `yarn lint``yarn typecheck``yarn test:coverage``yarn build` 在格式收敛后全部通过。
- Prettier 与 ESLint 不存在反复改写同一文件的规则冲突。 - Prettier 与 ESLint 不存在反复改写同一文件的规则冲突。
@@ -130,7 +141,7 @@ Prettier 初始 CI 只检查 Pull Request 新增或修改的受支持文件,
```sh ```sh
yarn --frozen-lockfile yarn --frozen-lockfile
yarn format:check yarn format:all:check
yarn lint yarn lint
yarn typecheck yarn typecheck
yarn test:coverage yarn test:coverage
@@ -154,7 +165,7 @@ PR-Agent、编辑器诊断和人工 review 可以补充判断,但不能替代
## 风险与回滚 ## 风险与回滚
| 风险 | 控制方式 | 回滚边界 | | 风险 | 控制方式 | 回滚边界 |
| --- | --- | --- | | ---------------------------- | ------------------------------------------ | ------------------------------ |
| ESLint 自动修复改变语义 | 默认 lint 只读;基础设施迁移不批量 fix | 独立回退规则或依赖提交 | | ESLint 自动修复改变语义 | 默认 lint 只读;基础设施迁移不批量 fix | 独立回退规则或依赖提交 |
| 格式化制造大面积冲突 | 改到即格式化;大型文件拆分机械 commit | 回退单文件格式 commit | | 格式化制造大面积冲突 | 改到即格式化;大型文件拆分机械 commit | 回退单文件格式 commit |
| 新规则让所有 PR 变红 | 先测量、再启用;使用受控基线 | 暂停单条规则,不回退整套工具链 | | 新规则让所有 PR 变红 | 先测量、再启用;使用受控基线 | 暂停单条规则,不回退整套工具链 |
+12 -19
View File
@@ -5,32 +5,20 @@ import globals from 'globals'
import tseslint from 'typescript-eslint' import tseslint from 'typescript-eslint'
import { defineConfig, globalIgnores } from 'eslint/config' import { defineConfig, globalIgnores } from 'eslint/config'
const javascriptFiles = [ const javascriptFiles = ['**/*.{js,mjs,cjs,jsx}']
'**/*.{js,mjs,cjs,jsx}',
]
const typescriptFiles = [ const typescriptFiles = ['**/*.{ts,tsx,mts,cts}', '**/*.vue']
'**/*.{ts,tsx,mts,cts}',
'**/*.vue',
]
const managedScriptExtensions = 'js,mjs,cjs,jsx,ts,tsx,mts,cts' const managedScriptExtensions = 'js,mjs,cjs,jsx,ts,tsx,mts,cts'
const managedFiles = [ const managedFiles = [...javascriptFiles, ...typescriptFiles]
...javascriptFiles,
...typescriptFiles,
]
const browserFiles = [ const browserFiles = [
`src/**/*.{${managedScriptExtensions},vue}`, `src/**/*.{${managedScriptExtensions},vue}`,
`examples/**/src/**/*.{${managedScriptExtensions},vue}`, `examples/**/src/**/*.{${managedScriptExtensions},vue}`,
] ]
const nodeFiles = [ const nodeFiles = ['**/*.config.{js,mjs,cjs,ts,mts,cts}', 'public/service.js', 'scripts/**/*.{js,mjs,cjs,ts,mts,cts}']
'**/*.config.{js,mjs,cjs,ts,mts,cts}',
'public/service.js',
'scripts/**/*.{js,mjs,cjs,ts,mts,cts}',
]
// TypeScript 关闭核心 no-undef;显式禁止浏览器域中的 Node-only globals,保证 JS、TS 与 Vue 使用同一运行时边界。 // TypeScript 关闭核心 no-undef;显式禁止浏览器域中的 Node-only globals,保证 JS、TS 与 Vue 使用同一运行时边界。
const browserGlobalNames = new Set(Object.keys(globals.browser)) const browserGlobalNames = new Set(Object.keys(globals.browser))
@@ -67,7 +55,8 @@ const viteGlobCallSelectors = [
const layoutGlobRestrictions = viteGlobCallSelectors.map(callSelector => ({ const layoutGlobRestrictions = viteGlobCallSelectors.map(callSelector => ({
selector: callSelector, selector: callSelector,
message: 'import.meta.glob is not allowed in @layouts because wildcard targets cannot be proven to stay within the module boundary.', message:
'import.meta.glob is not allowed in @layouts because wildcard targets cannot be proven to stay within the module boundary.',
})) }))
const sonarRules = { const sonarRules = {
@@ -105,6 +94,7 @@ export default defineConfig([
globalIgnores([ globalIgnores([
'**/node_modules/**', '**/node_modules/**',
'**/dist/**', '**/dist/**',
'**/dev-dist/**',
'**/coverage/**', '**/coverage/**',
'**/.worktrees/**', '**/.worktrees/**',
'**/vite.config.*.timestamp-*.mjs', '**/vite.config.*.timestamp-*.mjs',
@@ -124,9 +114,12 @@ export default defineConfig([
files: ['**/*.vue'], files: ['**/*.vue'],
rules: { rules: {
'vue/multi-word-component-names': 'off', 'vue/multi-word-component-names': 'off',
'vue/valid-v-slot': ['error', { 'vue/valid-v-slot': [
'error',
{
allowModifiers: true, allowModifiers: true,
}], },
],
}, },
}, },
{ {
+5
View File
@@ -17,6 +17,10 @@
"lint": "eslint . --max-warnings=0", "lint": "eslint . --max-warnings=0",
"lint:fix": "eslint . --fix", "lint:fix": "eslint . --fix",
"lint:suppressions:prune": "eslint . --prune-suppressions", "lint:suppressions:prune": "eslint . --prune-suppressions",
"format": "node scripts/format-changed.mjs --write",
"format:check": "node scripts/format-changed.mjs --check",
"format:all:check": "prettier . --check",
"test:format-config": "vitest run tests/config/format-changed.spec.ts",
"build:icons": "tsc -b src/@iconify && node src/@iconify/build-icons.js", "build:icons": "tsc -b src/@iconify && node src/@iconify/build-icons.js",
"postinstall": "npm run build:icons", "postinstall": "npm run build:icons",
"pkg": "pkg . -t node18-win-x64 -o MoviePilot-Frontend.exe" "pkg": "pkg . -t node18-win-x64 -o MoviePilot-Frontend.exe"
@@ -125,6 +129,7 @@
"msw": "2.15.0", "msw": "2.15.0",
"postcss": "^8.5.1", "postcss": "^8.5.1",
"postcss-html": "^1.5.0", "postcss-html": "^1.5.0",
"prettier": "^3.9.5",
"stylelint": "^16.13.2", "stylelint": "^16.13.2",
"stylelint-config-idiomatic-order": "^10.0.0", "stylelint-config-idiomatic-order": "^10.0.0",
"stylelint-config-standard-scss": "^14.0.0", "stylelint-config-standard-scss": "^14.0.0",
+201
View File
@@ -0,0 +1,201 @@
import { execFileSync, spawnSync } from 'node:child_process'
import { existsSync, lstatSync } from 'node:fs'
import { resolve } from 'node:path'
import process from 'node:process'
import { fileURLToPath, pathToFileURL } from 'node:url'
import * as prettier from 'prettier'
const DEFAULT_BASE_REFS = ['upstream/v2', 'origin/v2', 'v2']
const PRETTIER_ARGUMENT_BUDGET = 24_000
const prettierCliPath = fileURLToPath(new URL('../node_modules/prettier/bin/prettier.cjs', import.meta.url))
/** 解析 Git 的 NUL 分隔输出,保留文件名中的空格、换行和通配符字符。 */
function parseNullDelimited(output) {
return output.toString('utf8').split('\0').filter(Boolean)
}
/** 运行只返回仓库相对路径的 Git 查询。 */
function gitPaths(cwd, args) {
return parseNullDelimited(execFileSync('git', args, { cwd, encoding: 'buffer' }))
}
/** 判断本地仓库是否存在可用于三点 diff 的提交引用。 */
function hasCommitRef(cwd, ref) {
return (
spawnSync('git', ['rev-parse', '--verify', '--quiet', `${ref}^{commit}`], {
cwd,
stdio: 'ignore',
}).status === 0
)
}
/** 把外部引用解析为提交 SHA,后续 Git diff 不再解释用户提供的选项样式字符串。 */
function resolveCommit(cwd, ref) {
try {
return execFileSync('git', ['rev-parse', '--verify', '--end-of-options', `${ref}^{commit}`], {
cwd,
encoding: 'utf8',
}).trim()
} catch {
throw new Error(`无法解析 Git 提交引用:${ref}`)
}
}
/** 为本地命令选择现有 v2 基线;CI 应显式传入事件中的 base/head SHA。 */
function resolveDefaultBase(cwd) {
const base = DEFAULT_BASE_REFS.find(ref => hasCommitRef(cwd, ref))
if (!base) {
throw new Error('无法定位 v2 基线,请通过 --base <ref> 显式指定比较起点。')
}
return base
}
/**
* 收集当前分支相对基线的文件集合。
* 显式 base/head 模式只读取提交区间,默认本地模式还会合并暂存区、工作区和未跟踪文件。
*/
function collectChangedPaths({ cwd, base, head }) {
if (Boolean(base) !== Boolean(head)) {
throw new Error('--base 与 --head 必须一起使用。')
}
const explicitRange = Boolean(base)
const resolvedBase = base ?? resolveDefaultBase(cwd)
const resolvedHead = head ?? 'HEAD'
const baseCommit = resolveCommit(cwd, resolvedBase)
const headCommit = resolveCommit(cwd, resolvedHead)
const paths = new Set(
gitPaths(cwd, ['diff', '--name-only', '--diff-filter=ACMR', '-z', `${baseCommit}...${headCommit}`]),
)
if (!explicitRange) {
for (const filePath of gitPaths(cwd, ['diff', '--name-only', '--diff-filter=ACMR', '-z', 'HEAD'])) {
paths.add(filePath)
}
for (const filePath of gitPaths(cwd, ['ls-files', '--others', '--exclude-standard', '-z'])) {
paths.add(filePath)
}
}
return [...paths]
.filter(filePath => {
const absolutePath = resolve(cwd, filePath)
if (!existsSync(absolutePath)) return false
try {
return lstatSync(absolutePath).isFile()
} catch {
return false
}
})
.sort()
}
/** 使用仓库 ignore 文件和 Prettier parser 推断筛出真正受格式化工具管理的文件。 */
async function selectPrettierFiles(filePaths, cwd) {
const ignorePaths = ['.gitignore', '.prettierignore'].map(filePath => resolve(cwd, filePath)).filter(existsSync)
const selected = []
for (const filePath of filePaths) {
const absolutePath = resolve(cwd, filePath)
const fileInfo = await prettier.getFileInfo(absolutePath)
if (!fileInfo.inferredParser) continue
let ignored = false
for (const ignorePath of ignorePaths) {
if ((await prettier.getFileInfo(absolutePath, { ignorePath })).ignored) {
ignored = true
break
}
}
if (!ignored) selected.push(filePath)
}
return selected
}
/** 按保守的参数长度预算拆批,避免大型 PR 超过操作系统命令行上限。 */
function batchPaths(filePaths) {
const batches = []
let batch = []
let length = 0
for (const filePath of filePaths) {
if (batch.length > 0 && length + filePath.length + 1 > PRETTIER_ARGUMENT_BUDGET) {
batches.push(batch)
batch = []
length = 0
}
batch.push(filePath)
length += filePath.length + 1
}
if (batch.length > 0) batches.push(batch)
return batches
}
/** 通过参数数组调用项目内 Prettier,避免 shell 展开或拼接特殊文件名。 */
function runPrettier(filePaths, mode, cwd) {
if (filePaths.length === 0) {
console.log('没有需要 Prettier 处理的变更文件。')
return
}
for (const batch of batchPaths(filePaths)) {
const result = spawnSync(process.execPath, [prettierCliPath, mode, '--ignore-unknown', '--', ...batch], {
cwd,
stdio: 'inherit',
})
if (result.status !== 0) {
throw new Error(`Prettier ${mode} 失败,退出码:${result.status ?? 'unknown'}`)
}
}
}
/** 解析面向开发者和 CI 的稳定命令行契约。 */
function parseArguments(args) {
const options = { action: undefined, base: undefined, head: undefined }
for (let index = 0; index < args.length; index += 1) {
const argument = args[index]
if (['--check', '--write', '--list'].includes(argument)) {
if (options.action) throw new Error('只能指定 --check、--write 或 --list 中的一种操作。')
options.action = argument
continue
}
if (argument === '--base' || argument === '--head') {
const value = args[index + 1]
if (!value) throw new Error(`${argument} 缺少引用值。`)
options[argument.slice(2)] = value
index += 1
continue
}
throw new Error(`未知参数:${argument}`)
}
if (!options.action) throw new Error('必须指定 --check、--write 或 --list。')
return options
}
/** 执行变更文件选择与格式化;显式引用模式为后续 PR workflow 提供确定输入。 */
async function main() {
const cwd = process.cwd()
const { action, base, head } = parseArguments(process.argv.slice(2))
const changedPaths = collectChangedPaths({ cwd, base, head })
const selectedPaths = await selectPrettierFiles(changedPaths, cwd)
if (action === '--list') {
process.stdout.write(`${JSON.stringify(selectedPaths)}\n`)
return
}
runPrettier(selectedPaths, action, cwd)
}
if (process.argv[1] && pathToFileURL(process.argv[1]).href === import.meta.url) {
main().catch(error => {
console.error(error instanceof Error ? error.message : error)
process.exitCode = 1
})
}
+9 -7
View File
@@ -243,21 +243,23 @@ it.each(nodeOnlyGlobalNames)('拒绝浏览器 TypeScript 使用 Node 全局变
expect(ruleIds).toContain(restrictedGlobalsRule) expect(ruleIds).toContain(restrictedGlobalsRule)
}) })
it.each([ it.each(['example.config.mts', 'example.config.cts', 'scripts/example.mts', 'scripts/example.cts'])(
'example.config.mts', '使用 TypeScript parser 解析 %s',
'example.config.cts', async filePath => {
'scripts/example.mts',
'scripts/example.cts',
])('使用 TypeScript parser 解析 %s', async filePath => {
const ruleIds = await lintRuleIds('const value: string = "x"; console.log(value)', filePath) const ruleIds = await lintRuleIds('const value: string = "x"; console.log(value)', filePath)
expect(ruleIds).not.toContain(null) expect(ruleIds).not.toContain(null)
}) },
)
it('忽略 Vite 生成的临时配置模块', async () => { it('忽略 Vite 生成的临时配置模块', async () => {
await expect(eslint.isPathIgnored('vite.config.ts.timestamp-1784340502076-0129cef6d3bbd8.mjs')).resolves.toBe(true) await expect(eslint.isPathIgnored('vite.config.ts.timestamp-1784340502076-0129cef6d3bbd8.mjs')).resolves.toBe(true)
}) })
it('忽略 Vite PWA 开发构建产物', async () => {
await expect(eslint.isPathIgnored('dev-dist/registerSW.js')).resolves.toBe(true)
})
it('不忽略普通 MJS 源码', async () => { it('不忽略普通 MJS 源码', async () => {
await expect(eslint.isPathIgnored('scripts/example.mjs')).resolves.toBe(false) await expect(eslint.isPathIgnored('scripts/example.mjs')).resolves.toBe(false)
}) })
+127
View File
@@ -0,0 +1,127 @@
import { execFileSync, spawnSync } from 'node:child_process'
import { mkdtempSync, mkdirSync, readFileSync, rmSync, symlinkSync, writeFileSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { dirname, resolve } from 'node:path'
import { afterEach, describe, expect, it } from 'vitest'
const selectorScript = resolve(process.cwd(), 'scripts/format-changed.mjs')
const temporaryRepositories: string[] = []
function git(repository: string, ...args: string[]) {
return execFileSync('git', args, { cwd: repository, encoding: 'utf8' }).trim()
}
function write(repository: string, filePath: string, content: string) {
const absolutePath = resolve(repository, filePath)
mkdirSync(dirname(absolutePath), { recursive: true })
writeFileSync(absolutePath, content)
}
function createRepository() {
const repository = mkdtempSync(resolve(tmpdir(), 'moviepilot-format-changed-'))
temporaryRepositories.push(repository)
git(repository, 'init', '--initial-branch=v2')
git(repository, 'config', 'user.name', 'Format Test')
git(repository, 'config', 'user.email', 'format-test@example.invalid')
write(repository, '.gitignore', 'coverage/\n')
write(repository, '.prettierignore', 'generated/\n/vite.config.*.timestamp-*.mjs\n')
write(repository, 'src/modified.ts', "export const value = 'before'\n")
write(repository, 'src/rename old.ts', "export const renamed = 'before'\n")
write(repository, 'src/deleted.ts', 'export const deleted = true\n')
write(repository, 'coverage/tracked.ts', 'export const generated = true\n')
git(repository, 'add', '.')
git(repository, 'add', '--force', 'coverage/tracked.ts')
git(repository, 'commit', '-m', 'base')
return repository
}
function listSelected(repository: string, ...args: string[]) {
const output = execFileSync(process.execPath, [selectorScript, '--list', ...args], {
cwd: repository,
encoding: 'utf8',
})
return JSON.parse(output) as string[]
}
afterEach(() => {
for (const repository of temporaryRepositories.splice(0)) {
rmSync(repository, { recursive: true, force: true })
}
})
describe('Prettier 变更文件选择器', () => {
it('按 base 与 head 选择受支持文件并排除删除、忽略和符号链接', () => {
const repository = createRepository()
git(repository, 'switch', '-c', 'feature')
write(repository, 'src/modified.ts', "export const value='after'\n")
git(repository, 'mv', 'src/rename old.ts', 'src/rename [new].ts')
git(repository, 'rm', 'src/deleted.ts')
write(repository, 'src/new file.vue', '<template><div>new</div></template>\n')
write(repository, 'src/line\nbreak.ts', 'export const lineBreak=true\n')
symlinkSync('modified.ts', resolve(repository, 'src/linked.ts'))
write(repository, 'generated/ignored.ts', 'export const ignored=true\n')
write(repository, 'coverage/tracked.ts', 'export const generated=false\n')
write(repository, 'notes.bin', 'not prettier source\n')
git(repository, 'add', '-A')
git(repository, 'commit', '-m', 'feature')
expect(listSelected(repository, '--base', 'v2', '--head', 'HEAD')).toEqual([
'src/line\nbreak.ts',
'src/modified.ts',
'src/new file.vue',
'src/rename [new].ts',
])
})
it('本地模式合并分支、工作区和未跟踪文件,并排除删除、忽略和符号链接', () => {
const repository = createRepository()
git(repository, 'switch', '-c', 'feature')
write(repository, 'src/modified.ts', "export const value='working-tree'\n")
write(repository, 'src/untracked file.ts', 'export const untracked=true\n')
write(repository, 'scripts/feature.timestamp-parser.mjs', 'export const parser=true\n')
write(repository, 'vite.config.ts.timestamp-123-generated.mjs', 'export const temporary=true\n')
symlinkSync('modified.ts', resolve(repository, 'src/untracked-link.ts'))
write(repository, 'generated/untracked.ts', 'export const ignored=true\n')
git(repository, 'rm', 'src/deleted.ts')
expect(listSelected(repository)).toEqual([
'scripts/feature.timestamp-parser.mjs',
'src/modified.ts',
'src/untracked file.ts',
])
})
it('没有受支持的变更文件时正常通过', () => {
const repository = createRepository()
expect(listSelected(repository, '--base', 'HEAD', '--head', 'HEAD')).toEqual([])
expect(
spawnSync(process.execPath, [selectorScript, '--check', '--base', 'HEAD', '--head', 'HEAD'], {
cwd: repository,
}).status,
).toBe(0)
})
it('显式比较必须同时提供 base 与 head', () => {
const repository = createRepository()
const result = spawnSync(process.execPath, [selectorScript, '--check', '--base', 'HEAD'], {
cwd: repository,
encoding: 'utf8',
})
expect(result.status).toBe(1)
expect(result.stderr).toContain('--base 与 --head 必须一起使用。')
})
it('通过参数数组安全格式化特殊文件名', () => {
const repository = createRepository()
git(repository, 'switch', '-c', 'feature')
write(repository, '--special [file].ts', 'export const special=true\n')
execFileSync(process.execPath, [selectorScript, '--write'], { cwd: repository })
expect(readFileSync(resolve(repository, '--special [file].ts'), 'utf8')).toBe('export const special = true;\n')
})
})
+5
View File
@@ -6827,6 +6827,11 @@ prelude-ls@^1.2.1:
resolved "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz" resolved "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz"
integrity sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g== integrity sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==
prettier@^3.9.5:
version "3.9.5"
resolved "https://registry.yarnpkg.com/prettier/-/prettier-3.9.5.tgz#4fec97736e33b9d0b620b48914fe93b530e835ad"
integrity sha512-/FVl766LpUfB5vXgCYOYa0MeV/441Ia99AeICQIQFTY/Nw0roZwULcXpku5i1/m5kt/baz+s4Zogspd839HSMg==
pretty-bytes@^5.3.0: pretty-bytes@^5.3.0:
version "5.6.0" version "5.6.0"
resolved "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-5.6.0.tgz" resolved "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-5.6.0.tgz"