Lazy Loaded Modules with AOT - TypeError: '' is not a function when served from NGINX
Asked Answered
V

3

6

Here are my dist files to reproduce yourself:

The breakdown:

  • My dist build, with AOT and Lazy Loaded modules works fine when served with npm packages webpack-dev-server or live-server
  • It is only when I copy dist to NGINX html directory and NGINX serves the files that I see Javascript errors in Firefox and Chrome
  • I have tried many different webpack configurations.
  • I am not importing my Lazy Loaded modules in any Typescript file
  • With AOT compilation OFF my app and Lazy Modules serve fine from NGINX
  • The TypeError: '' is not a function error is coming from Lazy Loaded Modules being served with NGINX

I am using the official Angular package @ngtools/webpack to add AOT compilation to my Angular 5 app. This article explains how to use @ngtools/webpack to add AOT to a Webpack build project. Very simple, though the article does not mention the needed step to add the Lazy Load module file paths to tsconfig-aot.json. AOT fails with out that step.

All works great localhost with:

npm run serve

My npm run serve command is an in memory compilation and the resources are served localhost from memory using the npm package webpack-dev-server.

When I deploy to my development server, the compilation files are stored on disk and my dev server serves the resources with NGINX.

I have lazy loaded modules that, when loaded are throwing strange errors like this in Firefox:

TypeError: i0.\u0275crt is not a function

enter image description here

And this in Chrome:

ERROR TypeError: i0.ɵcrt is not a function

enter image description here

Going to more detail with the error in Chrome, here is the source maps line of code throwing the error: enter image description here

I see that the creation of var i0 is this line:

var i0 = __webpack_require__(/*! @angular/core */ "./node_modules/@angular/core/esm5/core.js");

On my dev server, node_modules as at the parent level with my dist folder: enter image description here

My files on my dev server:

enter image description here

Here is a comparison of the resource files served up from localhost:

enter image description here

And here is the resource files served up from the development server: enter image description here

OK so here comes my configuration and npm package versions:

webpack.config

 var merge = require('webpack-merge'),
    htmlPlugin = require('html-webpack-plugin'),
    revPlugin = require('webpack-rev-replace-plugin'),
    config = require('./build.config.json'),
    path = require('path'),
    extendedDefinePlugin = require('extended-define-webpack-plugin'),
    webpackDelPlugin = require('webpack-del-plugin'),
    openBrowserPlugin = require('open-browser-webpack-plugin'),
    uglifyJSPlugin = require('uglifyjs-webpack-plugin');
const AotPlugin = require('@ngtools/webpack').AngularCompilerPlugin;
//import {AngularCompilerPlugin} from '@ngtools/webpack';

