miridev-cli
Version:
Official CLI tool for deploying static sites to miri.dev - Deploy your websites in seconds
236 lines (192 loc) • 7.21 kB
JavaScript
const chalk = require('chalk');
const ora = require('ora');
const path = require('path');
const { exec } = require('child_process');
const { promisify } = require('util');
const { scanFiles, loadConfig, loadIgnoreFile, formatFileSize } = require('../utils/files');
const api = require('../utils/api');
const { getCurrentUser } = require('../utils/auth');
const execAsync = promisify(exec);
/**
* 배포 명령어 실행
*/
async function deploy(options) {
console.log(chalk.blue.bold('\n🚀 Starting deployment to miri.dev\n'));
try {
const deployDir = path.resolve(options.dir);
console.log(chalk.gray(`📂 Deploy directory: ${deployDir}`));
// 기존 사이트 배포인지 확인
if (options.site) {
const siteId = extractSiteId(options.site);
console.log(chalk.cyan(`🔄 Updating existing site: ${options.site}`));
console.log(chalk.gray(`🆔 Site ID: ${siteId}`));
}
// 1. 설정 파일 로드
const config = await loadConfig(options.config);
console.log(chalk.gray(`⚙️ Config file: ${options.config}`));
// 2. 빌드 프로세스 (설정된 경우)
if (!options.skipBuild && config.build && config.build.command) {
await runBuildProcess(config.build);
}
// 실제 배포 디렉토리 결정
const actualDeployDir = config.build && config.build.directory
? path.resolve(deployDir, config.build.directory)
: deployDir;
// 3. 파일 스캔
const ignorePatterns = [
...(config.deploy?.ignore || []),
...(await loadIgnoreFile(actualDeployDir))
];
const scanOptions = {
ignorePatterns,
includeHidden: config.deploy?.includeHidden || false,
maxFileSize: parseFileSize(config.deploy?.maxFileSize || '25MB')
};
const { files, totalSize, totalFiles } = await scanFiles(actualDeployDir, scanOptions);
// 4. 파일 목록 표시
console.log(chalk.cyan('\n📋 Files to deploy:'));
files.slice(0, 10).forEach(file => {
console.log(` ${chalk.gray('→')} ${file.relativePath} ${chalk.gray(`(${formatFileSize(file.size)})`)}`);
});
if (files.length > 10) {
console.log(` ${chalk.gray(`... and ${files.length - 10} more files`)}`);
}
// 5. 사용자 확인
const user = getCurrentUser();
if (user) {
console.log(chalk.gray(`\n👤 Deploying as: ${user.email} (${user.plan || 'basic'})`));
} else {
console.log(chalk.gray('\n👤 Deploying as guest (1 hour hosting)'));
}
// 6. 배포 실행
const deployType = options.site ? 'Updating site...' : 'Uploading files...';
const spinner = ora(deployType).start();
try {
const result = await api.deployFiles(files, {
siteName: options.name || config.site?.name,
customDomain: config.site?.customDomain,
existingSite: options.site ? extractSiteId(options.site) : null,
debug: options.debug
});
spinner.succeed(options.site ? 'Site updated!' : 'Upload completed!');
// 7. 결과 표시
if (options.site) {
console.log(chalk.green.bold('\n✨ Site update successful!\n'));
console.log(`🔄 Updated site: ${chalk.cyan.underline(result.site.url)}`);
} else {
console.log(chalk.green.bold('\n✨ Deployment successful!\n'));
console.log(`🌐 Site URL: ${chalk.cyan.underline(result.site.url)}`);
}
if (result.site.customDomain) {
console.log(`🔗 Custom domain: ${chalk.cyan.underline(`https://${result.site.customDomain}`)}`);
}
console.log(`📊 Files deployed: ${totalFiles}`);
console.log(`📦 Total size: ${formatFileSize(totalSize)}`);
console.log(`🆔 Site ID: ${result.site.id}`);
if (result.site.expiresAt) {
const expiryDate = new Date(result.site.expiresAt);
console.log(`⏰ Expires: ${expiryDate.toLocaleString()}`);
}
// 8. 브라우저에서 열기 (옵션)
if (options.open) {
await openInBrowser(result.site.url);
}
console.log(chalk.gray('\n💡 Tip: Use "miridev status" to check deployment status'));
} catch (error) {
spinner.fail(options.site ? 'Site update failed' : 'Upload failed');
throw error;
}
} catch (error) {
console.error(chalk.red.bold('\n✗ Deployment failed:'));
console.error(chalk.red(error.message));
if (error.message.includes('index.html')) {
console.log(chalk.yellow('\n💡 Make sure you have an index.html file in your project root'));
}
if (error.message.includes('파일 크기')) {
console.log(chalk.yellow('\n💡 Try reducing file sizes or exclude large files using .miriignore'));
}
if (error.message.includes('Site not found') || error.message.includes('Invalid site')) {
console.log(chalk.yellow('\n💡 Check the site ID/URL. Use "miridev sites" to list your sites'));
}
process.exit(1);
}
}
/**
* 빌드 프로세스 실행
*/
async function runBuildProcess(buildConfig) {
console.log(chalk.gray(`🔨 Running build command: ${buildConfig.command}`));
const spinner = ora('Building project...').start();
try {
const { stdout, stderr } = await execAsync(buildConfig.command, {
cwd: process.cwd(),
encoding: 'utf8'
});
spinner.succeed('Build completed');
if (stdout) {
console.log(chalk.gray('Build output:'));
console.log(chalk.gray(stdout));
}
if (stderr) {
console.log(chalk.yellow('Build warnings:'));
console.log(chalk.yellow(stderr));
}
} catch (error) {
spinner.fail('Build failed');
console.error(chalk.red('Build error:'));
console.error(chalk.red(error.stdout || error.message));
throw new Error('Build process failed');
}
}
/**
* 파일 크기 문자열을 바이트로 변환
*/
function parseFileSize(sizeStr) {
const units = {
B: 1,
KB: 1024,
MB: 1024 * 1024,
GB: 1024 * 1024 * 1024
};
const match = sizeStr.match(/^(\d+(\.\d+)?)\s*(B|KB|MB|GB)$/i);
if (!match) {
throw new Error(`Invalid file size format: ${sizeStr}`);
}
const size = parseFloat(match[1]);
const unit = match[3].toUpperCase();
return size * units[unit];
}
/**
* 브라우저에서 URL 열기
*/
async function openInBrowser(url) {
const open = require('open');
try {
await open(url);
console.log(chalk.gray(`🌐 Opened ${url} in browser`));
} catch (error) {
console.log(chalk.yellow(`⚠️ Could not open browser: ${error.message}`));
console.log(chalk.gray(`Please visit: ${url}`));
}
}
/**
* 사이트 ID 추출 (도메인 또는 ID에서)
*/
function extractSiteId(siteInput) {
// e7a5d1de.miri.dev -> e7a5d1de
if (siteInput.includes('.miri.dev')) {
return siteInput.split('.')[0];
}
// 이미 사이트 ID인 경우 (8자리 hex)
if (/^[a-f0-9]{8}$/.test(siteInput)) {
return siteInput;
}
// 전체 URL인 경우 (https://e7a5d1de.miri.dev)
const urlMatch = siteInput.match(/\/\/([a-f0-9]{8})\.miri\.dev/);
if (urlMatch) {
return urlMatch[1];
}
// 기본적으로 입력값 그대로 반환 (에러는 서버에서 처리)
return siteInput;
}
module.exports = deploy;