@dongmingzhixiu/vuexir-compiler 0.7.7 → 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.
@@ -109,6 +109,7 @@ declare function buildNavHost(bundleId: string, tabs: {
109
109
  mode?: string;
110
110
  }[], options?: {
111
111
  tabBar?: TabBarSpec;
112
+ nativeNavigationBar?: boolean;
112
113
  }): string;
113
114
  /**
114
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:文本绑定里引用的标识符若既不是页面状态也不是循环变量 → 视为未支持,回退空串 */
@@ -1381,7 +1391,7 @@ function emitNodeInner(node, ctx, depth, loop, useParamModifier = false, parentS
1381
1391
  const segs = p.bindParts
1382
1392
  .map((seg) => {
1383
1393
  if ('text' in seg)
1384
- return String(seg.text).replace(/\\/g, '\\\\').replace(/"/g, '\\"').replace(/\$/g, '${\'$\'}').replace(/\n/g, '\\n');
1394
+ return kotlinStringContents(String(seg.text));
1385
1395
  if ('bind' in seg)
1386
1396
  return '${' + safeBind(seg.bind, ctx, loop) + '}';
1387
1397
  if ('fieldOf' in seg)
@@ -1755,10 +1765,10 @@ import androidx.compose.ui.unit.dp
1755
1765
  import androidx.compose.ui.unit.sp
1756
1766
  `;
1757
1767
  const TAB_ICONS = ['home.fill', 'book.fill', 'square.grid.2x2.fill', 'bell.fill', 'gearshape.fill', 'explore', 'work'];
1758
- /** tab 文案拆 2 字一行(对位 iOS 窄 tab 自动换行视觉) */
1768
+ /** 纯汉字 tab 保持两字一行,其他标题由系统按可用宽度换行。 */
1759
1769
  function tabLabel(title) {
1760
- const t = title.replace(/\s+/g, '');
1761
- 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;
1762
1772
  }
1763
1773
  /**
1764
1774
  * 工程根推导(R75):路由文件可能在 src/router/index.ts,`@/` 别名需要相对「工程根」解析。
@@ -1878,14 +1888,20 @@ function parseRouter(src, dir, routerFile) {
1878
1888
  meta,
1879
1889
  });
1880
1890
  }
1881
- 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) => {
1882
1898
  if (!file || !existsSync(file))
1883
1899
  return false;
1884
- const tpl = readFileSync(file, 'utf-8').match(/<template>([\s\S]*)<\/template>/)?.[1] ?? '';
1885
- 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;
1886
1902
  };
1887
1903
  const tabs = routes
1888
- .filter((r) => r.meta.tab === 'true' && hasText(r.file))
1904
+ .filter((r) => r.meta.tab === 'true' && hasRenderableTemplate(r.file))
1889
1905
  .map((r, i) => ({
1890
1906
  id: r.name,
1891
1907
  title: r.meta.title ?? r.name,
@@ -1894,7 +1910,7 @@ function parseRouter(src, dir, routerFile) {
1894
1910
  path: r.path,
1895
1911
  }));
1896
1912
  const details = routes
1897
- .filter((r) => r.meta.tab !== 'true' && hasText(r.file))
1913
+ .filter((r) => r.meta.tab !== 'true' && hasRenderableTemplate(r.file))
1898
1914
  .map((r) => ({ id: r.name, title: r.meta.title ?? r.name, file: r.file, path: r.path }));
1899
1915
  if (tabs.length === 0) {
1900
1916
  warnings.push('router 未解析到任何 tab 路由(routes 需含 meta: { tab: true })');
@@ -2083,9 +2099,10 @@ function runTabs(appDir, opts = {}) {
2083
2099
  console.log(`[gen-android] - ${pageId}DetailView.kt · push「${d.title}」`);
2084
2100
  }
2085
2101
  // 3) AppNavHost.kt(router.ts → NavHost + NavigationBar)
2086
- let navHost = buildNavHost(bundleId, tabs, detailViews, { tabBar: opts.tabBar });
2087
- if (opts.nativeNavigationBar === false)
2088
- 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
+ });
2089
2106
  writeFileSync(join(pkgDir, 'AppNavHost.kt'), navHost);
2090
2107
  // P2:写 .vrn/manifest.json(工具链/模板版本 + 文件清单),供 vrn doctor 校验与增量重生成
2091
2108
  const metaDir = join(outDir, '.vrn');
@@ -2112,6 +2129,7 @@ function buildNavHost(bundleId, tabs, details, options = {}) {
2112
2129
  .map((t) => `composable("${t.id}") { TabScaffold("${t.id}", onSelectTab) { p -> ${toPascal(t.id)}Content(modifier = Modifier.padding(p)) } }
2113
2130
  composable("${t.path}") { TabScaffold("${t.id}", onSelectTab) { p -> ${toPascal(t.id)}Content(modifier = Modifier.padding(p)) } }`)
2114
2131
  .join('\n ');
2132
+ const showNavigationBar = options.nativeNavigationBar !== false;
2115
2133
  // M7 对位 iOS NavigationStack:详情页内层 Scaffold + TopAppBar(标题 + 返回箭头/backText、mode=transparent 透明背景)
2116
2134
  const detailRoutes = details
2117
2135
  .map((d) => {
@@ -2128,7 +2146,7 @@ function buildNavHost(bundleId, tabs, details, options = {}) {
2128
2146
  const barColor = d.mode === 'transparent' ? 'Color.Transparent' : d.bg ? colorExpr(d.bg) : 'InkBar';
2129
2147
  return `composable("${d.path}") {
2130
2148
  Scaffold(
2131
- containerColor = InkBg,
2149
+ containerColor = InkBg,${showNavigationBar ? `
2132
2150
  topBar = {
2133
2151
  TopAppBar(
2134
2152
  title = { Text(${kotlinStr(d.title)}, fontSize = 17.sp, fontWeight = FontWeight.Medium) },
@@ -2141,7 +2159,7 @@ function buildNavHost(bundleId, tabs, details, options = {}) {
2141
2159
  titleContentColor = Gold,
2142
2160
  ),
2143
2161
  )
2144
- },
2162
+ },` : ''}
2145
2163
  ) { innerPadding ->
2146
2164
  ${toPascal(d.id)}DetailContent(modifier = Modifier.padding(innerPadding))
2147
2165
  }
@@ -2152,8 +2170,8 @@ function buildNavHost(bundleId, tabs, details, options = {}) {
2152
2170
  .map((t) => `NavigationBarItem(
2153
2171
  selected = currentTab == "${t.id}",
2154
2172
  onClick = { onSelectTab("${t.id}") },
2155
- icon = { Icon(${ICON_MAP[t.icon] ?? 'Icons.Default.Home'}, contentDescription = "${t.title}") },
2156
- 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) },
2157
2175
  colors = NavigationBarItemDefaults.colors(
2158
2176
  selectedIconColor = Gold,
2159
2177
  selectedTextColor = Gold,
@@ -2217,7 +2235,7 @@ import androidx.navigation.compose.*
2217
2235
  @Composable
2218
2236
  fun TabScaffold(currentTab: String, onSelectTab: (String) -> Unit, content: @Composable (PaddingValues) -> Unit) {
2219
2237
  Scaffold(
2220
- containerColor = InkBg,
2238
+ containerColor = InkBg,${showNavigationBar ? `
2221
2239
  topBar = {
2222
2240
  TopAppBar(
2223
2241
  title = { Text(${tabTitleExpr}, fontSize = 17.sp, fontWeight = FontWeight.Medium) },
@@ -2226,7 +2244,7 @@ fun TabScaffold(currentTab: String, onSelectTab: (String) -> Unit, content: @Com
2226
2244
  titleContentColor = Gold,
2227
2245
  ),
2228
2246
  )
2229
- },
2247
+ },` : ''}
2230
2248
  bottomBar = {
2231
2249
  ${customBar || `NavigationBar(containerColor = InkBar) {\n ${barItems}\n }`}
2232
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 语义一致)
@@ -707,10 +707,14 @@ function emitNodeInner(node, ctx) {
707
707
  return pad(`VrnFractionWidth(fraction: max(0,min(1,${fraction}))) {`) + `\n${inner}\n${pad('}')}`;
708
708
  }
709
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' : '';
710
714
  // box-sizing:border-box(CSS 默认):固定 height 应包含 padding。
711
715
  // 故 height 为数字且存在 padding 时,padding 置于 frame(height) 之前、
712
716
  // frame 限定总视觉高度(否则 padding 加在 frame 外 → 视觉高 = height + 2×padding)。
713
- const size = sizeModifiers(props, axis, ctx);
717
+ const size = sizeModifiers(props, axis, ctx, fixedFrameAlignment);
714
718
  let padMods = paddingModifiers(props);
715
719
  let boxMods = boxModifiers(props);
716
720
  if (node.type === 'image' && props.objectFit === 'cover' && !props.clip) {
@@ -723,7 +727,7 @@ function emitNodeInner(node, ctx) {
723
727
  boxMods = boxMods.filter((m) => !m.startsWith('.background(') && !m.startsWith('.cornerRadius(') && !m.startsWith('.overlay('));
724
728
  }
725
729
  const box = typeof props.height === 'number' && (padMods.length > 0 || capsule !== undefined)
726
- ? [...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]
727
731
  : [...size, ...(capsule !== undefined ? [capsule] : []), ...padMods, ...boxMods];
728
732
  // M5 transform:scale / rotate / translate → 原生修饰符(视觉变换,不影响布局占位)
729
733
  const t = props.transform;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dongmingzhixiu/vuexir-compiler",
3
- "version": "0.7.7",
3
+ "version": "0.7.8",
4
4
  "description": "Vue SFC → IR → 原生源码(SwiftUI / Compose)编译器核心:纯函数、零 IO",
5
5
  "type": "module",
6
6
  "license": "MIT",