UNPKG

5.38 kBJavaScriptView Raw
1var chalk = require('chalk')
2var unyield = require('unyield')
3var chokidar = require('chokidar')
4var thunkify = require('thunkify')
5var superstatic = require('superstatic')
6
7var join = require('path').join
8var getport = thunkify(require('get-port'))
9var spawnLR = thunkify(require('./livereloader').spawnLR)
10var injectLR = require('./livereloader').injectLR
11var watchLR = require('./livereloader').watchLR
12var loadJson = require('./loader')
13var debounce = require('./debounce')
14
15function exists (file) {
16 try {
17 return require('fs').statSync(file)
18 } catch (e) {
19 return false
20 }
21}
22
23/*
24 * heh
25 */
26
27var eventSymbols = {
28 add: '+',
29 addDir: '+',
30 change: '◦',
31 unlink: '×',
32 unlinkDir: '×'
33}
34
35/*
36 * (Class) the runner
37 */
38
39function Runner (dir, options) {
40 if (exists(join(dir, 'metalsmith.json'))) {
41 this.metalsmith = loadJson(dir)
42 } else if (exists('metalsmith.js')) {
43 this.metalsmith = require(join(dir, 'metalsmith.js'))
44 } else {
45 throw new Error("Can't find metalsmith.json or metalsmith.js")
46 }
47 this.options = options
48 this.port = options && options.port || process.env.PORT || 3000
49 this.app = undefined
50 this.watcher = undefined
51 this.server = undefined
52 this.tinylr = undefined
53 this.lrport = undefined
54 this.lrwatcher = undefined
55}
56
57/*
58 * log
59 */
60
61Runner.prototype.log = require('./log').log
62
63/*
64 * performs an initial build the runs the server
65 */
66
67Runner.prototype.start = unyield(function * () {
68 this.log('start: NODE_ENV=' + process.env.NODE_ENV)
69 yield this.build()
70 if (process.env.NODE_ENV !== 'production') {
71 this.watch()
72 }
73 return yield this.serve()
74})
75
76/*
77 * stops everything
78 */
79
80Runner.prototype.close = function () {
81 ['watcher', 'server', 'tinylr', 'lrwatcher'].forEach(function (attr) {
82 if (this[attr]) {
83 this[attr].close()
84 this[attr] = undefined
85 }
86 }.bind(this))
87}
88
89/*
90 * starts the server.
91 */
92
93Runner.prototype.serve = unyield(function * () {
94 var ms = this.metalsmith
95 var options = this.options
96
97 var app = this.app = superstatic({
98 config: {
99 root: ms.destination()
100 },
101 debug: false
102 })
103
104 if (options.livereload !== false &&
105 process.env.NODE_ENV !== 'production') {
106 yield this.enableLR()
107 }
108
109 // Log
110 // (Kinda noisy, so let's take it out for now)
111 // app.use(this.requestLogger.bind(this))
112
113 // Listen
114 var listen = thunkify(app.listen.bind(app))
115 yield listen(this.port)
116 this.log('serve: listening on http://localhost:' + this.port)
117})
118
119/*
120 * logger middleware
121 */
122
123Runner.prototype.requestLogger = function (req, res, next) {
124 this.log('serve: ' + req.method + ' ' + req.url)
125 next()
126}
127
128/*
129 * enables Livereload
130 */
131
132Runner.prototype.enableLR = unyield(function * () {
133 var ms = this.metalsmith
134 var root = ms.destination()
135
136 this.lrport = yield getport()
137 this.tinylr = yield spawnLR(this.lrport)
138 this.lrwatcher = watchLR(root, this.tinylr, onChange.bind(this))
139 this.app.use(injectLR(this.lrport))
140 this.log('livereload: listening on port ' + this.lrport)
141
142 function onChange (files) {
143 this.logFiles('->', eventSymbols.change, ms._destination, files)
144 }
145})
146
147/*
148 * Logs multiple files
149 */
150
151Runner.prototype.logFiles = function (prefix, symbol, root, files) {
152 var maxFiles = 3
153 var pre = prefix + ': ' + (symbol || ' ') + ' '
154 var join = require('path').join
155
156 files = files.sort()
157
158 if (files.length <= maxFiles) {
159 files.forEach(function (file) {
160 file = join(root, file)
161 this.log(pre + file)
162 }.bind(this))
163 } else {
164 var file = join(root, files[0])
165 var n = files.length - 1
166 this.log(pre + file + ' (and ' + n + ' others)')
167 }
168}
169
170/*
171 * starts watching for changes
172 */
173
174Runner.prototype.watch = function () {
175 var ms = this.metalsmith
176
177 this.log('watch: waiting for changes, ^C to abort')
178
179 function onWatch (argsList) {
180 argsList.forEach(function (args) {
181 var event = args[0]
182 var path = args[1]
183 var symbol = eventSymbols[event] || event
184 this.log('<-: ' + symbol + ' ' + path)
185 }.bind(this))
186
187 this.build(function () {})
188 }
189
190 this.watcher = chokidar.watch(ms.directory(), {
191 ignored: ignoreSpec(ms),
192 ignoreInitial: true,
193 cwd: ms.directory()
194 })
195 .on('all', debounce(onWatch.bind(this), 20))
196}
197
198/*
199 * checks if a file should be ignored
200 */
201
202function ignoreSpec (ms) {
203 var dir = ms.directory()
204 var dest = ms.destination()
205
206 return function (path) {
207 return false ||
208 matches(path, 'node_modules', dir) ||
209 matches(path, 'bower_components', dir) ||
210 matches(path, '.git', dir) ||
211 matches(path, dest, dir)
212 }
213}
214
215/*
216 * checks if `path` is inside `parent` under `base`
217 */
218
219function matches (path, parent, base) {
220 if (path.substr(0, 1) !== '/') {
221 path = require('path').join(base, path)
222 }
223
224 if (parent.substr(0, 1) !== '/') {
225 parent = require('path').join(base, parent)
226 }
227
228 return (path.substr(0, parent.length) === parent)
229}
230
231/*
232 * performs a one-time build
233 */
234
235Runner.prototype.build = unyield(function * () {
236 var start = new Date()
237 var ms = this.metalsmith
238 var build = thunkify(ms.build.bind(ms))
239
240 try {
241 yield build()
242 var duration = new Date() - start
243 this.log(': ' + chalk.green('✓') + ' ' + duration + 'ms')
244 } catch (err) {
245 this.log('err: ' + err.message, 'err')
246 this.log('' + err.stack, 'err')
247 throw err
248 }
249})
250
251module.exports = Runner