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.
Both incoming and outgoing ports stored the initial converter in a local variable, so they never saw new converters from mutated managers. This commit fixes that problem by accessing the converter from the manager object every time.
If two app instances both gain a new effect, it is only set up for the first one. That’s because we checked the global `_Platform_effectManagers` to detect new effect managers, rather than the instance local `managers`.
Followup on commit f88f4a8. That commit fixed things for most effect managers, but not ports. If two app instances both gain the same new port, it is only set up for the first one, due to the global `_Platform_effectManagers`. This commit keeps track of effect managers per app instance in DEBUG mode, so it works for ports too.
The previous solution worked for the first hot reload. But if for example a `Time.every` was started after that, it would stay on the second hot reload’s `_Scheduler_enqueue`, while effect managers from the original code gets updated to a newer version of the function. This commit fixes the issue by always using the `_Scheduler_enqueue` from the original code.
Hot reloading an app re-draws it, which can trigger side effects from custom elements via `connectedCallback`, and the side effect can be to stop the app.
`app.stop` and `app.detachView` now take completion callbacks instead of returning the final DOM node. That’s because the final DOM node can’t be known until the final rendering is done, which is complicated due to custom element lifecycle callbacks. This also allows for implementing a message queue in the future. A message queue would make the mental model simpler: An update cannot run while another update is already running, and a draw cannot start during another draw. That queueing also means that we can’t return the final DOM node immediately.
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:
The fact that custom element can synchronously cause side effects while rendering complicates everything. The mental model would be much simpler if we could guarantee that an update cannot happen during another update, and a draw cannot start during another draw. That could be done by having a message queue. However, that requires a coordinated release with elm/core, which is out of scope for this PR. Either way, the API:s in this PR are prepared for that:
app.stopandapp.detachViewtake completion callbacks rather than synchronously returning the final DOM node, so they should work even with a future message queue.Closes elm/browser#122 (since I’ve documented how to init apps).