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