//Note : in package.json the last variable (dev) is the param delivered to this function { env: 'dev' }. 
module.exports = function (env) {
    console.log('env configuration', env.env);
    /**
     * configPerTarget is merged with build.config.json based on the env passed
     * currently no configuration properties, this configPerTarget not in use per se, keeping just in case - Ogden 4-12-2018
     */
    var configPerTarget = {
        localhost: {
        },
        development: {
        },
        test: {
        },
        staging: {
        },
        production: {
        },
        maintenance: {
        }
    };

    // Note : '__dirname' is the root file path.
    const ROOT_DIR = path.resolve(__dirname);
    const DIST_DIR = path.join(ROOT_DIR, config.dist);

    // If no env make it dev
    if (!env) {
        env = {};
        env.env = config.envDevelopment;
    }

    //merge config with env specific configPerTarget
    config = merge(config, configPerTarget[env.env]);

    // this takes path variables from build.config.json and builds it with given env
    var appConfigPath = config.envs + config.appConfig.replace('{env}', env.env);


    var webPackConfig = {
        entry: ['babel-polyfill', config.src + config.entry],//main.ts
        output: {
            path: path.resolve(__dirname, config.dist),
            filename: config.buildjs,
            sourceMapFilename: config.buildjsmap,
            chunkFilename: '[id].[hash:6].chunk.js'
        },
        module: {
            rules: [
                { test: /\.html$/, use: 'raw-loader' },
                { test: /\.css$/, use: 'raw-loader' },
                {
                    test: /\.ts$/,
                    loaders: [
                        'ts-loader',
                        'angular2-template-loader',
                        'angular-router-loader']
                },
                {
                    test: /\.scss$/,
                    exclude: /node_modules/,
                    loaders: ['style-loader', 'css-loader', 'sass-loader'],
                },
                //For images. 
                { test: /\.(jpe?g|png|gif|svg)$/i, loader: 'file-loader?name=app/assets/images/[name].[ext]' },
                {
                    test: /\.(ttf|eot|woff|woff2)$/,
                    loader: 'file-loader'
                },
            ]
        },
        //https://webpack.js.org/configuration/devtool/
        //Webpack 4.4 has its own mode development and production, which are environment modes
        //do Webpack 4.4 is handling the devtool sourcemap config where in the past it was not
        //looks like we no longer have to worry about setting devtool
        //https://github.com/damianobarbati/yarsk/blob/50b6f352a13ec2e778fa8b252f915550b6132964/config/webpack.config.js#L110
        //devtool: config.devtool,
        resolve: {
            modules: [__dirname + path.sep + 'src', __dirname, 'node_modules'],
            extensions: ['.js', '.ts', '.scss', '.css']
        },
        plugins: [
            new htmlPlugin({
                template: config.src + config.index
            }),
            new revPlugin({
                cwd: config.src,
                files: '**/*.html',
                outputPageName: function (filename) {
                    return filename;
                },
                modifyReved: function (filename) {
                    return filename.replace(/(\/style\/|\/script\/)/, '')
                }
            }),
            //Makes AppConfig variable available in the application code. 
            new extendedDefinePlugin({
                AppConfig: require(appConfigPath)
            }),
            //Usefull if you need remove some files or folders before compilation processes. 
            //currently not used (no dist file).
            new webpackDelPlugin({ match: path.join(DIST_DIR, '*.*') }),
            //opens browser after compilation.
            new openBrowserPlugin({ url: 'http://localhost:8080' })
        ]
    }

    //********************************AOT Compilation*************************************** */
    //-- AOT Compilation from this point on, currently AOT runs in all environments
    //this seems helpful because you get to see AOT build errors before pushing to build server
    //the downside might be more mangled code and harder to debug source code...

    if (env.env === config.envLocalhost) return webPackConfig;

    webPackConfig.module.rules.push(
        { test: /\.ts$/, loaders: ['@ngtools/webpack'] }
    );

    webPackConfig.plugins.push(new AotPlugin({
        tsConfigPath: './tsconfig-aot.json',
        //mainPath: path.resolve('./src/main.ts'),
        entryModule: path.join(config.src, 'app/app.module#AppModule')
    }));

    webPackConfig.optimization = {
        minimizer: [
            new uglifyJSPlugin({
                uglifyOptions: {
                    output: {
                        comments: false,
                        ascii_only: true
                    }
                }
            })
        ]
    }

    return webPackConfig;
}

package.json

