#!/usr/bin/env node import { createHash } from 'node:crypto'; import { existsSync, readdirSync, readFileSync } from 'node:fs'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..'); const baselinePath = path.join(repoRoot, 'scripts', 'server-node-freeze-baseline.json'); const needle = 'server-node'; const ignoredDirectories = new Set([ '.git', '.codex', '.codex-temp', '.idea', 'node_modules', 'dist', 'build', 'coverage', 'target', 'logs', ]); const ignoredFiles = new Set([ 'scripts/check-server-node-freeze.mjs', 'scripts/server-node-freeze-baseline.json', 'scripts/server-node-frozen.mjs', 'docs/audits/engineering/SERVER_NODE_FREEZE_AND_DEPRECATION_2026-04-24.md', ]); const allowedExtensions = new Set([ '.cjs', '.js', '.json', '.md', '.mjs', '.ps1', '.rs', '.toml', '.ts', '.tsx', '.yaml', '.yml', ]); function toRepoPath(absolutePath) { return path.relative(repoRoot, absolutePath).replaceAll(path.sep, '/'); } function hashLine(line) { return createHash('sha256').update(line.trim()).digest('hex'); } function walk(directory, output) { for (const entry of readdirSync(directory, { withFileTypes: true })) { if (entry.isDirectory()) { if (!ignoredDirectories.has(entry.name)) { walk(path.join(directory, entry.name), output); } continue; } const absolutePath = path.join(directory, entry.name); const repoPath = toRepoPath(absolutePath); if (ignoredFiles.has(repoPath)) { continue; } if (!allowedExtensions.has(path.extname(entry.name).toLowerCase())) { continue; } output.push(absolutePath); } } function collectReferences() { const files = []; walk(repoRoot, files); const references = new Map(); for (const file of files) { const repoPath = toRepoPath(file); const content = readFileSync(file, 'utf8'); const lines = content.split(/\r?\n/u); for (const line of lines) { if (!line.toLowerCase().includes(needle)) { continue; } const key = `${repoPath}\u0000${hashLine(line)}`; references.set(key, (references.get(key) || 0) + 1); } } return references; } function parseBaseline() { if (!existsSync(baselinePath)) { return new Map(); } const baseline = JSON.parse(readFileSync(baselinePath, 'utf8')); return new Map(Object.entries(baseline.references || {})); } const currentReferences = collectReferences(); const baselineReferences = parseBaseline(); const newReferences = []; for (const [key, count] of currentReferences.entries()) { const allowedCount = baselineReferences.get(key) || 0; if (count > allowedCount) { const [repoPath] = key.split('\u0000'); newReferences.push({ repoPath, count: count - allowedCount }); } } if (newReferences.length > 0) { console.error('检测到冻结后新增的 server-node 引用,请迁移到 server-rs 或更新废弃审计后再处理:'); for (const reference of newReferences.slice(0, 50)) { console.error(`- ${reference.repoPath} (+${reference.count})`); } if (newReferences.length > 50) { console.error(`... 另有 ${newReferences.length - 50} 处`); } process.exit(1); } console.log('server-node freeze guard passed: 未发现冻结后新增引用。');