Added package.js support for themes (and plugins)

Closes #2081
* Amended require-tree to populate availableThemes and availablePlugins to use full file names (`basename.ext`) as keys instead of just basename. This way `image.jpg`, `image.png`, `image.gif` won't overwrite the `image` key.
* Amended require-tree to allow package.json file parsing to return the contents of the file as json on the `package.json` key.
* settings api populates theme data `package` if it exists. Otherwise it assigns `false` to it
* `general.hbs` (salute) was reworked to if there is the package key on the theme is not false, it will use the `name` and `version` keys of that. You can break it by not having a `name` or `version` in the package.json file.
* Added error and warning messages for package.json file parse errors and misses
This commit is contained in:
Gabor Javorszky 2014-02-08 21:16:58 +00:00
parent 6b82bebd90
commit 212711d896
6 changed files with 66 additions and 18 deletions

View File

@ -69,7 +69,8 @@
<label for="activeTheme">Theme</label>
<select id="activeTheme" name="general[activeTheme]">
{{#each availableThemes}}
<option value="{{name}}" {{#if active}}selected{{/if}}>{{name}}</option>
<option value="{{name}}" {{#if active}}selected{{/if}}>{{#if package}}{{package.name}} - {{package.version}}{{else}}{{name}}{{/if}}</option>
{{#unless package}}<script>console.log('Hi! The theme named "{{name}}" does not have a package.json file or it\' malformed. This will be required in the future. For more info, see http://docs.ghost.org/themes/.');</script>{{/unless}}
{{/each}}
</select>
<p>Select a theme for your blog</p>

View File

@ -83,10 +83,15 @@ readSettingsResult = function (result) {
i,
item;
for (i = 0; i < themeKeys.length; i += 1) {
//do not include hidden files
if (themeKeys[i].indexOf('.') !== 0) {
//do not include hidden files or _messages
if (themeKeys[i].indexOf('.') !== 0 && themeKeys[i] !== '_messages') {
item = {};
item.name = themeKeys[i];
if (themes[themeKeys[i]].hasOwnProperty('package.json')) {
item.package = themes[themeKeys[i]]['package.json'];
} else {
item.package = false;
}
//data about files currently not used
//item.details = themes[themeKeys[i]];
if (themeKeys[i] === settings.activeTheme.value) {

View File

@ -118,7 +118,7 @@ frontendControllers = {
filters.doFilter('prePostsRender', post).then(function (post) {
api.settings.read('activeTheme').then(function (activeTheme) {
var paths = config().paths.availableThemes[activeTheme.value],
view = post.page && paths.hasOwnProperty('page') ? 'page' : 'post';
view = post.page && paths.hasOwnProperty('page.hbs') ? 'page' : 'post';
res.render(view, {post: post});
});
});

View File

@ -71,7 +71,7 @@ function initDbHashAndFirstRun() {
}
// Checks for the existence of the "built" javascript files from grunt concat.
// Returns a promise that will be resolved if all files exist or rejected if
// Returns a promise that will be resolved if all files exist or rejected if
// any are missing.
function builtFilesExist() {
var deferreds = [],
@ -262,7 +262,12 @@ function setup(server) {
startGhost
);
}
_.each(config().paths.availableThemes._messages.errors, function (error) {
errors.logError(error.message, error.context);
});
_.each(config().paths.availableThemes._messages.warns, function (warn) {
errors.logWarn(warn.message, warn.context);
});
});
}, function (err) {
errors.logErrorAndExit(err, err.context, err.help);

View File

@ -1,8 +1,10 @@
var when = require('when'),
keys = require('when/keys'),
fs = require('fs'),
path = require('path'),
extend = function (obj, source) {
var when = require('when'),
keys = require('when/keys'),
fs = require('fs'),
path = require('path'),
_ = require('lodash'),
messages = {errors: [], warns: []},
extend = function (obj, source) {
var key;
for (key in source) {
if (source.hasOwnProperty(key)) {
@ -11,6 +13,32 @@ var when = require('when'),
}
return obj;
},
parsePackageJson = function (path) {
var packageDeferred = when.defer(),
packagePromise = packageDeferred.promise,
jsonContainer;
fs.readFile(path, function (error, data) {
if (error) {
messages.errors.push({message: 'Could not read package.json file', context: path});
packageDeferred.resolve(false);
return;
}
try {
jsonContainer = JSON.parse(data);
if (jsonContainer.hasOwnProperty('name') && jsonContainer.hasOwnProperty('version')) {
packageDeferred.resolve(jsonContainer);
} else {
messages.errors.push({message: '"name" or "version" is missing from theme package.json file.', context: path});
packageDeferred.resolve(false);
}
} catch (e) {
messages.errors.push({message: 'Theme package.json file is malformed', context: path});
packageDeferred.resolve(false);
}
});
return when(packagePromise);
},
readDir = function (dir, options, depth) {
depth = depth || 0;
@ -36,14 +64,14 @@ var when = require('when'),
files.forEach(function (file) {
var fileDeferred = when.defer(),
filePromise = fileDeferred.promise,
ext = path.extname(file),
name = path.basename(file, ext),
fpath = path.join(dir, file);
subtree[name] = filePromise;
subtree[file] = filePromise;
fs.lstat(fpath, function (error, result) {
/*jslint unparam:true*/
if (result.isDirectory()) {
fileDeferred.resolve(readDir(fpath, options, depth + 1));
} else if (depth === 1 && file === "package.json") {
fileDeferred.resolve(parsePackageJson(fpath));
} else {
fileDeferred.resolve(fpath);
}
@ -61,6 +89,15 @@ var when = require('when'),
},
readAll = function (dir, options, depth) {
return when(readDir(dir, options, depth)).then(function (paths) {
// for all contents of the dir, I'm interested in the ones that are directories and within /theme/
if (typeof paths === "object" && dir.indexOf('theme') !== -1) {
_.each(paths, function (path, index) {
if (typeof path === 'object' && !path.hasOwnProperty('package.json') && index.indexOf('.') !== 0) {
messages.warns.push({message: 'Theme does not have a package.json file', context: index});
}
});
}
paths._messages = messages;
return paths;
});
};

View File

@ -195,10 +195,10 @@ describe('Frontend Controller', function () {
'availableThemes': {
'casper': {
'assets': null,
'default': '/content/themes/casper/default.hbs',
'index': '/content/themes/casper/index.hbs',
'page': '/content/themes/casper/page.hbs',
'post': '/content/themes/casper/post.hbs'
'default.hbs': '/content/themes/casper/default.hbs',
'index.hbs': '/content/themes/casper/index.hbs',
'page.hbs': '/content/themes/casper/page.hbs',
'post.hbs': '/content/themes/casper/post.hbs'
}
}
}