Skip to content

Commit

Permalink
feat: add schema validation to bundle process
Browse files Browse the repository at this point in the history
  • Loading branch information
Juned Kazi committed Feb 22, 2020
1 parent dbc7524 commit b86d440
Show file tree
Hide file tree
Showing 25 changed files with 560 additions and 135 deletions.
168 changes: 97 additions & 71 deletions lib/bundle-validator.js
Original file line number Diff line number Diff line change
@@ -1,24 +1,26 @@
require('colors');

var VALID_IMAGE_TYPES = ['.jpg', '.jpeg', '.png', '.gif'];
var WIDTH_COMPOSED = 600;
var HEIGHT_COMPOSED = 760;
var WIDTH_MOBILE = 304;
var HEIGHT_MOBILE = 540;
var WIDTH_DESKTOP = 2048;
var HEIGHT_DESKTOP = 2600;
var OBJECTS_TO_VALIDATE = [
const os = require('os');

const VALID_IMAGE_TYPES = ['.jpg', '.jpeg', '.png', '.gif'];
const WIDTH_COMPOSED = 600;
const HEIGHT_COMPOSED = 760;
const WIDTH_MOBILE = 304;
const HEIGHT_MOBILE = 540;
const WIDTH_DESKTOP = 2048;
const HEIGHT_DESKTOP = 2600;
const OBJECTS_TO_VALIDATE = [
'head.scripts',
'footer.scripts',
];

var _ = require('lodash');
var Async = require('async');
var Fs = require('fs');
var sizeOf = require('image-size');
var Path = require('path');
var Validator = require('jsonschema').Validator;
const _ = require('lodash');
const Async = require('async');
const Fs = require('fs');
const sizeOf = require('image-size');
const Path = require('path');
const Validator = require('ajv');
const ValidatorSchemaTranslations = require('./validator/schema-translations');
const JsonSchemaValidatorOptions = {schemaId: 'auto', allErrors: true};

/**
* Run some validations to ensure that the platform will accept the theme
Expand All @@ -35,6 +37,7 @@ function BundleValidator(themePath, themeConfig, isPrivate) {
// Array of tasks used in Async.series
this.validationTasks = [
validateThemeConfiguration.bind(this),
validateThemeSchema.bind(this),
validateSchemaTranslations.bind(this),
validateJspmSettings.bind(this),
];
Expand Down Expand Up @@ -63,16 +66,31 @@ BundleValidator.prototype.sizeOf = function (path, callback) {
return sizeOf(path, callback);
};

/**
* If theme schema exists we need to validate it make sure it passes all defined checks.
* @param callback
* @returns {*}
*/
function validateThemeSchema(callback) {
const validationSchema = './schemas/themeSchema.json';

if (this.themeConfig.schemaExists()) {
const error = validateJsonSchema('schema', require(validationSchema), this.themeConfig.getRawSchema());
if (error) {
return callback(error);
}
}

callback(null, true);
}

/**
* Ensure theme configuration exists and passes the json schema file
* @param callback
* @returns {*}
*/
function validateThemeConfiguration(callback) {
var v = new Validator();
var validationSchema = './themeConfig.schema.json';
var validation;
var errorMessage;
let validationSchema = './schemas/themeConfig.json';

if (!this.themeConfig.configExists()) {
return callback(
Expand All @@ -86,31 +104,45 @@ function validateThemeConfiguration(callback) {

// Validate against the theme registry config schema
if (this.isPrivate) {
validationSchema = './privateThemeConfig.schema.json';
validationSchema = './schemas/privateThemeConfig.json';
}

validation = v.validate(this.themeConfig.getRawConfig(), require(validationSchema));

if (validation.errors && validation.errors.length > 0) {
errorMessage = 'Your theme\'s config.json has errors:'.red;
validation.errors.forEach(error => {
errorMessage += '\r\nconfig'.red + error.stack.substring(8).red;
});

return callback(new Error(errorMessage));
const error = validateJsonSchema('config', require(validationSchema), this.themeConfig.getRawConfig());
if (error) {
return callback(error);
}

callback(null, true);
}

/**
*
* @param type
* @param schema
* @param data
* @returns {Error}
*/
function validateJsonSchema(type, schema, data) {
const validator = new Validator(JsonSchemaValidatorOptions);
validator.validate(schema, data);
if (validator.errors && validator.errors.length > 0) {
let errorMessage;
errorMessage = `Your theme's ${type}.json has errors:`;
validator.errors.forEach(error => {
errorMessage += os.EOL + type + error.dataPath + " " + error.message;
});
return new Error(errorMessage.red);
}
}

/**
* Ensure that schema translations exists and there are no missing or unused keys.
* @param {function} callback
* @return {function} callback
*/
function validateSchemaTranslations(callback) {
const validatorSchemaTranslations = new ValidatorSchemaTranslations();
const validator = new Validator();
const validator = new Validator(JsonSchemaValidatorOptions);

if (this.themeConfig.schemaExists()) {
validatorSchemaTranslations.setSchema(this.themeConfig.getRawSchema());
Expand All @@ -128,12 +160,12 @@ function validateSchemaTranslations(callback) {

const missedKeys = validatorSchemaTranslations.findMissedKeys();
const unusedKeys = validatorSchemaTranslations.findUnusedKeys();
const validation = validator.validate(
validatorSchemaTranslations.getTranslations(),
validator.validate(
validatorSchemaTranslations.getValidationSchema(),
validatorSchemaTranslations.getTranslations(),
);

if ((validation.errors && validation.errors.length) || missedKeys.length || unusedKeys.length) {
if ((validator.errors && validator.errors.length) || missedKeys.length || unusedKeys.length) {
let errorMessage = 'Your theme\'s schemaTranslations.json has errors:';

missedKeys.forEach(key => {
Expand All @@ -144,8 +176,8 @@ function validateSchemaTranslations(callback) {
errorMessage += '\r\nunused translation key "' + key + '"';
});

validation.errors.forEach(error => {
errorMessage += '\r\nschemaTranslations' + error.stack.substring(8);
validator.errors.forEach(error => {
errorMessage += '\r\nschemaTranslations' + error.message;
});

return callback(new Error(errorMessage.red));
Expand All @@ -160,8 +192,8 @@ function validateSchemaTranslations(callback) {
* @returns {*}
*/
function validateJspmSettings(callback) {
var configuration = this.themeConfig.getRawConfig();
var errorMessage;
const configuration = this.themeConfig.getRawConfig();
let errorMessage;

if (configuration.jspm) {
if (!fileExists(Path.join(this.themePath, configuration.jspm.jspm_packages_path))) {
Expand All @@ -182,9 +214,9 @@ function validateJspmSettings(callback) {
* @returns {*}
*/
function validateMetaImages(callback) {
var configuration = this.themeConfig.getConfig();
var imagePath = Path.resolve(this.themePath, 'meta', configuration.meta.composed_image);
var imageTasks = [];
const configuration = this.themeConfig.getConfig();
let imagePath = Path.resolve(this.themePath, 'meta', configuration.meta.composed_image);
let imageTasks = [];
var self = this;

if (!isValidImageType(imagePath)) {
Expand All @@ -203,7 +235,7 @@ function validateMetaImages(callback) {
}

configuration.variations.forEach(function (variation) {
var id = variation.id.blue;
const id = variation.id.blue;

imagePath = Path.resolve(this.themePath, 'meta', variation.meta.desktop_screenshot);

Expand All @@ -215,11 +247,11 @@ function validateMetaImages(callback) {
return callback(new Error('The path you specified for the '.red + id +
' variation\'s "desktop_screenshot" does not exist.'.red));
} else {
(function (path) {
((path => {
imageTasks.push(function (cb) {
validateImage.call(self, path, WIDTH_DESKTOP, HEIGHT_DESKTOP, cb);
});
}(imagePath));
})(imagePath));
}

imagePath = Path.resolve(this.themePath, 'meta', variation.meta.mobile_screenshot);
Expand All @@ -232,28 +264,28 @@ function validateMetaImages(callback) {
return callback(new Error('The path you specified for the '.red + id +
' variation\'s "mobile_screenshot" does not exist.'.red));
} else {
(function (path) {
imageTasks.push(function (cb) {
((path => {
imageTasks.push(cb => {
validateImage.call(self, path, WIDTH_MOBILE, HEIGHT_MOBILE, cb);
});
}(imagePath));
})(imagePath));
}
}.bind(this));

Async.parallel(imageTasks, function (err, result) {
Async.parallel(imageTasks, (err, result) => {
callback(err, result);
});
}

/**
* Check if file exist syncronous
* Check if file exist synchronous
* @param {string} path
* @return {boolean}
*/
function fileExists(path) {
try {
return !!Fs.statSync(path);
}
catch (e) {
} catch (e) {
return false;
}
}
Expand All @@ -263,11 +295,11 @@ function fileExists(path) {
* @param {array} assembledTemplates
* @param {function} callback
*/
BundleValidator.prototype.validateObjects = function (assembledTemplates, callback) {
Async.map(assembledTemplates, function (template, cb) {
var validated = [];
Object.keys(template).forEach(function (templateString) {
var match = OBJECTS_TO_VALIDATE.filter(function (element) {
BundleValidator.prototype.validateObjects = (assembledTemplates, callback) => {
Async.map(assembledTemplates, (template, cb) => {
let validated = [];
Object.keys(template).forEach(templateString => {
const match = OBJECTS_TO_VALIDATE.filter(function (element) {
return template[templateString].search(new RegExp('\{+\\s*' + element + '\\s*}+')) !== -1;
});

Expand All @@ -292,31 +324,25 @@ BundleValidator.prototype.validateObjects = function (assembledTemplates, callba


function isValidImageType(imagePath) {
var ext = Path.extname(imagePath);
const ext = Path.extname(imagePath);
return _.includes(VALID_IMAGE_TYPES, ext);
}

function validateImage(path, width, height, cb) {
var MAX_SIZE_COMPOSED = 1048576 * 2; //2MB
var MAX_SIZE_MOBILE = 1048576; //1MB
var MAX_SIZE_DESKTOP = 1048576 * 5; //5MB

this.sizeOf(path, function (err, dimensions) {
var failureMessage = '';
var imageWidth;
var imageHeight;
var stats;
var size;
const MAX_SIZE_COMPOSED = 1048576 * 2; //2MB
const MAX_SIZE_MOBILE = 1048576; //1MB
const MAX_SIZE_DESKTOP = 1048576 * 5; //5MB

this.sizeOf(path, (err, dimensions) => {
if (err) {
return cb(err);
}

imageHeight = dimensions.height;
imageWidth = dimensions.width;

stats = Fs.statSync(path);
size = stats['size'];
let failureMessage = '';
const imageHeight = dimensions.height;
const imageWidth = dimensions.width;
const stats = Fs.statSync(path);
const size = stats['size'];

if (width === WIDTH_DESKTOP && height === HEIGHT_DESKTOP && size > MAX_SIZE_DESKTOP) {
failureMessage = 'Image of size ' + size + ' bytes at path (' + path + ') '
Expand Down
22 changes: 22 additions & 0 deletions lib/bundle-validator.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -100,4 +100,26 @@ describe('BundleValidator', function () {
done();
});
});

it ('should validate theme schema successfully', function (done) {
const validator = new BundleValidator(themePath, themeConfig, false);
validator.validateTheme(error => {
expect(error).to.be.null();
done();
});
});

it ('should validate theme schema and throw errors', function (done) {
const themePath = Path.join(process.cwd(), 'test/_mocks/themes/invalid-schema');
themeConfig = ThemeConfig.getInstance(themePath);
themeConfig.getConfig();

const validator = new BundleValidator(themePath, themeConfig, false);

validator.validateTheme(error => {
expect(error instanceof Error).to.be.true();
expect(error.message).to.contain('schema[0].settings[0] should have required property \'content\'');
done();
});
});
});
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
{
"$schema": "http://json-schema.org/draft-04/schema#",
"id": "http://themes.bigcommerce.com/theme_packages/config",
"id": "http://themes.bigcommerce.com/theme_packages/privateThemeConfig",
"type": "object",
"properties": {
"name": {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
{
"$schema": "http://json-schema.org/draft-04/schema#",
"$id": "http://json-schema.org/draft-04/schema#",
"$id": "http://themes.bigcommerce.com/theme_packages/editorSchemaTranslations",
"title": "Theme translations",
"description": "Translations of strings in schema.json file of a theme",
"type": "object",
Expand Down
3 changes: 1 addition & 2 deletions lib/themeConfig.schema.json → lib/schemas/themeConfig.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
{
"$schema": "http://json-schema.org/draft-04/schema#",
"id": "http://themes.bigcommerce.com/theme_packages/config",
"id": "http://themes.bigcommerce.com/theme_packages/themeConfig",
"type": "object",
"properties": {
"name": {
Expand Down
Loading

0 comments on commit b86d440

Please sign in to comment.