diff --git a/bin/stencil-init.js b/bin/stencil-init.js index 18b03529..a4038e37 100755 --- a/bin/stencil-init.js +++ b/bin/stencil-init.js @@ -11,7 +11,8 @@ program .option('-u, --url [url]', 'Store URL') .option('-t, --token [token]', 'Access Token') .option('-p, --port [port]', 'Port') - .option('-h, --apiHost [host]', 'API Host'); + .option('-h, --apiHost [host]', 'API Host') + .option('-e, --envFile [file]', 'Env Vars File'); const cliOptions = prepareCommand(program); @@ -21,5 +22,6 @@ new StencilInit() accessToken: cliOptions.token, port: cliOptions.port, apiHost: cliOptions.apiHost, + envFile: cliOptions.envFile, }) .catch(printCliResultErrorAndExit); diff --git a/bin/stencil-start.js b/bin/stencil-start.js index c61120c8..7110fa18 100755 --- a/bin/stencil-start.js +++ b/bin/stencil-start.js @@ -20,6 +20,10 @@ program '-n, --no-cache', 'Turns off caching for API resource data per storefront page. The cache lasts for 5 minutes before automatically refreshing.', ) + .option( + '-e, --envFile [envFile]', + 'Load config from provided env file, prioritizing system vars.', + ) .option('-t, --timeout', 'Set a timeout for the bundle operation. Default is 20 secs', '60'); const cliOptions = prepareCommand(program); @@ -30,6 +34,7 @@ const options = { apiHost: cliOptions.host, tunnel: cliOptions.tunnel, cache: cliOptions.cache, + envFile: cliOptions.envFile, }; const timeout = cliOptions.timeout * 1000; // seconds diff --git a/lib/StencilConfigManager.js b/lib/StencilConfigManager.js index d69645eb..7ae324c6 100644 --- a/lib/StencilConfigManager.js +++ b/lib/StencilConfigManager.js @@ -1,6 +1,8 @@ require('colors'); const fsModule = require('fs'); +const osModule = require('os'); const path = require('path'); +const dotenv = require('dotenv'); const fsUtilsModule = require('./utils/fsUtils'); const { THEME_PATH, API_HOST } = require('../constants'); @@ -9,6 +11,7 @@ class StencilConfigManager { constructor({ themePath = THEME_PATH, fs = fsModule, + os = osModule, fsUtils = fsUtilsModule, logger = console, } = {}) { @@ -23,6 +26,7 @@ class StencilConfigManager { this.secretFieldsSet = new Set(['accessToken', 'githubToken']); this._fs = fs; + this._os = os; this._fsUtils = fsUtils; this._logger = logger; } @@ -32,7 +36,7 @@ class StencilConfigManager { * @param {boolean} ignoreMissingFields * @returns {object|null} */ - async read(ignoreFileNotExists = false, ignoreMissingFields = false) { + async read(ignoreFileNotExists = false, ignoreMissingFields = false, envFile = null) { if (this._fs.existsSync(this.oldConfigPath)) { let parsedConfig; try { @@ -51,6 +55,12 @@ class StencilConfigManager { ? await this._fsUtils.parseJsonFile(this.configPath) : null; const secretsConfig = await this._getSecretsConfig(generalConfig); + const envConfig = this._getConfigFromEnvVars(envFile); + + if (envConfig) { + const parsedConfig = { ...generalConfig, ...envConfig }; + return this._validateStencilConfig(parsedConfig, ignoreMissingFields); + } if (generalConfig || secretsConfig) { const parsedConfig = { ...generalConfig, ...secretsConfig }; @@ -67,11 +77,73 @@ class StencilConfigManager { /** * @param {object} config */ - async save(config) { + async save(config, envFile) { const { generalConfig, secretsConfig } = this._splitStencilConfig(config); - await this._fs.promises.writeFile(this.configPath, JSON.stringify(generalConfig, null, 2)); - await this._fs.promises.writeFile(this.secretsPath, JSON.stringify(secretsConfig, null, 2)); + if (envFile) { + await this._fs.promises.writeFile( + this.configPath, + JSON.stringify({ customLayouts: generalConfig.customLayouts }, null, 2), + ); + + this._setEnvValuesToFile( + { + STENCIL_ACCESS_TOKEN: secretsConfig.accessToken, + STENCIL_GITHUB_TOKEN: secretsConfig.githubToken, + STENCIL_STORE_URL: generalConfig.normalStoreUrl, + STENCIL_API_HOST: generalConfig.apiHost, + STENCIL_PORT: generalConfig.port, + }, + envFile, + ); + } else { + await this._fs.promises.writeFile( + this.configPath, + JSON.stringify(generalConfig, null, 2), + ); + await this._fs.promises.writeFile( + this.secretsPath, + JSON.stringify(secretsConfig, null, 2), + ); + } + } + + /** + * @param {Array.<{key: String, value: any}>} keyValPairs + * @param {string} envFile + */ + _setEnvValuesToFile(keyValPairs, envFile) { + const envFilePath = path.join(this.themePath, envFile); + + for (const [key, value] of Object.entries(keyValPairs)) { + if (!this._fs.existsSync(envFile)) { + this._fs.openSync(envFile, 'a'); + } + + const vars = this._fs.readFileSync(envFile, 'utf8').split(this._os.EOL); + + // Search for uncommented .env key-value line + const envLineRegex = new RegExp(`(? line.match(envLineRegex)); + + if (target !== -1) { + // Replace value if found + vars.splice(target, 1, `${key}=${value || ''}`); + } else if (vars.length === 1 && vars[0] === '') { + // Add at beginning of array if only content is empty string + vars.unshift(`${key}=${value || ''}`); + } else { + // For newline at the end if not found + if (vars[vars.length - 1] !== '') { + vars.push(''); + } + + // If key doesn't exist, add as new line + vars.splice(vars.length - 1, 0, `${key}=${value || ''}`); + } + + this._fs.writeFileSync(envFilePath, vars.join(this._os.EOL)); + } } /** @@ -111,6 +183,34 @@ class StencilConfigManager { : null; } + /** + * @private + * @returns {object | null} + */ + _getConfigFromEnvVars(envFile) { + if (!envFile) return null; + + dotenv.config({ path: path.join(this.themePath, envFile) }); + + const envConfig = { + normalStoreUrl: process.env.STENCIL_STORE_URL, + accessToken: process.env.STENCIL_ACCESS_TOKEN, + githubToken: process.env.STENCIL_GITHUB_TOKEN, + apiHost: process.env.STENCIL_API_HOST, + port: process.env.STENCIL_PORT, + }; + + if (!envConfig.normalStoreUrl || !envConfig.accessToken || !envConfig.port) { + return null; + } + + for (const [key, val] of Object.entries(envConfig)) { + if (!val) delete envConfig[key]; + } + + return envConfig; + } + /** * @private * @param {object} config diff --git a/lib/stencil-init.js b/lib/stencil-init.js index 3be7158b..af9f88bd 100644 --- a/lib/stencil-init.js +++ b/lib/stencil-init.js @@ -30,15 +30,16 @@ class StencilInit { * @param {string} cliOptions.normalStoreUrl * @param {string} cliOptions.accessToken * @param {number} cliOptions.port + * @param {string} [cliOptions.envFile] * @returns {Promise} */ async run(cliOptions = {}) { - const oldStencilConfig = await this.readStencilConfig(); + const oldStencilConfig = await this.readStencilConfig(cliOptions.envFile); const defaultAnswers = this.getDefaultAnswers(oldStencilConfig); const questions = this.getQuestions(defaultAnswers, cliOptions); const answers = await this.askQuestions(questions); const updatedStencilConfig = this.applyAnswers(oldStencilConfig, answers, cliOptions); - await this._stencilConfigManager.save(updatedStencilConfig); + await this._stencilConfigManager.save(updatedStencilConfig, updatedStencilConfig.envFile); this._logger.log( 'You are now ready to go! To start developing, run $ ' + 'stencil start'.cyan, @@ -48,11 +49,11 @@ class StencilInit { /** * @returns {object} */ - async readStencilConfig() { + async readStencilConfig(envFile) { let parsedConfig; try { - parsedConfig = await this._stencilConfigManager.read(true, true); + parsedConfig = await this._stencilConfigManager.read(true, true, envFile); } catch (err) { this._logger.error( 'Detected a broken stencil-cli config:\n', diff --git a/lib/stencil-start.js b/lib/stencil-start.js index 040665cb..dcfc3a08 100755 --- a/lib/stencil-start.js +++ b/lib/stencil-start.js @@ -54,7 +54,13 @@ class StencilStart { if (cliOptions.variation) { await this._themeConfigManager.setVariationByName(cliOptions.variation); } - const initialStencilConfig = await this._stencilConfigManager.read(); + + const initialStencilConfig = await this._stencilConfigManager.read( + false, + false, + cliOptions.envFile, + ); + // Use initial (before updates) port for BrowserSync const browserSyncPort = initialStencilConfig.port; diff --git a/package-lock.json b/package-lock.json index e620ecae..33ea651b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@bigcommerce/stencil-cli", - "version": "7.2.1", + "version": "7.2.3", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@bigcommerce/stencil-cli", - "version": "7.2.1", + "version": "7.2.3", "license": "BSD-4-Clause", "dependencies": { "@bigcommerce/stencil-paper": "4.10.4", @@ -26,6 +26,7 @@ "colors": "1.4.0", "commander": "^6.1.0", "confidence": "^5.0.1", + "dotenv": "^16.3.1", "form-data": "^3.0.0", "front-matter": "^4.0.2", "glob": "^7.1.6", @@ -7506,6 +7507,17 @@ "node": ">=8" } }, + "node_modules/dotenv": { + "version": "16.3.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.3.1.tgz", + "integrity": "sha512-IPzF4w4/Rd94bA9imS68tZBaYyBWSCE47V1RGuMrB94iyTOIEwRmVL2x/4An+6mETpLrKJ5hQkB8W4kFAadeIQ==", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/motdotla/dotenv?sponsor=1" + } + }, "node_modules/duplexer2": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/duplexer2/-/duplexer2-0.1.4.tgz", @@ -26758,6 +26770,11 @@ "is-obj": "^2.0.0" } }, + "dotenv": { + "version": "16.3.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.3.1.tgz", + "integrity": "sha512-IPzF4w4/Rd94bA9imS68tZBaYyBWSCE47V1RGuMrB94iyTOIEwRmVL2x/4An+6mETpLrKJ5hQkB8W4kFAadeIQ==" + }, "duplexer2": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/duplexer2/-/duplexer2-0.1.4.tgz", diff --git a/package.json b/package.json index 8b7c0a7b..fc09e85b 100644 --- a/package.json +++ b/package.json @@ -65,6 +65,7 @@ "colors": "1.4.0", "commander": "^6.1.0", "confidence": "^5.0.1", + "dotenv": "^16.3.1", "form-data": "^3.0.0", "front-matter": "^4.0.2", "glob": "^7.1.6",