Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

dynamically include another webpack bundle inside an angular application

I'm working on an Angular2 application that are bundled together with webpack. Everything works just fine. However this bundled application will be shipped independant and on runtime, before bootstrapping the app, I need to specify other NgModules into one of the modules I have bundled in my app. Basically think of it as two bundles included in index.html, and when bootstraped, bundle 1 goes into bundle2. Only bundle 2 bootstraps an app. So I've done a few experiments that I'd like to share with you to shed some light on this (I might be taking a rather dirty and completely wrong approach, so please correct if so. I don't mind dirty if it is the only way it can work though.). Only requirement is that nothing should be done during transpilation. The requirement is that I can include one or more bundles on my index.html and they can be hooked into the actual app without any prior knowledge of each other.

The reason I'm doing this in the first place is because i work on a platform where other developers can hook in code by installing extensions. This means that I deliver the initial bundle, that has no knowledge of other bundles on transpile time what-so-ever. This was easy in AngularJS, but moving to Angular2 and onwards, this is not so easy anymore.

I managed to delay bootstrapping the app with the following approach:

In main.ts:

window["platformBrowserDynamicRef"] = platformBrowserDynamic();
window["appModule"] = AppModule;

This works fine and when i call bootstrap manually, the app starts up just fine. I've created another bundle that independently works fine as another app.

When i try to bundle them together by importing the components in an implicit array like above, and i bootstrap the app, i get an error i don't know what to do about.

window["app"].modulesImport.push(CommonModule);
window["app"].modulesImport.push(FormsModule);
window["app"].modulesImport.push(BrowserModule);
window["app"].modulesImport.push(NgGridModule);
window["app"].modulesImport.push(ReactiveFormsModule);
window["app"].modulesImport.push(BrowserAnimationsModule);
window["app"].modulesImport.push(HttpModule);

@NgModule({
  imports: window["app"].modulesImport,
  declarations: [
      DYNAMIC_DIRECTIVES,
      PropertyFilterPipe,
      PropertyDataTypeFilterPipe,
      LanguageFilterPipe,      
      PropertyNameBlackListPipe      
  ],
  exports: [
      DYNAMIC_DIRECTIVES,
      CommonModule,
      FormsModule,
      HttpModule
  ]
})
export class PartsModule {

    static forRoot()
    {
        return {
            ngModule: PartsModule,
            providers: [ ], // not used here, but if singleton needed
        };
    }
}

Having the modules in an implicit array works fine, but as soon as i add a module from my other bundle i get the following error:

