@dongmingzhixiu/vuexir-compiler 0.5.7 → 0.7.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.
@@ -1,3 +1,4 @@
1
+ import { transformSync } from 'esbuild';
1
2
  /**
2
3
  * Vue SFC → IR 最小转换器(P1-lite)
3
4
  *
@@ -15,10 +16,49 @@
15
16
  * - 插值 {{ }}、v-for、v-if/v-show、:style/:class 绑定、组件、自定义标签
16
17
  * - script 逻辑(事件绑定 @click 忽略 + 提示;R12 逻辑分仓 L1/L2/L3 属后续阶段)
17
18
  */
18
- import { parse } from '@vue/compiler-sfc';
19
- import { NodeTypes } from '@vue/compiler-core';
20
- import { CssParseError, mergeIrProps, parseCss, parseInlineStyle } from './css.js';
19
+ import { parse, babelParse } from '@vue/compiler-sfc';
20
+ import { NodeTypes, walkIdentifiers } from '@vue/compiler-core';
21
+ import { CssParseError, mergeIrProps, parseCss, extractCssVariables, resolveCssRules } from './css.js';
22
+ import { parseSvg } from './svg.js';
21
23
  import { buildStoreRegistry, findStoreAliases, parseSharedConstArrays } from './stores.js';
24
+ // 样式上下文仅用于编译,不写入公开 IR。
25
+ let inlineComponentsFlag = false;
26
+ let hasLoopExpressions = false;
27
+ let liveStoreObjectsFlag = false;
28
+ let pageScriptBundleFlag = false;
29
+ let componentScopeSeq = 0;
30
+ let componentSlots = [];
31
+ let componentPath = [];
32
+ let templateAssets = {};
33
+ let styleRuleTables = new WeakMap();
34
+ let inheritedTextStyles = new WeakMap();
35
+ const TEXT_STYLE_KEYS = ['color', 'fontSize', 'fontWeight', 'textAlign', 'lineHeight', 'lineHeightMultiplier', 'letterSpacing', 'fontFamily'];
36
+ function cssElement(node) {
37
+ const attr = (name) => node.props.find((p) => p.type === NodeTypes.ATTRIBUTE && p.name === name);
38
+ const cls = attr('class');
39
+ const id = attr('id');
40
+ return { tag: node.tag, classes: cls && 'value' in cls ? cls.value?.content.split(/\s+/).filter(Boolean) ?? [] : [],
41
+ id: id && 'value' in id ? id.value?.content : undefined };
42
+ }
43
+ function applyTextInheritance(node, parent = {}) {
44
+ const own = inheritedTextStyles.get(node) ?? {};
45
+ const effective = { ...parent };
46
+ mergeIrProps(effective, own);
47
+ if (node.type === 'text' || node.type === 'button' || node.type === 'input' || node.type === 'vector') {
48
+ node.props ??= {};
49
+ const allowed = node.type === 'vector' ? ['color'] : node.type === 'text' ? TEXT_STYLE_KEYS : node.type === 'button'
50
+ ? ['color', 'fontSize', 'fontWeight'] : ['color', 'fontSize'];
51
+ for (const k of allowed) {
52
+ if ((k === 'lineHeight' && node.props.lineHeightMultiplier !== undefined) ||
53
+ (k === 'lineHeightMultiplier' && node.props.lineHeight !== undefined))
54
+ continue;
55
+ if ((own[k] === undefined || node.props[k] === undefined) && effective[k] !== undefined)
56
+ node.props[k] = effective[k];
57
+ }
58
+ }
59
+ for (const child of node.children ?? [])
60
+ applyTextInheritance(child, effective);
61
+ }
22
62
  export class VueCompileError extends Error {
23
63
  }
24
64
  /** R90 store getter 降级告警(flattenStorePath 无 warnings 参数,先收集后并入) */
@@ -34,10 +74,39 @@ let jsExprStateNames = [];
34
74
  /** 把一段 JS 表达式注册为运行时求值状态,返回状态名(模板 bind 用) */
35
75
  function addJsExprState(expr) {
36
76
  const name = `_vrnE${jsExprSeq++}`;
37
- // ① ref.value → stateStore.x;② 裸状态名 → stateStore.x(对象态/字符串态都要走 stateStore,
38
- // 否则 JS 里没有这个变量);③ 形如 fn( 的调用不重写(那是 utils bundle 提供的全局函数)
39
- let js = expr.replace(/\b([A-Za-z_$][\w$]*)\.value\b/g, (all, id) => jsExprStateNames.includes(id) ? `stateStore.${id}` : all);
40
- js = js.replace(/(?<![.\w$])([A-Za-z_$][\w$]*)(?!\s*\()/g, (all, id) => jsExprStateNames.includes(id) ? `stateStore.${id}` : all);
77
+ const edits = [];
78
+ const aliases = activeLoops.flatMap((l) => [l.itemKey, ...(l.indexKey ? [l.indexKey] : [])]);
79
+ let scoped = false;
80
+ let ast;
81
+ try {
82
+ ast = babelParse(`(${expr})`, { sourceType: 'module', plugins: ['typescript'] });
83
+ }
84
+ catch (error) {
85
+ throw new VueCompileError(`模板表达式无法解析「${expr.slice(0, 320)}」:${error.message}`);
86
+ }
87
+ walkIdentifiers(ast.program, (id, parent, _stack, isReference, isLocal) => {
88
+ if (!isReference || isLocal)
89
+ return;
90
+ if (aliases.includes(id.name))
91
+ scoped = true;
92
+ if (!jsExprStateNames.includes(id.name))
93
+ return;
94
+ const read = `__vrnRead(${JSON.stringify(id.name)})`;
95
+ if (parent?.type === 'MemberExpression' && parent.object === id && !parent.computed && parent.property.type === 'Identifier' && parent.property.name === 'value') {
96
+ edits.push({ start: parent.start - 1, end: parent.end - 1, value: read });
97
+ }
98
+ else {
99
+ const shorthand = parent?.type === 'ObjectProperty' && parent.shorthand;
100
+ edits.push({ start: id.start - 1, end: id.end - 1, value: shorthand ? `${id.name}: ${read}` : read });
101
+ }
102
+ });
103
+ let js = expr;
104
+ for (const edit of edits.sort((a, b) => b.start - a.start))
105
+ js = js.slice(0, edit.start) + edit.value + js.slice(edit.end);
106
+ if (scoped) {
107
+ hasLoopExpressions = true;
108
+ return '__vrnLoop:' + JSON.stringify({ expression: js, aliases });
109
+ }
41
110
  jsExprStates.push({ name, init: '', computed: js });
42
111
  return name;
43
112
  }
@@ -53,7 +122,7 @@ let storeAliases = new Map();
53
122
  function flattenStorePath(name) {
54
123
  // R123:页面脚本自己的响应式绑定(soon/nearest/…)由运行时同步填充,
55
124
  // 绝不能再被 store 折叠改写成 store_xxx(否则 v-for 会去读静态状态,永远是空)
56
- if (jsExprStateNames.includes(name))
125
+ if (jsExprStateNames.includes(name) || (liveStoreObjectsFlag && jsExprStateNames.includes(name.split('.')[0])))
57
126
  return name;
58
127
  // R89:computed 折叠出的派生名(all → ui_upcoming;all.length → ui_upcoming.length)
59
128
  if (derivedNames.size > 0) {
@@ -307,8 +376,76 @@ export function rewriteStateRefs(body, names) {
307
376
  const re = new RegExp(`\\b(${names.map((n) => n.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')).join('|')})\\.value\\b`, 'g');
308
377
  return body.replace(re, (_all, n) => `stateStore.${n}`);
309
378
  }
379
+ export const RUNTIME_BOOTSTRAP_SOURCE = `
380
+ if (!globalThis.__vrnBootstrapReady) {
381
+ globalThis.__vrnBootstrapReady = true;
382
+ // 宿主没有浏览器 window;仅提供业务常用的全局别名与视口事件接口,不引入 DOM 渲染。
383
+ if (typeof window === "undefined") {
384
+ var window = globalThis;
385
+ window.innerWidth = 390;
386
+ window.innerHeight = 844;
387
+ window.location = {hash:'',href:'app://vuexir'};
388
+ window.history = {replaceState:function(_state,_title,url){window.location.hash=String(url);}};
389
+ window.addEventListener = function() {};
390
+ window.removeEventListener = function() {};
391
+ }
392
+ // R72 定时器宿主 shim:宿主(JavaScriptCore)未注入时用宿主桥实现,保证 Promise/await 延时可用;
393
+ // 浏览器预览器已有原生 setTimeout,此处不覆盖。
394
+ var __timerSeq = 0;
395
+ var __timers = {};
396
+ if (typeof setTimeout !== "function") {
397
+ var setTimeout = function (fn, ms) {
398
+ var id = ++__timerSeq;
399
+ if (typeof __nativeSetTimeout === "function") {
400
+ __nativeSetTimeout(id, Number(ms) || 0);
401
+ __timers[id] = fn;
402
+ return id;
403
+ }
404
+ return 0;
405
+ };
406
+ var clearTimeout = function (id) {
407
+ delete __timers[id];
408
+ if (typeof __nativeClearTimeout === "function") __nativeClearTimeout(Number(id));
409
+ };
410
+ // 宿主回调入口(__nativeSetTimeout 到期后由原生调用)
411
+ var __vrnRunTimer = function (id) {
412
+ var fn = __timers[id];
413
+ delete __timers[id];
414
+ if (typeof fn === "function") fn();
415
+ };
416
+ var setInterval = function (fn, ms) {
417
+ var id = ++__timerSeq;
418
+ __timers[id] = { fn: fn, ms: Number(ms) || 0, repeat: true };
419
+ if (typeof __nativeSetInterval === "function") __nativeSetInterval(id, Number(ms) || 0);
420
+ return id;
421
+ };
422
+ var clearInterval = function (id) {
423
+ delete __timers[id];
424
+ if (typeof __nativeClearInterval === "function") __nativeClearInterval(Number(id));
425
+ };
426
+ var __vrnRunInterval = function (id) {
427
+ var t = __timers[id];
428
+ if (t && t.repeat && typeof t.fn === "function") t.fn();
429
+ };
430
+ }
431
+ if (typeof queueMicrotask !== "function") {
432
+ var queueMicrotask = function (fn) {
433
+ Promise.resolve().then(fn);
434
+ };
435
+ }
436
+
437
+
438
+ if (typeof localStorage === "undefined") {
439
+ var localStorage = {
440
+ getItem: function(k) { return typeof __lsGet === "function" ? __lsGet("vrn." + k) : null; },
441
+ setItem: function(k,v) { if(typeof __lsSet === "function") __lsSet("vrn." + k,String(v)); },
442
+ removeItem: function(k) { if(typeof __lsSet === "function") __lsSet("vrn." + k,null); }
443
+ };
444
+ }
445
+ }
446
+ `;
310
447
  /** 生成 runtime.js 完整内容(M3:stateStore Proxy + handlers + onMounted + localStorage shim) */
311
- export function generateRuntimeSource(states, handlers, onMounted, onMountedAsync = false) {
448
+ export function generateRuntimeSource(states, handlers, onMounted, onMountedAsync = false, externalScript = false) {
312
449
  // R96 computed:JS 运行时按表达式求值并写回 stateStore(写回经 Proxy → pushState 直达原生)
313
450
  const computedStates = states.filter((st) => st.computed);
314
451
  const computedBlock = computedStates.length === 0
@@ -331,7 +468,7 @@ __vrnRecompute();
331
468
  __vrnRecompute();
332
469
  // R122:页面脚本(__vrnPage)的真实值必须最后写入,否则会被静态 computed 覆盖成空
333
470
  __vrnSyncPage();
334
- if (++__n < 20 && typeof setTimeout === "function") setTimeout(__tick, 100);
471
+ if (typeof setTimeout === "function") setTimeout(__tick, 100);
335
472
  }
336
473
  __tick();
337
474
  })();
@@ -352,7 +489,7 @@ __vrnRecompute();
352
489
  : `"${s.init.replace(/"/g, '\\"')}"`;
353
490
  return ` ${s.name}: ${v}`;
354
491
  });
