Files
t/tools/verify.js
T
root 3117146281 feat: 打通 HBuilderX CLI 构建 + 修复文章闭环静态盲区
构建:
- 新增 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 构建失败
2026-09-12 01:02:45 +08:00

246 lines
8.0 KiB
JavaScript
Raw 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
'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);