#!/usr/bin/env node /** * Post-build: 给鸿蒙产物里所有"非页面"组件 .json 注入 componentPlaceholder. * * 触发原因: * uni-app 把每个 easycom diy-* 编译成 function () { return Promise.all([...]) } 工厂 * —— Vue 视为 asyncComponent. 鸿蒙/FormAbility 运行时在子组件级检查 placeholder, * 不会自页面级继承. 即使子组件 .json 的 usingComponents 已列出 diy-icon, * 仍触发 "is asyncComponent please add placeHolder". * * 检测策略: * 鸿蒙构建产物里没有 .vue 文件 (已编译成 .js / .hxml). 改用 .json 上的 usingComponents: * 若任意 diy-* 名称 (出现在右侧 target 路径 /components-diy/diy-*) → 该组件需要 placeholder. * * 工作流: * 1. 给定构建产物根目录 * 2. 递归遍历所有 .json * a. 跳过顶层 app.json 与登记在 app.json 的页面 (那些由 scripts/inject-placeholder.js 处理) * b. 跳过没有 usingComponents 的 * c. 扫 usingComponents, 收集所有 diy-* 名字 * d. 若已经含 componentPlaceholder, 跳过 * e. 否则在对象内插入 componentPlaceholder 块 * 3. 仅在文件变化时写回 */ const fs = require('fs'); const path = require('path'); const PROJECT_ROOT = path.join(__dirname, '..'); const DIY_DIR = path.join(PROJECT_ROOT, 'components-diy'); const APP_JSON_NAME = 'app.json'; function collectDiyNames() { if (!fs.existsSync(DIY_DIR)) return []; return fs.readdirSync(DIY_DIR) .filter(f => f.startsWith('diy-') && f.endsWith('.vue')) .map(f => f.slice(0, -'.vue'.length)) .sort(); } const DIY_LIST = collectDiyNames(); const DIY_SET = new Set(DIY_LIST); /** * 从 app.json 读取哪些路径是页面 (顶层 pages + 各级 subpackages[].pages). */ function collectPageSet(rootDir) { const appPath = path.join(rootDir, APP_JSON_NAME); const set = new Set(); if (!fs.existsSync(appPath)) return set; let json; try { json = JSON.parse(fs.readFileSync(appPath, 'utf-8')); } catch (e) { return set; } function addList(prefix, arr) { for (const p of arr || []) { const full = prefix ? `${prefix}/${p}` : p; set.add(full); } } addList('', json.pages); for (const sp of json.subpackages || []) { addList(sp.root, sp.pages); } return set; } function hasPlaceholder(txt) { return /"componentPlaceholder"\s*:/.test(txt); } /** * 把 componentPlaceholder 块插入到 JSON 顶层首个键之前, 末尾带逗号. * 输入: { "key1": ..., "key2": ... } * 输出: { "componentPlaceholder": { ... }, "key1": ..., "key2": ... } */ function injectPlaceholder(jsonText) { const sampleKey = jsonText.match(/[\r\n]([\t ]*)"[^"\r\n]+"\s*:/); if (!sampleKey) return null; const topIndent = sampleKey[1]; const innerIndent = topIndent + '\t'; const EOL = jsonText.includes('\r\n') ? '\r\n' : '\n'; const firstKeyMatch = jsonText.match(/[\r\n][\t ]*"[^"\r\n]+"\s*:/); if (!firstKeyMatch) return null; const eolLen = jsonText[firstKeyMatch.index] === '\r' ? 2 : 1; const insertLineStart = firstKeyMatch.index + eolLen; const blockLines = []; blockLines.push(`${topIndent}"componentPlaceholder": {`); for (let i = 0; i < DIY_LIST.length; i++) { const name = DIY_LIST[i]; const isLast = i === DIY_LIST.length - 1; blockLines.push(`${innerIndent}"${name}": "view"${isLast ? '' : ','}`); } blockLines.push(`${topIndent}},`); const blockText = blockLines.join(EOL) + EOL; const before = jsonText.slice(0, insertLineStart); const after = jsonText.slice(insertLineStart); const newText = before + blockText + after; return newText === jsonText ? null : newText; } function processJson(jsonPath, rootDir, pageSet) { const rel = path.relative(rootDir, jsonPath).replace(/\\/g, '/'); if (rel === APP_JSON_NAME) return 'app-skip'; // 顶层页面 .json 跳过 (页面级由 scripts/inject-placeholder.js 处理) if (pageSet.has(rel.replace(/\.json$/, ''))) return 'page-skip'; const txt = fs.readFileSync(jsonPath, 'utf-8'); if (hasPlaceholder(txt)) return 'has-placeholder'; const m = txt.match(/"usingComponents"\s*:\s*\{([\s\S]*?)\}/); if (!m) return 'no-usingComponents'; const inner = m[1]; const diyRefs = []; const reKey = /"([^"]+)"\s*:/g; let km; while ((km = reKey.exec(inner)) !== null) { if (DIY_SET.has(km[1])) diyRefs.push(km[1]); } if (diyRefs.length === 0) return 'no-diy'; const newText = injectPlaceholder(txt); if (!newText) return 'inject-skipped'; fs.writeFileSync(jsonPath, newText, 'utf-8'); return 'injected'; } function walkDir(root, onJson) { const entries = fs.readdirSync(root, { withFileTypes: true }); for (const e of entries) { if (e.name === 'node_modules' || e.name.startsWith('.')) continue; const p = path.join(root, e.name); if (e.isDirectory()) { walkDir(p, onJson); } else if (e.isFile() && e.name.endsWith('.json')) { onJson(p); } } } function run(rootDir) { if (!rootDir) { console.error('用法: node scripts/harmony-component-placeholder.js <产物根目录>'); process.exit(1); } if (!fs.existsSync(rootDir)) { console.error(`目录不存在: ${rootDir}`); process.exit(1); } const pageSet = collectPageSet(rootDir); console.log(`[HarmonyPlaceholder] 扫描根: ${rootDir}`); console.log(`[HarmonyPlaceholder] 页面清单大小: ${pageSet.size}, diy 组件清单大小: ${DIY_LIST.length}`); const stats = { injected: 0, hasPlaceholder: 0, pageSkip: 0, noDiy: 0, noUsingComponents: 0, errors: 0 }; walkDir(rootDir, (jsonPath) => { try { const rel = path.relative(rootDir, jsonPath).replace(/\\/g, '/'); const r = processJson(jsonPath, rootDir, pageSet); if (r === 'injected') { stats.injected++; console.log(` ✓ 注入: ${rel}`); } else if (r === 'has-placeholder') { stats.hasPlaceholder++; } else if (r === 'page-skip') { stats.pageSkip++; } else if (r === 'no-diy') { stats.noDiy++; } else if (r === 'no-usingComponents') { stats.noUsingComponents++; } } catch (e) { stats.errors++; console.error(` ERR ${jsonPath}: ${e.message}`); } }); console.log(`[HarmonyPlaceholder] 注入: ${stats.injected}, 已有 placeholder: ${stats.hasPlaceholder}, 跳过 (页面): ${stats.pageSkip}, 无 diy: ${stats.noDiy}, 无 usingComponents: ${stats.noUsingComponents}, 错误: ${stats.errors}`); } const target = process.argv[2]; run(target);