chore(mp-harmony): 新增子包配置一致性检查脚本 (防新增/删除漏改)

背景:
  mp-harmony 项目的子包 HAR 配置分散在 4 个地方:
    1. entry/har-deps-required.json5 的 requiredHarDeps (单一事实源)
    2. mp-harmony/build-profile.json5 的 modules 数组
    3. entry/oh-package.json5 的 dependencies
    4. ascf_build 实际目录

  新增/删除子包时, 任何一处忘改都会导致鸿蒙构建失败:
    - build-profile.json5 没加 → hvigor 不构建该模块
    - entry/oh-package.json5 没加 file: → ohpm 不 link 该 HAR
    - ascf_build/ 目录不存在 → ohpm "Field IS Empty Error"
    - har-deps-required.json5 没加 → check-har-deps 不会检查

改动:
  新增 scripts/check-subpackage-sync.js:
    - 读取 entry/har-deps-required.json5 作为单一事实源
    - 扫描 build-profile.json5 的 modules / entry/oh-package.json5
      的 file: 依赖 / ascf_build 实际目录 / 每个子包的关键文件齐备性
    - 任何一处不一致立即报错, 指出哪一处需要修改
    - 支持 --strict 模式 (CI 用, exit 1 阻断 build)
    - 已验证反向场景: 故意添加 pages_extra 到 build-profile.json5
      后跑 --strict, 立即报告 "build-profile.json5 多了 [pages_extra]"

使用方式:
  本地日常:  node scripts/check-subpackage-sync.js
  CI 严格:   node scripts/check-subpackage-sync.js --strict
  新增子包 SOP: 跑一次确认没漏, 不一致会列出缺失位置

依赖: 项目已有 json5 包 (devDependencies transitive), 无需新装
This commit is contained in:
2026-07-30 15:27:28 +08:00
parent d1c58f2ec8
commit 9d8324bb44

View File

