Files
eSIM-Tools/scripts/pre-commit-check.js
coderabbitai[bot] 9f0feccc35 📝 Add docstrings to feat/remove-turnstile-and-fixes
Docstrings generation was requested by @Silentely.

The following files were modified:

* `scripts/pre-commit-check.js`

These file types are not supported:
* `CLAUDE.md`
* `netlify.toml`
* `netlify/edge-functions/CLAUDE.md`
* `netlify/functions/CLAUDE.md`
* `src/giffgaff/giffgaff_modular.html`
* `src/simyo/simyo_modular.html`
2026-05-18 12:37:32 +00:00

236 lines
6.2 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env node
/**
* pre-commit 检查:
* 1) 仅处理 staged 文件安全格式化LF、行尾空白、文件尾换行
* 2) 检查 staged JS 语法;
* 3) 检查 staged JSON 语法;
* 4) 拦截代码文件中的智能引号,避免引号边界问题;
* 5) 保护 legacy 完整版页面,禁止提交其变更。
*/
const fs = require('fs');
const os = require('os');
const path = require('path');
const { execFileSync, spawnSync } = require('child_process');
let esbuild = null;
try {
esbuild = require('esbuild');
} catch (_) {
esbuild = null;
}
const TEXT_EXTENSIONS = new Set([
'.js',
'.mjs',
'.cjs',
'.json',
'.md',
'.css',
'.html',
'.yml',
'.yaml',
'.sh'
]);
const JS_EXTENSIONS = new Set(['.js', '.mjs', '.cjs']);
const SMART_QUOTES_RE = /[\u2018\u2019\u201C\u201D]/u;
const PROTECTED_FILES = [];
function git(args, options = {}) {
return execFileSync('git', args, {
encoding: 'utf8',
stdio: ['ignore', 'pipe', 'pipe'],
...options
});
}
function getStagedFiles() {
const output = git([
'diff',
'--cached',
'--name-only',
'--diff-filter=ACMR',
'-z'
]);
return output.split('\0').filter(Boolean);
}
/**
* Get the list of protected file paths that currently have staged changes.
* @returns {string[]} Array of relative paths for protected files that are staged; empty if none.
*/
function getTouchedProtectedFiles() {
const output = git([
'diff',
'--cached',
'--name-only',
'-z',
'--',
...PROTECTED_FILES
]);
return output.split('\0').filter(Boolean);
}
/**
* Determine whether a file should be normalized (text formatting enforced) based on its path.
* @param {string} relPath - File path relative to the repository root.
* @returns {boolean} `true` if the file extension is in TEXT_EXTENSIONS or the basename is `.gitignore` or `.gitattributes`, `false` otherwise.
*/
function shouldFormat(relPath) {
const ext = path.extname(relPath).toLowerCase();
if (TEXT_EXTENSIONS.has(ext)) {
return true;
}
const baseName = path.basename(relPath);
return baseName === '.gitignore' || baseName === '.gitattributes';
}
function normalizeText(content) {
let next = content.replace(/\r\n/g, '\n');
next = next.replace(/[ \t]+$/gm, '');
if (next.length > 0 && !next.endsWith('\n')) {
next += '\n';
}
return next;
}
function formatStagedFiles(stagedFiles) {
const updated = [];
for (const relPath of stagedFiles) {
if (!shouldFormat(relPath)) {
continue;
}
const absPath = path.resolve(relPath);
if (!fs.existsSync(absPath)) {
continue;
}
const original = fs.readFileSync(absPath, 'utf8');
const normalized = normalizeText(original);
if (normalized !== original) {
fs.writeFileSync(absPath, normalized, 'utf8');
git(['add', '--', relPath]);
updated.push(relPath);
}
}
return updated;
}
function readStagedContent(relPath) {
return git(['show', `:${relPath}`]);
}
function fallbackNodeSyntaxCheck(relPath, content) {
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'esim-precommit-'));
const ext = path.extname(relPath).toLowerCase() || '.js';
const tempFile = path.join(tempDir, `check${ext}`);
try {
fs.writeFileSync(tempFile, content, 'utf8');
const result = spawnSync(process.execPath, ['--check', tempFile], {
encoding: 'utf8'
});
if (result.status !== 0) {
throw new Error((result.stderr || result.stdout || '').trim());
}
} finally {
fs.rmSync(tempDir, { recursive: true, force: true });
}
}
function checkJavaScriptSyntax(relPath, content) {
if (esbuild) {
esbuild.transformSync(content, {
loader: 'js',
sourcefile: relPath,
charset: 'utf8'
});
return;
}
fallbackNodeSyntaxCheck(relPath, content);
}
function runChecks(stagedFiles) {
const failures = [];
for (const relPath of stagedFiles) {
const ext = path.extname(relPath).toLowerCase();
if (!JS_EXTENSIONS.has(ext) && ext !== '.json') {
continue;
}
const content = readStagedContent(relPath);
if (SMART_QUOTES_RE.test(content)) {
failures.push(
`${relPath}: 检测到智能引号U+201C/U+201D/U+2018/U+2019请改为标准半角引号。`
);
continue;
}
try {
if (JS_EXTENSIONS.has(ext)) {
checkJavaScriptSyntax(relPath, content);
} else if (ext === '.json') {
JSON.parse(content);
}
} catch (error) {
failures.push(`${relPath}: ${error.message}`);
}
}
return failures;
}
/**
* Run pre-commit validation and normalization for staged files, aborting the commit on violations.
*
* Checks for changes to protected files and exits with status 1 if any are touched. If there are no
* staged files, exits with status 0. For staged text files, normalizes line endings, trims trailing
* whitespace, ensures a trailing newline, writes changes back to the working tree and re-stages them.
* Then validates staged JavaScript and JSON files for smart-quote characters and syntax errors;
* any validation failures are printed and cause an exit with status 1. On success, prints a pass message.
*
* Exit codes:
* - 0: no staged files (early exit) or checks passed
* - 1: protected file touched or one or more validation failures
*/
function main() {
const touchedProtectedFiles = PROTECTED_FILES.length > 0 ? getTouchedProtectedFiles() : [];
if (touchedProtectedFiles.length > 0) {
console.error('\n❌ pre-commit 检查失败:');
for (const relPath of touchedProtectedFiles) {
console.error(` - ${relPath}: 该文件受保护,禁止提交改动。`);
}
console.error(' - 如确需调整,请改为维护对应 modular 版本文件。');
process.exit(1);
}
const stagedFiles = getStagedFiles();
if (stagedFiles.length === 0) {
process.exit(0);
}
const formatted = formatStagedFiles(stagedFiles);
if (formatted.length > 0) {
console.log(`✅ 已格式化并重新暂存 ${formatted.length} 个文件`);
}
const failures = runChecks(stagedFiles);
if (failures.length > 0) {
console.error('\n❌ pre-commit 检查失败:');
for (const failure of failures) {
console.error(` - ${failure}`);
}
process.exit(1);
}
console.log('✅ pre-commit 检查通过');
}
main();