Search code examples
angularwebpackgulpng-upgrade

AoT compilation for angular 8 downgraded module


In my, Angular project build is done by gulp, and I need to set up a hybrid angular app. Currently, I do it with webpack the following way: gulp runs webpack and injects it's bundle to dist folder previously produced by gulp. My webpack config:

module.exports = {
    mode: 'development',
    devtool: 'source-map',
    entry: path.resolve(__dirname, './src/main/app/app.module.angular.ts'),
    output: {
        filename: 'webpack.bundle.js'
    },
    module: {
        rules: [
            { test: /\.ts?$/, use: 'ts-loader', exclude: '/node_modules/' },
            { test: /\.html?$/, use: 'raw-loader' }
        ]
    },
    resolve: { extensions: ['.ts', '.js'] },
    plugins: [
        new htmlWebpackPlugin({
            template: '.' + outputPath + 'index.html',
            inject: false // <script> reference already in file
        })
    ],
    optimization: {
        minimize: false
    }
};

My gulp task that starts webpack:

gulp.task('main-webpack', (done) => {
    return gulp.src(path.join(paths.src, '/**/*.ts'))
        .pipe(webpackStream(require('../webpack.main.config.js'), webpack))
        .on('error', (err) => {
            gutil.log('WEBPACK ERROR', err);
        })
        .pipe(gulp.dest(gulp.paths.serve.main));
});

My angular downgraded module is defined and bootstrapped like this:

@NgModule({
  imports: [
    BrowserModule,
    CommonModule,
    HttpClientModule,
    TranslateModule.forRoot()
  ],
  declarations: COMPONENTS,
  entryComponents: COMPONENTS,
  providers: [...]
})

export class MainAppModule {
  constructor() { }
  ngDoBootstrap(){}             // required to start hybrid app
}

const bootstrapFn = (extraProviders: StaticProvider[]) => {
  const platformRef = platformBrowserDynamic(extraProviders);
  return platformRef.bootstrapModule(MainAppModule);
};

const MainAngularDowngradedModule = angular
  .module(
    'app.angular',
    [ downgradeModule(bootstrapFn)]
  );

After that i add 'app.angular' as a dependency to angularjs app root module and everything works well. But I would like to get remove angular compiler from bundle by using AoT compilation. I tried to use https://www.npmjs.com/package/@ngtools/webpack with the following webpack config:

    mode: 'production',
    devtool: false,
    entry: path.resolve(__dirname, './src/main/app/app.module.angular.ts'),
    output: { filename: 'webpack.bundle.js' },
    module: {
        rules: [
            { test: /\.ts?$/,  use: '@ngtools/webpack' },
            { test: /\.html?$/, use: 'raw-loader' }
        ]
    },
    resolve: { extensions: ['.ts', '.js'] },
    plugins: [
        new htmlWebpackPlugin({
            template: '.' + outputPath + 'index.html',
            inject: false
        }),
        new AngularCompilerPlugin({
            mainPath: './src/main/app/app.module.angular.ts',
            tsConfigPath: './tsconfig.json',
        })
    ],
    optimization:{
        minimize: false,
        noEmitOnErrors: true,
    }

When i start with config above i get this error:

angular.js:13920 Error: No NgModule metadata found for 'MainAppModule'.
    at NgModuleResolver.resolve (https://localhost:8080/main/webpack.bundle.js:53749:23)
    at CompileMetadataResolver.getNgModuleMetadata (https://localhost:8080/main/webpack.bundle.js:52856:43)
    at JitCompiler._loadModules (https://localhost:8080/main/webpack.bundle.js:59037:51)
    at JitCompiler._compileModuleAndComponents (https://localhost:8080/main/webpack.bundle.js:59018:36)
    at JitCompiler.compileModuleAsync (https://localhost:8080/main/webpack.bundle.js:58978:37)
    at CompilerImpl.compileModuleAsync (https://localhost:8080/main/webpack.bundle.js:107707:31)
    at compileNgModuleFactory__PRE_R3__ (https://localhost:8080/main/webpack.bundle.js:27322:21)
    at PlatformRef.bootstrapModule (https://localhost:8080/main/webpack.bundle.js:27536:16)
    at app_module_angular_bootstrapFn (https://localhost:8080/main/webpack.bundle.js:110384:24)
    at Object.<anonymous> (https://localhost:8080/main/webpack.bundle.js:109133:26) <shop1 class="ng-scope">

Also template reference in component definitions doesn't work the same with different weboack setups: jit setup requires: template: require('./shop.component.html').default; while aot works with: templateUrl: './shop.component.html'

How can i solve the error mentioned above? Is there a way to have both jit and aot coexist without need to do significant modifications in each component?

Thanks Sorry for too long question :)


Solution

  • I ended up fixing the problem by splitting app module from main module, having main.ts and main.aot.ts files which just setup downgraded module and import AppModule. main.aot.ts looks like this:

    import 'reflect-metadata';
    import * as angular from 'angular';
    import { platformBrowser } from '@angular/platform-browser';
    // in main.ts: import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
    
    import { enableProdMode } from '@angular/core';
    import { NgModule, StaticProvider, Inject } from '@angular/core';
    import { downgradeModule } from '@angular/upgrade/static';
    
    import { MainAppModuleNgFactory } from './app/app.module.angular.ngfactory';
    // in main.ts: import { MainAppModule } from './app/app.module.angular';
    
    enableProdMode(); // not present in main.ts
    
    const bootstrapFn = (extraProviders: StaticProvider[]) => {
      const platformRef = platformBrowser(extraProviders);
      return platformRef.bootstrapModuleFactory(MainAppModuleNgFactory);
    };
    
    const MainAngularDowngradedModule = angular
      .module( 'app.angular', [ downgradeModule(bootstrapFn)] );
    

    also webpack config for aot looks like this:

    module.exports = {
        mode: 'production',
        devtool: false,
        entry: path.resolve(__dirname, './src/main/main.aot.ts'),
        output: {
            filename: 'webpack.bundle.js'
        },
        module: {
            rules: [
                {
                    test: /\.ts?$/,
                    use: '@ngtools/webpack',
                    exclude: [
                        /node_modules/,
                        path.resolve(__dirname, './src/main/main.ts'),
                    ]
                },{
                    test: /\.html?$/,
                    use: 'raw-loader',
                },{
                    test: /\.scss?$/,
                    use: [
                        'style-loader',
                        'css-loader',
                        'sass-loader'
                    ]
                }
            ]
        },
        resolve: {
            extensions: ['.ts', '.js']
        },
        plugins: [
            new AngularCompilerPlugin({
                entryModule: path.resolve(__dirname, './src/main/main.aot.ts#MainAppModule'),
                tsConfigPath: './tsconfig.aot.json',
            }),
        ],
        optimization:{
            minimize: true,
            noEmitOnErrors: true,
        }
    };
    

    and, finally, gulp task looks like this:

    gulp.task('bundle:main-webpack', function(){
        const config = (conf.env === ENVIRONMENTS.PRODUCTION || conf.env === ENVIRONMENTS.STAGING)
              ? '../webpack.prod.config.js'
              : '../webpack.main.config.js';
    
        return gulp.src(paths.src + '/**/*.ts')
            .pipe(webpackStream(require(config), webpack))
            .on('error', (err) => {
                gutil.log('WEBPACK ERROR', err);
            })
            .pipe(gulp.dest(paths.dist + '/main'));
    });
    

    This setup still has some space for optimizations, but at least it doesn't fail :)