├── .gitignore ├── .travis.yml ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE.md ├── README.md ├── bin ├── run-js └── setup-logger.js ├── collaborators.md ├── docs └── api.md ├── lib ├── default-handlers.js ├── default-transforms.js ├── generate-bundle.js ├── handlers │ ├── coffeescript.js │ ├── javascript.js │ └── typescript.js ├── index.js ├── middleware │ └── error.js ├── router.js ├── routes │ ├── bundle-file.js │ ├── default-index.js │ ├── directory.js │ ├── html-file.js │ └── script-file.js └── template │ ├── bundle-error.html │ ├── error.html │ ├── index.html │ └── template.html ├── package.json └── test ├── basic.js ├── runners ├── default-index-page.js ├── error-page.js ├── html-file-page.js └── standard-page.js ├── scenarios.js ├── scenarios ├── bundle-error │ ├── expected │ │ └── bundle.js │ ├── index.js │ └── input │ │ └── foo.js ├── default-index │ └── index.js ├── error-404 │ └── index.js ├── html-file-no-script │ ├── expected │ │ └── page.html │ ├── index.js │ └── input │ │ └── test.html ├── html-file-with-script │ ├── expected │ │ ├── bundle.js │ │ └── page.html │ ├── index.js │ └── input │ │ ├── test.html │ │ └── test.js ├── single-file-index │ ├── expected │ │ └── bundle.js │ ├── index.js │ └── input │ │ └── index.js ├── single-file-non-index │ ├── expected │ │ └── bundle.js │ ├── index.js │ └── input │ │ └── foo.js ├── subdirectory-empty │ ├── index.js │ └── input │ │ └── foo │ │ └── .gitkeep └── subdirectory-with-index-html │ ├── expected │ └── page.html │ ├── index.js │ └── input │ └── foo │ └── index.html └── watch.js /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules/ 3 | .nyc_output/ 4 | coverage/ 5 | test/_test 6 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - '4' 4 | - '5' 5 | after_success: npm run travis-coveralls 6 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Code of Conduct 2 | 3 | As contributors and maintainers of this project, and in the interest of 4 | fostering an open and welcoming community, we pledge to respect all people who 5 | contribute through reporting issues, posting feature requests, updating 6 | documentation, submitting pull requests or patches, and other activities. 7 | 8 | We are committed to making participation in this project a harassment-free 9 | experience for everyone, regardless of level of experience, gender, gender 10 | identity and expression, sexual orientation, disability, personal appearance, 11 | body size, race, ethnicity, age, religion, or nationality. 12 | 13 | Examples of unacceptable behavior by participants include: 14 | 15 | * The use of sexualized language or imagery 16 | * Personal attacks 17 | * Trolling or insulting/derogatory comments 18 | * Public or private harassment 19 | * Publishing other's private information, such as physical or electronic 20 | addresses, without explicit permission 21 | * Other unethical or unprofessional conduct 22 | 23 | Project maintainers have the right and responsibility to remove, edit, or 24 | reject comments, commits, code, wiki edits, issues, and other contributions 25 | that are not aligned to this Code of Conduct, or to ban temporarily or 26 | permanently any contributor for other behaviors that they deem inappropriate, 27 | threatening, offensive, or harmful. 28 | 29 | By adopting this Code of Conduct, project maintainers commit themselves to 30 | fairly and consistently applying these principles to every aspect of managing 31 | this project. Project maintainers who do not follow or enforce the Code of 32 | Conduct may be permanently removed from the project team. 33 | 34 | This Code of Conduct applies both within project spaces and in public spaces 35 | when an individual is representing the project or its community. 36 | 37 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 38 | reported by contacting a project maintainer at . All 39 | complaints will be reviewed and investigated and will result in a response that 40 | is deemed necessary and appropriate to the circumstances. Maintainers are 41 | obligated to maintain confidentiality with regard to the reporter of an 42 | incident. 43 | 44 | 45 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 46 | version 1.3.0, available at 47 | [http://contributor-covenant.org/version/1/3/0/][version] 48 | 49 | [homepage]: http://contributor-covenant.org 50 | [version]: http://contributor-covenant.org/version/1/3/0/ 51 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Righteous! I'm happy that you want to contribute. :smile: 4 | 5 | * Make sure that you're read and understand the [Code of Conduct](CODE_OF_CONDUCT.md). 6 | * Check out the [issues tagged with the `starter` tag.](https://github.com/remixz/run-js/issues?q=is%3Aopen+is%3Aissue+label%3Astarter) 7 | 8 | ## run-js is an [OPEN Open Source Project](http://openopensource.org/) 9 | 10 | ### What? 11 | 12 | Individuals making significant and valuable contributions are given 13 | commit-access to the project to contribute as they see fit. This project 14 | is more like an open wiki than a standard guarded open source project. 15 | 16 | ### Rules 17 | 18 | There are a few basic ground-rules for contributors: 19 | 20 | 1. **No `--force` pushes** or modifying the Git history in any way. *(Exception: I use `git am -3` sometimes to clean up pull requests, and then commit them to the repo.)* 21 | 2. **Non-master branches** ought to be used for ongoing work. 22 | 3. **External API changes and significant modifications** ought to be subject to an **internal pull-request** to solicit feedback from other contributors. 23 | 4. Internal pull-requests to solicit feedback are *encouraged* for any other non-trivial contribution but left to the discretion of the contributor. 24 | 5. Contributors should adhere to the [JavaScript Standard code-style](https://github.com/feross/standard). 25 | 26 | ### Releases 27 | 28 | Declaring formal releases remains the prerogative of the project maintainer. 29 | 30 | ### Changes to this arrangement 31 | 32 | This is an experiment and feedback is welcome! This document may also be 33 | subject to pull-requests or changes by contributors where you believe 34 | you have something valuable to add or change. 35 | 36 | Get a copy of this manifesto as [markdown](https://raw.githubusercontent.com/openopensource/openopensource.github.io/master/Readme.md) and use it in your own projects. 37 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Zach Bruggeman and contributors 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # run-js 2 | 3 | [![Build Status](https://travis-ci.org/remixz/run-js.svg?branch=master)](https://travis-ci.org/remixz/run-js) 4 | [![Coverage Status](https://coveralls.io/repos/remixz/run-js/badge.svg?branch=master&service=github)](https://coveralls.io/github/remixz/run-js?branch=master) 5 | [![npm version](https://img.shields.io/npm/v/run-js.svg)](https://www.npmjs.com/package/run-js) 6 | [![js-standard-style](https://img.shields.io/badge/code%20style-standard-brightgreen.svg)](http://standardjs.com/) 7 | 8 | A prototyping server that just works. 9 | 10 | *Click to enlarge image:* 11 | [![run-js demo](https://s3.amazonaws.com/f.cl.ly/items/3U1J411P0L3x092D2h3F/run-js-demo2.gif)](https://s3.amazonaws.com/f.cl.ly/items/3U1J411P0L3x092D2h3F/run-js-demo2.gif) 12 | 13 | ## Installation 14 | 15 | Requires Node.js >=4.0.0. 16 | 17 | ```bash 18 | $ npm install run-js --global 19 | ``` 20 | 21 | ## Usage 22 | 23 | Enter a folder you want to run scripts in, and type `run-js`. 24 | 25 | ```bash 26 | $ cd your/folder 27 | $ run-js 28 | ``` 29 | 30 | It will print out the URL it's running on. From there, just visit any of your scripts in the browser, and they'll just work. 31 | 32 | For API usage, [see the documentation file.](docs/api.md) 33 | 34 | ## Features 35 | 36 | ### Instantly working scripts 37 | 38 | There's no HTML files you have to create, no compile steps for your code to work, and no need to even manually install dependencies. Just start `run-js` in a folder, write some code, and open it in the browser. `run-js` supports JavaScript (with ES2015 and JSX enabled via Babel), CoffeeScript, and TypeScript out of the box. When you require a dependency, `run-js` will automatically install it for you, if it's not installed already. Plus, the default HTML page includes a `
` tag with an `id` of `root`, so that you can quickly append elements from a library, such as React. 39 | 40 | ### Scripts as the index file 41 | 42 | Creating a file named `index.js` (or whatever type of file you prefer) will act as the index for the path you specify. For example, creating `index.js` in the root of where you ran `run-js` will use that script when you visit `http://localhost:60274`. 43 | 44 | ### Source maps 45 | 46 | No need to go through the hullabaloo of setting up source maps. They're just there, and they just work. 47 | 48 | ### Live reload 49 | 50 | When you make a change, the browser will automatically reload. Easy peasy. 51 | 52 | ### Custom HTML pages 53 | 54 | By default, `run-js` will render a page when you visit a file in the browser. However, if you need your own custom page, it's easy to do. Just create a `.html` file with the same name as your script. For example, if you had `foo.js`, create a `foo.html` in the same folder, and it'll use that for the template. It'll automatically insert your compiled script as well. (*Make sure to have a `` tag for this to work.*) 55 | 56 | ## Implementation 57 | 58 | run-js is powered by [Browserify](https://github.com/substack/node-browserify), and various transforms for it. I like [Webpack](https://github.com/webpack/webpack) as well, but I enjoy working with Browserify more, and find it easier to use overall, while still being able to do what I want to. I don't think run-js will need to change to Webpack, or some other future bundler, to get the functionality that's wanted. Of course, that could change... :wink:. The transform [installify](https://github.com/hughsk/installify) is used automatically install new dependencies. Really cool stuff! 59 | 60 | Aside from Browserify, run-js uses [Express](https://github.com/strongloop/express) to power the web server. Nothing too fancy there, really. run-js has an in-memory cache powered by [LevelUP](https://github.com/Level/levelup) and [MemDOWN](https://github.com/level/memdown). That could be migrated to a file cache pretty easily, but I'm not sure if it's really needed. It might be in the future, though, which is why I used LevelUP. 61 | 62 | ## Inspiration 63 | 64 | * [**@vjeux**](https://github.com/vjeux)'s challenge to create a better JavaScript prototyping environment: http://blog.vjeux.com/2015/javascript/challenge-best-javascript-setup-for-quick-prototyping.html 65 | * [**@ericclemmons**](https://github.com/ericclemmons)'s post about JavaScript fatigue: https://medium.com/@ericclemmons/javascript-fatigue-48d4011b6fc4 66 | * Modules such as [budo](https://github.com/mattdesl/budo), [beefy](https://github.com/chrisdickinson/beefy), and [wzrd](https://github.com/maxogden/wzrd), which all do a lot of what run-js does, but with less defaults, and just for running one file. I like those modules a lot, and I think they definitely work for a different type of workflow. The main difference with run-js is that it's aimed a bit more towards newbies, hence why it runs any file in the directory. Essentially, run-js is a playground: Everything just goes, and it's lots of fun! It's not really meant for serious work, but instead just trying things out. 67 | -------------------------------------------------------------------------------- /bin/run-js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 'use strict' 3 | /** 4 | * run-js - A prototyping server that just works. 5 | * 6 | * @author Zach Bruggeman 7 | */ 8 | 9 | const path = require('path') 10 | const mkdirp = require('mkdirp') 11 | const yargs = require('yargs') 12 | const open = require('opn') 13 | const bole = require('bole') 14 | const log = bole('run-js') 15 | const garnish = require('garnish') 16 | const RunJS = require('../lib') 17 | const setupLogger = require('./setup-logger') 18 | 19 | let argv = yargs 20 | .usage('Usage: run-js [--no-watch] [--dir=] [--port=<8080>] [path/to/script]\n\nPassing a relative path to a script file will automatically open it in the browser.') 21 | .options({ 22 | watch: { 23 | alias: 'w', 24 | type: 'boolean', 25 | default: true, 26 | description: 'Enables the LiveReload server. Enabled by default. Pass --no-watch to disable.' 27 | }, 28 | dir: { 29 | alias: 'd', 30 | type: 'string', 31 | default: process.cwd(), 32 | description: 'Directory for run-js to run in.' 33 | }, 34 | port: { 35 | alias: 'p', 36 | default: 60274, 37 | description: 'Port for the run-js server to listen on.' 38 | }, 39 | handler: { 40 | alias: 'h', 41 | type: 'array', 42 | default: [], 43 | description: 'File handler(s) to add to the default handlers provided by run-js.' 44 | }, 45 | transform: { 46 | alias: 't', 47 | type: 'array', 48 | default: [], 49 | description: 'Global transforms(s) to add to the default transforms provided by run-js.' 50 | }, 51 | plugin: { 52 | alias: 'pl', 53 | type: 'array', 54 | default: [], 55 | description: 'Global plugin(s) to add to the default plugins provided by run-js.' 56 | } 57 | }) 58 | .help('help') 59 | .version(require('../package.json').version) 60 | .strict() 61 | .argv 62 | 63 | // Create node_modules if it doesn't exist in the current folder 64 | mkdirp.sync(path.join(process.cwd(), 'node_modules')) 65 | 66 | if (!path.isAbsolute(argv.dir)) { 67 | argv.dir = path.resolve(process.cwd(), argv.dir) 68 | } 69 | 70 | let handlers = argv.handler.map(h => require(h)) 71 | let transforms = argv.transform.map(t => require(t)) 72 | let plugins = argv.plugin.map(p => require(p)) 73 | 74 | argv.handlers = require('../lib/default-handlers').concat(handlers) 75 | argv.transforms = require('../lib/default-transforms').concat(transforms) 76 | argv.plugins = [].concat(plugins) 77 | 78 | let logger = garnish({ 79 | level: 'info', 80 | name: 'run-js' 81 | }) 82 | 83 | logger.pipe(require('stdout-stream')) 84 | 85 | bole.output({ 86 | level: 'info', 87 | stream: logger 88 | }) 89 | 90 | let app = new RunJS(argv) 91 | 92 | setupLogger(app) 93 | 94 | app.start(err => { 95 | if (err) throw err 96 | log.info({ 97 | message: 'run-js is listening on', 98 | url: `http://localhost:${argv.port}` 99 | }) 100 | if (argv._[0]) { 101 | log.info(`Opening http://localhost:${argv.port}/${argv._[0]} in your browser.`) 102 | open(`http://localhost:${argv.port}/${argv._[0]}`) 103 | } else { 104 | log.info(`Open a file from this directory in the browser to see it in action.`) 105 | log.info('Example: Create a file named `foo.js` in this directory, and visit') 106 | log.info('http://localhost:60274/foo.js in your browser to see it ran.') 107 | } 108 | }) 109 | -------------------------------------------------------------------------------- /bin/setup-logger.js: -------------------------------------------------------------------------------- 1 | const log = require('bole')('run-js') 2 | const prettyBytes = require('pretty-bytes') 3 | 4 | function setupLogger (app) { 5 | app.on('request', (req, res, start) => { 6 | if (req.url.indexOf('__bundle/') > -1) return // don't log __bundle requests 7 | if (req.url === 'favicon.ico' && res.statusCode === 404) return // don't log favicon.ico 404s 8 | 9 | log.info({ 10 | elapsed: Date.now() - start, 11 | method: req.method, 12 | url: req.url, 13 | statusCode: res.statusCode 14 | }) 15 | }) 16 | 17 | app.on('bundle', info => { 18 | info.message = (info.cached ? 'Returned cached bundle for ' : 'Generated bundle for ') + `${info.file} (${prettyBytes(info.size)}) in ${info.bundleTime}ms` 19 | info.type = 'bundle' 20 | log.info(info) 21 | }) 22 | 23 | app.on('bundle:error', info => { 24 | info.type = 'bundle' 25 | log.error(info) 26 | }) 27 | 28 | app.once('watch:ready', () => { 29 | log.info({ 30 | message: 'Live reloading enabled.', 31 | type: 'watch' 32 | }) 33 | }) 34 | 35 | app.on('watch:all', (event, fp) => { 36 | log.info({ 37 | message: `File ${event}: ${fp}`, 38 | type: 'watch' 39 | }) 40 | }) 41 | 42 | app.on('watch:error', err => { 43 | log.error(err) 44 | }) 45 | 46 | return app 47 | } 48 | 49 | module.exports = setupLogger 50 | -------------------------------------------------------------------------------- /collaborators.md: -------------------------------------------------------------------------------- 1 | ## Collaborators 2 | 3 | run-js is only possible due to the excellent work of the following collaborators: 4 | 5 | 6 | 7 |
abaconGitHub/abacon
remixzGitHub/remixz
8 | -------------------------------------------------------------------------------- /docs/api.md: -------------------------------------------------------------------------------- 1 | # run-js api 2 | 3 | ## initializing 4 | ```js 5 | const RunJS = require('run-js') 6 | 7 | let app = new RunJS({ 8 | dir: process.cwd(), // directory to serve files from. defaults to current working directory 9 | watch: true, // enables/disables watch server for livereload. defaults to true. 10 | port: 60274, // port to listen on. defaults to 60274 11 | handlers: [], // array of file handler objects to use. none added by default. see below for format. 12 | transforms: [], // array of browserify transforms to use. none added by default. see below for format. 13 | plugins: [] // array of browserify plugins to use. none added by default. see below for format. 14 | }) 15 | ``` 16 | 17 | ## handlers, transforms & plugins 18 | 19 | * **handler** - run on files that match its `extension` property. should have 1 or more browserify transforms. 20 | 21 | ```js 22 | { 23 | extension: /\.jsx?$/, // regex for file extension to match 24 | transforms: [ // array of transforms to run on file. run from first to last. 25 | { 26 | module: require('babelify'), // should be a browserify transform 27 | opts: { // options to pass to the transform 28 | presets: [ require('babel-preset-es2015'), require('babel-preset-react') ] 29 | } 30 | } 31 | ], 32 | errorMessage: function (err) { // the message to display in the browser if there was a compilation error 33 | return `${err.message}\n\n${err.codeFrame}` 34 | } 35 | } 36 | ``` 37 | * **transform** - a browserify transform run on the file after the handler transforms it into JS. 38 | 39 | ```js 40 | { 41 | module: require('installify'), // should be a browserify transform 42 | opts: {} // options to pass to the transform 43 | } 44 | ``` 45 | 46 | * **plugin** - a browserify plugin run on the file after the handler transforms it into JS. 47 | 48 | ```js 49 | { 50 | module: require('browserify-hmr'), // should be a browserify plugin 51 | opts: {} // options to pass to the plugin 52 | } 53 | ``` 54 | 55 | the `run-js` api doesn't add any file handlers, transforms, or plugins by default. this is to make it as extensible as possible. to make it easier to develop with, however, a set of default handlers and transforms are provided with the module (and plugins in the future, if the module ends up using them by default). these are available with `require('run-js/lib/default-handlers')` and `require('run-js/lib/default-transforms')`, respectively. those can be passed to the `handlers` and `transforms` options in your `new RunJS` call. 56 | 57 | ## methods 58 | 59 | ### app.start(function (err) {}) 60 | 61 | starts the app. runs the passed callback when finished initializing. 62 | 63 | ### app.stop(function (err) {}) 64 | 65 | stops the app. runs the passed callback when finished stopping. 66 | 67 | ## events 68 | 69 | ### app.on('request', function (req, res, timestamp) {}) 70 | 71 | fired when a page is loaded. includes the request object, the response object, and a timestamp of when the request initiated. the timestamp can be used to determine how long it took to serve the response. 72 | 73 | ### app.on('bundle', function (info) {}) 74 | 75 | fired when a script bundle is finished generating. includes an info object, with this format: 76 | 77 | ```js 78 | { 79 | file: '/path/to/file', // absolute path to file that the bundle was generated for 80 | size: 31415, // size in bytes of the bundle 81 | bundleTime: 1337, // time in milliseconds of how long it took the bundle to generate 82 | cached: false // whether or not the bundle was returned from the internal cache 83 | } 84 | ``` 85 | 86 | ### app.on('bundle:error', function (err) {}) 87 | 88 | fired when the bundle errors. includes the error from the bundle generator. 89 | 90 | ### app.on('watch:ready', function () {}) 91 | 92 | fired when the watch server is ready. 93 | 94 | ### app.on('watch:all', function (event, filepath) {}) 95 | 96 | fired when there's a watch event. `event` may be [any of the events fired by chokidar.](https://github.com/paulmillr/chokidar#methods--events) 97 | 98 | ### app.on('watch:error', function (err) {}) 99 | 100 | fired when the watch server has an error. includes the error from `chokidar`. 101 | -------------------------------------------------------------------------------- /lib/default-handlers.js: -------------------------------------------------------------------------------- 1 | /** 2 | * run-js - A prototyping server that just works. 3 | * Array of the default file handlers. 4 | * 5 | * @author Zach Bruggeman 6 | */ 7 | 8 | module.exports = [ 9 | require('./handlers/javascript'), 10 | require('./handlers/coffeescript'), 11 | require('./handlers/typescript') 12 | ] 13 | -------------------------------------------------------------------------------- /lib/default-transforms.js: -------------------------------------------------------------------------------- 1 | /** 2 | * run-js - A prototyping server that just works. 3 | * Array of the default global transforms. 4 | * 5 | * @author Zach Bruggeman 6 | */ 7 | 8 | module.exports = [ 9 | { 10 | module: require('installify') 11 | }, 12 | { 13 | module: require('brfs') 14 | } 15 | ] 16 | -------------------------------------------------------------------------------- /lib/generate-bundle.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | /** 3 | * run-js - A prototyping server that just works. 4 | * 5 | * @author Zach Bruggeman 6 | */ 7 | 8 | const fs = require('fs') 9 | const path = require('path') 10 | const browserify = require('browserify') 11 | const concat = require('concat-stream') 12 | const ansiUp = require('ansi_up') 13 | const escapeHtml = require('escape-html') 14 | const _ = require('lodash') 15 | 16 | const errorTemplate = _.template(fs.readFileSync(path.resolve(__dirname, './template/bundle-error.html'))) 17 | 18 | function generateErrorScript (err) { 19 | let errorBox = errorTemplate({ 20 | name: err.name, 21 | message: ansiUp.ansi_to_html(escapeHtml(err.message)) 22 | }) 23 | 24 | // if it's not a string, and just included as a function that's interpolated by the returned string 25 | // it'll get mucked up during coverage testing 26 | let bundleJs = `function bundleError () { 27 | var template = ${JSON.stringify(errorBox)} 28 | if (typeof document === 'undefined') return 29 | document.addEventListener('DOMContentLoaded', function print () { 30 | var container = document.createElement('div') 31 | container.innerHTML = template 32 | document.body.appendChild(container) 33 | }) 34 | }` 35 | 36 | return `;(${bundleJs})()\n` 37 | } 38 | 39 | function generateBundle (opts, cb) { 40 | let b = browserify({ 41 | debug: true 42 | }) 43 | if (opts.handler.plugins) { 44 | opts.handler.plugins.forEach(plugin => b.plugin(plugin.module, plugin.opts)) 45 | } 46 | if (opts.handler.transforms) { 47 | opts.handler.transforms.forEach(transform => b.transform(transform.module, transform.opts)) 48 | } 49 | 50 | if (opts.plugins) { 51 | opts.plugins.forEach(plugin => b.plugin(plugin.module, plugin.opts)) 52 | } 53 | if (opts.transforms) { 54 | opts.transforms.forEach(transform => b.transform(transform.module, transform.opts)) 55 | } 56 | b.add(opts.file) 57 | 58 | let bd = b.bundle() 59 | let didError = false 60 | 61 | let bdStream = concat(body => { 62 | if (didError) return 63 | 64 | cb(null, body) 65 | }) 66 | 67 | // i'd do .once here, but tsify emits multiple errors, because... well, because it can, i guess. 68 | bd.on('error', err => { 69 | if (didError) return 70 | didError = true 71 | err.originalMessage = err.message 72 | err.message = opts.handler.errorMessage && opts.handler.errorMessage(err) || err.message 73 | let errorScript = generateErrorScript(err) 74 | 75 | cb(err, errorScript) 76 | }) 77 | 78 | bd.pipe(bdStream) 79 | } 80 | 81 | module.exports = generateBundle 82 | -------------------------------------------------------------------------------- /lib/handlers/coffeescript.js: -------------------------------------------------------------------------------- 1 | /** 2 | * run-js - A prototyping server that just works. 3 | * CoffeeScript file handler. Transforms using the `coffeeify` transformer. 4 | * 5 | * @author Zach Bruggeman 6 | */ 7 | 8 | module.exports = { 9 | extension: /\.coffee$/, 10 | transforms: [ 11 | { 12 | module: require('coffeeify') 13 | } 14 | ], 15 | errorMessage: function (err) { 16 | return err.annotated 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /lib/handlers/javascript.js: -------------------------------------------------------------------------------- 1 | /** 2 | * run-js - A prototyping server that just works. 3 | * JavaScript file handler. Transforms with `babelify`, using the `es2015` and `react` preset. 4 | * 5 | * @author Zach Bruggeman 6 | */ 7 | 8 | module.exports = { 9 | extension: /\.jsx?$/, // .js & .jsx 10 | transforms: [ 11 | { 12 | module: require('babelify'), 13 | opts: { 14 | presets: [ require('babel-preset-es2015'), require('babel-preset-react') ] 15 | } 16 | } 17 | ], 18 | errorMessage: function (err) { 19 | return `${err.message}\n\n${err.codeFrame}` 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /lib/handlers/typescript.js: -------------------------------------------------------------------------------- 1 | /** 2 | * run-js - A prototyping server that just works. 3 | * TypeScript file handler. Transforms using the `tsify` transformer. 4 | * 5 | * @author Zach Bruggeman 6 | */ 7 | 8 | module.exports = { 9 | extension: /\.ts$/, 10 | plugins: [ 11 | { 12 | module: require('tsify') 13 | } 14 | ], 15 | errorMessage: function (err) { 16 | return err.message 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | /** 3 | * run-js - A prototyping server that just works. 4 | * 5 | * @author Zach Bruggeman 6 | */ 7 | 8 | const path = require('path') 9 | const EventEmitter = require('events') 10 | const express = require('express') 11 | const chokidar = require('chokidar') 12 | const tinylr = require('tiny-lr') 13 | const _ = require('lodash') 14 | 15 | let createRouter = require('./router') 16 | let errorMiddleware = require('./middleware/error') 17 | 18 | function noop () {} 19 | 20 | class RunJS extends EventEmitter { 21 | constructor (opts) { 22 | super() 23 | this.opts = opts || {} 24 | _.defaults(this.opts, { 25 | dir: process.cwd(), 26 | watch: true, 27 | port: 60274, 28 | handlers: [], 29 | transforms: [], 30 | plugins: [] 31 | }) 32 | 33 | this.app = this._createApp() 34 | if (this.opts.watch) this.lr = this._createLiveReload() 35 | } 36 | 37 | start (cb) { 38 | if (!cb) cb = noop 39 | 40 | if (this.opts.watch) { 41 | this.watch = chokidar.watch(this.opts.dir, { 42 | ignored: /node_modules|\.git|^\..+|[\/\\]\..+/, 43 | ignoreInitial: true, 44 | useFsEvents: !(process.env.NODE_ENV === 'test') // fsevents has issues when testing, not removing the file listeners, causing process to hang 45 | }) 46 | 47 | this.watch.on('ready', () => this.emit('watch:ready')) 48 | 49 | this.watch.on('all', (event, fp) => { 50 | fp = path.resolve(this.opts.dir, fp) 51 | this.emit('watch:all', event, fp) 52 | this.lr.notifyClients([fp]) 53 | }) 54 | 55 | this.watch.on('error', err => this.emit('watch:error', err)) 56 | 57 | this.lr.listen() 58 | } 59 | 60 | this.server = this.app.listen(this.opts.port, cb) 61 | 62 | let connections = [] 63 | this.server.on('connection', conn => { 64 | let key = `${conn.remoteAddress}:${conn.remotePort}` 65 | connections[key] = conn 66 | conn.on('close', () => { 67 | delete connections[key] 68 | }) 69 | }) 70 | this.server.destroy = cb => { 71 | this.server.close(cb) 72 | for (let key in connections) { 73 | connections[key].destroy() 74 | } 75 | } 76 | } 77 | 78 | stop (cb) { 79 | if (!cb) cb = noop 80 | 81 | if (!this.server) { 82 | return cb(new Error('No server currently running.')) 83 | } 84 | 85 | if (this.opts.watch) { 86 | this.lr.close() 87 | this.watch.close() 88 | } 89 | 90 | this.removeAllListeners() 91 | this.server.destroy(cb) 92 | } 93 | 94 | _createApp () { 95 | let app = express() 96 | 97 | app.locals.instance = this 98 | app.locals.opts = this.opts 99 | 100 | this.router = createRouter(this.opts.handlers) 101 | app.use((req, res, next) => { 102 | let now = Date.now() 103 | res.on('finish', () => this.emit('request', req, res, now)) 104 | next() 105 | }) 106 | app.use(this.router) 107 | app.use(express.static(this.opts.dir)) 108 | app.use(errorMiddleware) 109 | 110 | return app 111 | } 112 | 113 | _createLiveReload () { 114 | let lr = tinylr({ port: 35729 }) 115 | 116 | return lr 117 | } 118 | } 119 | 120 | module.exports = RunJS 121 | -------------------------------------------------------------------------------- /lib/middleware/error.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | /** 3 | * run-js - A prototyping server that just works. 4 | * 5 | * @author Zach Bruggeman 6 | */ 7 | 8 | const fs = require('fs') 9 | const path = require('path') 10 | const _ = require('lodash') 11 | 12 | let pageTemplate = _.template(fs.readFileSync(path.join(__dirname, '../template/error.html'))) 13 | 14 | function errorHandler (err, req, res, next) { 15 | let status = 500 16 | let message = err.message 17 | 18 | if (err.code === 'ENOENT') { 19 | let pathType = (path.extname(err.path) !== '' ? 'file' : 'folder') 20 | status = 404 21 | message = `The ${pathType} ${req.path} was not found. Maybe you didn't create it yet?` 22 | } 23 | 24 | if (err.code === 'ENOINDEX') { 25 | status = 404 26 | message = `The folder ${req.path} doesn't have an index file. Try creating an index.js file in this folder.` 27 | } 28 | 29 | let template = pageTemplate({ 30 | status: status, 31 | message: message 32 | }) 33 | 34 | return res.status(status).send(template) 35 | } 36 | 37 | module.exports = errorHandler 38 | -------------------------------------------------------------------------------- /lib/router.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const express = require('express') 3 | 4 | let bundleFileRoute = require('./routes/bundle-file') 5 | let scriptFileRoute = require('./routes/script-file') 6 | let htmlFileRoute = require('./routes/html-file') 7 | let directoryRoute = require('./routes/directory') 8 | let defaultIndexRoute = require('./routes/default-index') 9 | 10 | function createRouter (handlers) { 11 | let router = express.Router() 12 | 13 | router.get('/__bundle/:file.bundle.js', bundleFileRoute) 14 | 15 | handlers.forEach(handler => { 16 | router.get(handler.extension, scriptFileRoute) 17 | }) 18 | 19 | router.get('**/*.html', htmlFileRoute) 20 | 21 | router.get('*', directoryRoute) 22 | 23 | router.get('/', defaultIndexRoute) 24 | 25 | return router 26 | } 27 | 28 | module.exports = createRouter 29 | -------------------------------------------------------------------------------- /lib/routes/bundle-file.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | /** 3 | * run-js - A prototyping server that just works. 4 | * 5 | * @author Zach Bruggeman 6 | */ 7 | 8 | const path = require('path') 9 | const levelup = require('levelup') 10 | const md5 = require('md5-file') 11 | 12 | let generateBundle = require('../generate-bundle') 13 | let db = levelup('/run-js', { db: require('memdown') }) 14 | 15 | function bundleFileRoute (req, res, next) { 16 | let now = Date.now() 17 | let opts = req.app.locals.opts 18 | let instance = req.app.locals.instance 19 | let fileName = req.params.file.replace(/-/g, '/') 20 | let filePath = path.join(opts.dir, fileName) 21 | 22 | md5(filePath, (err, hash) => { 23 | if (err) return next(err) 24 | 25 | let dbPath = `${filePath}.${hash}` 26 | 27 | db.get(dbPath, (err, script) => { 28 | if (err || !script) { 29 | let fileBase = path.basename(filePath) 30 | let handler = opts.handlers.find(h => h.extension.test(fileBase)) 31 | let bundleOpts = { 32 | file: filePath, 33 | handler: handler, 34 | transforms: opts.transforms, 35 | plugin: opts.plugins 36 | } 37 | 38 | generateBundle(bundleOpts, (err, script) => { 39 | if (err) { 40 | instance.emit('bundle:error', { 41 | file: fileName, 42 | message: err.originalMessage 43 | }) 44 | } else { 45 | let elapsed = Date.now() - now 46 | instance.emit('bundle', { 47 | file: fileName, 48 | size: script.length, 49 | bundleTime: elapsed, 50 | cached: false 51 | }) 52 | db.put(dbPath, script) 53 | } 54 | 55 | res.set('Content-Type', 'application/javascript') 56 | res.send(script) 57 | }) 58 | } else { 59 | let elapsed = Date.now() - now 60 | instance.emit('bundle', { 61 | file: fileName, 62 | size: script.length, 63 | bundleTime: elapsed, 64 | cached: true 65 | }) 66 | 67 | res.set('Content-Type', 'application/javascript') 68 | res.send(script) 69 | } 70 | }) 71 | }) 72 | } 73 | 74 | module.exports = bundleFileRoute 75 | -------------------------------------------------------------------------------- /lib/routes/default-index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | /** 3 | * run-js - A prototyping server that just works. 4 | * 5 | * @author Zach Bruggeman 6 | */ 7 | 8 | const path = require('path') 9 | 10 | function defaultIndexRoute (req, res, next) { 11 | return res.sendFile(path.resolve(__dirname, '../template/index.html')) 12 | } 13 | 14 | module.exports = defaultIndexRoute 15 | -------------------------------------------------------------------------------- /lib/routes/directory.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | /** 3 | * run-js - A prototyping server that just works. 4 | * 5 | * @author Zach Bruggeman 6 | */ 7 | 8 | const fs = require('fs') 9 | const path = require('path') 10 | const _ = require('lodash') 11 | 12 | let htmlFileRoute = require('./html-file') 13 | let pageTemplate = _.template(fs.readFileSync(path.join(__dirname, '../template/template.html'))) 14 | 15 | function directoryRoute (req, res, next) { 16 | let filePath = path.join(req.app.locals.opts.dir, req.path) 17 | 18 | fs.stat(filePath, (err, stat) => { 19 | if (err) return next(err) 20 | if (!stat.isDirectory()) return next() 21 | 22 | fs.readdir(filePath, (err, files) => { 23 | if (err) return next(err) 24 | 25 | let indexFile = files.find(file => file.indexOf('index') === 0) 26 | if (!indexFile && req.path === '/') return next() // we'll show the default page 27 | if (!indexFile) { 28 | let err = new Error() 29 | err.code = 'ENOINDEX' 30 | return next(err) 31 | } 32 | 33 | if (indexFile === 'index.html') { 34 | return htmlFileRoute(req, res, next) 35 | } 36 | 37 | let handlers = req.app.locals.opts.handlers 38 | let handler = handlers.find(h => h.extension.test(indexFile)) 39 | if (!handler) return next() 40 | 41 | let compiled = pageTemplate({ 42 | reqPath: path.normalize(`${req.path}/${indexFile}`), 43 | watch: req.app.locals.opts.watch 44 | }) 45 | res.send(compiled) 46 | }) 47 | }) 48 | } 49 | 50 | module.exports = directoryRoute 51 | -------------------------------------------------------------------------------- /lib/routes/html-file.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | /** 3 | * run-js - A prototyping server that just works. 4 | * 5 | * @author Zach Bruggeman 6 | */ 7 | 8 | const fs = require('fs') 9 | const path = require('path') 10 | const cheerio = require('cheerio') 11 | 12 | function htmlFileRoute (req, res, next) { 13 | let filePath = path.join(req.app.locals.opts.dir, req.path) 14 | 15 | if (path.extname(filePath) !== '.html') { 16 | // sent from the directory route, since it found an index.html 17 | filePath = filePath + '/index.html' 18 | } 19 | 20 | fs.readFile(filePath, (err, buf) => { 21 | if (err) return next(err) 22 | 23 | fs.readdir(path.dirname(filePath), (err, files) => { 24 | if (err) return next(err) 25 | 26 | let baseName = path.basename(filePath, '.html') 27 | let fileName = files.find(file => file.indexOf(baseName) === 0 && !file.includes('.html')) 28 | if (fileName) { 29 | let bundleName = path.normalize(`${path.dirname(req.path)}/${fileName}`).replace(/\/|\\/g, '-') 30 | let $ = cheerio.load(buf.toString()) 31 | $('body').append(``) 32 | if (req.app.locals.opts.watch) { 33 | $('body').append('') 34 | } 35 | res.send($.html()) 36 | } else { 37 | res.send(buf.toString()) 38 | } 39 | }) 40 | }) 41 | } 42 | 43 | module.exports = htmlFileRoute 44 | -------------------------------------------------------------------------------- /lib/routes/script-file.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | /** 3 | * run-js - A prototyping server that just works. 4 | * 5 | * @author Zach Bruggeman 6 | */ 7 | 8 | const fs = require('fs') 9 | const path = require('path') 10 | const _ = require('lodash') 11 | 12 | let pageTemplate = _.template(fs.readFileSync(path.join(__dirname, '../template/template.html'))) 13 | 14 | function scriptFileRoute (req, res, next) { 15 | if (req.query.raw === '') return next() 16 | 17 | let filePath = path.join(req.app.locals.opts.dir, req.path) 18 | fs.stat(filePath, err => { 19 | if (err) return next(err) 20 | 21 | if (!req.headers.accept.includes('text/html')) return next() 22 | 23 | let compiled = pageTemplate({ 24 | reqPath: req.path, 25 | watch: req.app.locals.opts.watch 26 | }) 27 | res.send(compiled) 28 | }) 29 | } 30 | 31 | module.exports = scriptFileRoute 32 | -------------------------------------------------------------------------------- /lib/template/bundle-error.html: -------------------------------------------------------------------------------- 1 | 30 | 31 |
32 |
33 |

