@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.
- package/dist/codegen/compose.d.ts +1 -0
- package/dist/codegen/compose.js +37 -19
- package/dist/codegen/swiftui.js +9 -5
- package/package.json +1 -1
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:文本绑定里引用的标识符若既不是页面状态也不是循环变量 → 视为未支持,回退空串 */
|
|
@@ -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)
|
|
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
|
-
/**
|
|
1768
|
+
/** 纯汉字 tab 保持两字一行,其他标题由系统按可用宽度换行。 */
|
|
1759
1769
|
function tabLabel(title) {
|
|
1760
|
-
const
|
|
1761
|
-
return
|
|
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
|
|
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
|
|
1885
|
-
return
|
|
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' &&
|
|
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' &&
|
|
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
|
-
|
|
2087
|
-
|
|
2088
|
-
|
|
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 =
|
|
2156
|
-
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) },
|
|
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
|
},
|
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 语义一致)
|
|
@@ -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;
|