UNPKG

docpad

Version:

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

2,036 lines (1,707 loc) 129 kB
##* # 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', (