{
  "name": "tsl-frontend",
  "version": "0.1.0",
  "scripts": {
    "test": "karma start",
    "build-localhost": "webpack --mode development --progress --colors --env.env localhost",
    "build-development": "webpack --mode development --progress --colors --env.env development",
    "build-staging": "webpack --mode production --progress --colors --env.env staging",
    "build-production": "webpack --mode production -p --progress --colors --env.env production",
    "build-maintenance": "webpack --mode production -p --progress --colors --env.env maintenance",
    "serve": "webpack-dev-server --mode development --inline --progress --colors --env.env development",
    "serve-production": "webpack-dev-server --mode production --inline --progress --colors --env.env development",
    "serve-localhost": "webpack-dev-server --mode development --inline --progress --colors --env.env localhost",
    "serve-host": "webpack-dev-server --host 0.0.0.0 --port 80 --disable-host-check --mode development --inline --progress --colors --env.env localhost",
    "serve-maintenance": "webpack-dev-server --mode development --inline --progress --colors --env.env maintenance"
  },
  "dependencies": {
    "@angular/animations": "^5.2.11",
    "@angular/cdk": "^2.0.0-beta.12",
    "@angular/common": "^5.2.11",
    "@angular/compiler": "^5.2.11",
    "@angular/compiler-cli": "^5.2.11",
    "@angular/core": "^5.2.11",
    "@angular/forms": "^5.2.11",
    "@angular/http": "^5.2.11",
    "@angular/material": "^2.0.0-beta.12",
    "@angular/platform-browser": "^5.2.11",
    "@angular/platform-browser-dynamic": "^5.2.11",
    "@angular/platform-server": "^5.2.11",
    "@angular/router": "^5.2.11",
    "@ng-bootstrap/ng-bootstrap": "^1.1.2",
    "@types/file-saver": "^1.3.0",
    "angular2-jwt": "^0.2.3",
    "angular2-text-mask": "^8.0.5",
    "bootstrap": "^4.1.2",
    "chart.js": "^2.7.2",
    "file-saver": "^1.3.8",
    "font-awesome": "^4.7.0",
    "moment": "2.18.1",
    "moment-timezone": "0.5.13",
    "ng2-bootstrap-modal": "1.0.1",
    "ng2-charts": "^1.6.0",
    "ng2-drag-drop": "^2.9.2",
    "ng2-page-scroll": "^4.0.0-beta.12",
    "ng2-toastr": "^4.1.2",
    "popper.js": "^1.14.3",
    "reflect-metadata": "0.1.8",
    "rxjs": "5.5.5",
    "systemjs": "0.19.40",
    "typescript": "^2.9.2",
    "xlsx": "^0.11.19",
    "zone.js": "^0.8.26"
  },
  "devDependencies": {
    "@ngtools/webpack": "^6.0.8",
    "@servicestack/client": "^1.0.14",
    "@types/file-saver": "^1.3.0",
    "@types/jasmine": "^2.8.8",
    "@types/node": "7.0.7",
    "angular-router-loader": "^0.6.0",
    "angular2-router-loader": "^0.3.5",
    "angular2-template-loader": "^0.6.2",
    "babel-polyfill": "^6.26.0",
    "css-loader": "^0.28.11",
    "extended-define-webpack-plugin": "^0.1.3",
    "extract-text-webpack-plugin": "^3.0.2",
    "file-loader": "^1.1.11",
    "file-saver": "^1.3.8",
    "html-webpack-plugin": "^4.0.0-alpha",
    "jasmine": "^2.99.0",
    "karma": "^1.7.0",
    "karma-sourcemap-loader": "^0.3.7",
    "karma-webpack": "^2.0.13",
    "ng-intercom": "^1.0.0-beta.5-2",
    "ng2-tree": "^2.0.0-rc.11",
    "node-sass": "^4.9.2",
    "open-browser-webpack-plugin": "0.0.5",
    "path": "^0.12.7",
    "raw-loader": "^0.5.1",
    "sass-loader": "^6.0.7",
    "style-loader": "^0.13.2",
    "text-mask-addons": "^3.7.2",
    "toposort": "^1.0.7",
    "ts-loader": "^4.4.2",
    "webpack": "^4.16.1",
    "webpack-cli": "^2.1.5",
    "webpack-del-plugin": "0.0.1",
    "webpack-dev-server": "^3.1.4",
    "webpack-merge": "^4.1.3",
    "webpack-rev-replace-plugin": "^0.1.1"
  }
}

tsconfig-aot.json (files array includes lazy loaded module paths)

{
    "compilerOptions": {
        "target": "es5", //most browsers currently understand this version of Javascript
        "experimentalDecorators": true, //Angular2 uses Component,Injectable etc
        "emitDecoratorMetadata": true, //Required for Angular2 to use the metadata in our components
        //"sourceMap": true
        "types": [
            "node",
            "jasmine"
        ],
        // "typeRoots": [
        //     "node_modules/@types"
        // ],
        "lib": [
            "es2015",
            "es2015.iterable",
            "dom"
        ]
    },
    "exclude": [
        "node_modules"
    ],
    "files": [
        "src/app/app.module.ts",
        "src/main.ts",
        "src/app.d.ts",
        "src/app/sandbox/sandbox.module.ts",
        "src/app/supplier-xchange/supplier-xchange.module.ts",
        "src/app/company-profile/company-profile.module.ts",
        "src/app/bom/bom.module.ts",
        "src/app/custom-price-column/custom-price-column.module.ts",
        "src/app/neca/neca.module.ts"
    ],
    "angularCompilerOptions": {
        "genDir": "aot", // Specify where Angular can create temporary AOT files
        "skipMetadataEmit": true // Don't generate not necessary metadata files. They are useful only if you're publishing an Angular UI library
    }
}

And here is my NGINX configuration:

daemon off;
user  nginx;
worker_processes  2;

error_log  /var/log/nginx/error.log warn;
pid        /var/run/nginx.pid;


events {
    worker_connections  1024;
    use epoll;
    accept_mutex off;
}


