@thelacanians/vue-native-cli 0.6.3 → 0.6.5

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
@@ -2,22 +2,39 @@
2
2
 
3
3
  // src/cli.ts
4
4
  import { program } from "commander";
5
+ import pc6 from "picocolors";
6
+ import { readFileSync as readFileSync3 } from "fs";
7
+ import { dirname as dirname2, join as join7 } from "path";
8
+ import { fileURLToPath as fileURLToPath2 } from "url";
5
9
 
6
10
  // src/commands/build.ts
7
11
  import { Command } from "commander";
8
12
  import { spawn, execSync } from "child_process";
9
- import { existsSync, readdirSync, mkdirSync, copyFileSync } from "fs";
10
- import { join, basename } from "path";
13
+ import { existsSync as existsSync2, readdirSync, mkdirSync, copyFileSync } from "fs";
14
+ import { join as join2, basename } from "path";
11
15
  import pc from "picocolors";
16
+
17
+ // src/config.ts
18
+ import { existsSync } from "fs";
19
+ import { join } from "path";
20
+ import { pathToFileURL } from "url";
21
+ var ConfigError = class extends Error {
22
+ constructor(message) {
23
+ super(message);
24
+ this.name = "ConfigError";
25
+ }
26
+ };
27
+
28
+ // src/commands/build.ts
12
29
  function findXcodeProject(iosDir) {
13
- if (!existsSync(iosDir)) return null;
30
+ if (!existsSync2(iosDir)) return null;
14
31
  for (const ext of [".xcworkspace", ".xcodeproj"]) {
15
32
  try {
16
33
  const entries = readdirSync(iosDir);
17
34
  const match = entries.find((e) => e.endsWith(ext));
18
35
  if (match) {
19
36
  return {
20
- path: join(iosDir, match),
37
+ path: join2(iosDir, match),
21
38
  isWorkspace: ext === ".xcworkspace"
22
39
  };
23
40
  }
@@ -27,13 +44,13 @@ function findXcodeProject(iosDir) {
27
44
  return null;
28
45
  }
29
46
  function findReleaseApk(androidDir) {
30
- const apkDir = join(androidDir, "app", "build", "outputs", "apk", "release");
31
- if (existsSync(apkDir)) {
47
+ const apkDir = join2(androidDir, "app", "build", "outputs", "apk", "release");
48
+ if (existsSync2(apkDir)) {
32
49
  try {
33
50
  const entries = readdirSync(apkDir);
34
51
  const apk = entries.find((e) => e.endsWith(".apk") && !e.includes("androidTest"));
35
52
  if (apk) {
36
- return join(apkDir, apk);
53
+ return join2(apkDir, apk);
37
54
  }
38
55
  } catch {
39
56
  }
@@ -41,13 +58,13 @@ function findReleaseApk(androidDir) {
41
58
  return null;
42
59
  }
43
60
  function findReleaseAab(androidDir) {
44
- const aabDir = join(androidDir, "app", "build", "outputs", "bundle", "release");
45
- if (existsSync(aabDir)) {
61
+ const aabDir = join2(androidDir, "app", "build", "outputs", "bundle", "release");
62
+ if (existsSync2(aabDir)) {
46
63
  try {
47
64
  const entries = readdirSync(aabDir);
48
65
  const aab = entries.find((e) => e.endsWith(".aab"));
49
66
  if (aab) {
50
- return join(aabDir, aab);
67
+ return join2(aabDir, aab);
51
68
  }
52
69
  } catch {
53
70
  }
@@ -55,17 +72,16 @@ function findReleaseAab(androidDir) {
55
72
  return null;
56
73
  }
57
74
  function ensureOutputDir(outputPath) {
58
- if (!existsSync(outputPath)) {
75
+ if (!existsSync2(outputPath)) {
59
76
  mkdirSync(outputPath, { recursive: true });
60
77
  }
61
78
  }
62
79
  var buildCommand = new Command("build").description("Create a release build of the app").argument("<platform>", "platform to build for (ios, android, macos)").option("--mode <mode>", "build mode", "release").option("--output <path>", "output directory for the build artifact", "./build").option("--scheme <scheme>", "Xcode scheme to build (iOS only)").option("--aab", "build Android App Bundle (.aab) instead of APK").action(async (platform, options) => {
63
80
  if (platform !== "ios" && platform !== "android" && platform !== "macos") {
64
- console.error(pc.red('Platform must be "ios", "android", or "macos"'));
65
- process.exit(1);
81
+ throw new ConfigError('Platform must be "ios", "android", or "macos"');
66
82
  }
67
83
  const cwd = process.cwd();
68
- const outputDir = join(cwd, options.output);
84
+ const outputDir = join2(cwd, options.output);
69
85
  const platformLabel = platform === "ios" ? "iOS" : platform === "android" ? "Android" : "macOS";
70
86
  console.log(pc.cyan(`
71
87
  Vue Native \u2014 ${options.mode.charAt(0).toUpperCase() + options.mode.slice(1)} Build (${platformLabel})
@@ -75,8 +91,7 @@ var buildCommand = new Command("build").description("Create a release build of t
75
91
  execSync("bun run vite build --mode production", { cwd, stdio: "inherit" });
76
92
  console.log(pc.green(" \u2713 Bundle built\n"));
77
93
  } catch {
78
- console.error(pc.red(" \u2717 Bundle build failed"));
79
- process.exit(1);
94
+ throw new ConfigError("Bundle build failed");
80
95
  }
81
96
  if (platform === "ios") {
82
97
  buildIOS(cwd, outputDir, options);
@@ -87,7 +102,7 @@ var buildCommand = new Command("build").description("Create a release build of t
87
102
  }
88
103
  });
89
104
  function buildIOS(cwd, outputDir, options) {
90
- const iosDir = join(cwd, "ios");
105
+ const iosDir = join2(cwd, "ios");
91
106
  const project = findXcodeProject(iosDir);
92
107
  if (!project) {
93
108
  console.log(pc.yellow(" No Xcode project found in ./ios/"));
@@ -98,7 +113,7 @@ function buildIOS(cwd, outputDir, options) {
98
113
  const projectFlag = project.isWorkspace ? "-workspace" : "-project";
99
114
  const scheme = options.scheme || project.path.split("/").pop()?.replace(/\.(xcworkspace|xcodeproj)$/, "") || "App";
100
115
  const configuration = options.mode === "release" ? "Release" : "Debug";
101
- const archivePath = join(outputDir, `${scheme}.xcarchive`);
116
+ const archivePath = join2(outputDir, `${scheme}.xcarchive`);
102
117
  ensureOutputDir(outputDir);
103
118
  console.log(pc.white(` Archiving ${scheme} (${configuration})...`));
104
119
  console.log(pc.dim(` Archive path: ${archivePath}`));
@@ -152,10 +167,10 @@ function buildIOS(cwd, outputDir, options) {
152
167
  xcodebuild.on("close", (code) => {
153
168
  if (code !== 0) {
154
169
  console.error(pc.red(` \u2717 Archive failed (exit code ${code})`));
155
- process.exit(1);
170
+ throw new ConfigError(`iOS archive failed with exit code ${code}`);
156
171
  }
157
172
  console.log(pc.green(" \u2713 Archive successful\n"));
158
- if (existsSync(archivePath)) {
173
+ if (existsSync2(archivePath)) {
159
174
  console.log(pc.green(` Archive: ${archivePath}`));
160
175
  console.log(pc.dim(" To export an IPA, open the archive in Xcode Organizer or run:"));
161
176
  console.log(pc.dim(` xcodebuild -exportArchive -archivePath "${archivePath}" -exportOptionsPlist ExportOptions.plist -exportPath "${outputDir}"
@@ -166,18 +181,18 @@ function buildIOS(cwd, outputDir, options) {
166
181
  });
167
182
  }
168
183
  function buildAndroid(cwd, outputDir, options) {
169
- const androidDir = join(cwd, "android");
170
- if (!existsSync(androidDir)) {
184
+ const androidDir = join2(cwd, "android");
185
+ if (!existsSync2(androidDir)) {
171
186
  console.log(pc.yellow(" No android/ directory found."));
172
187
  console.log(pc.dim(" To add Android support, create an Android project in the android/ directory."));
173
188
  console.log(pc.dim(" Bundle has been built to dist/vue-native-bundle.js\n"));
174
189
  return;
175
190
  }
176
- const gradlew = join(androidDir, "gradlew");
177
- if (!existsSync(gradlew)) {
178
- console.error(pc.red(" \u2717 gradlew not found in android/ directory"));
179
- console.log(pc.dim(" Make sure your Android project has the Gradle wrapper.\n"));
180
- process.exit(1);
191
+ const gradlew = join2(androidDir, "gradlew");
192
+ if (!existsSync2(gradlew)) {
193
+ throw new ConfigError(
194
+ "gradlew not found in android/ directory. Make sure your Android project has the Gradle wrapper."
195
+ );
181
196
  }
182
197
  const gradleTask = options.aab ? "bundleRelease" : "assembleRelease";
183
198
  const artifactType = options.aab ? "AAB" : "APK";
@@ -219,13 +234,13 @@ function buildAndroid(cwd, outputDir, options) {
219
234
  gradle.on("close", (code) => {
220
235
  if (code !== 0) {
221
236
  console.error(pc.red(` \u2717 Gradle build failed (exit code ${code})`));
222
- process.exit(1);
237
+ throw new ConfigError(`Android Gradle build failed with exit code ${code}`);
223
238
  }
224
239
  console.log(pc.green(" \u2713 Build successful\n"));
225
240
  if (options.aab) {
226
241
  const aabPath = findReleaseAab(androidDir);
227
242
  if (aabPath) {
228
- const destPath = join(outputDir, basename(aabPath));
243
+ const destPath = join2(outputDir, basename(aabPath));
229
244
  copyFileSync(aabPath, destPath);
230
245
  console.log(pc.green(` AAB copied to: ${destPath}`));
231
246
  console.log(pc.dim(" Upload this file to the Google Play Console.\n"));
@@ -236,7 +251,7 @@ function buildAndroid(cwd, outputDir, options) {
236
251
  } else {
237
252
  const apkPath = findReleaseApk(androidDir);
238
253
  if (apkPath) {
239
- const destPath = join(outputDir, basename(apkPath));
254
+ const destPath = join2(outputDir, basename(apkPath));
240
255
  copyFileSync(apkPath, destPath);
241
256
  console.log(pc.green(` APK copied to: ${destPath}`));
242
257
  console.log(pc.dim(' Install with: adb install -r "' + destPath + '"\n'));
@@ -248,7 +263,7 @@ function buildAndroid(cwd, outputDir, options) {
248
263
  });
249
264
  }
250
265
  function buildMacOS(cwd, outputDir, options) {
251
- const macosDir = join(cwd, "macos");
266
+ const macosDir = join2(cwd, "macos");
252
267
  const project = findXcodeProject(macosDir);
253
268
  if (!project) {
254
269
  console.log(pc.yellow(" No Xcode project found in ./macos/"));
@@ -259,7 +274,7 @@ function buildMacOS(cwd, outputDir, options) {
259
274
  const projectFlag = project.isWorkspace ? "-workspace" : "-project";
260
275
  const scheme = options.scheme || project.path.split("/").pop()?.replace(/\.(xcworkspace|xcodeproj)$/, "") || "App";
261
276
  const configuration = options.mode === "release" ? "Release" : "Debug";
262
- const archivePath = join(outputDir, `${scheme}.xcarchive`);
277
+ const archivePath = join2(outputDir, `${scheme}.xcarchive`);
263
278
  ensureOutputDir(outputDir);
264
279
  console.log(pc.white(` Archiving ${scheme} (${configuration}) for macOS...`));
265
280
  console.log(pc.dim(` Archive path: ${archivePath}`));
@@ -313,10 +328,10 @@ function buildMacOS(cwd, outputDir, options) {
313
328
  xcodebuild.on("close", (code) => {
314
329
  if (code !== 0) {
315
330
  console.error(pc.red(` \u2717 Archive failed (exit code ${code})`));
316
- process.exit(1);
331
+ throw new ConfigError(`macOS archive failed with exit code ${code}`);
317
332
  }
318
333
  console.log(pc.green(" \u2713 Archive successful\n"));
319
- if (existsSync(archivePath)) {
334
+ if (existsSync2(archivePath)) {
320
335
  console.log(pc.green(` Archive: ${archivePath}`));
321
336
  console.log(pc.dim(" To export a .app or .pkg, open the archive in Xcode Organizer or run:"));
322
337
  console.log(pc.dim(` xcodebuild -exportArchive -archivePath "${archivePath}" -exportOptionsPlist ExportOptions.plist -exportPath "${outputDir}"
@@ -330,29 +345,54 @@ function buildMacOS(cwd, outputDir, options) {
330
345
  // src/commands/create.ts
331
346
  import { Command as Command2 } from "commander";
332
347
  import { cp, mkdir, writeFile } from "fs/promises";
333
- import { join as join2, dirname } from "path";
348
+ import { join as join3, dirname } from "path";
334
349
  import { fileURLToPath } from "url";
335
- import { existsSync as existsSync2 } from "fs";
350
+ import { existsSync as existsSync3, readFileSync } from "fs";
336
351
  import pc2 from "picocolors";
337
- var VERSION = "0.6.2";
338
- var JS_PACKAGE_VERSION = "^0.6.2";
339
- var VITE_PLUGIN_VUE_VERSION = "^6.0.5";
340
- var VITE_VERSION = "^8.0.0";
352
+ function getTemplateVersions() {
353
+ try {
354
+ const cliDir2 = dirname(dirname(fileURLToPath(import.meta.url)));
355
+ const pkg2 = JSON.parse(readFileSync(join3(cliDir2, "package.json"), "utf8"));
356
+ const viteDevDep = pkg2.devDependencies?.vite;
357
+ const viteVuePluginDevDep = pkg2.devDependencies?.["@vitejs/plugin-vue"];
358
+ return {
359
+ NATIVE_VERSION: pkg2.version,
360
+ JS_PACKAGE_VERSION: `^${pkg2.version}`,
361
+ VITE_PLUGIN_VUE_VERSION: viteVuePluginDevDep ?? "^6.0.5",
362
+ VITE_VERSION: viteDevDep ?? "^8.0.0"
363
+ };
364
+ } catch {
365
+ return {
366
+ NATIVE_VERSION: "0.0.0",
367
+ JS_PACKAGE_VERSION: "^0.0.0",
368
+ VITE_PLUGIN_VUE_VERSION: "^6.0.5",
369
+ VITE_VERSION: "^8.0.0"
370
+ };
371
+ }
372
+ }
373
+ var { NATIVE_VERSION, JS_PACKAGE_VERSION, VITE_PLUGIN_VUE_VERSION, VITE_VERSION } = getTemplateVersions();
374
+ var VALID_NAME = /^[a-zA-Z0-9_-]+$/i;
341
375
  var createCommand = new Command2("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) => {
342
376
  const template = options.template;
343
377
  if (!["blank", "tabs", "drawer"].includes(template)) {
344
- console.error(pc2.red(`Invalid template "${template}". Choose: blank, tabs, drawer`));
345
- process.exit(1);
378
+ throw new ConfigError(
379
+ `Invalid template "${template}". Choose: blank, tabs, drawer`
380
+ );
346
381
  }
347
- const dir = join2(process.cwd(), name);
382
+ if (!VALID_NAME.test(name)) {
383
+ throw new ConfigError(
384
+ `Project name "${name}" must match /^[a-zA-Z0-9_-]+$/i`
385
+ );
386
+ }
387
+ const dir = join3(process.cwd(), name);
348
388
  console.log(pc2.cyan(`
349
389
  Creating Vue Native project: ${pc2.bold(name)} (template: ${template})
350
390
  `));
351
391
  try {
352
392
  await mkdir(dir, { recursive: true });
353
- await mkdir(join2(dir, "app"), { recursive: true });
354
- await mkdir(join2(dir, "app", "pages"), { recursive: true });
355
- await writeFile(join2(dir, "package.json"), JSON.stringify({
393
+ await mkdir(join3(dir, "app"), { recursive: true });
394
+ await mkdir(join3(dir, "app", "pages"), { recursive: true });
395
+ await writeFile(join3(dir, "package.json"), JSON.stringify({
356
396
  name,
357
397
  version: "0.0.1",
358
398
  private: true,
@@ -374,7 +414,7 @@ Creating Vue Native project: ${pc2.bold(name)} (template: ${template})
374
414
  "typescript": "^5.7.0"
375
415
  }
376
416
  }, null, 2));
377
- await writeFile(join2(dir, "vite.config.ts"), `import { defineConfig } from 'vite'
417
+ await writeFile(join3(dir, "vite.config.ts"), `import { defineConfig } from 'vite'
378
418
  import vue from '@vitejs/plugin-vue'
379
419
  import vueNative from '@thelacanians/vue-native-vite-plugin'
380
420
 
@@ -382,7 +422,7 @@ export default defineConfig({
382
422
  plugins: [vue(), vueNative()],
383
423
  })
384
424
  `);
385
- await writeFile(join2(dir, "tsconfig.json"), JSON.stringify({
425
+ await writeFile(join3(dir, "tsconfig.json"), JSON.stringify({
386
426
  compilerOptions: {
387
427
  target: "ES2020",
388
428
  module: "ESNext",
@@ -398,12 +438,12 @@ export default defineConfig({
398
438
  include: ["app/**/*", "env.d.ts"]
399
439
  }, null, 2));
400
440
  await generateTemplateFiles(dir, name, template);
401
- const iosDir = join2(dir, "ios");
402
- const iosSrcDir = join2(iosDir, "Sources");
441
+ const iosDir = join3(dir, "ios");
442
+ const iosSrcDir = join3(iosDir, "Sources");
403
443
  await mkdir(iosSrcDir, { recursive: true });
404
444
  const xcodeProjectName = name.replace(/[^a-zA-Z0-9]/g, "");
405
445
  const bundleId = `com.vuenative.${name.replace(/[^a-zA-Z0-9]/g, "").toLowerCase()}`;
406
- await writeFile(join2(iosDir, "project.yml"), `name: ${xcodeProjectName}
446
+ await writeFile(join3(iosDir, "project.yml"), `name: ${xcodeProjectName}
407
447
  options:
408
448
  bundleIdPrefix: com.vuenative
409
449
  deploymentTarget:
@@ -413,7 +453,7 @@ options:
413
453
  packages:
414
454
  VueNativeCore:
415
455
  url: https://github.com/abdul-hamid-achik/vue-native
416
- from: "${VERSION}"
456
+ from: "${NATIVE_VERSION}"
417
457
 
418
458
  targets:
419
459
  ${xcodeProjectName}:
@@ -434,7 +474,7 @@ targets:
434
474
  - path: ../dist/vue-native-bundle.js
435
475
  optional: true
436
476
  `);
437
- await writeFile(join2(iosSrcDir, "Info.plist"), `<?xml version="1.0" encoding="UTF-8"?>
477
+ await writeFile(join3(iosSrcDir, "Info.plist"), `<?xml version="1.0" encoding="UTF-8"?>
438
478
  <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
439
479
  <plist version="1.0">
440
480
  <dict>
@@ -500,7 +540,7 @@ targets:
500
540
  </dict>
501
541
  </plist>
502
542
  `);
503
- await writeFile(join2(iosSrcDir, "AppDelegate.swift"), `import UIKit
543
+ await writeFile(join3(iosSrcDir, "AppDelegate.swift"), `import UIKit
504
544
 
505
545
  @main
506
546
  class AppDelegate: UIResponder, UIApplicationDelegate {
@@ -524,7 +564,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
524
564
  }
525
565
  }
526
566
  `);
527
- await writeFile(join2(iosSrcDir, "SceneDelegate.swift"), `import UIKit
567
+ await writeFile(join3(iosSrcDir, "SceneDelegate.swift"), `import UIKit
528
568
  import VueNativeCore
529
569
 
530
570
  class SceneDelegate: UIResponder, UIWindowSceneDelegate {
@@ -552,29 +592,29 @@ class AppViewController: VueNativeViewController {
552
592
  #endif
553
593
  }
554
594
  `);
555
- const androidDir = join2(dir, "android");
556
- const androidAppDir = join2(androidDir, "app");
595
+ const androidDir = join3(dir, "android");
596
+ const androidAppDir = join3(androidDir, "app");
557
597
  const androidPkg = `com.vuenative.${name.replace(/[^a-zA-Z0-9]/g, "").toLowerCase()}`;
558
598
  const androidPkgPath = androidPkg.replace(/\./g, "/");
559
- const androidSrcDir = join2(androidAppDir, "src", "main");
560
- const androidKotlinDir = join2(androidSrcDir, "kotlin", androidPkgPath);
561
- const androidResValuesDir = join2(androidSrcDir, "res", "values");
562
- const androidResXmlDir = join2(androidSrcDir, "res", "xml");
563
- const androidDebugResXmlDir = join2(androidAppDir, "src", "debug", "res", "xml");
564
- const androidGradleWrapperDir = join2(androidDir, "gradle", "wrapper");
599
+ const androidSrcDir = join3(androidAppDir, "src", "main");
600
+ const androidKotlinDir = join3(androidSrcDir, "kotlin", androidPkgPath);
601
+ const androidResValuesDir = join3(androidSrcDir, "res", "values");
602
+ const androidResXmlDir = join3(androidSrcDir, "res", "xml");
603
+ const androidDebugResXmlDir = join3(androidAppDir, "src", "debug", "res", "xml");
604
+ const androidGradleWrapperDir = join3(androidDir, "gradle", "wrapper");
565
605
  await mkdir(androidKotlinDir, { recursive: true });
566
606
  await mkdir(androidResValuesDir, { recursive: true });
567
607
  await mkdir(androidResXmlDir, { recursive: true });
568
608
  await mkdir(androidDebugResXmlDir, { recursive: true });
569
609
  await mkdir(androidGradleWrapperDir, { recursive: true });
570
- await writeFile(join2(androidDir, "build.gradle.kts"), `// Top-level build file
610
+ await writeFile(join3(androidDir, "build.gradle.kts"), `// Top-level build file
571
611
  plugins {
572
612
  id("com.android.application") version "8.7.3" apply false
573
613
  id("com.android.library") version "8.7.3" apply false
574
614
  id("org.jetbrains.kotlin.android") version "2.0.21" apply false
575
615
  }
576
616
  `);
577
- await writeFile(join2(androidDir, "settings.gradle.kts"), `pluginManagement {
617
+ await writeFile(join3(androidDir, "settings.gradle.kts"), `pluginManagement {
578
618
  repositories {
579
619
  google()
580
620
  mavenCentral()
@@ -600,7 +640,7 @@ dependencyResolutionManagement {
600
640
  rootProject.name = "${name}"
601
641
  include(":app")
602
642
  `);
603
- await writeFile(join2(androidAppDir, "build.gradle.kts"), `plugins {
643
+ await writeFile(join3(androidAppDir, "build.gradle.kts"), `plugins {
604
644
  id("com.android.application")
605
645
  id("org.jetbrains.kotlin.android")
606
646
  }
@@ -642,19 +682,19 @@ android {
642
682
  }
643
683
 
644
684
  dependencies {
645
- implementation("com.vuenative:core:${VERSION}")
685
+ implementation("com.vuenative:core:${NATIVE_VERSION}")
646
686
  implementation("androidx.appcompat:appcompat:1.7.0")
647
687
  implementation("com.google.android.material:material:1.12.0")
648
688
  implementation("androidx.core:core-ktx:1.15.0")
649
689
  }
650
690
  `);
651
- await writeFile(join2(androidAppDir, "proguard-rules.pro"), `# Vue Native
691
+ await writeFile(join3(androidAppDir, "proguard-rules.pro"), `# Vue Native
652
692
  -keep class com.vuenative.** { *; }
653
693
 
654
694
  # J2V8
655
695
  -keep class com.eclipsesource.v8.** { *; }
656
696
  `);
657
- await writeFile(join2(androidSrcDir, "AndroidManifest.xml"), `<?xml version="1.0" encoding="utf-8"?>
697
+ await writeFile(join3(androidSrcDir, "AndroidManifest.xml"), `<?xml version="1.0" encoding="utf-8"?>
658
698
  <manifest xmlns:android="http://schemas.android.com/apk/res/android">
659
699
  <uses-permission android:name="android.permission.INTERNET" />
660
700
 
@@ -677,12 +717,12 @@ dependencies {
677
717
  </application>
678
718
  </manifest>
679
719
  `);
680
- await writeFile(join2(androidResValuesDir, "strings.xml"), `<?xml version="1.0" encoding="utf-8"?>
720
+ await writeFile(join3(androidResValuesDir, "strings.xml"), `<?xml version="1.0" encoding="utf-8"?>
681
721
  <resources>
682
722
  <string name="app_name">${name}</string>
683
723
  </resources>
684
724
  `);
685
- await writeFile(join2(androidResValuesDir, "themes.xml"), `<?xml version="1.0" encoding="utf-8"?>
725
+ await writeFile(join3(androidResValuesDir, "themes.xml"), `<?xml version="1.0" encoding="utf-8"?>
686
726
  <resources>
687
727
  <style name="Theme.VueNative" parent="Theme.MaterialComponents.Light.NoActionBar">
688
728
  <item name="colorPrimary">#4F46E5</item>
@@ -695,12 +735,12 @@ dependencies {
695
735
  </style>
696
736
  </resources>
697
737
  `);
698
- await writeFile(join2(androidResXmlDir, "network_security_config.xml"), `<?xml version="1.0" encoding="utf-8"?>
738
+ await writeFile(join3(androidResXmlDir, "network_security_config.xml"), `<?xml version="1.0" encoding="utf-8"?>
699
739
  <network-security-config>
700
740
  <base-config cleartextTrafficPermitted="false" />
701
741
  </network-security-config>
702
742
  `);
703
- await writeFile(join2(androidDebugResXmlDir, "network_security_config.xml"), `<?xml version="1.0" encoding="utf-8"?>
743
+ await writeFile(join3(androidDebugResXmlDir, "network_security_config.xml"), `<?xml version="1.0" encoding="utf-8"?>
704
744
  <network-security-config>
705
745
  <domain-config cleartextTrafficPermitted="true">
706
746
  <domain includeSubdomains="true">localhost</domain>
@@ -709,7 +749,7 @@ dependencies {
709
749
  </domain-config>
710
750
  </network-security-config>
711
751
  `);
712
- await writeFile(join2(androidKotlinDir, "MainActivity.kt"), `package ${androidPkg}
752
+ await writeFile(join3(androidKotlinDir, "MainActivity.kt"), `package ${androidPkg}
713
753
 
714
754
  import com.vuenative.core.VueNativeActivity
715
755
 
@@ -723,20 +763,20 @@ class MainActivity : VueNativeActivity() {
723
763
  }
724
764
  }
725
765
  `);
726
- await writeFile(join2(androidDir, "gradle.properties"), `# Project-wide Gradle settings
766
+ await writeFile(join3(androidDir, "gradle.properties"), `# Project-wide Gradle settings
727
767
  org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
728
768
  android.useAndroidX=true
729
769
  kotlin.code.style=official
730
770
  android.nonTransitiveRClass=true
731
771
  `);
732
- await writeFile(join2(androidGradleWrapperDir, "gradle-wrapper.properties"), `distributionBase=GRADLE_USER_HOME
772
+ await writeFile(join3(androidGradleWrapperDir, "gradle-wrapper.properties"), `distributionBase=GRADLE_USER_HOME
733
773
  distributionPath=wrapper/dists
734
774
  distributionUrl=https\\://services.gradle.org/distributions/gradle-8.11.1-bin.zip
735
775
  networkTimeout=10000
736
776
  zipStoreBase=GRADLE_USER_HOME
737
777
  zipStorePath=wrapper/dists
738
778
  `);
739
- await writeFile(join2(dir, "vue-native.config.ts"), `import { defineConfig } from '@thelacanians/vue-native-cli'
779
+ await writeFile(join3(dir, "vue-native.config.ts"), `import { defineConfig } from '@thelacanians/vue-native-cli'
740
780
 
741
781
  export default defineConfig({
742
782
  name: '${name}',
@@ -751,7 +791,7 @@ export default defineConfig({
751
791
  },
752
792
  })
753
793
  `);
754
- await writeFile(join2(dir, "env.d.ts"), `/// <reference types="vite/client" />
794
+ await writeFile(join3(dir, "env.d.ts"), `/// <reference types="vite/client" />
755
795
  declare module '*.vue' {
756
796
  import type { DefineComponent } from '@thelacanians/vue-native-runtime'
757
797
  const component: DefineComponent<{}, {}, any>
@@ -759,7 +799,7 @@ declare module '*.vue' {
759
799
  }
760
800
  declare const __DEV__: boolean
761
801
  `);
762
- await writeFile(join2(dir, ".gitignore"), `node_modules/
802
+ await writeFile(join3(dir, ".gitignore"), `node_modules/
763
803
  dist/
764
804
  *.xcuserstate
765
805
  *.xcuserdatad/
@@ -781,10 +821,10 @@ local.properties
781
821
  *.keystore
782
822
  *.jks
783
823
  `);
784
- const cliDir = dirname(dirname(fileURLToPath(import.meta.url)));
785
- const bundledNative = join2(cliDir, "native");
786
- if (existsSync2(bundledNative)) {
787
- const nativeDir = join2(dir, "native");
824
+ const cliDir2 = dirname(dirname(fileURLToPath(import.meta.url)));
825
+ const bundledNative = join3(cliDir2, "native");
826
+ if (existsSync3(bundledNative)) {
827
+ const nativeDir = join3(dir, "native");
788
828
  await cp(bundledNative, nativeDir, { recursive: true });
789
829
  console.log(pc2.dim(" Bundled native/ copied as fallback.\n"));
790
830
  }
@@ -800,12 +840,13 @@ local.properties
800
840
  console.log(pc2.dim(" cd android && gradle wrapper && cd .."));
801
841
  console.log(pc2.white(" vue-native run android\n"));
802
842
  } catch (err) {
803
- console.error(pc2.red(`Error creating project: ${err.message}`));
804
- process.exit(1);
843
+ throw new ConfigError(
844
+ `Error creating project: ${err.message}`
845
+ );
805
846
  }
806
847
  });
807
848
  async function generateTemplateFiles(dir, name, template) {
808
- const pagesDir = join2(dir, "app", "pages");
849
+ const pagesDir = join3(dir, "app", "pages");
809
850
  if (template === "blank") {
810
851
  await generateBlankTemplate(dir, pagesDir);
811
852
  } else if (template === "tabs") {
@@ -815,7 +856,7 @@ async function generateTemplateFiles(dir, name, template) {
815
856
  }
816
857
  }
817
858
  async function generateBlankTemplate(dir, pagesDir) {
818
- await writeFile(join2(dir, "app", "main.ts"), `import { createApp } from '@thelacanians/vue-native-runtime'
859
+ await writeFile(join3(dir, "app", "main.ts"), `import { createApp } from '@thelacanians/vue-native-runtime'
819
860
  import { createRouter } from '@thelacanians/vue-native-navigation'
820
861
  import App from './App.vue'
821
862
  import Home from './pages/Home.vue'
@@ -828,7 +869,7 @@ const app = createApp(App)
828
869
  app.use(router)
829
870
  app.start()
830
871
  `);
831
- await writeFile(join2(dir, "app", "App.vue"), `<template>
872
+ await writeFile(join3(dir, "app", "App.vue"), `<template>
832
873
  <VSafeArea :style="{ flex: 1, backgroundColor: '#ffffff' }">
833
874
  <RouterView />
834
875
  </VSafeArea>
@@ -838,7 +879,7 @@ app.start()
838
879
  import { RouterView } from '@thelacanians/vue-native-navigation'
839
880
  </script>
840
881
  `);
841
- await writeFile(join2(pagesDir, "Home.vue"), `<script setup lang="ts">
882
+ await writeFile(join3(pagesDir, "Home.vue"), `<script setup lang="ts">
842
883
  import { ref } from 'vue'
843
884
  import { createStyleSheet } from '@thelacanians/vue-native-runtime'
844
885
 
@@ -890,13 +931,13 @@ const styles = createStyleSheet({
890
931
  `);
891
932
  }
892
933
  async function generateTabsTemplate(dir, pagesDir) {
893
- await writeFile(join2(dir, "app", "main.ts"), `import { createApp } from '@thelacanians/vue-native-runtime'
934
+ await writeFile(join3(dir, "app", "main.ts"), `import { createApp } from '@thelacanians/vue-native-runtime'
894
935
  import App from './App.vue'
895
936
 
896
937
  const app = createApp(App)
897
938
  app.start()
898
939
  `);
899
- await writeFile(join2(dir, "app", "App.vue"), `<script setup lang="ts">
940
+ await writeFile(join3(dir, "app", "App.vue"), `<script setup lang="ts">
900
941
  import { createTabNavigator } from '@thelacanians/vue-native-navigation'
901
942
  import Home from './pages/Home.vue'
902
943
  import Settings from './pages/Settings.vue'
@@ -915,7 +956,7 @@ const { TabNavigator } = createTabNavigator()
915
956
  </VSafeArea>
916
957
  </template>
917
958
  `);
918
- await writeFile(join2(pagesDir, "Home.vue"), `<script setup lang="ts">
959
+ await writeFile(join3(pagesDir, "Home.vue"), `<script setup lang="ts">
919
960
  import { ref } from 'vue'
920
961
  import { createStyleSheet } from '@thelacanians/vue-native-runtime'
921
962
 
@@ -959,7 +1000,7 @@ const styles = createStyleSheet({
959
1000
  </VView>
960
1001
  </template>
961
1002
  `);
962
- await writeFile(join2(pagesDir, "Settings.vue"), `<script setup lang="ts">
1003
+ await writeFile(join3(pagesDir, "Settings.vue"), `<script setup lang="ts">
963
1004
  import { ref } from 'vue'
964
1005
  import { createStyleSheet } from '@thelacanians/vue-native-runtime'
965
1006
 
@@ -1003,13 +1044,13 @@ const styles = createStyleSheet({
1003
1044
  `);
1004
1045
  }
1005
1046
  async function generateDrawerTemplate(dir, pagesDir) {
1006
- await writeFile(join2(dir, "app", "main.ts"), `import { createApp } from '@thelacanians/vue-native-runtime'
1047
+ await writeFile(join3(dir, "app", "main.ts"), `import { createApp } from '@thelacanians/vue-native-runtime'
1007
1048
  import App from './App.vue'
1008
1049
 
1009
1050
  const app = createApp(App)
1010
1051
  app.start()
1011
1052
  `);
1012
- await writeFile(join2(dir, "app", "App.vue"), `<script setup lang="ts">
1053
+ await writeFile(join3(dir, "app", "App.vue"), `<script setup lang="ts">
1013
1054
  import { createDrawerNavigator } from '@thelacanians/vue-native-navigation'
1014
1055
  import Home from './pages/Home.vue'
1015
1056
  import About from './pages/About.vue'
@@ -1028,7 +1069,7 @@ const { DrawerNavigator } = createDrawerNavigator()
1028
1069
  </VSafeArea>
1029
1070
  </template>
1030
1071
  `);
1031
- await writeFile(join2(pagesDir, "Home.vue"), `<script setup lang="ts">
1072
+ await writeFile(join3(pagesDir, "Home.vue"), `<script setup lang="ts">
1032
1073
  import { createStyleSheet } from '@thelacanians/vue-native-runtime'
1033
1074
  import { useDrawer } from '@thelacanians/vue-native-navigation'
1034
1075
 
@@ -1081,7 +1122,7 @@ const styles = createStyleSheet({
1081
1122
  </VView>
1082
1123
  </template>
1083
1124
  `);
1084
- await writeFile(join2(pagesDir, "About.vue"), `<script setup lang="ts">
1125
+ await writeFile(join3(pagesDir, "About.vue"), `<script setup lang="ts">
1085
1126
  import { createStyleSheet } from '@thelacanians/vue-native-runtime'
1086
1127
  import { useDrawer } from '@thelacanians/vue-native-navigation'
1087
1128
 
@@ -1140,8 +1181,8 @@ const styles = createStyleSheet({
1140
1181
  import { Command as Command3 } from "commander";
1141
1182
  import { spawn as spawn2, execSync as execSync2 } from "child_process";
1142
1183
  import { readFile } from "fs/promises";
1143
- import { existsSync as existsSync3 } from "fs";
1144
- import { join as join3 } from "path";
1184
+ import { existsSync as existsSync4 } from "fs";
1185
+ import { join as join4 } from "path";
1145
1186
  import { watch } from "chokidar";
1146
1187
  import { WebSocketServer, WebSocket } from "ws";
1147
1188
  import pc3 from "picocolors";
@@ -1194,7 +1235,7 @@ function detectAndroidEmulators() {
1194
1235
  var devCommand = new Command3("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) => {
1195
1236
  const port = parseInt(options.port, 10);
1196
1237
  const cwd = process.cwd();
1197
- const bundlePath = join3(cwd, BUNDLE_FILE);
1238
+ const bundlePath = join4(cwd, BUNDLE_FILE);
1198
1239
  console.log(pc3.cyan("\n Vue Native Dev Server\n"));
1199
1240
  if (options.ios) {
1200
1241
  console.log(pc3.white(" Detecting iOS Simulators..."));
@@ -1290,15 +1331,15 @@ var devCommand = new Command3("dev").description("Start the Vue Native dev serve
1290
1331
  if (lanIP) {
1291
1332
  console.log(pc3.white(` LAN address: ${pc3.bold(`ws://${lanIP}:${port}`)}`));
1292
1333
  }
1293
- const iosDir = join3(cwd, "ios");
1294
- const androidDir = join3(cwd, "android");
1295
- if (existsSync3(iosDir)) {
1334
+ const iosDir = join4(cwd, "ios");
1335
+ const androidDir = join4(cwd, "android");
1336
+ if (existsSync4(iosDir)) {
1296
1337
  console.log(pc3.dim(` iOS Simulator: ws://localhost:${port}`));
1297
1338
  if (lanIP) {
1298
1339
  console.log(pc3.dim(` iOS Device (WiFi): ws://${lanIP}:${port}`));
1299
1340
  }
1300
1341
  }
1301
- if (existsSync3(androidDir)) {
1342
+ if (existsSync4(androidDir)) {
1302
1343
  console.log(pc3.dim(` Android emulator: ws://10.0.2.2:${port}`));
1303
1344
  if (lanIP) {
1304
1345
  console.log(pc3.dim(` Android Device: ws://${lanIP}:${port}`));
@@ -1362,28 +1403,28 @@ var devCommand = new Command3("dev").description("Start the Vue Native dev serve
1362
1403
  // src/commands/run.ts
1363
1404
  import { Command as Command4 } from "commander";
1364
1405
  import { spawn as spawn3, execSync as execSync3 } from "child_process";
1365
- import { existsSync as existsSync4, readdirSync as readdirSync2, readFileSync } from "fs";
1366
- import { join as join4 } from "path";
1406
+ import { existsSync as existsSync5, readdirSync as readdirSync2, readFileSync as readFileSync2 } from "fs";
1407
+ import { join as join5 } from "path";
1367
1408
  import pc4 from "picocolors";
1368
1409
  function findAppPath(_buildDir) {
1369
- const derivedDataBase = join4(
1410
+ const derivedDataBase = join5(
1370
1411
  process.env.HOME || "~",
1371
1412
  "Library/Developer/Xcode/DerivedData"
1372
1413
  );
1373
- if (existsSync4(derivedDataBase)) {
1414
+ if (existsSync5(derivedDataBase)) {
1374
1415
  try {
1375
1416
  const projects = readdirSync2(derivedDataBase);
1376
1417
  for (const project of projects.reverse()) {
1377
- const productsDir = join4(
1418
+ const productsDir = join5(
1378
1419
  derivedDataBase,
1379
1420
  project,
1380
1421
  "Build/Products/Debug-iphonesimulator"
1381
1422
  );
1382
- if (existsSync4(productsDir)) {
1423
+ if (existsSync5(productsDir)) {
1383
1424
  const entries = readdirSync2(productsDir);
1384
1425
  const app = entries.find((e) => e.endsWith(".app"));
1385
1426
  if (app) {
1386
- return join4(productsDir, app);
1427
+ return join5(productsDir, app);
1387
1428
  }
1388
1429
  }
1389
1430
  }
@@ -1393,10 +1434,10 @@ function findAppPath(_buildDir) {
1393
1434
  return null;
1394
1435
  }
1395
1436
  function readBundleId(iosDir) {
1396
- const plistPath = join4(iosDir, "Sources", "Info.plist");
1397
- if (existsSync4(plistPath)) {
1437
+ const plistPath = join5(iosDir, "Sources", "Info.plist");
1438
+ if (existsSync5(plistPath)) {
1398
1439
  try {
1399
- const content = readFileSync(plistPath, "utf8");
1440
+ const content = readFileSync2(plistPath, "utf8");
1400
1441
  const match = content.match(
1401
1442
  /<key>CFBundleIdentifier<\/key>\s*<string>([^<]+)<\/string>/
1402
1443
  );
@@ -1409,13 +1450,13 @@ function readBundleId(iosDir) {
1409
1450
  return "com.vuenative.app";
1410
1451
  }
1411
1452
  function findApkPath(androidDir) {
1412
- const apkDir = join4(androidDir, "app", "build", "outputs", "apk", "debug");
1413
- if (existsSync4(apkDir)) {
1453
+ const apkDir = join5(androidDir, "app", "build", "outputs", "apk", "debug");
1454
+ if (existsSync5(apkDir)) {
1414
1455
  try {
1415
1456
  const entries = readdirSync2(apkDir);
1416
1457
  const apk = entries.find((e) => e.endsWith(".apk") && !e.includes("androidTest"));
1417
1458
  if (apk) {
1418
- return join4(apkDir, apk);
1459
+ return join5(apkDir, apk);
1419
1460
  }
1420
1461
  } catch {
1421
1462
  }
@@ -1424,8 +1465,7 @@ function findApkPath(androidDir) {
1424
1465
  }
1425
1466
  var runCommand = new Command4("run").description("Build and run the app").argument("<platform>", "platform to run on (ios, android, macos)").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) => {
1426
1467
  if (platform !== "ios" && platform !== "android" && platform !== "macos") {
1427
- console.error(pc4.red('Platform must be "ios", "android", or "macos"'));
1428
- process.exit(1);
1468
+ throw new ConfigError('Platform must be "ios", "android", or "macos"');
1429
1469
  }
1430
1470
  const cwd = process.cwd();
1431
1471
  const platformLabel = platform === "ios" ? "iOS" : platform === "android" ? "Android" : "macOS";
@@ -1437,8 +1477,7 @@ var runCommand = new Command4("run").description("Build and run the app").argume
1437
1477
  execSync3("bun run vite build", { cwd, stdio: "inherit" });
1438
1478
  console.log(pc4.green(" \u2713 Bundle built\n"));
1439
1479
  } catch {
1440
- console.error(pc4.red(" \u2717 Bundle build failed"));
1441
- process.exit(1);
1480
+ throw new ConfigError("Bundle build failed");
1442
1481
  }
1443
1482
  if (platform === "ios") {
1444
1483
  runIOS(cwd, options);
@@ -1450,14 +1489,14 @@ var runCommand = new Command4("run").description("Build and run the app").argume
1450
1489
  });
1451
1490
  function runIOS(cwd, options) {
1452
1491
  let xcodeProject = null;
1453
- const iosDir = join4(cwd, "ios");
1454
- if (existsSync4(iosDir)) {
1492
+ const iosDir = join5(cwd, "ios");
1493
+ if (existsSync5(iosDir)) {
1455
1494
  for (const ext of [".xcworkspace", ".xcodeproj"]) {
1456
1495
  try {
1457
1496
  const entries = readdirSync2(iosDir);
1458
1497
  const match = entries.find((e) => e.endsWith(ext));
1459
1498
  if (match) {
1460
- xcodeProject = join4(iosDir, match);
1499
+ xcodeProject = join5(iosDir, match);
1461
1500
  break;
1462
1501
  }
1463
1502
  } catch {
@@ -1493,7 +1532,7 @@ function runIOS(cwd, options) {
1493
1532
  xcodebuild.on("close", (code) => {
1494
1533
  if (code !== 0) {
1495
1534
  console.error(pc4.red(` \u2717 Build failed (exit code ${code})`));
1496
- process.exit(1);
1535
+ throw new ConfigError(`iOS build failed with exit code ${code}`);
1497
1536
  }
1498
1537
  console.log(pc4.green(" \u2713 Build successful\n"));
1499
1538
  if (options.device) {
@@ -1501,7 +1540,7 @@ function runIOS(cwd, options) {
1501
1540
  return;
1502
1541
  }
1503
1542
  const simulatorName = options.simulator;
1504
- const bundleId = options.bundleId || readBundleId(join4(cwd, "ios"));
1543
+ const bundleId = options.bundleId || readBundleId(join5(cwd, "ios"));
1505
1544
  console.log(pc4.white(` Booting simulator "${simulatorName}"...`));
1506
1545
  try {
1507
1546
  execSync3(`xcrun simctl boot "${simulatorName}"`, { stdio: "pipe" });
@@ -1511,7 +1550,7 @@ function runIOS(cwd, options) {
1511
1550
  execSync3("open -a Simulator", { stdio: "pipe" });
1512
1551
  } catch {
1513
1552
  }
1514
- const appPath = findAppPath(join4(cwd, "ios"));
1553
+ const appPath = findAppPath(join5(cwd, "ios"));
1515
1554
  if (appPath) {
1516
1555
  console.log(pc4.white(` Installing app on simulator...`));
1517
1556
  try {
@@ -1519,7 +1558,7 @@ function runIOS(cwd, options) {
1519
1558
  console.log(pc4.green(" \u2713 App installed"));
1520
1559
  } catch (err) {
1521
1560
  console.error(pc4.red(` \u2717 Failed to install app: ${err.message}`));
1522
- process.exit(1);
1561
+ throw new ConfigError(`iOS app install failed: ${err.message}`);
1523
1562
  }
1524
1563
  console.log(pc4.white(` Launching ${bundleId}...`));
1525
1564
  try {
@@ -1528,7 +1567,7 @@ function runIOS(cwd, options) {
1528
1567
  `));
1529
1568
  } catch (err) {
1530
1569
  console.error(pc4.red(` \u2717 Failed to launch app: ${err.message}`));
1531
- process.exit(1);
1570
+ throw new ConfigError(`iOS app launch failed: ${err.message}`);
1532
1571
  }
1533
1572
  } else {
1534
1573
  console.log(pc4.yellow(" Could not locate .app bundle in DerivedData."));
@@ -1537,18 +1576,18 @@ function runIOS(cwd, options) {
1537
1576
  });
1538
1577
  }
1539
1578
  function runAndroid(cwd, options) {
1540
- const androidDir = join4(cwd, "android");
1541
- if (!existsSync4(androidDir)) {
1579
+ const androidDir = join5(cwd, "android");
1580
+ if (!existsSync5(androidDir)) {
1542
1581
  console.log(pc4.yellow(" No android/ directory found."));
1543
1582
  console.log(pc4.dim(" To add Android support, create an Android project in the android/ directory."));
1544
1583
  console.log(pc4.dim(" Bundle has been built to dist/vue-native-bundle.js\n"));
1545
1584
  return;
1546
1585
  }
1547
- const gradlew = join4(androidDir, "gradlew");
1548
- if (!existsSync4(gradlew)) {
1549
- console.error(pc4.red(" \u2717 gradlew not found in android/ directory"));
1550
- console.log(pc4.dim(" Make sure your Android project has the Gradle wrapper.\n"));
1551
- process.exit(1);
1586
+ const gradlew = join5(androidDir, "gradlew");
1587
+ if (!existsSync5(gradlew)) {
1588
+ throw new ConfigError(
1589
+ "gradlew not found in android/ directory. Make sure your Android project has the Gradle wrapper."
1590
+ );
1552
1591
  }
1553
1592
  console.log(pc4.white(" Building Android app with Gradle..."));
1554
1593
  const gradle = spawn3(
@@ -1587,7 +1626,7 @@ function runAndroid(cwd, options) {
1587
1626
  gradle.on("close", (code) => {
1588
1627
  if (code !== 0) {
1589
1628
  console.error(pc4.red(` \u2717 Gradle build failed (exit code ${code})`));
1590
- process.exit(1);
1629
+ throw new ConfigError(`Android Gradle build failed with exit code ${code}`);
1591
1630
  }
1592
1631
  console.log(pc4.green(" \u2713 Build successful\n"));
1593
1632
  const apkPath = findApkPath(androidDir);
@@ -1603,7 +1642,7 @@ function runAndroid(cwd, options) {
1603
1642
  } catch (err) {
1604
1643
  console.error(pc4.red(` \u2717 Failed to install APK: ${err.message}`));
1605
1644
  console.log(pc4.dim(" Make sure an emulator is running or a device is connected (adb devices).\n"));
1606
- process.exit(1);
1645
+ throw new ConfigError(`APK install failed: ${err.message}`);
1607
1646
  }
1608
1647
  const componentName = `${options.package}/${options.activity}`;
1609
1648
  console.log(pc4.white(` Launching ${componentName}...`));
@@ -1613,13 +1652,13 @@ function runAndroid(cwd, options) {
1613
1652
  `));
1614
1653
  } catch (err) {
1615
1654
  console.error(pc4.red(` \u2717 Failed to launch app: ${err.message}`));
1616
- process.exit(1);
1655
+ throw new ConfigError(`App launch failed: ${err.message}`);
1617
1656
  }
1618
1657
  });
1619
1658
  }
1620
1659
  function runMacOS(cwd, options) {
1621
- const macosDir = join4(cwd, "macos");
1622
- if (!existsSync4(macosDir)) {
1660
+ const macosDir = join5(cwd, "macos");
1661
+ if (!existsSync5(macosDir)) {
1623
1662
  console.log(pc4.yellow(" No macos/ directory found."));
1624
1663
  console.log(pc4.dim(" To add macOS support, create an Xcode project in the macos/ directory."));
1625
1664
  console.log(pc4.dim(" Bundle has been built to dist/vue-native-bundle.js\n"));
@@ -1631,7 +1670,7 @@ function runMacOS(cwd, options) {
1631
1670
  const entries = readdirSync2(macosDir);
1632
1671
  const match = entries.find((e) => e.endsWith(ext));
1633
1672
  if (match) {
1634
- xcodeProject = join4(macosDir, match);
1673
+ xcodeProject = join5(macosDir, match);
1635
1674
  break;
1636
1675
  }
1637
1676
  } catch {
@@ -1663,21 +1702,21 @@ function runMacOS(cwd, options) {
1663
1702
  xcodebuild.on("close", (code) => {
1664
1703
  if (code !== 0) {
1665
1704
  console.error(pc4.red(` \u2717 Build failed (exit code ${code})`));
1666
- process.exit(1);
1705
+ throw new ConfigError(`macOS build failed with exit code ${code}`);
1667
1706
  }
1668
1707
  console.log(pc4.green(" \u2713 Build successful\n"));
1669
- const derivedDataBase = join4(process.env.HOME || "~", "Library/Developer/Xcode/DerivedData");
1708
+ const derivedDataBase = join5(process.env.HOME || "~", "Library/Developer/Xcode/DerivedData");
1670
1709
  let appPath = null;
1671
- if (existsSync4(derivedDataBase)) {
1710
+ if (existsSync5(derivedDataBase)) {
1672
1711
  try {
1673
1712
  const projects = readdirSync2(derivedDataBase);
1674
1713
  for (const project of projects.reverse()) {
1675
- const productsDir = join4(derivedDataBase, project, "Build/Products/Debug");
1676
- if (existsSync4(productsDir)) {
1714
+ const productsDir = join5(derivedDataBase, project, "Build/Products/Debug");
1715
+ if (existsSync5(productsDir)) {
1677
1716
  const entries = readdirSync2(productsDir);
1678
1717
  const app = entries.find((e) => e.endsWith(".app"));
1679
1718
  if (app) {
1680
- appPath = join4(productsDir, app);
1719
+ appPath = join5(productsDir, app);
1681
1720
  break;
1682
1721
  }
1683
1722
  }
@@ -1703,18 +1742,18 @@ function runMacOS(cwd, options) {
1703
1742
 
1704
1743
  // src/commands/generate.ts
1705
1744
  import { Command as Command5 } from "commander";
1706
- import { existsSync as existsSync5 } from "fs";
1707
- import { join as join5 } from "path";
1745
+ import { existsSync as existsSync6 } from "fs";
1746
+ import { join as join6 } from "path";
1708
1747
  import pc5 from "picocolors";
1709
1748
  import { parseDirectory } from "@thelacanians/vue-native-sfc-parser";
1710
1749
  import { generateCode, writeGeneratedFiles, cleanGeneratedFiles, validateNativeBlocks, formatValidationErrors } from "@thelacanians/vue-native-codegen";
1711
1750
  var generateCommand = new Command5("generate").description("Generate native code from <native> blocks in Vue SFC files").option("--root <path>", "project root directory", process.cwd()).option("--watch", "watch mode - regenerate on file changes").option("--clean", "clean generated files before generating").option("--ios-output <path>", "iOS Swift output directory").option("--android-output <path>", "Android Kotlin output directory").option("--macos-output <path>", "macOS Swift output directory").option("--ts-output <path>", "TypeScript output directory").option("--no-swift", "disable Swift generation").option("--no-kotlin", "disable Kotlin generation").option("--no-typescript", "disable TypeScript generation").option("--exclude <patterns>", "patterns to exclude (comma-separated)").action(async (options) => {
1712
1751
  const cwd = options.root;
1713
- const appDir = join5(cwd, "app");
1714
- if (!existsSync5(appDir)) {
1715
- console.error(pc5.red(`Error: App directory not found at ${appDir}`));
1716
- console.error(pc5.yellow("Make sure you run this command from your project root."));
1717
- process.exit(1);
1752
+ const appDir = join6(cwd, "app");
1753
+ if (!existsSync6(appDir)) {
1754
+ throw new ConfigError(
1755
+ `App directory not found at ${appDir}. Make sure you run this command from your project root.`
1756
+ );
1718
1757
  }
1719
1758
  const codegenOptions = {
1720
1759
  root: cwd,
@@ -1761,7 +1800,7 @@ var generateCommand = new Command5("generate").description("Generate native code
1761
1800
  if (!validation.isValid) {
1762
1801
  console.log(pc5.red("\n\u274C Validation failed:"));
1763
1802
  console.log(formatValidationErrors(validation));
1764
- process.exit(1);
1803
+ throw new ConfigError("Validation failed");
1765
1804
  }
1766
1805
  if (validation.warnings.length > 0) {
1767
1806
  console.log(pc5.yellow(`\u26A0\uFE0F ${validation.warnings.length} warning${validation.warnings.length !== 1 ? "s" : ""}`));
@@ -1785,7 +1824,7 @@ var generateCommand = new Command5("generate").description("Generate native code
1785
1824
  codegenResult.errors.forEach((err) => {
1786
1825
  console.log(pc5.red(` - ${err.file} ${err.message}`));
1787
1826
  });
1788
- process.exit(1);
1827
+ throw new ConfigError("Code generation failed");
1789
1828
  }
1790
1829
  console.log(pc5.blue("\u{1F4DD} Writing files..."));
1791
1830
  const writeResult = writeGeneratedFiles(codegenResult, cwd);
@@ -1794,7 +1833,7 @@ var generateCommand = new Command5("generate").description("Generate native code
1794
1833
  writeResult.errors.forEach((err) => {
1795
1834
  console.log(pc5.red(` - ${err.message}`));
1796
1835
  });
1797
- process.exit(1);
1836
+ throw new ConfigError("Failed to write generated files");
1798
1837
  }
1799
1838
  console.log(pc5.green("\n\u2705 Generation complete!\n"));
1800
1839
  console.log(pc5.dim("Generated files:"));
@@ -1811,10 +1850,10 @@ var generateCommand = new Command5("generate").description("Generate native code
1811
1850
  }
1812
1851
  console.log(pc5.green("\u{1F389} Ready to build!\n"));
1813
1852
  } catch (error) {
1814
- console.error(pc5.red("\n\u274C Fatal error:"));
1815
- console.error(pc5.red(error.message));
1816
- console.error(pc5.dim(error.stack || ""));
1817
- process.exit(1);
1853
+ if (error instanceof ConfigError) throw error;
1854
+ throw new ConfigError(
1855
+ error.message || "Unknown generation error"
1856
+ );
1818
1857
  }
1819
1858
  }
1820
1859
  await runGeneration();
@@ -1844,10 +1883,18 @@ var generateCommand = new Command5("generate").description("Generate native code
1844
1883
  });
1845
1884
 
1846
1885
  // src/cli.ts
1847
- program.name("vue-native").description("Vue Native \u2014 build native iOS and Android apps with Vue.js").version("0.1.0");
1886
+ var cliDir = dirname2(fileURLToPath2(import.meta.url));
1887
+ var pkg = JSON.parse(readFileSync3(join7(cliDir, "..", "package.json"), "utf8"));
1888
+ program.name("vue-native").description("Vue Native \u2014 build native iOS and Android apps with Vue.js").version(pkg.version);
1848
1889
  program.addCommand(buildCommand);
1849
1890
  program.addCommand(createCommand);
1850
1891
  program.addCommand(devCommand);
1851
1892
  program.addCommand(runCommand);
1852
1893
  program.addCommand(generateCommand);
1853
- program.parse(process.argv);
1894
+ program.parseAsync(process.argv).catch((err) => {
1895
+ if (err instanceof ConfigError) {
1896
+ console.error(pc6.red(err.message));
1897
+ process.exit(1);
1898
+ }
1899
+ throw err;
1900
+ });
package/dist/config.d.ts CHANGED
@@ -37,10 +37,13 @@ interface ResolvedConfig extends VueNativeConfig {
37
37
  plugins: string[];
38
38
  }
39
39
  declare function defineConfig(config: VueNativeConfig): VueNativeConfig;
40
+ declare class ConfigError extends Error {
41
+ constructor(message: string);
42
+ }
40
43
  /**
41
44
  * Load and resolve the vue-native.config.{ts,js,mjs} file from the project root.
42
45
  * Returns null if no config file is found.
43
46
  */
44
47
  declare function loadConfig(cwd: string): Promise<ResolvedConfig | null>;
45
48
 
46
- export { type ResolvedConfig, type VueNativeConfig, defineConfig, loadConfig };
49
+ export { ConfigError, type ResolvedConfig, type VueNativeConfig, defineConfig, loadConfig };
package/dist/config.js CHANGED
@@ -2,24 +2,42 @@
2
2
  import { existsSync } from "fs";
3
3
  import { join } from "path";
4
4
  import { pathToFileURL } from "url";
5
- import pc from "picocolors";
6
5
  function defineConfig(config) {
7
6
  return config;
8
7
  }
8
+ var ConfigError = class extends Error {
9
+ constructor(message) {
10
+ super(message);
11
+ this.name = "ConfigError";
12
+ }
13
+ };
9
14
  function validateConfig(config) {
10
15
  if (typeof config !== "object" || config === null) return false;
11
16
  const c = config;
12
17
  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;
18
+ throw new ConfigError(
19
+ 'Config error: "name" is required and must be a non-empty string.'
20
+ );
15
21
  }
16
22
  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;
23
+ throw new ConfigError(
24
+ 'Config error: "bundleId" must be a valid reverse-domain identifier (e.g. com.example.myapp).'
25
+ );
19
26
  }
20
27
  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;
28
+ throw new ConfigError('Config error: "version" is required (e.g. "1.0.0").');
29
+ }
30
+ if (c.plugins !== void 0) {
31
+ if (!Array.isArray(c.plugins)) {
32
+ throw new ConfigError('Config error: "plugins" must be an array of strings.');
33
+ }
34
+ for (const plugin of c.plugins) {
35
+ if (typeof plugin !== "string") {
36
+ throw new ConfigError(
37
+ 'Config error: "plugins" must be an array of strings.'
38
+ );
39
+ }
40
+ }
23
41
  }
24
42
  return true;
25
43
  }
@@ -42,7 +60,7 @@ async function loadConfig(cwd) {
42
60
  const mod = await import(pathToFileURL(configPath).href);
43
61
  const raw = mod.default ?? mod;
44
62
  if (!validateConfig(raw)) {
45
- process.exit(1);
63
+ throw new ConfigError(`Invalid configuration in ${configPath}`);
46
64
  }
47
65
  const config = raw;
48
66
  const safeName = config.name.replace(/[^a-zA-Z0-9]/g, "");
@@ -61,12 +79,14 @@ async function loadConfig(cwd) {
61
79
  };
62
80
  return resolved;
63
81
  } catch (err) {
64
- console.error(pc.red(` Failed to load config from ${configPath}:`));
65
- console.error(pc.red(` ${err.message}`));
66
- process.exit(1);
82
+ if (err instanceof ConfigError) throw err;
83
+ throw new ConfigError(
84
+ `Failed to load config from ${configPath}: ${err.message}`
85
+ );
67
86
  }
68
87
  }
69
88
  export {
89
+ ConfigError,
70
90
  defineConfig,
71
91
  loadConfig
72
92
  };
@@ -5,6 +5,20 @@ plugins {
5
5
  id("org.jlleitschuh.gradle.ktlint")
6
6
  }
7
7
 
8
+ // Single source of truth for the published version: packages/runtime/package.json.
9
+ // Falls back to 0.0.0-SNAPSHOT when the JS workspace isn't present (standalone Android builds).
10
+ val publishedVersion: String = run {
11
+ val pkgJson = rootProject.file("../../packages/runtime/package.json")
12
+ if (!pkgJson.exists()) {
13
+ "0.0.0-SNAPSHOT"
14
+ } else {
15
+ Regex("\"version\"\\s*:\\s*\"([^\"]+)\"")
16
+ .find(pkgJson.readText())
17
+ ?.groupValues?.get(1)
18
+ ?: "0.0.0-SNAPSHOT"
19
+ }
20
+ }
21
+
8
22
  android {
9
23
  namespace = "com.vuenative.core"
10
24
  compileSdk = 35
@@ -119,7 +133,7 @@ afterEvaluate {
119
133
  create<MavenPublication>("release") {
120
134
  groupId = "com.vuenative"
121
135
  artifactId = "core"
122
- version = "0.4.14"
136
+ version = publishedVersion
123
137
  from(components["release"])
124
138
  }
125
139
  }
@@ -10,13 +10,16 @@ let package = Package(
10
10
  dependencies: [
11
11
  // Yoga layout engine — layoutBox/FlexLayout v2.x wraps Yoga 3.0.4
12
12
  // 2.1k stars, actively maintained (last release Dec 2025), full SPM support
13
- .package(url: "https://github.com/layoutBox/FlexLayout.git", from: "2.0.0")
13
+ .package(url: "https://github.com/layoutBox/FlexLayout.git", from: "2.0.0"),
14
+ // Shared cross-platform Swift code used by both iOS and macOS
15
+ .package(path: "../../shared/VueNativeShared")
14
16
  ],
15
17
  targets: [
16
18
  .target(
17
19
  name: "VueNativeCore",
18
20
  dependencies: [
19
- .product(name: "FlexLayout", package: "FlexLayout")
21
+ .product(name: "FlexLayout", package: "FlexLayout"),
22
+ "VueNativeShared"
20
23
  ],
21
24
  path: "Sources/VueNativeCore",
22
25
  resources: [
@@ -0,0 +1,124 @@
1
+ #if canImport(UIKit)
2
+ import JavaScriptCore
3
+ import UIKit
4
+ import VueNativeShared
5
+
6
+ /// iOS-specific requestAnimationFrame helper using CADisplayLink.
7
+ /// Registers requestAnimationFrame/cancelAnimationFrame on the JS context
8
+ /// and manages the display link lifecycle.
9
+ enum IOSRAFHelper {
10
+
11
+ /// Active display link for requestAnimationFrame. Accessed only from main thread.
12
+ private static var displayLink: CADisplayLink?
13
+
14
+ // MARK: - Registration
15
+
16
+ /// Register requestAnimationFrame/cancelAnimationFrame on the given JS context.
17
+ /// MUST be called on the JS queue after SharedJSPolyfills.register().
18
+ static func register(in runtime: JSRuntime) {
19
+ guard let context = runtime.context else { return }
20
+
21
+ // requestAnimationFrame(callback) -> rafId (String)
22
+ let requestAnimationFrame: @convention(block) (JSValue) -> JSValue = { [weak runtime] callback in
23
+ guard let runtime = runtime, let context = runtime.context else {
24
+ return JSValue(nullIn: JSContext.current())
25
+ }
26
+
27
+ // Store callback using shared helper and get an ID
28
+ let rafId = SharedJSPolyfills.storeRAFCallback(callback)
29
+
30
+ // Ensure display link is running
31
+ DispatchQueue.main.async {
32
+ if displayLink == nil {
33
+ let target = DisplayLinkTarget(runtime: runtime)
34
+ let link = CADisplayLink(target: target, selector: #selector(DisplayLinkTarget.handleFrame(_:)))
35
+ link.add(to: .main, forMode: .common)
36
+ displayLink = link
37
+ }
38
+ }
39
+
40
+ return JSValue(object: rafId, in: context)
41
+ }
42
+
43
+ // cancelAnimationFrame(rafId)
44
+ let cancelAnimationFrame: @convention(block) (JSValue) -> Void = { [weak runtime] rafId in
45
+ _ = runtime
46
+ guard let id = rafId.toString() else { return }
47
+ // Remove the callback using shared helper
48
+ SharedJSPolyfills.removeRAFCallback(id)
49
+ }
50
+
51
+ context.setObject(requestAnimationFrame, forKeyedSubscript: "requestAnimationFrame" as NSString)
52
+ context.setObject(cancelAnimationFrame, forKeyedSubscript: "cancelAnimationFrame" as NSString)
53
+ }
54
+
55
+ // MARK: - Reset
56
+
57
+ /// Reset RAF state and stop the display link.
58
+ /// Safe to call from any thread — display link operations are dispatched to main thread.
59
+ static func reset() {
60
+ DispatchQueue.main.async {
61
+ displayLink?.invalidate()
62
+ displayLink = nil
63
+ }
64
+ }
65
+
66
+ // MARK: - Display Link Callback
67
+
68
+ /// Called from the display link target on every frame.
69
+ /// Dispatches all pending RAF callbacks to the JS queue.
70
+ fileprivate static func fireRAFCallbacks(runtime: JSRuntime, timestamp: Double) {
71
+ // Snapshot and clear callbacks using shared helper (RAF is one-shot)
72
+ let callbacks = SharedJSPolyfills.drainRAFCallbacks()
73
+
74
+ guard !callbacks.isEmpty else {
75
+ // No pending callbacks — stop the display link
76
+ displayLink?.invalidate()
77
+ displayLink = nil
78
+ return
79
+ }
80
+
81
+ runtime.jsQueue.async { [weak runtime] in
82
+ guard let runtime = runtime, let context = runtime.context else { return }
83
+
84
+ for (_, callback) in callbacks {
85
+ if !callback.isUndefined {
86
+ callback.call(withArguments: [timestamp])
87
+ }
88
+ }
89
+
90
+ // Drain microtasks after all RAF callbacks
91
+ context.evaluateScript("void 0;")
92
+
93
+ // If no more callbacks pending, stop the display link
94
+ if !SharedJSPolyfills.hasRAFCallbacks {
95
+ DispatchQueue.main.async {
96
+ displayLink?.invalidate()
97
+ displayLink = nil
98
+ }
99
+ }
100
+ }
101
+ }
102
+ }
103
+
104
+ // MARK: - DisplayLinkTarget
105
+
106
+ /// Separate NSObject target for CADisplayLink to avoid retain cycles with JSRuntime.
107
+ private final class DisplayLinkTarget: NSObject {
108
+ private weak var runtime: JSRuntime?
109
+
110
+ init(runtime: JSRuntime) {
111
+ self.runtime = runtime
112
+ super.init()
113
+ }
114
+
115
+ @objc func handleFrame(_ link: CADisplayLink) {
116
+ guard let runtime = runtime else {
117
+ link.invalidate()
118
+ return
119
+ }
120
+ let timestamp = link.timestamp * 1000.0 // Convert to milliseconds
121
+ IOSRAFHelper.fireRAFCallbacks(runtime: runtime, timestamp: timestamp)
122
+ }
123
+ }
124
+ #endif
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@thelacanians/vue-native-cli",
3
- "version": "0.6.3",
3
+ "version": "0.6.5",
4
4
  "description": "CLI for creating and running Vue Native apps",
5
5
  "license": "MIT",
6
6
  "author": "Vue Native Contributors",
@@ -26,7 +26,7 @@
26
26
  "native"
27
27
  ],
28
28
  "scripts": {
29
- "prebuild": "rm -rf native && cp -r ../../native native",
29
+ "prebuild": "rm -rf native && rsync -a --exclude='.build/' --exclude='build/' --exclude='.gradle/' --exclude='.swiftpm/' --exclude='DerivedData/' --exclude='node_modules/' --exclude='.idea/' --exclude='build-tools/' --exclude='platform-tools/' --exclude='platforms/' --exclude='licenses/' --exclude='*.xcuserdata' --exclude='*.xcuserstate' --exclude='local.properties' --exclude='*.aar' --exclude='*.apk' --exclude='*.aab' ../../native/ native/",
30
30
  "build": "tsup",
31
31
  "dev": "tsup --watch",
32
32
  "test": "vitest run",
@@ -35,8 +35,8 @@
35
35
  "clean": "rm -rf dist"
36
36
  },
37
37
  "dependencies": {
38
- "@thelacanians/vue-native-sfc-parser": "^0.0.2",
39
- "@thelacanians/vue-native-codegen": "^0.0.2",
38
+ "@thelacanians/vue-native-sfc-parser": "^0.6.4",
39
+ "@thelacanians/vue-native-codegen": "^0.6.4",
40
40
  "commander": "^12.1.0",
41
41
  "ws": "^8.18.0",
42
42
  "chokidar": "^3.6.0",