@dongmingzhixiu/vuexir-compiler 0.4.0 → 0.4.2
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.js +7 -0
- package/dist/codegen/swiftui.js +10 -1
- package/dist/transform/vue2ir.js +93 -9
- package/package.json +1 -1
package/dist/codegen/compose.js
CHANGED
|
@@ -474,6 +474,13 @@ function kotlinCondExpr(expr, ctx, loop) {
|
|
|
474
474
|
const cmpField = s.match(/^([A-Za-z_$][\w$]*)\.([A-Za-z_$][\w$]*)\s*(==|!=)\s*(.+)$/);
|
|
475
475
|
if (cmpField)
|
|
476
476
|
return `vrnField(${cmpField[1]}, "${cmpField[2]}") ${cmpField[3]} ${cmpField[4]}`;
|
|
477
|
+
// R121:复合条件逐段翻译
|
|
478
|
+
if (/&&|\|\|/.test(s) && !/[()]/.test(s)) {
|
|
479
|
+
const parts = s.split(/(&&|\|\|)/);
|
|
480
|
+
return parts
|
|
481
|
+
.map((seg) => (seg === '&&' || seg === '||' ? seg : kotlinCondExpr(seg.trim(), ctx, loop)))
|
|
482
|
+
.join(' ');
|
|
483
|
+
}
|
|
477
484
|
const andExpr = s.match(/^([A-Za-z_$][\w$.]*)\s*&&\s*([A-Za-z_$][\w$.]*)$/);
|
|
478
485
|
if (andExpr) {
|
|
479
486
|
const truthy = (v) => v.includes('.') ? `vrnField(${v.split('.')[0]}, "${v.split('.').slice(1).join('.')}").isNotEmpty()` : `${v}.toString().isNotEmpty()`;
|
package/dist/codegen/swiftui.js
CHANGED
|
@@ -539,6 +539,15 @@ function swiftCondExpr(expr, known) {
|
|
|
539
539
|
// R110:数值状态直接当条件(Swift 不接受 Int 当 Bool)
|
|
540
540
|
if (/^[A-Za-z_$][\w$]*$/.test(s))
|
|
541
541
|
return `vrnTruthy(String(${s}))`;
|
|
542
|
+
// R121:复合条件(a && b / a || b / !a)→ 逐段翻译后重组(每段按单条件规则判真)
|
|
543
|
+
if (/&&|\|\|/.test(s) && !/__vrnField__/.test(s) && !/[()]/.test(s)) {
|
|
544
|
+
const parts = s.split(/(&&|\|\|)/);
|
|
545
|
+
return parts
|
|
546
|
+
.map((seg) => (seg === '&&' || seg === '||' ? seg : swiftCondExpr(seg.trim(), known)))
|
|
547
|
+
.join(' ');
|
|
548
|
+
}
|
|
549
|
+
if (/^![A-Za-z_$][\w$]*$/.test(s))
|
|
550
|
+
return `!vrnTruthy(String(${s.slice(1)}))`;
|
|
542
551
|
// R113:对象字段条件(form.avatarData)→ 走 vrnField 取 JSON 字段
|
|
543
552
|
const negField = s.match(/^!([A-Za-z_$][\w$]*)\.([A-Za-z_$][\w$]*)$/);
|
|
544
553
|
if (negField)
|
|
@@ -801,7 +810,7 @@ function emitNodeInner(node, ctx) {
|
|
|
801
810
|
let line = isRef
|
|
802
811
|
? pad(`Text(${props.ref})`)
|
|
803
812
|
: props.bindExpr !== undefined
|
|
804
|
-
? pad(`Text(${props.bindExpr})`)
|
|
813
|
+
? pad(`Text(${String(props.bindExpr).includes('.') ? '""' : props.bindExpr})`)
|
|
805
814
|
: props.fieldOf !== undefined
|
|
806
815
|
? pad(`Text(vrnField(${props.fieldOf.obj}, "${props.fieldOf.field}"${props.fieldOf.fallback !== undefined ? `, "${swiftStringEscape(props.fieldOf.fallback)}"` : ''}))`)
|
|
807
816
|
: props.dynField !== undefined
|
package/dist/transform/vue2ir.js
CHANGED
|
@@ -41,6 +41,8 @@ function addJsExprState(expr) {
|
|
|
41
41
|
jsExprStates.push({ name, init: '', computed: js });
|
|
42
42
|
return name;
|
|
43
43
|
}
|
|
44
|
+
/** R125 当前生效的 v-for 作用域栈(绕开 loopScope 透传不一致的问题) */
|
|
45
|
+
let activeLoops = [];
|
|
44
46
|
/** R115 是否允许未注册组件降级(由 vueToIr 选项设置) */
|
|
45
47
|
let allowUnknownComponentsFlag = false;
|
|
46
48
|
/** R104 脚本里声明过的名字(const/let/function)——未登记为 state 时走运行时求值 */
|
|
@@ -49,6 +51,10 @@ let scriptDeclaredNames = new Set();
|
|
|
49
51
|
let storeAliases = new Map();
|
|
50
52
|
/** R86 把 `alias.field.path` 拍平为 `alias_field.path`;getter/action 返回哨兵串 */
|
|
51
53
|
function flattenStorePath(name) {
|
|
54
|
+
// R123:页面脚本自己的响应式绑定(soon/nearest/…)由运行时同步填充,
|
|
55
|
+
// 绝不能再被 store 折叠改写成 store_xxx(否则 v-for 会去读静态状态,永远是空)
|
|
56
|
+
if (jsExprStateNames.includes(name))
|
|
57
|
+
return name;
|
|
52
58
|
// R89:computed 折叠出的派生名(all → ui_upcoming;all.length → ui_upcoming.length)
|
|
53
59
|
if (derivedNames.size > 0) {
|
|
54
60
|
const m0 = name.match(/^([A-Za-z_$][\w$]*)(\..+)?$/);
|
|
@@ -323,6 +329,8 @@ __vrnRecompute();
|
|
|
323
329
|
var __n = 0;
|
|
324
330
|
function __tick() {
|
|
325
331
|
__vrnRecompute();
|
|
332
|
+
// R122:页面脚本(__vrnPage)的真实值必须最后写入,否则会被静态 computed 覆盖成空
|
|
333
|
+
__vrnSyncPage();
|
|
326
334
|
if (++__n < 20 && typeof setTimeout === "function") setTimeout(__tick, 100);
|
|
327
335
|
}
|
|
328
336
|
__tick();
|
|
@@ -332,7 +340,7 @@ __vrnRecompute();
|
|
|
332
340
|
// M4b 动态列表态:list → JS 真数组('[]' 或 '["a","b"]' JSON 字面量,Swift JSONSerialization 可解码)
|
|
333
341
|
// R98:对象态在 JS 侧要是真对象(原生侧仍收 JSON 字符串),否则 {{ form.name }} 求值失败
|
|
334
342
|
const v = s.obj && typeof s.init === 'string'
|
|
335
|
-
? `JSON.parse(${JSON.stringify(s.init)})`
|
|
343
|
+
? `(function(){try{return JSON.parse(${JSON.stringify(s.init)});}catch(e){return {};}})()`
|
|
336
344
|
: s.list
|
|
337
345
|
? String(s.init)
|
|
338
346
|
: s.init === '[]'
|
|
@@ -470,6 +478,53 @@ var router = {
|
|
|
470
478
|
if (typeof __routerBack === "function") __routerBack();
|
|
471
479
|
}
|
|
472
480
|
};
|
|
481
|
+
// R118 页面状态同步:把页面脚本 bundle(__vrnPage)里的 ref/computed 值写进 stateStore,
|
|
482
|
+
// 模板里 {{ nearest }} / {{ soon }} 这类页面级派生值就有了真实来源
|
|
483
|
+
// 把数组里的嵌套对象拍平成「点键」,让 v-for 里的 row.item.name 能直接取到
|
|
484
|
+
function __vrnFlat(v) {
|
|
485
|
+
function flatObj(o, prefix, out) {
|
|
486
|
+
for (var k in o) {
|
|
487
|
+
var val = o[k];
|
|
488
|
+
var key = prefix ? prefix + "." + k : k;
|
|
489
|
+
if (val && typeof val === "object" && !Array.isArray(val)) flatObj(val, key, out);
|
|
490
|
+
else if (Array.isArray(val)) out[key] = "[" + val.length + "]";
|
|
491
|
+
else out[key] = String(val);
|
|
492
|
+
}
|
|
493
|
+
return out;
|
|
494
|
+
}
|
|
495
|
+
if (Array.isArray(v)) return v.map(function (it) { return (it && typeof it === "object") ? flatObj(it, "", {}) : it; });
|
|
496
|
+
return v;
|
|
497
|
+
}
|
|
498
|
+
function __vrnSyncPage() {
|
|
499
|
+
try {
|
|
500
|
+
var page = typeof __vrnPage !== "undefined" ? __vrnPage : (typeof globalThis !== "undefined" ? globalThis.__vrnPage : undefined);
|
|
501
|
+
if (!page) return;
|
|
502
|
+
for (var k in page) {
|
|
503
|
+
try {
|
|
504
|
+
var v = page[k];
|
|
505
|
+
var val;
|
|
506
|
+
if (v && typeof v === "object" && "value" in v) val = v.value; // ref / computed
|
|
507
|
+
else continue; // 函数/常量交给模板静态值
|
|
508
|
+
if (val === undefined || val === null) continue;
|
|
509
|
+
stateStore[k] = (typeof val === "object") ? JSON.stringify(__vrnFlat(val)) : val;
|
|
510
|
+
} catch (e) {}
|
|
511
|
+
}
|
|
512
|
+
} catch (e) {}
|
|
513
|
+
}
|
|
514
|
+
// R116:store 引导——原生端没有 main.ts 的启动序列,这里对所有 useXxxStore 单例触发 init()
|
|
515
|
+
(function () {
|
|
516
|
+
try {
|
|
517
|
+
var names = Object.getOwnPropertyNames(globalThis).filter(function (n) {
|
|
518
|
+
return /^use[A-Za-z0-9_$]*Store$/.test(n) && typeof globalThis[n] === "function";
|
|
519
|
+
});
|
|
520
|
+
names.forEach(function (n) {
|
|
521
|
+
try {
|
|
522
|
+
var st = globalThis[n]();
|
|
523
|
+
if (st && typeof st.init === "function") st.init();
|
|
524
|
+
} catch (e) {}
|
|
525
|
+
});
|
|
526
|
+
} catch (e) {}
|
|
527
|
+
})();
|
|
473
528
|
// handlers(<script setup> 函数体:ref.value → stateStore 重写)
|
|
474
529
|
${handlerSrc}
|
|
475
530
|
${computedBlock}${"" /* R102 见 computedBlock 内:仅当存在派生状态时才调度重算循环,避免无谓的定时器 */}// onMounted(Swift onAppear → invoke("__onMounted"))
|
|
@@ -926,6 +981,7 @@ export function vueToIr(source, opts = {}) {
|
|
|
926
981
|
jsExprSeq = 0;
|
|
927
982
|
jsExprStates = [];
|
|
928
983
|
allowUnknownComponentsFlag = opts.allowUnknownComponents === true;
|
|
984
|
+
activeLoops = [];
|
|
929
985
|
const warnings = [];
|
|
930
986
|
const { descriptor, errors } = parse(source, { filename: 'Screen.vue' });
|
|
931
987
|
if (errors.length > 0) {
|
|
@@ -1117,6 +1173,23 @@ export function vueToIr(source, opts = {}) {
|
|
|
1117
1173
|
if (value !== undefined)
|
|
1118
1174
|
stateInfo.states.push({ name, init: value });
|
|
1119
1175
|
}
|
|
1176
|
+
// R121:页面脚本里的 computed/普通绑定在运行时由 __vrnSyncPage 回填,
|
|
1177
|
+
// 这里先注册为状态(init 空串),模板的 v-for / v-if 就能引用它们
|
|
1178
|
+
{
|
|
1179
|
+
const RESERVED2 = new Set(['body', 'self', 'init', 'class', 'object', 'fun', 'val', 'var', 'when', 'is', 'in', 'store']);
|
|
1180
|
+
// 只注册 computed/ref/reactive 绑定(普通常量数组已由代码生成侧处理,重复注册会撞名)
|
|
1181
|
+
const reactiveBindings = new Set();
|
|
1182
|
+
for (const m2 of src.matchAll(/const\s+([A-Za-z_$][\w$]*)\s*=\s*(?:computed|ref|reactive|shallowRef|toRef)\s*\(/g)) {
|
|
1183
|
+
reactiveBindings.add(m2[1]);
|
|
1184
|
+
}
|
|
1185
|
+
for (const nm of reactiveBindings) {
|
|
1186
|
+
if (RESERVED2.has(nm))
|
|
1187
|
+
continue;
|
|
1188
|
+
if (stateInfo.states.some((st) => st.name === nm))
|
|
1189
|
+
continue;
|
|
1190
|
+
stateInfo.states.push({ name: nm, init: '' });
|
|
1191
|
+
}
|
|
1192
|
+
}
|
|
1120
1193
|
// R98:把当前状态名快照给表达式下沉用(walk 阶段据此把裸状态名重写成 stateStore.x)
|
|
1121
1194
|
jsExprStateNames = stateInfo.states.map((st) => st.name);
|
|
1122
1195
|
// R74:状态名撞原生保留成员(SwiftUI `body`、Kotlin 关键字)→ 自动改名 + 警告(不再只警告导致后续编译失败)
|
|
@@ -1347,12 +1420,18 @@ function walk(node, classProps, comboRules, scopedRules, pseudoRules, warnings,
|
|
|
1347
1420
|
if (tri)
|
|
1348
1421
|
return [{ type: 'text', props: { bindExpr: tri } }];
|
|
1349
1422
|
// M2:v-for 循环变量(item/index)优先于状态表
|
|
1350
|
-
const loopVar = loopScope.find((l) => l.itemKey === name || l.indexKey === name)
|
|
1423
|
+
const loopVar = loopScope.find((l) => l.itemKey === name || l.indexKey === name) ??
|
|
1424
|
+
[...activeLoops].reverse().find((l) => l.itemKey === name.split('.')[0] || l.indexKey === name.split('.')[0]);
|
|
1351
1425
|
if (loopVar) {
|
|
1352
|
-
// M4b 动态列表项属性访问 {{ item.field }} → dynField
|
|
1426
|
+
// M4b 动态列表项属性访问 {{ item.field }} / item.a.b → dynField(点路径拍平键)
|
|
1353
1427
|
const dot = name.split('.');
|
|
1354
|
-
if (dot.length
|
|
1355
|
-
return [{ type: 'text', props: { dynField: { var: dot[0], field: dot
|
|
1428
|
+
if (dot.length >= 2 && dot[0] === loopVar.itemKey && dot.slice(1).every((seg) => /^\w+$/.test(seg))) {
|
|
1429
|
+
return [{ type: 'text', props: { dynField: { var: dot[0], field: dot.slice(1).join('.') } } }];
|
|
1430
|
+
}
|
|
1431
|
+
// R126:循环体内出现复杂表达式(三元等)→ 原生端无法静态求值,置空避免非法代码
|
|
1432
|
+
if (!/^[A-Za-z_$][\w$]*(\.[A-Za-z_$][\w$]*)*$/.test(name)) {
|
|
1433
|
+
warnings.push(`v-for 项内复杂表达式「${name.slice(0, 30)}」暂不支持 → 已置空(R126)`);
|
|
1434
|
+
return [{ type: 'text', props: { text: ' ' } }];
|
|
1356
1435
|
}
|
|
1357
1436
|
return [{ type: 'text', bind: { key: name } }];
|
|
1358
1437
|
}
|
|
@@ -1637,10 +1716,12 @@ function walkElement(node, classProps, comboRules, scopedRules, pseudoRules, war
|
|
|
1637
1716
|
...node,
|
|
1638
1717
|
props: node.props.filter((p) => !(p.type === NodeTypes.DIRECTIVE && p.name === 'for')),
|
|
1639
1718
|
};
|
|
1719
|
+
activeLoops.push({ itemKey: vfSunk.itemKey, indexKey: vfSunk.indexKey });
|
|
1640
1720
|
const itemSunk = walkElement(innerNodeSunk, classProps, comboRules, scopedRules, pseudoRules, warnings, state, compTable, propRefs, [
|
|
1641
1721
|
...loopScope,
|
|
1642
1722
|
{ itemKey: vfSunk.itemKey, indexKey: vfSunk.indexKey },
|
|
1643
1723
|
], ancestorClasses);
|
|
1724
|
+
activeLoops.pop();
|
|
1644
1725
|
return { type: 'for', vfor: vfSunk, children: [itemSunk] };
|
|
1645
1726
|
}
|
|
1646
1727
|
}
|
|
@@ -1676,10 +1757,12 @@ function walkElement(node, classProps, comboRules, scopedRules, pseudoRules, war
|
|
|
1676
1757
|
throw new VueCompileError(`v-for 动态数据源「${vf.listState}」未在 <script setup> 中声明为 ref 数组(M4b)`);
|
|
1677
1758
|
}
|
|
1678
1759
|
const innerNode = { ...node, props: node.props.filter((p) => !(p.type === NodeTypes.DIRECTIVE && p.name === 'for')) };
|
|
1760
|
+
activeLoops.push({ itemKey: vf.itemKey, indexKey: vf.indexKey });
|
|
1679
1761
|
const item = walkElement(innerNode, classProps, comboRules, scopedRules, pseudoRules, warnings, state, compTable, propRefs, [
|
|
1680
1762
|
...loopScope,
|
|
1681
1763
|
{ itemKey: vf.itemKey, indexKey: vf.indexKey },
|
|
1682
1764
|
], ancestorClasses);
|
|
1765
|
+
activeLoops.pop();
|
|
1683
1766
|
return { type: 'for', vfor: vf, children: [item] };
|
|
1684
1767
|
}
|
|
1685
1768
|
// R76 Vue 内置容器组件(Transition/KeepAlive/Teleport/Suspense/Fragment)→ 透明展开:
|
|
@@ -1930,8 +2013,9 @@ function walkElement(node, classProps, comboRules, scopedRules, pseudoRules, war
|
|
|
1930
2013
|
// 动态列表项属性 {{ item.field }}:基名(dot[0])命中循环变量才成立
|
|
1931
2014
|
const dot = name.split('.');
|
|
1932
2015
|
const base = dot[0];
|
|
1933
|
-
const loopVar = loopScope.find((l) => l.itemKey === base || l.indexKey === base)
|
|
1934
|
-
|
|
2016
|
+
const loopVar = loopScope.find((l) => l.itemKey === base || l.indexKey === base) ??
|
|
2017
|
+
[...activeLoops].reverse().find((l) => l.itemKey === base || l.indexKey === base);
|
|
2018
|
+
const dyn = loopVar && dot.length >= 2 && dot[0] === loopVar.itemKey && dot.slice(1).every((seg) => /^\w+$/.test(seg));
|
|
1935
2019
|
if (!text && !parts.some((p) => 'bind' in p || 'dynField' in p || 'fieldOf' in p)) {
|
|
1936
2020
|
// M2 模板内三元(D1 小范围):cond ? 'a' : 'b' → Swift 三元字符串表达式(Text(expr))
|
|
1937
2021
|
const tri = parseTernaryExpr(name);
|
|
@@ -1941,7 +2025,7 @@ function walkElement(node, classProps, comboRules, scopedRules, pseudoRules, war
|
|
|
1941
2025
|
}
|
|
1942
2026
|
// M4b 单插值动态列表项属性 {{ item.field }} → 单段 bindParts
|
|
1943
2027
|
if (dyn) {
|
|
1944
|
-
parts.push({ dynField: { var: dot[0], field: dot
|
|
2028
|
+
parts.push({ dynField: { var: dot[0], field: dot.slice(1).join('.') } });
|
|
1945
2029
|
continue;
|
|
1946
2030
|
}
|
|
1947
2031
|
if (name.startsWith('\u0000store:')) {
|
|
@@ -1999,7 +2083,7 @@ function walkElement(node, classProps, comboRules, scopedRules, pseudoRules, war
|
|
|
1999
2083
|
}
|
|
2000
2084
|
// 混合段:静态文本之后的插值
|
|
2001
2085
|
if (dyn)
|
|
2002
|
-
parts.push({ dynField: { var: dot[0], field: dot
|
|
2086
|
+
parts.push({ dynField: { var: dot[0], field: dot.slice(1).join('.') } });
|
|
2003
2087
|
else if (/^\w+$/.test(name) && (loopVar || (state && state.states.some((s) => s.name === name))))
|
|
2004
2088
|
parts.push({ bind: name });
|
|
2005
2089
|
else if (propRefs)
|