catalyst-core 0.1.0-beta.5 → 0.1.0-beta.7
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/native/androidProject/app/src/main/java/io/yourname/androidproject/CustomWebview.kt +14 -2
- package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/WebCacheManager.kt +19 -12
- package/dist/native/androidSetup.js +3 -2
- package/dist/native/buildAndroid/assets.js +52 -0
- package/dist/native/buildAndroid/build.js +17 -0
- package/dist/native/buildAndroid/config.js +1 -0
- package/dist/native/buildAndroid/index.js +1 -0
- package/dist/native/buildAppAndroid.js +2 -148
- package/dist/native/buildAppIos.js +2 -306
- package/dist/native/buildIos/assets.js +8 -0
- package/dist/native/buildIos/build.js +39 -0
- package/dist/native/buildIos/config.js +71 -0
- package/dist/native/buildIos/index.js +6 -0
- package/dist/native/buildIos/plugins.js +45 -0
- package/dist/native/iosnativeWebView/Sources/Core/Constants/ConfigConstants.swift +2 -1
- package/dist/native/iosnativeWebView/iosnativeWebView/ContentView.swift +17 -9
- package/dist/native/iosnativeWebView/iosnativeWebViewTests/BootTimingUtilityTests.swift +1 -1
- package/dist/native/iosnativeWebView/iosnativeWebViewTests/ConfigMappingTests.swift +4 -2
- package/dist/native/setupEmulatorIos.js +6 -4
- package/dist/server/renderer/handler.js +1 -1
- package/dist/server/utils/userAgentUtil.js +9 -3
- package/mcp_v2/tools/knowledge.js +2 -2
- package/package.json +1 -1
package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/CustomWebview.kt
CHANGED
|
@@ -465,6 +465,18 @@ class CustomWebView(
|
|
|
465
465
|
return "build/public/$fileName"
|
|
466
466
|
}
|
|
467
467
|
|
|
468
|
+
private fun withCorsHeaders(response: WebResourceResponse): WebResourceResponse {
|
|
469
|
+
val responseHeaders = response.responseHeaders
|
|
470
|
+
if (responseHeaders.isNullOrEmpty()) {
|
|
471
|
+
response.responseHeaders = mutableMapOf(
|
|
472
|
+
"Access-Control-Allow-Origin" to "*",
|
|
473
|
+
"Access-Control-Allow-Methods" to "GET, OPTIONS",
|
|
474
|
+
"Access-Control-Allow-Headers" to "*"
|
|
475
|
+
)
|
|
476
|
+
}
|
|
477
|
+
return response
|
|
478
|
+
}
|
|
479
|
+
|
|
468
480
|
private fun getMimeType(path: String): String {
|
|
469
481
|
return when {
|
|
470
482
|
path.endsWith(".html") -> "text/html"
|
|
@@ -533,7 +545,7 @@ class CustomWebView(
|
|
|
533
545
|
if (BuildConfig.DEBUG) {
|
|
534
546
|
Log.d(TAG, "✅ SERVICE_WORKER: Served from cache: $url")
|
|
535
547
|
}
|
|
536
|
-
response
|
|
548
|
+
withCorsHeaders(response)
|
|
537
549
|
} else {
|
|
538
550
|
if (BuildConfig.DEBUG) {
|
|
539
551
|
Log.d(TAG, "❌ SERVICE_WORKER: Cache miss: $url")
|
|
@@ -827,7 +839,7 @@ class CustomWebView(
|
|
|
827
839
|
Log.d(TAG, "✅ WEBVIEW_CLIENT: Served from cache in ${duration}ms: $url")
|
|
828
840
|
}
|
|
829
841
|
metricsMonitor.recordCacheHit(url)
|
|
830
|
-
return response
|
|
842
|
+
return withCorsHeaders(response)
|
|
831
843
|
} else {
|
|
832
844
|
if (BuildConfig.DEBUG) {
|
|
833
845
|
Log.d(TAG, "❌ WEBVIEW_CLIENT: Cache miss, triggering async fetch: $url")
|
package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/WebCacheManager.kt
CHANGED
|
@@ -330,25 +330,27 @@ class WebCacheManager(private val context: Context, private val properties: java
|
|
|
330
330
|
val eTag = connection.getHeaderField("ETag")
|
|
331
331
|
val lastModified = connection.getHeaderField("Last-Modified")
|
|
332
332
|
|
|
333
|
+
// Capture all response headers for replay on cache hits
|
|
334
|
+
val savedHeaders = mutableMapOf<String, String>()
|
|
335
|
+
var i = 0
|
|
336
|
+
while (true) {
|
|
337
|
+
val key = connection.getHeaderFieldKey(i) ?: break
|
|
338
|
+
val value = connection.getHeaderField(i)
|
|
339
|
+
if (key.isNotEmpty()) savedHeaders[key] = value
|
|
340
|
+
i++
|
|
341
|
+
}
|
|
342
|
+
|
|
333
343
|
val responseBytes = connection.inputStream.use { it.readBytes() }
|
|
334
344
|
if (!isValidResponse(mimeType, responseBytes)) {
|
|
335
345
|
return@withContext null
|
|
336
346
|
}
|
|
337
347
|
|
|
338
348
|
// Create response for immediate use
|
|
339
|
-
val response = WebResourceResponse(
|
|
340
|
-
mimeType,
|
|
341
|
-
encoding,
|
|
342
|
-
ByteArrayInputStream(responseBytes)
|
|
343
|
-
)
|
|
349
|
+
val response = WebResourceResponse(mimeType, encoding, 200, "OK", savedHeaders, ByteArrayInputStream(responseBytes))
|
|
344
350
|
|
|
345
351
|
// Cache the response
|
|
346
352
|
val cacheEntry = CacheEntry(
|
|
347
|
-
response = WebResourceResponse(
|
|
348
|
-
mimeType,
|
|
349
|
-
encoding,
|
|
350
|
-
ByteArrayInputStream(responseBytes.copyOf())
|
|
351
|
-
),
|
|
353
|
+
response = WebResourceResponse(mimeType, encoding, 200, "OK", savedHeaders, ByteArrayInputStream(responseBytes.copyOf())),
|
|
352
354
|
eTag = eTag,
|
|
353
355
|
lastModified = lastModified
|
|
354
356
|
)
|
|
@@ -363,7 +365,8 @@ class WebCacheManager(private val context: Context, private val properties: java
|
|
|
363
365
|
mimeType = mimeType,
|
|
364
366
|
encoding = encoding,
|
|
365
367
|
eTag = eTag,
|
|
366
|
-
lastModified = lastModified
|
|
368
|
+
lastModified = lastModified,
|
|
369
|
+
responseHeaders = savedHeaders
|
|
367
370
|
))
|
|
368
371
|
if (BuildConfig.DEBUG) {
|
|
369
372
|
Log.d(TAG, "✅ Successfully cached response for: $url")
|
|
@@ -391,13 +394,17 @@ class WebCacheManager(private val context: Context, private val properties: java
|
|
|
391
394
|
val mimeType: String,
|
|
392
395
|
val encoding: String,
|
|
393
396
|
val eTag: String? = null,
|
|
394
|
-
val lastModified: String? = null
|
|
397
|
+
val lastModified: String? = null,
|
|
398
|
+
val responseHeaders: Map<String, String> = emptyMap()
|
|
395
399
|
) : Serializable
|
|
396
400
|
|
|
397
401
|
private fun createResponseFromCache(cacheFile: File, metadata: CacheMetadata): WebResourceResponse {
|
|
398
402
|
return WebResourceResponse(
|
|
399
403
|
metadata.mimeType,
|
|
400
404
|
metadata.encoding,
|
|
405
|
+
200,
|
|
406
|
+
"OK",
|
|
407
|
+
metadata.responseHeaders.ifEmpty { null },
|
|
401
408
|
BufferedInputStream(FileInputStream(cacheFile), BUFFER_SIZE)
|
|
402
409
|
)
|
|
403
410
|
}
|
|
@@ -3,6 +3,7 @@ try{await(0,_utils.runCommand)("brew install --cask zulu@17");}catch(error){if(e
|
|
|
3
3
|
if(!_fs.default.existsSync(javaHome)){throw new Error(`Java installation not found at ${javaHome}. Please verify the installation manually.`);}// Set up environment variables
|
|
4
4
|
const homeDir=process.env.HOME;const shellProfile=`${homeDir}/.zshrc`;const exportCommand=`\n# Java Configuration\nexport JAVA_HOME=${javaHome}\nexport PATH=$JAVA_HOME/bin:$PATH\n`;try{_fs.default.appendFileSync(shellProfile,exportCommand);}catch(error){throw new Error(`Failed to update ${shellProfile}: ${error.message}. Please add the following lines manually:\n${exportCommand}`);}// Set for current process
|
|
5
5
|
process.env.JAVA_HOME=javaHome;process.env.PATH=`${javaHome}/bin:${process.env.PATH}`;// Verify Java configuration
|
|
6
|
-
try{const javaVersion=(0,_utils.runCommand)("java -version");progress.log("Java installed and configured successfully","success");progress.log(`Java Version: ${javaVersion}`,"info");progress.log(`IMPORTANT: Please run the following commands to complete setup:`,"warning");progress.log(`1. source ${shellProfile}`,"info");progress.log(`2. echo $JAVA_HOME`,"info");return true;}catch(error){throw new Error(`Java installation verified but environment not configured. Please run:\n1. source ${shellProfile}\n2. echo $JAVA_HOME`);}}catch(error){progress.log(error.message,"error");progress.log("Manual Setup Instructions:","info");progress.log("1. Run: brew reinstall --cask zulu@17","info");progress.log("2. Add to ~/.zshrc:","info");progress.log(" export JAVA_HOME=/Library/Java/JavaVirtualMachines/zulu-17.jdk/Contents/Home","info");progress.log(" export PATH=$JAVA_HOME/bin:$PATH","info");progress.log("3. Run: source ~/.zshrc","info");throw error;}}async function validateJavaEnvironment(){progress.start("java");const hasJava=await checkJavaInstallation();if(!hasJava){progress.log("Java installation required. Starting installation process...");const javaInstalled=await installJava();if(!javaInstalled){progress.fail("java","Failed to set up Java environment");throw new Error("Failed to set up Java environment");}}progress.complete("java");}async function initializeConfig(){const configFile=_fs.default.readFileSync(configPath,"utf8");const config=JSON.parse(configFile);const{WEBVIEW_CONFIG}=config;if(!WEBVIEW_CONFIG||Object.keys(WEBVIEW_CONFIG).length===0){progress.log("WebView Config missing in "+configPath,"error");process.exit(1);}if(!WEBVIEW_CONFIG.android){WEBVIEW_CONFIG.android={};}return{WEBVIEW_CONFIG};}async function saveConfig(newConfig){try{const existingConfigFile=_fs.default.readFileSync(configPath,"utf8");const existingConfig=JSON.parse(existingConfigFile);const updatedConfig={...existingConfig,WEBVIEW_CONFIG:{...existingConfig.WEBVIEW_CONFIG,android:newConfig.WEBVIEW_CONFIG.android}};_fs.default.writeFileSync(configPath,JSON.stringify(updatedConfig,null,2));progress.log("Configuration saved successfully","success");}catch(error){progress.log("Failed to save configuration: "+error.message,"error");process.exit(1);}}function validateAndroidTools(androidConfig){const ANDROID_SDK=androidConfig.sdkPath;const EMULATOR_PATH=`${ANDROID_SDK}/emulator/emulator`;const ADB_PATH=`${ANDROID_SDK}/platform-tools/adb`;if(!ANDROID_SDK){throw new Error("Android SDK path is not configured");}if(!_fs.default.existsSync(ANDROID_SDK)){throw new Error(`Android SDK path does not exist: ${ANDROID_SDK}`);}if(!_fs.default.existsSync(ADB_PATH)){throw new Error(`ADB not found at: ${ADB_PATH}`);}try{(0,_utils.runCommand)(`${ADB_PATH} version`);progress.log("ADB is valid","success");}catch(error){throw new Error(`ADB is not working properly: ${error.message}`);}if(!_fs.default.existsSync(EMULATOR_PATH)){throw new Error(`Emulator not found at: ${EMULATOR_PATH}`);}try{(0,_utils.runCommand)(`${EMULATOR_PATH} -version`);progress.log("Emulator is valid","success");}catch(error){throw new Error(`Emulator is not working properly: ${error.message}`);}try{const avdOutput=(0,_utils.runCommand)(`${EMULATOR_PATH} -list-avds`);if(!avdOutput.includes(androidConfig.emulatorName)){throw new Error(`Specified emulator "${androidConfig.emulatorName}" not found in available AVDs`);}progress.log(`Emulator "${androidConfig.emulatorName}" exists`,"success");}catch(error){throw new Error(`Error checking emulator AVD: ${error.message}`);}progress.log("Android tools validation completed successfully!","success");return{EMULATOR_PATH,ADB_PATH};}async function checkEmulator(ADB_PATH){try{const devices=(0,_utils.runCommand)(`${ADB_PATH} devices`);return devices.includes("emulator");}catch(error){progress.log(`Error checking emulator: ${error}`,"error");return false;}}async function startEmulator(EMULATOR_PATH,androidConfig){progress.log(`Starting emulator: ${androidConfig.emulatorName}...`);//
|
|
7
|
-
(0,_child_process.exec)(`${EMULATOR_PATH} -avd ${androidConfig.emulatorName} -read-only > /dev/null &`,error=>{
|
|
6
|
+
try{const javaVersion=(0,_utils.runCommand)("java -version");progress.log("Java installed and configured successfully","success");progress.log(`Java Version: ${javaVersion}`,"info");progress.log(`IMPORTANT: Please run the following commands to complete setup:`,"warning");progress.log(`1. source ${shellProfile}`,"info");progress.log(`2. echo $JAVA_HOME`,"info");return true;}catch(error){throw new Error(`Java installation verified but environment not configured. Please run:\n1. source ${shellProfile}\n2. echo $JAVA_HOME`);}}catch(error){progress.log(error.message,"error");progress.log("Manual Setup Instructions:","info");progress.log("1. Run: brew reinstall --cask zulu@17","info");progress.log("2. Add to ~/.zshrc:","info");progress.log(" export JAVA_HOME=/Library/Java/JavaVirtualMachines/zulu-17.jdk/Contents/Home","info");progress.log(" export PATH=$JAVA_HOME/bin:$PATH","info");progress.log("3. Run: source ~/.zshrc","info");throw error;}}async function validateJavaEnvironment(){progress.start("java");const hasJava=await checkJavaInstallation();if(!hasJava){progress.log("Java installation required. Starting installation process...");const javaInstalled=await installJava();if(!javaInstalled){progress.fail("java","Failed to set up Java environment");throw new Error("Failed to set up Java environment");}}progress.complete("java");}async function initializeConfig(){const configFile=_fs.default.readFileSync(configPath,"utf8");const config=JSON.parse(configFile);const{WEBVIEW_CONFIG}=config;if(!WEBVIEW_CONFIG||Object.keys(WEBVIEW_CONFIG).length===0){progress.log("WebView Config missing in "+configPath,"error");process.exit(1);}if(!WEBVIEW_CONFIG.android){WEBVIEW_CONFIG.android={};}return{WEBVIEW_CONFIG};}async function saveConfig(newConfig){try{const existingConfigFile=_fs.default.readFileSync(configPath,"utf8");const existingConfig=JSON.parse(existingConfigFile);const updatedConfig={...existingConfig,WEBVIEW_CONFIG:{...existingConfig.WEBVIEW_CONFIG,android:newConfig.WEBVIEW_CONFIG.android}};_fs.default.writeFileSync(configPath,JSON.stringify(updatedConfig,null,2));progress.log("Configuration saved successfully","success");}catch(error){progress.log("Failed to save configuration: "+error.message,"error");process.exit(1);}}function validateAndroidTools(androidConfig){const ANDROID_SDK=androidConfig.sdkPath;const EMULATOR_PATH=`${ANDROID_SDK}/emulator/emulator`;const ADB_PATH=`${ANDROID_SDK}/platform-tools/adb`;if(!ANDROID_SDK){throw new Error("Android SDK path is not configured");}if(!_fs.default.existsSync(ANDROID_SDK)){throw new Error(`Android SDK path does not exist: ${ANDROID_SDK}`);}if(!_fs.default.existsSync(ADB_PATH)){throw new Error(`ADB not found at: ${ADB_PATH}`);}try{(0,_utils.runCommand)(`${ADB_PATH} version`);progress.log("ADB is valid","success");}catch(error){throw new Error(`ADB is not working properly: ${error.message}`);}if(!_fs.default.existsSync(EMULATOR_PATH)){throw new Error(`Emulator not found at: ${EMULATOR_PATH}`);}try{(0,_utils.runCommand)(`${EMULATOR_PATH} -version`);progress.log("Emulator is valid","success");}catch(error){throw new Error(`Emulator is not working properly: ${error.message}`);}try{const avdOutput=(0,_utils.runCommand)(`${EMULATOR_PATH} -list-avds`);if(!avdOutput.includes(androidConfig.emulatorName)){throw new Error(`Specified emulator "${androidConfig.emulatorName}" not found in available AVDs`);}progress.log(`Emulator "${androidConfig.emulatorName}" exists`,"success");}catch(error){throw new Error(`Error checking emulator AVD: ${error.message}`);}progress.log("Android tools validation completed successfully!","success");return{EMULATOR_PATH,ADB_PATH};}async function checkEmulator(ADB_PATH){try{const devices=(0,_utils.runCommand)(`${ADB_PATH} devices`);return devices.includes("emulator");}catch(error){progress.log(`Error checking emulator: ${error}`,"error");return false;}}async function startEmulator(EMULATOR_PATH,androidConfig){progress.log(`Starting emulator: ${androidConfig.emulatorName}...`);// eslint-disable-next-line security/detect-child-process
|
|
7
|
+
(0,_child_process.exec)(`${EMULATOR_PATH} -avd ${androidConfig.emulatorName} -read-only > /dev/null &`,error=>{// nosemgrep: javascript.lang.security.detect-child-process.detect-child-process
|
|
8
|
+
if(error){progress.log(`Error starting emulator: ${error}`,"error");}});}async function updateLocalProperties(sdkPath){progress.start("properties");try{(0,_utils.runCommand)(`cd ${pwd}/androidProject && ./gradlew updateSdkPath -PsdkPath="${sdkPath}"`);progress.log("Updated local.properties successfully","success");progress.complete("properties");}catch(error){progress.fail("properties",`Failed to update local.properties: ${error.message}`);throw error;}}async function setupAndroidEnvironment(){try{await validateJavaEnvironment();progress.start("config");await initializeConfig();const config=await(0,_utils.validateAndCompleteConfig)("android",configPath);progress.complete("config");progress.start("androidTools");const{EMULATOR_PATH,ADB_PATH}=validateAndroidTools(config.android);progress.complete("androidTools");progress.start("emulator");const emulatorRunning=await checkEmulator(ADB_PATH);if(!emulatorRunning){progress.log("No emulator running, attempting to start one...");await startEmulator(EMULATOR_PATH,config.android);}else{progress.log("Emulator already running, proceeding with installation...","success");}progress.complete("emulator");if(config.android?.sdkPath){await updateLocalProperties(config.android.sdkPath);}progress.start("saveConfig");await saveConfig({WEBVIEW_CONFIG:config});progress.complete("saveConfig");progress.start("setupServer");await(0,_setupServer.setupServer)(configPath);progress.complete("setupServer");progress.printTreeContent("Configuration Explanation",["WEBVIEW_CONFIG: Main configuration object for the WebView setup",{text:"port: Port number for the WebView server",indent:1,prefix:"├─ ",color:"gray"},{text:"android: Android-specific configuration",indent:1,prefix:"└─ ",color:"gray"},{text:"buildType: Build type (debug/release)",indent:2,prefix:"├─ ",color:"gray"},{text:"sdkPath: Android SDK path",indent:2,prefix:"├─ ",color:"gray"},{text:"emulatorName: Selected Android emulator name",indent:2,prefix:"└─ ",color:"gray"}]);progress.printTreeContent("Final Configuration",[JSON.stringify(config,null,2)]);process.exit(0);}catch(error){if(progress.currentStep){progress.fail(progress.currentStep.id,error.message);}process.exit(1);}}// Execute the main setup
|
|
8
9
|
setupAndroidEnvironment();
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"use strict";const fs=require("fs");const path=require("path");function createAssetsPhase(ctx){const{configPath,publicPath,pwd,progress}=ctx;async function copyBuildAssets(androidConfig,buildOptimisation=false){if(!buildOptimisation)return;progress.log("Copying build assets to Android project...","info");try{const sourcePath=`${process.cwd()}/build/`;const destPath=`${pwd}/androidProject/app/src/main/assets/build/`;ctx.runCommand(`mkdir -p ${destPath}`);ctx.runCommand(`rm -rf ${destPath}/*`);const excludePatterns=["route-manifest.json.gz","route-manifest.json.br"];if(buildOptimisation){progress.log("Running with build optimization...","info");const excludeParams=excludePatterns.map(pattern=>`--exclude="${pattern}"`).join(" ");const rsyncCommand=`rsync -av ${excludeParams} ${sourcePath} ${destPath}`;progress.log("Executing rsync command with exclusions...","info");ctx.runCommand(rsyncCommand);for(const pattern of excludePatterns){const checkCommand=`find ${destPath} -name "${pattern}" | wc -l`;const count=parseInt(ctx.runCommand(checkCommand).trim(),10);if(count>0){progress.log(`Warning: Found ${count} instances of excluded file ${pattern}`,"warning");ctx.runCommand(`find ${destPath} -name "${pattern}" -delete`);}}progress.log("Build assets copied with optimization (excluded route-manifest JSON files)","success");}else{progress.log("Running without build optimization...","info");const exclusions=excludePatterns.map(pattern=>`-not -name "${pattern}"`).join(" ");const copyCommand=`find ${sourcePath} -type f ${exclusions} -exec cp --parents {} ${destPath} \\;`;progress.log(`Executing copy command with exclusions...`,"info");ctx.runCommand(copyCommand);progress.log("Build assets copied successfully!","success");}}catch(error){throw new Error("Error copying build assets: "+error.message);}}async function copySplashscreenAssets(){try{const destPath=`${pwd}/androidProject/app/src/main/res`;const configFile=fs.readFileSync(configPath,"utf8");const config=JSON.parse(configFile);const{WEBVIEW_CONFIG={}}=config;if(!WEBVIEW_CONFIG.splashScreen)return;const androidPublicPath=`${process.cwd()}/public/android`;const imageFormats=["png","jpg","jpeg","gif","bmp","webp"];if(fs.existsSync(androidPublicPath)){const drawableDir=`${destPath}/drawable`;if(!fs.existsSync(drawableDir)){fs.mkdirSync(drawableDir,{recursive:true});}for(const format of imageFormats){const existingPath=`${destPath}/drawable/splashscreen.${format}`;if(fs.existsSync(existingPath)){fs.unlinkSync(existingPath);}}for(const format of imageFormats){const sourcePath=`${androidPublicPath}/splashscreen.${format}`;if(fs.existsSync(sourcePath)){fs.copyFileSync(sourcePath,`${destPath}/drawable/splashscreen.${format}`);break;}}}const backgroundColor=WEBVIEW_CONFIG.splashScreen.backgroundColor||"#ffffff";const themeFiles=[`${destPath}/values/themes.xml`,`${destPath}/values-night/themes.xml`];for(const themesFile of themeFiles){try{let content=fs.readFileSync(themesFile,"utf8");content=content.replace(/<item name="android:windowBackground">.*?<\/item>/,`<item name="android:windowBackground">${backgroundColor}</item>`);content=content.replace(/<item name="android:windowSplashScreenBackground" tools:targetApi="31">.*?<\/item>/,`<item name="android:windowSplashScreenBackground" tools:targetApi="31">${backgroundColor}</item>`);fs.writeFileSync(themesFile,content);}catch(e){if(e.code!=="ENOENT")throw e;}}}catch(error){progress.log(`Error processing splash screen: ${error.message}`,"warning");}}async function copyOfflinePage(){try{const sourcePath=`${process.cwd()}/public/offline.html`;const destDir=`${pwd}/androidProject/app/src/main/assets/offline`;const destPath=`${destDir}/offline.html`;if(!fs.existsSync(sourcePath)){progress.log("offline.html not found in public/; skipping offline asset copy","warning");return;}ctx.runCommand(`mkdir -p ${destDir}`);fs.copyFileSync(sourcePath,destPath);progress.log("offline.html copied to Android assets","success");}catch(error){progress.log(`Warning: Error copying offline.html: ${error.message}`,"warning");}}async function copyIconAssets(){try{const destPath=`${pwd}/androidProject/app/src/main/res`;const manifestPath=`${pwd}/androidProject/app/src/main/AndroidManifest.xml`;const androidIconDir=path.join(publicPath,"android","appIcons");// nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal - publicPath is process.cwd()/public, a trusted internal path.
|
|
2
|
+
const fallbackIconPath=path.join(__dirname,"../assets","catalyst.png");// nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal - __dirname is a fixed module path, not user input.
|
|
3
|
+
const fallbackExists=fs.existsSync(fallbackIconPath);const extensions=["png","jpg","jpeg"];if(!fs.existsSync(publicPath)){progress.log(`Warning: Public directory not found at ${publicPath}`,"warning");return;}const densities=[{key:"mdpi",dir:"mipmap-mdpi"},{key:"hdpi",dir:"mipmap-hdpi"},{key:"xhdpi",dir:"mipmap-xhdpi"},{key:"xxhdpi",dir:"mipmap-xxhdpi"},{key:"xxxhdpi",dir:"mipmap-xxxhdpi"}];const hasIconDirectory=fs.existsSync(androidIconDir)&&fs.lstatSync(androidIconDir).isDirectory();const cleanDensityDir=dir=>{if(!fs.existsSync(dir))return;for(const file of fs.readdirSync(dir)){if(file.startsWith("icon.")){fs.unlinkSync(path.join(dir,file));// nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal - dir is derived from pwd/destPath (internal), file comes from fs.readdirSync on that same dir.
|
|
4
|
+
}}};const findIconForDensity=densityKey=>{if(!hasIconDirectory)return null;for(const ext of extensions){const candidate=path.join(androidIconDir,`icon-${densityKey}.${ext}`);// nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal - androidIconDir is derived from publicPath (internal); densityKey and ext are fixed internal strings.
|
|
5
|
+
if(fs.existsSync(candidate))return{path:candidate,ext};}return null;};let hasCustomIcons=false;let usedFallback=false;for(const density of densities){const targetDir=path.join(destPath,density.dir);// nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal - destPath is derived from pwd (internal); density.dir is a fixed string from the densities array.
|
|
6
|
+
const source=findIconForDensity(density.key);cleanDensityDir(targetDir);if(source){fs.mkdirSync(targetDir,{recursive:true});const destination=path.join(targetDir,`icon.${source.ext}`);// nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal - targetDir is derived from pwd (internal); source.ext is from a fixed extensions array.
|
|
7
|
+
fs.copyFileSync(source.path,destination);hasCustomIcons=true;}}if(!hasCustomIcons&&fallbackExists){const targetDir=path.join(destPath,"mipmap-xxxhdpi");// nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal - destPath is derived from pwd (internal); "mipmap-xxxhdpi" is a fixed literal.
|
|
8
|
+
fs.mkdirSync(targetDir,{recursive:true});cleanDensityDir(targetDir);const destination=path.join(targetDir,"icon.png");// nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal - targetDir is derived from pwd (internal); "icon.png" is a fixed literal.
|
|
9
|
+
fs.copyFileSync(fallbackIconPath,destination);usedFallback=true;}const setManifestIcons=(iconValue,roundIconValue)=>{let current;try{current=fs.readFileSync(manifestPath,"utf8");}catch(e){if(e.code==="ENOENT")return;throw e;}const updated=current.replace(/android:icon="[^"]*"/g,`android:icon="${iconValue}"`).replace(/android:roundIcon="[^"]*"/g,`android:roundIcon="${roundIconValue}"`);if(updated!==current)fs.writeFileSync(manifestPath,updated);};if(!hasCustomIcons&&!usedFallback){progress.log("No custom Android icons found; using default template icons.","info");setManifestIcons("@mipmap/ic_launcher","@mipmap/ic_launcher_round");return;}setManifestIcons("@mipmap/icon","@mipmap/icon");if(hasCustomIcons){progress.log("Applied Android launcher icons from public/android/appIcons.","success");}if(usedFallback){progress.log("Used bundled Catalyst fallback icon for launcher.","info");}}catch(error){progress.log(`Warning: Error copying app icon assets: ${error.message}`,"warning");}}async function configureAppName(androidConfig){try{const destPath=`${pwd}/androidProject/app/src/main/res`;const stringsFile=`${destPath}/values/strings.xml`;let stringsContent=fs.readFileSync(stringsFile,"utf8");if(androidConfig.appName){stringsContent=stringsContent.replace(/<string name="app_name">.*?<\/string>/,`<string name="app_name">${androidConfig.appName}</string>`);fs.writeFileSync(stringsFile,stringsContent);progress.log(`App display name configured: ${androidConfig.appName}`,"success");}else{stringsContent=stringsContent.replace(/<string name="app_name">.*?<\/string>/,`<string name="app_name">Catalyst Application</string>`);fs.writeFileSync(stringsFile,stringsContent);progress.log("App display name reverted to default: Catalyst Application","info");}}catch(error){progress.log(`Warning: Error configuring app name: ${error.message}`,"warning");}}// ── Notification helpers ──────────────────────────────────────────────────
|
|
10
|
+
async function handleGoogleServicesJson(){try{const rootGoogleServicesPath=`${process.cwd()}/google-services.json`;const androidGoogleServicesPath=`${pwd}/androidProject/app/google-services.json`;if(fs.existsSync(rootGoogleServicesPath)){progress.log("Found google-services.json in root directory","info");const appDir=`${pwd}/androidProject/app`;if(!fs.existsSync(appDir)){fs.mkdirSync(appDir,{recursive:true});}fs.copyFileSync(rootGoogleServicesPath,androidGoogleServicesPath);progress.log("Copied google-services.json to androidProject/app/","success");return true;}else if(fs.existsSync(androidGoogleServicesPath)){progress.log("google-services.json already exists in androidProject/app/","info");return true;}else{progress.log("google-services.json not found - Firebase push notifications will not work","warning");progress.log("Place google-services.json in project root or src/native/androidProject/app/","info");return false;}}catch(error){progress.log(`Warning: Error handling google-services.json: ${error.message}`,"warning");return false;}}async function processNotifications(WEBVIEW_CONFIG){const hasNotificationConfig=!!WEBVIEW_CONFIG.notifications?.enabled;try{await cleanupNotificationPermissions();await cleanupNotificationResources();await cleanupNotificationMetadata();await cleanupNotificationAssets();if(!hasNotificationConfig){progress.log("Notifications disabled - cleaned up notification configurations","info");return;}const hasGoogleServices=await handleGoogleServicesJson();if(!hasGoogleServices){progress.log("Continuing without Firebase - only local notifications will work","warning");}await addNotificationPermissions();await generateNotificationResources(WEBVIEW_CONFIG.notifications);await addNotificationMetadata();await processNotificationAssets();progress.log("Notification configuration completed successfully!","success");}catch(error){progress.log(`Warning: Error processing notifications: ${error.message}`,"warning");}}async function addNotificationPermissions(){try{const manifestPath=`${pwd}/androidProject/app/src/main/AndroidManifest.xml`;let manifestContent=fs.readFileSync(manifestPath,"utf8");const permissionsXml=` <uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
|
11
|
+
<uses-permission android:name="android.permission.VIBRATE" />
|
|
12
|
+
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
|
13
|
+
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />`;manifestContent=manifestContent.replace(/(<uses-permission[^>]*>[\s\S]*?)(\s*<uses-feature)/,`$1\n${permissionsXml}$2`);fs.writeFileSync(manifestPath,manifestContent);progress.log("Added notification permissions to AndroidManifest.xml","success");}catch(error){throw new Error(`Failed to add notification permissions: ${error.message}`);}}async function generateNotificationResources(notificationConfig){try{const colorsPath=`${pwd}/androidProject/app/src/main/res/values/colors.xml`;let colorsContent=fs.readFileSync(colorsPath,"utf8");const notificationColorXml=` <color name="notification_color">${notificationConfig.color||"#007AFF"}</color>`;if(!colorsContent.includes('name="notification_color"')){colorsContent=colorsContent.replace(/(<\/resources>)/,`${notificationColorXml}\n$1`);fs.writeFileSync(colorsPath,colorsContent);progress.log("Added notification color to colors.xml","success");}}catch(error){throw new Error(`Failed to generate notification resources: ${error.message}`);}}async function addNotificationMetadata(){try{const manifestPath=`${pwd}/androidProject/app/src/main/AndroidManifest.xml`;let manifestContent=fs.readFileSync(manifestPath,"utf8");const metadataXml=`
|
|
14
|
+
<!-- Default notification configuration -->
|
|
15
|
+
<meta-data
|
|
16
|
+
android:name="default_notification_channel_id"
|
|
17
|
+
android:value="default_notifications" />
|
|
18
|
+
<meta-data
|
|
19
|
+
android:name="default_notification_icon"
|
|
20
|
+
android:resource="@drawable/ic_notification" />
|
|
21
|
+
<meta-data
|
|
22
|
+
android:name="default_notification_color"
|
|
23
|
+
android:resource="@color/notification_color" />
|
|
24
|
+
|
|
25
|
+
<!-- Firebase default notification configuration -->
|
|
26
|
+
<meta-data
|
|
27
|
+
android:name="com.google.firebase.messaging.default_notification_channel_id"
|
|
28
|
+
android:value="default" />
|
|
29
|
+
<meta-data
|
|
30
|
+
android:name="com.google.firebase.messaging.default_notification_icon"
|
|
31
|
+
android:resource="@drawable/ic_notification" />
|
|
32
|
+
<meta-data
|
|
33
|
+
android:name="com.google.firebase.messaging.default_notification_sound"
|
|
34
|
+
android:resource="@raw/notification_sound_default" />
|
|
35
|
+
|
|
36
|
+
<!-- Push Notification Service -->
|
|
37
|
+
<service
|
|
38
|
+
android:name="io.yourname.androidproject.utils.PushNotificationUtils"
|
|
39
|
+
android:exported="false">
|
|
40
|
+
<intent-filter>
|
|
41
|
+
<action android:name="com.google.firebase.MESSAGING_EVENT" />
|
|
42
|
+
</intent-filter>
|
|
43
|
+
</service>`;manifestContent=manifestContent.replace(/(\s*<\/application>)/,`${metadataXml}\n$1`);fs.writeFileSync(manifestPath,manifestContent);progress.log("Added notification metadata and push notification service to AndroidManifest.xml","success");}catch(error){throw new Error(`Failed to add notification metadata: ${error.message}`);}}async function processNotificationAssets(){try{const destPath=`${pwd}/androidProject/app/src/main/res`;const imageFormats=["png","jpg","jpeg","gif","bmp","svg","webp"];const audioFormats=["mp3","wav","ogg"];let assetsProcessed=0;const notificationIcons=[{sourceName:"notification-icon",resourceName:"ic_notification"},{sourceName:"notification-large",resourceName:"ic_notification_large"}];const notificationSounds=[{sourceName:"notification-sound-default",resourceName:"notification_sound_default"},{sourceName:"notification-sound-urgent",resourceName:"notification_sound_urgent"}];for(const icon of notificationIcons){for(const format of imageFormats){const existingIconPath=`${destPath}/drawable/${icon.resourceName}.${format}`;if(fs.existsSync(existingIconPath)){fs.unlinkSync(existingIconPath);progress.log(`Removed existing ${icon.resourceName}.${format}`,"info");}}}for(const sound of notificationSounds){for(const format of audioFormats){const existingSoundPath=`${destPath}/raw/${sound.resourceName}.${format}`;if(fs.existsSync(existingSoundPath)){fs.unlinkSync(existingSoundPath);progress.log(`Removed existing ${sound.resourceName}.${format}`,"info");}}}let iconFound=false;for(const icon of notificationIcons){for(const format of imageFormats){const iconImagePath=`${publicPath}/${icon.sourceName}.${format}`;if(fs.existsSync(iconImagePath)){const destImagePath=`${destPath}/drawable/${icon.resourceName}.${format}`;fs.copyFileSync(iconImagePath,destImagePath);progress.log(`Notification icon copied: ${icon.sourceName}.${format} -> ${icon.resourceName}.${format}`,"success");assetsProcessed++;if(icon.sourceName==="notification-icon"){iconFound=true;}break;}}}if(!iconFound){generateNotificationIconDrawable("ic_notification",destPath);progress.log("Generated default notification icon","info");}const rawDir=`${destPath}/raw`;if(!fs.existsSync(rawDir)){fs.mkdirSync(rawDir,{recursive:true});}for(const sound of notificationSounds){for(const format of audioFormats){const soundImagePath=`${publicPath}/${sound.sourceName}.${format}`;if(fs.existsSync(soundImagePath)){const destSoundPath=`${destPath}/raw/${sound.resourceName}.${format}`;fs.copyFileSync(soundImagePath,destSoundPath);progress.log(`Notification sound copied: ${sound.sourceName}.${format} -> ${sound.resourceName}.${format}`,"success");assetsProcessed++;break;}}}if(assetsProcessed>0){progress.log(`Processed ${assetsProcessed} notification assets from public/`,"success");}else{progress.log("No notification assets found in public/ - using defaults","info");}}catch(error){throw new Error(`Failed to process notification assets: ${error.message}`);}}function generateNotificationIconDrawable(iconName,destPath){const iconXml=`<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
|
44
|
+
android:width="24dp"
|
|
45
|
+
android:height="24dp"
|
|
46
|
+
android:viewportWidth="24"
|
|
47
|
+
android:viewportHeight="24"
|
|
48
|
+
android:tint="?attr/colorOnPrimary">
|
|
49
|
+
<path
|
|
50
|
+
android:fillColor="@android:color/white"
|
|
51
|
+
android:pathData="M12,22c1.1,0 2,-0.9 2,-2h-4c0,1.1 0.9,2 2,2zM18,16v-5c0,-3.07 -1.64,-5.64 -4.5,-6.32V4c0,-0.83 -0.67,-1.5 -1.5,-1.5s-1.5,0.67 -1.5,1.5v0.68C7.63,5.36 6,7.92 6,11v5l-2,2v1h16v-1l-2,-2z"/>
|
|
52
|
+
</vector>`;fs.writeFileSync(`${destPath}/drawable/${iconName}.xml`,iconXml);}async function cleanupNotificationPermissions(){try{const manifestPath=`${pwd}/androidProject/app/src/main/AndroidManifest.xml`;let manifestContent=fs.readFileSync(manifestPath,"utf8");const notificationPermissions=["android.permission.POST_NOTIFICATIONS","android.permission.VIBRATE","android.permission.WAKE_LOCK","android.permission.RECEIVE_BOOT_COMPLETED"];manifestContent=manifestContent.split("\n").filter(line=>{return!notificationPermissions.some(permission=>line.includes(`<uses-permission android:name="${permission}"`));}).join("\n");fs.writeFileSync(manifestPath,manifestContent);}catch(error){progress.log(`Warning: Error cleaning notification permissions: ${error.message}`,"warning");}}async function cleanupNotificationResources(){try{const colorsPath=`${pwd}/androidProject/app/src/main/res/values/colors.xml`;let colorsContent=fs.readFileSync(colorsPath,"utf8");const existingColorLine=colorsContent.split("\n").find(line=>line.includes('name="notification_color"'));if(existingColorLine){colorsContent=colorsContent.replace(existingColorLine,"");fs.writeFileSync(colorsPath,colorsContent);}}catch(error){progress.log(`Warning: Error cleaning notification resources: ${error.message}`,"warning");}}async function cleanupNotificationMetadata(){try{const manifestPath=`${pwd}/androidProject/app/src/main/AndroidManifest.xml`;let manifestContent=fs.readFileSync(manifestPath,"utf8");const metadataNames=["default_notification_channel_id","default_notification_icon","default_notification_color","com.google.firebase.messaging.default_notification_channel_id","com.google.firebase.messaging.default_notification_icon","com.google.firebase.messaging.default_notification_sound"];manifestContent=manifestContent.split("\n").filter(line=>{return!metadataNames.some(metadataName=>line.includes(`android:name="${metadataName}"`));}).join("\n");const serviceRegex=/\s*<!--\s*Push Notification Service\s*-->\s*<service[^>]*android:name="[^"]*PushNotificationUtils"[\s\S]*?<\/service>/gi;manifestContent=manifestContent.replace(serviceRegex,"");manifestContent=manifestContent.replace(/\s*<!--\s*Default notification configuration\s*-->/gi,"");manifestContent=manifestContent.replace(/\s*<!--\s*Firebase default notification configuration\s*-->/gi,"");manifestContent=manifestContent.replace(/\s*<!--\s*Push Notification Service\s*-->/gi,"");fs.writeFileSync(manifestPath,manifestContent);}catch(error){progress.log(`Warning: Error cleaning notification metadata: ${error.message}`,"warning");}}async function cleanupNotificationAssets(){try{const destPath=`${pwd}/androidProject/app/src/main/res`;const imageFormats=["png","jpg","jpeg","gif","bmp","svg","webp","xml"];const audioFormats=["mp3","wav","ogg"];const notificationIcons=["ic_notification","ic_notification_large"];const notificationSounds=["notification_sound_default","notification_sound_urgent"];for(const icon of notificationIcons){for(const format of imageFormats){const iconPath=`${destPath}/drawable/${icon}.${format}`;if(fs.existsSync(iconPath)){fs.unlinkSync(iconPath);}}}const rawDir=`${destPath}/raw`;if(fs.existsSync(rawDir)){for(const sound of notificationSounds){for(const format of audioFormats){const soundPath=`${rawDir}/${sound}.${format}`;if(fs.existsSync(soundPath)){fs.unlinkSync(soundPath);}}}}}catch(error){progress.log(`Warning: Error cleaning notification assets: ${error.message}`,"warning");}}return{copyBuildAssets,copySplashscreenAssets,copyOfflinePage,copyIconAssets,configureAppName,processNotifications};}module.exports=createAssetsPhase;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"use strict";const fs=require("fs");const path=require("path");const{buildAndroidAAB}=require("../renameAndroidProject.js");const DEFAULT_DEPLOYMENT_PATH="./deployment";const DEFAULT_OLD_PROJECT_NAME="androidProject";const DEFAULT_OVERWRITE_EXISTING=true;function createBuildPhase(ctx){const{configPath,pwd,progress,ANDROID_PACKAGE}=ctx;async function detectPhysicalDevice(ADB_PATH){try{progress.log("Detecting physical devices...","info");// nosemgrep: javascript.lang.security.audit.dangerous-spawn-shell-command.dangerous-spawn-shell-command - ADB_PATH is derived from androidConfig.sdkPath, a trusted internal config value.
|
|
2
|
+
const devices=ctx.runCommand(`${ADB_PATH} devices -l`);const lines=devices.split("\n").filter(line=>line.trim()&&!line.includes("List of devices"));for(const line of lines){const parts=line.trim().split(/\s+/);if(parts.length>=2&&parts[1]==="device"){const deviceId=parts[0];if(!deviceId.startsWith("emulator-")){try{// nosemgrep: javascript.lang.security.audit.dangerous-spawn-shell-command.dangerous-spawn-shell-command - deviceId comes from adb devices output, not user input.
|
|
3
|
+
const qemuCheck=ctx.runCommand(`${ADB_PATH} -s ${deviceId} shell getprop ro.kernel.qemu`);if(!qemuCheck.trim()){progress.log(`Physical device detected: ${deviceId}`,"success");let deviceModel="Unknown Device";try{// nosemgrep: javascript.lang.security.audit.dangerous-spawn-shell-command.dangerous-spawn-shell-command - deviceId comes from adb devices output, not user input.
|
|
4
|
+
const model=ctx.runCommand(`${ADB_PATH} -s ${deviceId} shell getprop ro.product.model`);deviceModel=model.trim()||deviceModel;}catch(e){// Ignore model detection errors
|
|
5
|
+
}return{id:deviceId,model:deviceModel};}}catch(error){continue;}}}}progress.log("No physical devices detected","info");return null;}catch(error){progress.log("Error detecting physical devices: "+error.message,"error");return null;}}async function testPhysicalDeviceInstallation(ADB_PATH,deviceId){try{progress.log(`Testing installation capability on device ${deviceId}...`,"info");// nosemgrep: javascript.lang.security.audit.dangerous-spawn-shell-command.dangerous-spawn-shell-command - deviceId comes from adb devices output, not user input.
|
|
6
|
+
const connectionTest=ctx.runCommand(`${ADB_PATH} -s ${deviceId} shell echo "test"`);if(!connectionTest.includes("test")){progress.log("Device connection test failed","error");return false;}try{// nosemgrep: javascript.lang.security.audit.dangerous-spawn-shell-command.dangerous-spawn-shell-command - deviceId comes from adb devices output, not user input.
|
|
7
|
+
const developerOptions=ctx.runCommand(`${ADB_PATH} -s ${deviceId} shell settings get global development_settings_enabled`);if(developerOptions.trim()!=="1"){progress.log("Developer options not enabled on device","warning");return false;}}catch(error){progress.log("Cannot verify developer options status","warning");}try{// nosemgrep: javascript.lang.security.audit.dangerous-spawn-shell-command.dangerous-spawn-shell-command - deviceId comes from adb devices output, not user input.
|
|
8
|
+
const usbDebugging=ctx.runCommand(`${ADB_PATH} -s ${deviceId} shell settings get global adb_enabled`);if(usbDebugging.trim()!=="1"){progress.log("USB debugging not enabled on device","warning");return false;}}catch(error){progress.log("Cannot verify USB debugging status","warning");}try{// nosemgrep: javascript.lang.security.audit.dangerous-spawn-shell-command.dangerous-spawn-shell-command - deviceId comes from adb devices output, not user input.
|
|
9
|
+
ctx.runCommand(`${ADB_PATH} -s ${deviceId} shell pm list packages -3 | head -1`);}catch(error){progress.log("Cannot access package manager on device","error");return false;}progress.log(`Device ${deviceId} is ready for installation`,"success");return true;}catch(error){progress.log(`Installation capability test failed: ${error.message}`,"error");return false;}}async function checkEmulator(ADB_PATH){try{// nosemgrep: javascript.lang.security.audit.dangerous-spawn-shell-command.dangerous-spawn-shell-command - ADB_PATH is derived from androidConfig.sdkPath, a trusted internal config value.
|
|
10
|
+
const devices=ctx.runCommand(`${ADB_PATH} devices`);return devices.includes("emulator");}catch(error){progress.log("Error checking emulator status: "+error.message,"error");return false;}}async function startEmulator(EMULATOR_PATH,androidConfig){progress.log(`Starting emulator: ${androidConfig.emulatorName}...`,"info");// nosemgrep: javascript.lang.security.audit.dangerous-spawn-shell-command.dangerous-spawn-shell-command - EMULATOR_PATH is derived from androidConfig.sdkPath, a trusted internal config value.
|
|
11
|
+
return ctx.runInteractiveCommand(EMULATOR_PATH,["-avd",androidConfig.emulatorName,"-read-only"],{}).then(()=>{progress.log("Emulator started successfully","success");}).catch(error=>{progress.log("Error starting emulator: "+error.message,"error");throw error;});}async function handleEmulatorSetup(ADB_PATH,EMULATOR_PATH,androidConfig){progress.log("Setting up emulator...","info");const emulatorRunning=await checkEmulator(ADB_PATH);if(!emulatorRunning){progress.log("No emulator running, attempting to start one...","info");await startEmulator(EMULATOR_PATH,androidConfig);await new Promise(resolve=>setTimeout(resolve,5000));}else{progress.log("Emulator already running","success");}return{type:"emulator",name:androidConfig.emulatorName};}async function buildApp(ADB_PATH,androidConfig,buildOptimisation,targetDevice=null){progress.log("Building and installing app...","info");try{// nosemgrep: javascript.lang.security.audit.dangerous-spawn-shell-command.dangerous-spawn-shell-command - pwd and configPath are internal resolved paths, not user input.
|
|
12
|
+
let buildCommand=`cd ${pwd}/androidProject && ./gradlew generateWebViewConfig -PconfigPath=${configPath} -PbuildOptimisation=${buildOptimisation} && ./gradlew clean installDebug -PconfigPath=${configPath} --quiet --console=rich`;if(targetDevice&&targetDevice.type==="physical"){buildCommand=buildCommand.replace("installDebug",`installDebug -Pandroid.injected.target.device=${targetDevice.id}`);}await ctx.runInteractiveCommand("sh",["-c",buildCommand],{"BUILD SUCCESSFUL":""});progress.log("App build and installation completed successfully!","success");}catch(error){throw new Error("Error building/installing app: "+error.message);}}async function launchApp(ADB_PATH,buildType="debug",targetDevice=null){if(!targetDevice){progress.log("No target device specified, skipping launch","warning");return;}if(targetDevice.type==="physical"){progress.log(`App installed on physical device: ${targetDevice.model}`,"success");progress.log("Manual launch required - check your device to open the app","info");return;}try{progress.log("Launching app on emulator...","info");const packageName=`${ANDROID_PACKAGE}${buildType==="debug"?".debug":""}`;// nosemgrep: javascript.lang.security.audit.dangerous-spawn-shell-command.dangerous-spawn-shell-command - ADB_PATH is derived from androidConfig.sdkPath, a trusted internal config value.
|
|
13
|
+
const launchCommand=`${ADB_PATH} shell monkey -p ${packageName} 1`;await ctx.runInteractiveCommand("sh",["-c",launchCommand],{});progress.log("App launched successfully on emulator!","success");}catch(error){progress.log(`Warning: Could not auto-launch app: ${error.message}`,"warning");progress.log("App was installed successfully, but auto-launch failed","info");}}async function createAABConfig(androidConfig){const DEFAULT_PROJECT_PATH=`${pwd}/androidProject`;const aabConfig={projectPath:androidConfig.projectPath||DEFAULT_PROJECT_PATH,deploymentPath:DEFAULT_DEPLOYMENT_PATH,oldProjectName:DEFAULT_OLD_PROJECT_NAME,overwriteExisting:androidConfig.overwriteExisting!==undefined?androidConfig.overwriteExisting:DEFAULT_OVERWRITE_EXISTING,newProjectName:androidConfig.newProjectName||androidConfig.appName||androidConfig.packageName?.split(".").pop()||"catalystapp",packageName:androidConfig.packageName||null,createSignedAAB:true,outputPath:androidConfig.outputPath||`${process.cwd()}/build-output`};if(androidConfig.keystoreConfig){aabConfig.keystoreConfig=androidConfig.keystoreConfig;}else if(androidConfig.keystore){aabConfig.keystoreConfig={keyAlias:androidConfig.keystore.alias||"release",storePassword:androidConfig.keystore.storePassword,keyPassword:androidConfig.keystore.keyPassword,validityYears:25,organizationInfo:{companyName:androidConfig.keystore.organizationName||"YourCompany",city:androidConfig.keystore.city||"YourCity",state:androidConfig.keystore.state||"YourState",countryCode:androidConfig.keystore.countryCode||"US"}};}progress.log(`AAB Configuration:`,"info");progress.log(` Project Path: ${aabConfig.projectPath} ${aabConfig.projectPath===DEFAULT_PROJECT_PATH?"(default)":"(configured)"}`,"info");progress.log(` Deployment Path: ${aabConfig.deploymentPath} ${aabConfig.deploymentPath===DEFAULT_DEPLOYMENT_PATH?"(default)":"(configured)"}`,"info");progress.log(` Old Project Name: ${aabConfig.oldProjectName} ${aabConfig.oldProjectName===DEFAULT_OLD_PROJECT_NAME?"(default)":"(configured)"}`,"info");progress.log(` New Project Name: ${aabConfig.newProjectName}`,"info");progress.log(` Overwrite Existing: ${aabConfig.overwriteExisting} ${aabConfig.overwriteExisting===DEFAULT_OVERWRITE_EXISTING?"(default)":"(configured)"}`,"info");progress.log(` Output Path: ${aabConfig.outputPath}`,"info");return aabConfig;}async function moveApkToOutputPath(buildType,BUILD_OUTPUT_PATH,appName){try{if(!BUILD_OUTPUT_PATH){progress.log("BUILD_OUTPUT_PATH not set, skipping APK move","warning");return null;}const currentDate=new Date().toLocaleDateString("en-GB").replace(/\//g,"-");const currentTime=new Date().toLocaleTimeString("en-US",{hour12:true,hour:"2-digit",minute:"2-digit",second:"2-digit"}).replace(/:/g,"-");let destinationApkFileName="";if(appName){destinationApkFileName=buildType==="release"?`${appName}-${currentTime}.release.apk`:`${appName}-${currentTime}.debug.apk`;}else{destinationApkFileName=buildType==="release"?`app-${currentTime}.release.apk`:`app-${currentTime}.debug.apk`;}const sourceApkFileName=buildType==="release"?`app.apk`:`app-debug.apk`;const sourceApkPath=path.join(pwd,"androidProject","app","build","outputs","apk",buildType,sourceApkFileName);// nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal - pwd is an internal resolved path; buildType is "debug"/"release" from config; sourceApkFileName is derived from buildType.
|
|
14
|
+
const destinationDir=path.join(process.cwd(),BUILD_OUTPUT_PATH,"native","android",currentDate,buildType);// nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal - BUILD_OUTPUT_PATH is from trusted config; buildType and currentDate are internal values.
|
|
15
|
+
const destinationApkPath=path.join(destinationDir,destinationApkFileName);// nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal - destinationDir and destinationApkFileName are derived from internal config values, not user input.
|
|
16
|
+
if(!fs.existsSync(sourceApkPath)){progress.log(`APK not found at source path: ${sourceApkPath}`,"warning");return null;}if(!fs.existsSync(destinationDir)){fs.mkdirSync(destinationDir,{recursive:true});}fs.copyFileSync(sourceApkPath,destinationApkPath);return destinationApkPath;}catch(error){progress.log(`Error moving APK: ${error.message}`,"error");return null;}}async function buildSignedAAB(androidConfig){progress.log("Building signed AAB for release...","info");try{progress.log("Generating webview configuration for release...","info");try{// nosemgrep: javascript.lang.security.audit.dangerous-spawn-shell-command.dangerous-spawn-shell-command - pwd and configPath are internal resolved paths, not user input.
|
|
17
|
+
const generateConfigCommand=`cd ${pwd}/androidProject && ./gradlew generateWebViewConfig -PconfigPath=${configPath}`;await ctx.runInteractiveCommand("sh",["-c",generateConfigCommand],{"BUILD SUCCESSFUL":""});progress.log("Webview config generated successfully","success");}catch(configError){progress.log(`Warning: Webview config generation failed: ${configError.message}`,"warning");throw new Error("Webview config generation is required for release builds");}const aabConfig=await createAABConfig(androidConfig);await buildAndroidAAB(aabConfig);progress.log("Signed AAB build completed successfully!","success");}catch(error){throw new Error("Error building signed AAB: "+error.message);}}return{detectPhysicalDevice,testPhysicalDeviceInstallation,checkEmulator,handleEmulatorSetup,startEmulator,buildApp,launchApp,createAABConfig,moveApkToOutputPath,buildSignedAAB};}module.exports=createBuildPhase;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"use strict";const fs=require("fs");function createConfigPhase(ctx){const{configPath,progress}=ctx;async function initializeConfig(){const configFile=fs.readFileSync(configPath,"utf8");const config=JSON.parse(configFile);const{WEBVIEW_CONFIG,BUILD_OUTPUT_PATH}=config;if(!WEBVIEW_CONFIG||Object.keys(WEBVIEW_CONFIG).length===0){throw new Error("WebView Config missing in "+configPath);}if(!WEBVIEW_CONFIG.android){throw new Error("Android config missing in WebView Config");}const buildType=WEBVIEW_CONFIG.android.buildType||"debug";progress.log(`Build Type: ${buildType}`,"info");if(buildType==="release"){progress.log("Release build detected - AAB will be generated","info");}return{WEBVIEW_CONFIG,BUILD_OUTPUT_PATH};}function validateAndroidTools(androidConfig){const ANDROID_SDK=androidConfig.sdkPath;const ADB_PATH=`${ANDROID_SDK}/platform-tools/adb`;const EMULATOR_PATH=`${ANDROID_SDK}/emulator/emulator`;progress.log("Validating Android tools...","info");if(!ANDROID_SDK){throw new Error("Android SDK path is not configured");}if(!fs.existsSync(ANDROID_SDK)){throw new Error(`Android SDK path does not exist: ${ANDROID_SDK}`);}if(!fs.existsSync(ADB_PATH)){throw new Error(`ADB not found at: ${ADB_PATH}`);}try{ctx.runCommand(`${ADB_PATH} version`);progress.log("ADB validation successful","success");}catch(error){throw new Error(`ADB is not working properly: ${error.message}`);}const buildType=androidConfig.buildType||"debug";if(buildType!=="release"){if(!fs.existsSync(EMULATOR_PATH)){throw new Error(`Emulator not found at: ${EMULATOR_PATH}`);}try{ctx.runCommand(`${EMULATOR_PATH} -version`);progress.log("Emulator validation successful","success");}catch(error){throw new Error(`Emulator is not working properly: ${error.message}`);}try{const avdOutput=ctx.runCommand(`${EMULATOR_PATH} -list-avds`);if(!avdOutput.includes(androidConfig.emulatorName)){throw new Error(`Specified emulator "${androidConfig.emulatorName}" not found in available AVDs`);}progress.log(`Emulator "${androidConfig.emulatorName}" exists`,"success");}catch(error){throw new Error(`Error checking emulator AVD: ${error.message}`);}}else{progress.log("Skipping emulator validation for release build","info");}progress.log("Android tools validation completed successfully!","success");return{ANDROID_SDK,ADB_PATH,EMULATOR_PATH};}return{initializeConfig,validateAndroidTools};}module.exports=createConfigPhase;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"use strict";const path=require("path");const{runCommand,runInteractiveCommand}=require("../utils.js");const TerminalProgress=require("../TerminalProgress.js").default;const{composeAndroidPlugins}=require("../pluginComposerAndroid.js");const{resolvePluginConfig,resolveInternalPluginsRoot}=require("../internalPluginUtils.js");const createConfigPhase=require("./config.js");const createAssetsPhase=require("./assets.js");const createBuildPhase=require("./build.js");const catalystCorePath=path.dirname(require.resolve("catalyst-core/package.json"));const pwd=path.join(catalystCorePath,"dist/native");const ANDROID_PACKAGE="io.yourname.androidproject";const steps={config:"Initialize Configuration",tools:"Validate Android Tools",emulator:"Check and Start Emulator",copyAssets:"Copy Build Assets",build:"Build and Install Application",aab:"Build Signed AAB"};const progressConfig={titlePaddingTop:2,titlePaddingBottom:1,stepPaddingLeft:4,stepSpacing:1,errorPaddingLeft:6,bottomMargin:2};function createAndroidBuild(config){const{WEBVIEW_CONFIG,BUILD_OUTPUT_PATH}=config;const configPath=`${process.cwd()}/config/config.json`;const publicPath=`${process.cwd()}/public`;const progress=new TerminalProgress(steps,"Catalyst Android Build",progressConfig);const ctx={WEBVIEW_CONFIG,BUILD_OUTPUT_PATH,configPath,publicPath,pwd,progress,ANDROID_PACKAGE,runCommand,runInteractiveCommand};const{initializeConfig,validateAndroidTools}=createConfigPhase(ctx);const{copyBuildAssets,copySplashscreenAssets,copyOfflinePage,copyIconAssets,configureAppName,processNotifications}=createAssetsPhase(ctx);const{detectPhysicalDevice,testPhysicalDeviceInstallation,handleEmulatorSetup,buildApp,launchApp,moveApkToOutputPath,buildSignedAAB}=createBuildPhase(ctx);async function buildAndroidApp(){let androidConfig=null;let targetDevice=null;try{progress.start("config");const{WEBVIEW_CONFIG:wvConfig,BUILD_OUTPUT_PATH:bop}=await initializeConfig();androidConfig=wvConfig.android;const buildType=androidConfig.buildType||"debug";const buildOptimisation=!!androidConfig.buildOptimisation||false;progress.complete("config");progress.start("tools");const{ADB_PATH,EMULATOR_PATH}=validateAndroidTools(androidConfig);progress.complete("tools");if(buildType!=="release"){progress.start("emulator");const physicalDevice=await detectPhysicalDevice(ADB_PATH);if(physicalDevice){progress.log(`Found physical device: ${physicalDevice.model} (${physicalDevice.id})`,"success");const canInstallOnPhysical=await testPhysicalDeviceInstallation(ADB_PATH,physicalDevice.id);if(canInstallOnPhysical){targetDevice={type:"physical",id:physicalDevice.id,model:physicalDevice.model};progress.log(`Using physical device: ${physicalDevice.model}`,"success");}else{progress.log("Physical device installation test failed, falling back to emulator","warning");targetDevice=await handleEmulatorSetup(ADB_PATH,EMULATOR_PATH,androidConfig);}}else{targetDevice=await handleEmulatorSetup(ADB_PATH,EMULATOR_PATH,androidConfig);}progress.complete("emulator");}else{progress.log("Skipping device setup for release build","info");}progress.start("copyAssets");await copyBuildAssets(androidConfig,buildOptimisation);await copySplashscreenAssets();await copyOfflinePage();await copyIconAssets();await configureAppName(androidConfig);const pluginConfig=resolvePluginConfig(wvConfig);composeAndroidPlugins({corePluginsRoot:resolveInternalPluginsRoot(catalystCorePath),androidProjectPath:`${pwd}/androidProject`,pluginConfig,log:(message,status="info")=>progress.log(message,status)});await processNotifications(wvConfig);progress.log(`Build optimization: ${buildOptimisation?"Enabled":"Disabled"}`,"info");progress.complete("copyAssets");let movedApkPath=null;if(buildType==="release"){progress.start("aab");await buildSignedAAB(androidConfig);progress.complete("aab");movedApkPath=await moveApkToOutputPath(buildType,bop,androidConfig.appName);}else{progress.start("build");await buildApp(ADB_PATH,androidConfig,buildOptimisation,targetDevice);await launchApp(ADB_PATH,buildType,targetDevice);progress.complete("build");movedApkPath=await moveApkToOutputPath(buildType,bop,androidConfig.appName);}const summaryItems=["Build completed successfully:",{text:`Build Type: ${buildType}`,indent:1,prefix:"├─ ",color:"gray"},{text:`SDK Path: ${androidConfig.sdkPath}`,indent:1,prefix:"├─ ",color:"gray"},{text:`Build Optimization: ${buildOptimisation?"Enabled":"Disabled"}`,indent:1,prefix:"├─ ",color:"gray"}];if(buildType==="release"){if(movedApkPath){summaryItems.push({text:`APK Build Location: ${movedApkPath}`,indent:1,prefix:"├─ ",color:"green"});}summaryItems.push({text:`Output: Signed AAB generated in build-output/`,indent:1,prefix:"└─ ",color:"green"});}else{if(movedApkPath){summaryItems.push({text:`APK Build Location: ${movedApkPath}`,indent:1,prefix:"├─ ",color:"green"});}if(targetDevice&&targetDevice.type==="physical"){summaryItems.push({text:`Target Device: ${targetDevice.model} (Physical)`,indent:1,prefix:"└─ ",color:"green"});}else{summaryItems.push({text:`Target Device: ${androidConfig.emulatorName} (Emulator)`,indent:1,prefix:"└─ ",color:"gray"});}}progress.printTreeContent("Build Summary",summaryItems);}catch(error){if(progress.currentStep){progress.fail(progress.currentStep.id,error.message);const troubleshootingItems=["Build failed. Please try the following steps:",{text:"Check if Android SDK is properly configured",indent:1,prefix:"├─ ",color:"yellow"},{text:"Verify build assets exist in the source directory",indent:1,prefix:"├─ ",color:"yellow"}];const buildType=androidConfig?.buildType||"debug";if(buildType!=="release"){if(error.message.includes("physical device")||error.message.includes("installation test")){troubleshootingItems.push("\nPhysical Device Issues:",{text:"Enable Developer Options on your device",indent:1,prefix:"├─ ",color:"yellow"},{text:"Enable USB Debugging in Developer Options",indent:1,prefix:"├─ ",color:"yellow"},{text:'Accept the "Allow USB Debugging" prompt on your device',indent:1,prefix:"├─ ",color:"yellow"},{text:"Try disconnecting and reconnecting your device",indent:1,prefix:"├─ ",color:"yellow"},{text:'Check if "adb devices" shows your device as authorized',indent:1,prefix:"├─ ",color:"yellow"});}troubleshootingItems.push({text:"Verify that the emulator exists and is working",indent:1,prefix:"├─ ",color:"yellow"},{text:"If using physical device, ensure it stays connected",indent:1,prefix:"├─ ",color:"yellow"});}else{troubleshootingItems.push({text:"Verify keystore configuration for release builds",indent:1,prefix:"├─ ",color:"yellow"},{text:"Check that keystore passwords are properly set",indent:1,prefix:"├─ ",color:"yellow"});}troubleshootingItems.push({text:'Run "npm run setupEmulator:android" to reconfigure Android settings',indent:1,prefix:"└─ ",color:"yellow"},"\nVerify Configuration:");if(androidConfig){troubleshootingItems.push({text:`Build Type: ${buildType}`,indent:1,prefix:"├─ ",color:"gray"},{text:`Android SDK Path: ${androidConfig.sdkPath||"Not configured"}`,indent:1,prefix:"├─ ",color:"gray"});if(buildType!=="release"){if(targetDevice){troubleshootingItems.push({text:`Target Device: ${targetDevice.type==="physical"?`${targetDevice.model} (Physical)`:`${androidConfig.emulatorName} (Emulator)`}`,indent:1,prefix:"└─ ",color:"gray"});}else{troubleshootingItems.push({text:`Selected Emulator: ${androidConfig.emulatorName||"Not configured"}`,indent:1,prefix:"└─ ",color:"gray"});}}else{troubleshootingItems.push({text:`Output Path: ${androidConfig.outputPath||"build-output/"}`,indent:1,prefix:"└─ ",color:"gray"});}}else{troubleshootingItems.push({text:"Configuration could not be loaded",indent:1,prefix:"├─ ",color:"red"},{text:"Check if config/config.json exists and has valid Android configuration",indent:1,prefix:"└─ ",color:"red"});}progress.printTreeContent("Troubleshooting Guide",troubleshootingItems);}throw error;}}async function buildAndroidForTesting(){try{const{WEBVIEW_CONFIG:wvConfig}=await initializeConfig();const androidConfig=wvConfig.android;const buildOptimisation=!!androidConfig.buildOptimisation||false;await copyBuildAssets(androidConfig,buildOptimisation);await copySplashscreenAssets();await copyOfflinePage();await copyIconAssets();await configureAppName(androidConfig);const pluginConfig=resolvePluginConfig(wvConfig);composeAndroidPlugins({corePluginsRoot:resolveInternalPluginsRoot(catalystCorePath),androidProjectPath:`${pwd}/androidProject`,pluginConfig,log:(message,status="info")=>progress.log(message,status)});await processNotifications(wvConfig);progress.log("✅ buildAndroidForTesting complete — project ready for gradlew test","success");return{success:true};}catch(error){progress.log("buildAndroidForTesting failed: "+error.message,"error");throw error;}}return{buildAndroidApp,buildAndroidForTesting,progress,WEBVIEW_CONFIG};}module.exports={createAndroidBuild,pwd};
|
|
@@ -1,148 +1,2 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
const DEFAULT_PROJECT_PATH=`${pwd}/androidProject`;const DEFAULT_DEPLOYMENT_PATH="./deployment";const DEFAULT_OLD_PROJECT_NAME="androidProject";// Use actual project name in catalyst
|
|
4
|
-
const DEFAULT_OVERWRITE_EXISTING=true;const steps={config:"Initialize Configuration",tools:"Validate Android Tools",emulator:"Check and Start Emulator",copyAssets:"Copy Build Assets",build:"Build and Install Application",aab:"Build Signed AAB"};const progressConfig={titlePaddingTop:2,titlePaddingBottom:1,stepPaddingLeft:4,stepSpacing:1,errorPaddingLeft:6,bottomMargin:2};const progress=new _TerminalProgress.default(steps,"Catalyst Android Build",progressConfig);async function initializeConfig(){const configFile=_fs.default.readFileSync(configPath,"utf8");const config=JSON.parse(configFile);const{WEBVIEW_CONFIG,BUILD_OUTPUT_PATH}=config;if(!WEBVIEW_CONFIG||Object.keys(WEBVIEW_CONFIG).length===0){throw new Error("WebView Config missing in "+configPath);}if(!WEBVIEW_CONFIG.android){throw new Error("Android config missing in WebView Config");}// Log build type information
|
|
5
|
-
const buildType=WEBVIEW_CONFIG.android.buildType||"debug";progress.log(`Build Type: ${buildType}`,"info");if(buildType==="release"){progress.log("Release build detected - AAB will be generated","info");}return{WEBVIEW_CONFIG,BUILD_OUTPUT_PATH};}function validateAndroidTools(androidConfig){const ANDROID_SDK=androidConfig.sdkPath;const ADB_PATH=`${ANDROID_SDK}/platform-tools/adb`;const EMULATOR_PATH=`${ANDROID_SDK}/emulator/emulator`;progress.log("Validating Android tools...","info");if(!ANDROID_SDK){throw new Error("Android SDK path is not configured");}if(!_fs.default.existsSync(ANDROID_SDK)){throw new Error(`Android SDK path does not exist: ${ANDROID_SDK}`);}if(!_fs.default.existsSync(ADB_PATH)){throw new Error(`ADB not found at: ${ADB_PATH}`);}try{;(0,_utils.runCommand)(`${ADB_PATH} version`);progress.log("ADB validation successful","success");}catch(error){throw new Error(`ADB is not working properly: ${error.message}`);}// Skip emulator validation for release builds
|
|
6
|
-
const buildType=androidConfig.buildType||"debug";if(buildType!=="release"){if(!_fs.default.existsSync(EMULATOR_PATH)){throw new Error(`Emulator not found at: ${EMULATOR_PATH}`);}try{;(0,_utils.runCommand)(`${EMULATOR_PATH} -version`);progress.log("Emulator validation successful","success");}catch(error){throw new Error(`Emulator is not working properly: ${error.message}`);}try{const avdOutput=(0,_utils.runCommand)(`${EMULATOR_PATH} -list-avds`);if(!avdOutput.includes(androidConfig.emulatorName)){throw new Error(`Specified emulator "${androidConfig.emulatorName}" not found in available AVDs`);}progress.log(`Emulator "${androidConfig.emulatorName}" exists`,"success");}catch(error){throw new Error(`Error checking emulator AVD: ${error.message}`);}}else{progress.log("Skipping emulator validation for release build","info");}progress.log("Android tools validation completed successfully!","success");return{ANDROID_SDK,ADB_PATH,EMULATOR_PATH};}async function detectPhysicalDevice(ADB_PATH){try{progress.log("Detecting physical devices...","info");const devices=(0,_utils.runCommand)(`${ADB_PATH} devices -l`);const lines=devices.split("\n").filter(line=>line.trim()&&!line.includes("List of devices"));for(const line of lines){const parts=line.trim().split(/\s+/);if(parts.length>=2&&parts[1]==="device"){const deviceId=parts[0];// Check if it's a physical device (not emulator)
|
|
7
|
-
if(!deviceId.startsWith("emulator-")){try{// Double-check it's not an emulator using device properties
|
|
8
|
-
const qemuCheck=(0,_utils.runCommand)(`${ADB_PATH} -s ${deviceId} shell getprop ro.kernel.qemu`);if(!qemuCheck.trim()){// Empty means physical device
|
|
9
|
-
progress.log(`Physical device detected: ${deviceId}`,"success");// Get device model/name for better logging
|
|
10
|
-
let deviceModel="Unknown Device";try{const model=(0,_utils.runCommand)(`${ADB_PATH} -s ${deviceId} shell getprop ro.product.model`);deviceModel=model.trim()||deviceModel;}catch(e){// Ignore model detection errors
|
|
11
|
-
}return{id:deviceId,model:deviceModel};}}catch(error){// If we can't check properties, skip this device
|
|
12
|
-
continue;}}}}progress.log("No physical devices detected","info");return null;}catch(error){progress.log("Error detecting physical devices: "+error.message,"error");return null;}}async function testPhysicalDeviceInstallation(ADB_PATH,deviceId){try{progress.log(`Testing installation capability on device ${deviceId}...`,"info");// Test 1: Basic ADB connection
|
|
13
|
-
const connectionTest=(0,_utils.runCommand)(`${ADB_PATH} -s ${deviceId} shell echo "test"`);if(!connectionTest.includes("test")){progress.log("Device connection test failed","error");return false;}// Test 2: Check if developer options are enabled
|
|
14
|
-
try{const developerOptions=(0,_utils.runCommand)(`${ADB_PATH} -s ${deviceId} shell settings get global development_settings_enabled`);if(developerOptions.trim()!=="1"){progress.log("Developer options not enabled on device","warning");return false;}}catch(error){progress.log("Cannot verify developer options status","warning");}// Test 3: Check USB debugging status
|
|
15
|
-
try{const usbDebugging=(0,_utils.runCommand)(`${ADB_PATH} -s ${deviceId} shell settings get global adb_enabled`);if(usbDebugging.trim()!=="1"){progress.log("USB debugging not enabled on device","warning");return false;}}catch(error){progress.log("Cannot verify USB debugging status","warning");}// Test 4: Check if we can access package manager
|
|
16
|
-
try{;(0,_utils.runCommand)(`${ADB_PATH} -s ${deviceId} shell pm list packages -3 | head -1`);}catch(error){progress.log("Cannot access package manager on device","error");return false;}progress.log(`Device ${deviceId} is ready for installation`,"success");return true;}catch(error){progress.log(`Installation capability test failed: ${error.message}`,"error");return false;}}async function checkEmulator(ADB_PATH){try{const devices=(0,_utils.runCommand)(`${ADB_PATH} devices`);return devices.includes("emulator");}catch(error){progress.log("Error checking emulator status: "+error.message,"error");return false;}}async function handleEmulatorSetup(ADB_PATH,EMULATOR_PATH,androidConfig){progress.log("Setting up emulator...","info");const emulatorRunning=await checkEmulator(ADB_PATH);if(!emulatorRunning){progress.log("No emulator running, attempting to start one...","info");await startEmulator(EMULATOR_PATH,androidConfig);await new Promise(resolve=>setTimeout(resolve,5000));}else{progress.log("Emulator already running","success");}return{type:"emulator",name:androidConfig.emulatorName};}async function startEmulator(EMULATOR_PATH,androidConfig){progress.log(`Starting emulator: ${androidConfig.emulatorName}...`,"info");return(0,_utils.runInteractiveCommand)(EMULATOR_PATH,["-avd",androidConfig.emulatorName,"-read-only"],{}).then(()=>{progress.log("Emulator started successfully","success");}).catch(error=>{progress.log("Error starting emulator: "+error.message,"error");throw error;});}async function copyBuildAssets(androidConfig,buildOptimisation=false){if(!buildOptimisation)return;progress.log("Copying build assets to Android project...","info");try{// Define source and destination paths
|
|
17
|
-
const sourcePath=`${process.env.PWD}/build/`;const destPath=`${pwd}/androidProject/app/src/main/assets/build/`// Create destination directory if it doesn't exist
|
|
18
|
-
;(0,_utils.runCommand)(`mkdir -p ${destPath}`)// Clear existing destination to avoid conflicts
|
|
19
|
-
;(0,_utils.runCommand)(`rm -rf ${destPath}/*`);// Files to exclude from copying
|
|
20
|
-
const excludePatterns=["route-manifest.json.gz","route-manifest.json.br"];if(buildOptimisation){progress.log("Running with build optimization...","info");const excludeParams=excludePatterns.map(pattern=>`--exclude="${pattern}"`).join(" ");const rsyncCommand=`rsync -av ${excludeParams} ${sourcePath} ${destPath}`;progress.log("Executing rsync command with exclusions...","info");(0,_utils.runCommand)(rsyncCommand);// Verify excluded files don't exist in destination
|
|
21
|
-
for(const pattern of excludePatterns){const checkCommand=`find ${destPath} -name "${pattern}" | wc -l`;const count=parseInt((0,_utils.runCommand)(checkCommand).trim(),10);if(count>0){progress.log(`Warning: Found ${count} instances of excluded file ${pattern}`,"warning");(0,_utils.runCommand)(`find ${destPath} -name "${pattern}" -delete`);}}progress.log("Build assets copied with optimization (excluded route-manifest JSON files)","success");}else{progress.log("Running without build optimization...","info");const exclusions=excludePatterns.map(pattern=>`-not -name "${pattern}"`).join(" ");const copyCommand=`find ${sourcePath} -type f ${exclusions} -exec cp --parents {} ${destPath} \\;`;progress.log(`Executing copy command with exclusions...`,"info");(0,_utils.runCommand)(copyCommand);progress.log("Build assets copied successfully!","success");}}catch(error){throw new Error("Error copying build assets: "+error.message);}}async function buildApp(ADB_PATH,androidConfig,buildOptimisation,targetDevice=null){progress.log("Building and installing app...","info");try{// Build command without monkey launch
|
|
22
|
-
let buildCommand=`cd ${pwd}/androidProject && ./gradlew generateWebViewConfig -PconfigPath=${configPath} -PbuildOptimisation=${buildOptimisation} && ./gradlew clean installDebug -PconfigPath=${configPath} --quiet --console=rich`;// Add device-specific install target if physical device
|
|
23
|
-
if(targetDevice&&targetDevice.type==="physical"){buildCommand=buildCommand.replace("installDebug",`installDebug -Pandroid.injected.target.device=${targetDevice.id}`);}await(0,_utils.runInteractiveCommand)("sh",["-c",buildCommand],{"BUILD SUCCESSFUL":""});progress.log("App build and installation completed successfully!","success");}catch(error){throw new Error("Error building/installing app: "+error.message);}}async function launchApp(ADB_PATH,buildType="debug",targetDevice=null){if(!targetDevice){progress.log("No target device specified, skipping launch","warning");return;}if(targetDevice.type==="physical"){progress.log(`App installed on physical device: ${targetDevice.model}`,"success");progress.log("Manual launch required - check your device to open the app","info");return;}// Launch on emulator
|
|
24
|
-
try{progress.log("Launching app on emulator...","info");const packageName=`${ANDROID_PACKAGE}${buildType==="debug"?".debug":""}`;const launchCommand=`${ADB_PATH} shell monkey -p ${packageName} 1`;await(0,_utils.runInteractiveCommand)("sh",["-c",launchCommand],{});progress.log("App launched successfully on emulator!","success");}catch(error){progress.log(`Warning: Could not auto-launch app: ${error.message}`,"warning");progress.log("App was installed successfully, but auto-launch failed","info");}}async function copySplashscreenAssets(){try{const destPath=`${pwd}/androidProject/app/src/main/res`;const configFile=_fs.default.readFileSync(configPath,"utf8");const config=JSON.parse(configFile);const{WEBVIEW_CONFIG={}}=config;if(!WEBVIEW_CONFIG.splashScreen)return;const androidPublicPath=`${process.env.PWD}/public/android`;const imageFormats=["png","jpg","jpeg","gif","bmp","webp"];// Copy splash screen image if exists
|
|
25
|
-
if(_fs.default.existsSync(androidPublicPath)){const drawableDir=`${destPath}/drawable`;if(!_fs.default.existsSync(drawableDir)){_fs.default.mkdirSync(drawableDir,{recursive:true});}for(const format of imageFormats){const existingPath=`${destPath}/drawable/splashscreen.${format}`;if(_fs.default.existsSync(existingPath)){_fs.default.unlinkSync(existingPath);}}for(const format of imageFormats){const sourcePath=`${androidPublicPath}/splashscreen.${format}`;if(_fs.default.existsSync(sourcePath)){_fs.default.copyFileSync(sourcePath,`${destPath}/drawable/splashscreen.${format}`);break;}}}// Update theme background color
|
|
26
|
-
const backgroundColor=WEBVIEW_CONFIG.splashScreen.backgroundColor||"#ffffff";const themeFiles=[`${destPath}/values/themes.xml`,`${destPath}/values-night/themes.xml`];for(const themesFile of themeFiles){if(_fs.default.existsSync(themesFile)){let content=_fs.default.readFileSync(themesFile,"utf8");// Replace windowBackground - matches any color value
|
|
27
|
-
content=content.replace(/<item name="android:windowBackground">.*?<\/item>/,`<item name="android:windowBackground">${backgroundColor}</item>`);// Replace splash screen background - matches any color value
|
|
28
|
-
content=content.replace(/<item name="android:windowSplashScreenBackground" tools:targetApi="31">.*?<\/item>/,`<item name="android:windowSplashScreenBackground" tools:targetApi="31">${backgroundColor}</item>`);_fs.default.writeFileSync(themesFile,content);}}}catch(error){progress.log(`Error processing splash screen: ${error.message}`,"warning");}}async function copyOfflinePage(){try{const sourcePath=`${process.env.PWD}/public/offline.html`;const destDir=`${pwd}/androidProject/app/src/main/assets/offline`;const destPath=`${destDir}/offline.html`;if(!_fs.default.existsSync(sourcePath)){progress.log("offline.html not found in public/; skipping offline asset copy","warning");return;}// Ensure destination directory exists
|
|
29
|
-
;(0,_utils.runCommand)(`mkdir -p ${destDir}`);_fs.default.copyFileSync(sourcePath,destPath);progress.log("offline.html copied to Android assets","success");}catch(error){progress.log(`Warning: Error copying offline.html: ${error.message}`,"warning");}}async function copyIconAssets(){try{const destPath=`${pwd}/androidProject/app/src/main/res`;const manifestPath=`${pwd}/androidProject/app/src/main/AndroidManifest.xml`;const androidIconDir=_path.default.join(publicPath,"android","appIcons");const fallbackIconPath=_path.default.join(__dirname,"assets","catalyst.png");const fallbackExists=_fs.default.existsSync(fallbackIconPath);const extensions=["png","jpg","jpeg"];if(!_fs.default.existsSync(publicPath)){progress.log(`Warning: Public directory not found at ${publicPath}`,"warning");return;}const densities=[{key:"mdpi",dir:"mipmap-mdpi"},{key:"hdpi",dir:"mipmap-hdpi"},{key:"xhdpi",dir:"mipmap-xhdpi"},{key:"xxhdpi",dir:"mipmap-xxhdpi"},{key:"xxxhdpi",dir:"mipmap-xxxhdpi"}];const hasIconDirectory=_fs.default.existsSync(androidIconDir)&&_fs.default.lstatSync(androidIconDir).isDirectory();const cleanDensityDir=dir=>{if(!_fs.default.existsSync(dir)){return;}for(const file of _fs.default.readdirSync(dir)){if(file.startsWith("icon.")){_fs.default.unlinkSync(_path.default.join(dir,file));}}};const findIconForDensity=densityKey=>{if(!hasIconDirectory){return null;}for(const ext of extensions){const candidate=_path.default.join(androidIconDir,`icon-${densityKey}.${ext}`);if(_fs.default.existsSync(candidate)){return{path:candidate,ext};}}return null;};let hasCustomIcons=false;let usedFallback=false;for(const density of densities){const targetDir=_path.default.join(destPath,density.dir);const source=findIconForDensity(density.key);cleanDensityDir(targetDir);if(source){_fs.default.mkdirSync(targetDir,{recursive:true});const destination=_path.default.join(targetDir,`icon.${source.ext}`);_fs.default.copyFileSync(source.path,destination);hasCustomIcons=true;}}if(!hasCustomIcons&&fallbackExists){const targetDir=_path.default.join(destPath,"mipmap-xxxhdpi");_fs.default.mkdirSync(targetDir,{recursive:true});cleanDensityDir(targetDir);const destination=_path.default.join(targetDir,"icon.png");_fs.default.copyFileSync(fallbackIconPath,destination);usedFallback=true;}const setManifestIcons=(iconValue,roundIconValue)=>{if(!_fs.default.existsSync(manifestPath))return;const current=_fs.default.readFileSync(manifestPath,"utf8");const updated=current.replace(/android:icon="[^"]*"/g,`android:icon="${iconValue}"`).replace(/android:roundIcon="[^"]*"/g,`android:roundIcon="${roundIconValue}"`);if(updated!==current){_fs.default.writeFileSync(manifestPath,updated);}};if(!hasCustomIcons&&!usedFallback){progress.log("No custom Android icons found; using default template icons.","info");setManifestIcons("@mipmap/ic_launcher","@mipmap/ic_launcher_round");return;}setManifestIcons("@mipmap/icon","@mipmap/icon");if(hasCustomIcons){progress.log("Applied Android launcher icons from public/android/appIcons.","success");}if(usedFallback){progress.log("Used bundled Catalyst fallback icon for launcher.","info");}}catch(error){progress.log(`Warning: Error copying app icon assets: ${error.message}`,"warning");}}async function configureAppName(androidConfig){try{const destPath=`${pwd}/androidProject/app/src/main/res`;const stringsFile=`${destPath}/values/strings.xml`;let stringsContent=_fs.default.readFileSync(stringsFile,"utf8");// Check if androidConfig.appName exists
|
|
30
|
-
if(androidConfig.appName){stringsContent=stringsContent.replace(/<string name="app_name">.*?<\/string>/,`<string name="app_name">${androidConfig.appName}</string>`);_fs.default.writeFileSync(stringsFile,stringsContent);progress.log(`App display name configured: ${androidConfig.appName}`,"success");}else{// No appName configured, revert to default
|
|
31
|
-
stringsContent=stringsContent.replace(/<string name="app_name">.*?<\/string>/,`<string name="app_name">Catalyst Application</string>`);_fs.default.writeFileSync(stringsFile,stringsContent);progress.log("App display name reverted to default: Catalyst Application","info");}}catch(error){progress.log(`Warning: Error configuring app name: ${error.message}`,"warning");}}async function handleGoogleServicesJson(){try{const rootGoogleServicesPath=`${process.env.PWD}/google-services.json`;const androidGoogleServicesPath=`${pwd}/androidProject/app/google-services.json`;// Check if google-services.json exists in the root directory
|
|
32
|
-
if(_fs.default.existsSync(rootGoogleServicesPath)){progress.log("Found google-services.json in root directory","info");// Create the app directory if it doesn't exist
|
|
33
|
-
const appDir=`${pwd}/androidProject/app`;if(!_fs.default.existsSync(appDir)){_fs.default.mkdirSync(appDir,{recursive:true});}// Copy the file to the Android project
|
|
34
|
-
_fs.default.copyFileSync(rootGoogleServicesPath,androidGoogleServicesPath);progress.log("Copied google-services.json to androidProject/app/","success");return true;}else if(_fs.default.existsSync(androidGoogleServicesPath)){progress.log("google-services.json already exists in androidProject/app/","info");return true;}else{progress.log("google-services.json not found - Firebase push notifications will not work","warning");progress.log("Place google-services.json in project root or src/native/androidProject/app/","info");return false;}}catch(error){progress.log(`Warning: Error handling google-services.json: ${error.message}`,"warning");return false;}}async function processNotifications(WEBVIEW_CONFIG){const hasNotificationConfig=!!WEBVIEW_CONFIG.notifications?.enabled;try{// Always clean up notification configurations first (regardless of config)
|
|
35
|
-
await cleanupNotificationPermissions();await cleanupNotificationResources();await cleanupNotificationMetadata();await cleanupNotificationAssets();if(!hasNotificationConfig){progress.log("Notifications disabled - cleaned up notification configurations","info");return;}// Handle google-services.json file for Firebase
|
|
36
|
-
const hasGoogleServices=await handleGoogleServicesJson();if(!hasGoogleServices){progress.log("Continuing without Firebase - only local notifications will work","warning");}// Only add configurations if notifications are enabled
|
|
37
|
-
await addNotificationPermissions();await generateNotificationResources(WEBVIEW_CONFIG.notifications);await addNotificationMetadata();await processNotificationAssets();progress.log("Notification configuration completed successfully!","success");}catch(error){progress.log(`Warning: Error processing notifications: ${error.message}`,"warning");}}async function addNotificationPermissions(){try{const manifestPath=`${pwd}/androidProject/app/src/main/AndroidManifest.xml`;let manifestContent=_fs.default.readFileSync(manifestPath,"utf8");const permissionsXml=` <uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
|
38
|
-
<uses-permission android:name="android.permission.VIBRATE" />
|
|
39
|
-
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
|
40
|
-
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />`;manifestContent=manifestContent.replace(/(<uses-permission[^>]*>[\s\S]*?)(\s*<uses-feature)/,`$1\n${permissionsXml}$2`);_fs.default.writeFileSync(manifestPath,manifestContent);progress.log("Added notification permissions to AndroidManifest.xml","success");}catch(error){throw new Error(`Failed to add notification permissions: ${error.message}`);}}async function generateNotificationResources(notificationConfig){try{const colorsPath=`${pwd}/androidProject/app/src/main/res/values/colors.xml`;let colorsContent=_fs.default.readFileSync(colorsPath,"utf8");const notificationColorXml=` <color name="notification_color">${notificationConfig.color||"#007AFF"}</color>`;if(!colorsContent.includes('name="notification_color"')){colorsContent=colorsContent.replace(/(<\/resources>)/,`${notificationColorXml}\n$1`);_fs.default.writeFileSync(colorsPath,colorsContent);progress.log("Added notification color to colors.xml","success");}}catch(error){throw new Error(`Failed to generate notification resources: ${error.message}`);}}async function addNotificationMetadata(){try{const manifestPath=`${pwd}/androidProject/app/src/main/AndroidManifest.xml`;let manifestContent=_fs.default.readFileSync(manifestPath,"utf8");const metadataXml=`
|
|
41
|
-
<!-- Default notification configuration -->
|
|
42
|
-
<meta-data
|
|
43
|
-
android:name="default_notification_channel_id"
|
|
44
|
-
android:value="default_notifications" />
|
|
45
|
-
<meta-data
|
|
46
|
-
android:name="default_notification_icon"
|
|
47
|
-
android:resource="@drawable/ic_notification" />
|
|
48
|
-
<meta-data
|
|
49
|
-
android:name="default_notification_color"
|
|
50
|
-
android:resource="@color/notification_color" />
|
|
51
|
-
|
|
52
|
-
<!-- Firebase default notification configuration -->
|
|
53
|
-
<meta-data
|
|
54
|
-
android:name="com.google.firebase.messaging.default_notification_channel_id"
|
|
55
|
-
android:value="default" />
|
|
56
|
-
<meta-data
|
|
57
|
-
android:name="com.google.firebase.messaging.default_notification_icon"
|
|
58
|
-
android:resource="@drawable/ic_notification" />
|
|
59
|
-
<meta-data
|
|
60
|
-
android:name="com.google.firebase.messaging.default_notification_sound"
|
|
61
|
-
android:resource="@raw/notification_sound_default" />
|
|
62
|
-
|
|
63
|
-
<!-- Push Notification Service -->
|
|
64
|
-
<service
|
|
65
|
-
android:name="io.yourname.androidproject.utils.PushNotificationUtils"
|
|
66
|
-
android:exported="false">
|
|
67
|
-
<intent-filter>
|
|
68
|
-
<action android:name="com.google.firebase.MESSAGING_EVENT" />
|
|
69
|
-
</intent-filter>
|
|
70
|
-
</service>`;manifestContent=manifestContent.replace(/(\s*<\/application>)/,`${metadataXml}\n$1`);_fs.default.writeFileSync(manifestPath,manifestContent);progress.log("Added notification metadata and push notification service to AndroidManifest.xml","success");}catch(error){throw new Error(`Failed to add notification metadata: ${error.message}`);}}async function processNotificationAssets(){try{const destPath=`${pwd}/androidProject/app/src/main/res`;const imageFormats=["png","jpg","jpeg","gif","bmp","svg","webp"];const audioFormats=["mp3","wav","ogg"];let assetsProcessed=0;// Define notification icons to process
|
|
71
|
-
const notificationIcons=[{sourceName:"notification-icon",resourceName:"ic_notification"},{sourceName:"notification-large",resourceName:"ic_notification_large"}];// Define notification sounds to process
|
|
72
|
-
const notificationSounds=[{sourceName:"notification-sound-default",resourceName:"notification_sound_default"},{sourceName:"notification-sound-urgent",resourceName:"notification_sound_urgent"}];// Remove existing notification icons to avoid conflicts
|
|
73
|
-
for(const icon of notificationIcons){for(const format of imageFormats){const existingIconPath=`${destPath}/drawable/${icon.resourceName}.${format}`;if(_fs.default.existsSync(existingIconPath)){_fs.default.unlinkSync(existingIconPath);progress.log(`Removed existing ${icon.resourceName}.${format}`,"info");}}}// Remove existing notification sounds to avoid conflicts
|
|
74
|
-
for(const sound of notificationSounds){for(const format of audioFormats){const existingSoundPath=`${destPath}/raw/${sound.resourceName}.${format}`;if(_fs.default.existsSync(existingSoundPath)){_fs.default.unlinkSync(existingSoundPath);progress.log(`Removed existing ${sound.resourceName}.${format}`,"info");}}}// Process notification icons
|
|
75
|
-
let iconFound=false;for(const icon of notificationIcons){for(const format of imageFormats){const iconImagePath=`${publicPath}/${icon.sourceName}.${format}`;if(_fs.default.existsSync(iconImagePath)){const destImagePath=`${destPath}/drawable/${icon.resourceName}.${format}`;_fs.default.copyFileSync(iconImagePath,destImagePath);progress.log(`Notification icon copied: ${icon.sourceName}.${format} -> ${icon.resourceName}.${format}`,"success");assetsProcessed++;// Track if we found the main notification icon
|
|
76
|
-
if(icon.sourceName==="notification-icon"){iconFound=true;}break;}}}// Generate default notification icon if not found
|
|
77
|
-
if(!iconFound){generateNotificationIconDrawable("ic_notification",destPath);progress.log("Generated default notification icon","info");}// Create raw directory if it doesn't exist
|
|
78
|
-
const rawDir=`${destPath}/raw`;if(!_fs.default.existsSync(rawDir)){_fs.default.mkdirSync(rawDir,{recursive:true});}// Process notification sounds
|
|
79
|
-
for(const sound of notificationSounds){for(const format of audioFormats){const soundImagePath=`${publicPath}/${sound.sourceName}.${format}`;if(_fs.default.existsSync(soundImagePath)){const destSoundPath=`${destPath}/raw/${sound.resourceName}.${format}`;_fs.default.copyFileSync(soundImagePath,destSoundPath);progress.log(`Notification sound copied: ${sound.sourceName}.${format} -> ${sound.resourceName}.${format}`,"success");assetsProcessed++;break;}}}if(assetsProcessed>0){progress.log(`Processed ${assetsProcessed} notification assets from public/`,"success");}else{progress.log("No notification assets found in public/ - using defaults","info");}}catch(error){throw new Error(`Failed to process notification assets: ${error.message}`);}}function generateNotificationIconDrawable(iconName,destPath){// Create vector drawable for notification icon as fallback
|
|
80
|
-
const iconXml=`<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
|
81
|
-
android:width="24dp"
|
|
82
|
-
android:height="24dp"
|
|
83
|
-
android:viewportWidth="24"
|
|
84
|
-
android:viewportHeight="24"
|
|
85
|
-
android:tint="?attr/colorOnPrimary">
|
|
86
|
-
<path
|
|
87
|
-
android:fillColor="@android:color/white"
|
|
88
|
-
android:pathData="M12,22c1.1,0 2,-0.9 2,-2h-4c0,1.1 0.9,2 2,2zM18,16v-5c0,-3.07 -1.64,-5.64 -4.5,-6.32V4c0,-0.83 -0.67,-1.5 -1.5,-1.5s-1.5,0.67 -1.5,1.5v0.68C7.63,5.36 6,7.92 6,11v5l-2,2v1h16v-1l-2,-2z"/>
|
|
89
|
-
</vector>`;_fs.default.writeFileSync(`${destPath}/drawable/${iconName}.xml`,iconXml);}async function cleanupNotificationPermissions(){try{const manifestPath=`${pwd}/androidProject/app/src/main/AndroidManifest.xml`;let manifestContent=_fs.default.readFileSync(manifestPath,"utf8");// Remove notification permissions
|
|
90
|
-
const notificationPermissions=["android.permission.POST_NOTIFICATIONS","android.permission.VIBRATE","android.permission.WAKE_LOCK","android.permission.RECEIVE_BOOT_COMPLETED"];manifestContent=manifestContent.split("\n").filter(line=>{return!notificationPermissions.some(permission=>line.includes(`<uses-permission android:name="${permission}"`));}).join("\n");_fs.default.writeFileSync(manifestPath,manifestContent);}catch(error){// Don't throw error for cleanup operations, just log
|
|
91
|
-
progress.log(`Warning: Error cleaning notification permissions: ${error.message}`,"warning");}}async function cleanupNotificationResources(){try{const colorsPath=`${pwd}/androidProject/app/src/main/res/values/colors.xml`;let colorsContent=_fs.default.readFileSync(colorsPath,"utf8");// Remove notification color
|
|
92
|
-
const existingColorLine=colorsContent.split("\n").find(line=>line.includes('name="notification_color"'));if(existingColorLine){colorsContent=colorsContent.replace(existingColorLine,"");_fs.default.writeFileSync(colorsPath,colorsContent);}}catch(error){progress.log(`Warning: Error cleaning notification resources: ${error.message}`,"warning");}}async function cleanupNotificationMetadata(){try{const manifestPath=`${pwd}/androidProject/app/src/main/AndroidManifest.xml`;let manifestContent=_fs.default.readFileSync(manifestPath,"utf8");// Remove notification metadata entries
|
|
93
|
-
const metadataNames=["default_notification_channel_id","default_notification_icon","default_notification_color","com.google.firebase.messaging.default_notification_channel_id","com.google.firebase.messaging.default_notification_icon","com.google.firebase.messaging.default_notification_sound"];manifestContent=manifestContent.split("\n").filter(line=>{return!metadataNames.some(metadataName=>line.includes(`android:name="${metadataName}"`));}).join("\n");// Remove Push Notification Service
|
|
94
|
-
const serviceRegex=/\s*<!--\s*Push Notification Service\s*-->\s*<service[^>]*android:name="[^"]*PushNotificationUtils"[\s\S]*?<\/service>/gi;manifestContent=manifestContent.replace(serviceRegex,"");// Clean up any standalone notification comments
|
|
95
|
-
manifestContent=manifestContent.replace(/\s*<!--\s*Default notification configuration\s*-->/gi,"");manifestContent=manifestContent.replace(/\s*<!--\s*Firebase default notification configuration\s*-->/gi,"");manifestContent=manifestContent.replace(/\s*<!--\s*Push Notification Service\s*-->/gi,"");_fs.default.writeFileSync(manifestPath,manifestContent);}catch(error){progress.log(`Warning: Error cleaning notification metadata: ${error.message}`,"warning");}}async function cleanupNotificationAssets(){try{const destPath=`${pwd}/androidProject/app/src/main/res`;const imageFormats=["png","jpg","jpeg","gif","bmp","svg","webp","xml"];const audioFormats=["mp3","wav","ogg"];// Define notification icons to clean up
|
|
96
|
-
const notificationIcons=["ic_notification","ic_notification_large"];// Define notification sounds to clean up
|
|
97
|
-
const notificationSounds=["notification_sound_default","notification_sound_urgent"];// Remove existing notification icons
|
|
98
|
-
for(const icon of notificationIcons){for(const format of imageFormats){const iconPath=`${destPath}/drawable/${icon}.${format}`;if(_fs.default.existsSync(iconPath)){_fs.default.unlinkSync(iconPath);}}}// Remove existing notification sounds
|
|
99
|
-
const rawDir=`${destPath}/raw`;if(_fs.default.existsSync(rawDir)){for(const sound of notificationSounds){for(const format of audioFormats){const soundPath=`${rawDir}/${sound}.${format}`;if(_fs.default.existsSync(soundPath)){_fs.default.unlinkSync(soundPath);}}}}}catch(error){progress.log(`Warning: Error cleaning notification assets: ${error.message}`,"warning");}}// Legacy function for backward compatibility
|
|
100
|
-
// async function installApp(ADB_PATH, androidConfig, buildOptimisation, buildType = "debug") {
|
|
101
|
-
// await buildApp(ADB_PATH, androidConfig, buildOptimisation, buildType)
|
|
102
|
-
// await launchApp(ADB_PATH, buildType, { type: "emulator" })
|
|
103
|
-
// }
|
|
104
|
-
async function createAABConfig(androidConfig){// Create AAB configuration based on WebView config with defaults applied
|
|
105
|
-
const aabConfig={// Apply defaults here in buildAppAndroid.js
|
|
106
|
-
projectPath:androidConfig.projectPath||DEFAULT_PROJECT_PATH,deploymentPath:DEFAULT_DEPLOYMENT_PATH,oldProjectName:DEFAULT_OLD_PROJECT_NAME,overwriteExisting:androidConfig.overwriteExisting!==undefined?androidConfig.overwriteExisting:DEFAULT_OVERWRITE_EXISTING,// Required field - use configured value or derive from other config
|
|
107
|
-
newProjectName:androidConfig.newProjectName||androidConfig.appName||androidConfig.packageName?.split(".").pop()||"catalystapp",// IMPORTANT: Pass packageName for renaming
|
|
108
|
-
packageName:androidConfig.packageName||null,// AAB specific settings
|
|
109
|
-
createSignedAAB:true,outputPath:androidConfig.outputPath||`${process.env.PWD}/build-output`};// Add keystore configuration if available
|
|
110
|
-
if(androidConfig.keystoreConfig){aabConfig.keystoreConfig=androidConfig.keystoreConfig;}else if(androidConfig.keystore){// Map old keystore format to new format
|
|
111
|
-
aabConfig.keystoreConfig={keyAlias:androidConfig.keystore.alias||"release",storePassword:androidConfig.keystore.storePassword,keyPassword:androidConfig.keystore.keyPassword,validityYears:25,organizationInfo:{companyName:androidConfig.keystore.organizationName||"YourCompany",city:androidConfig.keystore.city||"YourCity",state:androidConfig.keystore.state||"YourState",countryCode:androidConfig.keystore.countryCode||"US"}};}// Log the configuration being used
|
|
112
|
-
progress.log(`AAB Configuration:`,"info");progress.log(` Project Path: ${aabConfig.projectPath} ${aabConfig.projectPath===DEFAULT_PROJECT_PATH?"(default)":"(configured)"}`,"info");progress.log(` Deployment Path: ${aabConfig.deploymentPath} ${aabConfig.deploymentPath===DEFAULT_DEPLOYMENT_PATH?"(default)":"(configured)"}`,"info");progress.log(` Old Project Name: ${aabConfig.oldProjectName} ${aabConfig.oldProjectName===DEFAULT_OLD_PROJECT_NAME?"(default)":"(configured)"}`,"info");progress.log(` New Project Name: ${aabConfig.newProjectName}`,"info");progress.log(` Overwrite Existing: ${aabConfig.overwriteExisting} ${aabConfig.overwriteExisting===DEFAULT_OVERWRITE_EXISTING?"(default)":"(configured)"}`,"info");progress.log(` Output Path: ${aabConfig.outputPath}`,"info");return aabConfig;}async function moveApkToOutputPath(buildType,BUILD_OUTPUT_PATH,appName){try{// Check if required environment variables are set
|
|
113
|
-
if(!process.env.PWD||!BUILD_OUTPUT_PATH){progress.log("Environment variables PWD or BUILD_OUTPUT_PATH not set, skipping APK move","warning");return null;}const currentDate=new Date().toLocaleDateString("en-GB").replace(/\//g,"-");// DD-MM-YYYY format
|
|
114
|
-
const currentTime=new Date().toLocaleTimeString("en-US",{hour12:true,hour:"2-digit",minute:"2-digit",second:"2-digit"}).replace(/:/g,":");// HH-MM-SS AM/PM format
|
|
115
|
-
let destinationApkFileName="";// Construct source and destination paths
|
|
116
|
-
if(appName){destinationApkFileName=buildType==="release"?`${appName}-${currentTime}.release.apk`:`${appName}-${currentTime}.debug.apk`;}else{destinationApkFileName=buildType==="release"?`app-${currentTime}.release.apk`:`app-${currentTime}.debug.apk`;}const sourceApkFileName=buildType==="release"?`app.apk`:`app-debug.apk`;const sourceApkPath=_path.default.join(pwd,"androidProject","app","build","outputs","apk",buildType,sourceApkFileName);const destinationDir=_path.default.join(process.env.PWD,BUILD_OUTPUT_PATH,"native","android",currentDate,buildType);const destinationApkPath=_path.default.join(destinationDir,destinationApkFileName);// Check if source APK exists
|
|
117
|
-
if(!_fs.default.existsSync(sourceApkPath)){progress.log(`APK not found at source path: ${sourceApkPath}`,"warning");return null;}// Create destination directory if it doesn't exist
|
|
118
|
-
if(!_fs.default.existsSync(destinationDir)){_fs.default.mkdirSync(destinationDir,{recursive:true});}// Copy APK to destination
|
|
119
|
-
_fs.default.copyFileSync(sourceApkPath,destinationApkPath);return destinationApkPath;}catch(error){progress.log(`Error moving APK: ${error.message}`,"error");return null;}}async function buildSignedAAB(androidConfig){progress.log("Building signed AAB for release...","info");try{// Generate webview config BEFORE creating AAB
|
|
120
|
-
progress.log("Generating webview configuration for release...","info");try{const generateConfigCommand=`cd ${pwd}/androidProject && ./gradlew generateWebViewConfig -PconfigPath=${configPath}`;await(0,_utils.runInteractiveCommand)("sh",["-c",generateConfigCommand],{"BUILD SUCCESSFUL":""});progress.log("Webview config generated successfully","success");}catch(configError){progress.log(`Warning: Webview config generation failed: ${configError.message}`,"warning");throw new Error("Webview config generation is required for release builds");}// Create AAB configuration directly without temporary file
|
|
121
|
-
const aabConfig=await createAABConfig(androidConfig);// Call the AAB builder directly with config object
|
|
122
|
-
await(0,_renameAndroidProject.buildAndroidAAB)(aabConfig);progress.log("Signed AAB build completed successfully!","success");}catch(error){throw new Error("Error building signed AAB: "+error.message);}}async function buildAndroidApp(){// Initialize androidConfig outside try block to ensure it's available in catch
|
|
123
|
-
let androidConfig=null;let targetDevice=null;try{// Initialize configuration
|
|
124
|
-
progress.start("config");const{WEBVIEW_CONFIG,BUILD_OUTPUT_PATH}=await initializeConfig();androidConfig=WEBVIEW_CONFIG.android;const buildType=androidConfig.buildType||"debug";const buildOptimisation=!!androidConfig.buildOptimisation||false;progress.complete("config");// Validate tools and get paths
|
|
125
|
-
progress.start("tools");const{ADB_PATH,EMULATOR_PATH}=await validateAndroidTools(androidConfig);progress.complete("tools");// Device detection and setup (skip for release builds)
|
|
126
|
-
if(buildType!=="release"){progress.start("emulator");// Step 1: Check for physical device first
|
|
127
|
-
const physicalDevice=await detectPhysicalDevice(ADB_PATH);if(physicalDevice){progress.log(`Found physical device: ${physicalDevice.model} (${physicalDevice.id})`,"success");// Test if we can install on physical device
|
|
128
|
-
const canInstallOnPhysical=await testPhysicalDeviceInstallation(ADB_PATH,physicalDevice.id);if(canInstallOnPhysical){// Use physical device - skip emulator entirely
|
|
129
|
-
targetDevice={type:"physical",id:physicalDevice.id,model:physicalDevice.model};progress.log(`Using physical device: ${physicalDevice.model}`,"success");}else{// Physical device failed, fallback to emulator
|
|
130
|
-
progress.log("Physical device installation test failed, falling back to emulator","warning");targetDevice=await handleEmulatorSetup(ADB_PATH,EMULATOR_PATH,androidConfig);}}else{// No physical device, use emulator (current behavior)
|
|
131
|
-
targetDevice=await handleEmulatorSetup(ADB_PATH,EMULATOR_PATH,androidConfig);}progress.complete("emulator");}else{progress.log("Skipping device setup for release build","info");}// Copy build assets
|
|
132
|
-
progress.start("copyAssets");await copyBuildAssets(androidConfig,buildOptimisation);await copySplashscreenAssets();await copyOfflinePage();await copyIconAssets();await configureAppName(androidConfig);const pluginConfig=(0,_internalPluginUtils.resolvePluginConfig)(WEBVIEW_CONFIG);(0,_pluginComposerAndroid.composeAndroidPlugins)({corePluginsRoot:(0,_internalPluginUtils.resolveInternalPluginsRoot)(catalystCorePath),androidProjectPath:`${pwd}/androidProject`,pluginConfig,log:(message,status="info")=>progress.log(message,status)});await processNotifications(WEBVIEW_CONFIG);progress.log(`Build optimization: ${buildOptimisation?"Enabled":"Disabled"}`,"info");progress.complete("copyAssets");// Build based on type
|
|
133
|
-
let movedApkPath=null;if(buildType==="release"){// Build signed AAB for release
|
|
134
|
-
progress.start("aab");await buildSignedAAB(androidConfig);progress.complete("aab");// Move APK to output directory
|
|
135
|
-
movedApkPath=await moveApkToOutputPath(buildType,BUILD_OUTPUT_PATH,androidConfig.appName);}else{// Install debug app for development
|
|
136
|
-
progress.start("build");await buildApp(ADB_PATH,androidConfig,buildOptimisation,targetDevice);await launchApp(ADB_PATH,buildType,targetDevice);progress.complete("build");// Move APK to output directory
|
|
137
|
-
movedApkPath=await moveApkToOutputPath(buildType,BUILD_OUTPUT_PATH,androidConfig.appName);}// Print build summary
|
|
138
|
-
const summaryItems=["Build completed successfully:",{text:`Build Type: ${buildType}`,indent:1,prefix:"├─ ",color:"gray"},{text:`SDK Path: ${androidConfig.sdkPath}`,indent:1,prefix:"├─ ",color:"gray"},{text:`Build Optimization: ${buildOptimisation?"Enabled":"Disabled"}`,indent:1,prefix:"├─ ",color:"gray"}];if(buildType==="release"){// Add APK path for release builds
|
|
139
|
-
// Show moved APK location if move was successful
|
|
140
|
-
if(movedApkPath){summaryItems.push({text:`APK Build Location: ${movedApkPath}`,indent:1,prefix:"├─ ",color:"green"});}summaryItems.push({text:`Output: Signed AAB generated in build-output/`,indent:1,prefix:"└─ ",color:"green"});}else{// Add APK path for debug builds
|
|
141
|
-
// Show moved APK location if move was successful
|
|
142
|
-
if(movedApkPath){summaryItems.push({text:`APK Build Location: ${movedApkPath}`,indent:1,prefix:"├─ ",color:"green"});}if(targetDevice&&targetDevice.type==="physical"){summaryItems.push({text:`Target Device: ${targetDevice.model} (Physical)`,indent:1,prefix:"└─ ",color:"green"});}else{summaryItems.push({text:`Target Device: ${androidConfig.emulatorName} (Emulator)`,indent:1,prefix:"└─ ",color:"gray"});}}progress.printTreeContent("Build Summary",summaryItems);process.exit(0);}catch(error){if(progress.currentStep){progress.fail(progress.currentStep.id,error.message);const troubleshootingItems=["Build failed. Please try the following steps:",{text:"Check if Android SDK is properly configured",indent:1,prefix:"├─ ",color:"yellow"},{text:"Verify build assets exist in the source directory",indent:1,prefix:"├─ ",color:"yellow"}];// Use androidConfig safely with null checks
|
|
143
|
-
const buildType=androidConfig?.buildType||"debug";// Device-specific troubleshooting
|
|
144
|
-
if(buildType!=="release"){// Check if error is related to device issues
|
|
145
|
-
if(error.message.includes("physical device")||error.message.includes("installation test")){troubleshootingItems.push("\nPhysical Device Issues:",{text:"Enable Developer Options on your device",indent:1,prefix:"├─ ",color:"yellow"},{text:"Enable USB Debugging in Developer Options",indent:1,prefix:"├─ ",color:"yellow"},{text:'Accept the "Allow USB Debugging" prompt on your device',indent:1,prefix:"├─ ",color:"yellow"},{text:"Try disconnecting and reconnecting your device",indent:1,prefix:"├─ ",color:"yellow"},{text:'Check if "adb devices" shows your device as authorized',indent:1,prefix:"├─ ",color:"yellow"});}if(buildType==="release"){troubleshootingItems.push({text:"Verify keystore configuration for release builds",indent:1,prefix:"├─ ",color:"yellow"},{text:"Check that keystore passwords are properly set",indent:1,prefix:"├─ ",color:"yellow"});}else{troubleshootingItems.push({text:"Verify that the emulator exists and is working",indent:1,prefix:"├─ ",color:"yellow"},{text:"If using physical device, ensure it stays connected",indent:1,prefix:"├─ ",color:"yellow"});}}else{troubleshootingItems.push({text:"Verify keystore configuration for release builds",indent:1,prefix:"├─ ",color:"yellow"},{text:"Check that keystore passwords are properly set",indent:1,prefix:"├─ ",color:"yellow"});}troubleshootingItems.push({text:'Run "npm run setupEmulator:android" to reconfigure Android settings',indent:1,prefix:"└─ ",color:"yellow"},"\nVerify Configuration:");// Add configuration details only if androidConfig is available
|
|
146
|
-
if(androidConfig){troubleshootingItems.push({text:`Build Type: ${buildType}`,indent:1,prefix:"├─ ",color:"gray"},{text:`Android SDK Path: ${androidConfig.sdkPath||"Not configured"}`,indent:1,prefix:"├─ ",color:"gray"});if(buildType!=="release"){// Show device information if available
|
|
147
|
-
if(targetDevice){troubleshootingItems.push({text:`Target Device: ${targetDevice.type==="physical"?`${targetDevice.model} (Physical)`:`${androidConfig.emulatorName} (Emulator)`}`,indent:1,prefix:"└─ ",color:"gray"});}else{troubleshootingItems.push({text:`Selected Emulator: ${androidConfig.emulatorName||"Not configured"}`,indent:1,prefix:"└─ ",color:"gray"});}}else{troubleshootingItems.push({text:`Output Path: ${androidConfig.outputPath||"build-output/"}`,indent:1,prefix:"└─ ",color:"gray"});}}else{troubleshootingItems.push({text:"Configuration could not be loaded",indent:1,prefix:"├─ ",color:"red"},{text:"Check if config/config.json exists and has valid Android configuration",indent:1,prefix:"└─ ",color:"red"});}progress.printTreeContent("Troubleshooting Guide",troubleshootingItems);}process.exit(1);}}// Execute the main build process
|
|
148
|
-
buildAndroidApp();
|
|
1
|
+
"use strict";const{createAndroidBuild,pwd}=require("./buildAndroid/index.js");const{WEBVIEW_CONFIG,BUILD_OUTPUT_PATH}=require(`${process.cwd()}/config/config.json`);async function main(){const build=createAndroidBuild({WEBVIEW_CONFIG,BUILD_OUTPUT_PATH});const{buildAndroidApp}=build;try{await buildAndroidApp();}catch(error){process.exit(1);}process.exit(0);}if(require.main===module){main();}// Legacy re-exports for any tooling that imports from this file directly
|
|
2
|
+
module.exports={createAndroidBuild,pwd};
|