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.
2,036 lines (1,707 loc) • 129 kB
text/coffeescript
##*
# The central module for DocPad
# @module DocPad
##
# =====================================
# Requires
# Standard
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 = (i) -> Boolean(i)
###*
# 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
###
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()`
@create: (args...) -> return new @(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
@require: (name) ->
if name is '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")
# =================================
# Variables
# ---------------------------------
# Modules
# ---------------------------------
# Base
###*
# Events class
# @property {Object} Events
###
Events: Events
###*
# Model class
# Extension of the Backbone Model class
# http://backbonejs.org/#Model
# @property {Object} Model
###
Model: Model
###*
# Collection class
# Extension of the Backbone Collection class
# http://backbonejs.org/#Collection
# @property {Object} Collection
###
Collection: Collection
###*
# QueryCollection class
# Extension of the Query Engine QueryCollection class
# @property {Object} QueryCollection
###
QueryCollection: QueryCollection
# ---------------------------------
# Models
###*
# File Model class
# Extension of the Model class
# @property {Object} FileModel
###
FileModel: FileModel
###*
# Document Model class
# Extension of the File Model class
# @property {Object} DocumentModel
###
DocumentModel: DocumentModel
# ---------------------------------
# Collections
###*
# Collection of files in a DocPad project
# Extension of the QueryCollection class
# @property {Object} FilesCollection
###
FilesCollection: FilesCollection
###*
# Collection of elements in a DocPad project
# Extension of the Collection class
# @property {Object} ElementsCollection
###
ElementsCollection: ElementsCollection
###*
# Collection of metadata in a DocPad project
# Extension of the ElementsCollection class
# @property {Object} MetaCollection
###
MetaCollection: MetaCollection
###*
# Collection of JS script files in a DocPad project
# Extension of the ElementsCollection class
# @property {Object} ScriptsCollection
###
ScriptsCollection: ScriptsCollection
###*
# Collection of CSS style files in a DocPad project
# Extension of the ElementsCollection class
# @property {Object} StylesCollection
###
StylesCollection: StylesCollection
# ---------------------------------
# DocPad
###*
# DocPad's version number
# @private
# @property {Number} version
###
version: null
###*
# Get the DocPad version number
# @method getVersion
# @return {Number}
###
getVersion: ->
@version ?= require(@packagePath).version
return @version
###*
# Get the DocPad version string
# @method getVersionString
# @return {String}
###
getVersionString: ->
if docpadUtil.isLocalDocPadExecutable()
return util.format(@getLocale().versionLocal, @getVersion(), @corePath)
else
return util.format(@getLocale().versionGlobal, @getVersion(), @corePath)
###*
# The plugin version requirements
# @property {String} pluginVersion
###
pluginVersion: '2'
# Process getters
###*
# Get the process platform
# @method getProcessPlatform
# @return {Object}
###
getProcessPlatform: -> process.platform
###*
# Get the process version string
# @method getProcessVersion
# @return {String}
###
getProcessVersion: -> process.version.replace(/^v/,'')
###*
# Internal property. The caterpillar logger instance bound to DocPad
# @private
# @property {Object} loggerInstances
###
logger: null
###*
# Get the caterpillar logger instance bound to DocPad
# @method getLogger
# @return {Object} caterpillar logger
###
getLogger: -> @logger
###*
# Destructor. Destroy the caterpillar logger instances bound to DocPad
# @private
# @method {Object} destroyLoggers
###
destroyLoggers: ->
# @logger.end()
@logger = null
@
###*
# All the timers that exist within DocPad
# Used for closing them at shutdown
# @private
# @property {Object} timers
###
timers: null
###*
# 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) ->
@timers ?= {}
# Create a new timer
if type?
@timer(id) # clear
if type is 'timeout'
if time is -1
timer = setImmediate(method)
else
timer = setTimeout(method, time)
else if type is 'interval'
timer = setInterval(method, time)
else
throw new Errlop('unexpected type on new timer')
@timers[id] = {id, type, time, method, timer}
# Destroy an old timer
else if @timers[id]
if @timers[id].type is 'interval'
clearInterval(@timers[id].timer)
else if @timers[id].type is 'timeout'
if @timers[id].time is -1
clearImmediate?(@timers[id].timer)
else
clearTimeout(@timers[id].timer)
else
throw new Errlop('unexpected type on stored timer')
@timers[id] = null
@
###*
# Destructor. Destroy all the timers we have kept.
# @private
# @method {Object} destroyTimers
###
destroyTimers: (timer) ->
@timers ?= {}
for own key, value of @timers
@timer(key)
@
###*
# Instance of progress-title
# @private
# @property {Progress} progressInstance
###
progressInstance: null
###*
# 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) ->
# Prepare
docpad = @
config = docpad.getConfig()
debug = @getDebugging()
# Enabled
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 @progressInstance
@progressInstance.pause().configure(options).resume()
else
@progressInstance = Progress.create(options).start()
else if @progressInstance
@progressInstance.stop().configure(options)
# Return
return this
###*
# The action runner instance bound to docpad
# @private
# @property {Object} actionRunnerInstance
###
actionRunnerInstance: null
###*
# Get the action runner instance bound to docpad
# @method getActionRunner
# @return {Object} the action runner instance
###
getActionRunner: -> @actionRunnerInstance
###*
# Apply the passed DocPad action arguments
# @method {Object} action
# @param {Object} args
# @return {Object}
###
action: (action, opts, next) ->
# Prepare
[opts,next] = extractOptsAndCallback(opts,next)
locale = @getLocale()
# Log
@progressInstance?.resume()
@log 'debug', util.format(locale.actionStart, action)
# Act
docpadUtil.action.call @, action, opts, (args...) =>
# Prepare
err = args[0]
# Log
@progressInstance?.stop()
if err
@error(new Errlop(
util.format(locale.actionFailure, action),
err
))
else
@log 'debug', util.format(locale.actionSuccess, action)
# Act
return next?(args...)
# Chain
@
###*
# Event Listing. String array of event names.
# Whenever an event is created, it must be applied here to be available to plugins and configuration files
# Events must be sorted by the order of execution, not for a functional need, but for a documentation need
# Whenever this array changes, also update: https://docpad.bevry.me/events
# @private
# @property {Array} string array of event names
###
events: [
'loadPlugins' # fired each load
'extendCollections' # fired each load
'extendTemplateData' # fired each load
'docpadReady' # fired only once
'docpadDestroy' # fired once on shutdown
'consoleSetup' # fired once
'runBefore'
'runAfter'
'generateBefore'
'populateCollectionsBefore'
'populateCollections'
'contextualizeBefore'
'contextualizeAfter'
'renderBefore'
'renderCollectionBefore'
'render' # fired for each extension conversion
'renderDocument' # fired for each document render, including layouts and render passes
'renderCollectionAfter'
'renderAfter'
'writeBefore'
'writeAfter'
'generateAfter'
'generated'
'notify'
]
###*
# Get the list of available events
# @method getEvents
# @return {Object} string array of event names
###
getEvents: ->
@events
# ---------------------------------
# Collections
# Database collection
###*
# QueryEngine collection
# @private
# @property {Object} database
###
database: null
###*
# Description for getDatabase
# @method {Object} getDatabase
###
getDatabase: -> @database
###*
# Destructor. Destroy the DocPad database
# @private
# @method destroyDatabase
###
destroyDatabase: ->
if @database?
@database.destroy()
@database = null
@
###*
# Blocks
# @private
# @property {Object} blocks
###
blocks: null
### {
# 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) ->
block = @blocks[name]
if clone
classname = name[0].toUpperCase()+name[1..]+'Collection'
block = new @[classname](block.models)
return block
###*
# Set a block by name and value
# @method setBlock
# @param {String} name
# @param {Object} value
###
setBlock: (name,value) ->
if @blocks[name]?
@blocks[name].destroy()
if value
@blocks[name] = value
else
delete @blocks[name]
else
@blocks[name] = value
@
###*
# Get all blocks
# @method getBlocks
# @return {Object} collection of blocks
###
getBlocks: -> @blocks
###*
# Set all blocks
# @method setBlocks
# @param {Object} blocks
###
setBlocks: (blocks) ->
for own name,value of blocks
@setBlock(name,value)
@
###*
# Apply the passed function to each block
# @method eachBlock
# @param {Function} fn
###
eachBlock: (fn) ->
eachr(@blocks, fn)
@
###*
# Destructor. Destroy all blocks
# @private
# @method destroyBlocks
###
destroyBlocks: ->
if @blocks
for own name,block of @blocks
block.destroy()
@blocks[name] = null
@
###*
# The DocPad collections
# @private
# @property {Object} collections
###
collections: null
###*
# 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) ->
if value
if typeof value is 'string'
if value is 'database'
return @getDatabase()
else
for collection in @collections
if value in [collection.options.name, collection.options.key]
return collection
else
for collection in @collections
if value is 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 is 'string' and value isnt 'database'
@collections = @collections.filter (collection) ->
if value in [collection.options.name, collection.options.key]
collection?.destroy()
return false
else
return true
else if value isnt @getDatabase()
@collections = @collections.filter (collection) ->
if value is collection
collection?.destroy()
return false
else
return true
return null
###*
# Add a collection
# @method addCollection
# @param {Object} collection
###
addCollection: (collection) ->
if collection and collection not in [@getDatabase(), @getCollection(collection)]
@collections.push(collection)
@
###*
# 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 @getCollection(name) isnt collection
@destroyCollection(name)
@addCollection(collection)
else
@destroyCollection(name)
###*
# Get the DocPad project's collections
# @method getCollections
# @return {Object} the collections
###
getCollections: ->
return @collections
###*
# Set the DocPad project's collections
# @method setCollections
###
setCollections: (collections) ->
if Array.isArray(collections)
for value in collections
@addCollection(value)
else
for own name,value of collections
@setCollection(name, value)
@
###*
# Apply the passed function to each collection
# @method eachCollection
# @param {Function} fn
###
eachCollection: (fn) ->
fn(@getDatabase(), 'database')
for collection,index in @collections
fn(collection, collection.options.name or collection.options.key or index)
@
###*
# Destructor. Destroy the DocPad project's collections.
# @private
# @method destroyCollections
###
destroyCollections: ->
if @collections
for collection in @collections
collection.destroy()
@collections = []
@
# ---------------------------------
# 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) ->
key = JSON.stringify({query, sorting, paging})
collection = @getCollection(key)
unless collection
collection = @getDatabase().findAllLive(query, sorting, paging)
collection.options.key = key
@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) ->
file = @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) ->
query = $or: [{relativePath: $startsWith: path}, {fullPath: $startsWith: path}]
files = @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) ->
file = @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={}) ->
opts.collection ?= @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={}) ->
opts.collection ?= @getDatabase()
file = opts.collection.fuzzyFindOne(selector)
return file
# ---------------------------------
# Plugins
###*
# Plugins that are loading really slow
# @property {Object} slowPlugins
###
slowPlugins: null # {}
###*
# Loaded plugins indexed by name
# @property {Object} loadedPlugins
###
loadedPlugins: null # {}
# -----------------------------
# Paths
###*
# The DocPad directory
# @property {String} corePath
###
corePath: pathUtil.resolve(__dirname, '..', '..')
###*
# The DocPad library directory
# @private
# @property {String} libPath
###
libPath: __dirname
###*
# The main DocPad file
# @property {String} mainPath
###
mainPath: pathUtil.resolve(__dirname, 'docpad')
###*
# The DocPad package.json path
# @property {String} packagePath
###
packagePath: pathUtil.resolve(__dirname, '..', '..', 'package.json')
###*
# The DocPad locale path
# @property {String} localePath
###
localePath: pathUtil.resolve(__dirname, 'locale')
# -----------------------------
# Template Data
###*
# Description for initialTemplateData
# @private
# @property {Object} initialTemplateData
###
initialTemplateData: null # {}
###*
# Plugin's Extended Template Data
# @private
# @property {Object} pluginsTemplateData
###
pluginsTemplateData: null # {}
###*
# Get Complete Template Data
# @method getTemplateData
# @param {Object} userTemplateData
# @return {Object} templateData
###
getTemplateData: (userTemplateData) ->
# Prepare
userTemplateData or= {}
docpad = @
locale = @getLocale()
# Set the initial docpad template data
@initialTemplateData ?=
# Site Properties
site: {}
# Environment
getEnvironment: ->
return docpad.getEnvironment()
# Environments
getEnvironments: ->
return docpad.getEnvironments()
# Set that we reference other files
referencesOthers: (flag) ->
document = @getDocument()
document.referencesOthers()
return null
# Get the Document
getDocument: ->
return @documentModel
# Get a Path in respect to the current document
getPath: (path,parentPath) ->
document = @getDocument()
path = document.getPath(path, parentPath)
return path
# Get Files
getFiles: (query,sorting,paging) ->
@referencesOthers()
result = docpad.getFiles(query, sorting, paging)
return result
# Get another file's URL based on a relative path
getFile: (query,sorting,paging) ->
@referencesOthers()
result = docpad.getFile(query,sorting,paging)
return result
# Get Files At Path
getFilesAtPath: (path,sorting,paging) ->
@referencesOthers()
path = @getPath(path)
result = docpad.getFilesAtPath(path, sorting, paging)
return result
# Get another file's model based on a relative path
getFileAtPath: (relativePath) ->
@referencesOthers()
path = @getPath(relativePath)
result = docpad.getFileAtPath(path)
return result
# Get a specific file by its id
getFileById: (id) ->
@referencesOthers()
result = docpad.getFileById(id)
return result
# Get the entire database
getDatabase: ->
@referencesOthers()
return docpad.getDatabase()
# Get a pre-defined collection
getCollection: (name) ->
@referencesOthers()
return docpad.getCollection(name)
# Get a block
getBlock: (name) ->
return docpad.getBlock(name,true)
# Include another file taking in a relative path
include: (subRelativePath,strict=true) ->
file = @getFileAtPath(subRelativePath)
if file
if strict and file.get('rendered') is false
if docpad.getConfig().renderPasses is 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({}, @initialTemplateData, @pluginsTemplateData, @getConfig().templateData, userTemplateData)
# Add site data
templateData.site.url or= ''
templateData.site.date or= new Date()
templateData.site.keywords or= []
if typeChecker.isString(templateData.site.keywords)
templateData.site.keywords = templateData.site.keywords.split(/,\s*/g)
# Return
templateData
# -----------------------------
# Locales
###*
# Determined locale
# @private
# @property {Object} locale
###
locale: null
###*
# Get the locale (language code and locale code)
# @method getLocale
# @return {Object} locale
###
getLocale: (key) ->
unless @locale?
try
locales = @getPath('locales').map((locale) -> require(locale))
@locale = extendr.extend(locales...)
catch localeError
docpad.warn(new Errlop('Failed to load a locale', localeError))
try
@locale = require(@getPath('locale'))
catch err
docpad.fatal(new Errlop('Failed to load any locale', err))
@locale = {}
if key
return @locale[key] or key
else
return @locale
# -----------------------------
# Environments
###*
# Get the DocPad environment, eg: development,
# production or static
# @method getEnvironment
# @return {String} the environment
###
getEnvironment: ->
return @env
###*
# Get the environments
# @method getEnvironments
# @return {Array} array of environment strings
###
getEnvironments: ->
return @envs
# -----------------------------
# Configuration
###*
# Website Package Configuration
# @private
# @property {Object} websitePackageConfig
###
websitePackageConfig: null # {}
###*
# Merged Configuration
# Merged in the order of:
# - initialConfig
# - userConfig
# - websiteConfig
# - instanceConfig
# - environmentConfig
# Use getConfig to retrieve this value
# @private
# @property {Object} config
###
config: null # {}
###*
# Instance Configuration
# @private
# @property {Object} instanceConfig
###
instanceConfig: null # {}
###*
# Website Configuration
# Merged into the config property
# @private
# @property {Object} websiteConfig
###
websiteConfig: null # {}
###*
# User Configuraiton
# Merged into the config property
# @private
# @property {Object} userConfig
###
userConfig: null # {}
###*
# Initial Configuration. The default docpadConfig
# settings that can be overridden in a project's docpad.coffee file.
# Merged into the config property
# @private
# @property {Object} initialConfig
###
initialConfig:
# -----------------------------
# Plugins
# Whether or not we should use the global docpad instance
global: false
# Configuration to pass to any plugins pluginName: pluginConfiguration
plugins: {}
# -----------------------------
# Project Paths
# The project directory
rootPath: process.cwd()
# The project's package.json path
packagePath: 'package.json'
# The project's configuration paths
# Reads only the first one that exists
# If you want to read multiple configuration paths, then point it to a coffee|js file that requires
# the other paths you want and exports the merged config
configPaths: [
'docpad.js'
'docpad.coffee'
'docpad.json'
'docpad.cson'
]
# Plugin directories to load
pluginPaths: []
# Paths that we should watch for reload changes in
reloadPaths: []
# Paths that we should watch for regeneration changes in
regeneratePaths: []
# The DocPad debug log path (docpad-debug.log)
debugLogPath: 'docpad-debug.log'
# The User's configuration path (.docpad.cson)
userConfigPath: '.docpad.cson'
# -----------------------------
# Project Options
# The project's out directory
outPath: 'out'
# The project's source directory
sourcePaths: [
'source'
'src'
]
# The project's documents directories
# relative to the source path
documentsPaths: [
'documents'
'render'
]
# The project's files directories
# relative to the source path
filesPaths: [
'files'
'static'
'public'
]
# The project's layouts directory
# relative to the source path
layoutsPaths: [
'layouts'
]
# Ignored file patterns during directory parsing
ignorePaths: false
ignoreHiddenFiles: false
ignoreCommonPatterns: true
ignoreCustomPatterns: false
# Watch options
watchOptions: null
# -----------------------------
# Logging
# Log Level
# Which level of logging should we actually output
logLevel: 6
# Verbose
# Set log level to 7
verbose: false
# Debug
# Output all log messages to the debugLogPath
debug: false
# Color
# Whether or not our terminal output should have color
# `null` will default to what the terminal supports
color: docpadUtil.isTTY()
# Silent
# Will set the following
# logLevel = 3
# progress = welcome = false
silent: false
# Progress
# Whether or not we should display the progress in the terminal title bar
progress: true
# -----------------------------
# Other
# Catch our own exceptions (error events on the DocPad instance)
# use "error"/truthy to report
# use "fatal" to report and exit
catchOurExceptions: 'error'
# Catch any uncaught exception
# use "error" to report
# use "fatal"/truthy to report and exit
catchUncaughtExceptions: 'fatal'
# Whether or not DocPad is allowed to set the exit code on fatal errors
# May only work on node v0.11.8 and above
setExitCodeOnFatal: true
# Whether or not DocPad is allowed to set the exit code on standard errors
# May only work on node v0.11.8 and above
setExitCodeOnError: true
# Whether or not DocPad is allowed to set the exit code when some code has requested to
# May only work on node v0.11.8 and above
setExitCodeOnRequest: true
# The time to wait before cancelling a request
requestTimeout: 30*1000
# The time to wait when destroying DocPad
destroyDelay: -1
# Whether or not to destroy on exit
destroyOnExit: true
# Whether or not to destroy on signal interrupt (ctrl+c)
destroyOnSignalInterrupt: true
# The time to wait after a source file has changed before using it to regenerate
regenerateDelay: 100
# The time to wait before outputting the files we are waiting on
slowFilesDelay: 20*1000
# The time to wait before outputting the plugins we are waiting on
slowPluginsDelay: 20*1000
# Utilise the database cache
databaseCache: false # [false, true, 'write']
# Detect Encoding
# Should we attempt to auto detect the encoding of our files?
# Useful when you are using foreign encoding (e.g. GBK) for your files
detectEncoding: false
# Render Single Extensions
# Whether or not we should render single extensions by default
renderSingleExtensions: false
# Render Passes
# How many times should we render documents that reference other documents?
renderPasses: 1
# Powered By DocPad
# Whether or not we should include DocPad in the Powered-By meta header
# Please leave this enabled as it is a standard practice and promotes DocPad in the web eco-system
poweredByDocPad: true
# Template Data
# What data would you like to expose to your templates
templateData: {}
# Collections
# A hash of functions that create collections
collections: {}
# Events
# A hash of event handlers
events: {}
# Regenerate Every
# Performs a regenerate every x milliseconds, useful for always having the latest data
regenerateEvery: false
# Regerenate Every Options
# The generate options to use on the regenerate every call
regenerateEveryOptions:
populate: true
partial: false
# -----------------------------
# Environment Configuration
# Locale Code
# The code we shall use for our locale (e.g. en, fr, etc)
localeCode: null
# Environment
# Whether or not we are in production or development
# Separate environments using a comma or a space
env: null
# Environments
# Environment specific configuration to over-ride the global configuration
environments:
development:
# Only do these if we are running standalone (aka not included in a module)
welcome: isUser
progress: isUser
###*
# Get the DocPad configuration
# @method getConfig
# @return {Object} the DocPad configuration object
###
getConfig: ->
return @config or {}
# =================================
# 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...) =>
docpad = @
progress = docpad.progressInstance
tasks = TaskGroup.create(opts...)
# Listen to executing tasks and output their progress
tasks.on 'running', ->
config = tasks.getConfig()
name = tasks.getNames()
if progress
totals = tasks.getItemTotals()
progress.update(name, totals)
else
docpad.log('debug', name+' > running')
# Listen to executing tasks and output their progress
tasks.on 'item.add', (item) ->
config = tasks.getConfig()
name = item.getNames()
unless progress
docpad.log('debug', name+' > added')
# Listen to executing tasks and output their progress
item.on 'started', (item) ->
config = tasks.getConfig()
name = item.getNames()
if progress
totals = tasks.getItemTotals()
progress.update(name, totals)
else
docpad.log('debug', name+' > started')
# Listen to executing tasks and output their progress
item.done (err) ->
config = tasks.getConfig()
name = item.getNames()
if progress
totals = tasks.getItemTotals()
progress.update(name, totals)
else
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) ->
# Prepare
super()
[instanceConfig,next] = extractOptsAndCallback(instanceConfig, next)
docpad = @
# Allow DocPad to have unlimited event listeners
@setMaxListeners(0)
# Binders
# Using this over coffescript's => on class methods, ensures that the method length is kept
for methodName in "action log warn error fatal inspect notify checkRequest activeHandles onBeforeExit onSignalInterruptOne onSignalInterruptTwo onSignalInterruptThree destroyWatchers".split(/\s+/)
@[methodName] = @[methodName].bind(@)
# 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
@slowPlugins = {}
@loadedPlugins = {}
@pluginsTemplateData = {}
@collections = []
@blocks = {}
@websitePackageConfig = {}
@websiteConfig = {}
@userConfig = {}
@initialConfig = extendr.dereferenceJSON(@initialConfig)
@instanceConfig = instanceConfig or {}
@config = @mergeConfigs()
# Prepare the loggers
instanceConfig.logLevel ?= @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
@logger = logger
# Console
logger.pipe(filter).pipe(
new Human({ color: color })
).pipe(process.stdout)
# File
if instanceConfig.debug
logPath = @getPath(false, 'log')
safefs.unlink logPath, ->
logger
.pipe(
new Human(color: false)
)
.pipe(
fsUtil.createWriteStream(logPath)
)
# Forward log events to the logger
@on 'log', (args...) ->
docpad.log.apply(@, args)
# Setup configuration event wrappers
configEventContext = {docpad} # here to allow the config event context to persist between event calls
@getEvents().forEach (eventName) ->
# Bind to the event
docpad.on eventName, (opts,next) ->
eventHandler = docpad.getConfig().events?[eventName]
# Fire the config event handler for this event, if it exists
if typeChecker.isFunction(eventHandler)
args = [opts,next]
ambi(unbounded.binder.call(eventHandler, configEventContext), args...)
# It doesn't exist, so lets continue
else
next()
# Create our action runner
@actionRunnerInstance = @createTaskGroup('action runner', {abortOnError: false, destroyOnceDone: false}).whenDone (err) ->
docpad.progressInstance?.update('')
docpad.error(err) if err
# Setup the database
@database = new FilesCollection(null, {name:'database'})
.on('remove', (model,options) ->
# Skip if we are not a writeable file
return if model.get('write') is false
# 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 (model) ->
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', (model) ->
# Skip if we are not a writeable file
return if model.get('write') is false
# 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 (previousModel) ->
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 (existingModel) ->
if existingModel.id isnt model.id
modelPath = model.get('fullPath') or (model.get('relativePath')+':'+model.id)
existingModelPath = existingModel.get('fullPath') or (existingModel.get('relativePath')+':'+existingModel.id)
docpad.warn util.format(docpad.getLocale().outPathConflict, outPath, modelPath, existingModelPath)
# Return safely
return true
)
# Continue with load and ready
@action 'load ready', {}, (err) ->
if next?
next(err, docpad)
else if err
docpad.fatal(err)
# Chain
@
###*
# Has DocPad commenced destruction?
###
destroying: false
###*
# 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) ->
return @ if @destroying
@destroying = true
# Prepare
[opts,next] = extractOptsAndCallback(opts, next)
docpad = @
config = @getConfig()
locale = @getLocale()
# Log
docpad.log('info', locale.destroyDocPad)
# Drop all the remaining tasks
dropped = @getActionRunner().clearRemaining()
docpad.error("DocPad destruction had to drop #{Number(dropped)} action tasks") if dropped
# Destroy Timers
docpad.destroyTimers()
# Wait a configurable oment
docpad.timer 'destroy', 'timeout', config.destroyDelay, ->
# Destroy Plugins
docpad.emitSerial 'docpadDestroy', (eventError) ->
# Check
if eventError
# Note
err = new Errlop(
"DocPad's destroyEvent event failed",
eventError
)
docpad.fatal(err)
# Callback
return next?(err)
# Final closures and checks
try
# Destroy Timers
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 finalError
# Note
err = new Errlop(
"DocPad's final destruction efforts failed",
finalError
)
docpad.fatal(err)
return next?(err)
# Success
docpad.log(locale.destroyedDocPad) # log level omitted, as this will hit console.log
return next?()
# Chain
@
###*
# Emit event, serial
# @private
# @method emitSerial
# @param {String} eventName
# @param {Object} opts
# @param {Function} next
# @param {Error} next.err
###
emitSerial: (eventName, opts, next) ->
# Prepare
[opts,next] = extractOptsAndCallback(opts, next)
docpad = @
locale = docpad.getLocale()
# Log
docpad.log 'debug', util.format(locale.emittingEvent, eventName)
# Emit
super eventName, opts, (err) ->
# Check
return next(err) if err
# Log
docpad.log 'debug', util.format(locale.emittedEvent, eventName)
# Forward
return next(err)
# Chain
@
###*
# Emit event, parallel
# @private
# @method emitParallel
# @param {String} eventName
# @param {Object} opts
# @param {Function} next
# @param {Error} next.err
###
emitParallel: (eventName, opts, next) ->
# Prepare
[opts,next] = extractOptsAndCallback(opts, next)
docpad = @
locale = docpad.getLocale()
# Log
docpad.log 'debug', util.format(locale.emittingEvent, eventName)
# Emit
super eventName, opts, (err) ->
# Check
return next(err) if err
# Log
docpad.log 'debug', util.format(locale.emittedEvent, eventName)
# Forward
return next(err)
# Chain
@
# =================================
# Helpers
###*
# Get the ignore options for the DocPad project
# @method getIgnoreOpts
# @return {Array} string array of ignore options
###
getIgnoreOpts: ->
return pick(@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(@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(@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) ->
opts = extendr.extend(@getIgnoreOpts(), @config.watchOptions or {})
stalker = require('watchr').create(path)
for own key, value of listeners
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) ->
docpad = @
stalkers = []
tasks = new TaskGroup('watching directories').setConfig(concurrency:0).done (err) ->
if err
for stalker in stalkers
stalker.close()
next(err)
else
next(err, stalkers)
paths.forEach (path) ->
tasks.addTask "watching #{path}", (done) ->
# check if the dir exists first as reloadPaths may not apparently
safefs.exists path, (exists) ->
return done() unless exists
stalkers.push docpad.watchdir(path, listeners, done)
tasks.run()
# Chain
@
# =================================
# 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) ->
# Prepare
[instanceConfig,next] = extractOptsAndCallback(instanceConfig,next)
docpad = @
config = @getConfig()
locale = @getLocale()
# Render Single Extensions
@DocumentModel::defaults.renderSingleExtensions = config.renderSingleExtensions
# Fetch the plugins
pluginsList = Object.keys(@loadedPlugins).sort().join(', ')
# Welcome Output
docpad.log 'info', util.format(locale.welcome, @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, @getEnvironment())
# Prepare
tasks = @createTaskGroup('ready tasks').done (err) ->
# Error?
return docpad.error(err) if err
# All done, forward our DocPad instance onto our creator
return next?(null,docpad)
# kept here in case plugins use it
tasks.addTask 'welcome event', (complete) ->
# No welcome
return complete() unless config.welcome
# Welcome
docpad.emitSerial('welcome', {docpad}, complete)
tasks.addTask 'emit docpadReady', (complete) ->
docpad.emitSerial('docpadReady', {docpad}, complete)
# Run tasks
tasks.run()
# Chain
@
###*
# Performs the merging of the passed configuration objects
# @private
# @method mergeConfigs
###
mergeConfigs: (configPackages, destination = {}) ->
# A plugin is calling us with its configuration
unless 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
@env = (
@instanceConfig.env or @websiteConfig.env or @initialConfig.env or process.env.NODE_ENV or 'development'
)
@envs = @env.split(/[, ]+/)
# Merge the configurations together
configPackages = [@initialConfig, @userConfig, @websiteConfig, @instanceConfig]
# Figure out merging
configsToMerge = [destination]
for configPackage in configPackages
continue unless configPackage
configsToMerge.push(configPackage)
for env in @envs
envConfig = configPackage.environments?[env]
configsToMerge.push(envConfig) if envConfig
# Merge
return extendr.deep(configsToMerge...)
###*
# Legacy version of mergeConmergeConfigsfigurations
# @private
# @method mergeConfigurations
###
mergeConfigurations: (configPackages, [destination]) ->
return @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) ->
# Prepare
[instanceConfig,next] = extractOptsAndCallback(instanceConfig,next)
docpad = @
locale = @getLocale()
# 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(@instanceConfig, instanceConfig) if instanceConfig
# Merge the configurations together
@config = @mergeConfigs()
# Update the progress bar configuration
@updateProgress()
# Handle errors
process.removeListener('uncaughtException', @fatal)
process.removeListener('uncaughtException', @error)
@removeListener('error', @fatal)
@removeListener('error', @error)
if @config.catchExceptions # legacy
@config.catchOurExceptions = @config.catchUncaughtExceptions = 'error'
if @config.catchUncaughtExceptions
process.setMaxListeners(0)
if @config.catchUncaughtExceptions is 'error'
process.on('uncaughtException', @error)
else
process.on('uncaughtException', @fatal)
if @config.catchOurExceptions
if @config.catchUncaughtExceptions is 'fatal'
@on('error', @fatal)
else
@on('error', @error)
# Handle interrupt
process.removeListener('beforeExit', @onBeforeExit)
process.removeListener('SIGINT', @onSignalInterruptOne)
process.removeListener('SIGINT', @onSignalInterruptTwo)
process.removeListener('SIGINT', @onSignalInterruptThree)
if @config.destroyOnExit
process.once('beforeExit', @onBeforeExit)
if @config.destroyOnSignalInterrupt
process.once('SIGINT', @onSignalInterruptOne)
# Chain
@
onSignalInterruptOne: ->
# Log
@log('notice', "Signal Interrupt received, queued DocPad's destruction")
# Escalate next time
process.once('SIGINT', @onSignalInterruptTwo)
# Act
@action('destroy')
# Chain
@
onSignalInterruptTwo: ->
# Log
@log('alert', 'Signal Interrupt received again, closing stdin and dumping handles')
# Escalate next time
process.once('SIGINT', @onSignalInterruptThree)
# Handle any errors that occur when stdin is closed
# https://github.com/docpad/docpad/pull/1049
process.stdin?.once? 'error', (