http {
    include       /etc/nginx/mime.types;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;

    default_type  application/octet-stream;

    log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '
                      '$status $body_bytes_sent "$http_referer" '
                      '"$http_user_agent" "$http_x_forwarded_for"';

    access_log  /var/log/nginx/access.log  main;

    sendfile        on;
    #tcp_nopush     on;

    keepalive_timeout  65;

    client_max_body_size 300m;
    client_body_buffer_size 300k;
    large_client_header_buffers 8 64k;

    gzip  on;
    gzip_http_version 1.0;
    gzip_comp_level 6;
    gzip_min_length 0;
    gzip_buffers 16 8k;
    gzip_proxied any;
    gzip_types text/plain text/css text/xml text/javascript application/xml application/xml+rss application/javascript application/json;
    gzip_disable "MSIE [1-6]\.";
    gzip_vary on;

    include /etc/nginx/conf.d/*.conf;
}


server {
    listen 80 default_server;
    listen [::]:80 default_server;
    server_name _;

    # API Server
    # location /api/ {
    #     proxy_pass ${MY_API_URL}/;
    # }

    # Main
    location / {
        set $cors "true";
       if ($http_origin ~* (http:\/\/d\.mywebsite\.com\S*)$) {
            set $cors "true";
        }

        if ($request_method = 'OPTIONS') {
            set $cors "${cors}options";
        }

        if ($request_method = 'GET') {
            set $cors "${cors}get";
        }
        if ($request_method = 'POST') {
            set $cors "${cors}post";
        }

        if ($cors = "trueget") {
            add_header 'Access-Control-Allow-Origin' "$http_origin";
            add_header 'Access-Control-Allow-Credentials' 'true';
        }

        if ($cors = "truepost") {
            add_header 'Access-Control-Allow-Origin' "$http_origin";
            add_header 'Access-Control-Allow-Credentials' 'true';
        }

        if ($cors = "trueoptions") {
            add_header 'Access-Control-Allow-Origin' "$http_origin";
            add_header 'Access-Control-Allow-Credentials' 'true';
            add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS';
            add_header 'Access-Control-Allow-Headers' 'Authorization,Content-Type,Accept,Origin,User-Agent,DNT,Cache-Control,X-Mx-ReqToken,Keep-Alive,X-Requested-With,If-Modified-Since';
            add_header 'Content-Length' 0;
            add_header 'Content-Type' 'text/plain charset=UTF-8';
            return 204;
        }

        root   /usr/share/nginx/html;
        index  index.html index.htm;
        try_files $uri$args $uri$args/ /index.html;
    }

    #error_page  404              /404.html;

    # redirect server error pages to the static page /50x.html
    #
    error_page   500 502 503 504  /50x.html;
    location = /50x.html {
        root   /usr/share/nginx/html;
    }
}

Do think these files matter to this problem but just in case:

main.ts

//CSS STYLES
import './styles';
import 'reflect-metadata';
//Zone JS is required by Angular itself.
import 'zone.js/dist/zone';
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
import { AppModule } from './app/app.module';
import { enableProdMode } from '@angular/core';


//remaining in ProdMode even in dev because of  ExpressionChangedAfterItHasBeenCheckedError: Expression has changed after it was checked errors
//http://www.allenhashkey.com/web-development/angular2/angular-2-expression-changed-after-it-has-been-checked-exception/
enableProdMode();
// if (!AppConfig.isDevelopment) {
//     enableProdMode();
// }


platformBrowserDynamic().bootstrapModule(AppModule)
    .then(success => console.log('Bootstrap success'))
    .catch(err => console.error("Bootstrap module failure: ", err));

app.module.ts

// Vendor
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { LocationStrategy, HashLocationStrategy } from '@angular/common';
import { Injector } from '@angular/core';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { Ng2DragDropModule } from 'ng2-drag-drop';
import { ToastModule } from 'ng2-toastr/ng2-toastr';
import { Ng2PageScrollModule } from 'ng2-page-scroll';
// Routing
import { AppRoutingModule, routableComponents } from './app-routing.module';
//Components
import { AppComponent } from './app.component';
//Shared Module
import { SharedModule } from './@shared/shared.module';
//Feature Modules
import { CoreModule } from './@core/core.module';
import { DashboardModule } from './dashboard/dashboard.module';
import { ProductModule } from './product/product.module';
import { SearchModule } from './search/search.module';
import { LoginModule } from './login/login.module';
import { ExampleModule } from './example/example.module';
import { ProfileModule } from './profile/profile.module';
import { ResetPasswordModule } from './reset-password/reset-password.module';
import { EdataFlexModule } from './e-data-flex/e-data-flex.module';
import { SubmittalManagerModule } from './submittal-manager/submittal-manager.module';
import { PimModule } from './pim/pim.module';
import { AnalyticsModule } from './analytics/analytics.module';
import { InviteUserModule } from './invite-user/invite-user.module';
import { DownloadsModule } from './downloads/downloads.module';
import { SettingsModule } from './settings/settings.module';
import { ChangeBulletinModule } from './change-bulletin/change-bulletin.module';
//Singletons - A Singleton Service shall only be kept in app.module.ts "providers" (array)
//and it shall not be placed in any other component or service provider (array).
import { TokenService } from './@core/auth/token.service';
// Intercom Module
import { IntercomModule } from 'ng-intercom';
import { BootstrapModalModule } from 'ng2-bootstrap-modal';

@NgModule({
  imports: [
    SharedModule,
    BrowserModule,
    DashboardModule,
    ProductModule,
    SearchModule,
    ProfileModule,
    ExampleModule,
    LoginModule,
    CoreModule,
    ResetPasswordModule,
    EdataFlexModule,
    SubmittalManagerModule,
    PimModule,
    AnalyticsModule,
    InviteUserModule,
    DownloadsModule,
    SettingsModule,
    ChangeBulletinModule,
    //Do not import feature modules below "AppRoutingModule"
    AppRoutingModule,
    BrowserAnimationsModule,
    BootstrapModalModule.forRoot({container:document.body}),
    Ng2PageScrollModule.forRoot(),
    Ng2DragDropModule.forRoot(),
    ToastModule.forRoot(),
    IntercomModule.forRoot({
      appId:AppConfig.intercom["appId"], // TSO App Id
      updateOnRouterChange : true // will automatically run 'update' on router event changes.
    })
  ],
  declarations: [
    AppComponent,
    routableComponents
  ],
  providers: [
    { provide: LocationStrategy, useClass: HashLocationStrategy },
    TokenService
  ],
  bootstrap: [AppComponent]

})
export class AppModule {
  /**
     * Allows for retrieving singletons using `AppModule.injector.get(MyService)`
     * This is good to prevent injecting the service as constructor parameter.
     */
  static injector: Injector;
  constructor(injector: Injector) {
    AppModule.injector = injector;
  }
}
Vindictive answered 20/7, 2018 at 23:35 Comment(8)
This might be a stupid question, but are you using one of the build-* scripts before copying your destination files to your server?Relator
I am using the build-development script currently before copying the output from dist to my dev serverVindictive
Maybe going to the network tab (debug window) on your browser with localhost first, then comparing with your server, may help you figure out if there is anything missing.Relator
@closevoter Dear closevoter, as someone who has struggled a lot in the past with mysterious angular/webpack errors, this looks like a perfectly valid question to me...Relator
That is a good suggest, thanks, trying it outVindictive
@Relator I updated my question to include network resource screenshots of both localhost and dev serverVindictive
try to run with optimization:false and buildOptimizer:false so that you can see error more clear.Moron
@RaviSevta thanks, but I am not using @angular/cli, thus I am not using ng buildVindictive
E
3

