esa-cli
Version:
A CLI for operating Alibaba Cloud ESA Functions and Pages.
752 lines (751 loc) • 30.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 { execSync } from 'child_process';
import path from 'path';
import { exit } from 'process';
import { confirm as clackConfirm, isCancel, log, outro } from '@clack/prompts';
import chalk from 'chalk';
import fs from 'fs-extra';
import Haikunator from 'haikunator';
import t from '../../i18n/index.js';
import logger from '../../libs/logger.js';
import Template from '../../libs/templates/index.js';
import { execCommand, execWithLoginShell } from '../../utils/command.js';
import { getDirName } from '../../utils/fileUtils/base.js';
import { generateConfigFile, getCliConfig, getProjectConfig, getTemplatesConfig, templateHubPath, updateProjectConfigFile } from '../../utils/fileUtils/index.js';
import promptParameter from '../../utils/prompt.js';
import { commitAndDeployVersion } from '../common/utils.js';
export const getTemplateInstances = (templateHubPath) => {
return fs
.readdirSync(templateHubPath)
.filter((item) => {
const itemPath = path.join(templateHubPath, item);
return (fs.statSync(itemPath).isDirectory() &&
!['.git', 'node_modules', 'lib'].includes(item));
})
.map((item) => {
var _a;
const projectPath = path.join(templateHubPath, item);
const projectConfig = getProjectConfig(projectPath);
const templateName = (_a = projectConfig === null || projectConfig === void 0 ? void 0 : projectConfig.name) !== null && _a !== void 0 ? _a : '';
return new Template(projectPath, templateName);
});
};
export const transferTemplatesToSelectItem = (configs, templateInstanceList, lang) => {
if (!configs)
return [];
return configs.map((config) => {
var _a, _b;
const title = config.Title_EN;
const value = (_b = (_a = templateInstanceList.find((template) => {
return title === template.title;
})) === null || _a === void 0 ? void 0 : _a.path) !== null && _b !== void 0 ? _b : '';
const children = transferTemplatesToSelectItem(config.children, templateInstanceList, lang);
return {
label: lang === 'en' ? config.Title_EN : config.Title_ZH,
value: value,
hint: lang === 'en' ? config.Desc_EN : config.Desc_ZH,
children
};
});
};
export const preInstallDependencies = (targetPath) => __awaiter(void 0, void 0, void 0, function* () {
const packageJsonPath = path.join(targetPath, 'package.json');
if (fs.existsSync(packageJsonPath)) {
logger.log(t('init_install_dependence').d('⌛️ Installing dependencies...'));
execSync('npm install', { stdio: 'inherit', cwd: targetPath });
logger.success(t('init_install_dependencies_success').d('Dependencies installed successfully.'));
// Read and parse package.json to check for build script
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
if (packageJson.scripts && packageJson.scripts.build) {
logger.log(t('init_build_project').d('⌛️ Building project...'));
execSync('npm run build', { stdio: 'inherit', cwd: targetPath });
logger.success(t('init_build_project_success').d('Project built successfully.'));
}
else {
logger.log(t('no_build_script').d('No build script found in package.json, skipping build step.'));
}
// After build, try to infer assets directory if not explicitly known
try {
const candidates = ['dist', 'build', 'out'];
for (const dir of candidates) {
const abs = path.join(targetPath, dir);
if (fs.existsSync(abs) && fs.statSync(abs).isDirectory()) {
// Update config file if present and assets not set
const projectConfig = getProjectConfig(targetPath);
if (projectConfig) {
const { updateProjectConfigFile } = yield import('../../utils/fileUtils/index.js');
if (!projectConfig.assets || !projectConfig.assets.directory) {
yield updateProjectConfigFile({ assets: { directory: dir } }, targetPath);
logger.success(`Detected build output "${dir}" and updated assets.directory`);
}
}
break;
}
}
}
catch (_a) { }
}
});
export function checkAndUpdatePackage(packageName) {
return __awaiter(this, void 0, void 0, function* () {
try {
const spinner = logger.ora;
spinner.text = t('checking_template_update').d('Checking esa-template updates...');
spinner.start();
// Get currently installed version
const __dirname = getDirName(import.meta.url);
const packageJsonPath = path.join(__dirname, '../../../');
let versionInfo;
try {
versionInfo = execSync(`npm list ${packageName}`, {
cwd: packageJsonPath
}).toString();
}
catch (e) {
spinner.text = t('template_updating').d('Updating templates to latest...');
execSync(`rm -rf node_modules/${packageName}`, {
cwd: packageJsonPath
});
execSync(`npm install ${packageName}@latest`, {
cwd: packageJsonPath,
stdio: 'inherit'
});
spinner.stop();
logger.log(`├ ${t('template_updated_to_latest').d('Templates updated to latest.')}`);
return;
}
const match = versionInfo.match(new RegExp(`(${packageName})@([0-9.]+)`));
const currentVersion = match ? match[2] : '';
// Get latest version
const latestVersion = execSync(`npm view ${packageName} version`, {
cwd: packageJsonPath
})
.toString()
.trim();
if (currentVersion !== latestVersion) {
spinner.stop();
logger.log(t('display_current_esa_template_version').d(`Current esa-template version:`) +
chalk.green(currentVersion) +
' ' +
t('display_latest_esa_template_version').d(`Latest esa-template version:`) +
chalk.green(latestVersion));
logger.stopSpinner();
const isUpdate = yield clackConfirm({
message: t('is_update_to_latest_version').d('Do you want to update templates to latest version?')
});
if (!isCancel(isUpdate) && isUpdate) {
spinner.start(t('template_updating').d('Updating templates to latest...'));
execSync(`rm -rf node_modules/${packageName}`, {
cwd: packageJsonPath
});
execSync(`rm -rf package-lock.json`, {
cwd: packageJsonPath
});
execSync(`npm install ${packageName}@latest`, {
cwd: packageJsonPath,
stdio: 'inherit'
});
spinner.stop();
logger.log(`├ ${t('updated_esa_template_to_latest_version', { packageName }).d(`${packageName} updated successfully`)}`);
}
}
else {
spinner.stop();
logger.log(` ${t('checking_esa_template_finished').d(`Checking esa-template finished.`)}`);
t('esa_template_is_latest_version', { packageName }).d(`${packageName} is latest.`);
logger.divider();
}
}
catch (error) {
console.log(error);
if (error instanceof Error) {
logger.ora.fail(t('check_and_update_package_error').d('Error: An error occurred while checking and updating the package, skipping template update'));
}
}
});
}
export const getFrameworkConfig = (framework) => {
// Read template.jsonc from init directory
const templatePath = path.join(getDirName(import.meta.url), 'template.jsonc');
const jsonc = fs.readFileSync(templatePath, 'utf-8');
const json = JSON.parse(jsonc);
return json[framework];
};
/**
* 获取框架全部配置
* @returns 框架全部配置
*/
export const getAllFrameworkConfig = () => {
// Read template.jsonc from init directory
const templatePath = path.join(getDirName(import.meta.url), 'template.jsonc');
const jsonc = fs.readFileSync(templatePath, 'utf-8');
const json = JSON.parse(jsonc);
return json;
};
export function getInitParamsFromArgv(argv) {
const a = argv;
const HaikunatorCtor = Haikunator;
const haikunator = new HaikunatorCtor();
const params = {
name: ''
};
if (a.yes) {
params.name = haikunator.haikunate();
params.git = true;
params.deploy = true;
params.template = 'Hello World';
params.framework = undefined;
params.language = undefined;
params.yes = true;
}
if (typeof a.name === 'string')
params.name = a.name;
if (typeof a.template === 'string' && a.template) {
params.template = a.template;
params.framework = undefined;
params.language = undefined;
params.category = 'template';
}
else {
const fw = a.framework;
const lang = a.language;
if (fw) {
params.framework = fw;
params.category = 'framework';
}
if (lang) {
params.language = lang;
}
}
if (typeof a.git === 'boolean')
params.git = Boolean(a.git);
if (typeof a.deploy === 'boolean')
params.deploy = Boolean(a.deploy);
return params;
}
// Configure project name
export const configProjectName = (initParams) => __awaiter(void 0, void 0, void 0, function* () {
if (initParams.name) {
log.step(`Project name configured ${initParams.name}`);
return;
}
const HaikunatorCtor = Haikunator;
const haikunator = new HaikunatorCtor();
const defaultName = haikunator.haikunate();
const name = (yield promptParameter({
type: 'text',
question: `${t('init_input_name').d('Enter the name of project:')}`,
label: 'Project name',
defaultValue: defaultName,
validate: (input) => {
if (input === '' || input === undefined) {
initParams.name = defaultName;
return true;
}
const regex = /^[a-z0-9-]{2,}$/;
if (!regex.test(input)) {
return t('init_name_error').d('Error: The project name must be at least 2 characters long and can only contain lowercase letters, numbers, and hyphens.');
}
return true;
}
}));
initParams.name = name;
});
export const configCategory = (initParams) => __awaiter(void 0, void 0, void 0, function* () {
if (initParams.category || initParams.framework || initParams.template) {
return;
}
const initMode = (yield promptParameter({
type: 'select',
question: 'How would you like to initialize the project?',
label: 'Init mode',
choices: [
{ name: 'Framework Starter', value: 'framework' },
{
name: 'Function Template',
value: 'template'
}
]
}));
initParams.category = initMode;
});
/*
选择模板
如果选择的是framework,则选择具体的模版 vue /react等
如果选择的是template,则选择具体的模版 esa template
*/
export const configTemplate = (initParams) => __awaiter(void 0, void 0, void 0, function* () {
if (initParams.template) {
log.step(`Template configured ${initParams.template}`);
return;
}
if (initParams.framework) {
log.step(`Framework configured ${initParams.framework}`);
return;
}
if (initParams.category === 'template') {
const templateItems = prepareTemplateItems();
const selectedTemplatePath = yield promptParameter({
type: 'multiLevelSelect',
question: 'Select a template:',
treeItems: templateItems
});
if (!selectedTemplatePath)
return null;
// TODO
initParams.template = selectedTemplatePath;
}
else {
const allFrameworkConfig = getAllFrameworkConfig();
const fw = (yield promptParameter({
type: 'select',
question: 'Select a framework',
label: 'Framework',
choices: Object.keys(allFrameworkConfig).map((fw) => {
var _a;
return ({
name: allFrameworkConfig[fw].label,
value: fw,
hint: (_a = allFrameworkConfig[fw]) === null || _a === void 0 ? void 0 : _a.hint
});
})
}));
initParams.framework = fw;
}
});
export const configLanguage = (initParams) => __awaiter(void 0, void 0, void 0, function* () {
if (initParams.language) {
log.info(`Language configured ${initParams.language}`);
return;
}
const framework = initParams.framework;
if (!framework) {
log.info('Framework config not configured, language skipped');
return;
}
const frameworkConfig = getFrameworkConfig(framework);
if (frameworkConfig.language) {
const language = (yield promptParameter({
type: 'select',
question: t('init_language_select').d('Select programming language:'),
label: 'Language',
choices: [
{
name: t('init_language_typescript').d('TypeScript (.ts) - Type-safe JavaScript, recommended'),
value: 'typescript'
},
{
name: t('init_language_javascript').d('JavaScript (.js) - Traditional JavaScript'),
value: 'javascript'
}
],
defaultValue: 'typescript'
}));
initParams.language = language;
}
});
export const createProject = (initParams) => __awaiter(void 0, void 0, void 0, function* () {
var _a;
if (initParams.template) {
// resolve template value: it may be a filesystem path or a template title
let selectedTemplatePath = initParams.template;
if (!path.isAbsolute(selectedTemplatePath) ||
!fs.existsSync(selectedTemplatePath)) {
const instances = getTemplateInstances(templateHubPath);
const matched = instances.find((it) => it.title === initParams.template);
if (matched) {
selectedTemplatePath = matched.path;
}
}
if (!fs.existsSync(selectedTemplatePath)) {
outro(`Project creation failed: cannot resolve template "${initParams.template}"`);
exit(1);
}
const res = yield initializeProject(selectedTemplatePath, initParams.name);
if (!res) {
outro(`Project creation failed`);
exit(1);
}
}
if (initParams.framework) {
const framework = initParams.framework;
const frameworkConfig = getFrameworkConfig(framework);
const command = frameworkConfig.command;
const templateFlag = ((_a = frameworkConfig.language) === null || _a === void 0 ? void 0 : _a[initParams.language || 'typescript']) || '';
const extraParams = frameworkConfig.params || '';
const full = `${command} ${initParams.name} ${templateFlag} ${extraParams}`.trim();
const res = yield execWithLoginShell(full, {
interactive: true,
startText: `Starting to execute framework command ${chalk.gray(full)}`,
doneText: `Framework command executed ${chalk.gray(full)}`
});
if (!res.success) {
outro(`Framework command execution failed`);
exit(1);
}
}
});
export const installDependencies = (initParams) => __awaiter(void 0, void 0, void 0, function* () {
if (initParams.template) {
return;
}
const targetPath = path.join(process.cwd(), initParams.name);
const res = yield execCommand(['npm', 'install'], {
cwd: targetPath,
useSpinner: true,
silent: true,
startText: 'Installing dependencies',
doneText: 'Dependencies installed'
});
if (!res.success) {
outro(`Dependencies installation failed`);
exit(1);
}
});
/**
* Apply configured file edits (方式1: overwrite) after project scaffold
*/
export const applyFileEdits = (initParams) => __awaiter(void 0, void 0, void 0, function* () {
var _a;
if (!initParams.framework) {
return true;
}
const frameworkConfig = getFrameworkConfig(initParams.framework || '');
const edits = frameworkConfig.fileEdits || [];
if (!edits.length)
return true;
logger.startSubStep(`Applying file edits`);
const __dirname = getDirName(import.meta.url);
try {
const toRegexFromGlob = (pattern) => {
// Very small glob subset: *, ?, {a,b,c}
let escaped = pattern
.replace(/[-/\\^$+?.()|[\]{}]/g, '\\$&') // escape regex specials first
.replace(/\\\*/g, '.*')
.replace(/\\\?/g, '.');
// restore and convert {a,b} to (a|b)
escaped = escaped.replace(/\\\{([^}]+)\\\}/g, (_, inner) => {
const parts = inner.split(',').map((s) => s.trim());
return `(${parts.join('|')})`;
});
return new RegExp('^' + escaped + '$');
};
const targetPath = path.join(process.cwd(), initParams.name);
const listRootFiles = () => {
try {
return fs.readdirSync(targetPath);
}
catch (_a) {
return [];
}
};
for (const edit of edits) {
if (((_a = edit.when) === null || _a === void 0 ? void 0 : _a.language) && initParams.language) {
if (edit.when.language !== initParams.language)
continue;
}
let matchedFiles = [];
if (edit.matchType === 'exact') {
const absExact = path.join(targetPath, edit.match);
matchedFiles = fs.existsSync(absExact) ? [edit.match] : [];
}
else if (edit.matchType === 'glob') {
const regex = toRegexFromGlob(edit.match);
matchedFiles = listRootFiles().filter((name) => regex.test(name));
}
else if (edit.matchType === 'regex') {
const regex = new RegExp(edit.match);
matchedFiles = listRootFiles().filter((name) => regex.test(name));
}
if (!matchedFiles.length)
continue;
// resolve content
let payload = null;
if (edit.fromFile) {
const absFrom = path.isAbsolute(edit.fromFile)
? edit.fromFile
: path.join(__dirname, edit.fromFile);
payload = fs.readFileSync(absFrom, 'utf-8');
}
else if (typeof edit.content === 'string') {
payload = edit.content;
}
for (const rel of matchedFiles) {
const abs = path.join(targetPath, rel);
if (payload == null)
continue;
if (!fs.existsSync(abs))
continue; // Only overwrite existing files
fs.ensureDirSync(path.dirname(abs));
fs.writeFileSync(abs, payload, 'utf-8');
}
}
logger.endSubStep('File edits applied');
return true;
}
catch (_b) {
outro(`File edits application failed`);
exit(1);
}
});
export const installESACli = (initParams) => __awaiter(void 0, void 0, void 0, function* () {
const targetPath = path.join(process.cwd(), initParams.name);
const res = yield execCommand(['npm', 'install', '-D', 'esa-cli'], {
cwd: targetPath,
useSpinner: true,
silent: true,
startText: 'Installing ESA CLI',
doneText: 'ESA CLI installed in the project'
});
if (!res.success) {
outro(`ESA CLI installation failed`);
exit(1);
}
});
export const updateConfigFile = (initParams) => __awaiter(void 0, void 0, void 0, function* () {
var _a, _b;
const targetPath = path.join(process.cwd(), initParams.name);
const configFormat = 'jsonc';
logger.startSubStep(`Updating config file`);
try {
if (initParams.framework) {
const frameworkConfig = getFrameworkConfig(initParams.framework);
const assetsDirectory = (_a = frameworkConfig.assets) === null || _a === void 0 ? void 0 : _a.directory;
const notFoundStrategy = (_b = frameworkConfig.assets) === null || _b === void 0 ? void 0 : _b.notFoundStrategy;
yield generateConfigFile(initParams.name, {
assets: assetsDirectory ? { directory: assetsDirectory } : undefined
}, targetPath, configFormat, notFoundStrategy);
}
else {
// TODO revise template config file later
// console.log(
// 'test:',
// initParams.name,
// undefined,
// targetPath,
// configFormat
// );
// logger.startSubStep(`Updating config file`);
// await generateConfigFile(initParams.name, undefined, targetPath, 'toml');
}
logger.endSubStep('Config file updated');
}
catch (_c) {
outro(`Config file update failed`);
exit(1);
}
});
export const initGit = (initParams) => __awaiter(void 0, void 0, void 0, function* () {
var _a;
const frameworkConfig = getFrameworkConfig(initParams.framework || '');
if ((frameworkConfig === null || frameworkConfig === void 0 ? void 0 : frameworkConfig.useGit) === false) {
log.step('Git skipped');
return true;
}
const gitInstalled = yield isGitInstalled();
if (!gitInstalled) {
log.step('You have not installed Git, Git skipped');
return true;
}
if (!initParams.git) {
const initGit = (yield promptParameter({
type: 'confirm',
question: t('init_git').d('Do you want to init git in your project?'),
label: 'Init git',
defaultValue: false
}));
initParams.git = initGit;
}
if (initParams.git) {
const targetPath = path.join(process.cwd(), initParams.name);
const res = yield execCommand(['git', 'init'], {
cwd: targetPath,
silent: true,
startText: 'Initializing git',
doneText: 'Git initialized'
});
if (!res.success) {
outro(`Git initialization failed`);
exit(1);
}
// Ensure .gitignore exists and has sensible defaults
yield ensureGitignore(targetPath, (_a = frameworkConfig === null || frameworkConfig === void 0 ? void 0 : frameworkConfig.assets) === null || _a === void 0 ? void 0 : _a.directory);
}
return true;
});
export function getGitVersion() {
return __awaiter(this, void 0, void 0, function* () {
try {
let stdout = yield execCommand(['git', '--version'], {
useSpinner: false,
silent: true,
captureOutput: true
});
const gitVersion = stdout.stdout.replace(/^git\s+version\s+/, '');
return gitVersion;
}
catch (_a) {
log.error('Failed to get Git version');
return null;
}
});
}
export function isGitInstalled() {
return __awaiter(this, void 0, void 0, function* () {
return (yield getGitVersion()) !== '' && (yield getGitVersion()) !== null;
});
}
/**
* Create or update .gitignore in project root with sensible defaults.
* - Preserves existing entries and comments
* - Avoids duplicates
* - Adds framework assets directory if provided
*/
function ensureGitignore(projectRoot, assetsDirectory) {
return __awaiter(this, void 0, void 0, function* () {
try {
const gitignorePath = path.join(projectRoot, '.gitignore');
const defaults = [
'# Logs',
'logs',
'*.log',
'npm-debug.log*',
'yarn-debug.log*',
'yarn-error.log*',
'pnpm-debug.log*',
'',
'# Node modules',
'node_modules/',
'',
'# Build output',
'dist/',
'build/',
'out/',
'.next/',
'.nuxt/',
'coverage/',
'.vite/',
'',
'# Env files',
'.env',
'.env.local',
'.env.development.local',
'.env.test.local',
'.env.production.local',
'',
'# IDE/editor',
'.DS_Store',
'.idea/',
'.vscode/',
'',
'# Misc caches',
'.eslintcache',
'.parcel-cache/',
'.turbo/',
'.cache/'
];
// Include assets directory if provided and not a common default
if (assetsDirectory &&
!['dist', 'build', 'out'].includes(assetsDirectory.replace(/\/$/, ''))) {
defaults.push('', '# Project assets output', `${assetsDirectory}/`);
}
let existingContent = '';
if (fs.existsSync(gitignorePath)) {
existingContent = fs.readFileSync(gitignorePath, 'utf-8');
}
const existingLines = new Set(existingContent.split(/\r?\n/).map((l) => l.trimEnd()));
const toAppend = [];
for (const line of defaults) {
if (!existingLines.has(line)) {
toAppend.push(line);
existingLines.add(line);
}
}
// If nothing to add, keep as is
if (!toAppend.length)
return;
const newContent = existingContent
? `${existingContent.replace(/\n$/, '')}\n${toAppend.join('\n')}\n`
: `${toAppend.join('\n')}\n`;
fs.writeFileSync(gitignorePath, newContent, 'utf-8');
}
catch (_a) {
// Do not fail init due to .gitignore issues
}
});
}
export const buildProject = (initParams) => __awaiter(void 0, void 0, void 0, function* () {
if (initParams.template) {
return;
}
const targetPath = path.join(process.cwd(), initParams.name);
const res = yield execCommand(['npm', 'run', 'build'], {
useSpinner: true,
silent: true,
startText: 'Building project',
doneText: 'Project built',
cwd: targetPath
});
if (!res.success) {
outro(`Build project failed`);
exit(1);
}
});
export function prepareTemplateItems() {
var _a;
const templateInstanceList = getTemplateInstances(templateHubPath);
const templateConfig = getTemplatesConfig();
const cliConfig = getCliConfig();
const lang = (_a = cliConfig === null || cliConfig === void 0 ? void 0 : cliConfig.lang) !== null && _a !== void 0 ? _a : 'en';
return transferTemplatesToSelectItem(templateConfig, templateInstanceList, lang);
}
export const deployProject = (initParams) => __awaiter(void 0, void 0, void 0, function* () {
if (!initParams.deploy) {
log.step('Deploy project skipped');
return;
}
const targetPath = path.join(process.cwd(), initParams.name);
const res = yield commitAndDeployVersion(initParams.name, undefined, undefined, 'Init project', targetPath, 'all');
if (!res) {
outro(`Deploy project failed`);
exit(1);
}
});
export function initializeProject(selectedTemplatePath, name) {
return __awaiter(this, void 0, void 0, function* () {
const selectTemplate = new Template(selectedTemplatePath, name);
const projectConfig = getProjectConfig(selectedTemplatePath);
if (!projectConfig) {
logger.notInProject();
return null;
}
const targetPath = path.join(process.cwd(), name);
if (fs.existsSync(targetPath)) {
logger.block();
logger.tree([
`${chalk.bgRed(' ERROR ')} ${chalk.bold.red(t('init_abort').d('Initialization aborted'))}`,
`${chalk.gray(t('reason').d('Reason:'))} ${chalk.red(t('dir_already_exists').d('Target directory already exists'))}`,
`${chalk.gray(t('path').d('Path:'))} ${chalk.cyan(targetPath)}`,
chalk.gray(t('try').d('Try one of the following:')),
`- ${chalk.white(t('try_diff_name').d('Choose a different project name'))}`,
`- ${chalk.white(t('try_remove').d('Remove the directory:'))} ${chalk.yellow(`rm -rf "${name}”`)}`,
`- ${chalk.white(t('try_another_dir').d('Run the command in another directory'))}`
]);
logger.block();
return null;
}
yield fs.copy(selectedTemplatePath, targetPath);
projectConfig.name = name;
yield updateProjectConfigFile(projectConfig, targetPath);
yield preInstallDependencies(targetPath);
return { template: selectTemplate, targetPath };
});
}