构建: - 新增 tools/build.js:junction HBuilderX 工具链,CLI 构建 h5/mp-weixin - vue 指向补丁版 @dcloudio/uni-h5-vue(官方 npm vue 不导出 isInSSRComponentSetup) - 设 HX_APP_ROOT 避免退化成 H5 空壳产物;产物完整性校验 校验工具: - 新增 check-cloud-methods.js:acorn 解析云对象方法,比对 94 处调用点 - 新增 check-android-contract.js:Kotlin 侧云对象契约校验 - audit-project.js 修 downloadFile 误报(注释未剥离);tools/ 排除出扫描 - package.json 声明此前隐式依赖的 acorn 功能: - 补 uni-cms-articles.getPublishedArticles(安卓端依赖但此前不存在) - 修 u-parse <audio> 引用已移除组件导致 H5 构建失败
246 lines
8.0 KiB
JavaScript
246 lines
8.0 KiB
JavaScript
#!/usr/bin/env node
|
||
'use strict';
|
||
|
||
/**
|
||
* 一键验证:跑完所有检查器并汇总。
|
||
*
|
||
* node tools/verify.js # 全部检查
|
||
* node tools/verify.js --quick # 跳过耗时的编码扫描
|
||
*
|
||
* 退出码 0 表示全部通过,可用于 CI 或提交前自检。
|
||
*/
|
||
|
||
const { spawnSync } = require('child_process');
|
||
const path = require('path');
|
||
|
||
const ROOT = path.resolve(__dirname, '..');
|
||
const QUICK = process.argv.includes('--quick');
|
||
|
||
const CHECKS = [
|
||
{
|
||
name: '静态审计',
|
||
script: 'tools/audit-project.js',
|
||
desc: '编码 / 页面路由 / 云调用 / 云函数鉴权 / 敏感信息',
|
||
},
|
||
{
|
||
name: 'Vue SFC 校验',
|
||
script: 'tools/check-vue.js',
|
||
desc: '脚本语法 / 生命周期位置 / Vue2 残留 API',
|
||
},
|
||
{
|
||
name: '云对象方法校验',
|
||
script: 'tools/check-cloud-methods.js',
|
||
desc: 'importObject 调用点的方法名是否真实存在',
|
||
},
|
||
{
|
||
name: '安卓端契约校验',
|
||
script: 'tools/check-android-contract.js',
|
||
desc: 'Kotlin 侧调用的云对象方法是否存在',
|
||
},
|
||
];
|
||
|
||
const SELFTESTS = [
|
||
{ name: '静态审计自测', script: 'tools/audit-project.js', args: ['--selftest'] },
|
||
{ name: 'Vue 校验自测', script: 'tools/check-vue.js', args: ['--selftest'] },
|
||
{ name: '文章内容适配自测', script: 'tools/check-article-flow.js' },
|
||
{ name: '云对象方法校验自测', script: 'tools/check-cloud-methods.js', args: ['--selftest'] },
|
||
];
|
||
|
||
function run(script, args = []) {
|
||
const r = spawnSync(process.execPath, [path.join(ROOT, script), ...args], {
|
||
encoding: 'utf8',
|
||
cwd: ROOT,
|
||
});
|
||
return { code: r.status, out: (r.stdout || '') + (r.stderr || '') };
|
||
}
|
||
|
||
function summarize(out) {
|
||
// 从报告末尾抓 "结论: N 错误 / M 警告"
|
||
const m = /结论:\s*(\d+)\s*错误\s*\/\s*(\d+)\s*警告/.exec(out);
|
||
if (m) return { errors: Number(m[1]), warnings: Number(m[2]) };
|
||
// 自测输出
|
||
const f = /✗\s*(\d+)\s*项失败/.exec(out);
|
||
if (f) return { errors: Number(f[1]), warnings: 0 };
|
||
return null;
|
||
}
|
||
|
||
let failed = 0;
|
||
|
||
console.log('');
|
||
console.log('═══════════════════════════════════════════════');
|
||
console.log(' 军歌嘹亮 · 一键验证');
|
||
console.log('═══════════════════════════════════════════════');
|
||
|
||
// 第一阶段:先确认检查器本身可信
|
||
console.log('');
|
||
console.log('【检查器自测】');
|
||
for (const t of SELFTESTS) {
|
||
const { code, out } = run(t.script, t.args);
|
||
const ok = code === 0;
|
||
if (!ok) failed++;
|
||
console.log(` ${ok ? '✓' : '✗'} ${t.name}`);
|
||
if (!ok) {
|
||
console.log(
|
||
out
|
||
.split('\n')
|
||
.filter((l) => l.includes('✗'))
|
||
.map((l) => ' ' + l.trim())
|
||
.join('\n')
|
||
);
|
||
}
|
||
}
|
||
|
||
// 第二阶段:跑实际检查
|
||
console.log('');
|
||
console.log('【项目检查】');
|
||
const results = [];
|
||
for (const c of CHECKS) {
|
||
if (QUICK && c.name.includes('静态')) continue;
|
||
const { code, out } = run(c.script);
|
||
const s = summarize(out);
|
||
const ok = code === 0;
|
||
if (!ok) failed++;
|
||
results.push({ ...c, ok, summary: s });
|
||
const stat = s ? `${s.errors} 错误 / ${s.warnings} 警告` : code === 0 ? '通过' : '失败';
|
||
console.log(` ${ok ? '✓' : '✗'} ${c.name.padEnd(14)} ${stat}`);
|
||
}
|
||
|
||
// 第三阶段:数据与资源完整性
|
||
console.log('');
|
||
console.log('【数据与资源】');
|
||
const fs = require('fs');
|
||
|
||
const dataChecks = [
|
||
['分类种子数据', 'uniCloud-alipay/database/uni-cms-categories.init_data.json'],
|
||
['文章模板数据', 'uniCloud-alipay/database/cms-temp.init_data.json'],
|
||
['礼物种子数据', 'uniCloud-alipay/database/gifts.init_data.json'],
|
||
['热搜词数据', 'uniCloud-alipay/database/opendb-search-hot.init_data.json'],
|
||
['模板图片资源', 'static/template/default.png'],
|
||
['编辑器图标', 'static/editor-icons/text.png'],
|
||
['文章配图', 'static/article/wz1.png'],
|
||
];
|
||
|
||
for (const [label, rel] of dataChecks) {
|
||
const abs = path.join(ROOT, rel);
|
||
const exists = fs.existsSync(abs);
|
||
let detail = '';
|
||
if (exists) {
|
||
try {
|
||
const content = fs.readFileSync(abs, 'utf8');
|
||
if (rel.endsWith('.json')) {
|
||
const arr = JSON.parse(content);
|
||
detail = Array.isArray(arr) ? `${arr.length} 条` : '对象';
|
||
} else {
|
||
detail = `${(fs.statSync(abs).size / 1024).toFixed(1)} KB`;
|
||
}
|
||
} catch (e) {
|
||
detail = `解析失败: ${e.message}`;
|
||
}
|
||
}
|
||
if (!exists) failed++;
|
||
console.log(` ${exists ? '✓' : '✗'} ${label.padEnd(14)} ${detail}`);
|
||
}
|
||
|
||
// schema 部署完整性:主表 schema 缺失会导致线上集合无 schema 约束
|
||
const schemaChecks = [
|
||
['文章表', 'uniCloud-alipay/database/uni-cms-articles.schema.json'],
|
||
['分类表', 'uniCloud-alipay/database/uni-cms-categories.schema.json'],
|
||
['解锁记录表', 'uniCloud-alipay/database/uni-cms-unlock-record.schema.json'],
|
||
['文章内容扩展', 'uniCloud-alipay/database/uni-cms-articles.schema.ext.js'],
|
||
];
|
||
|
||
for (const [label, rel] of schemaChecks) {
|
||
const abs = path.join(ROOT, rel);
|
||
const exists = fs.existsSync(abs);
|
||
if (!exists) failed++;
|
||
console.log(` ${exists ? '✓' : '✗'} ${label.padEnd(14)} ${exists ? '已部署' : '缺失'}`);
|
||
}
|
||
|
||
// schema 与种子数据是 JSONC(uniCloud 允许行注释),需要用宽松方式解析
|
||
function parseJSONC(text) {
|
||
let out = '';
|
||
let i = 0;
|
||
const n = text.length;
|
||
while (i < n) {
|
||
const c = text[i];
|
||
// 字符串原样保留
|
||
if (c === '"') {
|
||
let j = i + 1;
|
||
while (j < n) {
|
||
if (text[j] === '\\') { j += 2; continue; }
|
||
if (text[j] === '"') break;
|
||
j++;
|
||
}
|
||
out += text.slice(i, Math.min(j + 1, n));
|
||
i = j + 1;
|
||
continue;
|
||
}
|
||
// 行注释:换成等量空白以保持结构
|
||
if (c === '/' && text[i + 1] === '/') {
|
||
const end = text.indexOf('\n', i);
|
||
const stop = end === -1 ? n : end;
|
||
out += ' '.repeat(stop - i);
|
||
i = stop;
|
||
continue;
|
||
}
|
||
out += c;
|
||
i++;
|
||
}
|
||
return JSON.parse(out);
|
||
}
|
||
|
||
// 所有 schema 与 db_init 都必须能被解析
|
||
const schemaFiles = fs
|
||
.readdirSync(path.join(ROOT, 'uniCloud-alipay/database'))
|
||
.filter((f) => f.endsWith('.schema.json') || f === 'db_init.json');
|
||
|
||
let badSchema = 0;
|
||
for (const f of schemaFiles) {
|
||
const abs = path.join(ROOT, 'uniCloud-alipay/database', f);
|
||
try {
|
||
const obj = parseJSONC(fs.readFileSync(abs, 'utf8'));
|
||
if (!obj || typeof obj !== 'object') throw new Error('根节点不是对象');
|
||
} catch (e) {
|
||
badSchema++;
|
||
failed++;
|
||
console.log(` ✗ ${f.padEnd(14)} ${e.message}`);
|
||
}
|
||
}
|
||
if (!badSchema) {
|
||
console.log(` ✓ ${'schema 解析'.padEnd(14)} ${schemaFiles.length} 个文件全部有效`);
|
||
}
|
||
|
||
// db_init 必须登记所有种子数据表,否则线上初始化会漏表
|
||
const initPath = path.join(ROOT, 'uniCloud-alipay/database/db_init.json');
|
||
try {
|
||
const init = parseJSONC(fs.readFileSync(initPath, "utf8"));
|
||
const registered = Object.keys(init);
|
||
const expected = [
|
||
'uni-cms-categories',
|
||
'cms-temp',
|
||
'gifts',
|
||
'opendb-search-hot',
|
||
'cms-articles-like',
|
||
'cms-articles-collect',
|
||
'cms-vote',
|
||
'cms-articles-log',
|
||
];
|
||
const missing = expected.filter((t) => !registered.includes(t));
|
||
if (missing.length) failed++;
|
||
console.log(
|
||
` ${missing.length ? '✗' : '✓'} ${'db_init 登记'.padEnd(14)} ${
|
||
missing.length ? '缺少: ' + missing.join(', ') : `${registered.length} 张表`
|
||
}`
|
||
);
|
||
} catch (e) {
|
||
failed++;
|
||
console.log(` ✗ db_init 登记 解析失败: ${e.message}`);
|
||
}
|
||
|
||
console.log('');
|
||
console.log('═══════════════════════════════════════════════');
|
||
console.log(failed ? `✗ ${failed} 项未通过` : '✓ 全部通过');
|
||
console.log('═══════════════════════════════════════════════');
|
||
|
||
process.exit(failed ? 1 : 0);
|