背景:
之前 vue.config.js 顶层 IIFE 只调 fixSubpackageModuleNames 修 5 个子包 name,
但漏改 build-profile.json5 / entry/oh-package.json5 / ascf_build/ 目录时
没有任何提示, 用户在 HBuilderX 看不到错误, 直到 DevEco ohpm 报错才发现。
改动:
1. scripts/fix-subpackage-module-names.js 新增 runSyncCheck() 函数:
- 读取 har-deps-required.json5 (whitelist) 作为单一事实源 (优先 json5 包,
失败回退手写解析)
- 扫描 3 个地方的一致性: build-profile.json5 modules / entry/oh-package.json5
file: 依赖 / ascf_build/ 实际目录
- 不一致时 console 打印明确错误 + 修复步骤 (1, 2, 3, 4 个文件)
- 默认不抛错 (silent: false 仅 console 输出, exitOnError: false)
- 暴露为 module.exports.runSyncCheck
2. vue.config.js 顶层 IIFE 集成:
- 修完 name 后调 runSyncCheck()
- HBuilderX 触发 require vue.config.js 时立即执行
- 不阻塞 vue-cli-service, 但 console 输出立刻告知用户漏改位置
- 鸿蒙端的硬性错误 (ohpm "Field IS Empty") 仍由 DevEco 抛出
端到端验证:
1. 故意在 whitelist 加 pages_extra (未同步其他 3 处)
→ vue.config.js require 立即输出:
✘ build-profile.json5 缺失子包: [pages_extra]
✘ entry/oh-package.json5 缺失 file: 依赖: [pages_extra]
✘ ascf_build/ 目录缺失子包: [pages_extra]
修复: 1. entry/har-deps-required.json5 ... 2. mp-harmony/build-profile.json5 ...
3. entry/oh-package.json5 ... 4. ascf_build/<pkg>/ 实际目录
2. 故意清空 pages_tool 的 name 字段
→ 修成 "pages_tool" + 跑 sync check, 输出 ✓ 5 个子包配置完全一致
module.exports 仍只有白名单字段 (productionSourceMap, configureWebpack),
vue-cli-service 不会报 "Invalid options" 错。
依赖: 项目已有 json5 包, 无需新装
429 lines
16 KiB
JavaScript
429 lines
16 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);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 同步子包配置一致性检查 (被 vue.config.js 顶层 IIFE 调用)
|
|
*
|
|
* 用途: HBuilderX 触发 require('./vue.config.js') 时, 在修完 name 之后
|
|
* 跑一次 4 处一致性检查, 立即告诉用户哪些地方没改。
|
|
*
|
|
* 检查项 (复用 SUBPACKAGE_NAMES 数组):
|
|
* 1. mp-harmony/build-profile.json5 的 modules 数组 (排除 entry)
|
|
* 2. entry/oh-package.json5 的 file: 依赖
|
|
* 3. ascf_build 实际目录
|
|
*
|
|
* 与 scripts/check-subpackage-sync.js 的区别:
|
|
* - check-subpackage-sync.js: CLI/CI 用, --strict 可 exit 1
|
|
* - runSyncCheck: vue.config.js IIFE 用, 默认不抛错, 仅打印
|
|
*
|
|
* @param {object} [opts]
|
|
* @param {boolean} [opts.silent=false] - true 时不打日志
|
|
* @param {boolean} [opts.exitOnError=false] - true 时不一致即抛错 (CLI 模式)
|
|
* @returns {{ok: boolean, errors: string[], warnings: string[]}}
|
|
*/
|
|
function runSyncCheck({ silent = false, exitOnError = false } = {}) {
|
|
const log = silent ? () => {} : console.log;
|
|
const ROOT = path.resolve(__dirname, '..');
|
|
|
|
// 读取白名单 (从 har-deps-required.json5 拿, 与 check-subpackage-sync.js 保持一致)
|
|
// 注: 优先用 json5 npm 包 (项目已装), 失败则回退到内置 SUBPACKAGE_NAMES
|
|
const configPath = path.join(ROOT, 'stub-app-projects', 'mp-harmony', 'entry', 'har-deps-required.json5');
|
|
let canonical = SUBPACKAGE_NAMES.slice().sort();
|
|
try {
|
|
if (fs.existsSync(configPath)) {
|
|
const raw = fs.readFileSync(configPath, 'utf8');
|
|
let cfg = null;
|
|
try {
|
|
// 优先: json5 包 (支持裸 key, 注释, 单引号字符串)
|
|
cfg = require('json5').parse(raw);
|
|
} catch (e1) {
|
|
// 回退: 手写 JSON5 解析 (剥注释后用 JSON.parse)
|
|
try {
|
|
const cleaned = stripJson5Comments(raw);
|
|
cfg = JSON.parse(cleaned);
|
|
} catch (e2) {
|
|
if (!silent) console.warn(` ⚠️ har-deps-required.json5 解析失败 (json5: ${e1.message}; json: ${e2.message}), 用内置 SUBPACKAGE_NAMES`);
|
|
}
|
|
}
|
|
if (cfg && Array.isArray(cfg.requiredHarDeps) && cfg.requiredHarDeps.length) {
|
|
canonical = cfg.requiredHarDeps.slice().sort();
|
|
}
|
|
}
|
|
} catch (e) {
|
|
if (!silent) console.warn(' ⚠️ 读取 har-deps-required.json5 失败, 用内置 SUBPACKAGE_NAMES:', e.message);
|
|
}
|
|
|
|
const canonicalSet = new Set(canonical);
|
|
const errors = [];
|
|
const warnings = [];
|
|
|
|
// 工具: 列出某目录下的子目录
|
|
function listSubdirs(p) {
|
|
try {
|
|
return fs.readdirSync(p, { withFileTypes: true })
|
|
.filter(e => e.isDirectory())
|
|
.map(e => e.name)
|
|
.sort();
|
|
} catch {
|
|
return [];
|
|
}
|
|
}
|
|
|
|
function diff(b) {
|
|
const bSet = new Set(b);
|
|
return {
|
|
onlyA: [...canonicalSet].filter(x => !bSet.has(x)),
|
|
onlyB: [...bSet].filter(x => !canonicalSet.has(x)),
|
|
};
|
|
}
|
|
|
|
// === 检查 1: build-profile.json5 ===
|
|
const buildProfilePath = path.join(ROOT, 'stub-app-projects', 'mp-harmony', 'build-profile.json5');
|
|
let buildProfileModules = [];
|
|
if (!fs.existsSync(buildProfilePath)) {
|
|
warnings.push(`build-profile.json5 不存在: ${buildProfilePath}`);
|
|
} else {
|
|
try {
|
|
const raw = fs.readFileSync(buildProfilePath, 'utf8').replace(/\/\/.*$/gm, '');
|
|
const bp = JSON.parse(raw);
|
|
buildProfileModules = (bp.modules || []).map(m => m.name).filter(n => n && n !== 'entry');
|
|
} catch (e) {
|
|
errors.push(`build-profile.json5 解析失败: ${e.message}`);
|
|
}
|
|
}
|
|
const d1 = diff(buildProfileModules);
|
|
if (d1.onlyA.length) errors.push(`build-profile.json5 缺失子包: [${d1.onlyA.join(', ')}]`);
|
|
if (d1.onlyB.length) errors.push(`build-profile.json5 多了子包: [${d1.onlyB.join(', ')}]`);
|
|
|
|
// === 检查 2: entry/oh-package.json5 ===
|
|
const ohPkgPath = path.join(ROOT, 'stub-app-projects', 'mp-harmony', 'entry', 'oh-package.json5');
|
|
let ohPkgDeps = [];
|
|
if (!fs.existsSync(ohPkgPath)) {
|
|
warnings.push(`entry/oh-package.json5 不存在: ${ohPkgPath}`);
|
|
} else {
|
|
try {
|
|
const raw = fs.readFileSync(ohPkgPath, 'utf8').replace(/\/\/.*$/gm, '');
|
|
const op = JSON.parse(raw);
|
|
const deps = op.dependencies || {};
|
|
ohPkgDeps = Object.keys(deps).filter(k => deps[k] && deps[k].startsWith('file:'));
|
|
} catch (e) {
|
|
errors.push(`entry/oh-package.json5 解析失败: ${e.message}`);
|
|
}
|
|
}
|
|
const d2 = diff(ohPkgDeps);
|
|
if (d2.onlyA.length) errors.push(`entry/oh-package.json5 缺失 file: 依赖: [${d2.onlyA.join(', ')}]`);
|
|
if (d2.onlyB.length) warnings.push(`entry/oh-package.json5 多了 file: 依赖: [${d2.onlyB.join(', ')}] (可能是新子包未更新白名单)`);
|
|
|
|
// === 检查 3: ascf_build 实际目录 ===
|
|
const ascfBuildRoot = path.join(ROOT, 'stub-app-projects', 'mp-harmony', 'ascf', 'ascf_build');
|
|
const ascfBuildDirs = listSubdirs(ascfBuildRoot);
|
|
const d3 = diff(ascfBuildDirs);
|
|
if (d3.onlyA.length) errors.push(`ascf_build/ 目录缺失子包: [${d3.onlyA.join(', ')}]`);
|
|
if (d3.onlyB.length) warnings.push(`ascf_build/ 多了子包: [${d3.onlyB.join(', ')}] (可能是历史遗留)`);
|
|
|
|
// === 输出 ===
|
|
if (silent) {
|
|
// 静默模式仅返回结果
|
|
return { ok: errors.length === 0, errors, warnings };
|
|
}
|
|
|
|
const c = (s, color) => {
|
|
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[color] + s + codes.reset;
|
|
};
|
|
|
|
if (errors.length === 0 && warnings.length === 0) {
|
|
log(c(`\n🔍 [sync check] ✓ ${canonical.length} 个子包在 3 处配置完全一致 (build-profile / oh-package / ascf_build)`, 'green'));
|
|
} else {
|
|
log(c(`\n🔍 [sync check] ${errors.length === 0 ? '⚠️' : '✘'} 4 处子包配置不一致:`, errors.length === 0 ? 'yellow' : 'red'));
|
|
for (const w of warnings) log(c(` ⚠️ ${w}`, 'yellow'));
|
|
for (const e of errors) log(c(` ✘ ${e}`, 'red'));
|
|
if (errors.length > 0) {
|
|
log(c(`\n 修复: 同步更新以下 4 个文件 (whitelist 是 har-deps-required.json5):`, 'gray'));
|
|
log(c(` 1. entry/har-deps-required.json5 (whitelist, 单一事实源)`, 'gray'));
|
|
log(c(` 2. mp-harmony/build-profile.json5 modules 数组`, 'gray'));
|
|
log(c(` 3. entry/oh-package.json5 dependencies 加 "file:../ascf/ascf_build/<pkg>"`, 'gray'));
|
|
log(c(` 4. ascf_build/<pkg>/ 实际目录`, 'gray'));
|
|
log(c(` DevEco Build 仍会因 ohpm 校验而失败, 修完再发`, 'gray'));
|
|
}
|
|
}
|
|
log('');
|
|
|
|
if (exitOnError && errors.length > 0) {
|
|
throw new Error(`[sync check] ${errors.length} 个不一致`);
|
|
}
|
|
|
|
return { ok: errors.length === 0, errors, warnings };
|
|
}
|
|
|
|
// 判断是否作为 CLI 直接执行 (vs 作为模块被 require)
|
|
if (require.main === module) {
|
|
main();
|
|
}
|
|
|
|
module.exports = {
|
|
fixSubpackageModuleNames,
|
|
runSyncCheck,
|
|
SUBPACKAGE_NAMES,
|
|
}; |