├── .babelrc ├── .gitignore ├── .npmignore ├── .travis.yml ├── LICENSE ├── README.md ├── babel-transform.js ├── examples ├── 01-basic-usage │ ├── .babelrc │ ├── README.md │ ├── package.json │ ├── server.js │ ├── site.js │ └── src │ │ ├── app.js │ │ ├── counter.js │ │ └── styles.js ├── 02-redux │ ├── .gitignore │ ├── LICENSE │ ├── README.md │ ├── package.json │ ├── public │ │ └── .gitkeep │ ├── scripts │ │ ├── build.js │ │ ├── dev.js │ │ ├── run-build.js │ │ ├── serve.js │ │ ├── server.js │ │ └── watch.js │ ├── site │ │ ├── favico.png │ │ ├── index.html │ │ └── index.js │ ├── src │ │ ├── actions │ │ │ ├── counter.js │ │ │ └── spec │ │ │ │ └── counter.test.js │ │ ├── components │ │ │ └── counter │ │ │ │ ├── index.js │ │ │ │ └── spec │ │ │ │ └── test.js │ │ ├── containers │ │ │ ├── app.js │ │ │ └── app │ │ │ │ └── spec │ │ │ │ └── test.js │ │ ├── reducers │ │ │ ├── counter.js │ │ │ ├── index.js │ │ │ └── spec │ │ │ │ └── counter.test.js │ │ ├── store │ │ │ └── configure-store.js │ │ └── test │ │ │ └── jsdom-react.js │ └── yarn.lock └── 03-build-systems │ ├── .babelrc │ ├── Gruntfile.js │ ├── README.md │ ├── gulpfile.js │ ├── package.json │ ├── server.js │ ├── site.js │ └── src │ └── app.js ├── index.js ├── package-lock.json ├── package.json ├── src ├── babel-transform │ └── main.js ├── browserify-plugin │ ├── console.js │ ├── main.js │ └── server.js └── reloading.js └── test ├── app ├── .babelrc ├── .gitignore ├── extra.js ├── package.json ├── server.js ├── site.js └── src │ ├── app.js │ ├── circular │ ├── first.js │ └── second.js │ ├── constants.js │ ├── counter.js │ ├── extra │ ├── body.js │ └── header.js │ ├── hooks.js │ └── hooks │ ├── accept.js │ └── noAccept.js ├── smokeTest.js └── utils.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015"] 3 | } 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .idea 3 | *.log 4 | npm-debug* 5 | node_modules 6 | lib 7 | .bundle 8 | bundle* 9 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | examples 2 | test 3 | !lib 4 | .idea -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "10" 4 | - "11" 5 | before_script: 6 | - npm install -g npm 7 | script: 8 | - cd test/app && npm i && cd ../.. 9 | - npm test 10 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) Matti Lankinen 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # LiveReactload 2 | 3 | Live code editing with Browserify and React. 4 | 5 | :exclamation: :exclamation: :exclamation: 6 | 7 | **ATTENTION! The upcoming 4.x version will be using the new [`react-hot-loader`](https://github.com/gaearon/react-hot-loader) 8 | and it is already available in npm as a beta tag. If you want to test it, check out the migration guide and 9 | installation instructions [here](https://github.com/milankinen/livereactload/tree/4.x)!** 10 | 11 | :exclamation: :exclamation: :exclamation: 12 | 13 | 14 | [![Gitter](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/milankinen/livereactload) 15 | [![npm version](https://badge.fury.io/js/livereactload.svg)](http://badge.fury.io/js/livereactload) 16 | [![Build Status](https://travis-ci.org/milankinen/livereactload.svg)](https://travis-ci.org/milankinen/livereactload) 17 | 18 | ## Motivation 19 | 20 | Hot reloading is de facto in today's front-end scene but unfortunately 21 | there isn't any decent implementation for Browserify yet. This is shame because 22 | (in my opinion) Browserify is the best bundling tool at the moment. 23 | 24 | Hence the goal of this project is to bring the hot reloading functionality 25 | to Browserify by honoring its principles: simplicity and modularity. 26 | 27 | 28 | ## How it works? 29 | 30 | LiveReactload can be used as a normal Browserify plugin. When applied to the bundle, 31 | it modifies the Browserify bundling pipeline so that the created bundle becomes 32 | "hot-reloadable". 33 | 34 | * LiveReactload starts the reloading server which watches the bundle changes 35 | and sends the changed contents to the browser via WebSocket. 36 | * When the changes arrive to the browser, LiveReactload client (included automatically 37 | in the bundle) analyzes the changes and reloads the changed modules 38 | 39 | Starting from version `2.0.0` LiveReactload utilizes [Dan Abramov](https://github.com/gaearon)'s 40 | [babel-plugin-react-transform](https://github.com/gaearon/babel-plugin-react-transform) and 41 | [react-proxy](https://github.com/gaearon/react-proxy), which means that hot-reloading 42 | capabilities are same as in Webpack. 43 | 44 | And because one photo tells more than a thousand words, watch [this video](https://vimeo.com/123513496) to see 45 | LiveReactload in action. 46 | 47 | ### Other implementations 48 | 49 | If you are a Webpack user, you probably want to check 50 | **[react-transform-boilerplate](https://github.com/gaearon/react-transform-boilerplate)**. 51 | 52 | If you want to stick with browserify, but use the Hot Module Reloading API (like webpack), you could use: **[browserify-hmr](https://github.com/AgentME/browserify-hmr)**, **[babel-plugin-react-transform](https://github.com/gaearon/babel-plugin-react-transform)** and 53 | **[react-transform-hmr](https://github.com/gaearon/react-transform-hmr)** 54 | 55 | 56 | ## Usage 57 | 58 | ### Pre-requirements 59 | 60 | LiveReactload requires `watchify`, `babelify` and `react >= 0.13.x` in order to 61 | work. 62 | 63 | ### Installation (Babel 6.x) 64 | 65 | Install pre-requirements (if not already exist) 66 | 67 | ```sh 68 | npm i --save react 69 | npm i --save-dev watchify 70 | ``` 71 | 72 | Install `babelify` and its dependencies 73 | 74 | ```sh 75 | npm i --save babelify babel-preset-es2015 babel-preset-react 76 | ``` 77 | 78 | Install React proxying components and LiveReactload 79 | 80 | ```sh 81 | npm i --save-dev livereactload react-proxy@1.x babel-plugin-react-transform 82 | ``` 83 | 84 | Create `.babelrc` file into project's root directory (or add `react-transform` extra 85 | if the file already exists). More information about `.babelrc` format and options 86 | can be found from [babel-plugin-react-transform](https://github.com/gaearon/babel-plugin-react-transform). 87 | 88 | ```javascript 89 | { 90 | "presets": ["es2015", "react"], 91 | "env": { 92 | "development": { 93 | "plugins": [ 94 | ["react-transform", { 95 | "transforms": [{ 96 | "transform": "livereactload/babel-transform", 97 | "imports": ["react"] 98 | }] 99 | }] 100 | ] 101 | } 102 | } 103 | } 104 | ``` 105 | 106 | And finally use LiveReactload as a Browserify plugin with `watchify`. For example: 107 | 108 | ```bash 109 | node_modules/.bin/watchify site.js -t babelify -p livereactload -o static/bundle.js 110 | ``` 111 | 112 | **That's it!** Now just start (live) coding! For more detailed example, please see 113 | **[the basic usage example](examples/01-basic-usage)**. 114 | 115 | ### Reacting to reload events 116 | 117 | Ideally your client code should be completely unaware of the reloading. However, 118 | some libraries like `redux` require a little hack for hot-reloading. That's why 119 | LiveReactload provides `module.onReload(..)` hook. 120 | 121 | By using this hook, you can add your own custom functionality that is 122 | executed in the browser only when the module reload occurs: 123 | 124 | ```javascript 125 | if (module.onReload) { 126 | module.onReload(() => { 127 | ... do something ... 128 | // returning true indicates that this module was updated correctly and 129 | // reloading should not propagate to the parent components (if non-true 130 | // value is returned, then parent module gets reloaded too) 131 | return true 132 | }); 133 | } 134 | ``` 135 | 136 | For more details, please see **[the redux example](examples/02-redux)**. 137 | 138 | ### How about build systems? 139 | 140 | LiveReactload is build system agnostic. It means that you can use LiveReactload with 141 | all build systems having Browserify and Watchify support. Please see 142 | **[build systems example](examples/03-build-systems)** for more information. 143 | 144 | 145 | ## When does it not work? 146 | 147 | Well... if you hide your state inside the modules then the reloading will lose 148 | the state. For example the following code will **not** work: 149 | 150 | ```javascript 151 | // counter.js 152 | const React = require('react') 153 | 154 | let totalClicks = 0 155 | 156 | export default React.createClass({ 157 | 158 | getInitialState() { 159 | return {clickCount: totalClicks} 160 | }, 161 | 162 | handleClick() { 163 | totalClicks += 1 164 | this.setState({clickCount: totalClicks}) 165 | }, 166 | 167 | 168 | render() { 169 | return ( 170 |
171 | 172 |
{this.state.clickCount}
173 |
174 | ) 175 | } 176 | }) 177 | ``` 178 | 179 | ## Configuration options 180 | 181 | You can configure the LiveReactload Browserify plugin by passing some options 182 | to it (`-p [ livereactload ]`, see Browserify docs for more information 183 | about config format). 184 | 185 | ### Available options 186 | 187 | LiveReactload supports the following configuration options 188 | 189 | #### `--no-server` 190 | 191 | Prevents reload server startup. If you are using LiveReactload plugin with Browserify 192 | (instead of watchify), you may want to enable this so that the process won't hang after 193 | bundling. This is not set by default. 194 | 195 | #### `--port ` 196 | 197 | Starts reload server to the given port and configures the bundle's client to 198 | connect to the server using this port. Default value is `4474` 199 | 200 | #### `--host ` 201 | 202 | Configures the reload client to use the given hostname when connecting to the 203 | reload server. You may need this if you are running the bundle in an another device. 204 | Default value is `localhost` 205 | 206 | #### `--no-dedupe` 207 | 208 | Disables Browserify module de-duplication. By default, de-duplication is enabled. 209 | However, sometimes this de-duplication with may cause an invalid bundle with LiveReactload. 210 | You can disable this de-duplication by using this flag. 211 | 212 | #### `--no-client` 213 | 214 | Omits the reload client from the generated bundle. 215 | 216 | #### `--ssl-cert ` and `--ssl-key ` 217 | 218 | Adds your custom SSL certificate and key to the reload web-server. This is needed if you 219 | want to use LiveReactLoad in HTTPS site. Parameters are paths to the actual files. 220 | 221 | #### `--no-babel` 222 | 223 | If you use a tool other than Babel to transform React syntax, this disables the in-browser warning that would otherwise appear. 224 | 225 | #### `--moduledir ` 226 | 227 | Directory pointing node modules where `livereactload` is installed. By default points to `/node_modules`. 228 | 229 | ## License 230 | 231 | MIT 232 | 233 | 234 | ## Contributing 235 | 236 | Please create a [Github issue](/../../issues) if problems occur. Pull request are also welcome 237 | and they can be created to the `development` branch. 238 | -------------------------------------------------------------------------------- /babel-transform.js: -------------------------------------------------------------------------------- 1 | 2 | module.exports = require("./lib/babel-transform/main.js") 3 | -------------------------------------------------------------------------------- /examples/01-basic-usage/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015", "react"], 3 | "env": { 4 | "development": { 5 | "plugins": [ 6 | ["react-transform", { 7 | "transforms": [{ 8 | "transform": "livereactload/babel-transform", 9 | "imports": ["react"] 10 | }] 11 | }] 12 | ] 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /examples/01-basic-usage/README.md: -------------------------------------------------------------------------------- 1 | # Basic LiveReactload 2.x usage 2 | 3 | This example demonstrates the minimal live reloading configuration. 4 | 5 | * `package.json` contains `watchify` script and `bundle:prod` for production bundle creation 6 | * `.babelrc` contains Babel transformation setup for development environment 7 | * Application code is in `src` folder 8 | 9 | You can run this example by typing 10 | 11 | npm i && npm run watch 12 | open http://localhost:3000 # OS X only 13 | 14 | After the server is started, you can edit source files from `src` and 15 | changes should be reloaded as they occur. 16 | 17 | And extra: **universal server side rendering** in `server.js` 18 | -------------------------------------------------------------------------------- /examples/01-basic-usage/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "livereactload-basic-usage-example", 3 | "version": "2.0.0", 4 | "author": "Matti Lankinen (https://github.com/milankinen)", 5 | "license": "MIT", 6 | "scripts": { 7 | "start": "babel-node server.js", 8 | "bundle:prod": "NODE_ENV=production browserify site.js -t babelify -g envify -g uglifyify > bundle.js", 9 | "watch": "npm run watch:server & npm run watch:bundle & wait", 10 | "watch:server": "nodemon --exec babel-node --ignore bundle.js -- server.js", 11 | "watch:bundle": "watchify site.js -v -t babelify -g envify -p livereactload -o bundle.js" 12 | }, 13 | "dependencies": { 14 | "babel-cli": "^6.10.1", 15 | "babel-preset-es2015": "^6.9.0", 16 | "babel-preset-react": "^6.11.1", 17 | "babelify": "^7.3.0", 18 | "browserify": "^13.0.1", 19 | "envify": "^3.4.1", 20 | "express": "^4.14.0", 21 | "react": "^15.2.1", 22 | "react-dom": "^15.2.1", 23 | "uglifyify": "^3.0.2" 24 | }, 25 | "devDependencies": { 26 | "babel-plugin-react-transform": "^2.0.2", 27 | "livereactload": "latest", 28 | "nodemon": "^1.9.2", 29 | "react-proxy": "^1.1.8", 30 | "watchify": "^3.7.0" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /examples/01-basic-usage/server.js: -------------------------------------------------------------------------------- 1 | import express from "express" 2 | import React from "react" 3 | import {renderToString} from "react-dom/server" 4 | import Application from "./src/app" 5 | 6 | const app = express() 7 | 8 | app.get("/", (req, res) => { 9 | const model = { 10 | counter1: 10000, 11 | counter2: -100000 12 | } 13 | res.send(` 14 | 15 | 16 | 2.x basic usage 17 | 18 | 19 |
${renderToString()}
20 | 23 | 24 | 25 | `) 26 | }) 27 | 28 | app.get("/static/bundle.js", function(req, res) { 29 | res.sendFile("bundle.js", {root: __dirname}) 30 | }) 31 | 32 | app.listen(3000) 33 | -------------------------------------------------------------------------------- /examples/01-basic-usage/site.js: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import {render} from "react-dom" 3 | import App from "./src/app" 4 | 5 | const initialModel = window.INITIAL_MODEL 6 | 7 | render(, document.getElementById("app")) 8 | -------------------------------------------------------------------------------- /examples/01-basic-usage/src/app.js: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import Counter from "./counter" 3 | import {reddish} from "./styles" 4 | 5 | const {Component} = React 6 | 7 | export default class App extends Component { 8 | render() { 9 | const {counter1, counter2} = this.props // see site.js and server.js 10 | return ( 11 |
12 |
13 | 17 | 21 |
22 |
23 | ) 24 | } 25 | } 26 | 27 | // LiveReactload supports also non-exported "inner" classes! 28 | class Header extends Component { 29 | render() { 30 | return ( 31 |

Tsers!!!

32 | ) 33 | } 34 | } 35 | 36 | // as well as old React.createClass({...}) syntax! 37 | const Footer = React.createClass({ 38 | render() { 39 | return ( 40 |
41 |

---

42 |

Try to change the code on-the-fly!

43 |
44 | ) 45 | } 46 | }) 47 | -------------------------------------------------------------------------------- /examples/01-basic-usage/src/counter.js: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import {blueish} from "./styles" 3 | 4 | 5 | export default class Counter extends React.Component { 6 | constructor(props) { 7 | super(props) 8 | this.state = {value: props.initialValue || 0} 9 | } 10 | 11 | 12 | componentDidMount() { 13 | const self = this 14 | tick() 15 | 16 | function tick() { 17 | const {step, interval} = self.props 18 | self.setState({ 19 | value: self.state.value + step, 20 | interval: setTimeout(tick, interval) 21 | }) 22 | } 23 | } 24 | 25 | componentWillUnmount() { 26 | clearTimeout(this.state.interval) 27 | } 28 | 29 | render() { 30 | return ( 31 |

{this.state.value}

32 | ) 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /examples/01-basic-usage/src/styles.js: -------------------------------------------------------------------------------- 1 | 2 | export const blueish = { 3 | "color": "blue" 4 | } 5 | 6 | export const reddish = { 7 | "color": "red" 8 | } 9 | -------------------------------------------------------------------------------- /examples/02-redux/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Runtime data 7 | pids 8 | *.pid 9 | *.seed 10 | 11 | # Directory for instrumented libs generated by jscoverage/JSCover 12 | lib-cov 13 | 14 | # Coverage directory used by tools like istanbul 15 | coverage 16 | 17 | # nyc test coverage 18 | .nyc_output 19 | 20 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 21 | .grunt 22 | 23 | # node-waf configuration 24 | .lock-wscript 25 | 26 | # Compiled binary addons (http://nodejs.org/api/addons.html) 27 | build/Release 28 | 29 | # Dependency directories 30 | node_modules 31 | jspm_packages 32 | 33 | # Optional npm cache directory 34 | .npm 35 | 36 | # Optional REPL history 37 | .node_repl_history 38 | 39 | #generated 40 | lib 41 | public/* 42 | !public/.gitkeep -------------------------------------------------------------------------------- /examples/02-redux/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 Gregor Adams 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /examples/02-redux/README.md: -------------------------------------------------------------------------------- 1 | # Flux with redux 2 | 3 | LiveReactload supports all Flux implementations that are hot module reloadable, e.g. redux and ffux. 4 | 5 | This example is copied from redux counter example and modified to use Browserify and LiveReactload instead of Webpack. A more complete version of this can be found here [redux-react-boilerplate](https://github.com/pixelass/redux-react-boilerplate). It includes css-modules, doc generation, browsersync and other features. 6 | 7 | - [Developing](#developing) 8 | * [Examples](#examples) 9 | - [What's included?](#whats-included) 10 | * [Libraries](#libraries) 11 | * [Transforms](#transforms) 12 | * [Coding style](#coding-style) 13 | * [Testing](#testing) 14 | * [Livereload](#livereload) 15 | 16 | ## Developing 17 | 18 | To start a dev server and start developing try the following commands 19 | 20 | * `start`: starts the dev server and builds the required files 21 | * `test`: runs test and lints files 22 | * `run dev`: starts the dev server and watches the required files 23 | * `run babel`: generates lib from source 24 | * `run build`: builds all files from source 25 | * `run watch`: builds and watches all files from source 26 | * `run lint`: lints javascript files 27 | 28 | ### Examples 29 | 30 | **Starts a simple http-server** 31 | 32 | ``` 33 | npm start 34 | ``` 35 | 36 | **Starts a simple http-server and watches files** 37 | 38 | ``` 39 | npm run dev 40 | ``` 41 | 42 | ## What's included? 43 | 44 | ### Libraries 45 | 46 | * [Redux](http://redux.js.org/) 47 | * [React](https://facebook.github.io/react/) 48 | 49 | 50 | ### Transforms 51 | 52 | * [Babel](http://babeljs.io/) 53 | * [stage 0](http://babeljs.io/docs/plugins/preset-stage-0/) 54 | * [es2015](http://babeljs.io/docs/plugins/preset-es2015/) 55 | * [react](http://babeljs.io/docs/plugins/preset-react/) 56 | * [Browserify](http://browserify.org/) 57 | 58 | You can change the rules inside the `package.json` file. 59 | 60 | * `babel`: `{}` 61 | * `browserify`: `{}` 62 | 63 | **defaults** (development settings needed for livereload) 64 | 65 | ```json 66 | { 67 | "babel": { 68 | "presets": [ 69 | "es2015", 70 | "stage-0", 71 | "react" 72 | ], 73 | "env": { 74 | "development": { 75 | "sourceMaps": "inline", 76 | "plugins": [ 77 | [ 78 | "react-transform", 79 | { 80 | "transforms": [ 81 | { 82 | "transform": "livereactload/babel-transform", 83 | "imports": [ 84 | "react" 85 | ] 86 | } 87 | ] 88 | } 89 | ] 90 | ] 91 | } 92 | } 93 | }, 94 | "browserify": { 95 | "transform": [ 96 | "babelify" 97 | ] 98 | } 99 | } 100 | ``` 101 | 102 | ### Coding style 103 | 104 | * JS: [xo](https://github.com/sindresorhus/xo) 105 | 106 | You can change the rules inside the `package.json` file. 107 | 108 | * `xo`: `{}` 109 | 110 | **defaults** 111 | 112 | ```json 113 | { 114 | "xo": { 115 | "space": true, 116 | "semicolon": true 117 | } 118 | } 119 | ``` 120 | 121 | ### Testing 122 | 123 | * [Ava](https://github.com/avajs/ava/) 124 | * [Sinon](http://sinonjs.org/) 125 | * [Coveralls](https://coveralls.io) 126 | * [nyc]() 127 | 128 | You can change the rules inside the `package.json` file. 129 | 130 | * `ava`: `{}` 131 | * `nyc`: `{}` 132 | 133 | **defaults** 134 | 135 | ```json 136 | { 137 | "ava": { 138 | "files": [ 139 | "src/**/spec/*.js" 140 | ], 141 | "source": [ 142 | "src/**/*.js" 143 | ], 144 | "presets": [ 145 | "@ava/stage-4", 146 | "@ava/transform-test-files" 147 | ], 148 | "failFast": true, 149 | "tap": true, 150 | "require": [ 151 | "babel-register" 152 | ], 153 | "babel": "inherit" 154 | }, 155 | "nyc": { 156 | "exclude": [ 157 | "src/store/**/*.js" 158 | ] 159 | } 160 | } 161 | ``` 162 | 163 | ### Livereload 164 | 165 | * [livereactload](https://github.com/milankinen/livereactload/) 166 | -------------------------------------------------------------------------------- /examples/02-redux/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "02-redux", 3 | "version": "1.0.0", 4 | "description": "redux example for livereload", 5 | "author": "Gregor Adams ", 6 | "keywords": [ 7 | "react", 8 | "redux", 9 | "react-redux", 10 | "boilerplate" 11 | ], 12 | "license": "MIT", 13 | "devPort": 3000, 14 | "main": "scripts/server.js", 15 | "scripts": { 16 | "build": "NODE_ENV=production node ./scripts/build", 17 | "browser-sync": "browser-sync start -s 'public' -f 'public/main.css'", 18 | "coveralls": "nyc report --reporter=text-lcov | coveralls", 19 | "report": "nyc report --reporter=html", 20 | "lint": "npm run eslint", 21 | "eslint": "xo {scripts,site,src}/**/*.js", 22 | "posttest": "npm run lint", 23 | "server": "node ./scripts/server", 24 | "dev": "node ./scripts/dev", 25 | "start": "npm run build && npm run server", 26 | "test": "nyc ava", 27 | "watch": "node ./scripts/watch" 28 | }, 29 | "dependencies": { 30 | "react": "^15.4.2", 31 | "react-redux": "^5.0.3", 32 | "redux": "^3.6.0", 33 | "redux-thunk": "^2.2.0" 34 | }, 35 | "peerDependencies": {}, 36 | "devDependencies": { 37 | "ava": "^0.18.2", 38 | "babel-cli": "^6.23.0", 39 | "babel-plugin-react-transform": "^2.0.2", 40 | "babel-preset-es2015": "^6.22.0", 41 | "babel-preset-react": "^6.23.0", 42 | "babel-preset-stage-0": "^6.22.0", 43 | "babelify": "^7.3.0", 44 | "browserify": "^14.1.0", 45 | "coveralls": "^2.11.16", 46 | "errorify": "^0.3.1", 47 | "eslint": "^3.17.0", 48 | "eslint-config-xo-react": "^0.10.0", 49 | "eslint-plugin-react": "^6.10.0", 50 | "fbjs": "^0.8.9", 51 | "http-server": "^0.9.0", 52 | "ignore-styles": "^5.0.1", 53 | "jsdom": "^9.11.0", 54 | "livereactload": "^3.2.0", 55 | "log": "^1.4.0", 56 | "nyc": "^10.1.2", 57 | "react-addons-test-utils": "^15.4.2", 58 | "react-dom": "^15.4.2", 59 | "react-proxy": "^1.1.8", 60 | "react-test-renderer": "^15.4.2", 61 | "sinon": "^1.17.7", 62 | "uglifyify": "^3.0.4", 63 | "watchify": "^3.9.0", 64 | "xo": "^0.17.1" 65 | }, 66 | "browserify": { 67 | "transform": [ 68 | "babelify" 69 | ] 70 | }, 71 | "babel": { 72 | "presets": [ 73 | "es2015", 74 | "stage-0", 75 | "react" 76 | ], 77 | "env": { 78 | "development": { 79 | "sourceMaps": "inline", 80 | "plugins": [ 81 | [ 82 | "react-transform", 83 | { 84 | "transforms": [ 85 | { 86 | "transform": "livereactload/babel-transform", 87 | "imports": [ 88 | "react" 89 | ] 90 | } 91 | ] 92 | } 93 | ] 94 | ] 95 | } 96 | } 97 | }, 98 | "xo": { 99 | "space": true, 100 | "semicolon": true, 101 | "extends": [ 102 | "xo-react" 103 | ] 104 | }, 105 | "stylelint": { 106 | "extends": "stylelint-config-standard", 107 | "rules": { 108 | "indentation": 2, 109 | "number-leading-zero": null 110 | } 111 | }, 112 | "ava": { 113 | "files": [ 114 | "src/**/spec/*.js" 115 | ], 116 | "source": [ 117 | "src/**/*.js" 118 | ], 119 | "presets": [ 120 | "@ava/stage-4", 121 | "@ava/transform-test-files" 122 | ], 123 | "failFast": true, 124 | "tap": false, 125 | "require": [ 126 | "babel-register", 127 | "ignore-styles" 128 | ], 129 | "babel": "inherit" 130 | }, 131 | "nyc": { 132 | "exclude": [ 133 | "src/store/**/*.js" 134 | ] 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /examples/02-redux/public/.gitkeep: -------------------------------------------------------------------------------- 1 | .gitkeep -------------------------------------------------------------------------------- /examples/02-redux/scripts/build.js: -------------------------------------------------------------------------------- 1 | const build = require('./run-build'); 2 | 3 | build(); 4 | -------------------------------------------------------------------------------- /examples/02-redux/scripts/dev.js: -------------------------------------------------------------------------------- 1 | const serve = require('./serve'); 2 | const build = require('./run-build'); 3 | 4 | build(true); 5 | serve(); 6 | -------------------------------------------------------------------------------- /examples/02-redux/scripts/run-build.js: -------------------------------------------------------------------------------- 1 | const exec = require('child_process').exec; 2 | const fs = require('fs'); 3 | const path = require('path'); 4 | const Log = require('log'); 5 | const browserify = require('browserify'); 6 | const watchify = require('watchify'); 7 | const errorify = require('errorify'); 8 | const livereactload = require('livereactload'); 9 | 10 | const log = new Log('info'); 11 | 12 | const demoFolder = path.join(__dirname, '../site'); 13 | const buildFolder = path.join(__dirname, '../public'); 14 | 15 | // add or remove files from this list 16 | // key: input file 17 | // value: output name (used for css and js) 18 | const fileMap = { 19 | 'index.js': 'main' 20 | }; 21 | // these files will be copied from the demoFolder to the buildFolder 22 | const demoFiles = [ 23 | 'index.html', 24 | 'favico.png' 25 | ]; 26 | const inputFiles = Object.keys(fileMap); 27 | 28 | // bash command to remove files 29 | const removeFiles = `rm -rf ${path.join(buildFolder, '*.{js,png,html}')}`; 30 | 31 | module.exports = watch => { 32 | exec(removeFiles, err => { 33 | if (err) { 34 | throw err; 35 | } 36 | // bash command to copy files 37 | const copyFiles = demoFiles.map(file => `cp ${path.join(demoFolder, file)} ${path.join(buildFolder, file)}`).join(';'); 38 | exec(copyFiles, err => { 39 | if (err) { 40 | throw err; 41 | } 42 | }); 43 | 44 | // create a bundler for each file 45 | inputFiles.forEach(file => { 46 | const inFile = path.join(demoFolder, file); 47 | const outFile = path.join(buildFolder, fileMap[file]); 48 | const plugin = [errorify]; 49 | 50 | if (watch) { 51 | plugin.push(watchify, livereactload); 52 | } 53 | 54 | const b = browserify({ 55 | entries: [inFile], 56 | plugin 57 | }); 58 | 59 | const bundle = () => { 60 | b.bundle().pipe(fs.createWriteStream(`${outFile}.js`)); 61 | }; 62 | 63 | // either uglify or watch 64 | if (watch) { 65 | b.on('update', bundle); 66 | } else { 67 | b.transform({ 68 | global: true 69 | }, 'uglifyify'); 70 | } 71 | 72 | b.on('log', message => log.info(message)); 73 | b.on('error', message => log.error(message)); 74 | 75 | bundle(); 76 | }); 77 | }); 78 | }; 79 | -------------------------------------------------------------------------------- /examples/02-redux/scripts/serve.js: -------------------------------------------------------------------------------- 1 | const exec = require('child_process').exec; 2 | const pkg = require('../package.json'); 3 | 4 | module.exports = () => { 5 | exec(`cd public && http-server -p ${pkg.devPort}`, err => { 6 | if (err) { 7 | throw err; 8 | } 9 | }); 10 | }; 11 | -------------------------------------------------------------------------------- /examples/02-redux/scripts/server.js: -------------------------------------------------------------------------------- 1 | const serve = require('./serve'); 2 | 3 | serve(); 4 | -------------------------------------------------------------------------------- /examples/02-redux/scripts/watch.js: -------------------------------------------------------------------------------- 1 | const build = require('./run-build'); 2 | 3 | build(true); 4 | -------------------------------------------------------------------------------- /examples/02-redux/site/favico.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/milankinen/livereactload/4ec023240308e0603f6e40c16f2ab1757eab467d/examples/02-redux/site/favico.png -------------------------------------------------------------------------------- /examples/02-redux/site/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Demo 8 | 9 | 10 | 11 |
12 | 13 | 14 | -------------------------------------------------------------------------------- /examples/02-redux/site/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {render} from 'react-dom'; 3 | import {Provider} from 'react-redux'; 4 | import App from '../src/containers/app'; 5 | import configureStore from '../src/store/configure-store'; 6 | 7 | const store = configureStore(); 8 | 9 | render( 10 | 11 | 12 | , 13 | global.document.getElementById('mountPoint') 14 | ); 15 | -------------------------------------------------------------------------------- /examples/02-redux/src/actions/counter.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @module actions/counter 3 | */ 4 | 5 | /** 6 | * @const 7 | * @type {String} 8 | */ 9 | export const INCREMENT_COUNTER = 'INCREMENT_COUNTER'; 10 | /** 11 | * @const 12 | * @type {String} 13 | */ 14 | export const DECREMENT_COUNTER = 'DECREMENT_COUNTER'; 15 | 16 | /** 17 | * incerement the counter 18 | * @return {object} 19 | */ 20 | export function increment() { 21 | return { 22 | type: INCREMENT_COUNTER 23 | }; 24 | } 25 | 26 | /** 27 | * decrement the counter 28 | * @return {object} 29 | */ 30 | export function decrement() { 31 | return { 32 | type: DECREMENT_COUNTER 33 | }; 34 | } 35 | 36 | /** 37 | * increment the counter if it is odd 38 | * @return {function} 39 | */ 40 | export function incrementIfOdd() { 41 | return (dispatch, getState) => { 42 | const {counter} = getState(); 43 | 44 | if (counter % 2 === 0) { 45 | return; 46 | } 47 | 48 | dispatch(increment()); 49 | }; 50 | } 51 | 52 | /** 53 | * increment the counter async 54 | * @return {function} 55 | */ 56 | export function incrementAsync(delay = 1000) { 57 | return dispatch => { 58 | setTimeout(() => { 59 | dispatch(increment()); 60 | }, delay); 61 | }; 62 | } 63 | -------------------------------------------------------------------------------- /examples/02-redux/src/actions/spec/counter.test.js: -------------------------------------------------------------------------------- 1 | import test from 'ava'; 2 | import sinon from 'sinon'; 3 | import * as actions from '../counter'; 4 | 5 | test('increment should create increment action', t => { 6 | const {stringify} = JSON; 7 | const passed = stringify(actions.increment()) === stringify({type: actions.INCREMENT_COUNTER}); 8 | t.true(passed); 9 | }); 10 | 11 | test('decrement should create decrement action', t => { 12 | const {stringify} = JSON; 13 | const passed = stringify(actions.decrement()) === stringify({type: actions.DECREMENT_COUNTER}); 14 | t.true(passed); 15 | }); 16 | 17 | test('incrementIfOdd should create increment action', t => { 18 | const fn = actions.incrementIfOdd(); 19 | /* istanbul ignore if */ 20 | if (typeof fn !== 'function') { 21 | t.fail(); 22 | } 23 | const dispatch = sinon.spy(); 24 | const getState = () => ({counter: 1}); 25 | fn(dispatch, getState); 26 | t.true(dispatch.calledWith({type: actions.INCREMENT_COUNTER})); 27 | }); 28 | 29 | test('incrementIfOdd shouldnt create increment action if counter is even', t => { 30 | const fn = actions.incrementIfOdd(); 31 | const dispatch = sinon.spy(); 32 | const getState = () => ({counter: 2}); 33 | fn(dispatch, getState); 34 | t.true(dispatch.callCount === 0); 35 | }); 36 | 37 | test('incrementAsync', t => { 38 | const fn = actions.incrementAsync(1); 39 | /* istanbul ignore if */ 40 | if (typeof fn !== 'function') { 41 | t.fail(); 42 | } 43 | const dispatch = sinon.spy(); 44 | fn(dispatch); 45 | setTimeout(() => { 46 | t.true(dispatch.calledWith({type: actions.INCREMENT_COUNTER})); 47 | }, 5); 48 | }); 49 | -------------------------------------------------------------------------------- /examples/02-redux/src/components/counter/index.js: -------------------------------------------------------------------------------- 1 | import React, {Component, PropTypes} from 'react'; 2 | 3 | /** 4 | * Counter component. renders a counter with buttons 5 | */ 6 | class Counter extends Component { 7 | /** 8 | * renders the component 9 | * @return {function} 10 | */ 11 | render() { 12 | const {increment, incrementIfOdd, incrementAsync, decrement, counter} = this.props; 13 | const handleIncrementAsync = () => incrementAsync(); 14 | return ( 15 |

16 | Clicked: {counter} times 17 | {' '} 18 | 19 | {' '} 20 | 21 | {' '} 22 | 23 | {' '} 24 | 25 |

26 | ); 27 | } 28 | } 29 | 30 | Counter.propTypes = { 31 | increment: PropTypes.func.isRequired, 32 | incrementIfOdd: PropTypes.func.isRequired, 33 | incrementAsync: PropTypes.func.isRequired, 34 | decrement: PropTypes.func.isRequired, 35 | counter: PropTypes.number.isRequired 36 | }; 37 | 38 | export default Counter; 39 | -------------------------------------------------------------------------------- /examples/02-redux/src/components/counter/spec/test.js: -------------------------------------------------------------------------------- 1 | import test from 'ava'; 2 | import sinon from 'sinon'; 3 | import React from 'react'; 4 | import ReactDOM from 'react-dom'; 5 | import TestUtils from 'react-addons-test-utils'; 6 | import jsdomReact from '../../../test/jsdom-react'; 7 | import Counter from '..'; 8 | 9 | function setup() { 10 | const actions = { 11 | increment: sinon.spy(), 12 | incrementIfOdd: sinon.spy(), 13 | incrementAsync: sinon.spy(), 14 | decrement: sinon.spy() 15 | }; 16 | const component = TestUtils.renderIntoDocument(); 17 | return { 18 | component: component, 19 | actions: actions, 20 | buttons: TestUtils.scryRenderedDOMComponentsWithTag(component, 'button').map(button => { 21 | return ReactDOM.findDOMNode(button); 22 | }), 23 | p: ReactDOM.findDOMNode(TestUtils.findRenderedDOMComponentWithTag(component, 'p')) 24 | }; 25 | } 26 | 27 | jsdomReact(); 28 | 29 | test('should display count', t => { 30 | const {p} = setup(); 31 | let passed = false; 32 | if (p.textContent.match(/^Clicked: 1 times/)) { 33 | passed = true; 34 | } 35 | t.true(passed); 36 | }); 37 | 38 | test('first button should call increment', t => { 39 | const {buttons, actions} = setup(); 40 | TestUtils.Simulate.click(buttons[0]); 41 | t.true(actions.increment.called); 42 | }); 43 | 44 | test('second button should call decrement', t => { 45 | const {buttons, actions} = setup(); 46 | TestUtils.Simulate.click(buttons[1]); 47 | t.true(actions.decrement.called); 48 | }); 49 | 50 | test('third button should call incrementIfOdd', t => { 51 | const {buttons, actions} = setup(); 52 | TestUtils.Simulate.click(buttons[2]); 53 | t.true(actions.incrementIfOdd.called); 54 | }); 55 | 56 | test('fourth button should call incrementAsync', t => { 57 | const {buttons, actions} = setup(); 58 | TestUtils.Simulate.click(buttons[3]); 59 | t.true(actions.incrementAsync.called); 60 | }); 61 | -------------------------------------------------------------------------------- /examples/02-redux/src/containers/app.js: -------------------------------------------------------------------------------- 1 | import {bindActionCreators} from 'redux'; 2 | import {connect} from 'react-redux'; 3 | import Counter from '../components/counter'; 4 | import * as CounterActions from '../actions/counter'; 5 | 6 | function mapStateToProps(state) { 7 | return { 8 | counter: state.counter 9 | }; 10 | } 11 | 12 | function mapDispatchToProps(dispatch) { 13 | return bindActionCreators(CounterActions, dispatch); 14 | } 15 | 16 | export default connect(mapStateToProps, mapDispatchToProps)(Counter); 17 | -------------------------------------------------------------------------------- /examples/02-redux/src/containers/app/spec/test.js: -------------------------------------------------------------------------------- 1 | import test from 'ava'; 2 | import sinon from 'sinon'; 3 | import React from 'react'; 4 | import ReactDOM from 'react-dom'; 5 | import TestUtils from 'react-addons-test-utils'; 6 | import jsdomReact from '../../../test/jsdom-react'; 7 | 8 | import {Provider} from 'react-redux'; 9 | import App from '..'; 10 | import configureStore from '../../../store/configure-store'; 11 | 12 | function setup(initialState) { 13 | const store = configureStore(initialState); 14 | const app = TestUtils.renderIntoDocument( 15 | 16 | 17 | 18 | ); 19 | return { 20 | app: app, 21 | buttons: TestUtils.scryRenderedDOMComponentsWithTag(app, 'button').map(button => { 22 | return ReactDOM.findDOMNode(button); 23 | }), 24 | p: ReactDOM.findDOMNode(TestUtils.findRenderedDOMComponentWithTag(app, 'p')) 25 | }; 26 | } 27 | 28 | jsdomReact(); 29 | 30 | test('should display initial count', t => { 31 | const {p} = setup(); 32 | let passed = false; 33 | if (p.textContent.match(/^Clicked: 0 times/)) { 34 | passed = true; 35 | } 36 | t.true(passed); 37 | }); 38 | 39 | test('should display updated count after increment button click', t => { 40 | const {buttons, p} = setup(); 41 | TestUtils.Simulate.click(buttons[0]); 42 | let passed = false; 43 | if (p.textContent.match(/^Clicked: 1 times/)) { 44 | passed = true; 45 | } 46 | t.true(passed); 47 | }); 48 | 49 | test('should display updated count after decrement button click', t => { 50 | const {buttons, p} = setup(); 51 | TestUtils.Simulate.click(buttons[1]); 52 | let passed = false; 53 | if (p.textContent.match(/^Clicked: -1 times/)) { 54 | passed = true; 55 | } 56 | t.true(passed); 57 | }); 58 | 59 | test('shouldnt change if even and if odd button clicked', t => { 60 | const {buttons, p} = setup(); 61 | TestUtils.Simulate.click(buttons[2]); 62 | let passed = false; 63 | if (p.textContent.match(/^Clicked: 0 times/)) { 64 | passed = true; 65 | } 66 | t.true(passed); 67 | }); 68 | 69 | test('should change if odd and if odd button clicked', t => { 70 | const {buttons, p} = setup({counter: 1}); 71 | TestUtils.Simulate.click(buttons[2]); 72 | let passed = false; 73 | if (p.textContent.match(/^Clicked: 2 times/)) { 74 | passed = true; 75 | } 76 | t.true(passed); 77 | }); 78 | -------------------------------------------------------------------------------- /examples/02-redux/src/reducers/counter.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @module reducers/counter 3 | */ 4 | 5 | import {INCREMENT_COUNTER, DECREMENT_COUNTER} from '../actions/counter'; 6 | 7 | /** 8 | * reducer for the counter 9 | * @param {number} state input state 10 | * @param {object} action object containing the action type 11 | * @param {string} action.type action type 12 | * @return {number} 13 | */ 14 | function counter(state = 0, action = {}) { 15 | switch (action.type) { 16 | case INCREMENT_COUNTER: 17 | return state + 1; 18 | case DECREMENT_COUNTER: 19 | return state - 1; 20 | default: 21 | return state; 22 | } 23 | } 24 | 25 | export default counter; 26 | -------------------------------------------------------------------------------- /examples/02-redux/src/reducers/index.js: -------------------------------------------------------------------------------- 1 | import {combineReducers} from 'redux'; 2 | import counter from './counter'; 3 | 4 | const rootReducer = combineReducers({ 5 | counter 6 | }); 7 | 8 | export default rootReducer; 9 | -------------------------------------------------------------------------------- /examples/02-redux/src/reducers/spec/counter.test.js: -------------------------------------------------------------------------------- 1 | import test from 'ava'; 2 | 3 | import counter from '../counter'; 4 | import {INCREMENT_COUNTER, DECREMENT_COUNTER} from '../../actions/counter'; 5 | 6 | test('should handle initial state', t => { 7 | t.is(counter(undefined, {}), 0); 8 | }); 9 | 10 | test('should handle INCREMENT_COUNTER', t => { 11 | t.is(counter(1, {type: INCREMENT_COUNTER}), 2); 12 | }); 13 | 14 | test('should handle DECREMENT_COUNTER', t => { 15 | t.is(counter(1, {type: DECREMENT_COUNTER}), 0); 16 | }); 17 | 18 | test('should handle unknown action type', t => { 19 | t.is(counter(1, {type: 'unknown'}), 1); 20 | }); 21 | -------------------------------------------------------------------------------- /examples/02-redux/src/store/configure-store.js: -------------------------------------------------------------------------------- 1 | import {createStore, applyMiddleware} from 'redux'; 2 | import thunk from 'redux-thunk'; 3 | import reducer from '../reducers'; 4 | 5 | const createStoreWithMiddleware = applyMiddleware( 6 | thunk 7 | )(createStore); 8 | 9 | export default function configureStore(initialState) { 10 | const store = createStoreWithMiddleware(reducer, initialState); 11 | 12 | // When using WebPack, module.hot.accept should be used. In LiveReactload, 13 | // same result can be achieved by using "module.onReload" hook. 14 | if (module.onReload) { 15 | module.onReload(() => { 16 | const nextReducer = require('../reducers'); 17 | store.replaceReducer(nextReducer.default || nextReducer); 18 | 19 | // return true to indicate that this module is accepted and 20 | // there is no need to reload its parent modules 21 | return true; 22 | }); 23 | } 24 | 25 | return store; 26 | } 27 | -------------------------------------------------------------------------------- /examples/02-redux/src/test/jsdom-react.js: -------------------------------------------------------------------------------- 1 | import ExecutionEnvironment from 'fbjs/lib/ExecutionEnvironment'; 2 | import {jsdom} from 'jsdom'; 3 | 4 | global.document = jsdom(''); 5 | global.window = global.document.defaultView; 6 | global.navigator = global.window.navigator; 7 | 8 | export default function jsdomReact() { 9 | ExecutionEnvironment.canUseDOM = true; 10 | } 11 | -------------------------------------------------------------------------------- /examples/03-build-systems/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015", "react"], 3 | "env": { 4 | "development": { 5 | "plugins": [ 6 | ["react-transform", { 7 | "transforms": [{ 8 | "transform": "livereactload/babel-transform", 9 | "imports": ["react"] 10 | }] 11 | }] 12 | ] 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /examples/03-build-systems/Gruntfile.js: -------------------------------------------------------------------------------- 1 | module.exports = function(grunt) { 2 | grunt.loadNpmTasks('grunt-browserify') 3 | grunt.loadNpmTasks('grunt-nodemon') 4 | grunt.loadNpmTasks('grunt-concurrent') 5 | 6 | grunt.registerTask('default', ['concurrent']) 7 | 8 | grunt.initConfig({ 9 | 10 | concurrent: { 11 | dev: { 12 | tasks: [ 13 | 'nodemon', // start server 14 | 'browserify:development' // start watchify with livereactload 15 | ], 16 | options: { 17 | logConcurrentOutput: true 18 | } 19 | } 20 | }, 21 | 22 | nodemon: { 23 | dev: { 24 | script: 'server.js', 25 | ignore: ['./bundle.js'] 26 | } 27 | }, 28 | 29 | browserify: { 30 | development: { 31 | src: './site.js', 32 | dest: './bundle.js', 33 | options: { 34 | // use watchify instead of browserify 35 | watch: true, 36 | keepAlive: true, 37 | transform: [['babelify', {}], ['envify', {global: true}]], 38 | // enable livereactload for dev builds 39 | plugin: ['livereactload'] 40 | } 41 | }, 42 | // this is meant for "production" bundle creation 43 | // use "grunt browserify:production" to create it 44 | production: { 45 | src: './site.js', 46 | dest: './bundle.js', 47 | options: { 48 | watch: false, 49 | transform: [['babelify', {}], ['envify', {global: true}]] 50 | } 51 | } 52 | } 53 | 54 | }) 55 | } 56 | -------------------------------------------------------------------------------- /examples/03-build-systems/README.md: -------------------------------------------------------------------------------- 1 | # Build system integrations 2 | 3 | LiveReactload is build system agnostic. It means that you can use LiveReactload with 4 | all build systems having Browserify and Watchify support. 5 | 6 | This example contains configurations for `grunt` and `gulp`. Note that since we are 7 | using `babelify` transform for both examples, `.babelrc` includes the configurations 8 | for `react-transform`. 9 | 10 | ## Grunt support 11 | 12 | You can use LiveReactload with `grunt-browserify` module normally as a Browserify plugin. 13 | Remember to add `watch: true` and `keepAlive: true` in your development config so that 14 | you are using `watchify` instead of `browserify! 15 | 16 | To run the grunt build, install `grunt-cli` and start grunt default task 17 | 18 | npm i -g grunt-cli 19 | grunt 20 | 21 | Then open localhost:3000 in your browser. 22 | 23 | In order to create a "production bundle", use `NODE_ENV=production grunt browserify:production` 24 | 25 | For more information, see `Gruntfile.js` 26 | 27 | 28 | ## Gulp support 29 | 30 | Use LiveReactload as a normal Browserify plugin when creating the bundler in `gulpfile.js`. 31 | No additional options are needed (but they can be passed if wanted). 32 | 33 | To run the gulp build, install `gulp-cli` and start gulp default task 34 | 35 | npm i -g gulp-cli 36 | gulp 37 | 38 | Then open localhost:3000 in your browser. 39 | 40 | In order to create a "production bundle", use `NODE_ENV=production gulp bundle:js` 41 | 42 | For more information, see `gulpfile.js` 43 | -------------------------------------------------------------------------------- /examples/03-build-systems/gulpfile.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var gulp = require("gulp"), 4 | gutil = require("gulp-util"), 5 | nodemon = require("gulp-nodemon"), 6 | source = require("vinyl-source-stream"), 7 | buffer = require("vinyl-buffer"), 8 | browserify = require("browserify"), 9 | watchify = require("watchify"), 10 | babelify = require("babelify"), 11 | envify = require("envify"), 12 | lrload = require("livereactload") 13 | 14 | 15 | var isProd = process.env.NODE_ENV === "production" 16 | 17 | 18 | function createBundler(useWatchify) { 19 | return browserify({ 20 | entries: [ "./site.js" ], 21 | transform: [ [babelify, {}], [envify, {}] ], 22 | plugin: isProd || !useWatchify ? [] : [ lrload ], // no additional configuration is needed 23 | debug: !isProd, 24 | cache: {}, 25 | packageCache: {}, 26 | fullPaths: !isProd // for watchify 27 | }) 28 | } 29 | 30 | gulp.task("bundle:js", function() { 31 | var bundler = createBundler(false) 32 | bundler 33 | .bundle() 34 | .pipe(source("bundle.js")) 35 | .pipe(gulp.dest(".")) 36 | }) 37 | 38 | gulp.task("watch:js", function() { 39 | // start JS file watching and rebundling with watchify 40 | var bundler = createBundler(true) 41 | var watcher = watchify(bundler) 42 | rebundle() 43 | return watcher 44 | .on("error", gutil.log) 45 | .on("update", rebundle) 46 | 47 | function rebundle() { 48 | gutil.log("Update JavaScript bundle") 49 | watcher 50 | .bundle() 51 | .on("error", gutil.log) 52 | .pipe(source("bundle.js")) 53 | .pipe(buffer()) 54 | .pipe(gulp.dest(".")) 55 | } 56 | }) 57 | 58 | gulp.task("watch:server", function() { 59 | nodemon({ script: "server.js", ext: "js", ignore: ["gulpfile.js", "bundle.js", "node_modules/*"] }) 60 | .on("change", function () {}) 61 | .on("restart", function () { 62 | console.log("Server restarted") 63 | }) 64 | }) 65 | 66 | gulp.task("default", ["watch:server", "watch:js"]) 67 | -------------------------------------------------------------------------------- /examples/03-build-systems/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "livereactload-build-systems-example", 3 | "version": "2.0.0", 4 | "author": "Matti Lankinen (https://github.com/milankinen)", 5 | "license": "MIT", 6 | "scripts": { 7 | "start": "node server.js" 8 | }, 9 | "dependencies": { 10 | "babel-preset-es2015": "^6.9.0", 11 | "babel-preset-react": "^6.11.1", 12 | "babelify": "^7.3.0", 13 | "browserify": "^13.0.1", 14 | "envify": "^3.4.1", 15 | "express": "^4.14.0", 16 | "react": "^15.2.1", 17 | "react-dom": "^15.2.1" 18 | }, 19 | "devDependencies": { 20 | "babel-plugin-react-transform": "^2.0.2", 21 | "grunt": "^1.0.1", 22 | "grunt-browserify": "^5.0.0", 23 | "grunt-concurrent": "^2.3.0", 24 | "grunt-nodemon": "^0.4.2", 25 | "gulp": "^3.9.1", 26 | "gulp-nodemon": "^2.1.0", 27 | "gulp-util": "^3.0.7", 28 | "livereactload": "latest", 29 | "nodemon": "^1.9.2", 30 | "react-proxy": "^1.1.8", 31 | "vinyl-buffer": "^1.0.0", 32 | "vinyl-source-stream": "^1.1.0", 33 | "watchify": "^3.7.0" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /examples/03-build-systems/server.js: -------------------------------------------------------------------------------- 1 | var express = require("express"), 2 | app = express() 3 | 4 | app.get("/", function (req, res) { 5 | res.send("" + 6 | "" + 7 | "" + 8 | "2.x build systems" + 9 | "" + 10 | "" + 11 | "
" + 12 | "" + 13 | "" + 14 | "") 15 | }) 16 | 17 | app.get("/static/bundle.js", function (req, res) { 18 | res.sendFile("bundle.js", {root: __dirname}) 19 | }) 20 | 21 | app.listen(3000) 22 | -------------------------------------------------------------------------------- /examples/03-build-systems/site.js: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import { render } from "react-dom" 3 | import App from "./src/app" 4 | 5 | const initialModel = window.INITIAL_MODEL 6 | 7 | render(, document.getElementById("app")) 8 | -------------------------------------------------------------------------------- /examples/03-build-systems/src/app.js: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | 3 | const systems = [ 4 | 'Gulp', 5 | 'Grunt', 6 | 'Tsers' 7 | ] 8 | 9 | export default class App extends React.Component { 10 | render() { 11 | return ( 12 |
13 |

