environment.coffee 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312
  1. ### environment.coffee ###
  2. path = require 'path'
  3. async = require 'async'
  4. fs = require 'fs'
  5. {EventEmitter} = require 'events'
  6. utils = require './utils'
  7. {Config} = require './config'
  8. {ContentPlugin, ContentTree, StaticFile} = require './content'
  9. {TemplatePlugin, loadTemplates} = require './templates'
  10. {logger} = require './logger'
  11. {render} = require './renderer'
  12. {runGenerator} = require './generator'
  13. {readJSON, readJSONSync} = utils
  14. class Environment extends EventEmitter
  15. ### The Wintersmith environment. ###
  16. utils: utils
  17. ContentTree: ContentTree
  18. ContentPlugin: ContentPlugin
  19. TemplatePlugin: TemplatePlugin
  20. constructor: (config, @workDir, @logger) ->
  21. ### Create a new Environment, *config* is a Config instance, *workDir* is the
  22. working directory and *logger* is a log instance implementing methods for
  23. error, warn, verbose and silly loglevels. ###
  24. @loadedModules = []
  25. @workDir = path.resolve @workDir
  26. @setConfig config
  27. @reset()
  28. reset: ->
  29. ### Reset environment and clear any loaded modules from require.cache ###
  30. @views = {none: (args..., callback) -> callback()}
  31. @generators = []
  32. @plugins = {StaticFile}
  33. @templatePlugins = []
  34. @contentPlugins = []
  35. @helpers = {}
  36. while id = @loadedModules.pop()
  37. @logger.verbose "unloading: #{ id }"
  38. delete require.cache[id]
  39. @setupLocals()
  40. setConfig: (@config) ->
  41. @contentsPath = @resolvePath @config.contents
  42. @templatesPath = @resolvePath @config.templates
  43. setupLocals: ->
  44. ### Resolve locals and loads any required modules. ###
  45. @locals = {}
  46. # Load locals json if necessary
  47. if typeof @config.locals == 'string'
  48. filename = @resolvePath @config.locals
  49. @logger.verbose "loading locals from: #{ filename }"
  50. @locals = readJSONSync filename
  51. else
  52. @locals = @config.locals
  53. # Load and add modules specified with the require option to the locals context.
  54. for alias, id of @config.require
  55. logger.verbose "loading module '#{ id }' available in locals as '#{ alias }'"
  56. if @locals[alias]?
  57. logger.warn "module '#{ id }' overwrites previous local with the same key ('#{ alias }')"
  58. try
  59. @locals[alias] = @loadModule id
  60. catch error
  61. logger.warn "unable to load '#{ id }': #{ error.message }"
  62. return
  63. resolvePath: (pathname) ->
  64. ### Resolve *pathname* in working directory, returns an absolute path. ###
  65. path.resolve @workDir, pathname or ''
  66. resolveContentsPath: (pathname) ->
  67. ### Resolve *pathname* in contents directory, returns an absolute path. ###
  68. path.resolve @contentsPath, pathname or ''
  69. resolveModule: (module) ->
  70. ### Resolve *module* to an absolute path, mimicking the node.js module loading system. ###
  71. switch module[0]
  72. when '.'
  73. require.resolve @resolvePath module
  74. when '/'
  75. require.resolve module
  76. else
  77. nodeDir = @resolvePath 'node_modules'
  78. try
  79. require.resolve path.join(nodeDir, module)
  80. catch error
  81. require.resolve module
  82. relativePath: (pathname) ->
  83. ### Resolve path relative to working directory. ###
  84. path.relative @workDir, pathname
  85. relativeContentsPath: (pathname) ->
  86. ### Resolve path relative to contents directory. ###
  87. path.relative @contentsPath, pathname
  88. registerContentPlugin: (group, pattern, plugin) ->
  89. ### Add a content *plugin* to the environment. Files in the contents directory
  90. matching the glob *pattern* will be instantiated using the plugin's `fromFile`
  91. factory method. The *group* argument is used to group the loaded instances under
  92. each directory. I.e. plugin instances with the group 'textFiles' can be found
  93. in `contents.somedir._.textFiles`. ###
  94. @logger.verbose "registering content plugin #{ plugin.name } that handles: #{ pattern }"
  95. @plugins[plugin.name] = plugin
  96. @contentPlugins.push
  97. group: group
  98. pattern: pattern
  99. class: plugin
  100. registerTemplatePlugin: (pattern, plugin) ->
  101. ### Add a template *plugin* to the environment. All files in the template directory
  102. matching the glob *pattern* will be passed to the plugin's `fromFile` classmethod. ###
  103. @logger.verbose "registering template plugin #{ plugin.name } that handles: #{ pattern }"
  104. @plugins[plugin.name] = plugin
  105. @templatePlugins.push
  106. pattern: pattern
  107. class: plugin
  108. registerGenerator: (group, generator) ->
  109. ### Add a generator to the environment. The generator function is called with the env and the
  110. current content tree. It should return a object with nested ContentPlugin instances.
  111. These will be merged into the final content tree. ###
  112. @generators.push
  113. group: group
  114. fn: generator
  115. registerView: (name, view) ->
  116. ### Add a view to the environment. ###
  117. @views[name] = view
  118. getContentGroups: ->
  119. ### Return an array of all registered content groups ###
  120. groups = []
  121. for plugin in @contentPlugins
  122. groups.push plugin.group unless plugin.group in groups
  123. for generator in @generators
  124. groups.push generator.group unless generator.group in groups
  125. return groups
  126. loadModule: (module, unloadOnReset=false) ->
  127. ### Requires and returns *module*, resolved from the current working directory. ###
  128. require 'coffee-script/register' if module[-7..] is '.coffee'
  129. @logger.silly "loading module: #{ module }"
  130. id = @resolveModule module
  131. @logger.silly "resolved: #{ id }"
  132. rv = require id
  133. @loadedModules.push id if unloadOnReset
  134. return rv
  135. loadPluginModule: (module, callback) ->
  136. ### Load a plugin *module*. Calls *callback* when plugin is done loading, or an error occurred. ###
  137. id = 'unknown'
  138. done = (error) ->
  139. error.message = "Error loading plugin '#{ id }': #{ error.message }" if error?
  140. callback error
  141. if typeof module is 'string'
  142. id = module
  143. try
  144. module = @loadModule module
  145. catch error
  146. done error
  147. return
  148. try
  149. module.call null, this, done
  150. catch error
  151. done error
  152. loadViewModule: (id, callback) ->
  153. ### Load a view *module* and add it to the environment. ###
  154. @logger.verbose "loading view: #{ id }"
  155. try
  156. module = @loadModule id, true
  157. catch error
  158. error.message = "Error loading view '#{ id }': #{ error.message }"
  159. callback error
  160. return
  161. @registerView path.basename(id), module
  162. callback()
  163. loadPlugins: (callback) ->
  164. ### Loads any plugin found in *@config.plugins*. ###
  165. async.series [
  166. # load default plugins
  167. (callback) =>
  168. async.forEachSeries @constructor.defaultPlugins, (plugin, callback) =>
  169. @logger.verbose "loading default plugin: #{ plugin }"
  170. id = require.resolve "./../plugins/#{ plugin }"
  171. module = require id
  172. @loadedModules.push id
  173. @loadPluginModule module, callback
  174. , callback
  175. # load user plugins
  176. (callback) =>
  177. async.forEachSeries @config.plugins, (plugin, callback) =>
  178. @logger.verbose "loading plugin: #{ plugin }"
  179. @loadPluginModule plugin, callback
  180. , callback
  181. ], callback
  182. loadViews: (callback) ->
  183. ### Loads files found in the *@config.views* directory and registers them as views. ###
  184. return callback() if not @config.views?
  185. async.waterfall [
  186. (callback) => fs.readdir @resolvePath(@config.views), callback
  187. (filenames, callback) =>
  188. modules = filenames.map (filename) => "#{ @config.views }/#{ filename }"
  189. async.forEach modules, @loadViewModule.bind(this), callback
  190. ], callback
  191. getContents: (callback) ->
  192. ### Build the ContentTree from *@contentsPath*, also runs any registered generators. ###
  193. async.waterfall [
  194. (callback) =>
  195. ContentTree.fromDirectory this, @contentsPath, callback
  196. (contents, callback) =>
  197. async.mapSeries @generators, (generator, callback) =>
  198. runGenerator this, contents, generator, callback
  199. , (error, generated) =>
  200. return callback(error, contents) if error? or generated.length is 0
  201. try
  202. tree = new ContentTree '', @getContentGroups()
  203. for gentree in generated
  204. ContentTree.merge tree, gentree
  205. ContentTree.merge tree, contents
  206. catch error
  207. return callback error
  208. callback null, tree
  209. ], callback
  210. getTemplates: (callback) ->
  211. ### Load templates. ###
  212. loadTemplates this, callback
  213. getLocals: (callback) ->
  214. ### Returns locals. ###
  215. # TODO: locals are no longer loaded async, this method should eventually be removed
  216. callback null, @locals
  217. load: (callback) ->
  218. ### Convenience method to load plugins, views, contents, templates and locals. ###
  219. async.waterfall [
  220. (callback) =>
  221. async.parallel [
  222. (callback) => @loadPlugins callback
  223. (callback) => @loadViews callback
  224. ], callback
  225. (_, callback) =>
  226. async.parallel
  227. contents: (callback) => @getContents callback
  228. templates: (callback) => @getTemplates callback
  229. locals: (callback) => @getLocals callback
  230. , callback
  231. ], callback
  232. preview: (callback) ->
  233. ### Start the preview server. Calls *callback* with the server instance when it is up and
  234. running or if an error occurs. NOTE: The returned server instance will be invalid if the
  235. config file changes and the server is restarted because of it. As a temporary workaround
  236. you can set the _restartOnConfChange key in settings to false. ###
  237. @mode = 'preview'
  238. server = require './server'
  239. server.run this, callback
  240. build: (outputDir, callback) ->
  241. ### Build the content tree and render it to *outputDir*. ###
  242. @mode = 'build'
  243. if arguments.length < 2
  244. # *outputDir* is optional and if omitted config.output is used
  245. callback = outputDir or ->
  246. outputDir = @resolvePath @config.output
  247. async.waterfall [
  248. (callback) =>
  249. @load callback
  250. (result, callback) =>
  251. {contents, templates, locals} = result
  252. render this, outputDir, contents, templates, locals, callback
  253. ], callback
  254. Environment.create = (config, workDir, log=logger) ->
  255. ### Set up a new environment using the default logger, *config* can be
  256. either a config object, a Config instance or a path to a config file. ###
  257. if typeof config is 'string'
  258. # working directory will be where the config file resides
  259. workDir ?= path.dirname config
  260. config = Config.fromFileSync config
  261. else
  262. workDir ?= process.cwd()
  263. if config not instanceof Config
  264. config = new Config config
  265. return new Environment config, workDir, log
  266. Environment.defaultPlugins = ['page', 'pug', 'markdown']
  267. ### Exports ###
  268. module.exports = {Environment}