architekt
Version:
A Dead Simple Static Site Generator Powered by Handlebars
179 lines (159 loc) • 6.28 kB
JavaScript
const commander = require('commander');
const fs = require('fs');
const log = require('ulog')('architekt:config');
const path = require('path');
const os = require('os');
const DEFAULT_CONFIG_FILE_NAME = 'architekt.json';
const DEFAULT_CONFIG = {
source: 'src/',
outDir: 'build/',
resources: { // relative to source directory
templateDir: 'views/',
controllerDir: 'data/',
partialDirs: ['partials'],
layoutDir: 'layouts/',
helperDir: 'helpers/',
assetDir: 'assets/'
},
assetDirs: [
'stylesheets/',
'scripts/',
'images'
]
};
class Config {
/**
* Creates a new configuration object.
*
* The object is initially populated
* with a set of default values for each setting. User settings are specified
* first by a config file. The constructor searches for a config file from the
* directory where the command was called, reads in the file, and overwrites
* the default settings with user-defined settings. These in turn can be
* overwritten by command line arguments.
*
* @param {commander.CommanderStatic} cmd The commander object containing command line arguments
*/
constructor(cmd) {
this.root = null;
this.source = 'src/';
this.outDir = 'build/';
this.resources = { // relative to source directory
templateDir: 'views/',
controllerDir: 'data/',
partialDirs: ['partials'],
layoutDir: 'layouts/',
helperDir: 'helpers/',
assetDir: 'assets/'
};
let config_file_name = cmd.configFile ? path.basename(cmd.configFile) : DEFAULT_CONFIG_FILE_NAME
let config_file_dir = cmd.configFile ? path.dirname(cmd.configFile) : undefined
log.debug(`Config file name: ${config_file_name}`);
log.debug(`Config file directory: ${config_file_dir}`);
this.root = this._resolveRoot(config_file_dir, config_file_name)
// Read user config file; store it as an object
let user_config = JSON.parse(fs.readFileSync(path.join(this.root, config_file_name), 'utf-8'))
// Override defaults with user config data
this._setConfig(user_config);
log.debug(`Project root: ${this.root}`)
// Command line arguments override both system defaults and user config data
log.debug(cmd);
[
'source',
'outDir'
].forEach(option => {
this[option] = (cmd && cmd[option]) || this[option]
});
[
'templateDir',
'controllerDir',
'layoutDir',
'helperDir',
'assetDir'
].forEach(resource => {
this.resources[resource] = cmd[resource] || this.resources[resource]
})
if (cmd.partialDirs) {
this.resources.partialDirs = cmd.partialDirs.split(',')
}
log.debug(`Resource dirs: ${this.resources}`)
}
/**
* Gets the absolute path to a subdirectory in the source code.
*
* @param {string} res The name of the subdirectory
*
* @returns {string | string[]} The path to the specified subdirectory
*
* @throws If the subdirectory name is invalid or if the subdirectory doesn't exist
*/
pathTo(res) {
if (!res || typeof res !== 'string')
throw new Error(`Invalid resource name: ${res}`);
if (this.hasOwnProperty(res)) {
if (typeof this[res] === 'string')
return path.join(this.root, this[res]);
else if (this[res] instanceof Array) {
return this[res].map(r => path.join(this.root, r));
}
}
if (this.resources.hasOwnProperty(res)) {
let resource = this.resources[res];
if (typeof resource === 'string')
return path.join(this.root, this.source, resource);
else if (resource instanceof Array) {
return resource.map(r => path.join(this.root, this.source, r));
}
}
throw new Error(`Resource "${res}" does not exist in the config`)
}
/**
* Finds the root directory of the website. A directory is the root directory
* if and only if it contains a `render.config.json` file.
*
* @returns {string} The path to the root directory
*
* @throws If the root directory could not be resolved
*/
_resolveRoot(root = process.cwd(), configName = DEFAULT_CONFIG_FILE_NAME) {
// @ts-ignore
const systemRoot = (os.platform === 'win32') ? process.cwd().split(path.sep)[0] : '/'
let found = false
for (
; // start at specified root
!found && root !== systemRoot; // Stop searching if config file is found or we reach the root dir
root = path.resolve(root, '..') // Go to parent directory, search again
) {
log.debug(`Resolving config ${configName} at ${root}...`)
/** @type {String[]} */
let filesInDir = fs.readdirSync(root)
if (filesInDir.includes(configName))
return root
}
throw new Error('"render" must be called within a project directory or subdirectory.')
}
/**
* Sets new values to the Config's settings. Only settings that already exist
* in the config will be set.
*
* @param {Object} config New values to populate the config object with
*
* @returns {void}
*/
_setConfig(config = {}) {
for (const setting in config) {
if (config.hasOwnProperty(setting) && this.hasOwnProperty(setting)) {
const settingVal = config[setting];
// Make sure the new setting is of the expected type
if (typeof settingVal === typeof this[setting])
this[setting] = settingVal;
else
log.warn(`WARNING: Tried to set config setting to incompatible type: expected ${typeof this[setting]}, got ${typeof settingVal}`);
}
}
}
}
module.exports.Config = Config;
module.exports.default = Config;
module.exports.DEFAULT_CONFIG = DEFAULT_CONFIG
module.exports.DEFAULT_CONFIG_FILE_NAME = DEFAULT_CONFIG_FILE_NAME