Repository navigation
Conversation
`Elm.hot.reload(newScope)` updates the `Elm` namespace and hot reloads all running Elm app instances. The compiler should first check that a hot reload is safe: If anything has changed in the "deep canonicalized" type of `main`, it is unsafe. Then the page should be reloaded instead. It needs support in elm/browser to work. (And in elm/virtual-dom for `main : Html msg` programs.)
These prevented apps from being garbage collected.
Ports are the only effect managers that can change. We need to not only check if the port already exists in the previous code, but also make sure that the correct data can flow through it, and that it goes in the right direction.
|
Thanks for suggesting these code changes. To set expectations:
Finally, please be patient with the core team. They are trying their best with limited resources. |
Co-authored-by: Jeroen Engels <jfm.engels@gmail.com>
| var _Platform_worker = F3(function(impl, flagDecoder, debugMetadata) | ||
| { | ||
| return _Platform_initialize( | ||
| flagDecoder, | ||
| args, | ||
| impl.__$init, | ||
| impl.__$update, | ||
| impl.__$subscriptions, | ||
| function() { return function() {} } | ||
| ); | ||
| var init = function(args) | ||
| { | ||
| return _Platform_initialize( | ||
| flagDecoder, | ||
| args, | ||
| null, | ||
| null, | ||
| null, | ||
| function() { return function() {} }, | ||
| impl | ||
| ); | ||
| }; | ||
|
|
||
| /**__DEBUG/ | ||
| init.hotReloadData = { | ||
| __$impl: impl, | ||
| __$platform_effectManagers: _Platform_effectManagers, | ||
| __$scheduler_enqueue: __Scheduler_enqueue | ||
| }; | ||
| //*/ | ||
|
|
||
| return init; |
There was a problem hiding this comment.
All functions that wrap _Platform_initialize (also in elm/browser) have been changed this way:
- Decrease from
F4toF3and move the last argument to an explicit function (calledinit) inside. That’s equivalent – just another way to express the curriedness. - Attach
.hotReloadDataon that inner function in__DEBUGmode.
The inner function is called init because that’s the init function that ends up at Elm.Main.init for example. So for each Elm entrypoint, there’s .init() to initialize an app as usual, and .init.hotReloadData for use when hot reloading already initialized apps of that entrypoint.
Code that I’ll comment on later removes .hotReloadData, so there’s no risk that people start depending on it.
| __Result_isOk(result) || __Debug_crash(2 /**__DEBUG/, __Json_errorToString(result.a) /**/); | ||
| var managers = {}; | ||
| var initPair = init(result.a); | ||
| var initPair = impl.__$init(result.a); |
There was a problem hiding this comment.
You’ll see this a lot in the diff: Instead of popping off stuff from from impl (like var init = impl.__$init; init()), we pass around the whole impl object and read from it all the time (impl.__$init()).
This allows us to simply mutate the impl object later (impl.__$init = newInit) to swap in new versions of the functions. That’s the core of how the hot reloading works!
| /**__DEBUG/ | ||
| app.hotReload = function(hotReloadData) |
There was a problem hiding this comment.
Just like with .hotReloadData that I mentioned earlier, code that I’ll comment on later removes this .hotReload method on apps, so there’s no risk that people will depend on it.
In other words, in non-production mode we “smuggle” some more data/functions out to _Platform_export so it can create Elm.hot.reload – that’s the only exposed thing.
| if (manager.__portSetup) | ||
| { | ||
| ports = ports || {}; | ||
| ports[key] = manager.__portSetup(key, sendToApp); |
There was a problem hiding this comment.
The reason for these changes is preventing a memory leak, that causes stopped apps never to be garbage collected. See the individual commits of this PR for the full story.
| obj[name] = _Platform_wrapInit(moduleName, exp); | ||
| scope['Elm'].hot.__hotReloadData[moduleName] = exp.hotReloadData; | ||
| delete exp.hotReloadData; |
There was a problem hiding this comment.
As mentioned earlier, this is where we remove .hotReloadData (from Elm.Main.init for example). Instead it is stored at Elm.hot.__hotReloadData["Main"] for example.
(Note that Elm.hot.__hotReloadData is a flat object. For Elm.Blog.Home the hot reload data is stored at Elm.hot.__hotReloadData["Blog.Home"].)
| var app = init(args); | ||
|
|
||
| var hotReload = app.hotReload; | ||
| delete app.hotReload; | ||
| var reloadFunction = function () | ||
| { | ||
| hotReload(scope['Elm'].hot.__hotReloadData[moduleName]); | ||
| }; | ||
| scope['Elm'].hot.__reloadFunctions.push(reloadFunction); |
There was a problem hiding this comment.
As mentioned earlier, this is where .hotReload is removed from app.
Instead we store it in Elm.hot.__reloadFunctions.
Also notice how app.hotReload is called with init.hotReloadData. In essence, this lets a running app get the impl from a new version of the same Elm entrypoint and update itself with it.
Also notice how we read from scope['Elm'].hot all the time. That’s because in _Platform_export__DEBUG we mutate stuff in scope['Elm'].hot when we hot reload.
It’s a bit mind-bend:y, but this allows apps initialized from some earlier version of Elm code have access to the latest stuff from the latest hot reload.
| return _List_fromArray(_List_toArray(xs).sort(function(a, b) { | ||
| var ord = A2(f, a, b); | ||
| return ord === __Basics_EQ ? 0 : ord === __Basics_LT ? -1 : 1; | ||
| return ord.$ === __Basics_EQ.$ ? 0 : ord.$ === __Basics_LT.$ ? -1 : 1; |
## Why [elm/core#1155](elm/core#1155) lets a development build reload itself through `Elm.hot.reload()`, with companion changes in `elm/browser` and `elm/virtual-dom`. The pull requests are still open, so this branch is an experiment: it uses them through the kernel patcher and keeps the elm-hot runtime for projects that do not patch. ## What changes - **Hot loader:** it injects both ways and the running code picks one. With `Elm.hot`, each new version of the module goes to `Elm.hot.reload()` and the elm-hot runtime stays off; without it, the elm-hot runtime works as before. The choice happens at runtime, because an optimized build still contains `Elm.hot` as dead code. When the hot loader is listed after the Elm loader, it now stops with a message that says to swap them. - **Patch archive:** it now holds `elm/core`, `elm/browser` and `elm/virtual-dom` from the branches of #1155, next to the existing `elm/html`. There is one patch set: a project either uses the patcher and gets every patch, or keeps the official kernel. - **Patches of one's own:** the patcher and the plugin take `patches`, a `.tar.gz` archive of a `patches/` folder or the folder itself. Such patches skip the check of the Elm version; the check of each package version against `elm.json` still applies. Archives are extracted into a temporary folder, not inside the installed package. Patches in another layout stop with the expected one, before anything is copied. - **Elm home:** the patcher, the plugin and the loader take an optional `elmHome`. Without it, each one uses the `ELM_HOME` of the environment, or `~/.elm`. A relative path starts from the folder that holds `elm.json`, or from `cwd` in the loader. To keep the patches to one project, give the plugin and the loader the same folder. - **Plugin:** it is renamed `ElmKernelPatcherPlugin`, and `isEnabled` also accepts a function that receives the webpack compiler. - **Changelog:** the entries under Unreleased in `CHANGELOG.md` describe each breaking change with how to fix it. ## Breaking changes - The plugin class is now `ElmKernelPatcherPlugin`, and its options type is `ElmKernelPatcherPluginOptions`. - `useArchive` is removed from the command line, the plugin and the library; `patches` replaces it. - `prepareArgs` takes an options object: `prepareArgs({ elmJsonFolder, elmHome, patches })`. - Projects that use the plugin now get the patched `elm/core` too, also in production builds; the hot reloading code is not in a build made with `--optimize`. ## Checks - Lint, format and every test suite pass. The tests cover relative and absolute paths for `elmHome` and `patches`, a patch archive and a patch folder of one's own, an Elm version outside the bundled ones, missing patches, and the `isEnabled` function. - The archive works with Elm 0.19.1 and 0.19.2. - In a browser with webpack-dev-server, with `elmHome: 'elm-home/elm-stuff'` on the plugin and on the loader: the update goes through `Elm.hot`, the model keeps its values, and `~/.elm` does not change. Without the patches, the update goes through the elm-hot runtime.
This takes all the learnings from elm-watch and implements hot reloading directly in Elm. I’m really happy with how it turned out, it’s pretty simple and clean. “Implements hot reloading” means that there’s now a
Elm.hot.reload()JavaScript function that tools can use. It’s up to the tool to do file watching and stuff like that.This also makes it possible to shut down an app, which is useful when embedding an Elm app in a web component or React component. Closes #886 and closes #1125
Those two are batched together since they touch on the same parts.
See the new javascript-interface.md file for all the details.
Companion PR:s:
Closes elm/browser#122 (since I’ve documented how to init apps).