@dongmingzhixiu/vuexir-compiler 0.1.1 → 0.2.1
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 +17 -3
- package/dist/codegen/compose.js +324 -39
- package/dist/codegen/swiftui.d.ts +1 -0
- package/dist/codegen/swiftui.js +65 -24
- package/dist/index.d.ts +2 -2
- package/dist/index.js +2 -2
- package/dist/ir/schema.d.ts +16 -0
- package/dist/ir/schema.js +4 -3
- package/dist/transform/stores.d.ts +55 -0
- package/dist/transform/stores.js +303 -0
- package/dist/transform/vue2ir.d.ts +4 -3
- package/dist/transform/vue2ir.js +594 -21
- package/package.json +7 -7
|
@@ -40,12 +40,23 @@ declare function emitNode(node: IrNode, ctx: ComposeCtx, depth: number, loop?: {
|
|
|
40
40
|
itemKey: string;
|
|
41
41
|
indexKey?: string;
|
|
42
42
|
}, useParamModifier?: boolean, parentStretch?: boolean): string;
|
|
43
|
-
/** R40 组件表:页面同目录 + 向上逐级查找 components/*.vue(对位 gen-ios loadComponents,支持样例根共享组件)
|
|
44
|
-
* 优先级:页面本地 components/ 覆盖祖先目录同名组件(先收集祖先、最后收集本地) */
|
|
45
43
|
declare function loadComponents(pageFile: string): Record<string, string>;
|
|
46
44
|
/** 组件 → 独立 Composable 文件(对位 iOS 组件独立 Swift 文件) */
|
|
47
45
|
declare function emitComponentFile(name: string, ui: IrNode, bundleId: string): string;
|
|
48
|
-
|
|
46
|
+
/**
|
|
47
|
+
* 工程根推导(R75):路由文件可能在 src/router/index.ts,`@/` 别名需要相对「工程根」解析。
|
|
48
|
+
* 自 startDir 向上找 含 package.json 或 src/ 的目录(最多 6 层)。
|
|
49
|
+
*/
|
|
50
|
+
export declare function findProjectRoot(startDir: string): string;
|
|
51
|
+
/**
|
|
52
|
+
* R75 router 解析器 v2:支持真实 Vue 工程的路由写法
|
|
53
|
+
* - 静态 import X from 'p' 与 动态 component: () => import('p')
|
|
54
|
+
* - `@/` 别名(映射到 <appDir>/src/)与相对路径
|
|
55
|
+
* - 多行路由对象(按 {} 配平扫描,不再依赖整行正则)
|
|
56
|
+
* - 路径参数(/detail/:id)→ 原生侧降级为无参路径 + 警告(R15)
|
|
57
|
+
* 返回 tabs/details + warnings(由调用方打印)
|
|
58
|
+
*/
|
|
59
|
+
declare function parseRouter(src: string, dir: string, routerFile?: string): {
|
|
49
60
|
tabs: {
|
|
50
61
|
id: string;
|
|
51
62
|
title: string;
|
|
@@ -59,6 +70,7 @@ declare function parseRouter(src: string, dir: string): {
|
|
|
59
70
|
file: string;
|
|
60
71
|
path: string;
|
|
61
72
|
}[];
|
|
73
|
+
warnings: string[];
|
|
62
74
|
};
|
|
63
75
|
export interface RunTabsOptions {
|
|
64
76
|
/** 产物根(默认 <repo>/compiler/generated/android 或 VRN_OUT_DIR) */
|
|
@@ -67,6 +79,8 @@ export interface RunTabsOptions {
|
|
|
67
79
|
projectName?: string;
|
|
68
80
|
/** bundle id(默认 com.vrn.<projectname 小写>) */
|
|
69
81
|
bundleId?: string;
|
|
82
|
+
/** 显式路由文件(支持 src/router/index.ts 等非标准命名;默认 <appDir>/router.ts) */
|
|
83
|
+
routerFile?: string;
|
|
70
84
|
/** P2:工程骨架模板由 @dongmingzhixiu/vuexir-templates 提供、CLI 注入(避免 compiler → templates 的依赖倒挂)
|
|
71
85
|
* 回调式:runTabs 计算完 projectName/bundleId 后再渲染模板,避免调用方拿不到最终值 */
|
|
72
86
|
renderGradle?: (projectName: string, bundleId: string) => string;
|
package/dist/codegen/compose.js
CHANGED
|
@@ -92,6 +92,7 @@ class MainActivity : ComponentActivity() {
|
|
|
92
92
|
override fun onCreate(savedInstanceState: Bundle?) {
|
|
93
93
|
super.onCreate(savedInstanceState)
|
|
94
94
|
Store.init(applicationContext)
|
|
95
|
+
VrnBridge.init(this)
|
|
95
96
|
// R-SYSTEMBAR:应用固定深色主题(InkBg/InkBar),与系统明暗设置无关。
|
|
96
97
|
// enableEdgeToEdge() 默认用 SystemBarStyle.auto(...),会跟随系统明暗切换图标明暗——
|
|
97
98
|
// 系统处于浅色模式时给出深色图标,落在深色顶栏/底栏上不可读(真机反馈:系统栏文字看不清)。
|
|
@@ -146,6 +147,40 @@ const RUNTIME_KT = (bundleId) => `package ${bundleId}
|
|
|
146
147
|
|
|
147
148
|
// Vuexir Android 运行时桥(对位 iOS RUNTIME_SWIFT)
|
|
148
149
|
/** R73 持久化桥:对位 iOS runtime 的 localStorage(UserDefaults)—— SharedPreferences 同步读写 */
|
|
150
|
+
/** R78 对象字段取值:状态里存 JSON 字符串时按 key 取值 */
|
|
151
|
+
// R85 点路径取值:path 支持 "a.b.c",末段可为 length/size(数组/对象/字符串长度)
|
|
152
|
+
fun vrnField(json: String, path: String, fallback: String = ""): String {
|
|
153
|
+
val raw = vrnFieldRaw(json, path)
|
|
154
|
+
return if (raw.isEmpty()) fallback else raw
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
fun vrnFieldRaw(json: String, path: String): String {
|
|
158
|
+
return try {
|
|
159
|
+
var cur: Any = org.json.JSONTokener(json).nextValue()
|
|
160
|
+
for (seg in path.split(".")) {
|
|
161
|
+
cur = when {
|
|
162
|
+
seg == "length" || seg == "size" -> when (cur) {
|
|
163
|
+
is org.json.JSONArray -> cur.length()
|
|
164
|
+
is org.json.JSONObject -> cur.length()
|
|
165
|
+
is String -> cur.length
|
|
166
|
+
else -> return ""
|
|
167
|
+
}
|
|
168
|
+
cur is org.json.JSONObject && cur.has(seg) -> cur.get(seg)
|
|
169
|
+
else -> return ""
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
when (cur) {
|
|
173
|
+
is String -> cur
|
|
174
|
+
is Boolean -> if (cur) "true" else "false"
|
|
175
|
+
is Double -> if (cur == Math.floor(cur) && !cur.isInfinite()) cur.toInt().toString() else cur.toString()
|
|
176
|
+
is Int, is Long -> cur.toString()
|
|
177
|
+
else -> cur.toString()
|
|
178
|
+
}
|
|
179
|
+
} catch (e: Throwable) {
|
|
180
|
+
""
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
149
184
|
object Store {
|
|
150
185
|
private var prefs: android.content.SharedPreferences? = null
|
|
151
186
|
fun init(ctx: android.content.Context) {
|
|
@@ -156,8 +191,82 @@ object Store {
|
|
|
156
191
|
fun remove(key: String) { prefs?.edit()?.remove(key)?.apply() }
|
|
157
192
|
}
|
|
158
193
|
|
|
194
|
+
/**
|
|
195
|
+
* R81 原生桥(Android)——编译期注入;Vue 侧 bridge.toast(...) 由编译器翻译为 VrnBridge.toast(...)
|
|
196
|
+
* 与 @dongmingzhixiu/vuexir-templates 的 VRN_BRIDGE_KT 保持一致
|
|
197
|
+
*/
|
|
198
|
+
object VrnBridge {
|
|
199
|
+
private var appCtx: android.content.Context? = null
|
|
200
|
+
private var currentActivity: android.app.Activity? = null
|
|
201
|
+
fun init(ctx: android.content.Context) {
|
|
202
|
+
if (appCtx == null) appCtx = ctx.applicationContext
|
|
203
|
+
if (ctx is android.app.Activity) currentActivity = ctx
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
fun vibrate(milliseconds: Int = 18) {
|
|
207
|
+
val ctx = appCtx ?: return
|
|
208
|
+
try {
|
|
209
|
+
val v = ctx.getSystemService(android.content.Context.VIBRATOR_SERVICE) as? android.os.Vibrator
|
|
210
|
+
if (v == null || !v.hasVibrator()) return
|
|
211
|
+
if (android.os.Build.VERSION.SDK_INT >= 26) {
|
|
212
|
+
v.vibrate(android.os.VibrationEffect.createOneShot(milliseconds.toLong(), android.os.VibrationEffect.DEFAULT_AMPLITUDE))
|
|
213
|
+
} else {
|
|
214
|
+
@Suppress("DEPRECATION")
|
|
215
|
+
v.vibrate(milliseconds.toLong())
|
|
216
|
+
}
|
|
217
|
+
} catch (e: Throwable) {
|
|
218
|
+
// 无马达/无权限:静默忽略
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
fun copy(text: String) {
|
|
223
|
+
val ctx = appCtx ?: return
|
|
224
|
+
try {
|
|
225
|
+
val cm = ctx.getSystemService(android.content.Context.CLIPBOARD_SERVICE) as android.content.ClipboardManager
|
|
226
|
+
cm.setPrimaryClip(android.content.ClipData.newPlainText("vrn", text))
|
|
227
|
+
} catch (e: Throwable) {
|
|
228
|
+
// 忽略
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
fun setStatusBar(style: String) {
|
|
233
|
+
val act = currentActivity ?: return
|
|
234
|
+
android.os.Handler(act.mainLooper).post {
|
|
235
|
+
val light = style != "dark"
|
|
236
|
+
val controller = androidx.core.view.WindowCompat.getInsetsController(act.window, act.window.decorView)
|
|
237
|
+
controller.isAppearanceLightStatusBars = !light
|
|
238
|
+
controller.isAppearanceLightNavigationBars = !light
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
fun keepScreenOn(on: Boolean) {
|
|
243
|
+
val act = currentActivity ?: return
|
|
244
|
+
android.os.Handler(act.mainLooper).post {
|
|
245
|
+
if (on) act.window.addFlags(android.view.WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
|
|
246
|
+
else act.window.clearFlags(android.view.WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
fun toast(message: String) {
|
|
250
|
+
val ctx = appCtx ?: return
|
|
251
|
+
android.os.Handler(ctx.mainLooper).post { android.widget.Toast.makeText(ctx, message, android.widget.Toast.LENGTH_SHORT).show() }
|
|
252
|
+
}
|
|
253
|
+
fun openUrl(url: String) {
|
|
254
|
+
val ctx = appCtx ?: return
|
|
255
|
+
try {
|
|
256
|
+
val i = android.content.Intent(android.content.Intent.ACTION_VIEW, android.net.Uri.parse(url)).apply { addFlags(android.content.Intent.FLAG_ACTIVITY_NEW_TASK) }
|
|
257
|
+
ctx.startActivity(i)
|
|
258
|
+
} catch (e: Throwable) {
|
|
259
|
+
toast("无法打开链接")
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
159
264
|
object Runtime {
|
|
160
265
|
var pushHandler: ((String) -> Unit)? = null
|
|
266
|
+
var backHandler: (() -> Unit)? = null
|
|
267
|
+
fun back() {
|
|
268
|
+
backHandler?.invoke()
|
|
269
|
+
}
|
|
161
270
|
fun push(route: String) {
|
|
162
271
|
pushHandler?.invoke(route)
|
|
163
272
|
}
|
|
@@ -228,6 +337,10 @@ function kotlinStr(s) {
|
|
|
228
337
|
}
|
|
229
338
|
/** 条件表达式 → Kotlin(对位 iOS swiftCondExpr:=== / !== 归一 + 单引号字符串转双引号) */
|
|
230
339
|
function kotlinCondExpr(expr) {
|
|
340
|
+
// R78 续(G12):对象字段条件 → 非空判断
|
|
341
|
+
const f = expr.match(/^__vrnField__:(\w+):(\w+)$/);
|
|
342
|
+
if (f)
|
|
343
|
+
return `vrnField(${f[1]}, "${f[2]}").isNotEmpty()`;
|
|
231
344
|
let s = expr.replace(/!==/g, '!=').replace(/===/g, '==');
|
|
232
345
|
s = s.replace(/'(\\[^]|[^'\\])*'/g, (m) => '"' + m.slice(1, -1).replace(/"/g, '\\"') + '"');
|
|
233
346
|
return s;
|
|
@@ -389,17 +502,35 @@ function arrangementExpr(props, kind) {
|
|
|
389
502
|
const parts = [];
|
|
390
503
|
const horiz = kind === 'Row' ? 'horizontalArrangement' : 'verticalArrangement';
|
|
391
504
|
const vert = kind === 'Row' ? 'verticalAlignment' : 'horizontalAlignment';
|
|
392
|
-
|
|
393
|
-
parts.push(`${horiz} = Arrangement.spacedBy(${dp(props.gap)})`);
|
|
505
|
+
// R79:gap 与 justify-content 共用同一个排列参数 → 必须合并(否则 Kotlin 报 duplicate argument)
|
|
394
506
|
const just = props.justifyContent;
|
|
395
|
-
|
|
507
|
+
const justAlign = just === 'center'
|
|
508
|
+
? kind === 'Row'
|
|
509
|
+
? 'Alignment.CenterHorizontally'
|
|
510
|
+
: 'Alignment.CenterVertically'
|
|
511
|
+
: just === 'end'
|
|
512
|
+
? kind === 'Row'
|
|
513
|
+
? 'Alignment.End'
|
|
514
|
+
: 'Alignment.Bottom'
|
|
515
|
+
: '';
|
|
516
|
+
if (props.gap !== undefined && justAlign !== '') {
|
|
517
|
+
parts.push(`${horiz} = Arrangement.spacedBy(${dp(props.gap)}, ${justAlign})`);
|
|
518
|
+
}
|
|
519
|
+
else if (props.gap !== undefined) {
|
|
520
|
+
parts.push(`${horiz} = Arrangement.spacedBy(${dp(props.gap)})`);
|
|
521
|
+
}
|
|
522
|
+
else if (just === 'center') {
|
|
396
523
|
parts.push(`${horiz} = Arrangement.Center`);
|
|
397
|
-
|
|
524
|
+
}
|
|
525
|
+
else if (just === 'end') {
|
|
398
526
|
parts.push(`${horiz} = Arrangement.${kind === 'Row' ? 'End' : 'Bottom'}`);
|
|
399
|
-
|
|
527
|
+
}
|
|
528
|
+
else if (just === 'spaceBetween') {
|
|
400
529
|
parts.push(`${horiz} = Arrangement.SpaceBetween`);
|
|
401
|
-
|
|
530
|
+
}
|
|
531
|
+
else if (just === 'spaceAround') {
|
|
402
532
|
parts.push(`${horiz} = Arrangement.SpaceAround`);
|
|
533
|
+
}
|
|
403
534
|
const align = props.alignItems;
|
|
404
535
|
if (align === 'center')
|
|
405
536
|
parts.push(`${vert} = Alignment.${kind === 'Row' ? 'CenterVertically' : 'CenterHorizontally'}`);
|
|
@@ -612,6 +743,20 @@ function asyncLineKotlin(line, ctx) {
|
|
|
612
743
|
return null;
|
|
613
744
|
return `if (${cond}) {`;
|
|
614
745
|
}
|
|
746
|
+
// R81/R84 原生桥语句:bridge.toast('x') / bridge.vibrate(18) / bridge.copy('x') …
|
|
747
|
+
const bm = line.match(/^bridge\.(toast|openUrl|vibrate|copy|setStatusBar|keepScreenOn)\((.+)\);?$/);
|
|
748
|
+
if (bm) {
|
|
749
|
+
const arg = jsExprToKotlin(bm[2].trim());
|
|
750
|
+
return arg === null ? null : `VrnBridge.${bm[1]}(${arg})`;
|
|
751
|
+
}
|
|
752
|
+
// G15 router 语句:router.push('x') / router.back() → Runtime.push / Runtime.back
|
|
753
|
+
const rp = line.match(/^router\.push\((.+)\);?$/);
|
|
754
|
+
if (rp) {
|
|
755
|
+
const a = jsExprToKotlin(rp[1].trim());
|
|
756
|
+
return a === null ? null : `Runtime.push(${a})`;
|
|
757
|
+
}
|
|
758
|
+
if (/^router\.back\(\)\s*;?$/.test(line))
|
|
759
|
+
return 'Runtime.back()';
|
|
615
760
|
// R73 localStorage(语句形态):setItem / removeItem
|
|
616
761
|
const lsSet = line.match(/^localStorage\.setItem\((.+),\s*(.+)\);?$/);
|
|
617
762
|
if (lsSet) {
|
|
@@ -682,6 +827,8 @@ function jsExprToKotlin(expr) {
|
|
|
682
827
|
s = s.replace(/\.length\b(?!\()/g, '.length()');
|
|
683
828
|
// R73 localStorage → Store(函数式写法:getItem 直接用;setItem/removeItem 走语句分支)
|
|
684
829
|
s = s.replace(/localStorage\.getItem\((.+)\)$/g, (_m, k) => `Store.get(${k})`);
|
|
830
|
+
// R81 bridge.* → VrnBridge.*(表达式形态)
|
|
831
|
+
s = s.replace(/\bbridge\.(toast|openUrl|vibrate|copy|setStatusBar|keepScreenOn)\(/g, (_m, fn) => `VrnBridge.${fn}(`);
|
|
685
832
|
// JS 单引号字符串 → Kotlin 双引号(Kotlin 的单引号是 Char 字面量,'x' 编译不过)
|
|
686
833
|
s = s.replace(/'([^']*)'/g, (_m, inner) => `"${inner.replace(/"/g, '\\"')}"`);
|
|
687
834
|
// JS 类型转换 → Kotlin
|
|
@@ -867,11 +1014,29 @@ function emitNodeInner(node, ctx, depth, loop, useParamModifier = false, parentS
|
|
|
867
1014
|
return String(seg.text).replace(/\\/g, '\\\\').replace(/"/g, '\\"').replace(/\$/g, '${\'$\'}').replace(/\n/g, '\\n');
|
|
868
1015
|
if ('bind' in seg)
|
|
869
1016
|
return '${' + loopField(seg.bind, loop) + '}';
|
|
1017
|
+
if ('fieldOf' in seg)
|
|
1018
|
+
return ('${vrnField(' +
|
|
1019
|
+
seg.fieldOf.obj +
|
|
1020
|
+
', "' +
|
|
1021
|
+
seg.fieldOf.field +
|
|
1022
|
+
'"' +
|
|
1023
|
+
(seg.fieldOf.fallback !== undefined ? ', "' + seg.fieldOf.fallback.replace(/"/g, '\\"') + '"' : '') +
|
|
1024
|
+
')}');
|
|
870
1025
|
return '${' + loopField(seg.dynField.var, loop) + '["' + seg.dynField.field + '"] ?: ""}';
|
|
871
1026
|
})
|
|
872
1027
|
.join('');
|
|
873
1028
|
textExpr = '"' + segs + '"';
|
|
874
1029
|
}
|
|
1030
|
+
else if (p.fieldOf) {
|
|
1031
|
+
textExpr =
|
|
1032
|
+
'"${vrnField(' +
|
|
1033
|
+
p.fieldOf.obj +
|
|
1034
|
+
', "' +
|
|
1035
|
+
p.fieldOf.field +
|
|
1036
|
+
'"' +
|
|
1037
|
+
(p.fieldOf.fallback !== undefined ? ', "' + p.fieldOf.fallback.replace(/"/g, '\\"') + '"' : '') +
|
|
1038
|
+
')}"';
|
|
1039
|
+
}
|
|
875
1040
|
else if (p.dynField) {
|
|
876
1041
|
textExpr = '"${' + loopField(p.dynField.var, loop) + '["' + p.dynField.field + '"] ?: ""}"';
|
|
877
1042
|
}
|
|
@@ -1071,6 +1236,26 @@ function emitNodeInner(node, ctx, depth, loop, useParamModifier = false, parentS
|
|
|
1071
1236
|
/* ================= Router 解析(对位 gen-ios.ts parseRouter) ================= */
|
|
1072
1237
|
/** R40 组件表:页面同目录 + 向上逐级查找 components/*.vue(对位 gen-ios loadComponents,支持样例根共享组件)
|
|
1073
1238
|
* 优先级:页面本地 components/ 覆盖祖先目录同名组件(先收集祖先、最后收集本地) */
|
|
1239
|
+
/** R86 扫描工程内的 Pinia store 源码(src/stores/*.ts),供编译器做状态折叠 */
|
|
1240
|
+
function loadStoreSources(anchor) {
|
|
1241
|
+
const table = {};
|
|
1242
|
+
let up = anchor;
|
|
1243
|
+
for (let i = 0; i < 6 && up !== dirname(up); i++) {
|
|
1244
|
+
for (const cand of [join(up, 'stores'), join(up, 'src', 'stores')]) {
|
|
1245
|
+
if (existsSync(cand)) {
|
|
1246
|
+
for (const f of readdirSync(cand)) {
|
|
1247
|
+
if (!/\.(ts|js|mts)$/.test(f))
|
|
1248
|
+
continue;
|
|
1249
|
+
table[f] = readFileSync(join(cand, f), 'utf-8');
|
|
1250
|
+
}
|
|
1251
|
+
if (Object.keys(table).length > 0)
|
|
1252
|
+
return table;
|
|
1253
|
+
}
|
|
1254
|
+
}
|
|
1255
|
+
up = dirname(up);
|
|
1256
|
+
}
|
|
1257
|
+
return table;
|
|
1258
|
+
}
|
|
1074
1259
|
function loadComponents(pageFile) {
|
|
1075
1260
|
const dirs = [];
|
|
1076
1261
|
let up = dirname(pageFile);
|
|
@@ -1144,51 +1329,139 @@ function tabLabel(title) {
|
|
|
1144
1329
|
const t = title.replace(/\s+/g, '');
|
|
1145
1330
|
return t.length > 2 ? t.match(/.{1,2}/g).join('\\n') : t;
|
|
1146
1331
|
}
|
|
1147
|
-
|
|
1332
|
+
/**
|
|
1333
|
+
* 工程根推导(R75):路由文件可能在 src/router/index.ts,`@/` 别名需要相对「工程根」解析。
|
|
1334
|
+
* 自 startDir 向上找 含 package.json 或 src/ 的目录(最多 6 层)。
|
|
1335
|
+
*/
|
|
1336
|
+
export function findProjectRoot(startDir) {
|
|
1337
|
+
let d = startDir;
|
|
1338
|
+
for (let i = 0; i < 6; i += 1) {
|
|
1339
|
+
if (existsSync(join(d, 'package.json')) || existsSync(join(d, 'src')))
|
|
1340
|
+
return d;
|
|
1341
|
+
const parent = dirname(d);
|
|
1342
|
+
if (parent === d)
|
|
1343
|
+
break;
|
|
1344
|
+
d = parent;
|
|
1345
|
+
}
|
|
1346
|
+
return startDir;
|
|
1347
|
+
}
|
|
1348
|
+
/**
|
|
1349
|
+
* R75 router 解析器 v2:支持真实 Vue 工程的路由写法
|
|
1350
|
+
* - 静态 import X from 'p' 与 动态 component: () => import('p')
|
|
1351
|
+
* - `@/` 别名(映射到 <appDir>/src/)与相对路径
|
|
1352
|
+
* - 多行路由对象(按 {} 配平扫描,不再依赖整行正则)
|
|
1353
|
+
* - 路径参数(/detail/:id)→ 原生侧降级为无参路径 + 警告(R15)
|
|
1354
|
+
* 返回 tabs/details + warnings(由调用方打印)
|
|
1355
|
+
*/
|
|
1356
|
+
function parseRouter(src, dir, routerFile) {
|
|
1357
|
+
const warnings = [];
|
|
1148
1358
|
const imports = {};
|
|
1149
1359
|
for (const m of src.matchAll(/import\s+(\w+)\s+from\s+['"]([^'"]+)['"]/g))
|
|
1150
1360
|
imports[m[1]] = m[2];
|
|
1361
|
+
/** 别名/相对路径 → 磁盘路径(先找 @/ → src,再退回 appDir 根) */
|
|
1362
|
+
const baseDir = routerFile ? dirname(routerFile) : dir;
|
|
1363
|
+
const resolveFile = (spec) => {
|
|
1364
|
+
const cands = [];
|
|
1365
|
+
if (spec.startsWith('@/')) {
|
|
1366
|
+
const rel = spec.slice(2);
|
|
1367
|
+
cands.push(join(dir, 'src', rel), join(dir, rel));
|
|
1368
|
+
}
|
|
1369
|
+
else if (spec.startsWith('./') || spec.startsWith('../')) {
|
|
1370
|
+
// 相对路径以「路由文件所在目录」为基准
|
|
1371
|
+
cands.push(join(baseDir, spec), join(dir, spec));
|
|
1372
|
+
}
|
|
1373
|
+
else {
|
|
1374
|
+
cands.push(join(dir, spec), join(dir, 'src', spec));
|
|
1375
|
+
}
|
|
1376
|
+
return cands.find((c) => existsSync(c)) ?? cands[0];
|
|
1377
|
+
};
|
|
1378
|
+
// 配平扫描所有「含 path:」的对象块
|
|
1379
|
+
const blocks = [];
|
|
1380
|
+
for (let i = 0; i < src.length; i += 1) {
|
|
1381
|
+
if (src[i] !== '{')
|
|
1382
|
+
continue;
|
|
1383
|
+
let depth = 0;
|
|
1384
|
+
let quote = '';
|
|
1385
|
+
let j = i;
|
|
1386
|
+
for (; j < src.length; j += 1) {
|
|
1387
|
+
const ch = src[j];
|
|
1388
|
+
if (quote) {
|
|
1389
|
+
if (ch === quote)
|
|
1390
|
+
quote = '';
|
|
1391
|
+
continue;
|
|
1392
|
+
}
|
|
1393
|
+
if (ch === '"' || ch === "'" || ch === '`') {
|
|
1394
|
+
quote = ch;
|
|
1395
|
+
continue;
|
|
1396
|
+
}
|
|
1397
|
+
if (ch === '{')
|
|
1398
|
+
depth += 1;
|
|
1399
|
+
else if (ch === '}') {
|
|
1400
|
+
depth -= 1;
|
|
1401
|
+
if (depth === 0)
|
|
1402
|
+
break;
|
|
1403
|
+
}
|
|
1404
|
+
}
|
|
1405
|
+
const block = src.slice(i, j + 1);
|
|
1406
|
+
if (/\bpath\s*:/.test(block))
|
|
1407
|
+
blocks.push(block);
|
|
1408
|
+
i = j;
|
|
1409
|
+
}
|
|
1151
1410
|
const routes = [];
|
|
1152
|
-
for (const
|
|
1411
|
+
for (const block of blocks) {
|
|
1412
|
+
const pm = block.match(/\bpath\s*:\s*['"]([^'"]*)['"]/);
|
|
1413
|
+
if (!pm)
|
|
1414
|
+
continue;
|
|
1415
|
+
const rawPath = pm[1];
|
|
1416
|
+
const nm = block.match(/\bname\s*:\s*['"]([^'"]*)['"]/);
|
|
1417
|
+
const cm = block.match(/\bcomponent\s*:\s*(?:\(\s*\)\s*=>\s*)?import\(\s*['"]([^'"]+)['"]\s*\)|\bcomponent\s*:\s*([A-Za-z_$][\w$]*)/);
|
|
1418
|
+
const compSpec = cm?.[1];
|
|
1419
|
+
const compIdent = cm?.[2];
|
|
1420
|
+
const file = compSpec ? resolveFile(compSpec) : compIdent ? resolveFile(imports[compIdent] ?? '') : '';
|
|
1421
|
+
// meta 解析(允许单行/多行)
|
|
1153
1422
|
const meta = {};
|
|
1154
|
-
const
|
|
1155
|
-
|
|
1156
|
-
const
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1423
|
+
const mm = block.match(/\bmeta\s*:\s*\{([\s\S]*?)\}/);
|
|
1424
|
+
if (mm) {
|
|
1425
|
+
for (const kv of mm[1].split(',')) {
|
|
1426
|
+
const r = kv.match(/^\s*(\w+)\s*:\s*(true|false|'([^']*)'|"([^"]*)"|\w+)\s*$/);
|
|
1427
|
+
if (!r)
|
|
1428
|
+
continue;
|
|
1429
|
+
meta[r[1]] = r[2] === 'true' ? 'true' : r[2] === 'false' ? 'false' : r[3] ?? r[4] ?? r[2];
|
|
1430
|
+
}
|
|
1160
1431
|
}
|
|
1161
|
-
|
|
1432
|
+
const path = rawPath.replace(/\/:\w+\??/g, '');
|
|
1433
|
+
if (path !== rawPath)
|
|
1434
|
+
warnings.push(`路由「${rawPath}」含路径参数,原生侧降级为无参路径「${path || '/'}」(R15)`);
|
|
1435
|
+
routes.push({
|
|
1436
|
+
path: path === '' ? '/' : path,
|
|
1437
|
+
name: nm?.[1] ?? (path.replace(/\//g, '-').replace(/^-|-$/g, '') || 'home'),
|
|
1438
|
+
component: compIdent ?? '',
|
|
1439
|
+
file,
|
|
1440
|
+
meta,
|
|
1441
|
+
});
|
|
1162
1442
|
}
|
|
1163
|
-
const
|
|
1164
|
-
|
|
1165
|
-
.filter((r) => {
|
|
1166
|
-
const p = join(dir, imports[r.component] ?? '');
|
|
1167
|
-
if (!existsSync(p))
|
|
1443
|
+
const hasText = (file) => {
|
|
1444
|
+
if (!file || !existsSync(file))
|
|
1168
1445
|
return true;
|
|
1169
|
-
const
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1446
|
+
const tpl = readFileSync(file, 'utf-8').match(/<template>([\s\S]*)<\/template>/)?.[1] ?? '';
|
|
1447
|
+
return tpl.replace(/<[^>]*>/g, ' ').replace(/\s+/g, ' ').trim().length > 0;
|
|
1448
|
+
};
|
|
1449
|
+
const tabs = routes
|
|
1450
|
+
.filter((r) => r.meta.tab === 'true' && hasText(r.file))
|
|
1174
1451
|
.map((r, i) => ({
|
|
1175
1452
|
id: r.name,
|
|
1176
1453
|
title: r.meta.title ?? r.name,
|
|
1177
|
-
file:
|
|
1454
|
+
file: r.file,
|
|
1178
1455
|
icon: r.meta.icon ?? TAB_ICONS[i % TAB_ICONS.length],
|
|
1179
1456
|
path: r.path,
|
|
1180
1457
|
}));
|
|
1181
1458
|
const details = routes
|
|
1182
1459
|
.filter((r) => r.meta.tab !== 'true')
|
|
1183
|
-
.map((r) => ({
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
}));
|
|
1189
|
-
if (tabs.length === 0)
|
|
1190
|
-
throw new Error('router.ts 未声明 tab 路由');
|
|
1191
|
-
return { tabs, details };
|
|
1460
|
+
.map((r) => ({ id: r.name, title: r.meta.title ?? r.name, file: r.file, path: r.path }));
|
|
1461
|
+
if (tabs.length === 0) {
|
|
1462
|
+
warnings.push('router 未解析到任何 tab 路由(routes 需含 meta: { tab: true })');
|
|
1463
|
+
}
|
|
1464
|
+
return { tabs, details, warnings };
|
|
1192
1465
|
}
|
|
1193
1466
|
/* ================= 工程生成(对位 runTabs) ================= */
|
|
1194
1467
|
/** Material Icons 映射(router meta.icon:iOS SF Symbols / 简写 → compose material icon) */
|
|
@@ -1212,8 +1485,10 @@ const ICON_MAP = {
|
|
|
1212
1485
|
explore: 'Icons.Filled.Explore', work: 'Icons.Filled.Work', settings: 'Icons.Filled.Settings',
|
|
1213
1486
|
};
|
|
1214
1487
|
function runTabs(appDir, opts = {}) {
|
|
1215
|
-
const routerSrc = readFileSync(join(appDir, 'router.ts'), 'utf-8');
|
|
1216
|
-
const { tabs, details } = parseRouter(routerSrc, appDir);
|
|
1488
|
+
const routerSrc = readFileSync(opts.routerFile ?? join(appDir, 'router.ts'), 'utf-8');
|
|
1489
|
+
const { tabs, details, warnings: routerWarnings } = parseRouter(routerSrc, findProjectRoot(appDir), opts.routerFile ?? join(appDir, 'router.ts'));
|
|
1490
|
+
for (const w of routerWarnings)
|
|
1491
|
+
console.log(`[gen-android] 路由提示: ${w}`);
|
|
1217
1492
|
console.log(`[gen-android] 多页模式: router.ts → ${tabs.length} 个 tab 页面` + (details.length ? ` + ${details.length} 个 push 页面` : ''));
|
|
1218
1493
|
const projectName = opts.projectName ? toPascal(opts.projectName) : toPascal(basename(appDir));
|
|
1219
1494
|
const bundleId = opts.bundleId ?? `com.vrn.${projectName.toLowerCase()}`;
|
|
@@ -1264,7 +1539,12 @@ function runTabs(appDir, opts = {}) {
|
|
|
1264
1539
|
}
|
|
1265
1540
|
const src = readFileSync(tab.file, 'utf-8');
|
|
1266
1541
|
const pageId = toPascal(tab.id);
|
|
1267
|
-
const r = vueToIr(src, {
|
|
1542
|
+
const r = vueToIr(src, {
|
|
1543
|
+
screenId: tab.id,
|
|
1544
|
+
title: tab.title,
|
|
1545
|
+
components: loadComponents(tab.file),
|
|
1546
|
+
storeSources: loadStoreSources(tab.file),
|
|
1547
|
+
});
|
|
1268
1548
|
if (r.warnings.length > 0) {
|
|
1269
1549
|
console.log(`[gen-android] 编译提示(${tab.id},${r.warnings.length} 条):`);
|
|
1270
1550
|
for (const w of r.warnings)
|
|
@@ -1297,7 +1577,12 @@ function runTabs(appDir, opts = {}) {
|
|
|
1297
1577
|
for (const d of details) {
|
|
1298
1578
|
const src = readFileSync(d.file, 'utf-8');
|
|
1299
1579
|
const pageId = toPascal(d.id);
|
|
1300
|
-
const r = vueToIr(src, {
|
|
1580
|
+
const r = vueToIr(src, {
|
|
1581
|
+
screenId: d.id,
|
|
1582
|
+
title: d.title,
|
|
1583
|
+
components: loadComponents(d.file),
|
|
1584
|
+
storeSources: loadStoreSources(d.file),
|
|
1585
|
+
});
|
|
1301
1586
|
if (r.warnings.length > 0) {
|
|
1302
1587
|
console.log(`[gen-android] 编译提示(${d.id},${r.warnings.length} 条):`);
|
|
1303
1588
|
for (const w of r.warnings)
|
|
@@ -36,6 +36,7 @@ export declare function buildTheme(tokens: {
|
|
|
36
36
|
fontNames: Map<string, string>;
|
|
37
37
|
};
|
|
38
38
|
/** R42 生成 Theme.swift(颜色/字体去重常量) */
|
|
39
|
+
export declare const VRN_HELPERS_SWIFT = "import Foundation\n\n// vuexir \u8FD0\u884C\u65F6\u8F85\u52A9\uFF08R78 \u5BF9\u8C61\u5B57\u6BB5\u53D6\u503C\uFF09\n/// R78 \u5BF9\u8C61\u5B57\u6BB5\u53D6\u503C\uFF1A\u72B6\u6001\u91CC\u5B58 JSON \u5B57\u7B26\u4E32\u65F6\u6309 key \u53D6\u503C\uFF08\u975E JSON \u2192 \u8FD4\u56DE\u7A7A\u4E32\uFF09\n/// R85 \u70B9\u8DEF\u5F84\u53D6\u503C\uFF1Akey \u652F\u6301 \"a.b.c\" \u4E0E\u672B\u6BB5 \".length\"\uFF08\u6570\u7EC4/\u5B57\u5178/\u5B57\u7B26\u4E32\u957F\u5EA6\uFF09\nfunc vrnField(_ json: String, _ path: String, _ fallback: String = \"\") -> String {\n let raw = vrnFieldRaw(json, path)\n return raw.isEmpty ? fallback : raw\n}\n\n/// \u539F\u59CB\u53D6\u503C\uFF08\u4E0D\u5957\u515C\u5E95\uFF09\uFF1A\u70B9\u8DEF\u5F84\u4E0B\u94BB\uFF0C\u7F3A\u5931\u8FD4\u56DE\u7A7A\u4E32\nprivate func vrnFieldRaw(_ json: String, _ path: String) -> String {\n guard let data = json.data(using: .utf8),\n let root = try? JSONSerialization.jsonObject(with: data, options: [.fragmentsAllowed]) else { return \"\" }\n var cur: Any = root\n for seg in path.split(separator: \".\").map(String.init) {\n if seg == \"length\" || seg == \"size\" {\n if let a = cur as? [Any] { cur = a.count }\n else if let d = cur as? [String: Any] { cur = d.count }\n else if let s = cur as? String { cur = s.count }\n else { return \"\" }\n continue\n }\n if let d = cur as? [String: Any], let next = d[seg] { cur = next } else { return \"\" }\n }\n if let s = cur as? String { return s }\n if let b = cur as? Bool { return b ? \"true\" : \"false\" }\n if let n = cur as? NSNumber { return n.stringValue }\n return String(describing: cur)\n}\n";
|
|
39
40
|
export declare function generateThemeSource(palette: {
|
|
40
41
|
colorNames: Map<string, string>;
|
|
41
42
|
fontNames: Map<string, string>;
|
package/dist/codegen/swiftui.js
CHANGED
|
@@ -116,6 +116,37 @@ export function buildTheme(tokens) {
|
|
|
116
116
|
return { colorNames, fontNames };
|
|
117
117
|
}
|
|
118
118
|
/** R42 生成 Theme.swift(颜色/字体去重常量) */
|
|
119
|
+
export const VRN_HELPERS_SWIFT = `import Foundation
|
|
120
|
+
|
|
121
|
+
// vuexir 运行时辅助(R78 对象字段取值)
|
|
122
|
+
/// R78 对象字段取值:状态里存 JSON 字符串时按 key 取值(非 JSON → 返回空串)
|
|
123
|
+
/// R85 点路径取值:key 支持 "a.b.c" 与末段 ".length"(数组/字典/字符串长度)
|
|
124
|
+
func vrnField(_ json: String, _ path: String, _ fallback: String = "") -> String {
|
|
125
|
+
let raw = vrnFieldRaw(json, path)
|
|
126
|
+
return raw.isEmpty ? fallback : raw
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/// 原始取值(不套兜底):点路径下钻,缺失返回空串
|
|
130
|
+
private func vrnFieldRaw(_ json: String, _ path: String) -> String {
|
|
131
|
+
guard let data = json.data(using: .utf8),
|
|
132
|
+
let root = try? JSONSerialization.jsonObject(with: data, options: [.fragmentsAllowed]) else { return "" }
|
|
133
|
+
var cur: Any = root
|
|
134
|
+
for seg in path.split(separator: ".").map(String.init) {
|
|
135
|
+
if seg == "length" || seg == "size" {
|
|
136
|
+
if let a = cur as? [Any] { cur = a.count }
|
|
137
|
+
else if let d = cur as? [String: Any] { cur = d.count }
|
|
138
|
+
else if let s = cur as? String { cur = s.count }
|
|
139
|
+
else { return "" }
|
|
140
|
+
continue
|
|
141
|
+
}
|
|
142
|
+
if let d = cur as? [String: Any], let next = d[seg] { cur = next } else { return "" }
|
|
143
|
+
}
|
|
144
|
+
if let s = cur as? String { return s }
|
|
145
|
+
if let b = cur as? Bool { return b ? "true" : "false" }
|
|
146
|
+
if let n = cur as? NSNumber { return n.stringValue }
|
|
147
|
+
return String(describing: cur)
|
|
148
|
+
}
|
|
149
|
+
`;
|
|
119
150
|
export function generateThemeSource(palette) {
|
|
120
151
|
const lines = ['import SwiftUI', '', '// GENERATED by vue-render-native compiler · R42 theme tokens', 'enum Theme {'];
|
|
121
152
|
const colors = [...palette.colorNames.entries()].map(([hex, name]) => {
|
|
@@ -471,6 +502,10 @@ function emitNode(node, ctx) {
|
|
|
471
502
|
}
|
|
472
503
|
/** JS 条件表达式 → Swift 兼容:===/!== 为 JS 严格比较,Swift 用 ==/!=;字符串字面量单引号 → 双引号 */
|
|
473
504
|
function swiftCondExpr(expr) {
|
|
505
|
+
// R78 续(G12):对象字段条件 → 非空判断
|
|
506
|
+
const f = expr.match(/^__vrnField__:(\w+):(\w+)$/);
|
|
507
|
+
if (f)
|
|
508
|
+
return `!vrnField(${f[1]}, "${f[2]}").isEmpty`;
|
|
474
509
|
let s = expr.replace(/!==/g, '!=').replace(/===/g, '==');
|
|
475
510
|
s = s.replace(/'(\\[^]|[^'\\])*'/g, (m) => '"' + m.slice(1, -1).replace(/"/g, '\\"') + '"');
|
|
476
511
|
return s;
|
|
@@ -715,26 +750,30 @@ function emitNodeInner(node, ctx) {
|
|
|
715
750
|
? pad(`Text(${props.ref})`)
|
|
716
751
|
: props.bindExpr !== undefined
|
|
717
752
|
? pad(`Text(${props.bindExpr})`)
|
|
718
|
-
: props.
|
|
719
|
-
? pad(`Text(${
|
|
720
|
-
: props.
|
|
721
|
-
? pad(`Text(
|
|
722
|
-
|
|
723
|
-
?
|
|
724
|
-
|
|
725
|
-
?
|
|
726
|
-
:
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
753
|
+
: props.fieldOf !== undefined
|
|
754
|
+
? pad(`Text(vrnField(${props.fieldOf.obj}, "${props.fieldOf.field}"${props.fieldOf.fallback !== undefined ? `, "${swiftStringEscape(props.fieldOf.fallback)}"` : ''}))`)
|
|
755
|
+
: props.dynField !== undefined
|
|
756
|
+
? pad(`Text(${dictFieldExpr(props.dynField.var, props.dynField.field)})`)
|
|
757
|
+
: props.bindParts !== undefined
|
|
758
|
+
? pad(`Text("${props.bindParts
|
|
759
|
+
.map((p) => 'text' in p
|
|
760
|
+
? swiftStringEscape(p.text)
|
|
761
|
+
: 'bind' in p
|
|
762
|
+
? `\\(${p.bind})`
|
|
763
|
+
: 'fieldOf' in p
|
|
764
|
+
? `\\(vrnField(${p.fieldOf.obj}, "${p.fieldOf.field}"${p.fieldOf.fallback !== undefined ? `, "${swiftStringEscape(p.fieldOf.fallback)}"` : ''}))`
|
|
765
|
+
: `\\(${dictFieldExpr(p.dynField.var, p.dynField.field)})`)
|
|
766
|
+
.join('')}")`)
|
|
767
|
+
: isBind
|
|
768
|
+
? ctx.loop && (bindKey === ctx.loop.itemKey || bindKey === ctx.loop.indexKey)
|
|
732
769
|
? pad(`Text("\\(${bindKey})")`)
|
|
733
|
-
: currentStateTypes[bindKey] === '
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
770
|
+
: currentStateTypes[bindKey] === 'number'
|
|
771
|
+
? pad(`Text("\\(${bindKey})")`)
|
|
772
|
+
: currentStateTypes[bindKey] === 'boolean'
|
|
773
|
+
// 布尔 state → String(describing:)(Text(Bool) 无构造匹配,typecheck 失败)
|
|
774
|
+
? pad(`Text(String(describing: ${bindKey}))`)
|
|
775
|
+
: pad(`Text(${bindKey})`)
|
|
776
|
+
: pad(`Text("${swiftStringEscape(content)}")`);
|
|
738
777
|
line += `.font(${fontExpr(fs, props.fontWeight, 'normal')})`;
|
|
739
778
|
// M2 :style 动态 → 运行时从 state 读取(fontSize 数值 / color hex 字符串)
|
|
740
779
|
if (props.fontSizeDynamic !== undefined) {
|
|
@@ -1060,11 +1099,13 @@ export function generateScreenSource(screen, opts = {}) {
|
|
|
1060
1099
|
if (opts.bindingMode === 'external') {
|
|
1061
1100
|
return ` @Binding var ${s.name}: ${typeof s.init === 'string' ? 'String' : typeof s.init === 'boolean' ? 'Bool' : 'Int'}`;
|
|
1062
1101
|
}
|
|
1063
|
-
const init = typeof s.init === 'string'
|
|
1064
|
-
?
|
|
1065
|
-
: typeof s.init === '
|
|
1066
|
-
? (s.init
|
|
1067
|
-
:
|
|
1102
|
+
const init = s.obj && typeof s.init === 'string'
|
|
1103
|
+
? `#"${s.init}"#` // R85 对象态:JSON 原文(vrnField 按点路径解析)
|
|
1104
|
+
: typeof s.init === 'string'
|
|
1105
|
+
? `"${swiftStringEscape(s.init)}"`
|
|
1106
|
+
: typeof s.init === 'boolean'
|
|
1107
|
+
? (s.init ? 'true' : 'false')
|
|
1108
|
+
: String(s.init);
|
|
1068
1109
|
return ` @State private var ${s.name} = ${init}`;
|
|
1069
1110
|
})
|
|
1070
1111
|
.join('\n');
|