Javascriptservices: .Net Core 2.0 Angular SPA Template VS 2017 Azure publish fails on lazy loaded module

Created on 26 Oct 2017  Â·  8Comments  Â·  Source: aspnet/JavaScriptServices

Hello,

I have a project I created with the latest angular .net Core 2.0 SPA template in Visual Studio 2017 v. 15.4.1. I upgraded the template to use the latest version of Angular and Webpack.

Everything builds and runs correctly when I run webpack commands from the command line however the production commands fail when trying to publish to Azure from Visual Studio with the following error below.

Note: When I remove the lazy loaded module the VS publish executes successfully. Also, unfortunately we could not leverage the server side pre-rendering offered by .Net Core as some components we use reference the DOM directly. So our index.cshtml looks like this:

<app>Loading</app>

Also, boot.server.ts and boot.browser.ts files remain unchanged from template.

I'm trying to publish the app to Azure with AOT compilation enabled and functioning, server side pre-rendering disabled, and with lazy loaded angular modules. What am I doing wrong here?

Any help is greatly appreciated!

=============================================
Publish Error:

ERROR in ./ClientApp/app/app-routing.module.ts
Module not found(0,0): Error : Can't resolve '.\features\admin-console\admin-console.module.ngfactory' in 'D:\GIT\EMS\Ems.Solution\Ems.UI\ClientApp\app'
    resolve '.\features\admin-console\admin-console.module.ngfactory' in 'D:\GIT\EMS\Ems.Solution\Ems.UI\ClientApp\app'
      using description file: D:\GIT\EMS\Ems.Solution\Ems.UI\package.json (relative path: ./ClientApp/app)
      after using description file: D:\GIT\EMS\Ems.Solution\Ems.UI\package.json (relative path: ./ClientApp/app)
        using description file: D:\GIT\EMS\Ems.Solution\Ems.UI\package.json (relative path: ./ClientApp/app/features/admin-console/admin-console.module.ngfactory)
          no extension
            D:\GIT\EMS\Ems.Solution\Ems.UI\ClientApp\app\features\admin-console\admin-console.module.ngfactory doesn't exist
          .js
            D:\GIT\EMS\Ems.Solution\Ems.UI\ClientApp\app\features\admin-console\admin-console.module.ngfactory.js doesn't exist
          .ts
            D:\GIT\EMS\Ems.Solution\Ems.UI\ClientApp\app\features\admin-console\admin-console.module.ngfactory.ts doesn't exist
          as directory
            D:\GIT\EMS\Ems.Solution\Ems.UI\ClientApp\app\features\admin-console\admin-console.module.ngfactory doesn't exist
    [D:\GIT\EMS\Ems.Solution\Ems.UI\ClientApp\app\features\admin-console\admin-console.module.ngfactory]
    [D:\GIT\EMS\Ems.Solution\Ems.UI\ClientApp\app\features\admin-console\admin-console.module.ngfactory.js]
    [D:\GIT\EMS\Ems.Solution\Ems.UI\ClientApp\app\features\admin-console\admin-console.module.ngfactory.ts]
    [D:\GIT\EMS\Ems.Solution\Ems.UI\ClientApp\app\features\admin-console\admin-console.module.ngfactory]
     @ ./ClientApp/app/app-routing.module.ts 6:137-206
     @ ./$$_gendir/ClientApp/app/app.module.ngfactory.ts
     @ ./ClientApp/boot.server.ts

=================================================
Here is my package.json:

