esa-cli
Version:
A CLI for operating Alibaba Cloud ESA Functions and Pages.
500 lines (499 loc) • 21.7 kB
JavaScript
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;
});
}