为什么已经有编译器,还需要自检脚本

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 发布文档不含敏感信息;
  • 签名配置未进入公共变更集;
  • 导出结构版本与代码常量一致。

总结

一个几十行的自检脚本可以承担四类职责:

  1. 应用身份:Bundle ID 和版本;
  2. 合规边界:权限与网络能力;
  3. 资源完整性:多语言和图标;
  4. 回归哨兵:关键功能没有被误删。

它的价值不在于替代编译器和测试,而是把项目独有的约束变成每次都能重复执行的检查。

本文案例来自“心晴手记(MoodMemoir)”HarmonyOS 项目的 validate_project.mjs

参考资料


Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