deveco_hmigbot 0.21.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +26 -0
- package/README.md +50 -0
- package/agents/hmigbot-worker.md +61 -0
- package/agents/hmigbot.md +22 -0
- package/agents/workflow-subagent.md +55 -0
- package/commands/hmigbot.md +17 -0
- package/dist/index.js +1 -0
- package/manifest.json +19 -0
- package/package.json +29 -0
- package/skills/migrate-core/FILES.md +26 -0
- package/skills/migrate-core/SKILL.md +484 -0
- package/skills/migrate-core/references/README.md +64 -0
- package/skills/migrate-core/references/flow/arkts-vector-gate.md +55 -0
- package/skills/migrate-core/references/flow/build-error-patterns.md +52 -0
- package/skills/migrate-core/references/flow/conventions-template.md +244 -0
- package/skills/migrate-core/references/flow/navigation-migration.md +42 -0
- package/skills/migrate-core/references/flow/platform-api-guards.md +59 -0
- package/skills/migrate-core/references/flow/platform-model-gaps.md +43 -0
- package/skills/migrate-core/references/flow/resource-conversion.md +46 -0
- package/skills/migrate-core/references/flow/ui-layout-semantics.md +124 -0
- package/skills/migrate-core/references/flow/unit-breakdown.md +42 -0
- package/skills/migrate-core/references/host-capabilities.md +24 -0
- package/skills/migrate-core/references/topics/app-identity.md +214 -0
- package/skills/migrate-core/references/topics/env-doctor.md +245 -0
- package/skills/migrate-core/references/topics/i18n/README.md +458 -0
- package/skills/migrate-core/references/topics/i18n/references/code-examples.md +304 -0
- package/skills/migrate-core/references/topics/i18n/references/common-pitfalls.md +354 -0
- package/skills/migrate-core/references/topics/i18n/references/dynamic-language-switch.md +464 -0
- package/skills/migrate-core/references/topics/i18n/references/language-codes.md +104 -0
- package/skills/migrate-core/references/topics/icon-sizing.md +98 -0
- package/skills/migrate-core/references/topics/library-migration/README.md +234 -0
- package/skills/migrate-core/references/topics/library-migration/closed-source-sdk.md +128 -0
- package/skills/migrate-core/references/topics/library-migration/download-api-decision.md +84 -0
- package/skills/migrate-core/references/topics/library-migration/library-mapping-table.md +100 -0
- package/skills/migrate-core/references/topics/library-migration/napi-compile-guide.md +84 -0
- package/skills/migrate-core/references/topics/library-migration/ohpm-search-guide.md +73 -0
- package/skills/migrate-core/references/topics/library-migration/stdlib-mapping-table.md +34 -0
- package/skills/migrate-core/references/topics/resources/aar-decompile.md +25 -0
- package/skills/migrate-core/references/topics/resources/conversion-rules.md +625 -0
- package/skills/migrate-core/references/topics/resources/dependency-analysis-rules.md +328 -0
- package/skills/migrate-core/references/topics/resources/material-design-icons.md +173 -0
- package/skills/migrate-core/references/topics/resources/svg-fix-patterns.md +175 -0
- package/skills/migrate-core/references/topics/resources/xml-drawable-to-svg-rules.md +513 -0
- package/skills/migrate-core/references/topics/system-capabilities/README.md +331 -0
- package/skills/migrate-core/references/topics/system-capabilities/avplayer-guide.md +161 -0
- package/skills/migrate-core/references/topics/system-capabilities/background-tasks.md +403 -0
- package/skills/migrate-core/references/topics/system-capabilities/browser-intent.md +121 -0
- package/skills/migrate-core/references/topics/system-capabilities/camera-picker.md +118 -0
- package/skills/migrate-core/references/topics/system-capabilities/document-picker.md +246 -0
- package/skills/migrate-core/references/topics/system-capabilities/file-utils.md +131 -0
- package/skills/migrate-core/references/topics/system-capabilities/permission-helper.md +112 -0
- package/skills/migrate-core/references/topics/system-capabilities/photo-access-helper.md +208 -0
- package/skills/migrate-core/references/topics/system-capabilities/print-management.md +213 -0
- package/skills/migrate-core/references/topics/system-capabilities/share-panel.md +177 -0
- package/skills/migrate-core/references/topics/system-capabilities/system-settings.md +322 -0
- package/skills/migrate-core/references/topics/system-capabilities/telephony-dial.md +49 -0
- package/skills/migrate-core/references/topics/system-capabilities/video-playback.md +42 -0
- package/skills/migrate-core/references/topics/system-capabilities/webview-patterns.md +38 -0
- package/skills/migrate-core/references/topics/ui-alignment/README.md +344 -0
- package/skills/migrate-core/references/topics/ui-alignment/references/dark-mode.md +47 -0
- package/skills/migrate-core/references/topics/ui-alignment/references/layout-mapping.md +301 -0
- package/skills/migrate-core/references/topics/ui-alignment/references/visual-patterns.md +411 -0
- package/skills/migrate-core/scripts/closure/check-anchors.mjs +186 -0
- package/skills/migrate-core/scripts/closure/check-api-guards.mjs +175 -0
- package/skills/migrate-core/scripts/closure/check-consumers.mjs +301 -0
- package/skills/migrate-core/scripts/closure/check-permissions.mjs +165 -0
- package/skills/migrate-core/scripts/closure/check-resources.mjs +130 -0
- package/skills/migrate-core/scripts/closure/check-routes.mjs +527 -0
- package/skills/migrate-core/scripts/closure/check-safearea.mjs +122 -0
- package/skills/migrate-core/scripts/closure/check-stubs.mjs +69 -0
- package/skills/migrate-core/scripts/closure/closure-suite.mjs +256 -0
- package/skills/migrate-core/scripts/closure/idioms.json +105 -0
- package/skills/migrate-core/scripts/convert/convert-resources.mjs +437 -0
- package/skills/migrate-core/scripts/feasibility/feasibility.mjs +235 -0
- package/skills/migrate-core/scripts/feasibility/tables/cross-platform.json +11 -0
- package/skills/migrate-core/scripts/feasibility/tables/deprecated-api.json +10 -0
- package/skills/migrate-core/scripts/feasibility/tables/imported-arkts-core.json +425 -0
- package/skills/migrate-core/scripts/feasibility/tables/lib-equivalence.json +206 -0
- package/skills/migrate-core/scripts/feasibility/tables/system-capabilities.json +22 -0
- package/skills/migrate-core/scripts/front.mjs +107 -0
- package/skills/migrate-core/scripts/interface/ark-extract.mjs +172 -0
- package/skills/migrate-core/scripts/interface/interface.mjs +152 -0
- package/skills/migrate-core/scripts/ledger/ledger.mjs +383 -0
- package/skills/migrate-core/scripts/ledger/parse-cards.mjs +98 -0
- package/skills/migrate-core/scripts/lib/literals.mjs +37 -0
- package/skills/migrate-core/scripts/lib/scan.mjs +315 -0
- package/skills/migrate-core/scripts/smoke/align-sdk.mjs +118 -0
- package/skills/migrate-core/scripts/smoke/ensure-sign.mjs +53 -0
- package/skills/migrate-core/scripts/smoke/smoke.mjs +238 -0
- package/skills/migrate-core/scripts/smoke/verdict.mjs +31 -0
- package/skills/migrate-core/scripts/smoke/walk.mjs +480 -0
- package/skills/migrate-core/scripts/transpile/mapping.json +76 -0
- package/skills/migrate-core/scripts/transpile/transpile-layout.mjs +404 -0
- package/skills/migrate-core/scripts/vectors/run-arkts-vectors.mjs +107 -0
- package/skills/migrate-core/scripts/vectors/setup-arkts-test.mjs +90 -0
- package/skills/migrate-core/scripts/wire/extractors.mjs +258 -0
- package/skills/migrate-core/scripts/wire/wire-routes.mjs +507 -0
- package/skills/migrate-core/templates/acceptance.js +365 -0
- package/skills/migrate-core/templates/explore.js +86 -0
- package/skills/migrate-core/templates/implement.js +211 -0
- package/skills/migrate-core/templates/mig_slices.js +491 -0
|
@@ -0,0 +1,507 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* wire-routes.mjs — 路由预接线生成器(静态前场件)。
|
|
4
|
+
*
|
|
5
|
+
* 从 AndroidManifest 提取 activity 全集 → 生成鸿蒙侧路由基建:
|
|
6
|
+
* common/Router.ets(Routes 枚举 + AppRouter 包装器,goHome 指向 launcher,pushName 供冒烟直达)
|
|
7
|
+
* pages/Index.ets(@Entry Navigation 壳 + 自动入栈 launcher + smokeRoute 直达钩子 + hmigSmoke 着陆日志)
|
|
8
|
+
* pages/<X>Page.ets(每路由骨架:Builder + NavDestination + 源锚点注释;注册页文件 = 切片交付文件)
|
|
9
|
+
* resources/base/profile/route_map.json(systemRouterMap,**唯一注册真源**)+ module.json5 routerMap 引用
|
|
10
|
+
* entryability/EntryAbility.ets 幂等补丁(onCreate 透传 smokeRoute)
|
|
11
|
+
* main_pages.json + wiring-report.json
|
|
12
|
+
*
|
|
13
|
+
* 效果:"页面可达"由构造保证——注册表生成即闭合(零 ghost),切片阶段只填行为不碰壳。
|
|
14
|
+
* 纪律:绝不覆盖已存在文件(跳过并记 report;Index 样板壳与 EntryAbility 透传除外,见各处注释);
|
|
15
|
+
* 写法采用已被 check-routes 实证解析的枚举+包装器模式(与基准靶同构)。
|
|
16
|
+
*
|
|
17
|
+
* 路由真源三轨(形态自适应,--src 模式全跑合并):
|
|
18
|
+
* ① AndroidManifest activity(多 Activity 应用)
|
|
19
|
+
* ② res/navigation/*.xml(Jetpack Navigation 配置)
|
|
20
|
+
* ③ Compose NavHost composable()(单 Activity Compose——manifest 只见得到壳,实证 1 vs 11)
|
|
21
|
+
* 单 Activity 壳让位规则:manifest 仅 1 activity 且②/③提出多路由时,壳不成页(壳=鸿蒙 Index 容器)。
|
|
22
|
+
* 目标注册单轨:resources/.../route_map.json(官方 systemRouterMap)是唯一真源——首跑全集、
|
|
23
|
+
* `--from-ledger spec/ledger/ledger.json` 二跑按页面账补漏(Routes 枚举/route_map 增量合并),
|
|
24
|
+
* 新页只经 route_map 注册即可被 pushPathByName 加载,切片阶段零共享文件接触。
|
|
25
|
+
*
|
|
26
|
+
* 用法:node wire-routes.mjs (--manifest <xml> | --src <android根>) --out <ets根>
|
|
27
|
+
* [--from-ledger <ledger.json>] [--launcher <路由名>] [--json report]
|
|
28
|
+
* 首页判不了(报告 launcherResolved=false)时用 --launcher 显式校正重跑,脚本不硬猜。
|
|
29
|
+
*/
|
|
30
|
+
import { readFileSync, writeFileSync, mkdirSync, existsSync, readdirSync, statSync } from 'node:fs';
|
|
31
|
+
import { join, dirname, resolve, relative, sep } from 'node:path';
|
|
32
|
+
import { extractNavGraph, extractComposeProject, findComposeHostFiles, normalizeRoute, extractAppInit } from './extractors.mjs';
|
|
33
|
+
|
|
34
|
+
const argv = process.argv.slice(2);
|
|
35
|
+
const argOf = (n, d) => { const i = argv.indexOf(n); return i >= 0 && argv[i + 1] ? argv[i + 1] : d; };
|
|
36
|
+
let MANIFEST = argOf('--manifest', null);
|
|
37
|
+
const SRC = argOf('--src', null);
|
|
38
|
+
const OUT = argOf('--out', null);
|
|
39
|
+
const JSON_OUT = argOf('--json', null);
|
|
40
|
+
const APP_CLASS = argOf('--app-class', null); // 多产品仓有多个 Application 候选时按所选产品显式指定(与 --launcher 同一套"脚本如实报、参数校正")
|
|
41
|
+
let manifestCandidates = []; // 全部候选 manifest(多模块):应用级初始化提取要查未被选中的模块(Application 类常不在承载 LAUNCHER 的模块里)
|
|
42
|
+
if (!MANIFEST && SRC) {
|
|
43
|
+
const stack = [resolve(SRC)];
|
|
44
|
+
// 收齐候选再选,不取第一个命中——多模块工程(wear/tv/lib 各有 manifest)按遍历序抓会错定
|
|
45
|
+
// launcher 模块(实证:误抓 wearOS manifest,launcher 定成手表端 Activity)。
|
|
46
|
+
const candidates = [];
|
|
47
|
+
while (stack.length) {
|
|
48
|
+
const d = stack.pop();
|
|
49
|
+
let entries; try { entries = readdirSync(d); } catch { continue; }
|
|
50
|
+
for (const e of entries) {
|
|
51
|
+
if (['build', '.git', 'node_modules', 'test', 'androidTest'].includes(e)) continue;
|
|
52
|
+
const p = join(d, e);
|
|
53
|
+
let st; try { st = statSync(p); } catch { continue; } // 坏符号链接不崩
|
|
54
|
+
if (st.isDirectory()) stack.push(p);
|
|
55
|
+
// 不要求 src/main/ 路径:Gradle 允许 manifest.srcFile 指到模块根(老布局工程实证),候选交给下面按内容排序
|
|
56
|
+
else if (e === 'AndroidManifest.xml' && !/[\\/](androidTest|test|debug|release)[\\/]/.test(p)) candidates.push(p);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
if (candidates.length) {
|
|
60
|
+
// 内容语义选主模块 manifest(不靠目录名约定——app/ 可叫任何名):MAIN+LAUNCHER 且非
|
|
61
|
+
// 手表/TV 形态优先(wear 模块也声明 LAUNCHER,须按 uses-feature/类别再判);多个同级取
|
|
62
|
+
// activity 数最多,再同取路径最短(确定性)。选择理由打印,操作者可核。
|
|
63
|
+
const meta = candidates.map(p => {
|
|
64
|
+
let x = ''; try { x = readFileSync(p, 'utf8').replace(/<!--[\s\S]*?-->/g, ''); } catch { /* 读失败按空 */ }
|
|
65
|
+
const hasLauncher = /android\.intent\.category\.LAUNCHER/.test(x) && /android\.intent\.action\.MAIN/.test(x);
|
|
66
|
+
return {
|
|
67
|
+
p,
|
|
68
|
+
launcher: hasLauncher,
|
|
69
|
+
watch: /android\.hardware\.type\.watch/.test(x),
|
|
70
|
+
// TV 形态 = 只有 leanback 入口(同一入口同时声明 LAUNCHER 与 LEANBACK_LAUNCHER 的是手机+TV 通用端,不算 TV)
|
|
71
|
+
tv: /android\.software\.leanback[^>]*required\s*=\s*["']true["']/.test(x) || (/LEANBACK_LAUNCHER/.test(x) && !hasLauncher),
|
|
72
|
+
acts: (x.match(/<activity[\s>]/g) || []).length,
|
|
73
|
+
};
|
|
74
|
+
});
|
|
75
|
+
const rank = (m) => (m.launcher && !m.watch && !m.tv ? 2 : m.launcher ? 1 : 0);
|
|
76
|
+
meta.sort((a, b) => rank(b) - rank(a) || b.acts - a.acts || a.p.length - b.p.length);
|
|
77
|
+
MANIFEST = meta[0].p;
|
|
78
|
+
manifestCandidates = meta.map(m => ({ path: m.p, watch: m.watch, tv: m.tv }));
|
|
79
|
+
if (candidates.length > 1) console.log(`[wire-routes] 多模块 manifest ×${candidates.length},选用 ${MANIFEST}(launcher:${meta[0].launcher ? '有' : '无'}·watch:${meta[0].watch ? '是' : '否'}·activities:${meta[0].acts});未选:${meta.slice(1).map(m => m.p).join(' , ')}`);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
if (!MANIFEST || !OUT) { console.error('usage: wire-routes.mjs (--manifest m.xml | --src root) --out <ets根> [--json report]'); process.exit(2); }
|
|
83
|
+
|
|
84
|
+
const xml = readFileSync(MANIFEST, 'utf8').replace(/<!--[\s\S]*?-->/g, '');
|
|
85
|
+
const routes = [];
|
|
86
|
+
// 整元素解析:开标签属性段只在引号外找 '>'(子标签的 `/>` 不再截断元素——旧写法使
|
|
87
|
+
// LAUNCHER 检测对标准 manifest 恒为假、静默回退 routes[0]);(?![-\w]) 排除 <activity-alias>;
|
|
88
|
+
// 属性支持单/双引号。
|
|
89
|
+
for (const m of xml.matchAll(/<activity(?![-\w])((?:"[^"]*"|'[^']*'|[^>"'])*)(\/>|>)/g)) {
|
|
90
|
+
const attrs = m[1];
|
|
91
|
+
const name = (attrs.match(/android:name\s*=\s*["']([^"']+)["']/) || [])[1];
|
|
92
|
+
if (!name) continue;
|
|
93
|
+
// 自闭合判定:属性段字符类会把结尾 '/' 吞进 attrs(m[2] 永远是 '>'),须回看 attrs 尾部
|
|
94
|
+
const selfClosing = m[2] === '/>' || /\/\s*$/.test(attrs);
|
|
95
|
+
let inner = '';
|
|
96
|
+
if (!selfClosing) {
|
|
97
|
+
const close = xml.indexOf('</activity>', m.index + m[0].length);
|
|
98
|
+
inner = close >= 0 ? xml.slice(m.index + m[0].length, close) : '';
|
|
99
|
+
}
|
|
100
|
+
const cls = name.split('.').pop().replace(/\$/g, '_'); // 内部类 $ 对文件名/工具链不安全
|
|
101
|
+
const route = cls.replace(/Activity$/, '') || cls;
|
|
102
|
+
const launcher = /android\.intent\.action\.MAIN\b/.test(inner) && /android\.intent\.category\.LAUNCHER\b/.test(inner);
|
|
103
|
+
const enabled = !/android:enabled\s*=\s*["']false["']/.test(attrs);
|
|
104
|
+
routes.push({ route, cls, fqcn: name, launcher, enabled });
|
|
105
|
+
}
|
|
106
|
+
// <activity-alias> 承载 MAIN+LAUNCHER(换启动图标/多入口的标准写法):首页 = 别名的 targetActivity。
|
|
107
|
+
// 只在没有 Activity 直接声明 LAUNCHER 时生效(实证:有应用把全部 LAUNCHER 都放在别名上,manifest 轨无首页,
|
|
108
|
+
// 被 Compose 子图的 startDestination 顶了位)。
|
|
109
|
+
if (!routes.some(r => r.launcher)) {
|
|
110
|
+
for (const m of xml.matchAll(/<activity-alias((?:"[^"]*"|'[^']*'|[^>"'])*)(\/>|>)/g)) {
|
|
111
|
+
const attrs = m[1];
|
|
112
|
+
if (m[2] === '/>' || /\/\s*$/.test(attrs)) continue;
|
|
113
|
+
const close = xml.indexOf('</activity-alias>', m.index + m[0].length);
|
|
114
|
+
const inner = close >= 0 ? xml.slice(m.index + m[0].length, close) : '';
|
|
115
|
+
if (!(/android\.intent\.action\.MAIN\b/.test(inner) && /android\.intent\.category\.LAUNCHER\b/.test(inner))) continue;
|
|
116
|
+
const target = (attrs.match(/android:targetActivity\s*=\s*["']([^"']+)["']/) || [])[1];
|
|
117
|
+
if (!target) continue;
|
|
118
|
+
const simple = target.split('.').pop().replace(/\$/g, '_');
|
|
119
|
+
const hit = routes.find(r => r.fqcn === target || r.cls === simple);
|
|
120
|
+
if (hit) { hit.launcher = true; console.log(`[wire-routes] LAUNCHER 声明在 activity-alias 上,首页取其 targetActivity:${hit.route}`); break; }
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
// ---- 三轨合并:navGraph + Compose(--src 时)----
|
|
124
|
+
const sourcesStat = { manifest: routes.length, navgraph: 0, compose: 0, ledger: 0 };
|
|
125
|
+
let appInit = null; // 应用级初始化真源(--src 时提取;见 extractors.extractAppInit)
|
|
126
|
+
if (SRC) {
|
|
127
|
+
const navXmls = [], kts = [], javas = [];
|
|
128
|
+
const st2 = [resolve(SRC)];
|
|
129
|
+
while (st2.length) {
|
|
130
|
+
const d = st2.pop();
|
|
131
|
+
let es; try { es = readdirSync(d, { withFileTypes: true }); } catch { continue; }
|
|
132
|
+
for (const e of es) {
|
|
133
|
+
if (['build', '.git', 'node_modules', 'test', 'androidTest'].includes(e.name) || e.name.startsWith('.')) continue;
|
|
134
|
+
const p = join(d, e.name);
|
|
135
|
+
if (e.isDirectory()) { st2.push(p); continue; }
|
|
136
|
+
if (/[\\/]res[\\/]navigation[\\/][^\\/]+\.xml$/.test(p)) navXmls.push(p);
|
|
137
|
+
else if (p.endsWith('.kt')) kts.push(p);
|
|
138
|
+
else if (p.endsWith('.java')) javas.push(p);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
const have = new Set(routes.map(r => r.route));
|
|
142
|
+
const add = (r, src) => {
|
|
143
|
+
if (have.has(r.route)) return;
|
|
144
|
+
have.add(r.route);
|
|
145
|
+
routes.push({ route: r.route, cls: r.route, fqcn: r.fqcn, launcher: r.launcher, enabled: true, source: src });
|
|
146
|
+
sourcesStat[src]++;
|
|
147
|
+
};
|
|
148
|
+
for (const nx of navXmls) for (const r of extractNavGraph(readFileSync(nx, 'utf8'))) add(r, 'navgraph');
|
|
149
|
+
const ktFiles = kts.map(p => { try { return { path: p, text: readFileSync(p, 'utf8') }; } catch { return { path: p, text: '' }; } });
|
|
150
|
+
// 应用级初始化真源(Application.onCreate / ContentProvider / androidx.startup)→ 报告 appInit:2b 装账据此生成 INIT 卡,
|
|
151
|
+
// 由一个地基单元认领接进 EntryAbility.onCreate。提取失败不影响路由(如实报)。
|
|
152
|
+
try {
|
|
153
|
+
const javaFiles = javas.map(p => { try { return { path: p, text: readFileSync(p, 'utf8') }; } catch { return { path: p, text: '' }; } });
|
|
154
|
+
const relCwd = (p) => relative(process.cwd(), resolve(p)).replace(/\\/g, '/');
|
|
155
|
+
const extras = manifestCandidates.filter(c => resolve(c.path) !== resolve(MANIFEST) && !c.watch && !c.tv)
|
|
156
|
+
.map(c => { try { return { path: relCwd(c.path), text: readFileSync(c.path, 'utf8').replace(/<!--[\s\S]*?-->/g, '') }; } catch { return null; } });
|
|
157
|
+
const ai = extractAppInit(xml, [...ktFiles, ...javaFiles], { manifest: relCwd(MANIFEST), extras, preferClass: APP_CLASS });
|
|
158
|
+
const relSrc = (p) => p ? relative(resolve(SRC), p).replace(/\\/g, '/') : null;
|
|
159
|
+
if (ai) appInit = { ...ai, file: relSrc(ai.file), onCreateSites: ai.onCreateSites.map(c => ({ ...c, file: relSrc(c.file) })) };
|
|
160
|
+
} catch (e) { console.log(`[wire-routes] ▲ 应用级初始化提取失败(不影响路由):${e.message}`); }
|
|
161
|
+
// 顶层 NavHost 宿主(两跳):launcher activity 文件 → 它调用的 @Composable → 定义文件;决定 ①Compose 起点取自哪个 NavHost
|
|
162
|
+
// ②单 Activity 是否真是壳。多模块工程各特性都有自己的 NavHost,不定位宿主就会把某个子流程的起点当首页(实测)。
|
|
163
|
+
let hostFiles = [];
|
|
164
|
+
{
|
|
165
|
+
const ml = routes.find(r => r.launcher);
|
|
166
|
+
if (ml) {
|
|
167
|
+
const simple = ml.fqcn.split('.').pop().replace(/\$/g, '_');
|
|
168
|
+
const moduleRoot = resolve(dirname(MANIFEST), '..', '..');
|
|
169
|
+
const cands = kts.filter(p => p.endsWith(`${sep}${simple}.kt`));
|
|
170
|
+
const actFile = cands.find(p => p.startsWith(moduleRoot)) || cands[0];
|
|
171
|
+
if (actFile) hostFiles = findComposeHostFiles(ktFiles, actFile);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
if (ktFiles.some(f => /\bNavHost\s*\(|\bNavDisplay\b|EntryProviderScope|entryProvider/.test(f.text))) {
|
|
175
|
+
for (const r of extractComposeProject(ktFiles, { hostFiles })) add(r, 'compose');
|
|
176
|
+
}
|
|
177
|
+
// 壳让位两则:
|
|
178
|
+
// ① 单 Activity 壳:整壳不成页(→ Index 容器),真首页取②/③的 launcher——须有宿主证据(activity 两跳内有 NavHost);
|
|
179
|
+
// 单 Activity 但无宿主 = 经典多屏 Activity 内嵌特性级 NavHost,首页保留为该 activity,Compose 路由作页内子路由。
|
|
180
|
+
const extra = routes.filter(r => r.source);
|
|
181
|
+
if (sourcesStat.manifest === 1 && extra.length >= 2 && (hostFiles.length || !extra.some(r => r.source === 'compose'))) {
|
|
182
|
+
const shell = routes.find(r => !r.source);
|
|
183
|
+
routes.splice(routes.indexOf(shell), 1);
|
|
184
|
+
console.log(`[wire-routes] 单 Activity 壳 ${shell.route} 让位(→ Index 容器);路由真源=${extra.some(r => r.source === 'compose') ? 'Compose NavHost' : 'navGraph'} ×${extra.length}`);
|
|
185
|
+
if (extra.some(r => r.launcher)) for (const r of routes) if (!r.source) r.launcher = false;
|
|
186
|
+
} else if (sourcesStat.manifest === 1 && extra.length >= 2) {
|
|
187
|
+
for (const r of extra) r.launcher = false;
|
|
188
|
+
console.log(`[wire-routes] 单 Activity 但非 Compose 壳(activity 两跳内无 NavHost/NavDisplay):首页保留 ${routes.find(r => !r.source)?.route},${extra.length} 条 Compose 路由作页内子路由`);
|
|
189
|
+
}
|
|
190
|
+
// ② 混合形态 launcher 让位(需壳证据,不靠假设):导航图有 startDestination 时,仅当
|
|
191
|
+
// manifest launcher activity **确证为装 NavHost 的壳**(其类源码或 setContentView 布局含
|
|
192
|
+
// NavHost 标记)才让位;无证据(闪屏/登录等真实首屏,自身实现转发链)→ 保留不动。
|
|
193
|
+
else if (extra.some(r => r.launcher) && SRC) {
|
|
194
|
+
const shellLauncher = routes.find(r => !r.source && r.launcher);
|
|
195
|
+
if (shellLauncher) {
|
|
196
|
+
const simple = shellLauncher.fqcn.split('.').pop();
|
|
197
|
+
let evidence = hostFiles.length > 0; // Compose 宿主两跳定位已证 launcher activity 是壳
|
|
198
|
+
const HOST_RE = /NavHostFragment|setupWithNavController|FragmentContainerView[\s\S]{0,400}?navGraph|\bNavHost\s*\(/;
|
|
199
|
+
const stk = [resolve(SRC)];
|
|
200
|
+
let clsText = '';
|
|
201
|
+
while (stk.length && !evidence) {
|
|
202
|
+
const d = stk.pop();
|
|
203
|
+
let es; try { es = readdirSync(d, { withFileTypes: true }); } catch { continue; }
|
|
204
|
+
for (const e of es) {
|
|
205
|
+
if (['build', '.git', 'node_modules', 'test', 'androidTest'].includes(e.name) || e.name.startsWith('.')) continue;
|
|
206
|
+
const p = join(d, e.name);
|
|
207
|
+
if (e.isDirectory()) { stk.push(p); continue; }
|
|
208
|
+
if (e.name === `${simple}.kt` || e.name === `${simple}.java`) {
|
|
209
|
+
try { clsText = readFileSync(p, 'utf8'); } catch { }
|
|
210
|
+
if (HOST_RE.test(clsText)) evidence = true;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
if (!evidence && clsText) {
|
|
215
|
+
// 二跳:setContentView(R.layout.X) → res/layout/X.xml 含 NavHost 标记
|
|
216
|
+
const lay = (clsText.match(/setContentView\s*\(\s*R\.layout\.(\w+)/) || [])[1];
|
|
217
|
+
if (lay) {
|
|
218
|
+
const stk2 = [resolve(SRC)];
|
|
219
|
+
while (stk2.length && !evidence) {
|
|
220
|
+
const d = stk2.pop();
|
|
221
|
+
let es; try { es = readdirSync(d, { withFileTypes: true }); } catch { continue; }
|
|
222
|
+
for (const e of es) {
|
|
223
|
+
if (['build', '.git', 'node_modules'].includes(e.name) || e.name.startsWith('.')) continue;
|
|
224
|
+
const p = join(d, e.name);
|
|
225
|
+
if (e.isDirectory()) { stk2.push(p); continue; }
|
|
226
|
+
if (e.name === `${lay}.xml` && p.includes(`${sep}layout`)) {
|
|
227
|
+
try { if (/NavHostFragment|app:navGraph|FragmentContainerView/.test(readFileSync(p, 'utf8'))) evidence = true; } catch { }
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
if (evidence) {
|
|
234
|
+
shellLauncher.launcher = false;
|
|
235
|
+
console.log(`[wire-routes] 混合形态:manifest launcher ${shellLauncher.route} 确证为 NavHost 壳,首页让位给导航图 startDestination(${extra.find(r => r.launcher)?.route})`);
|
|
236
|
+
} else {
|
|
237
|
+
for (const r of extra) r.launcher = false;
|
|
238
|
+
console.log(`[wire-routes] 混合形态:manifest launcher ${shellLauncher.route} 无 NavHost 壳证据(闪屏/登录类真实首屏)——保留为首页,导航图 startDestination 由其转发链到达`);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
// ---- 台账补漏(2b 装账后二跑):页面账里有而三轨没提到的,兜底成页 ----
|
|
244
|
+
const FROM_LEDGER = argOf('--from-ledger', null);
|
|
245
|
+
if (FROM_LEDGER && existsSync(FROM_LEDGER)) {
|
|
246
|
+
const l = JSON.parse(readFileSync(FROM_LEDGER, 'utf8'));
|
|
247
|
+
const have = new Set(routes.map(r => r.route));
|
|
248
|
+
for (const p of (l.pages || [])) {
|
|
249
|
+
const route = normalizeRoute(p.id || p.name || '');
|
|
250
|
+
if (!route || have.has(route)) continue;
|
|
251
|
+
have.add(route);
|
|
252
|
+
routes.push({ route, cls: route, fqcn: `ledger:${p.id}`, launcher: false, enabled: true, source: 'ledger' });
|
|
253
|
+
sourcesStat.ledger++;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
// 跨包同 simple name 去重:枚举成员/文件名会冲突 → 后者追加序号并记入报告
|
|
257
|
+
const seenRoute = new Map();
|
|
258
|
+
for (const r of routes) {
|
|
259
|
+
const n = (seenRoute.get(r.route) || 0) + 1;
|
|
260
|
+
seenRoute.set(r.route, n);
|
|
261
|
+
if (n > 1) { r.dedupedFrom = r.route; r.route = `${r.route}${n}`; r.cls = `${r.cls}${n}`; }
|
|
262
|
+
}
|
|
263
|
+
if (!routes.length) { console.error('manifest 未解析出任何 activity'); process.exit(2); }
|
|
264
|
+
// 首页判定:三轨真源 > --launcher 显式指定 > 未判定(取首条并大声报,报告 launcherResolved=false)。
|
|
265
|
+
// 不为个别形态硬猜(如 Navigation 3 经自定义包装 rememberNavigationState(startKey=…) 起栈,静态解析不到初始键)——
|
|
266
|
+
// 判不了就如实报,由流程用 --launcher <路由名> 重跑校正,别让首页错在静默里。
|
|
267
|
+
const LAUNCHER_ARG = argOf('--launcher', null);
|
|
268
|
+
if (LAUNCHER_ARG) { for (const r of routes) r.launcher = r.route === LAUNCHER_ARG; if (!routes.some(r => r.launcher)) { console.error(`--launcher ${LAUNCHER_ARG} 不在路由集:${routes.map(r => r.route).join(', ')}`); process.exit(2); } }
|
|
269
|
+
const launcherResolved = routes.some(r => r.launcher);
|
|
270
|
+
const launcher = routes.find(r => r.launcher) || routes[0];
|
|
271
|
+
if (!launcherResolved) console.log(`[wire-routes] ▲ 首页未能从路由真源判定(无 LAUNCHER/startDestination/rememberNavBackStack 可解),暂取 ${launcher.route};确认后 --launcher <路由名> 重跑校正`);
|
|
272
|
+
const CONST = r => r.route.replace(/([a-z0-9])([A-Z])/g, '$1_$2').toUpperCase();
|
|
273
|
+
|
|
274
|
+
const files = [];
|
|
275
|
+
function emit(relPath, content) {
|
|
276
|
+
const p = join(OUT, relPath);
|
|
277
|
+
// Index 壳特例:脚手架(create-project)必然先建 Hello World 样板 Index——"绝不覆盖"在此失效
|
|
278
|
+
// 会让 Navigation 壳永远生成不出来(真机只见样板页实证)。判定:现存 Index 无 Navigation(
|
|
279
|
+
// = 脚手架样板/非壳 → 备份后覆盖;已是壳(含 Navigation)则维持不覆盖。
|
|
280
|
+
if (relPath === 'pages/Index.ets' && existsSync(p)) {
|
|
281
|
+
const cur = readFileSync(p, 'utf8');
|
|
282
|
+
if (!/Navigation\s*\(/.test(cur)) {
|
|
283
|
+
writeFileSync(p + '.scaffold.bak', cur);
|
|
284
|
+
writeFileSync(p, content);
|
|
285
|
+
files.push({ file: relPath, action: 'overwrite(脚手架样板→Navigation 壳,原件 .scaffold.bak)' });
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
if (existsSync(p)) { files.push({ file: relPath, action: 'skip(已存在,绝不覆盖)' }); return; }
|
|
290
|
+
mkdirSync(dirname(p), { recursive: true });
|
|
291
|
+
writeFileSync(p, content);
|
|
292
|
+
files.push({ file: relPath, action: 'generated' });
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// ---- Router.ets ----
|
|
296
|
+
emit('common/Router.ets', `/**
|
|
297
|
+
* Router.ets —— 由 wire-routes 生成(源:${MANIFEST.split(sep).slice(-3).join('/')})。
|
|
298
|
+
* 路由注册与跳转唯一通道:页面只准 import AppRouter,禁散落 pushPathByName。
|
|
299
|
+
* 本文件归静态前场独占;切片阶段只填页面行为,不碰壳。
|
|
300
|
+
*/
|
|
301
|
+
export class NoParam {
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
export const enum Routes {
|
|
305
|
+
${routes.map(r => ` ${CONST(r)} = '${r.route}',`).join('\n')}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
export class AppRouter {
|
|
309
|
+
static readonly stack: NavPathStack = new NavPathStack();
|
|
310
|
+
|
|
311
|
+
static push(route: Routes, params?: Object, onPop?: (pop: PopInfo) => void): void {
|
|
312
|
+
const param: Object = params !== undefined ? params : new NoParam();
|
|
313
|
+
if (onPop !== undefined) {
|
|
314
|
+
AppRouter.stack.pushPathByName(route, param, onPop);
|
|
315
|
+
} else {
|
|
316
|
+
AppRouter.stack.pushPathByName(route, param);
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/** 按运行期路由名跳转(冒烟直达钩子/深链用;业务代码一律走 push(Routes.X)) */
|
|
321
|
+
static pushName(name: string, params?: Object): void {
|
|
322
|
+
AppRouter.stack.pushPathByName(name, params !== undefined ? params : new NoParam());
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
static back(): void {
|
|
326
|
+
AppRouter.stack.pop();
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
static backWithResult(result: Object): void {
|
|
330
|
+
AppRouter.stack.pop(result);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
static replace(route: Routes, params?: Object): void {
|
|
334
|
+
AppRouter.stack.replacePathByName(route, params !== undefined ? params : new NoParam());
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
static goHome(): void {
|
|
338
|
+
const names: Array<string> = AppRouter.stack.getAllPathName();
|
|
339
|
+
if (names.indexOf(Routes.${CONST(launcher)}) >= 0) {
|
|
340
|
+
AppRouter.stack.popToName(Routes.${CONST(launcher)});
|
|
341
|
+
return;
|
|
342
|
+
}
|
|
343
|
+
AppRouter.stack.clear();
|
|
344
|
+
AppRouter.stack.pushPathByName(Routes.${CONST(launcher)}, new NoParam());
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
`);
|
|
348
|
+
|
|
349
|
+
// ---- 每页骨架 ----
|
|
350
|
+
for (const r of routes) {
|
|
351
|
+
emit(`pages/${r.route}Page.ets`, `/**
|
|
352
|
+
* ${r.route}Page —— 骨架由 wire-routes 生成。源 activity:${r.fqcn}
|
|
353
|
+
* TODO(切片):迁移该页行为与布局(布局骨架由 transpile-layout 产出后并入此处)。
|
|
354
|
+
*/
|
|
355
|
+
import { AppRouter } from '../common/Router';
|
|
356
|
+
|
|
357
|
+
@Builder
|
|
358
|
+
export function ${r.route}PageBuilder() {
|
|
359
|
+
${r.route}Page()
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
@ComponentV2
|
|
363
|
+
struct ${r.route}Page {
|
|
364
|
+
build() {
|
|
365
|
+
NavDestination() {
|
|
366
|
+
Column() {
|
|
367
|
+
Text('${r.route}')
|
|
368
|
+
// TODO(切片): 页面内容。返回语义:AppRouter.back() / backWithResult(...)
|
|
369
|
+
}
|
|
370
|
+
.width('100%')
|
|
371
|
+
.height('100%')
|
|
372
|
+
}
|
|
373
|
+
.hideTitleBar(true)
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
`);
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
// ---- Index 壳(单一注册:路由真源只有 route_map.json;壳只管栈与冒烟钩子)----
|
|
380
|
+
// 不再生成 pageMap 比较分支:双注册=两份真源要模型同步维护(实测模型重写 Index 后 pageMap 丢失/只剩首页),
|
|
381
|
+
// 且 route_map-only 页在 navDestination 存在时是否可达官方未写明。单一注册下 hvigor 的
|
|
382
|
+
// ProcessRouterMap(pageSourceFile/buildFunction 缺失即构建失败)成为免费的注册门。
|
|
383
|
+
emit('pages/Index.ets', `/**
|
|
384
|
+
* Index —— Navigation 壳,由 wire-routes 生成。路由注册唯一真源 = resources/base/profile/route_map.json
|
|
385
|
+
* (systemRouterMap,${routes.length} 条),冷启自动入栈 launcher(${launcher.route})。
|
|
386
|
+
* 本文件归静态前场独占(冻结件):smokeRoute 直达钩子 + hmigSmoke 着陆日志供波级冒烟核证,勿删勿改。
|
|
387
|
+
*/
|
|
388
|
+
import { hilog } from '@kit.PerformanceAnalysisKit';
|
|
389
|
+
import { AppRouter, Routes } from '../common/Router';
|
|
390
|
+
|
|
391
|
+
@Entry
|
|
392
|
+
@ComponentV2
|
|
393
|
+
struct Index {
|
|
394
|
+
navStack: NavPathStack = AppRouter.stack;
|
|
395
|
+
|
|
396
|
+
aboutToAppear(): void {
|
|
397
|
+
// hmig:smokeLanding 着陆证据:每次页面显示打一条 hilog(smoke.mjs 经 hilog -x -T hmigSmoke 核对着陆页名)
|
|
398
|
+
AppRouter.stack.setInterception({
|
|
399
|
+
didShow: (from: NavDestinationContext | NavBar, to: NavDestinationContext | NavBar,
|
|
400
|
+
operation: NavigationOperation, isAnimated: boolean): void => {
|
|
401
|
+
let shown: string = 'navBar';
|
|
402
|
+
if (typeof to !== 'string') {
|
|
403
|
+
shown = (to as NavDestinationContext).pathInfo.name;
|
|
404
|
+
}
|
|
405
|
+
hilog.info(0x0000, 'hmigSmoke', 'didShow %{public}s', shown);
|
|
406
|
+
}
|
|
407
|
+
});
|
|
408
|
+
if (AppRouter.stack.size() === 0) {
|
|
409
|
+
// smokeRoute:波级冒烟直达钩子(aa start --ps smokeRoute <名>,EntryAbility 透传;正常启动为空走首页链)
|
|
410
|
+
// 两个独立 push(勿并三元):首页跳转保持静态常量形态,check-routes 首屏链路门才可解析
|
|
411
|
+
const smokeRoute = AppStorage.get<string>('smokeRoute') ?? '';
|
|
412
|
+
if (smokeRoute.length) {
|
|
413
|
+
AppRouter.pushName(smokeRoute);
|
|
414
|
+
} else {
|
|
415
|
+
AppRouter.push(Routes.${CONST(launcher)}, undefined);
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
build() {
|
|
421
|
+
Navigation(this.navStack) {
|
|
422
|
+
}
|
|
423
|
+
.mode(NavigationMode.Stack)
|
|
424
|
+
.hideTitleBar(true)
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
`);
|
|
428
|
+
emit('main_pages.json.generated', JSON.stringify({ src: ['pages/Index'] }, null, 2) + '\n');
|
|
429
|
+
|
|
430
|
+
// ---- EntryAbility 直达透传(幂等补丁):不管工程怎么建(create-project 华为模板 / 手工 scaffold),
|
|
431
|
+
// 都由本脚本自己打补丁——实测 create-project 工程无透传,Index 钩子读空静默落首页,波级冒烟 100% 假绿。
|
|
432
|
+
{
|
|
433
|
+
const eaPath = join(OUT, 'entryability', 'EntryAbility.ets');
|
|
434
|
+
if (!existsSync(eaPath)) {
|
|
435
|
+
files.push({ file: 'entryability/EntryAbility.ets', action: 'skip(不存在——建工程后重跑 wire-routes 打透传补丁)' });
|
|
436
|
+
} else {
|
|
437
|
+
const t = readFileSync(eaPath, 'utf8');
|
|
438
|
+
if (/smokeRoute/.test(t)) {
|
|
439
|
+
files.push({ file: 'entryability/EntryAbility.ets', action: 'skip(透传已在)' });
|
|
440
|
+
} else {
|
|
441
|
+
const m = t.match(/onCreate\s*\(\s*([A-Za-z_$][\w$]*)\s*:\s*Want\b[^)]*\)\s*(?::\s*void)?\s*\{/);
|
|
442
|
+
if (!m) {
|
|
443
|
+
files.push({ file: 'entryability/EntryAbility.ets', action: 'skip(未找到 onCreate(want: Want…)——手动加透传,写法见 Index 钩子注释)' });
|
|
444
|
+
} else {
|
|
445
|
+
const want = m[1];
|
|
446
|
+
const patch = `\n // hmig:smokeRoute 波级冒烟直达透传(smoke.mjs 经 aa start --ps smokeRoute 传入;正常启动无此参数)——冻结件,勿删\n` +
|
|
447
|
+
` const hmigSmokeRoute: Object | undefined = ${want}.parameters?.['smokeRoute'];\n` +
|
|
448
|
+
` if (typeof hmigSmokeRoute === 'string' && hmigSmokeRoute.length > 0) {\n` +
|
|
449
|
+
` AppStorage.setOrCreate('smokeRoute', hmigSmokeRoute);\n` +
|
|
450
|
+
` }`;
|
|
451
|
+
const at = m.index + m[0].length;
|
|
452
|
+
writeFileSync(eaPath, t.slice(0, at) + patch + t.slice(at));
|
|
453
|
+
files.push({ file: 'entryability/EntryAbility.ets', action: 'patch(onCreate 插入 smokeRoute 透传)' });
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
// ---- systemRouterMap(官方配置化注册,唯一真源):route_map.json 按 name 合并更新 ----
|
|
460
|
+
// --from-ledger 二跑新增页也只经此注册(无需动 Router/Index 共享文件——切片阶段零共享接触的关键)。
|
|
461
|
+
{
|
|
462
|
+
const rmPath = join(OUT, '..', 'resources', 'base', 'profile', 'route_map.json');
|
|
463
|
+
let rm = { routerMap: [] };
|
|
464
|
+
if (existsSync(rmPath)) { try { rm = JSON.parse(readFileSync(rmPath, 'utf8')); } catch { /* 坏文件重建 */ } }
|
|
465
|
+
const haveRm = new Set((rm.routerMap || []).map(e => e.name));
|
|
466
|
+
let added = 0;
|
|
467
|
+
for (const r of routes) {
|
|
468
|
+
if (haveRm.has(r.route)) continue;
|
|
469
|
+
rm.routerMap.push({ name: r.route, pageSourceFile: `src/main/ets/pages/${r.route}Page.ets`, buildFunction: `${r.route}PageBuilder` });
|
|
470
|
+
added++;
|
|
471
|
+
}
|
|
472
|
+
mkdirSync(dirname(rmPath), { recursive: true });
|
|
473
|
+
writeFileSync(rmPath, JSON.stringify(rm, null, 2) + '\n');
|
|
474
|
+
files.push({ file: '../resources/base/profile/route_map.json', action: added ? `merge(+${added})` : 'merge(无新增)' });
|
|
475
|
+
// module.json5 幂等插入 routerMap 引用
|
|
476
|
+
const modPath = join(OUT, '..', 'module.json5');
|
|
477
|
+
if (existsSync(modPath)) {
|
|
478
|
+
const t = readFileSync(modPath, 'utf8');
|
|
479
|
+
if (!/"routerMap"/.test(t)) {
|
|
480
|
+
const t2 = t.replace(/("module"\s*:\s*\{)/, `$1\n "routerMap": "$profile:route_map",`);
|
|
481
|
+
if (t2 !== t) { writeFileSync(modPath, t2); files.push({ file: '../module.json5', action: 'routerMap 引用已插入' }); }
|
|
482
|
+
}
|
|
483
|
+
} else files.push({ file: '../module.json5', action: 'skip(不存在——建工程后重跑或手动加 "routerMap": "$profile:route_map")' });
|
|
484
|
+
}
|
|
485
|
+
// ---- Router.ets 增量合并:二跑新增路由把缺失的 Routes 常量插入既有枚举(不覆盖既有内容)----
|
|
486
|
+
{
|
|
487
|
+
const routerPath = join(OUT, 'common', 'Router.ets');
|
|
488
|
+
if (existsSync(routerPath)) {
|
|
489
|
+
let t = readFileSync(routerPath, 'utf8');
|
|
490
|
+
const missing = routes.filter(r => !new RegExp(`\\b${CONST(r)}\\s*=`).test(t));
|
|
491
|
+
if (missing.length && /export const enum Routes \{/.test(t)) {
|
|
492
|
+
t = t.replace(/(export const enum Routes \{[\s\S]*?)(\n\})/, (_m, head, tail) =>
|
|
493
|
+
head + '\n' + missing.map(r => ` ${CONST(r)} = '${r.route}',`).join('\n') + tail);
|
|
494
|
+
writeFileSync(routerPath, t);
|
|
495
|
+
files.push({ file: 'common/Router.ets', action: `merge(+${missing.length} Routes 常量)` });
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
const report = { manifest: relative(process.cwd(), resolve(MANIFEST)), out: relative(process.cwd(), resolve(OUT)) || '.', launcher: launcher.route, launcherResolved, sources: sourcesStat, routes: routes.map(r => ({ route: r.route, from: r.fqcn, source: r.source || 'manifest', launcher: !!r.launcher, enabled: r.enabled !== false, ...(r.dedupedFrom ? { dedupedFrom: r.dedupedFrom, note: '跨包同 simple name,已追加序号防枚举/文件名冲突' } : {}) })), files, appInitScanned: !!SRC, appInit };
|
|
501
|
+
if (JSON_OUT) { mkdirSync(dirname(resolve(JSON_OUT)), { recursive: true }); writeFileSync(JSON_OUT, JSON.stringify(report, null, 2)); }
|
|
502
|
+
console.log(`[wire-routes] ${routes.length} 条路由(launcher=${launcher.route})→ ${OUT}`);
|
|
503
|
+
if (SRC) {
|
|
504
|
+
if (appInit) console.log(`[wire-routes] 应用级初始化:${appInit.applicationClass || '(无 Application 类,仅 provider/startup)'}${appInit.applicationClass && !appInit.classFound ? '(类文件未找到,按 manifest 如实报)' : ''} · onCreate 位点 ${appInit.onCreateSites.length}(${appInit.onCreateSites.map(s => `${s.cls}:${s.bodyLines} 行`).join('、') || '无'}) · provider ${appInit.providers.length} · startup 初始化器 ${appInit.startupInitializers.length}${appInit.di ? ` · DI=${appInit.di}` : ''}${appInit.ambiguous ? ` · ▲ 多产品仓:${appInit.applicationCandidates.length} 个 Application 候选(${appInit.applicationCandidates.map(c => c.applicationClass.split('.').pop()).join('/')}),取首个;按所选产品用 --app-class <类名> 重跑校正` : ''}——2b 装账 parse-cards --wiring 生成 INIT 卡,由地基单元读锚定源码逐项接进 EntryAbility.onCreate`);
|
|
505
|
+
else console.log('[wire-routes] 无自定义 Application / provider / startup 初始化器(无需 INIT 单元)');
|
|
506
|
+
}
|
|
507
|
+
for (const f of files) console.log(` ${f.action === 'generated' ? '+' : '⊘'} ${f.file}${f.action.startsWith('skip') ? ' ' + f.action : ''}`);
|