{
  "dependencies": {
    "@angular/animations": "4.4.6",
    "@angular/common": "4.4.6",
    "@angular/compiler": "4.4.6",
    "@angular/compiler-cli": "4.4.6",
    "@angular/core": "4.4.6",
    "@angular/forms": "4.4.6",
    "@angular/http": "4.4.6",
    "@angular/platform-browser": "4.4.6",
    "@angular/platform-browser-dynamic": "4.4.6",
    "@angular/platform-server": "4.4.6",
    "@angular/router": "4.4.6",
    "@ngtools/webpack": "1.7.4",
    "@types/webpack-env": "1.13.2",
    "angular-router-loader": "^0.6.0",
    "angular2-template-loader": "0.6.2",
    "aspnet-prerendering": "^3.0.1",
    "aspnet-webpack": "^2.0.1",
    "awesome-typescript-loader": "3.2.3",
    "bootstrap": "3.3.7",    
    "copy-webpack-plugin": "^4.2.0",
    "css": "2.2.1",
    "css-loader": "0.28.7",
    "es6-shim": "0.35.3",
    "event-source-polyfill": "0.0.11",
    "expose-loader": "0.7.3",
    "extract-text-webpack-plugin": "3.0.1",
    "file-loader": "1.1.5",
    "html-loader": "0.5.1",
    "isomorphic-fetch": "2.2.1",
    "jquery": "3.2.1",
    "json-loader": "0.5.7",
    "preboot": "5.1.5",
    "raw-loader": "0.5.1",
    "reflect-metadata": "0.1.10",
    "rxjs": "5.5.0",
    "style-loader": "0.19.0",
    "to-string-loader": "1.1.5",
    "typescript": "2.5.3",
    "url-loader": "0.6.2",
    "webpack": "3.8.1",
    "webpack-externals-plugin": "^1.0.0",
    "webpack-hot-middleware": "2.20.0",
    "webpack-merge": "4.1.0",
    "zone.js": "0.8.18"
  },
  "devDependencies": {
    "@types/chai": "4.0.4",
    "@types/jasmine": "2.6.0",
    "chai": "4.1.2",
    "jasmine-core": "2.8.0",
    "karma": "1.7.1",
    "karma-chai": "0.1.0",
    "karma-chrome-launcher": "2.2.0",
    "karma-cli": "1.0.1",
    "karma-jasmine": "1.1.0",
    "karma-webpack": "2.0.5"
  },
  "name": "xmserviceui",
  "private": true,
  "scripts": {
    "test": "karma start ClientApp/test/karma.conf.js"
  },
  "version": "1.0.0"
}

==========================================
Here is webpack.config.js:

const path = require('path');
const webpack = require('webpack');
const merge = require('webpack-merge');
const AotPlugin = require('@ngtools/webpack').AotPlugin;
const CheckerPlugin = require('awesome-typescript-loader').CheckerPlugin;
const CopyWebpackPlugin = require('copy-webpack-plugin');

