Add readThemes() utility to get a list of themes

refs #5923
- add read-themes module to get a list of themes
- replace readDirectory() usage with readThemes(), where only themes are needed
- test read-themes
- test read-directory
- test validate-themes
- test parse-package-json
- add tempfile testing utility to generate temporary paths
This commit is contained in:
vdemedes 2015-10-13 13:55:24 +02:00
parent 61c3e92d55
commit 05f44c4c64
7 changed files with 404 additions and 63 deletions

View File

@ -10,6 +10,7 @@ var path = require('path'),
knex = require('knex'),
validator = require('validator'),
readDirectory = require('../utils/read-directory'),
readThemes = require('../utils/read-themes'),
errors = require('../errors'),
configUrl = require('./url'),
packageInfo = require('../../../package.json'),
@ -75,7 +76,7 @@ ConfigManager.prototype.init = function (rawConfig) {
// just the object appropriate for this NODE_ENV
self.set(rawConfig);
return Promise.all([readDirectory(self._config.paths.themePath), readDirectory(self._config.paths.appPath)]).then(function (paths) {
return Promise.all([readThemes(self._config.paths.themePath), readDirectory(self._config.paths.appPath)]).then(function (paths) {
self._config.paths.availableThemes = paths[0];
self._config.paths.availableApps = paths[1];
return self._config;

View File

@ -4,7 +4,7 @@ var schema = require('../schema').tables,
Promise = require('bluebird'),
errors = require('../../errors'),
config = require('../../config'),
readDirectory = require('../../utils/read-directory'),
readThemes = require('../../utils/read-themes'),
validateSchema,
validateSettings,
@ -112,7 +112,7 @@ validateActiveTheme = function validateActiveTheme(themeName) {
// A Promise that will resolve to an object with a property for each installed theme.
// This is necessary because certain configuration data is only available while Ghost
// is running and at times the validations are used when it's not (e.g. tests)
availableThemes = readDirectory(config.paths.themePath);
availableThemes = readThemes(config.paths.themePath);
}
return availableThemes.then(function then(themes) {

View File

@ -22,7 +22,7 @@ function readDirectory(dir, options) {
}
ignore = options.ignore || [];
ignore.push('node_modules', 'bower_components');
ignore.push('node_modules', 'bower_components', '.DS_Store');
return readDir(dir)
.filter(function (filename) {

View File

@ -0,0 +1,46 @@
/**
* Dependencies
*/
var readDirectory = require('./read-directory'),
Promise = require('bluebird'),
join = require('path').join,
fs = require('fs'),
statFile = Promise.promisify(fs.stat);
/**
* Read themes
*/
function readThemes(dir) {
var originalTree;
return readDirectory(dir)
.tap(function (tree) {
originalTree = tree;
})
.then(Object.keys)
.filter(function (file) {
var path = join(dir, file);
return statFile(path).then(function (stat) {
return stat.isDirectory();
});
})
.then(function (directories) {
var themes = {};
directories.forEach(function (name) {
themes[name] = originalTree[name];
});
return themes;
});
}
/**
* Expose `read-themes`
*/
module.exports = readThemes;

View File

@ -2,7 +2,7 @@
* Dependencies
*/
var readDirectory = require('./read-directory'),
var readThemes = require('./read-themes'),
Promise = require('bluebird'),
_ = require('lodash');
@ -18,7 +18,7 @@ function validateThemes(dir) {
errors: []
};
return readDirectory(dir)
return readThemes(dir)
.tap(function (themes) {
_.each(themes, function (theme, name) {
var hasPackageJson, warning;

View File

@ -1,81 +1,352 @@
/*globals describe, it*/
/*jshint expr:true*/
var should = require('should'),
utils = require('../../server/utils');
parsePackageJson = require('../../server/utils/parse-package-json'),
validateThemes = require('../../server/utils/validate-themes'),
readDirectory = require('../../server/utils/read-directory'),
readThemes = require('../../server/utils/read-themes'),
tempfile = require('../utils/tempfile'),
utils = require('../../server/utils'),
join = require('path').join,
fs = require('fs');
// To stop jshint complaining
should.equal(true, true);
describe('Safe String', function () {
var safeString = utils.safeString,
options = {};
describe('Server Utilities', function () {
describe('Safe String', function () {
var safeString = utils.safeString,
options = {};
it('should remove beginning and ending whitespace', function () {
var result = safeString(' stringwithspace ', options);
result.should.equal('stringwithspace');
it('should remove beginning and ending whitespace', function () {
var result = safeString(' stringwithspace ', options);
result.should.equal('stringwithspace');
});
it('should remove non ascii characters', function () {
var result = safeString('howtowin✓', options);
result.should.equal('howtowin');
});
it('should replace spaces with dashes', function () {
var result = safeString('how to win', options);
result.should.equal('how-to-win');
});
it('should replace most special characters with dashes', function () {
var result = safeString('a:b/c?d#e[f]g!h$i&j(k)l*m+n,o;p=q\\r%s<t>u|v^w~x£y"z@1.2', options);
result.should.equal('a-b-c-d-e-f-g-h-i-j-k-l-m-n-o-p-q-r-s-t-u-v-w-x-y-z-1-2');
});
it('should remove special characters at the beginning of a string', function () {
var result = safeString('.Not special', options);
result.should.equal('not-special');
});
it('should remove apostrophes ', function () {
var result = safeString('how we shouldn\'t be', options);
result.should.equal('how-we-shouldnt-be');
});
it('should convert to lowercase', function () {
var result = safeString('This has Upper Case', options);
result.should.equal('this-has-upper-case');
});
it('should convert multiple dashes into a single dash', function () {
var result = safeString('This :) means everything', options);
result.should.equal('this-means-everything');
});
it('should remove trailing dashes from the result', function () {
var result = safeString('This.', options);
result.should.equal('this');
});
it('should handle pound signs', function () {
var result = safeString('WHOOPS! I spent all my £ again!', options);
result.should.equal('whoops-i-spent-all-my-again');
});
it('should properly handle unicode punctuation conversion', function () {
var result = safeString('に間違いがないか、再度確認してください。再読み込みしてください。', options);
result.should.equal('nijian-wei-iganaika-zai-du-que-ren-sitekudasai-zai-du-miip-misitekudasai');
});
it('should not lose or convert dashes if options are passed with truthy importing flag', function () {
var result,
options = {importing: true};
result = safeString('-slug-with-starting-ending-and---multiple-dashes-', options);
result.should.equal('-slug-with-starting-ending-and---multiple-dashes-');
});
it('should still remove/convert invalid characters when passed options with truthy importing flag', function () {
var result,
options = {importing: true};
result = safeString('-slug-&with-✓-invalid-characters-に\'', options);
result.should.equal('-slug--with--invalid-characters-ni');
});
});
it('should remove non ascii characters', function () {
var result = safeString('howtowin✓', options);
result.should.equal('howtowin');
describe('parse-package-json', function () {
it('should parse valid package.json', function (done) {
var pkgJson, tmpPath;
tmpPath = tempfile();
pkgJson = JSON.stringify({
name: 'test',
version: '0.0.0'
});
fs.writeFileSync(tmpPath, pkgJson);
parsePackageJson(tmpPath)
.then(function (pkg) {
pkg.should.eql({
name: 'test',
version: '0.0.0'
});
done();
})
.catch(done);
});
it('should fail when name is missing', function (done) {
var pkgJson, tmpPath;
tmpPath = tempfile();
pkgJson = JSON.stringify({
version: '0.0.0'
});
fs.writeFileSync(tmpPath, pkgJson);
parsePackageJson(tmpPath)
.then(function () {
done(new Error('parsePackageJson succeeded, but should\'ve failed'));
})
.catch(function (err) {
err.message.should.equal('"name" or "version" is missing from theme package.json file.');
err.context.should.equal(tmpPath);
err.help.should.equal('This will be required in future. Please see http://docs.ghost.org/themes/');
done();
});
});
it('should fail when version is missing', function (done) {
var pkgJson, tmpPath;
tmpPath = tempfile();
pkgJson = JSON.stringify({
name: 'test'
});
fs.writeFileSync(tmpPath, pkgJson);
parsePackageJson(tmpPath)
.then(function () {
done(new Error('parsePackageJson succeeded, but should\'ve failed'));
})
.catch(function (err) {
err.message.should.equal('"name" or "version" is missing from theme package.json file.');
err.context.should.equal(tmpPath);
err.help.should.equal('This will be required in future. Please see http://docs.ghost.org/themes/');
done();
});
});
it('should fail when JSON is invalid', function (done) {
var pkgJson, tmpPath;
tmpPath = tempfile();
pkgJson = '{name:"test"}';
fs.writeFileSync(tmpPath, pkgJson);
parsePackageJson(tmpPath)
.then(function () {
done(new Error('parsePackageJson succeeded, but should\'ve failed'));
})
.catch(function (err) {
err.message.should.equal('Theme package.json file is malformed');
err.context.should.equal(tmpPath);
err.help.should.equal('This will be required in future. Please see http://docs.ghost.org/themes/');
done();
});
});
it('should fail when file is missing', function (done) {
var tmpPath = tempfile();
parsePackageJson(tmpPath)
.then(function () {
done(new Error('parsePackageJson succeeded, but should\'ve failed'));
})
.catch(function (err) {
err.message.should.equal('Could not read package.json file');
err.context.should.equal(tmpPath);
done();
});
});
});
it('should replace spaces with dashes', function () {
var result = safeString('how to win', options);
result.should.equal('how-to-win');
describe('read-directory', function () {
it('should read directory recursively', function (done) {
var themePath = tempfile();
// create example theme
fs.mkdirSync(themePath);
fs.mkdirSync(join(themePath, 'partials'));
fs.writeFileSync(join(themePath, 'index.hbs'));
fs.writeFileSync(join(themePath, 'partials', 'navigation.hbs'));
readDirectory(themePath)
.then(function (tree) {
tree.should.eql({
partials: {
'navigation.hbs': join(themePath, 'partials', 'navigation.hbs')
},
'index.hbs': join(themePath, 'index.hbs')
});
done();
})
.catch(done);
});
it('should read directory and ignore unneeded items', function (done) {
var themePath = tempfile();
// create example theme
fs.mkdirSync(themePath);
fs.mkdirSync(join(themePath, 'partials'));
fs.writeFileSync(join(themePath, 'index.hbs'));
fs.writeFileSync(join(themePath, 'partials', 'navigation.hbs'));
// create some trash
fs.mkdirSync(join(themePath, 'node_modules'));
fs.mkdirSync(join(themePath, 'bower_components'));
fs.mkdirSync(join(themePath, '.git'));
fs.writeFileSync(join(themePath, '.DS_Store'));
readDirectory(themePath, {ignore: ['.git']})
.then(function (tree) {
tree.should.eql({
partials: {
'navigation.hbs': join(themePath, 'partials', 'navigation.hbs')
},
'index.hbs': join(themePath, 'index.hbs')
});
done();
})
.catch(done);
});
it('should read directory and parse package.json files', function (done) {
var themePath, pkgJson;
themePath = tempfile();
pkgJson = JSON.stringify({
name: 'test',
version: '0.0.0'
});
// create example theme
fs.mkdirSync(themePath);
fs.mkdirSync(join(themePath, 'partials'));
fs.writeFileSync(join(themePath, 'package.json'), pkgJson);
fs.writeFileSync(join(themePath, 'index.hbs'));
fs.writeFileSync(join(themePath, 'partials', 'navigation.hbs'));
readDirectory(themePath)
.then(function (tree) {
tree.should.eql({
partials: {
'navigation.hbs': join(themePath, 'partials', 'navigation.hbs')
},
'index.hbs': join(themePath, 'index.hbs'),
'package.json': {
name: 'test',
version: '0.0.0'
}
});
done();
})
.catch(done);
});
});
it('should replace most special characters with dashes', function () {
var result = safeString('a:b/c?d#e[f]g!h$i&j(k)l*m+n,o;p=q\\r%s<t>u|v^w~x£y"z@1.2', options);
result.should.equal('a-b-c-d-e-f-g-h-i-j-k-l-m-n-o-p-q-r-s-t-u-v-w-x-y-z-1-2');
describe('read-themes', function () {
it('should read directory and include only folders', function (done) {
var themesPath = tempfile();
fs.mkdirSync(themesPath);
// create trash
fs.writeFileSync(join(themesPath, 'casper.zip'));
fs.writeFileSync(join(themesPath, '.DS_Store'));
// create actual theme
fs.mkdirSync(join(themesPath, 'casper'));
fs.mkdirSync(join(themesPath, 'casper', 'partials'));
fs.writeFileSync(join(themesPath, 'casper', 'index.hbs'));
fs.writeFileSync(join(themesPath, 'casper', 'partials', 'navigation.hbs'));
readThemes(themesPath)
.then(function (tree) {
tree.should.eql({
casper: {
partials: {
'navigation.hbs': join(themesPath, 'casper', 'partials', 'navigation.hbs')
},
'index.hbs': join(themesPath, 'casper', 'index.hbs')
}
});
done();
})
.catch(done);
});
});
it('should remove special characters at the beginning of a string', function () {
var result = safeString('.Not special', options);
result.should.equal('not-special');
});
describe('validate-themes', function () {
it('should return warnings for themes without package.json', function (done) {
var themesPath, pkgJson;
it('should remove apostrophes ', function () {
var result = safeString('how we shouldn\'t be', options);
result.should.equal('how-we-shouldnt-be');
});
themesPath = tempfile();
pkgJson = JSON.stringify({
name: 'casper',
version: '1.0.0'
});
it('should convert to lowercase', function () {
var result = safeString('This has Upper Case', options);
result.should.equal('this-has-upper-case');
});
fs.mkdirSync(themesPath);
it('should convert multiple dashes into a single dash', function () {
var result = safeString('This :) means everything', options);
result.should.equal('this-means-everything');
});
fs.mkdirSync(join(themesPath, 'casper'));
fs.mkdirSync(join(themesPath, 'invalid-casper'));
it('should remove trailing dashes from the result', function () {
var result = safeString('This.', options);
result.should.equal('this');
});
fs.writeFileSync(join(themesPath, 'casper', 'package.json'), pkgJson);
it('should handle pound signs', function () {
var result = safeString('WHOOPS! I spent all my £ again!', options);
result.should.equal('whoops-i-spent-all-my-again');
});
validateThemes(themesPath)
.then(function () {
done(new Error('validateThemes succeeded, but should\'ve failed'));
})
.catch(function (result) {
result.errors.length.should.equal(0);
result.warnings.should.eql([{
message: 'Found a theme with no package.json file',
context: 'Theme name: invalid-casper',
help: 'This will be required in future. Please see http://docs.ghost.org/themes/'
}]);
it('should properly handle unicode punctuation conversion', function () {
var result = safeString('に間違いがないか、再度確認してください。再読み込みしてください。', options);
result.should.equal('nijian-wei-iganaika-zai-du-que-ren-sitekudasai-zai-du-miip-misitekudasai');
});
it('should not lose or convert dashes if options are passed with truthy importing flag', function () {
var result,
options = {importing: true};
result = safeString('-slug-with-starting-ending-and---multiple-dashes-', options);
result.should.equal('-slug-with-starting-ending-and---multiple-dashes-');
});
it('should still remove/convert invalid characters when passed options with truthy importing flag', function () {
var result,
options = {importing: true};
result = safeString('-slug-&with-✓-invalid-characters-に\'', options);
result.should.equal('-slug--with--invalid-characters-ni');
done();
});
});
});
});

View File

@ -0,0 +1,23 @@
/**
* Dependencies
*/
var join = require('path').join,
TMP_DIR = require('os').tmpdir();
/**
* Generate a temporary file path
*/
function tempfile() {
var randomString = Math.random().toString(36).substring(7);
return join(TMP_DIR, randomString);
}
/**
* Expose `tempfile`
*/
module.exports = tempfile;