UNPKG

docpad

Version:

DocPad is a dynamic static site generator. Write your content as files, or import your content from other external sources. Render the content with plugins. And deploy your static or dynamic website to your favourite hosting provider.

1,626 lines (1,525 loc) 176 kB
// Generated by CoffeeScript 2.5.1 //#* // The central module for DocPad // @module DocPad //# // ===================================== // Requires // Standard /** * Contains methods for managing the DocPad application. * Extends https://github.com/bevry/event-emitter-grouped * * You can use it like so: * * new DocPad(docpadConfig, function(err, docpad) { * if (err) return docpad.fatal(err) * return docpad.action(action, function(err) { * if (err) return docpad.fatal(err) * return console.log('OK') * }) * }) * * @class Docpad * @constructor * @extends EventEmitterGrouped */ var BasePlugin, CSON, Collection, DocPad, DocumentModel, ElementsCollection, Errlop, EventEmitterGrouped, Events, FileModel, FilesCollection, Filter, Human, Logger, MetaCollection, Model, PluginLoader, Progress, QueryCollection, ScriptsCollection, StylesCollection, TaskGroup, ambi, ansiStyles, balUtil, docpadUtil, eachr, envFile, extendr, extractOptsAndCallback, fetch, fsUtil, ignorefs, isTruthy, isUser, pathUtil, pick, queryEngine, rimraf, safefs, safeps, scandir, typeChecker, unbounded, union, uniq, util, hasProp = {}.hasOwnProperty, boundMethodCheck = function(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new Error('Bound instance method accessed before binding'); } }; util = require('util'); pathUtil = require('path'); // External ({Logger, Human, Filter} = require('caterpillar')); fsUtil = require('fs'); Errlop = require('errlop').default; queryEngine = require('query-engine'); ({uniq, union, pick} = require('underscore')); CSON = require('cson'); balUtil = require('bal-util'); scandir = require('scandirectory'); extendr = require('extendr'); eachr = require('eachr'); typeChecker = require('typechecker'); ambi = require('ambi'); unbounded = require('unbounded'); ({TaskGroup} = require('taskgroup')); safefs = require('safefs'); safeps = require('safeps'); ignorefs = require('ignorefs'); rimraf = require('rimraf'); Progress = require('progress-title'); fetch = require('node-fetch'); extractOptsAndCallback = require('extract-opts'); ({EventEmitterGrouped} = require('event-emitter-grouped')); envFile = require('envfile'); ansiStyles = require('ansistyles'); // Base ({Events, Model, Collection, QueryCollection} = require('./base')); // Utils docpadUtil = require('./util'); // Models FileModel = require('./models/file'); DocumentModel = require('./models/document'); // Collections FilesCollection = require('./collections/files'); ElementsCollection = require('./collections/elements'); MetaCollection = require('./collections/meta'); ScriptsCollection = require('./collections/scripts'); StylesCollection = require('./collections/styles'); // Plugins PluginLoader = require('@bevry/pluginloader').default; BasePlugin = require('docpad-baseplugin'); // --------------------------------- // Variables isUser = docpadUtil.isUser(); isTruthy = function(i) { return Boolean(i); }; DocPad = (function() { class DocPad extends EventEmitterGrouped { // Libraries // Here for legacy API reasons //@DocPad: DocPad //@Backbone: require('backbone') //@queryEngine: queryEngine // Allow for `DocPad.create()` as an alias for `new DocPad()` static create(...args) { return new this(...args); } // Require a local DocPad file // Before v6.73.0 this allowed requiring of files inside src/lib, as well as files inside src // After v6.73.0 it only allows requiring of files inside src/lib as that makes more sense // After v6.80.9 it only allows requiring specific aliases static require(name) { if (name === 'testers') { console.log(`' docpad.require('testers') is deprecated, replacement instructions at: https://github.com/docpad/docpad-plugintester`); return require('docpad-plugintester'); } else { throw new Errlop("docpad.require is limited to requiring: testers"); } } /** * Get the DocPad version number * @method getVersion * @return {Number} */ getVersion() { if (this.version == null) { this.version = require(this.packagePath).version; } return this.version; } /** * Get the DocPad version string * @method getVersionString * @return {String} */ getVersionString() { if (docpadUtil.isLocalDocPadExecutable()) { return util.format(this.getLocale().versionLocal, this.getVersion(), this.corePath); } else { return util.format(this.getLocale().versionGlobal, this.getVersion(), this.corePath); } } // Process getters /** * Get the process platform * @method getProcessPlatform * @return {Object} */ getProcessPlatform() { return process.platform; } /** * Get the process version string * @method getProcessVersion * @return {String} */ getProcessVersion() { return process.version.replace(/^v/, ''); } /** * Get the caterpillar logger instance bound to DocPad * @method getLogger * @return {Object} caterpillar logger */ getLogger() { return this.logger; } /** * Destructor. Destroy the caterpillar logger instances bound to DocPad * @private * @method {Object} destroyLoggers */ destroyLoggers() { // @logger.end() this.logger = null; return this; } /** * Create a timer and add it to the known timers * @method timer * @param {string} type - either timeout or interval * @param {number} time - the time to apply to the timer * @param {method} method - the method to use for the timer */ timer(id, type, time, method) { var timer; if (this.timers == null) { this.timers = {}; } // Create a new timer if (type != null) { this.timer(id); // clear if (type === 'timeout') { if (time === -1) { timer = setImmediate(method); } else { timer = setTimeout(method, time); } } else if (type === 'interval') { timer = setInterval(method, time); } else { throw new Errlop('unexpected type on new timer'); } this.timers[id] = {id, type, time, method, timer}; // Destroy an old timer } else if (this.timers[id]) { if (this.timers[id].type === 'interval') { clearInterval(this.timers[id].timer); } else if (this.timers[id].type === 'timeout') { if (this.timers[id].time === -1) { if (typeof clearImmediate === "function") { clearImmediate(this.timers[id].timer); } } else { clearTimeout(this.timers[id].timer); } } else { throw new Errlop('unexpected type on stored timer'); } this.timers[id] = null; } return this; } /** * Destructor. Destroy all the timers we have kept. * @private * @method {Object} destroyTimers */ destroyTimers(timer) { var key, ref, value; if (this.timers == null) { this.timers = {}; } ref = this.timers; for (key in ref) { if (!hasProp.call(ref, key)) continue; value = ref[key]; this.timer(key); } return this; } /** * Update the configuration of the progress instance, to either enable it or disable it * Progress will be enabled if DocPad config 'progress' is true * @private * @method updateProgress * @param {boolean} [enabled] manually enable or disable the progress bar */ updateProgress(enabled) { var config, debug, docpad, options; // Prepare docpad = this; config = docpad.getConfig(); debug = this.getDebugging(); // Enabled if (enabled == null) { enabled = config.progress; } // If we are in debug mode, then output more detailed title messages options = {}; if (debug) { options.verbose = true; options.interval = 0; } // options.log = true // If we wish to have it enabled if (enabled) { if (this.progressInstance) { this.progressInstance.pause().configure(options).resume(); } else { this.progressInstance = Progress.create(options).start(); } } else if (this.progressInstance) { this.progressInstance.stop().configure(options); } // Return return this; } /** * Get the action runner instance bound to docpad * @method getActionRunner * @return {Object} the action runner instance */ getActionRunner() { return this.actionRunnerInstance; } /** * Apply the passed DocPad action arguments * @method {Object} action * @param {Object} args * @return {Object} */ action(action, opts, next) { var locale, ref; // Prepare [opts, next] = extractOptsAndCallback(opts, next); locale = this.getLocale(); // Log if ((ref = this.progressInstance) != null) { ref.resume(); } this.log('debug', util.format(locale.actionStart, action)); // Act docpadUtil.action.call(this, action, opts, (...args) => { var err, ref1; // Prepare err = args[0]; // Log if ((ref1 = this.progressInstance) != null) { ref1.stop(); } if (err) { this.error(new Errlop(util.format(locale.actionFailure, action), err)); } else { this.log('debug', util.format(locale.actionSuccess, action)); } return typeof next === "function" ? next(...args) : void 0; }); return this; } /** * Get the list of available events * @method getEvents * @return {Object} string array of event names */ getEvents() { return this.events; } /** * Description for getDatabase * @method {Object} getDatabase */ getDatabase() { return this.database; } /** * Destructor. Destroy the DocPad database * @private * @method destroyDatabase */ destroyDatabase() { if (this.database != null) { this.database.destroy(); this.database = null; } return this; } /* { * A collection of meta elements meta: null # Elements Collection * A collection of script elements scripts: null # Scripts Collection * Collection of style elements styles: null # Styles Collection } */ /** * Get a block by block name. Optionally clone block. * @method getBlock * @param {String} name * @param {Object} [clone] * @return {Object} block */ getBlock(name, clone) { var block, classname; block = this.blocks[name]; if (clone) { classname = name[0].toUpperCase() + name.slice(1) + 'Collection'; block = new this[classname](block.models); } return block; } /** * Set a block by name and value * @method setBlock * @param {String} name * @param {Object} value */ setBlock(name, value) { if (this.blocks[name] != null) { this.blocks[name].destroy(); if (value) { this.blocks[name] = value; } else { delete this.blocks[name]; } } else { this.blocks[name] = value; } return this; } /** * Get all blocks * @method getBlocks * @return {Object} collection of blocks */ getBlocks() { return this.blocks; } /** * Set all blocks * @method setBlocks * @param {Object} blocks */ setBlocks(blocks) { var name, value; for (name in blocks) { if (!hasProp.call(blocks, name)) continue; value = blocks[name]; this.setBlock(name, value); } return this; } /** * Apply the passed function to each block * @method eachBlock * @param {Function} fn */ eachBlock(fn) { eachr(this.blocks, fn); return this; } /** * Destructor. Destroy all blocks * @private * @method destroyBlocks */ destroyBlocks() { var block, name, ref; if (this.blocks) { ref = this.blocks; for (name in ref) { if (!hasProp.call(ref, name)) continue; block = ref[name]; block.destroy(); this.blocks[name] = null; } } return this; } /** * Get a collection by collection name or key. * This is often accessed within the docpad.coffee * file or a layout/page via @getCollection. * Because getCollection returns a docpad collection, * a call to this method is often chained with a * QueryEngine style query. * * @getCollection('documents').findAllLive({relativeOutDirPath: 'posts'},[{date:-1}]) * * @method getCollection * @param {String} value * @return {Object} collection */ getCollection(value) { var collection, j, k, len, len1, ref, ref1; if (value) { if (typeof value === 'string') { if (value === 'database') { return this.getDatabase(); } else { ref = this.collections; for (j = 0, len = ref.length; j < len; j++) { collection = ref[j]; if (value === collection.options.name || value === collection.options.key) { return collection; } } } } else { ref1 = this.collections; for (k = 0, len1 = ref1.length; k < len1; k++) { collection = ref1[k]; if (value === collection) { return collection; } } } } return null; } /** * Destroy a collection by collection name or key * @method destroyCollection * @param {String} value * @return {Object} description */ destroyCollection(value) { if (value) { if (typeof value === 'string' && value !== 'database') { this.collections = this.collections.filter(function(collection) { if (value === collection.options.name || value === collection.options.key) { if (collection != null) { collection.destroy(); } return false; } else { return true; } }); } else if (value !== this.getDatabase()) { this.collections = this.collections.filter(function(collection) { if (value === collection) { if (collection != null) { collection.destroy(); } return false; } else { return true; } }); } } return null; } /** * Add a collection * @method addCollection * @param {Object} collection */ addCollection(collection) { if (collection && (collection !== this.getDatabase() && collection !== this.getCollection(collection))) { this.collections.push(collection); } return this; } /** * Set a name for a collection. * A collection can have multiple names * * The partials plugin (https://github.com/docpad/docpad-plugin-partials) * creates a live collection and passes this to setCollection with * the name 'partials'. * * # Add our partials collection * docpad.setCollection('partials', database.createLiveChildCollection() * .setQuery('isPartial', { * $or: * isPartial: true * fullPath: $startsWith: config.partialsPath * }) * .on('add', (model) -> * docpad.log('debug', util.format(locale.addingPartial, model.getFilePath())) * model.setDefaults( * isPartial: true * render: false * write: false * ) * ) * ) * * * @method setCollection * @param {String} name the name to give to the collection * @param {Object} collection a DocPad collection */ setCollection(name, collection) { if (collection) { if (name) { collection.options.name = name; if (this.getCollection(name) !== collection) { this.destroyCollection(name); } } return this.addCollection(collection); } else { return this.destroyCollection(name); } } /** * Get the DocPad project's collections * @method getCollections * @return {Object} the collections */ getCollections() { return this.collections; } /** * Set the DocPad project's collections * @method setCollections */ setCollections(collections) { var j, len, name, value; if (Array.isArray(collections)) { for (j = 0, len = collections.length; j < len; j++) { value = collections[j]; this.addCollection(value); } } else { for (name in collections) { if (!hasProp.call(collections, name)) continue; value = collections[name]; this.setCollection(name, value); } } return this; } /** * Apply the passed function to each collection * @method eachCollection * @param {Function} fn */ eachCollection(fn) { var collection, index, j, len, ref; fn(this.getDatabase(), 'database'); ref = this.collections; for (index = j = 0, len = ref.length; j < len; index = ++j) { collection = ref[index]; fn(collection, collection.options.name || collection.options.key || index); } return this; } /** * Destructor. Destroy the DocPad project's collections. * @private * @method destroyCollections */ destroyCollections() { var collection, j, len, ref; if (this.collections) { ref = this.collections; for (j = 0, len = ref.length; j < len; j++) { collection = ref[j]; collection.destroy(); } this.collections = []; } return this; } // --------------------------------- // Collection Helpers /** * Get all the files in the DocPad database (will use live collections) * @method getFiles * @param {Object} query * @param {Object} sorting * @param {Object} paging * @return {Object} collection */ getFiles(query, sorting, paging) { var collection, key; key = JSON.stringify({query, sorting, paging}); collection = this.getCollection(key); if (!collection) { collection = this.getDatabase().findAllLive(query, sorting, paging); collection.options.key = key; this.addCollection(collection); } return collection; } /** * Get a single file based on a query * @method getFile * @param {Object} query * @param {Object} sorting * @param {Object} paging * @return {Object} a file */ getFile(query, sorting, paging) { var file; file = this.getDatabase().findOne(query, sorting, paging); return file; } /** * Get files at a path * @method getFilesAtPath * @param {String} path * @param {Object} sorting * @param {Object} paging * @return {Object} files */ getFilesAtPath(path, sorting, paging) { var files, query; query = { $or: [ { relativePath: { $startsWith: path } }, { fullPath: { $startsWith: path } } ] }; files = this.getFiles(query, sorting, paging); return files; } /** * Get a file at a relative or absolute path or url * @method getFileAtPath * @param {String} path * @param {Object} sorting * @param {Object} paging * @return {Object} a file */ getFileAtPath(path, sorting, paging) { var file; file = this.getDatabase().fuzzyFindOne(path, sorting, paging); return file; } /** * Get a file by its id * @method getFileById * @param {String} id * @param {Object} [opts={}] * @return {Object} a file */ getFileById(id, opts = {}) { var file; if (opts.collection == null) { opts.collection = this.getDatabase(); } file = opts.collection.get(id); return file; } /** * Remove the query string from a url * Pathname convention taken from document.location.pathname * @method getUrlPathname * @param {String} url * @return {String} */ getUrlPathname(url) { return url.replace(/\?.*/, ''); } /** * Get a file by its selector (this is used to fetch layouts by their name) * @method getFileBySelector * @param {Object} selector * @param {Object} [opts={}] * @return {Object} a file */ getFileBySelector(selector, opts = {}) { var file; if (opts.collection == null) { opts.collection = this.getDatabase(); } file = opts.collection.fuzzyFindOne(selector); return file; } /** * Get Complete Template Data * @method getTemplateData * @param {Object} userTemplateData * @return {Object} templateData */ getTemplateData(userTemplateData) { var base, base1, base2, docpad, locale, templateData; // Prepare userTemplateData || (userTemplateData = {}); docpad = this; locale = this.getLocale(); // Set the initial docpad template data if (this.initialTemplateData == null) { this.initialTemplateData = { // Site Properties site: {}, // Environment getEnvironment: function() { return docpad.getEnvironment(); }, // Environments getEnvironments: function() { return docpad.getEnvironments(); }, // Set that we reference other files referencesOthers: function(flag) { var document; document = this.getDocument(); document.referencesOthers(); return null; }, // Get the Document getDocument: function() { return this.documentModel; }, // Get a Path in respect to the current document getPath: function(path, parentPath) { var document; document = this.getDocument(); path = document.getPath(path, parentPath); return path; }, // Get Files getFiles: function(query, sorting, paging) { var result; this.referencesOthers(); result = docpad.getFiles(query, sorting, paging); return result; }, // Get another file's URL based on a relative path getFile: function(query, sorting, paging) { var result; this.referencesOthers(); result = docpad.getFile(query, sorting, paging); return result; }, // Get Files At Path getFilesAtPath: function(path, sorting, paging) { var result; this.referencesOthers(); path = this.getPath(path); result = docpad.getFilesAtPath(path, sorting, paging); return result; }, // Get another file's model based on a relative path getFileAtPath: function(relativePath) { var path, result; this.referencesOthers(); path = this.getPath(relativePath); result = docpad.getFileAtPath(path); return result; }, // Get a specific file by its id getFileById: function(id) { var result; this.referencesOthers(); result = docpad.getFileById(id); return result; }, // Get the entire database getDatabase: function() { this.referencesOthers(); return docpad.getDatabase(); }, // Get a pre-defined collection getCollection: function(name) { this.referencesOthers(); return docpad.getCollection(name); }, // Get a block getBlock: function(name) { return docpad.getBlock(name, true); }, // Include another file taking in a relative path include: function(subRelativePath, strict = true) { var err, file; file = this.getFileAtPath(subRelativePath); if (file) { if (strict && file.get('rendered') === false) { if (docpad.getConfig().renderPasses === 1) { docpad.warn(util.format(locale.renderedEarlyViaInclude, subRelativePath)); } return null; } return file.getOutContent(); } else { err = new Errlop(util.format(locale.includeFailed, subRelativePath)); throw err; } } }; } // Fetch our result template data templateData = extendr.extend({}, this.initialTemplateData, this.pluginsTemplateData, this.getConfig().templateData, userTemplateData); // Add site data (base = templateData.site).url || (base.url = ''); (base1 = templateData.site).date || (base1.date = new Date()); (base2 = templateData.site).keywords || (base2.keywords = []); if (typeChecker.isString(templateData.site.keywords)) { templateData.site.keywords = templateData.site.keywords.split(/,\s*/g); } // Return return templateData; } /** * Get the locale (language code and locale code) * @method getLocale * @return {Object} locale */ getLocale(key) { var err, localeError, locales; if (this.locale == null) { try { locales = this.getPath('locales').map(function(locale) { return require(locale); }); this.locale = extendr.extend(...locales); } catch (error1) { localeError = error1; docpad.warn(new Errlop('Failed to load a locale', localeError)); try { this.locale = require(this.getPath('locale')); } catch (error1) { err = error1; docpad.fatal(new Errlop('Failed to load any locale', err)); this.locale = {}; } } } if (key) { return this.locale[key] || key; } else { return this.locale; } } // ----------------------------- // Environments /** * Get the DocPad environment, eg: development, * production or static * @method getEnvironment * @return {String} the environment */ getEnvironment() { return this.env; } /** * Get the environments * @method getEnvironments * @return {Array} array of environment strings */ getEnvironments() { return this.envs; } /** * Get the DocPad configuration * @method getConfig * @return {Object} the DocPad configuration object */ getConfig() { return this.config || {}; } // ================================= // Initialization Functions /** * Create our own custom TaskGroup instance for DocPad. * That will listen to tasks as they execute and provide debugging information. * @method createTaskGroup * @param {Object} opts * @return {TaskGroup} */ createTaskGroup(...opts) { var docpad, progress, tasks; boundMethodCheck(this, DocPad); docpad = this; progress = docpad.progressInstance; tasks = TaskGroup.create(...opts); // Listen to executing tasks and output their progress tasks.on('running', function() { var config, name, totals; config = tasks.getConfig(); name = tasks.getNames(); if (progress) { totals = tasks.getItemTotals(); return progress.update(name, totals); } else { return docpad.log('debug', name + ' > running'); } }); // Listen to executing tasks and output their progress tasks.on('item.add', function(item) { var config, name; config = tasks.getConfig(); name = item.getNames(); if (!progress) { docpad.log('debug', name + ' > added'); } // Listen to executing tasks and output their progress item.on('started', function(item) { var totals; config = tasks.getConfig(); name = item.getNames(); if (progress) { totals = tasks.getItemTotals(); return progress.update(name, totals); } else { return docpad.log('debug', name + ' > started'); } }); // Listen to executing tasks and output their progress return item.done(function(err) { var totals; config = tasks.getConfig(); name = item.getNames(); if (progress) { totals = tasks.getItemTotals(); return progress.update(name, totals); } else { return docpad.log('debug', name + ' > done'); } }); }); // Return return tasks; } /** * Constructor method. Sets up the DocPad instance. * next(err) * @method constructor * @param {Object} instanceConfig * @param {Function} next callback * @param {Error} next.err * @param {DocPad} next.docpad */ constructor(instanceConfig, next) { var color, configEventContext, docpad, filter, j, len, lineLevel, logPath, logger, methodName, ref; super(); this.createTaskGroup = this.createTaskGroup.bind(this); [instanceConfig, next] = extractOptsAndCallback(instanceConfig, next); docpad = this; // Allow DocPad to have unlimited event listeners this.setMaxListeners(0); ref = "action log warn error fatal inspect notify checkRequest activeHandles onBeforeExit onSignalInterruptOne onSignalInterruptTwo onSignalInterruptThree destroyWatchers".split(/\s+/); // Binders // Using this over coffescript's => on class methods, ensures that the method length is kept for (j = 0, len = ref.length; j < len; j++) { methodName = ref[j]; this[methodName] = this[methodName].bind(this); } // Adjust configPaths if (typeChecker.isString(instanceConfig.configPaths)) { instanceConfig.configPaths = [instanceConfig.configPaths]; } // Dereference and initialise advanced variables // we deliberately ommit initialTemplateData here, as it is setup in getTemplateData this.slowPlugins = {}; this.loadedPlugins = {}; this.pluginsTemplateData = {}; this.collections = []; this.blocks = {}; this.websitePackageConfig = {}; this.websiteConfig = {}; this.userConfig = {}; this.initialConfig = extendr.dereferenceJSON(this.initialConfig); this.instanceConfig = instanceConfig || {}; this.config = this.mergeConfigs(); // Prepare the loggers if (instanceConfig.logLevel == null) { instanceConfig.logLevel = this.initialConfig.logLevel; } color = instanceConfig.color; lineLevel = -1; if (instanceConfig.silent) { instanceConfig.logLevel = 3; // 3:error, 2:critical, 1:alert, 0:emergency instanceConfig.progress = instanceConfig.welcome = false; } if (instanceConfig.verbose || instanceConfig.debug) { instanceConfig.logLevel = 7; lineLevel = 7; } // Create the loggers logger = new Logger({ lineLevel: lineLevel }); filter = new Filter({ filterLevel: instanceConfig.logLevel }); // Apply the loggers this.logger = logger; // Console logger.pipe(filter).pipe(new Human({ color: color })).pipe(process.stdout); // File if (instanceConfig.debug) { logPath = this.getPath(false, 'log'); safefs.unlink(logPath, function() { return logger.pipe(new Human({ color: false })).pipe(fsUtil.createWriteStream(logPath)); }); } // Forward log events to the logger this.on('log', function(...args) { return docpad.log.apply(this, args); }); // Setup configuration event wrappers configEventContext = {docpad}; // here to allow the config event context to persist between event calls this.getEvents().forEach(function(eventName) { // Bind to the event return docpad.on(eventName, function(opts, next) { var args, eventHandler, ref1; eventHandler = (ref1 = docpad.getConfig().events) != null ? ref1[eventName] : void 0; // Fire the config event handler for this event, if it exists if (typeChecker.isFunction(eventHandler)) { args = [opts, next]; return ambi(unbounded.binder.call(eventHandler, configEventContext), ...args); } else { // It doesn't exist, so lets continue return next(); } }); }); // Create our action runner this.actionRunnerInstance = this.createTaskGroup('action runner', { abortOnError: false, destroyOnceDone: false }).whenDone(function(err) { var ref1; if ((ref1 = docpad.progressInstance) != null) { ref1.update(''); } if (err) { return docpad.error(err); } }); // Setup the database this.database = new FilesCollection(null, { name: 'database' }).on('remove', function(model, options) { var outPath, updatedModels; // Skip if we are not a writeable file if (model.get('write') === false) { return; } // Ensure we regenerate anything (on the next regeneration) that was using the same outPath outPath = model.get('outPath'); if (outPath) { updatedModels = docpad.database.findAll({outPath}); updatedModels.remove(model); if (updatedModels.length) { updatedModels.each(function(model) { return model.set({ 'mtime': new Date() }); }); docpad.log('info', 'Updated mtime for these models due to the removal of a similar one:', updatedModels.pluck('relativePath')); } } // Return safely return true; }).on('add change:outPath', function(model) { var existingModels, outPath, previousModels, previousOutPath; // Skip if we are not a writeable file if (model.get('write') === false) { return; } // Prepare outPath = model.get('outPath'); previousOutPath = model.previous('outPath'); // Check if we have changed our outPath if (previousOutPath) { // Ensure we regenerate anything (on the next regeneration) that was using the same outPath previousModels = docpad.database.findAll({ outPath: previousOutPath }); previousModels.remove(model); if (previousModels.length) { previousModels.each(function(previousModel) { return previousModel.set({ 'mtime': new Date() }); }); docpad.log('info', 'Updated mtime for these models due to the addition of a similar one:', previousModels.pluck('relativePath')); } } // Determine if there are any conflicts with the new outPath if (outPath) { existingModels = docpad.database.findAll({outPath}); existingModels.each(function(existingModel) { var existingModelPath, modelPath; if (existingModel.id !== model.id) { modelPath = model.get('fullPath') || (model.get('relativePath') + ':' + model.id); existingModelPath = existingModel.get('fullPath') || (existingModel.get('relativePath') + ':' + existingModel.id); return docpad.warn(util.format(docpad.getLocale().outPathConflict, outPath, modelPath, existingModelPath)); } }); } // Return safely return true; }); // Continue with load and ready this.action('load ready', {}, function(err) { if (next != null) { return next(err, docpad); } else if (err) { return docpad.fatal(err); } }); // Chain this; } /** * Destructor. Destroy the DocPad instance * This is an action, and should be called as such * E.g. docpad.action('destroy', next) * @method destroy * @param {Object} opts * @param {Function} next * @param {Error} next.err */ destroy(opts, next) { var config, docpad, dropped, locale; if (this.destroying) { return this; } this.destroying = true; // Prepare [opts, next] = extractOptsAndCallback(opts, next); docpad = this; config = this.getConfig(); locale = this.getLocale(); // Log docpad.log('info', locale.destroyDocPad); // Drop all the remaining tasks dropped = this.getActionRunner().clearRemaining(); if (dropped) { docpad.error(`DocPad destruction had to drop ${Number(dropped)} action tasks`); } // Destroy Timers docpad.destroyTimers(); // Wait a configurable oment docpad.timer('destroy', 'timeout', config.destroyDelay, function() { // Destroy Plugins return docpad.emitSerial('docpadDestroy', function(eventError) { var err, finalError; // Check if (eventError) { // Note err = new Errlop("DocPad's destroyEvent event failed", eventError); docpad.fatal(err); return typeof next === "function" ? next(err) : void 0; } try { // Destroy Timers // Final closures and checks docpad.destroyTimers(); // Destroy Plugins docpad.destroyPlugins(); // Destroy Watchers docpad.destroyWatchers(); // Destroy Blocks docpad.destroyBlocks(); // Destroy Collections docpad.destroyCollections(); // Destroy Database docpad.destroyDatabase(); // Destroy progress docpad.updateProgress(false); // Destroy Logging docpad.destroyLoggers(); // Destroy Process Listeners process.removeListener('uncaughtException', docpad.fatal); process.removeListener('uncaughtException', docpad.error); process.removeListener('beforeExit', docpad.onBeforeExit); process.removeListener('SIGINT', docpad.onSignalInterruptOne); process.removeListener('SIGINT', docpad.onSignalInterruptTwo); process.removeListener('SIGINT', docpad.onSignalInterruptThree); // Destroy DocPad Listeners docpad.removeAllListeners(); } catch (error1) { finalError = error1; // Note err = new Errlop("DocPad's final destruction efforts failed", finalError); docpad.fatal(err); return typeof next === "function" ? next(err) : void 0; } // Success docpad.log(locale.destroyedDocPad); // log level omitted, as this will hit console.log return typeof next === "function" ? next() : void 0; }); }); return this; } /** * Emit event, serial * @private * @method emitSerial * @param {String} eventName * @param {Object} opts * @param {Function} next * @param {Error} next.err */ emitSerial(eventName, opts, next) { var docpad, locale; // Prepare [opts, next] = extractOptsAndCallback(opts, next); docpad = this; locale = docpad.getLocale(); // Log docpad.log('debug', util.format(locale.emittingEvent, eventName)); // Emit super.emitSerial(eventName, opts, function(err) { if (err) { // Check return next(err); } // Log docpad.log('debug', util.format(locale.emittedEvent, eventName)); // Forward return next(err); }); return this; } /** * Emit event, parallel * @private * @method emitParallel * @param {String} eventName * @param {Object} opts * @param {Function} next * @param {Error} next.err */ emitParallel(eventName, opts, next) { var docpad, locale; // Prepare [opts, next] = extractOptsAndCallback(opts, next); docpad = this; locale = docpad.getLocale(); // Log docpad.log('debug', util.format(locale.emittingEvent, eventName)); // Emit super.emitParallel(eventName, opts, function(err) { if (err) { // Check return next(err); } // Log docpad.log('debug', util.format(locale.emittedEvent, eventName)); // Forward return next(err); }); return this; } // ================================= // Helpers /** * Get the ignore options for the DocPad project * @method getIgnoreOpts * @return {Array} string array of ignore options */ getIgnoreOpts() { return pick(this.config, ['ignorePaths', 'ignoreHiddenFiles', 'ignoreCommonPatterns', 'ignoreCustomPatterns']); } /** * Is the supplied path ignored? * @method isIgnoredPath * @param {String} path * @param {Object} [opts={}] * @return {Boolean} */ isIgnoredPath(path, opts = {}) { opts = extendr.extend(this.getIgnoreOpts(), opts); return ignorefs.isIgnoredPath(path, opts); } /** * Scan directory * @method scandir * @param {Object} [opts={}] */ //NB: How does this work? What is returned? //Does it require a callback (next) passed as //one of the options scandir(opts = {}) { opts = extendr.extend(this.getIgnoreOpts(), opts); return scandir(opts); } /** * Watch Directory. Wrapper around the Bevry watchr * module (https://github.com/bevry/watchr). Used * internally by DocPad to watch project documents * and files and then activate the regeneration process * when any of those items are updated. * @private * @method watchdir * @param {String} path - the path to watch * @param {Object} listeners - listeners to attach to the watcher * @param {Function} next - completion callback accepting error * @return {Object} the watcher */ watchdir(path, listeners, next) { var key, opts, stalker, value; opts = extendr.extend(this.getIgnoreOpts(), this.config.watchOptions || {}); stalker = require('watchr').create(path); for (key in listeners) { if (!hasProp.call(listeners, key)) continue; value = listeners[key]; stalker.on(key, value); } stalker.setConfig(opts); stalker.watch(next); return stalker; } /** * Watch Directories. Wrapper around watchdir. * @private * @method watchdirs * @param {Array} paths - the paths to watch * @param {Object} listeners - listeners to attach to the watcher * @param {Function} next - completion callback accepting error and watchers/stalkers */ watchdirs(paths, listeners, next) { var docpad, stalkers, tasks; docpad = this; stalkers = []; tasks = new TaskGroup('watching directories').setConfig({ concurrency: 0 }).done(function(err) { var j, len, stalker; if (err) { for (j = 0, len = stalkers.length; j < len; j++) { stalker = stalkers[j]; stalker.close(); } return next(err); } else { return next(err, stalkers); } }); paths.forEach(function(path) { return tasks.addTask(`watching ${path}`, function(done) { // check if the dir exists first as reloadPaths may not apparently return safefs.exists(path, function(exists) { if (!exists) { return done(); } return stalkers.push(docpad.watchdir(path, listeners, done)); }); }); }); tasks.run(); return this; } // ================================= // Setup and Loading /** * DocPad is ready. Peforms the tasks needed after DocPad construction * and DocPad has loaded. Triggers the docpadReady event. * next(err,docpadInstance) * @private * @method ready * @param {Object} [opts] * @param {Function} next * @param {Error} next.err * @param {Object} next.docpadInstance */ ready(opts, next) { var config, docpad, instanceConfig, locale, pluginsList, tasks; // Prepare [instanceConfig, next] = extractOptsAndCallback(instanceConfig, next); docpad = this; config = this.getConfig(); locale = this.getLocale(); // Render Single Extensions this.DocumentModel.prototype.defaults.renderSingleExtensions = config.renderSingleExtensions; // Fetch the plugins pluginsList = Object.keys(this.loadedPlugins).sort().join(', '); // Welcome Output docpad.log('info', util.format(locale.welcome, this.getVersionString())); docpad.log('notice', locale.welcomeDonate); docpad.log('info', locale.welcomeContribute); docpad.log('info', util.format(locale.welcomePlugins, pluginsList)); docpad.log('info', util.format(locale.welcomeEnvironment, this.getEnvironment())); // Prepare tasks = this.createTaskGroup('ready tasks').done(function(err) { if (err) { // Error? return docpad.error(err); } return typeof next === "function" ? next(null, docpad) : void 0; }); // kept here in case plugins use it tasks.addTask('welcome event', function(complete) { if (!config.welcome) { // No welcome return complete(); } // Welcome return docpad.emitSerial('welcome', {docpad}, complete); }); tasks.addTask('emit docpadReady', function(complete) { return docpad.emitSerial('docpadReady', {docpad}, complete); }); // Run tasks tasks.run(); return this; } /** * Performs the merging of the passed configuration objects * @private * @method mergeConfigs */ mergeConfigs(configPackages, destination = {}) { var configPackage, configsToMerge, env, envConfig, j, k, len, len1, ref, ref1; // A plugin is calling us with its configuration if (!configPackages) { // Apply the environment // websitePackageConfig.env is left out of the detection here as it is usually an object // that is already merged with our process.env by the environment runner // rather than a string which is the docpad convention this.env = this.instanceConfig.env || this.websiteConfig.env || this.initialConfig.env || process.env.NODE_ENV || 'development'; this.envs = this.env.split(/[, ]+/); // Merge the configurations together configPackages = [this.initialConfig, this.userConfig, this.websiteConfig, this.instanceConfig]; } // Figure out merging configsToMerge = [destination]; for (j = 0, len = configPackages.length; j < len; j++) { configPackage = configPackages[j]; if (!configPackage) { continue; } configsToMerge.push(configPackage); ref = this.envs; for (k = 0, len1 = ref.length; k < len1; k++) { env = ref[k]; envConfig = (ref1 = configPackage.environments) != null ? ref1[env] : void 0; if (envConfig) { configsToMerge.push(envConfig); } } } // Merge return extendr.deep(...configsToMerge); } /** * Legacy version of mergeConmergeConfigsfigurations * @private * @method mergeConfigurations */ mergeConfigurations(configPackages, [destination]) { return this.mergeConfigs(configPackages, destination); } /** * Set the DocPad configuration object. * Performs a number of tasks, including * merging the pass instanceConfig with DocPad's * other config objects. * next(err,config) * @private * @method setConfig * @param {Object} instanceConfig * @param {Object} next * @param {Error} next.err * @param {Object} next.config */ setConfig(instanceConfig) { var docpad, locale, next; // Prepare [instanceConfig, next] = extractOptsAndCallback(instanceConfig, next); docpad = this; locale = this.getLocale(); if (instanceConfig) { // Apply the instance configuration, generally we won't have it at this level // as it would have been applied earlier the load step extendr.deepDefaults(this.instanceConfig, instanceConfig); } // Merge the configurations together this.config = this.mergeConfig