module.exports = (env) => {
    // Configuration in common to both client-side and server-side bundles
    const isDevBuild = !(env && env.prod);
    const sharedConfig = {
        stats: { modules: false },
        context: __dirname,
        resolve: { extensions: [ '.js', '.ts' ] },
        output: {
            filename: '[name].js',
            publicPath: 'dist/' // Webpack dev middleware, if enabled, handles requests for this URL prefix
        },
        module: {
            rules: [
                { test: /\.ts$/, include: /ClientApp/, use: isDevBuild ? ['awesome-typescript-loader?silent=true', 'angular2-template-loader', 'angular-router-loader'] : ['@ngtools/webpack', 'angular-router-loader?aot=true'] },
                { test: /\.html$/, use: 'html-loader?minimize=false' },
                { test: /\.css$/, use: [ 'to-string-loader', isDevBuild ? 'css-loader' : 'css-loader?minimize' ] },
                { test: /\.(png|jpg|jpeg|gif|svg)$/, use: 'url-loader?limit=25000' }
            ]
        },
        plugins: [
            new CheckerPlugin(),
            // NOTE: We can remove this dependency by simply moving the assets folder to the wwwroot folder.
            new CopyWebpackPlugin([{
                from: './ClientApp/app/assets',
                to: './assets'
            }])
        ]
    };

    // Configuration for client-side bundle suitable for running in browsers
    const clientBundleOutputDir = './wwwroot/dist';
    const clientBundleConfig = merge(sharedConfig, {
        entry: { 'main-client': './ClientApp/boot.browser.ts' },
        output: { path: path.join(__dirname, clientBundleOutputDir) },
        plugins: [
            new webpack.DllReferencePlugin({
                context: __dirname,
                manifest: require('./wwwroot/dist/vendor-manifest.json')
            })
        ].concat(isDevBuild ? [
            // Plugins that apply in development builds only
            new webpack.SourceMapDevToolPlugin({
                filename: '[file].map', // Remove this line if you prefer inline source maps
                moduleFilenameTemplate: path.relative(clientBundleOutputDir, '[resourcePath]') // Point sourcemap entries to the original file locations on disk
            })
        ] : [
            // Plugins that apply in production builds only
            new webpack.optimize.UglifyJsPlugin(),
            new AotPlugin({
                tsConfigPath: './tsconfig.json',
                entryModule: path.join(__dirname, 'ClientApp/app/app.module#AppModule'),
                exclude: ['./**/*.server.ts']
            })
        ])
    });

    // Configuration for server-side (prerendering) bundle suitable for running in Node
    const serverBundleConfig = merge(sharedConfig, {
        resolve: { mainFields: ['main'] },
        entry: { 'main-server': './ClientApp/boot.server.ts' },
        plugins: [
            new webpack.DllReferencePlugin({
                context: __dirname,
                manifest: require('./ClientApp/dist/vendor-manifest.json'),
                sourceType: 'commonjs2',
                name: './vendor'
            })
        ].concat(isDevBuild ? [] : [
            // Plugins that apply in production builds only
            new AotPlugin({
                tsConfigPath: './tsconfig.json',
                entryModule: path.join(__dirname, 'ClientApp/app/app.module#AppModule'),
                exclude: ['./**/*.browser.ts']
            })
        ]),
        output: {
            libraryTarget: 'commonjs',
            path: path.join(__dirname, './ClientApp/dist')
        },
        target: 'node',
        devtool: 'inline-source-map'
    });

    return [clientBundleConfig, serverBundleConfig];
};

================================================
Here is main app routing module (app-routing.module.ts):

import { HomeComponent } from './features/publish/home/home.component';
import { PublishFinishComponent } from './features/publish/publish-finish.component';
import { CanActivateIfEntitled } from './common/can-activate-if-entitled';
import { EntitlementsService } from './common/services/entitlements.service';

 specific routes.
const appRoutes: Routes = [
    { path: 'ops/home', component: HomeComponent }, // Not block when not entitled
    { path: 'ops/publish-finish', component: PublishFinishComponent }, 

    // Admin-Console - lazy loaded.
    { path: 'admin', loadChildren: './features/admin-console/admin-console.module#AdminConsoleModule' },
    { path: '', redirectTo: 'admin', pathMatch: 'full' },
    { path: '**', redirectTo: 'admin' }
];

@NgModule({
    imports: [RouterModule.forRoot(appRoutes, { initialNavigation: 'enabled' })],
    exports: [RouterModule],
    providers: [ CanActivateIfEntitled, EntitlementsService ]
})
export class AppRoutingModule { }

Most helpful comment

I am also having a similar issue and would appreciate advice on how to address this.

All 8 comments

I am also having a similar issue and would appreciate advice on how to address this.

An additional note: I have the following defined in the ts.config.json file:

{
  "compilerOptions": {
    "module": "es2015",
    "moduleResolution": "node",
    "target": "es5",
    "sourceMap": true,
    "experimentalDecorators": true,
    "emitDecoratorMetadata": true,
    "removeComments": true,
    "suppressImplicitAnyIndexErrors": true,
    "strict": false,
    "lib": [ "es6", "dom" ],
    "types": [ "webpack-env" ]
  },
  "files": [
    "ClientApp/app/app.module.ts",
    "ClientApp/app/app-routing.module.ts",
    "ClientApp/app/features/admin-console/admin-console.module.ts"
  ],
  "exclude": [ "bin", "node_modules" ],
  "atom": { "rewriteTsconfig": false }
}

