#!/usr/bin/env node /* eslint-disable no-console */ /** * scripts/fix-subpackage-module-names.js * * 修正 ascf_build// 子包 HAR 元数据中的 name 字段,确保鸿蒙工具链 * (ohpm install / hvigor schema / 运行时依赖解析) 一致通过。 * * 涉及两个文件: * 1. ascf_build//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//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// 子包的 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//oh-package.json5 的 top-level `name` 字段 * * 默认内容参考项目内已有 HAR (components_diy / pages_goods 等): * { * "name": "", * "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//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/"`, 'gray')); log(c(` 4. ascf_build// 实际目录`, '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, };