@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 +201 -159
- package/dist/config.d.ts +4 -1
- package/dist/config.js +31 -11
- package/native/android/VueNativeCore/build.gradle.kts +1 -1
- package/native/ios/VueNativeCore/Package.swift +5 -2
- package/native/ios/VueNativeCore/Sources/VueNativeCore/Helpers/iOSRAFHelper.swift +124 -0
- package/package.json +3 -3
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 (!
|
|
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:
|
|
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 =
|
|
31
|
-
if (
|
|
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
|
|
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 =
|
|
45
|
-
if (
|
|
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
|
|
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 (!
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
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 (
|
|
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 =
|
|
170
|
-
if (!
|
|
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 =
|
|
177
|
-
if (!
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
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
|
-
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
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 (
|
|
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
|
|
345
|
+
import { join as join3, dirname } from "path";
|
|
334
346
|
import { fileURLToPath } from "url";
|
|
335
|
-
import { existsSync as
|
|
347
|
+
import { existsSync as existsSync3, readFileSync } from "fs";
|
|
336
348
|
import pc2 from "picocolors";
|
|
337
349
|
var VERSION = "0.6.2";
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
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
|
-
|
|
345
|
-
|
|
375
|
+
throw new ConfigError(
|
|
376
|
+
`Invalid template "${template}". Choose: blank, tabs, drawer`
|
|
377
|
+
);
|
|
346
378
|
}
|
|
347
|
-
|
|
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(
|
|
354
|
-
await mkdir(
|
|
355
|
-
await writeFile(
|
|
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(
|
|
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(
|
|
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 =
|
|
402
|
-
const iosSrcDir =
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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 =
|
|
556
|
-
const androidAppDir =
|
|
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 =
|
|
560
|
-
const androidKotlinDir =
|
|
561
|
-
const androidResValuesDir =
|
|
562
|
-
const androidResXmlDir =
|
|
563
|
-
const androidDebugResXmlDir =
|
|
564
|
-
const androidGradleWrapperDir =
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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 =
|
|
786
|
-
if (
|
|
787
|
-
const nativeDir =
|
|
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
|
-
|
|
804
|
-
|
|
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 =
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
|
1144
|
-
import { join as
|
|
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 =
|
|
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 =
|
|
1294
|
-
const androidDir =
|
|
1295
|
-
if (
|
|
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 (
|
|
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
|
|
1366
|
-
import { join as
|
|
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 =
|
|
1407
|
+
const derivedDataBase = join5(
|
|
1370
1408
|
process.env.HOME || "~",
|
|
1371
1409
|
"Library/Developer/Xcode/DerivedData"
|
|
1372
1410
|
);
|
|
1373
|
-
if (
|
|
1411
|
+
if (existsSync5(derivedDataBase)) {
|
|
1374
1412
|
try {
|
|
1375
1413
|
const projects = readdirSync2(derivedDataBase);
|
|
1376
1414
|
for (const project of projects.reverse()) {
|
|
1377
|
-
const productsDir =
|
|
1415
|
+
const productsDir = join5(
|
|
1378
1416
|
derivedDataBase,
|
|
1379
1417
|
project,
|
|
1380
1418
|
"Build/Products/Debug-iphonesimulator"
|
|
1381
1419
|
);
|
|
1382
|
-
if (
|
|
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
|
|
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 =
|
|
1397
|
-
if (
|
|
1434
|
+
const plistPath = join5(iosDir, "Sources", "Info.plist");
|
|
1435
|
+
if (existsSync5(plistPath)) {
|
|
1398
1436
|
try {
|
|
1399
|
-
const content =
|
|
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 =
|
|
1413
|
-
if (
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
1454
|
-
if (
|
|
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 =
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
1541
|
-
if (!
|
|
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 =
|
|
1548
|
-
if (!
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1652
|
+
throw new ConfigError(`App launch failed: ${err.message}`);
|
|
1617
1653
|
}
|
|
1618
1654
|
});
|
|
1619
1655
|
}
|
|
1620
1656
|
function runMacOS(cwd, options) {
|
|
1621
|
-
const macosDir =
|
|
1622
|
-
if (!
|
|
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 =
|
|
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
|
-
|
|
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 =
|
|
1705
|
+
const derivedDataBase = join5(process.env.HOME || "~", "Library/Developer/Xcode/DerivedData");
|
|
1670
1706
|
let appPath = null;
|
|
1671
|
-
if (
|
|
1707
|
+
if (existsSync5(derivedDataBase)) {
|
|
1672
1708
|
try {
|
|
1673
1709
|
const projects = readdirSync2(derivedDataBase);
|
|
1674
1710
|
for (const project of projects.reverse()) {
|
|
1675
|
-
const productsDir =
|
|
1676
|
-
if (
|
|
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 =
|
|
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
|
|
1707
|
-
import { join as
|
|
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 =
|
|
1714
|
-
if (!
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
|
|
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.
|
|
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
|
-
|
|
14
|
-
|
|
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
|
-
|
|
18
|
-
|
|
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
|
-
|
|
22
|
-
|
|
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
|
-
|
|
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
|
-
|
|
65
|
-
|
|
66
|
-
|
|
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
|
};
|
|
@@ -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
|
+
"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.
|
|
39
|
-
"@thelacanians/vue-native-codegen": "^0.
|
|
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",
|