Permalink
executable file 6505 lines (5444 sloc) 165 KB
##*
# The central module for DocPad
# @module DocPad
##
# =====================================
# This block *must* come first
# Important
pathUtil = require('path')
lazyRequire = require('lazy-require')
corePath = pathUtil.resolve(__dirname, '..', '..')
# Profile
if ('--profile' in process.argv)
# Debug
debugger
# Nodetime
if process.env.DOCPAD_PROFILER.indexOf('nodetime') isnt -1
throw new Error('NODETIME_KEY environment variable is undefined') unless process.env.NODETIME_KEY
console.log 'Loading profiling tool: nodetime'
require('lazy-require').sync 'nodetime', {cwd:corePath}, (err,nodetime) ->
if err
console.log 'Failed to load profiling tool: nodetime'
console.log err.stack or err
else
nodetime.profile({
accountKey: process.env.NODETIME_KEY
appName: 'DocPad'
})
console.log 'Profiling with nodetime with account key:', process.env.NODETIME_KEY
# Webkit Devtools
if process.env.DOCPAD_PROFILER.indexOf('webkit-devtools-agent') isnt -1
console.log 'Loading profiling tool: webkit-devtools-agent'
require('lazy-require').sync 'webkit-devtools-agent', {cwd:corePath}, (err, agent) ->
if err
console.log 'Failed to load profiling tool: webkit-devtools-agent'
console.log err.stack or err
else
agent.start()
console.log "Profiling with webkit-devtools-agent on pid #{process.pid} at http://127.0.0.1:9999/"
# V8 Profiler
if process.env.DOCPAD_PROFILER.indexOf('v8-profiler') isnt -1
console.log 'Loading profiling tool: v8-profiler'
require('lazy-require').sync 'v8-profiler-helper', {cwd:corePath}, (err, profiler) ->
if err
console.log 'Failed to load profiling tool: v8-profiler'
console.log err.stack or err
else
profiler.startProfile('docpad-profile')
console.log "Profiling with v8-profiler"
process.on 'exit', ->
profiler.stopProfile('docpad-profile')
# =====================================
# Requires
# Standard Library
util = require('util')
# External
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')
{TaskGroup} = require('taskgroup')
safefs = require('safefs')
safeps = require('safeps')
ignorefs = require('ignorefs')
rimraf = require('rimraf')
superAgent = require('superagent')
extractOptsAndCallback = require('extract-opts')
{EventEmitterGrouped} = require('event-emitter-grouped')
# 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('./plugin-loader')
BasePlugin = require('./plugin')
# ---------------------------------
# Helpers
setImmediate = global?.setImmediate or process.nextTick # node 0.8 b/c
# ---------------------------------
# Variables
isUser = docpadUtil.isUser()
###*
# Contains methods for managing the DocPad application.
# This includes managing a DocPad projects files and
# documents, watching directories, emitting events and
# managing the node.js/express.js web server.
# Extends https://github.com/bevry/event-emitter-grouped
#
# The class is instantiated in the docpad-server.js file
# which is the entry point for a DocPad application.
#
# new DocPad(docpadConfig, function(err, docpad) {
# if (err) {
# return docpadUtil.writeError(err);
# }
# return docpad.action(action, function(err) {
# if (err) {
# return docpadUtil.writeError(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()`
# Allow for `DocPad.createInstance()` as an alias for `new DocPad()` (legacy alias)
@create: (args...) -> return new @(args...)
@createInstance: (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
# Now it only allows requiring of files inside src/lib as that makes more sense
@require: (relativePath) ->
# Absolute the path
absolutePath = pathUtil.normalize(pathUtil.join(__dirname, relativePath))
# Check if we are actually a local docpad file
if absolutePath.replace(__dirname, '') is absolutePath
throw new Error("docpad.require is limited to local docpad files only: #{relativePath}")
# Require the path
return require(absolutePath)
# =================================
# Variables
# ---------------------------------
# Modules
# ---------------------------------
# Base
###*
# Events class
# https://github.com/docpad/docpad/blob/master/src/lib/base.coffee
# @property {Object} Events
###
Events: Events
###*
# Model class
# Extension of the Backbone Model class
# http://backbonejs.org/#Model
# https://github.com/docpad/docpad/blob/master/src/lib/base.coffee
# @property {Object} Model
###
Model: Model
###*
# Collection class
# Extension of the Backbone Collection class
# https://github.com/docpad/docpad/blob/master/src/lib/base.coffee
# http://backbonejs.org/#Collection
# @property {Object} Collection
###
Collection: Collection
###*
# QueryCollection class
# Extension of the Query Engine QueryCollection class
# https://github.com/docpad/docpad/blob/master/src/lib/base.coffee
# https://github.com/bevry/query-engine/blob/master/src/documents/lib/query-engine.js.coffee
# @property {Object} QueryCollection
###
QueryCollection: QueryCollection
# ---------------------------------
# Models
###*
# File Model class
# Extension of the Model class
# https://github.com/docpad/docpad/blob/master/src/lib/models/file.coffee
# @property {Object} FileModel
###
FileModel: FileModel
###*
# Document Model class
# Extension of the File Model class
# https://github.com/docpad/docpad/blob/master/src/lib/models/document.coffee
# @property {Object} DocumentModel
###
DocumentModel: DocumentModel
# ---------------------------------
# Collections
###*
# Collection of files in a DocPad project
# Extension of the QueryCollection class
# https://github.com/docpad/docpad/blob/master/src/lib/collections/files.coffee
# @property {Object} FilesCollection
###
FilesCollection: FilesCollection
###*
# Collection of elements in a DocPad project
# Extension of the Collection class
# https://github.com/docpad/docpad/blob/master/src/lib/collections/elements.coffee
# @property {Object} ElementsCollection
###
ElementsCollection: ElementsCollection
###*
# Collection of metadata in a DocPad project
# Extension of the ElementsCollection class
# https://github.com/docpad/docpad/blob/master/src/lib/collections/meta.coffee
# @property {Object} MetaCollection
###
MetaCollection: MetaCollection
###*
# Collection of JS script files in a DocPad project
# Extension of the ElementsCollection class
# https://github.com/docpad/docpad/blob/master/src/lib/collections/scripts.coffee
# @property {Object} ScriptsCollection
###
ScriptsCollection: ScriptsCollection
###*
# Collection of CSS style files in a DocPad project
# Extension of the ElementsCollection class
# https://github.com/docpad/docpad/blob/master/src/lib/collections/styles.coffee
# @property {Object} StylesCollection
###
StylesCollection: StylesCollection
###*
# Plugin Loader class
# https://github.com/docpad/docpad/blob/master/src/lib/plugin-loader.coffee
# Loads the DocPad plugins from the file system into
# a DocPad project
# @property {Object} PluginLoader
###
PluginLoader: PluginLoader
###*
# Base class for all DocPad plugins
# https://github.com/docpad/docpad/blob/master/src/lib/plugin.coffee
# @property {Object} BasePlugin
###
BasePlugin: BasePlugin
# ---------------------------------
# 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/,'')
###*
# The express.js server instance bound to DocPad.
# http://expressjs.com
# @private
# @property {Object} serverExpress
###
serverExpress: null
###*
# The Node.js http server instance bound to DocPad
# https://nodejs.org/api/http.html
# @private
# @property {Object} serverHttp
###
serverHttp: null
###*
# Get the DocPad express.js server instance and, optionally,
# the node.js https server instance
# @method getServer
# @param {Boolean} [both=false]
# @return {Object}
###
getServer: (both=false) ->
{serverExpress,serverHttp} = @
if both
return {serverExpress, serverHttp}
else
return serverExpress
###*
# Set the express.js server and node.js http server
# to bind to DocPad
# @method setServer
# @param {Object} servers
###
setServer: (servers) ->
# Apply
if servers.serverExpress and servers.serverHttp
@serverExpress = servers.serverExpress
@serverHttp = servers.serverHttp
# Cleanup
delete @config.serverHttp
delete @config.serverExpress
delete @config.server
###*
# Destructor. Close and destroy the node.js http server
# @private
# @method destroyServer
###
destroyServer: ->
@serverHttp?.close()
@serverHttp = null
# @TODO figure out how to destroy the express server
#
###*
# Internal property. The caterpillar logger instances bound to DocPad
# @private
# @property {Object} loggerInstances
###
loggerInstances: null
###*
# Get the caterpillar logger instance bound to DocPad
# @method getLogger
# @return {Object} caterpillar logger
###
getLogger: -> @loggerInstances?.logger
###*
# Get all the caterpillar logger instances bound to DocPad
# @method getLoggers
# @return {Object} collection of caterpillar loggers
###
getLoggers: -> @loggerInstances
###*
# Sets the caterpillar logger instances bound to DocPad
# @method setLoggers
# @param {Object} loggers
# @return {Object} logger instances bound to DocPad
###
setLoggers: (loggers) ->
if @loggerInstances
@warn @getLocale().loggersAlreadyDefined
else
@loggerInstances = loggers
@loggerInstances.logger.setConfig(dry:true)
@loggerInstances.console.setConfig(dry:false).pipe(process.stdout)
return loggers
###*
# Destructor. Destroy the caterpillar logger instances bound to DocPad
# @private
# @method {Object} destroyLoggers
###
destroyLoggers: ->
if @loggerInstances
for own key,value of @loggerInstances
value.end()
@
###*
# 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: (args...) -> docpadUtil.action.apply(@, args)
###*
# The error runner instance bound to DocPad
# @property {Object} errorRunnerInstance
###
errorRunnerInstance: null
###*
# Get the error runner instance
# @method {Object} getErrorRunner
# @return {Object} the error runner instance
###
getErrorRunner: -> @errorRunnerInstance
###*
# The track runner instance bound to DocPad
# @private
# @property {Object} trackRunnerInstance
###
trackRunnerInstance: null
###*
# Get the track runner instance
# @method getTrackRunner
# @return {Object} the track runner instance
###
getTrackRunner: -> @trackRunnerInstance
###*
# 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
# https://github.com/bevry/docpad/wiki/Events
# @private
# @property {Array} string array of event names
###
events: [
'extendTemplateData' # fired each load
'extendCollections' # fired each load
'docpadLoaded' # fired multiple times, first time command line configuration hasn't been applied yet
'docpadReady' # fired only once
'docpadDestroy' # fired once on shutdown
'consoleSetup' # fired once
'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'
'serverBefore'
'serverExtend'
'serverAfter'
'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
###*
# A FilesCollection of models updated
# from the DocPad database after each regeneration.
# @private
# @property {Object} databaseTempCache FileCollection of models
###
databaseTempCache: null
###*
# Description for getDatabase
# @method {Object} getDatabase
###
getDatabase: -> @database
###*
# Safe method for retrieving the database by
# either returning the database itself or the temporary
# database cache
# @method getDatabaseSafe
# @return {Object}
###
getDatabaseSafe: -> @databaseTempCache or @database
###*
# Destructor. Destroy the DocPad database
# @private
# @method destroyDatabase
###
destroyDatabase: ->
if @database?
@database.destroy()
@database = null
if @databaseTempCache?
@databaseTempCache.destroy()
@databaseTempCache = null
@
###*
# Files by url. Used to speed up fetching
# @private
# @property {Object} filesByUrl
###
filesByUrl: null
###*
# Files by Selector. Used to speed up fetching
# @private
# @property {Object} filesBySelector
###
filesBySelector: null
###*
# Files by Out Path. Used to speed up conflict detection. Do not use for anything else
# @private
# @property {Object} filesByOutPath
###
filesByOutPath: 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
# TODO: Does this still work???
###*
# Get a file by its url
# @method getFileByUrl
# @param {String} url
# @param {Object} [opts={}]
# @return {Object} a file
###
getFileByUrl: (url,opts={}) ->
opts.collection ?= @getDatabase()
file = opts.collection.get(@filesByUrl[url])
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 route and return
# it to the supplied callback.
# @method getFileByRoute
# @param {String} url
# @param {Object} next
# @param {Error} next.err
# @param {String} next.file
###
getFileByRoute: (url,next) ->
# Prepare
docpad = @
# If we have not performed a generation yet then wait until the initial generation has completed
if docpad.generated is false
# Wait until generation has completed and recall ourselves
docpad.once 'generated', ->
return docpad.getFileByRoute(url, next)
# hain
return @
# @TODO the above causes a signifcant delay when importing external documents (like tumblr data) into the database
# we need to figure out a better way of doing this
# perhaps it is via `writeSource: once` for imported documents
# or providing an option to disable this so it forward onto the static handler instead
# Prepare
database = docpad.getDatabaseSafe()
# Fetch
cleanUrl = docpad.getUrlPathname(url)
file = docpad.getFileByUrl(url, {collection:database}) or docpad.getFileByUrl(cleanUrl, {collection:database})
# Forward
next(null, file)
# Chain
@
# TODO: What on earth is a selector?
###*
# Get a file by its selector
# @method getFileBySelector
# @param {Object} selector
# @param {Object} [opts={}]
# @return {Object} a file
###
getFileBySelector: (selector,opts={}) ->
opts.collection ?= @getDatabase()
file = opts.collection.get(@filesBySelector[selector])
unless file
file = opts.collection.fuzzyFindOne(selector)
if file
@filesBySelector[selector] = file.id
return file
# ---------------------------------
# Skeletons
###*
# Skeletons Collection
# @private
# @property {Object} skeletonsCollection
###
skeletonsCollection: null
###*
# Get Skeletons
# Get all the available skeletons with their details and
# return this collection to the supplied callback.
# @method getSkeletons
# @param {Function} next
# @param {Error} next.err
# @param {Object} next.skeletonsCollection DocPad collection of skeletons
# @return {Object} DocPad skeleton collection
###
getSkeletons: (next) ->
# Prepare
docpad = @
locale = @getLocale()
# Check if we have cached locally
if @skeletonsCollection?
return next(null, @skeletonsCollection)
# Fetch the skeletons from the exchange
@skeletonsCollection = new Collection()
@skeletonsCollection.comparator = queryEngine.generateComparator(position:1, name:1)
@getExchange (err,exchange) ->
# Check
return next(err) if err
# Prepare
index = 0
# If we have the exchange data, then add the skeletons from it
if exchange
eachr exchange.skeletons, (skeleton, skeletonKey) ->
skeleton.id ?= skeletonKey
skeleton.name ?= skeletonKey
skeleton.position ?= index
docpad.skeletonsCollection.add(new Model(skeleton))
++index
# Add No Skeleton Option
docpad.skeletonsCollection.add(new Model(
id: 'none'
name: locale.skeletonNoneName
description: locale.skeletonNoneDescription
position: index
))
# Return Collection
return next(null, docpad.skeletonsCollection)
@
# ---------------------------------
# Plugins
###*
# Plugins that are loading really slow
# @property {Object} slowPlugins
###
slowPlugins: null # {}
###*
# Loaded plugins indexed by name
# @property {Object} loadedPlugins
###
loadedPlugins: null # {}
###*
# A listing of all the available extensions for DocPad
# @property {Object} exchange
###
exchange: null # {}
# -----------------------------
# Paths
###*
# The DocPad directory
# @property {String} corePath
###
corePath: corePath
###*
# 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')
###*
# The DocPad debug log path (docpad-debug.log)
# @property {String} debugLogPath
###
debugLogPath: pathUtil.join(process.cwd(), 'docpad-debug.log')
###*
# The User's configuration path (.docpad.cson)
# @property {String} userConfigPath
###
userConfigPath: '.docpad.cson'
# -----------------------------
# 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 Error(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= @getSimpleServerUrl()
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: ->
if @locale? is false
config = @getConfig()
codes = uniq [
'en'
safeps.getLanguageCode config.localeCode
safeps.getLanguageCode safeps.getLocaleCode()
safeps.getLocaleCode config.localeCode
safeps.getLocaleCode safeps.getLocaleCode()
]
locales = (@loadLocale(code) for code in codes)
@locale = extendr.extend(locales...)
return @locale
###*
# Load the locale
# @method loadLocale
# @param {String} code
# @return {Object} locale
###
loadLocale: (code) ->
# Check if it exists
localeFilename = "#{code}.cson"
localePath = pathUtil.join(@localePath, localeFilename)
return null unless safefs.existsSync(localePath)
# Load it
locale = CSON.parseCSONFile(localePath)
# Log the error in the background and continue
if locale instanceof Error
locale.context = "Failed to parse the CSON locale file: #{localePath}"
docpad.error(locale) # @TODO: should this be a fatal error instead?
return null
# Success
return locale
# -----------------------------
# Environments
###*
# Get the DocPad environment, eg: development,
# production or static
# @method getEnvironment
# @return {String} the environment
###
getEnvironment: ->
env = @getConfig().env or 'development'
return env
###*
# Get the environments
# @method getEnvironments
# @return {Array} array of environment strings
###
getEnvironments: ->
env = @getEnvironment()
envs = env.split(/[, ]+/)
return envs
# -----------------------------
# Configuration
###*
# Hash Key
# The key that we use to hash some data before sending it to our statistic server
# @private
# @property {String} string constant
###
hashKey: '7>9}$3hP86o,4=@T' # const
###*
# 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:
# Name
name: null
# Email
email: null
# Username
username: null
# Subscribed
subscribed: null
# Subcribe Try Again
# If our subscription has failed, when should we try again?
subscribeTryAgain: null
# Terms of Service
tos: null
# Identified
identified: 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
# Force re-install of all plugin dependencies
force: false
# Whether or not we should use the global docpad instance
global: false
# Whether or not we should enable plugins that have not been listed or not
enableUnlistedPlugins: true
# Plugins which should be enabled or not pluginName: pluginEnabled
enabledPlugins: {}
# Whether or not we should skip unsupported plugins
skipUnsupportedPlugins: true
# Whether or not to warn about uncompiled private plugins
warnUncompiledPrivatePlugins: true
# Configuration to pass to any plugins pluginName: pluginConfiguration
plugins: {}
# -----------------------------
# Project Paths
# The project directory
rootPath: process.cwd()
# The project's database cache path
databaseCachePath: '.docpad.db'
# 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: []
# The project's plugins directory
pluginsPaths: [
'node_modules'
'plugins'
]
# Paths that we should watch for reload changes in
reloadPaths: []
# Paths that we should watch for regeneration changes in
regeneratePaths: []
# 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 project's out directory
outPath: 'out'
# The project's src directory
srcPath: 'src'
# The project's documents directories
# relative to the srcPath
documentsPaths: [
'documents'
'render'
]
# The project's files directories
# relative to the srcPath
filesPaths: [
'files'
'static'
'public'
]
# The project's layouts directory
# relative to the srcPath
layoutsPaths: [
'layouts'
]
# Ignored file patterns during directory parsing
ignorePaths: false
ignoreHiddenFiles: false
ignoreCommonPatterns: true
ignoreCustomPatterns: false
# Watch options
watchOptions: null
# -----------------------------
# Server
# Port
# The port that the server should use
# Defaults to these environment variables:
# - PORT — Heroku, Nodejitsu, Custom
# - VCAP_APP_PORT — AppFog
# - VMC_APP_PORT — CloudFoundry
port: null
# Hostname
# The hostname we wish to listen to
# Defaults to these environment variables:
# HOSTNAME — Generic
# Do not set to "localhost" it does not work on heroku
hostname: null
# Max Age
# The caching time limit that is sent to the client
maxAge: 86400000
# Server
# The Express.js server that we want docpad to use
serverExpress: null
# The HTTP server that we want docpad to use
serverHttp: null
# Extend Server
# Whether or not we should extend the server with extra middleware and routing
extendServer: true
# Which middlewares would you like us to activate
# The standard middlewares (bodyParser, methodOverride, express router)
middlewareStandard: true
# The standard bodyParser middleware
middlewareBodyParser: true
# The standard methodOverride middleware
middlewareMethodOverride: true
# The standard express router middleware
middlewareExpressRouter: true
# Our own 404 middleware
middleware404: true
# Our own 500 middleware
middleware500: true
# -----------------------------
# Logging
# Log Level
# Which level of logging should we actually output
logLevel: (if ('-d' in process.argv) then 7 else 6)
# Catch uncaught exceptions
catchExceptions: true
# Report Errors
# Whether or not we should report our errors back to DocPad
# By default it is only enabled if we are not running inside a test
reportErrors: process.argv.join('').indexOf('test') is -1
# Report Statistics
# Whether or not we should report statistics back to DocPad
# By default it is only enabled if we are not running inside a test
reportStatistics: process.argv.join('').indexOf('test') is -1
# Color
# Whether or not our terminal output should have color
# `null` will default to what the terminal supports
color: null
# -----------------------------
# Other
# 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
# Offline
# Whether or not we should run in offline mode
# Offline will disable the following:
# - checkVersion
# - reportErrors
# - reportStatistics
offline: false
# Check Version
# Whether or not to check for newer versions of DocPad
checkVersion: false
# Welcome
# Whether or not we should display any custom welcome callbacks
welcome: false
# Prompts
# Whether or not we should display any prompts
prompts: false
# Progress
# Whether or not we should display any progress bars
# Requires prompts being true, and log level 6 or above
progress: true
# 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
# Helper Url
# Used for subscribing to newsletter, account information, and statistics etc
# Helper's source-code can be found at: https://github.com/docpad/helper
helperUrl: if true then 'http://helper.docpad.org/' else 'http://localhost:8000/'
# Safe Mode
# If enabled, we will try our best to sandbox our template rendering so that they cannot modify things outside of them
# Not yet implemented
safeMode: false
# 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:
# Always refresh from server
maxAge: false
# Only do these if we are running standalone (aka not included in a module)
checkVersion: isUser
welcome: isUser
prompts: isUser
###*
# Regenerate Timer
# When config.regenerateEvery is set to a value, we create a timer here
# @private
# @property {Object} regenerateTimer
###
regenerateTimer: null
###*
# Get the DocPad configuration. Commonly
# called within the docpad.coffee file or within
# plugins to access application specific configurations.
# serverExtend: (opts) ->
# Extract the server from the options
{server} = opts
docpad = @docpad
# As we are now running in an event,
# ensure we are using the latest copy of the docpad configuraiton
# and fetch our urls from it
latestConfig = docpad.getConfig()
oldUrls = latestConfig.templateData.site.oldUrls or []
newUrl = latestConfig.templateData.site.url
# Redirect any requests accessing one of our sites oldUrls to the new site url
server.use (req,res,next) ->
...
# @method getConfig
# @return {Object} the DocPad configuration object
###
getConfig: ->
return @config or {}
###*
# Get the port that DocPad is listening on (eg 9778)
# @method getPort
# @return {Number} the port number
###
getPort: ->
return @getConfig().port ? require('hostenv').PORT ? 9778
###*
# Get the Hostname
# @method getHostname
# @return {String}
###
getHostname: ->
return @getConfig().hostname ? require('hostenv').HOSTNAME ? '0.0.0.0'
###*
# Get address
# @method getServerUrl
# @param {Object} [opts={}]
# @return {String}
###
getServerUrl: (opts={}) ->
opts.hostname ?= @getHostname()
opts.port ?= @getPort()
opts.simple ?= false
if opts.simple is true and opts.hostname in ['0.0.0.0', '::', '::1']
return "http://127.0.0.1:#{opts.port}"
else
return "http://#{opts.hostname}:#{opts.port}"
###*
# Get simple server URL (changes 0.0.0.0, ::, and ::1 to 127.0.0.1)
# @method getSimpleServerUrl
# @param {Object} [opts={}]
# @param {Boolean} [opts.simple=true]
# @return {String}
###
getSimpleServerUrl: (opts={}) ->
opts.simple = true
return @getServerUrl(opts)
# =================================
# Initialization Functions
###*
# Constructor method. Sets up the DocPad instance.
# next(err)
# @method constructor
# @param {Object} instanceConfig
# @param {Function} next callback
# @param {Error} next.err
###
constructor: (instanceConfig,next) ->
# Prepare
[instanceConfig,next] = extractOptsAndCallback(instanceConfig, next)
docpad = @
# Create our own custom TaskGroup class for DocPad
# That will listen to tasks as they execute and provide debugging information
@TaskGroup = class extends TaskGroup
constructor: ->
# Prepare
super
tasks = @
# Listen to executing tasks and output their progress
tasks.on 'started', ->
config = tasks.getConfig()
name = tasks.getNames()
progress = config.progress
if progress
totals = tasks.getItemTotals()
progress.step(name).total(totals.total).setTick(totals.completed)
else
docpad.log('debug', name+' > started')
# Listen to executing tasks and output their progress
tasks.on 'item.add', (item) ->
config = tasks.getConfig()
name = item.getNames()
progress = config.progress
if progress
totals = tasks.getItemTotals()
progress.step(name).total(totals.total).setTick(totals.completed)
else
docpad.log('debug', name+' > added')
# Listen to executing tasks and output their progress
tasks.on 'item.started', (item) ->
config = tasks.getConfig()
name = item.getNames()
progress = config.progress
if progress
totals = tasks.getItemTotals()
progress.step(name).total(totals.total).setTick(totals.completed)
else
docpad.log('debug', name+' > started')
# Listen to executing tasks and output their progress
tasks.on 'item.done', (item, err) ->
config = tasks.getConfig()
name = item.getNames()
progress = config.progress
if progress
totals = tasks.getItemTotals()
progress.step(name).total(totals.total).setTick(totals.completed)
else
docpad.log('debug', name+' > done')
# Chain
@
# Binders
# Using this over coffescript's => on class methods, ensures that the method length is kept
for methodName in """
action
log warn error fatal inspector notify track identify subscribe checkRequest
serverMiddlewareRouter serverMiddlewareHeader serverMiddleware404 serverMiddleware500
destroyWatchers
""".split(/\s+/)
@[methodName] = @[methodName].bind(@)
# Allow DocPad to have unlimited event listeners
@setMaxListeners(0)
# 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(eventHandler.bind(configEventContext), args...)
# It doesn't exist, so lets continue
else
next()
# Create our action runner
@actionRunnerInstance = @TaskGroup.create('action runner').whenDone (err) ->
docpad.error(err) if err
# Create our track runner
@trackRunnerInstance = @TaskGroup.create('track runner').whenDone (err) ->
if err and docpad.getDebugging()
locale = docpad.getLocale()
docpad.warn(locale.trackError, err)
# Initialize the loggers
if (loggers = instanceConfig.loggers)
delete instanceConfig.loggers
else
# Create
logger = new (require('caterpillar').Logger)(lineOffset: 2)
# console
loggerConsole = logger
.pipe(
new (require('caterpillar-filter').Filter)
)
.pipe(
new (require('caterpillar-human').Human)
)
# Apply
loggers = {logger, console:loggerConsole}
# Apply the loggers
safefs.unlink(@debugLogPath, -> ) # Remove the old debug log file
@setLoggers(loggers) # Apply the logger streams
@setLogLevel(instanceConfig.logLevel ? @initialConfig.logLevel) # Set the default log level
# Log to bubbled events
@on 'log', (args...) ->
docpad.log.apply(@,args)
# Dereference and initialise advanced variables
# we deliberately ommit initialTemplateData here, as it is setup in getTemplateData
@slowPlugins = {}
@loadedPlugins = {}
@exchange = {}
@pluginsTemplateData = {}
@instanceConfig = {}
@collections = []
@blocks = {}
@filesByUrl = {}
@filesBySelector = {}
@filesByOutPath = {}
@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
# Delete the urls
for url in model.get('urls') or []
delete docpad.filesByUrl[url]
# 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)
updatedModels.each (model) ->
model.set('mtime': new Date())
# Log
docpad.log('debug', 'Updated mtime for these models due to remove of a similar one', updatedModels.pluck('relativePath'))
# Return safely
return true
)
.on('add change:urls', (model) ->
# Skip if we are not a writeable file
return if model.get('write') is false
# Delete the old urls
for url in model.previous('urls') or []
delete docpad.filesByUrl[url]
# Add the new urls
for url in model.get('urls')
docpad.filesByUrl[url] = model.cid
# Return safely
return true
)
.on('add change:outPath', (model) ->
# Skip if we are not a writeable file
return if model.get('write') is false
# Check if we have changed our outPath
previousOutPath = model.previous('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)
previousModels.each (model) ->
model.set('mtime': new Date())
# Log
docpad.log('debug', 'Updated mtime for these models due to addition of a similar one', previousModels.pluck('relativePath'))
# Update the cache entry with another file that has the same outPath or delete it if there aren't any others
previousModelId = docpad.filesByOutPath[previousOutPath]
if previousModelId is model.id
if previousModels.length
docpad.filesByOutPath[previousOutPath] = previousModelId
else
delete docpad.filesByOutPath[previousOutPath]
# Update the cache entry and fetch the latest if it was already set
if (outPath = model.get('outPath'))
existingModelId = docpad.filesByOutPath[outPath] ?= model.id
if existingModelId isnt model.id
existingModel = docpad.database.get(existingModelId)
if existingModel
# We have a conflict, let the user know
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)
else
# There reference was old, update it with our new one
docpad.filesByOutPath[outPath] = model.id
# Return safely
return true
)
@userConfig = extendr.dereference(@userConfig)
@initialConfig = extendr.dereference(@initialConfig)
# Extract action
if instanceConfig.action?
action = instanceConfig.action
else
action = 'load ready'
# Check if we want to perform an action
if action
@action action, instanceConfig, (err) ->
if next?
next(err, docpad)
else if err
docpad.fatal(err)
else
next?(null, docpad)
# Chain
@
###*
# 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) ->
# Prepare
[opts,next] = extractOptsAndCallback(opts, next)
docpad = @
# Destroy Regenerate Timer
docpad.destroyRegenerateTimer()
# Wait one second to wait for any logging to complete
docpadUtil.wait 1000, ->
# Destroy Plugins
docpad.emitSerial 'docpadDestroy', (err) ->
# Check
return next?(err) if err
# Destroy Plugins
docpad.destroyPlugins()
# Destroy Server
docpad.destroyServer()
# Destroy Watchers
docpad.destroyWatchers()
# Destroy Blocks
docpad.destroyBlocks()
# Destroy Collections
docpad.destroyCollections()
# Destroy Database
docpad.destroyDatabase()
# Destroy Logging
docpad.destroyLoggers()
# Destroy Process Listners
process.removeListener('uncaughtException', docpad.error)
# Destroy DocPad Listeners
docpad.removeAllListeners()
# Forward
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.
#
# Although it is possible to pass a range of options to watchdir
# in practice these options are provided as part of
# the DocPad config object with a number of default options
# specified in the DocPad config.
# @method watchdir
# @param {Object} [opts={}]
# @param {String} [opts.path] a single path to watch.
# @param {Array} [opts.paths] an array of paths to watch.
# @param {Function} [opts.listener] a single change listener to fire when a change occurs.
# @param {Array} [opts.listeners] an array of listeners.
# @param {Function} [opts.next] callback.
# @param {Object} [opts.stat] a file stat object to use for the path, instead of fetching a new one.
# @param {Number} [opts.interval=5007] for systems that poll to detect file changes, how often should it poll in millseconds.
# @param {Number} [opts.catupDelay=200] handles system swap file deletions and renaming
# @param {Array} [opts.preferredMethods=['watch','watchFile'] which order should we prefer our watching methods to be tried?.
# @param {Boolean} [opts.followLinks=true] follow symlinks, i.e. use stat rather than lstat.
# @param {Boolean|Array} [opts.ignorePaths=false] an array of full paths to ignore.
# @param {Boolean|Array} [opts.ignoreHiddenFiles=false] whether or not to ignored files which filename starts with a ".".
# @param {Boolean} [opts.ignoreCommonPatterns=true] whether or not to ignore common undesirable file patterns (e.g. .svn, .git, .DS_Store, thumbs.db, etc).
# @param {Boolean|Array} [opts.ignoreCustomPatterns=null] any custom ignore patterns that you would also like to ignore along with the common patterns.
# @return {Object} the watcher
###
watchdir: (opts={}) ->
opts = extendr.extend(@getIgnoreOpts(), opts, @config.watchOptions)
return require('watchr').watch(opts)
# =================================
# 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
# Version Check
@compareVersion()
# Welcome Prepare
if @getDebugging()
pluginsList = ("#{pluginName} v#{@loadedPlugins[pluginName].version}" for pluginName in Object.keys(@loadedPlugins).sort()).join(', ')
else
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 = new @TaskGroup 'ready tasks', next:(err) ->
# Error?
return docpad.error(err) if err
# All done, forward our DocPad instance onto our creator
return next?(null,docpad)
tasks.addTask 'welcome event', (complete) ->
# No welcome
return complete() unless config.welcome
# Welcome
docpad.emitSerial('welcome', {docpad}, complete)
tasks.addTask 'track', (complete) ->
# Identify
return docpad.identify(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 mergeConfigurations
# @param {Object} configPackages
# @param {Object} configsToMerge
###
mergeConfigurations: (configPackages,configsToMerge) ->
# Prepare
envs = @getEnvironments()
# Figure out merging
for configPackage in configPackages
continue unless configPackage
configsToMerge.push(configPackage)
for env in envs
envConfig = configPackage.environments?[env]
configsToMerge.push(envConfig) if envConfig
# Merge
extendr.safeDeepExtendPlainObjects(configsToMerge...)
# Chain
@
###*
# Set the instance configuration
# by merging the properties of the passed object
# with the existing DocPad instanceConfig object
# @private
# @method setInstanceConfig
# @param {Object} instanceConfig
###
setInstanceConfig: (instanceConfig) ->
# Merge in the instance configurations
if instanceConfig
logLevel = @getLogLevel()
extendr.safeDeepExtendPlainObjects(@instanceConfig, instanceConfig)
extendr.safeDeepExtendPlainObjects(@config, instanceConfig) if @config # @TODO document why there is the if
@setLogLevel(instanceConfig.logLevel) if instanceConfig.logLevel and instanceConfig.logLevel isnt logLevel
@
###*
# 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,next) ->
# 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
@setInstanceConfig(instanceConfig) if instanceConfig
# 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
@config.env = @instanceConfig.env or @websiteConfig.env or @initialConfig.env or process.env.NODE_ENV
# Merge configurations
configPackages = [@initialConfig, @userConfig, @websiteConfig, @instanceConfig]
configsToMerge = [@config]
docpad.mergeConfigurations(configPackages, configsToMerge)
# Extract and apply the server
@setServer extendr.safeShallowExtendPlainObjects({
serverHttp: @config.serverHttp
serverExpress: @config.serverExpress
}, @config.server)
# Extract and apply the logger
@setLogLevel(@config.logLevel)
# Resolve any paths
@config.rootPath = pathUtil.resolve(@config.rootPath)
@config.outPath = pathUtil.resolve(@config.rootPath, @config.outPath)
@config.srcPath = pathUtil.resolve(@config.rootPath, @config.srcPath)
@config.databaseCachePath = pathUtil.resolve(@config.rootPath, @config.databaseCachePath)
@config.packagePath = pathUtil.resolve(@config.rootPath, @config.packagePath)
# Resolve Documents, Files, Layouts paths
for type in ['documents','files','layouts']
typePaths = @config[type+'Paths']
for typePath,key in typePaths
typePaths[key] = pathUtil.resolve(@config.srcPath, typePath)
# Resolve Plugins paths
for type in ['plugins']
typePaths = @config[type+'Paths']
for typePath,key in typePaths
typePaths[key] = pathUtil.resolve(@config.rootPath, typePath)
# Bind the error handler, so we don't crash on errors
process.removeListener('uncaughtException', @error)
@removeListener('error', @error)
if @config.catchExceptions
process.setMaxListeners(0)
process.on('uncaughtException', @error)
@on('error', @error)
# Prepare the Post Tasks
postTasks = new @TaskGroup 'setConfig post tasks', next:(err) ->
return next(err, docpad.config)
###
postTasks.addTask 'lazy depedencnies: encoding', (complete) =>
return complete() unless @config.detectEncoding
return lazyRequire 'encoding', {cwd:corePath, stdio:'inherit'}, (err) ->
docpad.warn(locale.encodingLoadFailed) if err
return complete()
###
postTasks.addTask 'load plugins', (complete) ->
docpad.loadPlugins(complete)
postTasks.addTask 'extend collections', (complete) ->
docpad.extendCollections(complete)
postTasks.addTask 'fetch plugins templateData', (complete) ->
docpad.emitSerial('extendTemplateData', {templateData:docpad.pluginsTemplateData}, complete)
postTasks.addTask 'fire the docpadLoaded event', (complete) ->
docpad.emitSerial('docpadLoaded', complete)
# Fire post tasks
postTasks.run()
# Chain
@
###*
# Load the various configuration files from the
# file system. Set the instanceConfig.
# next(err,config)
# @private
# @method load
# @param {Object} instanceConfig
# @param {Function} next
# @param {Error} next.err
# @param {Object} next.config
###
load: (instanceConfig,next) ->
# Prepare
[instanceConfig,next] = extractOptsAndCallback(instanceConfig,next)
docpad = @
locale = @getLocale()
instanceConfig or= {}
# Reset non persistant configurations
@websitePackageConfig = {}
@websiteConfig = {}
@config = {}
# Merge in the instance configurations
@setInstanceConfig(instanceConfig)
# Prepare the Load Tasks
preTasks = new @TaskGroup 'load tasks', next:(err) =>
return next(err) if err
return @setConfig(next)
preTasks.addTask 'normalize the userConfigPath', (complete) =>
safeps.getHomePath (err,homePath) =>
return complete(err) if err
dropboxPath = pathUtil.resolve(homePath, 'Dropbox')
safefs.exists dropboxPath, (dropboxPathExists) =>
# @TODO: Implement checks here for
# https://github.com/bevry/docpad/issues/799
userConfigDirPath = if dropboxPathExists then dropboxPath else homePath
@userConfigPath = pathUtil.resolve(userConfigDirPath, @userConfigPath)
return complete()
preTasks.addTask "load the user's configuration", (complete) =>
configPath = @userConfigPath
docpad.log 'debug', util.format(locale.loadingUserConfig, configPath)
@loadConfigPath {configPath}, (err,data) =>
return complete(err) if err
# Apply loaded data
extendr.extend(@userConfig, data or {})
# Done
docpad.log 'debug', util.format(locale.loadingUserConfig, configPath)
return complete()
preTasks.addTask "load the anonymous user's configuration", (complete) =>
# Ignore if username is already identified
return complete() if @userConfig.username
# User is anonymous, set their username to the hashed and salted mac address
require('getmac').getMac (err,macAddress) =>
if err or !macAddress
docpad.warn(locale.macError, err)
return complete()
# Hash with salt
try
macAddressHash = require('crypto').createHmac('sha1', docpad.hashKey).update(macAddress).digest('hex')
catch err
return complete() if err
# Apply
if macAddressHash
@userConfig.name ?= "MAC #{macAddressHash}"
@userConfig.username ?= macAddressHash
# Next
return complete()
preTasks.addTask "load the website's package data", (complete) =>
rootPath = pathUtil.resolve(@instanceConfig.rootPath or @initialConfig.rootPath)
configPath = pathUtil.resolve(rootPath, @instanceConfig.packagePath or @initialConfig.packagePath)
docpad.log 'debug', util.format(locale.loadingWebsitePackageConfig, configPath)
@loadConfigPath {configPath}, (err,data) =>
return complete(err) if err
data or= {}
# Apply loaded data
@websitePackageConfig = data
# Done
docpad.log 'debug', util.format(locale.loadedWebsitePackageConfig, configPath)
return complete()
preTasks.addTask "read the .env file if it exists", (complete) =>
rootPath = pathUtil.resolve(@instanceConfig.rootPath or @websitePackageConfig.rootPath or @initialConfig.rootPath)
configPath = pathUtil.resolve(rootPath, '.env')
docpad.log 'debug', util.format(locale.loadingEnvConfig, configPath)
safefs.exists configPath, (exists) ->
return complete() unless exists
require('envfile').parseFile configPath, (err,data) ->
return complete(err) if err
for own key,value of data
process.env[key] = value
docpad.log 'debug', util.format(locale.loadingEnvConfig, configPath)
return complete()
preTasks.addTask "load the website's configuration", (complete) =>
docpad.log 'debug', util.format(locale.loadingWebsiteConfig)
rootPath = pathUtil.resolve(@instanceConfig.rootPath or @initialConfig.rootPath)
configPaths = @instanceConfig.configPaths or @initialConfig.configPaths
for configPath, index in configPaths
configPaths[index] = pathUtil.resolve(rootPath, configPath)
@loadConfigPath {configPaths}, (err,data) =>
return complete(err) if err
data or= {}
# Apply loaded data
extendr.extend(@websiteConfig, data)
# Done
docpad.log 'debug', util.format(locale.loadedWebsiteConfig)
return complete()
# Run the load tasks synchronously
preTasks.run()
# Chain
@
# =================================
# Configuration
###*
# Update user configuration with the passed data
# @method updateUserConfig
# @param {Object} [data={}]
# @param {Function} next
# @param {Error} next.err
###
updateUserConfig: (data={},next) ->
# Prepare
[data,next] = extractOptsAndCallback(data,next)
docpad = @
userConfigPath = @userConfigPath
# Apply back to our loaded configuration
# does not apply to @config as we would have to reparse everything
# and that appears to be an imaginary problem
extendr.extend(@userConfig, data) if data
# Convert to CSON
CSON.createCSONString @userConfig, (err, userConfigString) ->
if err
err.context = "Failed to create the CSON string for the user configuration"
return next(err)
# Write it
safefs.writeFile userConfigPath, userConfigString, 'utf8', (err) ->
# Forward
return next(err)
# Chain
@
###*
# Load a configuration url.
# @method loadConfigUrl
# @param {String} configUrl
# @param {Function} next
# @param {Error} next.err
# @param {Object} next.parsedData
###
loadConfigUrl: (configUrl,next) ->
# Prepare
docpad = @
locale = @getLocale()
# Log
docpad.log 'debug', util.format(locale.loadingConfigUrl, configUrl)
# Read the URL
superAgent
.get(configUrl)
.timeout(30*1000)
.end (err,res) ->
# Check
return next(err) if err
# Read the string using CSON
CSON.parseCSONString(res.text, next)
# Chain
@
###*
# Load the configuration from a file path
# passed as one of the options (opts.configPath) or
# from DocPad's configPaths
# @private
# @method loadConfigPath
# @param {Object} opts
# @param {Function} next
# @param {Error} next.err
# @param {Object} next.parsedData
###
loadConfigPath: (opts,next) ->
# Prepare
[opts,next] = extractOptsAndCallback(opts, next)
docpad = @
locale = @getLocale()
# Prepare
load = (configPath) ->
# Check
return next() unless configPath
# Log
docpad.log 'debug', util.format(locale.loadingConfigPath, configPath)
# Check that it exists
safefs.exists configPath, (exists) ->
return next() unless exists
# Prepare CSON Options
csonOptions =
cson: true
json: true
coffeescript: true
javascript: true
# Read the path using CSON
CSON.requireFile configPath, csonOptions, (err, data) ->
if err
err.context = util.format(locale.loadingConfigPathFailed, configPath)
return next(err)
# Check if the data is a function, if so, then execute it as one
while typeChecker.isFunction(data)
try
data = data(docpad)
catch err
return next(err)
unless typeChecker.isObject(data)
err = new Error("Loading the configuration #{docpad.inspector configPath} returned an invalid result #{docpad.inspector data}")
return next(err) if err
# Return the data
return next(null, data)
# Check
if opts.configPath
load(opts.configPath)
else
@getConfigPath opts, (err,configPath) ->
load(configPath)
# Chain
@
###*
# Get config paths and check that those
# paths exist
# @private
# @method getConfigPath
# @param {Object} opts
# @param {Object} next
# @param {Error} next.err
# @param {String} next.path
###
getConfigPath: (opts,next) ->
# Prepare
[opts,next] = extractOptsAndCallback(opts, next)
docpad = @
config = @getConfig()
result = null
# Ensure array
opts.configPaths ?= config.configPaths
opts.configPaths = [opts.configPaths] unless typeChecker.isArray(opts.configPaths)
tasks = new @TaskGroup 'getConfigPath tasks', next:(err) ->
return next(err, result)
# Determine our configuration path
opts.configPaths.forEach (configPath) ->
tasks.addTask "Checking if [#{configPath}] exists", (complete) ->
return complete() if result
safefs.exists configPath, (exists) ->
if exists
result = configPath
tasks.clear()
complete()
else
complete()
# Run them synchronously
tasks.run()
# Chain
@
###*
# Extend collecitons. Create DocPad's
# standard (documents, files
# layouts) and special (generate, referencesOthers,
# hasLayout, html, stylesheet) collections. Set blocks
# @private
# @method extendCollections
# @param {Function} next
# @param {Error} next.err
###
extendCollections: (next) ->
# Prepare
docpad = @
docpadConfig = @getConfig()
locale = @getLocale()
database = @getDatabase()
# Standard Collections
@setCollections({
# Standard Collections
documents: database.createLiveChildCollection()
.setQuery('isDocument', {
render: true
write: true
})
.on('add', (model) ->
docpad.log('debug', util.format(locale.addingDocument, model.getFilePath()))
)
files: database.createLiveChildCollection()
.setQuery('isFile', {
render: false
write: true
})
.on('add', (model) ->
docpad.log('debug', util.format(locale.addingFile, model.getFilePath()))
)
layouts: database.createLiveChildCollection()
.setQuery('isLayout', {
$or:
isLayout: true
fullPath: $startsWith: docpadConfig.layoutsPaths
})
.on('add', (model) ->
docpad.log('debug', util.format(locale.addingLayout, model.getFilePath()))
model.setDefaults({
isLayout: true
render: false
write: false
})
)
# Special Collections
generate: database.createLiveChildCollection()
.setQuery('generate', {
dynamic: false
ignored: false
})
.on('add', (model) ->
docpad.log('debug', util.format(locale.addingGenerate, model.getFilePath()))
)
referencesOthers: database.createLiveChildCollection()
.setQuery('referencesOthers', {
dynamic: false
ignored: false
referencesOthers: true
})
.on('add', (model) ->
docpad.log('debug', util.format(locale.addingReferencesOthers, model.getFilePath()))
)
hasLayout: database.createLiveChildCollection()
.setQuery('hasLayout', {
dynamic: false
ignored: false
layout: $exists: true
})
.on('add', (model) ->
docpad.log('debug', util.format(locale.addingHasLayout, model.getFilePath()))
)
html: database.createLiveChildCollection()
.setQuery('isHTML', {
write: true
outExtension: 'html'
})
.on('add', (model) ->
docpad.log('debug', util.format(locale.addingHtml, model.getFilePath()))
)
stylesheet: database.createLiveChildCollection()
.setQuery('isStylesheet', {
write: true
outExtension: 'css'
})
})
# Blocks
@setBlocks({
meta: new MetaCollection()
scripts: new ScriptsCollection()
styles: new StylesCollection()
})
# Custom Collections Group
tasks = new @TaskGroup "extendCollections tasks", concurrency:0, next:(err) ->
docpad.error(err) if err
docpad.emitSerial('extendCollections', next)
# Cycle through Custom Collections
eachr docpadConfig.collections or {}, (fn,name) ->
if !name or !typeChecker.isString(name)
err = new Error("Inside your DocPad configuration you have a custom collection with an invalid name of: #{docpad.inspector name}")
docpad.error(err)
return
if !fn or !typeChecker.isFunction(fn)
err = new Error("Inside your DocPad configuration you have a custom collection called #{docpad.inspector name} with an invalid method of: #{docpad.inspector fn}")
docpad.error(err)
return
tasks.addTask "creating the custom collection: #{name}", (complete) ->
# Init
ambi [fn.bind(docpad), fn], database, (err, collection) ->
# Check for error
if err
docpad.error(err)
return complete()
# Check the type of the collection
else unless collection instanceof QueryCollection
docpad.warn util.format(locale.errorInvalidCollection, name)
return complete()
# Make it a live collection
collection.live(true) if collection
# Apply the collection
docpad.setCollection(name, collection)
return complete()
# Run Custom collections
tasks.run()
# Chain
@
###*
# Reset collections. Perform a complete clean of our collections
# @private
# @method resetCollections
# @param {Object} opts
# @param {Function} next
# @param {Error} next.err
###
resetCollections: (opts,next) ->
# Prepare
[opts,next] = extractOptsAndCallback(opts, next)
docpad = @
database = docpad.getDatabase()
# Make it as if we have never generated before
docpad.generated = false
# Perform a complete clean of our collections
database.reset([])
meta = @getBlock('meta').reset([])
scripts = @getBlock('scripts').reset([])
styles = @getBlock('styles').reset([])
# ^ Backbone.js v1.1 changes the return values of these, however we change that in our Element class
# because if we didn't, all our skeletons would fail
# Add default block entries
meta.add("""<meta name="generator" content="DocPad v#{docpad.getVersion()}" />""") if docpad.getConfig().poweredByDocPad isnt false
# Reset caches
@filesByUrl = {}
@filesBySelector = {}
@filesByOutPath = {}
# Chain
next()
@
###*
# Initialise git repo
# @private
# @method initGitRepo
# @param {Object} opts
# @param {Function} next
# @param {Error} next.err
# @param {Object} next.results
###
initGitRepo: (opts,next) ->
# Prepare
[opts,next] = extractOptsAndCallback(opts, next)
docpad = @
config = @getConfig()
# Extract
opts.cwd ?= config.rootPath
opts.output ?= @getDebugging()
# Forward
safeps.initGitRepo(opts, next)
# Chain
@
###*
# Init node modules
# @private
# @method initNodeModules
# @param {Object} opts
# @param {Function} next
# @param {Error} next.err
# @param {Object} next.results
###
initNodeModules: (opts,next) ->
# Prepare
[opts,next] = extractOptsAndCallback(opts, next)
docpad = @
config = @getConfig()
# Extract
opts.cwd ?= config.rootPath
opts.output ?= docpad.getDebugging()
opts.force ?= if config.offline then false else true
# ^ @todo this line causes --force to be added, when it shouldn't be
opts.args ?= []
opts.args.push('--force') if config.force
opts.args.push('--no-registry') if config.offline
# Log
docpad.log('info', 'npm install') if opts.output
# Forward
safeps.initNodeModules(opts, next)
# Chain
@
###*
# Fix node package versions
# Combat to https://github.com/npm/npm/issues/4587#issuecomment-35370453
# @private
# @method fixNodePackageVersions
# @param {Object} opts
# @param {Function} next
# @param {Error} next.err
###
fixNodePackageVersions: (opts,next) ->
# Prepare
[opts,next] = extractOptsAndCallback(opts, next)
docpad = @
config = @getConfig()
# Extract
opts.packagePath ?= config.packagePath
# Read and replace
safefs.readFile opts.packagePath, (err,buffer) ->
data = buffer.toString()
data = data.replace(/("docpad(?:.*?)": ")\^/g, '$1~')
safefs.writeFile opts.packagePath, data, (err) ->
return next(err)
# Chain
@
###*
# Install node module. Same as running
# 'npm install' through the command line
# @private
# @method installNodeModule
# @param {Array} names
# @param {Object} opts
# @param {Function} next
# @param {Error} next.err
# @param {Object} next.result
###
installNodeModule: (names,opts,next) ->
# Prepare
[opts,next] = extractOptsAndCallback(opts, next)
docpad = @
config = @getConfig()
# Extract
opts.cwd ?= config.rootPath
opts.args ?= []
if docpad.getDebugging()
opts.stdio ?= 'inherit'
opts.global ?= false
opts.global = ['--global'] if opts.global is true
opts.global = [opts.global] if opts.global and Array.isArray(opts.global) is false
opts.save ?= !opts.global
opts.save = ['--save'] if opts.save is true
opts.save = [opts.save] if opts.save and Array.isArray(opts.save) is false
# Command
command = ['npm', 'install']
# Names
names = names.split(/[,\s]+/) unless typeChecker.isArray(names)
names.forEach (name) ->
# Check
return unless name
# Ensure latest if version isn't specfied
name += '@latest' if name.indexOf('@') is -1
# Push the name to the commands
command.push(name)
# Arguments
command.push(opts.args...)
command.push('--force') if config.force
command.push('--no-registry') if config.offline
command.push(opts.save...) if opts.save
command.push(opts.global...) if opts.global
# Log
docpad.log('info', command.join(' ')) if opts.output
# Forward
safeps.spawn(command, opts, next)
# Chain
@
###*
# Uninstall node module. Same as running
# 'npm uninstall' through the command line
# @private
# @method uninstallNodeModule
# @param {Array} names
# @param {Object} opts
# @param {Function} next
# @param {Error} next.err
# @param {Object} next.result
###
uninstallNodeModule: (names,opts,next) ->
# Prepare
[opts,next] = extractOptsAndCallback(opts, next)
docpad = @
config = @getConfig()
# Extract
opts.cwd ?= config.rootPath
opts.output ?= docpad.getDebugging()
opts.args ?= []
opts.global ?= false
opts.global = ['--global'] if opts.global is true
opts.global = [opts.global] if opts.global and Array.isArray(opts.global) is false
opts.save ?= !opts.global
opts.save = ['--save', '--save-dev'] if opts.save is true
opts.save = [opts.save] if opts.save and Array.isArray(opts.save) is false
# Command
command = ['npm', 'uninstall']
# Names
names = names.split(/[,\s]+/) unless typeChecker.isArray(names)
command.push(names...)
# Arguments
command.push(opts.args...)
command.push(opts.save...) if opts.save
command.push(opts.global...) if opts.global
# Log
docpad.log('info', command.join(' ')) if opts.output
# Forward
safeps.spawn(command, opts, next)
# Chain
@
# =================================
# Logging
###*
# Set the log level
# @private
# @method setLogLevel
# @param {Number} level
###
setLogLevel: (level) ->
@getLogger().setConfig({level})
if level is 7
loggers = @getLoggers()
if loggers.debug? is false
loggers.debug = loggers.logger
.pipe(
new (require('caterpillar-human').Human)(color:false)
)
.pipe(
require('fs').createWriteStream(@debugLogPath)
)
@
###*
# Get the log level
# @method getLogLevel
# @return {Number} the log level
###
getLogLevel: ->
return @getConfig().logLevel
###*
# Are we debugging?
# @method getDebugging
# @return {Boolean}
###
getDebugging: ->
return @getLogLevel() is 7
###*
# Handle a fatal error
# @private
# @method fatal
# @param {Object} err
###
fatal: (err) ->
docpad = @
config = @getConfig()
# Check
return @ unless err
# Handle
@error(err)
# Even though the error would have already been logged by the above
# Ensure it is definitely outputted in the case the above fails
docpadUtil.writeError(err)
# Destroy DocPad
@destroy()
# Chain
@
###*
# Inspect. Converts object to JSON string. Wrapper around nodes util.inspect method.
# Can't use the inspect namespace as for some silly reason it destroys everything
# @method inspector
# @param {Object} obj
# @param {Object} opts
# @return {String} JSON string of passed object
###
inspector: (obj, opts) ->
opts ?= {}
opts.colors ?= @getConfig().color
return docpadUtil.inspect(obj, opts)
###*
# Log arguments
# @property {Object} log
# @param {Mixed} args...
###
log: (args...) ->
# Log
logger = @getLogger() or console
logger.log.apply(logger, args)
# Chain
@
###*
# Create an error object
# @method createError
# @param {Object} err
# @param {Object} opts
# @return {Object} the error
###
# @TODO: Decide whether or not we should track warnings
# Previously we didn't, but perhaps it would be useful
# If the statistics gets polluted after a while, we will remove it
# Ask @balupton to check the stats after March 30th 2015
createError: (err, opts) ->
# Prepare
opts ?= {}
opts.level ?= err.level ? 'error'
opts.track ?= err.track ? true
opts.tracked ?= err.tracked ? false
opts.log ?= err.log ? true
opts.logged ?= err.logged ? false
opts.notify ?= err.notify ? true
opts.notified ?= err.notified ? false
opts.context ?= err.context if err.context?
# Ensure we have an error object
err = new Error(err) unless err.stack
# Add our options to the error object
for own key,value of opts
err[key] ?= value
# Return the error
return err
###*
# Create an error (tracks it) and log it
# @method error
# @param {Object} err
# @param {Object} [level='err']
###
error: (err, level='err') ->
# Prepare
docpad = @
# Create the error and track it
err = @createError(err, {level})
# Track the error
@trackError(err)
# Log the error
@logError(err)
# Notify the error
@notifyError(err)
# Chain
@
###*
# Log an error
# @method logError
# @param {Object} err
###
logError: (err) ->
# Prepare
docpad = @
locale = @getLocale()
# Track
if err and err.log isnt false and err.logged isnt true
err = @createError(err, {logged:true})
occured =
if err.level in ['warn', 'warning']
locale.warnOccured
else
locale.errorOccured
message =
if err.context
err.context+locale.errorFollows
else
occured
message += '\n\n'+err.stack.toString().trim()
message += '\n\n'+locale.errorSubmission
docpad.log(err.level, message)
# Chain
@
###*
# Track an error in the background
# @private
# @method trackError
# @param {Object} err
###
trackError: (err) ->
# Prepare
docpad = @
config = @getConfig()
# Track
if err and err.track isnt false and err.tracked isnt true and config.offline is false and config.reportErrors is true
err = @createError(err, {tracked:true})
data = {}
data.message = err.message
data.stack = err.stack.toString().trim() if err.stack
data.config = config
data.env = process.env
docpad.track('error', data)
# Chain
@
###*
# Notify error
# @private
# @method notifyError
# @param {Object} err
###
notifyError: (err) ->
# Prepare
docpad = @
locale = @getLocale()
# Check
if err.notify isnt false and err.notified isnt true
err.notified = true
occured =
if err.level in ['warn', 'warning']
locale.warnOccured
else
locale.errorOccured
docpad.notify(err.message, {title:occured})
# Chain
@
###*
# Log an error of level 'warn'
# @method warn
# @param {String} message
# @param {Object} err
# @return {Object} description
###
warn: (message, err) ->
# Handle
if err
err.context = message
err.level = 'warn'
@error(err)
else
err =
if message instanceof Error
message
else
new Error(message)
err.level = 'warn'
@error(err)
# Chain
@
###*
# Send a notify event to plugins (like growl)
# @method notify
# @param {String} message
# @param {Object} [opts={}]
###
notify: (message,opts={}) ->
# Prepare
docpad = @
# Emit
docpad.emitSerial 'notify', {message,opts}, (err) ->
docpad.error(err) if err
# Chain
@
###*
# Check Request
# @private
# @method checkRequest
# @param {Function} next
# @param {Error} next.err
# @param {Object} next.res
###
checkRequest: (next) ->
next ?= @error.bind(@)
return (err,res) ->
# Check
return next(err, res) if err
# Check
if res.body?.success is false or res.body?.error
err = new Error(res.body.error or 'unknown request error') # @TODO localise this
return next(err, res)
# Success
return next(null, res)
###*
# Subscribe to the DocPad email list.
# @private
# @method subscribe
# @param {Function} next
# @param {Error} next.err
###
subscribe: (next) ->
# Prepare
config = @getConfig()
# Check
if config.offline is false
if @userConfig?.email
# Data
data = {}
data.email = @userConfig.email # required
data.name = @userConfig.name or null
data.username = @userConfig.username or null
# Apply
superAgent
.post(config.helperUrl)
.type('json').set('Accept', 'application/json')
.query(
method: 'add-subscriber'
)
.send(data)
.timeout(30*1000)
.end @checkRequest next
else
err = new Error('Email not provided') # @TODO localise this
next?(err)
else
next?()
# Chain
@
###*
# Track
# @private
# @method track
# @param {String} name
# @param {Object} [things={}]
# @param {Function} next
# @param {Error} next.err
###
track: (name,things={},next) ->
# Prepare
docpad = @
config = @getConfig()
# Check
if config.offline is false and config.reportStatistics
# Data
data = {}
data.userId = @userConfig.username or null
data.event = name
data.properties = things
# Things
things.websiteName = @websitePackageConfig.name if @websitePackageConfig?.name
things.platform = @getProcessPlatform()
things.environment = @getEnvironment()
things.version = @getVersion()
things.nodeVersion = @getProcessVersion()
# Plugins
eachr docpad.loadedPlugins, (value,key) ->
things['plugin-'+key] = value.version or true
# Apply
trackRunner = docpad.getTrackRunner()
trackRunner.addTask 'track task', (complete) ->
superAgent
.post(config.helperUrl)
.type('json').set('Accept', 'application/json')
.query(
method: 'analytics'
action: 'track'
)
.send(data)
.timeout(30*1000)
.end docpad.checkRequest (err) ->
next?(err)
complete(err) # we pass the error here, as if we error, we want to stop all tracking
# Execute the tracker tasks
trackRunner.run()
else
next?()
# Chain
@
###*
# Identify DocPad user
# @private
# @method identify
# @param {Function} next
# @param {Error} next.err
###
identify: (next) ->
# Prepare
docpad = @
config = @getConfig()
# Check
if config.offline is false and config.reportStatistics and @userConfig?.username
# Data
data = {}
data.userId = @userConfig.username # required
data.traits = things = {}
# Things
now = new Date()
things.username = @userConfig.username # required
things.email = @userConfig.email or null
things.name = @userConfig.name or null
things.lastLogin = now.toISOString()
things.lastSeen = now.toISOString()
things.countryCode = safeps.getCountryCode()
things.languageCode = safeps.getLanguageCode()
things.platform = @getProcessPlatform()
things.version = @getVersion()
things.nodeVersion = @getProcessVersion()
# Is this a new user?
if docpad.userConfig.identified isnt true
# Update
things.created = now.toISOString()
# Create the new user
docpad.getTrackRunner().addTask 'create new user', (complete) ->
superAgent
.post(config.helperUrl)
.type('json').set('Accept', 'application/json')
.query(
method: 'analytics'
action: 'identify'
)
.send(data)
.timeout(30*1000)
.end docpad.checkRequest (err) ->
# Save the changes with these
docpad.updateUserConfig({identified:true}, complete)
# Or an existing user?
else
# Update the existing user's information witht he latest
docpad.getTrackRunner().addTask 'update user', (complete) ->
superAgent
.post(config.helperUrl)
.type('json').set('Accept', 'application/json')
.query(
method: 'analytics'
action: 'identify'
)
.send(data)
.timeout(30*1000)
.end docpad.checkRequest complete
# Chain
next?()
@
# =================================