@dongmingzhixiu/vuexir-compiler 0.5.4 → 0.5.6

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.
@@ -1542,9 +1542,16 @@ function parseRouter(src, dir, routerFile) {
1542
1542
  }
1543
1543
  return cands.find((c) => existsSync(c)) ?? cands[0];
1544
1544
  };
1545
- // 配平扫描所有「含 path:」的对象块
1545
+ // 配平扫描所有「含 path:」的对象块(从 routes: [...] 数组起点开始,避免把 createRouter 顶层对象整块当路由)
1546
1546
  const blocks = [];
1547
- for (let i = 0; i < src.length; i += 1) {
1547
+ let scanFrom = 0;
1548
+ const routesMarker = src.indexOf('routes:');
1549
+ if (routesMarker !== -1) {
1550
+ const arrIdx = src.indexOf('[', routesMarker);
1551
+ if (arrIdx !== -1)
1552
+ scanFrom = arrIdx;
1553
+ }
1554
+ for (let i = scanFrom; i < src.length; i += 1) {
1548
1555
  if (src[i] !== '{')
1549
1556
  continue;
1550
1557
  let depth = 0;
@@ -1609,7 +1616,7 @@ function parseRouter(src, dir, routerFile) {
1609
1616
  }
1610
1617
  const hasText = (file) => {
1611
1618
  if (!file || !existsSync(file))
1612
- return true;
1619
+ return false;
1613
1620
  const tpl = readFileSync(file, 'utf-8').match(/<template>([\s\S]*)<\/template>/)?.[1] ?? '';
1614
1621
  return tpl.replace(/<[^>]*>/g, ' ').replace(/\s+/g, ' ').trim().length > 0;
1615
1622
  };
@@ -1623,7 +1630,7 @@ function parseRouter(src, dir, routerFile) {
1623
1630
  path: r.path,
1624
1631
  }));
1625
1632
  const details = routes
1626
- .filter((r) => r.meta.tab !== 'true')
1633
+ .filter((r) => r.meta.tab !== 'true' && hasText(r.file))
1627
1634
  .map((r) => ({ id: r.name, title: r.meta.title ?? r.name, file: r.file, path: r.path }));
1628
1635
  if (tabs.length === 0) {
1629
1636
  warnings.push('router 未解析到任何 tab 路由(routes 需含 meta: { tab: true })');
@@ -49,6 +49,7 @@ export declare function generateScreenSource(screen: IrScreen, opts?: {
49
49
  colorNames: Map<string, string>;
50
50
  fontNames: Map<string, string>;
51
51
  };
52
+ cssVars?: Map<string, string>;
52
53
  /** M4a 多页:页面运行时引用(默认 Runtime.shared 单例;多页传 Runtime.forPage("<id>") 隔离 JS 上下文) */
53
54
  runtime?: string;
54
55
  /** M4b Tabs:状态提升到容器(页面用 @Binding、不生成 onAppear;绑定+onMounted 由容器统一驱动) */
@@ -68,4 +69,5 @@ export declare function generateComponentSource(name: string, ui: IrNode, opts?:
68
69
  colorNames: Map<string, string>;
69
70
  fontNames: Map<string, string>;
70
71
  };
72
+ cssVars?: Map<string, string>;
71
73
  }): string;
@@ -22,7 +22,16 @@
22
22
  * flex 仅支持容器主轴方向的 maxHeight/maxWidth:.infinity。
23
23
  */
24
24
  /* ============ 颜色表达式 ============ */