@@ -0,0 +1,209 @@
#!/usr/bin/env node
/* eslint-disable no-console */
/**
* scripts/check-subpackage-sync.js
*
* 检查 mp-harmony 项目的子包 HAR 配置在 4 个地方是否同步:
* 1. mp-harmony/build-profile.json5 的 modules 数组
* 2. entry/oh-package.json5 的 dependencies
* 3. entry/har-deps-required.json5 的 requiredHarDeps (单一事实源)
* 4. ascf_build 实际目录
*
* 任何一处不一致 → 立即报错并指出哪一处需要修改。
* 防止新增/删除子包时漏改某处导致 build/runtime 故障。
*
* 使用:
* node scripts/check-subpackage-sync.js
* node scripts/check-subpackage-sync.js --strict # 不一致即 exit 1 (CI 用)
*
* 输出:
* ✓ 5 个子包配置完全一致
* 或
* ✘ 缺失 [pages_extra] 在以下位置:
* - build-profile.json5 (mp-harmony)
* - entry/oh-package.json5
* 建议操作: ...
*/
'use strict';
const fs = require('fs');
const path = require('path');
const JSON5 = require('json5');
function loadJson5(filePath) {
const raw = fs.readFileSync(filePath, 'utf8');
return JSON5.parse(raw);
}
function exists(p) {
try {
fs.accessSync(p, fs.constants.F_OK);
return true;
} catch {
return false;
}
}
function listDirs(p) {
try {
return fs.readdirSync(p, { withFileTypes: true })
.filter(e => e.isDirectory())
.map(e => e.name)
.sort();
} catch {
return [];
}
}
function diff(a, b) {
const aSet = new Set(a);
const bSet = new Set(b);
const onlyA = [...aSet].filter(x => !bSet.has(x));
const onlyB = [...bSet].filter(x => !aSet.has(x));
return { onlyA, onlyB };
}
function color(s, c) {
if (!process.stdout.isTTY) return s;
const codes = { red: '\x1b[31m', green: '\x1b[32m', yellow: '\x1b[33m', cyan: '\x1b[36m', gray: '\x1b[90m', reset: '\x1b[0m' };
return codes[c] + s + codes.reset;
}
function main() {
const ROOT = path.resolve(__dirname, '..');
const strict = process.argv.includes('--strict');
// === 单一事实源: har-deps-required.json5 ===
const configPath = path.join(ROOT, 'stub-app-projects', 'mp-harmony', 'entry', 'har-deps-required.json5');
if (!exists(configPath)) {
console.error(color(`✘ 缺少白名单配置: ${configPath}`, 'red'));
process.exit(strict ? 1 : 0);
}
const config = loadJson5(configPath);
const canonical = (config.requiredHarDeps || []).slice().sort();
console.log(color('=== mp-harmony 子包配置一致性检查 ===', 'cyan'));
console.log(`白名单 (har-deps-required.json5): ${canonical.join(', ')}\n`);
const errors = [];
const warnings = [];
// === 检查 1: mp-harmony/build-profile.json5 的 modules 数组 ===
const buildProfilePath = path.join(ROOT, 'stub-app-projects', 'mp-harmony', 'build-profile.json5');
const buildProfileModules = [];
if (!exists(buildProfilePath)) {
errors.push(`build-profile.json5 不存在: ${buildProfilePath}`);
} else {
try {
const bp = loadJson5(buildProfilePath);
// modules 是顶层数组, 包含 entry + 5 个子包
// entry 也算一个 module, 但它不在我们的白名单里
const moduleNames = (bp.modules || []).map(m => m.name).filter(n => n && n !== 'entry');
buildProfileModules.push(...moduleNames);
const d = diff(canonical, buildProfileModules);
if (d.onlyA.length) {
errors.push(`build-profile.json5 缺失子包: [${d.onlyA.join(', ')}]`);
console.log(` ${color('✘', 'red')} build-profile.json5: 缺失 [${d.onlyA.join(', ')}]`);
} else if (d.onlyB.length) {
errors.push(`build-profile.json5 多了子包: [${d.onlyB.join(', ')}]`);
console.log(` ${color('✘', 'red')} build-profile.json5: 多了 [${d.onlyB.join(', ')}]`);
} else {
console.log(` ${color('✔', 'green')} build-profile.json5: ${buildProfileModules.length} 个子包模块与白名单一致`);
}
} catch (e) {
errors.push(`build-profile.json5 解析失败: ${e.message}`);
}
}
// === 检查 2: entry/oh-package.json5 的 dependencies ===
const ohPkgPath = path.join(ROOT, 'stub-app-projects', 'mp-harmony', 'entry', 'oh-package.json5');
const ohPkgDeps = [];
if (!exists(ohPkgPath)) {
errors.push(`entry/oh-package.json5 不存在: ${ohPkgPath}`);
} else {
try {
const op = loadJson5(ohPkgPath);
const deps = op.dependencies || {};
// 提取 5 个 file: 依赖 (key 是包名)
ohPkgDeps.push(...Object.keys(deps).filter(k => deps[k] && deps[k].startsWith('file:')));
const d = diff(canonical, ohPkgDeps);
if (d.onlyA.length) {
errors.push(`entry/oh-package.json5 缺失 file: 依赖: [${d.onlyA.join(', ')}]`);
console.log(` ${color('✘', 'red')} entry/oh-package.json5: 缺失 [${d.onlyA.join(', ')}]`);
} else if (d.onlyB.length) {
warnings.push(`entry/oh-package.json5 多了 file: 依赖: [${d.onlyB.join(', ')}] (可能是新的子包还未更新白名单)`);
console.log(` ${color('⚠', 'yellow')} entry/oh-package.json5: 多了 [${d.onlyB.join(', ')}]`);
} else {
console.log(` ${color('✔', 'green')} entry/oh-package.json5: ${ohPkgDeps.length} 个 file: 依赖与白名单一致`);
}
} catch (e) {
errors.push(`entry/oh-package.json5 解析失败: ${e.message}`);
}
}
// === 检查 3: ascf_build 实际目录 ===
const ascfBuildRoot = path.join(ROOT, 'stub-app-projects', 'mp-harmony', 'ascf', 'ascf_build');
const ascfBuildDirs = listDirs(ascfBuildRoot);
const ascfBuildD = diff(canonical, ascfBuildDirs);
if (ascfBuildD.onlyA.length) {
errors.push(`ascf_build/ 目录缺失子包: [${ascfBuildD.onlyA.join(', ')}]`);
console.log(` ${color('✘', 'red')} ascf_build/: 缺失 [${ascfBuildD.onlyA.join(', ')}]`);
} else if (ascfBuildD.onlyB.length) {
warnings.push(`ascf_build/ 多了子包: [${ascfBuildD.onlyB.join(', ')}] (可能是历史遗留)`);
console.log(` ${color('⚠', 'yellow')} ascf_build/: 多了 [${ascfBuildD.onlyB.join(', ')}]`);
} else {
console.log(` ${color('✔', 'green')} ascf_build/: ${ascfBuildDirs.length} 个子包目录与白名单一致`);
}
// === 检查 4: 每个子包的关键文件是否齐全 ===
console.log(color('--- 子包关键文件检查 ---', 'cyan'));
for (const pkg of canonical) {
const pkgRoot = path.join(ascfBuildRoot, pkg);
const requiredFiles = [
'oh-package.json5',
'build-profile.json5',
'hvigorfile.ts',
'obfuscation-rules.txt',
'src/main/module.json5',
];
const missing = requiredFiles.filter(f => !exists(path.join(pkgRoot, f)));
if (missing.length) {
errors.push(`${pkg} 缺失文件: [${missing.join(', ')}]`);
console.log(` ${color('✘', 'red')} ${pkg}: 缺失 [${missing.join(', ')}]`);
} else {
console.log(` ${color('✔', 'green')} ${pkg}: 关键文件齐备`);
}
}
// === 总结 ===
console.log();
if (warnings.length) {
console.log(color(`警告 (${warnings.length}):`, 'yellow'));
for (const w of warnings) console.log(' - ' + w);
}
if (errors.length) {
console.log();
console.log(color(`错误 (${errors.length}):`, 'red'));
for (const e of errors) console.log(' - ' + e);
console.log();
console.log(color('✘ 子包配置不一致, 鸿蒙构建可能失败。', 'red'));
console.log(color(' 修复步骤:', 'gray'));
console.log(color(' 1. 修改 entry/har-deps-required.json5 的 requiredHarDeps (单一事实源)', 'gray'));
console.log(color(' 2. 在 ascf_build/<pkg>/ 创建新子包模板 (含 oh-package.json5 / build-profile.json5 / ...) ', 'gray'));
console.log(color(' 3. 在 mp-harmony/build-profile.json5 的 modules 数组加新模块', 'gray'));
console.log(color(' 4. 在 entry/oh-package.json5 的 dependencies 加 "file:../ascf/ascf_build/<pkg>"', 'gray'));
console.log(color(' 5. 在 DevEco Sync 项目 (让 ohpm 处理 file: 依赖)', 'gray'));
console.log(color(' 6. 跑 node scripts/check-subpackage-sync.js --strict 验证', 'gray'));
process.exit(strict ? 1 : 0);
}
console.log(color(`✔ 5 个子包配置完全一致 (${canonical.length} 个包)`, 'green'));
process.exit(0);
}
try {
main();
} catch (e) {
console.error(color(`脚本异常: ${e.stack || e.message}`, 'red'));
process.exit(1);
}