Refactored helper registration code into a service

- The helper registration code is "framework" code and very specific
- At the moment the "theme engine" is full of lots of disparate theme related stuff
- I'm trying to make the frontend framework code clearer and also expand it to  make it more useful
- The helper system now also exposes 3 methods allowing you to register a directory, a helper or an alias
- I've updated the codebase to use these both for our core helpers and for "apps"
This commit is contained in:
Hannah Wolfe 2021-10-04 16:50:07 +01:00
parent 554f36de55
commit 9d7049cd3f
No known key found for this signature in database
GPG Key ID: 9F8C7532D0A6BA55
19 changed files with 116 additions and 124 deletions

View File

@ -111,10 +111,10 @@ async function initCore({ghostServer, config}) {
async function initServicesForFrontend() {
debug('Begin: initServicesForFrontend');
debug('Begin: Frontend Routing Settings');
debug('Begin: Routing Settings');
const routeSettings = require('./server/services/route-settings');
await routeSettings.init();
debug('End: Frontend Routing Settings');
debug('End: Routing Settings');
debug('Begin: Themes');
const themeService = require('./server/services/themes');
@ -124,6 +124,18 @@ async function initServicesForFrontend() {
debug('End: initServicesForFrontend');
}
/**
* Frontend is intended to be just Ghost's frontend
*/
async function initFrontend() {
debug('Begin: initFrontend');
const helperService = require('./frontend/services/helpers');
await helperService.init();
debug('End: initFrontend');
}
/**
* At the moment we load our express apps all in one go, they require themselves and are co-located
* What we want is to be able to optionally load various components and mount them
@ -331,6 +343,7 @@ async function bootGhost() {
debug('Begin: Load Ghost Services & Apps');
await initCore({ghostServer, config});
await initServicesForFrontend();
await initFrontend();
const ghostApp = await initExpressApps();
await initDynamicRouting();
await initServices({config});

View File

@ -1,5 +1,6 @@
const path = require('path');
const router = require('./lib/router');
const registerHelpers = require('./lib/helpers');
const urlUtils = require('../../../shared/url-utils');
// Dirty requires
@ -22,7 +23,8 @@ module.exports = {
let ampRoute = '*/amp/';
ghost.routeService.registerRouter(ampRoute, ampRouter);
registerHelpers(ghost);
ghost.helperService.registerDir(path.resolve(__dirname, './lib/helpers'));
// we use the {{ghost_head}} helper, but call it {{amp_ghost_head}}, so it's consistent
ghost.helperService.registerAlias('amp_ghost_head', 'ghost_head');
}
};

View File

@ -1,18 +0,0 @@
// Dirty require!
const ghostHead = require('../../../../helpers/ghost_head');
function registerAmpHelpers(ghost) {
ghost.helpers.registerAsync('amp_content', require('./amp_content'));
ghost.helpers.register('amp_components', require('./amp_components'));
ghost.helpers.register('amp_analytics', require('./amp_analytics'));
// we use the {{ghost_head}} helper, but call it {{amp_ghost_head}}, so it's consistent
ghost.helpers.registerAsync('amp_ghost_head', ghostHead);
// additional injected styles for use inside the single <style amp-custom> tag
ghost.helpers.register('amp_style', require('./amp_style'));
}
module.exports = registerAmpHelpers;

View File

