Skip to content

Commit

Permalink
feat: update "stencil pull" to use configurations API, improving perf…
Browse files Browse the repository at this point in the history
…ormance
  • Loading branch information
bookernath committed Sep 29, 2020
1 parent 741641a commit 2b142fc
Show file tree
Hide file tree
Showing 10 changed files with 185 additions and 58 deletions.
14 changes: 12 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,12 @@ Usage: stencil [options] [command]
Commands:
init Interactively create a .stencil file which configures how to run a BigCommerce store locally.
start Starts up BigCommerce store using theme files in the current directory.
start Starts up the BigCommerce storefront local development environment, using theme files in the current directory and data from the live store.
bundle Bundles up the theme into a zip file which can be uploaded to BigCommerce.
release Create a new release in the theme's github repository.
push Bundles up the theme into a zip file and uploads it to your store.
pull Pulls the configuration from the active theme on your live store and updates your local configuration.
download Downloads the theme files from the active theme on your live store.
help [cmd] display help for [cmd]
Options:
Expand All @@ -49,9 +51,17 @@ Run `stencil bundle` to validate your code and create a zip bundle file that can

Run `stencil release` to tag a new version of your theme, create a [GitHub release](https://help.github.com/articles/about-releases/)
in your theme repository, and upload the zip bundle file to the release assets.
This is useful for tracking your changesin your Theme, and is the tool we use to create new releases in BigCommerce
This is useful for tracking your changes in your Theme, and is the tool we use to create new releases in BigCommerce
[Cornerstone](https://github.com/bigcommerce/stencil) theme.

Run `stencil push` to bundle the local theme and upload it to your store, so it will be available in My Themes.
To push the theme and also activate it, use `stencil push -a`. To automatically delete the oldest theme if you are at
your theme limit, use `stencil push -d`. These can be used together, as `stencil push -a -d`.

Run `stencil pull` to sync changes to your theme configuration from your live store. For example, if Page Builder has
been used to change certain theme settings, this will update those settings in config.json in your theme files so you
don't overwrite them on your next upload.

## Features

### BrowserSync
Expand Down
12 changes: 7 additions & 5 deletions bin/stencil-download.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,14 @@ const program = require('../lib/commander');
const { API_HOST, PACKAGE_INFO, DOT_STENCIL_FILE_PATH } = require('../constants');
const stencilDownload = require('../lib/stencil-download');
const versionCheck = require('../lib/version-check');
const { printCliResultError } = require('../lib/cliCommon');
const { printCliResultErrorAndExit } = require('../lib/cliCommon');

program
.version(PACKAGE_INFO.version)
.option('--host [hostname]', 'specify the api host', API_HOST)
.option('--file [filename]', 'specify the filename to download only')
.option('--exclude [exclude]', 'specify a directory to exclude from download')
.option('-h, --host [hostname]', 'specify the api host', API_HOST)
.option('-f, --file [filename]', 'specify the filename to download only')
.option('-e, --exclude [exclude]', 'specify a directory to exclude from download')
.option('-c, --channel_id [channelId]', 'specify the channel ID of the storefront', parseInt)
.parse(process.argv);

if (!versionCheck()) {
Expand All @@ -26,6 +27,7 @@ const options = {
dotStencilFilePath: DOT_STENCIL_FILE_PATH,
exclude: ['parsed', 'manifest.json', ...extraExclude],
apiHost: cliOptions.host || API_HOST,
channelId: cliOptions.channel_id || 1,
file: cliOptions.file,
};

Expand All @@ -51,7 +53,7 @@ async function run (opts) {
try {
await stencilDownload(opts);
} catch (err) {
printCliResultError(err);
printCliResultErrorAndExit(err);
return;
}

Expand Down
18 changes: 10 additions & 8 deletions bin/stencil-pull.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,14 @@ const { DOT_STENCIL_FILE_PATH, PACKAGE_INFO, API_HOST } = require('../constants'
const program = require('../lib/commander');
const stencilPull = require('../lib/stencil-pull');
const versionCheck = require('../lib/version-check');
const { printCliResultError} = require('../lib/cliCommon');
const { printCliResultErrorAndExit } = require('../lib/cliCommon');

program
.version(PACKAGE_INFO.version)
.option('--host [hostname]', 'specify the api host', API_HOST)
.option('--save [filename]', 'specify the filename to save the config as', 'config.json')
.option('-s, --saved', 'get the saved configuration instead of the active one')
.option('-h, --host [hostname]', 'specify the api host', API_HOST)
.option('-f, --filename [filename]', 'specify the filename to save the config as', 'config.json')
.option('-c, --channel_id [channelId]', 'specify the channel ID of the storefront to pull configuration from', parseInt)
.parse(process.argv);

if (!versionCheck()) {
Expand All @@ -22,13 +24,13 @@ const cliOptions = program.opts();
const options = {
dotStencilFilePath: DOT_STENCIL_FILE_PATH,
apiHost: cliOptions.host || API_HOST,
saveConfigName: cliOptions.save,
saveConfigName: cliOptions.filename,
channelId: cliOptions.channel_id || 1,
saved: cliOptions.saved || false,
};

stencilPull(options, (err, result) => {
stencilPull(options, err => {
if (err) {
printCliResultError(err);
return;
printCliResultErrorAndExit(err);
}
console.log('ok'.green + ` -- Pulled active theme config to ${result.saveConfigName}`);
});
5 changes: 2 additions & 3 deletions bin/stencil-push.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ const { DOT_STENCIL_FILE_PATH, PACKAGE_INFO, API_HOST } = require('../constants'
const program = require('../lib/commander');
const stencilPush = require('../lib/stencil-push');
const versionCheck = require('../lib/version-check');
const { printCliResultError } = require('../lib/cliCommon');
const { printCliResultErrorAndExit } = require('../lib/cliCommon');

program
.version(PACKAGE_INFO.version)
Expand All @@ -31,8 +31,7 @@ const options = {
};
stencilPush(options, (err, result) => {
if (err) {
printCliResultError(err);
return;
printCliResultErrorAndExit(err);
}
console.log('ok'.green + ` -- ${result}`);
});
11 changes: 11 additions & 0 deletions lib/cliCommon.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,18 @@ function printCliResultError (error) {
console.log(messages.submitGithubIssue);
}

/**
* @param {Error} error
* @returns {void}
*/
function printCliResultErrorAndExit (error) {
printCliResultError(error);
// Exit with error code so automated systems recognize it as a failure
process.exit(1);
}

module.exports = {
printCliResultError,
printCliResultErrorAndExit,
messages,
};
4 changes: 2 additions & 2 deletions lib/stencil-download.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@ function stencilDownload(options) {
stencilPushUtils.readStencilConfigFile,
stencilPushUtils.getStoreHash,
stencilPushUtils.getThemes,
stencilPullUtils.selectActiveTheme,
stencilPullUtils.startThemeDownloadJob,
stencilPullUtils.getChannelActiveTheme,
stencilDownloadUtil.startThemeDownloadJob,
stencilPushUtils.pollForJobCompletion(({ download_url: downloadUrl }) => ({ downloadUrl })),
stencilDownloadUtil.downloadThemeFiles,
]);
Expand Down
17 changes: 17 additions & 0 deletions lib/stencil-download.utils.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
const tmp = require('tmp-promise');
const { extractZipFiles } = require('./archiveManager');
const { fetchFile } = require('./utils/networkUtils');
const themeApiClient = require('./theme-api-client');

const utils = {};

Expand All @@ -26,3 +27,19 @@ utils.downloadThemeFiles = async options => {

return options;
};

utils.startThemeDownloadJob = async options => {
const { config: { accessToken }, apiHost, activeTheme, storeHash } = options;

const { jobId } = await themeApiClient.downloadTheme({
accessToken: accessToken,
themeId: activeTheme.active_theme_uuid,
apiHost,
storeHash,
});

return {
...options,
jobId,
};
};
8 changes: 3 additions & 5 deletions lib/stencil-pull.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,8 @@ function stencilPull(options, callback) {
async.constant(options),
stencilPushUtils.readStencilConfigFile,
stencilPushUtils.getStoreHash,
stencilPushUtils.getThemes,
stencilPullUtils.selectActiveTheme,
stencilPullUtils.startThemeDownloadJob,
stencilPushUtils.pollForJobCompletion(({ download_url: downloadUrl }) => ({ downloadUrl })),
stencilPullUtils.downloadThemeConfig,
stencilPullUtils.getChannelActiveTheme,
stencilPullUtils.getThemeConfiguration,
stencilPullUtils.mergeThemeConfiguration,
], callback);
}
91 changes: 58 additions & 33 deletions lib/stencil-pull.utils.js
Original file line number Diff line number Diff line change
@@ -1,54 +1,79 @@
const themeApiClient = require('./theme-api-client');
const tmp = require('tmp-promise');
const { extractZipFiles } = require('./archiveManager');
const { fetchFile } = require('./utils/networkUtils');
const fs = require('fs');
const _ = require('lodash');

const utils = {};

module.exports = utils;

utils.selectActiveTheme = (options, callback) => {
const [activeTheme] = options.themes.filter(theme => theme.is_active).map(theme => theme.uuid);
utils.getChannelActiveTheme = async options => {
const { config: { accessToken }, apiHost, storeHash, channelId } = options;

callback(null, Object.assign({}, options, { activeTheme }));
const activeTheme = await themeApiClient.getChannelActiveTheme({ accessToken, apiHost, storeHash, channelId });

console.log('ok'.green + ` -- Fetched theme details for channel ${channelId}`);

return { ...options, activeTheme };
};

utils.startThemeDownloadJob = async options => {
const { config: { accessToken }, apiHost, activeTheme, storeHash } = options;
utils.getThemeConfiguration = async options => {
const { config: { accessToken }, apiHost, storeHash, activeTheme, saved } = options;

const themeId = activeTheme.active_theme_uuid;

const configurationId = saved ? activeTheme.saved_theme_configuration_uuid
: activeTheme.active_theme_configuration_uuid;

const { jobId } = await themeApiClient.downloadTheme({
accessToken: accessToken,
themeId: activeTheme,
apiHost,
storeHash,
});
const remoteThemeConfiguration = await themeApiClient.getThemeConfiguration({ accessToken, apiHost, storeHash,
themeId, configurationId });

return {
...options,
jobId,
};
console.log('ok'.green + ` -- Fetched ${saved ? 'saved' : 'active'} configuration`);

return { ...options, remoteThemeConfiguration };
};

utils.downloadThemeConfig = async options => {
const { path: tempThemePath, cleanup } = await tmp.file();
utils.mergeThemeConfiguration = async options => {
const { remoteThemeConfiguration } = options;

try {
await fetchFile(options.downloadUrl, tempThemePath);
} catch (err) {
throw new Error(`Unable to download theme config from ${options.downloadUrl}: ${err.message}`);
}
let rawConfig = fs.readFileSync('config.json');
let parsedConfig = JSON.parse(rawConfig);
let diffDetected = false;

console.log('ok'.green + ' -- Theme files downloaded');
console.log('ok'.green + ' -- Extracting theme config');
// For any keys the remote configuration has in common with the local configuration,
// overwrite the local configuration if the remote configuration differs
for (const [key, value] of Object.entries(remoteThemeConfiguration.settings)) {
if (key in parsedConfig.settings) {
// Check for different types, and throw an error if they are found
if (typeof parsedConfig.settings[key] !== typeof value) {
throw new Error(`Theme configuration key ${key} cannot be merged because it is not of the same type. Remote configuration is of type ${typeof value} while local configuration is of type ${typeof parsedConfig.settings[key]}.`);
}

const outputNames = {
'config.json': options.saveConfigName,
};
await extractZipFiles({ zipPath: tempThemePath, fileToExtract: 'config.json', outputNames });
// If a different value is found, overwrite the local config
if (!_.isEqual(parsedConfig.settings[key], value)) {
parsedConfig.settings[key] = value;
diffDetected = true;
}
}
}

console.log('ok'.green + ' -- Theme config extracted');
// Does a file need to be written?
if (diffDetected || options.saveConfigName !== 'config.json') {
if (diffDetected) {
console.log('ok'.green + ' -- Remote configuration merged with local configuration');
} else {
console.log('ok'.green + ' -- Remote and local configurations are in sync for all common keys');
}

await cleanup();
fs.writeFile(options.saveConfigName, JSON.stringify(parsedConfig, null, 2), function(err) {
if(err) {
console.error(err);
} else {
console.log('ok'.green + ` -- Configuration written to ${options.saveConfigName}`);
}
});
} else {
console.log('ok'.green + ` -- Remote and local configurations are in sync for all common keys, no action taken`);
}

return options;
};
63 changes: 63 additions & 0 deletions lib/theme-api-client.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,11 @@ const FormData = require('form-data');
const themeApiClient = {
activateThemeByVariationId,
deleteThemeById,
getChannelActiveTheme,
getJob,
getVariationsByThemeId,
getThemes,
getThemeConfiguration,
postTheme,
downloadTheme,
};
Expand Down Expand Up @@ -152,6 +154,67 @@ async function getThemes({ accessToken, apiHost, storeHash }) {
return payload.data;
}

/**
* @param {object} options
* @param {string} options.accessToken
* @param {string} options.apiHost
* @param {string} options.storeHash
* @param {int} options.channelId
* @returns {Promise<object[]>}
*/
async function getChannelActiveTheme({ accessToken, apiHost, storeHash, channelId}) {
const reqOpts = {
headers: {
'x-auth-client': 'stencil-cli',
'x-auth-token': accessToken,
},
};
const reqUrl = `${apiHost}/stores/${storeHash}/v3/channels/${channelId}/active-theme`;

const response = await fetch(reqUrl, reqOpts);
if (!response.ok) {
throw new Error(`Could not fetch active theme details for channel ${channelId}: ${response.statusText}`);
}
const payload = await response.json();

return payload.data;
}

/**
* @param {object} options
* @param {string} options.accessToken
* @param {string} options.apiHost
* @param {string} options.storeHash
* @param {string} options.themeId
* @param {string} options.configurationId
* @returns {Promise<object[]>}
*/
async function getThemeConfiguration({ accessToken, apiHost, storeHash,
themeId, configurationId}) {
const reqOpts = {
headers: {
'x-auth-client': 'stencil-cli',
'x-auth-token': accessToken,
},
};

const reqUrl = `${apiHost}/stores/${storeHash}/v3/themes/${themeId}`
+ `/configurations?uuid:in=${configurationId}`;

const response = await fetch(reqUrl, reqOpts);
if (!response.ok) {
throw new Error(`Could not fetch theme configuration: ${response.statusText}`);
}
const payload = await response.json();

// If configurations array is empty, the theme ID was valid but the configuration ID was not
if (!payload.data.length) {
throw new Error(`Configuration ID ${configurationId} not found for theme ID ${themeId}`);
}

return payload.data[0];
}

/**
* @param {object} options
* @param {string} options.themeId
Expand Down

0 comments on commit 2b142fc

Please sign in to comment.