Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Dynamically loading an external webpack bundled ngModule as a route handler

We want to divide our large frontend projects into multiple separately deployed projects which are easier to work with. I am trying to include a bundled ngModule to handle a route from within another app. The apps must be ignorant of each other's configuration. The bundles will share some large dependencies(like Angular) via globals. We don't need to shake across the bundles and we may just have to accept some duplicate dependencies.

The root router complains that

Error: No NgModule metadata found for 'TestsetModule'.

which leads me to believe the child module is not being angular compiled on load, or is not registering its module for some reason. I think it may be necessary to manually compile the module, but I'm not sure how to use this https://angular.io/api/core/Compiler#compileModuleAndAllComponentsAsync

The root app loads the child via a route:

import { ModuleWithProviders } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
const load = require("little-loader");


const routes: Routes = [
  { path: ``, loadChildren: () => new Promise(function (resolve) {
      load('http://localhost:3100/testset-module-bundle.js',(err: any) => {
        console.log('global loaded bundle is: ', (<any>global).TestsetModule )
        resolve((<any>global).TestsetModule)
      }
    )
  })}
];

export const HostRouting: ModuleWithProviders = RouterModule.forRoot(routes);

I also tried using angular router's string resolution syntax rather than this weird global thing you see but I had similar issues.

Here is the module which is being loaded, very standard except for the global export:

import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { HttpModule } from '@angular/http';
//import { MaterialModule } from '@angular/material';
import { FlexLayoutModule } from '@angular/flex-layout';
import { FormsModule }   from '@angular/forms';
import { LoggerModule, Level } from '@churro/ngx-log';

import { FeatureLoggerConfig } from './features/logger/services/feature-logger-config';


import { TestsetComponent } from './features/testset/testset.component';
import { TestsetRouting } from './testset.routing';

@NgModule({
    imports: [
        CommonModule,
        //MaterialModule,
        FlexLayoutModule,
        HttpModule,
        FormsModule,
        LoggerModule.forChild({
          moduleName: 'Testset',
          minLevel: Level.INFO
        }),
        TestsetRouting,
    ],
    declarations: [TestsetComponent],
    providers: [
      /* TODO: Providers go here */
    ]
})
class TestsetModule { }
(<any>global).TestsetModule = TestsetModule

export {TestsetModule as default, TestsetModule};

Here is the webpack configuration of the root bundle. Note the global exports via the poorly named "ProvidePlugin".

const webpack = require('webpack');
const AotPlugin = require('@ngtools/webpack').AotPlugin;
const path = require('path');
const BrowserSyncPlugin = require('browser-sync-webpack-plugin');
const IgnorePlugin = require('webpack/lib/IgnorePlugin');
const PolyfillsPlugin = require('webpack-polyfills-plugin');
const WebpackSystemRegister = require('webpack-system-register');


module.exports = (envOptions) => {
    envOptions = envOptions || {};
    const config = {

        entry: {
          'bundle': './root.ts'
        },
        output: {

          libraryTarget: 'umd',
          filename: '[name].js',//"bundle.[hash].js",
          chunkFilename: '[name]-chunk.js',
          path: __dirname
          },
          externals: {

          },
        resolve: {
            extensions: ['.ts', '.js', '.html'],
        },
        module: {
            rules: [
                { test: /\.html$/, loader: 'raw-loader' },
                { test: /\.css$/, loader: 'raw-loader' },

            ]
        },
        devtool: '#source-map',
        plugins: [
          new webpack.ProvidePlugin({
            'angular': '@angular/core',
            'ngrouter': '@angular/router',
            'ngxlog':'@churro/ngx-log'
          })

        ]
    };
    config.module.rules.push(
      { test: /\.ts$/, loaders: [
        'awesome-typescript-loader', 
        'angular-router-loader',
        'angular2-template-loader', 
        'source-map-loader'
      ] } 
    );
  }



    return config;
};

And here is the webpack configuration of the child bundle. Note the "externals" which look for angular as a global.

