UNPKG

esa-cli

Version:

A CLI for operating Alibaba Cloud ESA Functions and Pages.

500 lines (499 loc) 21.7 kB
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); }; import chalk from 'chalk'; import t from '../../i18n/index.js'; import { ApiService } from '../../libs/apiService.js'; import logger from '../../libs/logger.js'; import { ensureRoutineExists } from '../../utils/checkIsRoutineCreated.js'; import compress from '../../utils/compress.js'; import { getProjectConfig } from '../../utils/fileUtils/index.js'; import sleep from '../../utils/sleep.js'; import { checkIsLoginSuccess } from '../utils.js'; function normalizeNotFoundStrategy(value) { if (!value) return undefined; const lower = value.toLowerCase(); if (lower === 'singlepageapplication') { return 'SinglePageApplication'; } return value; } export function commitRoutineWithAssets(requestParams, zipBuffer) { return __awaiter(this, void 0, void 0, function* () { try { const server = yield ApiService.getInstance(); const apiResult = yield server.CreateRoutineWithAssetsCodeVersion(requestParams); if (!apiResult || !apiResult.data.OssPostConfig) { return { isSuccess: false, res: null }; } const ossConfig = apiResult.data.OssPostConfig; if (!ossConfig.OSSAccessKeyId || !ossConfig.Signature || !ossConfig.Url || !ossConfig.Key || !ossConfig.Policy) { console.error('Missing required OSS configuration fields'); return { isSuccess: false, res: null }; } let uploadSuccess = false; for (let i = 0; i < 3; i++) { uploadSuccess = yield server.uploadToOss({ OSSAccessKeyId: ossConfig.OSSAccessKeyId, Signature: ossConfig.Signature, Url: ossConfig.Url, Key: ossConfig.Key, Policy: ossConfig.Policy, XOssSecurityToken: ossConfig.XOssSecurityToken || '' }, zipBuffer); if (uploadSuccess) { break; } if (i < 2) { yield new Promise((resolve) => setTimeout(resolve, 2000)); } } return { isSuccess: uploadSuccess, res: apiResult }; } catch (error) { console.error('Error in createRoutineWithAssetsCodeVersion:', error); return { isSuccess: false, res: null }; } }); } /** * 通用的项目验证和初始化函数 * 包含目录检查、项目配置获取、登录检查、routine存在性检查 */ export function validateAndInitializeProject(name, projectPath) { return __awaiter(this, void 0, void 0, function* () { const projectConfig = getProjectConfig(projectPath); // allow missing config, derive name from cwd when not provided const projectName = name || (projectConfig === null || projectConfig === void 0 ? void 0 : projectConfig.name) || process.cwd().split(/[\\/]/).pop(); if (!projectName) { logger.notInProject(); return null; } logger.startSubStep('Checking login status'); const isSuccess = yield checkIsLoginSuccess(); if (!isSuccess) { logger.endSubStep('You are not logged in'); return null; } logger.endSubStep('Logged in'); yield ensureRoutineExists(projectName); return { projectConfig: projectConfig || null, projectName }; }); } /** * 通用的routine详情获取函数 */ export function getRoutineDetails(projectName) { return __awaiter(this, void 0, void 0, function* () { const server = yield ApiService.getInstance(); const req = { Name: projectName }; return yield server.getRoutine(req, false); }); } /** * 通用的代码压缩和提交函数 * 支持assets和普通代码两种模式 */ export function generateCodeVersion(projectName_1, description_1, entry_1, assets_1) { return __awaiter(this, arguments, void 0, function* (projectName, description, entry, assets, minify = false, projectPath, noBundle = false) { var _a; const { zip, sourceList, dynamicSources } = yield compress(entry, assets, minify, projectPath, noBundle); // Pretty print upload directory tree const buildTree = (paths, decorateTopLevel) => { const root = { children: new Map(), isFile: false }; const sorted = [...paths].sort((a, b) => a.localeCompare(b)); for (const p of sorted) { const parts = p.split('/').filter(Boolean); let node = root; for (let i = 0; i < parts.length; i++) { const part = parts[i]; if (!node.children.has(part)) { node.children.set(part, { children: new Map(), isFile: false }); } const child = node.children.get(part); if (i === parts.length - 1) child.isFile = true; node = child; } } const lines = []; const render = (node, prefix, depth) => { const entries = [...node.children.entries()]; entries.forEach(([_name, _child], idx) => { const isLast = idx === entries.length - 1; const connector = isLast ? '└ ' : '├ '; const nextPrefix = prefix + (isLast ? ' ' : '│ '); const displayName = depth === 0 ? decorateTopLevel(_name) : _name; lines.push(prefix + connector + displayName); render(_child, nextPrefix, depth + 1); }); }; render(root, '', 0); return lines.length ? lines : ['-']; }; const header = chalk.hex('#22c55e')('UPLOAD') + ' Files to be uploaded (source paths)'; logger.block(); logger.log(header); const dynamicSet = new Set(dynamicSources); const LIMIT = 300; const staticPaths = sourceList .filter((p) => !dynamicSet.has(p)) .sort((a, b) => a.localeCompare(b)); const dynamicPaths = sourceList .filter((p) => dynamicSet.has(p)) .sort((a, b) => a.localeCompare(b)); let omitted = 0; let shownStatic = staticPaths; if (staticPaths.length > LIMIT) { shownStatic = staticPaths.slice(0, LIMIT); omitted = staticPaths.length - LIMIT; } // Compute top-level markers based on whether a top-level bucket contains dynamic/static files const topLevelStats = new Map(); const addStat = (p, isDynamic) => { const top = p.split('/')[0] || p; const stat = topLevelStats.get(top) || { hasDynamic: false, hasStatic: false }; if (isDynamic) stat.hasDynamic = true; else stat.hasStatic = true; topLevelStats.set(top, stat); }; dynamicPaths.forEach((p) => addStat(p, true)); shownStatic.forEach((p) => addStat(p, false)); const dynamicMarker = chalk.bold.yellowBright(' (dynamic)'); const staticMarker = chalk.bold.greenBright(' (static)'); const decorateTopLevel = (name) => { const stat = topLevelStats.get(name); if (!stat) return name; if (stat.hasDynamic && stat.hasStatic) { return `${name}${dynamicMarker}${staticMarker}`; } if (stat.hasDynamic) return `${name}${dynamicMarker}`; if (stat.hasStatic) return `${name}${staticMarker}`; return name; }; const combined = [...dynamicPaths, ...shownStatic]; const treeLines = buildTree(combined, decorateTopLevel); for (const line of treeLines) { logger.log(line); } if (omitted > 0) { const note = chalk.gray(`Only show the first ${LIMIT} static files, omitted ${omitted} files`); logger.log(note); } logger.block(); const projectConfig = getProjectConfig(projectPath); const notFoundStrategy = normalizeNotFoundStrategy((_a = projectConfig === null || projectConfig === void 0 ? void 0 : projectConfig.assets) === null || _a === void 0 ? void 0 : _a.notFoundStrategy); logger.startSubStep('Generating code version'); const requestParams = { Name: projectName, CodeDescription: description, ExtraInfo: JSON.stringify({ Source: 'CLI' }) }; if (notFoundStrategy) { requestParams.ConfOptions = { NotFoundStrategy: notFoundStrategy }; } const res = yield commitRoutineWithAssets(requestParams, zip === null || zip === void 0 ? void 0 : zip.toBuffer()); if (res === null || res === void 0 ? void 0 : res.isSuccess) { return { isSuccess: true, res: res === null || res === void 0 ? void 0 : res.res }; } else { return { isSuccess: false, res: null }; } }); } /** * 根据 env 在一个或多个环境部署 */ export function deployToEnvironments(name, codeVersion, env) { return __awaiter(this, void 0, void 0, function* () { if (env === 'all') { const isStagingSuccess = yield deployCodeVersion(name, codeVersion, 'staging'); const isProdSuccess = yield deployCodeVersion(name, codeVersion, 'production'); return isStagingSuccess && isProdSuccess; } return yield deployCodeVersion(name, codeVersion, env); }); } /** * 通用的快速部署函数 * 结合了压缩、提交和部署的完整流程 */ export function commitAndDeployVersion(projectName_1, scriptEntry_1, assets_1) { return __awaiter(this, arguments, void 0, function* (projectName, scriptEntry, assets, description = '', projectPath, env = 'production', minify = false, version, noBundle = false) { var _a, _b, _c; const projectInfo = yield validateAndInitializeProject(projectName, projectPath); if (!projectInfo) { return false; } const { projectConfig } = projectInfo; // 2) Use existing version or generate a new one if (version) { logger.startSubStep(`Using existing version ${version}`); const deployed = yield deployToEnvironments(projectInfo.projectName, version, env); logger.endSubStep(deployed ? 'Deploy finished' : 'Deploy failed'); return deployed; } const res = yield generateCodeVersion(projectInfo.projectName, description, scriptEntry || (projectConfig === null || projectConfig === void 0 ? void 0 : projectConfig.entry), assets || ((_a = projectConfig === null || projectConfig === void 0 ? void 0 : projectConfig.assets) === null || _a === void 0 ? void 0 : _a.directory), minify || (projectConfig === null || projectConfig === void 0 ? void 0 : projectConfig.minify), projectPath, noBundle); const isCommitSuccess = res === null || res === void 0 ? void 0 : res.isSuccess; if (!isCommitSuccess) { logger.endSubStep('Generate version failed'); return false; } const codeVersion = (_c = (_b = res === null || res === void 0 ? void 0 : res.res) === null || _b === void 0 ? void 0 : _b.data) === null || _c === void 0 ? void 0 : _c.CodeVersion; if (!codeVersion) { logger.endSubStep('Missing CodeVersion in response'); return false; } logger.endSubStep(`Version generated: ${codeVersion}`); // 3) Deploy to specified environment(s) const deployed = yield deployToEnvironments(projectInfo.projectName, codeVersion, env); return deployed; }); } /** * 通用的版本部署函数 */ export function deployCodeVersion(name, codeVersion, environment) { return __awaiter(this, void 0, void 0, function* () { const server = yield ApiService.getInstance(); // Ensure the committed code version is ready before deploying const isReady = yield waitForCodeVersionReady(name, codeVersion, environment); if (!isReady) { logger.error('The code version is not ready for deployment.'); return false; } const res = yield server.createRoutineCodeDeployment({ Name: name, CodeVersions: [{ Percentage: 100, CodeVersion: codeVersion }], Strategy: 'percentage', Env: environment }); if (res) { return true; } else { return false; } }); } /** * Deploy specified multiple versions and their percentages */ export function deployCodeVersions(name, versions, env) { return __awaiter(this, void 0, void 0, function* () { const server = yield ApiService.getInstance(); const doDeploy = (targetEnv) => __awaiter(this, void 0, void 0, function* () { const res = yield server.createRoutineCodeDeployment({ Name: name, CodeVersions: versions.map((v) => ({ Percentage: v.percentage, CodeVersion: v.codeVersion })), Strategy: 'percentage', Env: targetEnv }); return !!res; }); if (env === 'all') { const s = yield doDeploy('staging'); const p = yield doDeploy('production'); return s && p; } return yield doDeploy(env); }); } /** * Poll routine code version status until it becomes ready */ export function waitForCodeVersionReady(name_1, codeVersion_1, env_1) { return __awaiter(this, arguments, void 0, function* (name, codeVersion, env, timeoutMs = 5 * 60 * 1000, intervalMs = 1000) { var _a, _b; if (!name || !codeVersion) { return false; } const server = yield ApiService.getInstance(); const start = Date.now(); logger.startSubStep(`Waiting for code version ${codeVersion} to be ready...`); while (Date.now() - start < timeoutMs) { try { const info = yield server.getRoutineCodeVersionInfo({ Name: name, CodeVersion: codeVersion }); const status = (_b = (_a = info === null || info === void 0 ? void 0 : info.data) === null || _a === void 0 ? void 0 : _a.Status) === null || _b === void 0 ? void 0 : _b.toLowerCase(); if (status === 'init') { yield sleep(intervalMs); continue; } else if (status === 'available') { logger.endSubStep(`Code version ${chalk.cyan(codeVersion)} is deployed to ${env}.`); return true; } else { logger.error(`Code version ${chalk.cyan(codeVersion)} build ${status}.`); return false; } } catch (e) { // swallow and retry until timeout } } logger.error(`⏰ Waiting for code version ${chalk.cyan(codeVersion)} timed out.`); return false; }); } /** * 通用的部署成功显示函数 * 显示部署成功信息、访问链接和后续操作指南 */ export function displayDeploySuccess(projectName_1) { return __awaiter(this, arguments, void 0, function* (projectName, showDomainGuide = true, showRouteGuide = true) { var _a; const service = yield ApiService.getInstance(); const res = yield service.getRoutine({ Name: projectName }); const defaultUrl = (_a = res === null || res === void 0 ? void 0 : res.data) === null || _a === void 0 ? void 0 : _a.DefaultRelatedRecord; const visitUrl = defaultUrl ? 'https://' + defaultUrl : ''; const accent = chalk.hex('#7C3AED'); const label = chalk.hex('#22c55e'); const subtle = chalk.gray; const title = `${chalk.bold('🚀 ')}${chalk.bold(t('init_deploy_success').d('Deploy Success'))}`; const lineUrl = `${label('URL')} ${visitUrl ? chalk.yellowBright(visitUrl) : subtle('-')}`; const lineProject = `${label('APP')} ${chalk.cyan(projectName || '-')}`; const lineCd = projectName ? `${label('TIP')} ${t('deploy_success_cd').d('Enter project directory')}: ${chalk.green(`cd ${projectName}`)}` : ''; const guides = []; if (showDomainGuide) { guides.push(`${label('TIP')} ${t('deploy_success_guide').d('Add a custom domain')}: ${chalk.green('esa-cli domain add <DOMAIN>')}`); } if (showRouteGuide) { guides.push(`${label('TIP')} ${t('deploy_success_guide_2').d('Add routes for a site')}: ${chalk.green('esa-cli route add -r <ROUTE> -s <SITE>')}`); } const tip = `${subtle(t('deploy_url_warn').d('The domain may take some time to take effect, please try again later.'))}`; const lines = [ accent(title), '', lineProject, lineUrl, lineCd ? '' : '', lineCd || '', guides.length ? '' : '', ...guides, guides.length ? '' : '', tip ]; const stripAnsi = (s) => s.replace(/\x1B\[[0-?]*[ -\/]*[@-~]/g, ''); const contentWidth = Math.max(...lines.map((l) => stripAnsi(l).length)); const borderColor = chalk.hex('#00D4FF').bold; const top = `${borderColor('╔')}${borderColor('═'.repeat(contentWidth + 2))}${borderColor('╗')}`; const bottom = `${borderColor('╚')}${borderColor('═'.repeat(contentWidth + 2))}${borderColor('╝')}`; const boxLines = [ top, ...lines.map((l) => { const pad = ' '.repeat(contentWidth - stripAnsi(l).length); const left = borderColor('║'); const right = borderColor('║'); return `${left} ${l}${pad} ${right}`; }), bottom ]; logger.block(); boxLines.forEach((l) => logger.log(l)); logger.block(); }); } /** * Parse --versions parameter and execute distributed deployment by percentage; print display and percentage allocation after successful deployment */ export function deployWithVersionPercentages(nameArg, versionsArg, env, projectPath) { return __awaiter(this, void 0, void 0, function* () { const raw = (versionsArg || []) .flatMap((v) => String(v).split(',')) .map((s) => s.trim()) .filter(Boolean); const pairs = raw.map((s) => { const [codeVersion, percentStr] = s.split(':'); return { codeVersion: codeVersion === null || codeVersion === void 0 ? void 0 : codeVersion.trim(), percentage: Number((percentStr || '').trim()) }; }); if (pairs.length > 2) { logger.error('Deploy failed: at most two versions are supported'); return false; } if (pairs.some((p) => !p.codeVersion || Number.isNaN(p.percentage) || p.percentage < 0)) { logger.error('Deploy failed: invalid --versions format. Use v1:80,v2:20'); return false; } if (pairs.length === 1) { if (pairs[0].percentage !== 100) { logger.error('Deploy failed: single version must be 100%'); return false; } } else if (pairs.length === 2) { const sum = pairs[0].percentage + pairs[1].percentage; if (sum !== 100) { logger.error('Deploy failed: percentages must sum to 100'); return false; } } const projectInfo = yield validateAndInitializeProject(nameArg, projectPath); if (!projectInfo) { return false; } const ok = yield deployCodeVersions(projectInfo.projectName, pairs, env); if (!ok) return false; yield displayDeploySuccess(projectInfo.projectName, true, true); logger.block(); logger.log('📦 Versions rollout:'); pairs.forEach((p) => { logger.log(`- ${p.codeVersion}: ${p.percentage}%`); }); logger.block(); return true; }); }