@ -1,10 +1,10 @@
const path = require('path');
const tpl = require('@tryghost/tpl');
const logging = require('@tryghost/logging');
const errors = require('@tryghost/errors');
const urlUtils = require('../../../shared/url-utils');
const middleware = require('./lib/middleware');
const router = require('./lib/router');
const registerHelpers = require('./lib/helpers');
const messages = {
urlCannotContainPrivateSubdir: {
@ -43,8 +43,7 @@ module.exports = {
checkSubdir();
ghost.routeService.registerRouter(privateRoute, router);
registerHelpers(ghost);
ghost.helperService.registerDir(path.resolve(__dirname, './lib/helpers'));
},
setupMiddleware: function setupMiddleware(siteApp) {

View File

@ -1,3 +0,0 @@
module.exports = function registerHelpers(ghost) {
ghost.helpers.register('input_password', require('./input_password'));
};

View File

@ -13,7 +13,7 @@ const Promise = require('bluebird');
const jsonpath = require('jsonpath');
const messages = {
mustBeCalledAsBlock: 'The {{{helperName}}} helper must be called as a block. E.g. {{#{helperName}}}...{{/{helperName}}}',
mustBeCalledAsBlock: 'The {\\{{helperName}}} helper must be called as a block. E.g. {{#{helperName}}}...{{/{helperName}}}',
invalidResource: 'Invalid resource given to get helper'
};
@ -194,3 +194,5 @@ module.exports = function get(resource, options) {
}
});
};
module.exports.async = true;

View File

@ -229,3 +229,5 @@ module.exports = function ghost_head(options) { // eslint-disable-line camelcase
return new SafeString(head.join('\n ').trim());
});
};
module.exports.async = true;

View File

@ -12,7 +12,7 @@ const Promise = require('bluebird');
const moment = require('moment');
const messages = {
mustBeCalledAsBlock: 'The {{{helperName}}} helper must be called as a block. E.g. {{#{helperName}}}...{{/{helperName}}}'
mustBeCalledAsBlock: 'The {\\{{helperName}}} helper must be called as a block. E.g. {{#{helperName}}}...{{/{helperName}}}'
};
const createFrame = hbs.handlebars.createFrame;
@ -102,3 +102,6 @@ module.exports = function prevNext(options) {
// With the guards out of the way, attempt to build the apiOptions, and then fetch the data
return fetch.call(this, options, data);
};
module.exports.async = true;
module.exports.alias = 'next_post';

View File

