diff --git a/src/android.js b/src/android.js index a0d104f..1cd541a 100644 --- a/src/android.js +++ b/src/android.js @@ -364,8 +364,10 @@ async function embed(args) { } async function invokeAndroidBuild(args) { + taskLogger.enableProgressBar(); taskLogger.start(androidBuildSteps[4].start); taskLogger.setTotal(androidBuildSteps[4].total); + taskLogger.incrementProgress(0.5); // Show initial progress (0% -> 25%) let keyStore, storePassword, keyAlias,keyPassword; if (args.buildType === 'debug' && !args.aKeyStore) { @@ -413,7 +415,7 @@ async function invokeAndroidBuild(args) { addProguardRule(); updateOptimizationFlags(); updateAndroidBuildGradleFile(args.buildType); - taskLogger.incrementProgress(1); + taskLogger.incrementProgress(0.5); // 25% -> 50% await generateSignedApk(keyStore, storePassword, keyAlias, keyPassword, args.packageType); taskLogger.succeed(androidBuildSteps[4].succeed); } else { @@ -422,12 +424,12 @@ async function invokeAndroidBuild(args) { label: loggerLabel, message: 'Updated build.gradle file with debug configuration' }); - taskLogger.incrementProgress(0.5) + taskLogger.incrementProgress(0.5) // 50% -> 75% try { await exec('./gradlew', ['assembleDebug'], { cwd: config.src + 'android' }); - taskLogger.incrementProgress(1.2) + taskLogger.incrementProgress(1.0) // 75% -> 100% taskLogger.succeed(androidBuildSteps[4].succeed); } catch(e) { console.error('error generating release apk. ', e); diff --git a/src/command.js b/src/command.js index 6ef30c4..fd7409f 100644 --- a/src/command.js +++ b/src/command.js @@ -237,6 +237,7 @@ async function extractRNZip(src) { async function setupBuildDirectory(src, dest, platform) { try{ + taskLogger.enableProgressBar(); taskLogger.setTotal(androidBuildSteps[0].total); taskLogger.start(androidBuildSteps[0].start); src = await extractRNZip(src); @@ -288,6 +289,7 @@ async function setupBuildDirectory(src, dest, platform) { }); global.logDirectory = logDirectory; logger.setLogDirectory(logDirectory); + taskLogger.incrementProgress(1); // Complete the final step taskLogger.info("Full log details can be found in: " + logDirectory); return { src: src, @@ -358,6 +360,7 @@ async function writeWmRNConfig(content) { // src points to unzip proj async function ejectProject(args) { try { + taskLogger.enableProgressBar(); taskLogger.start(androidBuildSteps[3].start); taskLogger.setTotal(androidBuildSteps[3].total); taskLogger.incrementProgress(1); @@ -403,6 +406,7 @@ async function ejectProject(args) { async function prepareProject(args) { try { + taskLogger.enableProgressBar(); taskLogger.setTotal(androidBuildSteps[1].total); taskLogger.start(androidBuildSteps[1].start); config.src = args.dest; @@ -441,6 +445,7 @@ async function prepareProject(args) { } taskLogger.incrementProgress(1); taskLogger.succeed(androidBuildSteps[1].succeed); + taskLogger.enableProgressBar(); taskLogger.setTotal(androidBuildSteps[2].total); taskLogger.start(androidBuildSteps[2].start); logger.info({ diff --git a/src/custom-logger/progress-bar.js b/src/custom-logger/progress-bar.js index 246a18d..5a14eb1 100644 --- a/src/custom-logger/progress-bar.js +++ b/src/custom-logger/progress-bar.js @@ -1,5 +1,4 @@ const chalk = require('chalk'); -const readline = require("readline"); class ProgressBar { constructor(options = {}) { @@ -16,14 +15,27 @@ class ProgressBar { this.completeColor = options.completeColor || null; this.incompleteColor = options.incompleteColor || null; this.textColor = options.textColor || null; + + // Performance optimizations + this.cachedOutput = ''; + this.lastValue = -1; + this.lastPercentage = -1; + this.etaCache = '?'; + this.etaCacheTime = 0; + this.etaCacheInterval = 2000; // Cache ETA for 2 seconds } start() { this.startTime = Date.now(); + this.invalidateCache(); } setProgress(value) { - this.value = Math.min(Math.max(0, value), this.total); + const newValue = Math.min(Math.max(0, value), this.total); + if (newValue !== this.value) { + this.value = newValue; + this.invalidateCache(); + } } incrementProgress(amount = 1) { @@ -31,64 +43,119 @@ class ProgressBar { } setTotal(total) { - this.total = total; + if (total !== this.total) { + this.total = total; + this.invalidateCache(); + } } enable() { this.showProgressBar = true; + this.invalidateCache(); } disable() { this.showProgressBar = false; + this.invalidateCache(); } status() { return this.showProgressBar; } + invalidateCache() { + this.cachedOutput = ''; + this.lastValue = -1; + this.lastPercentage = -1; + } + calculateETA() { - if (!this.startTime || this.value === 0) return '?'; - const elapsedTime = (Date.now() - this.startTime) / 1000; + if (!this.startTime || this.value === 0 || this.value === this.total) { + return '?'; + } + + const now = Date.now(); + + // Use cached ETA if it's still fresh + if (now - this.etaCacheTime < this.etaCacheInterval && this.etaCache !== '?') { + return this.etaCache; + } + + const elapsedTime = (now - this.startTime) / 1000; + if (elapsedTime < 2) { // Wait at least 2 seconds before calculating ETA + this.etaCache = '?'; + this.etaCacheTime = now; + return '?'; + } + const itemsPerSecond = this.value / elapsedTime; + if (itemsPerSecond <= 0) { + this.etaCache = '?'; + this.etaCacheTime = now; + return '?'; + } + const eta = Math.round((this.total - this.value) / itemsPerSecond); - return isFinite(eta) ? eta : '?'; + this.etaCache = isFinite(eta) && eta > 0 ? this.formatTime(eta) : '?'; + this.etaCacheTime = now; + + return this.etaCache; + } + + formatTime(seconds) { + if (seconds < 60) return `${seconds}s`; + const minutes = Math.floor(seconds / 60); + const remainingSeconds = seconds % 60; + return `${minutes}m ${remainingSeconds}s`; } render() { if (!this.showProgressBar) return ''; + const percentage = Math.floor((this.value / this.total) * 100); + + // Use cached output if nothing significant changed + if (this.value === this.lastValue && percentage === this.lastPercentage && this.cachedOutput) { + return this.cachedOutput; + } + const completeLength = Math.round((this.value / this.total) * this.barWidth); const incompleteLength = this.barWidth - completeLength; let completeBar = this.barCompleteChar.repeat(completeLength); let incompleteBar = this.barIncompleteChar.repeat(incompleteLength); + // Apply colors only if specified if (this.completeColor) completeBar = chalk[this.completeColor](completeBar); if (this.incompleteColor) incompleteBar = chalk[this.incompleteColor](incompleteBar); - let bar = completeBar + incompleteBar; + const bar = completeBar + incompleteBar; + + // Apply textColor only to non-bar parts to preserve bar colors let formattedText = this.barFormat .replace('{bar}', bar) - .replace('{percentage}', percentage) - .replace('{value}', this.value) - .replace('{total}', this.total) - .replace('{eta}', this.calculateETA()); + .replace('{percentage}', this.textColor ? chalk[this.textColor](percentage) : percentage) + .replace('{value}', this.textColor ? chalk[this.textColor](this.value) : this.value) + .replace('{total}', this.textColor ? chalk[this.textColor](this.total) : this.total) + .replace('{eta}', this.textColor ? chalk[this.textColor](this.calculateETA()) : this.calculateETA()); + + // Cache the result + this.cachedOutput = formattedText; + this.lastValue = this.value; + this.lastPercentage = percentage; - if (this.textColor){ - formattedText = chalk[this.textColor](formattedText); - } - readline.cursorTo(process.stdout, 0); - return formattedText; } } +// Create a more efficient overall progress bar (disabled by default) const overallProgressBar = new ProgressBar({ - showProgressBar: true, - barWidth: 40, + showProgressBar: false, // Disabled by default to avoid dual progress bars + barWidth: 30, // Slightly smaller for better performance completeColor: 'green', incompleteColor: 'gray', - textColor: 'cyan' + textColor: 'cyan', + barFormat: '[{bar}] {percentage}%' // Removed ETA for overall progress to improve performance }); module.exports = { diff --git a/src/custom-logger/task-logger.js b/src/custom-logger/task-logger.js index 3255aee..99d5182 100644 --- a/src/custom-logger/task-logger.js +++ b/src/custom-logger/task-logger.js @@ -16,13 +16,24 @@ class CustomSpinnerBar { this.spinner = options.spinner || [ "⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏", ]; - this.interval = options.interval || 80; this.stream = process.stderr; this.frameIndex = 0; this.isSpinning = false; this.spinnerInterval = null; - - this.progressBar = new ProgressBar(options); + this.lastOutput = ''; + this.lastProgressValue = -1; + this.renderCount = 0; + + // Only render every 10th frame to reduce I/O operations + this.renderThrottle = 10; + + // Set default colors if not provided + const progressBarOptions = { + completeColor: 'green', + incompleteColor: 'gray', + ...options + }; + this.progressBar = new ProgressBar(progressBarOptions); } start(text) { @@ -30,20 +41,35 @@ class CustomSpinnerBar { if (text) this.text = text; this.isSpinning = true; this.frameIndex = 0; + this.renderCount = 0; this.resetProgressBar(); this.progressBar.start(); - this.render(); - this.spinnerInterval = setInterval(() => this.render(), this.interval); + + // Render immediately on start + this.render(true); + + // Balanced interval for good responsiveness without performance impact + this.spinnerInterval = setInterval(() => { + this.frameIndex = (this.frameIndex + 1) % this.spinner.length; + this.renderCount++; + + // Render every 5th frame for balanced performance and responsiveness + if (this.renderCount % 5 === 0) { + this.render(); + } + }, 200); // Keep 200ms interval for good performance + return this; } stop() { if (global.verbose) return this; this.isSpinning = false; - clearInterval(this.spinnerInterval); - this.spinnerInterval = null; - readline.clearLine(this.stream, 0); - readline.cursorTo(this.stream, 0); + if (this.spinnerInterval) { + clearInterval(this.spinnerInterval); + this.spinnerInterval = null; + } + this.clearLine(); return this; } @@ -52,11 +78,9 @@ class CustomSpinnerBar { this.stop(); this.progressBar.setProgress(this.progressBar.total); - - let output = `${chalk.green("✔")} ${text || this.text}`; - output += " " + this.progressBar.render(); - - this.stream.write(`${output}\n`); + const output = `${chalk.green("✔")} ${text || this.text} ${this.progressBar.render()}`; + this.clearLine(); + this.writeLine(output); return this; } @@ -67,42 +91,69 @@ class CustomSpinnerBar { if(global.logDirectory){ finalText += chalk.gray(" Check logs at: ") + chalk.cyan(global.logDirectory); } - this.stream.write(`${chalk.red('✖')} ${chalk.bold.red(finalText)}\n`); + this.clearLine(); + this.writeLine(`${chalk.red('✖')} ${chalk.bold.red(finalText)}`); process.exit(1); - // return this; } info(text) { if (global.verbose) return this; this.stop(); - this.stream.write(`${chalk.blue("ℹ")} ${text || this.text}\n`); + this.clearLine(); + this.writeLine(`${chalk.blue("ℹ")} ${text || this.text}`); return this; } warn(text) { if (global.verbose) return this; this.stop(); - this.stream.write(`${chalk.yellow("⚠")} ${text || this.text}\n`); + this.clearLine(); + this.writeLine(`${chalk.yellow("⚠")} ${text || this.text}`); return this; } - render() { + render(force = false) { if (global.verbose) return; - readline.clearLine(this.stream, 0); - readline.cursorTo(this.stream, 0); - + const frame = this.spinner[this.frameIndex] || ''; const progressBar = this.progressBar?.render() || ''; const overallProgress = overallProgressBar?.render() || ''; + + // Show only one progress bar - prefer the local one if enabled, otherwise show overall + const displayProgress = this.progressBar.status() ? progressBar : (overallProgressBar.status() ? overallProgress : ''); + const output = `${chalk.cyan(frame)} ${this.text} ${displayProgress}`; + + // Only update if output changed or forced (performance optimization) + if (force || output !== this.lastOutput) { + // Additional check: skip if only spinner frame changed but progress is the same + const progressChanged = this.progressBar.value !== this.lastProgressValue; + if (force || progressChanged || !this.lastOutput) { + this.clearLine(); + this.stream.write(output); + this.lastOutput = output; + this.lastProgressValue = this.progressBar.value; + } + } + } - const output = `${chalk.cyan(frame)} ${this.text} ${progressBar} ${overallProgressBar.status() ?`| ${overallProgress}` : ''}`; - this.stream.write(output); + // Helper methods to reduce code duplication + clearLine() { + readline.clearLine(this.stream, 0); + readline.cursorTo(this.stream, 0); + } - this.frameIndex = (this.frameIndex + 1) % this.spinner.length; + writeLine(text) { + this.stream.write(`${text}\n`); } setText(text) { - this.text = text; + if (this.text !== text) { + this.text = text; + // Force render when text changes + if (this.isSpinning) { + this.render(true); + } + } return this; } @@ -113,14 +164,23 @@ class CustomSpinnerBar { } setProgress(value) { + const oldValue = this.progressBar.value; this.progressBar.setProgress(value); - overallProgressBar.setProgress(value); + + // Only update overall progress bar if local progress bar is not enabled + if (!this.progressBar.status()) { + overallProgressBar.setProgress(value); + } + + // Force render only on significant progress changes to avoid performance impact + if (Math.abs(value - oldValue) >= 0.5 && this.isSpinning) { + this.render(true); + } return this; } incrementProgress(amount = 1) { - this.progressBar.incrementProgress(amount); - overallProgressBar.incrementProgress(amount); + this.setProgress(this.progressBar.value + amount); return this; } @@ -144,4 +204,4 @@ class CustomSpinnerBar { module.exports = { spinnerBar: new CustomSpinnerBar(), createNewSpinnerBar: (options) => new CustomSpinnerBar({ ...options, newInstance: true }) -}; +}; \ No newline at end of file diff --git a/src/ios.js b/src/ios.js index d1c5b53..668f6ca 100644 --- a/src/ios.js +++ b/src/ios.js @@ -314,6 +314,7 @@ function findFile(path, nameregex) { async function xcodebuild(args, CODE_SIGN_IDENTITY_VAL, PROVISIONING_UUID, DEVELOPMENT_TEAM) { try { + taskLogger.enableProgressBar(); taskLogger.start(androidBuildSteps[4].start); taskLogger.setTotal(androidBuildSteps[4].total); let xcworkspacePath = findFile(config.src + 'ios', /\.xcworkspace?/) || findFile(config.src + 'ios', /\.xcodeproj?/); diff --git a/src/logger.js b/src/logger.js index 43d255b..19a5410 100644 --- a/src/logger.js +++ b/src/logger.js @@ -24,12 +24,29 @@ const consoleFormat = printf(({ return `${timestamp} [${label}] [${level}]: ${message}`; }); +const fileFormat = printf(({ + level, + message, + label, + timestamp +}) => { + // Skip decoration messages from exec commands in log files + if (label === 'exec' && message.includes('╔════════════════════════════════════╗')) { + return false; // Don't log this message to file + } + return `${timestamp} [${label}] [${level}]: ${message}`; +}); + const jsonFormat = printf(({ level, message, label, timestamp }) => { + // Skip decoration messages from exec commands in JSON log files + if (label === 'exec' && message.includes('╔════════════════════════════════════╗')) { + return false; // Don't log this message to file + } return JSON.stringify({ timestamp, label, @@ -76,7 +93,7 @@ logger.setLogDirectory = (path) => { }, format: combine( timestamp(), - consoleFormat + fileFormat ) }), new(transports.File)({ @@ -94,7 +111,7 @@ logger.setLogDirectory = (path) => { level: 'error', format: combine( timestamp(), - consoleFormat + fileFormat ), }) ]