diff --git a/.gitignore b/.gitignore index 1cd3404..2aa68ad 100644 --- a/.gitignore +++ b/.gitignore @@ -3,4 +3,7 @@ node_modules *.log .DS_Store bundle.js -bundle-expected.js \ No newline at end of file +bundle-expected.js +.tmp +*.swp +*.swo diff --git a/.npmignore b/.npmignore index 1663f92..ec15756 100644 --- a/.npmignore +++ b/.npmignore @@ -9,4 +9,5 @@ demo/ docs/ example/ .npmignore +.tmp LICENSE.md \ No newline at end of file diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..eb80bb5 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,10 @@ +language: node_js +node_js: + - "0.8" + - "0.10" + - "0.12" + - "4.0" + - "4.1" + - "5.0" +before_install: + - npm install -g npm@~2.14.5 diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..ddc1580 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,160 @@ +# 8.2.0 + +Add support for SSL (HTTPS) with `--ssl`, `--cert` and `--key` options. + +# 8.1.0 + +Add `--cors` flag to enable `Access-Control-Allow-Origin: *` + +# 8.0.4 + +Bump required deps. + +# 8.0.3 + +Fix `opts.live` as a string, allowing an array of options to be passed to filter file names. + +# 8.0.2 + +Fix flow so that bundling events start after server connects, also updated upstream in watchify-middleware. + +# 8.0.1 + +Fix parsing issue with LiveReload resp modifier. + +# 8.0.0 + +The server code has been refactored to use connect/express-style middleware stacking. Fixes [#80](https://github.com/mattdesl/budo/issues/80), [#79](https://github.com/mattdesl/budo/issues/79), [#124](https://github.com/mattdesl/budo/issues/124), [#128](https://github.com/mattdesl/budo/issues/128). + +##### Major Changes + +Functions for `opts.middleware` now assumes the following signature, and will not behave differently based on the number of arguments you specify: + + - `middleware(req, res, next)` + +##### Minor Changes + +The `middleware` options can now be an array of functions, or a single function. + +# 7.1.0 + +Added `--watch-glob` option which allows you to override the default watch glob without having to go through the `live()` / `watch()` API + +# 7.0.4 + +Small patch [#117](https://github.com/mattdesl/budo/pull/117) to fix a failing test in 7.0.3. + +# 7.0.3 + +- Bole no longer double-logs on shut-down and re-start. +- Fixed issue with request sizes being logged incorrectly in terminal + +# 7.0.1 .. 7.0.2 + +Small patches for [#110](https://github.com/mattdesl/budo/pull/110) and [#111](https://github.com/mattdesl/budo/pull/111). + +# 7.0.0 + +Fixes previous patch and also updates to garnish redesign, leading to new log styles. + +Since various ndjson flags have changed, this is a potentially breaking change. + +Also added a `--verbose` / `-v` option like watchify, which adds some additional debug messages. + +# 6.1.1 + +Fixes live reload for directory routes like `localhost:9966/mydir`. + +# 6.1.0 + +Search for `index.html` across all static `--dir` folders, finding the first one. + +# 6.0.0 + +##### Major Changes + +- `garnish` is now included by default in CLI and API + - you can use `--ndjson` and `ndjson: true` to have raw output (for custom pretty-printers) + +##### Minor Changes + +- added `--title` option for the default HTML title +- added `--css` option for a default style sheet + +# 5.0.0 + +##### Major Changes + +- you can just type `budo . | garnish` for the entry point (or `index.js`) +- added `--onupdate` for things like linting, see [the docs](docs/command-line-usage.md#--onupdate) +- if no `--host` is specified, resolves to internal IP + - you can still hit `localhost:9966` and it will work +- the ` +``` + +You can specify a different end point for the server with a colon. This is useful for relative and absolute paths, for example: + +```sh +budo /proj/foo/index.js:static/bundle.js +``` + +Now, you can use the following as your HTML: + +```html + +``` + +Also see the [`--serve` option](#multiple entries). + +## local installation + +If you are using these in your modules for demos/etc, you should save them locally so that others can get the same versions when they `git clone` and `npm install` your repo. + +```sh +npm install budo --save-dev +``` + +For local tools, we need to use [npm-scripts](https://docs.npmjs.com/misc/scripts). Open up your package.json and update `"scripts"` so it looks like this: + +```sh + "scripts": { + "start": "budo index.js" + }, +``` + +Now running the following will start the development server: + +```sh +npm run start +``` + +## live reload + +budō also includes support for [LiveReload](livereload.com). The `--live` argument injects a script tag into your HTML file and listens for a live reload server. + +```sh +budo index.js --live +``` + +Now when you save the `index.js` file, it will trigger a LiveReload event on your `localhost:9966` tab after watchify has finished bundling. It also listens to HTML and CSS reload, and injects stylesheets without a page refresh. + +From the command line, you can specify a filename glob to only trigger LiveReload in those cases. For example, to only allow CSS and HTML changes to trigger a LiveReload: + +```sh +budo index.js --live=*.{html,css} +``` + +*Note:* Your `index.html` must have a `` tag for the LiveReload script to get injected! + +## multiple entries + +Budo also supports multiple entry points; they will all get concatenated into a single bundle. If you aren't using a colon separator (`:`), the entry point will default to the first path. Or, you can explicitly set the path with the `--serve` option, as below: + +```sh +budo test/one.js test/two.js --serve static/bundle.js +``` + +Note the path here is relative to your `--dir` folder from where the `index.html` is being served. + +## browserify arguments + +Everything after the `--` argument will not be parsed/manipulated, and will be passed directly to browserify. + +```sh +budo main.js --live -- -t babelify -t glslify +``` + +You can also add a [`"browserify"` field](https://github.com/substack/browserify-handbook#browserifytransform-field) to your `package.json` file, and budo will use that config. This is not typically recommended for modules, but it can be useful when building applications. + +## launch + +To launch the browser once the server connects, you can use the `--open` or `-o` flag: + +```sh +budo index.js --open +``` + +Also see [opnr](https://github.com/mattdesl/opnr), which allows for a similar functionality without forcing it as a command-line flag. + +## `--onupdate` + +In the CLI, you can run shell commands when the bundle updates using the `--onupdate` option. For example, to lint with [standard](https://github.com/feross/standard) and provide an alert with [notify-error](https://github.com/mattdesl/notify-error): + +```sh +budo index.js --onupdate "standard | notify-error" +``` + +Now, when you save the bundle, `standard` will run on your directory. If lint errors are found, they will print to the console and show an alert notification: + + + +The flag is only available in the command-line. + +## internal IP + +By default, budo's server will listen on your internal IP. This address is the first message logged to terminal. + +This makes it easy to test during development across devices. + +You can specify another address with the `--host` flag. + +## pushstate + +You can set a `--pushstate` flag to make the server capable for HTML5 pushState Single-Page Applications. + +Now, any 404 requests (such as `/foo/bar/blah`) will get routed to the home `index.html`. + +It is suggested you add a `` in your `index.html` for this to work with nested paths: + +```html + + + + + +``` + +## hot module replacement + +The following can integrate easily with budo: + +- Generic HMR: [browserify-hmr](https://github.com/AgentME/browserify-hmr) +- React: [livereactload](https://github.com/milankinen/livereactload) +- Vue: [vueify](https://github.com/vuejs/vueify) (to be used with `browserify-hmr`) + +You can usually follow the steps in those tools, except instead of using `watchify`, we will use `budo` and pass our browserify options after a full stop `--`. + +Example with [livereactload](https://github.com/milankinen/livereactload): + +```sh +budo index.js:bundle.js -- -t babelify -p livereactload +``` + +Make sure you don't pass a `--live` flag to budo, otherwise it will trigger hard reloads on file save. \ No newline at end of file diff --git a/docs/comparisons.md b/docs/comparisons.md deleted file mode 100644 index 31657ca..0000000 --- a/docs/comparisons.md +++ /dev/null @@ -1,41 +0,0 @@ -# comparisons - -## budō - -[budō](https://github.com/mattdesl/budo) lies somewhere between the rich feature set of [Beefy](#beefy) and the small focus of [wzrd](#wzrd). It spawns a watchify process, produces ndjson logs, and includes some more experimental features for live-reloading, [script injection](https://github.com/mattdesl/budo-chrome), and rapid prototyping. - -## beefy - -[Beefy](https://github.com/chrisdickinson/beefy) is a feature-rich dev tool for browserify, and much of the inspiration for this project. However, it currently has some shortcomings[[1]](https://github.com/chrisdickinson/beefy/issues/49)[[2]](https://github.com/chrisdickinson/beefy/issues/63) and often feels clunky to deliver as a local dependency. It also takes a different approach to bundling, by using watchify's programmatic API rather than [execspawn](https://www.npmjs.com/package/npm-execspawn). - -```sh -#example ... -beefy index.js --open -``` - -## wzrd - -[wzrd](https://github.com/maxogden/wzrd) is a tiny spin-off of beefy that is ideal for [local dependencies](https://github.com/stackgl/learning-webgl-03/blob/db8f36a534b2a184924f8b890014ff3dd9a5b391/package.json#L6-L9). It introduces some novel ideas like entry mapping and ndjson output with tools like [garnish](https://github.com/mattdesl/garnish). - -However, incremental bundling is likely outside of its scope. - -```sh -#example ... -wzrd index.js:bundle.js | garnish -``` - -## wtch - -[wtch](https://github.com/mattdesl/wtch) is a small live-reload utility that budō builds on. It watches for JS/CSS/HTML changes and triggers a live-reload event. It can be used to augment wzrd with some watching capabilities. - -```sh -#example ... -wzrd index.js:bundle.js | wtch | garnish - -#with watchify ... -watchify index.js -o bundle.js | wtch bundle.js | garnish -``` - -## garnish - -[garnish](https://github.com/mattdesl/garnish) simply prettifies bole and ndjson log output from tools that decide to use it. This includes wzrd, wtch, and budō. \ No newline at end of file diff --git a/docs/tests-and-examples.md b/docs/tests-and-examples.md index 22cb209..06b36e0 100644 --- a/docs/tests-and-examples.md +++ b/docs/tests-and-examples.md @@ -18,7 +18,9 @@ Now you can run the following to see the unit tests: npm test ``` -## running the example +## running the examples + +See the [package.json](../package.json) `"scripts"` field for how these work. #### basic @@ -38,4 +40,11 @@ To run the example with live reloading: npm run live ``` -Again, open `localhost:9966` and try making changes to `example/app.js`, `example/index.html` or `example/theme.css`. The CSS should be injected without a page refresh, and HTML/JS content will trigger a page reload. \ No newline at end of file +Again, open `localhost:9966` and try making changes to `example/app.js`, `example/index.html` or `example/theme.css`. The CSS should be injected without a page refresh, and HTML/JS content will trigger a page reload. + +## other examples + +#### LESS on the fly + +See [budo-less](https://github.com/mattdesl/budo-less) for an example of how you can integrate LESS into budo, giving you the same rapid development cycle and CSS injection without any need to write files to disk during development. + diff --git a/example/app.js b/example/app.js index ee518e8..652aee7 100644 --- a/example/app.js +++ b/example/app.js @@ -1,30 +1,31 @@ -var dpr = window.devicePixelRatio||1 -var ctx = require('2d-context')() -var fit = require('canvas-fit')(ctx.canvas, window, dpr) +/*globals Image*/ +import createLoop from 'canvas-loop' +import createContext from '2d-context' -//setup canvas DOM state -window.addEventListener('resize', fit, false) -require('domready')(function() { - fit() - document.body.appendChild(ctx.canvas) +const context = createContext() +const canvas = context.canvas + +const app = createLoop(canvas, { + scale: window.devicePixelRatio }) +document.body.appendChild(canvas) -var img = new Image() +const img = new Image() +img.onload = () => app.start() img.src = 'baboon.png' -var time = 0 -require('raf-loop')(function(dt) { - var width = window.innerWidth, - height = window.innerHeight - ctx.clearRect(0, 0, width, height) +let time = 0 + +app.on('tick', (dt) => { + const [ width, height ] = app.shape + context.clearRect(0, 0, width, height) - time += dt/1000 + time += dt / 1000 - ctx.save() - ctx.scale(dpr, dpr) - ctx.fillRect(Math.sin(time)*50 + 200, 35, 150, 150) - ctx.fillText("from browserify!", 40, 40) - if (img.width > 0 || img.height > 0) - ctx.drawImage(img, 50, 50) - ctx.restore() -}).start() \ No newline at end of file + context.save() + context.scale(app.scale, app.scale) + context.fillRect(Math.sin(time) * 50 + 300, 50, 20, 40) + context.fillText('from browserify!', 40, 40) + context.drawImage(img, 50, 50) + context.restore() +}) diff --git a/example/demo/index.html b/example/demo/index.html new file mode 100644 index 0000000..8daa6f6 --- /dev/null +++ b/example/demo/index.html @@ -0,0 +1,10 @@ + + + + + + budo + + + + \ No newline at end of file diff --git a/example/index.html b/example/index.html index ac946c8..7136b5c 100644 --- a/example/index.html +++ b/example/index.html @@ -1,11 +1,12 @@ - - + + budo + \ No newline at end of file diff --git a/example/main.css b/example/main.css new file mode 100644 index 0000000..693b449 --- /dev/null +++ b/example/main.css @@ -0,0 +1,3 @@ +body { + background: #e1e1e1; +} \ No newline at end of file diff --git a/example/theme.css b/example/theme.css deleted file mode 100644 index c0f1b2d..0000000 --- a/example/theme.css +++ /dev/null @@ -1,3 +0,0 @@ -body { - background: #eee; -} \ No newline at end of file diff --git a/index.js b/index.js index f845e41..5004990 100644 --- a/index.js +++ b/index.js @@ -1,52 +1,75 @@ -var log = require('bole')('budo') -var minimist = require('minimist') -var portfinder = require('portfinder') -var xtend = require('xtend') -var assign = require('xtend/mutable') +var parseArgs = require('./lib/parse-args') +var budo = require('./lib/budo') +var color = require('term-color') +var stdout = require('stdout-stream') +var exec = require('child_process').exec -var getOutput = require('./lib/get-output') +module.exports = budo +module.exports.cli = budoCLI -var budo = require('./lib/budo') -var noop = function(){} - -module.exports = function(args, cb) { - cb = cb||noop - - var argv = minimist(args) - if (argv._.length === 0) { - console.error("No entry scripts specified!") - process.exit(1) - } - - argv.port = argv.port || 9966 - argv.dir = argv.dir || process.cwd() - getOutput(argv, function(err, output) { - if (err) { - console.error("Error: Could not create temp bundle.js directory") - process.exit(1) - } - //determine next port - portfinder.basePort = argv.port - portfinder.getPort(function(err, port) { - if (err) { - console.error("Error: Could not get available port") - process.exit(1) - } - - //run watchify server - var emitter = budo(args, xtend(argv, { - port: port, - output: output - })).on('error', function(err) { - console.error("Error running budo on", port, err) - process.exit(1) - }).on('exit', function() { - log.info('closing') - }) - - emitter.on('connect', function(result) { - cb(result) - }) - }) - }) -} \ No newline at end of file +function budoCLI (args, opts) { + var argv = parseArgs(args, opts) + + // if no stream is specified, default to stdout + if (argv.stream !== false) { + argv.stream = stdout + } + + var entries = argv._ + delete argv._ + + argv.browserifyArgs = argv['--'] + delete argv['--'] + + if (argv.version) { + console.log('budo v' + require('./package.json').version) + console.log('browserify v' + require('browserify/package.json').version) + console.log('watchify v' + require('watchify-middleware').getWatchifyVersion()) + return null + } + + if (argv.help) { + var help = require('path').join(__dirname, 'bin', 'help.txt') + require('fs').createReadStream(help) + .pipe(process.stdout) + return null + } + + if (argv.outfile) { + console.error(color.yellow('WARNING'), '--outfile has been removed in budo@3.0') + } + + if (typeof argv.port === 'string') { + argv.port = parseInt(argv.port, 10) + } + if (typeof argv.livePort === 'string') { + argv.livePort = parseInt(argv.livePort, 10) + } + + // opts.live can be a glob or a boolean + if (typeof argv.live === 'string' && /(true|false)/.test(argv.live)) { + argv.live = argv.live === 'true' + } + + // CLI only option for executing a child process + var instance = budo(entries, argv).on('error', exit) + var onUpdates = [].concat(argv.onupdate).filter(Boolean) + onUpdates.forEach(function (cmd) { + instance.on('update', execFunc(cmd)) + }) + + return instance +} + +function execFunc (cmd) { + return function run () { + var p = exec(cmd) + p.stderr.pipe(process.stderr) + p.stdout.pipe(process.stdout) + } +} + +function exit (err) { + console.log(color.red('ERROR'), err.message) + process.exit(1) +} diff --git a/lib/budo.js b/lib/budo.js index 1ad9f86..2db48bc 100644 --- a/lib/budo.js +++ b/lib/budo.js @@ -1,68 +1,306 @@ -var path = require('path') -var Emitter = require('events/') -var watchify = require('./watchify') -var minimist = require('minimist') +var bole = require('bole') var xtend = require('xtend') -var assign = require('xtend/mutable') -var http = require('./server').http -var log = require('bole')('budo') - -module.exports = function(watchifyArgs, opt) { - var output = opt.output - var port = opt.port || 9966 - var host = opt.host - - var emitter = new Emitter() - - //patch watchify args with new outfile - setOutfile(watchifyArgs, output.from) - //spin up watchify instance - var watchProc = watchify(watchifyArgs) - - var serverOpt = xtend(opt, { output: output }) - var server = http(serverOpt) - .on('error', function(err) { - emitter.emit('error', err) - }) - .listen(port, host, function(err) { - if (err) { - emitter.emit('error', new Error("Could not connect to server:", err)) - return - } - var uri = "http://"+(host||'localhost')+":"+port+"/" - log.info("Server running at", uri) - - //bug with chokidar@1.0.0-rc3 - //anything in OSX tmp dirs need a wildcard - //to work with fsevents - var glob = output.tmp - ? path.join(output.dir, '**.js') - : output.from - - //add the uri / output to budo instance - assign(emitter, { - uri: uri, - output: xtend(output, { glob: glob }) - }) - emitter.emit('connect', emitter) +var once = require('once') +var path = require('path') +var EventEmitter = require('events').EventEmitter +var isMatch = require('micromatch') +var openUrl = require('opn') +var internalIp = require('internal-ip') +var garnish = require('garnish') + +var defaults = require('./parse-args').defaults +var getPorts = require('./get-ports') +var createServer = require('./server') +var createBundler = require('./bundler') +var createFileWatch = require('./file-watch') +var createTinylr = require('./tinylr') +var mapEntry = require('./map-entry') + +var noop = function () {} + +module.exports = createBudo +function createBudo (entries, opts) { + var log = bole('budo') + + // if no entries are specified, just options + if (entries && !Array.isArray(entries) && typeof entries === 'object') { + opts = entries + entries = [] + } + + // do not mutate user options + opts = xtend({}, defaults, { stream: false }, opts) + entries = entries || [] + + // perhaps later this will be configurable + opts.cwd = process.cwd() + + // log to output stream + if (opts.stream) { + // by default, pretty-print to the stream with info logging + if (!opts.ndjson) { + var pretty = garnish({ + level: opts.verbose ? 'debug' : 'info', + name: 'budo' + }) + pretty.pipe(opts.stream) + opts.stream = pretty + } + + bole.output({ + stream: opts.stream, + level: 'debug' + }) + } + + // optionally allow as arrays + entries = [].concat(entries).filter(Boolean) + + var entryObjects = entries.map(mapEntry) + var entryFiles = entryObjects.map(function (entry) { + return entry.from + }) + + if (opts.serve && typeof opts.serve !== 'string') { + throw new TypeError('opts.serve must be a string or undefined') + } else if (!opts.serve && entries.length > 0) { + opts.serve = entryObjects[0].url + } + + // default to cwd + if (!opts.dir || opts.dir.length === 0) { + opts.dir = opts.cwd + } + + var emitter = new EventEmitter() + var bundler, middleware + + if (entries.length > 0) { + bundler = createBundler(entryFiles, opts) + middleware = bundler.middleware + + bundler.on('log', function (ev) { + if (ev.type === 'bundle') { + var time = ev.elapsed + ev.elapsed = time + ev.name = 'browserify' + ev.type = undefined + ev.colors = { + elapsed: time > 1000 ? 'yellow' : 'dim', + message: 'dim ' + } + log.info(ev) + } + }) + + // uncaught syntax errors should not stop the server + // this only happens when errorHandler: false + bundler.on('error', function (err) { + console.error('Error:', err.message ? err.message : err) + }) + bundler.on('bundle-error', emitter.emit.bind(emitter, 'bundle-error')) + bundler.on('update', emitter.emit.bind(emitter, 'update')) + bundler.on('pending', emitter.emit.bind(emitter, 'pending')) + + emitter.on('update', function (contents, deps) { + if (deps.length > 1) { + log.debug({ + name: 'browserify', + message: deps.length + ' files changed' }) - - emitter.close = function() { - watchProc.kill() - server.close() - emitter.emit('exit') + } + }) + } + + var defaultWatchGlob = opts.watchGlob || '**/*.{html,css}' + var server = createServer(middleware, opts) + var closed = false + var started = false + var fileWatcher = null + var tinylr = null + var deferredWatch = noop + var deferredLive = noop + + // keep track of the original host + // (can be undefined) + var hostAddress = opts.host + + // public API + emitter.close = once(close) + emitter.reload = reload + emitter.live = live + emitter.watch = watch + + // setup defaults for live reload / watchify + if (opts.live) { + emitter + .watch() + .live() + .on('watch', function (ev, file) { + if (ev !== 'change' && ev !== 'add') { + return + } + defaultFileEvent(file) + }) + .on('pending', function () { + defaultFileEvent(opts.serve) + }) + } + + // start portfinding + connect + getPorts(opts, handlePorts) + return emitter + + function defaultFileEvent (file) { + var filename = path.basename(file) + if ((Array.isArray(opts.live) || typeof opts.live === 'string') && + isMatch(filename, opts.live).length === 0) { + return + } + emitter.reload(file) + } + + function reload (file) { + process.nextTick(emitter.emit.bind(emitter, 'reload', file)) + if (tinylr) { + tinylr.reload(file) + } + } + + // enable file watch capabilities + function watch (glob, watchOpt) { + if (!started) { + deferredWatch = emitter.watch.bind(null, glob, watchOpt) + } else { + // destroy previous + if (fileWatcher) fileWatcher.close() + glob = glob && glob.length > 0 ? glob : defaultWatchGlob + glob = Array.isArray(glob) ? glob : [ glob ] + watchOpt = xtend({ poll: opts.poll }, watchOpt) + + fileWatcher = createFileWatch(glob, watchOpt) + fileWatcher.on('watch', emitter.emit.bind(emitter, 'watch')) + } + return emitter + } + + // enables LiveReload capabilities + function live (liveOpts) { + if (!started) { + deferredLive = emitter.live.bind(null, liveOpts) + } else { + // destroy previous + if (tinylr) tinylr.close() + + // default port + liveOpts = xtend({ port: opts.livePort }, liveOpts) + + // default to budo host + var hostName = liveOpts.host ? getHostAddress(liveOpts.host) : opts.host + + // the LiveReload ', - '', - '' - ].join('')) - return out -} \ No newline at end of file diff --git a/lib/error-handler.js b/lib/error-handler.js new file mode 100644 index 0000000..c641745 --- /dev/null +++ b/lib/error-handler.js @@ -0,0 +1,45 @@ +var stripAnsi = require('strip-ansi') + +module.exports = errorHandler + +function bundleError (message) { + console.error(message) + if (typeof document === 'undefined') { + return + } else if (!document.body) { + document.addEventListener('DOMContentLoaded', print) + } else { + print() + } + function print () { + var pre = document.createElement('pre') + pre.textContent = message + var style = { + position: 'fixed', + width: '100%', + zIndex: '100000', + height: '100%', + top: '0', + left: '0', + padding: '20px', + 'box-sizing': 'border-box', + 'word-wrap': 'break-word', + 'font-size': '16px', + 'font-family': 'monospace', + margin: '0', + background: '#fff', // or ffefef ? + color: '#ff0000' + } + + Object.keys(style).forEach(function (k) { + pre.style[k] = style[k] + }) + document.body.appendChild(pre) + } +} + +function errorHandler (err) { + console.error('%s', err) + var msgStr = stripAnsi(err.message) + return ';(' + bundleError + ')(' + JSON.stringify(msgStr) + ');' +} diff --git a/lib/file-watch.js b/lib/file-watch.js new file mode 100644 index 0000000..ee5a8f2 --- /dev/null +++ b/lib/file-watch.js @@ -0,0 +1,45 @@ +// a thin wrapper around chokidar file watching HTML / CSS +var watch = require('chokidar').watch +var xtend = require('xtend') +var Emitter = require('events/') + +var ignores = [ + 'node_modules/**', 'bower_components/**', + '.git', '.hg', '.svn', '.DS_Store', + '*.swp', 'thumbs.db', 'desktop.ini' +] + +module.exports = function (glob, opt) { + opt = xtend({ + usePolling: opt && opt.poll, + ignored: ignores, + ignoreInitial: true + }, opt) + + var emitter = new Emitter() + var closed = false + var ready = false + + var watcher = watch(glob, opt) + watcher.on('add', onWatch.bind(null, 'add')) + watcher.on('change', onWatch.bind(null, 'change')) + + // chokidar@1.0.0-r6 only allows close after ready event + watcher.once('ready', function () { + ready = true + if (closed) watcher.close() + }) + + function onWatch (event, path) { + emitter.emit('watch', event, path) + } + + emitter.close = function () { + if (closed) return + if (ready) watcher.close() + closed = true + } + return emitter +} + +module.exports.ignores = ignores diff --git a/lib/get-output.js b/lib/get-output.js deleted file mode 100644 index c7da330..0000000 --- a/lib/get-output.js +++ /dev/null @@ -1,30 +0,0 @@ -var path = require('path') -var tmpdir = require('./tmpdir') - -//get an output directory, from user or tmp dir -module.exports = function getOutput(argv, cb) { - var outfile = argv.o || argv.outfile - if (!outfile) { - var to = 'bundle.js' - tmpdir(function(err, filedir) { - var output - if (!err) { - var file = path.join(filedir, to) - output = { - tmp: true, - from: file, - to: to, - dir: filedir - } - } - cb(err, output) - }) - } else { - var from = path.join(argv.dir, outfile) - cb(null, { - from: from, - to: outfile, - dir: argv.dir - }) - } -} \ No newline at end of file diff --git a/lib/get-ports.js b/lib/get-ports.js new file mode 100644 index 0000000..866d2b8 --- /dev/null +++ b/lib/get-ports.js @@ -0,0 +1,25 @@ +var xtend = require('xtend') +var getPorts = require('get-ports') + +module.exports = getServerPorts +function getServerPorts (opt, cb) { + opt = xtend({ port: 9966, livePort: 35729 }, opt) + + // try to use exact port specified or the defaults + if (!opt.portfind) { + return process.nextTick(function () { + cb(null, { + port: opt.port, + livePort: opt.livePort + }) + }) + } + + // find our multiple available ports + getPorts([ opt.port, opt.livePort ], function (err, ports) { + if (err) return cb(err) + cb(null, { + port: ports[0], livePort: ports[1] + }) + }) +} diff --git a/lib/map-entry.js b/lib/map-entry.js new file mode 100644 index 0000000..da4e5b9 --- /dev/null +++ b/lib/map-entry.js @@ -0,0 +1,43 @@ +var path = require('path') +var url = require('url') +var resolve = require('resolve') + +module.exports = mapEntry +function mapEntry (file) { + if (file === '.') { + file = entry() + } + + var parts + // absolute path with letter drive, eg C:/ + if (/^[A-Z]\:[\/\\]+/.test(file)) { + parts = file.split(/\:(?:(?=[^\/\\]))/) + } else { + parts = file.split(':') + } + + var pathFrom, pathUrl + + if (parts.length > 1 && parts[1].length > 0) { + pathFrom = parts[0] + pathUrl = parts[1] + + if (pathFrom === '.') { + pathFrom = entry() + } + } else { + pathFrom = file + pathUrl = url.parse(path.basename(pathFrom)).pathname + } + + return { + url: pathUrl, + from: pathFrom + } +} + +function entry () { + var cwd = process.cwd() + var file = resolve.sync('.', { basedir: cwd }) + return file || 'index.js' +} diff --git a/lib/middleware.js b/lib/middleware.js new file mode 100644 index 0000000..531ef66 --- /dev/null +++ b/lib/middleware.js @@ -0,0 +1,144 @@ +// TODO: Expose this like webpack-dev-server middleware +var stacked = require('stacked') +var serveStatic = require('serve-static') +var defaultIndex = require('simple-html-index') +var logger = require('./simple-http-logger') +var urlLib = require('url') +var pushState = require('connect-pushstate') +var liveReload = require('inject-lr-script') +var urlTrim = require('url-trim') +var httpProxy = require('http-proxy') + +module.exports = budoMiddleware +function budoMiddleware (entryMiddleware, opts) { + opts = opts || {} + var staticPaths = [].concat(opts.dir).filter(Boolean) + if (staticPaths.length === 0) { + staticPaths = [ process.cwd() ] + } + + var entrySrc = opts.serve + var live = opts.live + var cors = opts.cors + var handler = stacked() + var middlewares = [].concat(opts.middleware).filter(Boolean) + + // Everything is logged except favicon.ico + var logHandler = logger({ + ignore: [ '/favicon.ico' ] + }) + handler.use(function (req, res, next) { + if (cors) { res.setHeader('Access-Control-Allow-Origin', '*') } + logHandler(req, res, next) + }) + + // User middleware(s) can override others + middlewares.forEach(function (middleware) { + if (typeof middleware !== 'function') { + throw new Error('middleware options must be functions') + } + handler.use(function (req, res, next) { + logHandler.type = 'middleware' + middleware(req, res, next) + }) + }) + + // Re-route for pushState support + if (opts.pushstate) handler.use(pushState()) + + // Inject liveReload snippet on response + var liveInjector = liveReload() + handler.use(function (req, res, next) { + if (!live || live.plugin) return next() + if (live.host) liveInjector.host = live.host + if (live.port) liveInjector.port = live.port + liveInjector(req, res, next) + }) + + // Entry (watchify) middleware + if (entryMiddleware) { + var entryRoute = '/' + urlLib.parse(entrySrc).pathname + handler.use(function (req, res, next) { + if (urlTrim(req.url) === urlTrim(entryRoute)) { + entryMiddleware(req, res, next) + } else { + next() + } + }) + } + + // Ignore favicon clutter + handler.mount('/favicon.ico', favicon) + + // If the user wishes to *always* serve + // a generated index instead of a static one. + if (opts.forceDefaultIndex) { + handler.use(indexHandler) + } + + // Static assets (html/images/etc) + staticPaths.forEach(function (rootFile) { + var staticHandler = serveStatic(rootFile) + handler.use(function (req, res, next) { + logHandler.type = 'static' + staticHandler(req, res, next) + }) + }) + + // Generates a default index.html + // when none is found locally. + handler.use(indexHandler) + + // Attach proxy + if (opts.proxy) { + console.log('Attaching proxy at: ' + opts.proxy) + var split = opts.proxy.trim().split('@') + var proxy = httpProxy.createProxyServer({}) + handler.mount(split[0], function (req, res, next) { + console.log('-- proxying -- : ' + split[1] + req.url) + proxy.web(req, res, { target: split[1] }, function (err) { + if (err) next() + }) + }) + } + + // Handle errors + handler.use(function (req, res) { + res.statusCode = 404 + res.end('404 not found: ' + req.url) + }) + + // Allow live options to be changed at runtime + handler.setLiveOptions = setLiveOptions + return handler + + function setLiveOptions (opts) { + live = opts + } + + function favicon (req, res) { + var maxAge = 345600 // 4 days + res.setHeader('Cache-Control', 'public, max-age=' + Math.floor(maxAge / 1000)) + res.setHeader('Content-Type', 'image/x-icon') + res.statusCode = 200 + res.end() + } + + function indexHandler (req, res, next) { + if (urlLib.parse(req.url).pathname === '/' || /\/index.html?/i.test(req.url)) { + // If we reach this, our response will be generated + // (not static from local file system) + logHandler.type = 'generated' + res.setHeader('content-type', 'text/html') + + var stream = opts.defaultIndex || defaultIndex + stream({ + entry: entrySrc, + title: opts.title, + css: opts.css + }).pipe(res) + } else { + next() + } + } +} diff --git a/lib/parse-args.js b/lib/parse-args.js new file mode 100644 index 0000000..ab6249c --- /dev/null +++ b/lib/parse-args.js @@ -0,0 +1,60 @@ +var minimist = require('minimist') +var xtend = require('xtend') + +module.exports = parseArgs +function parseArgs (args, opt) { + var argv = minimist(args, { + boolean: [ + 'stream', + 'debug', + 'errorHandler', + 'open', + 'portfind', + 'ndjson', + 'verbose', + 'cors', + 'ssl' + ], + string: [ + 'host', + 'port', + 'dir', + 'onupdate', + 'serve', + 'title', + 'watchGlob', + 'cert', + 'key', + 'proxy' + ], + default: module.exports.defaults, + alias: { + port: 'p', + ssl: 'S', + serve: 's', + cert: 'C', + key: 'K', + verbose: 'v', + help: 'h', + host: 'H', + dir: 'd', + live: 'l', + open: 'o', + watchGlob: [ 'wg', 'watch-glob' ], + errorHandler: 'error-handler', + 'live-port': ['L', 'livePort'], + pushstate: 'P' + }, + '--': true + }) + return xtend(argv, opt) +} + +module.exports.defaults = { + title: 'budo', + port: 9966, + debug: true, + stream: true, + errorHandler: true, + portfind: true +} diff --git a/lib/server.js b/lib/server.js index de018cc..94bebbf 100644 --- a/lib/server.js +++ b/lib/server.js @@ -1,63 +1,18 @@ -var ecstatic = require('ecstatic') -var Router = require('routes-router') var http = require('http') +var https = require('https') +var createMiddleware = require('./middleware') var fs = require('fs') -var path = require('path') -var log = require('bole')('budo') -var fs = require('fs') -var html = require('./default-index') -var inject = require('inject-lr-script') -module.exports.http = function(opts) { - var handler = module.exports.static(opts) - return http.createServer(handler) +module.exports = function createServer (entryMiddleware, opts) { + var httpsOpts = opts.ssl ? { + cert: fs.readFileSync(opts.cert || 'cert.pem'), + key: fs.readFileSync(opts.key || 'key.pem') + } : undefined + + var handler = createMiddleware(entryMiddleware, opts) + var server = httpsOpts + ? https.createServer(httpsOpts, handler) + : http.createServer(handler) + server.setLiveOptions = handler.setLiveOptions + return server } - -module.exports.static = function(opts) { - var basedir = opts.dir || process.cwd() - var staticHandler = ecstatic(basedir) - var router = Router() - - var live = opts.live - - var out = opts.output - var entryHandler = staticHandler - if (out.tmp) - entryHandler = ecstatic({ root: out.dir }) - - router.addRoute('/' + out.to, function(req, res, params) { - log.info({ url: req.url, type: 'static' }) - entryHandler(req, res) - }) - - router.addRoute('/', function(req, res, params) { - fs.exists(path.join(basedir, 'index.html'), function(exists) { - //inject LiveReload into HTML content if needed - if (live) - res = inject(res, { - host: opts.host, - port: opts['live-port'] - }) - - var type = exists ? 'static' : 'generated' - log.info({ url: req.url, type: type }) - - if (exists) - staticHandler(req, res) - else - generateIndex(out.to, req, res) - }) - }) - - router.addRoute('*', function(req, res, params) { - log.info({ url: req.url, type: 'static' }) - staticHandler(req, res) - }) - - return router - - function generateIndex(outfile, req, res) { - res.setHeader('content-type', 'text/html') - html({ outfile: outfile }).pipe(res) - } -} \ No newline at end of file diff --git a/lib/simple-http-logger.js b/lib/simple-http-logger.js new file mode 100644 index 0000000..d4798e7 --- /dev/null +++ b/lib/simple-http-logger.js @@ -0,0 +1,49 @@ +var log = require('bole')('budo') + +module.exports = simpleHttpLoggerMiddleware +function simpleHttpLoggerMiddleware (opts) { + opts = opts || {} + var ignores = [].concat(opts.ignore).filter(Boolean) + + var httpLogger = function simpleHttpLogger (req, res, next) { + if (ignores.indexOf(req.url) >= 0) return next() + if (!req.url) return next() + + var byteLength = 0 + var now = Date.now() + var onFinished = function () { + var elapsed = Date.now() - now + log.info({ + elapsed: elapsed, + contentLength: byteLength, + method: (req.method || 'GET').toUpperCase(), + url: req.url, + statusCode: res.statusCode, + type: httpLogger.type === 'static' ? undefined : httpLogger.type, + colors: { + elapsed: elapsed > 1000 ? 'yellow' : 'dim' + } + }) + } + + var isAlreadyLogging = res._simpleHttpLogger + res._simpleHttpLogger = true + + if (!isAlreadyLogging) { + var write = res.write + res.once('finish', onFinished) + + // catch content-length of payload + res.write = function (payload) { + if (payload) byteLength += payload.length + res.write = write + res.write.apply(res, arguments) + } + } + + next() + } + + httpLogger.type = 'static' + return httpLogger +} diff --git a/lib/tinylr.js b/lib/tinylr.js new file mode 100644 index 0000000..b3b099e --- /dev/null +++ b/lib/tinylr.js @@ -0,0 +1,54 @@ +// a thin wrapper around tiny-lr module +var log = require('bole')('budo') +var xtend = require('xtend') +var tinylr = require('tiny-lr') + +module.exports = function (opt) { + opt = xtend(opt) + if (typeof opt.port !== 'number') { + opt.port = 35729 + } + + var server = tinylr() + var closed = false + var ready = false + + server.listen(opt.port, opt.host || undefined, function () { + ready = true + if (closed) return server.close() + log.info({ message: 'LiveReload running on ' + opt.port }) + }) + + var serverImpl = server.server + serverImpl.removeAllListeners('error') + serverImpl.on('error', function (err) { + if (err.code === 'EADDRINUSE') { + process.stderr.write('ERROR: livereload not started, port ' + opt.port + ' is in use\n') + } else { + process.stderr.write((err.stack ? err.stack : err) + '\n') + } + close() + }) + + function close () { + if (closed) return + if (ready) server.close() + closed = true + } + + return { + close: close, + + reload: function reload (path) { + try { + server.changed({ + body: { + files: path ? [ path ] : '*' + } + }) + } catch (e) { + throw e + } + } + } +} diff --git a/lib/tmpdir.js b/lib/tmpdir.js deleted file mode 100644 index ca2aeb8..0000000 --- a/lib/tmpdir.js +++ /dev/null @@ -1,39 +0,0 @@ -var rimraf = require('rimraf') -var log = require('bole')('budo') -var tmp = require('tmp') - -tmp.setGracefulCleanup() - -module.exports = function(cb) { - tmp.dir({ - mode: '0755', - prefix: 'budo-' - }, - function(err, filepath) { - if (!err) { - process.on('exit', remove) - process.on('SIGINT', exit) - process.on('uncaughtException', exit) - log.debug('temp directory created at', filepath) - } - - cb(err, filepath) - - function remove(err) { - try { - rimraf.sync(filepath) - } catch(e) { - rimraf.sync(filepath) - } - if (err) - console.error(err.stack) - } - - function exit(err) { - if (err) - console.error(err.stack) - process.exit() - } - }) - -} \ No newline at end of file diff --git a/lib/watchify.js b/lib/watchify.js deleted file mode 100644 index d29598b..0000000 --- a/lib/watchify.js +++ /dev/null @@ -1,25 +0,0 @@ -var spawn = require('npm-execspawn') - -//Runs watchify with given args, returns process -module.exports = function(watchifyArgs) { - var cmd = ['watchify'] - .concat(watchifyArgs||[]) - .join(' ') - - var proc = spawn(cmd) - proc.stderr.on('data', function(err) { - process.stderr.write(err.toString()) - }) - - var hasClosed = false - process.on('close', handleClose) - process.on('exit', handleClose) - - return proc - - function handleClose() { - if (hasClosed) return - hasClosed = true - proc.kill() - } -} \ No newline at end of file diff --git a/package.json b/package.json index b56e1fc..8c74d0b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "budo", - "version": "0.1.12", + "version": "8.3.0", "description": "a browserify server for rapid prototyping", "main": "index.js", "bin": { @@ -14,46 +14,51 @@ }, "dependencies": { "bole": "^2.0.0", - "ecstatic": "^0.5.8", + "browserify": "^13.0.1", + "chokidar": "^1.0.1", + "connect-pushstate": "^1.0.0", "events": "^1.0.2", - "inject-lr-script": "^1.0.0", + "garnish": "^5.0.0", + "get-ports": "^1.0.2", + "http-proxy": "^1.13.3", + "inject-lr-script": "^2.0.0", + "internal-ip": "^1.0.1", + "micromatch": "^2.2.0", "minimist": "^1.1.0", - "npm-execspawn": "^1.0.6", - "portfinder": "^0.4.0", - "response-stream": "0.0.0", - "rimraf": "^2.2.8", - "routes-router": "^4.1.2", - "script-injector": "^0.1.7", - "through2": "^0.6.3", - "tmp": "0.0.24", - "wtch": "^3.1.0", + "once": "^1.3.2", + "opn": "^3.0.2", + "resolve": "^1.1.6", + "resp-modifier": "^6.0.0", + "serve-static": "^1.10.0", + "simple-html-index": "^1.1.0", + "stacked": "^1.1.1", + "stdout-stream": "^1.4.0", + "strip-ansi": "^3.0.0", + "term-color": "^1.0.1", + "tiny-lr": "^0.2.0", + "url-trim": "^1.0.0", + "watchify-middleware": "^1.6.0", "xtend": "^4.0.0" }, "devDependencies": { "2d-context": "^1.2.0", - "browserify": "^8.1.3", - "canvas-fit": "^1.2.0", - "concat-stream": "^1.4.7", - "domready": "^1.0.7", - "garnish": "^2.0.1", - "inject-lr-script": "^1.0.1", - "ndjson": "^1.3.0", - "npm-execspawn": "^1.0.6", - "raf-loop": "^1.0.1", + "babelify": "^6.3.0", + "brfs": "^1.4.0", + "canvas-loop": "^1.0.4", + "ndjson": "^1.4.1", "request": "^2.53.0", - "tap-finished": "0.0.1", - "tap-spec": "^2.2.1", - "tape": "^3.5.0", - "through2": "^0.6.3", - "tree-kill": "0.0.6", - "watchify": "^2.3.0", + "standard": "^5.2.1", + "tap-spec": "^4.1.0", + "tape": "^4.0.0", + "through2": "^2.0.0", + "tree-kill": "^1.0.0", "win-spawn": "^2.0.0" }, "scripts": { - "test": "node test/test-simple.js | tap-spec", - "start": "./bin/cmd.js example/app.js --dir example --verbose | garnish", - "live": "./bin/cmd.js example/app.js --dir example --live | garnish -v", - "live-plugin": "./bin/cmd.js example/app.js --dir example --live-plugin | garnish -v" + "test": "standard && tape test/test*.js | tap-spec", + "start": "./bin/cmd.js example/app.js:bundle.js -v --dir example -- -t babelify", + "pushstate": "./bin/cmd.js example/app.js:bundle.js -v --dir example --live --pushstate -- -t babelify", + "live": "./bin/cmd.js example/app.js:bundle.js --dir example --verbose --live -- -t babelify" }, "keywords": [ "browserify", diff --git a/test/app.js b/test/app.js deleted file mode 100644 index cfaeb47..0000000 --- a/test/app.js +++ /dev/null @@ -1,3 +0,0 @@ -process.nextTick(function() { - console.log(__dirname) -}) \ No newline at end of file diff --git a/test/fixtures/app-brfs.js b/test/fixtures/app-brfs.js new file mode 100644 index 0000000..c8fb15b --- /dev/null +++ b/test/fixtures/app-brfs.js @@ -0,0 +1,3 @@ +var fs = require('fs') +var text = fs.readFileSync(__dirname + '/text.txt', 'utf8') +console.log(text) diff --git a/test/fixtures/app.js b/test/fixtures/app.js new file mode 100644 index 0000000..ea1edc4 --- /dev/null +++ b/test/fixtures/app.js @@ -0,0 +1 @@ +console.log('from browserify') diff --git a/test/fixtures/first.js b/test/fixtures/first.js new file mode 100644 index 0000000..efe794c --- /dev/null +++ b/test/fixtures/first.js @@ -0,0 +1 @@ +global.start = 'foo' diff --git a/test/fixtures/one/one.txt b/test/fixtures/one/one.txt new file mode 100644 index 0000000..43dd47e --- /dev/null +++ b/test/fixtures/one/one.txt @@ -0,0 +1 @@ +one \ No newline at end of file diff --git a/test/fixtures/second.js b/test/fixtures/second.js new file mode 100644 index 0000000..ddbb3b1 --- /dev/null +++ b/test/fixtures/second.js @@ -0,0 +1,2 @@ +global.end = 'bar' +console.log(global.start + ' ' + global.end) diff --git a/test/fixtures/text.txt b/test/fixtures/text.txt new file mode 100644 index 0000000..f6ea049 --- /dev/null +++ b/test/fixtures/text.txt @@ -0,0 +1 @@ +foobar \ No newline at end of file diff --git a/test/fixtures/three/index.html b/test/fixtures/three/index.html new file mode 100644 index 0000000..1d144f6 --- /dev/null +++ b/test/fixtures/three/index.html @@ -0,0 +1 @@ +THREE \ No newline at end of file diff --git a/test/fixtures/two/index.html b/test/fixtures/two/index.html new file mode 100644 index 0000000..92be406 --- /dev/null +++ b/test/fixtures/two/index.html @@ -0,0 +1 @@ +TWO \ No newline at end of file diff --git a/test/fixtures/two/two.txt b/test/fixtures/two/two.txt new file mode 100644 index 0000000..64c5e58 --- /dev/null +++ b/test/fixtures/two/two.txt @@ -0,0 +1 @@ +two \ No newline at end of file diff --git a/test/fixtures/with space.js b/test/fixtures/with space.js new file mode 100644 index 0000000..2221b45 --- /dev/null +++ b/test/fixtures/with space.js @@ -0,0 +1 @@ +console.log('with space') diff --git a/test/test-api.js b/test/test-api.js new file mode 100644 index 0000000..10b7591 --- /dev/null +++ b/test/test-api.js @@ -0,0 +1,164 @@ +var test = require('tape') +var budo = require('../') +var through = require('through2') +var internalIp = require('internal-ip') + +test('uses internal IP when no host is given', function (t) { + t.plan(3) + t.timeoutAfter(10000) + var internal = internalIp() + + var app = budo('test/fixtures/app.js', { + dir: __dirname, + port: 8000 + }) + .on('error', function (err) { + t.fail(err) + }) + .on('connect', function (ev) { + t.equal(ev.uri, 'http://' + internal + ':8000/', 'uri matches') + t.equal(ev.host, internal, 'host defaults to internal ip') + app.close() + }) + .on('reload', function () { + t.fail('should not have received reload event') + }) + .on('watch', function () { + t.fail('should not have received watch event') + }) + .on('exit', function () { + t.ok(true, 'closing') + }) +}) + +test('gets connect info', function (t) { + t.plan(7) + t.timeoutAfter(10000) + + var app = budo('test/fixtures/app.js', { + dir: __dirname, + host: 'localhost', + port: 8000 + }) + .on('error', function (err) { + t.fail(err) + }) + .on('connect', function (ev) { + t.deepEqual(ev.entries, [ 'test/fixtures/app.js' ], 'entries matches') + t.equal(ev.serve, 'app.js', 'mapping matches') + t.equal(ev.uri, 'http://localhost:8000/', 'uri matches') + t.equal(ev.host, 'localhost', 'host is specified') + t.equal(ev.port, 8000, 'port matches') + t.equal(ev.dir, __dirname, 'dir matches') + app.close() + }) + .on('reload', function () { + t.fail('should not have received reload event') + }) + .on('watch', function () { + t.fail('should not have received watch event') + }) + .on('exit', function () { + t.ok(true, 'closing') + }) +}) + +test('entry mapping', function (t) { + t.plan(2) + t.timeoutAfter(10000) + + var app = budo(['test/fixtures/app:foo.js', 'test/fixtures/with space.js']) + .on('connect', function (ev) { + t.equal(ev.serve, 'foo.js', 'mapping matches') + t.deepEqual(ev.entries, ['test/fixtures/app', 'test/fixtures/with space.js'], 'from matches') + app.close() + }) +}) + +test('--serve allows explicit bundle renaming', function (t) { + t.plan(2) + t.timeoutAfter(5000) + + var app = budo(['test/fixtures/app', 'test/fixtures/with space.js'], { + serve: 'static/foo.js' + }) + .on('connect', function (ev) { + t.equal(ev.serve, 'static/foo.js', 'mapping matches') + t.deepEqual(ev.entries, ['test/fixtures/app', 'test/fixtures/with space.js'], 'from matches') + app.close() + }) +}) + +test('sets watch() and live() by default with live: true', function (t) { + t.plan(4) + t.timeoutAfter(3000) + + var app = budo('test/fixtures/app.js', { + dir: __dirname, + port: 8000, + live: true + }) + // the order is pending -> reload -> update + .once('pending', function () { + // bundle.js started changing + t.ok(true, 'got pending') + }) + .once('update', function () { + // bundle.js changed + t.ok(true, 'got update event') + app.close() + }) + .once('reload', function () { + // LiveReload triggered + t.ok(true, 'got reload event') + }) + .on('error', function (err) { + t.fail(err) + }) + .on('exit', function () { + t.ok(true, 'closing') + }) +}) + +test('should pipe JSON to specified stream', function (t) { + t.plan(3) + t.timeoutAfter(10000) + + var app = budo('test/fixtures/app.js', { + dir: __dirname, + ndjson: true, + stream: through(function (buf) { + t.ok(buf.length > 0, 'got some message') + t.doesNotThrow(function () { + return JSON.parse(buf.toString()) + }, 'produces valid JSON') + app.close() + }) + }) + .on('exit', function () { + t.ok(true, 'got exit event') + }) +}) + +test('allow setting live() manually', function (t) { + t.plan(3) + t.timeoutAfter(10000) + + var app = budo('test/fixtures/app.js', { + dir: __dirname, + port: 8000, + live: false + }) + .live() // start live server with watchify + .on('update', function () { + t.ok(true, 'got update') + app.reload() + }) + .on('reload', function () { + t.ok(true, 'got reload') + app.close() + }) + .on('exit', function () { + t.ok(true, 'got exit') + }) +}) diff --git a/test/test-args.js b/test/test-args.js new file mode 100644 index 0000000..9f54173 --- /dev/null +++ b/test/test-args.js @@ -0,0 +1,52 @@ +var test = require('tape') +var budo = require('../') +var browserify = require('browserify') +var path = require('path') +var brfs = require('brfs') + +var entry = 'test/fixtures/app-brfs.js' + +test('CLI supports browserify args after full stop', function (t) { + t.plan(1) + t.timeoutAfter(10000) + + doBundle(function (err, expected) { + if (err) return t.fail(err) + var app = budo.cli([ entry, '--no-stream', '--no-debug', '--', '-t', 'brfs', '--insert-globals' ]) + .once('update', function (contents) { + t.equal(contents.toString().length, expected.toString().length, 'matches bundler') + app.close() + }) + }) +}) + +test('API supports browserify JS object', function (t) { + t.plan(1) + t.timeoutAfter(10000) + + doBundle(function (err, expected) { + if (err) return t.fail(err) + var app = budo(entry, { + debug: false, + browserify: { + transform: brfs, + fullPaths: false, + insertGlobals: true + } + }).once('update', function (contents) { + t.equal(contents.toString().length, expected.toString().length, 'matches bundler') + app.close() + }) + }) +}) + +function doBundle (cb) { + var bundler = browserify({ + debug: false, + transform: brfs, + fullPaths: false, + insertGlobals: true + }) + bundler.add(path.resolve(entry)) + bundler.bundle(cb) +} diff --git a/test/test-bundle.js b/test/test-bundle.js new file mode 100644 index 0000000..9a3e9a0 --- /dev/null +++ b/test/test-bundle.js @@ -0,0 +1,137 @@ +var test = require('tape') +var budo = require('../') + +var request = require('request') +var xtend = require('xtend') +var browserify = require('browserify') +var path = require('path') +var vm = require('vm') +var mapEntry = require('../lib/map-entry') + +test('serves app.js', run('test/fixtures/app.js')) +test('entry mapping to bundle.js', run('test/fixtures/app.js:bundle.js')) +test('turns off debug', run('test/fixtures/app', { debug: false })) +test('brfs transform', run('test/fixtures/app-brfs', { + message: 'foobar', + browserify: { + transform: 'brfs' + } +})) + +test('bundles multiple', run([ + 'test/fixtures/first', + 'test/fixtures/second' +], { + message: 'foo bar' +})) +test('bundles multiple and serves as static/bundle.js', run([ + 'test/fixtures/first', + 'test/fixtures/second' +], { + message: 'foo bar', + serve: 'static/bundle.js', + shouldServe: 'static/bundle.js' +})) + +test('handles paths beginning with dot slash', run(['./test/fixtures/app.js'], { + message: 'from browserify', + shouldServe: 'app.js' +})) + +test('should also serve relative paths', run(['../budo/test/fixtures/app.js'], { + message: 'from browserify', + shouldServe: 'app.js' +})) + +var abs = path.resolve(__dirname, 'fixtures', 'app.js') +test('should serve absolute paths', run(abs, { + message: 'from browserify', + shouldServe: 'app.js' +})) +test('serve absolute with mapping', run(abs + ':boop.js', { + message: 'from browserify', + shouldServe: 'boop.js' +})) + +test('does not break on query params', run('test/fixtures/with space.js:bundle.js?debug=true', { + shouldServe: 'bundle.js?debug=true', + message: 'with space' +})) + +test('serves with spaces and entry to bundle.js', run('test/fixtures/with space.js', { + serve: 'bundle.js', + shouldServe: 'bundle.js', + message: 'with space' +})) + +test('serves with spaces default', run('test/fixtures/with space.js', { + message: 'with space', + shouldServe: 'with%20space.js' +})) + +function run (entries, opt) { + return function (t) { + matches(t, entries, opt) + } +} + +function matches (t, entries, opt) { + opt = xtend({ dir: __dirname, debug: true }, opt) + + var message = opt.message || 'from browserify' + var shouldServe = opt.shouldServe + delete opt.message + delete opt.shouldServe + + t.plan(shouldServe ? 5 : 4) + var uri + if (!Array.isArray(entries)) { + entries = [ entries ] + } + + var app = budo(entries, opt) + .on('connect', function (ev) { + if (shouldServe) { + t.equal(ev.serve, shouldServe, 'serves correct bundle file') + } + uri = ev.uri + ev.serve + t.ok(true, 'connected') + }) + .once('update', function () { + var b = browserify(xtend({ + debug: opt.debug + }, opt.browserify)) + entries.forEach(function (entry) { + entry = mapEntry(entry).from + b.add(path.resolve(entry)) + }) + + b.bundle(function (err, expected) { + if (err) t.fail(err) + request.get({ + uri: uri + }, function (err, resp, data) { + if (err) t.fail(err) + + // make sure what browserify bundles matches what budo bundles + t.equal(data.toString(), expected.toString(), 'bundles match') + + // also compare output of running both bundles + vm.runInNewContext(data.toString(), { + console: { log: log }, + global: {} + }) + + app.close() + }) + }) + }) + .on('exit', function () { + t.ok(true, 'closing') + }) + .on('error', t.fail.bind(t)) + + function log (msg) { + t.equal(msg, message, 'the output matches in both cases') + } +} diff --git a/test/test-close-immediate.js b/test/test-close-immediate.js new file mode 100644 index 0000000..60dfe53 --- /dev/null +++ b/test/test-close-immediate.js @@ -0,0 +1,46 @@ +var test = require('tape') +var budo = require('../') + +// watchify, chokidar and tinylr are a bit stubborn +// if you try to close them immediately after starting +// the watchers. +test('can close on connect with watch/live', function (t) { + t.plan(2) + t.timeoutAfter(10000) + + var app = budo('test/fixtures/app.js', { + dir: __dirname, + port: 8000 + }) + .live() + .watch() + .on('connect', function () { + t.ok(true, 'connected') + app.close() + }) + .on('update', function () { + t.fail(true, 'got update') + }) + .on('exit', function () { + t.ok(true, 'got exit') + }) +}) + +test('can close on first update', function (t) { + t.plan(3) + t.timeoutAfter(10000) + + var app = budo('test/fixtures/app.js', { + dir: __dirname + }) + .on('update', function () { + t.ok(true, 'got update') + app.close() + }) + .once('connect', function () { + t.ok(true, 'connected') + }) + .on('exit', function () { + t.ok(true, 'got exit') + }) +}) diff --git a/test/test-custom-server.js b/test/test-custom-server.js new file mode 100644 index 0000000..3acf0a1 --- /dev/null +++ b/test/test-custom-server.js @@ -0,0 +1,55 @@ +var test = require('tape') +var budo = require('../') +var path = require('path') +var request = require('request') + +var file = path.join(__dirname, 'fixtures', 'app.js') + +test('custom middleware', function (t) { + t.plan(1) + var b = budo(file, { + middleware: middleware + }).on('connect', function (ev) { + request.get({ uri: ev.uri + 'api' }, function (err, resp, body) { + b.close() + if (err) return t.fail(err) + t.equal(body, 'hello world', 'gets custom middleware') + }) + }) + + function middleware (req, res, next) { + if (req.url === '/api') { + res.statusCode = 200 + res.setHeader('Content-Type', 'text/plain') + res.end('hello world') + } else { + next() + } + } +}) + +test('stacking middlewares', function (t) { + t.plan(2) + var b = budo(file, { + middleware: [ + function (req, res, next) { + t.equal(req.url, '/api') + next() + }, + function (req, res, next) { + if (req.url === '/api') { + res.end('api contents') + } else { + next() + } + } + ], + serve: 'bundle.js' + }).on('connect', function (ev) { + request.get({ uri: ev.uri + 'api' }, function (err, resp, body) { + b.close() + if (err) return t.fail(err) + t.equal(body, 'api contents') + }) + }) +}) diff --git a/test/test-entries.js b/test/test-entries.js new file mode 100644 index 0000000..081bf2a --- /dev/null +++ b/test/test-entries.js @@ -0,0 +1,91 @@ +var budo = require('../') +var test = require('tape') +var ndjson = require('ndjson') +var request = require('request') +var path = require('path') +var entryMap = require('../lib/map-entry') + +// an HTML page with no ' + +test('accept "." entry point', function (t) { + // currently we can't test budo('.') because the index.js + // is node-specific, and crashes the build. + // when { cwd } option is supported, we could change this to + // a fixture folder + t.plan(2) + t.deepEqual(entryMap('.'), { + from: path.resolve(__dirname, '..', 'index.js'), + url: 'index.js' + }) + t.deepEqual(entryMap('.:bundle.js'), { + from: path.resolve(__dirname, '..', 'index.js'), + url: 'bundle.js' + }) +}) + +test('no arguments needed', function (t) { + t.plan(4) + var b = budo() + .on('connect', function (ev) { + t.deepEqual(ev.entries, []) + t.deepEqual(ev.serve, undefined) + request.get({ url: ev.uri }, function (err, resp, body) { + if (err) return t.fail(err) + t.equal(resp.statusCode, 200, 'still gets generated index.html') + t.equal(body, defaultIndex, 'should match default index') + b.close() + }) + }) +}) + +test('still supports serve', function (t) { + t.plan(4) + var b = budo({ + serve: 'foo.js' + }) + .on('connect', function (ev) { + t.deepEqual(ev.entries, []) + t.deepEqual(ev.serve, 'foo.js') + request.get({ url: ev.uri }, function (err, resp, body) { + if (err) return t.fail(err) + t.equal(resp.statusCode, 200, 'still gets generated index.html') + t.equal(body, scriptIndex) + b.close() + }) + }) +}) + +test('user can build their own server', function (t) { + t.plan(5) + var stream = ndjson.parse() + stream.on('data', function (data) { + if (data.url === '/foo.js') { + t.equal(data.type, 'middleware', 'got middleware') + } + }) + + var b = budo({ + serve: 'foo.js', + stream: stream, + ndjson: true, + middleware: function (req, res, next) { + if (req.url === '/foo.js') { + res.end('hello world') + } else { + next() + } + } + }) + .on('connect', function (ev) { + t.deepEqual(ev.entries, []) + t.deepEqual(ev.serve, 'foo.js') + request.get({ url: ev.uri + 'foo.js' }, function (err, resp, body) { + if (err) return t.fail(err) + t.equal(resp.statusCode, 200, 'gets foo.js route') + t.equal(body, 'hello world') + b.close() + }) + }) +}) diff --git a/test/test-event-order.js b/test/test-event-order.js new file mode 100644 index 0000000..67d4fce --- /dev/null +++ b/test/test-event-order.js @@ -0,0 +1,24 @@ +var test = require('tape') +var budo = require('../') +var path = require('path') + +test('event order should be correct', function (t) { + t.plan(1) + + var fixture = path.resolve(__dirname, 'fixtures', 'app.js') + var hasConnected = false + var hasUpdated = false + var app = budo(fixture) + .once('connect', function () { + if (hasUpdated) { + app.close() + t.fail('update before connect') + } + hasConnected = true + }) + .once('update', function () { + hasUpdated = true + t.equal(hasConnected, true, 'got update after connect') + app.close() + }) +}) diff --git a/test/test-live.js b/test/test-live.js new file mode 100644 index 0000000..07e22bc --- /dev/null +++ b/test/test-live.js @@ -0,0 +1,147 @@ +var test = require('tape') +var budo = require('../') +var path = require('path') +var request = require('request') +var fs = require('fs') +var source = fs.readFileSync(path.join(__dirname, 'fixtures', 'app.js'), 'utf8') +var internalIp = require('internal-ip') + +test('should inject LiveReload snippet', function (t) { + t.plan(4) + t.timeoutAfter(10000) + + var entry = path.join(__dirname, 'fixtures', 'app.js') + var app = budo(entry, { + dir: __dirname, + port: 8000, + host: 'localhost', + serve: 'app.js', + live: true + }) + .on('error', function (err) { + t.fail(err) + }) + .once('update', function () { + t.ok(true, 'update event triggered') + app.close() + }) + .on('reload', function (file) { + t.equal(file, 'app.js', 'reload event triggered') + }) + .on('connect', function (ev) { + matchesHTML(t, ev.uri) + setTimeout(function () { + fs.writeFile(entry, source) + }, 1000) + }) + .on('exit', function () { + t.ok(true, 'closing') + }) +}) + +test('manual LiveReload triggering', function (t) { + t.plan(4) + t.timeoutAfter(10000) + + var entry = path.join(__dirname, 'fixtures', 'app.js') + var app = budo(entry, { + dir: __dirname, + port: 8000, + host: 'localhost', + serve: 'app.js' + }) + .watch() + .live() + .on('error', function (err) { + t.fail(err) + }) + .on('update', function (file) { + t.equal(Buffer.isBuffer(file), true, 'update event triggered') + app.reload('app.js') + }) + .on('reload', function (file) { + t.equal(file, 'app.js', 'reload event triggered') + app.close() + }) + .on('connect', function (ev) { + matchesHTML(t, ev.uri) + setTimeout(function () { + fs.writeFile(entry, source) + }, 1000) + }) + .on('exit', function () { + t.ok(true, 'closing') + }) +}) + +test('should not inject LiveReload snippet', function (t) { + t.plan(2) + t.timeoutAfter(10000) + + var entry = path.join(__dirname, 'fixtures', 'app.js') + var app = budo(entry, { + dir: __dirname, + port: 8000, + host: 'localhost', + serve: 'app.js' + }) + .live({ plugin: true }) + .on('error', function (err) { + t.fail(err) + }) + .on('connect', function (ev) { + matchesHTML(t, ev.uri, getHTMLNoLive(), function () { + app.close() + }) + }) + .on('exit', function () { + t.ok(true, 'closing') + }) +}) + +test('LiveReload snippet host should default to budo host', function (t) { + t.plan(2) + t.timeoutAfter(10000) + + var addr = internalIp() + var entry = path.join(__dirname, 'fixtures', 'app.js') + var app = budo(entry, { + dir: __dirname, + port: 8000, + host: addr, + serve: 'app.js' + }) + .live() + .on('error', function (err) { + t.fail(err) + }) + .on('connect', function (ev) { + matchesHTML(t, ev.uri, getHTMLWithHost(addr), function () { + app.close() + }) + }) + .on('exit', function () { + t.ok(true, 'closing') + }) +}) + +function matchesHTML (t, uri, html, cb) { + request.get({ uri: uri + 'index.html' }, function (err, resp, body) { + if (err) t.fail(err) + t.equal(body, html || getHTML(), 'matches expected HTML') + + if (cb) cb() + }) +} + +function getHTMLNoLive () { + return 'budo' +} + +function getHTML () { + return 'budo' +} + +function getHTMLWithHost (ip) { + return 'budo' +} diff --git a/test/test-log.js b/test/test-log.js new file mode 100644 index 0000000..87d0851 --- /dev/null +++ b/test/test-log.js @@ -0,0 +1,33 @@ +var test = require('tape') +var through = require('through2') +var budo = require('../') + +var entry = 'test/fixtures/app.js' + +test('should log properly after restarting the server', function (t) { + var out = through.obj(function (item, _, next) { + if (item.name === 'budo' && item.type === 'connect') { + t.ok(item, 'budo start log message') + } + next() + }) + + // should only get two server startup messages + t.plan(2) + // start a self-closing server, and then do it again (once) when it exits + start().once('exit', start) + + function start () { + var b = budo(entry, { + dir: __dirname, + stream: out, + ndjson: true + }) + + return b.once('connect', function (ev) { + b.close() + }) + .on('error', t.fail.bind(t)) + } +}) + diff --git a/test/test-map-entry.js b/test/test-map-entry.js new file mode 100644 index 0000000..44e4d4b --- /dev/null +++ b/test/test-map-entry.js @@ -0,0 +1,21 @@ +var test = require('tape') +var mapEntry = require('../lib/map-entry') + +test('should map entry paths', function (t) { + t.deepEqual(mapEntry('foo.js'), { from: 'foo.js', url: 'foo.js' }) + t.deepEqual(mapEntry('foo/bar.js'), { from: 'foo/bar.js', url: 'bar.js' }) + t.deepEqual(mapEntry('./foo/bar.js'), { from: './foo/bar.js', url: 'bar.js' }) + t.deepEqual(mapEntry('foo/bar.js:bundle.js'), { from: 'foo/bar.js', url: 'bundle.js' }) + t.deepEqual(mapEntry('foo/bar.js:bundle.js?foo'), { from: 'foo/bar.js', url: 'bundle.js?foo' }) + t.deepEqual(mapEntry('f\\bar.js:bundle.js?foo'), { from: 'f\\bar.js', url: 'bundle.js?foo' }) + t.deepEqual(mapEntry('/absolute/path.js:bundle.js?foo'), { from: '/absolute/path.js', url: 'bundle.js?foo' }) + t.deepEqual(mapEntry('/absolute/path.js'), { from: '/absolute/path.js', url: 'path.js' }) + t.deepEqual(mapEntry('C:/absolute/path.js'), { from: 'C:/absolute/path.js', url: 'path.js' }) + t.deepEqual(mapEntry('C:/absolute/path.js:bundle.js'), { from: 'C:/absolute/path.js', url: 'bundle.js' }) + t.deepEqual(mapEntry('C://absolute//path.js:bundle.js'), { from: 'C://absolute//path.js', url: 'bundle.js' }) + t.deepEqual(mapEntry('C:\\absolute\\path.js:bundle.js'), { from: 'C:\\absolute\\path.js', url: 'bundle.js' }) + + // This one is failing. Maybe because of OS I am running tests on?? + // t.deepEqual(mapEntry('C:\\absolute\\path.js'), { from: 'C:\\absolute\\path.js', url: 'path.js' }) + t.end() +}) diff --git a/test/test-onupdate.js b/test/test-onupdate.js new file mode 100644 index 0000000..57f5121 --- /dev/null +++ b/test/test-onupdate.js @@ -0,0 +1,21 @@ +var test = require('tape') +var path = require('path') +var kill = require('tree-kill') + +var spawn = require('win-spawn') +var cli = path.resolve(__dirname, '..', 'bin', 'cmd.js') + +test('should trigger echo', function (t) { + t.plan(2) + t.timeoutAfter(5000) + var src = path.resolve(__dirname, 'fixtures', 'app.js') + var proc = spawn(cli, [ src, '--onupdate', 'echo FOO BAR', '--no-stream' ]) + + proc.stdout.on('data', function (buf) { + t.equal(buf.toString().trim(), 'FOO BAR') + proc.on('exit', function () { + t.ok(true, 'closed') + }) + kill(proc.pid) + }) +}) diff --git a/test/test-portfind.js b/test/test-portfind.js new file mode 100644 index 0000000..57abd37 --- /dev/null +++ b/test/test-portfind.js @@ -0,0 +1,75 @@ +var test = require('tape') +var budo = require('../') +var http = require('http') +var path = require('path') + +var file = path.join(__dirname, 'fixtures', 'app.js') + +test('user can disable portfinding', function (t) { + t.plan(1) + var server = http.createServer().listen(9966, function () { + var b = budo(file, { + port: 9966, + portfind: false + }) + b.on('error', function (err) { + t.equal(err.code, 'EADDRINUSE') + b.close() + server.close() + }) + }) +}) + +test('portfinds by default', function (t) { + t.plan(2) + var server = http.createServer().listen(9966, function () { + var b = budo(file, { + port: 9966, + livePort: 30000 + }) + b.on('error', t.fail) + b.on('connect', function (ev) { + t.equal(ev.port, 9967, 'gets port') + t.equal(ev.livePort, 30000, 'gets live port') + b.close() + server.close() + }) + }) +}) + +test('gets connect', function (t) { + t.plan(1) + var b = budo(file, { + port: 9966, + portfind: false + }) + b.on('connect', function (ev) { + t.equal(ev.port, 9966) + b.close() + }) +}) + +test('robust portfinding', function (t) { + t.plan(2) + var server0, server1 + + server0 = http.createServer().listen(8888, function () { + server1 = http.createServer().listen(8889, start) + server1.on('error', t.fail) + }).on('error', t.fail) + + function start () { + var b = budo(file, { + port: 8888, + livePort: 8888 + }) + b.on('connect', function (ev) { + t.equal(ev.port, 8890) + t.equal(ev.livePort, 8891) + b.close() + server0.close() + server1.close() + }) + b.on('error', t.fail) + } +}) diff --git a/test/test-proxy.js b/test/test-proxy.js new file mode 100644 index 0000000..d326500 --- /dev/null +++ b/test/test-proxy.js @@ -0,0 +1,52 @@ +var test = require('tape') +var budo = require('../') +var path = require('path') +var request = require('request') +var http = require('http') + +test('connect to proxy server api', function (t) { + t.plan(6) + t.timeoutAfter(20000) + + var server = http.createServer(function (req, res) { + // console.log('url: %s', req.url) + t.equal(req.url, '/api/hello') + res.writeHead(200, { 'Content-Type': 'application/json' }) + res.write(JSON.stringify({ message: 'hello world' })) + res.end() + }) + + server.on('close', function () { + t.ok(true, 'closing proxy server') + }) + + server.listen(3000, function () { + var entry = path.join(__dirname, 'fixtures', 'app.js') + var app = budo(entry, { + dir: __dirname, + port: 8000, + host: 'localhost', + serve: 'app.js', + proxy: '/api@http://localhost:3000/api' + }) + .on('error', function (err) { + t.fail(err) + }) + .on('connect', function (ev) { + setTimeout(function () { + request('http://localhost:8000/api/hello', function (error, response, body) { + if (error) t.fail(error) + t.ok(!error, 'no proxy error') + t.ok(body, 'received proxy response body') + var json = JSON.parse(body) + t.equal(json.message, 'hello world') + app.close() + server.close() + }) + }, 1000) + }) + .on('exit', function () { + t.ok(true, 'closing') + }) + }) +}) diff --git a/test/test-pushstate.js b/test/test-pushstate.js new file mode 100644 index 0000000..d93cf49 --- /dev/null +++ b/test/test-pushstate.js @@ -0,0 +1,22 @@ +var test = require('tape') +var budo = require('../') +var path = require('path') +var request = require('request') + +var file = path.join(__dirname, 'fixtures', 'app.js') +var html = 'budo' + +test('pushstate', function (t) { + t.plan(1) + var b = budo(file, { + port: 9966, + pushstate: true, + portfind: false + }).on('connect', function (ev) { + request({ uri: ev.uri + '/foobar' }, function (err, resp, body) { + b.close() + if (err) return t.fail(err) + t.equal(body, html, 'returns home index.html') + }) + }) +}) diff --git a/test/test-server.js b/test/test-server.js new file mode 100644 index 0000000..4a00382 --- /dev/null +++ b/test/test-server.js @@ -0,0 +1,130 @@ +var test = require('tape') +var budo = require('../') +var defaultHtml = require('simple-html-index') + +var request = require('request') +var entry = 'test/fixtures/app.js' + +test('default should serve on 9966', port(9966)) +test('should serve on specified port', port(3000, { port: 3000 })) + +test('should serve on --dir', function (t) { + t.plan(2) + var app = budo(entry, { dir: __dirname }) + .on('connect', function (ev) { + request.get({ + uri: ev.uri + 'fixtures/text.txt' + }, function (err, resp, body) { + if (err) t.fail(err) + t.equal(body.toString(), 'foobar', 'text matches') + app.close() + }) + }) + .on('exit', function () { + t.ok(true, 'closed') + }) + .on('error', t.fail.bind(t)) +}) + +test('support defaultIndex stream', function (t) { + t.plan(3) + + function html (opt) { + return defaultHtml({ + entry: opt.entry, + title: 'foobar', + css: 'main.css' + }) + } + + var app = budo(entry, { dir: __dirname, defaultIndex: html }) + .on('connect', function (ev) { + request.get({ + uri: ev.uri + }, function (err, resp, body) { + if (err) t.fail(err) + t.equal(resp.statusCode, 200) + t.equal(body, 'foobar') + app.close() + }) + }) + .on('exit', function () { + t.ok(true, 'closed') + }) + .on('error', t.fail.bind(t)) +}) + +test('support --title and --css', function (t) { + t.plan(3) + + var app = budo(entry, { + dir: __dirname, + title: 'foobar', + css: 'main.css' + }) + .on('connect', function (ev) { + request.get({ + uri: ev.uri + }, function (err, resp, body) { + if (err) t.fail(err) + t.equal(resp.statusCode, 200) + t.equal(body, 'foobar') + app.close() + }) + }) + .on('exit', function () { + t.ok(true, 'closed') + }) + .on('error', t.fail.bind(t)) +}) + +test('favicon.ico should have status code 200', function (t) { + t.plan(2) + var app = budo(entry, { dir: __dirname }) + .on('connect', function (ev) { + request.get({ + uri: ev.uri + 'favicon.ico' + }, function (err, resp) { + if (err) t.fail(err) + t.equal(resp.statusCode, 200) + app.close() + }) + }) + .on('exit', function () { + t.ok(true, 'closed') + }) + .on('error', t.fail.bind(t)) +}) + +function port (expected, opt) { + return function (t) { + t.plan(2) + var app = budo(entry, opt) + .on('connect', function (ev) { + t.ok(ev.port, expected, 'serves on ' + expected) + app.close() + }) + .on('exit', function () { + t.ok(true, 'closed') + }) + .on('error', t.fail.bind(t)) + } +} + +test('serve with CORS enable', function (t) { + t.plan(2) + var app = budo(entry, { dir: __dirname, cors: true }) + .on('connect', function (ev) { + request.get({ + uri: ev.uri + 'favicon.ico' + }, function (err, resp) { + if (err) t.fail(err) + t.equal(resp.headers['access-control-allow-origin'], '*') + app.close() + }) + }) + .on('exit', function () { + t.ok(true, 'closed') + }) + .on('error', t.fail.bind(t)) +}) diff --git a/test/test-simple.js b/test/test-simple.js deleted file mode 100644 index 938f7c3..0000000 --- a/test/test-simple.js +++ /dev/null @@ -1,174 +0,0 @@ -var rimraf = require('rimraf') -var fs = require('fs') -var path = require('path') -var spawn = require('win-spawn') -var npmSpawn = require('npm-execspawn') -var kill = require('tree-kill') - -var ndjson = require('ndjson') -var test = require('tape') -var concat = require('concat-stream') -var request = require('request') - -var cliPath = path.resolve(__dirname, '..', 'bin', 'cmd.js') - -//Some other tests needed: -// --live -// --live-plugin - -test('should fail without scripts', function(t) { - t.plan(1) - var proc = spawn(cliPath) - proc.stderr.pipe(concat(function(str) { - t.equal(str.toString().trim(), 'No entry scripts specified!') - })) -}) - -test('should run on available port', function(t) { - t.plan(1) - var proc = spawn(cliPath, ['app.js', '-o', 'bundle.js'], { cwd: __dirname, env: process.env }) - var expected = 'Server running at' - - proc.stdout.pipe(ndjson.parse()) - .on('data', function(data) { - var msg = (data && data.message)||'' - t.ok(msg.indexOf(expected)>=0, 'starts server') - kill(proc.pid) - }) - .on('error', function(err) { - t.fail(err) - kill(proc.pid) - }) -}) - -// TODO: fix this case -// test('should get a bundle.js', function(t) { -// var cwd = path.resolve(__dirname, '..') -// runBundleMatch(t, { -// watchify: ['app.js', '-v', '-o', 'bundle-expected.js'], -// budo: ['app.js'] -// }) -// }) - -test('should get a bundle.js with --outfile', function(t) { - var cwd = path.resolve(__dirname, '..') - runBundleMatch(t, { - watchify: ['app.js', '-v', '-o', 'bundle-expected.js'], - budo: ['app.js', '-o', 'bundle.js'] - }) -}) - -test('should get a bundle.js with --dir', function(t) { - var cwd = path.resolve(__dirname, '..') - runBundleMatch(t, { - cwd: cwd, - watchify: ['test/app.js', '-v', '-o', 'test/bundle-expected.js'], - budo: ['test/app.js', '-o', 'bundle.js', '--dir', 'test'] - }) -}) - -function runBundleMatch(t, opt) { - opt = opt||{} - - t.plan(1) - var cwd = opt.cwd || __dirname - var foundMsg = false - var bundle = path.join(__dirname, 'bundle.js') - var bundleExpected = path.join(__dirname, 'bundle-expected.js') - - //the expected bundle - var watchifyProc = npmSpawn('watchify '+ opt.watchify.join(' '), { cwd: cwd, env: process.env }) - watchifyProc.stderr.on('data',watchifyDone) - watchifyProc.stdout.on('data',watchifyDone) - - function watchifyDone(msg) { - var suc = msg.toString().indexOf('bundle-expected.js') - if (suc === -1) - t.fail('watchify process gave unexpected stdout/stderr message' ) - kill(watchifyProc.pid) - - var expected = fs.readFile(bundleExpected, 'utf8', function(err, data) { - if (err) - t.fail(err) - budoMatches(data) - }) - } - - function budoMatches(source) { - var proc = spawn(cliPath, opt.budo, { cwd: cwd, env: process.env }) - proc.on('exit', cleanup) - proc.stdout.pipe(ndjson.parse()) - .on('data', function(data) { - var msg = (data.message||'').toLowerCase() - var running = 'server running at ' - var idx = msg.indexOf(running) - if (idx >= 0) { - foundMsg = true - setTimeout(function() { //let bundling finish - var serverUrl = msg.substring(idx+running.length) - request.get({ - uri: serverUrl + '/bundle.js' - }, function(err, resp, data) { - t.equal(data, source, 'bundle matches') - kill(proc.pid) - }) - }, 1000) - } else if (!foundMsg) { - t.fail('no server running message in '+ msg) - kill(proc.pid) - } - }) - } - - function cleanup() { - rimraf(bundleExpected, function(err) { - if (err) console.error(err) - }) - rimraf(bundle, function(err) { - if (err) console.error(err) - }) - } -} - -test('should create and destroy tmpdir', function(t) { - t.plan(2) - var proc = spawn(cliPath, ['app.js'], { cwd: __dirname, env: process.env }) - var expected = 'temp directory created at ' - proc.stdout.pipe(ndjson.parse()) - .on('data', function(data) { - if (data.level !== 'debug') - return - - var msg = data && data.message - var idx = msg.indexOf(expected) - - if (idx === -1) { - t.fail('no temp dir created') - kill(proc.pid) - } else { - var path = msg.substring(idx+expected.length).trim() - t.ok(true, 'created tmp dir') - proc.on('exit', cleanup(path)) - kill(proc.pid, 'SIGINT') - } - }) - .on('error', function(err) { - t.fail(err) - kill(proc.pid) - }) - - function cleanup(path) { - return function() { - fs.exists(path, function(exists) { - if (exists) { - t.fail('tmpdir not cleaned up '+path) - rimraf(path, function(err) { - if (err) - console.error(err) - }) - } - else t.ok(true, 'tmpdir cleaned up') - }) - } - } -}) \ No newline at end of file diff --git a/test/test-static-folders.js b/test/test-static-folders.js new file mode 100644 index 0000000..128c163 --- /dev/null +++ b/test/test-static-folders.js @@ -0,0 +1,56 @@ +var test = require('tape') +var budo = require('../') +var path = require('path') +var request = require('request') + +var path1 = path.resolve(__dirname, 'fixtures', 'one') +var path2 = path.resolve(__dirname, 'fixtures', 'two') +var path3 = path.resolve(__dirname, 'fixtures', 'three') + +test('should serve multiple folders', function (t) { + t.plan(2) + + var app = budo({ + dir: [ path1, path2 ] + }).on('connect', function (ev) { + request.get({ uri: ev.uri + 'one.txt' }, function (err, res, body) { + if (err) return t.fail(err) + t.equal(body, 'one', 'gets one') + request.get({ uri: ev.uri + 'two.txt' }, function (err, res, body) { + if (err) return t.fail(err) + t.equal(body, 'two', 'gets two') + app.close() + }) + }) + }) +}) + +test('should find any index.html', function (t) { + t.plan(1) + + var expected = 'TWO' + var app = budo({ + dir: [ path1, path2 ] + }).on('connect', function (ev) { + request.get({ uri: ev.uri + 'index.html' }, function (err, res, body) { + if (err) return t.fail(err) + t.equal(body, expected, 'gets any index.html in all static folders') + app.close() + }) + }) +}) + +test('should find the first index.html', function (t) { + t.plan(1) + + var expected = 'THREE' + var app = budo({ + dir: [ path1, path3, path2 ] + }).on('connect', function (ev) { + request.get({ uri: ev.uri + 'index.html' }, function (err, res, body) { + if (err) return t.fail(err) + t.equal(body, expected, 'gets the first index.html in all static folders') + app.close() + }) + }) +})