Skip to content

Commit

Permalink
Improve bootstrap flow of a Ghost application
Browse files Browse the repository at this point in the history
addresses TryGhost#1789, TryGhost#1364

- Moves ./core/server/loader -> ./core/bootstrap.
The bootstrap file is only accessed once during startup,
and it’s sole job is to ensure a config.js file exists
(creating one if it doesn’t) and then validates
the contents of the config file.

Since this is directly related to the initializing 
the application is is appropriate to have 
it in the ./core folder, named bootstrap as that
is what it does.

This also improves the dependency graph, as now
the bootstrap file require’s the ./core/server/config
module and is responsible for passing in the validated
config file.

Whereas before we had ./core/server/config
require’ing ./core/server/loader and running its
init code and then passing that value back to itself,
the flow is now more straight forward of
./core/bootstrap handling initialization and then
instatiation of config module

- Merges ./core/server/config/paths into 
./core/server/config
This flow was always confusing me to that some config
options were on the config object, and some were on
the paths object.

This change now incorporates all of the variables
previously defined in config/paths directly
into the config module, and in extension,
the config.js file.

This means that you now have the option of deciding
at startup where the content directory for ghost
should reside.

- broke out loader tests in config_spec to bootstrap_spec

- updated all relevant files to now use config().paths

- moved urlFor and urlForPost function into 
 ./server/config/url.js
  • Loading branch information
hswolff committed Feb 7, 2014
1 parent cf86334 commit f16dc29
Show file tree
Hide file tree
Showing 25 changed files with 694 additions and 653 deletions.
4 changes: 2 additions & 2 deletions Gruntfile.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ var path = require('path'),
spawn = require('child_process').spawn,
buildDirectory = path.resolve(process.cwd(), '.build'),
distDirectory = path.resolve(process.cwd(), '.dist'),
config = require('./core/server/config'),
bootstrap = require('./core/bootstrap'),


// ## Build File Patterns
Expand Down Expand Up @@ -501,7 +501,7 @@ var path = require('path'),

grunt.registerTask('loadConfig', function () {
var done = this.async();
config.load().then(function () {
bootstrap().then(function () {
done();
});
});
Expand Down
3 changes: 3 additions & 0 deletions config.example.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,9 @@ config = {
host: '127.0.0.1',
// Port to be passed to node's `net.Server#listen()`, for iisnode set this to `process.env.PORT`
port: '2368'
},
paths: {
contentPath: path.join(__dirname, '/content/')
}
},

Expand Down
15 changes: 9 additions & 6 deletions core/server/config/loader.js → core/bootstrap.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,13 @@
var fs = require('fs'),
url = require('url'),
when = require('when'),
errors = require('../errorHandling'),
path = require('path'),
paths = require('./paths'),
errors = require('./server/errorHandling'),
config = require('./server/config'),

appRoot = paths().appRoot,
configExample = paths().configExample,
configFile = process.env.GHOST_CONFIG || paths().config,
appRoot = config().paths.appRoot,
configExample = config().paths.configExample,
configFile = process.env.GHOST_CONFIG || config().paths.config,
rejectMessage = 'Unable to load config';

function readConfigFile(envVal) {
Expand Down Expand Up @@ -112,8 +112,11 @@ function loadConfig() {
if (!configExists) {
pendingConfig = writeConfigFile();
}
when(pendingConfig).then(validateConfigEnvironment).then(loaded.resolve).otherwise(loaded.reject);
when(pendingConfig).then(validateConfigEnvironment).then(function (rawConfig) {
return config.init(rawConfig).then(loaded.resolve);
}).otherwise(loaded.reject);
});

return loaded.promise;
}

Expand Down
6 changes: 3 additions & 3 deletions core/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,13 @@
// Orchestrates the loading of Ghost
// When run from command line.

var config = require('./server/config'),
errors = require('./server/errorHandling');
var bootstrap = require('./bootstrap'),
errors = require('./server/errorHandling');

process.env.NODE_ENV = process.env.NODE_ENV || 'development';

