用 Node 脚本做 HarmonyOS 发布前静态自检:把项目约束写成代码
为什么已经有编译器,还需要自检脚本
ArkTS 编译器会检查语法和类型,却不会知道你的产品约束:
- Bundle ID 必须等于备案和市场后台中的值;
- 只允许一个设备认证权限;
- 中英日资源 Key 必须完全一致;
- 应用图标不能被小尺寸占位图替换;
- 本地优先 App 不应该突然加入网络权限;
- 统计页必须保留审核回归功能。
这些规则如果只写在 README 中,很容易在修改时被忽略。
更可靠的方式是:
文档说明为什么
脚本检查有没有违反
一、脚本不需要一开始就做成复杂工具
项目使用普通 Node ESM:
import fs from 'node:fs';
import path from 'node:path';
const root = path.resolve(
new URL('..', import.meta.url).pathname
);
const readJson = (relative) => JSON.parse(
fs.readFileSync(
path.join(root, relative),
'utf8'
)
);
失败函数统一设置退出码:
const fail = (message) => {
console.error(`FAIL: ${message}`);
process.exitCode = 1;
};
这样可以一次报告多个问题,而不是遇到第一项就退出。
二、检查 Bundle ID 和版本格式
const app = readJson('AppScope/app.json5').app;
const expectedBundle = '你的稳定 Bundle ID';
if (app.bundleName !== expectedBundle) {
fail(`bundleName must be ${expectedBundle}`);
}
if (
!Number.isInteger(app.versionCode) ||
app.versionCode <= 0
) {
fail('versionCode must be a positive integer');
}
if (!/^\d+\.\d+\.\d+$/.test(app.versionName)) {
fail('versionName must use x.y.z format');
}
编译器允许很多合法字符串,但团队可以定义更窄的版本规则。
这里检查的是格式和固定身份。是否比上一个发布版本递增,还需要读取发布记录、Git Tag 或 CI 参数。
三、把允许权限做成白名单
从模块配置读取:
const moduleProfile = readJson(
'entry/src/main/module.json5'
).module;
const permissions = moduleProfile.requestPermissions
.map((item) => item.name)
.sort();
项目只允许设备认证:
const expectedPermissions = [
'ohos.permission.ACCESS_BIOMETRIC'
].sort();
if (
JSON.stringify(permissions) !==
JSON.stringify(expectedPermissions)
) {
fail(
`unexpected permission set: ${permissions.join(', ')}`
);
}
白名单比“禁止几个高风险权限”更适合小型本地应用:任何新权限都会让检查失败,开发者必须同步更新代码、用途文案、隐私政策和测试清单。
四、比较三种语言的资源 Key
let canonicalEntryKeys = [];
for (const locale of [
'base',
'zh_CN',
'ja_JP'
]) {
const strings = readJson(
`entry/src/main/resources/${locale}` +
`/element/string.json`
).string;
const names = strings.map((item) => item.name);
if (new Set(names).size !== names.length) {
fail(`${locale} has duplicate string keys`);
}
const sortedNames = names.slice().sort();
if (locale === 'base') {
canonicalEntryKeys = sortedNames;
} else if (
JSON.stringify(sortedNames) !==
JSON.stringify(canonicalEntryKeys)
) {
// 输出 missing 和 extra
}
}
这个检查能捕获:
- 新增中文文案但漏英文/日文;
- 拼写错误导致某语言多出 Key;
- 同一个文件内重复定义;
- 删除功能时只删了一种语言。
它不能检查翻译质量,但至少保证资源结构完整。
五、应用名称资源也要单独检查
桌面应用名称位于 AppScope,不是页面模块资源。
for (const locale of ['base', 'zh_CN', 'ja_JP']) {
const appStrings = readJson(
`AppScope/resources/${locale}` +
`/element/string.json`
).string;
const names = appStrings.map((item) => item.name);
if (!names.includes('app_name')) {
fail(`${locale} AppScope is missing app_name`);
}
}
否则应用内已经是日文,桌面名称却可能回退基础语言。
六、用文件特征防止资源被占位符替换
项目检查图标文件大小:
for (const icon of [
'AppScope/resources/base/media/app_icon.png',
'entry/src/main/resources/base/media/icon.png'
]) {
const stat = fs.statSync(path.join(root, icon));
if (stat.size < 10000) {
fail(`${icon} looks like a placeholder`);
}
}
文件大小不是严格的视觉验证,但可以捕获常见事故:真实图标被一个很小的临时 PNG 覆盖。
可以继续加强:
- 读取图片宽高;
- 检查必须为 1024×1024;
- 检查颜色模式;
- 检查不含透明通道;
- 生成发布素材总览供人工确认。
七、扫描本地优先架构的明显回退
项目不使用网络,因此验证脚本检查:
for (const forbidden of [
'ohos.permission.INTERNET',
'http.request(',
'@ohos/axios'
]) {
if (indexSource.includes(forbidden)) {
fail(`unexpected network capability: ${forbidden}`);
}
}
这是低成本保护,但必须认识局限:
- 只能扫描列出的文件;
- 字符串可能出现误报;
- 无法识别所有间接依赖;
- 第三方包内部能力不一定出现在页面源码;
- 不是安全审计或依赖分析的替代品。
随着项目变大,可以改成遍历所有 .ets/.ts/.json5,再结合依赖清单和权限配置检查。
八、为审核回归功能保留“哨兵”
某些功能曾因重构丢失,可以用简单字符串做哨兵:
for (const required of [
'uiRevision',
'refreshUi()',
"this.named('completed_checkins')",
"this.named('reflection_cards')",
"this.named('summary')",
'eligibleDayIds(habit: Habit)'
]) {
if (!indexSource.includes(required)) {
fail(
`review regression coverage is missing: ${required}`
);
}
}
这种检查比真正的 UI 自动化测试弱,但能快速发现大段功能被误删。
命名或架构重构时,哨兵也需要更新,所以它更适合作为短期回归护栏,而不是永久规范。
九、不要让静态脚本读取或打印真实签名秘密
发布检查经常需要确认“签名已配置”,但脚本不应该把密码、密钥路径或证书内容输出到日志。
正确检查目标可以是:
- 公共仓库不包含已知敏感字段;
- 发布环境中必要文件存在;
- 构建产物已经签名;
- CI Secret 名称存在但不打印值;
- 日志中对路径和凭据做脱敏。
可以增加敏感模式扫描,但只输出文件和字段名称,不输出匹配到的秘密正文。
一旦秘密进入 Git 历史,仅从当前文件删除还不够,通常还需要轮换。
十、JSON5 解析是一个隐藏细节
示例使用 JSON.parse() 读取 .json5,前提是当前文件内容实际上符合严格 JSON:没有注释、尾随逗号或未加引号的 Key。
如果以后开始使用完整 JSON5 语法,脚本会解析失败。可选方案:
- 团队约定这些配置保持严格 JSON 子集;
- 引入 JSON5 解析库;
- 复用构建工具提供的配置解析能力。
文件扩展名不等于实际解析器能力,这一点应该在脚本说明中写清楚。
十一、如何接入日常流程
本地执行:
node scripts/validate_project.mjs
建议放在:
修改资源后
→ 静态自检
→ ArkTS 编译
→ HAP/APP 构建
→ 真机/模拟器回归
如果使用 CI,可以把脚本作为构建前置步骤。退出码非 0 时停止后续发布。
但不要因为脚本通过就跳过人工审核。它适合检查确定性约束,不适合判断截图是否好看、翻译是否自然、隐私文案是否准确。
十二、适合继续增加的检查项
- 版本号与上次发布记录比较;
- 三语言格式化占位符一致;
- 隐私政策 URL 使用 HTTPS 且可访问;
- 所有公开 URL 不指向测试域名;
- 设备类型与 QA 覆盖矩阵匹配;
- HAP/APP 产物存在且时间为本次构建;
- 图标尺寸和 Alpha 通道;
- Markdown 发布文档不含敏感信息;
- 签名配置未进入公共变更集;
- 导出结构版本与代码常量一致。
总结
一个几十行的自检脚本可以承担四类职责:
- 应用身份:Bundle ID 和版本;
- 合规边界:权限与网络能力;
- 资源完整性:多语言和图标;
- 回归哨兵:关键功能没有被误删。
它的价值不在于替代编译器和测试,而是把项目独有的约束变成每次都能重复执行的检查。
本文案例来自“心晴手记(MoodMemoir)”HarmonyOS 项目的
validate_project.mjs。
参考资料
更多推荐


所有评论(0)