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

956 lines (816 loc) • 27.6 kB
const fs = require('fs-extra'); const path = require('path'); const chalk = require('chalk'); const inquirer = require('inquirer'); const ora = require('ora'); const spawn = require('cross-spawn'); async function initCordova(options) { const projectRoot = findProjectRoot(); if (!projectRoot) { console.log(chalk.red('Not in a MusPE project directory')); return; } const spinner = ora('Initializing Cordova integration...').start(); try { // Check if Cordova CLI is installed await checkCordovaInstallation(); // Get project configuration const config = await getCordovaConfig(projectRoot, options); // Initialize Cordova project await initializeCordovaProject(projectRoot, config); // Configure Cordova hooks and plugins await setupCordovaHooks(projectRoot); await installDefaultPlugins(projectRoot, config); // Update MusPE configuration await updateMusPEConfig(projectRoot, config); // Generate Cordova-specific files await generateCordovaFiles(projectRoot, config); spinner.succeed('Cordova integration initialized successfully'); console.log(chalk.green('\n✨ Cordova integration completed!')); console.log(chalk.cyan('\nšŸ“± Next steps:')); console.log(` ${chalk.gray('$')} muspe cordova platform add ios`); console.log(` ${chalk.gray('$')} muspe cordova platform add android`); console.log(` ${chalk.gray('$')} muspe cordova build`); console.log(` ${chalk.gray('$')} muspe cordova run android`); console.log(chalk.gray('\nNote: Make sure you have the platform SDKs installed (Xcode for iOS, Android Studio for Android)')); } catch (error) { spinner.fail('Failed to initialize Cordova integration'); console.error(chalk.red(error.message)); } } async function cordovaCommand(command, args = [], options = {}) { const projectRoot = findProjectRoot(); if (!projectRoot) { console.log(chalk.red('Not in a MusPE project directory')); return; } const cordovaDir = path.join(projectRoot, 'cordova'); if (!await fs.pathExists(cordovaDir)) { console.log(chalk.red('Cordova not initialized. Run "muspe add cordova" first.')); return; } const spinner = ora(`Running cordova ${command}...`).start(); try { // Build web assets first if needed if (['build', 'run', 'emulate'].includes(command)) { await buildWebAssets(projectRoot); } // Run Cordova command await runCordovaCommand(cordovaDir, command, args, options); spinner.succeed(`Cordova ${command} completed`); } catch (error) { spinner.fail(`Cordova ${command} failed`); console.error(chalk.red(error.message)); } } function findProjectRoot() { let currentDir = process.cwd(); while (currentDir !== path.parse(currentDir).root) { const configPath = path.join(currentDir, 'muspe.config.js'); if (fs.existsSync(configPath)) { return currentDir; } currentDir = path.dirname(currentDir); } return null; } async function checkCordovaInstallation() { return new Promise((resolve, reject) => { const child = spawn('cordova', ['--version'], { stdio: 'pipe' }); child.on('close', (code) => { if (code === 0) { resolve(); } else { reject(new Error('Cordova CLI not found. Install it with: npm install -g cordova')); } }); child.on('error', () => { reject(new Error('Cordova CLI not found. Install it with: npm install -g cordova')); }); }); } async function getCordovaConfig(projectRoot, options) { const packageJson = await fs.readJSON(path.join(projectRoot, 'package.json')); if (options.interactive !== false) { const answers = await inquirer.prompt([ { type: 'input', name: 'appId', message: 'App ID (reverse domain format):', default: `com.muspe.${packageJson.name}`, validate: (input) => { if (!/^[a-zA-Z][a-zA-Z0-9_]*(\.[a-zA-Z][a-zA-Z0-9_]*)+$/.test(input)) { return 'Please enter a valid app ID (e.g., com.company.app)'; } return true; } }, { type: 'input', name: 'appName', message: 'App display name:', default: packageJson.name.charAt(0).toUpperCase() + packageJson.name.slice(1) }, { type: 'input', name: 'description', message: 'App description:', default: packageJson.description || `${packageJson.name} mobile app` }, { type: 'input', name: 'author', message: 'Author:', default: packageJson.author || 'MusPE Developer' }, { type: 'checkbox', name: 'platforms', message: 'Select platforms to add:', choices: [ { name: 'Android', value: 'android', checked: true }, { name: 'iOS', value: 'ios', checked: true }, { name: 'Browser', value: 'browser', checked: false } ] }, { type: 'checkbox', name: 'plugins', message: 'Select plugins to install:', choices: [ { name: 'Device Information', value: 'cordova-plugin-device', checked: true }, { name: 'Network Information', value: 'cordova-plugin-network-information', checked: true }, { name: 'Status Bar', value: 'cordova-plugin-statusbar', checked: true }, { name: 'Splash Screen', value: 'cordova-plugin-splashscreen', checked: true }, { name: 'Camera', value: 'cordova-plugin-camera', checked: false }, { name: 'File System', value: 'cordova-plugin-file', checked: false }, { name: 'Geolocation', value: 'cordova-plugin-geolocation', checked: false }, { name: 'InAppBrowser', value: 'cordova-plugin-inappbrowser', checked: false }, { name: 'Push Notifications', value: 'phonegap-plugin-push', checked: false } ] } ]); return { ...answers, projectName: packageJson.name }; } return { appId: options.appId || `com.muspe.${packageJson.name}`, appName: options.appName || packageJson.name, description: options.description || packageJson.description, author: options.author || packageJson.author, platforms: options.platforms || ['android', 'ios'], plugins: options.plugins || ['cordova-plugin-device', 'cordova-plugin-network-information'], projectName: packageJson.name }; } async function initializeCordovaProject(projectRoot, config) { const cordovaDir = path.join(projectRoot, 'cordova'); // Create Cordova directory if it doesn't exist await fs.ensureDir(cordovaDir); // Initialize Cordova project await runCommand('cordova', ['create', '.', config.appId, config.appName], { cwd: cordovaDir }); // Update config.xml with project details await updateConfigXml(cordovaDir, config); // Add platforms for (const platform of config.platforms) { try { await runCommand('cordova', ['platform', 'add', platform], { cwd: cordovaDir }); console.log(chalk.green(`āœ… Added ${platform} platform`)); } catch (error) { console.log(chalk.yellow(`āš ļø Failed to add ${platform} platform: ${error.message}`)); } } } async function updateConfigXml(cordovaDir, config) { const configPath = path.join(cordovaDir, 'config.xml'); let configXml = await fs.readFile(configPath, 'utf8'); // Update description configXml = configXml.replace( /<description>.*<\/description>/, `<description>${config.description}</description>` ); // Update author configXml = configXml.replace( /<author.*>.*<\/author>/, `<author email="dev@muspe.com" href="https://muspe.com">${config.author}</author>` ); // Add MusPE-specific preferences const preferences = ` <!-- MusPE Framework Preferences --> <preference name="ScrollEnabled" value="false" /> <preference name="BackupWebStorage" value="none" /> <preference name="SplashMaintainAspectRatio" value="true" /> <preference name="FadeSplashScreenDuration" value="300" /> <preference name="SplashShowOnlyFirstTime" value="false" /> <preference name="SplashScreen" value="screen" /> <preference name="SplashScreenDelay" value="3000" /> <preference name="AutoHideSplashScreen" value="false" /> <preference name="ShowSplashScreenSpinner" value="false" /> <!-- Security --> <preference name="AllowInlineMediaPlayback" value="false" /> <preference name="MediaPlaybackRequiresUserAction" value="false" /> <preference name="AllowUntrustedCerts" value="false" /> <preference name="DisallowOverscroll" value="false" /> <preference name="EnableViewportScale" value="false" /> <preference name="KeyboardDisplayRequiresUserAction" value="true" /> <preference name="SuppressesIncrementalRendering" value="false" /> <preference name="SuppressesLongPressGesture" value="false" /> <preference name="Suppresses3DTouchGesture" value="false" /> <!-- Android specific --> <platform name="android"> <preference name="AndroidLaunchMode" value="singleTop" /> <preference name="AndroidInsecureFileModeEnabled" value="false" /> <preference name="SplashMaintainAspectRatio" value="true" /> <preference name="loadUrlTimeoutValue" value="700000" /> </platform> <!-- iOS specific --> <platform name="ios"> <preference name="UIWebViewBounce" value="false" /> <preference name="DisallowOverscroll" value="true" /> <preference name="UIWebViewDecelerationSpeed" value="normal" /> <preference name="KeyboardDisplayRequiresUserAction" value="false" /> <preference name="HandleOpenURL" value="true" /> </platform>`; // Insert preferences before the closing widget tag configXml = configXml.replace('</widget>', preferences + '\n</widget>'); await fs.writeFile(configPath, configXml); } async function setupCordovaHooks(projectRoot) { const cordovaDir = path.join(projectRoot, 'cordova'); const hooksDir = path.join(cordovaDir, 'hooks'); const beforeBuildDir = path.join(hooksDir, 'before_build'); await fs.ensureDir(beforeBuildDir); // Create before_build hook to copy web assets const hookScript = `#!/usr/bin/env node const fs = require('fs-extra'); const path = require('path'); module.exports = function(context) { const projectRoot = path.resolve(context.opts.projectRoot, '..'); const webAssetsSource = path.join(projectRoot, 'dist'); const webAssetsTarget = path.join(context.opts.projectRoot, 'www'); console.log('Copying MusPE web assets...'); if (fs.existsSync(webAssetsSource)) { // Clear existing www directory fs.emptyDirSync(webAssetsTarget); // Copy built assets fs.copySync(webAssetsSource, webAssetsTarget); console.log('Web assets copied successfully'); } else { console.warn('No built assets found. Run "muspe build" first.'); } };`; await fs.writeFile(path.join(beforeBuildDir, '001_copy_web_assets.js'), hookScript); await fs.chmod(path.join(beforeBuildDir, '001_copy_web_assets.js'), '755'); } async function installDefaultPlugins(projectRoot, config) { const cordovaDir = path.join(projectRoot, 'cordova'); for (const plugin of config.plugins) { try { await runCommand('cordova', ['plugin', 'add', plugin], { cwd: cordovaDir }); console.log(chalk.green(`āœ… Installed plugin: ${plugin}`)); } catch (error) { console.log(chalk.yellow(`āš ļø Failed to install plugin ${plugin}: ${error.message}`)); } } } async function updateMusPEConfig(projectRoot, config) { const configPath = path.join(projectRoot, 'muspe.config.js'); let muspeConfig = {}; if (await fs.pathExists(configPath)) { delete require.cache[require.resolve(configPath)]; muspeConfig = require(configPath); } muspeConfig.cordova = { enabled: true, appId: config.appId, appName: config.appName, platforms: config.platforms, plugins: config.plugins, outputDir: './cordova/www', hooks: { beforeBuild: true, afterBuild: false } }; const configContent = `module.exports = ${JSON.stringify(muspeConfig, null, 2) .replace(/"/g, "'") .replace(/'([a-zA-Z_][a-zA-Z0-9_]*)':/g, '$1:')};`; await fs.writeFile(configPath, configContent); } async function generateCordovaFiles(projectRoot, config) { // Generate cordova-specific utilities const cordovaUtilsPath = path.join(projectRoot, 'src/utils/cordova.js'); const cordovaUtils = generateCordovaUtils(); await fs.writeFile(cordovaUtilsPath, cordovaUtils); // Generate native bridge service const cordovaServicePath = path.join(projectRoot, 'src/services/CordovaService.js'); const cordovaService = generateCordovaService(); await fs.writeFile(cordovaServicePath, cordovaService); // Update package.json with Cordova scripts const packageJsonPath = path.join(projectRoot, 'package.json'); const packageJson = await fs.readJSON(packageJsonPath); packageJson.scripts = { ...packageJson.scripts, 'cordova:init': 'muspe add cordova', 'cordova:build': 'muspe build && cd cordova && cordova build', 'cordova:run:android': 'muspe build && cd cordova && cordova run android', 'cordova:run:ios': 'muspe build && cd cordova && cordova run ios', 'cordova:emulate:android': 'muspe build && cd cordova && cordova emulate android', 'cordova:emulate:ios': 'muspe build && cd cordova && cordova emulate ios', 'cordova:platform:add': 'cd cordova && cordova platform add', 'cordova:plugin:add': 'cd cordova && cordova plugin add' }; await fs.writeJSON(packageJsonPath, packageJson, { spaces: 2 }); } function generateCordovaUtils() { return `// MusPE Cordova Utilities class MusPECordova { constructor() { this.isReady = false; this.readyCallbacks = []; this.plugins = {}; this.init(); } init() { if (typeof window !== 'undefined' && window.cordova) { document.addEventListener('deviceready', () => { this.isReady = true; this.detectPlugins(); this.runReadyCallbacks(); console.log('šŸ“± Cordova device ready'); }, false); // Handle pause/resume events document.addEventListener('pause', () => { MusPE.emit('app:pause'); }, false); document.addEventListener('resume', () => { MusPE.emit('app:resume'); }, false); // Handle back button document.addEventListener('backbutton', (e) => { e.preventDefault(); MusPE.emit('device:backbutton'); }, false); // Handle menu button document.addEventListener('menubutton', (e) => { MusPE.emit('device:menubutton'); }, false); } else { // Running in browser - simulate device ready setTimeout(() => { this.isReady = true; this.runReadyCallbacks(); console.log('🌐 Running in browser mode'); }, 100); } } ready(callback) { if (this.isReady) { callback(); } else { this.readyCallbacks.push(callback); } return this; } runReadyCallbacks() { this.readyCallbacks.forEach(callback => { try { callback(); } catch (error) { console.error('Cordova ready callback error:', error); } }); this.readyCallbacks = []; } detectPlugins() { // Detect available plugins const pluginChecks = { device: () => window.device, camera: () => navigator.camera, geolocation: () => navigator.geolocation, network: () => navigator.connection, statusBar: () => window.StatusBar, splashScreen: () => navigator.splashscreen, file: () => window.requestFileSystem, inAppBrowser: () => window.open !== window.parent.open }; Object.entries(pluginChecks).forEach(([name, check]) => { this.plugins[name] = { available: !!check(), instance: check() }; }); console.log('šŸ“¦ Available plugins:', Object.keys(this.plugins).filter(p => this.plugins[p].available)); } // Device information getDeviceInfo() { if (this.plugins.device?.available) { return { cordova: window.device.cordova, model: window.device.model, platform: window.device.platform, uuid: window.device.uuid, version: window.device.version, manufacturer: window.device.manufacturer, isVirtual: window.device.isVirtual, serial: window.device.serial }; } return null; } // Network information getNetworkInfo() { if (this.plugins.network?.available) { return { type: navigator.connection.type, isOnline: navigator.connection.type !== 'none', effectiveType: navigator.connection.effectiveType, downlink: navigator.connection.downlink, rtt: navigator.connection.rtt }; } return { isOnline: navigator.onLine }; } // Status bar control statusBar = { hide: () => { if (this.plugins.statusBar?.available) { window.StatusBar.hide(); } }, show: () => { if (this.plugins.statusBar?.available) { window.StatusBar.show(); } }, setStyle: (style) => { if (this.plugins.statusBar?.available) { if (style === 'dark') { window.StatusBar.styleDefault(); } else { window.StatusBar.styleLightContent(); } } }, setColor: (color) => { if (this.plugins.statusBar?.available && window.StatusBar.backgroundColorByHexString) { window.StatusBar.backgroundColorByHexString(color); } } }; // Splash screen control splashScreen = { hide: () => { if (this.plugins.splashScreen?.available) { navigator.splashscreen.hide(); } }, show: () => { if (this.plugins.splashScreen?.available) { navigator.splashscreen.show(); } } }; // Camera utilities camera = { getPicture: (options = {}) => { return new Promise((resolve, reject) => { if (!this.plugins.camera?.available) { reject(new Error('Camera plugin not available')); return; } const defaultOptions = { quality: 75, destinationType: Camera.DestinationType.FILE_URI, sourceType: Camera.PictureSourceType.CAMERA, encodingType: Camera.EncodingType.JPEG, targetWidth: 300, targetHeight: 300, mediaType: Camera.MediaType.PICTURE, allowEdit: true, correctOrientation: true, saveToPhotoAlbum: false }; navigator.camera.getPicture( resolve, reject, { ...defaultOptions, ...options } ); }); } }; // Geolocation utilities geolocation = { getCurrentPosition: (options = {}) => { return new Promise((resolve, reject) => { const defaultOptions = { enableHighAccuracy: true, timeout: 10000, maximumAge: 60000 }; navigator.geolocation.getCurrentPosition( resolve, reject, { ...defaultOptions, ...options } ); }); }, watchPosition: (callback, errorCallback, options = {}) => { const defaultOptions = { enableHighAccuracy: true, timeout: 10000, maximumAge: 60000 }; return navigator.geolocation.watchPosition( callback, errorCallback, { ...defaultOptions, ...options } ); }, clearWatch: (watchId) => { navigator.geolocation.clearWatch(watchId); } }; // InAppBrowser utilities inAppBrowser = { open: (url, target = '_blank', options = {}) => { if (!this.plugins.inAppBrowser?.available) { window.open(url, target); return null; } const defaultOptions = 'location=yes,hidden=no,clearcache=yes,clearsessioncache=yes'; const optionsString = typeof options === 'string' ? options : defaultOptions; return window.open(url, target, optionsString); } }; // Exit app exitApp() { if (navigator.app && navigator.app.exitApp) { navigator.app.exitApp(); } } // Check if running in Cordova isCordova() { return !!(window.cordova || window.PhoneGap || window.phonegap); } // Platform detection platform() { if (this.plugins.device?.available) { return window.device.platform.toLowerCase(); } return 'browser'; } isAndroid() { return this.platform() === 'android'; } isIOS() { return this.platform() === 'ios'; } } // Create global instance const cordova = new MusPECordova(); // Export for module usage if (typeof module !== 'undefined' && module.exports) { module.exports = { MusPECordova, cordova }; } // Make available globally if (typeof window !== 'undefined') { window.MusPECordova = MusPECordova; window.cordova = cordova; // Add to MusPE if available if (window.MusPE) { MusPE.cordova = cordova; } }`; } function generateCordovaService() { return `// MusPE Cordova Service - Native device integration class CordovaService { constructor() { this.isReady = false; this.deviceInfo = null; this.networkInfo = null; this.init(); } async init() { if (typeof window !== 'undefined' && (window.cordova || window.MusPECordova)) { await this.waitForDeviceReady(); this.setupEventListeners(); this.updateDeviceInfo(); this.updateNetworkInfo(); } } waitForDeviceReady() { return new Promise((resolve) => { if (window.MusPECordova) { window.MusPECordova.ready(() => { this.isReady = true; resolve(); }); } else { // Fallback for direct Cordova usage document.addEventListener('deviceready', () => { this.isReady = true; resolve(); }, false); } }); } setupEventListeners() { // Listen to MusPE events if available if (window.MusPE) { MusPE.on('app:pause', () => this.onPause()); MusPE.on('app:resume', () => this.onResume()); MusPE.on('device:backbutton', () => this.onBackButton()); MusPE.on('device:menubutton', () => this.onMenuButton()); } // Network events document.addEventListener('online', () => this.onOnline(), false); document.addEventListener('offline', () => this.onOffline(), false); } updateDeviceInfo() { if (window.MusPECordova) { this.deviceInfo = window.MusPECordova.getDeviceInfo(); } } updateNetworkInfo() { if (window.MusPECordova) { this.networkInfo = window.MusPECordova.getNetworkInfo(); } } // Event handlers onPause() { console.log('šŸ“± App paused'); // Save app state, pause animations, etc. if (window.MusPE) { MusPE.storage.local.set('app_last_pause', Date.now()); } } onResume() { console.log('šŸ“± App resumed'); // Restore app state, resume animations, etc. this.updateNetworkInfo(); if (window.MusPE) { const lastPause = MusPE.storage.local.get('app_last_pause'); if (lastPause) { const pauseDuration = Date.now() - lastPause; console.log(\`App was paused for \${pauseDuration}ms\`); } } } onBackButton() { console.log('šŸ“± Back button pressed'); // Handle back button - could show exit confirmation if (window.MusPE) { MusPE.emit('navigation:back'); } } onMenuButton() { console.log('šŸ“± Menu button pressed'); if (window.MusPE) { MusPE.emit('navigation:menu'); } } onOnline() { console.log('🌐 Device is online'); this.updateNetworkInfo(); if (window.MusPE) { MusPE.emit('network:online', this.networkInfo); } } onOffline() { console.log('šŸ“” Device is offline'); this.updateNetworkInfo(); if (window.MusPE) { MusPE.emit('network:offline'); } } // Public API methods getDeviceInfo() { return this.deviceInfo; } getNetworkInfo() { return this.networkInfo; } async takePicture(options = {}) { if (!window.MusPECordova) { throw new Error('Cordova not available'); } return window.MusPECordova.camera.getPicture(options); } async getCurrentLocation(options = {}) { if (!window.MusPECordova) { throw new Error('Cordova not available'); } return window.MusPECordova.geolocation.getCurrentPosition(options); } openInAppBrowser(url, options = {}) { if (!window.MusPECordova) { window.open(url, '_blank'); return null; } return window.MusPECordova.inAppBrowser.open(url, '_blank', options); } setStatusBarStyle(style, color) { if (window.MusPECordova) { window.MusPECordova.statusBar.setStyle(style); if (color) { window.MusPECordova.statusBar.setColor(color); } } } hideSplashScreen() { if (window.MusPECordova) { window.MusPECordova.splashScreen.hide(); } } exitApp() { if (window.MusPECordova) { window.MusPECordova.exitApp(); } } // Utility methods isCordova() { return !!(window.cordova || window.PhoneGap || window.phonegap); } isAndroid() { return window.MusPECordova ? window.MusPECordova.isAndroid() : false; } isIOS() { return window.MusPECordova ? window.MusPECordova.isIOS() : false; } getPlatform() { return window.MusPECordova ? window.MusPECordova.platform() : 'browser'; } } // Create singleton instance const cordovaService = new CordovaService(); // Export for module usage if (typeof module !== 'undefined' && module.exports) { module.exports = { CordovaService, cordovaService }; } // Auto-register service globally if (typeof window !== 'undefined') { window.CordovaService = CordovaService; window.cordovaService = cordovaService; // Register with MusPE if available if (window.MusPE) { MusPE.registerService('cordova', cordovaService); } }`; } async function buildWebAssets(projectRoot) { console.log(chalk.blue('Building web assets for Cordova...')); return new Promise((resolve, reject) => { const child = spawn('npm', ['run', 'build'], { cwd: projectRoot, stdio: 'inherit' }); child.on('close', (code) => { if (code === 0) { resolve(); } else { reject(new Error('Build failed')); } }); child.on('error', reject); }); } async function runCordovaCommand(cordovaDir, command, args, options) { return new Promise((resolve, reject) => { const child = spawn('cordova', [command, ...args], { cwd: cordovaDir, stdio: options.silent ? 'pipe' : 'inherit' }); child.on('close', (code) => { if (code === 0) { resolve(); } else { reject(new Error(`Cordova command failed with code ${code}`)); } }); child.on('error', reject); }); } async function 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); }); } module.exports = { initCordova, cordovaCommand };