module.exports = (envOptions) => {
    envOptions = envOptions || {};
    const config = {
        entry: {
          'testset-module-bundle': './src/index.ts'
        },
        output: {
          //library: 'TestsetModule',
          libraryTarget: 'umd',
          filename: '[name].js',//"bundle.[hash].js",
          chunkFilename: '[name]-chunk.js',
          path: path.resolve(__dirname, "dist")
          },
          externals: {
            //expect these to come from the app that imported us
            // name to be required : name from global
             'angular': '@angular/core',
             'ngrouter': '@angular/router',
             'ngxlog': '@churro/ngx-log'       
          },
        resolve: {
            extensions: ['.ts', '.js', '.html'],
        },
        module: {
            rules: [
                { test: /\.html$/, loader: 'raw-loader' },
                { test: /\.css$/, loader: 'raw-loader' },

            ]
        },
        devtool: '#source-map',
        plugins: [

        ]
    };

    config.module.rules.push(
      { test: /\.ts$/, loaders: [
        'awesome-typescript-loader', 
        'angular-router-loader',
        'angular2-template-loader', 
        'source-map-loader'
      ] }
    );
  }



    return config;
};

And for good measure here is my tsconfig file which 'awesome-typescript-loader' reads.

{
  "compilerOptions": {
    "target": "es5",
    "module": "es2015",
    "moduleResolution": "node",
    "sourceMap": true,
    "emitDecoratorMetadata": true,
    "experimentalDecorators": true,
    "removeComments": false,
    "noImplicitAny": true,
    "suppressImplicitAnyIndexErrors": true,
    "baseUrl": ".",
    "rootDir": "src",
    "outDir": "app",
    "paths": {
      "@capone/*": [
        "*"
      ],
      "@angular/*": [
        "node_modules/@angular/*"
      ],
      "rxjs/*": [
        "node_modules/rxjs/*"
      ]
    }
  },

  "exclude": ["node_modules", "src/node_modules", "compiled", "src/dev_wrapper_app"],
  "angularCompilerOptions": {
    "genDir": "./compiled",
    "skipMetadataEmit": true
  }
}

If you're still reading, awesome. I was able to get this working when both bundles are part of the same webpack config and the child module is just a chunk. Angular is designed to do that. But our use case is to have the children and parent be ignorant of each other until runtime.

like image 734
light24bulbs Avatar asked Oct 30 '17 15:10

light24bulbs


1 Answers

As you have mentioned

The apps must be ignorant of each other's configuration.

I had a similar problem in Angular2. I solved it by creating a sub-application. A separate sub-main.browser.ts and index.html file. It had its own dependencies, sharing the same node modules. Both main modules bootstrapping different app-component. We were working on Angular without angular-cli.

In webpack config, I added

entry: {

  'polyfills': './src/polyfills.browser.ts',
  'main' .   :     './src/main.browser.aot.ts',
  'sub-main' : '/src/sub-main.browser.ts'

},

and a more detailed HtmlWebpackPlugin. In the chunks, we load only modules that will be used in both the app. If we see polyfills is common.

   new HtmlWebpackPlugin({
    template: 'src/index.html',
    title: METADATA.title,
    chunksSortMode: 'dependency',
    metadata: METADATA,
    inject: 'head',
    chunks: ['polyfills','main']
  }),

  new HtmlWebpackPlugin({
    template: 'src/index2.html',
    title: 'Sub app',
    chunksSortMode: 'dependency',
    metadata: METADATA,
     inject: 'head',
    filename: './sub-app.html',
    chunks: ['polyfills','sub-main']
  }),

The next task was to create separate endpoints for both sub apps for dev environment.

devServer: {
      port: METADATA.port,
      host: METADATA.host,
      historyApiFallback: true,
      watchOptions: {
        aggregateTimeout: 300,
        poll: 1000
      },
      proxy: {
   "/sub-app": {
    target: "http://localhost:3009",
    bypass: function(req, res, proxyOptions) {
        return "/index2.html";
    }
  }
}
    },

Now when I build the project two different HTML files are generated. Each with their own javascript bundle dependencies and common assets. They can be deployed on a different server as well.

I was able to finish my POC with lots of trial and error. My suggestion will be to look a step above angular. See how webpack is deploying your current project. And If you can configure it to serve your purpose.

like image 148
Sharuk Ahmed Avatar answered Oct 23 '22 18:10

Sharuk Ahmed