Search code examples
user-interfaceangularpluggable

How to load Typescript module from a file known at runtime (in angular2+electron)


I'm trying to write a little Prove of Concept to find out whether "Angular2 in typescript + Electron" will fullfill the requirements of a future Desktop App project.

But I'm having trouble... Am I trying to achieve the impossible or is there a way to do it? Here's my context:


Short version:

How can i make this run:

@Injectable()
export class PluginManager {    
  getPluginComponentForEventType(eventtype: string): Type {
    //return is angular "Type", later consumed by Dynamic Loader
    var externalModule = require('../mtplugins/' + eventtype + 'plugin.ts');
    return externalModule.TheRenderer;
  }
}

Without getting this error:

"../mtplugins/foorendererplugin.ts not declared as a dependency"

Long Version with Context:

Core requirement is a kind of "dynamic frontend plugin system" as I would call it. This means: One part of my UI must be replaceable at runtime - but I don't know the components that will be inserted there in advance.

Example scenario:

My App receives a "foo" event:

leEvent: { eventtype: 'foo', payload: '...' }

So it will tell its Plugin manager: Hey I need a UI Component to render a 'foo' event.

The plugin manager searches for a 'fooplugin.ts' file which contains the component, loads the component class from this file dynamically and returns the type. If it does not have a foo plugin now, the user may download this file from the plugin market to the plugin folder and then tries again.

My app then uses DynamicComponentLoader.loadIntoLocation to integrate this dynamically loaded type to the dom and then tells the component to render(leEvent).

This last part with dynamic loading components in angular is no problem, my pain point is the plugin manager method to load a class from a file... The best thing I got so far is this:

@Injectable()
export class PluginManager {    
  getPluginComponentForEventType(eventtype: string): Type {
    //return is angular "Type", later consumed by Dynamic Loader
    var externalModule = require('../mtplugins/' + eventtype + 'plugin.ts');
    return externalModule.TheRenderer;
  }
}

I run my tsc with "--module commonjs".

When I call the method it tells me that "../mtplugins/foorendererplugin.ts not declared as a dependency"... well, I can't declare the dependency in advance because its only known at runtime.


So, is there a way to load typescript classes from files that are only known at runtime? In Java I think I would do it with a Classloader, so i feel like I need typescript's counterpart of that thing. I actually thought I've read that typescript with commonjs and require() would support this, so maybe this mechanism and angular's system.js integration are biting each other?

I'm new to angular2 and typescript and electron and node, so please forgive if I overlook something basic.


EDIT

As pointed out by Louy the error message is nothing typescript specific. It was thrown by System.js...

Again it turns out that a bit more time spent on learning the technologies you want to use is always a good investment. Indeed I did a novice mistake here: mixing up different module-loading systems. I thought I had to work with CommonJS to be able to load external modules at runtime, but runtime loading is well supported by System.js, too - that means:

instead of

require(..)

I simply had to use

declare var System: any; //System.js Loader
...
System.import('./app/' + eventtype+ 'plugin.js');

Then all you have to do is keep the plugins independent of actual code of the application's core (as this causes module chaos... and is an architectural smell either way), instead depend only on interfaces (Typescript ftw!).

And that's all you need for your pluggable angular2 UI :)


Solution

  • Why not make a dictionary of objects? You won't lose types this way.

    // import all of them...
    import pluginA from '../mtplugins/Aplugin.ts'
    
    // add all here
    const plugins: {[name: string]: /*typeof plugin*/} = {
      a: pluginA,
      // ...
    }
    
    // and then...
    
    var externalModule = plugins[eventtype];