Mine wasn't working, but I was so glad to find somebody else was having the same problem and the issue was only a few hours old. I saw you had that angular-router-loader?aot=true in your production loaders, which I was missing, so I threw that in -- and now it works! ¯_(ツ)_/¯

But you already have that, so no idea why mine is working and yours isn't 😕, but thank you!

Here's my webpack.config.js, package.json, tsconfig.json, and my route configuration. Maybe you'll see something that will help.
webpack.config.js

const path = require('path');
const webpack = require('webpack');
const merge = require('webpack-merge');
const AotPlugin = require('@ngtools/webpack').AotPlugin;
const CheckerPlugin = require('awesome-typescript-loader').CheckerPlugin;
const ExtractTextPlugin = require("extract-text-webpack-plugin");

module.exports = (env) => {
  // Configuration in common to both client-side and server-side bundles
  const isDevBuild = !(env && env.prod);

  const extractSass = new ExtractTextPlugin({
    //filename: "[name].[contenthash].css",
    filename: "[name].css",
    disable: isDevBuild
  });

  const sharedConfig = {
    stats: { modules: false },
    context: __dirname,
    resolve: { extensions: [ '.js', '.ts' ] },
    output: {
      filename: '[name].js',
      publicPath: 'dist/' // Webpack dev middleware, if enabled, handles requests for this URL prefix
    },
    module: {
      rules: [
        { test: /\.ts$/, include: /ClientApp/, use: isDevBuild ? ['awesome-typescript-loader?silent=true', 'angular2-template-loader', 'angular-router-loader'] : ['@ngtools/webpack', 'angular-router-loader?aot=true'] },
        { test: /\.html$/, use: 'html-loader?minimize=false' },
        { test: /\.css$/, use: [ 'to-string-loader', isDevBuild ? 'css-loader' : 'css-loader?minimize' ] },
        { test: /\.scss$/, use: extractSass.extract({ use: [{ loader: "css-loader" }, { loader: "sass-loader" }], fallback: "style-loader" }) },
        { test: /\.(png|jpg|jpeg|gif|svg)$/, use: 'url-loader?limit=25000' }
      ]
    },
    plugins: [new CheckerPlugin(), extractSass]
  };

  // Configuration for client-side bundle suitable for running in browsers
  const clientBundleOutputDir = './wwwroot/dist';
  const clientBundleConfig = merge(sharedConfig, {
    entry: { 'main-client': './ClientApp/boot.browser.ts' },
    output: { path: path.join(__dirname, clientBundleOutputDir) },
    plugins: [
      new webpack.DllReferencePlugin({
        context: __dirname,
        manifest: require('./wwwroot/dist/vendor-manifest.json')
      })
    ].concat(isDevBuild ? [
      // Plugins that apply in development builds only
      new webpack.SourceMapDevToolPlugin({
        filename: '[file].map', // Remove this line if you prefer inline source maps
        moduleFilenameTemplate: path.relative(clientBundleOutputDir, '[resourcePath]') // Point sourcemap entries to the original file locations on disk
      })
    ] : [
      // Plugins that apply in production builds only
      new webpack.optimize.UglifyJsPlugin(),
      new AotPlugin({
        tsConfigPath: './tsconfig.json',
        entryModule: path.join(__dirname, 'ClientApp/app/app.module.browser#AppModule'),
        exclude: ['./**/*.server.ts']
      })
    ])
  });

  // Configuration for server-side (prerendering) bundle suitable for running in Node
  const serverBundleConfig = merge(sharedConfig, {
    resolve: { mainFields: ['main'] },
    entry: { 'main-server': './ClientApp/boot.server.ts' },
    plugins: [
      new webpack.DllReferencePlugin({
        context: __dirname,
        manifest: require('./ClientApp/dist/vendor-manifest.json'),
        sourceType: 'commonjs2',
        name: './vendor'
      })
    ].concat(isDevBuild ? [] : [
      // Plugins that apply in production builds only
      new AotPlugin({
        tsConfigPath: './tsconfig.json',
        entryModule: path.join(__dirname, 'ClientApp/app/app.module.server#AppModule'),
        exclude: ['./**/*.browser.ts']
      })
    ]),
    output: {
      libraryTarget: 'commonjs',
      path: path.join(__dirname, './ClientApp/dist')
    },
    target: 'node',
    devtool: 'inline-source-map'
  });

  return [clientBundleConfig, serverBundleConfig];
};

