@thelacanians/vue-native-cli 0.1.2 → 0.3.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
@@ -9,10 +9,15 @@ import { Command } from "commander";
9
9
  import { mkdir, writeFile } from "fs/promises";
10
10
  import { join } from "path";
11
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) => {
12
+ var createCommand = new Command("create").description("Create a new Vue Native project").argument("<name>", "project name").option("-t, --template <template>", "project template (blank, tabs, drawer)", "blank").action(async (name, options) => {
13
+ const template = options.template;
14
+ if (!["blank", "tabs", "drawer"].includes(template)) {
15
+ console.error(pc.red(`Invalid template "${template}". Choose: blank, tabs, drawer`));
16
+ process.exit(1);
17
+ }
13
18
  const dir = join(process.cwd(), name);
14
19
  console.log(pc.cyan(`
15
- Creating Vue Native project: ${pc.bold(name)}
20
+ Creating Vue Native project: ${pc.bold(name)} (template: ${template})
16
21
  `));
17
22
  try {
18
23
  await mkdir(dir, { recursive: true });
@@ -29,12 +34,12 @@ Creating Vue Native project: ${pc.bold(name)}
29
34
  typecheck: "tsc --noEmit"
30
35
  },
31
36
  dependencies: {
32
- "@thelacanians/vue-native-runtime": "^0.1.0",
33
- "@thelacanians/vue-native-navigation": "^0.1.0",
37
+ "@thelacanians/vue-native-runtime": "^0.3.0",
38
+ "@thelacanians/vue-native-navigation": "^0.3.0",
34
39
  "vue": "^3.5.0"
35
40
  },
36
41
  devDependencies: {
37
- "@thelacanians/vue-native-vite-plugin": "^0.1.0",
42
+ "@thelacanians/vue-native-vite-plugin": "^0.3.0",
38
43
  "@vitejs/plugin-vue": "^5.0.0",
39
44
  "vite": "^6.1.0",
40
45
  "typescript": "^5.7.0"
@@ -60,79 +65,7 @@ export default defineConfig({
60
65
  },
61
66
  include: ["app/**/*"]
62
67
  }, 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
- `);
68
+ await generateTemplateFiles(dir, name, template);
136
69
  const iosDir = join(dir, "ios");
137
70
  const iosSrcDir = join(iosDir, "Sources");
138
71
  await mkdir(iosSrcDir, { recursive: true });
@@ -388,42 +321,487 @@ android.useAndroidX=true
388
321
  kotlin.code.style=official
389
322
  android.nonTransitiveRClass=true
390
323
  `);
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"));
324
+ await writeFile(join(dir, "vue-native.config.ts"), `import { defineConfig } from '@thelacanians/vue-native-cli'
325
+
326
+ export default defineConfig({
327
+ name: '${name}',
328
+ bundleId: '${bundleId}',
329
+ version: '1.0.0',
330
+ ios: {
331
+ deploymentTarget: '16.0',
332
+ },
333
+ android: {
334
+ minSdk: 21,
335
+ targetSdk: 34,
336
+ },
337
+ })
338
+ `);
339
+ await writeFile(join(dir, "env.d.ts"), `/// <reference types="vite/client" />
340
+ declare module '*.vue' {
341
+ import type { DefineComponent } from 'vue'
342
+ const component: DefineComponent<{}, {}, any>
343
+ export default component
344
+ }
345
+ declare const __DEV__: boolean
346
+ `);
347
+ await writeFile(join(dir, ".gitignore"), `node_modules/
348
+ dist/
349
+ *.xcuserstate
350
+ *.xcuserdatad/
351
+ DerivedData/
352
+ .build/
353
+ build/
354
+ .gradle/
355
+ local.properties
356
+ *.apk
357
+ *.aab
358
+ .DS_Store
359
+ `);
360
+ console.log(pc.green(" Project created successfully!\n"));
361
+ console.log(pc.white(" Next steps:\n"));
362
+ console.log(pc.white(` cd ${name}`));
363
+ console.log(pc.white(" bun install"));
364
+ console.log(pc.white(" vue-native dev\n"));
365
+ console.log(pc.white(" To run on iOS:"));
366
+ console.log(pc.white(" vue-native run ios\n"));
367
+ console.log(pc.white(" To run on Android:"));
368
+ console.log(pc.white(" vue-native run android\n"));
400
369
  } catch (err) {
401
370
  console.error(pc.red(`Error creating project: ${err.message}`));
402
371
  process.exit(1);
403
372
  }
404
373
  });
