├── test ├── mocha.opts ├── setup.js ├── support │ ├── ms.js │ ├── fixture.js │ └── runner.js ├── fixtures │ ├── with_plugin_test.js │ └── with_superstatic_test.js ├── standard_test.js ├── lib │ ├── helpers_test.js │ └── hashfile_test.js └── basic_test.js ├── fixtures ├── sample │ ├── src │ │ └── index.html │ └── metalsmith.json ├── with_plugin │ ├── src │ │ └── index.html │ └── metalsmith.json ├── with_plugin_error │ ├── src │ │ └── index.html │ └── metalsmith.json ├── with_superstatic │ ├── src │ │ └── about.html │ ├── superstatic.json │ └── metalsmith.json └── files │ ├── file1.txt │ └── file2.txt ├── .gitignore ├── lib ├── index.js ├── ensure_fresh.js ├── hashfile.js ├── helpers.js ├── livereloader.js ├── loader.js ├── reporter.js └── runner.js ├── index.js ├── .travis.yml ├── data └── metalsmith.js ├── bin └── metalsmith-start ├── package.json ├── README.md └── HISTORY.md /test/mocha.opts: -------------------------------------------------------------------------------- 1 | --require test/setup 2 | --recursive 3 | -------------------------------------------------------------------------------- /fixtures/sample/src/index.html: -------------------------------------------------------------------------------- 1 | werd -------------------------------------------------------------------------------- /fixtures/with_plugin/src/index.html: -------------------------------------------------------------------------------- 1 | werd -------------------------------------------------------------------------------- /fixtures/with_plugin_error/src/index.html: -------------------------------------------------------------------------------- 1 | werd -------------------------------------------------------------------------------- /fixtures/with_superstatic/src/about.html: -------------------------------------------------------------------------------- 1 | werd -------------------------------------------------------------------------------- /test/setup.js: -------------------------------------------------------------------------------- 1 | require('gnode') 2 | global.expect = require('expect') 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | fixtures/*/public 3 | /coverage 4 | _docpress 5 | -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | /* 2 | * usage: 3 | * require('metalsmith-start')(ms) 4 | */ 5 | -------------------------------------------------------------------------------- /fixtures/sample/metalsmith.json: -------------------------------------------------------------------------------- 1 | { 2 | "source": "./src", 3 | "destination": "./public" 4 | } 5 | -------------------------------------------------------------------------------- /fixtures/with_superstatic/superstatic.json: -------------------------------------------------------------------------------- 1 | { 2 | "public": "./public", 3 | "cleanUrls": true 4 | } 5 | -------------------------------------------------------------------------------- /fixtures/with_superstatic/metalsmith.json: -------------------------------------------------------------------------------- 1 | { 2 | "source": "./src", 3 | "destination": "./public" 4 | } 5 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | Runner: require('./lib/runner'), 3 | load: require('./lib/loader') 4 | } 5 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - 'stable' 4 | - '8' 5 | - '6' 6 | script: 7 | - npm run coverage 8 | -------------------------------------------------------------------------------- /fixtures/with_plugin_error/metalsmith.json: -------------------------------------------------------------------------------- 1 | { 2 | "source": "./src", 3 | "destination": "./public", 4 | "plugins": { 5 | "metalsmith-layouts": {} 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /test/support/ms.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Helper to make timeouts longer for CI environments 3 | */ 4 | 5 | module.exports = function ms (duration) { 6 | return process.env.CI ? duration * 5 : duration 7 | } 8 | -------------------------------------------------------------------------------- /fixtures/with_plugin/metalsmith.json: -------------------------------------------------------------------------------- 1 | { 2 | "source": "./src", 3 | "destination": "./public", 4 | "plugins": { 5 | "metalsmith-layouts": { 6 | "suppressNoFilesError": true 7 | } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /data/metalsmith.js: -------------------------------------------------------------------------------- 1 | var Metalsmith = require('metalsmith') 2 | 3 | var app = Metalsmith(__dirname) 4 | .source('./src') 5 | .destination('./public') 6 | 7 | if (module.parent) { 8 | module.exports = app 9 | } else { 10 | app.build(function (err) { if (err) throw err }) 11 | } 12 | -------------------------------------------------------------------------------- /test/support/fixture.js: -------------------------------------------------------------------------------- 1 | var path = require('path') 2 | var fs = require('fs') 3 | 4 | module.exports = fixture 5 | 6 | function fixture (name) { 7 | return path.join(__dirname, '../../fixtures', name) 8 | } 9 | 10 | fixture.file = function (name) { 11 | return fs.readFileSync(fixture(name), 'utf-8') 12 | } 13 | -------------------------------------------------------------------------------- /fixtures/files/file1.txt: -------------------------------------------------------------------------------- 1 | oblivious deliberately power skirts rock holiday laughter paint although evils silvery turn giant listen gladly opened appearing pa tariff continue lament cloth alaric giver july reel multiple penitence stale one masters christmas native ancient consent losing drift fighting hated sociable centuries downstairs ones other mary mentioned sit their novels systematically 2 | -------------------------------------------------------------------------------- /fixtures/files/file2.txt: -------------------------------------------------------------------------------- 1 | cry sword instances brushing loyalty class strait removed merchants homage ventured levee yet meanwhile allay cured or overthrown majestic barge inexorable sky speak wore deceive apply pans pomp copies pioneer brethren scandal highest glide zest specific presentation comparing unfinished counterpart betide terrible fed typical ointment current permitted takes taking agreement 2 | -------------------------------------------------------------------------------- /lib/ensure_fresh.js: -------------------------------------------------------------------------------- 1 | /* 2 | * connect middleware to suppress '304 Not Modified' statuses. this ensures 3 | * that data will always be returned and connect-livereload has a chance to 4 | * inject its scripts. 5 | */ 6 | 7 | module.exports = function (req, res, next) { 8 | if (req.headers['accept'] && ~req.headers['accept'].indexOf('text/html')) { 9 | delete req.headers['if-none-match'] 10 | delete req.headers['if-modified-since'] 11 | } 12 | next() 13 | } 14 | -------------------------------------------------------------------------------- /test/fixtures/with_plugin_test.js: -------------------------------------------------------------------------------- 1 | var fixture = require('../support/fixture') 2 | var runner = require('../support/runner') 3 | var ms = require('../support/ms') 4 | 5 | var request = require('supertest') 6 | 7 | describe('fixture: with plugin', function () { 8 | this.timeout(ms(2000)) 9 | 10 | runner(fixture('with_plugin')) 11 | 12 | it('works', function (next) { 13 | request(this.run.app).get('/') 14 | .expect(200) 15 | .end(next) 16 | }) 17 | }) 18 | -------------------------------------------------------------------------------- /test/fixtures/with_superstatic_test.js: -------------------------------------------------------------------------------- 1 | var fixture = require('../support/fixture') 2 | var runner = require('../support/runner') 3 | var ms = require('../support/ms') 4 | 5 | var request = require('supertest') 6 | 7 | describe('fixture: with_superstatic:', function () { 8 | this.timeout(ms(2000)) 9 | 10 | runner(fixture('with_superstatic')) 11 | 12 | it('honors cleanUrls', function (next) { 13 | request(this.run.app).get('/about') 14 | .expect(200) 15 | .end(next) 16 | }) 17 | }) 18 | -------------------------------------------------------------------------------- /test/standard_test.js: -------------------------------------------------------------------------------- 1 | /* global describe, it */ 2 | describe('coding style', function () { 3 | this.timeout(5000) 4 | 5 | it('conforms to standard', require('mocha-standard').files([ 6 | 'lib/**/*.js', 7 | 'index.js', 8 | 'bin/*' 9 | ])) 10 | 11 | it('tests conform to standard', require('mocha-standard').files([ 12 | 'test/**/*.js' 13 | ], { 14 | global: [ 15 | 'describe', 'it', 'before', 'after', 'beforeEach', 'afterEach', 16 | 'xdescribe', 'xit', 'expect' 17 | ] 18 | })) 19 | }) 20 | -------------------------------------------------------------------------------- /test/support/runner.js: -------------------------------------------------------------------------------- 1 | var Runner = require('../../lib/runner') 2 | var getport = require('get-port') 3 | 4 | module.exports = function runner (path) { 5 | global.before(function () { 6 | return getport().then(function (port) { 7 | this.port = port 8 | }.bind(this)) 9 | }) 10 | 11 | global.before(function () { 12 | this.run = new Runner(path, { port: this.port }) 13 | this.run.log = function () {} 14 | return this.run.start() 15 | }) 16 | 17 | global.after(function () { 18 | this.run.close() 19 | }) 20 | } 21 | -------------------------------------------------------------------------------- /lib/hashfile.js: -------------------------------------------------------------------------------- 1 | var crypto = require('crypto') 2 | var join = require('path').join 3 | var co = require('co') 4 | var readFile = require('mz/fs').readFile 5 | 6 | /** 7 | * Gets the hash of a single file. 8 | * 9 | * hashFile('image.jpg') 10 | */ 11 | 12 | exports.hashFile = co.wrap(function * (fname, digest) { 13 | var shasum = crypto.createHash(digest || 'sha1') 14 | 15 | try { 16 | var data = yield readFile(fname) 17 | } catch (e) { 18 | return null 19 | } 20 | 21 | shasum.update(data) 22 | return shasum.digest('hex') 23 | }) 24 | 25 | /** 26 | * Hashes multiple files. 27 | */ 28 | 29 | exports.hashFiles = co.wrap(function * (root, paths) { 30 | var hashes = yield paths.map(function (path) { 31 | return exports.hashFile(join(root, path)) 32 | }) 33 | 34 | var result = {} 35 | 36 | paths.forEach(function (path, idx) { 37 | result[path] = hashes[idx] 38 | }) 39 | 40 | return result 41 | }) 42 | -------------------------------------------------------------------------------- /test/lib/helpers_test.js: -------------------------------------------------------------------------------- 1 | /* global describe, it, expect */ 2 | 3 | var fixture = require('../support/fixture') 4 | var Helpers = require('../../lib/helpers') 5 | 6 | describe('helpers: isAsset()', function () { 7 | var isAsset = Helpers.isAsset 8 | 9 | it('works', function () { 10 | expect(isAsset('foo.html')).toEqual(true) 11 | expect(isAsset('foo.css')).toEqual(true) 12 | expect(isAsset('foo.jpg')).toEqual(true) 13 | }) 14 | 15 | it('rejects non-assets', function () { 16 | expect(isAsset('foo.map')).toEqual(false) 17 | expect(isAsset('foo')).toEqual(false) 18 | }) 19 | }) 20 | 21 | describe('helpers: diffHashes()', function () { 22 | var diffHashes = Helpers.diffHashes 23 | 24 | it('works', function () { 25 | var result = diffHashes({ 26 | 'index.js': 'aaa', 27 | 'style.css': 'bbb' 28 | }, { 29 | 'index.js': 'ccc', 30 | 'style.css': 'bbb' 31 | }) 32 | 33 | expect(result).toEqual([ 'index.js' ]) 34 | }) 35 | }) 36 | 37 | describe('helpers: safe()', function () { 38 | var safe = Helpers.safe 39 | 40 | it('works', function () { 41 | var stat = safe(require('mz/fs').stat) 42 | return stat('non-existent-file.txt') 43 | }) 44 | }) 45 | 46 | describe('helpers: filterFiles()', function () { 47 | var filterFiles = Helpers.filterFiles 48 | 49 | it('works', function () { 50 | return filterFiles(fixture('files/'), ['file1.txt', 'notafile.txt']) 51 | .then(function (result) { 52 | expect(result).toEqual(['file1.txt']) 53 | }) 54 | }) 55 | }) 56 | -------------------------------------------------------------------------------- /bin/metalsmith-start: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | require('gnode') 3 | 4 | if (typeof process.env.METALSMITH === 'undefined') { 5 | process.env.NODE_ENV = process.env.NODE_ENV || 'development' 6 | } 7 | 8 | var dir = process.cwd() 9 | var Runner = require('../lib/runner') 10 | // var program = require('commander') 11 | var fs = require('fs') 12 | var join = require('path').join 13 | var meow = require('meow') 14 | 15 | var program = meow([ 16 | 'Usage:', 17 | ' metalsmith-start [options]', 18 | '', 19 | 'Options:', 20 | ' -p, --port specify the port', 21 | ' -R, --no-livereload disable livereload', 22 | ' --config show sample config file', 23 | '', 24 | 'Other options:', 25 | ' -h, --help print usage information', 26 | ' -v, --version show version info and exit' 27 | ].join('\n'), { 28 | string: 'port', 29 | boolean: 'livereload', 30 | default: { 31 | livereload: true 32 | }, 33 | alias: { 34 | p: 'port', 35 | h: 'help', 36 | v: 'version' 37 | } 38 | }).flags 39 | 40 | if (program.r) { 41 | program.livereload = false 42 | delete program.r 43 | } 44 | 45 | try { 46 | if (program.config) { 47 | if (process.stdout.isTTY) { 48 | var chalk = require('chalk') 49 | process.stderr.write( 50 | chalk.green('// Instructions: save this file as metalsmith.js.\n') + 51 | chalk.green('// You can do this via \'metalstart --config > metalsmith.js\'.') + 52 | '\n\n') 53 | } 54 | var path = join(__dirname, '../data/metalsmith.js') 55 | var data = fs.readFileSync(path, 'utf-8') 56 | process.stdout.write(data) 57 | process.exit(0) 58 | } 59 | var r = new Runner(dir, program) 60 | r.start(function (err) { 61 | if (err) r.reporter.showErr(err) 62 | }) 63 | } catch (err) { 64 | console.error(err.message) 65 | } 66 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "metalsmith-start", 3 | "description": "Development server for Metalsmith.", 4 | "version": "2.0.1", 5 | "author": "Rico Sta. Cruz ", 6 | "bin": { 7 | "metalsmith-start": "bin/metalsmith-start", 8 | "metalstart": "bin/metalsmith-start" 9 | }, 10 | "bugs": { 11 | "url": "https://github.com/rstacruz/metalsmith-start/issues" 12 | }, 13 | "dependencies": { 14 | "chalk": "^2.4.1", 15 | "chokidar": "^2.0.4", 16 | "co": "4.6.0", 17 | "connect": "^3.6.6", 18 | "connect-livereload": "^0.6.0", 19 | "debounce-collect": "1.0.2", 20 | "get-port": "^4.0.0", 21 | "gnode": "0.1.2", 22 | "jstransformer-handlebars": "^1.1.0", 23 | "meow": "^5.0.0", 24 | "metalsmith": "^2.3.0", 25 | "mz": "^2.7.0", 26 | "object-assign": "^4.1.1", 27 | "observatory": "1.0.0", 28 | "superstatic": "^6.0.3", 29 | "throat": "^4.1.0", 30 | "thunkify": "2.1.2", 31 | "tiny-lr": "^1.1.1" 32 | }, 33 | "devDependencies": { 34 | "expect": "^23.6.0", 35 | "istanbul": "^0.4.5", 36 | "metalsmith-layouts": "^2.3.0", 37 | "mocha": "^5.2.0", 38 | "mocha-eventually": "1.1.0", 39 | "mocha-standard": "1.0.0", 40 | "standard": "^12.0.1", 41 | "supertest": "^3.4.2" 42 | }, 43 | "directories": { 44 | "test": "test" 45 | }, 46 | "homepage": "https://github.com/rstacruz/metalsmith-start#readme", 47 | "keywords": [ 48 | "jekyll", 49 | "metalsmith", 50 | "static" 51 | ], 52 | "license": "MIT", 53 | "main": "index.js", 54 | "repository": { 55 | "type": "git", 56 | "url": "git+https://github.com/rstacruz/metalsmith-start.git" 57 | }, 58 | "scripts": { 59 | "coverage": "istanbul cover _mocha -- --exit -R spec", 60 | "test": "mocha --exit" 61 | }, 62 | "docpress": { 63 | "css": "http://ricostacruz.com/docpress-rsc/style.css", 64 | "github": "rstacruz/metalsmith-start" 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /test/lib/hashfile_test.js: -------------------------------------------------------------------------------- 1 | var fixture = require('../support/fixture') 2 | 3 | describe('hashfile', function () { 4 | var hashFile = require('../../lib/hashfile').hashFile 5 | var hashFiles = require('../../lib/hashfile').hashFiles 6 | 7 | describe('hashFile()', function () { 8 | it('works', function () { 9 | return hashFile(fixture('files/file1.txt')) 10 | .then(function (res) { 11 | expect(res).toEqual('ce57c01c8bda67ce22ded81b28657213a99e69b3') 12 | }) 13 | }) 14 | 15 | it('works again', function () { 16 | return hashFile(fixture('files/file2.txt')) 17 | .then(function (res) { 18 | expect(res).toEqual('d06a59c73d2363d6c0692de0e3d7629a9480f901') 19 | }) 20 | }) 21 | 22 | it('can handle not founds', function () { 23 | return hashFile('ayylmao') 24 | .then(function (res) { 25 | expect(res).toEqual(null) 26 | }) 27 | }) 28 | 29 | it('can handle directories', function () { 30 | return hashFile(fixture('files/')) 31 | .then(function (res) { 32 | expect(res).toEqual(null) 33 | }) 34 | }) 35 | }) 36 | 37 | describe('hashFiles()', function () { 38 | beforeEach(function () { 39 | var files = [ 40 | 'file1.txt', 41 | 'file2.txt' 42 | ] 43 | 44 | return hashFiles(fixture('files'), files) 45 | .then(function (res) { 46 | this.result = res 47 | }.bind(this)) 48 | }) 49 | 50 | it('turns it into an object', function () { 51 | expect(this.result).toEqual(expect.any(Object)) 52 | }) 53 | 54 | it('has files for keys', function () { 55 | expect(Object.keys(this.result)).toContain('file1.txt') 56 | expect(Object.keys(this.result)).toContain('file2.txt') 57 | }) 58 | 59 | it('returns stuff', function () { 60 | expect(this.result['file1.txt']).toEqual( 61 | 'ce57c01c8bda67ce22ded81b28657213a99e69b3') 62 | }) 63 | }) 64 | }) 65 | -------------------------------------------------------------------------------- /lib/helpers.js: -------------------------------------------------------------------------------- 1 | var wrap = require('co').wrap 2 | 3 | /** 4 | * Given a thunk `fn`, return a thunk of it that returns `undefined` instead of 5 | * an error. Useful for, say, `fs.stat()`. 6 | */ 7 | 8 | var safe = function (fn) { 9 | return wrap(function * () { 10 | try { 11 | var result = yield fn.apply(this, arguments) 12 | return result 13 | } catch (e) { 14 | // noop 15 | } 16 | }) 17 | } 18 | 19 | exports.safe = safe 20 | 21 | var stat = safe(require('mz/fs').stat) 22 | 23 | /** 24 | * Given two objects, return a list of keys in `new` that values are changed in 25 | * `old`. Also, propagate the new keys into `old`. 26 | * 27 | * var old = { 28 | * 'index.html': 'abc' 29 | * 'script.js': 'def' 30 | * } 31 | * 32 | * var neww = { 33 | * 'index.html': 'xyz' 34 | * 'script.js': 'def' 35 | * } 36 | * 37 | * diffHashes(old, new) 38 | * => [ 'index.html' ] 39 | */ 40 | 41 | exports.diffHashes = function (old, neww) { 42 | var updated = [] 43 | 44 | Object.keys(neww).forEach(function (key) { 45 | if (!old[key] || old[key] !== neww[key]) { 46 | updated.push(key) 47 | } 48 | old[key] = neww[key] 49 | }) 50 | 51 | return updated 52 | } 53 | 54 | /** 55 | * Given a list of files `paths`, return only the files; weed out any 56 | * directories or whatnot. 57 | */ 58 | 59 | exports.filterFiles = wrap(function * (cwd, paths) { 60 | var stats = yield paths.map(function (path) { 61 | return stat(require('path').join(cwd, path)) 62 | }) 63 | 64 | var result = paths.filter(function (path, idx) { 65 | if (stats[idx] && stats[idx].isFile()) return true 66 | }) 67 | 68 | return result 69 | }) 70 | 71 | var ASSET_EXPR = /\.(css|js|html|jpe?g|png|gif|woff2?|otf|svg)$/ 72 | 73 | /* 74 | * Check if a file is a web asset. Things like `.map` should be stripped, 75 | * because these will cause unintentional livereloads. 76 | */ 77 | 78 | exports.isAsset = function (filename) { 79 | return ASSET_EXPR.test(filename) 80 | } 81 | -------------------------------------------------------------------------------- /lib/livereloader.js: -------------------------------------------------------------------------------- 1 | var Tinylr = require('tiny-lr') 2 | var chokidar = require('chokidar') 3 | var co = require('co') 4 | 5 | var debounce = require('debounce-collect') 6 | var hashFiles = require('./hashfile').hashFiles 7 | var isAsset = require('./helpers').isAsset 8 | var diffHashes = require('./helpers').diffHashes 9 | var filterFiles = require('./helpers').filterFiles 10 | 11 | /* 12 | * injects livereload into connect() server, and starts a livereload server at 13 | * a random port 14 | */ 15 | 16 | exports.spawnLR = co.wrap(function * (port) { 17 | var lrServer = new Tinylr() 18 | lrServer.listen(port) 19 | return lrServer 20 | }) 21 | 22 | /* 23 | * returns a watcher to update tinyLR 24 | */ 25 | 26 | exports.watchLR = function (root, lrServer, onChange) { 27 | var hashes = {} 28 | 29 | var update = co.wrap(function * (argsList) { 30 | // Get a list of paths that have been 'change'd or 'create'd. 31 | // If it's been deleted, mark it off. 32 | var paths = argsList.reduce(function (list, args) { 33 | var fname = args[1] 34 | if (!isAsset(fname)) return list 35 | 36 | if (args[0] === 'delete') { 37 | delete hashes[fname] 38 | } else { 39 | list.push(fname) 40 | } 41 | return list 42 | }, []) 43 | 44 | // Get rid of any non-files (directories) 45 | paths = yield filterFiles(root, paths) 46 | if (paths.length === 0) return 47 | 48 | // Get their hashes 49 | var newHashes = yield hashFiles(root, paths) 50 | 51 | // Compare with old 52 | var files = diffHashes(hashes, newHashes) 53 | if (files.length === 0) return 54 | 55 | // Call the callback 56 | if (onChange) onChange(files) 57 | 58 | files = files.map(escape) 59 | lrServer.changed({ 60 | body: { files: files } 61 | }) 62 | }) 63 | 64 | var uupdate = function (argsList) { 65 | update(argsList).catch(function (err) { throw err }) 66 | } 67 | 68 | return chokidar.watch(root, { 69 | ignoreInitial: true, 70 | cwd: root 71 | }) 72 | .on('all', debounce(uupdate, 50)) 73 | } 74 | -------------------------------------------------------------------------------- /test/basic_test.js: -------------------------------------------------------------------------------- 1 | /* global describe, it, beforeEach, afterEach */ 2 | var fixture = require('./support/fixture') 3 | var runner = require('./support/runner') 4 | var eventually = require('mocha-eventually') 5 | 6 | var request = require('supertest') 7 | var fs = require('fs') 8 | 9 | describe('my project', function () { 10 | this.timeout(10000) 11 | 12 | runner(fixture('sample')) 13 | 14 | beforeEach(function () { 15 | this.req = request('http://localhost:' + this.run.port) 16 | }) 17 | 18 | it('works', function (next) { 19 | request(this.run.app).get('/') 20 | .expect(200) 21 | .end(next) 22 | }) 23 | 24 | describe('livereload', function () { 25 | beforeEach(function () { 26 | this.req = request('http://localhost:' + this.run.lrport) 27 | }) 28 | 29 | it('has livereload', function (next) { 30 | this.req 31 | .get('/livereload.js') 32 | .set('Accept', 'text/html') 33 | .expect(200) 34 | .end(next) 35 | }) 36 | }) 37 | 38 | describe('livereload test', function () { 39 | beforeEach(function (next) { 40 | this.run.metalsmith.build(function (err, res) { 41 | if (err) throw err 42 | next() 43 | }) 44 | }) 45 | 46 | // just a sanity test really, let's make sure LiveReload doesn't throw 47 | // exceptions along the way 48 | it('works', function () { 49 | return eventually(function (next) { 50 | request(this.run.app).get('/') 51 | .expect(/./) 52 | .end(next) 53 | }.bind(this), 1000) 54 | }) 55 | }) 56 | 57 | describe('main port', function () { 58 | it('has livereload', function (next) { 59 | request(this.run.app).get('/') 60 | .set('Accept', 'text/html') 61 | .expect(/\/livereload.js/) 62 | .end(next) 63 | }) 64 | 65 | it('returns 404', function (next) { 66 | request(this.run.app).get('/aoeu') 67 | .expect(404) 68 | .end(next) 69 | }) 70 | }) 71 | 72 | describe('auto rebuilding', function () { 73 | beforeEach(function () { 74 | this.oldData = fixture.file('sample/src/index.html') 75 | }) 76 | 77 | afterEach(function () { 78 | fs.writeFileSync(fixture('sample/src/index.html'), this.oldData, 'utf-8') 79 | }) 80 | 81 | it('auto rebuilds', function () { 82 | fs.writeFileSync(fixture('sample/src/index.html'), 'werd', 'utf-8') 83 | return eventually(function (next) { 84 | request(this.run.app).get('/') 85 | .expect(/werd/) 86 | .end(next) 87 | }.bind(this), 8000) 88 | }) 89 | }) 90 | }) 91 | -------------------------------------------------------------------------------- /lib/loader.js: -------------------------------------------------------------------------------- 1 | var Metalsmith = require('metalsmith') 2 | var exists = require('fs').existsSync 3 | var assign = require('object-assign') 4 | var resolve = require('path').resolve 5 | 6 | /* 7 | * Defaults 8 | */ 9 | 10 | var defaults = { 11 | config: 'metalsmith.json' 12 | } 13 | 14 | /** 15 | * Returns metalsmith object for `dir` 16 | */ 17 | 18 | function ms (dir, options) { 19 | options = assign({}, defaults, options || {}) 20 | 21 | var path = resolve(dir, options.config) 22 | var json = loadJson(path, options.config) 23 | var metalsmith = loadMetalsmith(dir, json) 24 | loadPlugins(metalsmith, dir, json.plugins) 25 | 26 | return metalsmith 27 | } 28 | 29 | /* 30 | * Internal: Loads a JSON file 31 | */ 32 | 33 | function loadJson (path, config) { 34 | try { 35 | return require(path) 36 | } catch (e) { 37 | throw new Error('it seems like ' + config + ' is malformed.') 38 | } 39 | } 40 | 41 | /** 42 | * Initializes a metalsmith instance 43 | */ 44 | 45 | function loadMetalsmith (dir, json) { 46 | var metalsmith = new Metalsmith(dir) 47 | 48 | if (json.source) metalsmith.source(json.source) 49 | if (json.destination) metalsmith.destination(json.destination) 50 | if (json.concurrency) metalsmith.concurrency(json.concurrency) 51 | if (json.metadata) metalsmith.metadata(json.metadata) 52 | if (typeof json.clean === 'boolean') metalsmith.clean(json.clean) 53 | 54 | return metalsmith 55 | } 56 | 57 | /* 58 | * Loads `plugins` onto `metalsmith` instance 59 | */ 60 | 61 | function loadPlugins (metalsmith, dir, plugins) { 62 | normalize(plugins).forEach(function (plugin) { 63 | for (var name in plugin) { 64 | var opts = plugin[name] 65 | var mod 66 | 67 | try { 68 | var local = resolve(dir, name) 69 | var npm = resolve(dir, 'node_modules', name) 70 | 71 | if (exists(local) || exists(local + '.js')) { 72 | mod = require(local) 73 | } else if (exists(npm)) { 74 | mod = require(npm) 75 | } else { 76 | mod = require(name) 77 | } 78 | } catch (e) { 79 | throw new Error('failed to require plugin "' + name + '".') 80 | } 81 | 82 | try { 83 | metalsmith.use(mod(opts)) 84 | } catch (e) { 85 | // prepend with plugin 86 | e.message = '[' + name + '] ' + e.message 87 | throw e 88 | } 89 | } 90 | }) 91 | } 92 | 93 | /** 94 | * Normalize an `obj` of plugins. 95 | * 96 | * @param {Array or Object} obj 97 | * @return {Array} 98 | */ 99 | 100 | function normalize (obj) { 101 | if (obj instanceof Array) return obj 102 | var ret = [] 103 | 104 | for (var key in obj) { 105 | var plugin = {} 106 | plugin[key] = obj[key] 107 | ret.push(plugin) 108 | } 109 | 110 | return ret 111 | } 112 | 113 | module.exports = ms 114 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # metalsmith-start 2 | 3 | Development server for metalsmith. 4 | 5 | [![Status](https://travis-ci.org/rstacruz/metalsmith-start.svg?branch=master)](https://travis-ci.org/rstacruz/metalsmith-start "See test builds") 6 | 7 | - Consumes the standard `metalsmith.json`. 8 | - Consumes `metalsmith.js`. 9 | - Auto-recompiles when files are changed. 10 | - Starts development server and LiveReload. 11 | 12 |
13 | 14 | ## Command-line 15 | 16 | Run `metalsmith-start` or `metalstart` in your Metalsmith's project directory. 17 | 18 | ``` 19 | metalsmith-start 20 | ``` 21 | 22 | See `--help` for more options. 23 | 24 |
25 | 26 | ## Production 27 | 28 | metalsmith-start honors the following variables: 29 | 30 | * `NODE_ENV` 31 | * `PORT` 32 | 33 | If either `NODE_ENV` is set to `production`, then development features (such as LiveReload) will be disabled by default. 34 | 35 | This means that you can run a production setup using: 36 | 37 | ```sh 38 | env NODE_ENV=production PORT=4000 metalsmith-start 39 | ``` 40 | 41 | This also means you can push your repo to Heroku with no changes and it'll work just fine. 42 | 43 |
44 | 45 | ## Using metalsmith.js 46 | 47 | If a file called `metalsmith.js` is found in the current directory, it's assumed it's a JS module that returns a `Metalsmith` instance. 48 | 49 | Below is a sample metalsmith.js: 50 | 51 | ```js 52 | var Metalsmith = require('metalsmith') 53 | 54 | var app = Metalsmith(__dirname) 55 | .source('./src') 56 | .destination('./public') 57 | .use(...) 58 | 59 | if (module.parent) { 60 | module.exports = app 61 | } else { 62 | app.build(function (err) { if (err) throw err }) 63 | } 64 | ``` 65 | 66 |
67 | 68 | ## Superstatic 69 | 70 | If `superstatic.json` is found in the current directory, it'll automatically be picked up. This allows you to, say, use `cleanUrls` to allow pages to be served without the `.html` extension. 71 | 72 | See [superstatic] for more information. 73 | 74 | [superstatic]: https://www.npmjs.com/package/superstatic 75 | 76 |
77 | 78 | ## Programatic usage 79 | 80 | ```js 81 | var Runner = require('metalsmith-start').Runner 82 | 83 | var ms = new Metalsmith(dir) 84 | .use(...) 85 | .use(...) 86 | 87 | var r = new Runner(ms) 88 | r.start(function () { 89 | console.log('started on ' + r.port) 90 | }) 91 | ``` 92 | 93 |
94 | 95 | ## Thanks 96 | 97 | **metalsmith-start** © 2015+, Rico Sta. Cruz. Released under the [MIT] License.
98 | Authored and maintained by Rico Sta. Cruz with help from contributors ([list][contributors]). 99 | 100 | > [ricostacruz.com](http://ricostacruz.com)  ·  101 | > GitHub [@rstacruz](https://github.com/rstacruz)  ·  102 | > Twitter [@rstacruz](https://twitter.com/rstacruz) 103 | 104 | [MIT]: http://mit-license.org/ 105 | [contributors]: http://github.com/rstacruz/metalsmith-start/contributors 106 | -------------------------------------------------------------------------------- /HISTORY.md: -------------------------------------------------------------------------------- 1 | ## [v2.0.1] 2 | > Jan 5, 2016 3 | 4 | - Fix "Can't modify headers" error messages. 5 | 6 | ## [v2.0.0] 7 | > Jan 3, 2016 8 | 9 | - Update to [superstatic] v4.0.1. If you use `superstatic.json`, you will need to be update it; see [superstatic 4.0.0](https://github.com/firebase/superstatic/releases/tag/4.0.0) for info. 10 | - Shorten required console columns for wide mode 11 | - Internal: update get-port to v2.1.0 12 | - Internal: update to meow v3.5.0 13 | 14 | ## [v1.4.2] 15 | > Oct 16, 2015 16 | 17 | - Fix last version by bulding `connect` 18 | 19 | ## [v1.4.1] 20 | > Oct 16, 2015 21 | 22 | - Update superstatic to [v3.0.0](https://github.com/firebase/superstatic/blob/master/CHANGELOG.md) (from 1.2.3) 23 | 24 | ## [v1.3.4] 25 | > Oct 16, 2015 26 | 27 | - Fix jaggedness in status display 28 | 29 | ## [v1.3.3] 30 | > Oct 16, 2015 31 | 32 | - Update [observatory](https://www.npmjs.com/package/observatory) to v1.0.0 33 | 34 | ## [v1.3.2] 35 | - Oct 15, 2015 36 | 37 | - Oops, last version didn't work. 38 | 39 | ## [v1.3.1] 40 | > Oct 15, 2015 41 | 42 | - Fix issue with cursor behaving erratically. (https://github.com/dylang/observatory/pull/6) 43 | 44 | ## [v1.3.0] 45 | > Oct 15, 2015 46 | 47 | - Updated look of the CLI reporter. 48 | 49 | ## [v1.2.0] 50 | > Oct 9, 2015 51 | 52 | - Lock version dependencies; no functional changes. 53 | 54 | ## [v1.1.0] 55 | > Oct 5, 2015 56 | 57 | - Add support for programatically running a Metalsmith instance. 58 | 59 | ## [v1.0.1] 60 | > Sep 20, 2015 61 | 62 | - Improve logging format of plugin errors in metalsmith.json. 63 | 64 | ## [v1.0.0] 65 | > Sep 20, 2015 66 | 67 | - Declared as v1.0.0 - no functional changes. 68 | 69 | ## [v0.6.1] 70 | > Sep 20, 2015 71 | 72 | - Minor improvements to auto-compiling 73 | - Fix issue where writing .map files can refresh an entire page 74 | 75 | ## [v0.6.0] 76 | > Sep 20, 2015 77 | 78 | - Add support for [superstatic] configuration. 79 | 80 | [superstatic]: https://www.npmjs.com/package/superstatic 81 | 82 | ## [v0.5.0] 83 | > Sep 20, 2015 84 | 85 | - Significantly improve LiveReloading functionality. 86 | - Don't log 'serve' messages anymore (wasn't really nedeed). 87 | 88 | ## [v0.4.0] 89 | > Aug 14, 2015 90 | 91 | - Use NODE_ENV environment variable. 92 | - Disables watching and livereload on production. 93 | 94 | ## [v0.1.0] 95 | > Aug 14, 2015 96 | 97 | - Initial release. 98 | 99 | [superstatic]: https://www.npmjs.com/package/superstatic 100 | [v0.1.0]: https://github.com/rstacruz/metalsmith-start/tree/v0.1.0 101 | [v0.4.0]: https://github.com/rstacruz/metalsmith-start/compare/v0.1.0...v0.4.0 102 | [v0.5.0]: https://github.com/rstacruz/metalsmith-start/compare/v0.4.0...v0.5.0 103 | [v0.6.0]: https://github.com/rstacruz/metalsmith-start/compare/v0.5.0...v0.6.0 104 | [v0.6.1]: https://github.com/rstacruz/metalsmith-start/compare/v0.6.0...v0.6.1 105 | [v1.0.0]: https://github.com/rstacruz/metalsmith-start/compare/v0.6.1...v1.0.0 106 | [v1.0.1]: https://github.com/rstacruz/metalsmith-start/compare/v1.0.0...v1.0.1 107 | [v1.1.0]: https://github.com/rstacruz/metalsmith-start/compare/v1.0.1...v1.1.0 108 | [v1.2.0]: https://github.com/rstacruz/metalsmith-start/compare/v1.1.0...v1.2.0 109 | [v1.3.0]: https://github.com/rstacruz/metalsmith-start/compare/v1.2.0...v1.3.0 110 | [v1.3.1]: https://github.com/rstacruz/metalsmith-start/compare/v1.3.0...v1.3.1 111 | [v1.3.2]: https://github.com/rstacruz/metalsmith-start/compare/v1.3.1...v1.3.2 112 | [v1.3.3]: https://github.com/rstacruz/metalsmith-start/compare/v1.3.2...v1.3.3 113 | [v1.3.4]: https://github.com/rstacruz/metalsmith-start/compare/v1.3.3...v1.3.4 114 | [v1.4.1]: https://github.com/rstacruz/metalsmith-start/compare/v1.3.4...v1.4.1 115 | [v1.4.2]: https://github.com/rstacruz/metalsmith-start/compare/v1.4.1...v1.4.2 116 | [v2.0.0]: https://github.com/rstacruz/metalsmith-start/compare/v1.4.2...v2.0.0 117 | [v2.0.1]: https://github.com/rstacruz/metalsmith-start/compare/v2.0.0...v2.0.1 118 | -------------------------------------------------------------------------------- /lib/reporter.js: -------------------------------------------------------------------------------- 1 | var chalk = require('chalk') 2 | 3 | /* 4 | * Theme 5 | */ 6 | 7 | var c = { 8 | accent1: chalk.gray, // subtitle 9 | accent2: chalk.blue, // build text 10 | headline: chalk.bold, // main title 11 | url: chalk.underline, 12 | ok: chalk.green, 13 | red: chalk.red, 14 | mute: chalk.gray 15 | } 16 | 17 | var symbols = { 18 | add: c.ok('+'), 19 | addDir: c.ok('+'), 20 | change: c.accent1('↔'), 21 | unlink: c.red('×'), 22 | unlinkDir: c.red('×'), 23 | wait: c.mute('···'), 24 | error: c.red('err ✗'), 25 | x: c.red('✗'), 26 | ok: c.ok('✓'), 27 | off: c.red('off'), 28 | on: c.ok('✓ on') 29 | } 30 | 31 | function Reporter () { 32 | this.observatory = require('observatory').settings({ 33 | formatStatus: function (label, state) { return label }, 34 | width: 50, 35 | prefix: ' ' 36 | }) 37 | } 38 | 39 | Reporter.prototype = { 40 | add: function (msg) { 41 | var task = this.observatory.add(msg) 42 | task.render = customRender 43 | return task 44 | }, 45 | 46 | // First run 47 | start: function (banner, port) { 48 | this.add('') 49 | this.add(c.headline(banner)) 50 | this.add(c.accent1('starting ' + process.env.NODE_ENV + ' - ^C to exit')) 51 | this.add('') 52 | 53 | this.tasks = { 54 | build: this.add(' first build').status(symbols.wait), 55 | // watch: this.add(' watching updates').status(symbols.wait), 56 | livereload: this.add(' livereload').status(symbols.wait), 57 | serve: this.add(' ' + c.url('http://localhost:' + port)).status(symbols.wait), 58 | _: this.add(''), 59 | status: this.add(' ' + c.accent1('Starting up...')).update() 60 | } 61 | }, 62 | 63 | // First run: build ok 64 | firstBuildOk: function (res) { 65 | this.tasks.build.done(c.ok(res.duration + 'ms')) 66 | }, 67 | 68 | // First run: build fail 69 | firstBuildError: function (err) { 70 | this.tasks.build.fail(symbols.error) 71 | this.showErr(err) 72 | }, 73 | 74 | // First run: LR off 75 | livereloadOff: function () { 76 | this.tasks.livereload.fail(symbols.off) 77 | }, 78 | 79 | // First run: LR on 80 | livereloadOn: function () { 81 | this.tasks.livereload.done(symbols.on) 82 | }, 83 | 84 | // First run: Watch on 85 | watchOn: function () { 86 | // this.tasks.watch.done(symbols.on) 87 | }, 88 | 89 | // First run: finally running 90 | running: function () { 91 | this.tasks.serve.done(symbols.on) 92 | 93 | this.tasks.status.description = ' ' + c.headline('Running') 94 | this.tasks.status.update() 95 | }, 96 | 97 | // Watch was triggered 98 | buildStart: function (event, files, argsList) { 99 | var symbol = symbols[event] || ' ' 100 | var fname = filesMessage(files, { short: true }) 101 | 102 | var task = this.add(symbol + ' ' + fname).status(symbols.wait) 103 | this.lastTask = task 104 | return task 105 | }, 106 | 107 | // Building complete 108 | buildOk: function (task, res) { 109 | task.done(c.ok(res.duration + 'ms')) 110 | }, 111 | 112 | // Building failed 113 | buildFail: function (task, err) { 114 | task.fail(symbols.error) 115 | this.showErr(err) 116 | }, 117 | 118 | // LiveReload was updated 119 | buildTo: function (files) { 120 | var msg = c.accent2('→ ' + filesMessage(files)) 121 | if (process.stdout.columns > 90 && this.lastTask) { 122 | this.lastTask.details(msg) 123 | } else { 124 | this.add(msg).update() 125 | } 126 | }, 127 | 128 | // Show an error 129 | showErr: function (err) { 130 | this.add('') 131 | this.add(symbols.x + ' ' + err.message) 132 | err.stack.split('\n').slice(1).forEach(function (line) { 133 | this.add(' ' + c.mute(line.trim())) 134 | }.bind(this)) 135 | this.add('') 136 | } 137 | } 138 | 139 | function filesMessage (files, options) { 140 | if (files.length === 1) { 141 | return files[0] 142 | } else if (options && options.short) { 143 | return files[0] + ' (+' + (files.length - 1) + ')' 144 | } else { 145 | return files[0] + ' (+' + (files.length - 1) + ' more)' 146 | } 147 | } 148 | 149 | var settings = require('observatory/lib/settings') 150 | var out = require('observatory/lib/out') 151 | 152 | function customRender () { 153 | var task = this 154 | var statusLabel = '' + settings.formatStatus(task.statusLabel, task.state) 155 | var statusWidth = 7 156 | var output = 157 | settings.prefix() + 158 | out.padding(statusWidth - out.ln(statusLabel)) + 159 | statusLabel + 160 | ' ' + 161 | task.description + 162 | (task.detailsLabel.toString().length ? ( 163 | out.padding(settings.width() - statusWidth - out.ln(task.description) - 2) + 164 | ' ' + 165 | task.detailsLabel) : '') 166 | var length = out.ln(output) 167 | var clear = out.padding(task.longest - length + 1) 168 | task.longest = Math.max(length, task.longest) 169 | return output + clear 170 | } 171 | 172 | module.exports = Reporter 173 | -------------------------------------------------------------------------------- /lib/runner.js: -------------------------------------------------------------------------------- 1 | var wrap = require('co').wrap 2 | var chokidar = require('chokidar') 3 | var thunkify = require('thunkify') 4 | var superstatic = require('superstatic') 5 | var connect = require('connect') 6 | var join = require('path').join 7 | 8 | var getport = require('get-port') 9 | var spawnLR = require('./livereloader').spawnLR 10 | var watchLR = require('./livereloader').watchLR 11 | var loadJson = require('./loader') 12 | var debounce = require('debounce-collect') 13 | var ensureFresh = require('./ensure_fresh') 14 | var Reporter = require('./reporter') 15 | var throat = require('throat') 16 | 17 | function exists (file) { 18 | try { 19 | return require('fs').statSync(file) 20 | } catch (e) { 21 | return false 22 | } 23 | } 24 | 25 | /* 26 | * (Class) the runner. 27 | * 28 | * Pass it a `dir`, which can be a directory string, or a Metalsmith instance. 29 | * 30 | * var app = metalsmith('.') 31 | * var r = new Runner(app) 32 | * 33 | * If a directory is passed to it, it will look for `metalsmith.js` or 34 | * `metalsmith.json`. 35 | * 36 | * Then run it. 37 | * 38 | * r.start((err) => { if (err) throw err }) 39 | * 40 | * Available options: 41 | * 42 | * - `port` (Number) 43 | * - `livereload` (Boolean) 44 | */ 45 | 46 | function Runner (dir, options) { 47 | if (!options) options = {} 48 | if (isMetalsmith(dir)) { 49 | this.metalsmith = dir 50 | dir = this.metalsmith.directory() 51 | } else if (exists(join(dir, 'metalsmith.json'))) { 52 | this.metalsmith = loadJson(dir) 53 | } else if (exists('metalsmith.js')) { 54 | this.metalsmith = require(join(dir, 'metalsmith.js')) 55 | } else { 56 | throw new Error("Can't find metalsmith.json or metalsmith.js") 57 | } 58 | this.options = options 59 | this.port = (options && options.port) || process.env.PORT || 3000 60 | this.app = undefined 61 | this.watcher = undefined 62 | this.server = undefined 63 | this.tinylr = undefined 64 | this.lrport = undefined 65 | this.lrwatcher = undefined 66 | this.banner = (options && options.banner) || 'Metalsmith' 67 | } 68 | 69 | /* 70 | * performs an initial build the runs the server 71 | */ 72 | 73 | Runner.prototype.start = wrap(function * () { 74 | this.reporter = new Reporter() 75 | this.reporter.start(this.banner, this.port) 76 | 77 | try { 78 | var res = yield this.build() 79 | this.reporter.firstBuildOk(res) 80 | } catch (err) { 81 | this.reporter.firstBuildError(err) 82 | } 83 | 84 | if (process.env.NODE_ENV !== 'production') this.watch() 85 | return yield this.serve() 86 | }) 87 | 88 | /* 89 | * stops everything 90 | */ 91 | 92 | Runner.prototype.close = function () { 93 | ['watcher', 'server', 'tinylr', 'lrwatcher'].forEach(function (attr) { 94 | if (this[attr]) { 95 | this[attr].close() 96 | this[attr] = undefined 97 | } 98 | }.bind(this)) 99 | } 100 | 101 | Runner.prototype.useLivereload = function () { 102 | return this.options.livereload !== false && 103 | process.env.NODE_ENV !== 'production' 104 | } 105 | 106 | /* 107 | * starts the server. 108 | */ 109 | 110 | Runner.prototype.serve = wrap(function * () { 111 | var ms = this.metalsmith 112 | var app = this.app = connect() 113 | 114 | if (this.useLivereload()) { 115 | yield this.enableLR() 116 | } else { 117 | this.reporter.livereloadOff() 118 | } 119 | 120 | app.use(ensureFresh) 121 | app.use(superstatic({ 122 | config: ssConfig(ms), 123 | cwd: ms.directory(), 124 | debug: false 125 | })) 126 | 127 | // Listen 128 | var listen = thunkify(app.listen.bind(app)) 129 | yield listen(this.port) 130 | 131 | // Update status 132 | this.reporter.running() 133 | }) 134 | 135 | /* 136 | * enables Livereload 137 | */ 138 | 139 | Runner.prototype.enableLR = wrap(function * () { 140 | var ms = this.metalsmith 141 | var root = ms.destination() 142 | 143 | this.lrport = yield getport() 144 | this.tinylr = yield spawnLR(this.lrport) 145 | this.lrwatcher = watchLR(root, this.tinylr, onChange.bind(this)) 146 | this.app.use(require('connect-livereload')({ port: this.lrport })) 147 | this.reporter.livereloadOn() 148 | 149 | function onChange (files) { 150 | this.reporter.buildTo(files) 151 | } 152 | }) 153 | 154 | /* 155 | * starts watching for changes 156 | */ 157 | 158 | Runner.prototype.watch = function () { 159 | var ms = this.metalsmith 160 | 161 | this.reporter.watchOn() 162 | 163 | var build = throat(1, this.build.bind(this)) 164 | 165 | var onWatch = wrap(function * (argsList) { 166 | var files = argsList.map(function (args) { return args[1] }) 167 | var task = this.reporter.buildStart(argsList[0][0], files) 168 | 169 | try { 170 | var res = yield build() 171 | this.reporter.buildOk(task, res) 172 | return res 173 | } catch (err) { 174 | this.reporter.buildFail(task, err) 175 | throw err 176 | } 177 | }.bind(this)) 178 | 179 | this.watcher = chokidar.watch(ms.directory(), { 180 | ignored: ignoreSpec(ms), 181 | ignoreInitial: true, 182 | cwd: ms.directory() 183 | }) 184 | .on('all', debounce(onWatch.bind(this), 20)) 185 | } 186 | 187 | /* 188 | * checks if a file should be ignored 189 | */ 190 | 191 | function ignoreSpec (ms) { 192 | var dir = ms.directory() 193 | var dest = ms.destination() 194 | 195 | return function (path) { 196 | return false || 197 | matches(path, 'node_modules', dir) || 198 | matches(path, 'bower_components', dir) || 199 | matches(path, '.git', dir) || 200 | matches(path, dest, dir) 201 | } 202 | } 203 | 204 | /* 205 | * checks if `path` is inside `parent` under `base` 206 | */ 207 | 208 | function matches (path, parent, base) { 209 | if (path.substr(0, 1) !== '/') { 210 | path = require('path').join(base, path) 211 | } 212 | 213 | if (parent.substr(0, 1) !== '/') { 214 | parent = require('path').join(base, parent) 215 | } 216 | 217 | return (path.substr(0, parent.length) === parent) 218 | } 219 | 220 | /* 221 | * performs a one-time build 222 | */ 223 | 224 | Runner.prototype.build = wrap(function * () { 225 | var start = new Date() 226 | var ms = this.metalsmith 227 | var build = thunkify(ms.build.bind(ms)) 228 | 229 | try { 230 | yield build() 231 | var duration = new Date() - start 232 | return { duration: duration } 233 | } catch (err) { 234 | throw err 235 | } 236 | }) 237 | 238 | function isMetalsmith (obj) { 239 | return typeof obj === 'object' && 240 | typeof obj.directory === 'function' 241 | } 242 | 243 | function ssConfig (ms) { 244 | var ssConfig = hasSuperstatic(ms.directory()) 245 | if (ssConfig) return ssConfig 246 | 247 | return { cwd: ms.destination() } 248 | } 249 | 250 | function hasSuperstatic (dir) { 251 | function t (fn) { 252 | var path = join(dir, fn) 253 | if (exists(path)) return path 254 | } 255 | 256 | return t('superstatic.json') || t('divshot.json') || t('firebase.json') 257 | } 258 | 259 | module.exports = Runner 260 | --------------------------------------------------------------------------------