@dongmingzhixiu/vuexir 0.1.1 → 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 +2 -2
- package/dist/config.js +7 -3
- package/dist/gen-android.js +4 -0
- package/dist/gen-ios.js +75 -54
- package/package.json +6 -6
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
|
-
/**
|
|
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
|
|
67
|
-
|
|
68
|
-
|
|
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);
|
package/dist/gen-android.js
CHANGED
|
@@ -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 } =
|
|
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, {
|
|
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.
|
|
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.
|
|
25
|
-
"@dongmingzhixiu/vuexir-templates": "^0.
|
|
24
|
+
"@dongmingzhixiu/vuexir-compiler": "^0.2.0",
|
|
25
|
+
"@dongmingzhixiu/vuexir-templates": "^0.2.0"
|
|
26
26
|
},
|
|
27
27
|
"engines": {
|
|
28
28
|
"node": ">=18"
|
|
@@ -37,12 +37,12 @@
|
|
|
37
37
|
},
|
|
38
38
|
"repository": {
|
|
39
39
|
"type": "git",
|
|
40
|
-
"url": "git+https://github.com/dongmingzhixiu/
|
|
40
|
+
"url": "git+https://github.com/dongmingzhixiu/vuexir.git",
|
|
41
41
|
"directory": "packages/cli"
|
|
42
42
|
},
|
|
43
|
-
"homepage": "https://github.com/dongmingzhixiu/
|
|
43
|
+
"homepage": "https://github.com/dongmingzhixiu/vuexir#readme",
|
|
44
44
|
"bugs": {
|
|
45
|
-
"url": "https://github.com/dongmingzhixiu/
|
|
45
|
+
"url": "https://github.com/dongmingzhixiu/vuexir/issues"
|
|
46
46
|
},
|
|
47
47
|
"author": "panvu"
|
|
48
48
|
}
|