diff --git a/scripts/harmony-component-placeholder.js b/scripts/harmony-component-placeholder.js new file mode 100644 index 0000000..76bfeea --- /dev/null +++ b/scripts/harmony-component-placeholder.js @@ -0,0 +1,180 @@ +#!/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); \ No newline at end of file diff --git a/scripts/harmony.patch.js b/scripts/harmony.patch.js index 37f5929..7358b14 100644 --- a/scripts/harmony.patch.js +++ b/scripts/harmony.patch.js @@ -58,6 +58,19 @@ async function copyAssets(mode = 'production') { } await Promise.all(copyPromises); + + // 给所有子组件 .json 注入 componentPlaceholder, 避免 "diy-* is asyncComponent" 报错. + // 见 scripts/harmony-component-placeholder.js 说明. 该脚本跳过页面级, 只处理组件. + try { + const { execFileSync } = require('child_process'); + execFileSync(process.execPath, [ + path.join(__dirname, 'harmony-component-placeholder.js'), + destDir, + ], { stdio: 'inherit', cwd }); + } catch (e) { + console.warn('[harmony.patch] ⚠️ harmony-component-placeholder 注入失败:', e.message); + } + console.log('HarmonyOS 资源拷贝完成'); } catch (err) { console.error('发生错误:', err);