GithubHelp home page GithubHelp logo

alexisvincent / systemjs-hmr Goto Github PK

View Code? Open in Web Editor NEW
27.0 6.0 11.0 856 KB

Hot Module Replacement for SystemJS

License: MIT License

JavaScript 63.15% HTML 1.91% TypeScript 34.94%
systemjs-hmr systemjs-hot-reloader hmr-enabler hot-reload hot-reloading hmr systemjs jspm jspm-devtools

systemjs-hmr's Introduction

SystemJS HMR

npm version MIT Licence

systemjs-hmr provides the official hot module replacement implementation for SystemJS via a System.reload function and extends SystemJS with an System.unload function to cleanly unload modules (js, css, scss etc) from the browser.

Please note, this library will not give you hot reloading out of the box, if thats what you are looking for, checkout systemjs-hot-reloader or systemjs-tools

Goal

The goal of this project is to implement HMR primitives for SystemJS that can be battle tested and later added to the core project. SystemJS HMR is meant to be used as an HMR enabler for library creators rather then providing a full HMR experience for application developers, if you're looking to implement HMR in your own project take a look at systemjs-hot-reloader or systemjs-tools both of which use this project under the hood.

We want to introduce a minimal API change to SystemJS and build in such a fashion as to enable smooth assimilation into core further down the line. This project will only implement the logic required to enable HMR, and as such things akin to the eventing api found in systemjs-hot-reloader or systemjs-tools are left to the library/application developer.

Usage

Install with your client-side package manager

  • jspm install npm:systemjs-hmr
  • yarn add systemjs-hmr
  • npm install systemjs-hmr

systemjs-hmr requires SystemJS >0.19.x or >=0.20.8.

systemjs-hmr MUST load before your application code otherwise SystemJS won't know how to resolve your @hot imports. So either add a script tag to your header after loading SystemJS

<script src="jspm_packages/npm/systemjs-hmr@version/dist/systemjs-hmr.js"></script>

or import systemjs-hmr before importing your app code.

<script>
    System.import('systemjs-hmr').then(() => {
        System.import('app/app.js')
    })
</script>

Until SystemJS does automatically, you need to tell SystemJS how to handle the @hot imports when building your app. To do this, add the following to your jspm config file.

{
  ...
  "map": {
    ...
    "@hot": "@empty"
  }
}

systemjs-hmr will automatically set SystemJS.trace = true, so you no longer need to set this manually, as with previous versions.

State Hydration and Safe Module Unloads

