shopify-theme-deployer
Version:
A Node.js CLI tool that deploys Shopify theme code to multiple stores simultaneously using the Shopify CLI
186 lines (167 loc) • 5.29 kB
JavaScript
const { Command } = require('commander')
const chalk = require('chalk')
// Utilities
const Logger = require('./utils/Logger')
const Spinner = require('./utils/Spinner')
const CommandRunner = require('./utils/CommandRunner')
const ErrorParser = require('./utils/ErrorParser')
// Core modules
const StoreValidator = require('./core/StoreValidator')
const BackupManager = require('./core/BackupManager')
const RetryManager = require('./core/RetryManager')
const Deployer = require('./core/Deployer')
class ThemeDeployer {
constructor() {
this.storesFile = 'stores.json'
this.logFile = `deployment-${new Date()
.toISOString()
.replace(/[:.]/g, '-')
.slice(0, -5)}.log`
this.program = new Command()
// Initialize utilities
this.logger = new Logger(this.logFile)
this.spinner = new Spinner()
this.commandRunner = new CommandRunner()
this.errorParser = new ErrorParser()
// Initialize core modules
this.storeValidator = new StoreValidator(this.logger, this.spinner)
this.backupManager = new BackupManager(
this.logger,
this.spinner,
this.commandRunner,
this.errorParser
)
this.retryManager = new RetryManager(
this.logger,
this.spinner,
this.errorParser
)
this.deployer = new Deployer(
this.logger,
this.spinner,
this.commandRunner,
this.errorParser,
this.backupManager,
this.retryManager
)
this.setupCommander()
this.setupProcessHandlers()
}
setupCommander() {
this.program
.name('shopify-deploy')
.description(
'Deploy Shopify theme code to multiple stores simultaneously'
)
.version('1.0.0')
.option('-f, --file <path>', 'specify stores file', 'stores.json')
.option('-l, --list', 'list all stores in the configuration')
.option('-s, --store <url>', 'deploy to a specific store by URL')
.option('-v, --verbose', 'enable verbose logging')
.option('--init', 'create example stores.json file')
.option(
'--continue-on-error',
'continue deploying to other stores even if one fails'
)
.option('--dry-run', 'preview deployment without actually deploying')
.option(
'--parallel',
'deploy to stores in parallel instead of sequentially'
)
.option(
'--max-concurrent <number>',
'maximum concurrent deployments (default: 3)',
'3'
)
.option(
'--retry <count>',
'number of retry attempts for failed deployments (default: 3)',
'3'
)
.option(
'--retry-delay <ms>',
'base delay between retries in milliseconds (default: 1000)',
'1000'
)
.option('--backup', 'backup themes before deployment')
.option(
'--backup-dir <path>',
'directory for theme backups (default: ./backups)',
'./backups'
)
.parse()
}
// Setup process handlers to clean up spinners
setupProcessHandlers() {
const cleanup = () => {
if (this.spinner.isActive()) {
this.spinner.stop()
}
}
process.on('SIGINT', () => {
cleanup()
process.exit(0)
})
process.on('SIGTERM', () => {
cleanup()
process.exit(0)
})
}
// Main execution function
async run() {
try {
const options = this.program.opts()
// Set stores file if specified (must be done before init)
if (options.file) {
this.storesFile = options.file
}
// Handle init command
if (options.init) {
await this.storeValidator.initStoresFile(this.storesFile)
return
}
// Check dependencies and validate stores file
await this.deployer.checkDependencies(options)
const storesData = await this.storeValidator.validateStoresFile(
this.storesFile,
options
)
// Handle different modes
if (options.list) {
this.storeValidator.listStores(storesData)
} else if (options.store) {
await this.deployer.deployToSpecificStore(storesData, options.store, {
continueOnError: options.continueOnError,
verbose: options.verbose,
dryRun: options.dryRun,
retry: parseInt(options.retry),
retryDelay: parseInt(options.retryDelay),
backup: options.backup,
backupDir: options.backupDir,
})
} else {
await this.deployer.deployToAllStores(storesData, {
continueOnError: options.continueOnError,
verbose: options.verbose,
dryRun: options.dryRun,
parallel: options.parallel,
maxConcurrent: parseInt(options.maxConcurrent),
retry: parseInt(options.retry),
retryDelay: parseInt(options.retryDelay),
backup: options.backup,
backupDir: options.backupDir,
})
}
// Log completion
this.logger.info(`Log file: ${this.logFile}`)
} catch (error) {
this.logger.error(`Unexpected error: ${error.message}`)
if (options && options.verbose) {
console.error(error.stack)
}
await this.logger.log(`UNEXPECTED_ERROR: ${error.message}`)
process.exit(1)
}
}
}
module.exports = ThemeDeployer