@puls-atlas/cli
Version:
The Puls Atlas CLI tool for managing Atlas projects
200 lines • 6.85 kB
JavaScript
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();
}
});
};