(see #2 for discussion / proposals)

When hot module replacement is added to an application there are a few modifications we may need to make to our code base, since the assumption that your code will run exactly once has been broken.

When a new version of a module is imported it might very well want to reinitialize it's own state based on the state of the previous module instance, to deal with this case and to cleanly unload your module from the registry you can import the previous instance of your module as you would any other module, as well as export an __unload function.

/**
 * You can import the previous instance of your module as you would any other module.
 * On first load, module == false.
 */
import { module } from '@hot'

/** 
 * Since all exports of the previous instance are available, you can simply export any state you might want to persist.
 *
 * Here we set and export the state of the file. If 'module == false' (first load),
 * then initialise the state to {}, otherwise set the state to the previously exported
 * state.
 */
export const _state = module ? module._state : {}

/**
 * If you're module needs to run some 'cleanup' code before being unloaded from the system, it can do so,
 * by exporting an `__unload` function that will be run just before the module is deleted from the registry.
 *
 * Here you would unsubscribe from listeners, or any other task that might cause issues in your application,
 * or prevent the module from being garbage collected.
 *
 * See SystemJS.unload API for more information.
 */
export const __unload = () => {
    console.log('Unload something (unsubscribe from listeners, disconnect from socket, etc...)')
    // force unload React components
    ReactDOM.unmountComponentAtNode(DOMNode);	// your container node
}

API

SystemJS.reload(moduleName, [options])

Where

  • moduleName is a String of the same kind you would provide to System.load or System.import when importing a module.
  • options is an optional object containing information to help speedup the reload process (module sources, dependency trees, etc.)

options has the following (all optional) keys (but the API is still being built so you can expect this to change)

entries : [String] An array of top level entry points into the application. If entry points aren't provided, systemjs-hmr will discover them automatically (slower).

others Other options will be exposed to speedup the reload process. For example, pre-calculated dependency trees, pre-fetched module sources, etc.

SystemJS.unload(moduleName)

A drop in replacement for SystemJS.delete(moduleName).

Checks if the module exports an __unload function, i.e. if typeof SystemJS.get(moduleName).__unload === 'function', if so, this function is executed.

Finally, SystemJS.delete(moduleName) is executed.

Extending hot-reloading for your own loader

In a traditional application one does not usually have to deal with modules being deleted/unloaded, naturally HMR requires us to start thinking about what happens when we unload a module in-order to replace it. Now unloading javascript is naturally different then say to css. With javascript we need to let the module itself handle some of the unloading (unsubscribing from event listeners, unmounting dom nodes, etc) and then delete it from the SystemJS registry. With css however, we simply need to delete the link node from the DOM.

Evidently this is a plugin level decision and as such as a loader author, if you would like to make your loader compatible with HMR, simply make sure the instantiated JS module (that will be set in the registry) exports an __unload function. This will be called by SystemJS.unload during the HMR process.

This ends up cleanly catering for the general case where a module is deleted (via SystemJS.unload) as well as the reload situation.

Roadmap

systemjs-hmr's People

Contributors

alexisvincent avatar dazinator avatar frederikschubert avatar jjrv avatar joeldenning avatar mpfau avatar singulariteehee avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar

systemjs-hmr's Issues

Build Bundle

I want to pass the source files through a rollup (with babel), so that I can get a nice single small bundle.

This will also allow easy usage of something like the babel runtime transform.

If anyone has some time and wants to contribute to this project. This is would appreciated.

reload deps children

export default class Button extends Component 
import Button from "button.jsx"
render(){
    return (<Button>button</Button>)
}

button.jsx on change app.jsx should fire reload

Prevent reloading dependants

I didn't find a mechanism to prevent reloading dependant modules all the way up to the root. It's usually a good idea if the code wasn't specifically designed for hot reloading, but I've got at least two cases where the reload shouldn't propagate further:

  • CSS files. Stylesheet updates triggering reloads of the importing JavaScript code seems rarely necessary, but still needed in rare cases (code reading the stylesheet directly or reacting to the computed style).
  • Flux-style one-way data flow frameworks designed for hot reloading. For example, updating a Vue.js component doesn't normally require reloading files that use it. Vue itself handles any necessary reinitialization.

Is there a way to prevent the propagation? If not, I'd suggest two possible mechanisms for consideration:

  • Method or options object importable from @hot to configure reload behaviour.
  • Meta option passed to System.config, allowing to toggle hot reload and its propagation through SystemJS configuration mechanisms per-package, per-extension, per-file etc.

Basically we could control for each source file:

  • Reload when the file changes?
  • Reload when an imported file changes?
  • Pass the reload event to dependants which then reload (unless they disable it through the previous option)?

parentKey.indexOf is not a function

systemjs-hmr.js:12432 Error: parentKey.indexOf is not a function
  Resolving http://localhost:9080/src/dev/css-parser.js to 1
    at getParentMetadata (http://localhost:9080/jspm_packages/system.src.js:1563:42)
    at SystemJSLoader$1.normalize (http://localhost:9080/jspm_packages/system.src.js:1580:24)
    at http://localhost:9080/jspm_packages/system.src.js:318:27
From previous event:
    at eval (http://localhost:9080/jspm_packages/npm/[email protected]/dist/systemjs-hmr.js:12422:12)
From previous event:
    at eval (http://localhost:9080/jspm_packages/npm/[email protected]/dist/systemjs-hmr.js:12417:10)(anonymous function) @ systemjs-hmr.js:12432tryCatcher @ systemjs-hmr.js:6747Promise._settlePromiseFromHandler @ systemjs-hmr.js:4892Promise._settlePromise @ systemjs-hmr.js:4949Promise._settlePromise0 @ systemjs-hmr.js:4994Promise._settlePromises @ systemjs-hmr.js:5069(anonymous function) @ systemjs-hmr.js:2033

script errors on load?

Having put these two scripts on a page:

 <script src="~/lib/systemjs/system.js"></script>
 <script src="~/lib/systemjs-hmr/dist/systemjs-hmr.js"></script>

The systemjs-hmr.js script errors on load with:

image

image

This is using [email protected] and [email protected]

Am I doing something stupid?

__unload function and AMD

I have an AMD module, however when I reload it, systemjs-hmr doesn't see the "__unload" function and so it never fires.

Here is my AMD module, perhaps I am not exporting it correctly?

define("ModuleA", ["require", "exports", "@hot"], function (require, exports, module) {
    "use strict";

    var self = {};
    self.addModuleToList = function (listId, moduleName) {
        var ul = document.getElementById(listId);
        var li = document.createElement("li");
        li.appendChild(document.createTextNode(moduleName + " Loaded.. I am productive!"));
        ul.appendChild(li);      
    };

    self._state = module ? module._state : {};
    self.addModuleToList("modules", "ModuleA");
    var unload = () => {
        console.log('Unload something (unsubscribe from listeners, disconnect from socket, etc...)');
    };

    self.__unload = unload;


    exports.__unload = () => {
        console.log('Unload something (unsubscribe from listeners, disconnect from socket, etc...)')
        // force unload React components
    };

    return self;

});

Debugging a reload the issue occurs here:

 if (typeof module.__unload == 'function') module.__unload();

Basically, module at this point doesnt have any unload() function - it looks like this in the console:

module
j {Symbol(): {…}}

However what does work is:

module.default.__unload

Is this caused by the way I am exporting this function? (If so, what is the correct way?) or do you think this might just be a systemjs change? (if so I'll include the fix mentioned above in my PR)
Also tried with systemjs 0.20.19 and the same problem occurred, making me think it must be more down to my AMD definition and me not guessing the correct way to export this unload function!

Deleting old source maps

I don't even know if this is possible, but one thing that would be awesome is if the source mapped files in dev tools were automatically updated with the new code. Right now what seems to happen is that the code is being hot reloaded correctly, but the source maps for the hot reloaded file still have the old code.

Is this something that others have run into? Is it even possible to get the browser to update source maps?

systemjs-hmr is searching for Maintainers!

This issue was created by Maintainers Wanted 🤓

Support us by leaving a star on Github! 🌟

systemjs-hmr is searching for new Maintainers! 👨‍💻 📬

Do you use systemjs-hmr personally or at work and would like this project to be further developed and improved?

Or are you already a contributor and ready to take the next step to becoming a maintainer?

If you are interested, comment here below on this issue 👇 or drop me a message on Twitter! 🙌

Add debounce

Add debounce to prevent multi file loads to thrash

Rollup and import { module } from '@hot'

I am not sure if anyone has experience with rollup here, but I am hitting an issue.
Given a file with this import:

import hot from '@hot';

At runtime, you can configure system.js to map that to @empty and that works.

However if you try to bundle this file using rollup, it complains that it can't resolve @hot.
I can't figure out a way to configure rollup to handle this import at build time.. Any ideas?

Next Major (systemjs-hmr and systems-hot-reloader)

systemjs-hmr doesn't yet support SystemJS 0.20. Since JSPM 17 now uses SystemJS 0.20 by default, HMR is broken for a lot of people. This, merged with the fact that we currently have two implementations of hot-reloading introduces some nuance into the upgrade process. This issue is tracking what needs to happen to land a shiny new release (hopefully within the next day or two).

Current State

  1. systemjs-hot-reloader relies on systemjs-hmr for its hot reloading implementation, although this is as of now, unreleased (on npm)
  2. systemjs-hmr contains two hmr implementations lib/index.js (current systems-hot-reloader) implementation, and lib/next.js (simpler/new implementation)
  3. Neither currently support SystemJS 0.20
  4. The implementations have differing state unload and reload mechanisms
  5. The implementations have differing module discovery mechanisms

I don't want to end up maintaining 2 different HMR implementations for obvious reasons. I also think this is a good an opportunity as any for the community to move onto the new implementation, resulting in me only having to build and test SystemJS 0.20 in the simpler implementation. However I also don't want to force everyone to change how their state reloading works (even though it is a very simple modification).

Plan

To get a working SystemJS 0.20 fix out as soon as possible, I'm going to make the new version compatible, update the docs and push that. Then folks that want/need SystemJS 0.20 support can add the necessary modifications to their code base. This can then be released along with a new version of systemjs-hot-reloader.

Then, to create a smooth upgrade experience, I'm going to build backwards compatibility for the old API into the new implementation (with deprecations). Which can come as a minor version bump.

Update 1

I'm instead going to complete the work to get systemjs-hmr backwards compatable with the old mechanism of reloading before publishing a release. I feel this will cause less tension. All that needs to be completed still is the process whereby systemjs-hmr determines the entry points of the application. And in the same time this will mean a speed boost for the current implementation.

systemjs-hmr v2.0.3 (released)

  • SystemJS 0.20 support
  • systemjs-hmr usage reworking
  • testing
  • docs

Backwards Compatibility

  • __unload
  • __reload
  • module discovery
  • module discovery in the event of an error

systemjs-hot-reloader v1

SystemJS Docs Update

  • Update JSPM and SystemJS docs

State Hydration Proposals/Discussion

As I can't comment on he readme I'm using this issue.

Great write up of Javascript Plugin Reload Hook solutions.

I wrote the original __reload hook (alexisvincent/systemjs-hot-reloader#23) but I agree that this complicates things, as it runs after the regular initiation is run, you'll need hooks to adapt what's already initiated (like Redux's replaceReducer).

You might find the following discussion interesting: alexisvincent/systemjs-hot-reloader#34

For example Proposal #3 would give you access to the old module, but a good point of mikz is that this requires you to make your state public (export it).

I like my last idea: alexisvincent/systemjs-hot-reloader#34 (comment), which I turned into a simple utility: simple systemjs-hot-reloader-store utility. This enables you to use the old state during the regular initiation of the module and doesn't require exporting the state. It also doesn't require any special hooks.
It's big downside however is that you manually need to pick a unique name.

Maybe there is a way to enable every module to import a @hotState, which is a
unique object per module. This can then used to store state in and on reload, when it's there, it can be used for rehydration. This fixes the unique name issue and the module author can decide if he want's to store anything and what is stored. (I stole the name from: alexisvincent/systemjs-hot-reloader#34 (comment))

Better Error Handling

The current version is not particularly robust and fails when encountering bad or unexpected data. This should be resolved

Question with @hot

I was wondering, given that @hot is mapped to @empty - how / where does this get set to an actual instance of the old module when the module is reloaded? Is this something that systemjs-hmr does?

Display err.originalErr

When I get errors in my module from system hmr, the stack trace doesn't have the right source mappings. I fixed for myself by using err.originalErr || err instead of just err. I am not sure what the value is in having the modified broken error, if there is any. I think we should either send originalErr only or both to the console.

Handle circular dependencies

Currently the order of reloading of a module's dependant modules is not defined by the order in which they were loaded.
This leads to errors when there is a circular dependency.
I suggest to take the order of loading into account when deleting and reloading the modules.

The systemjs-hmr polyfill must be loading after SystemJS has loaded

I've been a long time user of systemjs and the systemjs-hot-reloader and I'm having some problems updating to the latest version.
I keep getting the following error:

The systemjs-hmr polyfill must be loading after SystemJS has loaded

I'm having this issue in a very basic example. Even with the following html code I'm getting this error:

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <title></title>
    <script type="text/javascript" src="jspm_packages/system.src.js"></script>
    <script type="text/javascript" src="jspm.config.js"></script>
  </head>
  <body>
    <script type="text/javascript">
      System.import('systemjs-hot-reloader');
    </script>
  </body>
</html>

I'm opening the error in the systemjs-hmr project since that's where the error is coming from.
Versions I'm using:

  • jspm 0.17.0-beta.41
  • SystemJS v0.20.10 Dev
  • systemjs-hot-reloader 1.1.0
  • systemjs-hmr 2.0.9

Preemptive File Loading

[Edited to include pertinent comments]

One should be able to precompile files and pass them to systemjs-hmr for a quicker reload cycle.

Something like this

System.reload("app/a.js", {
   from: {
      "app/a.js": {
         source: 'module a source code'
      },
      "app/b.js": {
         source: 'module b source code'
      },
   }
})

So that the hot-loader can precompile and send over the changed files. This will essentially only save the load time latencies that would be incurred by System.import

TypeError: Object.values is not a function

TypeError: Object.values is not a function
    at Object.getDependencies (http://localhost:9080/jspm_packages/npm/[email protected]/dist/systemjs-hmr.js:10909:63)
    at Object.hasDependency (http://localhost:9080/jspm_packages/npm/[email protected]/dist/systemjs-hmr.js:10914:20)
    at eval (http://localhost:9080/jspm_packages/npm/[email protected]/dist/systemjs-hmr.js:10922:22)
    at Array.filter (native)
    at Object.getDependents (http://localhost:9080/jspm_packages/npm/[email protected]/dist/systemjs-hmr.js:10921:27)
    at findDependants (http://localhost:9080/jspm_packages/npm/[email protected]/dist/systemjs-hmr.js:11079:38)
    at eval (http://localhost:9080/jspm_packages/npm/[email protected]/dist/systemjs-hmr.js:11143:16)
From previous event:
    at SystemJSLoader$1.System.reload (http://localhost:9080/jspm_packages/npm/[email protected]/dist/systemjs-hmr.js:11140:28)
    at fileChanged (http://localhost:9080/jspm_packages/npm/[email protected]/dist/index.js:8615:12)
    at Socket.eval (http://localhost:9080/jspm_packages/npm/[email protected]/dist/index.js:8643:58)
    at Socket.Emitter.emit (http://localhost:9080/jspm_packages/npm/[email protected]/dist/index.js:4138:20)
    at Socket.onevent (http://localhost:9080/jspm_packages/npm/[email protected]/dist/index.js:7212:10)
    at Socket.onpacket (http://localhost:9080/jspm_packages/npm/[email protected]/dist/index.js:7170:12)
    at Manager.eval (http://localhost:9080/jspm_packages/npm/[email protected]/dist/index.js:6939:15)
    at Manager.Emitter.emit (http://localhost:9080/jspm_packages/npm/[email protected]/dist/index.js:4138:20)
    at Manager.ondecoded (http://localhost:9080/jspm_packages/npm/[email protected]/dist/index.js:7778:8)
    at Decoder.eval (http://localhost:9080/jspm_packages/npm/[email protected]/dist/index.js:6939:15)
    at Decoder.Emitter.emit (http://localhost:9080/jspm_packages/npm/[email protected]/dist/index.js:2193:20)
    at Decoder.add (http://localhost:9080/jspm_packages/npm/[email protected]/dist/index.js:2635:12)
    at Manager.ondata (http://localhost:9080/jspm_packages/npm/[email protected]/dist/index.js:7768:16)
    at Socket$1.eval (http://localhost:9080/jspm_packages/npm/[email protected]/dist/index.js:6939:15)
    at Socket$1.Emitter.emit (http://localhost:9080/jspm_packages/npm/[email protected]/dist/index.js:4138:20)
    at Socket$1.onPacket (http://localhost:9080/jspm_packages/npm/[email protected]/dist/index.js:6578:14)
    at WS.eval (http://localhost:9080/jspm_packages/npm/[email protected]/dist/index.js:6395:10)
    at WS.Emitter.emit (http://localhost:9080/jspm_packages/npm/[email protected]/dist/index.js:4138:20)
    at WS.Transport$1.onPacket (http://localhost:9080/jspm_packages/npm/[email protected]/dist/index.js:4315:8)

chrome 52.0.2743.116 (64-bit)

Every other hot reload fails with browser exception

I am using systemjs-tools backend, which is nicely sending updates. However, after the first reload event, my application only updates on every other change. The browser always gets an exception like this when it doesn't work:

systemjs-hmr.js:12430 Error: Fetch error: 404 
  Instantiating https://localhost:8183/smc/app/main.js - bundle/components.js
  Loading https://localhost:8183/smc/app/main.js - bundle/components.js
    at system.src.js:1458
    at <anonymous>

As you can see, the app/main.js is being triggered to reload, but it is containing my application's bundled dependencies for some reason on every other change.

I have configured jspm bundling with config injection such that that my application is bundled in two parts:

  • all app dependencies go in "components.js" to avoid rebundling cost of all dependencies
  • all app code bundled to "dependencies.js" to trigger reload in systemjs-tools

I turned on debug for systemjs-hmr, and noticed in the browser the difference between the two events being processed:


:8183/smc/jspm_packages/npm/[email protected]/dist/systemjs-hmr.js:1796 systemjs-hmr:log dependency tree purged, reimporting entries +1ms 
Array(1)
0
:
"app/main.js - bundle/components.js"
length
:
1
__proto__
:
Array(0)
systemjs-hmr.js:1796 systemjs-hmr:log dependency tree purged, reimporting entries +1ms 
(3) ["https://localhost:8183/smc/app/main.js", "https://localhost:8183/smc/bundle/dependencies.js", "https://localhost:8183/smc/bundle/components.js"]
0
:
"https://localhost:8183/smc/app/main.js"
1
:
"https://localhost:8183/smc/bundle/dependencies.js"
2
:
"https://localhost:8183/smc/bundle/components.js"
length
:
3
__proto__
:
Array(0)

image

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.