me / scripts /format-check-changed.js
cheymin's picture
Upload 136 files
e1ae2c6 verified
const fs = require('node:fs');
const path = require('node:path');
const { execFileSync } = require('node:child_process');
const { createLogger, isVerbose, startTimer } = require('../src/generator/utils/logger');
const log = createLogger('format:check:changed');
function runGit(args, cwd, options = {}) {
const { allowFailure = false, stdio } = options;
try {
return execFileSync('git', args, { cwd, encoding: 'utf8', stdio: stdio || 'pipe' }).trim();
} catch (error) {
if (allowFailure) return null;
throw error;
}
}
function tryReadGithubEvent(eventPath) {
if (!eventPath) return null;
try {
const raw = fs.readFileSync(eventPath, 'utf8');
return JSON.parse(raw);
} catch {
return null;
}
}
function isAllZerosSha(value) {
return typeof value === 'string' && /^0{40}$/.test(value);
}
function getDiffRangeFromGithubEvent(event) {
if (!event || typeof event !== 'object') return null;
if (event.pull_request && event.pull_request.base && event.pull_request.head) {
const base = event.pull_request.base.sha;
const head = event.pull_request.head.sha;
if (base && head) return { base, head };
}
if (event.before && (event.after || event.head_commit)) {
const base = event.before;
const head = event.after || (event.head_commit && event.head_commit.id);
if (base && head && !isAllZerosSha(base)) return { base, head };
}
return null;
}
function gitObjectExists(repoRoot, sha) {
if (!sha) return false;
const result = runGit(['cat-file', '-e', `${sha}^{commit}`], repoRoot, { allowFailure: true });
return result !== null;
}
function isShallowRepository(repoRoot) {
const result = runGit(['rev-parse', '--is-shallow-repository'], repoRoot, { allowFailure: true });
return result === 'true';
}
function tryFetchMoreHistory(repoRoot) {
// 仅在 CI 场景兜底:actions/checkout 若是浅克隆,可能缺少 base commit,导致 diff range 失败
try {
if (isShallowRepository(repoRoot)) {
execFileSync('git', ['fetch', '--prune', '--no-tags', '--unshallow'], {
cwd: repoRoot,
stdio: 'inherit',
});
return true;
}
} catch {
// ignore
}
try {
execFileSync('git', ['fetch', '--prune', '--no-tags', '--depth=200', 'origin'], {
cwd: repoRoot,
stdio: 'inherit',
});
return true;
} catch {
return false;
}
}
function collectHeadChangedFiles(repoRoot) {
const output = runGit(
['show', '--name-only', '--diff-filter=ACMR', '--pretty=format:', 'HEAD'],
repoRoot,
{ allowFailure: true }
);
if (!output) return [];
return output
.split('\n')
.map((line) => line.trim())
.filter(Boolean);
}
function collectChangedFiles(repoRoot, range) {
if (!range) return [];
const diffArgs = ['diff', '--name-only', '--diff-filter=ACMR', `${range.base}..${range.head}`];
const baseExists = gitObjectExists(repoRoot, range.base);
const headExists = gitObjectExists(repoRoot, range.head);
if (!baseExists || !headExists) {
log.warn('检测到 diff range 所需提交缺失,尝试补全 git 历史(避免浅克隆导致失败)');
tryFetchMoreHistory(repoRoot);
}
const output = runGit(diffArgs, repoRoot, { allowFailure: true });
if (!output) {
log.warn('无法计算 revision range,回退为 HEAD 变更文件(可能仅覆盖最后一次提交)');
return collectHeadChangedFiles(repoRoot);
}
return output
.split('\n')
.map((line) => line.trim())
.filter(Boolean);
}
function collectWorkingTreeChangedFiles(repoRoot) {
const files = new Set();
const unstaged = runGit(['diff', '--name-only', '--diff-filter=ACMR', 'HEAD'], repoRoot, {
allowFailure: true,
});
const staged = runGit(['diff', '--cached', '--name-only', '--diff-filter=ACMR'], repoRoot, {
allowFailure: true,
});
[unstaged, staged].forEach((block) => {
if (!block) return;
block
.split('\n')
.map((line) => line.trim())
.filter(Boolean)
.forEach((filePath) => files.add(filePath));
});
return Array.from(files).sort();
}
function shouldCheckFile(filePath) {
const normalized = filePath.split(path.sep).join('/');
if (normalized === 'package-lock.json') return false;
// 这些文件历史上未统一为 Prettier 风格;避免为了启用检查产生巨量格式化 diff
if (normalized === 'src/generator/main.js' || normalized === 'src/runtime/index.js') return false;
// 与现有 npm scripts 的检查范围对齐:不检查 docs/ 与 templates/
const allowedRoots = ['src/', 'scripts/', 'test/', '.github/', 'config/'];
const isRootFile = !normalized.includes('/');
const hasAllowedRoot = allowedRoots.some((prefix) => normalized.startsWith(prefix));
const isAllowedPath =
hasAllowedRoot || (isRootFile && (normalized.endsWith('.md') || normalized.endsWith('.json')));
if (!isAllowedPath) return false;
const ext = path.extname(normalized).toLowerCase();
return ['.js', '.json', '.md', '.yml', '.yaml'].includes(ext);
}
function resolvePrettierBin(repoRoot) {
const base = path.join(repoRoot, 'node_modules', '.bin', 'prettier');
if (fs.existsSync(base)) return base;
if (fs.existsSync(`${base}.cmd`)) return `${base}.cmd`;
return null;
}
function main() {
const repoRoot = path.resolve(__dirname, '..');
const elapsedMs = startTimer();
log.info('开始');
const event = tryReadGithubEvent(process.env.GITHUB_EVENT_PATH);
const range = getDiffRangeFromGithubEvent(event);
const candidateFiles = range
? collectChangedFiles(repoRoot, range)
: collectWorkingTreeChangedFiles(repoRoot);
const filesToCheck = candidateFiles.filter(shouldCheckFile);
if (filesToCheck.length === 0) {
log.ok('未发现需要检查的文件,跳过');
return;
}
const prettierBin = resolvePrettierBin(repoRoot);
if (!prettierBin) {
log.error('未找到 prettier,可先运行 npm ci / npm install');
process.exitCode = 1;
return;
}
log.info('准备检查文件格式', { files: filesToCheck.length });
if (isVerbose()) {
filesToCheck.forEach((filePath) => log.info('待检查', { file: filePath }));
}
try {
execFileSync(prettierBin, ['--check', ...filesToCheck], { cwd: repoRoot, stdio: 'inherit' });
log.ok('通过', { ms: elapsedMs(), files: filesToCheck.length });
} catch (error) {
log.error('未通过', {
ms: elapsedMs(),
files: filesToCheck.length,
exit: error && error.status ? error.status : 1,
});
process.exitCode = error && error.status ? error.status : 1;
}
}
main();