It's because when you use AOT, the code is uglified with some funny UTF-8 symbols; ɵ in i0.ɵcrt in your case

You need to tell nginx to use UTF-8 charset

server {
  listen 80 default_server;
  listen [::]:80 default_server;
  server_name _;

  charset UTF-8; #<==== Add this
Emeraldemerge answered 26/7, 2018 at 9:44 Comment(2)
thank you so much, I spent a great deal of time trying to figure out what the issue was hereVindictive
No worries. It was easy to reproduce when I downloaded you files :)Emeraldemerge
D
0

Replace your current nginx.conf file in the /etc/nginx folder with the following,

#worker_processes 2;

events {
    worker_connections 1024;
}
http {
   sendfile on;

   gzip on;
   gzip_disable "msie6";

   gzip_vary on;
   gzip_proxied any;
   gzip_comp_level 6;
   gzip_buffers 16 8k;
   gzip_http_version 1.1;
   gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript;

    log_format timed_combined '$remote_addr - $remote_user [$time_local] '
    '"$request" $status $body_bytes_sent '
    '"$http_referer" "$http_user_agent" '
    '$request_time $upstream_response_time $pipe';

    access_log /var/log/nginx/access.log timed_combined;
    error_log /var/log/nginx/error.log;

    include /etc/nginx/mime.types;
    default_type application/octet-stream;

    server {
        listen 80;
        server_name _;
        return 404;
    }

   server {
        listen 80;
        server_name www.example.com;

        location / {
           root /home/pokusr/propok-frontend/dist;
           try_files $uri /index.html;
        }
        location ~* "^/[a-z0-9]{40}.(css|js)$" {
            root /home/usr/proj_path/dist/;
            access_log off;
            expires max;
        }
    }
}

Lazy loaded routes will have the hash values in it and location ~* "^/[a-z0-9]{40}.(css|js)$" { this will solve the problem

Discretion answered 26/7, 2018 at 9:51 Comment(0)
C
0

In Angular 8 we need to change old way of lazy loading style to dynamic import and need to change module: 'esnext' and target: 'es2015' in tsconfig.json file.

Cowboy answered 12/12, 2019 at 13:40 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.