@dongmingzhixiu/vuexir 0.5.4 → 0.5.6
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/bundle.js +5 -1
- package/dist/cli.js +19 -3
- package/dist/config.js +3 -2
- package/dist/gen-ios.js +181 -20
- package/package.json +1 -1
package/dist/bundle.js
CHANGED
|
@@ -121,7 +121,11 @@ export function bundlePageScript(pageFile, projectRoot) {
|
|
|
121
121
|
},
|
|
122
122
|
});
|
|
123
123
|
const code = out.outputFiles?.[0]?.text ?? '';
|
|
124
|
-
|
|
124
|
+
// 页面导出提升为全局:Swift onTapGesture → Runtime.invoke("goDetail(...)") 裸调用
|
|
125
|
+
// 需在全局作用域找到函数;__vrnPage 是 esbuild IIFE 的 globalName(模块导出对象)
|
|
126
|
+
const promoted = code +
|
|
127
|
+
`\n;(function(){var m=__vrnPage;if(!m)return;for(var k in m){try{if(typeof globalThis[k]==='undefined'){if(m[k]&&m[k].__v_isRef){Object.defineProperty(globalThis,k,{get:function(){return m[k].value;},configurable:true});}else{globalThis[k]=m[k];}}}catch(e){}}})();`;
|
|
128
|
+
return promoted ? { code: promoted, names } : null;
|
|
125
129
|
}
|
|
126
130
|
catch (e) {
|
|
127
131
|
if (process.env.VRN_DEBUG_BUNDLE)
|
package/dist/cli.js
CHANGED
|
@@ -65,7 +65,23 @@ export async function generate(args, platform) {
|
|
|
65
65
|
const resolved = explicit ? { entry: explicit.entry, multiPage: explicit.multiPage } : resolveEntry(cfg);
|
|
66
66
|
const { entry, multiPage } = resolved;
|
|
67
67
|
/** 传给生成器的入口(约定:路由文件路径 / 含 router.ts 的目录 / 含 App.vue 的目录) */
|
|
68
|
-
|
|
68
|
+
// R142:多页入口统一解析成「具体的路由文件」,避免目录里没有 router.ts 时退回样例模式
|
|
69
|
+
let routerFile;
|
|
70
|
+
if (multiPage) {
|
|
71
|
+
if (/\.(ts|js|mts|mjs)$/.test(entry) && existsSync(entry)) {
|
|
72
|
+
routerFile = entry;
|
|
73
|
+
}
|
|
74
|
+
else {
|
|
75
|
+
for (const cand of ['router.ts', 'index.ts', 'router/index.ts', 'router.js']) {
|
|
76
|
+
const full = join(entry, cand);
|
|
77
|
+
if (existsSync(full)) {
|
|
78
|
+
routerFile = full;
|
|
79
|
+
break;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
const inputDir = routerFile ? dirname(routerFile) : explicit ? explicit.dir : multiPage ? entry : dirname(entry);
|
|
69
85
|
const projectName = cfg.projectName ?? deriveProjectName(cfg, entry);
|
|
70
86
|
const bundleId = cfg.bundleId ?? `com.vrn.${projectName.toLowerCase()}`;
|
|
71
87
|
const outRoot = platformOutDir(cfg, platform, '').replace(/\/$/, '');
|
|
@@ -77,14 +93,14 @@ export async function generate(args, platform) {
|
|
|
77
93
|
if (!multiPage) {
|
|
78
94
|
throw new Error('[vrn] Android 生成目前要求多页入口 router.ts(单页 App.vue 请先补 router.ts)');
|
|
79
95
|
}
|
|
80
|
-
return generateAndroid({ input: inputDir, outDirRoot: outRoot, projectName, bundleId });
|
|
96
|
+
return generateAndroid({ input: inputDir, outDirRoot: outRoot, projectName, bundleId, ...(routerFile ? { routerFile } : {}) });
|
|
81
97
|
}
|
|
82
98
|
const out = generateIos({
|
|
83
99
|
input: inputDir,
|
|
84
100
|
outDirRoot: outRoot,
|
|
85
101
|
projectName,
|
|
86
102
|
bundleId,
|
|
87
|
-
...(
|
|
103
|
+
...(routerFile ? { routerFile } : {}),
|
|
88
104
|
});
|
|
89
105
|
if (!out)
|
|
90
106
|
throw new Error('[vrn] iOS 生成未返回产物目录');
|
package/dist/config.js
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
*/
|
|
12
12
|
import { existsSync, readFileSync, statSync } from 'node:fs';
|
|
13
13
|
import { dirname, isAbsolute, join, resolve } from 'node:path';
|
|
14
|
-
const CONFIG_FILES = ['vrn.config.ts', 'vrn.config.mts', 'vrn.config.js', 'vrn.config.mjs', 'vrn.config.json'];
|
|
14
|
+
const CONFIG_FILES = ['vuexir.config.ts', 'vuexir.config.mts', 'vuexir.config.js', 'vuexir.config.mjs', 'vuexir.config.json', 'vrn.config.ts', 'vrn.config.mts', 'vrn.config.js', 'vrn.config.mjs', 'vrn.config.json'];
|
|
15
15
|
/** 读取 vrn.config(json 直接解析;ts/mjs 用动态 import,失败则忽略并提示) */
|
|
16
16
|
export async function loadConfig(projectRoot) {
|
|
17
17
|
for (const file of CONFIG_FILES) {
|
|
@@ -59,8 +59,9 @@ export function resolveEntry(cfg) {
|
|
|
59
59
|
return { entry: a, multiPage: false };
|
|
60
60
|
throw new Error(`entry 目录内没有 router.ts / App.vue:${p}`);
|
|
61
61
|
}
|
|
62
|
+
// R142:任何 .ts/.js 入口都按「路由表」处理(src/router/index.ts 这类命名很常见)
|
|
62
63
|
if (existsSync(p))
|
|
63
|
-
return { entry: p, multiPage:
|
|
64
|
+
return { entry: p, multiPage: /\.(ts|js|mts|mjs)$/.test(p) };
|
|
64
65
|
throw new Error(`vrn.config.entry 指向的文件不存在:${p}`);
|
|
65
66
|
}
|
|
66
67
|
const routerCandidates = [
|
package/dist/gen-ios.js
CHANGED
|
@@ -228,7 +228,14 @@ const RUNTIME_SWIFT = `final class Runtime {
|
|
|
228
228
|
|
|
229
229
|
/// M3 注册 @State 绑定:JS stateStore.<name> 写回 → <setter>(onAppear 时由 View 注册)
|
|
230
230
|
func bindState(_ name: String, _ setter: @escaping (Any) -> Void) {
|
|
231
|
-
|
|
231
|
+
// R-THEME:ui_skin 变化 → ThemeColor.currentSkin + UserDefaults 同步 → 各视图 @AppStorage("vrn.skin") 自动重绘换肤
|
|
232
|
+
stateBindings[name] = { v in
|
|
233
|
+
setter(v)
|
|
234
|
+
if name == "ui_skin" || name == "ui.skin", let s = v as? String {
|
|
235
|
+
ThemeColor.currentSkin = s
|
|
236
|
+
UserDefaults.standard.set(s, forKey: "vrn.skin")
|
|
237
|
+
}
|
|
238
|
+
}
|
|
232
239
|
}
|
|
233
240
|
|
|
234
241
|
/// M3 Swift → JS 推送:@State 变更(v-model 输出等)→ stateStore 同步(__fromSwift 防回环)
|
|
@@ -254,6 +261,17 @@ const RUNTIME_SWIFT = `final class Runtime {
|
|
|
254
261
|
if name.contains("(") { ctx.evaluateScript(name) }
|
|
255
262
|
else { ctx.evaluateScript("\\(name)()") }
|
|
256
263
|
}
|
|
264
|
+
|
|
265
|
+
/// R-LOOPITEM:v-for 项点击时把当前 item 注入 JS 全局 __vrnLoop,
|
|
266
|
+
/// invoke 表达式里的循环变量引用(goDetail(row.item.id))才能求值
|
|
267
|
+
func setLoopItem(_ value: Any) {
|
|
268
|
+
guard let json = try? JSONSerialization.data(withJSONObject: value),
|
|
269
|
+
let s = String(data: json, encoding: .utf8) else { return }
|
|
270
|
+
let escaped = s.replacingOccurrences(of: "\\\\", with: "\\\\\\\\")
|
|
271
|
+
.replacingOccurrences(of: "\\"", with: "\\\\\\"")
|
|
272
|
+
.replacingOccurrences(of: "\\n", with: "\\\\n")
|
|
273
|
+
ctx.evaluateScript("__vrnLoop = JSON.parse(\\"\\(escaped)\\")")
|
|
274
|
+
}
|
|
257
275
|
}
|
|
258
276
|
`;
|
|
259
277
|
const APP_SWIFT = (projectName, viewName, screenTitle) => `import SwiftUI
|
|
@@ -282,18 +300,18 @@ struct ${projectName}App: App {
|
|
|
282
300
|
}
|
|
283
301
|
}
|
|
284
302
|
|
|
285
|
-
/// R68:全局 UINavigationBar
|
|
303
|
+
/// R68:全局 UINavigationBar 外观(R-THEME:bg/ink/primary 走 ThemeColor 皮肤表)
|
|
286
304
|
func configureGlobalNavigationBar() {
|
|
287
305
|
let appearance = UINavigationBarAppearance()
|
|
288
306
|
appearance.configureWithOpaqueBackground()
|
|
289
|
-
appearance.backgroundColor = UIColor(
|
|
290
|
-
let titleColor = UIColor(
|
|
307
|
+
appearance.backgroundColor = UIColor(ThemeColor.color("bg"))
|
|
308
|
+
let titleColor = UIColor(ThemeColor.color("ink"))
|
|
291
309
|
appearance.titleTextAttributes = [.foregroundColor: titleColor]
|
|
292
310
|
appearance.largeTitleTextAttributes = [.foregroundColor: titleColor]
|
|
293
311
|
UINavigationBar.appearance().standardAppearance = appearance
|
|
294
312
|
UINavigationBar.appearance().scrollEdgeAppearance = appearance
|
|
295
313
|
UINavigationBar.appearance().compactAppearance = appearance
|
|
296
|
-
UINavigationBar.appearance().tintColor = UIColor(
|
|
314
|
+
UINavigationBar.appearance().tintColor = UIColor(ThemeColor.color("primary"))
|
|
297
315
|
}
|
|
298
316
|
`;
|
|
299
317
|
/** 屏/工程名 → PascalCase(my-app → MyApp;用于 Swift 类型名与 bundle id 派生) */
|
|
@@ -496,7 +514,10 @@ function runOne(input) {
|
|
|
496
514
|
const themeTokens = collectTheme(screen.ui, ...components.map((c) => c.ui));
|
|
497
515
|
const theme = buildTheme(themeTokens);
|
|
498
516
|
const themeSrc = generateThemeSource(theme);
|
|
499
|
-
const
|
|
517
|
+
const cssVars = loadCssVars(input);
|
|
518
|
+
// R-THEME:皮肤 token 表(var(--x) 按皮肤分组 → ThemeColors.swift,换肤原生跟随)
|
|
519
|
+
writeFileSync(join(srcDir, 'ThemeColors.swift'), buildThemeColorsSwift(loadSkinVars(input)));
|
|
520
|
+
const swift = generateScreenSource(screen, { warnings: genWarnings, theme, cssVars });
|
|
500
521
|
if (genWarnings.length > 0) {
|
|
501
522
|
console.log(`[gen-ios] 布局降级提示(${genWarnings.length} 条,R-H3/H4/H5):`);
|
|
502
523
|
for (const w of genWarnings)
|
|
@@ -506,12 +527,12 @@ function runOne(input) {
|
|
|
506
527
|
writeFileSync(join(srcDir, 'App.swift'), APP_SWIFT(projectName, viewName, screen.title));
|
|
507
528
|
writeFileSync(join(srcDir, 'recite.json'), JSON.stringify(screen, null, 2));
|
|
508
529
|
// 事件运行时(<script setup> 逻辑源码;无逻辑时生成空文件保持 Bundle 引用成立)
|
|
509
|
-
writeFileSync(join(srcDir, 'runtime.js'), runtimeSrc ?? '');
|
|
530
|
+
writeFileSync(join(srcDir, 'runtime.js'), injectSkinBridge(runtimeSrc ?? ''));
|
|
510
531
|
// R41 组件:每个 components/Xxx.vue → 独立 XxxView.swift(struct XxxView;页面引用 XxxView(attrs))
|
|
511
532
|
const compFiles = [];
|
|
512
533
|
for (const comp of components) {
|
|
513
534
|
const compWarnings = [];
|
|
514
|
-
const src = generateComponentSource(comp.name, comp.ui, { warnings: compWarnings, theme });
|
|
535
|
+
const src = generateComponentSource(comp.name, comp.ui, { warnings: compWarnings, theme, cssVars });
|
|
515
536
|
writeFileSync(join(srcDir, `${comp.name}View.swift`), src);
|
|
516
537
|
compFiles.push({ name: `${comp.name}View.swift`, bytes: src.length });
|
|
517
538
|
}
|
|
@@ -587,11 +608,11 @@ ${tabs
|
|
|
587
608
|
.join('\n')}
|
|
588
609
|
}
|
|
589
610
|
.frame(height: 56)
|
|
590
|
-
.background(
|
|
611
|
+
.background(ThemeColor.color("surface-2"))
|
|
591
612
|
.overlay(Divider(), alignment: .top)
|
|
592
613
|
}
|
|
593
|
-
// R68
|
|
594
|
-
.background(${swiftColorExpr(rootBg)})
|
|
614
|
+
// R68:容器背景兜底——页面内容不足或底部安全区不露白(R-THEME:默认深色 → ThemeColor 皮肤 bg)
|
|
615
|
+
.background(${rootBg === '#0a0f1e' || rootBg === '#0a0F1E' || !rootBg ? 'ThemeColor.color("bg")' : swiftColorExpr(rootBg)})
|
|
595
616
|
${details.length > 0
|
|
596
617
|
? ` .navigationDestination(item: $pushedRoute) { route in
|
|
597
618
|
Group {
|
|
@@ -636,18 +657,18 @@ struct ${projectName}App: App {
|
|
|
636
657
|
}
|
|
637
658
|
}
|
|
638
659
|
|
|
639
|
-
/// R68:全局 UINavigationBar
|
|
660
|
+
/// R68:全局 UINavigationBar 外观(R-THEME:bg/ink/primary 走 ThemeColor 皮肤表)
|
|
640
661
|
func configureGlobalNavigationBar() {
|
|
641
662
|
let appearance = UINavigationBarAppearance()
|
|
642
663
|
appearance.configureWithOpaqueBackground()
|
|
643
|
-
appearance.backgroundColor = UIColor(
|
|
644
|
-
let titleColor = UIColor(
|
|
664
|
+
appearance.backgroundColor = UIColor(ThemeColor.color("bg"))
|
|
665
|
+
let titleColor = UIColor(ThemeColor.color("ink"))
|
|
645
666
|
appearance.titleTextAttributes = [.foregroundColor: titleColor]
|
|
646
667
|
appearance.largeTitleTextAttributes = [.foregroundColor: titleColor]
|
|
647
668
|
UINavigationBar.appearance().standardAppearance = appearance
|
|
648
669
|
UINavigationBar.appearance().scrollEdgeAppearance = appearance
|
|
649
670
|
UINavigationBar.appearance().compactAppearance = appearance
|
|
650
|
-
UINavigationBar.appearance().tintColor = UIColor(
|
|
671
|
+
UINavigationBar.appearance().tintColor = UIColor(ThemeColor.color("primary"))
|
|
651
672
|
}
|
|
652
673
|
`;
|
|
653
674
|
/** 计算页面 state 在全部 state 平铺数组中的下标(TAB_APP_SWIFT 生成 @State 命名 p<i>_<name> 用) */
|
|
@@ -662,12 +683,148 @@ const TAB_ICONS = ['house.fill', 'book.fill', 'gearshape.fill', 'person.fill', '
|
|
|
662
683
|
/** 编译期解析 router.ts 子集:import 组件映射 + routes 表(path/name/component/meta.tab/title/icon)
|
|
663
684
|
* M4b:非 tab 路由(无 meta.tab)→ details 列表(router.push → NavigationStack push) */
|
|
664
685
|
/** M4a 多页目录模式:samples/<name>/ 含 router.ts + pages/*.vue → TabView 工程 */
|
|
686
|
+
/** 加载项目全局 CSS 变量(tokens.css 等 :root 定义)→ 供 parseColor 解析 var(--xxx) */
|
|
687
|
+
function loadCssVars(appDir) {
|
|
688
|
+
const root = findProjectRoot(appDir); // appDir 可能是 src/router 等子目录 → 回退到项目根
|
|
689
|
+
const map = new Map();
|
|
690
|
+
const roots = [join(root, 'src/styles/tokens.css'), join(root, 'src/tokens.css'), join(root, 'styles/tokens.css'), join(root, 'src/styles/app.css')];
|
|
691
|
+
for (const p of roots) {
|
|
692
|
+
if (!existsSync(p))
|
|
693
|
+
continue;
|
|
694
|
+
const css = readFileSync(p, 'utf-8');
|
|
695
|
+
for (const m of css.matchAll(/(--[\w-]+)\s*:\s*([^;{}]+);/g)) {
|
|
696
|
+
map.set(m[1], m[2].trim());
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
return map;
|
|
700
|
+
}
|
|
701
|
+
/**
|
|
702
|
+
* R-THEME:JS 侧换肤桥——页面 bundle 的 ui.setSkin(skin) 调用后,
|
|
703
|
+
* 把皮肤广播进 stateStore.ui_skin → Proxy set → __setState → Swift bindState → ThemeColor.currentSkin → 原生换肤
|
|
704
|
+
* 注入位置:esbuild 产物中 ui store 的 setSkin 方法体开头(stateStore 在 RUNTIME 模板段,函数被调用时已就绪)
|
|
705
|
+
*/
|
|
706
|
+
function injectSkinBridge(js) {
|
|
707
|
+
if (!js || js.indexOf('setSkin') === -1)
|
|
708
|
+
return js;
|
|
709
|
+
return js.replace(/setSkin\s*\(\s*skin\s*\)\s*\{/g, 'setSkin(skin) { if(typeof stateStore!=="undefined"&&stateStore){try{stateStore["ui_skin"]=skin;}catch(e){}}');
|
|
710
|
+
}
|
|
711
|
+
/**
|
|
712
|
+
* R-THEME:按皮肤分组解析 tokens.css 的 var(--x)
|
|
713
|
+
* 选择器 → 皮肤名::root/.skin-warm → warm;.skin-mint → mint;.skin-lavender → lavender;.skin-night → night
|
|
714
|
+
* 返回 Map<skin, Map<varName, value>>;:root 覆盖块(半径/字体等)并入 warm
|
|
715
|
+
*/
|
|
716
|
+
function loadSkinVars(appDir) {
|
|
717
|
+
const root = findProjectRoot(appDir);
|
|
718
|
+
const skins = new Map();
|
|
719
|
+
const roots = [join(root, 'src/styles/tokens.css'), join(root, 'src/tokens.css'), join(root, 'styles/tokens.css')];
|
|
720
|
+
for (const p of roots) {
|
|
721
|
+
if (!existsSync(p))
|
|
722
|
+
continue;
|
|
723
|
+
const css = readFileSync(p, 'utf-8');
|
|
724
|
+
// 按 } 切块,取块头选择器判断皮肤归属
|
|
725
|
+
for (const block of css.split('}')) {
|
|
726
|
+
const selLine = block.split('{')[0] ?? '';
|
|
727
|
+
let skin = '';
|
|
728
|
+
if (/\.skin-warm/.test(selLine) || /:root/.test(selLine))
|
|
729
|
+
skin = 'warm';
|
|
730
|
+
else if (/\.skin-mint/.test(selLine))
|
|
731
|
+
skin = 'mint';
|
|
732
|
+
else if (/\.skin-lavender/.test(selLine))
|
|
733
|
+
skin = 'lavender';
|
|
734
|
+
else if (/\.skin-night/.test(selLine))
|
|
735
|
+
skin = 'night';
|
|
736
|
+
if (!skin)
|
|
737
|
+
continue;
|
|
738
|
+
const body = block.slice(block.indexOf('{') + 1);
|
|
739
|
+
let map = skins.get(skin);
|
|
740
|
+
if (!map) {
|
|
741
|
+
map = new Map();
|
|
742
|
+
skins.set(skin, map);
|
|
743
|
+
}
|
|
744
|
+
for (const m of body.matchAll(/(--[\w-]+)\s*:\s*([^;{}]+);/g)) {
|
|
745
|
+
map.set(m[1], m[2].trim());
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
return skins;
|
|
750
|
+
}
|
|
751
|
+
/** var 值 → Swift Color 字面量(hex/rgb/rgba;非颜色返回 null) */
|
|
752
|
+
function cssValueToSwiftColor(v) {
|
|
753
|
+
const t = v.trim();
|
|
754
|
+
if (t.startsWith('#')) {
|
|
755
|
+
let hex = t.slice(1);
|
|
756
|
+
if (hex.length === 3)
|
|
757
|
+
hex = hex.split('').map((h) => h + h).join('');
|
|
758
|
+
const n = parseInt(hex, 16);
|
|
759
|
+
return `Color(red: ${(n >> 16) & 255}/255.0, green: ${(n >> 8) & 255}/255.0, blue: ${n & 255}/255.0)`;
|
|
760
|
+
}
|
|
761
|
+
const m = t.match(/rgba?\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)(?:\s*,\s*([\d.]+))?\s*\)/);
|
|
762
|
+
if (m) {
|
|
763
|
+
const a = m[4] !== undefined ? +m[4] : 1;
|
|
764
|
+
return `Color(red: ${+m[1]}/255.0, green: ${+m[2]}/255.0, blue: ${+m[3]}/255.0, opacity: ${a})`;
|
|
765
|
+
}
|
|
766
|
+
if (/^rgba?\(\s*[\d.]+\s*[,\s]/.test(t))
|
|
767
|
+
return null;
|
|
768
|
+
if (t.startsWith('var(') || /^[a-z]+$/i.test(t))
|
|
769
|
+
return null;
|
|
770
|
+
return null;
|
|
771
|
+
}
|
|
772
|
+
/** 生成 ThemeColors.swift:4 皮肤 token 表 + currentSkin + color(name) 解析 */
|
|
773
|
+
function buildThemeColorsSwift(skins) {
|
|
774
|
+
const order = ['warm', 'mint', 'lavender', 'night'];
|
|
775
|
+
const lines = [];
|
|
776
|
+
lines.push('import SwiftUI');
|
|
777
|
+
lines.push('');
|
|
778
|
+
lines.push('/// Vuexir 皮肤 token 表:由 tokens.css 的 var(--x) 按皮肤分组生成');
|
|
779
|
+
lines.push('/// 换肤:JS ui.setSkin → stateStore.ui_skin → __setState → ThemeColor.currentSkin → 视图重建自动取新色');
|
|
780
|
+
lines.push('enum ThemeColor {');
|
|
781
|
+
lines.push(' static var currentSkin = UserDefaults.standard.string(forKey: "vrn.skin") ?? "warm"');
|
|
782
|
+
lines.push(' static func color(_ name: String) -> Color {');
|
|
783
|
+
lines.push(' let skin = UserDefaults.standard.string(forKey: "vrn.skin") ?? currentSkin');
|
|
784
|
+
lines.push(' switch skin {');
|
|
785
|
+
const names = new Set();
|
|
786
|
+
for (const skin of order)
|
|
787
|
+
names.add(skin);
|
|
788
|
+
for (const skin of order) {
|
|
789
|
+
const map = skins.get(skin);
|
|
790
|
+
if (!map)
|
|
791
|
+
continue;
|
|
792
|
+
const ref = skin === 'warm' ? 'warmMap' : `${skin}Map`;
|
|
793
|
+
const fallback = skin === 'warm' ? 'Color.black' : 'warmMap[name] ?? Color.black';
|
|
794
|
+
lines.push(` case "${skin}": return ${ref}[name] ?? ${fallback}`);
|
|
795
|
+
}
|
|
796
|
+
lines.push(' default: return warmMap[name] ?? Color.black');
|
|
797
|
+
lines.push(' }');
|
|
798
|
+
lines.push(' }');
|
|
799
|
+
for (const skin of order) {
|
|
800
|
+
const map = skins.get(skin);
|
|
801
|
+
if (!map)
|
|
802
|
+
continue;
|
|
803
|
+
const entries = [];
|
|
804
|
+
for (const [k, v] of map) {
|
|
805
|
+
const c = cssValueToSwiftColor(v);
|
|
806
|
+
if (c)
|
|
807
|
+
entries.push(` "${k.slice(2)}": ${c}`);
|
|
808
|
+
}
|
|
809
|
+
if (entries.length === 0)
|
|
810
|
+
continue;
|
|
811
|
+
lines.push(` private static let ${skin === 'warm' ? 'warmMap' : `${skin}Map`}: [String: Color] = [`);
|
|
812
|
+
lines.push(entries.join(',\n'));
|
|
813
|
+
lines.push(' ]');
|
|
814
|
+
}
|
|
815
|
+
lines.push('}');
|
|
816
|
+
return lines.join('\n') + '\n';
|
|
817
|
+
}
|
|
665
818
|
function runTabs(appDir, opts = {}) {
|
|
666
819
|
const routerSrc = readFileSync(opts.routerFile ?? join(appDir, 'router.ts'), 'utf-8');
|
|
667
820
|
const { tabs, details, warnings: routerWarnings } = parseRouterV2(routerSrc, findProjectRoot(appDir), opts.routerFile ?? join(appDir, 'router.ts'));
|
|
668
821
|
for (const w of routerWarnings)
|
|
669
822
|
console.log(`[gen-ios] 路由提示: ${w}`);
|
|
670
823
|
console.log(`[gen-ios] 多页模式: router.ts → ${tabs.length} 个 tab 页面` + (details.length ? ` + ${details.length} 个 push 页面` : ''));
|
|
824
|
+
const cssVars = loadCssVars(appDir);
|
|
825
|
+
// R-THEME:皮肤 token 表(var(--x) 按皮肤分组 → ThemeColors.swift,换肤原生跟随)
|
|
826
|
+
const skinVars = loadSkinVars(appDir);
|
|
827
|
+
const themeColorsSwift = buildThemeColorsSwift(skinVars);
|
|
671
828
|
const projectName = FORCED_PROJECT_NAME ?? toIdentifier(basename(appDir));
|
|
672
829
|
const bundleId = FORCED_BUNDLE_ID ?? `com.vrn.${projectName.toLowerCase()}`;
|
|
673
830
|
const outDir = join(OUT_ROOT, projectName);
|
|
@@ -675,6 +832,7 @@ function runTabs(appDir, opts = {}) {
|
|
|
675
832
|
// 写前清空源目录:删除页面后残留的旧 View/runtime 仍会进 Xcode 工程(幽灵页面)
|
|
676
833
|
rmSync(srcDir, { recursive: true, force: true });
|
|
677
834
|
mkdirSync(srcDir, { recursive: true });
|
|
835
|
+
writeFileSync(join(srcDir, 'ThemeColors.swift'), themeColorsSwift);
|
|
678
836
|
// R97 P2:把可独立运行的 utils 打成 IIFE,前置到每个页面 runtime 之前,
|
|
679
837
|
// 让 computed 里引用的外部函数(countChars / normalize …)在原生 JS 运行时可用
|
|
680
838
|
const utilsBundle = bundleProjectUtils(findProjectRoot(appDir));
|
|
@@ -720,6 +878,7 @@ function runTabs(appDir, opts = {}) {
|
|
|
720
878
|
const swift = generateScreenSource(r.screen, {
|
|
721
879
|
warnings: genWarnings,
|
|
722
880
|
theme: undefined,
|
|
881
|
+
cssVars,
|
|
723
882
|
// runtime 页键与 JS 资源名统一用标识符化 pageId(tab.id 可能含非法字符;也避免与 detail 撞名)
|
|
724
883
|
runtime: `Runtime.forPage("${pageId}")`,
|
|
725
884
|
bindingMode: 'external',
|
|
@@ -732,7 +891,7 @@ function runTabs(appDir, opts = {}) {
|
|
|
732
891
|
const pageBundle = bundlePageScript(tab.file, findProjectRoot(appDir));
|
|
733
892
|
if (pageBundle)
|
|
734
893
|
console.log(`[gen-ios] 页面脚本打包:${tab.id}(${pageBundle.names.length} 个绑定)`);
|
|
735
|
-
writeFileSync(join(srcDir, `runtime_${pageId}.js`), utilsBundle.code + '\n' + (pageBundle?.code ?? '') + '\n' + (r.runtime ?? ''));
|
|
894
|
+
writeFileSync(join(srcDir, `runtime_${pageId}.js`), injectSkinBridge(utilsBundle.code + '\n' + (pageBundle?.code ?? '') + '\n' + (r.runtime ?? '')));
|
|
736
895
|
pageFiles.push(`${pageId}View.swift`);
|
|
737
896
|
pageRuntimes.push({ id: pageId, src: r.runtime ?? '' });
|
|
738
897
|
pageStates.push({
|
|
@@ -784,6 +943,7 @@ function runTabs(appDir, opts = {}) {
|
|
|
784
943
|
const swift = generateScreenSource(r.screen, {
|
|
785
944
|
warnings: genWarnings,
|
|
786
945
|
theme: undefined,
|
|
946
|
+
cssVars,
|
|
787
947
|
// runtime 页键与 tab 页同规则标识符化 + Detail 后缀——
|
|
788
948
|
// tab/detail 同名时(R-VFOBJ-C)JS 资源文件与 forPage 键都独立,不会互相覆盖
|
|
789
949
|
runtime: `Runtime.forPage("${pageId}Detail")`,
|
|
@@ -791,7 +951,7 @@ function runTabs(appDir, opts = {}) {
|
|
|
791
951
|
for (const w of genWarnings)
|
|
792
952
|
console.log(`[gen-ios] 生成提示(${d.id},push 页): ${w}`);
|
|
793
953
|
writeFileSync(join(srcDir, viewFile), swift.replace(/Text\("(_vrn[EAC][0-9]+)"\)/g, 'Text($1)'));
|
|
794
|
-
writeFileSync(join(srcDir, `runtime_${pageId}Detail.js`), utilsBundle.code + '\n' + (r.runtime ?? ''));
|
|
954
|
+
writeFileSync(join(srcDir, `runtime_${pageId}Detail.js`), injectSkinBridge(utilsBundle.code + '\n' + (r.runtime ?? '')));
|
|
795
955
|
pageFiles.push(viewFile);
|
|
796
956
|
pageRuntimes.push({ id: `${pageId}Detail`, src: r.runtime ?? '' });
|
|
797
957
|
detailPages.push({ id: d.id, title: d.title, view: viewName, path: d.path });
|
|
@@ -820,7 +980,7 @@ function runTabs(appDir, opts = {}) {
|
|
|
820
980
|
continue;
|
|
821
981
|
compSeen.add(comp.name);
|
|
822
982
|
const compWarnings = [];
|
|
823
|
-
const src = generateComponentSource(comp.name, comp.ui, { warnings: compWarnings, theme });
|
|
983
|
+
const src = generateComponentSource(comp.name, comp.ui, { warnings: compWarnings, theme, cssVars });
|
|
824
984
|
writeFileSync(join(srcDir, `${comp.name}View.swift`), src);
|
|
825
985
|
console.log(`[gen-ios] - ${comp.name}View.swift (${src.length} bytes) · R41 component view`);
|
|
826
986
|
}
|
|
@@ -858,8 +1018,7 @@ function buildThemeFromPieces(pieces) {
|
|
|
858
1018
|
*/
|
|
859
1019
|
export function generateIos(opts = {}) {
|
|
860
1020
|
// R141:直接给路由文件(src/router/index.ts)时,目录里并没有 router.ts
|
|
861
|
-
|
|
862
|
-
return runTabs(dirname(opts.routerFile), { routerFile: opts.routerFile });
|
|
1021
|
+
// 先注入全局(OUT_ROOT / 工程名 / bundleId),再按入口形态分派
|
|
863
1022
|
if (opts.outDirRoot)
|
|
864
1023
|
OUT_ROOT = opts.outDirRoot;
|
|
865
1024
|
if (opts.samplesRoot)
|
|
@@ -868,6 +1027,8 @@ export function generateIos(opts = {}) {
|
|
|
868
1027
|
FORCED_PROJECT_NAME = opts.projectName;
|
|
869
1028
|
if (opts.bundleId)
|
|
870
1029
|
FORCED_BUNDLE_ID = opts.bundleId;
|
|
1030
|
+
if (opts.routerFile)
|
|
1031
|
+
return runTabs(dirname(opts.routerFile), { routerFile: opts.routerFile });
|
|
871
1032
|
return runMain(opts.input);
|
|
872
1033
|
}
|
|
873
1034
|
/** P2:写 .vrn/manifest.json(工具链/模板版本 + 文件清单)并对齐陈旧文件 */
|