增加提交前 TypeScript 自动格式化
Project CI / Repository checks (push) Successful in 53s
Project CI / Frontend tests (push) Successful in 3m9s
Project CI / Backend tests (push) Successful in 4m19s
Project CI / Native shell tests (push) Successful in 16m9s

接入 Husky 与 lint-staged,仅格式化暂存的 TS/TSX 文件
补充部分暂存和未暂存内容隔离回归测试
更新共享开发工作流中的提交前格式化规则
This commit is contained in:
2026-08-12 10:43:29 +08:00
parent 1363b9374c
commit e118d66699
5 changed files with 1004 additions and 1 deletions
+1
View File
@@ -0,0 +1 @@
npm run format:staged
@@ -611,6 +611,14 @@ npm run check:server-rs-ddd
- 页面交互 smoke
- 移动端视口检查
### 提交前 TypeScript 自动格式化
仓库级 Git `pre-commit` hook 通过 `lint-staged`,只对当前已暂存的 `*.ts``*.tsx` 文件运行 Prettier 自动格式化,并把格式化结果更新到本次提交的暂存区;未暂存的其他文件不进入格式化范围。格式化或暂存恢复失败时提交会中止,应先处理失败原因并重新检查 staged diff,不能等 CI 再暴露格式问题。
部分暂存同一 TS / TSX 文件时,`lint-staged` 会临时隐藏该文件未暂存的改动,以暂存快照执行格式化,随后恢复未暂存内容。因此提交前后都应分别检查 `git diff --cached``git diff`,确认格式化后的暂存内容属于本次提交,未暂存工作没有被误带入;若恢复产生冲突,先人工整理暂存边界再重新提交。
`git commit --no-verify` 会绕过该 hook,只允许在已明确原因的紧急场景使用。绕过时仍须对本次暂存的 TS / TSX 文件手动执行等价的 Prettier 格式化、重新暂存并核对 staged diff`--no-verify` 不代表可以跳过格式化或其他提交门禁。
前端原则:
- 移动端优先,再兼容网页端。
+895
View File
File diff suppressed because it is too large Load Diff
+9 -1
View File
@@ -4,6 +4,7 @@
"version": "0.0.0",
"type": "module",
"scripts": {
"prepare": "husky",
"dev": "node scripts/dev.mjs",
"dev:spacetime": "node scripts/dev.mjs spacetime",
"dev:api-server": "node scripts/dev.mjs api-server",
@@ -38,6 +39,7 @@
"preview": "node scripts/vite-cli.mjs preview",
"clean": "node -e \"require('fs').rmSync('dist', { recursive: true, force: true })\"",
"check:encoding": "node scripts/check-encoding.mjs",
"check:pre-commit-format": "node --test scripts/pre-commit-format.test.mjs",
"check:rustfmt": "cargo fmt --all --manifest-path server-rs/Cargo.toml -- --check",
"check:spacetime-schema": "node scripts/check-spacetime-schema-guard.mjs",
"check:production-ops": "node scripts/check-production-ops-guardrails.mjs",
@@ -88,10 +90,11 @@
"lint:guardrails": "npm run lint:eslint",
"typecheck": "tsc -p tsconfig.typecheck-guardrails.json --noEmit",
"typecheck:guardrails": "npm run typecheck",
"lint": "npm run check:encoding && npm run check:rustfmt && npm run check:spacetime-schema && npm run check:production-ops && npm run check:maintenance-page && npm run lint:eslint && npm run typecheck",
"lint": "npm run check:encoding && npm run check:pre-commit-format && npm run check:rustfmt && npm run check:spacetime-schema && npm run check:production-ops && npm run check:maintenance-page && npm run lint:eslint && npm run typecheck",
"lint:fix": "eslint . --ext .ts,.tsx,.js,.mjs,.cjs --fix && prettier --write .",
"format": "prettier --write .",
"format:check": "prettier --check .",
"format:staged": "lint-staged",
"test": "vitest run",
"test:watch": "vitest",
"container:init": "node scripts/container-compose.mjs init",
@@ -205,6 +208,9 @@
"vite": "^6.2.0",
"zustand": "^5.0.14"
},
"lint-staged": {
"*.{ts,tsx}": "prettier --write"
},
"devDependencies": {
"@colbymchenry/codegraph": "^0.8.0",
"@tauri-apps/cli": "^2.11.2",
@@ -226,7 +232,9 @@
"eslint-plugin-simple-import-sort": "^10.0.0",
"eslint-plugin-unused-imports": "^3.2.0",
"globals": "^13.24.0",
"husky": "9.1.7",
"jsdom": "^22.1.0",
"lint-staged": "16.4.0",
"prettier": "^3.3.3",
"tailwindcss": "^4.1.14",
"tsx": "^4.21.0",
+91
View File
@@ -0,0 +1,91 @@
import assert from 'node:assert/strict';
import { execFileSync, spawnSync } from 'node:child_process';
import { mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { delimiter, dirname, join, resolve } from 'node:path';
import { test } from 'node:test';
import { fileURLToPath } from 'node:url';
const repoRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..');
const packageJson = JSON.parse(
readFileSync(join(repoRoot, 'package.json'), 'utf8'),
);
test('pre-commit hook 仅格式化暂存的 TypeScript 快照', () => {
assert.equal(packageJson.scripts.prepare, 'husky');
assert.equal(packageJson.scripts['format:staged'], 'lint-staged');
assert.deepEqual(packageJson['lint-staged'], {
'*.{ts,tsx}': 'prettier --write',
});
assert.equal(
readFileSync(join(repoRoot, '.husky', 'pre-commit'), 'utf8'),
'npm run format:staged\n',
);
const tempRepo = mkdtempSync(
join(tmpdir(), 'genarrative-pre-commit-format-'),
);
try {
git(tempRepo, 'init', '--quiet');
git(tempRepo, 'config', 'user.email', 'pre-commit-test@example.invalid');
git(tempRepo, 'config', 'user.name', 'Pre-commit Test');
const sourcePath = join(tempRepo, 'sample.ts');
writeFileSync(sourcePath, 'const original = 1;\nconst keep = 2;\n');
git(tempRepo, 'add', 'sample.ts');
git(
tempRepo,
'-c',
'commit.gpgsign=false',
'commit',
'--quiet',
'-m',
'baseline',
);
writeFileSync(sourcePath, 'const original={value:1}\nconst keep = 2;\n');
git(tempRepo, 'add', 'sample.ts');
writeFileSync(
sourcePath,
'const original={value:1}\nconst keep={unstaged:true}\n',
);
const lintStaged = spawnSync(
process.execPath,
[
join(repoRoot, 'node_modules', 'lint-staged', 'bin', 'lint-staged.js'),
'--config',
'-',
],
{
cwd: tempRepo,
encoding: 'utf8',
env: {
...process.env,
PATH: `${join(repoRoot, 'node_modules', '.bin')}${delimiter}${process.env.PATH ?? ''}`,
},
input: JSON.stringify(packageJson['lint-staged']),
},
);
assert.equal(
lintStaged.status,
0,
`${lintStaged.stdout ?? ''}${lintStaged.stderr ?? ''}`,
);
assert.equal(
git(tempRepo, 'show', ':sample.ts'),
'const original = { value: 1 };\nconst keep = 2;\n',
);
assert.equal(
readFileSync(sourcePath, 'utf8'),
'const original = { value: 1 };\nconst keep={unstaged:true}\n',
);
} finally {
rmSync(tempRepo, { force: true, recursive: true });
}
});
function git(cwd, ...args) {
return execFileSync('git', args, { cwd, encoding: 'utf8' });
}