@dongmingzhixiu/vuexir-compiler 0.3.21 → 0.4.1
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
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()`;
|
|
@@ -1700,6 +1707,7 @@ function runTabs(appDir, opts = {}) {
|
|
|
1700
1707
|
title: tab.title,
|
|
1701
1708
|
components: loadComponents(tab.file),
|
|
1702
1709
|
storeSources: loadStoreSources(tab.file),
|
|
1710
|
+
allowUnknownComponents: true,
|
|
1703
1711
|
});
|
|
1704
1712
|
if (r.warnings.length > 0) {
|
|
1705
1713
|
console.log(`[gen-android] 编译提示(${tab.id},${r.warnings.length} 条):`);
|
|
@@ -1738,6 +1746,7 @@ function runTabs(appDir, opts = {}) {
|
|
|
1738
1746
|
title: d.title,
|
|
1739
1747
|
components: loadComponents(d.file),
|
|
1740
1748
|
storeSources: loadStoreSources(d.file),
|
|
1749
|
+
allowUnknownComponents: true,
|
|
1741
1750
|
});
|
|
1742
1751
|
if (r.warnings.length > 0) {
|
|
1743
1752
|
console.log(`[gen-android] 编译提示(${d.id},${r.warnings.length} 条):`);
|
package/dist/codegen/swiftui.js
CHANGED
|
@@ -64,6 +64,7 @@ function rgbHex(r, g, b) {
|
|
|
64
64
|
return [r, g, b].map((v) => Math.round(v).toString(16).padStart(2, '0')).join('');
|
|
65
65
|
}
|
|
66
66
|
/** 当前工程的色板(hex → Theme 名 / fontKey → Theme 名);generateScreenSource 设置 */
|
|
67
|
+
let currentStateNames = new Set();
|
|
67
68
|
let currentColorNames;
|
|
68
69
|
let currentFontNames;
|
|
69
70
|
/** R43:frame 组合抽离为 View 扩展方法(theme 模式启用) */
|
|
@@ -511,11 +512,12 @@ function emitNode(node, ctx) {
|
|
|
511
512
|
/** JS 条件表达式 → Swift 兼容:===/!== 为 JS 严格比较,Swift 用 ==/!=;字符串字面量单引号 → 双引号 */
|
|
512
513
|
function swiftCondExpr(expr, known) {
|
|
513
514
|
// R114:条件引用原生端不存在的标识符 → true(保留内容),避免整页编译失败
|
|
514
|
-
|
|
515
|
+
const knownSet = known && known.size > 0 ? known : currentStateNames;
|
|
516
|
+
if (knownSet && knownSet.size > 0) {
|
|
515
517
|
// 先剥掉字符串字面量,避免把 "success" 之类当成标识符
|
|
516
518
|
const stripped = expr.replace(/'[^']*'|"[^"]*"/g, '""');
|
|
517
519
|
const ids = [...stripped.matchAll(/(?<![.\w$])([A-Za-z_$][\w$]*)/g)].map((m) => m[1]);
|
|
518
|
-
const unknown = ids.filter((id) => !
|
|
520
|
+
const unknown = ids.filter((id) => !knownSet.has(id) && !['true', 'false', 'Math', 'String', 'Number', 'vrnTruthy'].includes(id));
|
|
519
521
|
if (unknown.length > 0)
|
|
520
522
|
return 'true';
|
|
521
523
|
}
|
|
@@ -537,6 +539,15 @@ function swiftCondExpr(expr, known) {
|
|
|
537
539
|
// R110:数值状态直接当条件(Swift 不接受 Int 当 Bool)
|
|
538
540
|
if (/^[A-Za-z_$][\w$]*$/.test(s))
|
|
539
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)}))`;
|
|
540
551
|
// R113:对象字段条件(form.avatarData)→ 走 vrnField 取 JSON 字段
|
|
541
552
|
const negField = s.match(/^!([A-Za-z_$][\w$]*)\.([A-Za-z_$][\w$]*)$/);
|
|
542
553
|
if (negField)
|
|
@@ -1127,6 +1138,7 @@ function navBarModifiers(screen, warnings) {
|
|
|
1127
1138
|
}
|
|
1128
1139
|
export function generateScreenSource(screen, opts = {}) {
|
|
1129
1140
|
const __stateNames = new Set((screen.state ?? []).map((st) => st.name));
|
|
1141
|
+
currentStateNames = __stateNames;
|
|
1130
1142
|
const rt = opts.runtime ?? 'Runtime.shared';
|
|
1131
1143
|
// R42:色板存在时颜色/字体走 Theme token;R43:frame 走 View 扩展短调用
|
|
1132
1144
|
currentColorNames = opts.theme?.colorNames;
|
|
@@ -65,6 +65,8 @@ export declare function resolveSetCall(handler: string, state: ScreenStateInfo,
|
|
|
65
65
|
export interface Vue2IrOptions {
|
|
66
66
|
/** R86 Pinia store 源码(文件名 → 源码);提供后页面可用 `const ui = useUiStore()` 读状态 */
|
|
67
67
|
storeSources?: Record<string, string>;
|
|
68
|
+
/** R115:允许未注册的自定义组件降级为占位(工程迁移用;库默认仍按 R13 报错) */
|
|
69
|
+
allowUnknownComponents?: boolean;
|
|
68
70
|
screenId?: string;
|
|
69
71
|
title?: string;
|
|
70
72
|
/** 设计稿高度(默认 640,R16 适配基准) */
|
package/dist/transform/vue2ir.js
CHANGED
|
@@ -41,12 +41,18 @@ function addJsExprState(expr) {
|
|
|
41
41
|
jsExprStates.push({ name, init: '', computed: js });
|
|
42
42
|
return name;
|
|
43
43
|
}
|
|
44
|
+
/** R115 是否允许未注册组件降级(由 vueToIr 选项设置) */
|
|
45
|
+
let allowUnknownComponentsFlag = false;
|
|
44
46
|
/** R104 脚本里声明过的名字(const/let/function)——未登记为 state 时走运行时求值 */
|
|
45
47
|
let scriptDeclaredNames = new Set();
|
|
46
48
|
/** R86 store 别名表:`ui` → StoreDef(每次 vueToIr 重建;与 vforConsts 同风格) */
|
|
47
49
|
let storeAliases = new Map();
|
|
48
50
|
/** R86 把 `alias.field.path` 拍平为 `alias_field.path`;getter/action 返回哨兵串 */
|
|
49
51
|
function flattenStorePath(name) {
|
|
52
|
+
// R123:页面脚本自己的响应式绑定(soon/nearest/…)由运行时同步填充,
|
|
53
|
+
// 绝不能再被 store 折叠改写成 store_xxx(否则 v-for 会去读静态状态,永远是空)
|
|
54
|
+
if (jsExprStateNames.includes(name))
|
|
55
|
+
return name;
|
|
50
56
|
// R89:computed 折叠出的派生名(all → ui_upcoming;all.length → ui_upcoming.length)
|
|
51
57
|
if (derivedNames.size > 0) {
|
|
52
58
|
const m0 = name.match(/^([A-Za-z_$][\w$]*)(\..+)?$/);
|
|
@@ -321,6 +327,8 @@ __vrnRecompute();
|
|
|
321
327
|
var __n = 0;
|
|
322
328
|
function __tick() {
|
|
323
329
|
__vrnRecompute();
|
|
330
|
+
// R122:页面脚本(__vrnPage)的真实值必须最后写入,否则会被静态 computed 覆盖成空
|
|
331
|
+
__vrnSyncPage();
|
|
324
332
|
if (++__n < 20 && typeof setTimeout === "function") setTimeout(__tick, 100);
|
|
325
333
|
}
|
|
326
334
|
__tick();
|
|
@@ -330,7 +338,7 @@ __vrnRecompute();
|
|
|
330
338
|
// M4b 动态列表态:list → JS 真数组('[]' 或 '["a","b"]' JSON 字面量,Swift JSONSerialization 可解码)
|
|
331
339
|
// R98:对象态在 JS 侧要是真对象(原生侧仍收 JSON 字符串),否则 {{ form.name }} 求值失败
|
|
332
340
|
const v = s.obj && typeof s.init === 'string'
|
|
333
|
-
? `JSON.parse(${JSON.stringify(s.init)})`
|
|
341
|
+
? `(function(){try{return JSON.parse(${JSON.stringify(s.init)});}catch(e){return {};}})()`
|
|
334
342
|
: s.list
|
|
335
343
|
? String(s.init)
|
|
336
344
|
: s.init === '[]'
|
|
@@ -468,6 +476,53 @@ var router = {
|
|
|
468
476
|
if (typeof __routerBack === "function") __routerBack();
|
|
469
477
|
}
|
|
470
478
|
};
|
|
479
|
+
// R118 页面状态同步:把页面脚本 bundle(__vrnPage)里的 ref/computed 值写进 stateStore,
|
|
480
|
+
// 模板里 {{ nearest }} / {{ soon }} 这类页面级派生值就有了真实来源
|
|
481
|
+
// 把数组里的嵌套对象拍平成「点键」,让 v-for 里的 row.item.name 能直接取到
|
|
482
|
+
function __vrnFlat(v) {
|
|
483
|
+
function flatObj(o, prefix, out) {
|
|
484
|
+
for (var k in o) {
|
|
485
|
+
var val = o[k];
|
|
486
|
+
var key = prefix ? prefix + "." + k : k;
|
|
487
|
+
if (val && typeof val === "object" && !Array.isArray(val)) flatObj(val, key, out);
|
|
488
|
+
else if (Array.isArray(val)) out[key] = "[" + val.length + "]";
|
|
489
|
+
else out[key] = String(val);
|
|
490
|
+
}
|
|
491
|
+
return out;
|
|
492
|
+
}
|
|
493
|
+
if (Array.isArray(v)) return v.map(function (it) { return (it && typeof it === "object") ? flatObj(it, "", {}) : it; });
|
|
494
|
+
return v;
|
|
495
|
+
}
|
|
496
|
+
function __vrnSyncPage() {
|
|
497
|
+
try {
|
|
498
|
+
var page = typeof __vrnPage !== "undefined" ? __vrnPage : (typeof globalThis !== "undefined" ? globalThis.__vrnPage : undefined);
|
|
499
|
+
if (!page) return;
|
|
500
|
+
for (var k in page) {
|
|
501
|
+
try {
|
|
502
|
+
var v = page[k];
|
|
503
|
+
var val;
|
|
504
|
+
if (v && typeof v === "object" && "value" in v) val = v.value; // ref / computed
|
|
505
|
+
else continue; // 函数/常量交给模板静态值
|
|
506
|
+
if (val === undefined || val === null) continue;
|
|
507
|
+
stateStore[k] = (typeof val === "object") ? JSON.stringify(__vrnFlat(val)) : val;
|
|
508
|
+
} catch (e) {}
|
|
509
|
+
}
|
|
510
|
+
} catch (e) {}
|
|
511
|
+
}
|
|
512
|
+
// R116:store 引导——原生端没有 main.ts 的启动序列,这里对所有 useXxxStore 单例触发 init()
|
|
513
|
+
(function () {
|
|
514
|
+
try {
|
|
515
|
+
var names = Object.getOwnPropertyNames(globalThis).filter(function (n) {
|
|
516
|
+
return /^use[A-Za-z0-9_$]*Store$/.test(n) && typeof globalThis[n] === "function";
|
|
517
|
+
});
|
|
518
|
+
names.forEach(function (n) {
|
|
519
|
+
try {
|
|
520
|
+
var st = globalThis[n]();
|
|
521
|
+
if (st && typeof st.init === "function") st.init();
|
|
522
|
+
} catch (e) {}
|
|
523
|
+
});
|
|
524
|
+
} catch (e) {}
|
|
525
|
+
})();
|
|
471
526
|
// handlers(<script setup> 函数体:ref.value → stateStore 重写)
|
|
472
527
|
${handlerSrc}
|
|
473
528
|
${computedBlock}${"" /* R102 见 computedBlock 内:仅当存在派生状态时才调度重算循环,避免无谓的定时器 */}// onMounted(Swift onAppear → invoke("__onMounted"))
|
|
@@ -923,6 +978,7 @@ export function vueToIr(source, opts = {}) {
|
|
|
923
978
|
storeGetterWarnings = [];
|
|
924
979
|
jsExprSeq = 0;
|
|
925
980
|
jsExprStates = [];
|
|
981
|
+
allowUnknownComponentsFlag = opts.allowUnknownComponents === true;
|
|
926
982
|
const warnings = [];
|
|
927
983
|
const { descriptor, errors } = parse(source, { filename: 'Screen.vue' });
|
|
928
984
|
if (errors.length > 0) {
|
|
@@ -1114,6 +1170,23 @@ export function vueToIr(source, opts = {}) {
|
|
|
1114
1170
|
if (value !== undefined)
|
|
1115
1171
|
stateInfo.states.push({ name, init: value });
|
|
1116
1172
|
}
|
|
1173
|
+
// R121:页面脚本里的 computed/普通绑定在运行时由 __vrnSyncPage 回填,
|
|
1174
|
+
// 这里先注册为状态(init 空串),模板的 v-for / v-if 就能引用它们
|
|
1175
|
+
{
|
|
1176
|
+
const RESERVED2 = new Set(['body', 'self', 'init', 'class', 'object', 'fun', 'val', 'var', 'when', 'is', 'in', 'store']);
|
|
1177
|
+
// 只注册 computed/ref/reactive 绑定(普通常量数组已由代码生成侧处理,重复注册会撞名)
|
|
1178
|
+
const reactiveBindings = new Set();
|
|
1179
|
+
for (const m2 of src.matchAll(/const\s+([A-Za-z_$][\w$]*)\s*=\s*(?:computed|ref|reactive|shallowRef|toRef)\s*\(/g)) {
|
|
1180
|
+
reactiveBindings.add(m2[1]);
|
|
1181
|
+
}
|
|
1182
|
+
for (const nm of reactiveBindings) {
|
|
1183
|
+
if (RESERVED2.has(nm))
|
|
1184
|
+
continue;
|
|
1185
|
+
if (stateInfo.states.some((st) => st.name === nm))
|
|
1186
|
+
continue;
|
|
1187
|
+
stateInfo.states.push({ name: nm, init: '' });
|
|
1188
|
+
}
|
|
1189
|
+
}
|
|
1117
1190
|
// R98:把当前状态名快照给表达式下沉用(walk 阶段据此把裸状态名重写成 stateStore.x)
|
|
1118
1191
|
jsExprStateNames = stateInfo.states.map((st) => st.name);
|
|
1119
1192
|
// R74:状态名撞原生保留成员(SwiftUI `body`、Kotlin 关键字)→ 自动改名 + 警告(不再只警告导致后续编译失败)
|
|
@@ -1735,6 +1808,10 @@ function walkElement(node, classProps, comboRules, scopedRules, pseudoRules, war
|
|
|
1735
1808
|
const pos = compPosition.get(comp.name) ?? {};
|
|
1736
1809
|
return componentUseNode(node, comp, pos, warnings);
|
|
1737
1810
|
}
|
|
1811
|
+
if (allowUnknownComponentsFlag) {
|
|
1812
|
+
warnings.push(`自定义组件 <${node.tag}> 未迁移 → 已降级为占位(R115)`);
|
|
1813
|
+
return { type: 'group', props: { flexDirection: 'column', padding: 8 }, children: [{ type: 'text', props: { text: ' ' } }] };
|
|
1814
|
+
}
|
|
1738
1815
|
throw new VueCompileError(`未知标签 <${node.tag}>(元素归一化表外;组件/自定义标签暂不支持,R13)`);
|
|
1739
1816
|
}
|
|
1740
1817
|
// overflow:auto(div 等容器)→ 节点升级为 scroll(生成 ScrollView,内容区可滚动占满剩余)
|
|
@@ -1924,7 +2001,7 @@ function walkElement(node, classProps, comboRules, scopedRules, pseudoRules, war
|
|
|
1924
2001
|
const dot = name.split('.');
|
|
1925
2002
|
const base = dot[0];
|
|
1926
2003
|
const loopVar = loopScope.find((l) => l.itemKey === base || l.indexKey === base);
|
|
1927
|
-
const dyn = loopVar && dot.length
|
|
2004
|
+
const dyn = loopVar && dot.length >= 2 && dot[0] === loopVar.itemKey && dot.slice(1).every((seg) => /^\w+$/.test(seg));
|
|
1928
2005
|
if (!text && !parts.some((p) => 'bind' in p || 'dynField' in p || 'fieldOf' in p)) {
|
|
1929
2006
|
// M2 模板内三元(D1 小范围):cond ? 'a' : 'b' → Swift 三元字符串表达式(Text(expr))
|
|
1930
2007
|
const tri = parseTernaryExpr(name);
|
|
@@ -1934,7 +2011,7 @@ function walkElement(node, classProps, comboRules, scopedRules, pseudoRules, war
|
|
|
1934
2011
|
}
|
|
1935
2012
|
// M4b 单插值动态列表项属性 {{ item.field }} → 单段 bindParts
|
|
1936
2013
|
if (dyn) {
|
|
1937
|
-
parts.push({ dynField: { var: dot[0], field: dot
|
|
2014
|
+
parts.push({ dynField: { var: dot[0], field: dot.slice(1).join('.') } });
|
|
1938
2015
|
continue;
|
|
1939
2016
|
}
|
|
1940
2017
|
if (name.startsWith('\u0000store:')) {
|
|
@@ -2091,7 +2168,11 @@ function walkElement(node, classProps, comboRules, scopedRules, pseudoRules, war
|
|
|
2091
2168
|
}
|
|
2092
2169
|
// R108:文本节点丢弃布局类属性(原生端 width/height/flexDirection 只对容器有意义)
|
|
2093
2170
|
if (mapping.type === 'text') {
|
|
2094
|
-
const LAYOUT_ONLY = [
|
|
2171
|
+
const LAYOUT_ONLY = [
|
|
2172
|
+
'height', 'width', 'flexDirection', 'justifyContent', 'alignItems', 'flex', 'gap',
|
|
2173
|
+
'clip', 'overflow', 'marginBottom', 'marginRight', 'marginLeft', 'borderSides',
|
|
2174
|
+
'radius', 'opacity', 'position', 'absTop', 'absLeft', 'absRight', 'absBottom',
|
|
2175
|
+
];
|
|
2095
2176
|
const dropped = LAYOUT_ONLY.filter((k) => props[k] !== undefined);
|
|
2096
2177
|
if (dropped.length > 0) {
|
|
2097
2178
|
for (const k of dropped)
|