Dynamic import from npm package?

I notice all the examples of dynamic importing use imports from within the same meteor project (E.g. import('./myComponent')). Is it possible to dynamically load from an npm package? I wrote a React component to do the dynamic loading (didn’t know about react-loadable, ops) and as long as there is a static import for that package somewhere in the project, it will load fine. If the all the references to the npm-module are dynamic, I see the following in the browser console:

Uncaught (in promise) Error: Cannot find module 'name-of-npm-module'
    at makeMissingError (modules-runtime.js:231)
    at Function.require.resolve (modules-runtime.js:263)
    at Module.resolve (modules-runtime.js:121)
    at modules-runtime.js:183
    at meteor.js:1109
    at <anonymous>

This is coming from import('name-of-npm-module') from within my react loading component. If I add import { SomeComponent } from 'name-of-npm-module'; Anywhere in my client-side code, it will load fine. I’m using Meteor 1.5.1.

Can we not dynamically load npm packages? Is there something else preventing the loading?

Thanks,
Dave

I should have also mentioned, the npm package is in my package.json, of course.

1 Like

You need to destructure your import statement. See this thread:

Maybe I’m misunderstanding, but I’m not sure why destructuring would matter in this case. I see where you need destructuring to pick out the component you need from the module (or default), but in this case import() can’t even seem to find the npm module. Destructuring the particular export out of the module won’t help because we don’t have the module yet, right? I feel like we are talking about two different things. I must be missing something.

I’m mapping what used to be:

import { someComponent } from 'some-npm-module';

to this:

const pathOrModuleName = 'some-npm-module';
const componentName = 'someComponent';
const theComponent = import(pathOrModuleName).then(module => module[componentName])

Which is pretty much straight out of the first example in the blog.

Path and component are being passed in, in this case, from react props, but I’m trying to simplify for anyone who might be reading this thread who isn’t familiar with react.

If I have import 'some-npm-module' anywhere in my client code, it works fine. If it is not there, I get the error. Of course, that doesn’t help because then ‘some-npm-module’ is in my bundle and it’s no smaller.

I don’t need the extra destructuring step from the link you provided since I’m not importing multiple modules in one go with Promise.all.

1 Like

You did also install the package?

meteor npm i

Sorry for my late reply.

Yes, the package is installed. In fact, our development startup script runs meteor npm install every time we start, just in case.

So, just to bottom this out, is the package in your package.json? If it’s not, meteor npm install will never put it there. In other words, when you first installed the package did you use meteor npm install --save some-package-name?

Yes. It’s in there and it would never work, regardless of dynamic import, without it.

This is an old project that started in the pre-1.0 days and upgraded through the versions. Could there be a missing atmosphere package (in .meteor/packages)? It’s using react, not blaze, and many of the blaze-related packages have been removed. Are there dependencies of dynamic-import that wouldn’t get pulled in automatically?

Oops - forgot you’d stated that in the OP.

It may be worth bumping package versions with meteor update --all-packages. I’m assuming the ecmascript package is installed or you’d have seen syntax errors beforehand?

You could also try deleting node_modules completely and refresh it from scratch - I’ve seen some posts in the forums which suggest that changing npm versions through updates can leave node_modules in a weird state.

I deleted the project’s node-modules folder and ran meteor npm install followed by meteor update --all-packages.
From meteor, I received:

Changes to your projects package version selections from updating package versions:
                                              
ecmascript-runtime-client  upgraded from 0.4.2 to 0.4.3
mongo                      upgraded from 1.1.19 to 1.1.22
npm-mongo                  upgraded from 2.2.24 to 2.2.30

The ecmascript-runtime update made me hopeful, but alas, nothing changed.

Interestingly, when I added console.log(import('name-of-npm-module').then(r=>console.log('done:',r))); to the page component’s render method and, I this in the browser console:

...
done: Module { all the modules exports here }

This also correctly loads the component on the page, just like it was statically imported. So it is working, but just not when I load only from the dynamic react component. Now I need to dig into determining the difference between the component and the page’s render method. At least it’s a start. I find it incredibly strange that the react component can’t see the npm module until something else loads it, and then it can see it just fine.

Thanks for your help. I’ll do some digging and report back if I find anything definitive. Maybe it will save someone else some time.

Thanks!
Dave

1 Like

Update: Some interesting (interesting as in confusing) data here. From within my react component’s render() method, this works:

import('npm-module')
      .then(r => console.log('Imported', r))
      .catch(error => console.log('Caught Error from import:', error.stack));

This doesn’t (wrapping the import call in a closure):

