@dongmingzhixiu/vuexir 0.0.0-stage → 0.1.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/LICENSE +21 -0
- package/README.md +19 -2
- package/bin/vrn.mjs +23 -0
- package/dist/cli.d.ts +19 -0
- package/dist/cli.js +203 -0
- package/dist/config.d.ts +37 -0
- package/dist/config.js +80 -0
- package/dist/gen-android.d.ts +10 -0
- package/dist/gen-android.js +48 -0
- package/dist/gen-ios.d.ts +29 -0
- package/dist/gen-ios.js +884 -0
- package/dist/main.d.ts +1 -0
- package/dist/main.js +4 -0
- package/package.json +46 -4
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 panvu
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,20 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @vrn/cli
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
```bash
|
|
4
|
+
npx vrn build ios # Vue SFC → IR → Swift 源码 →(--run 时)xcodegen + xcodebuild
|
|
5
|
+
npx vrn build android # 同上,产出 Compose 源码 → gradle assembleDebug
|
|
6
|
+
npx vrn gen ios # 只生成源码,不调用平台构建
|
|
7
|
+
npx vrn doctor # 环境自检:Xcode / xcodegen / JDK / ANDROID_HOME / adb(P2)
|
|
8
|
+
npx vrn preview # 多页预览器(P3 起由 @vrn/vite-plugin 承载)
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
配置(`vrn.config.ts`,P1 落地):
|
|
12
|
+
|
|
13
|
+
```ts
|
|
14
|
+
export default {
|
|
15
|
+
entry: 'src/router.ts',
|
|
16
|
+
outDir: 'native',
|
|
17
|
+
projectName: 'MyApp',
|
|
18
|
+
bundleId: 'com.example.myapp',
|
|
19
|
+
}
|
|
20
|
+
```
|
package/bin/vrn.mjs
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* vrn 可执行入口(@vrn/cli)。
|
|
4
|
+
* 说明:源码为 TS,运行期用 vite-node 加载(vite-node 是 CLI 的运行时依赖);
|
|
5
|
+
* 发布版可改用预编译 dist(P4 处理),此处在未编译时回退到 vite-node。
|
|
6
|
+
*/
|
|
7
|
+
import { spawnSync } from 'node:child_process';
|
|
8
|
+
import { existsSync } from 'node:fs';
|
|
9
|
+
import { dirname, join } from 'node:path';
|
|
10
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
11
|
+
|
|
12
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
13
|
+
const entry = join(here, '..', 'src', 'cli.ts');
|
|
14
|
+
|
|
15
|
+
const dist = join(here, '..', 'dist', 'cli.js');
|
|
16
|
+
if (existsSync(dist)) {
|
|
17
|
+
const { run } = await import(pathToFileURL(dist).href);
|
|
18
|
+
process.exit(await run(process.argv.slice(2)));
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// 开发/未编译形态:交给 vite-node(其 CLI 会加载 TS 并设置好 Vite 解析)
|
|
22
|
+
const r = spawnSync('npx', ['vite-node', entry, ...process.argv.slice(2)], { stdio: 'inherit' });
|
|
23
|
+
process.exit(r.status ?? 1);
|
package/dist/cli.d.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
type Platform = 'ios' | 'android';
|
|
2
|
+
interface CliArgs {
|
|
3
|
+
command: 'gen' | 'build' | 'doctor' | 'help';
|
|
4
|
+
platform?: Platform;
|
|
5
|
+
run: boolean;
|
|
6
|
+
input?: string;
|
|
7
|
+
outDir?: string;
|
|
8
|
+
projectName?: string;
|
|
9
|
+
bundleId?: string;
|
|
10
|
+
entry?: string;
|
|
11
|
+
projectRoot?: string;
|
|
12
|
+
}
|
|
13
|
+
export declare function parseArgs(argv: string[]): CliArgs;
|
|
14
|
+
/** 生成指定平台;返回产物目录 */
|
|
15
|
+
export declare function generate(args: CliArgs, platform: Platform): Promise<string>;
|
|
16
|
+
/** vrn doctor:环境自检(生成链路依赖的外部工具) */
|
|
17
|
+
export declare function doctor(): boolean;
|
|
18
|
+
export declare function run(argv: string[]): Promise<number>;
|
|
19
|
+
export {};
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @dongmingzhixiu/vuexir 命令实现(R-PKG:CLI 是唯一负责路径/IO/进程的层)
|
|
3
|
+
*
|
|
4
|
+
* vrn gen ios|android [--entry <p>] [--out <dir>] [--name <n>] [--bundle <id>]
|
|
5
|
+
* vrn build ios|android [--run] # gen + (--run 时)调用 xcodegen/xcodebuild | gradle
|
|
6
|
+
* vrn doctor # 环境自检
|
|
7
|
+
*
|
|
8
|
+
* 配置来源见 config.ts(vrn.config.* + CLI 覆盖)。生成器在导入前设置 VRN_NO_AUTO_RUN=1,
|
|
9
|
+
* 避免模块加载时的自动执行。
|
|
10
|
+
*/
|
|
11
|
+
import { existsSync } from 'node:fs';
|
|
12
|
+
import { readdirSync } from 'node:fs';
|
|
13
|
+
import { dirname, join } from 'node:path';
|
|
14
|
+
import { spawnSync } from 'node:child_process';
|
|
15
|
+
import { readFileSync } from 'node:fs';
|
|
16
|
+
import { deriveProjectName, loadConfig, platformOutDir, resolveConfig, resolveEntry } from './config.js';
|
|
17
|
+
import { TEMPLATE_VERSION } from '@dongmingzhixiu/vuexir-templates';
|
|
18
|
+
// 关键:生成器模块的默认入口是「导入即执行」(旧脚本行为),CLI 复用其函数前必须关闭该行为
|
|
19
|
+
process.env.VRN_NO_AUTO_RUN = '1';
|
|
20
|
+
export function parseArgs(argv) {
|
|
21
|
+
const args = { command: 'help', run: false };
|
|
22
|
+
const rest = [];
|
|
23
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
24
|
+
const a = argv[i];
|
|
25
|
+
const next = () => argv[++i] ?? '';
|
|
26
|
+
if (a === '--run')
|
|
27
|
+
args.run = true;
|
|
28
|
+
else if (a === '--out')
|
|
29
|
+
args.outDir = next();
|
|
30
|
+
else if (a === '--name')
|
|
31
|
+
args.projectName = next();
|
|
32
|
+
else if (a === '--bundle')
|
|
33
|
+
args.bundleId = next();
|
|
34
|
+
else if (a === '--entry')
|
|
35
|
+
args.entry = next();
|
|
36
|
+
else if (a === '--root')
|
|
37
|
+
args.projectRoot = next();
|
|
38
|
+
else if (a.startsWith('--'))
|
|
39
|
+
console.warn(`[vrn] 未知参数已忽略:${a}`);
|
|
40
|
+
else
|
|
41
|
+
rest.push(a);
|
|
42
|
+
}
|
|
43
|
+
const cmd = rest[0];
|
|
44
|
+
if (cmd === 'gen' || cmd === 'build' || cmd === 'doctor')
|
|
45
|
+
args.command = cmd;
|
|
46
|
+
const plat = rest.find((r) => r === 'ios' || r === 'android');
|
|
47
|
+
if (plat)
|
|
48
|
+
args.platform = plat;
|
|
49
|
+
// 位置参数(非子命令、非平台)视为入口(样例名/目录/路径)
|
|
50
|
+
args.input = rest.slice(1).find((r) => r !== 'ios' && r !== 'android');
|
|
51
|
+
return args;
|
|
52
|
+
}
|
|
53
|
+
/** 生成指定平台;返回产物目录 */
|
|
54
|
+
export async function generate(args, platform) {
|
|
55
|
+
const fileConfig = await loadConfig(args.projectRoot ?? process.cwd());
|
|
56
|
+
const cfg = resolveConfig(fileConfig, {
|
|
57
|
+
projectRoot: args.projectRoot,
|
|
58
|
+
outDir: args.outDir,
|
|
59
|
+
entry: args.entry,
|
|
60
|
+
projectName: args.projectName,
|
|
61
|
+
bundleId: args.bundleId,
|
|
62
|
+
});
|
|
63
|
+
// 显式入口(CLI 位置参数)优先于 config.entry
|
|
64
|
+
const explicit = args.input ? await resolveExplicitEntry(args.input, cfg) : undefined;
|
|
65
|
+
const resolved = explicit ? { entry: explicit.entry, multiPage: explicit.multiPage } : resolveEntry(cfg);
|
|
66
|
+
const { entry, multiPage } = resolved;
|
|
67
|
+
/** 传给生成器的入口目录(生成器约定:目录内含 router.ts / App.vue) */
|
|
68
|
+
const inputDir = explicit ? explicit.dir : dirname(entry);
|
|
69
|
+
const projectName = cfg.projectName ?? deriveProjectName(cfg, entry);
|
|
70
|
+
const bundleId = cfg.bundleId ?? `com.vrn.${projectName.toLowerCase()}`;
|
|
71
|
+
const outRoot = platformOutDir(cfg, platform, '').replace(/\/$/, '');
|
|
72
|
+
// 动态导入:生成器模块在 import 时执行默认入口(旧脚本兼容),必须先置开关再加载
|
|
73
|
+
process.env.VRN_NO_AUTO_RUN = '1';
|
|
74
|
+
const { generateIos } = await import('./gen-ios.js');
|
|
75
|
+
const { generateAndroid } = await import('./gen-android.js');
|
|
76
|
+
if (platform === 'android') {
|
|
77
|
+
if (!multiPage) {
|
|
78
|
+
throw new Error('[vrn] Android 生成目前要求多页入口 router.ts(单页 App.vue 请先补 router.ts)');
|
|
79
|
+
}
|
|
80
|
+
return generateAndroid({ input: inputDir, outDirRoot: outRoot, projectName, bundleId });
|
|
81
|
+
}
|
|
82
|
+
const out = generateIos({ input: inputDir, outDirRoot: outRoot, projectName, bundleId });
|
|
83
|
+
if (!out)
|
|
84
|
+
throw new Error('[vrn] iOS 生成未返回产物目录');
|
|
85
|
+
return out;
|
|
86
|
+
}
|
|
87
|
+
/** 位置参数解析:样例名 / 目录 / .vue 路径 → { 目录, 入口文件, 是否多页 }
|
|
88
|
+
* 说明:生成器(gen-ios/gen-android)接收入口**目录**(内含 router.ts / App.vue) */
|
|
89
|
+
async function resolveExplicitEntry(input, cfg) {
|
|
90
|
+
const candidates = [
|
|
91
|
+
input,
|
|
92
|
+
join(cfg.projectRoot, input),
|
|
93
|
+
join(cfg.projectRoot, 'examples', input),
|
|
94
|
+
join(process.cwd(), input),
|
|
95
|
+
];
|
|
96
|
+
for (const c of candidates) {
|
|
97
|
+
if (!existsSync(c))
|
|
98
|
+
continue;
|
|
99
|
+
if (c.endsWith('.vue'))
|
|
100
|
+
return { dir: dirname(c), entry: c, multiPage: false };
|
|
101
|
+
if (existsSync(join(c, 'router.ts')))
|
|
102
|
+
return { dir: c, entry: join(c, 'router.ts'), multiPage: true };
|
|
103
|
+
if (existsSync(join(c, 'App.vue')))
|
|
104
|
+
return { dir: c, entry: join(c, 'App.vue'), multiPage: false };
|
|
105
|
+
}
|
|
106
|
+
throw new Error(`[vrn] 找不到入口「${input}」(已尝试:\n ${candidates.join('\n ')})`);
|
|
107
|
+
}
|
|
108
|
+
/** vrn doctor:环境自检(生成链路依赖的外部工具) */
|
|
109
|
+
export function doctor() {
|
|
110
|
+
const checks = [
|
|
111
|
+
{ name: 'node', cmd: 'node', args: ['--version'], hint: '安装 Node.js ≥ 18' },
|
|
112
|
+
{ name: 'xcodegen', cmd: 'xcodegen', args: ['--version'], hint: 'brew install xcodegen(iOS 工程生成)' },
|
|
113
|
+
{ name: 'xcodebuild', cmd: 'xcrun', args: ['--find', 'xcodebuild'], hint: '安装 Xcode 并 xcode-select --install' },
|
|
114
|
+
{ name: 'swiftc', cmd: 'xcrun', args: ['--find', 'swiftc'], hint: '随 Xcode 提供(iOS 源码类型检查)' },
|
|
115
|
+
{ name: 'gradle', cmd: 'gradle', args: ['--version'], hint: 'brew install gradle 或设置 VRN_GRADLE' },
|
|
116
|
+
{ name: 'adb', cmd: 'adb', args: ['version'], hint: 'Android SDK platform-tools(装到真机需要)' },
|
|
117
|
+
];
|
|
118
|
+
let ok = true;
|
|
119
|
+
for (const c of checks) {
|
|
120
|
+
// 兼容自定义安装位置:VRN_GRADLE / ANDROID_HOME 下的 platform-tools
|
|
121
|
+
const custom = {
|
|
122
|
+
gradle: process.env.VRN_GRADLE ?? (existsSync(join(process.env.HOME ?? '', 'gradle-dist/gradle-8.9/bin/gradle')) ? join(process.env.HOME ?? '', 'gradle-dist/gradle-8.9/bin/gradle') : undefined),
|
|
123
|
+
adb: process.env.ANDROID_HOME ? join(process.env.ANDROID_HOME, 'platform-tools/adb') : undefined,
|
|
124
|
+
};
|
|
125
|
+
const bin = custom[c.name] ?? c.cmd;
|
|
126
|
+
const r = spawnSync(bin, c.args, { encoding: 'utf-8' });
|
|
127
|
+
// 自定义路径下以「文件存在」判定(部分环境禁止直接执行,如沙箱)
|
|
128
|
+
const good = r.status === 0 || (custom[c.name] !== undefined && existsSync(bin));
|
|
129
|
+
if (!good)
|
|
130
|
+
ok = false;
|
|
131
|
+
console.log(`${good ? '✓' : '✗'} ${c.name.padEnd(11)} ${good ? (r.stdout.split('\n')[0] ?? '').trim() : `缺失 → ${c.hint}`}`);
|
|
132
|
+
}
|
|
133
|
+
// P2:工程产物版本校验(.vrn/manifest.json 记录生成时的工具链/模板版本)
|
|
134
|
+
const outRoot = join(process.cwd(), 'native');
|
|
135
|
+
const scanDirs = [];
|
|
136
|
+
for (const plat of ['ios', 'android']) {
|
|
137
|
+
const d = join(outRoot, plat);
|
|
138
|
+
if (existsSync(d))
|
|
139
|
+
for (const name of readdirSync(d))
|
|
140
|
+
scanDirs.push(join(d, name));
|
|
141
|
+
}
|
|
142
|
+
for (const dir of scanDirs) {
|
|
143
|
+
const mf = join(dir, '.vrn/manifest.json');
|
|
144
|
+
if (!existsSync(mf)) {
|
|
145
|
+
console.log(`! ${dir.split('/').slice(-2).join('/')} 缺少 .vrn/manifest.json → 建议 vrn gen 重生成`);
|
|
146
|
+
continue;
|
|
147
|
+
}
|
|
148
|
+
try {
|
|
149
|
+
const m = JSON.parse(readFileSync(mf, 'utf-8'));
|
|
150
|
+
const stale = m.templateVersion !== TEMPLATE_VERSION;
|
|
151
|
+
if (stale)
|
|
152
|
+
ok = false;
|
|
153
|
+
console.log(`${stale ? '✗' : '✓'} ${dir.split('/').slice(-2).join('/')} 模板版本 ${m.templateVersion ?? '?'}${stale ? ` ≠ 当前 ${TEMPLATE_VERSION} → 重生成(vrn gen)` : ''}`);
|
|
154
|
+
}
|
|
155
|
+
catch {
|
|
156
|
+
console.log(`! ${dir} manifest 解析失败 → 重生成`);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
const androidHome = process.env.ANDROID_HOME ?? process.env.ANDROID_SDK_ROOT;
|
|
160
|
+
console.log(`${androidHome ? '✓' : '✗'} ANDROID_HOME ${androidHome ?? '未设置 → export ANDROID_HOME=$HOME/Library/Android/sdk'}`);
|
|
161
|
+
if (!androidHome)
|
|
162
|
+
ok = false;
|
|
163
|
+
return ok;
|
|
164
|
+
}
|
|
165
|
+
export async function run(argv) {
|
|
166
|
+
const args = parseArgs(argv);
|
|
167
|
+
if (args.command === 'help') {
|
|
168
|
+
console.log(`vrn —— Vue SFC → iOS(SwiftUI) / Android(Compose) 原生工程
|
|
169
|
+
|
|
170
|
+
用法:
|
|
171
|
+
vrn gen ios|android [入口] [--out native] [--name MyApp] [--bundle com.x.y]
|
|
172
|
+
vrn build ios|android [入口] [--run] # 生成后可选调用平台构建
|
|
173
|
+
vrn doctor # 环境自检
|
|
174
|
+
|
|
175
|
+
配置:<projectRoot>/vrn.config.ts(entry / outDir / projectName / bundleId)
|
|
176
|
+
`);
|
|
177
|
+
return 0;
|
|
178
|
+
}
|
|
179
|
+
if (args.command === 'doctor')
|
|
180
|
+
return doctor() ? 0 : 1;
|
|
181
|
+
if (!args.platform) {
|
|
182
|
+
console.error('[vrn] 请指定平台:vrn gen ios | vrn gen android');
|
|
183
|
+
return 2;
|
|
184
|
+
}
|
|
185
|
+
const outDir = await generate(args, args.platform);
|
|
186
|
+
console.log(`[vrn] ${args.platform} 工程已生成:${outDir}`);
|
|
187
|
+
if (args.command === 'build' && args.run) {
|
|
188
|
+
const cmd = args.platform === 'android'
|
|
189
|
+
? [process.env.VRN_GRADLE ?? 'gradle', ['assembleDebug']]
|
|
190
|
+
: ['xcodegen', ['generate']];
|
|
191
|
+
const r = spawnSync(cmd[0], cmd[1], { cwd: outDir, stdio: 'inherit' });
|
|
192
|
+
if (r.status !== 0) {
|
|
193
|
+
console.error(`[vrn] 平台构建命令失败:${cmd[0]}(可在工程目录手动执行)`);
|
|
194
|
+
return r.status ?? 1;
|
|
195
|
+
}
|
|
196
|
+
if (args.platform === 'ios') {
|
|
197
|
+
const project = outDir.split('/').pop();
|
|
198
|
+
const b = spawnSync('xcodebuild', ['-project', `${project}.xcodeproj`, '-scheme', project, '-configuration', 'Debug', '-destination', 'platform=iOS Simulator,name=iPhone 17', '-derivedDataPath', 'build', 'build', 'CODE_SIGNING_ALLOWED=NO'], { cwd: outDir, stdio: 'inherit' });
|
|
199
|
+
return b.status ?? 1;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
return 0;
|
|
203
|
+
}
|
package/dist/config.d.ts
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
export interface VrnConfig {
|
|
2
|
+
/** Vue 工程根(默认 cwd) */
|
|
3
|
+
projectRoot: string;
|
|
4
|
+
/** 入口:router.ts(多页)或单个 .vue(单页),相对 projectRoot */
|
|
5
|
+
entry?: string;
|
|
6
|
+
/** 生成产物根目录(相对 projectRoot);platform 子目录由 CLI 追加 */
|
|
7
|
+
outDir: string;
|
|
8
|
+
/** 工程名(生成 Xcode/Gradle 工程名;默认由入口目录推导) */
|
|
9
|
+
projectName?: string;
|
|
10
|
+
/** bundle id(默认 com.vrn.<projectname>) */
|
|
11
|
+
bundleId?: string;
|
|
12
|
+
/** 生成页面集合(多页模式;省略则用 entry 的 router.ts) */
|
|
13
|
+
platform?: {
|
|
14
|
+
ios?: {
|
|
15
|
+
deploymentTarget?: string;
|
|
16
|
+
};
|
|
17
|
+
android?: {
|
|
18
|
+
minSdk?: number;
|
|
19
|
+
compileSdk?: number;
|
|
20
|
+
};
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
/** 读取 vrn.config(json 直接解析;ts/mjs 用动态 import,失败则忽略并提示) */
|
|
24
|
+
export declare function loadConfig(projectRoot: string): Promise<Partial<VrnConfig>>;
|
|
25
|
+
/** 合并默认值 + 配置文件 + CLI 覆盖 */
|
|
26
|
+
export declare function resolveConfig(base: Partial<VrnConfig> & {
|
|
27
|
+
projectRoot?: string;
|
|
28
|
+
}, overrides?: Partial<VrnConfig>): VrnConfig;
|
|
29
|
+
/** 入口解析:显式 entry → 目录下 router.ts → App.vue → 抛错并列出候选 */
|
|
30
|
+
export declare function resolveEntry(cfg: VrnConfig): {
|
|
31
|
+
entry: string;
|
|
32
|
+
multiPage: boolean;
|
|
33
|
+
};
|
|
34
|
+
/** 平台输出目录:<outDir>/<platform>/<ProjectName> */
|
|
35
|
+
export declare function platformOutDir(cfg: VrnConfig, platform: 'ios' | 'android', projectName: string): string;
|
|
36
|
+
/** 工程名推导:projectName → 入口目录名 → 'VrnApp'(PascalCase) */
|
|
37
|
+
export declare function deriveProjectName(cfg: VrnConfig, entry: string): string;
|
package/dist/config.js
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @dongmingzhixiu/vuexir 配置解析(R-PKG:CLI 只认「项目根 + vrn.config」两个事实来源)
|
|
3
|
+
*
|
|
4
|
+
* 解析顺序(后者覆盖前者):
|
|
5
|
+
* 1. 内置默认值(projectRoot = process.cwd(),outDir = <root>/native)
|
|
6
|
+
* 2. vrn.config.ts / vrn.config.mts / vrn.config.js / vrn.config.mjs / vrn.config.json
|
|
7
|
+
* 3. 命令行参数(--out / --entry / --name / --bundle 等)
|
|
8
|
+
*
|
|
9
|
+
* 兼容期:未提供 vrn.config 时,若 <root>/router.ts 或 <root>/App.vue 存在,按「目录即项目」处理;
|
|
10
|
+
* 仓库内开发(旧行为)通过 `--repo-root`(或 cwd 落在仓库内)解析到 examples/<name>。
|
|
11
|
+
*/
|
|
12
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
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'];
|
|
15
|
+
/** 读取 vrn.config(json 直接解析;ts/mjs 用动态 import,失败则忽略并提示) */
|
|
16
|
+
export async function loadConfig(projectRoot) {
|
|
17
|
+
for (const file of CONFIG_FILES) {
|
|
18
|
+
const p = join(projectRoot, file);
|
|
19
|
+
if (!existsSync(p))
|
|
20
|
+
continue;
|
|
21
|
+
try {
|
|
22
|
+
if (file.endsWith('.json')) {
|
|
23
|
+
return JSON.parse(readFileSync(p, 'utf-8'));
|
|
24
|
+
}
|
|
25
|
+
const mod = (await import(`file://${p}`));
|
|
26
|
+
return mod.default ?? {};
|
|
27
|
+
}
|
|
28
|
+
catch (e) {
|
|
29
|
+
console.warn(`[vrn] 读取 ${file} 失败:${e instanceof Error ? e.message : String(e)}`);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return {};
|
|
33
|
+
}
|
|
34
|
+
/** 合并默认值 + 配置文件 + CLI 覆盖 */
|
|
35
|
+
export function resolveConfig(base, overrides = {}) {
|
|
36
|
+
const projectRoot = resolve(overrides.projectRoot ?? base.projectRoot ?? process.cwd());
|
|
37
|
+
const outDirRaw = overrides.outDir ?? base.outDir ?? 'native';
|
|
38
|
+
return {
|
|
39
|
+
projectRoot,
|
|
40
|
+
entry: overrides.entry ?? base.entry,
|
|
41
|
+
outDir: isAbsolute(outDirRaw) ? outDirRaw : join(projectRoot, outDirRaw),
|
|
42
|
+
projectName: overrides.projectName ?? base.projectName,
|
|
43
|
+
bundleId: overrides.bundleId ?? base.bundleId,
|
|
44
|
+
platform: { ...(base.platform ?? {}), ...(overrides.platform ?? {}) },
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
/** 入口解析:显式 entry → 目录下 router.ts → App.vue → 抛错并列出候选 */
|
|
48
|
+
export function resolveEntry(cfg) {
|
|
49
|
+
const candidates = [];
|
|
50
|
+
if (cfg.entry) {
|
|
51
|
+
const p = isAbsolute(cfg.entry) ? cfg.entry : join(cfg.projectRoot, cfg.entry);
|
|
52
|
+
if (existsSync(p))
|
|
53
|
+
return { entry: p, multiPage: p.endsWith('router.ts') || p.endsWith('router.js') };
|
|
54
|
+
throw new Error(`vrn.config.entry 指向的文件不存在:${p}`);
|
|
55
|
+
}
|
|
56
|
+
const router = join(cfg.projectRoot, 'src/router.ts');
|
|
57
|
+
const routerRoot = join(cfg.projectRoot, 'router.ts');
|
|
58
|
+
for (const r of [router, routerRoot]) {
|
|
59
|
+
if (existsSync(r))
|
|
60
|
+
return { entry: r, multiPage: true };
|
|
61
|
+
candidates.push(r);
|
|
62
|
+
}
|
|
63
|
+
for (const a of [join(cfg.projectRoot, 'src/App.vue'), join(cfg.projectRoot, 'App.vue')]) {
|
|
64
|
+
if (existsSync(a))
|
|
65
|
+
return { entry: a, multiPage: false };
|
|
66
|
+
candidates.push(a);
|
|
67
|
+
}
|
|
68
|
+
throw new Error(`未找到入口(多页 src/router.ts 或单页 src/App.vue)。已尝试:\n ${candidates.join('\n ')}\n` +
|
|
69
|
+
`可在 vrn.config.ts 中用 entry 显式指定。`);
|
|
70
|
+
}
|
|
71
|
+
/** 平台输出目录:<outDir>/<platform>/<ProjectName> */
|
|
72
|
+
export function platformOutDir(cfg, platform, projectName) {
|
|
73
|
+
return join(cfg.outDir, platform, projectName);
|
|
74
|
+
}
|
|
75
|
+
/** 工程名推导:projectName → 入口目录名 → 'VrnApp'(PascalCase) */
|
|
76
|
+
export function deriveProjectName(cfg, entry) {
|
|
77
|
+
const raw = cfg.projectName ?? dirname(entry).split('/').pop() ?? 'VrnApp';
|
|
78
|
+
const cleaned = raw.replace(/[^A-Za-z0-9]+/g, ' ').trim().split(/\s+/).pop() ?? 'VrnApp';
|
|
79
|
+
return cleaned.charAt(0).toUpperCase() + cleaned.slice(1);
|
|
80
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export interface GenerateOptions {
|
|
2
|
+
/** 入口:样例名 / 目录 / 显式路径(默认 vuexir-demo) */
|
|
3
|
+
input?: string;
|
|
4
|
+
/** 产物根(默认 VRN_OUT_DIR 或仓库内 compiler/generated/android) */
|
|
5
|
+
outDirRoot?: string;
|
|
6
|
+
projectName?: string;
|
|
7
|
+
bundleId?: string;
|
|
8
|
+
}
|
|
9
|
+
/** 生成 Android(Compose)工程;返回产物目录 */
|
|
10
|
+
export declare function generateAndroid(opts?: GenerateOptions): string;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* gen-android.ts —— Android 工程生成入口脚本(对位 gen-ios.ts)
|
|
3
|
+
*
|
|
4
|
+
* 链路:vuexir-demo/(router.ts + pages/*.vue) → vueToIr → validateIr → compose.ts → Gradle 工程
|
|
5
|
+
* 输出:compiler/generated/android/<ProjectName>/(gradle assembleDebug 可构建)
|
|
6
|
+
*
|
|
7
|
+
* 运行:
|
|
8
|
+
* npx vite-node compiler/scripts/gen-android.ts # 默认 vuexir-demo
|
|
9
|
+
* npx vite-node compiler/scripts/gen-android.ts <sample名|路径> # 指定样例
|
|
10
|
+
*
|
|
11
|
+
* codegen 核心在 compiler/src/codegen/compose.ts(对位 swiftui.ts)。
|
|
12
|
+
*/
|
|
13
|
+
import { existsSync } from 'node:fs';
|
|
14
|
+
import { join, dirname } from 'node:path';
|
|
15
|
+
import { fileURLToPath } from 'node:url';
|
|
16
|
+
import { runTabs, resolveInput } from '@dongmingzhixiu/vuexir-compiler';
|
|
17
|
+
import { TEMPLATE_VERSION, buildGradle } from '@dongmingzhixiu/vuexir-templates';
|
|
18
|
+
// R-PKG:产物根不再假设仓库布局——VRN_OUT_DIR 优先(CLI 注入),否则回退到仓库内 compiler/generated/android
|
|
19
|
+
const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..', '..', '..');
|
|
20
|
+
const OUT_ROOT = process.env.VRN_OUT_DIR ?? join(ROOT, 'compiler/generated/android');
|
|
21
|
+
/** 样例根(仓库内开发用;用户工程由 CLI 传入显式入口) */
|
|
22
|
+
const SAMPLES_ROOT = process.env.VRN_SAMPLES_ROOT ?? join(ROOT, 'examples');
|
|
23
|
+
/** 生成 Android(Compose)工程;返回产物目录 */
|
|
24
|
+
export function generateAndroid(opts = {}) {
|
|
25
|
+
const input = resolveInput(opts.input);
|
|
26
|
+
if (!existsSync(join(input, 'router.ts'))) {
|
|
27
|
+
throw new Error(`[vrn] ${input} 缺少 router.ts(多页模式需要 router.ts;单页请用 gen-ios 路径或补 router.ts)`);
|
|
28
|
+
}
|
|
29
|
+
return runTabs(input, {
|
|
30
|
+
outDirRoot: opts.outDirRoot,
|
|
31
|
+
projectName: opts.projectName,
|
|
32
|
+
bundleId: opts.bundleId,
|
|
33
|
+
renderGradle: (projName, bid) => buildGradle(bid),
|
|
34
|
+
templateVersion: TEMPLATE_VERSION,
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
// 直接执行(旧脚本用法保持兼容):npx vite-node packages/cli/src/gen-android.ts [样例名|路径]
|
|
38
|
+
// CLI(packages/cli/src/cli.ts)导入本模块前设置 VRN_NO_AUTO_RUN=1,避免 import 时重复生成。
|
|
39
|
+
// 说明:vite-node 下 process.argv[1] 是 vite-node 自身路径,不能用「是否为主模块」判定。
|
|
40
|
+
if (process.env.VRN_NO_AUTO_RUN !== '1') {
|
|
41
|
+
try {
|
|
42
|
+
generateAndroid({ input: process.argv[2] });
|
|
43
|
+
}
|
|
44
|
+
catch (e) {
|
|
45
|
+
console.error(e instanceof Error ? e.message : String(e));
|
|
46
|
+
process.exit(1);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* gen-ios.ts —— iOS 工程生成脚本(P1-lite 主入口)
|
|
3
|
+
*
|
|
4
|
+
* 输入:Vue SFC(默认 compiler/samples/hello/Hello.vue)或 IR JSON(.json 后缀)
|
|
5
|
+
* 管线:Vue SFC → (vueToIr) → IR → (validateIr) → SwiftUI 源码 + 独立 Xcode 工程
|
|
6
|
+
*
|
|
7
|
+
* 运行:
|
|
8
|
+
* npx vite-node compiler/scripts/gen-ios.ts # 默认 Hello.vue
|
|
9
|
+
* npx vite-node compiler/scripts/gen-ios.ts path/My.vue # 指定 Vue 文件
|
|
10
|
+
* npx vite-node compiler/scripts/gen-ios.ts path/screen.ir.json # 或手写 IR
|
|
11
|
+
*
|
|
12
|
+
* 产物:compiler/generated/ios/<ProjectName>/{App.swift, ScreenView.swift, recite.json, project.yml}
|
|
13
|
+
* 后续:cd compiler/generated/ios/<ProjectName> && xcodegen generate && xcodebuild ...
|
|
14
|
+
*/
|
|
15
|
+
export interface GenerateIosOptions {
|
|
16
|
+
/** 样例名 / 目录 / 显式 .vue 或 .json 路径;'all' 批量;省略 = 默认 vuexir-demo */
|
|
17
|
+
input?: string;
|
|
18
|
+
/** 产物根(<outDir>/<ProjectName>) */
|
|
19
|
+
outDirRoot?: string;
|
|
20
|
+
/** 样例根(默认 examples/) */
|
|
21
|
+
samplesRoot?: string;
|
|
22
|
+
projectName?: string;
|
|
23
|
+
bundleId?: string;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* 生成 iOS(SwiftUI)工程。返回产物目录;失败抛错(不调用 process.exit,便于 CLI 复用)。
|
|
27
|
+
* 说明:本函数沿用既有 main 逻辑(多页/单页/批量),仅把路径来源从「仓库布局」改成可注入。
|
|
28
|
+
*/
|
|
29
|
+
export declare function generateIos(opts?: GenerateIosOptions): string | undefined;
|