Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add capability to specify an env file to load configuration from in Stencil CLI commands #1143

Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion bin/stencil-init.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand All @@ -21,5 +22,6 @@ new StencilInit()
accessToken: cliOptions.token,
port: cliOptions.port,
apiHost: cliOptions.apiHost,
envFile: cliOptions.envFile,
})
.catch(printCliResultErrorAndExit);
5 changes: 5 additions & 0 deletions bin/stencil-start.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -30,6 +34,7 @@ const options = {
apiHost: cliOptions.host,
tunnel: cliOptions.tunnel,
cache: cliOptions.cache,
envFile: cliOptions.envFile,
};

const timeout = cliOptions.timeout * 1000; // seconds
Expand Down
108 changes: 104 additions & 4 deletions lib/StencilConfigManager.js
Original file line number Diff line number Diff line change
@@ -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');
Expand All @@ -9,6 +11,7 @@ class StencilConfigManager {
constructor({
themePath = THEME_PATH,
fs = fsModule,
os = osModule,
fsUtils = fsUtilsModule,
logger = console,
} = {}) {
Expand All @@ -23,6 +26,7 @@ class StencilConfigManager {
this.secretFieldsSet = new Set(['accessToken', 'githubToken']);

this._fs = fs;
this._os = os;
this._fsUtils = fsUtils;
this._logger = logger;
}
Expand All @@ -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 {
Expand All @@ -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 };
Expand All @@ -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(`(?<!#\\s*)${key}(?==)`);
const target = vars.findIndex((line) => 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));
}
}

/**
Expand Down Expand Up @@ -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
Expand Down
9 changes: 5 additions & 4 deletions lib/stencil-init.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,15 +30,16 @@ class StencilInit {
* @param {string} cliOptions.normalStoreUrl
* @param {string} cliOptions.accessToken
* @param {number} cliOptions.port
* @param {string} [cliOptions.envFile]
* @returns {Promise<void>}
*/
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,
Expand All @@ -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',
Expand Down
8 changes: 7 additions & 1 deletion lib/stencil-start.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down
21 changes: 19 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down