- pnpm workspaces / tsconfig 双工程(node · web)/ 路径别名(§11.1) - eslint flat(§11.3 + §12.5 可机检项)/ dependency-cruiser 基线八条 + JCAI 增补(§11.4) - tools/module-tiers.json + gen-depcruise-tiers.ts 生成 tier-order 逐层规则 - tools/api-report.mjs 契约快照只留契约面(ADR-005) - docker-compose(PG16)+ db/init 四 schema;种子数据五岗位五员工冻结为常量 - docs/debt.md · docs/adr/ADR-005 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KeDBx2PHLavbURXB8LJJ7m
108 lines
3.4 KiB
JavaScript
108 lines
3.4 KiB
JavaScript
#!/usr/bin/env node
|
|
/**
|
|
* 契约快照门禁 —— 编码规范 §11.5。
|
|
*
|
|
* tsc --emitDeclarationOnly 出全量 .d.ts,只留**契约面**落 api-report/:
|
|
* - packages/contracts/** (前后端唯一共享点,§8.1)
|
|
* - packages/<pkg>/src/index.d.ts
|
|
* - apps/api/src/modules/<m>/{index,contract,port}.d.ts
|
|
* 然后与 git 比对:改 internal/ 时 api-report/ 纹丝不动;碰了公开契约,diff 立刻可见。
|
|
*/
|
|
import { execFileSync } from 'node:child_process';
|
|
import { cpSync, existsSync, mkdirSync, readdirSync, rmSync, statSync } from 'node:fs';
|
|
import { dirname, join, relative, sep } from 'node:path';
|
|
import { fileURLToPath } from 'node:url';
|
|
|
|
const root = dirname(dirname(fileURLToPath(import.meta.url)));
|
|
const tmp = join(root, '.api-tmp');
|
|
const out = join(root, 'api-report');
|
|
|
|
function run(cmd, args, opts = {}) {
|
|
return execFileSync(cmd, args, { cwd: root, encoding: 'utf8', stdio: 'pipe', ...opts });
|
|
}
|
|
|
|
rmSync(tmp, { recursive: true, force: true });
|
|
|
|
try {
|
|
run('node', [
|
|
join(root, 'node_modules', 'typescript', 'bin', 'tsc'),
|
|
'-p',
|
|
'tsconfig.node.json',
|
|
'--emitDeclarationOnly',
|
|
'--declaration',
|
|
'--noEmit',
|
|
'false',
|
|
'--outDir',
|
|
'.api-tmp',
|
|
]);
|
|
} catch (err) {
|
|
process.stderr.write((err.stdout ?? '') + (err.stderr ?? ''));
|
|
process.stderr.write('\napi:check —— 声明生成失败,先修 tsc。\n');
|
|
process.exit(1);
|
|
}
|
|
|
|
/** 契约面判据:相对 .api-tmp 的路径 */
|
|
function isContractFace(rel) {
|
|
const p = rel.split(sep).join('/');
|
|
if (p.startsWith('packages/contracts/')) return true;
|
|
if (/^packages\/[^/]+\/src\/index\.d\.ts$/.test(p)) return true;
|
|
if (/^apps\/api\/src\/modules\/[^/]+\/(index|contract|port)\.d\.ts$/.test(p)) return true;
|
|
return false;
|
|
}
|
|
|
|
function walk(dir, acc = []) {
|
|
if (!existsSync(dir)) return acc;
|
|
for (const name of readdirSync(dir)) {
|
|
const full = join(dir, name);
|
|
if (statSync(full).isDirectory()) walk(full, acc);
|
|
else if (full.endsWith('.d.ts')) acc.push(full);
|
|
}
|
|
return acc;
|
|
}
|
|
|
|
rmSync(out, { recursive: true, force: true });
|
|
mkdirSync(out, { recursive: true });
|
|
|
|
let kept = 0;
|
|
for (const file of walk(tmp)) {
|
|
const rel = relative(tmp, file);
|
|
if (!isContractFace(rel)) continue;
|
|
const dest = join(out, rel);
|
|
mkdirSync(dirname(dest), { recursive: true });
|
|
cpSync(file, dest);
|
|
kept += 1;
|
|
}
|
|
rmSync(tmp, { recursive: true, force: true });
|
|
|
|
process.stdout.write(`api:check —— 契约面 ${kept} 个声明文件 → api-report/\n`);
|
|
|
|
if (process.argv.includes('--update')) {
|
|
process.stdout.write('api:check —— --update:已刷新快照,请连同变更申请一并提交。\n');
|
|
process.exit(0);
|
|
}
|
|
|
|
let status = '';
|
|
try {
|
|
status = run('git', ['status', '--porcelain', '--', 'api-report']);
|
|
} catch {
|
|
process.stdout.write('api:check —— 非 git 仓库,跳过比对。\n');
|
|
process.exit(0);
|
|
}
|
|
|
|
if (status.trim() === '') {
|
|
process.stdout.write('api:check —— 公开契约无变化 ✔\n');
|
|
process.exit(0);
|
|
}
|
|
|
|
process.stderr.write('\napi:check —— 公开契约发生变化(§12.4):\n' + status + '\n');
|
|
try {
|
|
process.stderr.write(run('git', ['diff', '--', 'api-report']));
|
|
} catch {
|
|
/* 首次建立基线时无 diff 可打 */
|
|
}
|
|
process.stderr.write(
|
|
'\n契约变更须:①depcruise 反查下游 ②同步 CONTRACT.md ③本快照单独说明 ④破坏性走三步弃用。\n' +
|
|
'确认无误后:pnpm api:check --update && git add api-report\n',
|
|
);
|
|
process.exit(1);
|