<%= name %>

34 | 35 |
<%= message %>
36 |
37 |
38 | -------------------------------------------------------------------------------- /lib/template/error.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | run-js – <%= status %> 6 | 18 | 19 | 20 |
21 |

Error: <%= status %>

22 |

23 | <%= message %> 24 |

25 |
26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /lib/template/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | run-js – Welcome 6 | 22 | 23 | 24 |
25 |

Welcome to run-js!

26 |

27 | run-js is the prototyping server that just works. Just put a JavaScript (including ES2015 and JSX), CoffeeScript, or TypeScript file in this directory, and browse to it. Have fun! 28 |

29 |

30 | (You can replace this page by making an index file, such as index.js, or whatever language you write in. index.html will work too.) 31 |

32 | 33 | 34 | 35 | 36 |
37 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /lib/template/template.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | run-js – <%= reqPath %> 6 | 7 | 8 | 9 |
10 | 11 | <% if (watch) { %> 12 | <% } %> 13 | 14 | 15 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "run-js", 3 | "version": "2.1.1", 4 | "description": "A prototyping server that just works.", 5 | "main": "lib/index.js", 6 | "bin": { 7 | "run-js": "./bin/run-js" 8 | }, 9 | "scripts": { 10 | "test": "standard && NODE_ENV=test nyc tap test/*.js", 11 | "travis-coveralls": "npm test && nyc report --reporter=text-lcov | coveralls" 12 | }, 13 | "repository": { 14 | "type": "git", 15 | "url": "git+https://github.com/remixz/run-js.git" 16 | }, 17 | "author": "Zach Bruggeman ", 18 | "license": "MIT", 19 | "bugs": { 20 | "url": "https://github.com/remixz/run-js/issues" 21 | }, 22 | "homepage": "https://github.com/remixz/run-js#readme", 23 | "dependencies": { 24 | "ansi_up": "^1.3.0", 25 | "babel-preset-es2015": "^6.3.13", 26 | "babel-preset-react": "^6.3.13", 27 | "babelify": "^7.2.0", 28 | "bole": "^2.0.0", 29 | "brfs": "^1.4.2", 30 | "browserify": "^12.0.1", 31 | "cheerio": "^0.19.0", 32 | "chokidar": "^1.4.2", 33 | "coffeeify": "^2.0.1", 34 | "concat-stream": "^1.5.1", 35 | "escape-html": "^1.0.3", 36 | "express": "^4.13.3", 37 | "garnish": "^5.0.1", 38 | "installify": "^1.0.2", 39 | "levelup": "^1.3.1", 40 | "lodash": "^3.10.1", 41 | "md5-file": "^2.0.4", 42 | "memdown": "^1.1.0", 43 | "mkdirp": "^0.5.1", 44 | "opn": "^3.0.3", 45 | "pretty-bytes": "^3.0.0", 46 | "stdout-stream": "^1.4.0", 47 | "tiny-lr": "^0.2.1", 48 | "tsify": "^0.13.1", 49 | "yargs": "^3.31.0" 50 | }, 51 | "devDependencies": { 52 | "async": "^1.5.0", 53 | "coveralls": "^2.11.6", 54 | "nyc": "^5.1.0", 55 | "request": "^2.67.0", 56 | "rimraf": "^2.5.0", 57 | "standard": "^5.4.1", 58 | "tap": "^3.0.0" 59 | }, 60 | "standard": { 61 | "ignore": [ 62 | "test/scenarios/**" 63 | ] 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /test/basic.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const tap = require('tap') 3 | 4 | let RunJS = require('../lib') 5 | 6 | const appOpts = { 7 | port: 9999, 8 | watch: false 9 | } 10 | 11 | tap.test('basic test – initialization', t => { 12 | let app = new RunJS(appOpts) 13 | 14 | t.type(app.start, 'function', 'app.start is a function') 15 | t.type(app.stop, 'function', 'app.stop is a function') 16 | t.type(app.opts, 'object', 'app.opts is an object') 17 | t.end() 18 | }) 19 | 20 | tap.test('basic test – stopping server before starting', t => { 21 | let app = new RunJS(appOpts) 22 | 23 | app.stop(err => { 24 | t.type(err, Error, 'app instance should return error server is stopped when it\'s not running') 25 | t.end() 26 | }) 27 | }) 28 | -------------------------------------------------------------------------------- /test/runners/default-index-page.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const fs = require('fs') 3 | const path = require('path') 4 | const request = require('request') 5 | 6 | let defaultIndex = fs.readFileSync(path.join(__dirname, '../../lib/template/index.html')).toString() 7 | 8 | function defaultIndexPageRunner (testInfo, expected, t, cb) { 9 | request(`http://localhost:9999`, (err, res, body) => { 10 | t.error(err, 'request should complete') 11 | t.equals(res.statusCode, 200, `page status code should be 200`) 12 | t.equals(body, defaultIndex, 'page output should be correct') 13 | 14 | cb() 15 | }) 16 | } 17 | 18 | module.exports = defaultIndexPageRunner 19 | -------------------------------------------------------------------------------- /test/runners/error-page.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const fs = require('fs') 3 | const path = require('path') 4 | const request = require('request') 5 | const _ = require('lodash') 6 | 7 | let pageTemplate = _.template(fs.readFileSync(path.join(__dirname, '../../lib/template/error.html'))) 8 | 9 | function errorPageRunner (status, message) { 10 | return function (testInfo, expected, t, cb) { 11 | request(`http://localhost:9999${testInfo.url}`, (err, res, body) => { 12 | t.error(err, 'request should complete') 13 | 14 | t.equals(res.statusCode, status, `should have expected ${status} error code`) 15 | 16 | let expectedPage = pageTemplate({ 17 | status: status, 18 | message: message 19 | }) 20 | 21 | t.equals(body, expectedPage, 'page output should be correct') 22 | 23 | cb() 24 | }) 25 | } 26 | } 27 | 28 | module.exports = errorPageRunner 29 | -------------------------------------------------------------------------------- /test/runners/html-file-page.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const request = require('request') 3 | const async = require('async') 4 | 5 | function htmlFilePageRunner (testInfo, expected, t, cb) { 6 | let url = testInfo.url 7 | if (testInfo.bundleUrl) { 8 | url = testInfo.bundleUrl 9 | } 10 | let bundlePath = `/__bundle/${url.replace(/\//g, '-').split('.html')[0]}.js.bundle.js` 11 | 12 | async.series([ 13 | function getPage (cb) { 14 | request({ 15 | uri: `http://localhost:9999${url}`, 16 | headers: { 17 | Accept: 'text/html' 18 | } 19 | }, (err, res, body) => { 20 | t.error(err, 'page request shouldn\'t fail') 21 | t.equals(res.statusCode, 200, 'page status code should be 200') 22 | 23 | t.equals(body, testInfo.expectedHtml, 'page output should be correct') 24 | cb() 25 | }) 26 | }, 27 | function getBundle (cb) { 28 | if (testInfo.noExpected) return cb() 29 | request(`http://localhost:9999${bundlePath}`, (err, res, body) => { 30 | t.error(err, 'bundle request shouldn\'t fail') 31 | t.equals(res.statusCode, 200, 'bundle status code should be 200') 32 | 33 | // test for sourcemap, then remove for script equality check, since sourcemap can differ between environments 34 | t.ok(body.indexOf('//# sourceMappingURL') > -1, 'inline sourcemap should exist') 35 | body = body.split('//# sourceMappingURL')[0] 36 | 37 | t.equals(body, expected.split('//# sourceMappingURL')[0], 'bundle output should be correct') 38 | cb() 39 | }) 40 | } 41 | ], cb) 42 | } 43 | 44 | module.exports = htmlFilePageRunner 45 | -------------------------------------------------------------------------------- /test/runners/standard-page.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const fs = require('fs') 3 | const path = require('path') 4 | const request = require('request') 5 | const async = require('async') 6 | const _ = require('lodash') 7 | 8 | let pageTemplate = _.template(fs.readFileSync(path.join(__dirname, '../../lib/template/template.html'))) 9 | 10 | function standardPageRunner (testInfo, expected, t, cb) { 11 | let url = testInfo.url 12 | if (testInfo.bundleUrl) { 13 | url = testInfo.bundleUrl 14 | } 15 | let bundlePath = `/__bundle/${url.replace(/\//g, '-')}.bundle.js` 16 | let didCache = false 17 | 18 | async.series([ 19 | function getPage (cb) { 20 | request({ 21 | uri: `http://localhost:9999${testInfo.url}`, 22 | headers: { 23 | Accept: 'text/html' 24 | } 25 | }, (err, res, body) => { 26 | t.error(err, 'page request shouldn\'t fail') 27 | t.equals(res.statusCode, 200, 'page status code should be 200') 28 | 29 | let expectedPage = pageTemplate({ 30 | reqPath: url, 31 | watch: false 32 | }) 33 | 34 | t.equals(body, expectedPage, 'page output should be correct') 35 | cb() 36 | }) 37 | }, 38 | function getBundle (cb) { 39 | request(`http://localhost:9999${bundlePath}`, (err, res, body) => { 40 | t.error(err, 'bundle request shouldn\'t fail') 41 | t.equals(res.statusCode, 200, 'bundle status code should be 200') 42 | 43 | if (!testInfo.noSourceMap) { 44 | // test for sourcemap, then remove for script equality check, since sourcemap can differ between environments 45 | t.ok(body.indexOf('//# sourceMappingURL') > -1, 'inline sourcemap should exist') 46 | body = body.split('//# sourceMappingURL')[0] 47 | expected = expected.split('//# sourceMappingURL')[0] 48 | } 49 | 50 | if (testInfo.match) { 51 | t.match(body, testInfo.match, 'bundle output should be correct') 52 | } else { 53 | t.equals(body, expected, 'bundle output should be correct') 54 | } 55 | if (testInfo.cache && !didCache) { 56 | t.comment('running test again, for cached data') 57 | didCache = true 58 | getBundle(cb) 59 | } else { 60 | cb() 61 | } 62 | }) 63 | } 64 | ], cb) 65 | } 66 | 67 | module.exports = standardPageRunner 68 | -------------------------------------------------------------------------------- /test/scenarios.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const fs = require('fs') 3 | const path = require('path') 4 | const tap = require('tap') 5 | const async = require('async') 6 | 7 | let RunJS = require('../lib') 8 | 9 | fs.readdir(path.resolve(__dirname, './scenarios'), (err, tests) => { 10 | if (err) throw err 11 | 12 | async.eachSeries(tests, (testName, cb) => { 13 | let testDirectory = path.resolve(__dirname, './scenarios', testName) 14 | let testInput = path.join(testDirectory, 'input') 15 | tap.test(`scenario - ${testName}`, t => { 16 | let testInfo = require(testDirectory) 17 | let expected = '' 18 | if (!testInfo.noExpected) { 19 | expected = fs.readFileSync(path.join(testDirectory, 'expected/bundle.js')).toString() 20 | } 21 | 22 | let app = new RunJS({ 23 | dir: testInfo.dir || testInput, 24 | watch: testInfo.watch || false, 25 | port: 9999, 26 | handlers: require('../lib/default-handlers'), 27 | transforms: require('../lib/default-transforms') 28 | }) 29 | 30 | app.start(err => { 31 | t.error(err, 'server started up successfully') 32 | testInfo.runner(testInfo, expected, t, (err) => { 33 | t.error(err, 'no errors from test runner') 34 | t.end() 35 | app.stop(cb) 36 | }) 37 | }) 38 | }) 39 | }) 40 | }) 41 | -------------------------------------------------------------------------------- /test/scenarios/bundle-error/expected/bundle.js: -------------------------------------------------------------------------------- 1 | ;(function bundleError () { 2 | var template = "\n\n
\n
\n

SyntaxError

\n\n
/Users/zach/run-js/foo.js: Unterminated string constant (1:10) while parsing file: /Users/zach/run-js/foo.js\n\n> 1 | var foo = 'bar\n    |           ^\n  2 | 
\n
\n
\n" 3 | if (typeof document === 'undefined') return 4 | document.addEventListener('DOMContentLoaded', function print () { 5 | var container = document.createElement('div') 6 | container.innerHTML = template 7 | document.body.appendChild(container) 8 | }) 9 | })() 10 | -------------------------------------------------------------------------------- /test/scenarios/bundle-error/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | url: '/foo.js', 3 | runner: require('../../runners/standard-page'), 4 | noSourceMap: true, 5 | match: 'function bundleError ()' 6 | } 7 | -------------------------------------------------------------------------------- /test/scenarios/bundle-error/input/foo.js: -------------------------------------------------------------------------------- 1 | var foo = 'bar 2 | -------------------------------------------------------------------------------- /test/scenarios/default-index/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | runner: require('../../runners/default-index-page'), 3 | noExpected: true, 4 | dir: process.cwd() 5 | } 6 | -------------------------------------------------------------------------------- /test/scenarios/error-404/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | url: '/404', 3 | runner: require('../../runners/error-page')(404, 'The folder /404 was not found. Maybe you didn\'t create it yet?'), 4 | noExpected: true 5 | } 6 | -------------------------------------------------------------------------------- /test/scenarios/html-file-no-script/expected/page.html: -------------------------------------------------------------------------------- 1 | 2 | html file 3 | 4 | -------------------------------------------------------------------------------- /test/scenarios/html-file-no-script/index.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | const path = require('path') 3 | 4 | module.exports = { 5 | url: '/test.html', 6 | runner: require('../../runners/html-file-page'), 7 | expectedHtml: fs.readFileSync(path.join(__dirname, 'expected', 'page.html')).toString(), 8 | noExpected: true 9 | } 10 | -------------------------------------------------------------------------------- /test/scenarios/html-file-no-script/input/test.html: -------------------------------------------------------------------------------- 1 | 2 | html file 3 | 4 | -------------------------------------------------------------------------------- /test/scenarios/html-file-with-script/expected/bundle.js: -------------------------------------------------------------------------------- 1 | (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o 2 | html file 3 | 4 | -------------------------------------------------------------------------------- /test/scenarios/html-file-with-script/index.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | const path = require('path') 3 | 4 | module.exports = { 5 | url: '/test.html', 6 | runner: require('../../runners/html-file-page'), 7 | expectedHtml: fs.readFileSync(path.join(__dirname, 'expected', 'page.html')).toString() 8 | } 9 | -------------------------------------------------------------------------------- /test/scenarios/html-file-with-script/input/test.html: -------------------------------------------------------------------------------- 1 | 2 | html file 3 | 4 | -------------------------------------------------------------------------------- /test/scenarios/html-file-with-script/input/test.js: -------------------------------------------------------------------------------- 1 | console.log('html file') 2 | -------------------------------------------------------------------------------- /test/scenarios/single-file-index/expected/bundle.js: -------------------------------------------------------------------------------- 1 | (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o 2 | index inside subdirectory 3 | 4 | -------------------------------------------------------------------------------- /test/scenarios/subdirectory-with-index-html/index.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | const path = require('path') 3 | 4 | module.exports = { 5 | url: '/foo', 6 | runner: require('../../runners/html-file-page'), 7 | expectedHtml: fs.readFileSync(path.join(__dirname, 'expected', 'page.html')).toString(), 8 | noExpected: true 9 | } 10 | -------------------------------------------------------------------------------- /test/scenarios/subdirectory-with-index-html/input/foo/index.html: -------------------------------------------------------------------------------- 1 | 2 | index inside subdirectory 3 | 4 | -------------------------------------------------------------------------------- /test/watch.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const fs = require('fs') 3 | const tap = require('tap') 4 | const request = require('request') 5 | const async = require('async') 6 | const mkdirp = require('mkdirp') 7 | const rimraf = require('rimraf') 8 | 9 | let RunJS = require('../lib') 10 | 11 | const appOpts = { 12 | dir: __dirname, 13 | watch: true, 14 | port: 9999 15 | } 16 | 17 | async.series([ 18 | cb => { 19 | tap.test('watch – server starts', t => { 20 | let app = new RunJS(appOpts) 21 | 22 | app.start(err => { 23 | t.error(err, 'server started up successfully') 24 | 25 | request({ 26 | uri: 'http://localhost:35729', 27 | json: true 28 | }, (err, res, body) => { 29 | t.error(err, 'request should complete') 30 | t.equals(res.statusCode, 200, 'request should have status code 200') 31 | 32 | t.equals(body.tinylr, 'Welcome', 'request body property `tinylr` should have expected response') 33 | t.end() 34 | app.stop(cb) 35 | }) 36 | }) 37 | }) 38 | }, 39 | 40 | cb => { 41 | tap.test('watch – modifying files', t => { 42 | let app = new RunJS(appOpts) 43 | mkdirp.sync(__dirname + '/_test') 44 | 45 | app.start(err => { 46 | t.error(err, 'server started up successfully') 47 | 48 | app.on('watch:ready', () => { 49 | fs.writeFileSync(__dirname + '/_test/file.js') 50 | }) 51 | 52 | app.on('watch:all', (event, fp) => { 53 | t.equals(event, 'add', 'watch event should be correct') 54 | t.equals(fp, __dirname + '/_test/file.js', 'watch file path should be correct') 55 | app.stop(() => { 56 | rimraf(__dirname + '/_test', cb) 57 | }) 58 | t.end() 59 | }) 60 | 61 | app.on('watch:error', err => { 62 | t.bailout(err) 63 | }) 64 | }) 65 | }) 66 | } 67 | ]) 68 | --------------------------------------------------------------------------------