#!/usr/bin/env node /** * 代码质量全面检查脚本 * 检查所有变更的完整性和代码质量 */ const BuildLogger = require('./logger.js'); const fs = require('fs'); const path = require('path'); const { execSync, spawnSync } = require('child_process'); const projectRoot = path.resolve(__dirname, '..'); const projectRootPrefix = projectRoot.endsWith(path.sep) ? projectRoot : `${projectRoot}${path.sep}`; // 检查项配置 const checks = { // 1. 语法检查(对齐当前生产路径:build-static + Netlify Functions) syntaxCheck: { name: '语法检查', files: [ 'server.js', 'scripts/build-static.js', 'scripts/transpile-dist-js.js', 'scripts/check-asset-links.js', 'src/js/middleware/validation.js', 'netlify/functions/_shared/middleware.js', 'netlify/functions/_shared/cors.js', 'netlify/functions/_shared/rate-limiter.js', 'netlify/functions/health.js', 'netlify/functions/public-config.js', 'netlify/functions/notifications.js', 'netlify/functions/giffgaff-graphql.js', 'netlify/functions/giffgaff-mfa-challenge.js', 'netlify/functions/giffgaff-mfa-validation.js', 'netlify/functions/giffgaff-sms-activate.js', 'netlify/functions/auto-activate-esim.js', 'netlify/functions/giffgaff-token-exchange.js', 'netlify/functions/verify-cookie.js', 'netlify/edge-functions/bff-proxy.js', 'netlify/edge-functions/markdown-negotiation.js' ] }, // 2. 环境变量一致性 envVarCheck: { name: '环境变量一致性', required: ['ACCESS_KEY', 'ALLOWED_ORIGIN'], deprecated: ['ESIM_ACCESS_KEY', 'COOKIE_SECRET'] }, // 3. 依赖完整性 dependencyCheck: { name: '依赖完整性', unused: ['cookie-parser'] }, // 4. 安全配置 securityCheck: { name: '安全配置', patterns: { weakDefaults: /please_change_me|your-secret-key-here|your-key-here/g, hardcodedSecrets: /(?:password|secret|key)\s*=\s*['"][^'"]{10,}['"]/gi, consoleLog: /console\.log\(/g } } }; let totalChecks = 0; let passedChecks = 0; let failedChecks = 0; /** * 将仓库相对路径解析为绝对路径,并确保结果仍在 projectRoot 内。 * 拒绝空路径、绝对路径与目录穿越,避免 child_process / fs 误用仓库外文件。 * @param {string} relativePath * @returns {string|null} */ function resolveSafePath(relativePath) { if (typeof relativePath !== 'string' || relativePath.length === 0) { return null; } if (path.isAbsolute(relativePath)) { return null; } const fullPath = path.resolve(projectRoot, relativePath); if (fullPath !== projectRoot && !fullPath.startsWith(projectRootPrefix)) { return null; } return fullPath; } // 辅助函数 function checkFile(filePath) { const fullPath = resolveSafePath(filePath); if (!fullPath) { BuildLogger.error(`非法路径: ${filePath}`); return false; } if (!fs.existsSync(fullPath)) { BuildLogger.error(`文件不存在: ${filePath}`); return false; } // 使用参数数组调用,避免 shell 拼接带来的命令注入面 const result = spawnSync('node', ['-c', fullPath], { stdio: 'pipe', encoding: 'utf8' }); if (result.error) { BuildLogger.error(`语法检查失败: ${filePath}`); BuildLogger.error(result.error.message); return false; } if (result.status !== 0) { BuildLogger.error(`语法错误: ${filePath}`); const detail = (result.stderr && result.stderr.trim()) || (result.stdout && result.stdout.trim()) || `node -c 退出码 ${result.status}${result.signal ? ` (signal ${result.signal})` : ''}`; BuildLogger.error(detail); return false; } return true; } function checkEnvExample() { const envPath = path.join(projectRoot, 'env.example'); const content = fs.readFileSync(envPath, 'utf8'); const issues = []; // 检查必需变量 checks.envVarCheck.required.forEach(varName => { if (!content.includes(`${varName}=`)) { issues.push(`缺少必需环境变量: ${varName}`); } }); // 检查废弃变量 checks.envVarCheck.deprecated.forEach(varName => { if (content.includes(`${varName}=`)) { issues.push(`包含废弃环境变量: ${varName}`); } }); return issues; } /** * 在给定相对路径列表中搜索正则匹配。 * 非法路径不会静默跳过,而是记入 invalidFiles,由调用方判定失败。 * @returns {{ results: Array<{file: string, matches: number}>, invalidFiles: string[] }} */ function searchInFiles(pattern, files, excludeContext = []) { const results = []; const invalidFiles = []; files.forEach(file => { const fullPath = resolveSafePath(file); if (!fullPath) { invalidFiles.push(file || '(empty)'); return; } if (!fs.existsSync(fullPath)) { return; } const content = fs.readFileSync(fullPath, 'utf8'); const lines = content.split('\n'); let matchCount = 0; lines.forEach((line, index) => { if (pattern.test(line)) { // 检查是否在排除的上下文中(如安全检查代码) const isExcluded = excludeContext.some(ctx => { const contextLine = lines[index]; const prevLine = lines[index - 1] || ''; return contextLine.includes(ctx) || prevLine.includes(ctx); }); if (!isExcluded) { matchCount++; } } }); if (matchCount > 0) { results.push({ file, matches: matchCount }); } }); return { results, invalidFiles }; } function checkPackageJson() { const pkgPath = path.join(projectRoot, 'package.json'); const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8')); const issues = []; checks.dependencyCheck.unused.forEach(dep => { if (pkg.dependencies && pkg.dependencies[dep]) { issues.push(`未使用的依赖: ${dep} (dependencies)`); } if (pkg.devDependencies && pkg.devDependencies[dep]) { issues.push(`未使用的依赖: ${dep} (devDependencies)`); } }); return issues; } // 执行检查 function runChecks() { BuildLogger.title('代码质量全面检查'); // 1. 语法检查 BuildLogger.step(1, 5, checks.syntaxCheck.name); let syntaxPassed = 0; checks.syntaxCheck.files.forEach(file => { totalChecks++; if (checkFile(file)) { BuildLogger.check(true, file); syntaxPassed++; passedChecks++; } else { BuildLogger.check(false, file); failedChecks++; } }); BuildLogger.progress(`${syntaxPassed}/${checks.syntaxCheck.files.length} 文件通过语法检查\n`); // 2. 环境变量检查 BuildLogger.step(2, 5, checks.envVarCheck.name); totalChecks++; const envIssues = checkEnvExample(); if (envIssues.length === 0) { BuildLogger.check(true, 'env.example 配置正确'); passedChecks++; } else { BuildLogger.check(false, 'env.example 存在问题:'); envIssues.forEach(issue => BuildLogger.error(` - ${issue}`)); failedChecks++; } // 3. 依赖检查 BuildLogger.step(3, 5, checks.dependencyCheck.name); totalChecks++; const depIssues = checkPackageJson(); if (depIssues.length === 0) { BuildLogger.check(true, 'package.json 依赖正确'); passedChecks++; } else { BuildLogger.check(false, 'package.json 存在问题:'); depIssues.forEach(issue => BuildLogger.warn(` - ${issue}`)); failedChecks++; } // 4. 安全配置检查 BuildLogger.step(4, 5, checks.securityCheck.name); // 检查弱密钥(排除安全检查代码中的引用) totalChecks++; const { results: weakDefaults, invalidFiles: weakDefaultInvalidFiles } = searchInFiles( checks.securityCheck.patterns.weakDefaults, ['env.example', 'netlify/functions/_shared/middleware.js'], ['if (ACCESS_KEY ===', '安全警告', '警告', '检查'] ); if (weakDefaultInvalidFiles.length > 0) { BuildLogger.check(false, '弱默认配置扫描路径非法:'); weakDefaultInvalidFiles.forEach((file) => BuildLogger.error(` - ${file}`)); failedChecks++; } else if (weakDefaults.length === 0) { BuildLogger.check(true, '无弱默认配置'); passedChecks++; } else { BuildLogger.check(false, '发现弱默认配置:'); weakDefaults.forEach(r => BuildLogger.warn(` - ${r.file}: ${r.matches}处`)); failedChecks++; } // 5. 生产资源脱链检查(禁止发布不存在的静态路径) BuildLogger.step(5, 5, '生产资源脱链检查'); totalChecks++; try { execSync('node scripts/check-asset-links.js', { cwd: projectRoot, stdio: 'pipe', encoding: 'utf8' }); BuildLogger.check(true, 'HTML 本地资源引用完整'); passedChecks++; } catch (error) { BuildLogger.check(false, '发现脱链或无效路径'); const output = `${error.stdout || ''}${error.stderr || ''}${error.message || ''}`; output.split('\n').filter(Boolean).forEach((line) => BuildLogger.error(` ${line}`)); failedChecks++; } // 统计报告 BuildLogger.title('\n检查结果汇总'); BuildLogger.log(`总检查项: ${totalChecks}`); BuildLogger.success(`通过: ${passedChecks}`); if (failedChecks > 0) { BuildLogger.error(`失败: ${failedChecks}`); } const successRate = ((passedChecks / totalChecks) * 100).toFixed(1); BuildLogger.log(`\n通过率: ${successRate}%`); if (failedChecks === 0) { BuildLogger.success('\n✅ 所有检查通过! 代码质量良好。'); return 0; } else { BuildLogger.error('\n❌ 存在质量问题,请修复后重新检查。'); return 1; } } // 执行 const exitCode = runChecks(); process.exit(exitCode);