增加提交前 TypeScript 自动格式化
接入 Husky 与 lint-staged,仅格式化暂存的 TS/TSX 文件 补充部分暂存和未暂存内容隔离回归测试 更新共享开发工作流中的提交前格式化规则
This commit is contained in:
Executable
+1
@@ -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` 不代表可以跳过格式化或其他提交门禁。
|
||||
|
||||
前端原则:
|
||||
|
||||
- 移动端优先,再兼容网页端。
|
||||
|
||||
Generated
+895
File diff suppressed because it is too large
Load Diff
+9
-1
@@ -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",
|
||||
|
||||
@@ -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' });
|
||||
}
|
||||
Reference in New Issue
Block a user