25
+ let currentCssVars;
25
26
  function parseColor(c) {
27
+ const trimmed = c.trim();
28
+ if (trimmed.startsWith('var(')) {
29
+ // R-THEME:var(--x) 保留语义 → ThemeColor.color("x"),皮肤切换时原生颜色跟随
30
+ const m = trimmed.match(/^var\(\s*(--[\w-]+)/);
31
+ if (m)
32
+ return { mode: 'theme', name: m[1].slice(2), r: 0, g: 0, b: 0, a: 1 };
33
+ return { r: 0, g: 0, b: 0, a: 1 };
34
+ }
26
35
  if (c.startsWith('#')) {
27
36
  let hex = c.slice(1);
28
37
  if (hex.length === 3)
@@ -36,7 +45,10 @@ function parseColor(c) {
36
45
  return { r: 0, g: 0, b: 0, a: 1 };
37
46
  }
38
47
  function colorExpr(c) {
39
- const { r, g, b, a } = parseColor(c);
48
+ const p = parseColor(c);
49
+ if (p.mode === 'theme' && p.name)
50
+ return `ThemeColor.color("${p.name}")`;
51
+ const { r, g, b, a } = p;
40
52
  // R42:色板已收集时 → Theme.<name> 引用(抽离重复颜色字面量)
41
53
  const name = currentColorNames?.get(rgbHex(r, g, b));
42
54
  const base = name !== undefined ? `Theme.${name}` : `Color(red: ${r}/255.0, green: ${g}/255.0, blue: ${b}/255.0)`;
@@ -276,6 +288,7 @@ function toSwiftIdentifier(id) {
276
288
  const FW_MAP = { normal: '.regular', medium: '.medium', bold: '.bold' };
277
289
  const ALIGN_MAP = { start: '.leading', center: '.center', end: '.trailing', left: '.leading', right: '.trailing' };
278
290
  const FLEX_START_MAP = { start: '.leading', center: '.center', end: '.trailing' };
291
+ const V_ALIGN_MAP = { start: '.top', center: '.center', end: '.bottom' };
279
292
  /** R-H:节点自身高度是否确定(数字 height / height:'fill';或 flex 子项且父链确定) */
280
293
  function heightDefinite(props, parentDefinite) {
281
294
  // 'fill' = 撑满父高(root 高度固定,fill 仅出现在 root 内层包裹 → 高度确定链成立)
@@ -516,10 +529,15 @@ function emitNode(node, ctx) {
516
529
  }
517
530
  /** JS 条件表达式 → Swift 兼容:===/!== 为 JS 严格比较,Swift 用 ==/!=;字符串字面量单引号 → 双引号 */
518
531
  function swiftCondExpr(expr, known) {
519
- // R128:下沉的条件状态 → 判真
532
+ // R128:下沉的条件状态 → 判真;不在已知集(组件/无状态上下文)→ true 保留内容
520
533
  const sunk = expr.match(/^__vrnCond:(\w+)$/);
521
- if (sunk)
522
- return `vrnTruthy(${sunk[1]})`;
534
+ if (sunk) {
535
+ const nm = sunk[1];
536
+ const kset = known && known.size > 0 ? known : currentStateNames;
537
+ if (kset && kset.size > 0 && !kset.has(nm))
538
+ return 'true';
539
+ return `vrnTruthy(${nm})`;
540
+ }
523
541
  // R114:条件引用原生端不存在的标识符 → true(保留内容),避免整页编译失败
524
542
  const knownSet = known && known.size > 0 ? known : currentStateNames;
525
543
  if (knownSet && knownSet.size > 0) {
@@ -735,7 +753,7 @@ function emitNodeInner(node, ctx) {
735
753
  // CSS align-items 默认 stretch(子项拉伸到容器交叉轴全宽),SwiftUI VStack 无 stretch
736
754
  // → 子项显式 .frame(maxWidth:.infinity) 实现(内容仍左对齐)
737
755
  const alignRaw = props.alignItems ?? 'stretch';
738
- const align = isRow ? (ALIGN_MAP[alignRaw] ?? '.top') : (FLEX_START_MAP[alignRaw] ?? '.leading');
756
+ const align = isRow ? (V_ALIGN_MAP[alignRaw] ?? '.top') : (FLEX_START_MAP[alignRaw] ?? '.leading');
739
757
  const stack = hasAbsolute ? 'ZStack' : isRow ? 'HStack' : 'VStack';
740
758
  const stackAlign = hasAbsolute ? '.topLeading' : align;
741
759
  // R132:ZStack 里必须把「非绝对定位」的兄弟包成一个流式子容器,
@@ -881,7 +899,7 @@ function emitNodeInner(node, ctx) {
881
899
  // M3:onSet 内联赋值后推送 stateStore(JS 侧状态同步,供 handler 读取)
882
900
  if (props.onSet || props.onClick) {
883
901
  // R-VFOBJ:onClick 含循环变量字段(router.push(item.path))→ Swift 内联展开(JS 无 item 作用域)
884
- const loopAction = props.onClick ? loopClickAction(props.onClick, ctx.loop) : null;
902
+ const loopAction = props.onClick ? loopClickAction(props.onClick, ctx.loop, ctx.runtime) : null;
885
903
  const action = props.onSet
886
904
  ? `action: { ${props.onSet.var} = ${props.onSet.kind === 'expr'
887
905
  ? String(props.onSet.value)
@@ -1117,7 +1135,7 @@ function emitNodeInner(node, ctx) {
1117
1135
  // 需 maxWidth:.infinity 以在父容器占满宽(组件 View 默认大小=内容)——父 row/column 的
1118
1136
  // alignItems:stretch 语义由使用点 frame 承担;组件根自身不携带布局。
1119
1137
  if (props.onClick) {
1120
- const loopAction = loopClickAction(props.onClick, ctx.loop);
1138
+ const loopAction = loopClickAction(props.onClick, ctx.loop, ctx.runtime);
1121
1139
  const action = loopAction
1122
1140
  ? `action: { ${loopAction} }`
1123
1141
  : `action: { ${ctx.runtime}.invoke("${swiftStringEscape(props.onClick)}") }`;
@@ -1151,6 +1169,13 @@ function emitNodeInner(node, ctx) {
1151
1169
  }));
1152
1170
  let body;
1153
1171
  if (vf.listState) {
1172
+ // 组件/无状态上下文:_vrnE# 下沉状态不可求值 → 空 ForEach(编译保底,不渲染该项)
1173
+ const knownS = ctx.stateNames && ctx.stateNames.size > 0 ? ctx.stateNames : currentStateNames;
1174
+ const unresolved = /^_vrnE\d+$/.test(vf.listState) && (!knownS || !knownS.has(vf.listState));
1175
+ if (unresolved) {
1176
+ ctx.warnings.push(`${ctx.path}: v-for 数据源「${vf.listState}」原生端不可求值(组件内无状态上下文),该项已置空`);
1177
+ return 'EmptyView()';
1178
+ }
1154
1179
  // 动态列表:解码失败或空数组时保持空(ForEach 不渲染);if 语句不能直接挂修饰符 → Group 包裹
1155
1180
  body =
1156
1181
  `Group {\n` +
@@ -1259,6 +1284,7 @@ export function generateScreenSource(screen, opts = {}) {
1259
1284
  currentColorNames = opts.theme?.colorNames;
1260
1285
  currentFontNames = opts.theme?.fontNames;
1261
1286
  currentFrameShort = opts.theme !== undefined;
1287
+ currentCssVars = opts.cssVars;
1262
1288
  currentStateTypes = Object.fromEntries((screen.state ?? []).map((s) => [
1263
1289
  s.name,
1264
1290
  typeof s.init === 'string' ? 'string' : typeof s.init === 'boolean' ? 'boolean' : 'number',
@@ -1302,6 +1328,8 @@ export function generateScreenSource(screen, opts = {}) {
1302
1328
  `// source: ${screen.id}.ir.json (${screen.width ?? 360}x${screen.height ?? 640})`,
1303
1329
  '',
1304
1330
  `struct ${structName}: View {`,
1331
+ ' // R-THEME:皮肤切换触发视图重建(UserDefaults "vrn.skin" 变化 → body 重算 → ThemeColor.color 取新皮肤色)',
1332
+ ' @AppStorage("vrn.skin") private var skin = "warm"',
1305
1333
  ...(stateDecls ? [stateDecls, ''] : []),
1306
1334
  ...(needDismiss ? [' @Environment(' + '\\' + '.dismiss) private var dismiss', ''] : []),
1307
1335
  // R-VFOBJ-D:静态 v-for 数据属性(数据与布局分离,body 只留 ForEach + 组件引用)
@@ -1383,6 +1411,10 @@ function actionExpr(props, ctx) {
1383
1411
  return `{ ${props.onSet.var} = ${lit}; ${ctx.runtime}.pushState("${props.onSet.var}", ${props.onSet.var}) }`;
1384
1412
  }
1385
1413
  if (props.onClick !== undefined) {
1414
+ // R-LOOPITEM:onTapGesture 路径同样支持循环变量(row 只在 Swift 闭包作用域)
1415
+ const loopAction = loopClickAction(props.onClick, ctx.loop, ctx.runtime);
1416
+ if (loopAction)
1417
+ return `{ ${loopAction} }`;
1386
1418
  return `{ ${ctx.runtime}.invoke("${swiftStringEscape(props.onClick)}") }`;
1387
1419
  }
1388
1420
  return undefined;
@@ -1390,22 +1422,51 @@ function actionExpr(props, ctx) {
1390
1422
  /** R-VFOBJ:onClick 表达式含循环变量字段 → 返回 Swift 内联 action 体;否则 null。
1391
1423
  * 支持 router.push(item.path) → Runtime.routerPushHandler?(item["path"] ?? "");
1392
1424
  * 其他含循环字段表达式不支持(warning 由调用方给出)。 */
1393
- function loopClickAction(onClick, loop) {
1425
+ function loopClickAction(onClick, loop, runtime) {
1394
1426
  if (!loop)
1395
1427
  return null;
1396
- const re = new RegExp(`${loop.itemKey}\\.([A-Za-z_][\\w$]*)`, 'g');
1428
+ const itemKey = loop.itemKey;
1429
+ const re = new RegExp(`${itemKey}\\.([A-Za-z_][\\w$]*)`, 'g');
1397
1430
  if (!re.test(onClick))
1398
1431
  return null;
1399
1432
  const m = onClick.match(/^router\.push\((.*)\)$/s);
1400
- if (!m)
1401
- return null;
1402
- const arg = m[1].replace(re, `${loop.itemKey}["$1"] ?? ""`);
1403
- return `Runtime.routerPushHandler?(${arg})`;
1433
+ if (m) {
1434
+ const arg = m[1].replace(re, `${loop.itemKey}["$1"] ?? ""`);
1435
+ return `Runtime.routerPushHandler?(${arg})`;
1436
+ }
1437
+ // R-LOOPITEM:非 router.push 且引用循环变量(goDetail(row.item.id))——
1438
+ // row 只在 Swift ForEach 闭包作用域,JS 全局不存在 → 先注入 __vrnLoop 再 invoke
1439
+ const replaced = onClick.replace(re, '__vrnLoop.$1');
1440
+ return `${runtime}.setLoopItem(${itemKey}); ${runtime}.invoke("${swiftStringEscape(replaced)}")`;
1404
1441
  }
1405
1442
  /** 收集组件 IR 树中所有 text ref → prop 名(组件 View 的 let 属性集合) */
1443
+ /** 表达式 → 根标识符(dialog.title → dialog;!allOpen → allOpen;__vrnField__:dialog:title → dialog;下沉条件 → '') */
1444
+ function rootIdent(expr) {
1445
+ const e = expr.replace(/^!/, '');
1446
+ const f = e.match(/^__vrnField__:(\w+)/);
1447
+ if (f)
1448
+ return f[1];
1449
+ if (e.startsWith('__vrnCond:') || e.startsWith('__vrnField__:'))
1450
+ return '';
1451
+ const id = e.match(/^([A-Za-z_$][\w$]*)/);
1452
+ return id ? id[1] : '';
1453
+ }
1406
1454
  function collectRefs(node, out) {
1407
- if (node.type === 'text' && node.props?.ref !== undefined)
1408
- out.add(node.props.ref);
1455
+ if (node.type === 'text' && node.props?.ref !== undefined) {
1456
+ const r = rootIdent(node.props.ref);
1457
+ if (r)
1458
+ out.add(r);
1459
+ }
1460
+ if (node.condition) {
1461
+ const r = rootIdent(node.condition);
1462
+ if (r)
1463
+ out.add(r);
1464
+ }
1465
+ if (node.type === 'for' && node.vfor?.listState && !/^_vrnE\d+$/.test(node.vfor.listState)) {
1466
+ const r = rootIdent(node.vfor.listState);
1467
+ if (r)
1468
+ out.add(r);
1469
+ }
1409
1470
  for (const c of node.children ?? [])
1410
1471
  collectRefs(c, out);
1411
1472
  return out;
@@ -1425,7 +1486,9 @@ export function generateComponentSource(name, ui, opts = {}) {
1425
1486
  currentFrameShort = true;
1426
1487
  currentStateTypes = {};
1427
1488
  }
1489
+ currentCssVars = opts.cssVars;
1428
1490
  const props = collectRefs(ui, new Set());
1491
+ currentStateNames = props; // 组件上下文:已知标识符 = props(清掉页面残留 state 名,下沉条件正确降级)
1429
1492
  const dataProps = [];
1430
1493
  const body = emitNode(ui, {
1431
1494
  parentAxis: 'column',
@@ -1435,6 +1498,7 @@ export function generateComponentSource(name, ui, opts = {}) {
1435
1498
  warnings,
1436
1499
  path: `${name}View.body`,
1437
1500
  dataProps,
1501
+ stateNames: props, // 组件已知标识符 = props 集合(v-if/v-for 下沉状态不在内 → 降级 true)
1438
1502
  });
1439
1503
  const propDecls = [...props]
1440
1504
  .map((p) => ` let ${p}: String`)
@@ -1446,6 +1510,8 @@ export function generateComponentSource(name, ui, opts = {}) {
1446
1510
  `// source: components/${name}.vue`,
1447
1511
  '',
1448
1512
  `struct ${name}View: View {`,
1513
+ ' // R-THEME:皮肤切换触发组件视图重建(@AppStorage("vrn.skin") 变化 → body 重算 → ThemeColor.color 取新皮肤色)',
1514
+ ' @AppStorage("vrn.skin") private var skin = "warm"',
1449
1515
  ...(propDecls ? [propDecls, ''] : []),
1450
1516
  // R-VFOBJ-D:静态 v-for 数据属性(组件内同样分离)
1451
1517
  ...(dataProps.length ? [dataProps.join('\n'), ''] : []),
package/dist/ir/schema.js CHANGED
@@ -24,7 +24,8 @@ export const NODE_TYPES = [
24
24
  const HEX_COLOR = /^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/;
25
25
  const RGB_COLOR = /^rgba?\(\s*\d{1,3}\s*,\s*\d{1,3}\s*,\s*\d{1,3}(?:\s*,\s*[\d.]+)?\s*\)$/;
26
26
  function isColor(v) {
27
- return typeof v === 'string' && (HEX_COLOR.test(v) || RGB_COLOR.test(v));
27
+ return (typeof v === 'string' &&
28
+ (HEX_COLOR.test(v) || RGB_COLOR.test(v) || /^var\([^)]*\)$/.test(v.trim())));
28
29
  }
29
30
  function isSize(v) {
30
31
  return v === 'fill' || v === 'auto' || (typeof v === 'number' && v >= 0 && Number.isFinite(v));
@@ -116,14 +117,14 @@ const PROPS_VALIDATORS = {
116
117
  const NODE_ALLOWED_PROPS = {
117
118
  root: ['width', 'height', 'background'],
118
119
  group: ['width', 'height', 'padding', 'gap', 'flex', 'flexDirection', 'justifyContent', 'alignItems', 'background', 'radius', 'borderWidth', 'borderColor', 'marginTopAuto', 'marginTop', 'marginLeftAuto', 'marginBottom', 'marginRight', 'marginLeft', 'borderSides', 'clip', 'onClick', 'onSet', 'position', 'absTop', 'absLeft', 'absRight', 'absBottom', 'transform', 'show', 'opacity', 'gridColumns'],
119
- text: ['width', 'text', 'ref', 'bind', 'bindExpr', 'dynField', 'bindParts', 'fontSize', 'fontWeight', 'textAlign', 'lineHeight', 'color', 'padding', 'colorActive', 'marginLeftAuto', 'marginTop', 'colorDynamic', 'fontSizeDynamic', 'position', 'absTop', 'absLeft', 'absRight', 'absBottom', 'transform', 'show', 'flex', 'opacity', 'onClick', 'onSet', 'fieldOf'],
120
+ text: ['width', 'text', 'ref', 'bind', 'bindExpr', 'dynField', 'bindParts', 'fontSize', 'fontWeight', 'textAlign', 'lineHeight', 'color', 'padding', 'background', 'colorActive', 'marginLeftAuto', 'marginTop', 'colorDynamic', 'fontSizeDynamic', 'position', 'absTop', 'absLeft', 'absRight', 'absBottom', 'transform', 'show', 'flex', 'opacity', 'onClick', 'onSet', 'fieldOf'],
120
121
  image: ['width', 'height', 'src', 'objectFit', 'radius', 'borderSides', 'borderWidth', 'borderColor', 'clip', 'flex', 'marginTop', 'marginBottom', 'marginRight', 'marginLeft', 'position', 'absTop', 'absLeft', 'absRight', 'absBottom', 'transform', 'show', 'opacity'],
121
122
  button: ['width', 'height', 'label', 'bind', 'background', 'radius', 'borderWidth', 'borderColor', 'fontSize', 'fontWeight', 'color', 'flex', 'padding', 'marginTopAuto', 'marginTop', 'marginBottom', 'marginRight', 'marginLeft', 'borderSides', 'clip', 'onClick', 'onSet', 'colorDynamic', 'fontSizeDynamic', 'position', 'absTop', 'absLeft', 'absRight', 'absBottom', 'transform', 'show', 'pressBg', 'opacity'],
122
123
  input: ['width', 'height', 'value', 'placeholder', 'fontSize', 'color', 'background', 'padding', 'radius', 'borderWidth', 'borderColor', 'borderSides', 'clip', 'flex', 'marginTop', 'marginTopAuto', 'marginBottom', 'marginRight', 'marginLeft', 'onSet', 'colorDynamic', 'fontSizeDynamic', 'position', 'absTop', 'absLeft', 'absRight', 'absBottom', 'transform', 'show'],
123
- scroll: ['width', 'height', 'padding', 'gap', 'flexDirection', 'background', 'showsIndicators', 'flex'],
124
- spacer: ['width', 'height', 'flex'],
125
- divider: ['width', 'height', 'background', 'borderWidth', 'borderColor', 'marginTop'],
126
- component: ['name', 'attrs', 'flex', 'width', 'height', 'marginTop', 'marginTopAuto', 'marginLeftAuto', 'onClick'],
124
+ scroll: ['width', 'height', 'padding', 'gap', 'flexDirection', 'background', 'showsIndicators', 'flex', 'justifyContent', 'alignItems', 'radius', 'borderWidth', 'borderColor', 'marginTop', 'position', 'absTop', 'absLeft', 'absRight', 'absBottom', 'transform', 'show', 'opacity', 'borderSides', 'clip'],
125
+ spacer: ['width', 'height', 'flex', 'background', 'padding', 'radius', 'borderWidth', 'borderColor', 'marginTop', 'alignSelf', 'opacity'],
126
+ divider: ['width', 'height', 'background', 'borderWidth', 'borderColor', 'marginTop', 'marginBottom', 'marginLeft', 'marginRight', 'padding', 'radius', 'flex', 'opacity', 'position', 'absTop', 'absLeft', 'absRight', 'absBottom', 'transform', 'show'],
127
+ component: ['name', 'attrs', 'flex', 'width', 'height', 'marginTop', 'marginTopAuto', 'marginLeftAuto', 'marginBottom', 'marginLeft', 'marginRight', 'background', 'radius', 'borderWidth', 'borderColor', 'padding', 'position', 'absTop', 'absLeft', 'absRight', 'absBottom', 'transform', 'show', 'opacity', 'onClick'],
127
128
  for: ['flexDirection', 'gap', 'padding', 'background', 'flex', 'width', 'height', 'showsIndicators', 'borderSides', 'clip', 'position', 'absTop', 'absLeft', 'absRight', 'absBottom', 'transform', 'show'],
128
129
  };
129
130
  /** 必须有 children 的节点 */
@@ -25,6 +25,8 @@ const num = (v) => {
25
25
  /** 与 schema isColor 同源的 CSS 颜色校验:仅 #rgb / #rrggbb / rgb[a]() / hsl[a]()(named/var() 原生端不可解析,拒绝) */
26
26
  function isCssColor(v) {
27
27
  const s = v.trim();
28
+ if (/^var\([^)]*\)$/.test(s))
29
+ return true; // CSS 变量(含 fallback 形态):值留给 codegen 按项目 tokens 解析
28
30
  if (/^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/.test(s))
29
31
  return true;
30
32
  if (/^rgba?\(\s*[\d.]+%?\s*,\s*[\d.]+%?\s*,\s*[\d.]+%?\s*(,\s*([\d.]+%?|0))?\s*\)$/.test(s))
@@ -1747,9 +1747,15 @@ function walkElement(node, classProps, comboRules, scopedRules, pseudoRules, war
1747
1747
  const expr = gm[3].trim();
1748
1748
  // Vue3 语义:`v-for="i in 3"` 迭代 1..3
1749
1749
  const rangeM = expr.match(/^(\d+)$/);
1750
+ // R143:数据源可能是数组、数值(v-for="i in total" 迭代 1..N)或 props 字符串,
1751
+ // 用运行时自适应表达式兜住全部形态
1750
1752
  const sunk = addJsExprState(rangeM
1751
1753
  ? `JSON.stringify(Array.from({ length: ${rangeM[1]} }, function (_, i) { return i + 1; }))`
1752
- : `JSON.stringify(${expr})`);
1754
+ : `JSON.stringify((function (v) {` +
1755
+ ` if (Array.isArray(v)) return v;` +
1756
+ ` var n = Number(v);` +
1757
+ ` if (!isNaN(n) && n > 0) return Array.from({ length: n }, function (_, i) { return i + 1; });` +
1758
+ ` return []; })(${expr}))`);
1753
1759
  vf = {
1754
1760
  listState: sunk,
1755
1761
  itemKey: (gm[1] ? gm[1].split(',')[0] : gm[2]).trim(),
@@ -1763,10 +1769,17 @@ function walkElement(node, classProps, comboRules, scopedRules, pseudoRules, war
1763
1769
  }
1764
1770
  // M4b:动态列表数据源必须已声明为 ref 数组状态(编译期校验,防悬空引用)
1765
1771
  // 表达式下沉产生的状态(_vrnE*)在 walk 之后才并入页面状态,这里放行
1772
+ // R143:未声明的数据源(组件 props / 数值 / 未注册变量)→ 运行时自适应下沉,不再整页失败
1766
1773
  if (vf.listState &&
1767
1774
  !/^_vrnE\d+$/.test(vf.listState) &&
1768
- !(state && state.states.some((s) => s.name === vf.listState))) {
1769
- throw new VueCompileError(`v-for 动态数据源「${vf.listState}」未在 <script setup> 中声明为 ref 数组(M4b)`);
1775
+ !(state && state.states.some((s) => s.name === vf.listState)) &&
1776
+ propRefs === undefined // 页面模式才下沉;组件模式(Set 存在,含空)保留数据源原名
1777
+ ) {
1778
+ const srcName = vf.listState;
1779
+ const sunk2 = addJsExprState(`JSON.stringify((function (v) { if (Array.isArray(v)) return v; var n = Number(v); ` +
1780
+ `if (!isNaN(n) && n > 0) return Array.from({ length: n }, function (_, i) { return i + 1; }); return []; })(${srcName}))`);
1781
+ warnings.push(`v-for 数据源「${srcName}」未声明为状态 → 已在运行时自适应求值(R143)`);
1782
+ vf.listState = sunk2;
1770
1783
  }
1771
1784
  const innerNode = { ...node, props: node.props.filter((p) => !(p.type === NodeTypes.DIRECTIVE && p.name === 'for')) };
1772
1785
  activeLoops.push({ itemKey: vf.itemKey, indexKey: vf.indexKey });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dongmingzhixiu/vuexir-compiler",
3
- "version": "0.5.4",
3
+ "version": "0.5.6",
4
4
  "description": "Vue SFC → IR → 原生源码(SwiftUI / Compose)编译器核心:纯函数、零 IO",
5
5
  "type": "module",
6
6
  "license": "MIT",