From f114df77391ffa125f4d3523b912f6fdeb199c4c Mon Sep 17 00:00:00 2001 From: Jenni Nurmi Date: Mon, 14 Jan 2019 11:22:57 +0200 Subject: [PATCH] Edit environment variables after .env file is created Merge to edit --- .env-template | 2 +- scripts/config.js | 526 ++++++++++++++++++++++++++++++---------------- 2 files changed, 346 insertions(+), 182 deletions(-) diff --git a/.env-template b/.env-template index 48e5ab7d..1cad4b45 100644 --- a/.env-template +++ b/.env-template @@ -3,7 +3,7 @@ # # Note: You also need to set Stripe secret key in Flex Console. # -REACT_APP_SHARETRIBE_SDK_CLIENT_ID= +REACT_APP_SHARETRIBE_SDK_CLIENT_ID=change-me REACT_APP_STRIPE_PUBLISHABLE_KEY= REACT_APP_MAPBOX_ACCESS_TOKEN= diff --git a/scripts/config.js b/scripts/config.js index d579a8aa..618b5f6b 100644 --- a/scripts/config.js +++ b/scripts/config.js @@ -3,207 +3,78 @@ const fs = require('fs'); const readline = require('readline'); const chalk = require('chalk'); -const mandatoryVariables = [ - { - type: 'input', - name: 'REACT_APP_SHARETRIBE_SDK_CLIENT_ID', - message: `What is your Flex client ID? -${chalk.dim( - 'Client ID is needed for connecting with Flex API. You can find your client ID from Flex Console.' - )} -`, - validate: function(value) { - if (value.match(/^[a-z0-9]{8}-[a-z0-9]{4}-[a-z0-9]{4}-[a-z0-9]{4}-[a-z0-9]{12}$/i)) { - return true; - } - return 'Please enter valid Flex Client ID. You can check it form Flex Console!'; - }, - }, - { - type: 'input', - name: 'REACT_APP_STRIPE_PUBLISHABLE_KEY', - message: `What is your Stripe publishable key? -${chalk.dim( - `Stripe publishable API key is for generating tokens with Stripe API. Use test key (prefix pk_test_) for development. The secret key needs to be added to Flex Console. -If you don't set the Stripe key, payment's won't work in the application.` - )} -`, - validate: function(value) { - if (value.match(/^pk_/)) { - return true; - } - return 'Please enter Stripe publishable key with prefix pk_!'; - }, - }, - { - type: 'input', - name: 'REACT_APP_MAPBOX_ACCESS_TOKEN', - message: `What is your Mapbox access token? -${chalk.dim( - `Mapbox is the default map provider of the application. Sign up for Mapbox and go the to account page. Then click Create access token. For more information see the: Integrating to map providers documentation. -If you don't set the Mapbox key, the map components won't work in the application.` - )} -`, - }, -]; - -const defaultVariables = [ - { - type: 'input', - name: 'REACT_APP_SHARETRIBE_MARKETPLACE_CURRENCY', - message: `What is your marketplace currency? -${chalk.dim( - 'The currency used in the Marketplace must be in ISO 4217 currency code. For example USD, EUR, CAD, AUD, etc. The default value is USD.' - )} -`, - default: function() { - return 'USD'; - }, - validate: function(value) { - if (value.match(/^[a-zA-Z]{3}$/)) { - return true; - } - return 'Please enter currency in ISO 4217 format (e.g. USD, EUR, CAD...)'; - }, - }, - { - type: 'input', - name: 'REACT_APP_CANONICAL_ROOT_URL', - message: `What is your canonical root URL? -${chalk.dim( - 'Canonical root URL of the marketplace is needed for social media sharing and SEO optimization. When developing the template application locally URL is usually http://localhost:3000' - )} -`, - default: function() { - return 'http://localhost:3000'; - }, - }, - { - type: 'confirm', - name: 'REACT_APP_AVAILABILITY_ENABLED', - message: `Do you want to enable availability calendar? -${chalk.dim( - 'This setting enables the Availability Calendar for listings. The default value for this setting is true.' - )} -`, - default: true, - }, - { - type: 'confirm', - name: 'REACT_APP_DEFAULT_SEARCHES_ENABLED', - message: `Do you want to enable default search suggestions? -${chalk.dim( - 'This setting enables the Default Search Suggestions in location autocomplete search input. The default value for this setting is true.' - )} -`, - default: true, - }, -]; - -const updateEnvFile = data => { - let content = ''; - data.map(line => { - content = content + line.toString(); - }); - - fs.writeFileSync('./.env', content); -}; - -const checkIfSameLine = (answers, line) => { - let foundKey; - if (answers) { - Object.keys(answers).map(key => { - if (line.includes(key)) { - foundKey = key; - } - }); - } - return foundKey; -}; - -// Read all lines from existing .env file to array. If line matches one of the keys in user's answers update add value to that line. Otherwise keep the original line. -const readLines = answers => { - return new Promise((resolve, reject) => { - const rl = readline.createInterface({ - input: require('fs').createReadStream('./.env'), - }); - - const data = []; - rl.on('line', function(line) { - const key = checkIfSameLine(answers, line); - if (key) { - const value = `${answers[key]}`; - - data.push(`${key}=${value.trim()}\n`); - } else { - data.push(`${line}\n`); - } - }); - - rl.on('close', () => { - resolve(data); - }); - }); -}; - -// Create new .env file using .env-template -const createEnvFile = () => { - fs.copyFileSync('./.env-template', './.env', err => { - if (err) throw err; - }); -}; +/** + * If config script is running with flag --check only check if .env file exists. + * Show error if .env file doesn't exist. + * + * When running the script without flag .env file is created. + * User can fill in environment variables needed for running the application. + * + * If .env file already exists user can edit the values configured in the file. + * Old values are used as default. + * + */ const run = () => { + const hasEnvFile = fs.existsSync(`./.env`); + if (process.argv[2] && process.argv[2] === '--check') { - if (!fs.existsSync(`./.env`)) { + if (!hasEnvFile) { process.on('exit', code => { console.log(` ${chalk.bold.red(`You don't have required .env file!`)} -Some environment variables are required before starting the app. You can create the .env file and configure the variables by running ${chalk.cyan.bold( +Some environment variables are required before starting Flex template for web. You can create the .env file and configure the variables by running ${chalk.cyan.bold( 'yarn run config' )} `); }); - process.exit(1); } - } else if (fs.existsSync(`./.env`)) { + } else if (hasEnvFile) { console.log(` -.env file already exists. You can edit the variables directly from the file. Remember to restart the application after editing the environment variables! - +${chalk.bold.green('.env file already exists!')} +Remember to restart the application after editing the environment variables! You can also edit environment variables by editing the .env file directly in your text editor. `); - } else { - createEnvFile(); - - console.log(chalk.yellow.bold(`Required variables:`)); inquirer - .prompt(mandatoryVariables) + .prompt([ + { + type: 'confirm', + name: 'editEnvFile', + message: 'Do you want to edit the .env file?', + default: false, + }, + ]) .then(answers => { - return readLines(answers); - }) - .then(data => { - updateEnvFile(data); - - console.log(chalk.yellow.bold(`Variables with default values:`)); - inquirer - .prompt(defaultVariables) - .then(answers => { - return readLines(answers); - }) - .then(data => { - updateEnvFile(data); - console.log(` -${chalk.green.bold('Environment variables saved succesfully!')} - -Note that the .env file is a hidden file so it might not be visible directly in directory listing. If you want to update the environment variables you need to edit the file. Remember to restart the application after editing the environment variables! - -Start the Flex template application by running ${chalk.bold.cyan('yarn run dev')} - `); - }); + if (answers.editEnvFile) { + const settings = findSavedValues(); + askQuestions(settings); + } + }); + } else { + inquirer + .prompt([ + { + type: 'confirm', + name: 'createEmptyEnv', + message: `Do you want to configure required environment variables now? +${chalk.dim( + `If you don't set up variables now .env file is created based on .env-template file. The application will not work correctly without Flex client ID, Stripe publishable API key and MapBox acces token. +We recommend setting up the required variables before starting the application!` + )}`, + default: true, + }, + ]) + .then(answers => { + createEnvFile(); + if (answers.createEmptyEnv) { + askQuestions(); + } else { + showSuccessMessage(); + } }) .catch(err => { console.log(chalk.red(`An error occurred due to: ${err.message}`)); @@ -211,4 +82,297 @@ Start the Flex template application by running ${chalk.bold.cyan('yarn run dev') } }; +/** + * Questions for mandatory variables. + * + * @param {*} settings - list of values used as default if user is editing the .env file + * + */ +const mandatoryVariables = settings => { + const clientIdDefaultMaybe = + settings && + settings.REACT_APP_SHARETRIBE_SDK_CLIENT_ID !== '' && + settings.REACT_APP_SHARETRIBE_SDK_CLIENT_ID !== 'change-me' + ? { default: settings.REACT_APP_SHARETRIBE_SDK_CLIENT_ID } + : {}; + const stripeDefaultMaybe = + settings && settings.REACT_APP_STRIPE_PUBLISHABLE_KEY !== '' + ? { default: settings.REACT_APP_STRIPE_PUBLISHABLE_KEY } + : {}; + const mapBoxDefaultMaybe = + settings && settings.REACT_APP_MAPBOX_ACCESS_TOKEN !== '' + ? { default: settings.REACT_APP_MAPBOX_ACCESS_TOKEN } + : {}; + const currencyDefault = + settings && settings.REACT_APP_SHARETRIBE_MARKETPLACE_CURRENCY !== '' + ? settings.REACT_APP_SHARETRIBE_MARKETPLACE_CURRENCY + : 'USD'; + + return [ + { + type: 'input', + name: 'REACT_APP_SHARETRIBE_SDK_CLIENT_ID', + message: `What is your Flex client ID? +${chalk.dim( + 'Client ID is needed for connecting with Flex API. You can find your client ID from Flex Console.' + )} +`, + validate: function(value) { + if (value.match(/^[a-z0-9]{8}-[a-z0-9]{4}-[a-z0-9]{4}-[a-z0-9]{4}-[a-z0-9]{12}$/i)) { + return true; + } + return 'Please enter valid Flex Client ID. You can check it form Flex Console!'; + }, + ...clientIdDefaultMaybe, + }, + { + type: 'input', + name: 'REACT_APP_STRIPE_PUBLISHABLE_KEY', + message: `What is your Stripe publishable key? +${chalk.dim( + `Stripe publishable API key is for generating tokens with Stripe API. Use test key (prefix pk_test_) for development. Remember to also add the Stripe secret key to Console before testing the payments. +If you don't set the Stripe key, payments won't work in the application.` + )} +`, + validate: function(value) { + if (value.match(/^pk_/) || value === '') { + return true; + } + return 'Please enter Stripe publishable key with prefix pk_!'; + }, + ...stripeDefaultMaybe, + }, + { + type: 'input', + name: 'REACT_APP_MAPBOX_ACCESS_TOKEN', + message: `What is your Mapbox access token? +${chalk.dim( + `Mapbox is the default map provider of the application. Sign up for Mapbox and go the to account page. Then click Create access token. For more information see the: Integrating to map providers documentation. +If you don't set the Mapbox key, the map components won't work in the application.` + )} +`, + ...mapBoxDefaultMaybe, + }, + { + type: 'input', + name: 'REACT_APP_SHARETRIBE_MARKETPLACE_CURRENCY', + message: `What is your marketplace currency? +${chalk.dim( + 'The currency used in the Marketplace must be in ISO 4217 currency code. For example USD, EUR, CAD, AUD, etc. The default value is USD.' + )} +`, + default: function() { + return currencyDefault; + }, + validate: function(value) { + if (value.match(/^[a-zA-Z]{3}$/)) { + return true; + } + return 'Please enter currency in ISO 4217 format (e.g. USD, EUR, CAD...)'; + }, + }, + ]; +}; + +/** + * Questions for advances variables. User can choose to edit these or not. + * + * @param {*} settings - list of values used as default if user is editing the .env file + * + */ + +const advancedSettings = settings => { + const rootUrlDefault = settings ? settings.REACT_APP_CANONICAL_ROOT_URL : null; + const availabilityDefault = settings + ? settings.REACT_APP_AVAILABILITY_ENABLED.toLowerCase() === 'true' + : true; + const searchesDefault = settings + ? settings.REACT_APP_DEFAULT_SEARCHES_ENABLED.toLowerCase() === 'true' + : true; + + return [ + { + type: 'confirm', + name: 'showAdvancedSettings', + message: 'Do you want to edit advanced settings?', + default: false, + }, + { + type: 'input', + name: 'REACT_APP_CANONICAL_ROOT_URL', + message: `What is your canonical root URL? +${chalk.dim( + 'Canonical root URL of the marketplace is needed for social media sharing and SEO optimization. When developing the template application locally URL is usually http://localhost:3000' + )} +`, + default: function() { + return rootUrlDefault ? rootUrlDefault : 'http://localhost:3000'; + }, + when: function(answers) { + return answers.showAdvancedSettings; + }, + }, + { + type: 'confirm', + name: 'REACT_APP_AVAILABILITY_ENABLED', + message: `Do you want to enable availability calendar? +${chalk.dim( + 'This setting enables the Availability Calendar for listings. The default value for this setting is true.' + )} +`, + default: availabilityDefault, + when: function(answers) { + return answers.showAdvancedSettings; + }, + }, + { + type: 'confirm', + name: 'REACT_APP_DEFAULT_SEARCHES_ENABLED', + message: `Do you want to enable default search suggestions? +${chalk.dim( + 'This setting enables the Default Search Suggestions in location autocomplete search input. The default value for this setting is true.' + )} +`, + default: searchesDefault, + when: function(answers) { + return answers.showAdvancedSettings; + }, + }, + ]; +}; + +const askQuestions = settings => { + inquirer + .prompt(mandatoryVariables(settings)) + .then(answers => { + return readLines(answers); + }) + .then(values => { + const data = getData(values); + updateEnvFile(data); + + console.log(chalk.yellow.bold(`Advanced settings:`)); + inquirer + .prompt(advancedSettings(settings)) + .then(answers => { + return readLines(answers); + }) + .then(values => { + const data = getData(values); + updateEnvFile(data); + showSuccessMessage(); + }); + }) + .catch(err => { + console.log(chalk.red(`An error occurred due to: ${err.message}`)); + }); +}; + +const showSuccessMessage = () => { + console.log(` +${chalk.green.bold('.env file saved succesfully!')} + +Start the Flex template application by running ${chalk.bold.cyan('yarn run dev')} + +Note that the .env file is a hidden file so it might not be visible directly in directory listing. If you want to update the environment variables run ${chalk.cyan.bold( + 'yarn run config' + )} again or edit the .env file directly. Remember to restart the application after editing the environment variables! +`); +}; + +/** + * Create new .env file using .env-template + */ +const createEnvFile = () => { + fs.copyFileSync('./.env-template', './.env', err => { + if (err) throw err; + }); +}; + +const findSavedValues = () => { + const savedEnvFile = fs.readFileSync('./.env').toString(); + + const settings = savedEnvFile.split('\n').reduce((obj, line) => { + const splits = line.split('='); + const key = splits[0].trim(); + if (splits.length > 1) { + obj[key] = splits + .slice(1) + .join('=') + .trim(); + } + return obj; + }, {}); + + return settings; +}; + +/** + * Read all lines from existing .env file to array. + * If line matches one of the keys in user's answers update add value to that line. + * Otherwise keep the original line. + * + * @param {object} answers object containing all the inputs user has given to questions + * + * @return returns a Promise that resolves with values object containing user input and read lines + */ + +const readLines = answers => { + return new Promise((resolve, reject) => { + const rl = readline.createInterface({ + input: fs.createReadStream('./.env'), + }); + + const lines = []; + rl.on('line', function(line) { + lines.push(line); + }); + + rl.on('close', () => { + const values = { answers, lines }; + resolve(values); + }); + }); +}; + +const getData = values => { + const { lines, answers } = values; + + const data = lines.map(line => { + const key = getKeyFromAnswers(answers, line); + if (key) { + const value = `${answers[key]}`; + return `${key}=${value.trim()}\n`; + } else { + return `${line}\n`; + } + }); + + return data; +}; + +const updateEnvFile = data => { + fs.writeFileSync('./.env', data.join('')); +}; + +/** + * Check if line contains the key from the user answers. + * + * @param {object} answers array of answers user has given to questions + * @param {string} line line from the existing .env file + * + * @returns key if the key matches to the one in the line + */ +const getKeyFromAnswers = (answers, line) => { + let foundKey; + if (answers) { + foundKey = Object.keys(answers).find(function(key) { + if (line.includes(key)) { + return key; + } + }); + } + return foundKey; +}; + run();