@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.
- package/dist/codegen/compose.d.ts +2 -0
- package/dist/codegen/compose.js +50 -21
- package/dist/codegen/swiftui.js +21 -7
- package/package.json +1 -1
|
@@ -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)。
|
package/dist/codegen/compose.js
CHANGED
|
@@ -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
|
|
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'
|
|
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 = {
|
|
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)
|
|
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
|
-
/**
|
|
1768
|
+
/** 纯汉字 tab 保持两字一行,其他标题由系统按可用宽度换行。 */
|
|
1748
1769
|
function tabLabel(title) {
|
|
1749
|
-
const
|
|
1750
|
-
return
|
|
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
|
|
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
|
|
1874
|
-
return
|
|
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' &&
|
|
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' &&
|
|
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
|
-
|
|
2076
|
-
|
|
2077
|
-
|
|
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 =
|
|
2145
|
-
label = { Text(
|
|
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
|
},
|
package/dist/codegen/swiftui.js
CHANGED
|
@@ -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
|
|
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
|
|
1002
|
+
body.push(indent(L + 1) + frameExpr('fillW', columnStretchAlignment(c)));
|
|
989
1003
|
}
|
|
990
1004
|
}
|
|
991
1005
|
});
|