#!/usr/bin/env node /** * 契约快照门禁 —— 编码规范 §11.5。 * * tsc --emitDeclarationOnly 出全量 .d.ts,只留**契约面**落 api-report/: * - packages/contracts/** (前后端唯一共享点,§8.1) * - packages//src/index.d.ts * - apps/api/src/modules//{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);