How to reimport module with ES6 import
Asked Answered
O

2

6

I need to reimport module integration-test/integration upon every run as this module can have code dynamically changed in it at run time. I am using NodeJS with experimental modules in order to be able to run ES6 Javascript.

It seems require enables you to delete modules after you require them with the following code delete require.cache[require.resolve('./integration-test/integration.js')]. How do I replicate this with ES6 import?

//FIXME delete cached module here
import("./integration-test/integration").then((module) => {
    console.log('Importing integration');
    stub = module;
    if (module) resolve();
    else reject();
});

I do have a very inelegant solution to this in which I write a new file with a new name and then import that as a different module, however, this is far less than ideal and would like to avoid it if possible due to the memory leak issues.

Oulu answered 7/6, 2019 at 10:32 Comment(4)
From some reading, it appears that the import cache is not accessible. I'll leave the question open in case there is some workaround.Oulu
Why to you mutate the content of the file in side the module? one simple refactor, may help, expose a function that each call will give you the same result, then it is not a problem to invoke this function multiple times.Heterologous
@Heterologous the content of the file is code submit by a user via a frontend in a quasi-IDE environment and hence will change many times whilst the server is running. I have resolved the issue by migrating my codebase to use require rather than import and deleting the cache by means mentioned in my initial post.Oulu
Ok, thanx for the explanation :]Heterologous
J
7

You can use query string to ignore cache. See https://github.com/nodejs/help/issues/1399.

Here is the sample code.

// currentTime.mjs
export const currentTime = Date.now();
// main.mjs
(async () => {
  console.log('import', await import('./currentTime.mjs'));

  // wait for 1 sec
  await new Promise(resolve => setTimeout(resolve, 1000));
  console.log('import again', await import('./currentTime.mjs'));

  // wait for 1 sec
  await new Promise(resolve => setTimeout(resolve, 1000));
  console.log('import again with query', await import('./currentTime.mjs?foo=bar'));
})();

Run this with node --experimental-modules main.mjs.

$ node --experimental-modules main.mjs 
(node:79178) ExperimentalWarning: The ESM module loader is experimental.
import [Module] { currentTime: 1569746632637 }
import again [Module] { currentTime: 1569746632637 }
import again with query [Module] { currentTime: 1569746634652 }

As you can see, currentTime.mjs is reimported and the new value is assigned to currentTime when it is imported with query string.

Just answered 29/9, 2019 at 8:57 Comment(4)
Note: this generally ok, if you are only going to do it a couple times with small scripts. This does not work well, if you reload modules thousands of times or have very large scripts. This creates a memory leak, as essentially you have every "version" in memory and it never gets freed. If you are using NodeJS, its best to use a module like 'clear-module' to "free" the module before reimporting.Philbrick
You also have to do this for the entire import/export tree. If the thing you import also imports something, then it to must include a cache busting querystring. This solution doesn't really work for reloading everything below a single import.Basketball
I found a workaround (might not work for all use cases). If you need to re-import a dependency tree, you can run the task using node worker_threads, and re-import whatever it is you need there. You can do this multiple times and it doesn't seem to leak memory, as long as your workers are cleaned up. The docs have a good example of how to do this: nodejs.org/api/worker_threads.htmlBasketball
Does it supposed to work in the filesystem? If I add a query string, I get MODULE_NOT_FOUND. module type related settings: * moduleResolution": "node" * target": "ESNext" * "module": "NodeNext" * "esModuleInterop": true, And I have type: commonjs in package.json. I will try the solution in the previous comment. I believe that should be an answer here actually.Checkerberry
L
1

I ran into this issue while making an app that basically allowed developers to actively try out their code and make quick on-the-fly changes without recompiling the rest of the project every time. I didn’t want to have to reload the whole app every time a change was made. Using a query string (which is the current accepted answer) works wells when each module is standalone. But as mentioned does not work if the imported module also imports other modules itself. For example: module A imports module B imports module C. If you make changes to C and then try reimporting A or B by adding a query string, then the original version of C is still used and not reimported. The solution I found was to change how I was serving the js files I planned on dynamically (re)importing. I serve those files using a ResourceHandler on a jetty server (in java). So I override it's get Resource method to allow for an optional path part.

  ResourceHandler staticRes = new ResourceHandler() {
    
    @Override
    public Resource getResource(String path) {
      Resource resource = super.getResource(path);
      if( !resource.exists()) {
        int trimIndex = path.indexOf( "/", 1 );
        if(trimIndex > 1) {
          resource = super.getResource(path.substring(trimIndex));
        }
      }
      return resource;
    }
  };
  
  staticRes.setResourceBase( "../plugins/javascript/" );
  
  ContextHandler staticCtx = new ContextHandler();
  staticCtx.setContextPath( "/import" );
  staticCtx.setHandler( staticRes );
  ...

On my client side (TypeScript/JavaScript) I had a helper class that would do the dynamic imports and create the import URL

let importURL = serverURL + "/import/" + Date.now() + "/" + desiredImportFile;

As long as module A B and C all use relative imports then reimporting would import the newest versions of all the files. I am sure there is a better way to do this with jetty; I just knew I could make that way work. I suspect whatever tool you use to serve your dynamic js probably has some way of adding an optional path part as well. This is not really an elegant solution as it means that every time you import a module to use it you will have a different copy but the old imported modules are never cleaned up. This is not really a concern for me as this was really meant for a training and active development application. In production Date.now() would not be added to the path and the server doesn't even need to change.

Locksmith answered 12/4, 2022 at 14:41 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.