feat(scripts): add harmony component placeholder injection logic
新增鸿蒙组件级componentPlaceholder自动注入脚本,解决diy组件编译为asyncComponent导致的运行时报错,自动遍历处理非页面组件的json配置文件,同时添加异常捕获避免构建中断
This commit is contained in:
180
scripts/harmony-component-placeholder.js
Normal file
180
scripts/harmony-component-placeholder.js
Normal file
@@ -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);
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user