const doimport = p => import(p);
doimport('npm-module')
      .then(r => console.log('Imported', r))
      .catch(error => console.log('Caught Error from import:', error.stack));

And neither does this (providing a const string to import):

const name = 'npm-module';
import(name)
      .then(r => console.log('Imported', r))
      .catch(error => console.log('Caught Error from import:', error.stack));

So the ONLY way it works for me is if you statically provide the npm-module name to import (e.g. the name is literally present at compile-time without any variable substitution etc). I’ve also tracked down at least a next step as to why they fail.

It comes down to modules-runtime.fileAppendId (NOTE: looking at the source of fileAppendId would be helpful here):

  • When import() works, ‘npm-module’ is present in the file.content of node_modules. That is fileAppendId(File[node_modules], ‘npm-module’, [extensions array]) returns a valid object corresponding to ‘npm-module’, as it should to import/require the package.

  • When import() does not work, ‘npm-module’ is not present in the file.content of node_modules. Thus, the client has no idea this package exists and it can never be resolved.

I know this is probably not shocking (a missing file can’t be resolved, right?), but it’s verification that the import/require process is itself working as it should, but that the ‘npm-module’ is completely missing (from the client’s point of view) in failure cases.

Next up: to find out where that module gets lost.

UPDATE:
In working cases, web.browser/dynamic/node_modules/ contains ‘npm-modules’ and in non-working cases, the npm module is missing. So…

Throwing the ball back to @robfallows or anyone else who wants to chime in. How can I get ‘npm-module’ to stay in my dynamic/node_modules folder if it’s not imported literally?

Am I going to have to have a file that includes a bunch of import(‘npm-module’) statements and never reference the file? Nope. That doesn’t work.

As a workaround I changed my react component to take the import promise as a param instead of the import path. Thus I changed the signature from

<DynamicComponent modulePath='npm-module' component='some-component' />

to

<DynamicComponent module={import('npm-module')} component='some-component' />

This allows the compiler to see the import and everything is happy. The only other solution that I can see would be for there to be some sort of “hint” provided in the code to the compiler that a dependency on a npm module exists even though it’s not literally specified within the import(…). I could see something like

@dependency 'npm-module'

at the top of a .js file to provide such a hint. I doubt that’s on the roadmap for now.

@robfallows, if you have other ideas or if I’m missing something, let me know . Otherwise, I think this issue is resolved. Thanks again for your help.

1 Like

Can you show how you implemented DynamicComponent ?

So can we import npm modules dynamically? if yes none of the above answers worked for me.
Can anyone give a short example perhaps on github of working code?

Try something like this:

render() {
    const resolveComponent = ({ dynamicModule, componentName, children, ...props }) => {
      dynamicModule && dynamicModule.then(resolvedModule => {
        const { state = {} } = this;
        const component = resolvedModule[componentName];
        this.element = React.createElement(component, props, children);
        this.setState(state); // trigger re-render
        // do not return and add & null after this
        // to wait for the promise to resolve before rendering
      }).catch(error => {
        window.console && console.error(error && error.stack || error);
      });
      return null;
    };
    return (
      <div>{resolveComponent(this.props)}</div>
    )
  }

Thanks but if I import React dynamically I get error React is not defined and nothing loads on page.
in your above code if I do <DynamicComponent modulePath='React' component="Login" />

and in DynamicComonent

export default class DynamicComponent extends React.Component{
render(){
//your code
}
}
will the React.Component work?

You never mentioned that you intended to load react dynamically. If your app is based on react, my first thought is that React should be in your statically-loaded application bundle. If you want to do it dynamically, you’ll have to start a separate thread. That’s off-topic for this one. I was simply discussing a dynamically-loaded component.

Re: in your above code if I do <DynamicComponent modulePath='React' component="Login" /> ... will the React.Component work?

There is much more that I added to that component that I was not making public here. My goal is to help you by providing a strong starting-point to get your version working, but I was not intending to make a component you could simply drop-in to your app. Asynchronous programming can be hard. It’s best to understand what you are doing, rather than just copying and pasting. Because there was other code that I removed, there is a good chance that I forgot something.

I suggest too, though, that modulePath and component are not prop names that are even considered in the resolveComponent inner function so your render line will not work as written. Maybe something along the lines of<DynamicComponent dynamicModule={import('myNpmModule')} componentName="someExportedComponent" aPropName="something" anotherProp="somethingElse"/>?

Dave

1 Like

Thanks, I was just looking for a possibility of importing npm modules dynamically that I am using in my project along with react components of my app which is pretty easy.