├── 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 | [](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 |
--------------------------------------------------------------------------------