Files
lucky_shop/scripts/fix-subpackage-module-names.js
ZF sun 8ebd7918fb chore(mp-harmony): 修正子包 HAR name 字段 + 纳入元数据到 git
背景:
  mp-harmony 项目的 5 个 ascf shared HAR 子包 (components_diy / pages_goods /
  pages_order / pages_promotion / pages_tool) 在 ascf_build 目录下, 关键
  元数据 (oh-package.json5 / module.json5 / build-profile.json5 /
  Index.ets / hvigorfile.ts / obfuscation-rules.txt) 之前被 .gitignore
  整体忽略, 从未被 git 跟踪, 导致 "module.name 被清空" 等回归无法被
  git diff / blame 追踪。

改动:
  1. .gitignore 改造
     - 不再整体 ignore ascf_build
     - 精确 ignore 已知产物路径 (rawfile/**/*.pack.js / rawfile/**/*.html
       / rawfile/**/*.json / build/ / oh_modules/ / .preview/ / .cxx/
       / .test/ / node_modules/)
     - ! 取反保留 HAR 必需文件 (placeholder/index.*, rawfile/index.js,
       base/element/*.json)
     - 补充 entry / hvigor / 鸿蒙签名材料 / IDE 临时文件 ignore 规则
  2. 首次将 5 个 HAR 包的元数据纳入 git (40 个文件)
  3. 新增 scripts/fix-subpackage-module-names.js: 任何入口 require
     vue.config.js 时自动修复 oh-package.json5.name + module.json5.module.name
     双重覆盖, 防止 DevEco ohpm 报 "00604007 Field IS Empty Error" 或
     hvigor 报 "Schema validate failed"
  4. vue.config.js 顶层 IIFE 触发修复 (任何 require 立即生效, 不依赖
     webpack 钩子)
  5. entry/scripts/check-har-deps.js 扩展: 校验 ascf_build 子包
     module.json5.module.name 字段 (兜底校验)
  6. entry/har-deps-required.json5 扩展: 新增 subpackageModuleJson5
     路径映射 + ohModulesRoots 校验资源齐备性
  7. entry/README-HAR-DEPS.md 更新: 故障速查表新增 module.name 相关
     报错条目

预期效果:
  - HBuilderX 触发 vue-cli-service build → vue.config.js 自动修复子包
    name → DevEco ohpm install / hvigor schema 校验通过
  - check-har-deps.js 兜底, 任何回归立即被检测并报错
  - 团队成员 git pull 后能立即看到 ascf_build 元数据, 可用 git diff
    跟踪 module.name 等关键字段变更
2026-07-30 14:26:55 +08:00

272 lines
9.0 KiB
JavaScript

#!/usr/bin/env node
/* eslint-disable no-console */
/**
* scripts/fix-subpackage-module-names.js
*
* 修正 ascf_build/<pkg>/ 子包 HAR 元数据中的 name 字段,确保鸿蒙工具链
* (ohpm install / hvigor schema / 运行时依赖解析) 一致通过。
*
* 涉及两个文件:
* 1. ascf_build/<pkg>/oh-package.json5 — top-level `name`
* ohpm install 会读取这个字段,空字符串会报:
* "00604007 Field IS Empty Error: The name of package .../pages_tool cannot be empty"
*
* 2. ascf_build/<pkg>/src/main/module.json5 — `module.name`
* hvigor build 时 schema 校验要求匹配 ^[a-zA-Z][0-9a-zA-Z_.]*$,
* 空字符串会报:
* "Schema validate failed: must match pattern ^[a-zA-Z][0-9a-zA-Z_.]*$"
*
* 修复策略:
* 遍历 SUBPACKAGE_NAMES 数组里的包, 用包目录名覆盖两处的 name,
* 保留其他字段 (version / description / dependencies / deviceTypes 等)。
* 如果文件不存在, 按合理默认值新建一份。
*
* 使用方式:
* # 命令 1: 直接跑这个脚本
* node scripts/fix-subpackage-module-names.js
*
* # 命令 2: 作为模块 require
* node -e "require('./scripts/fix-subpackage-module-names').fixSubpackageModuleNames()"
*
* # 命令 3: 由 vue.config.js 顶层 IIFE 自动调用
* # (HBuilderX / vue-cli-service / node -e 触发时, 自动执行)
*
* # CI 兜底校验:
* node stub-app-projects/mp-harmony/entry/scripts/check-har-deps.js --strict
*/
'use strict';
const fs = require('fs');
const path = require('path');
const SUBPACKAGE_NAMES = [
'components_diy',
'pages_goods',
'pages_order',
'pages_promotion',
'pages_tool',
];
/**
* 修正 ascf_build/<pkg>/ 子包的 name 字段 (oh-package.json5 + module.json5)
*
* @param {object} [opts]
* @param {string} [opts.ascfBuildRoot] - ascf_build 根目录, 默认基于本脚本位置推算
* @param {boolean} [opts.silent=false] - true 时不打日志
* @returns {{fixedCount: number, results: Array<{pkg: string, file: string, status: 'created'|'fixed'|'unchanged'|'error', message?: string}>}}
*/
function fixSubpackageModuleNames({ ascfBuildRoot, silent = false } = {}) {
const root = ascfBuildRoot
|| path.resolve(__dirname, '..', 'stub-app-projects', 'mp-harmony', 'ascf', 'ascf_build');
const log = silent ? () => {} : console.log;
log(`\n🔧 [fix-subpackage-module-names] 修正 ascf_build 子包 name (oh-package.json5 + module.json5)...`);
const results = [];
for (const pkg of SUBPACKAGE_NAMES) {
results.push(...fixOhPackageJson5({ pkg, root, silent: true, log }));
results.push(...fixModuleJson5({ pkg, root, silent: true, log }));
}
const fixedCount = results.filter(r => r.status === 'fixed' || r.status === 'created').length;
log(` 📊 修正完成: ${fixedCount} 个文件被更新\n`);
return { fixedCount, results };
}
/**
* 修正 ascf_build/<pkg>/oh-package.json5 的 top-level `name` 字段
*
* 默认内容参考项目内已有 HAR (components_diy / pages_goods 等):
* {
* "name": "<pkg>",
* "version": "1.0.0",
* "description": "Please describe the basic information.",
* "main": "Index.ets",
* "author": "",
* "license": "Apache-2.0",
* "packageType": "InterfaceHar",
* "dependencies": {}
* }
*/
function fixOhPackageJson5({ pkg, root, silent, log }) {
const ohPkgJson5Path = path.join(root, pkg, 'oh-package.json5');
const defaultDoc = {
name: pkg,
version: '1.0.0',
description: 'Please describe the basic information.',
main: 'Index.ets',
author: '',
license: 'Apache-2.0',
packageType: 'InterfaceHar',
dependencies: {},
};
let current = null;
if (fs.existsSync(ohPkgJson5Path)) {
try {
// oh-package.json5 是 JSON5, 可能含 // 注释。Node 内置 JSON.parse 不能处理,
// 用 stripJson5Comments + JSON.parse 处理最常见情况。
const raw = fs.readFileSync(ohPkgJson5Path, 'utf8');
const cleaned = stripJson5Comments(raw);
current = JSON.parse(cleaned);
} catch (e) {
if (!silent) log(` ⚠️ ${pkg}/oh-package.json5 解析失败, 使用默认: ${e.message}`);
current = null;
}
}
const relPath = `${pkg}/oh-package.json5`;
if (!current || typeof current !== 'object') {
try {
fs.mkdirSync(path.dirname(ohPkgJson5Path), { recursive: true });
fs.writeFileSync(ohPkgJson5Path, JSON.stringify(defaultDoc, null, 2) + '\n', 'utf8');
if (!silent) log(` 🆕 ${relPath}: 已创建 (name="${pkg}")`);
return [{ pkg, file: 'oh-package.json5', status: 'created' }];
} catch (e) {
if (!silent) log(` ❌ ${relPath}: 创建失败: ${e.message}`);
return [{ pkg, file: 'oh-package.json5', status: 'error', message: e.message }];
}
}
const oldName = current.name;
if (oldName !== pkg) {
current.name = pkg;
try {
fs.writeFileSync(ohPkgJson5Path, JSON.stringify(current, null, 2) + '\n', 'utf8');
if (!silent) log(` ✅ ${relPath}: name "${oldName || '(空)'}" → "${pkg}"`);
return [{ pkg, file: 'oh-package.json5', status: 'fixed', oldName }];
} catch (e) {
if (!silent) log(` ❌ ${relPath}: 写入失败: ${e.message}`);
return [{ pkg, file: 'oh-package.json5', status: 'error', message: e.message }];
}
}
if (!silent) log(` ✔️ ${relPath}: name = "${pkg}" (无须修正)`);
return [{ pkg, file: 'oh-package.json5', status: 'unchanged' }];
}
/**
* 修正 ascf_build/<pkg>/src/main/module.json5 的 `module.name` 字段
*/
function fixModuleJson5({ pkg, root, silent, log }) {
const moduleJson5Path = path.join(root, pkg, 'src', 'main', 'module.json5');
const defaultDoc = {
module: {
name: pkg,
type: 'shared',
description: 'subpackage',
deviceTypes: ['phone', 'tablet'],
deliveryWithInstall: true,
installationFree: true,
},
};
let current = null;
if (fs.existsSync(moduleJson5Path)) {
try {
const raw = fs.readFileSync(moduleJson5Path, 'utf8');
const cleaned = stripJson5Comments(raw);
current = JSON.parse(cleaned);
} catch (e) {
if (!silent) log(` ⚠️ ${pkg}/src/main/module.json5 解析失败, 使用默认: ${e.message}`);
current = null;
}
}
const relPath = `${pkg}/src/main/module.json5`;
if (!current || !current.module || typeof current.module !== 'object') {
try {
fs.mkdirSync(path.dirname(moduleJson5Path), { recursive: true });
fs.writeFileSync(moduleJson5Path, JSON.stringify(defaultDoc, null, 2) + '\n', 'utf8');
if (!silent) log(` 🆕 ${relPath}: 已创建 (name="${pkg}")`);
return [{ pkg, file: 'module.json5', status: 'created' }];
} catch (e) {
if (!silent) log(` ❌ ${relPath}: 创建失败: ${e.message}`);
return [{ pkg, file: 'module.json5', status: 'error', message: e.message }];
}
}
const oldName = current.module.name;
if (oldName !== pkg) {
current.module.name = pkg;
try {
fs.writeFileSync(moduleJson5Path, JSON.stringify(current, null, 2) + '\n', 'utf8');
if (!silent) log(` ✅ ${relPath}: module.name "${oldName || '(空)'}" → "${pkg}"`);
return [{ pkg, file: 'module.json5', status: 'fixed', oldName }];
} catch (e) {
if (!silent) log(` ❌ ${relPath}: 写入失败: ${e.message}`);
return [{ pkg, file: 'module.json5', status: 'error', message: e.message }];
}
}
if (!silent) log(` ✔️ ${relPath}: module.name = "${pkg}" (无须修正)`);
return [{ pkg, file: 'module.json5', status: 'unchanged' }];
}
/**
* 简单 JSON5 注释剥离
* 处理 // 单行注释 和 /* 块注释 *\/, 跳过字符串字面量内的 //
* 注意: 不处理裸 key、单引号字符串、Unicode 转义等 JSON5 扩展
* (本项目实际遇到的文件都是标准 JSON + 注释, 足够覆盖)
*/
function stripJson5Comments(input) {
let out = '';
let i = 0;
const n = input.length;
while (i < n) {
const c = input[i];
const next = i + 1 < n ? input[i + 1] : '';
if (c === '/' && next === '*') {
i += 2;
while (i < n && !(input[i] === '*' && input[i + 1] === '/')) i++;
i += 2;
continue;
}
if (c === '/' && next === '/') {
i += 2;
while (i < n && input[i] !== '\n') i++;
continue;
}
if (c === '"' || c === "'") {
const quote = c;
out += c;
i++;
while (i < n) {
const sc = input[i];
if (sc === '\\') {
out += sc + (input[i + 1] || '');
i += 2;
continue;
}
out += sc;
i++;
if (sc === quote) break;
}
continue;
}
out += c;
i++;
}
return out;
}
// 顶层 IIFE: 直接执行脚本时自动跑 (CLI 行为)
function main() {
try {
fixSubpackageModuleNames();
} catch (e) {
console.error('[fix-subpackage-module-names] 执行失败:', e.message);
process.exit(1);
}
}
// 判断是否作为 CLI 直接执行 (vs 作为模块被 require)
if (require.main === module) {
main();
}
module.exports = {
fixSubpackageModuleNames,
SUBPACKAGE_NAMES,
};