@dongmingzhixiu/vuexir-compiler 0.1.1 → 0.2.0
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/dist/codegen/compose.d.ts +17 -3
- package/dist/codegen/compose.js +324 -39
- package/dist/codegen/swiftui.d.ts +1 -0
- package/dist/codegen/swiftui.js +65 -24
- package/dist/index.d.ts +2 -2
- package/dist/index.js +2 -2
- package/dist/ir/schema.d.ts +16 -0
- package/dist/ir/schema.js +4 -3
- package/dist/transform/stores.d.ts +55 -0
- package/dist/transform/stores.js +303 -0
- package/dist/transform/vue2ir.d.ts +4 -3
- package/dist/transform/vue2ir.js +594 -21
- package/package.json +7 -7
package/dist/transform/vue2ir.js
CHANGED
|
@@ -18,21 +18,100 @@
|
|
|
18
18
|
import { parse } from '@vue/compiler-sfc';
|
|
19
19
|
import { NodeTypes } from '@vue/compiler-core';
|
|
20
20
|
import { CssParseError, mergeIrProps, parseCss, parseInlineStyle } from './css.js';
|
|
21
|
+
import { buildStoreRegistry, findStoreAliases, parseSharedConstArrays } from './stores.js';
|
|
21
22
|
export class VueCompileError extends Error {
|
|
22
23
|
}
|
|
24
|
+
/** R90 store getter 降级告警(flattenStorePath 无 warnings 参数,先收集后并入) */
|
|
25
|
+
let storeGetterWarnings = [];
|
|
26
|
+
/** R89 派生名表:`all` → `ui_upcoming`(computed 折叠成状态点路径) */
|
|
27
|
+
let derivedNames = new Map();
|
|
28
|
+
/** R88 store 文件导出的常量数组(v-for 编译期内联用) */
|
|
29
|
+
let storeConstArrays = {};
|
|
30
|
+
/** R86 store 别名表:`ui` → StoreDef(每次 vueToIr 重建;与 vforConsts 同风格) */
|
|
31
|
+
let storeAliases = new Map();
|
|
32
|
+
/** R86 把 `alias.field.path` 拍平为 `alias_field.path`;getter/action 返回哨兵串 */
|
|
33
|
+
function flattenStorePath(name) {
|
|
34
|
+
// R89:computed 折叠出的派生名(all → ui_upcoming;all.length → ui_upcoming.length)
|
|
35
|
+
if (derivedNames.size > 0) {
|
|
36
|
+
const m0 = name.match(/^([A-Za-z_$][\w$]*)(\..+)?$/);
|
|
37
|
+
if (m0 && derivedNames.has(m0[1]))
|
|
38
|
+
return derivedNames.get(m0[1]) + (m0[2] ?? '');
|
|
39
|
+
}
|
|
40
|
+
if (storeAliases.size === 0)
|
|
41
|
+
return name;
|
|
42
|
+
const m = name.match(/^([A-Za-z_$][\w$]*)\.(.+)$/);
|
|
43
|
+
if (!m)
|
|
44
|
+
return name;
|
|
45
|
+
const def = storeAliases.get(m[1]);
|
|
46
|
+
if (!def)
|
|
47
|
+
return name;
|
|
48
|
+
const segs = m[2].split('.');
|
|
49
|
+
const field = segs[0];
|
|
50
|
+
// R86 续:getter 若可折叠成「从某个 state 字段出发的成员链」,直接拍成该字段的点路径
|
|
51
|
+
const getter = def.getters.find((g) => g.name === field);
|
|
52
|
+
if (getter && getter.stateField) {
|
|
53
|
+
const tail = [getter.path, ...segs.slice(1)].filter(Boolean).join('.');
|
|
54
|
+
return [`${m[1]}_${getter.stateField}`, tail].filter(Boolean).join('.');
|
|
55
|
+
}
|
|
56
|
+
// R90 含计算逻辑的 getter:降级为读取底层 state 字段(筛选/排序/派生字段语义丢失)
|
|
57
|
+
if (getter && getter.refField) {
|
|
58
|
+
// 链式解析:getter → 另一个 getter → … → state 字段(如 soon → upcoming → list)
|
|
59
|
+
let field2 = getter.refField;
|
|
60
|
+
const seen = new Set([field]);
|
|
61
|
+
let guard = 0;
|
|
62
|
+
while (field2 && !def.state.some((f) => f.name === field2) && guard++ < 6 && !seen.has(field2)) {
|
|
63
|
+
seen.add(field2);
|
|
64
|
+
field2 = def.getters.find((g) => g.name === field2)?.refField;
|
|
65
|
+
}
|
|
66
|
+
if (field2 && def.state.some((f) => f.name === field2)) {
|
|
67
|
+
const msg = `store getter「${m[1]}.${field}」含计算逻辑(遍历/过滤/排序等),原生端无法求值 → 已降级为直接读取底层状态「${m[1]}_${field2}」,派生语义丢失(R90)`;
|
|
68
|
+
if (!storeGetterWarnings.includes(msg))
|
|
69
|
+
storeGetterWarnings.push(msg);
|
|
70
|
+
return [`${m[1]}_${field2}`, ...segs.slice(1)].filter(Boolean).join('.');
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
if (getter && getter.multi) {
|
|
74
|
+
const msg = `store getter「${m[1]}.${field}」依赖多个 state 字段,无法降级 → 已生成占位(R90)`;
|
|
75
|
+
if (!storeGetterWarnings.includes(msg))
|
|
76
|
+
storeGetterWarnings.push(msg);
|
|
77
|
+
return `\u0000store:${m[1]}.${m[2]}`;
|
|
78
|
+
}
|
|
79
|
+
if (!def.state.some((f) => f.name === field))
|
|
80
|
+
return `\u0000store:${m[1]}.${m[2]}`;
|
|
81
|
+
return [`${m[1]}_${field}`, ...segs.slice(1)].join('.');
|
|
82
|
+
}
|
|
23
83
|
/** R-VFOBJ-C:解析 <script setup> 顶层 `const rows = [ ... ]`(静态数组字面量)→ { name: 数组内文 }
|
|
24
84
|
* 模板 `v-for="(item, i) in rows"` 引用变量名时查表展开(编译期常量,非 M4b 动态状态);
|
|
25
85
|
* 自动排除 ref([...])(`=` 后非 `[` 不匹配)、navBar 对象(非数组)与函数体(保守容忍,与 extractScreenState 一致)
|
|
26
86
|
*/
|
|
27
87
|
export function extractScreenConstArrays(script) {
|
|
28
88
|
const out = {};
|
|
29
|
-
const
|
|
89
|
+
// 允许类型标注(const WAYS: {...}[] = [)且用括号配平,避免嵌套数组被首个 ] 截断(R88)
|
|
90
|
+
const re = /const\s+([A-Za-z_$][\w$]*)\s*(?::[^=]+)?=\s*\[/g;
|
|
30
91
|
let m;
|
|
31
92
|
while ((m = re.exec(script))) {
|
|
32
|
-
const
|
|
33
|
-
|
|
93
|
+
const at = script.indexOf('[', m.index + m[0].length - 1);
|
|
94
|
+
if (at === -1)
|
|
95
|
+
continue;
|
|
96
|
+
let depth = 0;
|
|
97
|
+
let end = -1;
|
|
98
|
+
for (let i = at; i < script.length; i++) {
|
|
99
|
+
if (script[i] === '[')
|
|
100
|
+
depth++;
|
|
101
|
+
else if (script[i] === ']') {
|
|
102
|
+
depth--;
|
|
103
|
+
if (depth === 0) {
|
|
104
|
+
end = i;
|
|
105
|
+
break;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
if (end === -1)
|
|
110
|
+
continue;
|
|
111
|
+
const inner = script.slice(at + 1, end).trim();
|
|
34
112
|
if (inner)
|
|
35
113
|
out[m[1]] = inner;
|
|
114
|
+
re.lastIndex = end + 1;
|
|
36
115
|
}
|
|
37
116
|
return out;
|
|
38
117
|
}
|
|
@@ -57,6 +136,37 @@ export function extractScreenState(script) {
|
|
|
57
136
|
const json = '[' + m[2].trim().replace(/'([^']*)'/g, '"$1"') + ']';
|
|
58
137
|
states.push({ name: m[1], init: json, list: true });
|
|
59
138
|
}
|
|
139
|
+
// R85 对象态:const board = ref({ month: { label: '10 月' }, tags: '生日' })
|
|
140
|
+
// init 存规范化 JSON 字符串(双端一致:Swift/Compose 交给 vrnField 点路径解析,JS 侧解析为对象)
|
|
141
|
+
for (const m of script.matchAll(/const\s+(\w+)\s*=\s*ref(?:<[^>]*>)?\(\s*\{/g)) {
|
|
142
|
+
const start = (m.index ?? 0) + m[0].length - 1;
|
|
143
|
+
let depth = 0;
|
|
144
|
+
let end = -1;
|
|
145
|
+
for (let i = start; i < script.length; i++) {
|
|
146
|
+
const ch = script[i];
|
|
147
|
+
if (ch === '{')
|
|
148
|
+
depth++;
|
|
149
|
+
else if (ch === '}') {
|
|
150
|
+
depth--;
|
|
151
|
+
if (depth === 0) {
|
|
152
|
+
end = i;
|
|
153
|
+
break;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
if (end === -1)
|
|
158
|
+
continue;
|
|
159
|
+
const raw = script.slice(start, end + 1);
|
|
160
|
+
// 规范化:单引号 → 双引号;裸键加引号(仅限简单字面量对象,复杂表达式会被 vrnField 忽略该层)
|
|
161
|
+
const json = raw
|
|
162
|
+
.replace(/'([^']*)'/g, (_a, inner) => `"${inner.replace(/"/g, '\\"')}"`)
|
|
163
|
+
.replace(/([{,]\s*)([A-Za-z_$][\w$]*)\s*:/g, (_a, pre, key) => `${pre}"${key}":`)
|
|
164
|
+
.replace(/,(\s*[}\]])/g, '$1')
|
|
165
|
+
.replace(/\s*\n\s*/g, ' ')
|
|
166
|
+
.replace(/\s{2,}/g, ' ')
|
|
167
|
+
.trim();
|
|
168
|
+
states.push({ name: m[1], init: json, obj: true });
|
|
169
|
+
}
|
|
60
170
|
// 箭头函数(有参):const f = (p) => { x.value = p } / const f = p => { ... }
|
|
61
171
|
for (const m of script.matchAll(/const\s+(\w+)\s*=\s*(?:\(\s*([A-Za-z_]\w*)\s*\)|([A-Za-z_]\w*))\s*=>\s*\{\s*(\w+)\.value\s*=\s*([^;{}]+)\s*;?\s*\}/g)) {
|
|
62
172
|
setters[m[1]] = { target: m[4], param: (m[2] ?? m[3]) };
|
|
@@ -264,6 +374,39 @@ if (typeof queueMicrotask !== "function") {
|
|
|
264
374
|
};
|
|
265
375
|
}
|
|
266
376
|
|
|
377
|
+
// R81 原生桥:bridge.toast / bridge.openUrl(iOS 由 __nativeToast/__nativeOpenUrl 宿主函数实现;
|
|
378
|
+
// Android 由编译器把 bridge.* 翻译成 VrnBridge.*;浏览器预览退回 alert/console)
|
|
379
|
+
// R81/R84 原生桥:编译期注入的实现(iOS VrnBridge.swift / Android VrnBridge.kt)
|
|
380
|
+
// 未注入原生实现时(浏览器预览)退化为 console / window 语义,保证同一份代码两端可跑
|
|
381
|
+
function __vrnNative(name) {
|
|
382
|
+
return typeof globalThis[name] === "function" || typeof (typeof self !== "undefined" ? self[name] : undefined) === "function";
|
|
383
|
+
}
|
|
384
|
+
var bridge = {
|
|
385
|
+
isNative: __vrnNative("__nativeToast"),
|
|
386
|
+
toast: function (msg) {
|
|
387
|
+
if (__vrnNative("__nativeToast")) __nativeToast(String(msg));
|
|
388
|
+
else if (typeof console !== "undefined") console.log("[bridge.toast]", msg);
|
|
389
|
+
},
|
|
390
|
+
openUrl: function (url) {
|
|
391
|
+
if (__vrnNative("__nativeOpenUrl")) __nativeOpenUrl(String(url));
|
|
392
|
+
else if (typeof window !== "undefined") window.open(String(url), "_blank");
|
|
393
|
+
},
|
|
394
|
+
vibrate: function (ms) {
|
|
395
|
+
if (__vrnNative("__nativeVibrate")) __nativeVibrate(Number(ms) > 0 ? Number(ms) : 18);
|
|
396
|
+
else if (typeof navigator !== "undefined" && navigator.vibrate) navigator.vibrate(Number(ms) > 0 ? Number(ms) : 18);
|
|
397
|
+
},
|
|
398
|
+
copy: function (text) {
|
|
399
|
+
if (__vrnNative("__nativeCopy")) __nativeCopy(String(text));
|
|
400
|
+
else if (typeof navigator !== "undefined" && navigator.clipboard) navigator.clipboard.writeText(String(text));
|
|
401
|
+
},
|
|
402
|
+
setStatusBar: function (style) {
|
|
403
|
+
if (__vrnNative("__nativeSetStatusBar")) __nativeSetStatusBar(String(style));
|
|
404
|
+
},
|
|
405
|
+
keepScreenOn: function (on) {
|
|
406
|
+
if (__vrnNative("__nativeKeepScreenOn")) __nativeKeepScreenOn(!!on);
|
|
407
|
+
}
|
|
408
|
+
};
|
|
409
|
+
|
|
267
410
|
// localStorage → 原生 UserDefaults(D7:vrn. 前缀 + 同步读写)
|
|
268
411
|
var localStorage = {
|
|
269
412
|
getItem: function (k) { return typeof __lsGet === "function" ? __lsGet("vrn." + k) : null; },
|
|
@@ -326,7 +469,81 @@ export function swiftExpr(exp) {
|
|
|
326
469
|
}
|
|
327
470
|
/** @click="fn(arg)" → 若 fn 是已提取的 setter,内联为状态赋值 { var, value, kind }(M1 扩展无参表达式 setter)
|
|
328
471
|
* M2:直接赋值表达式 `selected = item` / `selected = i`(列表选中)同样内联 */
|
|
329
|
-
|
|
472
|
+
/**
|
|
473
|
+
* R87 store action 折叠:把 `ui.setSkin('night')` 解析成页面状态赋值。
|
|
474
|
+
* 只认三种简单体:`this.f = <字面量>` / `this.f = <形参>`(调用处实参替代)/ `this.f++`。
|
|
475
|
+
* 返回 null 表示不可折叠(调用方保留原样并给 warning)。
|
|
476
|
+
*/
|
|
477
|
+
function resolveStoreAction(alias, actionName, argsRaw, warn) {
|
|
478
|
+
const def = storeAliases.get(alias);
|
|
479
|
+
if (!def)
|
|
480
|
+
return null;
|
|
481
|
+
const act = def.actions.find((a) => a.name === actionName);
|
|
482
|
+
if (!act)
|
|
483
|
+
return null;
|
|
484
|
+
// 只取第一条语句:`this.skin = skin; void setSetting(...)` 里状态赋值可折叠,持久化副作用丢弃(给 warning)
|
|
485
|
+
const stmts = act.body
|
|
486
|
+
.split(/[\n;]+/)
|
|
487
|
+
.map((t) => t.trim())
|
|
488
|
+
.filter(Boolean);
|
|
489
|
+
const body = (stmts[0] ?? '').replace(/;\s*$/, '').trim();
|
|
490
|
+
const args = argsRaw
|
|
491
|
+
.split(',')
|
|
492
|
+
.map((a) => a.trim())
|
|
493
|
+
.filter(Boolean);
|
|
494
|
+
// this.f = <rhs>
|
|
495
|
+
let m = body.match(/^this\.(\w+)\s*=\s*([\s\S]+)$/);
|
|
496
|
+
if (m) {
|
|
497
|
+
const field = m[1];
|
|
498
|
+
if (!def.state.some((f) => f.name === field))
|
|
499
|
+
return null;
|
|
500
|
+
let rhs = m[2].trim();
|
|
501
|
+
const pIdx = act.params.indexOf(rhs);
|
|
502
|
+
if (pIdx !== -1) {
|
|
503
|
+
if (args.length <= pIdx)
|
|
504
|
+
return null;
|
|
505
|
+
rhs = args[pIdx];
|
|
506
|
+
}
|
|
507
|
+
else if (act.params.some((p) => new RegExp(`\\b${p}\\b`).test(rhs))) {
|
|
508
|
+
return null; // 体里对形参做了运算 → 不折叠
|
|
509
|
+
}
|
|
510
|
+
if (stmts.length > 1) {
|
|
511
|
+
warn?.(`store action「${actionName}」已折叠状态赋值,其余语句(${stmts.slice(1).join('; ')})未折叠(R87)`);
|
|
512
|
+
}
|
|
513
|
+
if (/^-?\d+(\.\d+)?$/.test(rhs))
|
|
514
|
+
return { var: `${alias}_${field}`, value: Number(rhs), kind: 'literal' };
|
|
515
|
+
if (/^'[^']*'$|^"[^"]*"$/.test(rhs))
|
|
516
|
+
return { var: `${alias}_${field}`, value: rhs.slice(1, -1), kind: 'literal' };
|
|
517
|
+
const expr = swiftExpr(rhs);
|
|
518
|
+
return expr === null ? null : { var: `${alias}_${field}`, value: expr, kind: 'expr' };
|
|
519
|
+
}
|
|
520
|
+
// this.f++ / this.f--
|
|
521
|
+
m = body.match(/^this\.(\w+)\s*(\+\+|--)$/);
|
|
522
|
+
if (m && def.state.some((f) => f.name === m[1])) {
|
|
523
|
+
if (stmts.length > 1)
|
|
524
|
+
warn?.(`store action「${actionName}」已折叠状态赋值,其余语句(${stmts.slice(1).join('; ')})未折叠(R87)`);
|
|
525
|
+
const v = `${alias}_${m[1]}`;
|
|
526
|
+
return { var: v, value: `${v} ${m[2] === '++' ? '+' : '-'} 1`, kind: 'expr' };
|
|
527
|
+
}
|
|
528
|
+
return null;
|
|
529
|
+
}
|
|
530
|
+
/** R87 把 handler 体内 `alias.action(...)` 折成 `stateStore.alias_field = <值>`(双端可译) */
|
|
531
|
+
function foldStoreActionsInBody(body) {
|
|
532
|
+
if (storeAliases.size === 0)
|
|
533
|
+
return body;
|
|
534
|
+
let out = body;
|
|
535
|
+
for (const [alias, def] of storeAliases) {
|
|
536
|
+
for (const act of def.actions) {
|
|
537
|
+
const re = new RegExp(`\\b${alias}\\.${act.name}\\(([^)]*)\\)`, 'g');
|
|
538
|
+
out = out.replace(re, (whole, args) => {
|
|
539
|
+
const r = resolveStoreAction(alias, act.name, args);
|
|
540
|
+
return r === null ? whole : `stateStore.${r.var} = ${r.kind === 'literal' && typeof r.value === 'string' ? JSON.stringify(r.value) : String(r.value)}`;
|
|
541
|
+
});
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
return out;
|
|
545
|
+
}
|
|
546
|
+
export function resolveSetCall(handler, state, warn) {
|
|
330
547
|
const t = handler.trim();
|
|
331
548
|
// M2 直接赋值:目标必须是已声明状态
|
|
332
549
|
const assign = t.match(/^([A-Za-z_]\w*)\s*=\s*(.+)$/);
|
|
@@ -341,6 +558,13 @@ export function resolveSetCall(handler, state) {
|
|
|
341
558
|
return { var: assign[1], value: expr, kind: 'expr' };
|
|
342
559
|
return null;
|
|
343
560
|
}
|
|
561
|
+
// R87 store action:`ui.setSkin('night')` → onSet 赋值(两端统一走 onSet)
|
|
562
|
+
const sa = t.match(/^([A-Za-z_$][\w$]*)\.([A-Za-z_$][\w$]*)\(([^)]*)\)$/);
|
|
563
|
+
if (sa && storeAliases.has(sa[1])) {
|
|
564
|
+
const r = resolveStoreAction(sa[1], sa[2], sa[3], warn);
|
|
565
|
+
if (r)
|
|
566
|
+
return r;
|
|
567
|
+
}
|
|
344
568
|
const m = t.match(/^(\w+)\(([^)]*)\)$/);
|
|
345
569
|
if (!m)
|
|
346
570
|
return null;
|
|
@@ -464,6 +688,20 @@ function parseVFor(exp, warnings, constArrays) {
|
|
|
464
688
|
const body = m[3].trim();
|
|
465
689
|
// M4b 动态列表:in <stateName>(stateStore 数组;Swift 侧 JSON 字符串解码渲染)
|
|
466
690
|
// R-VFOBJ-C:引用 script 顶层 const 数组(const rows = [...])→ 编译期展开,优先于动态状态判定
|
|
691
|
+
// R88 store 常量数组(store 文件里 export const SKINS = [...])与页面常量数组同等待遇
|
|
692
|
+
if (body in storeConstArrays && !(constArrays && body in constArrays)) {
|
|
693
|
+
const vf = parseVForArrayInner(storeConstArrays[body], itemKey, indexKey, warnings);
|
|
694
|
+
if (vf)
|
|
695
|
+
vf.srcName = body;
|
|
696
|
+
return vf;
|
|
697
|
+
}
|
|
698
|
+
// R88 store 状态数组:v-for="x in ui.list" → 拍平为 ui_list 后按动态列表处理
|
|
699
|
+
{
|
|
700
|
+
const flat = flattenStorePath(body);
|
|
701
|
+
if (flat !== body && /^[A-Za-z_$][\w$]*$/.test(flat)) {
|
|
702
|
+
return { listState: flat, itemKey, indexKey };
|
|
703
|
+
}
|
|
704
|
+
}
|
|
467
705
|
if (/^[A-Za-z_]\w*$/.test(body)) {
|
|
468
706
|
if (constArrays && body in constArrays) {
|
|
469
707
|
const inner = constArrays[body];
|
|
@@ -569,9 +807,21 @@ function parseObjectLiteral(s) {
|
|
|
569
807
|
const key = trimmed.slice(0, ci).trim().replace(/^['"]|['"]$/g, '');
|
|
570
808
|
const valRaw = trimmed.slice(ci + 1).trim();
|
|
571
809
|
const lit = literalValue(valRaw);
|
|
572
|
-
if (
|
|
573
|
-
|
|
574
|
-
|
|
810
|
+
if (typeof lit === 'boolean') {
|
|
811
|
+
// R88:布尔值按字符串保留(列表项属性表只存 string|number,渲染时按文本用)
|
|
812
|
+
obj[key] = lit ? 'true' : 'false';
|
|
813
|
+
continue;
|
|
814
|
+
}
|
|
815
|
+
if (lit !== undefined) {
|
|
816
|
+
obj[key] = lit;
|
|
817
|
+
continue;
|
|
818
|
+
}
|
|
819
|
+
// R88:嵌套数组/对象值 → 序列化为 JSON 文本保底(不作为可渲染字段,但不因此整段丢弃)
|
|
820
|
+
if (/^[\[{]/.test(valRaw) && /[\]}]$/.test(valRaw)) {
|
|
821
|
+
obj[key] = valRaw.replace(/'([^']*)'/g, (_a, inner) => `"${inner}"`);
|
|
822
|
+
continue;
|
|
823
|
+
}
|
|
824
|
+
return null;
|
|
575
825
|
}
|
|
576
826
|
return Object.keys(obj).length > 0 ? obj : null;
|
|
577
827
|
}
|
|
@@ -588,6 +838,14 @@ const TAG_MAP = {
|
|
|
588
838
|
li: { type: 'group' },
|
|
589
839
|
p: { type: 'text', defaultProps: { fontSize: 14 } },
|
|
590
840
|
span: { type: 'text', defaultProps: { fontSize: 14 } },
|
|
841
|
+
// R89 续:行内语义标签按纯文本处理(原生端无 HTML 语义,但文本内容必须保留)
|
|
842
|
+
b: { type: 'text', defaultProps: { fontSize: 14 } },
|
|
843
|
+
strong: { type: 'text', defaultProps: { fontSize: 14 } },
|
|
844
|
+
i: { type: 'text', defaultProps: { fontSize: 14 } },
|
|
845
|
+
em: { type: 'text', defaultProps: { fontSize: 14 } },
|
|
846
|
+
small: { type: 'text', defaultProps: { fontSize: 12 } },
|
|
847
|
+
u: { type: 'text', defaultProps: { fontSize: 14 } },
|
|
848
|
+
code: { type: 'text', defaultProps: { fontSize: 13 } },
|
|
591
849
|
h1: { type: 'text', defaultProps: { fontSize: 24, fontWeight: 'bold' } },
|
|
592
850
|
h2: { type: 'text', defaultProps: { fontSize: 20, fontWeight: 'bold' } },
|
|
593
851
|
h3: { type: 'text', defaultProps: { fontSize: 17, fontWeight: 'medium' } },
|
|
@@ -605,6 +863,10 @@ let vforConsts = {};
|
|
|
605
863
|
export function vueToIr(source, opts = {}) {
|
|
606
864
|
compPosition = new Map();
|
|
607
865
|
vforConsts = {};
|
|
866
|
+
storeAliases = new Map();
|
|
867
|
+
storeConstArrays = {};
|
|
868
|
+
derivedNames = new Map();
|
|
869
|
+
storeGetterWarnings = [];
|
|
608
870
|
const warnings = [];
|
|
609
871
|
const { descriptor, errors } = parse(source, { filename: 'Screen.vue' });
|
|
610
872
|
if (errors.length > 0) {
|
|
@@ -687,6 +949,8 @@ export function vueToIr(source, opts = {}) {
|
|
|
687
949
|
let navBarConfig = null;
|
|
688
950
|
let onMountedBody;
|
|
689
951
|
let onMountedAsync = false;
|
|
952
|
+
/** R74 保留名状态的重命名映射(旧名 → 新名) */
|
|
953
|
+
const stateRenames = new Map();
|
|
690
954
|
let stateInfo;
|
|
691
955
|
const script = descriptor.scriptSetup ?? descriptor.script;
|
|
692
956
|
if (script && script.content.trim()) {
|
|
@@ -706,12 +970,71 @@ export function vueToIr(source, opts = {}) {
|
|
|
706
970
|
warnings.push(`handler「${h.name}」函数体疑似含 TS 类型标注(如 const x: number = 1),runtime.js 将解析失败——请移除类型标注(R13 仅支持 JS 子集)`);
|
|
707
971
|
}
|
|
708
972
|
}
|
|
709
|
-
//
|
|
973
|
+
// R86 Pinia 折叠:`const ui = useUiStore()` → 把 store.state 拍平成页面状态 `ui_<field>`
|
|
974
|
+
if (opts.storeSources) {
|
|
975
|
+
for (const src of Object.values(opts.storeSources)) {
|
|
976
|
+
storeConstArrays = { ...storeConstArrays, ...parseSharedConstArrays(src) };
|
|
977
|
+
}
|
|
978
|
+
const registry = buildStoreRegistry(opts.storeSources);
|
|
979
|
+
storeAliases = findStoreAliases(src, registry);
|
|
980
|
+
for (const [alias, def] of storeAliases) {
|
|
981
|
+
for (const f of def.state) {
|
|
982
|
+
const flat = `${alias}_${f.name}`;
|
|
983
|
+
if (stateInfo.states.some((st) => st.name === flat))
|
|
984
|
+
continue;
|
|
985
|
+
stateInfo.states.push({ name: flat, init: f.init, ...(f.obj ? { obj: true } : {}), ...(f.list ? { list: true } : {}) });
|
|
986
|
+
}
|
|
987
|
+
}
|
|
988
|
+
}
|
|
989
|
+
// R89 computed 折叠:`const all = computed(() => store.upcoming)` → 派生名指向扁平状态点路径
|
|
990
|
+
for (const cm of src.matchAll(/const\s+([A-Za-z_$][\w$]*)\s*=\s*computed\s*\(\s*\(\s*\)\s*=>\s*([^)]+?)\s*\)/g)) {
|
|
991
|
+
const expr = cm[2].trim();
|
|
992
|
+
const flat = flattenStorePath(expr);
|
|
993
|
+
if (flat !== expr && /^[A-Za-z_$][\w$.]*$/.test(flat)) {
|
|
994
|
+
derivedNames.set(cm[1], flat);
|
|
995
|
+
}
|
|
996
|
+
}
|
|
997
|
+
// R89 常量折叠:模块级纯常量(含可静态求值的模板字面量)注入为状态
|
|
998
|
+
for (const km of src.matchAll(/const\s+([A-Za-z_$][\w$]*)\s*(?::[^=]+)?=\s*(`[^`]*`|'[^']*'|"[^"]*"|-?\d+(?:\.\d+)?)\s*$/gm)) {
|
|
999
|
+
const raw = km[2];
|
|
1000
|
+
const name = km[1];
|
|
1001
|
+
if (stateInfo.states.some((st) => st.name === name))
|
|
1002
|
+
continue;
|
|
1003
|
+
let value;
|
|
1004
|
+
if (/^-?\d+(\.\d+)?$/.test(raw))
|
|
1005
|
+
value = Number(raw);
|
|
1006
|
+
else if (raw.startsWith('`')) {
|
|
1007
|
+
// 模板字面量:仅允许 Date/Math/数字/字符串等可静态求值的片段,否则跳过
|
|
1008
|
+
const inner = raw.slice(1, -1);
|
|
1009
|
+
if (/\$\{[^}]*\}/.test(inner)) {
|
|
1010
|
+
// 白名单改为黑名单:允许中文等任意字面字符,但禁止访问宿主/业务对象
|
|
1011
|
+
if (!/\b(window|document|globalThis|require|process|fetch|store|props|this|localStorage)\b/.test(inner)) {
|
|
1012
|
+
try {
|
|
1013
|
+
value = String(new Function(`return \`${inner}\`;`)());
|
|
1014
|
+
}
|
|
1015
|
+
catch {
|
|
1016
|
+
/* 求值失败 → 跳过 */
|
|
1017
|
+
}
|
|
1018
|
+
}
|
|
1019
|
+
}
|
|
1020
|
+
else {
|
|
1021
|
+
value = inner;
|
|
1022
|
+
}
|
|
1023
|
+
}
|
|
1024
|
+
else {
|
|
1025
|
+
value = raw.slice(1, -1);
|
|
1026
|
+
}
|
|
1027
|
+
if (value !== undefined)
|
|
1028
|
+
stateInfo.states.push({ name, init: value });
|
|
1029
|
+
}
|
|
1030
|
+
// R74:状态名撞原生保留成员(SwiftUI `body`、Kotlin 关键字)→ 自动改名 + 警告(不再只警告导致后续编译失败)
|
|
710
1031
|
{
|
|
711
1032
|
const RESERVED = ['body', 'self', 'init', 'class', 'object', 'fun', 'val', 'var', 'when', 'is', 'in'];
|
|
712
1033
|
for (const st of stateInfo?.states ?? []) {
|
|
713
1034
|
if (RESERVED.includes(st.name)) {
|
|
714
|
-
|
|
1035
|
+
const next = `${st.name}Text`;
|
|
1036
|
+
stateRenames.set(st.name, next);
|
|
1037
|
+
warnings.push(`状态名「${st.name}」与原生保留成员冲突 → 将自动改名「${next}」(R74)`);
|
|
715
1038
|
}
|
|
716
1039
|
}
|
|
717
1040
|
}
|
|
@@ -723,6 +1046,28 @@ export function vueToIr(source, opts = {}) {
|
|
|
723
1046
|
onMountedAsync = /(^|[^\w$.])await\s/.test(onMountedBody) || /onMounted\s*\(\s*async\b/.test(src);
|
|
724
1047
|
onMountedBody = rewriteStateRefs(onMountedBody, (stateInfo?.states ?? []).map((s) => s.name));
|
|
725
1048
|
}
|
|
1049
|
+
// R87:handler/onMounted 内的 store action 调用 → 状态赋值(iOS 走 JS stateStore,Android 走内联赋值)
|
|
1050
|
+
if (storeAliases.size > 0) {
|
|
1051
|
+
for (const h of handlers ?? [])
|
|
1052
|
+
h.body = foldStoreActionsInBody(h.body);
|
|
1053
|
+
if (onMountedBody !== undefined)
|
|
1054
|
+
onMountedBody = foldStoreActionsInBody(onMountedBody);
|
|
1055
|
+
}
|
|
1056
|
+
// R74 续:先在 handler/onMounted 源码里改名,再生成 runtime(否则 JS 侧仍指向旧名)
|
|
1057
|
+
if (stateRenames.size > 0) {
|
|
1058
|
+
const renameSrc = (src) => {
|
|
1059
|
+
let out = src;
|
|
1060
|
+
for (const [from, to] of stateRenames) {
|
|
1061
|
+
out = out.replace(new RegExp(`\\bstateStore\\.${from}\\b`, 'g'), `stateStore.${to}`);
|
|
1062
|
+
out = out.replace(new RegExp(`\\b${from}\\.value\\b`, 'g'), `${to}.value`);
|
|
1063
|
+
}
|
|
1064
|
+
return out;
|
|
1065
|
+
};
|
|
1066
|
+
for (const h of handlers ?? [])
|
|
1067
|
+
h.body = renameSrc(h.body);
|
|
1068
|
+
if (onMountedBody !== undefined)
|
|
1069
|
+
onMountedBody = renameSrc(onMountedBody);
|
|
1070
|
+
}
|
|
726
1071
|
if (handlers.length > 0 || onMountedBody !== undefined) {
|
|
727
1072
|
runtime = generateRuntimeSource(stateInfo?.states ?? [], handlers, onMountedBody, onMountedAsync);
|
|
728
1073
|
}
|
|
@@ -736,6 +1081,21 @@ export function vueToIr(source, opts = {}) {
|
|
|
736
1081
|
compPosition.set(c.name, tree.position);
|
|
737
1082
|
return { name: c.name, ui: tree.ui };
|
|
738
1083
|
});
|
|
1084
|
+
// R74 续:把重命名应用到 handler/onMounted 源码与 IR 引用(否则绑定仍指向旧名 → 编译失败)
|
|
1085
|
+
if (stateRenames.size > 0) {
|
|
1086
|
+
const renameSrc = (src) => {
|
|
1087
|
+
let out = src;
|
|
1088
|
+
for (const [from, to] of stateRenames) {
|
|
1089
|
+
out = out.replace(new RegExp(`\\bstateStore\\.${from}\\b`, 'g'), `stateStore.${to}`);
|
|
1090
|
+
out = out.replace(new RegExp(`\\b${from}\\.value\\b`, 'g'), `${to}.value`);
|
|
1091
|
+
}
|
|
1092
|
+
return out;
|
|
1093
|
+
};
|
|
1094
|
+
for (const h of handlers ?? [])
|
|
1095
|
+
h.body = renameSrc(h.body);
|
|
1096
|
+
if (onMountedBody !== undefined)
|
|
1097
|
+
onMountedBody = renameSrc(onMountedBody);
|
|
1098
|
+
}
|
|
739
1099
|
const children = ast.children.flatMap((c) => walk(c, classProps, comboRules, scopedRules, pseudoRules, warnings, stateInfo, compTable));
|
|
740
1100
|
let pageRootBg;
|
|
741
1101
|
// 3) 组装 screen:root(width fill + height 基准)包一层 group(承载根元素布局 props)
|
|
@@ -774,8 +1134,64 @@ export function vueToIr(source, opts = {}) {
|
|
|
774
1134
|
checkClickHandlers(c);
|
|
775
1135
|
};
|
|
776
1136
|
checkClickHandlers(ui);
|
|
1137
|
+
// R74 续:IR 树里的状态引用改名(bind/show/onSet/bindParts/fieldOf/condition/动态样式/输入值/列表态)
|
|
1138
|
+
if (stateRenames.size > 0) {
|
|
1139
|
+
// 状态表本身改名(放在 walk 之后,保证 {{ body }} 在解析时仍能命中原名)
|
|
1140
|
+
for (const st of stateInfo?.states ?? []) {
|
|
1141
|
+
const to = stateRenames.get(st.name);
|
|
1142
|
+
if (to !== undefined)
|
|
1143
|
+
st.name = to;
|
|
1144
|
+
}
|
|
1145
|
+
const rn = (v) => (v === undefined ? undefined : stateRenames.get(v) ?? v);
|
|
1146
|
+
const rnCond = (c) => {
|
|
1147
|
+
if (c === undefined)
|
|
1148
|
+
return undefined;
|
|
1149
|
+
let out = c;
|
|
1150
|
+
for (const [from, to] of stateRenames)
|
|
1151
|
+
out = out.replace(new RegExp(`\\b${from}\\b`, 'g'), to);
|
|
1152
|
+
return out;
|
|
1153
|
+
};
|
|
1154
|
+
const walkRen = (n) => {
|
|
1155
|
+
const p = n.props;
|
|
1156
|
+
if (p) {
|
|
1157
|
+
for (const k of ['bind', 'show', 'value', 'colorDynamic', 'fontSizeDynamic']) {
|
|
1158
|
+
if (typeof p[k] === 'string')
|
|
1159
|
+
p[k] = rn(p[k]);
|
|
1160
|
+
}
|
|
1161
|
+
if (p.onSet && typeof p.onSet === 'object') {
|
|
1162
|
+
const os = p.onSet;
|
|
1163
|
+
os.var = rn(os.var);
|
|
1164
|
+
}
|
|
1165
|
+
if (p.fieldOf && typeof p.fieldOf === 'object') {
|
|
1166
|
+
const f = p.fieldOf;
|
|
1167
|
+
f.obj = rn(f.obj);
|
|
1168
|
+
}
|
|
1169
|
+
if (Array.isArray(p.bindParts)) {
|
|
1170
|
+
for (const seg of p.bindParts) {
|
|
1171
|
+
if (typeof seg.bind === 'string')
|
|
1172
|
+
seg.bind = rn(seg.bind);
|
|
1173
|
+
if (seg.fieldOf && typeof seg.fieldOf === 'object') {
|
|
1174
|
+
const f = seg.fieldOf;
|
|
1175
|
+
f.obj = rn(f.obj);
|
|
1176
|
+
}
|
|
1177
|
+
}
|
|
1178
|
+
}
|
|
1179
|
+
if (n.vfor && n.vfor.listState)
|
|
1180
|
+
n.vfor.listState = rn(n.vfor.listState);
|
|
1181
|
+
}
|
|
1182
|
+
n.condition = rnCond(n.condition);
|
|
1183
|
+
if (n.active)
|
|
1184
|
+
n.active.when = rnCond(n.active.when);
|
|
1185
|
+
for (const c of n.children ?? [])
|
|
1186
|
+
walkRen(c);
|
|
1187
|
+
};
|
|
1188
|
+
walkRen(ui);
|
|
1189
|
+
}
|
|
777
1190
|
// 5) <script setup> 逻辑源码 → 运行时 JS(R12:事件逻辑由原生 JS 引擎执行)
|
|
778
1191
|
// 仅支持 JS 子集(无 TS 类型标注、无 import);超出即编译期警告(后续阶段扩展)
|
|
1192
|
+
for (const w of storeGetterWarnings)
|
|
1193
|
+
if (!warnings.includes(w))
|
|
1194
|
+
warnings.push(w);
|
|
779
1195
|
return {
|
|
780
1196
|
screen: {
|
|
781
1197
|
id: opts.screenId ?? 'vue-screen',
|
|
@@ -804,11 +1220,23 @@ function walk(node, classProps, comboRules, scopedRules, pseudoRules, warnings,
|
|
|
804
1220
|
case NodeTypes.COMMENT:
|
|
805
1221
|
return [];
|
|
806
1222
|
case NodeTypes.INTERPOLATION: {
|
|
807
|
-
const
|
|
1223
|
+
const nameRaw = textOf(node.content).trim();
|
|
1224
|
+
// R86:store 访问先拍平(ui.dialog.title → ui_dialog.title);getter/action 返回哨兵
|
|
1225
|
+
const name = flattenStorePath(normalizeTemplateExpr(nameRaw, warnings));
|
|
808
1226
|
// 组件树模式(propRefs 传入=非空):{{ 任意名 }} → text ref(Swift Text(prop),
|
|
809
1227
|
// prop 名由组件树实际引用自动收集,不强制 defineProps 声明)
|
|
810
|
-
if (propRefs)
|
|
1228
|
+
if (propRefs) {
|
|
1229
|
+
// R78 续(G13):组件内 {{ obj.field }} → 组件 prop 是字符串(JSON)→ 运行时取值
|
|
1230
|
+
const cf = name.match(/^([A-Za-z_$][\w$]*)\.([A-Za-z_$][\w$]*)$/);
|
|
1231
|
+
if (cf && propRefs.has(cf[1])) {
|
|
1232
|
+
return [{ type: 'text', props: { fieldOf: { obj: cf[1], field: cf[2] } } }];
|
|
1233
|
+
}
|
|
811
1234
|
return [{ type: 'text', props: { ref: name } }];
|
|
1235
|
+
}
|
|
1236
|
+
if (name.startsWith('\u0000store:')) {
|
|
1237
|
+
warnings.push(`插值 {{ ${name.slice(7)} }} 是 Pinia store 的 getter/action,R86 目前只折叠 state 字段 → 已生成占位文本`);
|
|
1238
|
+
return [{ type: 'text', props: { text: '\u200B' } }];
|
|
1239
|
+
}
|
|
812
1240
|
// M2 模板内三元(D1 小范围):cond ? 'a' : 'b' → Swift 三元字符串表达式(Text(expr))
|
|
813
1241
|
const tri = parseTernaryExpr(name);
|
|
814
1242
|
if (tri)
|
|
@@ -830,6 +1258,29 @@ function walk(node, classProps, comboRules, scopedRules, pseudoRules, warnings,
|
|
|
830
1258
|
}
|
|
831
1259
|
throw new VueCompileError(`插值 {{ ${name} }}:未在 <script setup> 中声明为 ref 状态(M1 白名单:仅支持已声明状态)`);
|
|
832
1260
|
}
|
|
1261
|
+
// R92 点路径兜底:{{ a.b ?? '默认' }} → fieldOf + fallback
|
|
1262
|
+
{
|
|
1263
|
+
const fbm = name.match(/^@@fb@@([^@]+)@@([\s\S]*)$/);
|
|
1264
|
+
if (fbm) {
|
|
1265
|
+
// 内部路径可能仍是 store 别名/computed 派生名 → 先拍平
|
|
1266
|
+
const p2 = matchFieldPath(flattenStorePath(fbm[1]));
|
|
1267
|
+
if (p2 && (state?.states ?? []).some((st) => st.name === p2.obj)) {
|
|
1268
|
+
return [{ type: 'text', props: { fieldOf: { obj: p2.obj, field: p2.field, fallback: fbm[2] } } }];
|
|
1269
|
+
}
|
|
1270
|
+
}
|
|
1271
|
+
}
|
|
1272
|
+
// R78/R85 对象字段插值 {{ obj.field }} / {{ obj.a.b }} / {{ obj.list.length }}:
|
|
1273
|
+
// obj 是页面状态 → 运行时按点路径取值;否则明确警告 + 空文本降级
|
|
1274
|
+
{
|
|
1275
|
+
const fld = matchFieldPath(name);
|
|
1276
|
+
if (fld) {
|
|
1277
|
+
const isState = (state?.states ?? []).some((st) => st.name === fld.obj);
|
|
1278
|
+
if (isState)
|
|
1279
|
+
return [{ type: 'text', props: { fieldOf: { obj: fld.obj, field: fld.field } } }];
|
|
1280
|
+
warnings.push(`插值 {{ ${name} }} 的对象「${fld.obj}」不是页面状态(如 Pinia store)→ 已生成占位文本;请把该字段提升为 ref(R78/R85)`);
|
|
1281
|
+
return [{ type: 'text', props: { text: ' ' } }];
|
|
1282
|
+
}
|
|
1283
|
+
}
|
|
833
1284
|
throw new VueCompileError(`插值表达式 {{ ${name} }} 暂不支持(M1 仅支持纯状态名;三元/比较表达式 M2 白名单)`);
|
|
834
1285
|
}
|
|
835
1286
|
case NodeTypes.ELEMENT:
|
|
@@ -840,6 +1291,19 @@ function walk(node, classProps, comboRules, scopedRules, pseudoRules, warnings,
|
|
|
840
1291
|
}
|
|
841
1292
|
}
|
|
842
1293
|
/** M2 模板内三元插值:`cond ? 'a' : 'b'` → Swift 三元字符串表达式;cond 需过 swiftExpr 白名单 */
|
|
1294
|
+
/**
|
|
1295
|
+
* R85 成员链匹配:`a.b` / `a.b.c` / `a.list.length`
|
|
1296
|
+
* 返回 { obj: 首段, field: 余下点路径 };非纯成员链(含调用/下标/运算符)返回 null。
|
|
1297
|
+
*/
|
|
1298
|
+
function matchFieldPath(name) {
|
|
1299
|
+
if (!/^[A-Za-z_$][\w$]*(\.[A-Za-z_$][\w$]*)+$/.test(name))
|
|
1300
|
+
return null;
|
|
1301
|
+
const segs = name.split('.');
|
|
1302
|
+
// length/size 只允许出现在末段(作为属性读,而非方法调用)
|
|
1303
|
+
if (segs.slice(1, -1).some((s) => s === 'length' || s === 'size'))
|
|
1304
|
+
return null;
|
|
1305
|
+
return { obj: segs[0], field: segs.slice(1).join('.') };
|
|
1306
|
+
}
|
|
843
1307
|
function parseTernaryExpr(exp) {
|
|
844
1308
|
const t = exp.trim();
|
|
845
1309
|
const m = t.match(/^(.+?)\s*\?\s*'([^']+)'\s*:\s*'([^']+)'$/);
|
|
@@ -850,6 +1314,29 @@ function parseTernaryExpr(exp) {
|
|
|
850
1314
|
return null;
|
|
851
1315
|
return `${cond} ? "${m[2]}" : "${m[3]}"`;
|
|
852
1316
|
}
|
|
1317
|
+
/**
|
|
1318
|
+
* R91 模板表达式归一化(在成员链/状态判定之前统一处理):
|
|
1319
|
+
* ① 可选链 `a?.b` → `a.b`(原生端 vrnField 对缺失键返回空串,近似等价;仅提示一次);
|
|
1320
|
+
* ② 空值兜底 `a || 'x'` / `a ?? 'x'`(a 为纯状态名)→ `a ? 'x' : 'x'` 交给三元通道。
|
|
1321
|
+
* 其余形态(方法调用、字符串方法、模板字面量)不在此处理,按原逻辑报错。
|
|
1322
|
+
*/
|
|
1323
|
+
function normalizeTemplateExpr(name, warnings) {
|
|
1324
|
+
let out = name;
|
|
1325
|
+
if (out.includes('?.')) {
|
|
1326
|
+
out = out.replace(/\?\./g, '.');
|
|
1327
|
+
const msg = `模板可选链「${name}」已归一化为「${out}」——原生端字段缺失时返回空串(近似 null 安全,R91)`;
|
|
1328
|
+
if (!warnings.includes(msg))
|
|
1329
|
+
warnings.push(msg);
|
|
1330
|
+
}
|
|
1331
|
+
const fb = out.match(/^([A-Za-z_$][\w$]*)\s*(?:\|\||\?\?)\s*'([^']*)'$/);
|
|
1332
|
+
if (fb)
|
|
1333
|
+
return `${fb[1]} ? '${fb[2]}' : '${fb[1]}'`;
|
|
1334
|
+
// R92 点路径上的兜底:`a.b ?? '默认'` / `a.b.c || '默认'`(原生端 vrnField 带 fallback)
|
|
1335
|
+
const fb2 = out.match(/^([A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*)+)\s*(?:\|\||\?\?)\s*'([^']*)'$/);
|
|
1336
|
+
if (fb2)
|
|
1337
|
+
return `@@fb@@${fb2[1]}@@${fb2[2]}`;
|
|
1338
|
+
return out;
|
|
1339
|
+
}
|
|
853
1340
|
function textOf(n) {
|
|
854
1341
|
return n?.content ?? '';
|
|
855
1342
|
}
|
|
@@ -1017,6 +1504,22 @@ function walkElement(node, classProps, comboRules, scopedRules, pseudoRules, war
|
|
|
1017
1504
|
], ancestorClasses);
|
|
1018
1505
|
return { type: 'for', vfor: vf, children: [item] };
|
|
1019
1506
|
}
|
|
1507
|
+
// R76 Vue 内置容器组件(Transition/KeepAlive/Teleport/Suspense/Fragment)→ 透明展开:
|
|
1508
|
+
// 原生端无过渡动画/缓存语义,但内容必须保留(否则整页编译失败)
|
|
1509
|
+
if (['Transition', 'TransitionGroup', 'KeepAlive', 'Teleport', 'Suspense', 'Fragment'].includes(node.tag)) {
|
|
1510
|
+
const kids = [];
|
|
1511
|
+
for (const child of node.children) {
|
|
1512
|
+
kids.push(...walk(child, classProps, comboRules, scopedRules, pseudoRules, warnings, state, compTable, propRefs, loopScope, ancestorClasses));
|
|
1513
|
+
}
|
|
1514
|
+
if (kids.length === 0) {
|
|
1515
|
+
warnings.push(`<${node.tag}> 内无内容,已忽略`);
|
|
1516
|
+
return { type: 'group', props: { flexDirection: 'column' }, children: [{ type: 'text', props: { text: ' ' } }] };
|
|
1517
|
+
}
|
|
1518
|
+
warnings.push(`<${node.tag}> 已透明展开(原生端无过渡/缓存语义,仅保留内容,R76)`);
|
|
1519
|
+
if (kids.length === 1)
|
|
1520
|
+
return kids[0];
|
|
1521
|
+
return { type: 'group', props: { flexDirection: 'column' }, children: kids };
|
|
1522
|
+
}
|
|
1020
1523
|
// <template>:逻辑容器(v-if 条件渲染 / 纯透传),不产生 UI 节点
|
|
1021
1524
|
if (node.tag === 'template') {
|
|
1022
1525
|
const children = [];
|
|
@@ -1142,7 +1645,7 @@ function walkElement(node, classProps, comboRules, scopedRules, pseudoRules, war
|
|
|
1142
1645
|
// @click="handler" → 状态赋值(setter 内联)优先;否则保留 onClick(JS 运行时调用)
|
|
1143
1646
|
// M3 规范化:无参调用 `inc()` → `inc`(Swift invoke 统一 evaluateScript("<名>()"));带参保留原样
|
|
1144
1647
|
if (event === 'click' && handler) {
|
|
1145
|
-
const set = state && resolveSetCall(handler, state);
|
|
1648
|
+
const set = state && resolveSetCall(handler, state, (m) => warnings.push(m));
|
|
1146
1649
|
if (set) {
|
|
1147
1650
|
Object.assign(props, { onSet: set });
|
|
1148
1651
|
}
|
|
@@ -1229,13 +1732,13 @@ function walkElement(node, classProps, comboRules, scopedRules, pseudoRules, war
|
|
|
1229
1732
|
parts.push({ text: child.content });
|
|
1230
1733
|
}
|
|
1231
1734
|
else if (child.type === NodeTypes.INTERPOLATION) {
|
|
1232
|
-
const name = textOf(child.content).trim();
|
|
1735
|
+
const name = flattenStorePath(normalizeTemplateExpr(textOf(child.content).trim(), warnings));
|
|
1233
1736
|
// 动态列表项属性 {{ item.field }}:基名(dot[0])命中循环变量才成立
|
|
1234
1737
|
const dot = name.split('.');
|
|
1235
1738
|
const base = dot[0];
|
|
1236
1739
|
const loopVar = loopScope.find((l) => l.itemKey === base || l.indexKey === base);
|
|
1237
1740
|
const dyn = loopVar && dot.length === 2 && dot[0] === loopVar.itemKey && /^\w+$/.test(dot[1]);
|
|
1238
|
-
if (!text && !parts.some((p) => 'bind' in p || 'dynField' in p)) {
|
|
1741
|
+
if (!text && !parts.some((p) => 'bind' in p || 'dynField' in p || 'fieldOf' in p)) {
|
|
1239
1742
|
// M2 模板内三元(D1 小范围):cond ? 'a' : 'b' → Swift 三元字符串表达式(Text(expr))
|
|
1240
1743
|
const tri = parseTernaryExpr(name);
|
|
1241
1744
|
if (tri) {
|
|
@@ -1247,20 +1750,51 @@ function walkElement(node, classProps, comboRules, scopedRules, pseudoRules, war
|
|
|
1247
1750
|
parts.push({ dynField: { var: dot[0], field: dot[1] } });
|
|
1248
1751
|
continue;
|
|
1249
1752
|
}
|
|
1753
|
+
if (name.startsWith('\u0000store:')) {
|
|
1754
|
+
warnings.push(`插值 {{ ${name.slice(7)} }} 是 Pinia store 的 getter/action,R86 目前只折叠 state 字段 → 已生成占位文本`);
|
|
1755
|
+
parts.push({ text: '\u200B' });
|
|
1756
|
+
continue;
|
|
1757
|
+
}
|
|
1250
1758
|
// M2:v-for 循环变量(item/index)优先于状态表
|
|
1251
1759
|
if (/^\w+$/.test(name)) {
|
|
1252
1760
|
// 纯单插值:组件 View 内 → ref(Text(prop));循环变量/页面状态 → bind
|
|
1253
1761
|
if (propRefs) {
|
|
1254
1762
|
refOnly = name;
|
|
1763
|
+
// 后面可能还有静态文本 → 同步进 parts,否则最终会走 props.text 分支把插值整段丢掉
|
|
1764
|
+
parts.push({ bind: name });
|
|
1255
1765
|
}
|
|
1256
1766
|
else if (loopVar || (state && state.states.some((s) => s.name === name))) {
|
|
1257
1767
|
refOnly = name;
|
|
1768
|
+
parts.push({ bind: name });
|
|
1258
1769
|
}
|
|
1259
1770
|
else {
|
|
1260
1771
|
throw new VueCompileError(`插值 {{ ${name} }}:未在 <script setup> 中声明为 ref 状态(M1 白名单:仅支持已声明状态)`);
|
|
1261
1772
|
}
|
|
1262
1773
|
continue;
|
|
1263
1774
|
}
|
|
1775
|
+
// R92 点路径兜底(元素文本分支)
|
|
1776
|
+
{
|
|
1777
|
+
const fbm2 = name.match(/^@@fb@@([^@]+)@@([\s\S]*)$/);
|
|
1778
|
+
if (fbm2) {
|
|
1779
|
+
const p3 = matchFieldPath(flattenStorePath(fbm2[1]));
|
|
1780
|
+
if (p3 && (state?.states ?? []).some((st) => st.name === p3.obj)) {
|
|
1781
|
+
parts.push({ fieldOf: { obj: p3.obj, field: p3.field, fallback: fbm2[2] } });
|
|
1782
|
+
continue;
|
|
1783
|
+
}
|
|
1784
|
+
}
|
|
1785
|
+
}
|
|
1786
|
+
// R78/R85 对象字段插值 {{ obj.field }} / {{ obj.a.b }} / {{ obj.list.length }}(单插值)
|
|
1787
|
+
const fld2 = matchFieldPath(name);
|
|
1788
|
+
if (fld2) {
|
|
1789
|
+
const isState2 = (state?.states ?? []).some((st) => st.name === fld2.obj) || (propRefs?.has(fld2.obj) ?? false);
|
|
1790
|
+
if (isState2) {
|
|
1791
|
+
parts.push({ fieldOf: { obj: fld2.obj, field: fld2.field } });
|
|
1792
|
+
continue;
|
|
1793
|
+
}
|
|
1794
|
+
warnings.push(`插值 {{ ${name} }} 的对象「${fld2.obj}」不是页面状态(如 Pinia store)→ 已生成占位文本;请把该字段提升为 ref(R78/R85)`);
|
|
1795
|
+
parts.push({ text: ' ' });
|
|
1796
|
+
continue;
|
|
1797
|
+
}
|
|
1264
1798
|
throw new VueCompileError(`插值表达式 {{ ${name} }} 暂不支持(M1 仅支持纯状态名;三元/比较表达式 M2 白名单;动态列表项属性 M4b)`);
|
|
1265
1799
|
}
|
|
1266
1800
|
// 混合段:静态文本之后的插值
|
|
@@ -1270,8 +1804,16 @@ function walkElement(node, classProps, comboRules, scopedRules, pseudoRules, war
|
|
|
1270
1804
|
parts.push({ bind: name });
|
|
1271
1805
|
else if (propRefs)
|
|
1272
1806
|
parts.push({ bind: name });
|
|
1273
|
-
else
|
|
1274
|
-
|
|
1807
|
+
else {
|
|
1808
|
+
// R85 混合文本内的成员链插值:`共 {{ board.month.label }} 条` / `{{ board.tags.length }}`
|
|
1809
|
+
const mp = matchFieldPath(name);
|
|
1810
|
+
if (mp && (state?.states ?? []).some((st) => st.name === mp.obj)) {
|
|
1811
|
+
parts.push({ fieldOf: { obj: mp.obj, field: mp.field } });
|
|
1812
|
+
}
|
|
1813
|
+
else {
|
|
1814
|
+
throw new VueCompileError(`混合文本内插值 {{ ${name} }} 暂不支持(仅支持已声明状态 / 循环变量 / 动态列表项属性 / 已声明对象的点路径 R85)`);
|
|
1815
|
+
}
|
|
1816
|
+
}
|
|
1275
1817
|
}
|
|
1276
1818
|
else if (child.type === NodeTypes.COMMENT) {
|
|
1277
1819
|
// 忽略注释
|
|
@@ -1292,9 +1834,13 @@ function walkElement(node, classProps, comboRules, scopedRules, pseudoRules, war
|
|
|
1292
1834
|
props.bind = refOnly;
|
|
1293
1835
|
}
|
|
1294
1836
|
}
|
|
1295
|
-
else if (parts.length > 1 || (parts.length === 1 && ('bind' in parts[0] || 'dynField' in parts[0]))) {
|
|
1837
|
+
else if (parts.length > 1 || (parts.length === 1 && ('bind' in parts[0] || 'dynField' in parts[0] || 'fieldOf' in parts[0]))) {
|
|
1296
1838
|
props.bindParts = parts;
|
|
1297
1839
|
}
|
|
1840
|
+
else if (parts.length === 1 && 'text' in parts[0] && !text.trim()) {
|
|
1841
|
+
// 只有占位文本(如不可折叠的 store getter/action):渲染空文本而不是报错
|
|
1842
|
+
props.text = parts[0].text;
|
|
1843
|
+
}
|
|
1298
1844
|
else {
|
|
1299
1845
|
const t = text.trim();
|
|
1300
1846
|
if (!t) {
|
|
@@ -1326,9 +1872,30 @@ function walkElement(node, classProps, comboRules, scopedRules, pseudoRules, war
|
|
|
1326
1872
|
children.push(...walk(child, classProps, comboRules, scopedRules, pseudoRules, warnings, state, compTable, propRefs, loopScope, [...ancestorClasses, ...elementClasses]));
|
|
1327
1873
|
}
|
|
1328
1874
|
}
|
|
1329
|
-
//
|
|
1875
|
+
// R88 续:容器节点不接受文本专属样式(color/fontSize/fontWeight/textAlign/lineHeight)
|
|
1876
|
+
// 原生端容器没有「字体继承」,这类属性会直接触发 IR 校验拒收 → 明确降级 + 警告
|
|
1877
|
+
if (mapping.type === 'group' || mapping.type === 'for') {
|
|
1878
|
+
const TEXT_ONLY = ['color', 'fontSize', 'fontWeight', 'textAlign', 'lineHeight'];
|
|
1879
|
+
const dropped = TEXT_ONLY.filter((k) => props[k] !== undefined);
|
|
1880
|
+
if (dropped.length > 0) {
|
|
1881
|
+
for (const k of dropped)
|
|
1882
|
+
delete props[k];
|
|
1883
|
+
warnings.push(`<${node.tag}> 是容器,文本样式(${dropped.join('/')})在原生端不继承 → 已忽略;请把样式下沉到具体文本节点(R88)`);
|
|
1884
|
+
}
|
|
1885
|
+
}
|
|
1886
|
+
// R77 空容器(<div class="spacer" /> 等占位写法)→ 降级为 spacer 占位 + 警告,而不是中断整个编译
|
|
1330
1887
|
if (mapping.type === 'group' && children.length === 0) {
|
|
1331
|
-
|
|
1888
|
+
warnings.push(`<${node.tag}> 无子节点 → 降级为占位 spacer(R17/R77;可用 padding/height 表达留白)`);
|
|
1889
|
+
const keep = {};
|
|
1890
|
+
const src = props;
|
|
1891
|
+
for (const k of ['width', 'height', 'flex', 'marginTop', 'background']) {
|
|
1892
|
+
if (src[k] !== undefined)
|
|
1893
|
+
keep[k] = src[k];
|
|
1894
|
+
}
|
|
1895
|
+
const spacer = { type: 'spacer', props: keep };
|
|
1896
|
+
if (cond)
|
|
1897
|
+
spacer.condition = normCond(cond);
|
|
1898
|
+
return spacer;
|
|
1332
1899
|
}
|
|
1333
1900
|
// overflow:auto → scroll 节点(消费中转标记,避免进入 schema 校验白名单)
|
|
1334
1901
|
const propsAny = props;
|
|
@@ -1339,8 +1906,14 @@ function walkElement(node, classProps, comboRules, scopedRules, pseudoRules, war
|
|
|
1339
1906
|
const irNode = { type: (nodeType ?? mapping.type), props };
|
|
1340
1907
|
if (children.length > 0)
|
|
1341
1908
|
irNode.children = children;
|
|
1342
|
-
if (cond)
|
|
1343
|
-
|
|
1909
|
+
if (cond) {
|
|
1910
|
+
const nc = normCond(cond);
|
|
1911
|
+
// R78 续(G12):条件里的对象字段(v-if="dialog.title")→ 标记,由 codegen 映射为「非空判断」
|
|
1912
|
+
const mCond = nc.match(/^([A-Za-z_$][\w$]*)\.([A-Za-z_$][\w$]*)$/);
|
|
1913
|
+
const ownerKnown = mCond !== null &&
|
|
1914
|
+
((state?.states ?? []).some((st) => st.name === mCond[1]) || (propRefs?.has(mCond[1]) ?? false));
|
|
1915
|
+
irNode.condition = ownerKnown && mCond ? `__vrnField__:${mCond[1]}:${mCond[2]}` : nc;
|
|
1916
|
+
}
|
|
1344
1917
|
if (activeWhen && activeProps) {
|
|
1345
1918
|
irNode.active = { when: activeWhen, props: activeProps };
|
|
1346
1919
|
// active 的 color 注入所有后代 text(选中态金色覆盖子元素显式色)
|