Uncaught Error: Unexpected value 'ExtensionsModule' imported by the module 'PartsModule'. Please add a @NgModule annotation.
    at syntaxError (http://localhost:3002/dist/app.bundle.js:43864:34) [<root>]
    at http://localhost:3002/dist/app.bundle.js:56319:44 [<root>]
    at Array.forEach (native) [<root>]
    at CompileMetadataResolver.getNgModuleMetadata (http://localhost:3002/dist/app.bundle.js:56302:49) [<root>]
    at CompileMetadataResolver.getNgModuleSummary (http://localhost:3002/dist/app.bundle.js:56244:52) [<root>]
    at http://localhost:3002/dist/app.bundle.js:56317:72 [<root>]
    at Array.forEach (native) [<root>]
    at CompileMetadataResolver.getNgModuleMetadata (http://localhost:3002/dist/app.bundle.js:56302:49) [<root>]
    at CompileMetadataResolver.getNgModuleSummary (http://localhost:3002/dist/app.bundle.js:56244:52) [<root>]
    at http://localhost:3002/dist/app.bundle.js:56317:72 [<root>]
    at Array.forEach (native) [<root>]
    at CompileMetadataResolver.getNgModuleMetadata (http://localhost:3002/dist/app.bundle.js:56302:49) [<root>]
    at JitCompiler._loadModules (http://localhost:3002/dist/app.bundle.js:67404:64) [<root>]
    at JitCompiler._compileModuleAndComponents (http://localhost:3002/dist/app.bundle.js:67363:52) [<root>]

I've also tried to AoT compile the ExtensionsModule which doesn't work either. I also tried dynamically compiling the components on runtime using the JitCompiler to see if I could get them into the app that way around. Same error though.

Are components decorated during boostrapping of the app or during transpilation? Peeking at the modules in my implicit array I can see they are decorated, but mine is not. Is that webpack inserting the calling code of the decorator method? I assume the decorator information is lost during transpilation, so maybe i need to temporary store it somehow so i can grab it elsewhere. (note that extensionsmodule in as the first element doesn't contain the decorator array)

modules that should be imported in my module

I guess Im looking for pointers on how to move forward from here.Maybe the NgModule annotation function would be a good place to start. Couldn't find that anywhere though.

EDIT

digging more into the issue I've tried the lazy loading approach using the Router, where first I'm getting this error, trying to load it with a js file (network tab shows that it never issues the request for the file):

const appRoutes: Routes = [
      {
        path: 'edit-entity/:type/:id/:culture',
        component: EntityEditor,
        resolve: {
            entity: EntityResolver,
            navigation: NavigationResolver
        },
        data: { shouldDetach: true},
        loadChildren: "/dist/extensions.bundle.js#ExtensionsBundle",

      },
      {
        path: '',
        component: Dashboard,
        resolve: {
            navigation: NavigationResolver
        },
        data: { shouldDetach: true}        
      }
    ];

Resolves in the following error: "Could not find module with name ExtensionsModule.js"

Secondly i tried using a loadChildren function like this:

const appRoutes: Routes = [
      {
        path: 'edit-entity/:type/:id/:culture',
        component: EntityEditor,
        resolve: {
            entity: EntityResolver,
            navigation: NavigationResolver
        },
        data: { shouldDetach: true},
        loadChildren: () => {
            //"/dist/extensions.bundle.js#ExtensionsModule",            
            // return System.import('./dynamic.module').then((comp: any) => {
            //     return comp.otherExport;
            // });

            return window["lux"].modulesLazyImport[0];
        }
      },
      {
        path: '',
        component: Dashboard,
        resolve: {
            navigation: NavigationResolver
        },
        data: { shouldDetach: true}        
      }
    ];

Resolves in:

app.bundle.js:1548 ERROR Error: Uncaught (in promise): Error: No NgModule metadata found for 'ExtensionsModule'.
Error: No NgModule metadata found for 'ExtensionsModule'.
    at NgModuleResolver.resolve (app.bundle.js:55709) [angular]
    at CompileMetadataResolver.getNgModuleMetadata (app.bundle.js:56288) [angular]
    at JitCompiler._loadModules (app.bundle.js:67404) [angular]
    at JitCompiler._compileModuleAndComponents (app.bundle.js:67363) [angular]
    at JitCompiler.compileModuleAsync (app.bundle.js:67325) [angular]
    at ModuleBoundCompiler.compileModuleAsync (app.bundle.js:67693) [angular]
    at MergeMapSubscriber.project (app.bundle.js:30608) [angular]
    at MergeMapSubscriber._tryNext (app.bundle.js:69744) [angular]
    at MergeMapSubscriber._next (app.bundle.js:69734) [angular]
    at MergeMapSubscriber.Subscriber.next (app.bundle.js:14584) [angular]
    at ScalarObservable._subscribe (app.bundle.js:69206) [angular]
    at ScalarObservable.Observable.subscribe (app.bundle.js:118) [angular]
    at MergeMapOperator.call (app.bundle.js:69709) [angular]
    at Observable.subscribe (app.bundle.js:115) [angular]
    at NgModuleResolver.resolve (app.bundle.js:55709) [angular]
    at CompileMetadataResolver.getNgModuleMetadata (app.bundle.js:56288) [angular]
    at JitCompiler._loadModules (app.bundle.js:67404) [angular]
    at JitCompiler._compileModuleAndComponents (app.bundle.js:67363) [angular]
    at JitCompiler.compileModuleAsync (app.bundle.js:67325) [angular]
    at ModuleBoundCompiler.compileModuleAsync (app.bundle.js:67693) [angular]
    at MergeMapSubscriber.project (app.bundle.js:30608) [angular]
    at MergeMapSubscriber._tryNext (app.bundle.js:69744) [angular]
    at MergeMapSubscriber._next (app.bundle.js:69734) [angular]
    at MergeMapSubscriber.Subscriber.next (app.bundle.js:14584) [angular]
    at ScalarObservable._subscribe (app.bundle.js:69206) [angular]
    at ScalarObservable.Observable.subscribe (app.bundle.js:118) [angular]
    at MergeMapOperator.call (app.bundle.js:69709) [angular]
    at Observable.subscribe (app.bundle.js:115) [angular]
    at resolvePromise (app.bundle.js:77503) [angular]
    at resolvePromise (app.bundle.js:77488) [angular]
    at :3000/dist/app.bundle.js:77537:17 [angular]
    at Object.onInvokeTask (app.bundle.js:4599) [angular]
    at ZoneDelegate.invokeTask (app.bundle.js:77289) [angular]
    at Zone.runTask (app.bundle.js:77179) [<root> => angular]
    at drainMicroTaskQueue (app.bundle.js:77433) [<root>]
    at HTMLDivElement.ZoneTask.invoke (app.bundle.js:77364) [<root>]

EDIT I thought that maybe the error was because my annontations were never called. Not sure if that is the case. I desiced to go ES5 on it and push a component to my implicit list like below. The same error originates after all when trying to push a component to the declarations part of the parts.module.ts.

    var HelloWorldComponent = function () {

};

HelloWorldComponent.annotations = [
    new ng.core.Component({
        selector: 'hello-world',
        template: '<h1>Hello World!</h1>',
    })
];

window["lux"].componentsLazyImport.push(HelloWorldComponent);

So is this a bug in Angular 4.0.0 or is it not possible at all to create a plugin mechanism?

Best regards Morten

like image 938
Morten Skjoldager Avatar asked Apr 04 '17 09:04

Morten Skjoldager


1 Answers

The only way to achieve this I could find was to drop webpack, as that seems to want all information at build time, and use SystemJS which at runtime can load modules it does not know about

like image 123
Steverob2k Avatar answered Sep 24 '22 08:09

Steverob2k