Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

How will hot reloading be accomplished? #22

Open
joeldenning opened this issue Aug 3, 2021 · 11 comments
Open

How will hot reloading be accomplished? #22

joeldenning opened this issue Aug 3, 2021 · 11 comments

Comments

@joeldenning
Copy link

joeldenning commented Aug 3, 2021

I see in the use cases documentation that it mentions hot reloading as a use case that you're hoping to support - is there a proposal for how that would be accomplished?

I am interested in it for the various node loaders that I help maintain at https://github.com/node-loader, such as an import maps loader, babel loader, http loader, postcss loader, etc. I've been using a user-land implementation of loader chaining to combine these, but am interested in also hot reloading them. I explored it a bit with adding query parameters to the module urls during resolve(), which succeeds in reinstantiating the module but does not delete or replace the older, existing module.

I also help maintain SystemJS, which has its own loader implementation, so I'm interested to see if the nodejs implementation has any similarities.

@giltayar
Copy link
Contributor

giltayar commented Aug 3, 2021

Not totally sure, but this article that explains how I implemented "proxyquire" like functionality for ESM may help. Mocking libraries such as these need to enable importing different module implementations on every import, which they do by using the fact that module cache key is a URL (with query parameters that can be manipulated). This is probably similar to what you'd want to do with hot reloaders?

https://dev.to/giltayar/mock-all-you-want-supporting-es-modules-in-the-testdouble-js-mocking-library-3gh1

@joeldenning
Copy link
Author

Thanks for the response - the approach in the quibble article is similar to what I've experimented with that adds query strings to specifiers during the resolve hook. However, the problem with that approach is that, as far as I know, there's no current way of replacing a module in the nodejs registry - when you add or modify the query parameter during resolve, it just instantiates a new module rather than replacing the module at the file path. So Quibble has to expose a global module api since using the nodejs module loader isn't sufficient.

The use case I've been exploring is a project similar to nodemon or node-dev, except hot reloading the ES modules that changed rather than restarting the nodejs process. For that, I think that an api would be needed to tell the nodejs module loader that it should re-instantiate the module (including calling getSource and transformSource again for it, and then updating live bindings for the exports to those of the reinstantiated module rather than the original module). In other words, the internal ModuleMap would set a new Module object for a URL.

The other part of it would be exposing an api for individual modules to clean up any memory when they are deleted from the module map. The import.meta.hot proposal is similar to this, except I believe (not sure) that it's intended use case is to update live bindings for a module without replacing it in the ModuleMap, rather than for cleaning up memory.

I'm sure there are lots of use cases to consider and lots of varying approaches that could be proposed. And I'm not sure if a full replacement of the Module within the ModuleMap would be feasible (eg its unclear what it would do if the new module has different exports than the previous one). If the approach I suggested above is interesting to you, I'm happy to propose an API for it. And if not, I am interested to hear others thoughts on what would be a good api.

@aral
Copy link

aral commented Aug 5, 2021

I’m currently working on a new Node.js server framework for the Small Web that is based on ESM loaders and so I’m also interested in this.

Just like everyone else, I’m currently just overloading modules using a random query string fragment but this is, of course, a memory leak. While this is perhaps tolerable during development, I’d also like to explore hot reloading in production where updates of the app are made possible without entirely restarting the server.

I’m pretty sure you’re far more aware of the considerations around this than I am but if there’s any feedback or assistance I can provide, just let me know and I’d be happy to.

@bmeck
Copy link
Member

bmeck commented Aug 6, 2021

there is an upcoming PR that implements this functionality and has a test case that allows replacing the global specifiers resolution nodejs/node#39240 ; the JS spec itself does not allow for replacement of values, only new values so indeed you must generate new module source texts and cannot remove references to old source texts.

@joeldenning
Copy link
Author

joeldenning commented Aug 6, 2021

there is an upcoming PR that implements this functionality

@bmeck I read through nodejs/node#39240, but it seems related more to communication between loaders and mocking, rather than with hot reloading? It's quite possible I'm missing something or that we're using different definitions of hot reloading.

the JS spec itself does not allow for replacement of values, only new values so indeed you must generate new module source texts and cannot remove references to old source texts.

I wasn't aware of this - could you share a link to the part of the spec that forbids this? I was under the impression that an API for manipulating the module registry was mostly left undefined rather than forbidden by current specs.

