@dongmingzhixiu/vuexir-compiler 0.7.6 → 0.7.8

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.
@@ -9,6 +9,7 @@ declare const NAV_TEST_KT: (bundleId: string) => string;
9
9
  interface ComposeCtx {
10
10
  parentAxis?: 'row' | 'column' | 'box';
11
11
  inVerticalScroll?: boolean;
12
+ parentColumnHeightBounded?: boolean;
12
13
  runtimeAsset?: string;
13
14
  warnings: string[];
14
15
  pageId: string;
@@ -108,6 +109,7 @@ declare function buildNavHost(bundleId: string, tabs: {
108
109
  mode?: string;
109
110
  }[], options?: {
110
111
  tabBar?: TabBarSpec;
112
+ nativeNavigationBar?: boolean;
111
113
  }): string;
112
114
  /**
113
115
  * 输入解析(R-PKG):CLI 通过 VRN_SAMPLES_ROOT 注入样例根(默认 <repo>/examples)。
@@ -11,6 +11,8 @@
11
11
  import { readFileSync, writeFileSync, mkdirSync, existsSync, readdirSync, rmSync } from 'node:fs';
12
12
  import { join, dirname, basename } from 'node:path';
13
13
  import { fileURLToPath } from 'node:url';
14
+ import { NodeTypes } from '@vue/compiler-core';
15
+ import { parse } from '@vue/compiler-sfc';
14
16
  import { loadProjectAssets, materializeProjectAssets } from './project-assets.js';
15
17
  import { extractCssVariables } from '../transform/css.js';
16
18
  import { loadProjectStyles } from './project-styles.js';
@@ -476,8 +478,16 @@ function dp(n) {
476
478
  function sp(n) {
477
479
  return `${n}.sp`;
478
480
  }
481
+ function kotlinStringContents(value) {
482
+ return value
483
+ .replace(/\\/g, '\\\\')
484
+ .replace(/"/g, '\\"')
485
+ .replace(/\$/g, () => '\\$')
486
+ .replace(/\n/g, '\\n')
487
+ .replace(/\r/g, '\\r');
488
+ }
479
489
  function kotlinStr(s) {
480
- return `"${s.replace(/\\/g, '\\\\').replace(/"/g, '\\"').replace(/\$/g, '${\'$\'}').replace(/\n/g, '\\n').replace(/\r/g, '\\r')}"`;
490
+ return `"${kotlinStringContents(s)}"`;
481
491
  }
482
492
  /** 条件表达式 → Kotlin(对位 iOS swiftCondExpr:=== / !== 归一 + 单引号字符串转双引号) */
483
493
  /** R114:文本绑定里引用的标识符若既不是页面状态也不是循环变量 → 视为未支持,回退空串 */
@@ -741,7 +751,10 @@ function borderModifiers(props) {
741
751
  return mods;
742
752
  }
743
753
  function flexModifier(props, ctx) {
744
- if (ctx?.parentAxis === 'box' || (ctx?.inVerticalScroll && ctx.parentAxis !== 'row'))
754
+ if (ctx?.parentAxis === 'box')
755
+ return '';
756
+ if (ctx?.inVerticalScroll && ctx.parentAxis !== 'row' &&
757
+ (!ctx.parentColumnHeightBounded || (props.flex ?? 0) <= 0))
745
758
  return '';
746
759
  if (props.flex === undefined && props.minWidth === 0 && ctx?.parentAxis === 'row')
747
760
  return 'Modifier.weight(1f, fill = false)';
@@ -1216,6 +1229,10 @@ function emitNodeInner(node, ctx, depth, loop, useParamModifier = false, parentS
1216
1229
  (nd.type === 'group' && stretchModifiers(parentStretch, nd).length > 0) ||
1217
1230
  (ctx.parentAxis === 'row' && p.flex !== undefined);
1218
1231
  const rowContentWidth = ctx.parentAxis === 'row' && p.width === undefined && p.flex === undefined;
1232
+ // 普通列的固定高度或父列分配的完整权重重新提供有限的垂直空间。
1233
+ const columnHeightBounded = kind === 'Column' && nd.type === 'group' &&
1234
+ (typeof p.height === 'number' ||
1235
+ (ctx.parentAxis === 'column' && ctx.parentColumnHeightBounded === true && (p.flex ?? 0) > 0));
1219
1236
  return nd.children
1220
1237
  .map((c) => {
1221
1238
  const cp = (c.props ?? {});
@@ -1249,7 +1266,11 @@ function emitNodeInner(node, ctx, depth, loop, useParamModifier = false, parentS
1249
1266
  : columnHasWidth && !hasDeclaredWidth(cp);
1250
1267
  const stretch = kind === 'Column' && (p.alignItems === undefined || p.alignItems === 'stretch') &&
1251
1268
  autoWidth && cp.position !== 'absolute' && !cp.marginLeftAuto;
1252
- const childCtx = { ...ctx, parentAxis: kind === 'Row' ? 'row' : kind === 'Box' ? 'box' : 'column' };
1269
+ const childCtx = {
1270
+ ...ctx,
1271
+ parentAxis: kind === 'Row' ? 'row' : kind === 'Box' ? 'box' : 'column',
1272
+ parentColumnHeightBounded: columnHeightBounded,
1273
+ };
1253
1274
  const result = emitNode(c, childCtx, d, loop, false, stretch);
1254
1275
  if (childCtx.needsScope)
1255
1276
  ctx.needsScope = true;
@@ -1370,7 +1391,7 @@ function emitNodeInner(node, ctx, depth, loop, useParamModifier = false, parentS
1370
1391
  const segs = p.bindParts
1371
1392
  .map((seg) => {
1372
1393
  if ('text' in seg)
1373
- return String(seg.text).replace(/\\/g, '\\\\').replace(/"/g, '\\"').replace(/\$/g, '${\'$\'}').replace(/\n/g, '\\n');
1394
+ return kotlinStringContents(String(seg.text));
1374
1395
  if ('bind' in seg)
1375
1396
  return '${' + safeBind(seg.bind, ctx, loop) + '}';
1376
1397
  if ('fieldOf' in seg)
@@ -1744,10 +1765,10 @@ import androidx.compose.ui.unit.dp
1744
1765
  import androidx.compose.ui.unit.sp
1745
1766
  `;
1746
1767
  const TAB_ICONS = ['home.fill', 'book.fill', 'square.grid.2x2.fill', 'bell.fill', 'gearshape.fill', 'explore', 'work'];
1747
- /** tab 文案拆 2 字一行(对位 iOS 窄 tab 自动换行视觉) */
1768
+ /** 纯汉字 tab 保持两字一行,其他标题由系统按可用宽度换行。 */
1748
1769
  function tabLabel(title) {
1749
- const t = title.replace(/\s+/g, '');
1750
- return t.length > 2 ? t.match(/.{1,2}/g).join('\\n') : t;
1770
+ const compact = title.replace(/\s+/g, '');
1771
+ return /^\p{Script=Han}+$/u.test(compact) ? compact.match(/.{1,2}/gu).join('\n') : title;
1751
1772
  }
1752
1773
  /**
1753
1774
  * 工程根推导(R75):路由文件可能在 src/router/index.ts,`@/` 别名需要相对「工程根」解析。
@@ -1867,14 +1888,20 @@ function parseRouter(src, dir, routerFile) {
1867
1888
  meta,
1868
1889
  });
1869
1890
  }
1870
- const hasText = (file) => {
1891
+ const hasRenderableNode = (node) => {
1892
+ if (node.type === NodeTypes.ELEMENT) {
1893
+ return node.tag !== 'template' || node.children.some(hasRenderableNode);
1894
+ }
1895
+ return node.type === NodeTypes.INTERPOLATION || (node.type === NodeTypes.TEXT && node.content.trim() !== '');
1896
+ };
1897
+ const hasRenderableTemplate = (file) => {
1871
1898
  if (!file || !existsSync(file))
1872
1899
  return false;
1873
- const tpl = readFileSync(file, 'utf-8').match(/<template>([\s\S]*)<\/template>/)?.[1] ?? '';
1874
- return tpl.replace(/<[^>]*>/g, ' ').replace(/\s+/g, ' ').trim().length > 0;
1900
+ const { descriptor } = parse(readFileSync(file, 'utf-8'), { filename: file });
1901
+ return descriptor.template?.ast?.children.some(hasRenderableNode) ?? false;
1875
1902
  };
1876
1903
  const tabs = routes
1877
- .filter((r) => r.meta.tab === 'true' && hasText(r.file))
1904
+ .filter((r) => r.meta.tab === 'true' && hasRenderableTemplate(r.file))
1878
1905
  .map((r, i) => ({
1879
1906
  id: r.name,
1880
1907
  title: r.meta.title ?? r.name,
@@ -1883,7 +1910,7 @@ function parseRouter(src, dir, routerFile) {
1883
1910
  path: r.path,
1884
1911
  }));
1885
1912
  const details = routes
1886
- .filter((r) => r.meta.tab !== 'true' && hasText(r.file))
1913
+ .filter((r) => r.meta.tab !== 'true' && hasRenderableTemplate(r.file))
1887
1914
  .map((r) => ({ id: r.name, title: r.meta.title ?? r.name, file: r.file, path: r.path }));
1888
1915
  if (tabs.length === 0) {
1889
1916
  warnings.push('router 未解析到任何 tab 路由(routes 需含 meta: { tab: true })');
@@ -2072,9 +2099,10 @@ function runTabs(appDir, opts = {}) {
2072
2099
  console.log(`[gen-android] - ${pageId}DetailView.kt · push「${d.title}」`);
2073
2100
  }
2074
2101
  // 3) AppNavHost.kt(router.ts → NavHost + NavigationBar)
2075
- let navHost = buildNavHost(bundleId, tabs, detailViews, { tabBar: opts.tabBar });
2076
- if (opts.nativeNavigationBar === false)
2077
- navHost = navHost.replace(/ topBar = \{[\s\S]*? \},\n bottomBar =/, ' bottomBar =');
2102
+ const navHost = buildNavHost(bundleId, tabs, detailViews, {
2103
+ tabBar: opts.tabBar,
2104
+ nativeNavigationBar: opts.nativeNavigationBar,
2105
+ });
2078
2106
  writeFileSync(join(pkgDir, 'AppNavHost.kt'), navHost);
2079
2107
  // P2:写 .vrn/manifest.json(工具链/模板版本 + 文件清单),供 vrn doctor 校验与增量重生成
2080
2108
  const metaDir = join(outDir, '.vrn');
@@ -2101,6 +2129,7 @@ function buildNavHost(bundleId, tabs, details, options = {}) {
2101
2129
  .map((t) => `composable("${t.id}") { TabScaffold("${t.id}", onSelectTab) { p -> ${toPascal(t.id)}Content(modifier = Modifier.padding(p)) } }
2102
2130
  composable("${t.path}") { TabScaffold("${t.id}", onSelectTab) { p -> ${toPascal(t.id)}Content(modifier = Modifier.padding(p)) } }`)
2103
2131
  .join('\n ');
2132
+ const showNavigationBar = options.nativeNavigationBar !== false;
2104
2133
  // M7 对位 iOS NavigationStack:详情页内层 Scaffold + TopAppBar(标题 + 返回箭头/backText、mode=transparent 透明背景)
2105
2134
  const detailRoutes = details
2106
2135
  .map((d) => {
@@ -2117,7 +2146,7 @@ function buildNavHost(bundleId, tabs, details, options = {}) {
2117
2146
  const barColor = d.mode === 'transparent' ? 'Color.Transparent' : d.bg ? colorExpr(d.bg) : 'InkBar';
2118
2147
  return `composable("${d.path}") {
2119
2148
  Scaffold(
2120
- containerColor = InkBg,
2149
+ containerColor = InkBg,${showNavigationBar ? `
2121
2150
  topBar = {
2122
2151
  TopAppBar(
2123
2152
  title = { Text(${kotlinStr(d.title)}, fontSize = 17.sp, fontWeight = FontWeight.Medium) },
@@ -2130,7 +2159,7 @@ function buildNavHost(bundleId, tabs, details, options = {}) {
2130
2159
  titleContentColor = Gold,
2131
2160
  ),
2132
2161
  )
2133
- },
2162
+ },` : ''}
2134
2163
  ) { innerPadding ->
2135
2164
  ${toPascal(d.id)}DetailContent(modifier = Modifier.padding(innerPadding))
2136
2165
  }
@@ -2141,8 +2170,8 @@ function buildNavHost(bundleId, tabs, details, options = {}) {
2141
2170
  .map((t) => `NavigationBarItem(
2142
2171
  selected = currentTab == "${t.id}",
2143
2172
  onClick = { onSelectTab("${t.id}") },
2144
- icon = { Icon(${ICON_MAP[t.icon] ?? 'Icons.Default.Home'}, contentDescription = "${t.title}") },
2145
- label = { Text("${tabLabel(t.title)}", fontSize = 10.sp, textAlign = TextAlign.Center) },
2173
+ icon = { Icon(${ICON_MAP[t.icon] ?? 'Icons.Default.Home'}, contentDescription = ${kotlinStr(t.title)}) },
2174
+ label = { Text(${kotlinStr(tabLabel(t.title))}, fontSize = 10.sp, textAlign = TextAlign.Center) },
2146
2175
  colors = NavigationBarItemDefaults.colors(
2147
2176
  selectedIconColor = Gold,
2148
2177
  selectedTextColor = Gold,
@@ -2206,7 +2235,7 @@ import androidx.navigation.compose.*
2206
2235
  @Composable
2207
2236
  fun TabScaffold(currentTab: String, onSelectTab: (String) -> Unit, content: @Composable (PaddingValues) -> Unit) {
2208
2237
  Scaffold(
2209
- containerColor = InkBg,
2238
+ containerColor = InkBg,${showNavigationBar ? `
2210
2239
  topBar = {
2211
2240
  TopAppBar(
2212
2241
  title = { Text(${tabTitleExpr}, fontSize = 17.sp, fontWeight = FontWeight.Medium) },
@@ -2215,7 +2244,7 @@ fun TabScaffold(currentTab: String, onSelectTab: (String) -> Unit, content: @Com
2215
2244
  titleContentColor = Gold,
2216
2245
  ),
2217
2246
  )
2218
- },
2247
+ },` : ''}
2219
2248
  bottomBar = {
2220
2249
  ${customBar || `NavigationBar(containerColor = InkBar) {\n ${barItems}\n }`}
2221
2250
  },
@@ -346,7 +346,7 @@ function gapInheritFor(child, parentGap, siblings) {
346
346
  return parentGap;
347
347
  }
348
348
  /* ============ 节点发射 ============ */
349
- function sizeModifiers(props, axis, ctx) {
349
+ function sizeModifiers(props, axis, ctx, fixedFrameAlignment) {
350
350
  const mods = [];
351
351
  const { width, height, flex } = props;
352
352
  if (props.widthDynamic)
@@ -356,11 +356,11 @@ function sizeModifiers(props, axis, ctx) {
356
356
  if (width === 'fill')
357
357
  mods.push(frameExpr('fillW'));
358
358
  else if (typeof width === 'number')
359
- mods.push(`.frame(width: ${width})`);
359
+ mods.push(`.frame(width: ${width}${fixedFrameAlignment})`);
360
360
  if (height === 'fill')
361
361
  mods.push('.frame(maxHeight: .infinity)');
362
362
  if (typeof height === 'number')
363
- mods.push(`.frame(height: ${height})`);
363
+ mods.push(`.frame(height: ${height}${fixedFrameAlignment})`);
364
364
  if (flex !== undefined) {
365
365
  // R-H3:flex 沿父容器主轴拉伸占满剩余空间;但父容器高度不确定时(auto 链)无剩余空间可分配,
366
366
  // SwiftUI 的 .infinity 会沿祖先链漂移到屏幕高度 → 降级为内容高并告警(与 CSS flex-grow 语义一致)
@@ -510,6 +510,16 @@ export function boxModifiers(props) {
510
510
  mods.push('.clipped()');
511
511
  return mods;
512
512
  }
513
+ function columnStretchAlignment(node) {
514
+ const props = node.props;
515
+ // 将常规行放到拉伸区域的尾端,不插入会多占一份 gap 的 Spacer。
516
+ if (node.type === 'group' && props?.flexDirection === 'row' && props.justifyContent === 'end' &&
517
+ props.widthDynamic === undefined && props.widthFraction === undefined && props.widthFractionDynamic === undefined &&
518
+ !props.flexWrap && !props.gridColumns && !node.children?.some((child) => child.props?.position === 'absolute')) {
519
+ return '.trailing';
520
+ }
521
+ return props?.textAlign === 'center' ? '.center' : props?.textAlign === 'right' ? '.trailing' : '.leading';
522
+ }
513
523
  function emitChildren(children, axis, ctx, alignRaw = 'stretch', stretchCross = false) {
514
524
  if (!children || children.length === 0)
515
525
  return '';
@@ -562,7 +572,7 @@ function emitChildren(children, axis, ctx, alignRaw = 'stretch', stretchCross =
562
572
  // column:仅默认 stretch 时子项填满父宽(div block 语义);显式 start/center/end → 内容宽对齐
563
573
  const explicitCol = alignRaw === 'start' || alignRaw === 'center' || alignRaw === 'end';
564
574
  if (!explicitCol) {
565
- return body + '\n' + indent(ctx.level) + frameExpr('fillW', c.props?.textAlign === 'center' ? '.center' : c.props?.textAlign === 'right' ? '.trailing' : '.leading');
575
+ return body + '\n' + indent(ctx.level) + frameExpr('fillW', columnStretchAlignment(c));
566
576
  }
567
577
  return body;
568
578
  }
@@ -697,10 +707,14 @@ function emitNodeInner(node, ctx) {
697
707
  return pad(`VrnFractionWidth(fraction: max(0,min(1,${fraction}))) {`) + `\n${inner}\n${pad('}')}`;
698
708
  }
699
709
  const axis = ctx.parentAxis ?? 'column';
710
+ // 绝对定位偏移以固定容器的左上角为原点。SwiftUI 默认会居中放置偏移前的 ZStack。
711
+ const fixedFrameAlignment = node.type === 'group' && !props.flexWrap && props.gridColumns === undefined &&
712
+ node.children?.length && node.children.every((child) => child.props?.position === 'absolute')
713
+ ? ', alignment: .topLeading' : '';
700
714
  // box-sizing:border-box(CSS 默认):固定 height 应包含 padding。
701
715
  // 故 height 为数字且存在 padding 时,padding 置于 frame(height) 之前、
702
716
  // frame 限定总视觉高度(否则 padding 加在 frame 外 → 视觉高 = height + 2×padding)。
703
- const size = sizeModifiers(props, axis, ctx);
717
+ const size = sizeModifiers(props, axis, ctx, fixedFrameAlignment);
704
718
  let padMods = paddingModifiers(props);
705
719
  let boxMods = boxModifiers(props);
706
720
  if (node.type === 'image' && props.objectFit === 'cover' && !props.clip) {
@@ -713,7 +727,7 @@ function emitNodeInner(node, ctx) {
713
727
  boxMods = boxMods.filter((m) => !m.startsWith('.background(') && !m.startsWith('.cornerRadius(') && !m.startsWith('.overlay('));
714
728
  }
715
729
  const box = typeof props.height === 'number' && (padMods.length > 0 || capsule !== undefined)
716
- ? [...size.filter((m) => !m.startsWith('.frame(height: ')), ...(capsule !== undefined ? [capsule] : []), ...padMods, `.frame(height: ${props.height})`, ...boxMods]
730
+ ? [...size.filter((m) => !m.startsWith('.frame(height: ')), ...(capsule !== undefined ? [capsule] : []), ...padMods, `.frame(height: ${props.height}${fixedFrameAlignment})`, ...boxMods]
717
731
  : [...size, ...(capsule !== undefined ? [capsule] : []), ...padMods, ...boxMods];
718
732
  // M5 transform:scale / rotate / translate → 原生修饰符(视觉变换,不影响布局占位)
719
733
  const t = props.transform;
@@ -985,7 +999,7 @@ function emitNodeInner(node, ctx) {
985
999
  // 显式 start/center/end → 子项按内容宽对齐(CSS 交叉轴对齐语义)
986
1000
  const explicitCol = (c.props?.alignSelf !== undefined && c.props.alignSelf !== 'stretch') || props.alignItems === 'start' || props.alignItems === 'center' || props.alignItems === 'end';
987
1001
  if (!explicitCol) {
988
- body.push(indent(L + 1) + frameExpr('fillW', c.props?.textAlign === 'center' ? '.center' : c.props?.textAlign === 'right' ? '.trailing' : '.leading'));
1002
+ body.push(indent(L + 1) + frameExpr('fillW', columnStretchAlignment(c)));
989
1003
  }
990
1004
  }
991
1005
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dongmingzhixiu/vuexir-compiler",
3
- "version": "0.7.6",
3
+ "version": "0.7.8",
4
4
  "description": "Vue SFC → IR → 原生源码(SwiftUI / Compose)编译器核心:纯函数、零 IO",
5
5
  "type": "module",
6
6
  "license": "MIT",