package.json

{
  "name": "invitr",
  "private": true,
  "version": "0.0.0",
  "scripts": {
    "test": "karma start ClientApp/test/karma.conf.js"
  },
  "dependencies": {
    "@angular/animations": "4.2.5",
    "@angular/common": "4.2.5",
    "@angular/compiler": "4.2.5",
    "@angular/compiler-cli": "4.2.5",
    "@angular/core": "4.2.5",
    "@angular/forms": "4.2.5",
    "@angular/http": "4.2.5",
    "@angular/platform-browser": "4.2.5",
    "@angular/platform-browser-dynamic": "4.2.5",
    "@angular/platform-server": "4.2.5",
    "@angular/router": "4.2.5",
    "@ngrx/store": "^4.0.3",
    "@ngtools/webpack": "1.5.0",
    "@types/lodash": "^4.14.76",
    "@types/webpack-env": "1.13.0",
    "angular-router-loader": "^0.6.0",
    "angular2-template-loader": "0.6.2",
    "aspnet-prerendering": "^3.0.1",
    "aspnet-webpack": "^2.0.1",
    "awesome-typescript-loader": "3.2.1",
    "bootstrap": "3.3.7",
    "css": "2.2.1",
    "css-loader": "0.28.4",
    "es6-shim": "0.35.3",
    "event-source-polyfill": "0.0.9",
    "expose-loader": "0.7.3",
    "extract-text-webpack-plugin": "2.1.2",
    "file-loader": "0.11.2",
    "html-loader": "0.4.5",
    "isomorphic-fetch": "2.2.1",
    "jquery": "3.2.1",
    "json-loader": "0.5.4",
    "lodash": "https://registry.npmjs.org/lodash/-/lodash-4.17.4.tgz",
    "ngx-cookie": "^1.0.0",
    "node-sass": "^4.5.3",
    "preboot": "4.5.2",
    "raw-loader": "0.5.1",
    "reflect-metadata": "0.1.10",
    "rxjs": "5.4.2",
    "sass-loader": "^6.0.6",
    "style-loader": "0.18.2",
    "to-string-loader": "1.1.5",
    "typescript": "2.4.1",
    "url-loader": "0.5.9",
    "webpack": "https://registry.npmjs.org/webpack/-/webpack-2.5.1.tgz",
    "webpack-hot-middleware": "2.18.2",
    "webpack-merge": "4.1.0",
    "zone.js": "0.8.12"
  },
  "devDependencies": {
    "@types/chai": "4.0.1",
    "@types/jasmine": "2.5.53",
    "chai": "4.0.2",
    "jasmine-core": "2.6.4",
    "karma": "1.7.0",
    "karma-chai": "0.1.0",
    "karma-chrome-launcher": "2.2.0",
    "karma-cli": "1.0.1",
    "karma-jasmine": "1.1.0",
    "karma-webpack": "2.0.3"
  }
}

tsconfig.json

