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

562 lines (457 loc) • 18.8 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'); const plist = require('plist'); // Enhanced iOS Build System for MusPE class IOSBuilder { constructor(projectRoot, config) { this.projectRoot = projectRoot; this.config = config; this.platformsDir = path.join(projectRoot, 'platforms', 'ios'); 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 { ios: { bundleId: 'com.muspe.app', version: '1.0.0', buildNumber: 1, deploymentTarget: '12.0', configuration: 'Release' } }; } async build(options = {}) { // Check if running on macOS if (process.platform !== 'darwin') { throw new Error('iOS builds can only be performed on macOS'); } const spinner = ora('Building iOS application...').start(); try { // Check Xcode installation await this.checkXcodeInstallation(); // Ensure Cordova platform is added await this.ensurePlatform(); // Configure build settings await this.configureBuild(); // Copy web assets await this.copyWebAssets(); // Configure iOS-specific files await this.configureIOSFiles(); // Build the app const buildType = options.release ? 'release' : 'debug'; await this.buildApp(buildType, options); spinner.succeed('iOS build completed successfully'); // Display build information await this.displayBuildInfo(buildType); } catch (error) { spinner.fail('iOS build failed'); throw error; } } async checkXcodeInstallation() { try { await this.runCommand('xcodebuild', ['-version']); } catch (error) { throw new Error('Xcode is not installed or not properly configured. Please install Xcode from the App Store.'); } } async ensurePlatform() { if (!await fs.pathExists(this.platformsDir)) { console.log(chalk.blue('Adding iOS platform...')); await this.runCordovaCommand('platform', ['add', 'ios']); } } async configureBuild() { // Configure Info.plist await this.configureInfoPlist(); // Configure build settings await this.configureXcodeProject(); // Configure entitlements await this.configureEntitlements(); // Configure provisioning profiles (if provided) await this.configureCodeSigning(); } async configureInfoPlist() { const appName = this.getAppName(); const plistPath = path.join(this.platformsDir, appName, `${appName}-Info.plist`); if (await fs.pathExists(plistPath)) { const plistContent = plist.parse(await fs.readFile(plistPath, 'utf8')); // Update bundle identifier plistContent.CFBundleIdentifier = this.buildConfig.ios.bundleId; // Update version information plistContent.CFBundleShortVersionString = this.buildConfig.ios.version; plistContent.CFBundleVersion = this.buildConfig.ios.buildNumber.toString(); // Update app name plistContent.CFBundleDisplayName = this.config.name || 'MusPE App'; plistContent.CFBundleName = this.config.name || 'MusPE App'; // Add usage descriptions if (this.buildConfig.ios.plist) { Object.keys(this.buildConfig.ios.plist).forEach(key => { plistContent[key] = this.buildConfig.ios.plist[key]; }); } // Configure supported orientations plistContent.UISupportedInterfaceOrientations = [ 'UIInterfaceOrientationPortrait', 'UIInterfaceOrientationLandscapeLeft', 'UIInterfaceOrientationLandscapeRight' ]; plistContent['UISupportedInterfaceOrientations~ipad'] = [ 'UIInterfaceOrientationPortrait', 'UIInterfaceOrientationLandscapeLeft', 'UIInterfaceOrientationLandscapeRight', 'UIInterfaceOrientationPortraitUpsideDown' ]; // Configure status bar plistContent.UIStatusBarHidden = false; plistContent.UIViewControllerBasedStatusBarAppearance = false; // Save updated plist const updatedPlist = plist.build(plistContent); await fs.writeFile(plistPath, updatedPlist); } } async configureXcodeProject() { const appName = this.getAppName(); const projectPath = path.join(this.platformsDir, `${appName}.xcodeproj`, 'project.pbxproj'); if (await fs.pathExists(projectPath)) { let projectContent = await fs.readFile(projectPath, 'utf8'); // Update deployment target projectContent = projectContent.replace( /IPHONEOS_DEPLOYMENT_TARGET = [^;]+;/g, `IPHONEOS_DEPLOYMENT_TARGET = ${this.buildConfig.ios.deploymentTarget};` ); // Update bundle identifier projectContent = projectContent.replace( /PRODUCT_BUNDLE_IDENTIFIER = [^;]+;/g, `PRODUCT_BUNDLE_IDENTIFIER = ${this.buildConfig.ios.bundleId};` ); // Update marketing version projectContent = projectContent.replace( /MARKETING_VERSION = [^;]+;/g, `MARKETING_VERSION = ${this.buildConfig.ios.version};` ); // Update current project version projectContent = projectContent.replace( /CURRENT_PROJECT_VERSION = [^;]+;/g, `CURRENT_PROJECT_VERSION = ${this.buildConfig.ios.buildNumber};` ); // Configure code signing (if specified) if (this.buildConfig.ios.codeSign) { projectContent = projectContent.replace( /CODE_SIGN_IDENTITY = [^;]+;/g, `CODE_SIGN_IDENTITY = "${this.buildConfig.ios.codeSign.identity}";` ); if (this.buildConfig.ios.codeSign.profile) { projectContent = projectContent.replace( /PROVISIONING_PROFILE_SPECIFIER = [^;]+;/g, `PROVISIONING_PROFILE_SPECIFIER = "${this.buildConfig.ios.codeSign.profile}";` ); } if (this.buildConfig.ios.codeSign.team) { projectContent = projectContent.replace( /DEVELOPMENT_TEAM = [^;]+;/g, `DEVELOPMENT_TEAM = ${this.buildConfig.ios.codeSign.team};` ); } } await fs.writeFile(projectPath, projectContent); } } async configureEntitlements() { if (!this.buildConfig.ios.capabilities) return; const appName = this.getAppName(); const entitlementsPath = path.join(this.platformsDir, appName, `${appName}.entitlements`); const entitlements = { 'com.apple.developer.team-identifier': this.buildConfig.ios.codeSign?.team || 'TEAM_ID' }; // Add capabilities this.buildConfig.ios.capabilities.forEach(capability => { switch (capability) { case 'com.apple.developer.networking.wifi-info': entitlements['com.apple.developer.networking.wifi-info'] = true; break; case 'com.apple.developer.location.services': entitlements['com.apple.security.location'] = true; break; // Add more capabilities as needed } }); const entitlementsXml = plist.build(entitlements); await fs.writeFile(entitlementsPath, entitlementsXml); } async configureCodeSigning() { // This would involve configuring provisioning profiles // In a real implementation, you'd need to: // 1. Download provisioning profiles from Apple Developer // 2. Install certificates in keychain // 3. Configure Xcode project with correct signing settings console.log(chalk.blue('Code signing configuration...')); if (this.buildConfig.ios.codeSign?.team) { console.log(chalk.gray(`Development Team: ${this.buildConfig.ios.codeSign.team}`)); } if (this.buildConfig.ios.codeSign?.profile) { console.log(chalk.gray(`Provisioning Profile: ${this.buildConfig.ios.codeSign.profile}`)); } } async copyWebAssets() { const webAssetsPath = path.join(this.projectRoot, 'www'); const iosAssetsPath = path.join(this.platformsDir, 'www'); if (await fs.pathExists(webAssetsPath)) { await fs.copy(webAssetsPath, iosAssetsPath); } } async configureIOSFiles() { // Configure app icons await this.configureIcons(); // Configure launch screens await this.configureLaunchScreens(); // Configure splash screens await this.configureSplashScreens(); } async configureIcons() { const iconsDir = path.join(this.projectRoot, 'assets', 'icons'); const appName = this.getAppName(); const iosIconsDir = path.join(this.platformsDir, appName, 'Images.xcassets', 'AppIcon.appiconset'); if (await fs.pathExists(iconsDir) && await fs.pathExists(iosIconsDir)) { // iOS icon sizes and their corresponding files const iconSizes = [ { size: '20x20', scale: '2x', filename: 'icon-20@2x.png' }, { size: '20x20', scale: '3x', filename: 'icon-20@3x.png' }, { size: '29x29', scale: '2x', filename: 'icon-29@2x.png' }, { size: '29x29', scale: '3x', filename: 'icon-29@3x.png' }, { size: '40x40', scale: '2x', filename: 'icon-40@2x.png' }, { size: '40x40', scale: '3x', filename: 'icon-40@3x.png' }, { size: '60x60', scale: '2x', filename: 'icon-60@2x.png' }, { size: '60x60', scale: '3x', filename: 'icon-60@3x.png' }, { size: '76x76', scale: '1x', filename: 'icon-76.png' }, { size: '76x76', scale: '2x', filename: 'icon-76@2x.png' }, { size: '83.5x83.5', scale: '2x', filename: 'icon-83.5@2x.png' }, { size: '1024x1024', scale: '1x', filename: 'icon-1024.png' } ]; // Copy icon files for (const icon of iconSizes) { const iconSrc = path.join(iconsDir, icon.filename); const iconDest = path.join(iosIconsDir, icon.filename); if (await fs.pathExists(iconSrc)) { await fs.copy(iconSrc, iconDest); } } // Generate Contents.json for icon set const contentsJson = { images: iconSizes.map(icon => ({ size: icon.size, idiom: icon.size.includes('76') || icon.size.includes('83.5') ? 'ipad' : 'iphone', filename: icon.filename, scale: icon.scale })), info: { version: 1, author: 'muspe' } }; await fs.writeJSON(path.join(iosIconsDir, 'Contents.json'), contentsJson, { spaces: 2 }); } } async configureLaunchScreens() { const appName = this.getAppName(); const launchScreenPath = path.join(this.platformsDir, appName, 'CDVLaunchScreen.storyboard'); if (await fs.pathExists(launchScreenPath)) { // Configure launch screen storyboard let storyboard = await fs.readFile(launchScreenPath, 'utf8'); // Update app name in launch screen storyboard = storyboard.replace( /text="[^"]*"/g, `text="${this.config.name || 'MusPE App'}"` ); await fs.writeFile(launchScreenPath, storyboard); } } async configureSplashScreens() { const splashDir = path.join(this.projectRoot, 'assets', 'splash'); const appName = this.getAppName(); const iosSplashDir = path.join(this.platformsDir, appName, 'Images.xcassets', 'LaunchImage.launchimage'); if (await fs.pathExists(splashDir) && await fs.pathExists(iosSplashDir)) { // iOS splash screen configurations const splashConfigs = [ { filename: 'Default@2x~iphone.png', width: 640, height: 960 }, { filename: 'Default-568h@2x~iphone.png', width: 640, height: 1136 }, { filename: 'Default-667h.png', width: 750, height: 1334 }, { filename: 'Default-736h.png', width: 1242, height: 2208 }, { filename: 'Default-Landscape-736h.png', width: 2208, height: 1242 }, { filename: 'Default-Portrait~ipad.png', width: 768, height: 1024 }, { filename: 'Default-Landscape~ipad.png', width: 1024, height: 768 }, { filename: 'Default-Portrait@2x~ipad.png', width: 1536, height: 2048 }, { filename: 'Default-Landscape@2x~ipad.png', width: 2048, height: 1536 } ]; // Copy splash screen files for (const splash of splashConfigs) { const splashSrc = path.join(splashDir, splash.filename); const splashDest = path.join(iosSplashDir, splash.filename); if (await fs.pathExists(splashSrc)) { await fs.copy(splashSrc, splashDest); } } } } async buildApp(buildType, options) { const configuration = buildType === 'release' ? 'Release' : 'Debug'; const buildArgs = ['--configuration', configuration]; if (options.device) { buildArgs.push('--device'); } else if (options.simulator) { buildArgs.push('--emulator'); } if (options.verbose) { buildArgs.push('--verbose'); } await this.runCordovaCommand('build', ['ios', ...buildArgs]); // For release builds, also create an archive if (buildType === 'release' && options.archive) { await this.createArchive(); } } async createArchive() { const appName = this.getAppName(); const workspacePath = path.join(this.platformsDir, `${appName}.xcworkspace`); const archivePath = path.join(this.platformsDir, 'build', `${appName}.xcarchive`); console.log(chalk.blue('Creating Xcode archive...')); const archiveArgs = [ '-workspace', workspacePath, '-scheme', appName, '-configuration', 'Release', '-archivePath', archivePath, 'archive' ]; await this.runCommand('xcodebuild', archiveArgs); console.log(chalk.green(`Archive created: ${archivePath}`)); } async exportIPA(archivePath, exportPath, exportMethod = 'development') { // Create export options plist const exportOptions = { method: exportMethod, // development, ad-hoc, app-store, enterprise uploadBitcode: false, uploadSymbols: true, compileBitcode: false }; if (this.buildConfig.ios.codeSign?.team) { exportOptions.teamID = this.buildConfig.ios.codeSign.team; } const exportOptionsPath = path.join(this.platformsDir, 'ExportOptions.plist'); const exportOptionsPlist = plist.build(exportOptions); await fs.writeFile(exportOptionsPath, exportOptionsPlist); // Export IPA const exportArgs = [ '-exportArchive', '-archivePath', archivePath, '-exportPath', exportPath, '-exportOptionsPlist', exportOptionsPath ]; await this.runCommand('xcodebuild', exportArgs); } async displayBuildInfo(buildType) { console.log(chalk.green('\n✨ iOS build completed!')); console.log(chalk.cyan('\nšŸ“± Build artifacts:')); const appName = this.getAppName(); const buildPath = path.join(this.platformsDir, 'build'); if (await fs.pathExists(buildPath)) { console.log(` ${chalk.gray('Build Path:')} ${buildPath}`); const appPath = path.join(buildPath, buildType + '-iphoneos', `${appName}.app`); if (await fs.pathExists(appPath)) { console.log(` ${chalk.gray('App Bundle:')} ${appPath}`); } const archivePath = path.join(buildPath, `${appName}.xcarchive`); if (await fs.pathExists(archivePath)) { console.log(` ${chalk.gray('Archive:')} ${archivePath}`); } } console.log(chalk.cyan('\nšŸ“‹ Next steps:')); console.log(` ${chalk.gray('Simulator:')} muspe cordova emulate ios`); console.log(` ${chalk.gray('Device:')} muspe cordova run ios --device`); if (buildType === 'release') { console.log(` ${chalk.gray('Archive:')} Open Xcode → Window → Organizer`); console.log(` ${chalk.gray('Upload:')} Upload to App Store Connect`); } } getAppName() { return this.config.name?.replace(/[^a-zA-Z0-9]/g, '') || 'MusPEApp'; } 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); }); } async runCommand(command, args, options = {}) { return new Promise((resolve, reject) => { const child = spawn(command, args, { stdio: 'pipe', ...options }); child.on('close', (code) => { if (code === 0) { resolve(); } else { reject(new Error(`Command ${command} failed with code ${code}`)); } }); child.on('error', reject); }); } // App Store Connect integration async uploadToAppStore(ipaPath, options = {}) { // This would use Transporter CLI or altool for uploading to App Store Connect console.log(chalk.blue('App Store upload functionality would be implemented here')); console.log(chalk.gray('Manual upload: Open Xcode → Window → Organizer → Distribute App')); // Example using altool (deprecated but still functional): // xcrun altool --upload-app --type ios --file ${ipaPath} --username ${appleId} --password ${appSpecificPassword} // New method using Transporter: // xcrun iTMSTransporter -m upload -f ${ipaPath} -u ${appleId} -p ${appSpecificPassword} } async validateBuild(ipaPath) { // Validate the IPA before uploading const validateArgs = [ 'altool', '--validate-app', '--type', 'ios', '--file', ipaPath ]; if (process.env.APPLE_ID && process.env.APP_SPECIFIC_PASSWORD) { validateArgs.push('--username', process.env.APPLE_ID); validateArgs.push('--password', process.env.APP_SPECIFIC_PASSWORD); } try { await this.runCommand('xcrun', validateArgs); console.log(chalk.green('āœ… IPA validation successful')); } catch (error) { console.error(chalk.red('āŒ IPA validation failed')); throw error; } } } module.exports = { IOSBuilder };