@thelacanians/vue-native-cli 0.6.3 → 0.6.4

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,36 @@
2
2
 
3
3
  // src/cli.ts
4
4
  import { program } from "commander";
5
+ import pc6 from "picocolors";
5
6
 
6
7
  // src/commands/build.ts
7
8
  import { Command } from "commander";
8
9
  import { spawn, execSync } from "child_process";
9
- import { existsSync, readdirSync, mkdirSync, copyFileSync } from "fs";
10
- import { join, basename } from "path";
10
+ import { existsSync as existsSync2, readdirSync, mkdirSync, copyFileSync } from "fs";
11
+ import { join as join2, basename } from "path";
11
12
  import pc from "picocolors";
13
+
14
+ // src/config.ts
15
+ import { existsSync } from "fs";
16
+ import { join } from "path";
17
+ import { pathToFileURL } from "url";
18
+ var ConfigError = class extends Error {
19
+ constructor(message) {
20
+ super(message);
21
+ this.name = "ConfigError";
22
+ }
23
+ };
24
+
25
+ // src/commands/build.ts
12
26
  function findXcodeProject(iosDir) {
13
- if (!existsSync(iosDir)) return null;
27
+ if (!existsSync2(iosDir)) return null;
14
28
  for (const ext of [".xcworkspace", ".xcodeproj"]) {
15
29
  try {
16
30
  const entries = readdirSync(iosDir);
17
31
  const match = entries.find((e) => e.endsWith(ext));
18
32
  if (match) {
19
33
  return {
20
- path: join(iosDir, match),
34
+ path: join2(iosDir, match),
21
35
  isWorkspace: ext === ".xcworkspace"
22
36
  };
23
37
  }
@@ -27,13 +41,13 @@ function findXcodeProject(iosDir) {
27
41
  return null;
28
42
  }
29
43
  function findReleaseApk(androidDir) {
30
- const apkDir = join(androidDir, "app", "build", "outputs", "apk", "release");
31
- if (existsSync(apkDir)) {
44
+ const apkDir = join2(androidDir, "app", "build", "outputs", "apk", "release");
45
+ if (existsSync2(apkDir)) {
32
46
  try {
33
47
  const entries = readdirSync(apkDir);
34
48
  const apk = entries.find((e) => e.endsWith(".apk") && !e.includes("androidTest"));
35
49
  if (apk) {
36
- return join(apkDir, apk);
50
+ return join2(apkDir, apk);
37
51
  }
38
52
  } catch {
39
53
  }
@@ -41,13 +55,13 @@ function findReleaseApk(androidDir) {
41
55
  return null;
42
56
  }
43
57
  function findReleaseAab(androidDir) {
44
- const aabDir = join(androidDir, "app", "build", "outputs", "bundle", "release");
45
- if (existsSync(aabDir)) {
58
+ const aabDir = join2(androidDir, "app", "build", "outputs", "bundle", "release");
59
+ if (existsSync2(aabDir)) {
46
60
  try {
47
61
  const entries = readdirSync(aabDir);
48
62
  const aab = entries.find((e) => e.endsWith(".aab"));
49
63
  if (aab) {
50
- return join(aabDir, aab);
64
+ return join2(aabDir, aab);
51
65
  }
52
66
  } catch {
53
67
  }
@@ -55,17 +69,16 @@ function findReleaseAab(androidDir) {
55
69
  return null;
56
70
  }
57
71
  function ensureOutputDir(outputPath) {
58
- if (!existsSync(outputPath)) {
72
+ if (!existsSync2(outputPath)) {
59
73
  mkdirSync(outputPath, { recursive: true });
60
74
  }
61
75
  }
62
76
  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
77
  if (platform !== "ios" && platform !== "android" && platform !== "macos") {
64
- console.error(pc.red('Platform must be "ios", "android", or "macos"'));
65
- process.exit(1);
78
+ throw new ConfigError('Platform must be "ios", "android", or "macos"');
66
79
  }
67
80
  const cwd = process.cwd();
68
- const outputDir = join(cwd, options.output);
81
+ const outputDir = join2(cwd, options.output);
69
82
  const platformLabel = platform === "ios" ? "iOS" : platform === "android" ? "Android" : "macOS";
70
83
  console.log(pc.cyan(`
71
84
  Vue Native \u2014 ${options.mode.charAt(0).toUpperCase() + options.mode.slice(1)} Build (${platformLabel})
@@ -75,8 +88,7 @@ var buildCommand = new Command("build").description("Create a release build of t
75
88
  execSync("bun run vite build --mode production", { cwd, stdio: "inherit" });
76
89
  console.log(pc.green(" \u2713 Bundle built\n"));
77
90
  } catch {
78
- console.error(pc.red(" \u2717 Bundle build failed"));
79
- process.exit(1);
91
+ throw new ConfigError("Bundle build failed");
80
92
  }
81
93
  if (platform === "ios") {
82
94
  buildIOS(cwd, outputDir, options);
@@ -87,7 +99,7 @@ var buildCommand = new Command("build").description("Create a release build of t
87
99
  }
88
100
  });
89
101
  function buildIOS(cwd, outputDir, options) {
90
- const iosDir = join(cwd, "ios");
102
+ const iosDir = join2(cwd, "ios");
91
103
  const project = findXcodeProject(iosDir);
92
104
  if (!project) {
93
105
  console.log(pc.yellow(" No Xcode project found in ./ios/"));
@@ -98,7 +110,7 @@ function buildIOS(cwd, outputDir, options) {
98
110
  const projectFlag = project.isWorkspace ? "-workspace" : "-project";
99
111
  const scheme = options.scheme || project.path.split("/").pop()?.replace(/\.(xcworkspace|xcodeproj)$/, "") || "App";
100
112
  const configuration = options.mode === "release" ? "Release" : "Debug";
101
- const archivePath = join(outputDir, `${scheme}.xcarchive`);
113
+ const archivePath = join2(outputDir, `${scheme}.xcarchive`);
102
114
  ensureOutputDir(outputDir);
103
115
  console.log(pc.white(` Archiving ${scheme} (${configuration})...`));
104
116
  console.log(pc.dim(` Archive path: ${archivePath}`));
@@ -152,10 +164,10 @@ function buildIOS(cwd, outputDir, options) {
152
164
  xcodebuild.on("close", (code) => {
153
165
  if (code !== 0) {
154
166
  console.error(pc.red(` \u2717 Archive failed (exit code ${code})`));
155
- process.exit(1);
167
+ throw new ConfigError(`iOS archive failed with exit code ${code}`);
156
168
  }
157
169
  console.log(pc.green(" \u2713 Archive successful\n"));
158
- if (existsSync(archivePath)) {
170
+ if (existsSync2(archivePath)) {
159
171
  console.log(pc.green(` Archive: ${archivePath}`));
160
172
  console.log(pc.dim(" To export an IPA, open the archive in Xcode Organizer or run:"));
161
173
  console.log(pc.dim(` xcodebuild -exportArchive -archivePath "${archivePath}" -exportOptionsPlist ExportOptions.plist -exportPath "${outputDir}"
@@ -166,18 +178,18 @@ function buildIOS(cwd, outputDir, options) {
166
178
  });
167
179
  }
168
180
  function buildAndroid(cwd, outputDir, options) {
169
- const androidDir = join(cwd, "android");
170
- if (!existsSync(androidDir)) {
181
+ const androidDir = join2(cwd, "android");
182
+ if (!existsSync2(androidDir)) {
171
183
  console.log(pc.yellow(" No android/ directory found."));
172
184
  console.log(pc.dim(" To add Android support, create an Android project in the android/ directory."));
173
185
  console.log(pc.dim(" Bundle has been built to dist/vue-native-bundle.js\n"));
174
186
  return;
175
187
  }
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);
188
+ const gradlew = join2(androidDir, "gradlew");
189
+ if (!existsSync2(gradlew)) {
190
+ throw new ConfigError(
191
+ "gradlew not found in android/ directory. Make sure your Android project has the Gradle wrapper."
192
+ );
181
193
  }
182
194
  const gradleTask = options.aab ? "bundleRelease" : "assembleRelease";
183
195
  const artifactType = options.aab ? "AAB" : "APK";
@@ -219,13 +231,13 @@ function buildAndroid(cwd, outputDir, options) {
219
231
  gradle.on("close", (code) => {
220
232
  if (code !== 0) {
221
233
  console.error(pc.red(` \u2717 Gradle build failed (exit code ${code})`));
222
- process.exit(1);
234
+ throw new ConfigError(`Android Gradle build failed with exit code ${code}`);
223
235
  }
224
236
  console.log(pc.green(" \u2713 Build successful\n"));
225
237
  if (options.aab) {
226
238
  const aabPath = findReleaseAab(androidDir);
227
239
  if (aabPath) {
228
- const destPath = join(outputDir, basename(aabPath));
240
+ const destPath = join2(outputDir, basename(aabPath));
229
241
  copyFileSync(aabPath, destPath);
230
242
  console.log(pc.green(` AAB copied to: ${destPath}`));
231
243
  console.log(pc.dim(" Upload this file to the Google Play Console.\n"));
@@ -236,7 +248,7 @@ function buildAndroid(cwd, outputDir, options) {
236
248
  } else {
237
249
  const apkPath = findReleaseApk(androidDir);
238
250
  if (apkPath) {
239
- const destPath = join(outputDir, basename(apkPath));
251
+ const destPath = join2(outputDir, basename(apkPath));
240
252
  copyFileSync(apkPath, destPath);
241
253
  console.log(pc.green(` APK copied to: ${destPath}`));
242
254
  console.log(pc.dim(' Install with: adb install -r "' + destPath + '"\n'));
@@ -248,7 +260,7 @@ function buildAndroid(cwd, outputDir, options) {
248
260
  });
249
261
  }
250
262
  function buildMacOS(cwd, outputDir, options) {
251
- const macosDir = join(cwd, "macos");
263
+ const macosDir = join2(cwd, "macos");
252
264
  const project = findXcodeProject(macosDir);
253
265
  if (!project) {
254
266
  console.log(pc.yellow(" No Xcode project found in ./macos/"));
@@ -259,7 +271,7 @@ function buildMacOS(cwd, outputDir, options) {
259
271
  const projectFlag = project.isWorkspace ? "-workspace" : "-project";
260
272
  const scheme = options.scheme || project.path.split("/").pop()?.replace(/\.(xcworkspace|xcodeproj)$/, "") || "App";
261
273
  const configuration = options.mode === "release" ? "Release" : "Debug";
262
- const archivePath = join(outputDir, `${scheme}.xcarchive`);
274
+ const archivePath = join2(outputDir, `${scheme}.xcarchive`);
263
275
  ensureOutputDir(outputDir);
264
276
  console.log(pc.white(` Archiving ${scheme} (${configuration}) for macOS...`));
265
277
  console.log(pc.dim(` Archive path: ${archivePath}`));
@@ -313,10 +325,10 @@ function buildMacOS(cwd, outputDir, options) {
313
325
  xcodebuild.on("close", (code) => {
314
326
  if (code !== 0) {
315
327
  console.error(pc.red(` \u2717 Archive failed (exit code ${code})`));
316
- process.exit(1);
328
+ throw new ConfigError(`macOS archive failed with exit code ${code}`);
317
329
  }
318
330
  console.log(pc.green(" \u2713 Archive successful\n"));
319
- if (existsSync(archivePath)) {
331
+ if (existsSync2(archivePath)) {
320
332
  console.log(pc.green(` Archive: ${archivePath}`));
321
333
  console.log(pc.dim(" To export a .app or .pkg, open the archive in Xcode Organizer or run:"));
322
334
  console.log(pc.dim(` xcodebuild -exportArchive -archivePath "${archivePath}" -exportOptionsPlist ExportOptions.plist -exportPath "${outputDir}"
@@ -330,29 +342,54 @@ function buildMacOS(cwd, outputDir, options) {
330
342
  // src/commands/create.ts
331
343
  import { Command as Command2 } from "commander";
332
344
  import { cp, mkdir, writeFile } from "fs/promises";
333
- import { join as join2, dirname } from "path";
345
+ import { join as join3, dirname } from "path";
334
346
  import { fileURLToPath } from "url";
335
- import { existsSync as existsSync2 } from "fs";
347
+ import { existsSync as existsSync3, readFileSync } from "fs";
336
348
  import pc2 from "picocolors";
337
349
  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";
350
+ function getTemplateVersions() {
351
+ try {
352
+ const cliDir = dirname(dirname(fileURLToPath(import.meta.url)));
353
+ const pkg = JSON.parse(readFileSync(join3(cliDir, "package.json"), "utf8"));
354
+ const jsVersion = `^${pkg.version}`;
355
+ const viteDevDep = pkg.devDependencies?.vite;
356
+ const viteVuePluginDevDep = pkg.devDependencies?.["@vitejs/plugin-vue"];
357
+ return {
358
+ JS_PACKAGE_VERSION: jsVersion,
359
+ VITE_PLUGIN_VUE_VERSION: viteVuePluginDevDep ?? "^6.0.5",
360
+ VITE_VERSION: viteDevDep ?? "^8.0.0"
361
+ };
362
+ } catch {
363
+ return {
364
+ JS_PACKAGE_VERSION: `^${VERSION}`,
365
+ VITE_PLUGIN_VUE_VERSION: "^6.0.5",
366
+ VITE_VERSION: "^8.0.0"
367
+ };
368
+ }
369
+ }
370
+ var { JS_PACKAGE_VERSION, VITE_PLUGIN_VUE_VERSION, VITE_VERSION } = getTemplateVersions();
371
+ var VALID_NAME = /^[a-zA-Z0-9_-]+$/i;
341
372
  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
373
  const template = options.template;
343
374
  if (!["blank", "tabs", "drawer"].includes(template)) {
344
- console.error(pc2.red(`Invalid template "${template}". Choose: blank, tabs, drawer`));
345
- process.exit(1);
375
+ throw new ConfigError(
376
+ `Invalid template "${template}". Choose: blank, tabs, drawer`
377
+ );
346
378
  }
347
- const dir = join2(process.cwd(), name);
379
+ if (!VALID_NAME.test(name)) {
380
+ throw new ConfigError(
381
+ `Project name "${name}" must match /^[a-zA-Z0-9_-]+$/i`
382
+ );
383
+ }
384
+ const dir = join3(process.cwd(), name);
348
385
  console.log(pc2.cyan(`
349
386
  Creating Vue Native project: ${pc2.bold(name)} (template: ${template})
350
387
  `));
351
388
  try {
352
389
  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({
390
+ await mkdir(join3(dir, "app"), { recursive: true });
391
+ await mkdir(join3(dir, "app", "pages"), { recursive: true });
392
+ await writeFile(join3(dir, "package.json"), JSON.stringify({
356
393
  name,
357
394
  version: "0.0.1",
358
395
  private: true,
@@ -374,7 +411,7 @@ Creating Vue Native project: ${pc2.bold(name)} (template: ${template})
374
411
  "typescript": "^5.7.0"
375
412
  }
376
413
  }, null, 2));
377
- await writeFile(join2(dir, "vite.config.ts"), `import { defineConfig } from 'vite'
414
+ await writeFile(join3(dir, "vite.config.ts"), `import { defineConfig } from 'vite'
378
415
  import vue from '@vitejs/plugin-vue'
379
416
  import vueNative from '@thelacanians/vue-native-vite-plugin'
380
417
 
@@ -382,7 +419,7 @@ export default defineConfig({
382
419
  plugins: [vue(), vueNative()],
383
420
  })
384
421
  `);
385
- await writeFile(join2(dir, "tsconfig.json"), JSON.stringify({
422
+ await writeFile(join3(dir, "tsconfig.json"), JSON.stringify({
386
423
  compilerOptions: {
387
424
  target: "ES2020",
388
425
  module: "ESNext",
@@ -398,12 +435,12 @@ export default defineConfig({
398
435
  include: ["app/**/*", "env.d.ts"]
399
436
  }, null, 2));
400
437
  await generateTemplateFiles(dir, name, template);
401
- const iosDir = join2(dir, "ios");
402
- const iosSrcDir = join2(iosDir, "Sources");
438
+ const iosDir = join3(dir, "ios");
439
+ const iosSrcDir = join3(iosDir, "Sources");
403
440
  await mkdir(iosSrcDir, { recursive: true });
404
441
  const xcodeProjectName = name.replace(/[^a-zA-Z0-9]/g, "");
405
442
  const bundleId = `com.vuenative.${name.replace(/[^a-zA-Z0-9]/g, "").toLowerCase()}`;
406
- await writeFile(join2(iosDir, "project.yml"), `name: ${xcodeProjectName}
443
+ await writeFile(join3(iosDir, "project.yml"), `name: ${xcodeProjectName}
407
444
  options:
408
445
  bundleIdPrefix: com.vuenative
409
446
  deploymentTarget:
@@ -434,7 +471,7 @@ targets:
434
471
  - path: ../dist/vue-native-bundle.js
435
472
  optional: true
436
473
  `);
437
- await writeFile(join2(iosSrcDir, "Info.plist"), `<?xml version="1.0" encoding="UTF-8"?>
474
+ await writeFile(join3(iosSrcDir, "Info.plist"), `<?xml version="1.0" encoding="UTF-8"?>
438
475
  <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
439
476
  <plist version="1.0">
440
477
  <dict>
@@ -500,7 +537,7 @@ targets:
500
537
  </dict>
501
538
  </plist>
502
539
  `);
503
- await writeFile(join2(iosSrcDir, "AppDelegate.swift"), `import UIKit
540
+ await writeFile(join3(iosSrcDir, "AppDelegate.swift"), `import UIKit
504
541
 
505
542
  @main
506
543
  class AppDelegate: UIResponder, UIApplicationDelegate {
@@ -524,7 +561,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
524
561
  }
525
562
  }
526
563
  `);
527
- await writeFile(join2(iosSrcDir, "SceneDelegate.swift"), `import UIKit
564
+ await writeFile(join3(iosSrcDir, "SceneDelegate.swift"), `import UIKit
528
565
  import VueNativeCore
529
566
 
530
567
  class SceneDelegate: UIResponder, UIWindowSceneDelegate {
@@ -552,29 +589,29 @@ class AppViewController: VueNativeViewController {
552
589
  #endif
553
590
  }
554
591
  `);
555
- const androidDir = join2(dir, "android");
556
- const androidAppDir = join2(androidDir, "app");
592
+ const androidDir = join3(dir, "android");
593
+ const androidAppDir = join3(androidDir, "app");
557
594
  const androidPkg = `com.vuenative.${name.replace(/[^a-zA-Z0-9]/g, "").toLowerCase()}`;
558
595
  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");
596
+ const androidSrcDir = join3(androidAppDir, "src", "main");
597
+ const androidKotlinDir = join3(androidSrcDir, "kotlin", androidPkgPath);
598
+ const androidResValuesDir = join3(androidSrcDir, "res", "values");
599
+ const androidResXmlDir = join3(androidSrcDir, "res", "xml");
600
+ const androidDebugResXmlDir = join3(androidAppDir, "src", "debug", "res", "xml");
601
+ const androidGradleWrapperDir = join3(androidDir, "gradle", "wrapper");
565
602
  await mkdir(androidKotlinDir, { recursive: true });
566
603
  await mkdir(androidResValuesDir, { recursive: true });
567
604
  await mkdir(androidResXmlDir, { recursive: true });
568
605
  await mkdir(androidDebugResXmlDir, { recursive: true });
569
606
  await mkdir(androidGradleWrapperDir, { recursive: true });
570
- await writeFile(join2(androidDir, "build.gradle.kts"), `// Top-level build file
607
+ await writeFile(join3(androidDir, "build.gradle.kts"), `// Top-level build file
571
608
  plugins {
572
609
  id("com.android.application") version "8.7.3" apply false
573
610
  id("com.android.library") version "8.7.3" apply false
574
611
  id("org.jetbrains.kotlin.android") version "2.0.21" apply false
575
612
  }
576
613
  `);
577
- await writeFile(join2(androidDir, "settings.gradle.kts"), `pluginManagement {
614
+ await writeFile(join3(androidDir, "settings.gradle.kts"), `pluginManagement {
578
615
  repositories {
579
616
  google()
580
617
  mavenCentral()
@@ -600,7 +637,7 @@ dependencyResolutionManagement {
600
637
  rootProject.name = "${name}"
601
638
  include(":app")
602
639
  `);
603
- await writeFile(join2(androidAppDir, "build.gradle.kts"), `plugins {
640
+ await writeFile(join3(androidAppDir, "build.gradle.kts"), `plugins {
604
641
  id("com.android.application")
605
642
  id("org.jetbrains.kotlin.android")
606
643
  }
@@ -648,13 +685,13 @@ dependencies {
648
685
  implementation("androidx.core:core-ktx:1.15.0")
649
686
  }
650
687
  `);
651
- await writeFile(join2(androidAppDir, "proguard-rules.pro"), `# Vue Native
688
+ await writeFile(join3(androidAppDir, "proguard-rules.pro"), `# Vue Native
652
689
  -keep class com.vuenative.** { *; }
653
690
 
654
691
  # J2V8
655
692
  -keep class com.eclipsesource.v8.** { *; }
656
693
  `);
657
- await writeFile(join2(androidSrcDir, "AndroidManifest.xml"), `<?xml version="1.0" encoding="utf-8"?>
694
+ await writeFile(join3(androidSrcDir, "AndroidManifest.xml"), `<?xml version="1.0" encoding="utf-8"?>
658
695
  <manifest xmlns:android="http://schemas.android.com/apk/res/android">
659
696
  <uses-permission android:name="android.permission.INTERNET" />
660
697
 
@@ -677,12 +714,12 @@ dependencies {
677
714
  </application>
678
715
  </manifest>
679
716
  `);
680
- await writeFile(join2(androidResValuesDir, "strings.xml"), `<?xml version="1.0" encoding="utf-8"?>
717
+ await writeFile(join3(androidResValuesDir, "strings.xml"), `<?xml version="1.0" encoding="utf-8"?>
681
718
  <resources>
682
719
  <string name="app_name">${name}</string>
683
720
  </resources>
684
721
  `);
685
- await writeFile(join2(androidResValuesDir, "themes.xml"), `<?xml version="1.0" encoding="utf-8"?>
722
+ await writeFile(join3(androidResValuesDir, "themes.xml"), `<?xml version="1.0" encoding="utf-8"?>
686
723
  <resources>
687
724
  <style name="Theme.VueNative" parent="Theme.MaterialComponents.Light.NoActionBar">
688
725
  <item name="colorPrimary">#4F46E5</item>
@@ -695,12 +732,12 @@ dependencies {
695
732
  </style>
696
733
  </resources>
697
734
  `);
698
- await writeFile(join2(androidResXmlDir, "network_security_config.xml"), `<?xml version="1.0" encoding="utf-8"?>
735
+ await writeFile(join3(androidResXmlDir, "network_security_config.xml"), `<?xml version="1.0" encoding="utf-8"?>
699
736
  <network-security-config>
700
737
  <base-config cleartextTrafficPermitted="false" />
701
738
  </network-security-config>
702
739
  `);
703
- await writeFile(join2(androidDebugResXmlDir, "network_security_config.xml"), `<?xml version="1.0" encoding="utf-8"?>
740
+ await writeFile(join3(androidDebugResXmlDir, "network_security_config.xml"), `<?xml version="1.0" encoding="utf-8"?>
704
741
  <network-security-config>
705
742
  <domain-config cleartextTrafficPermitted="true">
706
743
  <domain includeSubdomains="true">localhost</domain>
@@ -709,7 +746,7 @@ dependencies {
709
746
  </domain-config>
710
747
  </network-security-config>
711
748
  `);
712
- await writeFile(join2(androidKotlinDir, "MainActivity.kt"), `package ${androidPkg}
749
+ await writeFile(join3(androidKotlinDir, "MainActivity.kt"), `package ${androidPkg}
713
750
 
714
751
  import com.vuenative.core.VueNativeActivity
715
752
 
@@ -723,20 +760,20 @@ class MainActivity : VueNativeActivity() {
723
760
  }
724
761
  }
725
762
  `);
726
- await writeFile(join2(androidDir, "gradle.properties"), `# Project-wide Gradle settings
763
+ await writeFile(join3(androidDir, "gradle.properties"), `# Project-wide Gradle settings
727
764
  org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
728
765
  android.useAndroidX=true
729
766
  kotlin.code.style=official
730
767
  android.nonTransitiveRClass=true
731
768
  `);
732
- await writeFile(join2(androidGradleWrapperDir, "gradle-wrapper.properties"), `distributionBase=GRADLE_USER_HOME
769
+ await writeFile(join3(androidGradleWrapperDir, "gradle-wrapper.properties"), `distributionBase=GRADLE_USER_HOME
733
770
  distributionPath=wrapper/dists
734
771
  distributionUrl=https\\://services.gradle.org/distributions/gradle-8.11.1-bin.zip
735
772
  networkTimeout=10000
736
773
  zipStoreBase=GRADLE_USER_HOME
737
774
  zipStorePath=wrapper/dists
738
775
  `);
739
- await writeFile(join2(dir, "vue-native.config.ts"), `import { defineConfig } from '@thelacanians/vue-native-cli'
776
+ await writeFile(join3(dir, "vue-native.config.ts"), `import { defineConfig } from '@thelacanians/vue-native-cli'
740
777
 
741
778
  export default defineConfig({
742
779
  name: '${name}',
@@ -751,7 +788,7 @@ export default defineConfig({
751
788
  },
752
789
  })
753
790
  `);
754
- await writeFile(join2(dir, "env.d.ts"), `/// <reference types="vite/client" />
791
+ await writeFile(join3(dir, "env.d.ts"), `/// <reference types="vite/client" />
755
792
  declare module '*.vue' {
756
793
  import type { DefineComponent } from '@thelacanians/vue-native-runtime'
757
794
  const component: DefineComponent<{}, {}, any>
@@ -759,7 +796,7 @@ declare module '*.vue' {
759
796
  }
760
797
  declare const __DEV__: boolean
761
798
  `);
762
- await writeFile(join2(dir, ".gitignore"), `node_modules/
799
+ await writeFile(join3(dir, ".gitignore"), `node_modules/
763
800
  dist/
764
801
  *.xcuserstate
765
802
  *.xcuserdatad/
@@ -782,9 +819,9 @@ local.properties
782
819
  *.jks
783
820
  `);
784
821
  const cliDir = dirname(dirname(fileURLToPath(import.meta.url)));
785
- const bundledNative = join2(cliDir, "native");
786
- if (existsSync2(bundledNative)) {
787
- const nativeDir = join2(dir, "native");
822
+ const bundledNative = join3(cliDir, "native");
823
+ if (existsSync3(bundledNative)) {
824
+ const nativeDir = join3(dir, "native");
788
825
  await cp(bundledNative, nativeDir, { recursive: true });
789
826
  console.log(pc2.dim(" Bundled native/ copied as fallback.\n"));
790
827
  }
@@ -800,12 +837,13 @@ local.properties
800
837
  console.log(pc2.dim(" cd android && gradle wrapper && cd .."));
801
838
  console.log(pc2.white(" vue-native run android\n"));
802
839
  } catch (err) {
803
- console.error(pc2.red(`Error creating project: ${err.message}`));
804
- process.exit(1);
840
+ throw new ConfigError(
841
+ `Error creating project: ${err.message}`
842
+ );
805
843
  }
806
844
  });
807
845
  async function generateTemplateFiles(dir, name, template) {
808
- const pagesDir = join2(dir, "app", "pages");
846
+ const pagesDir = join3(dir, "app", "pages");
809
847
  if (template === "blank") {
810
848
  await generateBlankTemplate(dir, pagesDir);
811
849
  } else if (template === "tabs") {
@@ -815,7 +853,7 @@ async function generateTemplateFiles(dir, name, template) {
815
853
  }
816
854
  }
817
855
  async function generateBlankTemplate(dir, pagesDir) {
818
- await writeFile(join2(dir, "app", "main.ts"), `import { createApp } from '@thelacanians/vue-native-runtime'
856
+ await writeFile(join3(dir, "app", "main.ts"), `import { createApp } from '@thelacanians/vue-native-runtime'
819
857
  import { createRouter } from '@thelacanians/vue-native-navigation'
820
858
  import App from './App.vue'
821
859
  import Home from './pages/Home.vue'
@@ -828,7 +866,7 @@ const app = createApp(App)
828
866
  app.use(router)
829
867
  app.start()
830
868
  `);
831
- await writeFile(join2(dir, "app", "App.vue"), `<template>
869
+ await writeFile(join3(dir, "app", "App.vue"), `<template>
832
870
  <VSafeArea :style="{ flex: 1, backgroundColor: '#ffffff' }">
833
871
  <RouterView />
834
872
  </VSafeArea>
@@ -838,7 +876,7 @@ app.start()
838
876
  import { RouterView } from '@thelacanians/vue-native-navigation'
839
877
  </script>
840
878
  `);
841
- await writeFile(join2(pagesDir, "Home.vue"), `<script setup lang="ts">
879
+ await writeFile(join3(pagesDir, "Home.vue"), `<script setup lang="ts">
842
880
  import { ref } from 'vue'
843
881
  import { createStyleSheet } from '@thelacanians/vue-native-runtime'
844
882
 
@@ -890,13 +928,13 @@ const styles = createStyleSheet({
890
928
  `);
891
929
  }
892
930
  async function generateTabsTemplate(dir, pagesDir) {
893
- await writeFile(join2(dir, "app", "main.ts"), `import { createApp } from '@thelacanians/vue-native-runtime'
931
+ await writeFile(join3(dir, "app", "main.ts"), `import { createApp } from '@thelacanians/vue-native-runtime'
894
932
  import App from './App.vue'
895
933
 
896
934
  const app = createApp(App)
897
935
  app.start()
898
936
  `);
899
- await writeFile(join2(dir, "app", "App.vue"), `<script setup lang="ts">
937
+ await writeFile(join3(dir, "app", "App.vue"), `<script setup lang="ts">
900
938
  import { createTabNavigator } from '@thelacanians/vue-native-navigation'
901
939
  import Home from './pages/Home.vue'
902
940
  import Settings from './pages/Settings.vue'
@@ -915,7 +953,7 @@ const { TabNavigator } = createTabNavigator()
915
953
  </VSafeArea>
916
954
  </template>
917
955
  `);
918
- await writeFile(join2(pagesDir, "Home.vue"), `<script setup lang="ts">
956
+ await writeFile(join3(pagesDir, "Home.vue"), `<script setup lang="ts">
919
957
  import { ref } from 'vue'
920
958
  import { createStyleSheet } from '@thelacanians/vue-native-runtime'
921
959
 
@@ -959,7 +997,7 @@ const styles = createStyleSheet({
959
997
  </VView>
960
998
  </template>
961
999
  `);
962
- await writeFile(join2(pagesDir, "Settings.vue"), `<script setup lang="ts">
1000
+ await writeFile(join3(pagesDir, "Settings.vue"), `<script setup lang="ts">
963
1001
  import { ref } from 'vue'
964
1002
  import { createStyleSheet } from '@thelacanians/vue-native-runtime'
965
1003
 
@@ -1003,13 +1041,13 @@ const styles = createStyleSheet({
1003
1041
  `);
1004
1042
  }
1005
1043
  async function generateDrawerTemplate(dir, pagesDir) {
1006
- await writeFile(join2(dir, "app", "main.ts"), `import { createApp } from '@thelacanians/vue-native-runtime'
1044
+ await writeFile(join3(dir, "app", "main.ts"), `import { createApp } from '@thelacanians/vue-native-runtime'
1007
1045
  import App from './App.vue'
1008
1046
 
1009
1047
  const app = createApp(App)
1010
1048
  app.start()
1011
1049
  `);
1012
- await writeFile(join2(dir, "app", "App.vue"), `<script setup lang="ts">
1050
+ await writeFile(join3(dir, "app", "App.vue"), `<script setup lang="ts">
1013
1051
  import { createDrawerNavigator } from '@thelacanians/vue-native-navigation'
1014
1052
  import Home from './pages/Home.vue'
1015
1053
  import About from './pages/About.vue'
@@ -1028,7 +1066,7 @@ const { DrawerNavigator } = createDrawerNavigator()
1028
1066
  </VSafeArea>
1029
1067
  </template>
1030
1068
  `);
1031
- await writeFile(join2(pagesDir, "Home.vue"), `<script setup lang="ts">
1069
+ await writeFile(join3(pagesDir, "Home.vue"), `<script setup lang="ts">
1032
1070
  import { createStyleSheet } from '@thelacanians/vue-native-runtime'
1033
1071
  import { useDrawer } from '@thelacanians/vue-native-navigation'
1034
1072
 
@@ -1081,7 +1119,7 @@ const styles = createStyleSheet({
1081
1119
  </VView>
1082
1120
  </template>
1083
1121
  `);
1084
- await writeFile(join2(pagesDir, "About.vue"), `<script setup lang="ts">
1122
+ await writeFile(join3(pagesDir, "About.vue"), `<script setup lang="ts">
1085
1123
  import { createStyleSheet } from '@thelacanians/vue-native-runtime'
1086
1124
  import { useDrawer } from '@thelacanians/vue-native-navigation'
1087
1125
 
@@ -1140,8 +1178,8 @@ const styles = createStyleSheet({
1140
1178
  import { Command as Command3 } from "commander";
1141
1179
  import { spawn as spawn2, execSync as execSync2 } from "child_process";
1142
1180
  import { readFile } from "fs/promises";
1143
- import { existsSync as existsSync3 } from "fs";
1144
- import { join as join3 } from "path";
1181
+ import { existsSync as existsSync4 } from "fs";
1182
+ import { join as join4 } from "path";
1145
1183
  import { watch } from "chokidar";
1146
1184
  import { WebSocketServer, WebSocket } from "ws";
1147
1185
  import pc3 from "picocolors";
@@ -1194,7 +1232,7 @@ function detectAndroidEmulators() {
1194
1232
  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
1233
  const port = parseInt(options.port, 10);
1196
1234
  const cwd = process.cwd();
1197
- const bundlePath = join3(cwd, BUNDLE_FILE);
1235
+ const bundlePath = join4(cwd, BUNDLE_FILE);
1198
1236
  console.log(pc3.cyan("\n Vue Native Dev Server\n"));
1199
1237
  if (options.ios) {
1200
1238
  console.log(pc3.white(" Detecting iOS Simulators..."));
@@ -1290,15 +1328,15 @@ var devCommand = new Command3("dev").description("Start the Vue Native dev serve
1290
1328
  if (lanIP) {
1291
1329
  console.log(pc3.white(` LAN address: ${pc3.bold(`ws://${lanIP}:${port}`)}`));
1292
1330
  }
1293
- const iosDir = join3(cwd, "ios");
1294
- const androidDir = join3(cwd, "android");
1295
- if (existsSync3(iosDir)) {
1331
+ const iosDir = join4(cwd, "ios");
1332
+ const androidDir = join4(cwd, "android");
1333
+ if (existsSync4(iosDir)) {
1296
1334
  console.log(pc3.dim(` iOS Simulator: ws://localhost:${port}`));
1297
1335
  if (lanIP) {
1298
1336
  console.log(pc3.dim(` iOS Device (WiFi): ws://${lanIP}:${port}`));
1299
1337
  }
1300
1338
  }
1301
- if (existsSync3(androidDir)) {
1339
+ if (existsSync4(androidDir)) {
1302
1340
  console.log(pc3.dim(` Android emulator: ws://10.0.2.2:${port}`));
1303
1341
  if (lanIP) {
1304
1342
  console.log(pc3.dim(` Android Device: ws://${lanIP}:${port}`));
@@ -1362,28 +1400,28 @@ var devCommand = new Command3("dev").description("Start the Vue Native dev serve
1362
1400
  // src/commands/run.ts
1363
1401
  import { Command as Command4 } from "commander";
1364
1402
  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";
1403
+ import { existsSync as existsSync5, readdirSync as readdirSync2, readFileSync as readFileSync2 } from "fs";
1404
+ import { join as join5 } from "path";
1367
1405
  import pc4 from "picocolors";
1368
1406
  function findAppPath(_buildDir) {
1369
- const derivedDataBase = join4(
1407
+ const derivedDataBase = join5(
1370
1408
  process.env.HOME || "~",
1371
1409
  "Library/Developer/Xcode/DerivedData"
1372
1410
  );
1373
- if (existsSync4(derivedDataBase)) {
1411
+ if (existsSync5(derivedDataBase)) {
1374
1412
  try {
1375
1413
  const projects = readdirSync2(derivedDataBase);
1376
1414
  for (const project of projects.reverse()) {
1377
- const productsDir = join4(
1415
+ const productsDir = join5(
1378
1416
  derivedDataBase,
1379
1417
  project,
1380
1418
  "Build/Products/Debug-iphonesimulator"
1381
1419
  );
1382
- if (existsSync4(productsDir)) {
1420
+ if (existsSync5(productsDir)) {
1383
1421
  const entries = readdirSync2(productsDir);
1384
1422
  const app = entries.find((e) => e.endsWith(".app"));
1385
1423
  if (app) {
1386
- return join4(productsDir, app);
1424
+ return join5(productsDir, app);
1387
1425
  }
1388
1426
  }
1389
1427
  }
@@ -1393,10 +1431,10 @@ function findAppPath(_buildDir) {
1393
1431
  return null;
1394
1432
  }
1395
1433
  function readBundleId(iosDir) {
1396
- const plistPath = join4(iosDir, "Sources", "Info.plist");
1397
- if (existsSync4(plistPath)) {
1434
+ const plistPath = join5(iosDir, "Sources", "Info.plist");
1435
+ if (existsSync5(plistPath)) {
1398
1436
  try {
1399
- const content = readFileSync(plistPath, "utf8");
1437
+ const content = readFileSync2(plistPath, "utf8");
1400
1438
  const match = content.match(
1401
1439
  /<key>CFBundleIdentifier<\/key>\s*<string>([^<]+)<\/string>/
1402
1440
  );
@@ -1409,13 +1447,13 @@ function readBundleId(iosDir) {
1409
1447
  return "com.vuenative.app";
1410
1448
  }
1411
1449
  function findApkPath(androidDir) {
1412
- const apkDir = join4(androidDir, "app", "build", "outputs", "apk", "debug");
1413
- if (existsSync4(apkDir)) {
1450
+ const apkDir = join5(androidDir, "app", "build", "outputs", "apk", "debug");
1451
+ if (existsSync5(apkDir)) {
1414
1452
  try {
1415
1453
  const entries = readdirSync2(apkDir);
1416
1454
  const apk = entries.find((e) => e.endsWith(".apk") && !e.includes("androidTest"));
1417
1455
  if (apk) {
1418
- return join4(apkDir, apk);
1456
+ return join5(apkDir, apk);
1419
1457
  }
1420
1458
  } catch {
1421
1459
  }
@@ -1424,8 +1462,7 @@ function findApkPath(androidDir) {
1424
1462
  }
1425
1463
  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
1464
  if (platform !== "ios" && platform !== "android" && platform !== "macos") {
1427
- console.error(pc4.red('Platform must be "ios", "android", or "macos"'));
1428
- process.exit(1);
1465
+ throw new ConfigError('Platform must be "ios", "android", or "macos"');
1429
1466
  }
1430
1467
  const cwd = process.cwd();
1431
1468
  const platformLabel = platform === "ios" ? "iOS" : platform === "android" ? "Android" : "macOS";
@@ -1437,8 +1474,7 @@ var runCommand = new Command4("run").description("Build and run the app").argume
1437
1474
  execSync3("bun run vite build", { cwd, stdio: "inherit" });
1438
1475
  console.log(pc4.green(" \u2713 Bundle built\n"));
1439
1476
  } catch {
1440
- console.error(pc4.red(" \u2717 Bundle build failed"));
1441
- process.exit(1);
1477
+ throw new ConfigError("Bundle build failed");
1442
1478
  }
1443
1479
  if (platform === "ios") {
1444
1480
  runIOS(cwd, options);
@@ -1450,14 +1486,14 @@ var runCommand = new Command4("run").description("Build and run the app").argume
1450
1486
  });
1451
1487
  function runIOS(cwd, options) {
1452
1488
  let xcodeProject = null;
1453
- const iosDir = join4(cwd, "ios");
1454
- if (existsSync4(iosDir)) {
1489
+ const iosDir = join5(cwd, "ios");
1490
+ if (existsSync5(iosDir)) {
1455
1491
  for (const ext of [".xcworkspace", ".xcodeproj"]) {
1456
1492
  try {
1457
1493
  const entries = readdirSync2(iosDir);
1458
1494
  const match = entries.find((e) => e.endsWith(ext));
1459
1495
  if (match) {
1460
- xcodeProject = join4(iosDir, match);
1496
+ xcodeProject = join5(iosDir, match);
1461
1497
  break;
1462
1498
  }
1463
1499
  } catch {
@@ -1493,7 +1529,7 @@ function runIOS(cwd, options) {
1493
1529
  xcodebuild.on("close", (code) => {
1494
1530
  if (code !== 0) {
1495
1531
  console.error(pc4.red(` \u2717 Build failed (exit code ${code})`));
1496
- process.exit(1);
1532
+ throw new ConfigError(`iOS build failed with exit code ${code}`);
1497
1533
  }
1498
1534
  console.log(pc4.green(" \u2713 Build successful\n"));
1499
1535
  if (options.device) {
@@ -1501,7 +1537,7 @@ function runIOS(cwd, options) {
1501
1537
  return;
1502
1538
  }
1503
1539
  const simulatorName = options.simulator;
1504
- const bundleId = options.bundleId || readBundleId(join4(cwd, "ios"));
1540
+ const bundleId = options.bundleId || readBundleId(join5(cwd, "ios"));
1505
1541
  console.log(pc4.white(` Booting simulator "${simulatorName}"...`));
1506
1542
  try {
1507
1543
  execSync3(`xcrun simctl boot "${simulatorName}"`, { stdio: "pipe" });
@@ -1511,7 +1547,7 @@ function runIOS(cwd, options) {
1511
1547
  execSync3("open -a Simulator", { stdio: "pipe" });
1512
1548
  } catch {
1513
1549
  }
1514
- const appPath = findAppPath(join4(cwd, "ios"));
1550
+ const appPath = findAppPath(join5(cwd, "ios"));
1515
1551
  if (appPath) {
1516
1552
  console.log(pc4.white(` Installing app on simulator...`));
1517
1553
  try {
@@ -1519,7 +1555,7 @@ function runIOS(cwd, options) {
1519
1555
  console.log(pc4.green(" \u2713 App installed"));
1520
1556
  } catch (err) {
1521
1557
  console.error(pc4.red(` \u2717 Failed to install app: ${err.message}`));
1522
- process.exit(1);
1558
+ throw new ConfigError(`iOS app install failed: ${err.message}`);
1523
1559
  }
1524
1560
  console.log(pc4.white(` Launching ${bundleId}...`));
1525
1561
  try {
@@ -1528,7 +1564,7 @@ function runIOS(cwd, options) {
1528
1564
  `));
1529
1565
  } catch (err) {
1530
1566
  console.error(pc4.red(` \u2717 Failed to launch app: ${err.message}`));
1531
- process.exit(1);
1567
+ throw new ConfigError(`iOS app launch failed: ${err.message}`);
1532
1568
  }
1533
1569
  } else {
1534
1570
  console.log(pc4.yellow(" Could not locate .app bundle in DerivedData."));
@@ -1537,18 +1573,18 @@ function runIOS(cwd, options) {
1537
1573
  });
1538
1574
  }
1539
1575
  function runAndroid(cwd, options) {
1540
- const androidDir = join4(cwd, "android");
1541
- if (!existsSync4(androidDir)) {
1576
+ const androidDir = join5(cwd, "android");
1577
+ if (!existsSync5(androidDir)) {
1542
1578
  console.log(pc4.yellow(" No android/ directory found."));
1543
1579
  console.log(pc4.dim(" To add Android support, create an Android project in the android/ directory."));
1544
1580
  console.log(pc4.dim(" Bundle has been built to dist/vue-native-bundle.js\n"));
1545
1581
  return;
1546
1582
  }
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);
1583
+ const gradlew = join5(androidDir, "gradlew");
1584
+ if (!existsSync5(gradlew)) {
1585
+ throw new ConfigError(
1586
+ "gradlew not found in android/ directory. Make sure your Android project has the Gradle wrapper."
1587
+ );
1552
1588
  }
1553
1589
  console.log(pc4.white(" Building Android app with Gradle..."));
1554
1590
  const gradle = spawn3(
@@ -1587,7 +1623,7 @@ function runAndroid(cwd, options) {
1587
1623
  gradle.on("close", (code) => {
1588
1624
  if (code !== 0) {
1589
1625
  console.error(pc4.red(` \u2717 Gradle build failed (exit code ${code})`));
1590
- process.exit(1);
1626
+ throw new ConfigError(`Android Gradle build failed with exit code ${code}`);
1591
1627
  }
1592
1628
  console.log(pc4.green(" \u2713 Build successful\n"));
1593
1629
  const apkPath = findApkPath(androidDir);
@@ -1603,7 +1639,7 @@ function runAndroid(cwd, options) {
1603
1639
  } catch (err) {
1604
1640
  console.error(pc4.red(` \u2717 Failed to install APK: ${err.message}`));
1605
1641
  console.log(pc4.dim(" Make sure an emulator is running or a device is connected (adb devices).\n"));
1606
- process.exit(1);
1642
+ throw new ConfigError(`APK install failed: ${err.message}`);
1607
1643
  }
1608
1644
  const componentName = `${options.package}/${options.activity}`;
1609
1645
  console.log(pc4.white(` Launching ${componentName}...`));
@@ -1613,13 +1649,13 @@ function runAndroid(cwd, options) {
1613
1649
  `));
1614
1650
  } catch (err) {
1615
1651
  console.error(pc4.red(` \u2717 Failed to launch app: ${err.message}`));
1616
- process.exit(1);
1652
+ throw new ConfigError(`App launch failed: ${err.message}`);
1617
1653
  }
1618
1654
  });
1619
1655
  }
1620
1656
  function runMacOS(cwd, options) {
1621
- const macosDir = join4(cwd, "macos");
1622
- if (!existsSync4(macosDir)) {
1657
+ const macosDir = join5(cwd, "macos");
1658
+ if (!existsSync5(macosDir)) {
1623
1659
  console.log(pc4.yellow(" No macos/ directory found."));
1624
1660
  console.log(pc4.dim(" To add macOS support, create an Xcode project in the macos/ directory."));
1625
1661
  console.log(pc4.dim(" Bundle has been built to dist/vue-native-bundle.js\n"));
@@ -1631,7 +1667,7 @@ function runMacOS(cwd, options) {
1631
1667
  const entries = readdirSync2(macosDir);
1632
1668
  const match = entries.find((e) => e.endsWith(ext));
1633
1669
  if (match) {
1634
- xcodeProject = join4(macosDir, match);
1670
+ xcodeProject = join5(macosDir, match);
1635
1671
  break;
1636
1672
  }
1637
1673
  } catch {
@@ -1663,21 +1699,21 @@ function runMacOS(cwd, options) {
1663
1699
  xcodebuild.on("close", (code) => {
1664
1700
  if (code !== 0) {
1665
1701
  console.error(pc4.red(` \u2717 Build failed (exit code ${code})`));
1666
- process.exit(1);
1702
+ throw new ConfigError(`macOS build failed with exit code ${code}`);
1667
1703
  }
1668
1704
  console.log(pc4.green(" \u2713 Build successful\n"));
1669
- const derivedDataBase = join4(process.env.HOME || "~", "Library/Developer/Xcode/DerivedData");
1705
+ const derivedDataBase = join5(process.env.HOME || "~", "Library/Developer/Xcode/DerivedData");
1670
1706
  let appPath = null;
1671
- if (existsSync4(derivedDataBase)) {
1707
+ if (existsSync5(derivedDataBase)) {
1672
1708
  try {
1673
1709
  const projects = readdirSync2(derivedDataBase);
1674
1710
  for (const project of projects.reverse()) {
1675
- const productsDir = join4(derivedDataBase, project, "Build/Products/Debug");
1676
- if (existsSync4(productsDir)) {
1711
+ const productsDir = join5(derivedDataBase, project, "Build/Products/Debug");
1712
+ if (existsSync5(productsDir)) {
1677
1713
  const entries = readdirSync2(productsDir);
1678
1714
  const app = entries.find((e) => e.endsWith(".app"));
1679
1715
  if (app) {
1680
- appPath = join4(productsDir, app);
1716
+ appPath = join5(productsDir, app);
1681
1717
  break;
1682
1718
  }
1683
1719
  }
@@ -1703,18 +1739,18 @@ function runMacOS(cwd, options) {
1703
1739
 
1704
1740
  // src/commands/generate.ts
1705
1741
  import { Command as Command5 } from "commander";
1706
- import { existsSync as existsSync5 } from "fs";
1707
- import { join as join5 } from "path";
1742
+ import { existsSync as existsSync6 } from "fs";
1743
+ import { join as join6 } from "path";
1708
1744
  import pc5 from "picocolors";
1709
1745
  import { parseDirectory } from "@thelacanians/vue-native-sfc-parser";
1710
1746
  import { generateCode, writeGeneratedFiles, cleanGeneratedFiles, validateNativeBlocks, formatValidationErrors } from "@thelacanians/vue-native-codegen";
1711
1747
  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
1748
  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);
1749
+ const appDir = join6(cwd, "app");
1750
+ if (!existsSync6(appDir)) {
1751
+ throw new ConfigError(
1752
+ `App directory not found at ${appDir}. Make sure you run this command from your project root.`
1753
+ );
1718
1754
  }
1719
1755
  const codegenOptions = {
1720
1756
  root: cwd,
@@ -1761,7 +1797,7 @@ var generateCommand = new Command5("generate").description("Generate native code
1761
1797
  if (!validation.isValid) {
1762
1798
  console.log(pc5.red("\n\u274C Validation failed:"));
1763
1799
  console.log(formatValidationErrors(validation));
1764
- process.exit(1);
1800
+ throw new ConfigError("Validation failed");
1765
1801
  }
1766
1802
  if (validation.warnings.length > 0) {
1767
1803
  console.log(pc5.yellow(`\u26A0\uFE0F ${validation.warnings.length} warning${validation.warnings.length !== 1 ? "s" : ""}`));
@@ -1785,7 +1821,7 @@ var generateCommand = new Command5("generate").description("Generate native code
1785
1821
  codegenResult.errors.forEach((err) => {
1786
1822
  console.log(pc5.red(` - ${err.file} ${err.message}`));
1787
1823
  });
1788
- process.exit(1);
1824
+ throw new ConfigError("Code generation failed");
1789
1825
  }
1790
1826
  console.log(pc5.blue("\u{1F4DD} Writing files..."));
1791
1827
  const writeResult = writeGeneratedFiles(codegenResult, cwd);
@@ -1794,7 +1830,7 @@ var generateCommand = new Command5("generate").description("Generate native code
1794
1830
  writeResult.errors.forEach((err) => {
1795
1831
  console.log(pc5.red(` - ${err.message}`));
1796
1832
  });
1797
- process.exit(1);
1833
+ throw new ConfigError("Failed to write generated files");
1798
1834
  }
1799
1835
  console.log(pc5.green("\n\u2705 Generation complete!\n"));
1800
1836
  console.log(pc5.dim("Generated files:"));
@@ -1811,10 +1847,10 @@ var generateCommand = new Command5("generate").description("Generate native code
1811
1847
  }
1812
1848
  console.log(pc5.green("\u{1F389} Ready to build!\n"));
1813
1849
  } 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);
1850
+ if (error instanceof ConfigError) throw error;
1851
+ throw new ConfigError(
1852
+ error.message || "Unknown generation error"
1853
+ );
1818
1854
  }
1819
1855
  }
1820
1856
  await runGeneration();
@@ -1850,4 +1886,10 @@ program.addCommand(createCommand);
1850
1886
  program.addCommand(devCommand);
1851
1887
  program.addCommand(runCommand);
1852
1888
  program.addCommand(generateCommand);
1853
- program.parse(process.argv);
1889
+ program.parseAsync(process.argv).catch((err) => {
1890
+ if (err instanceof ConfigError) {
1891
+ console.error(pc6.red(err.message));
1892
+ process.exit(1);
1893
+ }
1894
+ throw err;
1895
+ });
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
  };
@@ -119,7 +119,7 @@ afterEvaluate {
119
119
  create<MavenPublication>("release") {
120
120
  groupId = "com.vuenative"
121
121
  artifactId = "core"
122
- version = "0.4.14"
122
+ version = "0.6.2"
123
123
  from(components["release"])
124
124
  }
125
125
  }
@@ -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.4",
4
4
  "description": "CLI for creating and running Vue Native apps",
5
5
  "license": "MIT",
6
6
  "author": "Vue Native Contributors",
@@ -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",