mirror of
https://github.com/Silentely/eSIM-Tools.git
synced 2026-09-03 06:24:20 +08:00
🔧 chore: 增加生产资源脱链检查并完善质量门禁
新增 check-asset-links,接入 quality-check 与 pre-push;修正 resource-hints 中已失效的 Webpack 产物路径。
This commit is contained in:
@@ -26,6 +26,7 @@
|
||||
"compress": "node scripts/compress.js",
|
||||
"security-check": "node scripts/security-check.js",
|
||||
"quality-check": "node scripts/quality-check.js",
|
||||
"check:links": "node scripts/check-asset-links.js",
|
||||
"deploy-prepare": "node scripts/deploy-prepare.js",
|
||||
"deploy-analyze": "node scripts/deploy-analyze.js",
|
||||
"deploy-test": "node scripts/test-deploy-config.js"
|
||||
|
||||
137
scripts/check-asset-links.js
Normal file
137
scripts/check-asset-links.js
Normal file
@@ -0,0 +1,137 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* 生产静态资源脱链检查
|
||||
* 扫描 HTML 中的本地 script/link/href/src,确认文件在仓库或 dist 中存在。
|
||||
* 禁止把不存在的路径发布到生产。
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const projectRoot = path.join(__dirname, '..');
|
||||
const htmlRoots = [
|
||||
path.join(projectRoot, 'index.html'),
|
||||
path.join(projectRoot, 'src', 'giffgaff', 'giffgaff_modular.html'),
|
||||
path.join(projectRoot, 'src', 'simyo', 'simyo_modular.html')
|
||||
];
|
||||
|
||||
const ATTR_RE = /\b(?:src|href)=["']([^"']+)["']/gi;
|
||||
const SKIP_PREFIXES = [
|
||||
'http://',
|
||||
'https://',
|
||||
'//',
|
||||
'data:',
|
||||
'mailto:',
|
||||
'tel:',
|
||||
'javascript:',
|
||||
'#',
|
||||
'blob:'
|
||||
];
|
||||
|
||||
function shouldSkip(url) {
|
||||
return SKIP_PREFIXES.some((p) => url.startsWith(p));
|
||||
}
|
||||
|
||||
// Netlify redirects 中的“软路由”(无静态文件,但生产可访问)
|
||||
const KNOWN_ROUTES = new Set(['/giffgaff', '/simyo']);
|
||||
|
||||
function looksLikeStaticAsset(cleanPath) {
|
||||
// 有扩展名的视为静态资源;无扩展名多为路由入口
|
||||
const base = path.basename(cleanPath);
|
||||
return base.includes('.');
|
||||
}
|
||||
|
||||
function resolveLocal(url) {
|
||||
// 去掉 query/hash
|
||||
const clean = url.split('?')[0].split('#')[0];
|
||||
if (!clean || clean.endsWith('/')) return null;
|
||||
if (KNOWN_ROUTES.has(clean)) return null;
|
||||
if (!looksLikeStaticAsset(clean)) return null;
|
||||
|
||||
// 仅检查站点内绝对路径
|
||||
if (clean.startsWith('/')) {
|
||||
const fromRoot = path.join(projectRoot, clean.slice(1));
|
||||
const fromDist = path.join(projectRoot, 'dist', clean.slice(1));
|
||||
return { clean, candidates: [fromRoot, fromDist] };
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function collectUrls(html) {
|
||||
const urls = new Set();
|
||||
let match;
|
||||
while ((match = ATTR_RE.exec(html)) !== null) {
|
||||
urls.add(match[1]);
|
||||
}
|
||||
return [...urls];
|
||||
}
|
||||
|
||||
function main() {
|
||||
const missing = [];
|
||||
let checked = 0;
|
||||
|
||||
for (const htmlPath of htmlRoots) {
|
||||
if (!fs.existsSync(htmlPath)) {
|
||||
missing.push({ file: htmlPath, url: '(file missing)', reason: 'HTML 入口不存在' });
|
||||
continue;
|
||||
}
|
||||
|
||||
const html = fs.readFileSync(htmlPath, 'utf8');
|
||||
const urls = collectUrls(html);
|
||||
const relHtml = path.relative(projectRoot, htmlPath);
|
||||
|
||||
for (const url of urls) {
|
||||
if (shouldSkip(url)) continue;
|
||||
const resolved = resolveLocal(url);
|
||||
if (!resolved) continue;
|
||||
|
||||
checked++;
|
||||
const exists = resolved.candidates.some((p) => fs.existsSync(p));
|
||||
if (!exists) {
|
||||
missing.push({
|
||||
file: relHtml,
|
||||
url: resolved.clean,
|
||||
reason: '本地文件不存在(源码与 dist 均未找到)'
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 额外:构建产物中禁止残留 Webpack 路径提示(若 resource-hints 被挂载)
|
||||
const forbiddenHints = [
|
||||
'/dist/js/main.js',
|
||||
'/dist/js/vendors.js',
|
||||
'/dist/css/design-system.css'
|
||||
];
|
||||
const resourceHintsPath = path.join(projectRoot, 'src', 'js', 'modules', 'resource-hints.js');
|
||||
if (fs.existsSync(resourceHintsPath)) {
|
||||
const content = fs.readFileSync(resourceHintsPath, 'utf8');
|
||||
for (const bad of forbiddenHints) {
|
||||
if (content.includes(bad)) {
|
||||
missing.push({
|
||||
file: 'src/js/modules/resource-hints.js',
|
||||
url: bad,
|
||||
reason: '遗留 Webpack 产物路径,生产构建不会生成该文件'
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`[check-asset-links] 已检查本地资源引用: ${checked}`);
|
||||
|
||||
if (missing.length === 0) {
|
||||
console.log('[check-asset-links] ✅ 无脱链');
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
console.error('[check-asset-links] ❌ 发现脱链/无效路径:');
|
||||
missing.forEach((m) => {
|
||||
console.error(` - ${m.file}: ${m.url} (${m.reason})`);
|
||||
});
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
main();
|
||||
@@ -103,6 +103,11 @@ function buildSteps(mode) {
|
||||
{
|
||||
name: 'pre-commit 检查',
|
||||
args: ['run', 'precommit:check']
|
||||
},
|
||||
{
|
||||
// 生产 HTML 本地资源脱链门禁(不改运行时,仅静态校验)
|
||||
name: '生产资源脱链检查',
|
||||
args: ['run', 'check:links']
|
||||
}
|
||||
];
|
||||
|
||||
|
||||
@@ -13,21 +13,30 @@ const projectRoot = path.join(__dirname, '..');
|
||||
|
||||
// 检查项配置
|
||||
const checks = {
|
||||
// 1. 语法检查
|
||||
// 1. 语法检查(对齐当前生产路径:build-static + Netlify Functions)
|
||||
syntaxCheck: {
|
||||
name: '语法检查',
|
||||
files: [
|
||||
'server.js',
|
||||
'webpack.config.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/functions/verify-cookie.js',
|
||||
'netlify/edge-functions/bff-proxy.js',
|
||||
'netlify/edge-functions/markdown-negotiation.js'
|
||||
]
|
||||
},
|
||||
|
||||
@@ -153,7 +162,7 @@ function runChecks() {
|
||||
BuildLogger.title('代码质量全面检查');
|
||||
|
||||
// 1. 语法检查
|
||||
BuildLogger.step(1, 4, checks.syntaxCheck.name);
|
||||
BuildLogger.step(1, 5, checks.syntaxCheck.name);
|
||||
let syntaxPassed = 0;
|
||||
checks.syntaxCheck.files.forEach(file => {
|
||||
totalChecks++;
|
||||
@@ -169,7 +178,7 @@ function runChecks() {
|
||||
BuildLogger.progress(`${syntaxPassed}/${checks.syntaxCheck.files.length} 文件通过语法检查\n`);
|
||||
|
||||
// 2. 环境变量检查
|
||||
BuildLogger.step(2, 4, checks.envVarCheck.name);
|
||||
BuildLogger.step(2, 5, checks.envVarCheck.name);
|
||||
totalChecks++;
|
||||
const envIssues = checkEnvExample();
|
||||
if (envIssues.length === 0) {
|
||||
@@ -182,7 +191,7 @@ function runChecks() {
|
||||
}
|
||||
|
||||
// 3. 依赖检查
|
||||
BuildLogger.step(3, 4, checks.dependencyCheck.name);
|
||||
BuildLogger.step(3, 5, checks.dependencyCheck.name);
|
||||
totalChecks++;
|
||||
const depIssues = checkPackageJson();
|
||||
if (depIssues.length === 0) {
|
||||
@@ -195,7 +204,7 @@ function runChecks() {
|
||||
}
|
||||
|
||||
// 4. 安全配置检查
|
||||
BuildLogger.step(4, 4, checks.securityCheck.name);
|
||||
BuildLogger.step(4, 5, checks.securityCheck.name);
|
||||
|
||||
// 检查弱密钥(排除安全检查代码中的引用)
|
||||
totalChecks++;
|
||||
@@ -213,6 +222,24 @@ function runChecks() {
|
||||
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}`);
|
||||
|
||||
@@ -13,14 +13,15 @@ class ResourceHintsManager {
|
||||
'https://appapi.simyo.nl'
|
||||
],
|
||||
dnsPrefetch: [],
|
||||
// 生产构建为原生 ES 模块静态托管,禁止 preload 已废弃的 Webpack 产物路径
|
||||
preload: {
|
||||
fonts: [],
|
||||
scripts: [
|
||||
{ href: '/dist/js/main.js', as: 'script' },
|
||||
{ href: '/dist/js/vendors.js', as: 'script' }
|
||||
{ href: '/src/js/home.js', as: 'script' },
|
||||
{ href: '/src/js/bootstrap-footer.js', as: 'script' }
|
||||
],
|
||||
styles: [
|
||||
{ href: '/dist/css/design-system.css', as: 'style' }
|
||||
{ href: '/src/styles/design-system.css', as: 'style' }
|
||||
],
|
||||
images: []
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user