function startGhost(app) {
config.load().then(function () {
bootstrap().then(function () {
var ghost = require('./server');
ghost(app);
}).otherwise(errors.logAndThrowError);
Expand Down
6 changes: 3 additions & 3 deletions core/server/api/db.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ var dataExport = require('../data/export'),
nodefn = require('when/node/function'),
_ = require('lodash'),
schema = require('../data/schema').tables,
configPaths = require('../config/paths'),
config = require('../config'),
api = {},

db;
Expand All @@ -20,7 +20,7 @@ db = {
/*jslint unparam:true*/
return dataExport().then(function (exportedData) {
// Save the exported data to the file system for download
var fileName = path.join(configPaths().exportPath, 'exported-' + (new Date().getTime()) + '.json');
var fileName = path.join(config().paths.exportPath, 'exported-' + (new Date().getTime()) + '.json');

return nodefn.call(fs.writeFile, fileName, JSON.stringify(exportedData)).then(function () {
return when(fileName);
Expand All @@ -39,7 +39,7 @@ db = {
};

return api.notifications.add(notification).then(function () {
res.redirect(configPaths().debugPath);
res.redirect(config().paths.debugPath);
});
});
});
Expand Down
2 changes: 1 addition & 1 deletion core/server/api/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ function cacheInvalidationHeader(req, result) {
} else if (endpoint === 'posts') {
cacheInvalidate = "/, /page/*, /rss/, /rss/*";
if (id && jsonResult.slug) {
return config.paths.urlForPost(settings, jsonResult).then(function (postUrl) {
return config.urlForPost(settings, jsonResult).then(function (postUrl) {
return cacheInvalidate + ', ' + postUrl;
});
}
Expand Down
2 changes: 1 addition & 1 deletion core/server/api/settings.js
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ readSettingsResult = function (result) {
settings[member.attributes.key] = val;
}
})).then(function () {
return when(config.paths().availableThemes).then(function (themes) {
return when(config().paths.availableThemes).then(function (themes) {
var themeKeys = Object.keys(themes),
res = [],
i,
Expand Down
2 changes: 1 addition & 1 deletion core/server/apps/loader.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ var path = require('path'),
function getAppRelativePath(name, relativeTo) {
relativeTo = relativeTo || __dirname;

return path.relative(relativeTo, path.join(config.paths().appPath, name));
return path.relative(relativeTo, path.join(config().paths.appPath, name));
}

// Load apps through a psuedo sandbox
Expand Down
120 changes: 98 additions & 22 deletions core/server/config/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,36 +3,112 @@
// This file itself is a wrapper for the root level config.js file.
// All other files that need to reference config.js should use this file.

var loader = require('./loader'),
paths = require('./paths'),
var path = require('path'),
when = require('when'),
url = require('url'),
_ = require('lodash'),
requireTree = require('../require-tree'),
theme = require('./theme'),
ghostConfig;
configUrl = require('./url'),
ghostConfig = {},
appRoot = path.resolve(__dirname, '../../../'),
corePath = path.resolve(appRoot, 'core/');

function updateConfig(config) {
var localPath,
contentPath,
subdir;

// Merge passed in config object onto
// the cached ghostConfig object
_.merge(ghostConfig, config);

// Protect against accessing a non-existant object.
// This ensures there's always at least a paths object
// because it's referenced in multiple places.
ghostConfig.paths = ghostConfig.paths || {};

// Parse local path location
if (ghostConfig.url) {
localPath = url.parse(ghostConfig.url).path;
// Remove trailing slash
if (localPath !== '/') {
localPath = localPath.replace(/\/$/, '');
}
}

subdir = localPath === '/' ? '' : localPath;

// Allow contentPath to be over-written by passed in config object
// Otherwise default to default content path location
contentPath = ghostConfig.paths.contentPath || path.resolve(appRoot, 'content');

_.merge(ghostConfig, {
paths: {
'appRoot': appRoot,
'subdir': subdir,
'config': path.join(appRoot, 'config.js'),
'configExample': path.join(appRoot, 'config.example.js'),
'corePath': corePath,

'contentPath': contentPath,
'themePath': path.resolve(contentPath, 'themes'),
'appPath': path.resolve(contentPath, 'apps'),
'imagesPath': path.resolve(contentPath, 'images'),
'imagesRelPath': 'content/images',

'adminViews': path.join(corePath, '/server/views/'),
'helperTemplates': path.join(corePath, '/server/helpers/tpl/'),
'exportPath': path.join(corePath, '/server/data/export/'),
'lang': path.join(corePath, '/shared/lang/'),
'debugPath': subdir + '/ghost/debug/',

'availableThemes': ghostConfig.paths.availableThemes || [],
'availableApps': ghostConfig.paths.availableApps || [],
'builtScriptPath': path.join(corePath, 'built/scripts/')
}
});

// Also pass config object to
// configUrl object to maintain
// clean depedency tree
configUrl.setConfig(ghostConfig);

return ghostConfig;
}

function initConfig(rawConfig) {
// Cache the config.js object's environment
// object so we can later refer to it.
// Note: this is not the entirety of config.js,
// just the object appropriate for this NODE_ENV
ghostConfig = updateConfig(rawConfig);

return when.all([requireTree(ghostConfig.paths.themePath), requireTree(ghostConfig.paths.appPath)]).then(function (paths) {
ghostConfig.paths.availableThemes = paths[0];
ghostConfig.paths.availableApps = paths[1];
return ghostConfig;
});
}

// Returns NODE_ENV config object
function config() {
// @TODO: get rid of require statement.
// This is currently needed for tests to load config file
// successfully. While running application we should never
// have to directly delegate to the config.js file.
return ghostConfig || require(paths().config)[process.env.NODE_ENV];
}
if (_.isEmpty(ghostConfig)) {
try {
ghostConfig = require(path.resolve(__dirname, '../../../', 'config.js'))[process.env.NODE_ENV] || {};
} catch (ignore) {/*jslint sloppy: true */}
ghostConfig = updateConfig(ghostConfig);
}

function loadConfig() {
return loader().then(function (config) {
// Cache the config.js object's environment
// object so we can later refer to it.
// Note: this is not the entirety of config.js,
// just the object appropriate for this NODE_ENV
ghostConfig = config;

// can't load theme settings yet as we don't have the API,
// but we can load the paths
return paths.update(config.url);
});
return ghostConfig;
}

config.load = loadConfig;
config.paths = paths;
config.theme = theme;

module.exports = config;
module.exports = config;
module.exports.init = initConfig;
module.exports.theme = theme;
module.exports.urlFor = configUrl.urlFor;
module.exports.urlForPost = configUrl.urlForPost;
76 changes: 13 additions & 63 deletions core/server/config/paths.js → core/server/config/url.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,66 +2,17 @@
// the codebase.

var moment = require('moment'),
path = require('path'),
when = require('when'),
url = require('url'),
_ = require('lodash'),
requireTree = require('../require-tree'),
appRoot = path.resolve(__dirname, '../../../'),
corePath = path.resolve(appRoot, 'core/'),
contentPath = path.resolve(appRoot, 'content/'),
themePath = path.resolve(contentPath + '/themes'),
appPath = path.resolve(contentPath + '/apps'),
themeDirectories = requireTree(themePath),
appDirectories = requireTree(appPath),
localPath = '',
configUrl = '',

availableThemes,
availableApps;


function paths() {
var subdir = localPath === '/' ? '' : localPath;

return {
'appRoot': appRoot,
'subdir': subdir,
'config': path.join(appRoot, 'config.js'),
'configExample': path.join(appRoot, 'config.example.js'),
'contentPath': contentPath,
'corePath': corePath,
'themePath': themePath,
'appPath': appPath,
'imagesPath': path.resolve(contentPath, 'images/'),
'imagesRelPath': 'content/images',
'adminViews': path.join(corePath, '/server/views/'),
'helperTemplates': path.join(corePath, '/server/helpers/tpl/'),
'exportPath': path.join(corePath, '/server/data/export/'),
'lang': path.join(corePath, '/shared/lang/'),
'debugPath': subdir + '/ghost/debug/',
'availableThemes': availableThemes,
'availableApps': availableApps,
'builtScriptPath': path.join(corePath, 'built/scripts/')
};
}

// TODO: remove configURL and give direct access to config object?
// TODO: not called when executing tests
function update(configURL) {
configUrl = configURL;
localPath = url.parse(configURL).path;

// Remove trailing slash
if (localPath !== '/') {
localPath = localPath.replace(/\/$/, '');
}

return when.all([themeDirectories, appDirectories]).then(function (paths) {
availableThemes = paths[0];
availableApps = paths[1];
return;
});
ghostConfig = '';

// ## setConfig
// Simple utility function to allow
// passing of the ghostConfig
// object here to be used locally
// to ensure clean depedency graph
// (i.e. no circular dependencies).
function setConfig(config) {
ghostConfig = config;
}

// ## createUrl
Expand All @@ -85,9 +36,9 @@ function createUrl(urlPath, absolute) {

// create base of url, always ends without a slash
if (absolute) {
output += configUrl.replace(/\/$/, '');
output += ghostConfig.url.replace(/\/$/, '');
} else {
output += paths().subdir;
output += ghostConfig.paths.subdir;
}

// append the path, always starts and ends with a slash
Expand Down Expand Up @@ -186,7 +137,6 @@ function urlForPost(settings, post, absolute) {
});
}

module.exports = paths;
module.exports.update = update;
module.exports.setConfig = setConfig;
module.exports.urlFor = urlFor;
module.exports.urlForPost = urlForPost;
Loading

0 comments on commit f16dc29

Please sign in to comment.