vaderjs-native 1.0.37 → 1.0.38
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/LICENSE +21 -21
- package/bun.lock +48 -0
- package/cli/android/build.ts +378 -378
- package/cli/android/dev.ts +129 -129
- package/cli.ts +353 -279
- package/jsconfig.json +6 -6
- package/main.ts +1002 -1026
- package/package.json +18 -18
- package/plugins/index.ts +63 -63
- /package/{README.MD → README.md} +0 -0
package/cli/android/build.ts
CHANGED
|
@@ -1,379 +1,379 @@
|
|
|
1
|
-
import path from "path";
|
|
2
|
-
import fsSync, { existsSync } from "fs";
|
|
3
|
-
//@ts-ignore
|
|
4
|
-
import pkg from "../../package.json" assert { type: "json" };
|
|
5
|
-
import fs from "fs/promises";
|
|
6
|
-
import os from "os";
|
|
7
|
-
import { spawn, execSync } from "child_process";
|
|
8
|
-
import { ensureAndroidInstalled, findAndroidSdk } from "./sdk.js";
|
|
9
|
-
import { logger } from "../logger.js";
|
|
10
|
-
import { loadConfig } from "../../main.js";
|
|
11
|
-
import { Config } from "../../config/index.js";
|
|
12
|
-
import { fetchBinary } from "../binaries/fetch.js";
|
|
13
|
-
|
|
14
|
-
const PROJECT_ROOT = process.cwd();
|
|
15
|
-
const DIST_DIR = path.join(PROJECT_ROOT, "dist");
|
|
16
|
-
|
|
17
|
-
/* ---------------- Helpers ---------------- */
|
|
18
|
-
function getLocalIP(): string {
|
|
19
|
-
const interfaces = os.networkInterfaces();
|
|
20
|
-
for (const list of Object.values(interfaces)) {
|
|
21
|
-
for (const iface of list || []) {
|
|
22
|
-
if (iface.family === "IPv4" && !iface.internal) return iface.address;
|
|
23
|
-
}
|
|
24
|
-
}
|
|
25
|
-
return "127.0.0.1";
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
function patchPermissions(buildDir: string) {
|
|
29
|
-
const manifestPath = path.join(buildDir, "app", "src", "main", "AndroidManifest.xml");
|
|
30
|
-
if (!existsSync(manifestPath)) return;
|
|
31
|
-
|
|
32
|
-
let content = fsSync.readFileSync(manifestPath, "utf8");
|
|
33
|
-
|
|
34
|
-
content = content.replace(/<uses-permission android:name="[^"]*" \/>/g, "");
|
|
35
|
-
|
|
36
|
-
const basePerms = [
|
|
37
|
-
' <uses-permission android:name="android.permission.INTERNET" />',
|
|
38
|
-
' <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />'
|
|
39
|
-
];
|
|
40
|
-
|
|
41
|
-
const applicationIndex = content.indexOf("<application");
|
|
42
|
-
if (applicationIndex !== -1) {
|
|
43
|
-
const beforeApplication = content.substring(0, applicationIndex);
|
|
44
|
-
const afterApplication = content.substring(applicationIndex);
|
|
45
|
-
content = beforeApplication + basePerms.join("\n") + "\n" + afterApplication;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
fsSync.writeFileSync(manifestPath, content, "utf8");
|
|
49
|
-
logger.success("✅ Android permissions patched");
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
async function ensureLocalProperties(buildDir: string, sdkPath?: string) {
|
|
53
|
-
const localPropsPath = path.join(buildDir, "local.properties");
|
|
54
|
-
|
|
55
|
-
if (!sdkPath) {
|
|
56
|
-
const sdkInfo = findAndroidSdk();
|
|
57
|
-
sdkPath = sdkInfo?.sdkPath;
|
|
58
|
-
if (!sdkPath) throw new Error("Android SDK not found");
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
await fs.writeFile(
|
|
62
|
-
localPropsPath,
|
|
63
|
-
`sdk.dir=${sdkPath.replace(/\\/g, "\\\\")}\n`
|
|
64
|
-
);
|
|
65
|
-
|
|
66
|
-
logger.success(`✅ Created local.properties → ${sdkPath}`);
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
async function copyDir(src: string, dest: string) {
|
|
70
|
-
const entries = await fs.readdir(src, { withFileTypes: true });
|
|
71
|
-
await fs.mkdir(dest, { recursive: true });
|
|
72
|
-
await Promise.all(entries.map(async (entry) => {
|
|
73
|
-
const srcPath = path.join(src, entry.name);
|
|
74
|
-
const destPath = path.join(dest, entry.name);
|
|
75
|
-
if (entry.isDirectory()) {
|
|
76
|
-
await copyDir(srcPath, destPath);
|
|
77
|
-
} else {
|
|
78
|
-
if (entry.name.endsWith('.kt')) {
|
|
79
|
-
const content = await fs.readFile(srcPath, "utf8");
|
|
80
|
-
await fs.writeFile(destPath, content, "utf8");
|
|
81
|
-
} else {
|
|
82
|
-
await fs.copyFile(srcPath, destPath);
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
}));
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
async function removeDir(dir: string) {
|
|
89
|
-
if (existsSync(dir)) {
|
|
90
|
-
try {
|
|
91
|
-
await fs.rm(dir, { recursive: true, force: true });
|
|
92
|
-
} catch (error: any) {
|
|
93
|
-
if (error.code === 'EBUSY') {
|
|
94
|
-
logger.warn(`⚠️ Directory ${dir} is busy, retrying...`);
|
|
95
|
-
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
96
|
-
try {
|
|
97
|
-
await fs.rm(dir, { recursive: true, force: true });
|
|
98
|
-
} catch (retryError) {
|
|
99
|
-
logger.error(`❌ Failed to remove ${dir} after retry`);
|
|
100
|
-
throw retryError;
|
|
101
|
-
}
|
|
102
|
-
} else {
|
|
103
|
-
throw error;
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
/* ---------------- Main Patches ---------------- */
|
|
110
|
-
async function patchMainActivity(buildDir: string, APP_ID: string, isDev: boolean, config: Config) {
|
|
111
|
-
const javaDir = path.join(buildDir, "app", "src", "main", "java");
|
|
112
|
-
const packageDir = path.join(javaDir, ...APP_ID.split("."));
|
|
113
|
-
const mainActivityPath = path.join(packageDir, "MainActivity.kt");
|
|
114
|
-
|
|
115
|
-
if (!existsSync(mainActivityPath)) throw new Error(`MainActivity.kt not found in ${packageDir}`);
|
|
116
|
-
|
|
117
|
-
let content = await fs.readFile(mainActivityPath, "utf8");
|
|
118
|
-
|
|
119
|
-
content = content.replace(/package \{\{APP_PACKAGE\}\}/g, `package ${APP_ID}`);
|
|
120
|
-
|
|
121
|
-
const baseUrl = isDev
|
|
122
|
-
? `"http://${getLocalIP()}:${config.port || 3000}/"`
|
|
123
|
-
: `"file:///android_asset/${APP_ID}/"`;
|
|
124
|
-
|
|
125
|
-
content = content.replace(/private\s+val\s+baseUrl\s*=\s*"[^"]*"/, `private val baseUrl = ${baseUrl}`);
|
|
126
|
-
content = content.replace(/\{\{BASE_URL\}\}/g, baseUrl);
|
|
127
|
-
await fs.writeFile(mainActivityPath, content, "utf8");
|
|
128
|
-
logger.success(`✅ MainActivity patched → ${baseUrl} (${isDev ? "DEV" : "PROD"} mode)`);
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
async function copyAssets(buildDir: string, APP_ID: string) {
|
|
132
|
-
const assetsDir = path.join(buildDir, "app", "src", "main", "assets", APP_ID);
|
|
133
|
-
await removeDir(assetsDir);
|
|
134
|
-
|
|
135
|
-
if (!existsSync(DIST_DIR)) {
|
|
136
|
-
await fs.mkdir(assetsDir, { recursive: true });
|
|
137
|
-
await fs.writeFile(path.join(assetsDir, "index.html"), "<h1>No build output found</h1>");
|
|
138
|
-
logger.warn("⚠️ Dist folder empty, created placeholder index.html");
|
|
139
|
-
return;
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
await copyDir(DIST_DIR, assetsDir);
|
|
143
|
-
logger.success(`✅ Assets copied → ${assetsDir}`);
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
async function renamePackage(buildDir: string, oldPackage: string, newPackage: string) {
|
|
147
|
-
const javaDir = path.join(buildDir, "app", "src", "main", "java");
|
|
148
|
-
const oldDir = path.join(javaDir, ...oldPackage.split("."));
|
|
149
|
-
const newDir = path.join(javaDir, ...newPackage.split("."));
|
|
150
|
-
|
|
151
|
-
if (!existsSync(oldDir)) {
|
|
152
|
-
logger.warn(`⚠️ Source directory not found: ${oldDir}`);
|
|
153
|
-
return;
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
await fs.mkdir(path.dirname(newDir), { recursive: true });
|
|
157
|
-
|
|
158
|
-
if (existsSync(newDir)) {
|
|
159
|
-
await removeDir(newDir);
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
await copyDir(oldDir, newDir);
|
|
163
|
-
logger.success(`✅ Renamed package: ${oldPackage} → ${newPackage}`);
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
async function patchGradleFiles(buildDir: string, APP_ID: string) {
|
|
167
|
-
const buildGradlePath = path.join(buildDir, "app", "build.gradle.kts");
|
|
168
|
-
if (!existsSync(buildGradlePath)) return;
|
|
169
|
-
|
|
170
|
-
let content = fsSync.readFileSync(buildGradlePath, "utf8");
|
|
171
|
-
content = content.replace(/\{\{APP_PACKAGE\}\}/g, APP_ID);
|
|
172
|
-
fsSync.writeFileSync(buildGradlePath, content, "utf8");
|
|
173
|
-
logger.success("✅ Gradle files patched with package name");
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
function patchAppMeta(buildDir: string, config: Config) {
|
|
177
|
-
const manifestPath = path.join(buildDir, "app", "src", "main", "AndroidManifest.xml");
|
|
178
|
-
if (!existsSync(manifestPath)) return;
|
|
179
|
-
|
|
180
|
-
let content = fsSync.readFileSync(manifestPath, "utf8");
|
|
181
|
-
|
|
182
|
-
if (config.app?.name) {
|
|
183
|
-
content = content.replace(/android:label="[^"]*"/, `android:label="${config.app.name}"`);
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
if (config.app?.version) {
|
|
187
|
-
content = content.replace(/android:versionCode="[^"]*"/, `android:versionCode="${config.app.version.code}"`);
|
|
188
|
-
content = content.replace(/android:versionName="[^"]*"/, `android:versionName="${config.app.version.name}"`);
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
fsSync.writeFileSync(manifestPath, content, "utf8");
|
|
192
|
-
logger.success("✅ App metadata patched");
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
export async function addDeepLinks(buildDir: string) {
|
|
196
|
-
const config: Config = await loadConfig(PROJECT_ROOT);
|
|
197
|
-
|
|
198
|
-
if (!config.platforms?.android?.deepLinks || config.platforms.android.deepLinks.length === 0) {
|
|
199
|
-
return;
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
const manifestPath = path.join(buildDir, 'app', 'src', 'main', 'AndroidManifest.xml');
|
|
203
|
-
let manifest = fsSync.readFileSync(manifestPath, 'utf8');
|
|
204
|
-
|
|
205
|
-
const deepLinks = config.platforms.android.deepLinks;
|
|
206
|
-
const intentFilters = deepLinks.map(scheme => `
|
|
207
|
-
<intent-filter android:autoVerify="true">
|
|
208
|
-
<action android:name="android.intent.action.VIEW" />
|
|
209
|
-
<category android:name="android.intent.category.DEFAULT" />
|
|
210
|
-
<category android:name="android.intent.category.BROWSABLE" />
|
|
211
|
-
<data android:scheme="${scheme}" />
|
|
212
|
-
</intent-filter>
|
|
213
|
-
`).join('\n');
|
|
214
|
-
|
|
215
|
-
manifest = manifest.replace(
|
|
216
|
-
/<activity[^>]*MainActivity[^>]*>/,
|
|
217
|
-
`$&\n${intentFilters}`
|
|
218
|
-
);
|
|
219
|
-
|
|
220
|
-
fsSync.writeFileSync(manifestPath, manifest);
|
|
221
|
-
logger.success(`✅ Added ${deepLinks.length} deep link(s)`);
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
/* ---------------- Main Build Function ---------------- */
|
|
225
|
-
export async function buildAndroid(isDev = false) {
|
|
226
|
-
const config: Config = await loadConfig(process.cwd());
|
|
227
|
-
const APP_ID = config.app?.id || "com.vaderjs.app";
|
|
228
|
-
const BUILD_SRC = await fetchBinary("android", pkg.binaryVersion);
|
|
229
|
-
const BUILD_DIR = path.join(process.cwd(), "build", "android-src", APP_ID);
|
|
230
|
-
|
|
231
|
-
logger.step("🚀 Android Build");
|
|
232
|
-
ensureAndroidInstalled();
|
|
233
|
-
|
|
234
|
-
// 1️⃣ Clean old build folder
|
|
235
|
-
try {
|
|
236
|
-
await removeDir(BUILD_DIR);
|
|
237
|
-
} catch (error) {
|
|
238
|
-
logger.warn(`⚠️ Could not clean build directory, continuing...`);
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
// 2️⃣ Copy template
|
|
242
|
-
await copyDir(BUILD_SRC, BUILD_DIR);
|
|
243
|
-
|
|
244
|
-
// 3️⃣ Rename package and patch files
|
|
245
|
-
await renamePackage(BUILD_DIR, "myapp", APP_ID);
|
|
246
|
-
|
|
247
|
-
await ensureLocalProperties(BUILD_DIR);
|
|
248
|
-
|
|
249
|
-
// Patch AndroidBridge.kt directly
|
|
250
|
-
const bridgePath = path.join(BUILD_DIR, "app", "src", "main", "java", ...APP_ID.split("."), "AndroidBridge.kt");
|
|
251
|
-
if (existsSync(bridgePath)) {
|
|
252
|
-
let bridgeContent = await fs.readFile(bridgePath, "utf8");
|
|
253
|
-
|
|
254
|
-
// Remove any existing package declaration
|
|
255
|
-
bridgeContent = bridgeContent.replace(/^package\s+[^\n]+\n/, '');
|
|
256
|
-
|
|
257
|
-
// Add correct package declaration at the beginning
|
|
258
|
-
bridgeContent = `package ${APP_ID}\n\n${bridgeContent}`;
|
|
259
|
-
|
|
260
|
-
await fs.writeFile(bridgePath, bridgeContent, "utf8");
|
|
261
|
-
logger.success("✅ AndroidBridge patched with package name");
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
await patchMainActivity(BUILD_DIR, APP_ID, isDev, config);
|
|
265
|
-
await patchGradleFiles(BUILD_DIR, APP_ID);
|
|
266
|
-
|
|
267
|
-
// 4️⃣ Remove old myapp folder
|
|
268
|
-
await removeDir(path.join(BUILD_DIR, "app", "src", "main", "java", "myapp"));
|
|
269
|
-
|
|
270
|
-
// 5️⃣ Clean Gradle artifacts
|
|
271
|
-
await removeDir(path.join(BUILD_DIR, "app", "build"));
|
|
272
|
-
|
|
273
|
-
// 6️⃣ Apply patches and copy assets
|
|
274
|
-
patchPermissions(BUILD_DIR);
|
|
275
|
-
patchAppMeta(BUILD_DIR, config);
|
|
276
|
-
await copyAssets(BUILD_DIR, APP_ID);
|
|
277
|
-
|
|
278
|
-
if (config.platforms?.android?.deepLinks) {
|
|
279
|
-
await addDeepLinks(BUILD_DIR);
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
// 7️⃣ Gradle build - FIXED: Use Bun.spawn instead of Node's spawn
|
|
283
|
-
let gradleCmd = process.platform === "win32"
|
|
284
|
-
? path.join(BUILD_DIR, "gradlew.bat")
|
|
285
|
-
: path.join(BUILD_DIR, "gradlew");
|
|
286
|
-
|
|
287
|
-
// Check if gradlew exists
|
|
288
|
-
if (!existsSync(gradleCmd)) {
|
|
289
|
-
logger.error(`❌ Gradle wrapper not found at: ${gradleCmd}`);
|
|
290
|
-
logger.info("⚠️ Trying to use system gradle...");
|
|
291
|
-
gradleCmd = "gradle";
|
|
292
|
-
} else {
|
|
293
|
-
logger.info(`✅ Found gradlew at: ${gradleCmd}`);
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
logger.info("⚙️ Running Gradle assembleDebug...");
|
|
297
|
-
|
|
298
|
-
// Use Bun.spawn which handles paths better
|
|
299
|
-
try {
|
|
300
|
-
const proc = Bun.spawn([gradleCmd, "assembleDebug", "--no-daemon"], {
|
|
301
|
-
cwd: BUILD_DIR,
|
|
302
|
-
stdout: "inherit",
|
|
303
|
-
stderr: "inherit",
|
|
304
|
-
stdin: "inherit"
|
|
305
|
-
});
|
|
306
|
-
|
|
307
|
-
const exitCode = await proc.exited;
|
|
308
|
-
|
|
309
|
-
if (exitCode !== 0) {
|
|
310
|
-
throw new Error(`❌ Gradle failed with exit code ${exitCode}`);
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
logger.success("✅ Gradle build completed successfully");
|
|
314
|
-
} catch (error: any) {
|
|
315
|
-
logger.error(`❌ Failed to run Gradle: ${error.message}`);
|
|
316
|
-
|
|
317
|
-
// Fallback: try using cmd /c for Windows
|
|
318
|
-
if (process.platform === "win32") {
|
|
319
|
-
logger.info("🔄 Trying fallback method with cmd /c...");
|
|
320
|
-
try {
|
|
321
|
-
const cmd = `cd /d "${BUILD_DIR}" && "${gradleCmd}" assembleDebug --no-daemon`;
|
|
322
|
-
logger.info(`📝 Running: ${cmd}`);
|
|
323
|
-
|
|
324
|
-
const proc = Bun.spawn(["cmd", "/c", cmd], {
|
|
325
|
-
stdout: "inherit",
|
|
326
|
-
stderr: "inherit",
|
|
327
|
-
stdin: "inherit"
|
|
328
|
-
});
|
|
329
|
-
|
|
330
|
-
const exitCode = await proc.exited;
|
|
331
|
-
|
|
332
|
-
if (exitCode !== 0) {
|
|
333
|
-
throw new Error(`❌ Fallback also failed with exit code ${exitCode}`);
|
|
334
|
-
}
|
|
335
|
-
|
|
336
|
-
logger.success("✅ Gradle build completed with fallback method");
|
|
337
|
-
} catch (fallbackError: any) {
|
|
338
|
-
throw new Error(`❌ All Gradle build attempts failed: ${fallbackError.message}`);
|
|
339
|
-
}
|
|
340
|
-
} else {
|
|
341
|
-
throw error;
|
|
342
|
-
}
|
|
343
|
-
}
|
|
344
|
-
|
|
345
|
-
// 8️⃣ Cleanup Java processes if needed
|
|
346
|
-
try {
|
|
347
|
-
if (process.platform === "win32") {
|
|
348
|
-
execSync("taskkill /F /IM java.exe /T 2>nul", { stdio: "ignore" });
|
|
349
|
-
} else {
|
|
350
|
-
execSync("pkill -f java 2>/dev/null", { stdio: "ignore" });
|
|
351
|
-
}
|
|
352
|
-
} catch {}
|
|
353
|
-
|
|
354
|
-
// 9️⃣ Copy APK to top-level build folder
|
|
355
|
-
const APK_SRC = path.join(BUILD_DIR, "app", "build", "outputs", "apk", "debug", "app-debug.apk");
|
|
356
|
-
const APK_DEST_DIR = path.join(PROJECT_ROOT, "build");
|
|
357
|
-
|
|
358
|
-
if (!existsSync(APK_DEST_DIR)) {
|
|
359
|
-
await fs.mkdir(APK_DEST_DIR, { recursive: true });
|
|
360
|
-
}
|
|
361
|
-
|
|
362
|
-
const APK_DEST = path.join(APK_DEST_DIR, `${APP_ID}-debug.apk`);
|
|
363
|
-
|
|
364
|
-
if (!existsSync(APK_SRC)) {
|
|
365
|
-
// Try alternative APK location
|
|
366
|
-
const altApkPath = path.join(BUILD_DIR, "app", "build", "outputs", "apk", "debug", "app-debug.apk");
|
|
367
|
-
if (existsSync(altApkPath)) {
|
|
368
|
-
await fs.copyFile(altApkPath, APK_DEST);
|
|
369
|
-
logger.success(`✅ APK ready → ${APK_DEST}`);
|
|
370
|
-
return APK_DEST;
|
|
371
|
-
}
|
|
372
|
-
throw new Error(`❌ APK not found after build. Checked: ${APK_SRC}`);
|
|
373
|
-
}
|
|
374
|
-
|
|
375
|
-
await fs.copyFile(APK_SRC, APK_DEST);
|
|
376
|
-
logger.success(`✅ APK ready → ${APK_DEST}`);
|
|
377
|
-
|
|
378
|
-
return APK_DEST;
|
|
1
|
+
import path from "path";
|
|
2
|
+
import fsSync, { existsSync } from "fs";
|
|
3
|
+
//@ts-ignore
|
|
4
|
+
import pkg from "../../package.json" assert { type: "json" };
|
|
5
|
+
import fs from "fs/promises";
|
|
6
|
+
import os from "os";
|
|
7
|
+
import { spawn, execSync } from "child_process";
|
|
8
|
+
import { ensureAndroidInstalled, findAndroidSdk } from "./sdk.js";
|
|
9
|
+
import { logger } from "../logger.js";
|
|
10
|
+
import { loadConfig } from "../../main.js";
|
|
11
|
+
import { Config } from "../../config/index.js";
|
|
12
|
+
import { fetchBinary } from "../binaries/fetch.js";
|
|
13
|
+
|
|
14
|
+
const PROJECT_ROOT = process.cwd();
|
|
15
|
+
const DIST_DIR = path.join(PROJECT_ROOT, "dist");
|
|
16
|
+
|
|
17
|
+
/* ---------------- Helpers ---------------- */
|
|
18
|
+
function getLocalIP(): string {
|
|
19
|
+
const interfaces = os.networkInterfaces();
|
|
20
|
+
for (const list of Object.values(interfaces)) {
|
|
21
|
+
for (const iface of list || []) {
|
|
22
|
+
if (iface.family === "IPv4" && !iface.internal) return iface.address;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
return "127.0.0.1";
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function patchPermissions(buildDir: string) {
|
|
29
|
+
const manifestPath = path.join(buildDir, "app", "src", "main", "AndroidManifest.xml");
|
|
30
|
+
if (!existsSync(manifestPath)) return;
|
|
31
|
+
|
|
32
|
+
let content = fsSync.readFileSync(manifestPath, "utf8");
|
|
33
|
+
|
|
34
|
+
content = content.replace(/<uses-permission android:name="[^"]*" \/>/g, "");
|
|
35
|
+
|
|
36
|
+
const basePerms = [
|
|
37
|
+
' <uses-permission android:name="android.permission.INTERNET" />',
|
|
38
|
+
' <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />'
|
|
39
|
+
];
|
|
40
|
+
|
|
41
|
+
const applicationIndex = content.indexOf("<application");
|
|
42
|
+
if (applicationIndex !== -1) {
|
|
43
|
+
const beforeApplication = content.substring(0, applicationIndex);
|
|
44
|
+
const afterApplication = content.substring(applicationIndex);
|
|
45
|
+
content = beforeApplication + basePerms.join("\n") + "\n" + afterApplication;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
fsSync.writeFileSync(manifestPath, content, "utf8");
|
|
49
|
+
logger.success("✅ Android permissions patched");
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async function ensureLocalProperties(buildDir: string, sdkPath?: string) {
|
|
53
|
+
const localPropsPath = path.join(buildDir, "local.properties");
|
|
54
|
+
|
|
55
|
+
if (!sdkPath) {
|
|
56
|
+
const sdkInfo = findAndroidSdk();
|
|
57
|
+
sdkPath = sdkInfo?.sdkPath;
|
|
58
|
+
if (!sdkPath) throw new Error("Android SDK not found");
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
await fs.writeFile(
|
|
62
|
+
localPropsPath,
|
|
63
|
+
`sdk.dir=${sdkPath.replace(/\\/g, "\\\\")}\n`
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
logger.success(`✅ Created local.properties → ${sdkPath}`);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
async function copyDir(src: string, dest: string) {
|
|
70
|
+
const entries = await fs.readdir(src, { withFileTypes: true });
|
|
71
|
+
await fs.mkdir(dest, { recursive: true });
|
|
72
|
+
await Promise.all(entries.map(async (entry) => {
|
|
73
|
+
const srcPath = path.join(src, entry.name);
|
|
74
|
+
const destPath = path.join(dest, entry.name);
|
|
75
|
+
if (entry.isDirectory()) {
|
|
76
|
+
await copyDir(srcPath, destPath);
|
|
77
|
+
} else {
|
|
78
|
+
if (entry.name.endsWith('.kt')) {
|
|
79
|
+
const content = await fs.readFile(srcPath, "utf8");
|
|
80
|
+
await fs.writeFile(destPath, content, "utf8");
|
|
81
|
+
} else {
|
|
82
|
+
await fs.copyFile(srcPath, destPath);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}));
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
async function removeDir(dir: string) {
|
|
89
|
+
if (existsSync(dir)) {
|
|
90
|
+
try {
|
|
91
|
+
await fs.rm(dir, { recursive: true, force: true });
|
|
92
|
+
} catch (error: any) {
|
|
93
|
+
if (error.code === 'EBUSY') {
|
|
94
|
+
logger.warn(`⚠️ Directory ${dir} is busy, retrying...`);
|
|
95
|
+
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
96
|
+
try {
|
|
97
|
+
await fs.rm(dir, { recursive: true, force: true });
|
|
98
|
+
} catch (retryError) {
|
|
99
|
+
logger.error(`❌ Failed to remove ${dir} after retry`);
|
|
100
|
+
throw retryError;
|
|
101
|
+
}
|
|
102
|
+
} else {
|
|
103
|
+
throw error;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/* ---------------- Main Patches ---------------- */
|
|
110
|
+
async function patchMainActivity(buildDir: string, APP_ID: string, isDev: boolean, config: Config) {
|
|
111
|
+
const javaDir = path.join(buildDir, "app", "src", "main", "java");
|
|
112
|
+
const packageDir = path.join(javaDir, ...APP_ID.split("."));
|
|
113
|
+
const mainActivityPath = path.join(packageDir, "MainActivity.kt");
|
|
114
|
+
|
|
115
|
+
if (!existsSync(mainActivityPath)) throw new Error(`MainActivity.kt not found in ${packageDir}`);
|
|
116
|
+
|
|
117
|
+
let content = await fs.readFile(mainActivityPath, "utf8");
|
|
118
|
+
|
|
119
|
+
content = content.replace(/package \{\{APP_PACKAGE\}\}/g, `package ${APP_ID}`);
|
|
120
|
+
|
|
121
|
+
const baseUrl = isDev
|
|
122
|
+
? `"http://${getLocalIP()}:${config.port || 3000}/"`
|
|
123
|
+
: `"file:///android_asset/${APP_ID}/"`;
|
|
124
|
+
|
|
125
|
+
content = content.replace(/private\s+val\s+baseUrl\s*=\s*"[^"]*"/, `private val baseUrl = ${baseUrl}`);
|
|
126
|
+
content = content.replace(/\{\{BASE_URL\}\}/g, baseUrl);
|
|
127
|
+
await fs.writeFile(mainActivityPath, content, "utf8");
|
|
128
|
+
logger.success(`✅ MainActivity patched → ${baseUrl} (${isDev ? "DEV" : "PROD"} mode)`);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
async function copyAssets(buildDir: string, APP_ID: string) {
|
|
132
|
+
const assetsDir = path.join(buildDir, "app", "src", "main", "assets", APP_ID);
|
|
133
|
+
await removeDir(assetsDir);
|
|
134
|
+
|
|
135
|
+
if (!existsSync(DIST_DIR)) {
|
|
136
|
+
await fs.mkdir(assetsDir, { recursive: true });
|
|
137
|
+
await fs.writeFile(path.join(assetsDir, "index.html"), "<h1>No build output found</h1>");
|
|
138
|
+
logger.warn("⚠️ Dist folder empty, created placeholder index.html");
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
await copyDir(DIST_DIR, assetsDir);
|
|
143
|
+
logger.success(`✅ Assets copied → ${assetsDir}`);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
async function renamePackage(buildDir: string, oldPackage: string, newPackage: string) {
|
|
147
|
+
const javaDir = path.join(buildDir, "app", "src", "main", "java");
|
|
148
|
+
const oldDir = path.join(javaDir, ...oldPackage.split("."));
|
|
149
|
+
const newDir = path.join(javaDir, ...newPackage.split("."));
|
|
150
|
+
|
|
151
|
+
if (!existsSync(oldDir)) {
|
|
152
|
+
logger.warn(`⚠️ Source directory not found: ${oldDir}`);
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
await fs.mkdir(path.dirname(newDir), { recursive: true });
|
|
157
|
+
|
|
158
|
+
if (existsSync(newDir)) {
|
|
159
|
+
await removeDir(newDir);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
await copyDir(oldDir, newDir);
|
|
163
|
+
logger.success(`✅ Renamed package: ${oldPackage} → ${newPackage}`);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
async function patchGradleFiles(buildDir: string, APP_ID: string) {
|
|
167
|
+
const buildGradlePath = path.join(buildDir, "app", "build.gradle.kts");
|
|
168
|
+
if (!existsSync(buildGradlePath)) return;
|
|
169
|
+
|
|
170
|
+
let content = fsSync.readFileSync(buildGradlePath, "utf8");
|
|
171
|
+
content = content.replace(/\{\{APP_PACKAGE\}\}/g, APP_ID);
|
|
172
|
+
fsSync.writeFileSync(buildGradlePath, content, "utf8");
|
|
173
|
+
logger.success("✅ Gradle files patched with package name");
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function patchAppMeta(buildDir: string, config: Config) {
|
|
177
|
+
const manifestPath = path.join(buildDir, "app", "src", "main", "AndroidManifest.xml");
|
|
178
|
+
if (!existsSync(manifestPath)) return;
|
|
179
|
+
|
|
180
|
+
let content = fsSync.readFileSync(manifestPath, "utf8");
|
|
181
|
+
|
|
182
|
+
if (config.app?.name) {
|
|
183
|
+
content = content.replace(/android:label="[^"]*"/, `android:label="${config.app.name}"`);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
if (config.app?.version) {
|
|
187
|
+
content = content.replace(/android:versionCode="[^"]*"/, `android:versionCode="${config.app.version.code}"`);
|
|
188
|
+
content = content.replace(/android:versionName="[^"]*"/, `android:versionName="${config.app.version.name}"`);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
fsSync.writeFileSync(manifestPath, content, "utf8");
|
|
192
|
+
logger.success("✅ App metadata patched");
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
export async function addDeepLinks(buildDir: string) {
|
|
196
|
+
const config: Config = await loadConfig(PROJECT_ROOT);
|
|
197
|
+
|
|
198
|
+
if (!config.platforms?.android?.deepLinks || config.platforms.android.deepLinks.length === 0) {
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const manifestPath = path.join(buildDir, 'app', 'src', 'main', 'AndroidManifest.xml');
|
|
203
|
+
let manifest = fsSync.readFileSync(manifestPath, 'utf8');
|
|
204
|
+
|
|
205
|
+
const deepLinks = config.platforms.android.deepLinks;
|
|
206
|
+
const intentFilters = deepLinks.map(scheme => `
|
|
207
|
+
<intent-filter android:autoVerify="true">
|
|
208
|
+
<action android:name="android.intent.action.VIEW" />
|
|
209
|
+
<category android:name="android.intent.category.DEFAULT" />
|
|
210
|
+
<category android:name="android.intent.category.BROWSABLE" />
|
|
211
|
+
<data android:scheme="${scheme}" />
|
|
212
|
+
</intent-filter>
|
|
213
|
+
`).join('\n');
|
|
214
|
+
|
|
215
|
+
manifest = manifest.replace(
|
|
216
|
+
/<activity[^>]*MainActivity[^>]*>/,
|
|
217
|
+
`$&\n${intentFilters}`
|
|
218
|
+
);
|
|
219
|
+
|
|
220
|
+
fsSync.writeFileSync(manifestPath, manifest);
|
|
221
|
+
logger.success(`✅ Added ${deepLinks.length} deep link(s)`);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/* ---------------- Main Build Function ---------------- */
|
|
225
|
+
export async function buildAndroid(isDev = false) {
|
|
226
|
+
const config: Config = await loadConfig(process.cwd());
|
|
227
|
+
const APP_ID = config.app?.id || "com.vaderjs.app";
|
|
228
|
+
const BUILD_SRC = await fetchBinary("android", pkg.binaryVersion);
|
|
229
|
+
const BUILD_DIR = path.join(process.cwd(), "build", "android-src", APP_ID);
|
|
230
|
+
|
|
231
|
+
logger.step("🚀 Android Build");
|
|
232
|
+
ensureAndroidInstalled();
|
|
233
|
+
|
|
234
|
+
// 1️⃣ Clean old build folder
|
|
235
|
+
try {
|
|
236
|
+
await removeDir(BUILD_DIR);
|
|
237
|
+
} catch (error) {
|
|
238
|
+
logger.warn(`⚠️ Could not clean build directory, continuing...`);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// 2️⃣ Copy template
|
|
242
|
+
await copyDir(BUILD_SRC, BUILD_DIR);
|
|
243
|
+
|
|
244
|
+
// 3️⃣ Rename package and patch files
|
|
245
|
+
await renamePackage(BUILD_DIR, "myapp", APP_ID);
|
|
246
|
+
|
|
247
|
+
await ensureLocalProperties(BUILD_DIR);
|
|
248
|
+
|
|
249
|
+
// Patch AndroidBridge.kt directly
|
|
250
|
+
const bridgePath = path.join(BUILD_DIR, "app", "src", "main", "java", ...APP_ID.split("."), "AndroidBridge.kt");
|
|
251
|
+
if (existsSync(bridgePath)) {
|
|
252
|
+
let bridgeContent = await fs.readFile(bridgePath, "utf8");
|
|
253
|
+
|
|
254
|
+
// Remove any existing package declaration
|
|
255
|
+
bridgeContent = bridgeContent.replace(/^package\s+[^\n]+\n/, '');
|
|
256
|
+
|
|
257
|
+
// Add correct package declaration at the beginning
|
|
258
|
+
bridgeContent = `package ${APP_ID}\n\n${bridgeContent}`;
|
|
259
|
+
|
|
260
|
+
await fs.writeFile(bridgePath, bridgeContent, "utf8");
|
|
261
|
+
logger.success("✅ AndroidBridge patched with package name");
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
await patchMainActivity(BUILD_DIR, APP_ID, isDev, config);
|
|
265
|
+
await patchGradleFiles(BUILD_DIR, APP_ID);
|
|
266
|
+
|
|
267
|
+
// 4️⃣ Remove old myapp folder
|
|
268
|
+
await removeDir(path.join(BUILD_DIR, "app", "src", "main", "java", "myapp"));
|
|
269
|
+
|
|
270
|
+
// 5️⃣ Clean Gradle artifacts
|
|
271
|
+
await removeDir(path.join(BUILD_DIR, "app", "build"));
|
|
272
|
+
|
|
273
|
+
// 6️⃣ Apply patches and copy assets
|
|
274
|
+
patchPermissions(BUILD_DIR);
|
|
275
|
+
patchAppMeta(BUILD_DIR, config);
|
|
276
|
+
await copyAssets(BUILD_DIR, APP_ID);
|
|
277
|
+
|
|
278
|
+
if (config.platforms?.android?.deepLinks) {
|
|
279
|
+
await addDeepLinks(BUILD_DIR);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// 7️⃣ Gradle build - FIXED: Use Bun.spawn instead of Node's spawn
|
|
283
|
+
let gradleCmd = process.platform === "win32"
|
|
284
|
+
? path.join(BUILD_DIR, "gradlew.bat")
|
|
285
|
+
: path.join(BUILD_DIR, "gradlew");
|
|
286
|
+
|
|
287
|
+
// Check if gradlew exists
|
|
288
|
+
if (!existsSync(gradleCmd)) {
|
|
289
|
+
logger.error(`❌ Gradle wrapper not found at: ${gradleCmd}`);
|
|
290
|
+
logger.info("⚠️ Trying to use system gradle...");
|
|
291
|
+
gradleCmd = "gradle";
|
|
292
|
+
} else {
|
|
293
|
+
logger.info(`✅ Found gradlew at: ${gradleCmd}`);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
logger.info("⚙️ Running Gradle assembleDebug...");
|
|
297
|
+
|
|
298
|
+
// Use Bun.spawn which handles paths better
|
|
299
|
+
try {
|
|
300
|
+
const proc = Bun.spawn([gradleCmd, "assembleDebug", "--no-daemon"], {
|
|
301
|
+
cwd: BUILD_DIR,
|
|
302
|
+
stdout: "inherit",
|
|
303
|
+
stderr: "inherit",
|
|
304
|
+
stdin: "inherit"
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
const exitCode = await proc.exited;
|
|
308
|
+
|
|
309
|
+
if (exitCode !== 0) {
|
|
310
|
+
throw new Error(`❌ Gradle failed with exit code ${exitCode}`);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
logger.success("✅ Gradle build completed successfully");
|
|
314
|
+
} catch (error: any) {
|
|
315
|
+
logger.error(`❌ Failed to run Gradle: ${error.message}`);
|
|
316
|
+
|
|
317
|
+
// Fallback: try using cmd /c for Windows
|
|
318
|
+
if (process.platform === "win32") {
|
|
319
|
+
logger.info("🔄 Trying fallback method with cmd /c...");
|
|
320
|
+
try {
|
|
321
|
+
const cmd = `cd /d "${BUILD_DIR}" && "${gradleCmd}" assembleDebug --no-daemon`;
|
|
322
|
+
logger.info(`📝 Running: ${cmd}`);
|
|
323
|
+
|
|
324
|
+
const proc = Bun.spawn(["cmd", "/c", cmd], {
|
|
325
|
+
stdout: "inherit",
|
|
326
|
+
stderr: "inherit",
|
|
327
|
+
stdin: "inherit"
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
const exitCode = await proc.exited;
|
|
331
|
+
|
|
332
|
+
if (exitCode !== 0) {
|
|
333
|
+
throw new Error(`❌ Fallback also failed with exit code ${exitCode}`);
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
logger.success("✅ Gradle build completed with fallback method");
|
|
337
|
+
} catch (fallbackError: any) {
|
|
338
|
+
throw new Error(`❌ All Gradle build attempts failed: ${fallbackError.message}`);
|
|
339
|
+
}
|
|
340
|
+
} else {
|
|
341
|
+
throw error;
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
// 8️⃣ Cleanup Java processes if needed
|
|
346
|
+
try {
|
|
347
|
+
if (process.platform === "win32") {
|
|
348
|
+
execSync("taskkill /F /IM java.exe /T 2>nul", { stdio: "ignore" });
|
|
349
|
+
} else {
|
|
350
|
+
execSync("pkill -f java 2>/dev/null", { stdio: "ignore" });
|
|
351
|
+
}
|
|
352
|
+
} catch {}
|
|
353
|
+
|
|
354
|
+
// 9️⃣ Copy APK to top-level build folder
|
|
355
|
+
const APK_SRC = path.join(BUILD_DIR, "app", "build", "outputs", "apk", "debug", "app-debug.apk");
|
|
356
|
+
const APK_DEST_DIR = path.join(PROJECT_ROOT, "build");
|
|
357
|
+
|
|
358
|
+
if (!existsSync(APK_DEST_DIR)) {
|
|
359
|
+
await fs.mkdir(APK_DEST_DIR, { recursive: true });
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
const APK_DEST = path.join(APK_DEST_DIR, `${APP_ID}-debug.apk`);
|
|
363
|
+
|
|
364
|
+
if (!existsSync(APK_SRC)) {
|
|
365
|
+
// Try alternative APK location
|
|
366
|
+
const altApkPath = path.join(BUILD_DIR, "app", "build", "outputs", "apk", "debug", "app-debug.apk");
|
|
367
|
+
if (existsSync(altApkPath)) {
|
|
368
|
+
await fs.copyFile(altApkPath, APK_DEST);
|
|
369
|
+
logger.success(`✅ APK ready → ${APK_DEST}`);
|
|
370
|
+
return APK_DEST;
|
|
371
|
+
}
|
|
372
|
+
throw new Error(`❌ APK not found after build. Checked: ${APK_SRC}`);
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
await fs.copyFile(APK_SRC, APK_DEST);
|
|
376
|
+
logger.success(`✅ APK ready → ${APK_DEST}`);
|
|
377
|
+
|
|
378
|
+
return APK_DEST;
|
|
379
379
|
}
|