374
+ async function generateTemplateFiles(dir, name, template) {
375
+ const pagesDir = join(dir, "app", "pages");
376
+ if (template === "blank") {
377
+ await generateBlankTemplate(dir, pagesDir);
378
+ } else if (template === "tabs") {
379
+ await generateTabsTemplate(dir, pagesDir);
380
+ } else if (template === "drawer") {
381
+ await generateDrawerTemplate(dir, pagesDir);
382
+ }
383
+ }
384
+ async function generateBlankTemplate(dir, pagesDir) {
385
+ await writeFile(join(dir, "app", "main.ts"), `import { createApp } from 'vue'
386
+ import { createRouter } from '@thelacanians/vue-native-navigation'
387
+ import App from './App.vue'
388
+ import Home from './pages/Home.vue'
389
+
390
+ const router = createRouter([
391
+ { name: 'Home', component: Home },
392
+ ])
393
+
394
+ const app = createApp(App)
395
+ app.use(router)
396
+ app.start()
397
+ `);
398
+ await writeFile(join(dir, "app", "App.vue"), `<template>
399
+ <VSafeArea :style="{ flex: 1, backgroundColor: '#ffffff' }">
400
+ <RouterView />
401
+ </VSafeArea>
402
+ </template>
403
+
404
+ <script setup lang="ts">
405
+ import { RouterView } from '@thelacanians/vue-native-navigation'
406
+ </script>
407
+ `);
408
+ await writeFile(join(pagesDir, "Home.vue"), `<script setup lang="ts">
409
+ import { ref } from 'vue'
410
+ import { createStyleSheet } from '@thelacanians/vue-native-runtime'
411
+
412
+ const count = ref(0)
413
+
414
+ const styles = createStyleSheet({
415
+ container: {
416
+ flex: 1,
417
+ justifyContent: 'center',
418
+ alignItems: 'center',
419
+ padding: 24,
420
+ },
421
+ title: {
422
+ fontSize: 28,
423
+ fontWeight: 'bold',
424
+ color: '#1a1a1a',
425
+ marginBottom: 8,
426
+ textAlign: 'center',
427
+ },
428
+ subtitle: {
429
+ fontSize: 16,
430
+ color: '#666',
431
+ textAlign: 'center',
432
+ marginBottom: 32,
433
+ },
434
+ button: {
435
+ backgroundColor: '#4f46e5',
436
+ paddingHorizontal: 32,
437
+ paddingVertical: 14,
438
+ borderRadius: 12,
439
+ },
440
+ buttonText: {
441
+ color: '#ffffff',
442
+ fontSize: 18,
443
+ fontWeight: '600',
444
+ },
445
+ })
446
+ </script>
447
+
448
+ <template>
449
+ <VView :style="styles.container">
450
+ <VText :style="styles.title">Hello, Vue Native!</VText>
451
+ <VText :style="styles.subtitle">Edit app/pages/Home.vue to get started.</VText>
452
+ <VButton :style="styles.button" :onPress="() => count++">
453
+ <VText :style="styles.buttonText">Count: {{ count }}</VText>
454
+ </VButton>
455
+ </VView>
456
+ </template>
457
+ `);
458
+ }
459
+ async function generateTabsTemplate(dir, pagesDir) {
460
+ await writeFile(join(dir, "app", "main.ts"), `import { createApp } from 'vue'
461
+ import App from './App.vue'
462
+
463
+ const app = createApp(App)
464
+ app.start()
465
+ `);
466
+ await writeFile(join(dir, "app", "App.vue"), `<script setup lang="ts">
467
+ import { createTabNavigator } from '@thelacanians/vue-native-navigation'
468
+ import Home from './pages/Home.vue'
469
+ import Settings from './pages/Settings.vue'
470
+
471
+ const { TabNavigator } = createTabNavigator()
472
+ </script>
473
+
474
+ <template>
475
+ <VSafeArea :style="{ flex: 1, backgroundColor: '#ffffff' }">
476
+ <TabNavigator
477
+ :screens="[
478
+ { name: 'home', label: 'Home', icon: 'H', component: Home },
479
+ { name: 'settings', label: 'Settings', icon: 'S', component: Settings },
480
+ ]"
481
+ />
482
+ </VSafeArea>
483
+ </template>
484
+ `);
485
+ await writeFile(join(pagesDir, "Home.vue"), `<script setup lang="ts">
486
+ import { ref } from 'vue'
487
+ import { createStyleSheet } from '@thelacanians/vue-native-runtime'
488
+
489
+ const count = ref(0)
490
+
491
+ const styles = createStyleSheet({
492
+ container: {
493
+ flex: 1,
494
+ justifyContent: 'center',
495
+ alignItems: 'center',
496
+ padding: 24,
497
+ },
498
+ title: {
499
+ fontSize: 28,
500
+ fontWeight: 'bold',
501
+ color: '#1a1a1a',
502
+ marginBottom: 16,
503
+ },
504
+ button: {
505
+ backgroundColor: '#4f46e5',
506
+ paddingHorizontal: 32,
507
+ paddingVertical: 14,
508
+ borderRadius: 12,
509
+ marginTop: 16,
510
+ },
511
+ buttonText: {
512
+ color: '#ffffff',
513
+ fontSize: 18,
514
+ fontWeight: '600',
515
+ },
516
+ })
517
+ </script>
518
+
519
+ <template>
520
+ <VView :style="styles.container">
521
+ <VText :style="styles.title">Home</VText>
522
+ <VText>Count: {{ count }}</VText>
523
+ <VButton :style="styles.button" :onPress="() => count++">
524
+ <VText :style="styles.buttonText">Increment</VText>
525
+ </VButton>
526
+ </VView>
527
+ </template>
528
+ `);
529
+ await writeFile(join(pagesDir, "Settings.vue"), `<script setup lang="ts">
530
+ import { ref } from 'vue'
531
+ import { createStyleSheet } from '@thelacanians/vue-native-runtime'
532
+
533
+ const darkMode = ref(false)
534
+
535
+ const styles = createStyleSheet({
536
+ container: {
537
+ flex: 1,
538
+ padding: 24,
539
+ },
540
+ title: {
541
+ fontSize: 28,
542
+ fontWeight: 'bold',
543
+ color: '#1a1a1a',
544
+ marginBottom: 24,
545
+ },
546
+ row: {
547
+ flexDirection: 'row',
548
+ justifyContent: 'space-between',
549
+ alignItems: 'center',
550
+ paddingVertical: 12,
551
+ borderBottomWidth: 1,
552
+ borderBottomColor: '#e0e0e0',
553
+ },
554
+ label: {
555
+ fontSize: 16,
556
+ color: '#333',
557
+ },
558
+ })
559
+ </script>
560
+
561
+ <template>
562
+ <VView :style="styles.container">
563
+ <VText :style="styles.title">Settings</VText>
564
+ <VView :style="styles.row">
565
+ <VText :style="styles.label">Dark Mode</VText>
566
+ <VSwitch v-model="darkMode" />
567
+ </VView>
568
+ </VView>
569
+ </template>
570
+ `);
571
+ }
572
+ async function generateDrawerTemplate(dir, pagesDir) {
573
+ await writeFile(join(dir, "app", "main.ts"), `import { createApp } from 'vue'
574
+ import App from './App.vue'
575
+
576
+ const app = createApp(App)
577
+ app.start()
578
+ `);
579
+ await writeFile(join(dir, "app", "App.vue"), `<script setup lang="ts">
580
+ import { createDrawerNavigator } from '@thelacanians/vue-native-navigation'
581
+ import Home from './pages/Home.vue'
582
+ import About from './pages/About.vue'
583
+
584
+ const { DrawerNavigator } = createDrawerNavigator()
585
+ </script>
586
+
587
+ <template>
588
+ <VSafeArea :style="{ flex: 1, backgroundColor: '#ffffff' }">
589
+ <DrawerNavigator
590
+ :screens="[
591
+ { name: 'home', label: 'Home', icon: 'H', component: Home },
592
+ { name: 'about', label: 'About', icon: 'A', component: About },
593
+ ]"
594
+ />
595
+ </VSafeArea>
596
+ </template>
597
+ `);
598
+ await writeFile(join(pagesDir, "Home.vue"), `<script setup lang="ts">
599
+ import { createStyleSheet } from '@thelacanians/vue-native-runtime'
600
+ import { useDrawer } from '@thelacanians/vue-native-navigation'
601
+
602
+ const { toggleDrawer } = useDrawer()
603
+
604
+ const styles = createStyleSheet({
605
+ container: {
606
+ flex: 1,
607
+ padding: 24,
608
+ },
609
+ header: {
610
+ flexDirection: 'row',
611
+ alignItems: 'center',
612
+ marginBottom: 24,
613
+ },
614
+ menuButton: {
615
+ backgroundColor: '#f0f0f0',
616
+ paddingHorizontal: 12,
617
+ paddingVertical: 8,
618
+ borderRadius: 8,
619
+ marginRight: 16,
620
+ },
621
+ menuText: {
622
+ fontSize: 18,
623
+ },
624
+ title: {
625
+ fontSize: 24,
626
+ fontWeight: 'bold',
627
+ color: '#1a1a1a',
628
+ },
629
+ body: {
630
+ fontSize: 16,
631
+ color: '#666',
632
+ lineHeight: 24,
633
+ },
634
+ })
635
+ </script>
636
+
637
+ <template>
638
+ <VView :style="styles.container">
639
+ <VView :style="styles.header">
640
+ <VButton :style="styles.menuButton" :onPress="toggleDrawer">
641
+ <VText :style="styles.menuText">Menu</VText>
642
+ </VButton>
643
+ <VText :style="styles.title">Home</VText>
644
+ </VView>
645
+ <VText :style="styles.body">
646
+ Swipe from the left or tap Menu to open the drawer.
647
+ </VText>
648
+ </VView>
649
+ </template>
650
+ `);
651
+ await writeFile(join(pagesDir, "About.vue"), `<script setup lang="ts">
652
+ import { createStyleSheet } from '@thelacanians/vue-native-runtime'
653
+ import { useDrawer } from '@thelacanians/vue-native-navigation'
654
+
655
+ const { toggleDrawer } = useDrawer()
656
+
657
+ const styles = createStyleSheet({
658
+ container: {
659
+ flex: 1,
660
+ padding: 24,
661
+ },
662
+ header: {
663
+ flexDirection: 'row',
664
+ alignItems: 'center',
665
+ marginBottom: 24,
666
+ },
667
+ menuButton: {
668
+ backgroundColor: '#f0f0f0',
669
+ paddingHorizontal: 12,
670
+ paddingVertical: 8,
671
+ borderRadius: 8,
672
+ marginRight: 16,
673
+ },
674
+ menuText: {
675
+ fontSize: 18,
676
+ },
677
+ title: {
678
+ fontSize: 24,
679
+ fontWeight: 'bold',
680
+ color: '#1a1a1a',
681
+ },
682
+ body: {
683
+ fontSize: 16,
684
+ color: '#666',
685
+ lineHeight: 24,
686
+ },
687
+ })
688
+ </script>
689
+
690
+ <template>
691
+ <VView :style="styles.container">
692
+ <VView :style="styles.header">
693
+ <VButton :style="styles.menuButton" :onPress="toggleDrawer">
694
+ <VText :style="styles.menuText">Menu</VText>
695
+ </VButton>
696
+ <VText :style="styles.title">About</VText>
697
+ </VView>
698
+ <VText :style="styles.body">
699
+ Built with Vue Native.
700
+ </VText>
701
+ </VView>
702
+ </template>
703
+ `);
704
+ }
405
705
 