Hello!

14 |
    {systems.map(s =>
  • {s}
  • )}
15 |
16 | ) 17 | } 18 | } 19 | 20 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 2 | module.exports = require("./lib/browserify-plugin/main.js") 3 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "livereactload", 3 | "version": "3.5.0", 4 | "description": "Live code editing with Browserify and React", 5 | "author": "Matti Lankinen (https://github.com/milankinen)", 6 | "license": "MIT", 7 | "repository": { 8 | "type": "git", 9 | "url": "git@github.com:milankinen/livereactload.git" 10 | }, 11 | "keywords": [ 12 | "react", 13 | "livereload", 14 | "browserify-plugin", 15 | "hmr", 16 | "browserify" 17 | ], 18 | "main": "index.js", 19 | "scripts": { 20 | "prepublish": "npm run dist", 21 | "dist": "npm run compile && npm run remove-maps", 22 | "precompile": "rm -rf lib/*", 23 | "compile": "babel src --out-dir lib", 24 | "postcompile": "cp src/reloading.js lib/", 25 | "remove-maps": "find lib -type f -name '*.map' -delete", 26 | "pretest": "npm run compile", 27 | "test": "babel-tape-runner test/**/*Test.js ; EXIT_VALUE=$? ; killall node ; exit $EXIT_VALUE" 28 | }, 29 | "dependencies": { 30 | "cli-color": "^1.1.0", 31 | "convert-source-map": "^1.3.0", 32 | "crc": "^3.4.4", 33 | "left-pad": "^1.1.3", 34 | "lodash": "^4.17.11", 35 | "offset-sourcemap-lines": "^1.0.0", 36 | "through2": "^2.0.3", 37 | "umd": "^3.0.1", 38 | "ws": "^6.1.x", 39 | "react-proxy": "^1.1.8" 40 | }, 41 | "devDependencies": { 42 | "babel-cli": "^6.18.0", 43 | "babel-preset-es2015": "^6.18.0", 44 | "babel-tape-runner": "2.0.1", 45 | "bluebird": "3.4.7", 46 | "chokidar": "1.6.1", 47 | "shelljs": "0.7.5", 48 | "tape": "4.6.3", 49 | "zombie": "4.2.1" 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/babel-transform/main.js: -------------------------------------------------------------------------------- 1 | import {getForceUpdate, createProxy} from "react-proxy" 2 | 3 | module.exports = function babelPluginLiveReactload({filename, components, imports, locals}) { 4 | const [React] = imports 5 | const forceUpdate = getForceUpdate(React) 6 | 7 | return function applyProxy(Component, uniqueId) { 8 | const {displayName, isInFunction = false} = components[uniqueId] 9 | const proxies = getProxies() 10 | 11 | if (!proxies || isInFunction) { 12 | return Component 13 | } 14 | 15 | const id = filename + "$$" + uniqueId 16 | if (!proxies[id]) { 17 | const proxy = createProxy(Component) 18 | proxies[id] = proxy 19 | return mark(proxy.get()) 20 | } else { 21 | console.log(" > Patch component :: ", displayName || uniqueId) 22 | const proxy = proxies[id] 23 | const instances = proxy.update(Component) 24 | setTimeout(() => instances.forEach(forceUpdate), 0) 25 | return mark(proxy.get()) 26 | } 27 | } 28 | } 29 | 30 | function mark(Component) { 31 | if (!Component.__$$LiveReactLoadable) { 32 | Object.defineProperty(Component, '__$$LiveReactLoadable', { 33 | configurable: false, 34 | writable: false, 35 | enumerable: false, 36 | value: true 37 | }) 38 | } 39 | return Component 40 | } 41 | 42 | function getProxies() { 43 | try { 44 | if (typeof window !== "undefined") { 45 | return (window.$$LiveReactLoadProxies = window.$$LiveReactLoadProxies || {}) 46 | } 47 | } catch (ignore) { 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/browserify-plugin/console.js: -------------------------------------------------------------------------------- 1 | import clc from "cli-color" 2 | 3 | export function log(msg, ...data) { 4 | const t = /T([0-9:.]+)Z/g.exec(new Date().toISOString())[1] 5 | console.log( 6 | clc.green(`[${t}] LiveReactload`), 7 | "::", 8 | clc.cyan(msg) 9 | ) 10 | data.forEach(d => console.log(clc.yellow(" >"), clc.yellow(d))) 11 | } 12 | -------------------------------------------------------------------------------- /src/browserify-plugin/main.js: -------------------------------------------------------------------------------- 1 | import _ from "lodash" 2 | import umd from "umd" 3 | import through from "through2" 4 | import crc from "crc" 5 | import {readFileSync} from "fs" 6 | import {resolve} from "path" 7 | import convertSourceMaps from 'convert-source-map' 8 | import offsetSourceMaps from 'offset-sourcemap-lines' 9 | import leftPad from 'left-pad' 10 | import {startServer} from "./server" 11 | import {log} from "./console" 12 | import loader from "../reloading" 13 | 14 | 15 | function LiveReactloadPlugin(b, opts = {}) { 16 | const { 17 | port = 4474, 18 | host = null, 19 | babel = true, 20 | client = true, 21 | dedupe = true, 22 | debug = false, 23 | moduledir = null, 24 | "ssl-cert": sslCert = null, 25 | "ssl-key": sslKey = null 26 | } = opts; 27 | 28 | // server is alive as long as watchify is running 29 | const server = opts.server !== false ? startServer({port: Number(port), sslCert, sslKey}) : null 30 | 31 | const clientOpts = { 32 | // assuming that livereload package is in global mdule directory (node_modules) 33 | // and this file is in ./lib/babel-plugin folder 34 | nodeModulesRoot: moduledir || resolve(__dirname, "../../.."), 35 | port: Number(port), 36 | host: host, 37 | clientEnabled: client, 38 | debug: debug, 39 | babel: babel 40 | } 41 | 42 | b.on("reset", addHooks) 43 | addHooks() 44 | 45 | function addHooks() { 46 | // this cache object is preserved over single bundling 47 | // pipeline so when next bundling occurs, this cache 48 | // object is thrown away 49 | const mappings = {}, pathById = {}, pathByIdx = {} 50 | const entries = [] 51 | let standalone = null 52 | 53 | const idToPath = id => 54 | pathById[id] || (_.isString(id) && id) || throws("Full path not found for id: " + id) 55 | 56 | const idxToPath = idx => 57 | pathByIdx[idx] || (_.isString(idx) && idx) || throws("Full path not found for index: " + idx) 58 | 59 | if (server) { 60 | b.pipeline.on("error", server.notifyBundleError) 61 | } 62 | 63 | b.pipeline.get("record").push(through.obj( 64 | function transform(row, enc, next) { 65 | const s = _.get(row, "options._flags.standalone") 66 | if (s) { 67 | standalone = s 68 | } 69 | next(null, row) 70 | } 71 | )) 72 | 73 | b.pipeline.get("sort").push(through.obj( 74 | function transform(row, enc, next) { 75 | const {id, index, file} = row 76 | pathById[id] = file 77 | pathByIdx[index] = file 78 | next(null, row) 79 | } 80 | )) 81 | 82 | if (!dedupe) { 83 | b.pipeline.splice("dedupe", 1, through.obj()) 84 | if (b.pipeline.get("dedupe")) { 85 | log("Other plugins have added de-duplicate transformations. --no-dedupe is not effective") 86 | } 87 | } else { 88 | b.pipeline.splice("dedupe", 0, through.obj( 89 | function transform(row, enc, next) { 90 | const cloned = _.extend({}, row) 91 | if (row.dedupeIndex) { 92 | cloned.dedupeIndex = idxToPath(row.dedupeIndex) 93 | } 94 | if (row.dedupe) { 95 | cloned.dedupe = idToPath(row.dedupe) 96 | } 97 | next(null, cloned) 98 | } 99 | )) 100 | } 101 | 102 | b.pipeline.get("label").push(through.obj( 103 | function transform(row, enc, next) { 104 | const {id, file, source, deps, entry} = row 105 | const converter = convertSourceMaps.fromSource(source) 106 | let sourceWithoutMaps = source 107 | let adjustedSourcemap = '' 108 | let hash; 109 | 110 | if (converter) { 111 | const sources = converter.getProperty("sources") || []; 112 | sourceWithoutMaps = convertSourceMaps.removeComments(source) 113 | hash = getHash(sourceWithoutMaps) 114 | converter.setProperty("sources", sources.map(source => source += "?version=" + hash)) 115 | adjustedSourcemap = convertSourceMaps.fromObject(offsetSourceMaps(converter.toObject(), 1)).toComment() 116 | } else { 117 | hash = getHash(source) 118 | } 119 | 120 | if (entry) { 121 | entries.push(file) 122 | } 123 | mappings[file] = [sourceWithoutMaps, deps, {id: file, hash: hash, browserifyId: id, sourcemap: adjustedSourcemap}] 124 | next(null, row) 125 | }, 126 | function flush(next) { 127 | next() 128 | } 129 | )) 130 | 131 | b.pipeline.get("wrap").push(through.obj( 132 | function transform(row, enc, next) { 133 | next(null) 134 | }, 135 | function flush(next) { 136 | const pathById = _.fromPairs(_.toPairs(mappings).map(([file, [s, d, {browserifyId: id}]]) => [id, file])) 137 | const idToPath = id => 138 | pathById[id] || (_.isString(id) && id) 139 | 140 | const depsToPaths = deps => 141 | _.reduce(deps, (m, v, k) => { 142 | let id = idToPath(v); 143 | if (id) { 144 | m[k] = id; 145 | } 146 | return m; 147 | }, {}) 148 | 149 | const withFixedDepsIds = _.mapValues(mappings, ([src, deps, meta]) => [ 150 | src, 151 | depsToPaths(deps), 152 | meta 153 | ]) 154 | const args = [ 155 | withFixedDepsIds, 156 | entries, 157 | clientOpts 158 | ] 159 | let bundleSrc = 160 | `(${loader.toString()})(${args.map(a => JSON.stringify(a, null, 2)).join(", ")});` 161 | if (standalone) { 162 | bundleSrc = umd(standalone, `return ${bundleSrc}`) 163 | } 164 | 165 | this.push(new Buffer(bundleSrc, "utf8")) 166 | if (server) { 167 | server.notifyReload(withFixedDepsIds) 168 | } 169 | next() 170 | } 171 | )) 172 | } 173 | 174 | function throws(msg) { 175 | throw new Error(msg) 176 | } 177 | 178 | function getHash(data) { 179 | const crcHash = leftPad(crc.crc32(data).toString(16), 8, "0") 180 | return new Buffer(crcHash, "hex") 181 | .toString("base64") 182 | .replace(/=/g,"") 183 | } 184 | } 185 | 186 | module.exports = LiveReactloadPlugin 187 | -------------------------------------------------------------------------------- /src/browserify-plugin/server.js: -------------------------------------------------------------------------------- 1 | import {Server} from "ws" 2 | import {log} from "./console" 3 | import https from 'https'; 4 | import {readFileSync} from 'fs'; 5 | 6 | function logError(error) { 7 | if (error) { 8 | log(error) 9 | } 10 | } 11 | 12 | export function startServer({port, sslKey, sslCert}) { 13 | if ((sslCert && !sslKey) || (!sslCert && sslKey)) { 14 | throw new Error('You need both a certificate AND key in order to use SSL'); 15 | } 16 | 17 | let wss; 18 | if (sslCert && sslKey) { 19 | const key = readFileSync(sslKey, 'utf8'); 20 | const cert = readFileSync(sslCert, 'utf8'); 21 | const credentials = {key, cert}; 22 | const server = https.createServer(credentials); 23 | server.listen(port); 24 | wss = new Server({server}); 25 | } else { 26 | wss = new Server({port}); 27 | } 28 | 29 | 30 | log("Reload server up and listening in port " + port + "...") 31 | 32 | const server = { 33 | notifyReload(metadata) { 34 | if (wss.clients.length) { 35 | log("Notify clients about bundle change...") 36 | } 37 | wss.clients.forEach(client => { 38 | client.send(JSON.stringify({ 39 | type: "change", 40 | data: metadata 41 | }), logError) 42 | }) 43 | }, 44 | notifyBundleError(error) { 45 | if (wss.clients.length) { 46 | log("Notify clients about bundle error...") 47 | } 48 | wss.clients.forEach(client => { 49 | client.send(JSON.stringify({ 50 | type: "bundle_error", 51 | data: { error: error.toString() } 52 | }), logError) 53 | }) 54 | } 55 | } 56 | 57 | wss.on("connection", client => { 58 | log("New client connected") 59 | }) 60 | 61 | return server 62 | } 63 | -------------------------------------------------------------------------------- /src/reloading.js: -------------------------------------------------------------------------------- 1 | /*eslint semi: "error"*/ 2 | 3 | /** 4 | * This is modified version of Browserify's original module loader function, 5 | * made to support LiveReactLoad's reloading functionality. 6 | * 7 | * 8 | * @param mappings 9 | * An object containing modules and their metadata, created by 10 | * LiveReactLoad plugin. The structure of the mappings object is: 11 | * { 12 | * [module_id]: [ 13 | * "...module_source...", 14 | * { 15 | * "module": target_id, 16 | * "./another/module": target_id, 17 | * ... 18 | * }, 19 | * { 20 | * hash: "32bit_hash_from_source", 21 | * isEntry: true|false 22 | * } 23 | * ], 24 | * ... 25 | * } 26 | * 27 | * @param entryPoints 28 | * List of bundle's entry point ids. At the moment, only one entry point 29 | * is supported by LiveReactLoad 30 | * @param options 31 | * LiveReactLoad options passed from the CLI/plugin params 32 | */ 33 | function loader(mappings, entryPoints, options) { 34 | 35 | if (entryPoints.length > 1) { 36 | throw new Error( 37 | "LiveReactLoad supports only one entry point at the moment" 38 | ) 39 | } 40 | 41 | var entryId = entryPoints[0]; 42 | 43 | var scope = { 44 | mappings: mappings, 45 | cache: {}, 46 | reloading: false, 47 | reloadHooks: {}, 48 | reload: function (fn) { 49 | scope.reloading = true; 50 | try { 51 | fn(); 52 | } finally { 53 | scope.reloading = false; 54 | } 55 | } 56 | }; 57 | 58 | 59 | function startClient() { 60 | if (!options.clientEnabled) { 61 | return; 62 | } 63 | if (typeof window.WebSocket === "undefined") { 64 | warn("WebSocket API not available, reloading is disabled"); 65 | return; 66 | } 67 | var protocol = window.location.protocol === "https:" ? "wss" : "ws"; 68 | var url = protocol + "://" + (options.host || window.location.hostname); 69 | if (options.port != 80) { 70 | url = url + ":" + options.port; 71 | } 72 | var ws = new WebSocket(url); 73 | ws.onopen = function () { 74 | info("WebSocket client listening for changes..."); 75 | }; 76 | ws.onmessage = function (m) { 77 | var msg = JSON.parse(m.data); 78 | if (msg.type === "change") { 79 | handleBundleChange(msg.data); 80 | } else if (msg.type === "bundle_error") { 81 | handleBundleError(msg.data); 82 | } 83 | } 84 | } 85 | 86 | function compile(mapping) { 87 | var body = mapping[0]; 88 | if (typeof body !== "function") { 89 | debug("Compiling module", mapping[2]) 90 | var compiled = compileModule(body, mapping[2].sourcemap); 91 | mapping[0] = compiled; 92 | mapping[2].source = body; 93 | } 94 | } 95 | 96 | function compileModule(source, sourcemap) { 97 | var toModule = new Function( 98 | "__livereactload_source", "__livereactload_sourcemap", 99 | "return eval('function __livereactload_module(require, module, exports){\\n' + __livereactload_source + '\\n}; __livereactload_module;' + (__livereactload_sourcemap || ''));" 100 | ); 101 | return toModule(source, sourcemap) 102 | } 103 | 104 | function unknownUseCase() { 105 | throw new Error( 106 | "Unknown use-case encountered! Please raise an issue: " + 107 | "https://github.com/milankinen/livereactload/issues" 108 | ) 109 | } 110 | 111 | // returns loaded module from cache or if not found, then 112 | // loads it from the source and caches it 113 | function load(id, recur) { 114 | var mappings = scope.mappings; 115 | var cache = scope.cache; 116 | 117 | if (!cache[id]) { 118 | if (!mappings[id]) { 119 | var req = typeof require == "function" && require; 120 | if (req) return req(id); 121 | var error = new Error("Cannot find module '" + id + "'"); 122 | error.code = "MODULE_NOT_FOUND"; 123 | throw error; 124 | } 125 | 126 | var hook = scope.reloadHooks[id]; 127 | var module = cache[id] = { 128 | exports: {}, 129 | __accepted: false, 130 | onReload: function (hook) { 131 | scope.reloadHooks[id] = hook; 132 | } 133 | }; 134 | 135 | mappings[id][0].call(module.exports, function require(path) { 136 | var targetId = mappings[id][1][path]; 137 | return load(targetId ? targetId : path); 138 | }, module, module.exports, unknownUseCase, mappings, cache, entryPoints); 139 | 140 | if (scope.reloading && typeof hook === "function") { 141 | // it's important **not** to assign to module.__accepted because it would point 142 | // to the old module object during the reload event! 143 | cache[id].__accepted = hook() 144 | } 145 | 146 | } 147 | return cache[id].exports; 148 | } 149 | 150 | /** 151 | * Patches the existing modules with new sources and returns a list of changes 152 | * (module id and old mapping. ATTENTION: This function does not do any reloading yet. 153 | * 154 | * @param mappings 155 | * New mappings 156 | * @returns {Array} 157 | * List of changes 158 | */ 159 | function patch(mappings) { 160 | var compile = scope.compile; 161 | var changes = []; 162 | 163 | keys(mappings).forEach(function (id) { 164 | var old = scope.mappings[id]; 165 | var mapping = mappings[id]; 166 | var meta = mapping[2]; 167 | if (!old || old[2].hash !== meta.hash) { 168 | compile(mapping); 169 | scope.mappings[id] = mapping; 170 | changes.push([id, old]); 171 | } 172 | }); 173 | return changes; 174 | } 175 | 176 | /** 177 | * Reloads modules based on the given changes. If reloading fails, this function 178 | * tries to restore old implementation. 179 | * 180 | * @param changes 181 | * Changes array received from "patch" function 182 | */ 183 | function reload(changes) { 184 | var changedModules = changes.map(function (c) { 185 | return c[0]; 186 | }); 187 | var newMods = changes.filter(function (c) { 188 | return !c[1]; 189 | }).map(function (c) { 190 | return c[0]; 191 | }); 192 | 193 | scope.reload(function () { 194 | try { 195 | info("Applying changes..."); 196 | debug("Changed modules", changedModules); 197 | debug("New modules", newMods); 198 | evaluate(entryId, {}); 199 | info("Reload complete!"); 200 | } catch (e) { 201 | error("Error occurred while reloading changes. Restoring old implementation..."); 202 | console.error(e); 203 | console.error(e.stack); 204 | try { 205 | restore(); 206 | evaluate(entryId, {}); 207 | info("Restored!"); 208 | } catch (re) { 209 | error("Restore failed. You may need to refresh your browser... :-/"); 210 | console.error(re); 211 | console.error(re.stack); 212 | } 213 | } 214 | }) 215 | 216 | 217 | function evaluate(id, changeCache) { 218 | if (id in changeCache) { 219 | debug("Circular dependency detected for module", id, "not traversing any further..."); 220 | return changeCache[id]; 221 | } 222 | if (isExternalModule(id)) { 223 | debug("Module", id, "is an external module. Do not reload"); 224 | return false; 225 | } 226 | var module = getModule(id); 227 | debug("Evaluate module details", module); 228 | 229 | // initially mark change status to follow module's change status 230 | // TODO: how to propagate change status from children to this without causing infinite recursion? 231 | var meChanged = contains(changedModules, id); 232 | changeCache[id] = meChanged; 233 | if (id in scope.cache) { 234 | delete scope.cache[id]; 235 | } 236 | 237 | var deps = module.deps.filter(isLocalModule); 238 | var depsChanged = deps.map(function (dep) { 239 | return evaluate(dep, changeCache); 240 | }); 241 | 242 | // In the case of circular dependencies, the module evaluation stops because of the 243 | // changeCache check above. Also module cache should be clear. However, if some circular 244 | // dependency (or its descendant) gets reloaded, it (re)loads new version of this 245 | // module back to cache. That's why we need to ensure that we're not 246 | // 1) reloading module twice (so that we don't break cross-refs) 247 | // 2) reload any new version if there is no need for reloading 248 | // 249 | // Hence the complex "scope.cache" stuff... 250 | // 251 | var isReloaded = module.cached !== undefined && id in scope.cache; 252 | var depChanged = any(depsChanged); 253 | 254 | if (isReloaded || depChanged || meChanged) { 255 | debug("Module changed", id, isReloaded, depChanged, meChanged); 256 | if (!isReloaded) { 257 | var msg = contains(newMods, id) ? " > Add new module ::" : " > Reload module ::"; 258 | console.log(msg, id); 259 | load(id); 260 | } else { 261 | console.log(" > Already reloaded ::", id); 262 | } 263 | changeCache[id] = !allExportsProxies(id) && !isAccepted(id); 264 | return changeCache[id]; 265 | } else { 266 | // restore old version of the module 267 | if (module.cached !== undefined) { 268 | scope.cache[id] = module.cached; 269 | } 270 | return false; 271 | } 272 | } 273 | 274 | function allExportsProxies(id) { 275 | var e = scope.cache[id].exports; 276 | return isProxy(e) || (isPlainObj(e) && all(vals(e), isProxy)); 277 | 278 | function isProxy(x) { 279 | return x && !!x.__$$LiveReactLoadable; 280 | } 281 | } 282 | 283 | function isAccepted(id) { 284 | var accepted = scope.cache[id].__accepted; 285 | scope.cache[id].__accepted = false; 286 | if (accepted === true) { 287 | console.log(" > Manually accepted") 288 | } 289 | return accepted === true; 290 | } 291 | 292 | function restore() { 293 | changes.forEach(function (c) { 294 | var id = c[0], mapping = c[1]; 295 | if (mapping) { 296 | debug("Restore old mapping", id); 297 | scope.mappings[id] = mapping; 298 | } else { 299 | debug("Delete new mapping", id); 300 | delete scope.mappings[id]; 301 | } 302 | }) 303 | } 304 | } 305 | 306 | function getModule(id) { 307 | return { 308 | deps: vals(scope.mappings[id][1]), 309 | meta: scope.mappings[id][2], 310 | cached: scope.cache[id] 311 | }; 312 | } 313 | 314 | function handleBundleChange(newMappings) { 315 | info("Bundle changed"); 316 | var changes = patch(newMappings); 317 | if (changes.length > 0) { 318 | reload(changes); 319 | } else { 320 | info("Nothing to reload"); 321 | } 322 | } 323 | 324 | function handleBundleError(data) { 325 | error("Bundling error occurred"); 326 | error(data.error); 327 | } 328 | 329 | 330 | // prepare mappings before starting the app 331 | forEachValue(scope.mappings, compile); 332 | 333 | if (options.babel) { 334 | if (isReactTransformEnabled(scope.mappings)) { 335 | info("LiveReactLoad Babel transform detected. Ready to rock!"); 336 | } else { 337 | warn( 338 | "Could not detect LiveReactLoad transform (livereactload/babel-transform). " + 339 | "Please see instructions how to setup the transform:\n\n" + 340 | "https://github.com/milankinen/livereactload#installation" 341 | ); 342 | } 343 | } 344 | 345 | scope.compile = compile; 346 | scope.load = load; 347 | 348 | debug("Options:", options); 349 | debug("Entries:", entryPoints, entryId); 350 | 351 | startClient(); 352 | // standalone bundles may need the exports from entry module 353 | return load(entryId); 354 | 355 | 356 | // this function is stringified in browserify process and appended to the bundle 357 | // so these helper functions must be inlined into this function, otherwise 358 | // the function is not working 359 | 360 | function isReactTransformEnabled(mappings) { 361 | return any(vals(mappings), function (mapping) { 362 | var source = mapping[2].source; 363 | return source && source.indexOf("__$$LiveReactLoadable") !== -1; 364 | }); 365 | } 366 | 367 | function isLocalModule(id) { 368 | return id.indexOf(options.nodeModulesRoot) === -1 369 | } 370 | 371 | function isExternalModule(id) { 372 | return !(id in scope.mappings); 373 | } 374 | 375 | function keys(obj) { 376 | return obj ? Object.keys(obj) : []; 377 | } 378 | 379 | function vals(obj) { 380 | return keys(obj).map(function (key) { 381 | return obj[key]; 382 | }); 383 | } 384 | 385 | function contains(col, val) { 386 | for (var i = 0; i < col.length; i++) { 387 | if (col[i] === val) return true; 388 | } 389 | return false; 390 | } 391 | 392 | function all(col, f) { 393 | if (!f) { 394 | f = function (x) { 395 | return x; 396 | }; 397 | } 398 | for (var i = 0; i < col.length; i++) { 399 | if (!f(col[i])) return false; 400 | } 401 | return true; 402 | } 403 | 404 | function any(col, f) { 405 | if (!f) { 406 | f = function (x) { 407 | return x; 408 | }; 409 | } 410 | for (var i = 0; i < col.length; i++) { 411 | if (f(col[i])) return true; 412 | } 413 | return false; 414 | } 415 | 416 | function forEachValue(obj, fn) { 417 | keys(obj).forEach(function (key) { 418 | if (obj.hasOwnProperty(key)) { 419 | fn(obj[key]); 420 | } 421 | }); 422 | } 423 | 424 | function isPlainObj(x) { 425 | return typeof x == 'object' && x.constructor == Object; 426 | } 427 | 428 | function debug() { 429 | if (options.debug) { 430 | console.log.apply(console, ["LiveReactload [DEBUG] ::"].concat(Array.prototype.slice.call(arguments))); 431 | } 432 | } 433 | 434 | function info(msg) { 435 | console.info("LiveReactload ::", msg); 436 | } 437 | 438 | function warn(msg) { 439 | console.warn("LiveReactload ::", msg); 440 | } 441 | 442 | function error(msg) { 443 | console.error("LiveReactload ::", msg); 444 | } 445 | } 446 | 447 | module.exports = loader; 448 | module.exports["default"] = loader; 449 | 450 | -------------------------------------------------------------------------------- /test/app/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015", "react"], 3 | "env": { 4 | "development": { 5 | "plugins": [ 6 | "transform-runtime", 7 | ["react-transform", { 8 | "transforms": [{ 9 | "transform": "livereactload/babel-transform", 10 | "imports": ["react"] 11 | }] 12 | }] 13 | ] 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /test/app/.gitignore: -------------------------------------------------------------------------------- 1 | # staging folder for tests 2 | .src 3 | -------------------------------------------------------------------------------- /test/app/extra.js: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import Header from "./.src/extra/header" 3 | import Body from "./.src/extra/body" 4 | 5 | class Box extends React.Component { 6 | render() { 7 | return ( 8 |
9 |
10 | 11 |
12 | ) 13 | } 14 | } 15 | 16 | module.exports = { 17 | Box 18 | } 19 | -------------------------------------------------------------------------------- /test/app/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "livereactload-test-app", 3 | "version": "0.0.1", 4 | "scripts": { 5 | "start": "babel-node server.js & npm run watch:app & npm run watch:extra & wait", 6 | "bundle:vendor": "browserify -o bundle.vendor.js -v -t babelify -r react -r react-dom", 7 | "watch:app": "watchify site.js -v -t babelify -p [ livereactload --port=4477 --moduledir=$(pwd)/node_modules ] -x react -x react-dom --ig -o bundle.app.js", 8 | "watch:extra": "watchify extra.js -v -s Extra -t babelify -p [ livereactload --port=4478 --moduledir=$(pwd)/node_modules ] -x react -x react-dom -o bundle.extra.js" 9 | }, 10 | "dependencies": { 11 | "babel": "^6.5.2", 12 | "babel-cli": "^6.10.1", 13 | "babel-plugin-transform-runtime": "^6.9.0", 14 | "babel-preset-es2015": "^6.9.0", 15 | "babel-preset-react": "^6.11.1", 16 | "babel-runtime": "^6.9.2", 17 | "babelify": "^7.3.0", 18 | "express": "^4.14.0", 19 | "react": "^15.2.1", 20 | "react-dom": "^15.2.1", 21 | "redux": "^3.5.2" 22 | }, 23 | "devDependencies": { 24 | "babel-plugin-react-transform": "^2.0.2", 25 | "browserify": "^13.0.1", 26 | "livereactload": "file:../..", 27 | "watchify": "^3.7.0" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /test/app/server.js: -------------------------------------------------------------------------------- 1 | const express = require("express"), 2 | app = express() 3 | 4 | app.get("/", (req, res) => { 5 | res.send(` 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | 14 | `) 15 | }); 16 | 17 | (["vendor", "app", "extra"]).forEach(name => { 18 | app.get(`/static/bundle.${name}.js`, function(req, res) { 19 | res.sendFile(`bundle.${name}.js`, {root: __dirname}) 20 | }) 21 | }) 22 | 23 | 24 | app.listen(3077, () => { 25 | console.log("Server started") 26 | }) 27 | -------------------------------------------------------------------------------- /test/app/site.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { render } from "react-dom"; 3 | import App from "./.src/app"; 4 | 5 | // we are requiring redux here even though it's not used because 6 | // redux 3.0.0 contains some dependencies that are de-duplicated 7 | // by Browserify, thus breaking all the time -- we can get most 8 | // of these bugs caught by adding this require (the tests get broken 9 | // when dedupe handling gets broken) 10 | require("redux"); 11 | 12 | console.log("Increment site.js reload counter..."); 13 | window._siteReloadCounter = 14 | "_siteReloadCounter" in window ? window._siteReloadCounter + 1 : 0; 15 | 16 | const MyApp = React.createClass({ 17 | render() { 18 | return window._siteReloadCounter === 0 ? ( 19 | 20 | ) : ( 21 |
22 | This text should never occur because LiveReactLoad should stop reload 23 | propagation if module returns only React components (like src/app.js 24 | does). 25 |
26 | ); 27 | } 28 | }); 29 | 30 | render(, document.getElementById("app")); 31 | -------------------------------------------------------------------------------- /test/app/src/app.js: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import Counter from "./counter" 3 | import {MAGIC} from "./constants" 4 | import {lolbal} from "./circular/second" 5 | 6 | require("./hooks") 7 | 8 | const {Box} = window.Extra 9 | 10 | export default React.createClass({ 11 | render() { 12 | return ( 13 |
14 |

Hello world

15 |

Magic number is {MAGIC}

16 | 17 | 18 |
19 | {lolbal()} 20 |
21 |
22 | ) 23 | } 24 | }) 25 | -------------------------------------------------------------------------------- /test/app/src/circular/first.js: -------------------------------------------------------------------------------- 1 | import {MAGIC} from "./second" 2 | 3 | export function bal() { 4 | return "bal" + MAGIC 5 | } 6 | -------------------------------------------------------------------------------- /test/app/src/circular/second.js: -------------------------------------------------------------------------------- 1 | import * as first from "./first" 2 | 3 | export const MAGIC = "!!" 4 | 5 | export function lolbal() { 6 | return "lol" + first.bal() 7 | } 8 | -------------------------------------------------------------------------------- /test/app/src/constants.js: -------------------------------------------------------------------------------- 1 | 2 | 3 | export const MAGIC = 10 4 | -------------------------------------------------------------------------------- /test/app/src/counter.js: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | 3 | export default React.createClass({ 4 | getInitialState() { 5 | return {value: 10} 6 | }, 7 | 8 | increment() { 9 | this.setState({value: this.state.value + 1}) 10 | }, 11 | 12 | render() { 13 | return ( 14 |
15 |

16 | Counter '{this.props.name}' value is {this.state.value} 17 |

18 | 19 |
20 | ) 21 | } 22 | }) 23 | -------------------------------------------------------------------------------- /test/app/src/extra/body.js: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | 3 | export default React.createClass({ 4 | render() { 5 | return ( 6 |
Extra body!!
7 | ) 8 | } 9 | }) 10 | -------------------------------------------------------------------------------- /test/app/src/extra/header.js: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | 3 | export default React.createClass({ 4 | render() { 5 | return ( 6 |
Extra header!!
7 | ) 8 | } 9 | }) 10 | -------------------------------------------------------------------------------- /test/app/src/hooks.js: -------------------------------------------------------------------------------- 1 | 2 | require("./hooks/accept") 3 | require("./hooks/noAccept") 4 | 5 | if ("_hooksReloadCount" in window) { 6 | window._hooksReloadCount = window._hooksReloadCount + 1 7 | } else { 8 | window._hooksReloadCount = 0 9 | } 10 | 11 | -------------------------------------------------------------------------------- /test/app/src/hooks/accept.js: -------------------------------------------------------------------------------- 1 | 2 | export const MSG = "foo" 3 | 4 | window._acceptReloaded = false 5 | 6 | if (module.onReload) { 7 | module.onReload(() => { 8 | window._acceptReloaded = true 9 | return true 10 | }) 11 | } 12 | -------------------------------------------------------------------------------- /test/app/src/hooks/noAccept.js: -------------------------------------------------------------------------------- 1 | 2 | export const MSG = "foo" 3 | 4 | window._noAcceptReloaded = false 5 | 6 | if (module.onReload) { 7 | module.onReload(() => { 8 | window._noAcceptReloaded = true 9 | return false 10 | }) 11 | } 12 | -------------------------------------------------------------------------------- /test/smokeTest.js: -------------------------------------------------------------------------------- 1 | import test from "tape" 2 | import Browser from "zombie" 3 | import {startServer, wait, updateSources} from "./utils" 4 | 5 | const server = startServer() 6 | const browser = new Browser() 7 | 8 | test("smoke tests", assert => { 9 | wait(10000) 10 | .then(() => ( 11 | browser.visit("http://localhost:3077/") 12 | .then(() => wait(500)) 13 | .then(() => browser.pressButton("button.inc")) 14 | )) 15 | .then(testInitialConditions) 16 | .then(testSingleFileUpdatingTriggersReload) 17 | .then(testStandaloneBundleReload) 18 | .then(testCircularDepsGetReloaded) 19 | .then(testReloadPropagationToParentModule) 20 | .then(testMultipleUpdatesAreAggregatedIntoOneReload) 21 | .then(testOnReloadHook) 22 | .then(() => assert.end() || process.exit(0)) 23 | .finally(() => server.kill()) 24 | .timeout(40000) 25 | .catch(e => console.error(e) || process.exit(1)) 26 | .done() 27 | 28 | 29 | function testInitialConditions() { 30 | assert.comment("test that initial page contents are satisfied") 31 | browser.assert.success() 32 | browser.assert.text(".header", "Hello world") 33 | browser.assert.text(".counter-title", "Counter 'foo' value is 11") 34 | browser.assert.text(".extra-body", "Extra body!!") 35 | browser.assert.text(".circular", "lolbal!!") 36 | } 37 | 38 | 39 | function testSingleFileUpdatingTriggersReload() { 40 | assert.comment("test that single file updating triggers the reloading") 41 | const updateSrcP = 42 | updateSources(browser, [ 43 | { 44 | file: "app.js", 45 | find: "Hello world", 46 | replace: "Tsers!" 47 | } 48 | ]) 49 | return updateSrcP 50 | .then(() => { 51 | browser.assert.text(".header", "Tsers!") 52 | }) 53 | } 54 | 55 | 56 | function testStandaloneBundleReload() { 57 | assert.comment("test that changes from standalone bundle gets reloaded") 58 | const updateSrcP = 59 | updateSources(browser, [ 60 | { 61 | file: "extra/body.js", 62 | find: "Extra body", 63 | replace: "Extra mod-body" 64 | } 65 | ]) 66 | return updateSrcP 67 | .then(() => { 68 | browser.assert.text(".extra-body", "Extra mod-body!!") 69 | }) 70 | } 71 | 72 | 73 | function testCircularDepsGetReloaded() { 74 | assert.comment("test that changes to circular dependencies get reloaded correctly") 75 | const updateSrcP = 76 | updateSources(browser, [ 77 | { 78 | file: "circular/second.js", 79 | find: "!!", 80 | replace: "??" 81 | } 82 | ]) 83 | return updateSrcP 84 | .then(() => { 85 | browser.assert.text(".circular", "lolbal??") 86 | }) 87 | } 88 | 89 | 90 | function testReloadPropagationToParentModule() { 91 | assert.comment("test that reloading propagates to parent modules if exports is not a React component") 92 | const updateSrcP = 93 | updateSources(browser, [ 94 | { 95 | file: "constants.js", 96 | find: "10", 97 | replace: "1337" 98 | } 99 | ]) 100 | return updateSrcP 101 | .then(() => { 102 | browser.assert.text(".magic", "Magic number is 1337") 103 | }) 104 | } 105 | 106 | 107 | function testMultipleUpdatesAreAggregatedIntoOneReload() { 108 | assert.comment("test that multiple file updates are aggregated to single reload event") 109 | const updateSrcP = 110 | updateSources(browser, [ 111 | { 112 | file: "counter.js", 113 | find: "+ 1", 114 | replace: "- 3" 115 | }, 116 | { 117 | file: "app.js", 118 | find: "foo", 119 | replace: "bar" 120 | } 121 | ]) 122 | return updateSrcP 123 | .then(() => browser.pressButton("button.inc")) 124 | .then(() => { 125 | // result: 11 - 3 = 8, see counter.js 126 | browser.assert.text(".counter-title", "Counter 'bar' value is 8") 127 | }) 128 | } 129 | 130 | function testOnReloadHook() { 131 | assert.comment("test onReload hook initial conditions") 132 | assert.equals(browser.window._hooksReloadCount, 0) 133 | assert.equals(browser.window._acceptReloaded, false) 134 | assert.equals(browser.window._noAcceptReloaded, false) 135 | 136 | return testNoAcceptPropagatesReloadToParent().then(testAcceptDoesntPropagateReloadToParent) 137 | 138 | function testNoAcceptPropagatesReloadToParent() { 139 | assert.comment("test that if onReload hook does't return true, then reloading is propagated to the parent module") 140 | const updateSrcP = 141 | updateSources(browser, [ 142 | { 143 | file: "hooks/noAccept.js", 144 | find: "foo", 145 | replace: "bar" 146 | } 147 | ]) 148 | return updateSrcP 149 | .then(() => { 150 | assert.equals(browser.window._noAcceptReloaded, true) 151 | assert.equals(browser.window._hooksReloadCount, 1) 152 | }) 153 | } 154 | 155 | function testAcceptDoesntPropagateReloadToParent() { 156 | assert.comment("test that if onReload hook returns true, then reloading doesn't propagate to the parent module") 157 | const updateSrcP = 158 | updateSources(browser, [ 159 | { 160 | file: "hooks/accept.js", 161 | find: "foo", 162 | replace: "bar" 163 | } 164 | ]) 165 | return updateSrcP 166 | .then(() => { 167 | assert.equals(browser.window._acceptReloaded, true) 168 | assert.equals(browser.window._hooksReloadCount, 1) 169 | }) 170 | } 171 | } 172 | 173 | }) 174 | 175 | -------------------------------------------------------------------------------- /test/utils.js: -------------------------------------------------------------------------------- 1 | import Promise from "bluebird"; 2 | import chokidar from "chokidar"; 3 | import fs from "fs"; 4 | import { resolve } from "path"; 5 | import * as sh from "shelljs"; 6 | 7 | export function startServer() { 8 | execInApp("mkdir -p .src && rm -rf .src/* && cp -R src/* .src"); 9 | //execInApp("mkdir -p node_modules && rm -rf node_modules/livereactload") 10 | //execInApp("npm i") 11 | execInApp("npm run bundle:vendor"); 12 | return execInApp("npm start", { async: true }); 13 | } 14 | 15 | export function execInApp(cmd, opts) { 16 | return sh.exec(`cd ${resolve(__dirname, "app")} && ${cmd}`, opts); 17 | } 18 | 19 | export function wait(time) { 20 | return new Promise(resolve => setTimeout(resolve, time)); 21 | } 22 | 23 | export function updateSources(browser, files) { 24 | const promises = files.map( 25 | ({ file, find, replace }) => 26 | new Promise((res, rej) => { 27 | const filename = resolve(__dirname, "app/.src", file); 28 | const watcher = chokidar.watch(filename, { 29 | persistent: false, 30 | usePolling: true, 31 | interval: 100 32 | }); 33 | 34 | watcher.on("change", () => { 35 | watcher.close(); 36 | res(); 37 | }); 38 | watcher.on("error", e => { 39 | watcher.close(); 40 | rej(e); 41 | }); 42 | watcher.on("ready", () => { 43 | console.log("Update source file", file); 44 | const content = fs.readFileSync(filename).toString(); 45 | const newContent = content.replace(find, replace); 46 | fs.writeFileSync(filename, newContent); 47 | }); 48 | }) 49 | ); 50 | 51 | return Promise.all(promises) 52 | .timeout(1000) 53 | .then(() => wait(1000)) 54 | .then(() => browser.wait(100)); 55 | } 56 | --------------------------------------------------------------------------------