@ -1,13 +1,14 @@
const helpers = require('../../services/theme-engine/handlebars/register');
const helperService = require('../../services/helpers');
const routingService = require('../../services/routing');
module.exports.getInstance = function getInstance() {
const appRouter = routingService.registry.getRouter('appRouter');
return {
helpers: {
register: helpers.registerThemeHelper.bind(helpers),
registerAsync: helpers.registerAsyncThemeHelper.bind(helpers)
helperService: {
registerAlias: helperService.registerAlias.bind(helperService),
registerHelper: helperService.registerHelper.bind(helperService),
registerDir: helperService.registerDir.bind(helperService)
},
// Expose the route service...
routeService: {

View File

@ -1,9 +1,9 @@
const Promise = require('bluebird');
const errors = require('@tryghost/errors');
const hbs = require('../../theme-engine/engine');
const config = require('../../../../shared/config');
const logging = require('@tryghost/logging');
const {hbs} = require('../rendering');
// Register an async handlebars helper for a given handlebars instance
function asyncHelperWrapper(hbsInstance, name, fn) {
hbsInstance.registerAsyncHelper(name, function returnAsync(context, options, cb) {
@ -13,7 +13,7 @@ function asyncHelperWrapper(hbsInstance, name, fn) {
options = undefined;
}
// Wrap the function passed in with a when.resolve so it can return either a promise or a value
// Wrap the function passed in with a Promise.resolve so it can return either a promise or a value
Promise.resolve(fn.call(this, context, options)).then(function asyncHelperSuccess(result) {
cb(result);
}).catch(function asyncHelperError(err) {
@ -25,7 +25,7 @@ function asyncHelperWrapper(hbsInstance, name, fn) {
}
});
const result = config.get('env') === 'development' ? wrappedErr : '';
const result = process.env.NODE_ENV === 'development' ? wrappedErr : '';
logging.error(wrappedErr);

View File

@ -0,0 +1,18 @@
const registry = require('./registry');
const path = require('path');
// Initialise Ghost's own helpers
// This is a weird place for this to live!
const init = async () => {
const helperPath = path.join(__dirname, '../../', 'helpers');
return await registry.registerDir(helperPath);
};
// Oh look! A framework for helpers :D
module.exports = {
registerAlias: registry.registerAlias,
registerDir: registry.registerDir,
registerHelper: registry.registerHelper,
init
};

View File

@ -0,0 +1,45 @@
const glob = require('glob');
const path = require('path');
const handlebars = require('./handlebars');
// Internal Cache
const registry = {};
const registerHelper = (name, helperFn) => {
if (registry[name]) {
return;
}
registry[name] = helperFn;
if (helperFn.async) {
handlebars.registerAsyncThemeHelper(name, helperFn);
} else {
handlebars.registerThemeHelper(name, helperFn);
}
};
const registerDir = (helperPath) => {
let helperFiles = glob.sync('!(index).js', {cwd: helperPath});
helperFiles.forEach((helper) => {
const name = helper.replace(/.js$/, '');
const fn = require(path.join(helperPath, helper));
registerHelper(name, fn);
if (fn.alias) {
registerHelper(fn.alias, fn);
}
});
};
const registerAlias = (alias, name) => {
registerHelper(alias, registry[name]);
};
module.exports = {
registerAlias,
registerHelper,
registerDir
};

View File

@ -2,7 +2,7 @@
* This is a loose concept of a frontend rendering framework
* Note: everything here gets deep-required from the theme-engine
* This indicates that the theme engine is a set of services, rather than a single service
* and could do with a refactor.
* and could do with a further refactor.
*
* This at least keeps the deep requires in a single place.
*/

View File

@ -1,54 +0,0 @@
const register = require('./register');
const loader = require('./loader');
const coreHelpers = loader.getHelpers();
const registerThemeHelper = register.registerThemeHelper;
const registerAsyncThemeHelper = register.registerAsyncThemeHelper;
const registerAllCoreHelpers = function registerAllCoreHelpers() {
// Register theme helpers
registerThemeHelper('asset', coreHelpers.asset);
registerThemeHelper('author', coreHelpers.author);
registerThemeHelper('authors', coreHelpers.authors);
registerThemeHelper('body_class', coreHelpers.body_class);
registerThemeHelper('cancel_link', coreHelpers.cancel_link);
registerThemeHelper('concat', coreHelpers.concat);
registerThemeHelper('content', coreHelpers.content);
registerThemeHelper('products', coreHelpers.products);
registerThemeHelper('date', coreHelpers.date);
registerThemeHelper('encode', coreHelpers.encode);
registerThemeHelper('excerpt', coreHelpers.excerpt);
registerThemeHelper('foreach', coreHelpers.foreach);
registerThemeHelper('ghost_foot', coreHelpers.ghost_foot);
registerThemeHelper('has', coreHelpers.has);
registerThemeHelper('is', coreHelpers.is);
registerThemeHelper('img_url', coreHelpers.img_url);
registerThemeHelper('lang', coreHelpers.lang);
registerThemeHelper('link', coreHelpers.link);
registerThemeHelper('link_class', coreHelpers.link_class);
registerThemeHelper('match', coreHelpers.match);
registerThemeHelper('meta_description', coreHelpers.meta_description);
registerThemeHelper('meta_title', coreHelpers.meta_title);
registerThemeHelper('navigation', coreHelpers.navigation);
registerThemeHelper('page_url', coreHelpers.page_url);
registerThemeHelper('pagination', coreHelpers.pagination);
registerThemeHelper('plural', coreHelpers.plural);
registerThemeHelper('post_class', coreHelpers.post_class);
registerThemeHelper('price', coreHelpers.price);
registerThemeHelper('raw', coreHelpers.raw);
registerThemeHelper('reading_time', coreHelpers.reading_time);
registerThemeHelper('t', coreHelpers.t);
registerThemeHelper('tags', coreHelpers.tags);
registerThemeHelper('title', coreHelpers.title);
registerThemeHelper('twitter_url', coreHelpers.twitter_url);
registerThemeHelper('facebook_url', coreHelpers.facebook_url);
registerThemeHelper('url', coreHelpers.url);
// Async theme helpers
registerAsyncThemeHelper('ghost_head', coreHelpers.ghost_head);
registerAsyncThemeHelper('next_post', coreHelpers.prev_post);
registerAsyncThemeHelper('prev_post', coreHelpers.prev_post);
registerAsyncThemeHelper('get', coreHelpers.get);
};
module.exports = coreHelpers;
module.exports.loadCoreHelpers = registerAllCoreHelpers;

View File

@ -1,19 +0,0 @@
const glob = require('glob');
const path = require('path');
const helperPath = path.join(__dirname, '../../../', 'helpers');
module.exports.getHelpers = () => {
const helpers = {};
// We use glob here because it's already a dependency
// If we want to get rid of glob we could use E.g. requiredir
// Or require('fs').readdirSync(__dirname + '/')
let helperFiles = glob.sync('!(index).js', {cwd: helperPath});
helperFiles.forEach((helper) => {
let name = helper.replace(/.js$/, '');
helpers[name] = require(path.join(helperPath, helper));
});
return helpers;
};

View File

@ -3,6 +3,5 @@ const active = require('./active');
module.exports = {
getActive: active.get,
setActive: active.set,
loadCoreHelpers: require('./handlebars/helpers').loadCoreHelpers,
middleware: require('./middleware')
};

View File

@ -114,8 +114,9 @@ module.exports = function setupSiteApp(options = {}) {
// We do this here, at the top level, because helpers require so much stuff.
// Moving this to being inside themes, where it probably should be requires the proxy to be refactored
// Else we end up with circular dependencies
themeEngine.loadCoreHelpers();
debug('Helpers done');
// themeEngine.loadCoreHelpers();
// themeEngine.registerHandlebarsHelpers();
// debug('Helpers done');
// Global handling for member session, ensures a member is logged in to the frontend
siteApp.use(membersService.middleware.loadMemberSession);

View File

@ -1,6 +1,6 @@
const should = require('should');
const sinon = require('sinon');
const helpers = require('../../../../core/frontend/services/theme-engine/handlebars/register');
const helpers = require('../../../../core/frontend/services/helpers');
const AppProxy = require('../../../../core/frontend/services/apps/proxy');
const routing = require('../../../../core/frontend/services/routing');
@ -19,16 +19,17 @@ describe('Apps', function () {
it('creates a ghost proxy', function () {
const appProxy = AppProxy.getInstance('TestApp');
should.exist(appProxy.helpers);
should.exist(appProxy.helpers.register);
should.exist(appProxy.helpers.registerAsync);
should.exist(appProxy.helperService);
should.exist(appProxy.helperService.registerAlias);
should.exist(appProxy.helperService.registerDir);
should.exist(appProxy.helperService.registerHelper);
});
it('allows helper registration', function () {
const registerSpy = sinon.stub(helpers, 'registerThemeHelper');
const registerSpy = sinon.stub(helpers, 'registerHelper');
const appProxy = AppProxy.getInstance('TestApp');
appProxy.helpers.register('myTestHelper', sinon.stub().returns('test result'));
appProxy.helperService.registerHelper('myTestHelper', sinon.stub().returns('test result'));
registerSpy.called.should.equal(true);
});

View File

@ -3,7 +3,7 @@ const _ = require('lodash');
const hbs = require('../../../../../core/frontend/services/theme-engine/engine');
// Stuff we are testing
const helpers = require('../../../../../core/frontend/services/theme-engine/handlebars/helpers');
const helpers = require('../../../../../core/frontend/services/helpers');
describe('Helpers', function () {
const hbsHelpers = ['each', 'if', 'unless', 'with', 'helperMissing', 'blockHelperMissing', 'log', 'lookup', 'block', 'contentFor'];
@ -20,7 +20,7 @@ describe('Helpers', function () {
describe('Load Core Helpers', function () {
before(function () {
hbs.express4();
helpers.loadCoreHelpers();
helpers.init();
});
// This will work when we finish refactoring