UNPKG

@puls-atlas/cli

Version:

The Puls Atlas CLI tool for managing Atlas projects

200 lines 6.85 kB
import fs from 'fs'; import path from 'path'; import inquirer from 'inquirer'; import { isEqual } from 'underscore'; import { execSync, logger, selectProject, listModuleExports } from './../../utils/index.js'; const snakeCase = str => str.replace(/[A-Z]/g, letter => `_${letter.toLowerCase()}`); const getPackageJson = dir => { const packageJsonPath = path.join(dir, 'package.json'); if (!fs.existsSync(packageJsonPath)) { logger.error('No package.json file found. ' + 'Make sure you run this command in the root of the Atlas project.'); } return JSON.parse(fs.readFileSync(packageJsonPath)); }; const deployRuntimeConfig = async (project, config) => { const variables = Object.keys(config).map(key => `runtime.${snakeCase(key)}="${config[key]}"`); execSync(`firebase functions:config:set ${variables.join(' ')}`, { project }); }; const selectInteractive = async functionsDir => { const functions = listModuleExports(path.join(functionsDir, 'index.js'), { recursive: true }); const { selectedFunctions } = await inquirer.prompt([{ loop: false, type: 'checkbox', name: 'selectedFunctions', message: 'Select the functions you want to deploy:', choices: functions.map(name => ({ name: ` ${name}`, short: name, value: name, checked: false })) }]); return selectedFunctions; }; const fromEditor = async () => { const { selectedFunctions } = await inquirer.prompt([{ loop: false, type: 'editor', name: 'selectedFunctions', message: 'Paste or type the functions you want to deploy (one per line):', default: '' }]); return selectedFunctions.split('\n').map(name => name.trim()); }; const selectCodebase = async () => { const firebaseJsonPath = path.join(process.cwd(), 'firebase.json'); if (!fs.existsSync(firebaseJsonPath)) { logger.warn('No firebase.json file found.'); return null; } const firebaseJson = JSON.parse(fs.readFileSync(firebaseJsonPath)); if (firebaseJson.functions?.length > 1) { const { codebase } = await inquirer.prompt([{ type: 'list', name: 'codebase', message: 'Select a codebase to deploy functions from:', choices: firebaseJson.functions.map(({ codebase, source }) => ({ name: `${codebase} (${source})`, value: codebase, short: codebase })) }]); return codebase; } return null; }; export default async (functions, options = {}) => { const functionsDir = path.join(process.cwd(), 'functions'); if (!fs.existsSync(functionsDir)) { logger.error('No functions folder found. ' + 'Make sure you run this command in the root of the Atlas project.'); } if (functions.length === 0) { if (options['interactive']) { functions = await selectInteractive(functionsDir); } else if (options['editor']) { functions = await fromEditor(); } else { const { inputType } = await inquirer.prompt([{ type: 'list', name: 'inputType', default: 'interactive', message: 'How do you want to select the functions to deploy?', choices: [{ name: 'Interactive (recommended)', value: 'interactive', short: 'interactive' }, { name: 'Editor', value: 'editor', short: 'editor' }] }]); if (inputType === 'interactive') { functions = await selectInteractive(functionsDir); } if (inputType === 'editor') { functions = await fromEditor(); } } } if (functions.length === 0) { logger.error('No functions specified. Please specify at least one function to deploy.'); } const codebase = options['codebase'] ?? (await selectCodebase()); const packageJson = getPackageJson(path.join(functionsDir, codebase ?? '')); if (!packageJson.engines || !packageJson.engines.node) { logger.error('No node engine target defined. ' + 'Add it to your package.json to continue, eg: "engines": { "node": "20" } '); } selectProject('.firebaserc', { promptMessage: 'Select a Google Cloud Project to deploy to:' }).then(async ({ projectId, config }) => { const spinner = logger.spinner('Loading current configuration...'); const stdout = execSync('firebase functions:config:get', { cwd: functionsDir, project: projectId, stdio: 'pipe' }).toString(); if (config.config && config.config[projectId]) { const currentConfig = JSON.parse(stdout); const runtime = currentConfig.runtime || {}; if (!isEqual(runtime, config.config[projectId])) { spinner.warn('Runtime config is out-of-date'); logger.info(`Current: ${stdout.trim()}`); logger.info(`New: ${JSON.stringify(config.config[projectId])}`); logger.break(); const confirm = await inquirer.prompt({ type: 'confirm', name: 'value', default: false, message: 'Found a new or updated environment configuration for this project. ' + 'Deploy this configuration to Firestore as well?' }); if (confirm.value) { deployRuntimeConfig(projectId, config.config[projectId]); } } else { spinner.succeed('Runtime config is up-to-date'); } } else { logger.warning('DID NOT FIND CONFIGURATION VARIABLES FOR THIS PROJECT IN .firebaserc'); logger.warning('THIS FUNCTION WILL ASSUME IT IS RUNNING IN DEVELOPMENT IF NO CONFIGURATION HAS BEEN DEPLOYED IN THE PAST'); } return projectId; }).then(async projectId => { logger.break(); logger.log('Overview:'); logger.log('---------'); logger.log(chalk => `Project: ${chalk.yellow(projectId)}`); codebase && logger.log(chalk => `Codebase: ${chalk.yellow(codebase)}`); logger.log(chalk => `Node: ${chalk.yellow(packageJson.engines.node)}`); logger.log('Functions:'); functions.forEach(name => logger.log(chalk => chalk.yellow(` - ${name}`))); logger.break(); const confirm = await inquirer.prompt([{ type: 'confirm', name: 'value', default: false, message: 'Continue deploying to Firebase Functions?' }]); return { isConfirmed: confirm.value, projectId }; }).then(async ({ isConfirmed, projectId }) => { if (isConfirmed) { const prefix = codebase ? `${codebase}:` : ''; try { execSync('firebase deploy', { project: projectId, cwd: functionsDir, only: functions.map(name => `functions:${prefix}${name.replace('.', '-')}`).join(',') }); } catch (error) { logger.error('Deployment failed. See the error above for details.'); } } else { logger.warning('Deployment canceled by the user.'); process.exit(); } }); };