355
- const handlerSrc = handlers
492
+ const handlerSrc = (externalScript ? [] : handlers)
356
493
  .map((h) => `${h.async ? 'async ' : ''}function ${h.name}(${h.params}) {\n${indentBody(h.body)}\n${
357
494
  // R96/R98:改完状态后重算派生值(computed / 表达式下沉),否则界面不会跟着变
358
495
  (states.some((st) => st.computed) ? ' if (typeof __vrnRecompute === "function") __vrnRecompute();\n' : '')}}`)
@@ -367,6 +504,7 @@ Object.keys(__stateInit).forEach(function (k) { __stateStore[k] = __stateInit[k]
367
504
  var __fromSwift = false;
368
505
  var stateStore = new Proxy(__stateStore, {
369
506
  set: function (t, k, v) {
507
+ if(t[k] === v) return true;
370
508
  t[k] = v;
371
509
  if (!__fromSwift && typeof __setState === "function") {
372
510
  // M4b 动态列表:数组/对象 → JSON 字符串写回原生(Swift 侧解码渲染)
@@ -385,50 +523,7 @@ function fetch(url) {
385
523
  });
386
524
  });
387
525
  }
388
- // R72 定时器宿主 shim:宿主(JavaScriptCore)未注入时用宿主桥实现,保证 Promise/await 延时可用;
389
- // 浏览器预览器已有原生 setTimeout,此处不覆盖。
390
- var __timerSeq = 0;
391
- var __timers = {};
392
- if (typeof setTimeout !== "function") {
393
- var setTimeout = function (fn, ms) {
394
- var id = ++__timerSeq;
395
- if (typeof __nativeSetTimeout === "function") {
396
- __nativeSetTimeout(id, Number(ms) || 0);
397
- __timers[id] = fn;
398
- return id;
399
- }
400
- return 0;
401
- };
402
- var clearTimeout = function (id) {
403
- delete __timers[id];
404
- if (typeof __nativeClearTimeout === "function") __nativeClearTimeout(Number(id));
405
- };
406
- // 宿主回调入口(__nativeSetTimeout 到期后由原生调用)
407
- var __vrnRunTimer = function (id) {
408
- var fn = __timers[id];
409
- delete __timers[id];
410
- if (typeof fn === "function") fn();
411
- };
412
- var setInterval = function (fn, ms) {
413
- var id = ++__timerSeq;
414
- __timers[id] = { fn: fn, ms: Number(ms) || 0, repeat: true };
415
- if (typeof __nativeSetInterval === "function") __nativeSetInterval(id, Number(ms) || 0);
416
- return id;
417
- };
418
- var clearInterval = function (id) {
419
- delete __timers[id];
420
- if (typeof __nativeClearInterval === "function") __nativeClearInterval(Number(id));
421
- };
422
- var __vrnRunInterval = function (id) {
423
- var t = __timers[id];
424
- if (t && t.repeat && typeof t.fn === "function") t.fn();
425
- };
426
- }
427
- if (typeof queueMicrotask !== "function") {
428
- var queueMicrotask = function (fn) {
429
- Promise.resolve().then(fn);
430
- };
431
- }
526
+ ${RUNTIME_BOOTSTRAP_SOURCE}
432
527
 
433
528
  // R81 原生桥:bridge.toast / bridge.openUrl(iOS 由 __nativeToast/__nativeOpenUrl 宿主函数实现;
434
529
  // Android 由编译器把 bridge.* 翻译成 VrnBridge.*;浏览器预览退回 alert/console)
@@ -478,23 +573,33 @@ var router = {
478
573
  if (typeof __routerBack === "function") __routerBack();
479
574
  }
480
575
  };
576
+ // 模板求值使用真实页面 ref/computed,避免把对象 JSON 当对象、数值当字符串。
577
+ function __vrnWrite(name,value) {
578
+ var page=typeof __vrnPage !== "undefined" ? __vrnPage : undefined;
579
+ if(page && page[name] && typeof page[name] === "object" && "value" in page[name]) page[name].value=value;
580
+ stateStore[name]=value;
581
+ }
582
+ function __vrnRead(name) {
583
+ var page = typeof __vrnPage !== "undefined" ? __vrnPage : undefined;
584
+ if(page && name in page) {
585
+ var value=page[name];
586
+ return value && typeof value === "object" && "value" in value ? value.value : value;
587
+ }
588
+ return stateStore[name];
589
+ }
481
590
  // R118 页面状态同步:把页面脚本 bundle(__vrnPage)里的 ref/computed 值写进 stateStore,
482
591
  // 模板里 {{ nearest }} / {{ soon }} 这类页面级派生值就有了真实来源
483
592
  // 把数组里的嵌套对象拍平成「点键」,让 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;
593
+ function __vrnFlat(v) { return v; }
594
+ function __vrnEvalTemplate(expression, json) {
595
+ try {
596
+ var bindings=JSON.parse(json), names=Object.keys(bindings), values=names.map(function(k){return bindings[k];});
597
+ var fn=Function.apply(null,names.concat('return ('+expression+');'));
598
+ var value=fn.apply(null,values);
599
+ return value == null ? '' : typeof value === 'object' ? JSON.stringify(value) : String(value);
600
+ } catch(e) { return ''; }
497
601
  }
602
+
498
603
  function __vrnSyncPage() {
499
604
  try {
500
605
  var page = typeof __vrnPage !== "undefined" ? __vrnPage : (typeof globalThis !== "undefined" ? globalThis.__vrnPage : undefined);
@@ -510,9 +615,11 @@ function __vrnSyncPage() {
510
615
  var v = page[k];
511
616
  var val;
512
617
  if (v && typeof v === "object" && "value" in v) val = v.value; // ref / computed
618
+ else if (v && typeof v === "object" && Object.prototype.hasOwnProperty.call(__stateInit,k)) val=v;
513
619
  else continue; // 函数/常量交给模板静态值
514
620
  if (val === undefined || val === null) continue;
515
- stateStore[k] = (typeof val === "object") ? JSON.stringify(__vrnFlat(val)) : val;
621
+ stateStore[k] = (typeof val === "object") ? JSON.stringify(__vrnFlat(val)) :
622
+ typeof __stateInit[k] === "string" ? String(val) : val;
516
623
  } catch (e) {}
517
624
  }
518
625
  } catch (e) {}
@@ -534,7 +641,7 @@ function __vrnSyncPage() {
534
641
  // handlers(<script setup> 函数体:ref.value → stateStore 重写)
535
642
  ${handlerSrc}
536
643
  ${computedBlock}${"" /* R102 见 computedBlock 内:仅当存在派生状态时才调度重算循环,避免无谓的定时器 */}// onMounted(Swift onAppear → invoke("__onMounted"))
537
- ${onMounted !== undefined ? `${onMountedAsync ? 'async ' : ''}function __onMounted() {\n${indentBody(onMounted)}\n}` : ''}`;
644
+ ${externalScript ? 'function __onMounted() { if(typeof __vrnSetNativeRoute === "function") __vrnSetNativeRoute(); if(typeof __vrnPage !== "undefined" && __vrnPage.__vrnMountPage) __vrnPage.__vrnMountPage(); }' : onMounted !== undefined ? `${onMountedAsync ? 'async ' : ''}function __onMounted() {\n${indentBody(onMounted)}\n}` : ''}`;
538
645
  }
539
646
  /** 简单缩进(handler 体统一 2 空格缩进) */
540
647
  function indentBody(body) {
@@ -751,7 +858,21 @@ function parseDynamicStyle(exp, warnings, state) {
751
858
  const key = m[1];
752
859
  const val = m[2].trim();
753
860
  const isState = /^[A-Za-z_]\w*$/.test(val) && state?.states.some((st) => st.name === val);
754
- if (key === 'color' && isState) {
861
+ if (key === 'width' || key === 'height') {
862
+ if (/%/.test(val)) {
863
+ if (key === 'width')
864
+ out.widthFractionDynamic = addJsExprState(`String(parseFloat(${val})/100)`);
865
+ else
866
+ warnings.push(`:style ${key} 百分比高度尚未映射:${val}`);
867
+ continue;
868
+ }
869
+ const literal = val.match(/^['"]([\d.]+)px['"]$/);
870
+ if (literal)
871
+ out[key] = Number(literal[1]);
872
+ else
873
+ out[key + 'Dynamic'] = addJsExprState(`String(parseFloat(${val}))`);
874
+ }
875
+ else if (key === 'color' && isState) {
755
876
  out.colorDynamic = val;
756
877
  }
757
878
  else if (key === 'fontSize') {
@@ -977,6 +1098,16 @@ let compPosition = new Map();
977
1098
  /** R-VFOBJ-C:script 顶层 const 数组表(const rows = [...] → rows: 数组内文);vueToIr 每次调用重置 */
978
1099
  let vforConsts = {};
979
1100
  export function vueToIr(source, opts = {}) {
1101
+ hasLoopExpressions = false;
1102
+ inlineComponentsFlag = opts.inlineComponents === true;
1103
+ liveStoreObjectsFlag = opts.liveStoreObjects === true;
1104
+ pageScriptBundleFlag = opts.pageScriptBundle === true;
1105
+ componentScopeSeq = 0;
1106
+ componentSlots = [];
1107
+ componentPath = [];
1108
+ templateAssets = opts.assets ?? {};
1109
+ styleRuleTables = new WeakMap();
1110
+ inheritedTextStyles = new WeakMap();
980
1111
  compPosition = new Map();
981
1112
  vforConsts = {};
982
1113
  storeAliases = new Map();
@@ -984,6 +1115,7 @@ export function vueToIr(source, opts = {}) {
984
1115
  storeConstArrays = {};
985
1116
  derivedNames = new Map();
986
1117
  storeGetterWarnings = [];
1118
+ jsExprStateNames = [];
987
1119
  jsExprSeq = 0;
988
1120
  jsExprStates = [];
989
1121
  allowUnknownComponentsFlag = opts.allowUnknownComponents === true;
@@ -996,16 +1128,17 @@ export function vueToIr(source, opts = {}) {
996
1128
  if (!descriptor.template) {
997
1129
  throw new VueCompileError('缺少 <template> 块');
998
1130
  }
1131
+ const cssVariables = extractCssVariables(opts.globalStyles ?? []);
999
1132
  // 1) CSS 子集 → 类规则表
1000
1133
  const cssWarnings = [];
1001
1134
  const classProps = new Map();
1002
1135
  let comboRules = [];
1003
1136
  let scopedRules = [];
1004
1137
  let pseudoRules = [];
1005
- for (const styleBlock of descriptor.styles) {
1138
+ for (const styleBlock of [...(opts.globalStyles ?? []).map((content) => ({ content })), ...descriptor.styles]) {
1006
1139
  let r;
1007
1140
  try {
1008
- r = parseCss(styleBlock.content, cssWarnings);
1141
+ r = parseCss(styleBlock.content, cssWarnings, cssVariables);
1009
1142
  }
1010
1143
  catch (e) {
1011
1144
  if (e instanceof CssParseError)
@@ -1015,13 +1148,32 @@ export function vueToIr(source, opts = {}) {
1015
1148
  for (const [cls, props] of r.classProps) {
1016
1149
  classProps.set(cls, { ...(classProps.get(cls) ?? {}), ...props });
1017
1150
  }
1151
+ styleRuleTables.set(classProps, [...(styleRuleTables.get(classProps) ?? []), ...r.styleRules]);
1018
1152
  comboRules = comboRules.concat(r.comboRules);
1019
1153
  scopedRules = scopedRules.concat(r.scopedRules);
1020
1154
  pseudoRules = pseudoRules.concat(r.pseudoRules);
1021
1155
  }
1022
1156
  // 1.5) 组件表(R41 独立原生 View):components/*.vue → { name, template AST, 组件样式规则, defineProps 名 }
1023
1157
  const compTable = [];
1024
- for (const [name, src] of Object.entries(opts.components ?? {})) {
1158
+ const availableComponents = opts.components ?? {};
1159
+ const reachable = new Set();
1160
+ const visitComponents = (template) => {
1161
+ const visit = (node) => {
1162
+ if (node.type === NodeTypes.ELEMENT) {
1163
+ const name = Object.keys(availableComponents).find((key) => key === node.tag || key === toPascalCase(node.tag));
1164
+ if (name && !reachable.has(name)) {
1165
+ reachable.add(name);
1166
+ const nested = parse(availableComponents[name]).descriptor.template?.ast;
1167
+ if (nested)
1168
+ visitComponents(nested);
1169
+ }
1170
+ }
1171
+ node.children?.forEach(visit);
1172
+ };
1173
+ visit(template);
1174
+ };
1175
+ visitComponents(descriptor.template.ast);
1176
+ for (const [name, src] of Object.entries(availableComponents).filter(([name]) => reachable.has(name))) {
1025
1177
  const c = parse(src, { filename: `${name}.vue` });
1026
1178
  if (c.errors.length > 0) {
1027
1179
  throw new VueCompileError(`组件 ${name} 解析失败:${c.errors.map((e) => e.message).join('; ')}`);
@@ -1033,10 +1185,10 @@ export function vueToIr(source, opts = {}) {
1033
1185
  let cc = [];
1034
1186
  let cs = [];
1035
1187
  let cps = [];
1036
- for (const styleBlock of c.descriptor.styles) {
1188
+ for (const styleBlock of [...(opts.globalStyles ?? []).map((content) => ({ content })), ...c.descriptor.styles]) {
1037
1189
  let r;
1038
1190
  try {
1039
- r = parseCss(styleBlock.content, cssWarnings);
1191
+ r = parseCss(styleBlock.content, cssWarnings, cssVariables);
1040
1192
  }
1041
1193
  catch (e) {
1042
1194
  if (e instanceof CssParseError)
@@ -1045,6 +1197,7 @@ export function vueToIr(source, opts = {}) {
1045
1197
  }
1046
1198
  for (const [cls, props] of r.classProps)
1047
1199
  cp.set(cls, { ...(cp.get(cls) ?? {}), ...props });
1200
+ styleRuleTables.set(cp, [...(styleRuleTables.get(cp) ?? []), ...r.styleRules]);
1048
1201
  cc = cc.concat(r.comboRules);
1049
1202
  cs = cs.concat(r.scopedRules);
1050
1203
  cps = cps.concat(r.pseudoRules);
@@ -1112,6 +1265,8 @@ export function vueToIr(source, opts = {}) {
1112
1265
  const registry = buildStoreRegistry(opts.storeSources);
1113
1266
  storeAliases = findStoreAliases(src, registry);
1114
1267
  for (const [alias, def] of storeAliases) {
1268
+ if (liveStoreObjectsFlag && !stateInfo.states.some((st) => st.name === alias))
1269
+ stateInfo.states.push({ name: alias, init: '{}', obj: true });
1115
1270
  for (const f of def.state) {
1116
1271
  const flat = `${alias}_${f.name}`;
1117
1272
  if (stateInfo.states.some((st) => st.name === flat))
@@ -1243,14 +1398,15 @@ export function vueToIr(source, opts = {}) {
1243
1398
  }
1244
1399
  const hasComputed = (stateInfo?.states ?? []).some((st) => st.computed);
1245
1400
  if (handlers.length > 0 || onMountedBody !== undefined || hasComputed) {
1246
- runtime = generateRuntimeSource(stateInfo?.states ?? [], handlers, onMountedBody, onMountedAsync);
1401
+ runtime = generateRuntimeSource(stateInfo?.states ?? [], handlers, onMountedBody, onMountedAsync, opts.pageScriptBundle);
1247
1402
  }
1248
1403
  }
1249
1404
  // 3.5) 组件树(R41):每个组件 → 独立 Swift View 的 IR 树(组件样式独立作用域;
1250
1405
  // {{ prop }} → text ref;组件根布局 props(flex/width/marginTop)剥离并交使用点)。
1251
1406
  // 必须先于页面 walk 构建:使用点 component 节点需要组件根剥离出的位置 props。
1252
- const components = compTable.map((c) => {
1407
+ const components = (inlineComponentsFlag ? [] : compTable).map((c) => {
1253
1408
  const tree = buildComponentTree(c, warnings);
1409
+ applyTextInheritance(tree.ui);
1254
1410
  c.refs = collectRefs(tree.ui);
1255
1411
  compPosition.set(c.name, tree.position);
1256
1412
  return { name: c.name, ui: tree.ui };
@@ -1270,7 +1426,7 @@ export function vueToIr(source, opts = {}) {
1270
1426
  if (onMountedBody !== undefined)
1271
1427
  onMountedBody = renameSrc(onMountedBody);
1272
1428
  }
1273
- const children = ast.children.flatMap((c) => walk(c, classProps, comboRules, scopedRules, pseudoRules, warnings, stateInfo, compTable));
1429
+ const children = walkSiblings(ast.children, (c) => walk(c, classProps, comboRules, scopedRules, pseudoRules, warnings, stateInfo, compTable));
1274
1430
  let pageRootBg;
1275
1431
  // 3) 组装 screen:root(width fill + height 基准)包一层 group(承载根元素布局 props)
1276
1432
  // M4b:模板根容器(.page 等)的 background 提升到 root——root ZStack 背景层必须铺满全屏,
@@ -1300,13 +1456,30 @@ export function vueToIr(source, opts = {}) {
1300
1456
  const oc = n.props?.onClick;
1301
1457
  if (typeof oc === 'string') {
1302
1458
  const name = oc.replace(/\(\)$/, '');
1303
- if (!handlerNames.has(name) && !/^(router\.|Router\.)/.test(name)) {
1459
+ if (!opts.pageScriptBundle && !handlerNames.has(name) && !/^(router\.|Router\.)/.test(name)) {
1304
1460
  warnings.push(`@click「${oc}」引用的函数未提取为 runtime handler(仅支持 function 声明;箭头函数/Options API 暂不支持),原生端点击将无响应`);
1305
1461
  }
1306
1462
  }
1307
1463
  for (const c of n.children ?? [])
1308
1464
  checkClickHandlers(c);
1309
1465
  };
1466
+ const documentStyles = {};
1467
+ const documentAncestors = [];
1468
+ for (const element of [{ tag: 'html', classes: [] }, { tag: 'body', classes: [] }, { tag: 'div', id: 'app', classes: [] }]) {
1469
+ const resolved = resolveCssRules(styleRuleTables.get(classProps) ?? [], element, documentAncestors, '', warnings);
1470
+ for (const k of TEXT_STYLE_KEYS)
1471
+ if (resolved[k] !== undefined)
1472
+ mergeIrProps(documentStyles, { [k]: resolved[k] });
1473
+ if (!ui.props?.background && !ui.props?.gradient && (resolved.background !== undefined || resolved.gradient !== undefined)) {
1474
+ ui.props ??= {};
1475
+ if (resolved.gradient)
1476
+ ui.props.gradient = resolved.gradient;
1477
+ else
1478
+ ui.props.background = resolved.background;
1479
+ }
1480
+ documentAncestors.push(element);
1481
+ }
1482
+ applyTextInheritance(ui, documentStyles);
1310
1483
  checkClickHandlers(ui);
1311
1484
  // R74 续:IR 树里的状态引用改名(bind/show/onSet/bindParts/fieldOf/condition/动态样式/输入值/列表态)
1312
1485
  if (stateRenames.size > 0) {
@@ -1372,9 +1545,11 @@ export function vueToIr(source, opts = {}) {
1372
1545
  if (!stateInfo.states.some((x) => x.name === st.name))
1373
1546
  stateInfo.states.push({ ...st });
1374
1547
  }
1375
- runtime = generateRuntimeSource(stateInfo.states, handlers ?? [], onMountedBody, onMountedAsync);
1548
+ runtime = generateRuntimeSource(stateInfo.states, handlers ?? [], onMountedBody, onMountedAsync, opts.pageScriptBundle);
1376
1549
  warnings.push(`模板表达式下沉:${jsExprStates.length} 处动态表达式改由 JS 运行时求值(R98)`);
1377
1550
  }
1551
+ if (!runtime && (opts.pageScriptBundle || hasLoopExpressions))
1552
+ runtime = generateRuntimeSource(stateInfo?.states ?? [], handlers ?? [], onMountedBody, onMountedAsync, opts.pageScriptBundle);
1378
1553
  for (const w of storeGetterWarnings)
1379
1554
  if (!warnings.includes(w))
1380
1555
  warnings.push(w);
@@ -1387,16 +1562,62 @@ export function vueToIr(source, opts = {}) {
1387
1562
  height: opts.height ?? 640,
1388
1563
  ...(stateInfo && stateInfo.states.length > 0 ? { state: stateInfo.states } : {}),
1389
1564
  ...(handlers && handlers.length > 0 ? { handlers } : {}),
1390
- ...(onMountedBody !== undefined ? { onMounted: onMountedBody, ...(onMountedAsync ? { onMountedAsync: true } : {}) } : {}),
1565
+ ...(opts.pageScriptBundle ? { onMounted: '__vrnPage.__vrnMountPage()' } : onMountedBody !== undefined ? { onMounted: onMountedBody, ...(onMountedAsync ? { onMountedAsync: true } : {}) } : {}),
1391
1566
  ...(navBarConfig !== null ? { navBar: navBarConfig } : {}),
1392
1567
  ui,
1393
1568
  },
1394
1569
  ...(components.length > 0 ? { components } : {}),
1395
- warnings,
1570
+ warnings: [...new Set(warnings)],
1396
1571
  ...(runtime !== undefined ? { runtime } : {}),
1397
1572
  };
1398
1573
  }
1399
1574
  /* ============ 模板 AST 遍历 ============ */
1575
+ /** 按 Vue 相邻兄弟语义编译条件链:只有前面所有分支都未命中,才检查当前分支。 */
1576
+ function walkSiblings(nodes, visit) {
1577
+ const result = [];
1578
+ let conditions;
1579
+ for (const node of nodes) {
1580
+ if (node.type === NodeTypes.COMMENT || (node.type === NodeTypes.TEXT && !node.content.trim())) {
1581
+ result.push(...visit(node));
1582
+ continue;
1583
+ }
1584
+ if (node.type !== NodeTypes.ELEMENT) {
1585
+ conditions = undefined;
1586
+ result.push(...visit(node));
1587
+ continue;
1588
+ }
1589
+ const branch = node.props.find((p) => p.type === NodeTypes.DIRECTIVE &&
1590
+ ['if', 'else-if', 'else'].includes(p.name));
1591
+ if (!branch || branch.type !== NodeTypes.DIRECTIVE) {
1592
+ conditions = undefined;
1593
+ result.push(...visit(node));
1594
+ continue;
1595
+ }
1596
+ const own = branch.exp && branch.exp.type === NodeTypes.SIMPLE_EXPRESSION ? branch.exp.content.trim() : '';
1597
+ if (branch.name === 'if') {
1598
+ conditions = [own];
1599
+ result.push(...visit(node));
1600
+ continue;
1601
+ }
1602
+ if (!conditions)
1603
+ throw new VueCompileError(`v-${branch.name} 缺少相邻的 v-if / v-else-if`);
1604
+ if (branch.name === 'else-if' && !own)
1605
+ throw new VueCompileError('v-else-if 缺少条件表达式');
1606
+ const excluded = conditions.map((c) => `(${c})`).join(' || ');
1607
+ const expression = `!(${excluded})${own ? ` && (${own})` : ''}`;
1608
+ // 转成普通 v-if,让 template、组件、循环和普通元素复用同一条件处理路径。
1609
+ const rewritten = { ...node, props: node.props.map((p) => p === branch ? {
1610
+ ...branch, name: 'if', exp: { type: NodeTypes.SIMPLE_EXPRESSION, content: expression,
1611
+ isStatic: false, constType: 0, loc: branch.loc },
1612
+ } : p) };
1613
+ result.push(...visit(rewritten));
1614
+ if (branch.name === 'else')
1615
+ conditions = undefined;
1616
+ else
1617
+ conditions.push(own);
1618
+ }
1619
+ return result;
1620
+ }
1400
1621
  function walk(node, classProps, comboRules, scopedRules, pseudoRules, warnings, state, compTable = [], propRefs, loopScope = [], ancestorClasses = []) {
1401
1622
  switch (node.type) {
1402
1623
  case NodeTypes.TEXT: {
@@ -1407,6 +1628,8 @@ function walk(node, classProps, comboRules, scopedRules, pseudoRules, warnings,
1407
1628
  return [];
1408
1629
  case NodeTypes.INTERPOLATION: {
1409
1630
  const nameRaw = textOf(node.content).trim();
1631
+ if (activeLoops.length && !/^[A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*)*$/.test(nameRaw))
1632
+ return [{ type: 'text', props: { bind: addJsExprState(nameRaw) } }];
1410
1633
  // R86:store 访问先拍平(ui.dialog.title → ui_dialog.title);getter/action 返回哨兵
1411
1634
  const name = flattenStorePath(normalizeTemplateExpr(nameRaw, warnings));
1412
1635
  // 组件树模式(propRefs 传入=非空):{{ 任意名 }} → text ref(Swift Text(prop),
@@ -1481,6 +1704,10 @@ function walk(node, classProps, comboRules, scopedRules, pseudoRules, warnings,
1481
1704
  return [{ type: 'text', props: { bind: addJsExprState(nameRaw) } }];
1482
1705
  }
1483
1706
  case NodeTypes.ELEMENT:
1707
+ if (node.tag === 'slot' && componentSlots.length) {
1708
+ const attr = node.props.find((p) => p.type === NodeTypes.ATTRIBUTE && p.name === 'name');
1709
+ return componentSlots[componentSlots.length - 1](attr && 'value' in attr ? attr.value?.content ?? 'default' : 'default');
1710
+ }
1484
1711
  return [walkElement(node, classProps, comboRules, scopedRules, pseudoRules, warnings, state, compTable, propRefs, loopScope, ancestorClasses)];
1485
1712
  default:
1486
1713
  warnings.push(`模板节点类型 ${node.type} 已忽略`);
@@ -1693,9 +1920,29 @@ function buildComponentTree(comp, warnings) {
1693
1920
  }
1694
1921
  function walkElement(node, classProps, comboRules, scopedRules, pseudoRules, warnings, state, compTable = [], propRefs, loopScope = [], ancestorClasses = []) {
1695
1922
  let elementClasses = []; // M5 后代选择器:本元素 class 清单(子节点递归传祖先链)
1696
- // R107:SVG 图标(svg/path/circle/line/rect/polygon/g/defs/use)纯装饰 → 忽略,不阻断编译
1697
- if (['svg', 'path', 'circle', 'line', 'rect', 'polygon', 'polyline', 'g', 'defs', 'use', 'text', 'ellipse'].includes(node.tag) && node.tag !== 'text') {
1698
- return { type: 'spacer', props: { height: 0 } };
1923
+ if (node.tag === 'slot' && componentSlots.length) {
1924
+ const nameAttr = node.props.find((p) => p.type === NodeTypes.ATTRIBUTE && p.name === 'name');
1925
+ const name = nameAttr && 'value' in nameAttr ? nameAttr.value?.content ?? 'default' : 'default';
1926
+ const kids = componentSlots[componentSlots.length - 1](name);
1927
+ return kids.length === 1 ? kids[0] : { type: 'group', props: { flexDirection: 'column' }, children: kids.length ? kids : [{ type: 'spacer', props: { height: 0 } }] };
1928
+ }
1929
+ if (node.tag === 'svg') {
1930
+ const rules = styleRuleTables.get(classProps) ?? [];
1931
+ const own = resolveCssRules(rules, cssElement(node), ancestorClasses, '', warnings);
1932
+ const attr = (name) => { const p = node.props.find((p) => p.type === NodeTypes.ATTRIBUTE && p.name === name); return p && 'value' in p ? p.value?.content : undefined; };
1933
+ const vector = parseSvg(node, (el, parents) => resolveCssRules(rules, cssElement(el), [...ancestorClasses, ...parents.map(cssElement)], '', warnings), warnings, addJsExprState);
1934
+ const props = { vector, width: own.width === 'fill' ? 'fill' : typeof own.width === 'number' ? own.width : Number(attr('width')) || 24,
1935
+ height: own.height === 'fill' || own.flex === 1 ? 'fill' : typeof own.height === 'number' ? own.height : Number(attr('height')) || 24 };
1936
+ if (vector.dynamic && own.width === 'fill' && own.height === undefined) {
1937
+ delete props.height;
1938
+ props.aspectRatio = vector.viewBox[2] / vector.viewBox[3];
1939
+ }
1940
+ for (const k of ['opacity', 'color', 'position', 'absTop', 'absLeft', 'absRight', 'absBottom', 'transform'])
1941
+ if (own[k] !== undefined)
1942
+ props[k] = own[k];
1943
+ const ir = { type: 'vector', props };
1944
+ inheritedTextStyles.set(ir, own);
1945
+ return ir;
1699
1946
  }
1700
1947
  // R104:<br> 换行 → 占位间距(原生端没有行内换行语义)
1701
1948
  if (node.tag === 'br') {
@@ -1706,7 +1953,29 @@ function walkElement(node, classProps, comboRules, scopedRules, pseudoRules, war
1706
1953
  if (forDir) {
1707
1954
  // AttributeNode 无 exp;DirectiveNode 才有(find 命中 DIRECTIVE 但 TS 不窄化 union)
1708
1955
  const exp = 'exp' in forDir ? forDir.exp : undefined;
1709
- const forExp = exp && exp.type === NodeTypes.SIMPLE_EXPRESSION ? exp.content : '';
1956
+ let forExp = exp && exp.type === NodeTypes.SIMPLE_EXPRESSION ? exp.content : '';
1957
+ const tuple = forExp.match(/^\[\s*([A-Za-z_$][\w$]*)\s*,\s*([A-Za-z_$][\w$]*)\s*\]\s+(?:in|of)\s+([\s\S]+)$/);
1958
+ if (tuple) {
1959
+ const item = `_vrnTuple${jsExprSeq++}`;
1960
+ const copy = structuredClone(node);
1961
+ const rewrite = (n) => {
1962
+ if (n.type === NodeTypes.INTERPOLATION) {
1963
+ for (let i = 1; i <= 2; i++)
1964
+ n.content.content = n.content.content.replace(new RegExp(`(?<![.\\w$])${tuple[i]}\\b`, 'g'), `${item}[${i - 1}]`);
1965
+ }
1966
+ for (const p of n.props ?? [])
1967
+ if (p.type === NodeTypes.DIRECTIVE && p.exp && p.name !== 'for') {
1968
+ for (let i = 1; i <= 2; i++)
1969
+ p.exp.content = p.exp.content.replace(new RegExp(`(?<![.\\w$])${tuple[i]}\\b`, 'g'), `${item}[${i - 1}]`);
1970
+ }
1971
+ n.children?.forEach(rewrite);
1972
+ };
1973
+ rewrite(copy);
1974
+ const dir = copy.props.find((p) => p.type === NodeTypes.DIRECTIVE && p.name === 'for');
1975
+ if (dir && 'exp' in dir && dir.exp && dir.exp.type === NodeTypes.SIMPLE_EXPRESSION)
1976
+ dir.exp.content = `${item} in ${tuple[3]}`;
1977
+ return walkElement(copy, classProps, comboRules, scopedRules, pseudoRules, warnings, state, compTable, propRefs, loopScope, ancestorClasses);
1978
+ }
1710
1979
  // R105:`v-for="x in stats"` 里的 stats 是 computed/普通变量(不是 ref 状态)
1711
1980
  // → 下沉成运行时求值的 JSON 数组状态,列表照常渲染
1712
1981
  let sunkList;
@@ -1771,7 +2040,7 @@ function walkElement(node, classProps, comboRules, scopedRules, pseudoRules, war
1771
2040
  // 表达式下沉产生的状态(_vrnE*)在 walk 之后才并入页面状态,这里放行
1772
2041
  // R143:未声明的数据源(组件 props / 数值 / 未注册变量)→ 运行时自适应下沉,不再整页失败
1773
2042
  if (vf.listState &&
1774
- !/^_vrnE\d+$/.test(vf.listState) &&
2043
+ !/^_vrnE\d+$/.test(vf.listState) && !vf.listState.startsWith('__vrnLoop:') &&
1775
2044
  !(state && state.states.some((s) => s.name === vf.listState)) &&
1776
2045
  propRefs === undefined // 页面模式才下沉;组件模式(Set 存在,含空)保留数据源原名
1777
2046
  ) {
@@ -1794,9 +2063,7 @@ function walkElement(node, classProps, comboRules, scopedRules, pseudoRules, war
1794
2063
  // 原生端无过渡动画/缓存语义,但内容必须保留(否则整页编译失败)
1795
2064
  if (['Transition', 'TransitionGroup', 'KeepAlive', 'Teleport', 'Suspense', 'Fragment'].includes(node.tag)) {
1796
2065
  const kids = [];
1797
- for (const child of node.children) {
1798
- kids.push(...walk(child, classProps, comboRules, scopedRules, pseudoRules, warnings, state, compTable, propRefs, loopScope, ancestorClasses));
1799
- }
2066
+ kids.push(...walkSiblings(node.children, (child) => walk(child, classProps, comboRules, scopedRules, pseudoRules, warnings, state, compTable, propRefs, loopScope, ancestorClasses)));
1800
2067
  if (kids.length === 0) {
1801
2068
  warnings.push(`<${node.tag}> 内无内容,已忽略`);
1802
2069
  return { type: 'group', props: { flexDirection: 'column' }, children: [{ type: 'text', props: { text: ' ' } }] };
@@ -1816,18 +2083,12 @@ function walkElement(node, classProps, comboRules, scopedRules, pseudoRules, war
1816
2083
  if (p.name === 'if' && p.exp && p.exp.type === NodeTypes.SIMPLE_EXPRESSION) {
1817
2084
  cond = p.exp.content.trim();
1818
2085
  }
1819
- else if (p.name === 'else' || p.name === 'else-if') {
1820
- // R15 不静默:v-else/v-else-if 不支持,静默渲染会恒显示该分支
1821
- warnings.push(`<template v-${p.name}> 暂不支持——该分支将无条件渲染(R15 降级表)`);
1822
- }
1823
- }
1824
- for (const child of node.children) {
1825
- children.push(...walk(child, classProps, comboRules, scopedRules, pseudoRules, warnings, state, compTable, propRefs, loopScope, ancestorClasses));
1826
2086
  }
2087
+ children.push(...walkSiblings(node.children, (child) => walk(child, classProps, comboRules, scopedRules, pseudoRules, warnings, state, compTable, propRefs, loopScope, ancestorClasses)));
1827
2088
  if (cond) {
1828
2089
  // 条件渲染容器:以 group 承载子节点(不参与布局,仅 if 分支)
1829
2090
  // sourceTag='template':CSS 中 <template> 透明,父容器 gap 可透传(R-G2)
1830
- return { type: 'group', props: { flexDirection: 'column' }, sourceTag: 'template', condition: normCond(cond), children };
2091
+ return { type: 'group', props: { flexDirection: 'column' }, sourceTag: 'template', condition: `__vrnCond:${addJsExprState(`String(${cond})`)}`, children };
1831
2092
  }
1832
2093
  // 无 v-if:透传(合并到父 children)
1833
2094
  // 透传多节点时由调用方展开;此处约定 template 内单根(Vue 本身要求多根可由父承载)
@@ -1837,12 +2098,122 @@ function walkElement(node, classProps, comboRules, scopedRules, pseudoRules, war
1837
2098
  throw new VueCompileError('<template> 无内容');
1838
2099
  return { type: 'group', props: { flexDirection: 'column' }, children };
1839
2100
  }
1840
- const mapping = TAG_MAP[node.tag];
2101
+ let mapping = TAG_MAP[node.tag];
2102
+ if (inlineComponentsFlag && node.tag === 'button' && node.children.some((c) => c.type === NodeTypes.ELEMENT && c.tag !== 'svg' && c.tag !== 'br'))
2103
+ mapping = { type: 'group' };
1841
2104
  if (!mapping) {
1842
2105
  // R41 组件使用点:查组件表(精确名 + kebab→Pascal 兜底)→ component IR 节点(独立 View 引用)
1843
2106
  const comp = compTable.find((c) => c.name === node.tag) ??
1844
2107
  compTable.find((c) => c.name === toPascalCase(node.tag));
1845
2108
  if (comp) {
2109
+ if (inlineComponentsFlag) {
2110
+ if (componentPath.includes(comp.name))
2111
+ throw new VueCompileError(`组件递归引用无法静态展开:${[...componentPath, comp.name].join(' → ')}`);
2112
+ const bindings = {};
2113
+ const script = comp.descriptor.scriptSetup?.content ?? '';
2114
+ const defaults = script.match(/defineProps[\s\S]*?>\s*\(\s*\)\s*,\s*\{([\s\S]*?)\}/)?.[1] ?? '';
2115
+ for (const pair of splitTopLevel(defaults)) {
2116
+ const m = pair.match(/^\s*(\w+)\s*:\s*([\s\S]+)$/);
2117
+ if (m)
2118
+ bindings[m[1]] = m[2].trim();
2119
+ }
2120
+ for (const name of comp.propNames)
2121
+ bindings[name] ??= "''";
2122
+ for (const p of node.props) {
2123
+ if (p.type === NodeTypes.ATTRIBUTE && comp.propNames.has(p.name))
2124
+ bindings[p.name] = p.value ? JSON.stringify(p.value.content) : 'true';
2125
+ if (p.type === NodeTypes.DIRECTIVE && p.name === 'bind' && p.arg?.type === NodeTypes.SIMPLE_EXPRESSION && p.exp?.type === NodeTypes.SIMPLE_EXPRESSION)
2126
+ bindings[p.arg.content] = p.exp.content;
2127
+ }
2128
+ const prefix = `_vrnC${componentScopeSeq++}_`;
2129
+ for (const st of extractScreenState(script).states) {
2130
+ const name = prefix + st.name;
2131
+ bindings[st.name] = name;
2132
+ state?.states.push({ ...st, name });
2133
+ jsExprStateNames.push(name);
2134
+ }
2135
+ const rewrite = (expression) => {
2136
+ const edits = [];
2137
+ let ast;
2138
+ try {
2139
+ ast = babelParse(`(${expression})`, { sourceType: 'module', plugins: ['typescript'] });
2140
+ }
2141
+ catch (error) {
2142
+ throw new VueCompileError(`组件 ${comp.name} 表达式解析失败:${expression.slice(0, 500)};${error.message}`);
2143
+ }
2144
+ walkIdentifiers(ast.program, (id, parent, _stack, isReference, isLocal) => {
2145
+ if (!isReference || isLocal)
2146
+ return;
2147
+ if (id.name === 'props' && parent?.type === 'MemberExpression' && !parent.computed && parent.property.type === 'Identifier' && bindings[parent.property.name] !== undefined) {
2148
+ edits.push({ start: parent.start - 1, end: parent.end - 1, value: `(${bindings[parent.property.name]})` });
2149
+ }
2150
+ else if (bindings[id.name] !== undefined) {
2151
+ const member = parent?.type === 'MemberExpression' && parent.object === id && !parent.computed && parent.property.type === 'Identifier' && parent.property.name === 'value' ? parent : id;
2152
+ edits.push({ start: member.start - 1, end: member.end - 1, value: parent?.type === 'ObjectProperty' && parent.shorthand ? `${id.name}: (${bindings[id.name]})` : `(${bindings[id.name]})` });
2153
+ }
2154
+ });
2155
+ let out = expression;
2156
+ for (const e of edits.sort((a, b) => b.start - a.start))
2157
+ out = out.slice(0, e.start) + e.value + out.slice(e.end);
2158
+ return out;
2159
+ };
2160
+ const scriptAst = babelParse(script, { sourceType: 'module', plugins: ['typescript'] });
2161
+ const asJs = (value) => transformSync('(' + value + ')', { loader: 'ts', target: 'es2019' }).code.trim().replace(/;$/, '');
2162
+ for (const declaration of scriptAst.program.body) {
2163
+ if (declaration.type === 'FunctionDeclaration' && declaration.id) {
2164
+ bindings[declaration.id.name] = rewrite(asJs(script.slice(declaration.start, declaration.end)));
2165
+ }
2166
+ if (declaration.type !== 'VariableDeclaration')
2167
+ continue;
2168
+ for (const d of declaration.declarations) {
2169
+ if (d.id.type !== 'Identifier' || !d.init || bindings[d.id.name] !== undefined)
2170
+ continue;
2171
+ if (['NumericLiteral', 'StringLiteral', 'BooleanLiteral', 'ObjectExpression', 'ArrayExpression'].includes(d.init.type))
2172
+ bindings[d.id.name] = rewrite(asJs(script.slice(d.init.start, d.init.end)));
2173
+ else if (d.init.type === 'CallExpression' && d.init.callee.type === 'Identifier' && d.init.callee.name === 'computed') {
2174
+ const arg = d.init.arguments[0];
2175
+ if (arg?.type === 'ArrowFunctionExpression')
2176
+ bindings[d.id.name] = rewrite('(' + asJs(script.slice(arg.start, arg.end)) + ')()');
2177
+ }
2178
+ }
2179
+ }
2180
+ for (const key of Object.keys(bindings))
2181
+ bindings[key] = rewrite(bindings[key]);
2182
+ const roots = structuredClone(comp.descriptor.template.ast.children);
2183
+ const patch = (n) => {
2184
+ if (n.type === NodeTypes.INTERPOLATION)
2185
+ n.content.content = rewrite(n.content.content);
2186
+ for (const p of n.props ?? [])
2187
+ if (p.type === NodeTypes.DIRECTIVE && p.exp) {
2188
+ const event = p.exp.content.match(/^emit\(\s*['"]([^'"]+)['"]\s*\)$/);
2189
+ const handler = event ? node.props.find((d) => d.type === NodeTypes.DIRECTIVE && d.name === 'on' && d.arg?.type === NodeTypes.SIMPLE_EXPRESSION && d.arg.content === event[1]) : undefined;
2190
+ p.exp.content = handler && 'exp' in handler && handler.exp?.type === NodeTypes.SIMPLE_EXPRESSION ? handler.exp.content : rewrite(p.exp.content);
2191
+ }
2192
+ n.children?.forEach(patch);
2193
+ };
2194
+ roots.forEach(patch);
2195
+ componentSlots.push((name) => {
2196
+ const children = node.children.flatMap((child) => {
2197
+ if (child.type === NodeTypes.ELEMENT && child.tag === 'template') {
2198
+ const slot = child.props.find((p) => p.type === NodeTypes.DIRECTIVE && p.name === 'slot');
2199
+ if (slot && slot.type === NodeTypes.DIRECTIVE && slot.arg?.type === NodeTypes.SIMPLE_EXPRESSION && slot.arg.content === name)
2200
+ return child.children;
2201
+ return [];
2202
+ }
2203
+ return name === 'default' ? [child] : [];
2204
+ });
2205
+ return walkSiblings(children, (child) => walk(child, classProps, comboRules, scopedRules, pseudoRules, warnings, state, compTable, propRefs, loopScope, ancestorClasses));
2206
+ });
2207
+ componentPath.push(comp.name);
2208
+ try {
2209
+ const kids = walkSiblings(roots, (child) => walk(child, comp.classProps, comp.comboRules, comp.scopedRules, comp.pseudoRules, warnings, state, compTable, undefined, loopScope, ancestorClasses));
2210
+ return kids.length === 1 ? kids[0] : { type: 'group', props: { flexDirection: 'column' }, children: kids };
2211
+ }
2212
+ finally {
2213
+ componentPath.pop();
2214
+ componentSlots.pop();
2215
+ }
2216
+ }
1846
2217
  const pos = compPosition.get(comp.name) ?? {};
1847
2218
  return componentUseNode(node, comp, pos, warnings);
1848
2219
  }
@@ -1856,6 +2227,10 @@ function walkElement(node, classProps, comboRules, scopedRules, pseudoRules, war
1856
2227
  let nodeType;
1857
2228
  // 指令检测:v-for / v-if / v-show / :style / :class / 事件绑定 → 结构或逻辑绑定
1858
2229
  const props = { ...(mapping.defaultProps ?? {}) };
2230
+ const inlineAttr = node.props.find((p) => p.type === NodeTypes.ATTRIBUTE && p.name === 'style');
2231
+ const inlineStyle = inlineAttr && 'value' in inlineAttr ? inlineAttr.value?.content ?? '' : '';
2232
+ const resolvedStyle = resolveCssRules(styleRuleTables.get(classProps) ?? [], cssElement(node), ancestorClasses, inlineStyle, warnings);
2233
+ mergeIrProps(props, resolvedStyle);
1859
2234
  let cond;
1860
2235
  let activeWhen;
1861
2236
  let activeProps;
@@ -1875,7 +2250,9 @@ function walkElement(node, classProps, comboRules, scopedRules, pseudoRules, war
1875
2250
  if (name === 'show') {
1876
2251
  // M5:v-show → show prop(始终渲染占位 + opacity 动画切换;区别于 v-if 条件渲染)
1877
2252
  if (p.exp && p.exp.type === NodeTypes.SIMPLE_EXPRESSION && p.exp.content.trim()) {
1878
- props.show = normCond(p.exp.content.trim());
2253
+ const raw = p.exp.content.trim();
2254
+ const flat = flattenStorePath(raw);
2255
+ props.show = /^[A-Za-z_$][\w$]*$/.test(flat) && state?.states.some((s) => s.name === flat) ? flat : addJsExprState(`String(Boolean(${raw}))`);
1879
2256
  continue;
1880
2257
  }
1881
2258
  throw new VueCompileError('v-show 需要非空条件表达式');
@@ -1905,7 +2282,11 @@ function walkElement(node, classProps, comboRules, scopedRules, pseudoRules, war
1905
2282
  }
1906
2283
  }
1907
2284
  else if (mapping.type === 'image' && argName === 'src') {
1908
- warnings.push(':src 动态绑定暂不支持,已忽略(请改用静态 src)');
2285
+ const source = p.exp && p.exp.type === NodeTypes.SIMPLE_EXPRESSION ? p.exp.content.trim() : '';
2286
+ if (templateAssets[source])
2287
+ props.src = templateAssets[source];
2288
+ else
2289
+ warnings.push(`:src ${source} 尚无原生图片映射,需提供 assets 或静态 src`);
1909
2290
  }
1910
2291
  else {
1911
2292
  warnings.push(`:${argName} 动态绑定暂不支持,已忽略`);
@@ -1935,7 +2316,7 @@ function walkElement(node, classProps, comboRules, scopedRules, pseudoRules, war
1935
2316
  // @click="handler" → 状态赋值(setter 内联)优先;否则保留 onClick(JS 运行时调用)
1936
2317
  // M3 规范化:无参调用 `inc()` → `inc`(Swift invoke 统一 evaluateScript("<名>()"));带参保留原样
1937
2318
  if (event === 'click' && handler) {
1938
- const set = state && resolveSetCall(handler, state, (m) => warnings.push(m));
2319
+ const set = !pageScriptBundleFlag && state && resolveSetCall(handler, state, (m) => warnings.push(m));
1939
2320
  if (set) {
1940
2321
  Object.assign(props, { onSet: set });
1941
2322
  }
@@ -1955,41 +2336,15 @@ function walkElement(node, classProps, comboRules, scopedRules, pseudoRules, war
1955
2336
  const attrValue = p.value?.content;
1956
2337
  if (attrName === 'class' && attrValue) {
1957
2338
  elementClasses = attrValue.split(/\s+/).filter(Boolean);
1958
- // 组合规则匹配:元素同时具备全部类时覆盖(优先级最高)
1959
- const matchedCombos = comboRules.filter((r) => r.classes.every((c) => elementClasses.includes(c)));
1960
- const comboMembers = new Set(matchedCombos.flatMap((r) => r.classes));
1961
- for (const cls of elementClasses) {
1962
- const p2 = classProps.get(cls);
1963
- if (p2)
1964
- mergeIrProps(props, p2);
1965
- else if (!comboMembers.has(cls)) {
1966
- warnings.push(`class「${cls}」无对应样式规则,已忽略`);
1967
- }
1968
- }
1969
- for (const rule of matchedCombos)
1970
- mergeIrProps(props, rule.props);
1971
- // M5 后代/子选择器:目标类命中 + 祖先链匹配(direct:直接父含最近祖先类;后代:祖先链上存在)
1972
- const matchedScoped = scopedRules.filter((r) => {
1973
- if (!elementClasses.includes(r.target))
1974
- return false;
1975
- if (r.direct) {
1976
- const parentCls = (ancestorClasses[0] ?? '').split(/\s+/).filter(Boolean);
1977
- return r.ancestors.every((a) => parentCls.includes(a));
1978
- }
1979
- const chain = ancestorClasses.join(' ');
1980
- return r.ancestors.every((a) => chain.split(/\s+/).includes(a));
1981
- });
1982
- for (const rule of matchedScoped)
1983
- mergeIrProps(props, rule.props);
1984
2339
  // M5 伪类按压态:.btn:active → pressBg(button 生成按压样式;:hover 在 iOS 无意义,仅取 active)
1985
2340
  for (const cls of elementClasses) {
1986
2341
  const pa = pseudoRules.find((r) => r.cls === cls && r.pseudo === 'active');
1987
- if (pa)
2342
+ if (pa && mapping.type === 'button')
1988
2343
  mergeIrProps(props, pa.props);
1989
2344
  }
1990
2345
  }
1991
2346
  else if (attrName === 'style' && attrValue) {
1992
- mergeIrProps(props, parseInlineStyle(attrValue, warnings));
2347
+ // 已按 CSS 级联优先级统一处理,避免属性顺序影响样式。
1993
2348
  }
1994
2349
  else if (mapping.type === 'image' && attrName === 'src' && attrValue) {
1995
2350
  props.src = attrValue;
@@ -2004,15 +2359,22 @@ function walkElement(node, classProps, comboRules, scopedRules, pseudoRules, war
2004
2359
  warnings.push(`属性 ${attrName}="${attrValue}" 暂不支持,已忽略`);
2005
2360
  }
2006
2361
  }
2362
+ const inlineTags = new Set(['span', 'b', 'strong', 'i', 'em', 'small', 'u', 'code']);
2363
+ const content = node.children.filter((c) => c.type !== NodeTypes.COMMENT && !(c.type === NodeTypes.TEXT && !c.content.trim()));
2364
+ const readable = (n) => n.type === NodeTypes.INTERPOLATION || (n.type === NodeTypes.TEXT && !!n.content.trim()) || (n.type === NodeTypes.ELEMENT && n.children.some(readable));
2365
+ if (inlineComponentsFlag && mapping.type === 'group' && props.flexDirection === undefined && props.gridColumns === undefined && content.some(readable) && content.every((c) => c.type === NodeTypes.TEXT || c.type === NodeTypes.INTERPOLATION || (c.type === NodeTypes.ELEMENT && inlineTags.has(c.tag))))
2366
+ mapping = { type: 'text' };
2367
+ if (mapping.type === 'text' && content.length === 0 && ['i', 'span', 'b'].includes(node.tag))
2368
+ mapping = { type: 'spacer' };
2007
2369
  // R108:button 只接受 label/bind —— 布局属性丢弃、缺 label 时占位(避免整页失败)
2008
2370
  if (mapping.type === 'button') {
2009
- const badProps = ['flexDirection', 'justifyContent', 'alignItems', 'gap', 'flex', 'textAlign', 'lineHeight', 'overflow', 'clip'];
2371
+ const badProps = ['flexDirection', 'justifyContent', 'alignItems', 'gap', 'flex', 'textAlign', 'lineHeight', 'lineHeightMultiplier', 'overflow', 'clip'];
2010
2372
  for (const k of badProps)
2011
2373
  delete props[k];
2012
2374
  }
2013
2375
  if (mapping.type === 'input') {
2014
2376
  // R108:input 只接受 value/placeholder 等,文本样式属性一律丢弃
2015
- for (const k of ['textAlign', 'lineHeight', 'fontWeight', 'flexDirection', 'justifyContent', 'alignItems']) {
2377
+ for (const k of ['textAlign', 'lineHeight', 'lineHeightMultiplier', 'fontWeight', 'flexDirection', 'justifyContent', 'alignItems']) {
2016
2378
  delete props[k];
2017
2379
  }
2018
2380
  }
@@ -2022,8 +2384,39 @@ function walkElement(node, classProps, comboRules, scopedRules, pseudoRules, war
2022
2384
  // 组件 View 内 {{ prop }}(propRefs)→ 单插值时可整体作为 text ref(Text(prop))
2023
2385
  // - 其余(group/image):递归 walk
2024
2386
  const children = [];
2025
- let prevCondExpr;
2026
- if (mapping.type === 'text') {
2387
+ if (mapping.type === 'text' && node.children.some((c) => c.type === NodeTypes.ELEMENT && inlineTags.has(c.tag))) {
2388
+ const runs = [];
2389
+ for (const child of node.children) {
2390
+ if (child.type === NodeTypes.COMMENT)
2391
+ continue;
2392
+ if (child.type === NodeTypes.TEXT) {
2393
+ runs.push({ text: child.content });
2394
+ continue;
2395
+ }
2396
+ if (child.type === NodeTypes.ELEMENT && child.tag === 'br') {
2397
+ runs.push({ text: '\n' });
2398
+ continue;
2399
+ }
2400
+ const nodes = walk(child, classProps, comboRules, scopedRules, pseudoRules, warnings, state, compTable, propRefs, loopScope, [...ancestorClasses, cssElement(node)]);
2401
+ for (const n of nodes) {
2402
+ if (n.type !== 'text') {
2403
+ warnings.push(`行内元素 ${node.tag} 的非文本子节点需额外布局,已保留可读文字`);
2404
+ continue;
2405
+ }
2406
+ const run = { ...n.props };
2407
+ if (inheritedTextStyles.get(n)?.fontSize === undefined && n.props?.fontSize === 14)
2408
+ delete run.fontSize;
2409
+ if (n.bind)
2410
+ run.bind = n.bind.key;
2411
+ runs.push(run);
2412
+ }
2413
+ }
2414
+ if (runs.length)
2415
+ props.textRuns = runs;
2416
+ else
2417
+ props.text = '';
2418
+ }
2419
+ else if (mapping.type === 'text') {
2027
2420
  let text = '';
2028
2421
  let refOnly;
2029
2422
  let exprOnly;
@@ -2035,6 +2428,11 @@ function walkElement(node, classProps, comboRules, scopedRules, pseudoRules, war
2035
2428
  parts.push({ text: child.content });
2036
2429
  }
2037
2430
  else if (child.type === NodeTypes.INTERPOLATION) {
2431
+ const rawExpression = textOf(child.content).trim();
2432
+ if (activeLoops.length && !/^[A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*)*$/.test(rawExpression)) {
2433
+ parts.push({ bind: addJsExprState(rawExpression) });
2434
+ continue;
2435
+ }
2038
2436
  const name = flattenStorePath(normalizeTemplateExpr(textOf(child.content).trim(), warnings));
2039
2437
  // 动态列表项属性 {{ item.field }}:基名(dot[0])命中循环变量才成立
2040
2438
  const dot = name.split('.');
@@ -2177,6 +2575,14 @@ function walkElement(node, classProps, comboRules, scopedRules, pseudoRules, war
2177
2575
  else {
2178
2576
  for (const child of node.children) {
2179
2577
  if (mapping.type === 'button') {
2578
+ if (child.type === NodeTypes.ELEMENT && child.tag === 'svg') {
2579
+ const rules = styleRuleTables.get(classProps) ?? [];
2580
+ const parents = [...ancestorClasses, cssElement(node)];
2581
+ const style = resolveCssRules(rules, cssElement(child), parents, '', warnings);
2582
+ props.icon = { vector: parseSvg(child, (el, path) => resolveCssRules(rules, cssElement(el), [...parents, ...path.map(cssElement)], '', warnings), warnings, addJsExprState),
2583
+ width: typeof style.width === 'number' ? style.width : 20, height: typeof style.height === 'number' ? style.height : 20 };
2584
+ continue;
2585
+ }
2180
2586
  if (child.type === NodeTypes.ELEMENT && (child.tag === 'br' || !elementInnerText(child).trim()))
2181
2587
  continue; // R106/R107:按钮内换行/纯图标忽略
2182
2588
  if (child.type === NodeTypes.TEXT && child.content.trim()) {
@@ -2226,56 +2632,40 @@ function walkElement(node, classProps, comboRules, scopedRules, pseudoRules, war
2226
2632
  }
2227
2633
  continue;
2228
2634
  }
2229
- // R128:v-else / v-else-if 与前一兄弟互补(此前无条件渲染,空状态块常显)
2230
- const __kids = walk(child, classProps, comboRules, scopedRules, pseudoRules, warnings, state, compTable, propRefs, loopScope, [
2231
- ...ancestorClasses,
2232
- ...elementClasses,
2233
- ]);
2234
- if (child.type === NodeTypes.ELEMENT) {
2235
- const dirs = child.props.filter((p2) => p2.type === NodeTypes.DIRECTIVE);
2236
- const isElse = dirs.some((d) => d.name === 'else');
2237
- const elseIf = dirs.find((d) => d.name === 'else-if');
2238
- if (isElse || elseIf) {
2239
- const prev = prevCondExpr;
2240
- const own = elseIf?.exp?.content?.trim();
2241
- if (prev) {
2242
- const condExpr = own ? `(${prev}) && (${own})` : `!(${prev})`;
2243
- const stName = addJsExprState(`String(${condExpr})`);
2244
- for (const k of __kids)
2245
- k.condition = `__vrnCond:${stName}`;
2246
- }
2247
- }
2248
- const ifDir = dirs.find((d) => d.name === 'if');
2249
- if (ifDir?.exp?.content)
2250
- prevCondExpr = ifDir.exp.content.trim();
2251
- }
2252
- children.push(...__kids);
2253
- }
2254
- }
2255
- // R108:文本节点丢弃布局类属性(原生端 width/height/flexDirection 只对容器有意义)
2256
- if (mapping.type === 'text') {
2257
- const LAYOUT_ONLY = [
2258
- 'height', 'width', 'flexDirection', 'justifyContent', 'alignItems', 'flex', 'gap',
2259
- 'clip', 'overflow', 'marginBottom', 'marginRight', 'marginLeft', 'borderSides',
2260
- 'radius', 'opacity', 'position', 'absTop', 'absLeft', 'absRight', 'absBottom',
2261
- ];
2262
- const dropped = LAYOUT_ONLY.filter((k) => props[k] !== undefined);
2263
- if (dropped.length > 0) {
2264
- for (const k of dropped)
2265
- delete props[k];
2266
- warnings.push(`<${node.tag}> 是文本节点,布局属性(${dropped.join('/')})已忽略(R108)`);
2267
- }
2268
- }
2269
- // R88 续:容器节点不接受文本专属样式(color/fontSize/fontWeight/textAlign/lineHeight)
2270
- // 原生端容器没有「字体继承」,这类属性会直接触发 IR 校验拒收 → 明确降级 + 警告
2271
- if (mapping.type === 'group' || mapping.type === 'for') {
2272
- const TEXT_ONLY = ['color', 'fontSize', 'fontWeight', 'textAlign', 'lineHeight'];
2273
- const dropped = TEXT_ONLY.filter((k) => props[k] !== undefined);
2274
- if (dropped.length > 0) {
2275
- for (const k of dropped)
2276
- delete props[k];
2277
- warnings.push(`<${node.tag}> 是容器,文本样式(${dropped.join('/')})在原生端不继承 → 已忽略;请把样式下沉到具体文本节点(R88)`);
2278
2635
  }
2636
+ if (mapping.type !== 'button') {
2637
+ children.push(...walkSiblings(node.children, (child) => walk(child, classProps, comboRules, scopedRules, pseudoRules, warnings, state, compTable, propRefs, loopScope, [...ancestorClasses, cssElement(node)])));
2638
+ }
2639
+ }
2640
+ // 静态装饰伪元素生成原生盒子,保持相对于所属元素的层级。
2641
+ if (mapping.type === 'group' && children.length) {
2642
+ const rules = styleRuleTables.get(classProps) ?? [];
2643
+ for (const marker of ['__vrnPseudoBefore', '__vrnPseudoAfter']) {
2644
+ const decorated = rules.filter((rule) => rule.selector.at(-1)?.compound === '.' + marker);
2645
+ if (!decorated.length)
2646
+ continue;
2647
+ const pseudo = resolveCssRules(decorated, { tag: 'span', classes: [marker] }, [...ancestorClasses, cssElement(node)], '', warnings);
2648
+ if (!Object.keys(pseudo).length)
2649
+ continue;
2650
+ const layer = { type: 'spacer', props: pseudo };
2651
+ const foreground = children.findIndex((child) => (child.props?.zIndex ?? 0) > 0);
2652
+ children.splice(foreground >= 0 ? foreground : marker === '__vrnPseudoBefore' ? 0 : children.length, 0, layer);
2653
+ }
2654
+ }
2655
+ // 文本节点保留已有双端映射的盒模型属性,只删除容器布局中转字段。
2656
+ if (mapping.type === 'text' || mapping.type === 'spacer') {
2657
+ for (const k of ['flexDirection', 'justifyContent', 'alignItems', 'gap', 'overflow', 'gridColumns'])
2658
+ delete props[k];
2659
+ }
2660
+ const typography = {};
2661
+ for (const k of TEXT_STYLE_KEYS)
2662
+ if (props[k] !== undefined)
2663
+ typography[k] = props[k];
2664
+ if (['p', 'span', 'b', 'strong', 'i', 'em', 'u', 'option'].includes(node.tag) && resolvedStyle.fontSize === undefined)
2665
+ delete typography.fontSize;
2666
+ if (mapping.type === 'group' || mapping.type === 'for' || mapping.type === 'spacer') {
2667
+ for (const k of TEXT_STYLE_KEYS)
2668
+ delete props[k];
2279
2669
  }
2280
2670
  // R108 兜底:按钮最终必须有可见文案(label 或 bind),否则整页校验失败
2281
2671
  if (mapping.type === 'button' && props.label === undefined && props.bind === undefined) {
@@ -2287,13 +2677,13 @@ function walkElement(node, classProps, comboRules, scopedRules, pseudoRules, war
2287
2677
  warnings.push(`<${node.tag}> 无子节点 → 降级为占位 spacer(R17/R77;可用 padding/height 表达留白)`);
2288
2678
  const keep = {};
2289
2679
  const src = props;
2290
- for (const k of ['width', 'height', 'flex', 'marginTop', 'background']) {
2680
+ for (const k of ['width', 'height', 'widthFraction', 'widthFractionDynamic', 'flex', 'marginTop', 'background', 'gradient', 'radius', 'shadow', 'borderWidth', 'borderColor', 'padding', 'minHeight', 'minWidth', 'opacity']) {
2291
2681
  if (src[k] !== undefined)
2292
2682
  keep[k] = src[k];
2293
2683
  }
2294
2684
  const spacer = { type: 'spacer', props: keep };
2295
2685
  if (cond)
2296
- spacer.condition = normCond(cond);
2686
+ spacer.condition = pageScriptBundleFlag ? `__vrnCond:${addJsExprState(`String(Boolean(${cond}))`)}` : normCond(cond);
2297
2687
  return spacer;
2298
2688
  }
2299
2689
  // overflow:auto → scroll 节点(消费中转标记,避免进入 schema 校验白名单)
@@ -2303,6 +2693,7 @@ function walkElement(node, classProps, comboRules, scopedRules, pseudoRules, war
2303
2693
  delete propsAny.overflow;
2304
2694
  }
2305
2695
  const irNode = { type: (nodeType ?? mapping.type), props };
2696
+ inheritedTextStyles.set(irNode, typography);
2306
2697
  if (children.length > 0)
2307
2698
  irNode.children = children;
2308
2699
  if (cond) {
@@ -2317,14 +2708,16 @@ function walkElement(node, classProps, comboRules, scopedRules, pseudoRules, war
2317
2708
  /^![A-Za-z_$][\w$]*(\.[A-Za-z_$][\w$]*)?$/.test(nc) ||
2318
2709
  /^[A-Za-z_$][\w$]*(\.length)?\s*(==|!=|>|<|>=|<=)\s*('.*'|".*"|-?\d+)$/.test(nc);
2319
2710
  const usesLoopVar = activeLoops.some((l) => new RegExp(`\\b${l.itemKey}\\b`).test(nc));
2320
- if (!simpleCond && !usesLoopVar) {
2321
- const stName = addJsExprState(`String(${nc})`);
2711
+ if (pageScriptBundleFlag || !simpleCond || usesLoopVar) {
2712
+ const stName = addJsExprState(`String(Boolean(${cond}))`);
2322
2713
  irNode.condition = `__vrnCond:${stName}`;
2323
2714
  }
2324
2715
  else {
2325
2716
  irNode.condition = ownerKnown && mCond ? `__vrnField__:${mCond[1]}:${mCond[2]}` : nc;
2326
2717
  }
2327
2718
  }
2719
+ if (activeWhen && activeLoops.length)
2720
+ activeWhen = '__vrnCond:' + addJsExprState(`String(Boolean(${activeWhen}))`);
2328
2721
  if (activeWhen && activeProps) {
2329
2722
  irNode.active = { when: activeWhen, props: activeProps };
2330
2723
  // active 的 color 注入所有后代 text(选中态金色覆盖子元素显式色)