UNPKG

muspe-cli

Version:

MusPE Advanced Framework v2.1.3 - Mobile User-friendly Simple Progressive Engine with Enhanced CLI Tools, Specialized E-Commerce Templates, Material Design 3, Progressive Enhancement, Mobile Optimizations, Performance Analysis, and Enterprise-Grade Develo

474 lines (384 loc) 15 kB
const fs = require('fs-extra'); const path = require('path'); const chalk = require('chalk'); const ora = require('ora'); const spawn = require('cross-spawn'); const yaml = require('js-yaml'); // Enhanced Android Build System for MusPE class AndroidBuilder { constructor(projectRoot, config) { this.projectRoot = projectRoot; this.config = config; this.platformsDir = path.join(projectRoot, 'platforms', 'android'); this.buildConfig = this.loadBuildConfig(); } loadBuildConfig() { try { const configPath = path.join(this.projectRoot, 'config', 'mobile-build.yml'); if (fs.existsSync(configPath)) { const configFile = fs.readFileSync(configPath, 'utf8'); return yaml.load(configFile); } } catch (error) { console.warn(chalk.yellow('Warning: Could not load mobile build config, using defaults')); } return this.getDefaultConfig(); } getDefaultConfig() { return { android: { packageName: 'com.muspe.app', versionCode: 1, versionName: '1.0.0', compileSdkVersion: 34, targetSdkVersion: 34, minSdkVersion: 24, buildType: 'release' } }; } async build(options = {}) { const spinner = ora('Building Android application...').start(); try { // Ensure Cordova platform is added await this.ensurePlatform(); // Configure build settings await this.configureBuild(); // Copy web assets await this.copyWebAssets(); // Configure Android-specific files await this.configureAndroidFiles(); // Build the APK/AAB const buildType = options.release ? 'release' : 'debug'; await this.buildApp(buildType, options); spinner.succeed('Android build completed successfully'); // Display build information await this.displayBuildInfo(buildType); } catch (error) { spinner.fail('Android build failed'); throw error; } } async ensurePlatform() { if (!await fs.pathExists(this.platformsDir)) { console.log(chalk.blue('Adding Android platform...')); await this.runCordovaCommand('platform', ['add', 'android']); } } async configureBuild() { // Configure build.gradle await this.configureBuildGradle(); // Configure AndroidManifest.xml await this.configureManifest(); // Configure gradle.properties await this.configureGradleProperties(); // Configure strings.xml await this.configureStrings(); } async configureBuildGradle() { const buildGradlePath = path.join(this.platformsDir, 'app', 'build.gradle'); if (await fs.pathExists(buildGradlePath)) { let buildGradle = await fs.readFile(buildGradlePath, 'utf8'); // Update Android configuration buildGradle = buildGradle.replace( /compileSdkVersion \d+/, `compileSdkVersion ${this.buildConfig.android.compileSdkVersion}` ); buildGradle = buildGradle.replace( /targetSdkVersion \d+/, `targetSdkVersion ${this.buildConfig.android.targetSdkVersion}` ); buildGradle = buildGradle.replace( /minSdkVersion \d+/, `minSdkVersion ${this.buildConfig.android.minSdkVersion}` ); // Add optimization settings for release builds if (!buildGradle.includes('buildTypes')) { buildGradle = buildGradle.replace( 'android {', `android { buildTypes { release { minifyEnabled ${this.buildConfig.android.minifyEnabled || true} shrinkResources ${this.buildConfig.android.shrinkResources || true} proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' } debug { debuggable true } }` ); } // Add signing configuration if available if (this.buildConfig.android.signing) { const signingConfig = ` signingConfigs { release { keyAlias '${this.buildConfig.android.signing.alias}' keyPassword '${process.env.ANDROID_KEY_PASSWORD || this.buildConfig.android.signing.keyPassword}' storeFile file('${this.buildConfig.android.signing.keystore}') storePassword '${process.env.ANDROID_KEYSTORE_PASSWORD || this.buildConfig.android.signing.storePassword}' } }`; if (!buildGradle.includes('signingConfigs')) { buildGradle = buildGradle.replace('android {', `android {${signingConfig}`); } // Update release build type to use signing config buildGradle = buildGradle.replace( 'release {', `release { signingConfig signingConfigs.release` ); } await fs.writeFile(buildGradlePath, buildGradle); } } async configureManifest() { const manifestPath = path.join(this.platformsDir, 'app', 'src', 'main', 'AndroidManifest.xml'); if (await fs.pathExists(manifestPath)) { let manifest = await fs.readFile(manifestPath, 'utf8'); // Update package name manifest = manifest.replace( /package="[^"]*"/, `package="${this.buildConfig.android.packageName}"` ); // Update version code and name manifest = manifest.replace( /android:versionCode="\d+"/, `android:versionCode="${this.buildConfig.android.versionCode}"` ); manifest = manifest.replace( /android:versionName="[^"]*"/, `android:versionName="${this.buildConfig.android.versionName}"` ); // Add permissions if specified if (this.buildConfig.android.permissions) { const permissionsXml = this.buildConfig.android.permissions .map(permission => ` <uses-permission android:name="${permission}" />`) .join('\n'); if (!manifest.includes(this.buildConfig.android.permissions[0])) { manifest = manifest.replace( '<manifest', `<manifest xmlns:android="http://schemas.android.com/apk/res/android">\n${permissionsXml}\n\n<manifest` ); } } // Add features if specified if (this.buildConfig.android.features) { const featuresXml = this.buildConfig.android.features .map(feature => ` <uses-feature android:name="${feature}" android:required="false" />`) .join('\n'); if (!manifest.includes(this.buildConfig.android.features[0])) { manifest = manifest.replace( '</manifest>', ` ${featuresXml}\n</manifest>` ); } } await fs.writeFile(manifestPath, manifest); } } async configureGradleProperties() { const gradlePropsPath = path.join(this.platformsDir, 'gradle.properties'); const gradleProps = ` # MusPE Android Build Configuration org.gradle.jvmargs=${this.buildConfig.android.gradle?.jvmArgs || '-Xmx4096m -XX:MaxPermSize=512m'} org.gradle.daemon=${this.buildConfig.android.gradle?.daemon || true} org.gradle.parallel=${this.buildConfig.android.gradle?.parallel || true} org.gradle.configureondemand=${this.buildConfig.android.gradle?.configureondemand || true} # Android specific android.useAndroidX=true android.enableJetifier=true android.enableR8.fullMode=true # Performance optimizations org.gradle.caching=true org.gradle.vfs.watch=true `; await fs.writeFile(gradlePropsPath, gradleProps); } async configureStrings() { const stringsPath = path.join(this.platformsDir, 'app', 'src', 'main', 'res', 'values', 'strings.xml'); if (await fs.pathExists(stringsPath)) { const appName = this.config.name || 'MusPE App'; const stringsXml = `<?xml version='1.0' encoding='utf-8'?> <resources> <string name="app_name">${appName}</string> <string name="launcher_name">@string/app_name</string> <string name="activity_name">@string/launcher_name</string> </resources>`; await fs.writeFile(stringsPath, stringsXml); } } async copyWebAssets() { const webAssetsPath = path.join(this.projectRoot, 'www'); const androidAssetsPath = path.join(this.platformsDir, 'app', 'src', 'main', 'assets', 'www'); if (await fs.pathExists(webAssetsPath)) { await fs.copy(webAssetsPath, androidAssetsPath); } } async configureAndroidFiles() { // Configure ProGuard rules for release builds await this.configureProGuard(); // Configure app icons await this.configureIcons(); // Configure splash screens await this.configureSplashScreens(); } async configureProGuard() { const proguardPath = path.join(this.platformsDir, 'app', 'proguard-rules.pro'); const proguardRules = ` # MusPE ProGuard Configuration -keep class * extends java.util.ListResourceBundle { protected Object[][] getContents(); } # Keep line numbers for better crash reports -keepattributes SourceFile,LineNumberTable # Cordova specific -keep class org.apache.cordova.** { *; } -keep class org.crosswalk.** { *; } -keep class cordova.** { *; } # MusPE specific -keep class com.muspe.** { *; } # WebView JavaScript interface -keepclassmembers class * { @android.webkit.JavascriptInterface <methods>; } # Prevent obfuscation of JavaScript interface -keepattributes JavascriptInterface -keepattributes *Annotation* # Keep native methods -keepclasseswithmembernames class * { native <methods>; } `; await fs.writeFile(proguardPath, proguardRules); } async configureIcons() { const iconsDir = path.join(this.projectRoot, 'assets', 'icons'); const androidIconsDir = path.join(this.platformsDir, 'app', 'src', 'main', 'res'); if (await fs.pathExists(iconsDir)) { // Copy icon files to appropriate density folders const densities = ['mdpi', 'hdpi', 'xhdpi', 'xxhdpi', 'xxxhdpi']; for (const density of densities) { const densityDir = path.join(androidIconsDir, `drawable-${density}`); await fs.ensureDir(densityDir); // Copy app icon const iconSrc = path.join(iconsDir, `icon-${density}.png`); const iconDest = path.join(densityDir, 'icon.png'); if (await fs.pathExists(iconSrc)) { await fs.copy(iconSrc, iconDest); } } } } async configureSplashScreens() { const splashDir = path.join(this.projectRoot, 'assets', 'splash'); const androidSplashDir = path.join(this.platformsDir, 'app', 'src', 'main', 'res'); if (await fs.pathExists(splashDir)) { // Copy splash screens to appropriate density folders const densities = ['land-mdpi', 'land-hdpi', 'land-xhdpi', 'land-xxhdpi', 'land-xxxhdpi', 'port-mdpi', 'port-hdpi', 'port-xhdpi', 'port-xxhdpi', 'port-xxxhdpi']; for (const density of densities) { const densityDir = path.join(androidSplashDir, `drawable-${density}`); await fs.ensureDir(densityDir); // Copy splash screen const splashSrc = path.join(splashDir, `splash-${density}.png`); const splashDest = path.join(densityDir, 'splash.png'); if (await fs.pathExists(splashSrc)) { await fs.copy(splashSrc, splashDest); } } } } async buildApp(buildType, options) { const buildArgs = [buildType]; if (options.bundle) { buildArgs.push('--packageType=bundle'); } if (options.verbose) { buildArgs.push('--verbose'); } await this.runCordovaCommand('build', ['android', '--' + buildType, ...buildArgs]); } async displayBuildInfo(buildType) { console.log(chalk.green('\n✨ Android build completed!')); console.log(chalk.cyan('\n📱 Build artifacts:')); const apkPath = path.join(this.platformsDir, 'app', 'build', 'outputs', 'apk', buildType); const bundlePath = path.join(this.platformsDir, 'app', 'build', 'outputs', 'bundle', buildType); if (await fs.pathExists(apkPath)) { const apkFiles = await fs.readdir(apkPath); apkFiles.forEach(file => { if (file.endsWith('.apk')) { console.log(` ${chalk.gray('APK:')} ${path.join(apkPath, file)}`); } }); } if (await fs.pathExists(bundlePath)) { const bundleFiles = await fs.readdir(bundlePath); bundleFiles.forEach(file => { if (file.endsWith('.aab')) { console.log(` ${chalk.gray('AAB:')} ${path.join(bundlePath, file)}`); } }); } console.log(chalk.cyan('\n📋 Next steps:')); console.log(` ${chalk.gray('Install:')} muspe cordova run android --device`); console.log(` ${chalk.gray('Test:')} muspe cordova emulate android`); if (buildType === 'release') { console.log(` ${chalk.gray('Upload:')} Upload APK/AAB to Google Play Console`); } } async runCordovaCommand(command, args) { return new Promise((resolve, reject) => { const child = spawn('cordova', [command, ...args], { cwd: this.projectRoot, stdio: 'inherit' }); child.on('close', (code) => { if (code === 0) { resolve(); } else { reject(new Error(`Cordova command failed with code ${code}`)); } }); child.on('error', reject); }); } // Signing and Distribution async generateKeystore(options) { const keystorePath = path.join(this.projectRoot, 'android-release-key.keystore'); const keytoolArgs = [ '-genkey', '-v', '-keystore', keystorePath, '-alias', options.alias || 'muspe-app', '-keyalg', 'RSA', '-keysize', '2048', '-validity', '10000', '-dname', `CN=${options.commonName || 'MusPE App'}, OU=${options.organizationalUnit || 'Development'}, O=${options.organization || 'MusPE'}, L=${options.locality || 'City'}, S=${options.state || 'State'}, C=${options.country || 'US'}`, '-storepass', options.storePassword, '-keypass', options.keyPassword ]; return new Promise((resolve, reject) => { const child = spawn('keytool', keytoolArgs, { stdio: 'inherit' }); child.on('close', (code) => { if (code === 0) { console.log(chalk.green(`Keystore generated: ${keystorePath}`)); resolve(keystorePath); } else { reject(new Error(`Keystore generation failed with code ${code}`)); } }); child.on('error', reject); }); } async uploadToPlayStore(options) { // Implementation for Google Play Console upload // This would require Google Play Developer API setup console.log(chalk.blue('Play Store upload functionality would be implemented here')); console.log(chalk.gray('Manual upload: https://play.google.com/console/developers')); } } module.exports = { AndroidBuilder };