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
JavaScript
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 };