#!/usr/bin/env node /* eslint-disable no-console */ /** * 鸿蒙元服务 entry 模块依赖校验脚本 * * 目的: * 防止 entry 的 HAP 打包时丢失 5 个 ascf shared HAR 子包依赖, * 否则运行时会找不到 components-diy/diy-group.pack.js 等子包 chunk, * DIY 首页 + 商品分类 + 订单 + 营销页全部失效 (componentPlaceholder fallback 成 view)。 * * 使用: * node scripts/check-har-deps.js # 仅校验 entry/build/.../module.json * node scripts/check-har-deps.js --rawfile # 同时校验 rawfile 资源是否就位 * node scripts/check-har-deps.js --strict # 任一缺失即 exit 1 * * 默认情况: * 缺失时仅打印警告 (CI 友好, 不阻塞开发), 加 --strict 才严格失败。 * * 依赖: * ./har-deps-required.json5 (本脚本的单一事实来源) */ 'use strict'; const fs = require('fs'); const path = require('path'); const parseJson5 = require('./load-json5'); const ENTRY_DIR = path.resolve(__dirname, '..'); const CONFIG_PATH = path.join(ENTRY_DIR, 'har-deps-required.json5'); function loadJson5(filePath) { const raw = fs.readFileSync(filePath, 'utf8'); return parseJson5(raw); } function exists(p) { try { fs.accessSync(p, fs.constants.F_OK); return true; } catch { return false; } } function listDir(p) { try { return fs.readdirSync(p); } catch { return []; } } function color(s, c) { const codes = { red: '\x1b[31m', green: '\x1b[32m', yellow: '\x1b[33m', cyan: '\x1b[36m', gray: '\x1b[90m', reset: '\x1b[0m', }; return process.stdout.isTTY ? codes[c] + s + codes.reset : s; } function main() { const args = process.argv.slice(2); const checkRawfile = args.includes('--rawfile'); const strict = args.includes('--strict'); if (!exists(CONFIG_PATH)) { console.error(color(`✘ 缺少白名单配置: ${CONFIG_PATH}`, 'red')); process.exit(strict ? 1 : 0); } const config = loadJson5(CONFIG_PATH); const requiredHarDeps = config.requiredHarDeps || []; const expectedSources = config.expectedDependencySources || {}; const moduleJsonRel = (config.buildArtifacts && config.buildArtifacts.moduleJson) || 'build/default/intermediates/package/default/module.json'; const moduleJsonPath = path.join(ENTRY_DIR, moduleJsonRel); console.log(color('=== HAR 子包依赖校验 ===', 'cyan')); console.log(`配置白名单: ${requiredHarDeps.join(', ')}`); console.log(`module.json: ${moduleJsonPath}`); console.log(); const errors = []; const warnings = []; // ---- 1. 校验 module.json dependencies ---- if (!exists(moduleJsonPath)) { warnings.push(`module.json 不存在 (${moduleJsonPath}), 跳过依赖检查。可能尚未 Build。`); } else { let mj; try { mj = JSON.parse(fs.readFileSync(moduleJsonPath, 'utf8')); } catch (e) { errors.push(`module.json 解析失败: ${e.message}`); } if (mj) { const deps = (mj.module && mj.module.dependencies) || mj.dependencies || []; const depNames = new Set(deps.map(d => d.moduleName)); for (const har of requiredHarDeps) { if (depNames.has(har)) { console.log(color(` ✔ module.json.dependencies 包含 ${har}`, 'green')); } else { errors.push(`module.json.dependencies 缺失 ${har} (实际: ${[...depNames].join(', ') || '空'})`); console.log(color(` ✘ module.json.dependencies 缺失 ${har}`, 'red')); } } } } // ---- 2. 校验 oh_modules 子包资源是否就位 (可选) ---- if (checkRawfile) { console.log(); console.log(color('--- oh_modules 子包资源检查 ---', 'cyan')); const ohModulesRoots = (config.ohModulesRoots) || {}; const ohModulesDir = path.join(ENTRY_DIR, 'oh_modules'); if (!exists(ohModulesDir)) { warnings.push(`oh_modules 目录不存在 (${ohModulesDir}), 请先在 DevEco 中 Sync 项目`); } for (const har of requiredHarDeps) { const cfg = ohModulesRoots[har]; if (!cfg) continue; const subDir = path.join(ohModulesDir, cfg.pkg, 'src', 'main', 'resources', 'rawfile', cfg.root); if (!exists(subDir)) { errors.push(`oh_modules/${cfg.pkg}/.../rawfile/${cfg.root} 缺失 (鸿蒙运行时会找不到组件)`); console.log(color(` ✘ oh_modules/${cfg.pkg}/.../rawfile/${cfg.root} 缺失`, 'red')); } else { const files = listDir(subDir); const packFiles = files.filter(f => f.endsWith('.pack.js')); const inner = listDir(subDir).filter(f => { try { return fs.statSync(path.join(subDir, f)).isDirectory(); } catch { return false; } }); const totalPack = packFiles.length + inner.reduce((acc, d) => { try { return acc + fs.readdirSync(path.join(subDir, d)).filter(f => f.endsWith('.pack.js')).length; } catch { return acc; } }, 0); if (totalPack === 0) { warnings.push(`oh_modules/${cfg.pkg}/.../rawfile/${cfg.root} 存在但无 .pack.js`); console.log(color(` ⚠ oh_modules/${cfg.pkg}/.../rawfile/${cfg.root} 无 .pack.js`, 'yellow')); } else { console.log(color(` ✔ oh_modules/${cfg.pkg}/.../rawfile/${cfg.root} 存在, ${totalPack} 个 .pack.js`, 'green')); } } } } // ---- 3. 校验 oh-package.json5 的 dependencies 与白名单一致 ---- const ohPkgPath = path.join(ENTRY_DIR, 'oh-package.json5'); if (exists(ohPkgPath)) { console.log(); console.log(color('--- oh-package.json5 检查 ---', 'cyan')); const ohPkg = loadJson5(ohPkgPath); const ohDeps = ohPkg.dependencies || {}; for (const har of requiredHarDeps) { if (!(har in ohDeps)) { errors.push(`oh-package.json5.dependencies 缺失 ${har}`); console.log(color(` ✘ oh-package.json5 缺失 ${har}`, 'red')); } else { const expected = expectedSources[har]; const actual = ohDeps[har]; if (expected && actual !== expected) { warnings.push(`oh-package.json5 中 ${har} 的 source 不是预期: ${actual} (预期 ${expected})`); console.log(color(` ⚠ ${har} source 异常: ${actual}`, 'yellow')); } else { console.log(color(` ✔ oh-package.json5 包含 ${har} (${actual})`, 'green')); } } } } // ---- 4. 校验 ascf_build 子包 module.json5 的 module.name 字段 ---- // 防止 vue.config.js 重新生成时把 module.name 清空, 导致 hvigor schema 校验失败 console.log(); console.log(color('--- ascf_build 子包 module.json5 检查 ---', 'cyan')); const mpHarmonyDir = path.resolve(ENTRY_DIR, '..'); const subpackageModuleJson5 = config.subpackageModuleJson5 || {}; for (const har of requiredHarDeps) { const rel = subpackageModuleJson5[har]; if (!rel) continue; const moduleJson5Path = path.resolve(mpHarmonyDir, rel); if (!exists(moduleJson5Path)) { warnings.push(`ascf_build/${har}/src/main/module.json5 缺失 (${moduleJson5Path})`); console.log(color(` ⚠ ${moduleJson5Path} 不存在`, 'yellow')); continue; } let mj; try { mj = loadJson5(moduleJson5Path); } catch (e) { errors.push(`${har} 的 module.json5 解析失败: ${e.message}`); console.log(color(` ✘ ${har} module.json5 解析失败`, 'red')); continue; } const moduleName = mj && mj.module && mj.module.name; if (!moduleName || moduleName !== har) { errors.push(`${har} 的 module.json5.module.name 异常: "${moduleName}" (应为 "${har}", 常见原因: vue.config.js 重新生成时未写入 packageName)`); console.log(color(` ✘ ${har} module.name = "${moduleName}" (应为 "${har}")`, 'red')); } else { console.log(color(` ✔ ${har} module.name = "${moduleName}"`, 'green')); } } // ---- 总结 ---- console.log(); if (warnings.length) { console.log(color(`警告 (${warnings.length}):`, 'yellow')); for (const w of warnings) console.log(' - ' + w); } if (errors.length) { console.log(); console.log(color(`错误 (${errors.length}):`, 'red')); for (const e of errors) console.log(' - ' + e); console.log(); console.log(color('✘ 校验失败: entry 模块缺少必要的 HAR 子包依赖。', 'red')); console.log(color(' 原因: 这会导致运行时找不到 components-diy/diy-group.pack.js 等子包 chunk,', 'gray')); console.log(color(' DIY 首页 + 商品分类 + 订单 + 营销页全部失效, 12s splash 兜底超时。', 'gray')); console.log(color(' 修复: 在 entry/oh-package.json5 的 dependencies 里补齐 5 个 file: 依赖,', 'gray')); console.log(color(' 同步 ./har-deps-required.json5, 然后重新 Build。', 'gray')); process.exit(strict ? 1 : 0); } console.log(color('✔ 校验通过', 'green')); } try { main(); } catch (e) { console.error(color(`脚本异常: ${e.stack || e.message}`, 'red')); process.exit(1); }