@joeldenning
Copy link
Author

joeldenning commented Aug 6, 2021

Just like everyone else, I’m currently just overloading modules using a random query string fragment but this is, of course, a memory leak. While this is perhaps tolerable during development, I’d also like to explore hot reloading in production where updates of the app are made possible without entirely restarting the server.

@aral this is also what I would look to solve for. I think that a module should be modifiable / replaceable without restarting the NodeJS process, since the query string approach results in memory leaks and also requires reloading all dependent modules (not just the changed module) so that they get the new version of the hot reloaded module.

Do you have any ideas for an API that would allow for this? Here are some of the thoughts I had when thinking about possible solutions:

// One idea
import.meta.reload('./some-file.js').then(() => {
  console.log("All loader hooks, including getSource/transformSource were re-run. The resulting module replaced the old module entirely. Live bindings for dependent modules have been updated")
});

// Another idea
import.meta.delete('./some-file.js')
import('./some-file.js').then(() => {
  console.log("All loader hooks, including getSource/transformSource were re-run. The resulting module replaced the old module entirely. Live bindings for dependent modules have been updated")
})

// A rough idea of how modules could fully clean up memory when being hot reloaded
import.meta.hot.dispose = function () {
  console.log("This module is about to be deleted from the registry and unlinked! Time to clean up any memory")
}

@hpx7
Copy link

hpx7 commented Aug 24, 2021

since the query string approach results in memory leaks and also requires reloading all dependent modules (not just the changed module) so that they get the new version of the hot reloaded module.

@joeldenning maybe I'm missing something but doesn't the query string approach fail to reload dependent modules since they are cached?

My use case is to reload a local module (along with its dependencies) every time it or its dependencies are modified. While this works for direct changes to the module, I'm not sure how to handle modifications to one of the module's local dependencies (simply re-importing the module doesn't work since it's dependencies are cached, and I don't want to have to require all imports of the module to use query strings for cache busting).

Basically I have code that looks like this:

import module from "module";
import dependencyTree from "dependency-tree";
import chokidar from "chokidar";

let impl = new (await import("../impl")).Impl();
const deps = dependencyTree.toList({
  directory: ".",
  filename: module.createRequire(import.meta.url).resolve("../impl"),
  filter: (path) => !path.includes("node_modules"),
});
chokidar.watch(deps).on("change", async () => {
  try {
    impl = new (await import(`../impl.ts#${Math.random()}`)).Impl(); // use query string for cache busting
  } catch (e) {
    console.error("Failed to reload:", e);
  }
});

This works for direct edits to impl.ts but not for edits to one of impl.ts's dependencies.

Once chained loaders are implemented maybe I can use something like https://github.com/vinsonchuong/hot-esm to solve this

@joeldenning
Copy link
Author

maybe I'm missing something but doesn't the query string approach fail to reload dependent modules since they are cached?

Yes, the dependent modules are not reinstantiated unless you add a query string to them, too.

This is part of why I do not consider the query string approach to be hot reloading - it's just adding more modules to the registry without reloading existing modules. As far as I know, there is no current method of achieving real hot reloading where a module and its dependencies are updated.

@DerekNonGeneric
Copy link
Contributor

Hi @joeldenning,

I am interested in it for the various node loaders that I help maintain at https://github.com/node-loader, such as an import maps loader, babel loader, http loader, postcss loader, etc.

Thank you for organizing a set of use-cases as you have. I have not had a moment to review all of what you have collected, but I have another loader (loader339) that I have been meaning to transfer to the @nodejs organization after some improvements are made. It is intended to support the APM use case. We have sort of been using it to help guide the business use-cases (just made it public for your viewing pleasure, but might keep it public since it is already receiving stars in private).

How will hot reloading be accomplished?

You may need to make your own module graph. That is not really a case I have been working on, but thanks for bringing it up — surely someone will want to get that solved as well.

@hpx7
Copy link

hpx7 commented Oct 6, 2021

I wanted to link a hot-reloading loader which has been sufficient for my use case: https://github.com/vinsonchuong/hot-esm

@laverdet
Copy link

laverdet commented Aug 8, 2023

I implemented a full-featured hot reloading loader:
https://www.npmjs.com/package/dynohot

It supports live bindings like Webpack, and the full .accept(specifier)-style hot interface. Great for running an API server without nodemon or similar nuclear solutions.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

7 participants