├── .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 | [](https://gitter.im/milankinen/livereactload)
15 | [](http://badge.fury.io/js/livereactload)
16 | [](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 |
Increment
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 |
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 |
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 | Increment if odd
23 | {' '}
24 | Increment async
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 |
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 |
--------------------------------------------------------------------------------