Home

Awesome

cycle-hmr

:fire: Hot reloading of cycle.js dataflows without need of restarting your app.

Demo (editing cyclic component's DOM vtree)

ezgif com-resize 1

What does it give to me and why I want to use it?

You can edit code of your components and immediately have last saved version injected into the running application without loosing state. It will just improves your dev workflow feedback loop and save your time. Well, it is :fire: hot reloading - you must use it!

How does it work?

cycle-hmr utilizes "standard" HMR approach - replacing internals of existing instances with newly updated versions on the fly. It is achieved by proxying cycle components (like it is done for example in React Hot Reloader). In cycle.js application components are pure functions that output sink streams, that makes it quite straightforward to transparently and safely extend them. When updated version of module with cyclic functions arrives (using some hot reload technique) we replace components while their runtime keeping the rest application parts not touched (though of course injection of updated components potentially my cause some "unexpected" effects).

What do I need to use it?

Supported stream libraries

cycle-hmr is stream library agnostic. So you can use it with any library supported by cyclejs (rx4, rx5, most, xstream) - it will detect and use needed stream adapter, but you should have it installed in your dependencies, if cycle-hmr will not find valid adapter it will just not proxy your streams.

Usage

1) Install from npm

npm install cycle-hmr --save-dev

Also my need to install adapters, but usually they are installed with your cycle run modules (like @cycle/xstream-run)

npm install @cycle/rxjs-adapter @cycle/xstream-adapter --save

2) Configure babel plugin

cycle-hmr comes with babel plugin (as dependency).

You should include the babel plugin (in .babelrc) and point where files with cyclic dataflows are located using include option (you may also/instead use exclude option to point files that should not be processed), like this:

.babelrc:

{
  "env": {
    "development": {
      "plugins": [
        ["cycle-hmr", {
          "include": "**/cycles/**"      
        }]
      ]
    }
  }
}

You want to use some env setting because you probably need cycle-hmr only in development mode.

Note also that include/exclude is glob matchers and not relative paths or regexps. This also to files that are processed by babel transpiler, so no need to include extension.

If you don't use include/exclude options, no modules will be processed by default. But you can include files individually supplying them with comment on the top:

/* @cycle-hmr */

To exclude individual files from processing, use comment on the top:

/* @no-cycle-hmr */

Why do I need to point to dataflow files and why babel plugin is needed at all?

As it was said to work cycle-hmr need have your dataflow functions wrapped with special proxy. You could do it manually actually:

 import {hmrProxy} from 'cycle-hrm'
 ...
 // you should also provide hmrProxy with globally unique ID  
 // wich must be preserved between module reloads
 let proxied = hmrProxy(MyDataflowComponent, module.id + 'MyDataflowComponent')
 
 export proxied as MyDataflowComponent

but it is probably not the case, and you won't do this in normal development workflow.

That is why we use babel plugin, it will statically analyze your code and wrap needed dataflows with proxy. And as long as cycle dataflows are just pure JS functions (they are not components classes that extend some basic class like for example in React) it is difficult (or impossible?) to automagically detect and extract cycle dataflows from the code, at least until we have some special marks/flags/decorators for them. So for now we choose a strategy just to wrap all the exports assuming they are (can be) dataflows.

Specifically babel plugin for each processed module (file) wraps all the export declarations with transparent proxy that enables hot replacement of the exported functions (and as you understand we need to proxy only exported dataflows). It also utilities also hot.accept() webpack/browserify HMR API to work with those bundlers.

*Note: if it proxies something that it should not, well, unlikely that it will break anything - HMR proxy wrapper is transparent for non-cyclic exports. But it is recommended to have only dataflow exports in processed modules. *

Additional plugin usage options:

{
  "development": {
    "plugins": [
      ["cycle-hmr/xstream", {
        "testExportName": "^[A-Z]",
        "include": "*"      
      }]
    ]
  }
}

this will process all the files, but will proxy only named exports starting with capital letter (not it will not include default exports in this case, to include them you would use ^([A-Z]|default) regExp expression).

3) Configure bundler/loader for hot-reloading

It is easy to use cycle-hmr with webpack or browserify.

Webpack

Setup you hot reloading workflow with webpack-dev-server and babel-loader using this need parts of config:

  ...
  entry: [
    'webpack-dev-server/client?http://localhost:' + process.env.PORT,
    'webpack/hot/only-dev-server',
    './app.js' // your app's entry
  ]
  module: {
    loaders: [{
      test: /\.js$/,
      loader: 'babel' // .babelrc should plug in `cycle-hmr`
    }]
  },
  plugins: [
    new webpack.NoErrorsPlugin(), // use it
    new webpack.HotModuleReplacementPlugin(),
    new webpack.IgnorePlugin(/most-adapter/) // for adaperts not installed
  ],
  devServer: {
    hot: true,
  ...

NB! If you use CLI to run WDS (webpack-dev-server with --hot and --inline options) and have issues when dealing with compile and runtime errors (for example webpack HMR does recover after such errors or reloads the page) recommendation is to use node API for launching WDS:

var webpack = require('webpack');
var WebpackDevServer = require('webpack-dev-server');
var config = require('./webpack.config');
new WebpackDevServer(webpack(config), config.devServer)
  .listen(process.env.PORT);

Browserify

Use browserify-hmr plugin and babelify. Use --ingnore-missing option to ignore missing dependencies. For example launch with budo server:

budo entry.js -- -t babelify --ignore-missing -p browserify-hmr

4) Turn on debug output if needed

If there is something wrong and you don't understand what (for example HMR does not work), you may want to turn on the debug output: It will show what happens to components that are proxied.

Fo this add proxy.debug option to cycle-hmr babel plugin:

{
  "plugins": [
    ["cycle-hmr", {
      "include": "**/cycles/**",
      "proxy": {
        "debug": "info"
      }
    }]
  ]
}

This will turn on debug output for all modules, but you can turn on it individually using the comment on the top of the module:

 /* @cycle-hmr-debug */

Licence

MIT.