@dongmingzhixiu/vuexir 0.1.2 → 0.2.0

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/cli.js CHANGED
@@ -64,8 +64,8 @@ export async function generate(args, platform) {
64
64
  const explicit = args.input ? await resolveExplicitEntry(args.input, cfg) : undefined;
65
65
  const resolved = explicit ? { entry: explicit.entry, multiPage: explicit.multiPage } : resolveEntry(cfg);
66
66
  const { entry, multiPage } = resolved;
67
- /** 传给生成器的入口目录(生成器约定:目录内含 router.ts / App.vue) */
68
- const inputDir = explicit ? explicit.dir : dirname(entry);
67
+ /** 传给生成器的入口(约定:路由文件路径 / 含 router.ts 的目录 / 含 App.vue 的目录) */
68
+ const inputDir = explicit ? explicit.dir : multiPage ? entry : dirname(entry);
69
69
  const projectName = cfg.projectName ?? deriveProjectName(cfg, entry);
70
70
  const bundleId = cfg.bundleId ?? `com.vrn.${projectName.toLowerCase()}`;
71
71
  const outRoot = platformOutDir(cfg, platform, '').replace(/\/$/, '');
package/dist/config.js CHANGED
@@ -63,9 +63,13 @@ export function resolveEntry(cfg) {
63
63
  return { entry: p, multiPage: p.endsWith('router.ts') || p.endsWith('router.js') };
64
64
  throw new Error(`vrn.config.entry 指向的文件不存在:${p}`);
65
65
  }
66
- const router = join(cfg.projectRoot, 'src/router.ts');
67
- const routerRoot = join(cfg.projectRoot, 'router.ts');
68
- for (const r of [router, routerRoot]) {
66
+ const routerCandidates = [
67
+ join(cfg.projectRoot, 'src/router.ts'),
68
+ join(cfg.projectRoot, 'src/router/index.ts'),
69
+ join(cfg.projectRoot, 'src/router/index.js'),
70
+ join(cfg.projectRoot, 'router.ts'),
71
+ ];
72
+ for (const r of routerCandidates) {
69
73
  if (existsSync(r))
70
74
  return { entry: r, multiPage: true };
71
75
  candidates.push(r);
@@ -23,6 +23,10 @@ const SAMPLES_ROOT = process.env.VRN_SAMPLES_ROOT ?? join(ROOT, 'examples');
23
23
  /** 生成 Android(Compose)工程;返回产物目录 */
24
24
  export function generateAndroid(opts = {}) {
25
25
  const input = resolveInput(opts.input);
26
+ // 直接给路由文件(src/router/index.ts 等)→ 用其目录 + 显式 routerFile
27
+ if (/\.(ts|js|mts)$/.test(input) && existsSync(input)) {
28
+ return runTabs(dirname(input), { routerFile: input, outDirRoot: opts.outDirRoot, projectName: opts.projectName, bundleId: opts.bundleId, renderGradle: (p, b) => buildGradle(b), templateVersion: TEMPLATE_VERSION });
29
+ }
26
30
  if (!existsSync(join(input, 'router.ts'))) {
27
31
  throw new Error(`[vrn] ${input} 缺少 router.ts(多页模式需要 router.ts;单页请用 gen-ios 路径或补 router.ts)`);
28
32
  }
package/dist/gen-ios.js CHANGED
@@ -15,9 +15,10 @@
15
15
  import { readFileSync, writeFileSync, mkdirSync, existsSync, readdirSync, statSync, copyFileSync, rmSync } from 'node:fs';
16
16
  import { basename, dirname, join } from 'node:path';
17
17
  import { fileURLToPath } from 'node:url';
18
- import { TEMPLATE_VERSION, projectYml as PROJECT_YML, templateFiles } from '@dongmingzhixiu/vuexir-templates';
18
+ import { TEMPLATE_VERSION, VRN_BRIDGE_SWIFT, projectYml as PROJECT_YML, templateFiles } from '@dongmingzhixiu/vuexir-templates';
19
+ import { parseRouter as parseRouterV2, findProjectRoot } from '@dongmingzhixiu/vuexir-compiler';
19
20
  import { validateIr } from '@dongmingzhixiu/vuexir-compiler';
20
- import { generateScreenSource, generateComponentSource, collectTheme, buildTheme, generateThemeSource } from '@dongmingzhixiu/vuexir-compiler';
21
+ import { generateScreenSource, generateComponentSource, collectTheme, buildTheme, generateThemeSource, VRN_HELPERS_SWIFT } from '@dongmingzhixiu/vuexir-compiler';
21
22
  import { vueToIr, VueCompileError } from '@dongmingzhixiu/vuexir-compiler';
22
23
  // R-PKG:产物与样例根不再假设仓库布局(CLI 通过 VRN_OUT_DIR / VRN_SAMPLES_ROOT 注入)
23
24
  const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..', '..', '..');
@@ -159,6 +160,33 @@ const RUNTIME_SWIFT = `final class Runtime {
159
160
  }
160
161
  ctx.setObject(clearI, forKeyedSubscript: "__nativeClearInterval" as NSString)
161
162
 
163
+ // R81 原生桥宿主函数:bridge.toast / bridge.openUrl(编译期注入的 VrnBridge.swift 提供实现)
164
+ let nativeToast: @convention(block) (String) -> Void = { msg in
165
+ VrnBridge.toast(msg)
166
+ }
167
+ ctx.setObject(nativeToast, forKeyedSubscript: "__nativeToast" as NSString)
168
+ let nativeOpenUrl: @convention(block) (String) -> Void = { url in
169
+ VrnBridge.openUrl(url)
170
+ }
171
+ ctx.setObject(nativeOpenUrl, forKeyedSubscript: "__nativeOpenUrl" as NSString)
172
+ // R84 桥能力扩展:震动 / 剪贴板 / 状态栏 / 屏幕常亮
173
+ let nativeVibrate: @convention(block) (Double) -> Void = { ms in
174
+ VrnBridge.vibrate(Int(ms))
175
+ }
176
+ ctx.setObject(nativeVibrate, forKeyedSubscript: "__nativeVibrate" as NSString)
177
+ let nativeCopy: @convention(block) (String) -> Void = { text in
178
+ VrnBridge.copy(text)
179
+ }
180
+ ctx.setObject(nativeCopy, forKeyedSubscript: "__nativeCopy" as NSString)
181
+ let nativeSetStatusBar: @convention(block) (String) -> Void = { style in
182
+ VrnBridge.setStatusBar(style)
183
+ }
184
+ ctx.setObject(nativeSetStatusBar, forKeyedSubscript: "__nativeSetStatusBar" as NSString)
185
+ let nativeKeepScreenOn: @convention(block) (Bool) -> Void = { on in
186
+ VrnBridge.keepScreenOn(on)
187
+ }
188
+ ctx.setObject(nativeKeepScreenOn, forKeyedSubscript: "__nativeKeepScreenOn" as NSString)
189
+
162
190
  // M4b router 桥:router.push('/path') / router.back() → 容器导航(静态 handler 由 ContentView onAppear 注册)
163
191
  let routerPush: @convention(block) (String) -> Void = { path in
164
192
  DispatchQueue.main.async { Runtime.routerPushHandler?(path) }
@@ -322,6 +350,26 @@ function tabNavBarModifiers(tabs) {
322
350
  }
323
351
  /** R40 组件表:输入 .vue 同目录 components/*.vue → { PascalCase 名: SFC 源码 }
324
352
  * R-VFOBJ:并向上查找样例根 components/(共享组件目录,如 vuexir-demo/components/VirRow.vue) */
353
+ /** R86 扫描工程内的 Pinia store 源码(src/stores/*.ts),供编译器做状态折叠 */
354
+ function loadStoreSources(anchor) {
355
+ const table = {};
356
+ let up = anchor;
357
+ for (let i = 0; i < 6 && up !== dirname(up); i++) {
358
+ for (const cand of [join(up, 'stores'), join(up, 'src', 'stores')]) {
359
+ if (existsSync(cand)) {
360
+ for (const f of readdirSync(cand)) {
361
+ if (!/\.(ts|js|mts)$/.test(f))
362
+ continue;
363
+ table[f] = readFileSync(join(cand, f), 'utf-8');
364
+ }
365
+ if (Object.keys(table).length > 0)
366
+ return table;
367
+ }
368
+ }
369
+ up = dirname(up);
370
+ }
371
+ return table;
372
+ }
325
373
  function loadComponents(input) {
326
374
  const dirs = new Set();
327
375
  const local = join(dirname(input), 'components');
@@ -381,6 +429,7 @@ function runOne(input) {
381
429
  ? dirName
382
430
  : fileName;
383
431
  const r = vueToIr(readFileSync(input, 'utf-8'), {
432
+ storeSources: loadStoreSources(input),
384
433
  screenId: toIdentifier(idSource),
385
434
  title: idSource,
386
435
  // R41 组件表:同目录 components/*.vue(PascalCase 文件名 → SFC 源码)
@@ -454,6 +503,8 @@ function runOne(input) {
454
503
  }
455
504
  // R42 Theme token:颜色/字体抽离(去重常量,页面与组件共享引用)
456
505
  writeFileSync(join(srcDir, 'Theme.swift'), themeSrc);
506
+ writeFileSync(join(srcDir, 'VrnHelpers.swift'), VRN_HELPERS_SWIFT);
507
+ writeFileSync(join(srcDir, 'VrnBridge.swift'), VRN_BRIDGE_SWIFT);
457
508
  writeFileSync(join(outDir, 'project.yml'), PROJECT_YML(projectName, bundleId));
458
509
  writeManifest(outDir, 'ios', projectName, bundleId, ['ScreenView.swift', ...compFiles.map((f) => f.name), 'Theme.swift', 'App.swift', 'project.yml', 'runtime.js', 'recite.json']);
459
510
  console.log(`[gen-ios] 已生成工程: ${outDir}`);
@@ -596,58 +647,12 @@ function flatIndex(tabs, tabIdx, stateIdx) {
596
647
  const TAB_ICONS = ['house.fill', 'book.fill', 'gearshape.fill', 'person.fill', 'square.grid.2x2.fill'];
597
648
  /** 编译期解析 router.ts 子集:import 组件映射 + routes 表(path/name/component/meta.tab/title/icon)
598
649
  * M4b:非 tab 路由(无 meta.tab)→ details 列表(router.push → NavigationStack push) */
599
- function parseRouter(src, dir) {
600
- const imports = {};
601
- for (const m of src.matchAll(/import\s+(\w+)\s+from\s+['"]([^'"]+)['"]/g))
602
- imports[m[1]] = m[2];
603
- const routes = [];
604
- for (const m of src.matchAll(/\{\s*path:\s*'([^']*)'\s*,\s*name:\s*'([^']*)'\s*,\s*component:\s*(\w+)(?:\s*,\s*meta:\s*\{([^}]*)\})?\s*\}/g)) {
605
- const meta = {};
606
- const raw = m[4] ?? '';
607
- for (const kv of raw.split(',')) {
608
- const mm = kv.match(/^\s*(\w+):\s*(true|false|'([^']*)'|"([^"]*)"|\w+)\s*$/);
609
- if (!mm)
610
- continue;
611
- meta[mm[1]] = mm[2] === 'true' ? 'true' : mm[2] === 'false' ? 'false' : mm[3] ?? mm[4] ?? mm[2];
612
- }
613
- routes.push({ path: m[1], name: m[2], component: m[3], meta });
614
- }
615
- // R-VFOBJ-E:空 tab 自动隐藏——tab 页模板无任何文本内容(纯空壳/占位)时从 tab 栏移除;
616
- // 有内容(row/line/标题/组件/任意文本)则保留。组合式 API 等文档型 tab 有内容即保留。
617
- const tabs = routes
618
- .filter((r) => r.meta.tab === 'true')
619
- .filter((r) => {
620
- const p = join(dir, imports[r.component] ?? '');
621
- if (!existsSync(p))
622
- return true;
623
- const s = readFileSync(p, 'utf-8');
624
- const tpl = s.match(/<template>([\s\S]*)<\/template>/)?.[1] ?? '';
625
- const textOnly = tpl.replace(/<[^>]*>/g, ' ').replace(/\s+/g, ' ').trim();
626
- return textOnly.length > 0;
627
- })
628
- .map((r, i) => ({
629
- id: r.name,
630
- title: r.meta.title ?? r.name,
631
- file: join(dir, imports[r.component] ?? ''),
632
- icon: r.meta.icon ?? TAB_ICONS[i % TAB_ICONS.length],
633
- }));
634
- const details = routes
635
- .filter((r) => r.meta.tab !== 'true')
636
- .map((r) => ({
637
- id: r.name,
638
- title: r.meta.title ?? r.name,
639
- file: join(dir, imports[r.component] ?? ''),
640
- path: r.path,
641
- }));
642
- if (tabs.length === 0) {
643
- throw new Error('router.ts 未声明 tab 路由(routes 需含 meta: { tab: true })');
644
- }
645
- return { tabs, details };
646
- }
647
650
  /** M4a 多页目录模式:samples/<name>/ 含 router.ts + pages/*.vue → TabView 工程 */
648
- function runTabs(appDir) {
649
- const routerSrc = readFileSync(join(appDir, 'router.ts'), 'utf-8');
650
- const { tabs, details } = parseRouter(routerSrc, appDir);
651
+ function runTabs(appDir, opts = {}) {
652
+ const routerSrc = readFileSync(opts.routerFile ?? join(appDir, 'router.ts'), 'utf-8');
653
+ const { tabs, details, warnings: routerWarnings } = parseRouterV2(routerSrc, findProjectRoot(appDir), opts.routerFile ?? join(appDir, 'router.ts'));
654
+ for (const w of routerWarnings)
655
+ console.log(`[gen-ios] 路由提示: ${w}`);
651
656
  console.log(`[gen-ios] 多页模式: router.ts → ${tabs.length} 个 tab 页面` + (details.length ? ` + ${details.length} 个 push 页面` : ''));
652
657
  const projectName = FORCED_PROJECT_NAME ?? toIdentifier(basename(appDir));
653
658
  const bundleId = FORCED_BUNDLE_ID ?? `com.vrn.${projectName.toLowerCase()}`;
@@ -669,6 +674,7 @@ function runTabs(appDir) {
669
674
  screenId: pageId,
670
675
  title: tab.title,
671
676
  components: loadComponents(tab.file),
677
+ storeSources: loadStoreSources(tab.file),
672
678
  });
673
679
  if (r.warnings.length > 0) {
674
680
  console.log(`[gen-ios] 编译提示(${tab.id},${r.warnings.length} 条):`);
@@ -729,7 +735,12 @@ function runTabs(appDir) {
729
735
  // screenId 同步带 Detail → struct 名 = <Id>DetailView,与文件/view 引用一致(防 Swift 重复声明)
730
736
  const viewFile = `${pageId}DetailView.swift`;
731
737
  const viewName = `${pageId}DetailView`;
732
- const r = vueToIr(src, { screenId: `${pageId}Detail`, title: d.title, components: loadComponents(d.file) });
738
+ const r = vueToIr(src, {
739
+ screenId: `${pageId}Detail`,
740
+ title: d.title,
741
+ components: loadComponents(d.file),
742
+ storeSources: loadStoreSources(d.file),
743
+ });
733
744
  for (const w of r.warnings)
734
745
  console.log(`[gen-ios] 编译提示(${d.id},push 页): ${w}`);
735
746
  const v = validateIr(r.screen);
@@ -769,6 +780,8 @@ function runTabs(appDir) {
769
780
  }
770
781
  const theme = buildThemeFromPieces(themePieces);
771
782
  writeFileSync(join(srcDir, 'Theme.swift'), generateThemeSource(theme));
783
+ writeFileSync(join(srcDir, 'VrnHelpers.swift'), VRN_HELPERS_SWIFT);
784
+ writeFileSync(join(srcDir, 'VrnBridge.swift'), VRN_BRIDGE_SWIFT);
772
785
  // R-VFOBJ:多页组件合并生成(各页组件表去重 → 独立 XxxView.swift;页面引用点已生成)
773
786
  const compSeen = new Set();
774
787
  for (const p of themePieces) {
@@ -868,6 +881,14 @@ function runMain(arg) {
868
881
  if (arg && existsSync(join(arg, 'router.ts'))) {
869
882
  return runTabs(join(arg));
870
883
  }
884
+ // 直接给路由文件(src/router/index.ts 等):用其所在目录 + 显式 routerFile
885
+ if (arg && /\.(ts|js|mts)$/.test(arg) && existsSync(arg)) {
886
+ return runTabs(dirname(arg), { routerFile: arg });
887
+ }
888
+ // 单页工程:arg 为含 App.vue 的目录(Vite 常见结构 src/App.vue)
889
+ if (arg && existsSync(join(arg, 'App.vue'))) {
890
+ return runOne(join(arg, 'App.vue'));
891
+ }
871
892
  // M4a 多页目录模式:arg 指向含 router.ts 的目录(如 samples/tabs)
872
893
  if (arg && existsSync(join(process.cwd(), arg, 'router.ts'))) {
873
894
  return runTabs(join(process.cwd(), arg));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dongmingzhixiu/vuexir",
3
- "version": "0.1.2",
3
+ "version": "0.2.0",
4
4
  "description": "vrn 命令行:build ios|android、preview、doctor",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -21,8 +21,8 @@
21
21
  "LICENSE"
22
22
  ],
23
23
  "dependencies": {
24
- "@dongmingzhixiu/vuexir-compiler": "^0.1.0",
25
- "@dongmingzhixiu/vuexir-templates": "^0.1.0"
24
+ "@dongmingzhixiu/vuexir-compiler": "^0.2.0",
25
+ "@dongmingzhixiu/vuexir-templates": "^0.2.0"
26
26
  },
27
27
  "engines": {
28
28
  "node": ">=18"