src/services/init-manager.js
const chalk = require('chalk');
const clipboardy = require('clipboardy');
const fs = require('fs');
const { EOL } = require('os');
const Context = require('@forestadmin/context');
const { handleError } = require('../utils/error');
const { getInteractiveOptions } = require('../utils/option-parser');
const projectCreateOptions = require('./projects/create/options');
const SUCCESS_MESSAGE_ENV_VARIABLES_COPIED_IN_ENV_FILE =
'Copying the environment variables in your `.env` file';
const SUCCESS_MESSAGE_ENV_FILE_CREATED_AND_FILLED =
'Creating a new `.env` file containing your environment variables';
const SUCCESS_MESSAGE_DISPLAY_ENV_VARIABLES =
'Here are the environment variables you need to copy in your configuration file:\n';
const SUCCESS_MESSAGE_ENV_VARIABLES_COPIED_TO_CLIPBOARD = 'Automatically copied to your clipboard!';
const ERROR_MESSAGE_PROJECT_IN_V1 =
'This project does not support branches yet. Please migrate your environments from your Project settings first.';
const ERROR_MESSAGE_NOT_RIGHT_PERMISSION_LEVEL =
"You need the 'Admin' or 'Developer' permission level on this project to use branches.";
const ERROR_MESSAGE_PROJECT_BY_ENV_NOT_FOUND =
'Your project was not found. Please check your environment secret.';
const ERROR_MESSAGE_PROJECT_BY_OPTION_NOT_FOUND = 'The project you specified does not exist.';
const ERROR_MESSAGE_NO_PRODUCTION_OR_REMOTE_ENVIRONMENT =
'You cannot create your development environment until this project has either a remote or a production environment.';
const ERROR_MESSAGE_ENVIRONMENT_OWNER_UNICITY =
'You already have a development environment on this project.';
const VALIDATION_REGEX_URL = /^https?:\/\/.*/i;
const VALIDATION_REGEX_HTTPS = /^http((s:\/\/.*)|(s?:\/\/(localhost|127\.0\.0\.1).*))/i;
const SPLIT_URL_REGEX = new RegExp('(\\w+)://([\\w\\-\\.]+)(:(\\d+))?');
const ENV_VARIABLES_AUTO_FILLING_PREFIX =
'\n\n# ℹ️ The content below was automatically added by the `forest init` command ⤵️\n';
function handleInitError(rawError) {
const error = handleError(rawError);
switch (error) {
case 'Dev Workflow disabled.':
return ERROR_MESSAGE_PROJECT_IN_V1;
case 'Forbidden':
return ERROR_MESSAGE_NOT_RIGHT_PERMISSION_LEVEL;
case 'Project by env secret not found':
return ERROR_MESSAGE_PROJECT_BY_ENV_NOT_FOUND;
case 'Project not found':
return ERROR_MESSAGE_PROJECT_BY_OPTION_NOT_FOUND;
case 'No production/remote environment.':
return ERROR_MESSAGE_NO_PRODUCTION_OR_REMOTE_ENVIRONMENT;
case 'A user can have only one development environment per project.':
return ERROR_MESSAGE_ENVIRONMENT_OWNER_UNICITY;
case 'An environment with this name already exists. Please choose another name.':
return ERROR_MESSAGE_ENVIRONMENT_OWNER_UNICITY;
default:
return error;
}
}
async function handleDatabaseConfiguration() {
const { inquirer, env } = Context.inject();
const response = await inquirer.prompt([
{
type: 'confirm',
name: 'confirm',
message: "You don't have a DATABASE_URL yet. Do you need help setting it?",
},
]);
if (!response.confirm) return null;
return getInteractiveOptions(
{
databaseDialect: projectCreateOptions.databaseDialectV1,
databaseName: projectCreateOptions.databaseName,
databaseSchema: projectCreateOptions.databaseSchema,
databaseHost: projectCreateOptions.databaseHost,
databasePort: projectCreateOptions.databasePort,
databaseUser: projectCreateOptions.databaseUser,
databasePassword: projectCreateOptions.databasePassword,
databaseSSL: projectCreateOptions.databaseSSL,
mongoDBSRV: projectCreateOptions.mongoDBSRV,
},
env,
);
}
function validateEndpoint(input) {
if (!VALIDATION_REGEX_URL.test(input)) {
return 'Application input must be a valid url.';
}
if (!VALIDATION_REGEX_HTTPS.test(input)) {
return 'HTTPS protocol is mandatory, except for localhost and 127.0.0.1.';
}
return true;
}
function getApplicationPortFromCompleteEndpoint(endpoint) {
return endpoint.match(SPLIT_URL_REGEX)[4];
}
function getContentToAddInDotenvFile(environmentVariables) {
const { keyGenerator } = Context.inject();
const authSecret = keyGenerator.generate();
let contentToAddInDotenvFile = '';
if (environmentVariables.applicationPort) {
contentToAddInDotenvFile += `APPLICATION_PORT=${environmentVariables.applicationPort}\n`;
}
if (environmentVariables.databaseUrl) {
contentToAddInDotenvFile += `DATABASE_URL=${environmentVariables.databaseUrl}\n`;
}
if (environmentVariables.databaseSchema) {
contentToAddInDotenvFile += `DATABASE_SCHEMA=${environmentVariables.databaseSchema}\n`;
}
if (environmentVariables.databaseSSL !== undefined) {
contentToAddInDotenvFile += `DATABASE_SSL=${environmentVariables.databaseSSL}\n`;
}
contentToAddInDotenvFile += `FOREST_AUTH_SECRET=${authSecret}\n`;
contentToAddInDotenvFile += `FOREST_ENV_SECRET=${environmentVariables.forestEnvSecret}`;
contentToAddInDotenvFile += EOL;
return contentToAddInDotenvFile;
}
function commentExistingVariablesInAFile(fileData, environmentVariables) {
const variablesToComment = {
'FOREST_AUTH_SECRET=': '# FOREST_AUTH_SECRET=',
'FOREST_ENV_SECRET=': '# FOREST_ENV_SECRET=',
};
if (environmentVariables.applicationPort) {
variablesToComment['APPLICATION_PORT='] = '# APPLICATION_PORT=';
}
if (environmentVariables.databaseUrl) {
variablesToComment['DATABASE_URL='] = '# DATABASE_URL=';
variablesToComment['DATABASE_SCHEMA='] = '# DATABASE_SCHEMA=';
variablesToComment['DATABASE_SSL='] = '# DATABASE_SSL=';
}
const variablesToCommentRegex = new RegExp(
Object.keys(variablesToComment)
.map(key => `((?<!# )${key})`)
.join('|'),
'g',
);
return fileData.replace(variablesToCommentRegex, match => variablesToComment[match]);
}
function amendDotenvFile(environmentVariables) {
const { assertPresent, spinner } = Context.inject();
assertPresent({ spinner });
let newEnvFileData = getContentToAddInDotenvFile(environmentVariables);
spinner.start({ text: SUCCESS_MESSAGE_ENV_VARIABLES_COPIED_IN_ENV_FILE });
const existingEnvFileData = fs.readFileSync('.env', 'utf8');
if (existingEnvFileData) {
const amendedExistingFileData = commentExistingVariablesInAFile(
existingEnvFileData,
environmentVariables,
);
// NOTICE: We add the prefix only if the existing file was not empty.
newEnvFileData = amendedExistingFileData + ENV_VARIABLES_AUTO_FILLING_PREFIX + newEnvFileData;
}
fs.writeFileSync('.env', newEnvFileData);
spinner.success();
}
function createDotenvFile(environmentVariables) {
const { assertPresent, spinner } = Context.inject();
assertPresent({ spinner });
const contentToAdd = getContentToAddInDotenvFile(environmentVariables);
spinner.start({ text: SUCCESS_MESSAGE_ENV_FILE_CREATED_AND_FILLED });
fs.writeFileSync('.env', contentToAdd);
spinner.success();
}
async function displayEnvironmentVariablesAndCopyToClipboard(environmentVariables) {
const { logger } = Context.inject();
const variablesToDisplay = getContentToAddInDotenvFile(environmentVariables);
logger.info(SUCCESS_MESSAGE_DISPLAY_ENV_VARIABLES + chalk.black.bgCyan(variablesToDisplay));
await clipboardy
.write(variablesToDisplay)
.then(() => logger.info(chalk.italic(SUCCESS_MESSAGE_ENV_VARIABLES_COPIED_TO_CLIPBOARD)))
.catch(() => null);
}
module.exports = {
handleInitError,
handleDatabaseConfiguration,
validateEndpoint,
getApplicationPortFromCompleteEndpoint,
amendDotenvFile,
createDotenvFile,
displayEnvironmentVariablesAndCopyToClipboard,
};