@thelacanians/vue-native-cli 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/dist/cli.d.ts ADDED
@@ -0,0 +1 @@
1
+ #!/usr/bin/env node
package/dist/cli.js ADDED
@@ -0,0 +1,754 @@
1
+ #!/usr/bin/env node
2
+ #!/usr/bin/env node
3
+
4
+ // src/cli.ts
5
+ import { program } from "commander";
6
+
7
+ // src/commands/create.ts
8
+ import { Command } from "commander";
9
+ import { mkdir, writeFile } from "fs/promises";
10
+ import { join } from "path";
11
+ import pc from "picocolors";
12
+ var createCommand = new Command("create").description("Create a new Vue Native project").argument("<name>", "project name").action(async (name) => {
13
+ const dir = join(process.cwd(), name);
14
+ console.log(pc.cyan(`
15
+ Creating Vue Native project: ${pc.bold(name)}
16
+ `));
17
+ try {
18
+ await mkdir(dir, { recursive: true });
19
+ await mkdir(join(dir, "app"), { recursive: true });
20
+ await mkdir(join(dir, "app", "pages"), { recursive: true });
21
+ await writeFile(join(dir, "package.json"), JSON.stringify({
22
+ name,
23
+ version: "0.0.1",
24
+ private: true,
25
+ type: "module",
26
+ scripts: {
27
+ dev: "vue-native dev",
28
+ build: "vite build",
29
+ typecheck: "tsc --noEmit"
30
+ },
31
+ dependencies: {
32
+ "@thelacanians/vue-native-runtime": "^0.1.0",
33
+ "@thelacanians/vue-native-navigation": "^0.1.0",
34
+ "vue": "^3.5.0"
35
+ },
36
+ devDependencies: {
37
+ "@thelacanians/vue-native-vite-plugin": "^0.1.0",
38
+ "@vitejs/plugin-vue": "^5.0.0",
39
+ "vite": "^6.1.0",
40
+ "typescript": "^5.7.0"
41
+ }
42
+ }, null, 2));
43
+ await writeFile(join(dir, "vite.config.ts"), `import { defineConfig } from 'vite'
44
+ import vue from '@vitejs/plugin-vue'
45
+ import vueNative from '@thelacanians/vue-native-vite-plugin'
46
+
47
+ export default defineConfig({
48
+ plugins: [vue(), vueNative()],
49
+ })
50
+ `);
51
+ await writeFile(join(dir, "tsconfig.json"), JSON.stringify({
52
+ compilerOptions: {
53
+ target: "ES2020",
54
+ module: "ESNext",
55
+ moduleResolution: "bundler",
56
+ strict: true,
57
+ jsx: "preserve",
58
+ lib: ["ES2020"],
59
+ types: []
60
+ },
61
+ include: ["app/**/*"]
62
+ }, null, 2));
63
+ await writeFile(join(dir, "app", "main.ts"), `import { createApp } from 'vue'
64
+ import { createRouter } from '@thelacanians/vue-native-navigation'
65
+ import App from './App.vue'
66
+ import Home from './pages/Home.vue'
67
+
68
+ const router = createRouter([
69
+ { name: 'Home', component: Home },
70
+ ])
71
+
72
+ const app = createApp(App)
73
+ app.use(router)
74
+ app.start()
75
+ `);
76
+ await writeFile(join(dir, "app", "App.vue"), `<template>
77
+ <VSafeArea :style="{ flex: 1, backgroundColor: '#ffffff' }">
78
+ <RouterView />
79
+ </VSafeArea>
80
+ </template>
81
+
82
+ <script setup lang="ts">
83
+ import { RouterView } from '@thelacanians/vue-native-navigation'
84
+ </script>
85
+ `);
86
+ await writeFile(join(dir, "app", "pages", "Home.vue"), `<template>
87
+ <VView :style="styles.container">
88
+ <VText :style="styles.title">Hello, Vue Native! \u{1F389}</VText>
89
+ <VText :style="styles.subtitle">Edit app/pages/Home.vue to get started.</VText>
90
+ <VButton :style="styles.button" @press="count++">
91
+ <VText :style="styles.buttonText">Count: {{ count }}</VText>
92
+ </VButton>
93
+ </VView>
94
+ </template>
95
+
96
+ <script setup lang="ts">
97
+ import { ref } from 'vue'
98
+ import { createStyleSheet } from 'vue'
99
+
100
+ const count = ref(0)
101
+
102
+ const styles = createStyleSheet({
103
+ container: {
104
+ flex: 1,
105
+ justifyContent: 'center',
106
+ alignItems: 'center',
107
+ padding: 24,
108
+ },
109
+ title: {
110
+ fontSize: 28,
111
+ fontWeight: 'bold',
112
+ color: '#1a1a1a',
113
+ marginBottom: 8,
114
+ textAlign: 'center',
115
+ },
116
+ subtitle: {
117
+ fontSize: 16,
118
+ color: '#666',
119
+ textAlign: 'center',
120
+ marginBottom: 32,
121
+ },
122
+ button: {
123
+ backgroundColor: '#4f46e5',
124
+ paddingHorizontal: 32,
125
+ paddingVertical: 14,
126
+ borderRadius: 12,
127
+ },
128
+ buttonText: {
129
+ color: '#ffffff',
130
+ fontSize: 18,
131
+ fontWeight: '600',
132
+ },
133
+ })
134
+ </script>
135
+ `);
136
+ const iosDir = join(dir, "ios");
137
+ const iosSrcDir = join(iosDir, "Sources");
138
+ await mkdir(iosSrcDir, { recursive: true });
139
+ const xcodeProjectName = name.replace(/[^a-zA-Z0-9]/g, "");
140
+ const bundleId = `com.vuenative.${name.replace(/[^a-zA-Z0-9]/g, "").toLowerCase()}`;
141
+ await writeFile(join(iosDir, "project.yml"), `name: ${xcodeProjectName}
142
+ options:
143
+ bundleIdPrefix: com.vuenative
144
+ deploymentTarget:
145
+ iOS: "16.0"
146
+ xcodeVersion: "15.0"
147
+
148
+ packages:
149
+ VueNativeCore:
150
+ path: ../native/ios
151
+
152
+ targets:
153
+ ${xcodeProjectName}:
154
+ type: application
155
+ platform: iOS
156
+ sources:
157
+ - Sources
158
+ dependencies:
159
+ - package: VueNativeCore
160
+ settings:
161
+ base:
162
+ PRODUCT_BUNDLE_IDENTIFIER: ${bundleId}
163
+ INFOPLIST_FILE: Sources/Info.plist
164
+ SWIFT_VERSION: "5.9"
165
+ GENERATE_INFOPLIST_FILE: false
166
+ resources:
167
+ - path: ../dist/vue-native-bundle.js
168
+ optional: true
169
+ `);
170
+ await writeFile(join(iosSrcDir, "Info.plist"), `<?xml version="1.0" encoding="UTF-8"?>
171
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
172
+ <plist version="1.0">
173
+ <dict>
174
+ <key>CFBundleDevelopmentRegion</key>
175
+ <string>en</string>
176
+ <key>CFBundleDisplayName</key>
177
+ <string>${name}</string>
178
+ <key>CFBundleExecutable</key>
179
+ <string>$(EXECUTABLE_NAME)</string>
180
+ <key>CFBundleIdentifier</key>
181
+ <string>${bundleId}</string>
182
+ <key>CFBundleInfoDictionaryVersion</key>
183
+ <string>6.0</string>
184
+ <key>CFBundleName</key>
185
+ <string>$(PRODUCT_NAME)</string>
186
+ <key>CFBundlePackageType</key>
187
+ <string>APPL</string>
188
+ <key>CFBundleShortVersionString</key>
189
+ <string>1.0</string>
190
+ <key>CFBundleVersion</key>
191
+ <string>1</string>
192
+ <key>LSRequiresIPhoneOS</key>
193
+ <true/>
194
+ <key>UIApplicationSceneManifest</key>
195
+ <dict>
196
+ <key>UIApplicationSupportsMultipleScenes</key>
197
+ <false/>
198
+ <key>UISceneConfigurations</key>
199
+ <dict>
200
+ <key>UIWindowSceneSessionRoleApplication</key>
201
+ <array>
202
+ <dict>
203
+ <key>UISceneConfigurationName</key>
204
+ <string>Default Configuration</string>
205
+ <key>UISceneDelegateClassName</key>
206
+ <string>$(PRODUCT_MODULE_NAME).SceneDelegate</string>
207
+ </dict>
208
+ </array>
209
+ </dict>
210
+ </dict>
211
+ <key>UILaunchScreen</key>
212
+ <dict/>
213
+ <key>UISupportedInterfaceOrientations</key>
214
+ <array>
215
+ <string>UIInterfaceOrientationPortrait</string>
216
+ <string>UIInterfaceOrientationLandscapeLeft</string>
217
+ <string>UIInterfaceOrientationLandscapeRight</string>
218
+ </array>
219
+ </dict>
220
+ </plist>
221
+ `);
222
+ await writeFile(join(iosSrcDir, "AppDelegate.swift"), `import UIKit
223
+
224
+ @main
225
+ class AppDelegate: UIResponder, UIApplicationDelegate {
226
+ func application(
227
+ _ application: UIApplication,
228
+ didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
229
+ ) -> Bool {
230
+ return true
231
+ }
232
+
233
+ // MARK: UISceneSession Lifecycle
234
+ func application(
235
+ _ application: UIApplication,
236
+ configurationForConnecting connectingSceneSession: UISceneSession,
237
+ options: UIScene.ConnectionOptions
238
+ ) -> UISceneConfiguration {
239
+ return UISceneConfiguration(
240
+ name: "Default Configuration",
241
+ sessionRole: connectingSceneSession.role
242
+ )
243
+ }
244
+ }
245
+ `);
246
+ await writeFile(join(iosSrcDir, "SceneDelegate.swift"), `import UIKit
247
+ import VueNativeCore
248
+
249
+ class SceneDelegate: UIResponder, UIWindowSceneDelegate {
250
+ var window: UIWindow?
251
+
252
+ func scene(
253
+ _ scene: UIScene,
254
+ willConnectTo session: UISceneSession,
255
+ options connectionOptions: UIScene.ConnectionOptions
256
+ ) {
257
+ guard let windowScene = scene as? UIWindowScene else { return }
258
+
259
+ let window = UIWindow(windowScene: windowScene)
260
+ window.rootViewController = AppViewController()
261
+ window.makeKeyAndVisible()
262
+ self.window = window
263
+ }
264
+ }
265
+
266
+ class AppViewController: VueNativeViewController {
267
+ override var bundleName: String { "vue-native-bundle" }
268
+
269
+ #if DEBUG
270
+ override var devServerURL: URL? { URL(string: "ws://localhost:8174") }
271
+ #endif
272
+ }
273
+ `);
274
+ const androidDir = join(dir, "android");
275
+ const androidAppDir = join(androidDir, "app");
276
+ const androidPkg = `com.vuenative.${name.replace(/[^a-zA-Z0-9]/g, "").toLowerCase()}`;
277
+ const androidPkgPath = androidPkg.replace(/\./g, "/");
278
+ const androidSrcDir = join(androidAppDir, "src", "main");
279
+ const androidKotlinDir = join(androidSrcDir, "kotlin", androidPkgPath);
280
+ await mkdir(androidKotlinDir, { recursive: true });
281
+ await writeFile(join(androidDir, "build.gradle.kts"), `// Top-level build file
282
+ plugins {
283
+ id("com.android.application") version "8.2.2" apply false
284
+ id("com.android.library") version "8.2.2" apply false
285
+ id("org.jetbrains.kotlin.android") version "1.9.22" apply false
286
+ }
287
+ `);
288
+ await writeFile(join(androidDir, "settings.gradle.kts"), `pluginManagement {
289
+ repositories {
290
+ google()
291
+ mavenCentral()
292
+ gradlePluginPortal()
293
+ }
294
+ }
295
+ dependencyResolutionManagement {
296
+ repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
297
+ repositories {
298
+ google()
299
+ mavenCentral()
300
+ maven { url = uri("https://jitpack.io") }
301
+ }
302
+ }
303
+
304
+ rootProject.name = "${name}"
305
+ include(":app")
306
+ include(":VueNativeCore")
307
+ project(":VueNativeCore").projectDir = file("../native/android/VueNativeCore")
308
+ `);
309
+ await writeFile(join(androidAppDir, "build.gradle.kts"), `plugins {
310
+ id("com.android.application")
311
+ id("org.jetbrains.kotlin.android")
312
+ }
313
+
314
+ android {
315
+ namespace = "${androidPkg}"
316
+ compileSdk = 34
317
+
318
+ defaultConfig {
319
+ applicationId = "${androidPkg}"
320
+ minSdk = 21
321
+ targetSdk = 34
322
+ versionCode = 1
323
+ versionName = "1.0"
324
+ }
325
+
326
+ buildTypes {
327
+ release {
328
+ isMinifyEnabled = false
329
+ proguardFiles(
330
+ getDefaultProguardFile("proguard-android-optimize.txt"),
331
+ "proguard-rules.pro"
332
+ )
333
+ }
334
+ }
335
+
336
+ compileOptions {
337
+ sourceCompatibility = JavaVersion.VERSION_17
338
+ targetCompatibility = JavaVersion.VERSION_17
339
+ }
340
+
341
+ kotlinOptions {
342
+ jvmTarget = "17"
343
+ }
344
+ }
345
+
346
+ dependencies {
347
+ implementation(project(":VueNativeCore"))
348
+ }
349
+ `);
350
+ await writeFile(join(androidSrcDir, "AndroidManifest.xml"), `<?xml version="1.0" encoding="utf-8"?>
351
+ <manifest xmlns:android="http://schemas.android.com/apk/res/android">
352
+ <uses-permission android:name="android.permission.INTERNET" />
353
+
354
+ <application
355
+ android:allowBackup="true"
356
+ android:label="${name}"
357
+ android:supportsRtl="true"
358
+ android:theme="@style/Theme.AppCompat.Light.NoActionBar"
359
+ android:usesCleartextTraffic="true">
360
+ <activity
361
+ android:name=".MainActivity"
362
+ android:exported="true">
363
+ <intent-filter>
364
+ <action android:name="android.intent.action.MAIN" />
365
+ <category android:name="android.intent.category.LAUNCHER" />
366
+ </intent-filter>
367
+ </activity>
368
+ </application>
369
+ </manifest>
370
+ `);
371
+ await writeFile(join(androidKotlinDir, "MainActivity.kt"), `package ${androidPkg}
372
+
373
+ import com.vuenative.core.VueNativeActivity
374
+
375
+ class MainActivity : VueNativeActivity() {
376
+ override fun getBundleAssetPath(): String {
377
+ return "vue-native-bundle.js"
378
+ }
379
+
380
+ override fun getDevServerUrl(): String? {
381
+ return "ws://10.0.2.2:8174"
382
+ }
383
+ }
384
+ `);
385
+ await writeFile(join(androidDir, "gradle.properties"), `# Project-wide Gradle settings
386
+ org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
387
+ android.useAndroidX=true
388
+ kotlin.code.style=official
389
+ android.nonTransitiveRClass=true
390
+ `);
391
+ console.log(pc.green("\u2713 Project created successfully!\n"));
392
+ console.log(pc.white("Next steps:\n"));
393
+ console.log(pc.white(` cd ${name}`));
394
+ console.log(pc.white(" bun install"));
395
+ console.log(pc.white(" vue-native dev\n"));
396
+ console.log(pc.white("To run on iOS:"));
397
+ console.log(pc.white(" vue-native run ios\n"));
398
+ console.log(pc.white("To run on Android:"));
399
+ console.log(pc.white(" vue-native run android\n"));
400
+ } catch (err) {
401
+ console.error(pc.red(`Error creating project: ${err.message}`));
402
+ process.exit(1);
403
+ }
404
+ });
405
+
406
+ // src/commands/dev.ts
407
+ import { Command as Command2 } from "commander";
408
+ import { spawn } from "child_process";
409
+ import { readFile } from "fs/promises";
410
+ import { join as join2 } from "path";
411
+ import { watch } from "chokidar";
412
+ import { WebSocketServer, WebSocket } from "ws";
413
+ import pc2 from "picocolors";
414
+ var DEFAULT_PORT = 8174;
415
+ var BUNDLE_FILE = "dist/vue-native-bundle.js";
416
+ var devCommand = new Command2("dev").description("Start the Vue Native dev server with hot reload").option("-p, --port <port>", "WebSocket port for hot reload", String(DEFAULT_PORT)).action(async (options) => {
417
+ const port = parseInt(options.port, 10);
418
+ const cwd = process.cwd();
419
+ const bundlePath = join2(cwd, BUNDLE_FILE);
420
+ console.log(pc2.cyan("\n\u26A1 Vue Native Dev Server\n"));
421
+ const wss = new WebSocketServer({ port });
422
+ const clients = /* @__PURE__ */ new Set();
423
+ wss.on("connection", (ws) => {
424
+ clients.add(ws);
425
+ ws.send(JSON.stringify({ type: "connected" }));
426
+ console.log(pc2.green(` iOS client connected (${clients.size} total)`));
427
+ readFile(bundlePath, "utf8").then((bundle) => {
428
+ if (ws.readyState === WebSocket.OPEN) {
429
+ ws.send(JSON.stringify({ type: "bundle", bundle }));
430
+ console.log(pc2.dim(` Sent bundle to new client (${Math.round(bundle.length / 1024)}KB)`));
431
+ }
432
+ }).catch(() => {
433
+ });
434
+ ws.on("close", () => {
435
+ clients.delete(ws);
436
+ console.log(pc2.dim(` iOS client disconnected (${clients.size} remaining)`));
437
+ });
438
+ ws.on("message", (data) => {
439
+ try {
440
+ const msg = JSON.parse(data.toString());
441
+ if (msg.type === "pong") return;
442
+ } catch {
443
+ }
444
+ });
445
+ });
446
+ wss.on("error", (err) => {
447
+ console.error(pc2.red(`WebSocket server error: ${err.message}`));
448
+ });
449
+ console.log(pc2.white(` Hot reload server: ${pc2.bold(`ws://localhost:${port}`)}`));
450
+ console.log(pc2.dim(" Waiting for iOS app to connect...\n"));
451
+ console.log(pc2.white(" Starting Vite build watcher...\n"));
452
+ const vite = spawn(
453
+ "bun",
454
+ ["run", "vite", "build", "--watch", "--mode", "development"],
455
+ { cwd, stdio: "pipe" }
456
+ );
457
+ vite.stdout?.on("data", (data) => {
458
+ const text = data.toString().trim();
459
+ if (text) console.log(pc2.dim(` [vite] ${text}`));
460
+ });
461
+ vite.stderr?.on("data", (data) => {
462
+ const text = data.toString().trim();
463
+ if (text) console.log(pc2.yellow(` [vite] ${text}`));
464
+ });
465
+ vite.on("error", (err) => {
466
+ console.error(pc2.red(`Vite error: ${err.message}`));
467
+ });
468
+ const watcher = watch(bundlePath, {
469
+ persistent: true,
470
+ ignoreInitial: false,
471
+ awaitWriteFinish: { stabilityThreshold: 100, pollInterval: 50 }
472
+ });
473
+ watcher.on("add", broadcastBundle);
474
+ watcher.on("change", broadcastBundle);
475
+ async function broadcastBundle() {
476
+ try {
477
+ const bundle = await readFile(bundlePath, "utf8");
478
+ const payload = JSON.stringify({ type: "bundle", bundle });
479
+ let sent = 0;
480
+ for (const client of clients) {
481
+ if (client.readyState === WebSocket.OPEN) {
482
+ client.send(payload);
483
+ sent++;
484
+ }
485
+ }
486
+ console.log(pc2.green(` \u2713 Bundle updated (${Math.round(bundle.length / 1024)}KB) \u2192 sent to ${sent} client(s)`));
487
+ } catch (err) {
488
+ }
489
+ }
490
+ setInterval(() => {
491
+ for (const client of clients) {
492
+ if (client.readyState === WebSocket.OPEN) {
493
+ client.send(JSON.stringify({ type: "ping" }));
494
+ }
495
+ }
496
+ }, 3e4);
497
+ process.on("SIGINT", () => {
498
+ console.log(pc2.yellow("\n Shutting down dev server..."));
499
+ vite.kill();
500
+ wss.close();
501
+ process.exit(0);
502
+ });
503
+ });
504
+
505
+ // src/commands/run.ts
506
+ import { Command as Command3 } from "commander";
507
+ import { spawn as spawn2, execSync } from "child_process";
508
+ import { existsSync, readdirSync, readFileSync } from "fs";
509
+ import { join as join3 } from "path";
510
+ import pc3 from "picocolors";
511
+ function findAppPath(buildDir) {
512
+ const derivedDataBase = join3(
513
+ process.env.HOME || "~",
514
+ "Library/Developer/Xcode/DerivedData"
515
+ );
516
+ if (existsSync(derivedDataBase)) {
517
+ try {
518
+ const projects = readdirSync(derivedDataBase);
519
+ for (const project of projects.reverse()) {
520
+ const productsDir = join3(
521
+ derivedDataBase,
522
+ project,
523
+ "Build/Products/Debug-iphonesimulator"
524
+ );
525
+ if (existsSync(productsDir)) {
526
+ const entries = readdirSync(productsDir);
527
+ const app = entries.find((e) => e.endsWith(".app"));
528
+ if (app) {
529
+ return join3(productsDir, app);
530
+ }
531
+ }
532
+ }
533
+ } catch {
534
+ }
535
+ }
536
+ return null;
537
+ }
538
+ function readBundleId(iosDir) {
539
+ const plistPath = join3(iosDir, "Sources", "Info.plist");
540
+ if (existsSync(plistPath)) {
541
+ try {
542
+ const content = readFileSync(plistPath, "utf8");
543
+ const match = content.match(
544
+ /<key>CFBundleIdentifier<\/key>\s*<string>([^<]+)<\/string>/
545
+ );
546
+ if (match) {
547
+ return match[1];
548
+ }
549
+ } catch {
550
+ }
551
+ }
552
+ return "com.vuenative.app";
553
+ }
554
+ function findApkPath(androidDir) {
555
+ const apkDir = join3(androidDir, "app", "build", "outputs", "apk", "debug");
556
+ if (existsSync(apkDir)) {
557
+ try {
558
+ const entries = readdirSync(apkDir);
559
+ const apk = entries.find((e) => e.endsWith(".apk") && !e.includes("androidTest"));
560
+ if (apk) {
561
+ return join3(apkDir, apk);
562
+ }
563
+ } catch {
564
+ }
565
+ }
566
+ return null;
567
+ }
568
+ var runCommand = new Command3("run").description("Build and run the app").argument("<platform>", "platform to run on (ios, android)").option("--device", "run on physical device instead of simulator").option("--scheme <scheme>", "Xcode scheme to build").option("--simulator <name>", "simulator name", "iPhone 16").option("--bundle-id <id>", "app bundle identifier").option("--package <name>", "Android package name", "com.vuenative.app").option("--activity <name>", "Android activity name", ".MainActivity").action(async (platform, options) => {
569
+ if (platform !== "ios" && platform !== "android") {
570
+ console.error(pc3.red('Platform must be "ios" or "android"'));
571
+ process.exit(1);
572
+ }
573
+ const cwd = process.cwd();
574
+ console.log(pc3.cyan(`
575
+ \u{1F4F1} Vue Native \u2014 Run ${platform === "ios" ? "iOS" : "Android"}
576
+ `));
577
+ console.log(pc3.white(" Building JS bundle..."));
578
+ try {
579
+ execSync("bun run vite build", { cwd, stdio: "inherit" });
580
+ console.log(pc3.green(" \u2713 Bundle built\n"));
581
+ } catch {
582
+ console.error(pc3.red(" \u2717 Bundle build failed"));
583
+ process.exit(1);
584
+ }
585
+ if (platform === "ios") {
586
+ runIOS(cwd, options);
587
+ } else {
588
+ runAndroid(cwd, options);
589
+ }
590
+ });
591
+ function runIOS(cwd, options) {
592
+ let xcodeProject = null;
593
+ const iosDir = join3(cwd, "ios");
594
+ if (existsSync(iosDir)) {
595
+ for (const ext of [".xcworkspace", ".xcodeproj"]) {
596
+ try {
597
+ const entries = readdirSync(iosDir);
598
+ const match = entries.find((e) => e.endsWith(ext));
599
+ if (match) {
600
+ xcodeProject = join3(iosDir, match);
601
+ break;
602
+ }
603
+ } catch {
604
+ }
605
+ }
606
+ }
607
+ if (!xcodeProject) {
608
+ console.log(pc3.yellow(" No Xcode project found in ./ios/"));
609
+ console.log(pc3.dim(" To add iOS support, create an Xcode project in the ios/ directory."));
610
+ console.log(pc3.dim(" Bundle has been built to dist/vue-native-bundle.js\n"));
611
+ return;
612
+ }
613
+ const isWorkspace = xcodeProject.endsWith(".xcworkspace");
614
+ const scheme = options.scheme || xcodeProject.split("/").pop()?.replace(/\.(xcworkspace|xcodeproj)$/, "") || "App";
615
+ const destination = options.device ? "generic/platform=iOS" : `platform=iOS Simulator,name=${options.simulator}`;
616
+ const projectFlag = isWorkspace ? "-workspace" : "-project";
617
+ console.log(pc3.white(` Building ${scheme} for ${options.device ? "device" : options.simulator}...`));
618
+ const xcodebuild = spawn2(
619
+ "xcodebuild",
620
+ [projectFlag, xcodeProject, "-scheme", scheme, "-destination", destination, "build"],
621
+ {
622
+ cwd,
623
+ stdio: "pipe",
624
+ env: { ...process.env, DEVELOPER_DIR: "/Applications/Xcode.app/Contents/Developer" }
625
+ }
626
+ );
627
+ xcodebuild.stderr?.on("data", (data) => {
628
+ const text = data.toString().trim();
629
+ if (text.includes("error:") || text.includes("warning:")) {
630
+ console.log(pc3.dim(` ${text}`));
631
+ }
632
+ });
633
+ xcodebuild.on("close", (code) => {
634
+ if (code !== 0) {
635
+ console.error(pc3.red(` \u2717 Build failed (exit code ${code})`));
636
+ process.exit(1);
637
+ }
638
+ console.log(pc3.green(" \u2713 Build successful\n"));
639
+ if (options.device) {
640
+ console.log(pc3.green(" App built for device. Install via Xcode.\n"));
641
+ return;
642
+ }
643
+ const simulatorName = options.simulator;
644
+ const bundleId = options.bundleId || readBundleId(join3(cwd, "ios"));
645
+ console.log(pc3.white(` Booting simulator "${simulatorName}"...`));
646
+ try {
647
+ execSync(`xcrun simctl boot "${simulatorName}"`, { stdio: "pipe" });
648
+ } catch {
649
+ }
650
+ try {
651
+ execSync("open -a Simulator", { stdio: "pipe" });
652
+ } catch {
653
+ }
654
+ const appPath = findAppPath(join3(cwd, "ios"));
655
+ if (appPath) {
656
+ console.log(pc3.white(` Installing app on simulator...`));
657
+ try {
658
+ execSync(`xcrun simctl install booted "${appPath}"`, { stdio: "pipe" });
659
+ console.log(pc3.green(" \u2713 App installed"));
660
+ } catch (err) {
661
+ console.error(pc3.red(` \u2717 Failed to install app: ${err.message}`));
662
+ process.exit(1);
663
+ }
664
+ console.log(pc3.white(` Launching ${bundleId}...`));
665
+ try {
666
+ execSync(`xcrun simctl launch booted "${bundleId}"`, { stdio: "pipe" });
667
+ console.log(pc3.green(` \u2713 App launched on ${simulatorName}
668
+ `));
669
+ } catch (err) {
670
+ console.error(pc3.red(` \u2717 Failed to launch app: ${err.message}`));
671
+ process.exit(1);
672
+ }
673
+ } else {
674
+ console.log(pc3.yellow(" Could not locate .app bundle in DerivedData."));
675
+ console.log(pc3.dim(" Try running the app from Xcode directly.\n"));
676
+ }
677
+ });
678
+ }
679
+ function runAndroid(cwd, options) {
680
+ const androidDir = join3(cwd, "android");
681
+ if (!existsSync(androidDir)) {
682
+ console.log(pc3.yellow(" No android/ directory found."));
683
+ console.log(pc3.dim(" To add Android support, create an Android project in the android/ directory."));
684
+ console.log(pc3.dim(" Bundle has been built to dist/vue-native-bundle.js\n"));
685
+ return;
686
+ }
687
+ const gradlew = join3(androidDir, "gradlew");
688
+ if (!existsSync(gradlew)) {
689
+ console.error(pc3.red(" \u2717 gradlew not found in android/ directory"));
690
+ console.log(pc3.dim(" Make sure your Android project has the Gradle wrapper.\n"));
691
+ process.exit(1);
692
+ }
693
+ console.log(pc3.white(" Building Android app with Gradle..."));
694
+ const gradle = spawn2(
695
+ "./gradlew",
696
+ ["assembleDebug"],
697
+ {
698
+ cwd: androidDir,
699
+ stdio: "pipe",
700
+ env: { ...process.env }
701
+ }
702
+ );
703
+ gradle.stdout?.on("data", (data) => {
704
+ const text = data.toString().trim();
705
+ if (text) {
706
+ console.log(pc3.dim(` ${text}`));
707
+ }
708
+ });
709
+ gradle.stderr?.on("data", (data) => {
710
+ const text = data.toString().trim();
711
+ if (text.includes("ERROR") || text.includes("FAILURE")) {
712
+ console.log(pc3.red(` ${text}`));
713
+ }
714
+ });
715
+ gradle.on("close", (code) => {
716
+ if (code !== 0) {
717
+ console.error(pc3.red(` \u2717 Gradle build failed (exit code ${code})`));
718
+ process.exit(1);
719
+ }
720
+ console.log(pc3.green(" \u2713 Build successful\n"));
721
+ const apkPath = findApkPath(androidDir);
722
+ if (!apkPath) {
723
+ console.log(pc3.yellow(" Could not locate debug APK."));
724
+ console.log(pc3.dim(" Expected at android/app/build/outputs/apk/debug/\n"));
725
+ return;
726
+ }
727
+ console.log(pc3.white(" Installing APK on device/emulator..."));
728
+ try {
729
+ execSync(`adb install -r "${apkPath}"`, { stdio: "pipe" });
730
+ console.log(pc3.green(" \u2713 APK installed"));
731
+ } catch (err) {
732
+ console.error(pc3.red(` \u2717 Failed to install APK: ${err.message}`));
733
+ console.log(pc3.dim(" Make sure an emulator is running or a device is connected (adb devices).\n"));
734
+ process.exit(1);
735
+ }
736
+ const componentName = `${options.package}/${options.activity}`;
737
+ console.log(pc3.white(` Launching ${componentName}...`));
738
+ try {
739
+ execSync(`adb shell am start -n "${componentName}"`, { stdio: "pipe" });
740
+ console.log(pc3.green(` \u2713 App launched
741
+ `));
742
+ } catch (err) {
743
+ console.error(pc3.red(` \u2717 Failed to launch app: ${err.message}`));
744
+ process.exit(1);
745
+ }
746
+ });
747
+ }
748
+
749
+ // src/cli.ts
750
+ program.name("vue-native").description("Vue Native \u2014 build native iOS and Android apps with Vue.js").version("0.1.0");
751
+ program.addCommand(createCommand);
752
+ program.addCommand(devCommand);
753
+ program.addCommand(runCommand);
754
+ program.parse(process.argv);
package/package.json ADDED
@@ -0,0 +1,41 @@
1
+ {
2
+ "name": "@thelacanians/vue-native-cli",
3
+ "version": "0.1.0",
4
+ "description": "CLI for creating and running Vue Native apps",
5
+ "license": "MIT",
6
+ "author": "Vue Native Contributors",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "https://github.com/abdul-hamid-achik/vue-native",
10
+ "directory": "packages/cli"
11
+ },
12
+ "publishConfig": {
13
+ "access": "public"
14
+ },
15
+ "type": "module",
16
+ "bin": {
17
+ "vue-native": "./dist/cli.js"
18
+ },
19
+ "exports": {
20
+ ".": "./dist/cli.js"
21
+ },
22
+ "files": ["dist"],
23
+ "scripts": {
24
+ "build": "tsup",
25
+ "dev": "tsup --watch",
26
+ "typecheck": "tsc --noEmit",
27
+ "clean": "rm -rf dist"
28
+ },
29
+ "dependencies": {
30
+ "commander": "^12.1.0",
31
+ "ws": "^8.18.0",
32
+ "chokidar": "^3.6.0",
33
+ "picocolors": "^1.1.0"
34
+ },
35
+ "devDependencies": {
36
+ "@types/ws": "^8.5.13",
37
+ "@types/node": "^22.0.0",
38
+ "tsup": "^8.4.0",
39
+ "typescript": "^5.7.0"
40
+ }
41
+ }