Skip to content

Commit

Permalink
Add feature to allow pulling live config files (#550)
Browse files Browse the repository at this point in the history
* Add .nvmrc file for easy NVM version switching

* feat: add pull command to pull live config settings

* fix: eslint issues

* fix: downgrade language features used to be compatible with test suite

* fix: remove erroneous semi-colon

* fix: remove more erroneous semi-colons

* fix: remove yet another erroneous semi-colon
  • Loading branch information
LordZardeck authored Feb 4, 2020
1 parent d4fce7b commit 43558fc
Show file tree
Hide file tree
Showing 11 changed files with 248 additions and 26 deletions.
2 changes: 1 addition & 1 deletion .eslintrc
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"parserOptions": {
"ecmaVersion": 6
"ecmaVersion": 8
},
"ecmaFeatures": {
"modules": true
Expand Down
1 change: 1 addition & 0 deletions .nvmrc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
v10.16.3
1 change: 1 addition & 0 deletions bin/stencil
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,5 @@ program
.command('bundle', 'Bundles up the theme into a zip file which can be uploaded to BigCommerce.')
.command('release', 'Create a new release in the theme\'s github repository.')
.command('push', 'Bundles up the theme into a zip file and uploads it to your store.')
.command('pull', 'Pulls currently active theme config files and overwrites local copy')
.parse(process.argv);
34 changes: 34 additions & 0 deletions bin/stencil-pull
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
#!/usr/bin/env node

require('colors');
const apiHost = 'https://api.bigcommerce.com';
const dotStencilFilePath = './.stencil';
const options = { dotStencilFilePath };
const pkg = require('../package.json');
const Program = require('commander');
const stencilPull = require('../lib/stencil-pull');
const versionCheck = require('../lib/version-check');
const themeApiClient = require('../lib/theme-api-client');

Program
.version(pkg.version)
.option('--host [hostname]', 'specify the api host', apiHost)
.option('--save [filename]', 'specify the filename to save the config as', 'config.json')
.parse(process.argv);

if (!versionCheck()) {
return;
}

stencilPull(Object.assign({}, options, {
apiHost: Program.host || apiHost,
saveConfigName: Program.save,
}), (err, result) => {
if (err) {
console.log("\n\n" + 'not ok'.red + ` -- ${err} see details below:`);
themeApiClient.printErrorMessages(err.messages);
console.log('If this error persists, please visit https://github.com/bigcommerce/stencil-cli/issues and submit an issue.');
} else {
console.log('ok'.green + ` -- Pulled active theme config to ${result.saveConfigName}`);
}
});
20 changes: 20 additions & 0 deletions lib/stencil-pull.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
'use strict';
const async = require('async');
let stencilPushUtils = require('./stencil-push.utils');
let stencilPullUtils = require('./stencil-pull.utils');

module.exports = stencilPull;

function stencilPull(options, callback) {
options = options || {};
async.waterfall([
async.constant(options),
stencilPushUtils.readStencilConfigFile,
stencilPushUtils.getStoreHash,
stencilPushUtils.getThemes,
stencilPullUtils.selectActiveTheme,
stencilPullUtils.startThemeDownloadJob,
stencilPushUtils.pollForJobCompletion(({ download_url: downloadUrl }) => ({ downloadUrl })),
stencilPullUtils.downloadThemeConfig,
], callback);
}
106 changes: 106 additions & 0 deletions lib/stencil-pull.utils.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
const themeApiClient = require('./theme-api-client');
const request = require("request");
const yauzl = require('yauzl');
const fs = require('fs');
const path = require('path');
const tmp = require('tmp');
const utils = {};

module.exports = utils;

utils.selectActiveTheme = (options, callback) => {
const [activeTheme] = options.themes.filter(theme => theme.is_active).map(theme => theme.uuid);

callback(null, Object.assign({}, options, { activeTheme }));
};

utils.startThemeDownloadJob = (options, callback) => {
const config = options.config;

themeApiClient.downloadTheme({
accessToken: config.accessToken,
apiHost: options.apiHost,
themeId: options.activeTheme,
clientId: 'stencil-cli',
storeHash: options.storeHash,
}, (error, result) => {
if (error) {
error.name = 'ThemeUploadError';
return callback(error);
}

callback(null, Object.assign({}, options, {
jobId: result.jobId,
}));
});
};

utils.downloadThemeConfig = (options, callback) => {
tmp.file(function _tempFileCreated(err, tempThemePath, fd, cleanupCallback) {
if (err) {
callback(err);
}

(
new Promise(
(resolve, reject) =>
request(options.downloadUrl)
.pipe(fs.createWriteStream(tempThemePath))
.on('finish', () => resolve(tempThemePath))
.on('error', reject)
)
)
.then(tempThemePath =>
new Promise(
(resolve, reject) =>
yauzl.open(tempThemePath, { lazyEntries: true }, (error, zipFile) => {
if (error) {
return reject(error);
}

zipFile.readEntry();
zipFile.on('entry', entry => {
if (!/config\.json/.test(entry.fileName)) {
zipFile.readEntry();
return;
}

zipFile.openReadStream(entry, (readStreamError, readStream) => {
if (readStreamError) {
return reject(readStreamError);
}

let configFileData = '';

readStream.on('end', () => {
resolve(JSON.parse(configFileData));
zipFile.close();
});
readStream.on('data', chunk => {
configFileData += chunk;
});
});
});
})
)
)
.then(
liveStencilConfig =>
new Promise(
(resolve, reject) =>
fs.writeFile(path.resolve(options.saveConfigName), JSON.stringify(liveStencilConfig, null, 2), error => {
if (error) {
reject(error);
}

resolve();
})
)
)
.then(() => {
cleanupCallback();
callback(null, options);
})
.catch(callback);
})
};
2 changes: 1 addition & 1 deletion lib/stencil-push.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ function stencilPush(options, callback) {
utils.deleteThemesIfNecessary,
utils.uploadBundleAgainIfNecessary,
utils.notifyUserOfThemeUploadCompletion,
utils.pollForJobCompletion(),
utils.pollForJobCompletion(data => ({ themeId: data.theme_id })),
utils.promptUserWhetherToApplyTheme,
utils.getVariations,
utils.promptUserForVariation,
Expand Down
32 changes: 18 additions & 14 deletions lib/stencil-push.utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,10 @@ utils.readStencilConfigFile = (options, callback) => {
utils.getStoreHash = (options, callback) => {
options = validateOptions(options, ['config.normalStoreUrl']);

Wreck.get(`https://${options.config.normalStoreUrl.replace(/http(s?):\/\//, '').split('/')[0]}/admin/oauth/info`, { json: true, rejectUnauthorized: false }, (error, response, payload) => {
Wreck.get(`https://${options.config.normalStoreUrl.replace(/http(s?):\/\//, '').split('/')[0]}/admin/oauth/info`, {
json: true,
rejectUnauthorized: false,
}, (error, response, payload) => {
if (error) {
error.name = 'StoreHashReadError';
return callback(error);
Expand Down Expand Up @@ -152,11 +155,11 @@ utils.promptUserToDeleteThemesIfNecessary = (options, callback) => {
}

if (options.deleteOldest) {
const oldestTheme = options.themes
.filter(theme => theme.is_private && !theme.is_active)
.map(theme => ({uuid: theme.uuid, updated_at : new Date(theme.updated_at).valueOf()}))
.reduce((prev, current) => prev.updated_at < current.updated_at ? prev : current)
return callback(null, Object.assign({}, options, {themeIdsToDelete: [oldestTheme.uuid]}));
const oldestTheme = options.themes
.filter(theme => theme.is_private && !theme.is_active)
.map(theme => ({ uuid: theme.uuid, updated_at: new Date(theme.updated_at).valueOf() }))
.reduce((prev, current) => prev.updated_at < current.updated_at ? prev : current)
return callback(null, Object.assign({}, options, { themeIdsToDelete: [oldestTheme.uuid] }));
}

const questions = [{
Expand Down Expand Up @@ -231,7 +234,7 @@ utils.markJobComplete = () => {
console.log('ok'.green + ' -- Theme Processing Finished');
};

utils.pollForJobCompletion = () => {
utils.pollForJobCompletion = resultFilter => {
return async.retryable({
interval: 1000,
errorFilter: err => {
Expand All @@ -243,10 +246,10 @@ utils.pollForJobCompletion = () => {
return false;
},
times: Number.POSITIVE_INFINITY,
}, utils.checkIfJobIsComplete);
}, utils.checkIfJobIsComplete(resultFilter));
};

utils.checkIfJobIsComplete = (options, callback) => {
utils.checkIfJobIsComplete = resultFilter => (options, callback) => {
const config = options.config;

themeApiClient.getJob({
Expand All @@ -256,6 +259,7 @@ utils.checkIfJobIsComplete = (options, callback) => {
storeHash: options.storeHash,
bundleZipPath: options.bundleZipPath,
jobId: options.jobId,
resultFilter,
}, (error, result) => {
if (error) {
return callback(error);
Expand All @@ -281,7 +285,7 @@ utils.promptUserWhetherToApplyTheme = (options, callback) => {
Inquirer.prompt(questions, answers => {
callback(null, Object.assign({}, options, { applyTheme: answers.applyTheme }));
});
};
}
};

utils.getVariations = (options, callback) => {
Expand All @@ -298,7 +302,7 @@ utils.getVariations = (options, callback) => {
}, (error, result) => {
if (error) {
return callback(error);
};
}
if (options.activate !== true && options.activate !== undefined) {
const findVariation = result.variations.find(item => item.name === options.activate);
if (!findVariation || !findVariation.uuid) {
Expand All @@ -309,7 +313,7 @@ utils.getVariations = (options, callback) => {
callback(null, Object.assign({}, options, { variationId: result.variations[0].uuid }));
} else {
callback(null, Object.assign({}, options, result));
};
}
});
};

Expand All @@ -331,7 +335,7 @@ utils.promptUserForVariation = (options, callback) => {
Inquirer.prompt(questions, answers => {
callback(null, Object.assign({}, options, answers));
});
};
}
};

utils.requestToApplyVariationWithRetrys = () => {
Expand Down Expand Up @@ -371,4 +375,4 @@ utils.requestToApplyVariation = (options, callback) => {

utils.notifyUserOfCompletion = (options, callback) => {
callback(null, 'Stencil Push Finished');
};
};
51 changes: 42 additions & 9 deletions lib/theme-api-client.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ const themeApiClient = {
getVariationsByThemeId,
getThemes,
postTheme,
downloadTheme,
};

module.exports = themeApiClient;
Expand Down Expand Up @@ -70,14 +71,21 @@ function getJob(options, callback) {
let error;

if (err) {
err.name = "JobCompletionStatusCheckError"
err.name = "JobCompletionStatusCheckError";
return callback(err);
}

if (res.statusCode === 404) {
error = new Error('Job Failed');
error.name = "JobCompletionStatusCheckError";
error.messages = [{ message: payload.title }];
return callback(error);
}

if (res.statusCode !== 200 || payload.data.status === 'FAILED') {
error = new Error('Job Failed');
error.name = "JobCompletionStatusCheckError";
error.messages = payload.data.errors
error.messages = payload.data.errors;
return callback(error);
}

Expand All @@ -87,23 +95,20 @@ function getJob(options, callback) {
return callback(error);
}

callback(null, Object.assign({}, options, {
themeId: payload.data.result.theme_id,
}));
callback(null, Object.assign({}, options, (options.resultFilter || new Function)(payload.data.result)));
});
}

function printErrorMessages(errors_array) {
if (!Array.isArray(errors_array)) {
console.log("unknown error".red)
console.log("unknown error".red);
return false
}

for (var i = 0; i < errors_array.length; i++) {
try{
try {
console.log(errors_array[i].message.red + '\n');
}
catch(err) {
} catch (err) {
continue;
}
}
Expand Down Expand Up @@ -203,3 +208,31 @@ function postTheme(options, callback) {
});
}

function downloadTheme(options, callback) {
const opts = {
headers: {
'cache-control': 'no-cache',
'x-auth-token': options.accessToken,
'x-auth-client': 'stencil-cli',
},
json: true,
method: 'POST',
url: `${options.apiHost}/stores/${options.storeHash}/v3/themes/${options.themeId}/actions/download`,
body: {
which: 'last_activated',
},
};

request(opts, (error, response, payload) => {
if (error || response.statusCode !== 200) {
const err = new Error(error || payload);
err.name = "ThemeDownloadError";

return callback(err);
}

callback(null, {
jobId: payload.job_id,
});
});
}
Loading

0 comments on commit 43558fc

Please sign in to comment.