UNPKG

esa-cli

Version:

A CLI for operating Alibaba Cloud ESA Functions and Pages.

752 lines (751 loc) 30.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 { 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 }; }); }