apostrophe-site
Version:
Create sites powered by the Apostrophe 2 CMS with a minimum of boilerplate code
508 lines (459 loc) • 17.9 kB
JavaScript
// Currently limited to a single instance due to dependency on Appy.
// TODO: consider making appy support multiple instances or removing
// the need for Appy
var appy = require('appy');
var async = require('async');
var uploadfs = require('uploadfs');
var fs = require('fs');
var path = require('path');
var _ = require('underscore');
var extend = require('extend');
var nodemailer = require('nodemailer');
var schemas = require('apostrophe-schemas');
module.exports = function(options) {
return new AposSite(options);
};
function AposSite(options) {
var self = this;
var localJsLocals = {};
self.apos = require('apostrophe')();
self.root = options.root;
self.rootDir = path.dirname(self.root.filename);
// If you don't like our default set of allowed tags and attributes. This must
// be generous enough to encompass at least all the tags in your styles menu, etc.
self.sanitizeHtmlOptions = options.sanitizeHtml;
// Fetch local overrides for this server, like minify: true or uploadfs configuration
if (fs.existsSync(self.rootDir + '/data/local.js')) {
var local = require(self.rootDir + '/data/local.js');
// There may be nunjucks locals; if we just merge them in, we'll
// clobber options.locals if it is a function. Set it aside to
// merge later. -Tom
localJsLocals = local.locals;
delete local.locals;
extend(true, options, local);
}
self.uploadfs = uploadfs();
if (!self.root) {
throw 'Specify the `root` option and set it to `module` (yes, really)';
}
self.shortName = options.shortName;
if (!self.shortName) {
throw "Specify the `shortName` option and set it to the name of your project's repository or folder";
}
self.hostName = options.hostName;
if (!self.hostName) {
throw "Specify the `hostName` option and set it to the preferred hostname of your site, such as mycompany.com";
}
self.adminPassword = options.adminPassword;
if (!self.adminPassword) {
throw "Specify the `adminPassword` option and set it to a secure password for admin access. This account is always valid in addition to accounts in the database";
}
self.imageSizes = self.apos.defaultImageSizes;
if (options.imageSizes) {
self.imageSizes = options.imageSizes;
}
if (options.addImageSizes) {
self.imageSizes = self.imageSizes.concat(options.addImageSizes);
}
if (!options.sessionSecret) {
throw "Specify the `sessionSecret` option. This should be a secure password not used for any other purpose.";
}
self.title = options.title;
self.sessionSecret = options.sessionSecret;
self.locals = options.locals || {};
if (typeof(self.locals) === 'function') {
// We need access to the site object inside a function
self.locals = self.locals(self);
}
extend(true, self.locals, localJsLocals);
_.defaults(self.locals, {
siteTitle: self.title,
shortName: self.shortName,
hostName: self.hostName
});
self.minify = options.minify;
var uploadfsDefaultSettings = {
backend: 'local',
uploadsPath: self.rootDir + '/public/uploads',
uploadsUrl: '/uploads',
tempPath: self.rootDir + '/data/temp/uploadfs',
// Register Apostrophe's standard image sizes. Notice you could
// concatenate your own list of sizes if you had a need to
imageSizes: self.imageSizes
};
uploadfsSettings = {};
extend(true, uploadfsSettings, uploadfsDefaultSettings);
extend(true, uploadfsSettings, options.uploadfs || {});
if (options.uploadsUrl) {
// for bc
uploadfsSettings.uploadsUrl = options.uploadsUrl;
}
var mailerOptions = options.mailer || {};
_.defaults(mailerOptions, {
transport: 'sendmail',
transportOptions: {},
});
self.mailer = nodemailer.createTransport(mailerOptions.transport, mailerOptions.transportOptions);
appy.bootstrap({
// We're not sure if appy is installed as our dependency
// or as the project's, but we know that WE are a direct
// dependency of the project. So we are able to tell
// appy where the project's root folder is.
rootDir: path.dirname(path.dirname(__dirname)),
// Allows gzip transfer encoding to be shut off if desired
compress: options.compress,
// Don't bother with viewEngine, we'll use apos.partial() if we want to
// render anything directly
auth: self.apos.appyAuth({
loginPage: function(data) {
// TODO: this is a hack and doesn't allow for some other module to
// supply the password reset capability
if (self.modules['apostrophe-people']) {
data.resetAvailable = true;
}
return self.pages.decoratePageContent({ content: self.apos.partial('login', data), when: 'anon' });
},
redirect: function(user) {
if (options.redirectAfterLogin) {
return options.redirectAfterLogin(user);
}
return '/';
},
adminPassword: self.adminPassword
}),
beforeSignin: self.apos.appyBeforeSignin,
sessionSecret: self.sessionSecret,
db: {
uri: (options.db && options.db.uri) || undefined,
host: (options.db && options.db.host) || 'localhost',
port: (options.db && options.db.port) || 27017,
name: (options.db && options.db.name) || options.shortName,
user: (options.db && options.db.user) || undefined,
password: (options.db && options.db.password) || undefined,
collections: (options.db && options.db.collections) || []
},
// Supplies LESS middleware
static: self.rootDir + '/public',
ready: function(appArg, dbArg)
{
self.app = appArg;
self.db = dbArg;
async.series([ createTemp, initUploadfs, initApos, initSchemas, initPages, initModules, bridgeModules, setRoutes, servePages, pushAssets, endAssets, afterInit ], go);
}
});
function createTemp(callback) {
ensureDir(uploadfsSettings.tempPath);
return callback(null);
}
function initUploadfs(callback) {
self.uploadfs.init(uploadfsSettings, callback);
}
function ensureDir(p) {
var needed = [];
while (!fs.existsSync(p)) {
needed.unshift(p);
p = path.dirname(p);
}
_.each(needed, function(p) {
fs.mkdirSync(p);
});
}
function initApos(callback) {
// Let the apostrophe module know to pass the site object as the first
// argument to tasks that accept a fourth argument
self.apos._taskContext = self;
return self.apos.init({
db: self.db,
app: self.app,
uploadfs: self.uploadfs,
locals: self.locals,
filterTag: options.filterTag,
// Allows us to extend shared layouts
partialPaths: [ self.rootDir + '/views/global' ],
minify: self.minify,
sanitizeHtml: self.sanitizeHtmlOptions,
mediaLibrary: options.mediaLibrary || {},
lockups: options.lockups || {}
}, callback);
}
function initSchemas(callback) {
var schemasOptions = {};
extend(true, schemasOptions, options.schemas);
schemasOptions.apos = self.apos;
schemasOptions.app = self.app;
self.schemas = require('apostrophe-schemas')(schemasOptions, callback);
}
function initPages(callback) {
var pagesOptions = {};
extend(true, pagesOptions, options.pages);
pagesOptions.apos = self.apos;
pagesOptions.app = self.app;
pagesOptions.schemas = self.schemas;
self.pages = require('apostrophe-pages')(pagesOptions, function(err) {
if (err) {
return callback(err);
}
self.schemas.setPages(self.pages);
return callback(null);
});
}
function initModules(callback) {
self.modules = {};
var modulesConfig = options.modules || [];
if ((!modulesConfig) || Array.isArray(modulesConfig)) {
throw "modules option must be an object with a property for each module. The property name must be the name of the module, such as \"apostrophe-twitter\", and the value must be an object which may contain options. An empty object is acceptable for some modules.";
}
return async.eachSeries(_.keys(modulesConfig), function(name, callback) {
var config = modulesConfig[name];
_.defaults(config, {
app: self.app,
apos: self.apos,
pages: self.pages,
schemas: self.schemas,
mailer: self.mailer
});
var localFolder = self.rootDir + '/lib/modules/' + name;
var localIndex = localFolder + '/index.js';
var npmName = config.extend || name;
var localFound = false;
var npmFound = false;
// Factory function accepts options and callback, returns an object to manage this module
var factory;
if (!fs.existsSync(localFolder)) {
// Directly installing an npm module with no subclassing of any kind,
// not even a folder with alternate templates. That's allowed
factory = self.root.require(npmName);
npmFound = true;
} else {
localFound = true;
// Module exists locally. Was it also installed via npm? If so, subclass
var base;
try {
base = self.root.require(npmName);
npmFound = true;
} catch (e) {
// That's OK, this module simply only exists locally
}
if (!base) {
// Exists locally only (well, it had better)
try {
factory = require(localIndex);
} catch (e) {
console.error('Unable to find ' + localIndex + '. Either you forgot to npm install something, or you forgot to set the extend property for this module.');
throw e;
}
} else {
// Inject a subclass at this point which provides the
// right folder name for templates, so we can skip
// index.js locally entirely or have one that doesn't bother
// with that tedious step
// For access to the apostrophe-site instance
var site = self;
var InlineConstruct = function(optionsArg, callback) {
var self = this;
// Locate the constructor of the base. This ought to be
// base.Construct but we didn't think that far ahead, so
// figure it out if necessary
var Super = base.Construct;
if (!Super) {
var npmConstructor = guessConstructor(npmName);
Super = base[npmConstructor];
if (!Super) {
throw "Unable to figure out constructor function name for the module " + npmName + ", my best guess was " + npmConstructor + ". This module must export a function that returns an object when given options and a callback, and the constructor for use when subclassing should be the Construct property of that function, or a property named " + npmConstructor + ". If you get this error for an Apostrophe module please report it as a bug.";
}
}
var options = {};
extend(true, options, optionsArg);
// We use guessConstructor to come up with a reasonable URL for assets
// served by this local module, prefixing it with 'my'. It'll also get passed
// through apos.cssName. So "apostrophe-blog" becomes /my-blog.
var myConstructor = guessConstructor(name);
options.modules = (options.modules || []).concat([{ dir: localFolder, name: 'my' + myConstructor }]);
// Provide information about the site to each module
options.site = {
title: site.title,
shortName: site.shortName,
hostName: site.hostName
};
return Super.call(self, options, callback);
};
var inlineFactory = function(options, callback) {
return new InlineConstruct(options, callback);
};
inlineFactory.Construct = InlineConstruct;
// If there is a local subclass, require it and inject our inline class
// as its superclass
if (fs.existsSync(localIndex)) {
factory = require(localIndex);
// Make our inline subclass available to the module. It's OK if the
// module doesn't care and does its own requiring and subclassing,
// but this sure is convenient
factory.Super = InlineConstruct;
} else {
// No local index.js, just template overrides etc. Use the
// inline class directly
factory = inlineFactory;
}
}
}
// What should the constructor on the browser side be called?
// And what should it extend?
//
// apostrophe-snippets is smart enough to synthesize it if needed
if (npmFound && localFound) {
if (!config.browser) {
config.browser = {};
}
if (!config.browser.construct) {
if (name === npmName) {
// Emphasizes we're subclassing something with an otherwise similar name
config.browser.construct = 'My' + guessConstructor(name);
} else {
config.browser.construct = guessConstructor(name);
}
}
if (!config.browser.baseConstruct) {
// Browser-side constructors from npm always start with Apos
config.browser.baseConstruct = 'Apos' + guessConstructor(npmName);
}
}
if (factory.length === 1) {
// Requires no callback. Replace it with a wrapper that does
var originalFactory = factory;
factory = function(config, callback) {
setImmediate(function() {
return callback(null);
});
return originalFactory(config);
};
}
self.modules[name] = factory(config, function(err) {
if (err) {
console.error("Error configuring module " + name);
throw err;
}
if (!self.modules[name]) {
throw 'No module found for ' + name;
}
return callback(null);
});
}, callback);
}
// If a module is interested, give it a reference to the other modules.
// This allows the groups module to access the people module, for instance.
function bridgeModules(callback) {
_.each(self.modules, function(module, name) {
if (module.setBridge) {
module.setBridge(self.modules);
}
});
return callback(null);
}
// Last chance to set routes before the wildcard route for pages
function setRoutes(callback) {
if (options.setRoutes) {
return options.setRoutes(callback);
} else {
return callback(null);
}
}
function servePages(callback) {
// Always set up the page loaders for any active modules that have them,
// and for a virtual page named "global" which is super handy for footers etc.
var loaders = [ 'global', self.pages.searchLoader ];
_.each(self.modules, function(module, name) {
if (module.loader) {
loaders.push(module.loader);
}
});
// Append any configured page loaders
if (options.pages && options.pages.load) {
loaders = loaders.concat(options.pages.load);
}
// Extend sensible defaults with custom settings
var pagesOptions = {};
extend(true, pagesOptions, {
templatePath: self.rootDir + '/views/pages'
});
extend(true, pagesOptions, options.pages || {});
// The merged loaders must win
pagesOptions.load = loaders;
var serve = self.pages.serve(pagesOptions);
// All this does is call app.get('*', ... some middleware ... , serve) but
// since the middleware option is an array we need to build a complete
// array of options and use app.get.apply
var appGetArguments = [ '*' ];
appGetArguments = appGetArguments.concat(pagesOptions.middleware || []);
// Allow each module to add pages.serve middleware too
_.each(self.modules, function(module) {
if (module.middleware) {
appGetArguments = appGetArguments.concat(module.middleware);
}
});
appGetArguments.push(serve);
self.app.get.apply(self.app, appGetArguments);
return callback(null);
}
function pushAssets(callback) {
_.each((options.assets && options.assets.stylesheets) || [], function(name) {
if (typeof(name) === 'object') {
pushAsset('stylesheet', name.name, name);
} else {
pushAsset('stylesheet', name, {});
}
});
_.each((options.assets && options.assets.scripts) || [], function(name) {
if (typeof(name) === 'object') {
pushAsset('script', name.name, name);
} else {
pushAsset('script', name, {});
}
});
function pushAsset(type, name, _options) {
var options = {
fs: self.rootDir,
web: '',
when: 'always'
};
extend(true, options, _options);
return self.apos.pushAsset(type, name, options);
}
return callback();
}
function endAssets(callback) {
return async.series({
beforeEndAssets: function(callback) {
if (!options.beforeEndAssets) {
return callback(null);
}
return options.beforeEndAssets(callback);
},
endAssets: function(callback) {
return self.apos.endAssets(callback);
}
}, callback);
}
function afterInit(callback) {
if (options.afterInit) {
return options.afterInit(callback);
}
return callback(null);
}
function go(err) {
if (err) {
throw err;
}
// Command line tasks
if (self.apos.startTask(options.tasks || {})) {
// Chill and let the task run until it's done, don't try to listen or exit
return;
}
return appy.listen();
}
// Convert a module name to the probable name of its constructor property
// (we look for Construct first, as newer modules go that way)
function guessConstructor(name) {
name = name.replace(/^apostrophe\-/, '');
return self.apos.capitalizeFirst(self.apos.camelName(name));
}
}