406
706
  // src/commands/dev.ts
407
707
  import { Command as Command2 } from "commander";
408
- import { spawn } from "child_process";
708
+ import { spawn, execSync } from "child_process";
409
709
  import { readFile } from "fs/promises";
710
+ import { existsSync } from "fs";
410
711
  import { join as join2 } from "path";
411
712
  import { watch } from "chokidar";
412
713
  import { WebSocketServer, WebSocket } from "ws";
413
714
  import pc2 from "picocolors";
414
715
  var DEFAULT_PORT = 8174;
415
716
  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) => {
717
+ function detectIOSSimulators() {
718
+ try {
719
+ const output = execSync("xcrun simctl list devices available -j", {
720
+ stdio: "pipe",
721
+ encoding: "utf8"
722
+ });
723
+ const data = JSON.parse(output);
724
+ const simulators = [];
725
+ for (const [runtime, devices] of Object.entries(data.devices ?? {})) {
726
+ if (!runtime.includes("iOS")) continue;
727
+ for (const device of devices) {
728
+ if (device.isAvailable !== false) {
729
+ simulators.push({
730
+ name: device.name,
731
+ udid: device.udid,
732
+ state: device.state
733
+ });
734
+ }
735
+ }
736
+ }
737
+ return simulators;
738
+ } catch {
739
+ return [];
740
+ }
741
+ }
742
+ function bootSimulator(udid) {
743
+ try {
744
+ execSync(`xcrun simctl boot "${udid}"`, { stdio: "pipe" });
745
+ } catch {
746
+ }
747
+ try {
748
+ execSync("open -a Simulator", { stdio: "pipe" });
749
+ } catch {
750
+ }
751
+ }
752
+ function detectAndroidEmulators() {
753
+ try {
754
+ const output = execSync("adb devices", { stdio: "pipe", encoding: "utf8" });
755
+ const lines = output.split("\n").filter((l) => l.includes("device") && !l.startsWith("List"));
756
+ return lines.map((l) => l.split(" ")[0]).filter(Boolean);
757
+ } catch {
758
+ return [];
759
+ }
760
+ }
761
+ 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)).option("--ios", "auto-detect and launch iOS Simulator").option("--android", "auto-detect Android emulator").option("--simulator <name>", "specify iOS Simulator name").action(async (options) => {
417
762
  const port = parseInt(options.port, 10);
418
763
  const cwd = process.cwd();
419
764
  const bundlePath = join2(cwd, BUNDLE_FILE);
420
- console.log(pc2.cyan("\n\u26A1 Vue Native Dev Server\n"));
765
+ console.log(pc2.cyan("\n Vue Native Dev Server\n"));
766
+ if (options.ios) {
767
+ console.log(pc2.white(" Detecting iOS Simulators..."));
768
+ const simulators = detectIOSSimulators();
769
+ if (simulators.length === 0) {
770
+ console.log(pc2.yellow(" No iOS Simulators found. Install Xcode and create a simulator."));
771
+ } else {
772
+ let target = simulators.find((s) => s.state === "Booted");
773
+ if (!target && options.simulator) {
774
+ target = simulators.find((s) => s.name === options.simulator);
775
+ }
776
+ if (!target) {
777
+ target = simulators.find((s) => s.name.includes("iPhone")) ?? simulators[0];
778
+ }
779
+ if (target) {
780
+ if (target.state !== "Booted") {
781
+ console.log(pc2.white(` Booting ${target.name}...`));
782
+ bootSimulator(target.udid);
783
+ }
784
+ console.log(pc2.green(` iOS Simulator ready: ${target.name}`));
785
+ }
786
+ }
787
+ console.log();
788
+ }
789
+ if (options.android) {
790
+ console.log(pc2.white(" Detecting Android emulators..."));
791
+ const emulators = detectAndroidEmulators();
792
+ if (emulators.length === 0) {
793
+ console.log(pc2.yellow(" No Android emulators detected. Start one via Android Studio or `emulator -avd <name>`."));
794
+ } else {
795
+ console.log(pc2.green(` Android emulator(s) connected: ${emulators.join(", ")}`));
796
+ }
797
+ console.log();
798
+ }
421
799
  const wss = new WebSocketServer({ port });
422
800
  const clients = /* @__PURE__ */ new Set();
423
801
  wss.on("connection", (ws) => {
424
802
  clients.add(ws);
425
803
  ws.send(JSON.stringify({ type: "connected" }));
426
- console.log(pc2.green(` iOS client connected (${clients.size} total)`));
804
+ console.log(pc2.green(` Client connected (${clients.size} total)`));
427
805
  readFile(bundlePath, "utf8").then((bundle) => {
428
806
  if (ws.readyState === WebSocket.OPEN) {
429
807
  ws.send(JSON.stringify({ type: "bundle", bundle }));
@@ -433,7 +811,7 @@ var devCommand = new Command2("dev").description("Start the Vue Native dev serve
433
811
  });
434
812
  ws.on("close", () => {
435
813
  clients.delete(ws);
436
- console.log(pc2.dim(` iOS client disconnected (${clients.size} remaining)`));
814
+ console.log(pc2.dim(` Client disconnected (${clients.size} remaining)`));
437
815
  });
438
816
  ws.on("message", (data) => {
439
817
  try {
@@ -447,7 +825,15 @@ var devCommand = new Command2("dev").description("Start the Vue Native dev serve
447
825
  console.error(pc2.red(`WebSocket server error: ${err.message}`));
448
826
  });
449
827
  console.log(pc2.white(` Hot reload server: ${pc2.bold(`ws://localhost:${port}`)}`));
450
- console.log(pc2.dim(" Waiting for iOS app to connect...\n"));
828
+ const iosDir = join2(cwd, "ios");
829
+ const androidDir = join2(cwd, "android");
830
+ if (existsSync(iosDir)) {
831
+ console.log(pc2.dim(` iOS app should connect to ws://localhost:${port}`));
832
+ }
833
+ if (existsSync(androidDir)) {
834
+ console.log(pc2.dim(` Android emulator should connect to ws://10.0.2.2:${port}`));
835
+ }
836
+ console.log(pc2.dim(" Waiting for app to connect...\n"));
451
837
  console.log(pc2.white(" Starting Vite build watcher...\n"));
452
838
  const vite = spawn(
453
839
  "bun",
@@ -483,8 +869,8 @@ var devCommand = new Command2("dev").description("Start the Vue Native dev serve
483
869
  sent++;
484
870
  }
485
871
  }
486
- console.log(pc2.green(` \u2713 Bundle updated (${Math.round(bundle.length / 1024)}KB) \u2192 sent to ${sent} client(s)`));
487
- } catch (err) {
872
+ console.log(pc2.green(` Bundle updated (${Math.round(bundle.length / 1024)}KB) -> sent to ${sent} client(s)`));
873
+ } catch {
488
874
  }
489
875
  }
490
876
  setInterval(() => {
@@ -504,16 +890,16 @@ var devCommand = new Command2("dev").description("Start the Vue Native dev serve
504
890
 
505
891
  // src/commands/run.ts
506
892
  import { Command as Command3 } from "commander";
507
- import { spawn as spawn2, execSync } from "child_process";
508
- import { existsSync, readdirSync, readFileSync } from "fs";
893
+ import { spawn as spawn2, execSync as execSync2 } from "child_process";
894
+ import { existsSync as existsSync2, readdirSync, readFileSync } from "fs";
509
895
  import { join as join3 } from "path";
510
896
  import pc3 from "picocolors";
511
- function findAppPath(buildDir) {
897
+ function findAppPath(_buildDir) {
512
898
  const derivedDataBase = join3(
513
899
  process.env.HOME || "~",
514
900
  "Library/Developer/Xcode/DerivedData"
515
901
  );
516
- if (existsSync(derivedDataBase)) {
902
+ if (existsSync2(derivedDataBase)) {
517
903
  try {
518
904
  const projects = readdirSync(derivedDataBase);
519
905
  for (const project of projects.reverse()) {
@@ -522,7 +908,7 @@ function findAppPath(buildDir) {
522
908
  project,
523
909
  "Build/Products/Debug-iphonesimulator"
524
910
  );
525
- if (existsSync(productsDir)) {
911
+ if (existsSync2(productsDir)) {
526
912
  const entries = readdirSync(productsDir);
527
913
  const app = entries.find((e) => e.endsWith(".app"));
528
914
  if (app) {
@@ -537,7 +923,7 @@ function findAppPath(buildDir) {
537
923
  }
538
924
  function readBundleId(iosDir) {
539
925
  const plistPath = join3(iosDir, "Sources", "Info.plist");
540
- if (existsSync(plistPath)) {
926
+ if (existsSync2(plistPath)) {
541
927
  try {
542
928
  const content = readFileSync(plistPath, "utf8");
543
929
  const match = content.match(
@@ -553,7 +939,7 @@ function readBundleId(iosDir) {
553
939
  }
554
940
  function findApkPath(androidDir) {
555
941
  const apkDir = join3(androidDir, "app", "build", "outputs", "apk", "debug");
556
- if (existsSync(apkDir)) {
942
+ if (existsSync2(apkDir)) {
557
943
  try {
558
944
  const entries = readdirSync(apkDir);
559
945
  const apk = entries.find((e) => e.endsWith(".apk") && !e.includes("androidTest"));
@@ -576,7 +962,7 @@ var runCommand = new Command3("run").description("Build and run the app").argume
576
962
  `));
577
963
  console.log(pc3.white(" Building JS bundle..."));
578
964
  try {
579
- execSync("bun run vite build", { cwd, stdio: "inherit" });
965
+ execSync2("bun run vite build", { cwd, stdio: "inherit" });
580
966
  console.log(pc3.green(" \u2713 Bundle built\n"));
581
967
  } catch {
582
968
  console.error(pc3.red(" \u2717 Bundle build failed"));
@@ -591,7 +977,7 @@ var runCommand = new Command3("run").description("Build and run the app").argume
591
977
  function runIOS(cwd, options) {
592
978
  let xcodeProject = null;
593
979
  const iosDir = join3(cwd, "ios");
594
- if (existsSync(iosDir)) {
980
+ if (existsSync2(iosDir)) {
595
981
  for (const ext of [".xcworkspace", ".xcodeproj"]) {
596
982
  try {
597
983
  const entries = readdirSync(iosDir);
@@ -644,18 +1030,18 @@ function runIOS(cwd, options) {
644
1030
  const bundleId = options.bundleId || readBundleId(join3(cwd, "ios"));
645
1031
  console.log(pc3.white(` Booting simulator "${simulatorName}"...`));
646
1032
  try {
647
- execSync(`xcrun simctl boot "${simulatorName}"`, { stdio: "pipe" });
1033
+ execSync2(`xcrun simctl boot "${simulatorName}"`, { stdio: "pipe" });
648
1034
  } catch {
649
1035
  }
650
1036
  try {
651
- execSync("open -a Simulator", { stdio: "pipe" });
1037
+ execSync2("open -a Simulator", { stdio: "pipe" });
652
1038
  } catch {
653
1039
  }
654
1040
  const appPath = findAppPath(join3(cwd, "ios"));
655
1041
  if (appPath) {
656
1042
  console.log(pc3.white(` Installing app on simulator...`));
657
1043
  try {
658
- execSync(`xcrun simctl install booted "${appPath}"`, { stdio: "pipe" });
1044
+ execSync2(`xcrun simctl install booted "${appPath}"`, { stdio: "pipe" });
659
1045
  console.log(pc3.green(" \u2713 App installed"));
660
1046
  } catch (err) {
661
1047
  console.error(pc3.red(` \u2717 Failed to install app: ${err.message}`));
@@ -663,7 +1049,7 @@ function runIOS(cwd, options) {
663
1049
  }
664
1050
  console.log(pc3.white(` Launching ${bundleId}...`));
665
1051
  try {
666
- execSync(`xcrun simctl launch booted "${bundleId}"`, { stdio: "pipe" });
1052
+ execSync2(`xcrun simctl launch booted "${bundleId}"`, { stdio: "pipe" });
667
1053
  console.log(pc3.green(` \u2713 App launched on ${simulatorName}
668
1054
  `));
669
1055
  } catch (err) {
@@ -678,14 +1064,14 @@ function runIOS(cwd, options) {
678
1064
  }
679
1065
  function runAndroid(cwd, options) {
680
1066
  const androidDir = join3(cwd, "android");
681
- if (!existsSync(androidDir)) {
1067
+ if (!existsSync2(androidDir)) {
682
1068
  console.log(pc3.yellow(" No android/ directory found."));
683
1069
  console.log(pc3.dim(" To add Android support, create an Android project in the android/ directory."));
684
1070
  console.log(pc3.dim(" Bundle has been built to dist/vue-native-bundle.js\n"));
685
1071
  return;
686
1072
  }
687
1073
  const gradlew = join3(androidDir, "gradlew");
688
- if (!existsSync(gradlew)) {
1074
+ if (!existsSync2(gradlew)) {
689
1075
  console.error(pc3.red(" \u2717 gradlew not found in android/ directory"));
690
1076
  console.log(pc3.dim(" Make sure your Android project has the Gradle wrapper.\n"));
691
1077
  process.exit(1);
@@ -726,7 +1112,7 @@ function runAndroid(cwd, options) {
726
1112
  }
727
1113
  console.log(pc3.white(" Installing APK on device/emulator..."));
728
1114
  try {
729
- execSync(`adb install -r "${apkPath}"`, { stdio: "pipe" });
1115
+ execSync2(`adb install -r "${apkPath}"`, { stdio: "pipe" });
730
1116
  console.log(pc3.green(" \u2713 APK installed"));
731
1117
  } catch (err) {
732
1118
  console.error(pc3.red(` \u2717 Failed to install APK: ${err.message}`));
@@ -736,7 +1122,7 @@ function runAndroid(cwd, options) {
736
1122
  const componentName = `${options.package}/${options.activity}`;
737
1123
  console.log(pc3.white(` Launching ${componentName}...`));
738
1124
  try {
739
- execSync(`adb shell am start -n "${componentName}"`, { stdio: "pipe" });
1125
+ execSync2(`adb shell am start -n "${componentName}"`, { stdio: "pipe" });
740
1126
  console.log(pc3.green(` \u2713 App launched
741
1127
  `));
742
1128
  } catch (err) {
@@ -0,0 +1,46 @@
1
+ interface VueNativeConfig {
2
+ /** Display name for the app. */
3
+ name: string;
4
+ /** Bundle identifier (e.g. com.example.myapp). */
5
+ bundleId: string;
6
+ /** App version string (semver). */
7
+ version: string;
8
+ /** iOS-specific configuration. */
9
+ ios?: {
10
+ /** Minimum iOS deployment target. Default: "16.0". */
11
+ deploymentTarget?: string;
12
+ /** Xcode scheme name (auto-derived from name if omitted). */
13
+ scheme?: string;
14
+ };
15
+ /** Android-specific configuration. */
16
+ android?: {
17
+ /** Minimum Android SDK version. Default: 21. */
18
+ minSdk?: number;
19
+ /** Target Android SDK version. Default: 34. */
20
+ targetSdk?: number;
21
+ /** Android package name (defaults to bundleId). */
22
+ packageName?: string;
23
+ };
24
+ /** List of Vue Native plugins to include. */
25
+ plugins?: string[];
26
+ }
27
+ interface ResolvedConfig extends VueNativeConfig {
28
+ ios: {
29
+ deploymentTarget: string;
30
+ scheme: string;
31
+ };
32
+ android: {
33
+ minSdk: number;
34
+ targetSdk: number;
35
+ packageName: string;
36
+ };
37
+ plugins: string[];
38
+ }
39
+ declare function defineConfig(config: VueNativeConfig): VueNativeConfig;
40
+ /**
41
+ * Load and resolve the vue-native.config.{ts,js,mjs} file from the project root.
42
+ * Returns null if no config file is found.
43
+ */
44
+ declare function loadConfig(cwd: string): Promise<ResolvedConfig | null>;
45
+
46
+ export { type ResolvedConfig, type VueNativeConfig, defineConfig, loadConfig };
package/dist/config.js ADDED
@@ -0,0 +1,72 @@
1
+ // src/config.ts
2
+ import { existsSync } from "fs";
3
+ import { join } from "path";
4
+ import { pathToFileURL } from "url";
5
+ import pc from "picocolors";
6
+ function defineConfig(config) {
7
+ return config;
8
+ }
9
+ function validateConfig(config) {
10
+ if (typeof config !== "object" || config === null) return false;
11
+ const c = config;
12
+ if (typeof c.name !== "string" || c.name.length === 0) {
13
+ console.error(pc.red(' Config error: "name" is required and must be a non-empty string.'));
14
+ return false;
15
+ }
16
+ if (typeof c.bundleId !== "string" || !/^[a-z][a-z0-9]*(\.[a-z][a-z0-9]*)+$/i.test(c.bundleId)) {
17
+ console.error(pc.red(' Config error: "bundleId" must be a valid reverse-domain identifier (e.g. com.example.myapp).'));
18
+ return false;
19
+ }
20
+ if (typeof c.version !== "string" || c.version.length === 0) {
21
+ console.error(pc.red(' Config error: "version" is required (e.g. "1.0.0").'));
22
+ return false;
23
+ }
24
+ return true;
25
+ }
26
+ var CONFIG_FILES = [
27
+ "vue-native.config.ts",
28
+ "vue-native.config.js",
29
+ "vue-native.config.mjs"
30
+ ];
31
+ async function loadConfig(cwd) {
32
+ let configPath = null;
33
+ for (const filename of CONFIG_FILES) {
34
+ const candidate = join(cwd, filename);
35
+ if (existsSync(candidate)) {
36
+ configPath = candidate;
37
+ break;
38
+ }
39
+ }
40
+ if (!configPath) return null;
41
+ try {
42
+ const mod = await import(pathToFileURL(configPath).href);
43
+ const raw = mod.default ?? mod;
44
+ if (!validateConfig(raw)) {
45
+ process.exit(1);
46
+ }
47
+ const config = raw;
48
+ const safeName = config.name.replace(/[^a-zA-Z0-9]/g, "");
49
+ const resolved = {
50
+ ...config,
51
+ ios: {
52
+ deploymentTarget: config.ios?.deploymentTarget ?? "16.0",
53
+ scheme: config.ios?.scheme ?? safeName
54
+ },
55
+ android: {
56
+ minSdk: config.android?.minSdk ?? 21,
57
+ targetSdk: config.android?.targetSdk ?? 34,
58
+ packageName: config.android?.packageName ?? config.bundleId
59
+ },
60
+ plugins: config.plugins ?? []
61
+ };
62
+ return resolved;
63
+ } catch (err) {
64
+ console.error(pc.red(` Failed to load config from ${configPath}:`));
65
+ console.error(pc.red(` ${err.message}`));
66
+ process.exit(1);
67
+ }
68
+ }
69
+ export {
70
+ defineConfig,
71
+ loadConfig
72
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@thelacanians/vue-native-cli",
3
- "version": "0.1.2",
3
+ "version": "0.3.0",
4
4
  "description": "CLI for creating and running Vue Native apps",
5
5
  "license": "MIT",
6
6
  "author": "Vue Native Contributors",
@@ -17,7 +17,8 @@
17
17
  "vue-native": "./dist/cli.js"
18
18
  },
19
19
  "exports": {
20
- ".": "./dist/cli.js"
20
+ ".": "./dist/config.js",
21
+ "./cli": "./dist/cli.js"
21
22
  },
22
23
  "files": ["dist", "README.md"],
23
24
  "scripts": {