{
  "compilerOptions": {
    "module": "es2015",
    "moduleResolution": "node",
    "target": "es5",
    "sourceMap": true,
    "experimentalDecorators": true,
    "emitDecoratorMetadata": true,
    "skipDefaultLibCheck": true,
    "skipLibCheck": true, // Workaround for https://github.com/angular/angular/issues/17863. Remove this if you upgrade to a fixed version of Angular.
    "strict": true,
    "lib": [ "es6", "dom" ],
    "types": [ "webpack-env" ]
  },
  "exclude": [ "bin", "node_modules" ],
  "atom": { "rewriteTsconfig": false }
}

app.routes.ts

import { Routes } from '@angular/router';
import { RedirectAuthGuard } from "./core/auth/router-guards/redirect-auth.guard";

import { HomeComponent } from "./home/home.component";
import { LoginComponent } from "./login/login.component";
import { LoginLocalComponent } from "./login/login-local.component";
import { RegisterComponent } from "./login/register.component";
import { MenuComponent } from "./menu/menu.component";

export const AppRoutes: Routes = [
  { path: '', component: HomeComponent, canActivate: [RedirectAuthGuard], pathMatch: 'full' },
  { path: 'landing', component: HomeComponent },
  { path: 'login', component: LoginComponent, canActivate: [RedirectAuthGuard] },
  { path: 'login-local', component: LoginLocalComponent, canActivate: [RedirectAuthGuard] },
  { path: 'register', component: RegisterComponent, canActivate: [RedirectAuthGuard] },
  { path: 'menu', component: MenuComponent },
  { path: 'account', loadChildren: './+account/account.module#AccountModule' },
  { path: 'admin', loadChildren: './+admin/admin.module#AdminModule' },
  { path: 'dashboard', loadChildren: './+dashboard/dashboard.module#DashboardModule' },
  { path: 'store', loadChildren: './+storefront/storefront.module#StorefrontModule' },
  { path: 'rsvp', loadChildren: './+rsvp/rsvp.module#RsvpModule' },
  { path: '**', redirectTo: 'home' }
];

Good luck! Sorry I can't be of more assistance 😕

Thanks @FreedCapybara for providing a working example.

Hi @SteveSandersonMS but sorry this is not working. Please re-open this. Lazy loading modules is not working when server side pre-rendering is not being leveraged.

@FreedCapybara does your project use the Server Side Pre-rendering?

Also, on a side note @SteveSandersonMS one thing that I think would be really helpful to a lot of devs - in general - is a blog post or some article with an in depth explanation of how the angular SPA template packaging is designed. It would be great to really have a solid understanding of how the webpack configuration is set up as well as how it leverages AOT and server side pre-rendering. Also, an explanation of how it works when server side pre-rendering is not leveraged is important too as some components (due to their implementation) prohibit server side pre-rendering.

There are so many different ways to configure an Angular project with webpack and it seems that every time there is an upgrade to Angular or Webpack or some other key component something seems to go wrong and its is so painful to try and troubleshoot these types of issues.

I think these templates are fantastic and really appreciate the work you all have done so far. I believe a deep dive into the design of the packaging process as well as how these templates leverage AOT, server side pre-rendering, and lazy loading would be super helpful.

@RichDef77 Sorry to hear it's still not working. Did you manage to find out what's different between your setup and @FreedCapybara's? In what way does the code posted above by @FreedCapybara not work, and yet why is it working for @FreedCapybara?

From what I can see I'm using a newer version of angular - 4.4.6, webpack - 3.8.1. and ngtools/webpack - 1.7.4.

Also, I know my in my project I am not leveraging server side pre-rendering at all because we must consume a component made by another department that utilizes direct DOM access via the angular ElementRef.

I have seen other examples of AOT and lazy loading with the following:

import { AppModuleNgFactory } from '../aot/app/app.module.ngfactory';
enableProdMode();
platformBrowser().bootstrapModuleFactory(AppModuleNgFactory);

I see that in our template boot.browser.ts we use:
const modulePromise = platformBrowserDynamic().bootstrapModule(AppModule);

I'm not sure if that's relevant, however.

Was this page helpful?
0 / 5 - 0 ratings