I am attempting to convert my angular 6 application to server side rendering (for SEO purposes), and everything seems to compile without error. Except when I actually navigate to the to localhost, I and getting the full error of
Error: No NgModule metadata found for '[object Object]'.
at NgModuleResolver.resolve (/execroot/angular/packages/compiler/src/ng_module_resolver.ts:31:15)
at CompileMetadataResolver.getNgModuleMetadata (/execroot/angular/packages/compiler/src/metadata_resolver.ts:509:41)
at JitCompiler._loadModules (/execroot/angular/packages/compiler/src/jit/compiler.ts:127:49)
at JitCompiler._compileModuleAndComponents (/execroot/angular/packages/compiler/src/jit/compiler.ts:107:32)
at JitCompiler.compileModuleAsync (/execroot/angular/packages/compiler/src/jit/compiler.ts:61:33)
at CompilerImpl.compileModuleAsync (/execroot/angular/packages/platform-browser-dynamic/src/compiler_factory.ts:57:27)
at /execroot/nguniversal/modules/express-engine/src/main.ts:130:16
at new ZoneAwarePromise (/Users/melliotfrost/projects/myapp/node_modules/zone.js/dist/zone-node.js:891:29)
at getFactory (/execroot/nguniversal/modules/express-engine/src/main.ts:115:10)
at View.engine (/execroot/nguniversal/modules/express-engine/src/main.ts:92:7)
I am running the commands npm run build:ssr && npm run serve:ssr
app.module.ts (Main module)
//import modules
import { BrowserModule } from '@angular/platform-browser';
import { NgModule, APP_INITIALIZER, PLATFORM_ID, APP_ID, Inject } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { HttpClientModule, HTTP_INTERCEPTORS } from '@angular/common/http';
import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
import { ColorPickerModule } from 'ngx-color-picker';
import { SortablejsModule } from 'angular-sortablejs';
import { FroalaEditorModule, FroalaViewModule } from 'angular-froala-wysiwyg';
import { Ng2SearchPipeModule } from 'ng2-search-filter';
import { MomentModule } from 'angular2-moment';
import { UIView } from '@uirouter/angular'
import { FileUploadModule } from 'ng2-file-upload';
import { CustomFormsModule } from 'ng4-validators';
import { NgSelectModule } from '@ng-select/ng-select';
import { NgPipesModule } from 'angular-pipes';
import { NgxPaginationModule } from 'ngx-pagination';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { BusyModule } from 'angular2-busy';
import { CookieModule } from 'ngx-cookie';
//TODO will be fixed shortly
//import { TextareaAutosizeModule } from 'ngx-textarea-autosize';
import { Location, LocationStrategy, HashLocationStrategy, isPlatformBrowser } from '@angular/common';
import * as _ from 'lodash';
//pipes
import { AppPipes } from './pipes/pipes';
//Directives
import { AppDirectives } from './directives/directives';
//Components
import { AppComponents, ModalComponents } from './components/components';
import { AppComponent } from'./app.component';
//Services
import { AppResources } from './components/resources';
import { AppServices } from './services/services';
import { AppLoadService } from '../config/app.load.service';
//HTTP Interceptors
import { APIInterceptor } from './services/api.interceptor';
const interceptors = [
{ provide: HTTP_INTERCEPTORS, useClass: APIInterceptor, multi: true }
];
//States
import { AppRoutingModule } from './app-routing.module';
export function init_app(appLoadService: AppLoadService) {
return () => appLoadService.load();
}
//Platform Info
const IMPORTS = [
BrowserModule.withServerTransition({ appId: 'myapp' }),
FormsModule,
BrowserAnimationsModule,
HttpClientModule,
NgbModule.forRoot(),
ColorPickerModule,
SortablejsModule.forRoot({ animation: 150 }),
FroalaEditorModule.forRoot(),
FroalaViewModule.forRoot(),
Ng2SearchPipeModule,
MomentModule,
FileUploadModule,
AppRoutingModule,
CustomFormsModule,
NgSelectModule,
NgPipesModule,
NgxPaginationModule,
BusyModule,
CookieModule.forRoot()
//TextareaAutosizeModule
];
const DECLARATIONS = [
AppPipes,
AppDirectives,
AppComponents
];
const PROVIDERS = [
AppResources,
AppServices,
interceptors,
Location,
AppLoadService,
{ provide: APP_INITIALIZER, useFactory: init_app, deps: [AppLoadService], multi: true },
];
@NgModule({
declarations: DECLARATIONS,
imports: IMPORTS,
providers: PROVIDERS,
entryComponents: ModalComponents,
bootstrap: [ AppComponent ],
})
export class AppModule {
constructor(
@Inject(PLATFORM_ID) private platformId: Object,
@Inject(APP_ID) private appId: string) {
const platform = isPlatformBrowser(platformId) ? 'in the browser' : 'on the server';
console.log(`Running ${platform} with appId=${appId}`);
}
}
main.ts
import { enableProdMode } from '@angular/core';
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
import { AppModule } from './app/app.module';
import { environment } from './environments/environment';
if (environment.production) {
enableProdMode();
}
platformBrowserDynamic().bootstrapModule(AppModule).catch(err => console.log(err));
tsconfig.app.json
{
"extends": "../tsconfig.json",
"compilerOptions": {
"outDir": "../out-tsc/app",
"baseUrl": "./",
"module": "es2015",
"types": []
},
"exclude": [
"test.ts",
"**/*.spec.ts"
]
}
app.server.module.ts (Server module)
import { enableProdMode } from '@angular/core';
export { AppServerModule } from './app/app.server.module';
enableProdMode();
server.main.ts
tsconfig.server.json
{
"extends": "../tsconfig.json",
"compilerOptions": {
"outDir": "../out-tsc/server",
"baseUrl": "./",
"module": "commonjs",
"types": []
},
"exclude": [
"test.ts",
"**/*.spec.ts"
],
"angularCompilerOptions": {
"entryModule": "app/app.server.module#AppServerModule"
}
}
server.ts (server)
webpackage.server.config.js (webpack configuration for generate server.js)
//******************** BEGING deps *******************//
import 'zone.js/dist/zone-node';
import 'reflect-metadata';
import express = require('express');
import path = require('path');
import compression = require('compression');
import fs = require('fs');
import http = require('http');
import https = require('https');
import git = require('git-rev');
import request = require('request');
import fallback = require('express-history-api-fallback');
import bodyParser = require('body-parser');
import sslRedirect = require('heroku-ssl-redirect');
import { join } from 'path';
const ejs = require('ejs');
//******************** END deps *******************//
//
//******************** BEGING angular deps *******************//
import { enableProdMode } from '@angular/core';
import { ngExpressEngine } from '@nguniversal/express-engine';
import { provideModuleMap } from '@nguniversal/module-map-ngfactory-loader';
//
//******************** END angular deps *******************//
//******************** BEGING constants *******************//
//
require('dotenv').config();
enableProdMode();
const app = express();
const API_URL = {
dev: 'https://dev-api.myapp.com',
feature: 'https://localhost:1337',
stage: 'https://test-api.myapp.com',
demo: 'https://demo-api.myapp.com',
prod: 'https://api.myapp.com'
};
const root = 'dist';
const DIST_FOLDER = join(process.cwd(), 'dist');
//******************** END constants *******************//
//
//****************** BEGIN ANGULAR UNIVERSAL CONFIG *****************//
const domino = require('domino');
const template = fs.readFileSync(path.join(root + '/browser', 'index.html')).toString();
const win = domino.createWindow(template);
global['window'] = win;
global['document'] = win.document;
global['DOMTokenList'] = win.DOMTokenList;
global['Node'] = win.Node;
global['Text'] = win.Text;
global['HTMLElement'] = win.HTMLElement;
global['navigator'] = win.navigator;
global['MutationObserver'] = getMockMutationObserver();
function getMockMutationObserver() {
return class {
observe(node, options) {
}
disconnect() {
}
takeRecords() {
return [];
}
};
}
var { AppServerModuleNgFactory, LAZY_MODULE_MAP } = require('./dist/server/main');
console.log(LAZY_MODULE_MAP);
console.log(AppServerModuleNgFactory);
//****************** END ANGULAR UNIVERSAL CONFIG *****************//
//******************** BEGING server configs *******************//
app.use(sslRedirect());
app.use(bodyParser.json())
//view enginer
app.engine('html', ngExpressEngine({
bootstrap: AppServerModuleNgFactory,
providers: [
provideModuleMap(LAZY_MODULE_MAP)
]
}));
app.set('view engine', 'html');
app.set('views', join(DIST_FOLDER, 'browser'));
//******************** END server configs *******************//
//************* BEGIN ROUTES *****************/
//server initial page
//app.get('/', function(req, res){
//res.sendFile(path.join(__dirname + '/dist/index.html'));
//});
app.get('/polyfills.bundle.js', (req, res) => {
res.sendFile(path.join(__dirname + '/dist/polyfills.bundle.js'));
});
app.get('/styles.bundle.js', (req, res) => {
res.sendFile(path.join(__dirname + '/dist/styles.bundle.js'));
});
app.get('/scripts.bundle.js', (req, res) => {
res.sendFile(path.join(__dirname + '/dist/scripts.bundle.js'));
});
app.get('/vendor.bundle.js', (req, res) => {
res.sendFile(path.join(__dirname + '/dist/vendor.bundle.js'));
});
app.get('/main.bundle.js', (req, res) => {
res.sendFile(path.join(__dirname + '/dist/main.bundle.js'));
});
app.get('/inline.bundle.js', (req, res) => {
res.sendFile(path.join(__dirname + '/dist/inline.bundle.js'));
});
//API routes
app.get('/api/envInfo', function(req, res){
git.branch(function(branch){
git.short(function(hash){
res.json({
env: process.env.APP_ENV || 'dev',
hash: hash,
branch: branch
});
});
});
});
app.get('/api/badge/:id', function(req, res){
var badgeUrl = API_URL[process.env.APP_ENV || 'dev'] + '/badge/' + req.params.id + '?withuser=true';
request.get(badgeUrl, (err, response, body) => {
ejs.renderFile('./views/badge.ejs', { cert: JSON.parse(body) }, (err, html) => {
res.send(html);
});
});
});
app.get('/api/microcourse/:id', function(req, res){
var microcourseUrl = API_URL[process.env.APP_ENV || 'dev'] + '/earnedMicrocourse/' + req.params.id;
request.get(microcourseUrl, (err, response, body) => {
ejs.renderFile('./views/badge.ejs', { cert: JSON.parse(body) }, (err, html) => {
res.send(html);
});
});
});
app.get('/api/user/:id', function(req, res){
var userUrl = API_URL[process.env.APP_ENV || 'dev'] + '/user/' + req.params.id;
request.get(userUrl, (err, response, body) => {
var user = JSON.parse(body);
if(user.profileVisible){
ejs.renderFile('./views/user.ejs', { user: JSON.parse(body) }, (err, html) => {
res.send(html);
});
} else {
res.redirect('/#/');
}
});
});
app.get('/api/badge/:id/share', function(req, res){
var badgeUrl = API_URL[process.env.APP_ENV || 'dev'] + '/badge/' + req.params.id + '?withuser=true&hide=true';
request.get(badgeUrl, (err, response, body) => {
ejs.renderFile('./views/badge.ejs', { badge: JSON.parse(body) }, (err, html) => {
res.send(html);
});
});
});
app.post('/api/theme', require('./routes/generateTheme'));
//Angular Routes
//
app.get('*.*', express.static(join(DIST_FOLDER, 'browser')));
app.get('*', (req, res) => {
res.render('index', {
req: req,
res: res,
providers: [
{
provide: 'REQUEST', useValue: (req)
},
{
provide: 'RESPONSE', useValue: (res)
}
]
});
});
//***************** END Routes *****************//
//
//***************BEGIN app configurations *******************//
//server gzipped content
app.use(compression());
app.use('/images', express.static('images'));
app.use('/assets', express.static('assets'));
app.use('/dist', express.static('dist'));
app.use('/node_modules', express.static('node_modules'));
app.use('/app', express.static('app'));
app.use('/public', express.static('public'));
app.use(fallback('index.htm', { root: root }));
//*************** END app configurations *******************//
//*************** BEGIN server instantiation *******************//
//SSL termination for heroku is handled by the load balancer
//but locally is handled by node
if(!process.env.APP_ENV || (process.env.APP_ENV === 'feature')){
var credentials = {
key: fs.readFileSync(path.join('ssl', 'certs', 'server', 'privkey.pem')),
cert: fs.readFileSync(path.join('ssl', 'certs', 'server', 'fullchain.pem')),
ca: fs.readFileSync(path.join('ssl', 'certs', 'ca', 'my-root-ca.crt.pem'))
};
var httpsServer = https.createServer(credentials, app);
httpsServer.listen(process.env.PORT || 8080);
} else {
var httpServer = http.createServer(app);
httpServer.listen(process.env.PORT || 8080);
}
//*************** END server instantiation *******************//
const path = require('path');
const webpack = require('webpack');
module.exports = {
entry: { server: './server.ts' },
resolve: { extensions: ['.js', '.ts'] },
target: 'node',
mode: 'none',
// this makes sure we include node_modules and other 3rd party libraries
externals: [/node_modules/],
output: {
path: __dirname,
filename: '[name].js'
},
module: {
rules: [{ test: /\.ts$/, loader: 'ts-loader' }]
},
plugins: [
// Temporary Fix for issue: https://github.com/angular/angular/issues/11580
// for 'WARNING Critical dependency: the request of a dependency is an expression'
new webpack.ContextReplacementPlugin(
/(.+)?angular(\\|\/)core(.+)?/,
path.join(__dirname, 'src'), // location of your src
{} // a map of your routes
),
new webpack.ContextReplacementPlugin(
/(.+)?express(\\|\/)(.+)?/,
path.join(__dirname, 'src'),
{}
),
]
};
angular.json (cli configuration)
{
"$schema": "./node_modules/@angular/cli/lib/config/schema.json",
"version": 1,
"newProjectRoot": "projects",
"projects": {
"browser": {
"root": "",
"sourceRoot": "src",
"projectType": "application",
"architect": {
"build": {
"builder": "@angular-devkit/build-angular:browser",
"options": {
"outputPath": "dist/browser",
"index": "src/index.html",
"main": "src/main.ts",
"tsConfig": "src/tsconfig.app.json",
"polyfills": "src/polyfills.ts",
"assets": [
"src/assets"
],
"styles": [
"src/styles.scss",
"node_modules/froala-editor/css/froala_editor.pkgd.min.css",
"node_modules/froala-editor/css/froala_style.min.css",
"node_modules/sweetalert2/dist/sweetalert2.min.css",
"node_modules/pnotify/dist/pnotify.css",
"node_modules/@ng-select/ng-select/themes/default.theme.css",
"node_modules/angular2-busy/build/style/busy.css"
],
"scripts": [
"node_modules/sweetalert2/dist/sweetalert2.min.js",
"node_modules/lodash/lodash.min.js",
"node_modules/jquery-watch/jquery-watch.min.js",
"node_modules/bootstrap/dist/js/bootstrap.min.js",
"node_modules/socket.io-client/dist/socket.io.js"
]
},
"configurations": {
"production": {
"optimization": true,
"outputHashing": "all",
"sourceMap": false,
"extractCss": true,
"namedChunks": false,
"aot": true,
"extractLicenses": true,
"vendorChunk": false,
"buildOptimizer": true,
"fileReplacements": [
{
"replace": "src/environments/environment.ts",
"with": "src/environments/environment.prod.ts"
}
]
},
"demo": {
"optimization": true,
"outputHashing": "all",
"sourceMap": false,
"extractCss": true,
"namedChunks": false,
"aot": true,
"extractLicenses": true,
"vendorChunk": false,
"buildOptimizer": true,
"fileReplacements": [
{
"replace": "src/environments/environment.ts",
"with": "src/environments/environment.demo.ts"
}
]
},
"feature": {
"fileReplacements": [
{
"replace": "src/environments/environment.ts",
"with": "src/environments/environment.feature.ts"
}
]
},
"stage": {
"optimization": true,
"outputHashing": "all",
"sourceMap": false,
"extractCss": true,
"namedChunks": false,
"aot": true,
"extractLicenses": true,
"vendorChunk": false,
"buildOptimizer": true,
"fileReplacements": [
{
"replace": "src/environments/environment.ts",
"with": "src/environments/environment.staging.ts"
}
]
}
}
},
"server": {
"builder": "@angular-devkit/build-angular:server",
"options": {
"outputPath": "dist/server",
"main": "src/server.main.ts",
"tsConfig": "src/tsconfig.server.json",
"bundleDependencies": "all"
},
"configurations": {
"production": {
"browserTarget": "browser:build:production"
},
"demo": {
"browserTarget": "browser:build:demo"
},
"feature": {
"browserTarget": "browser:build:feature"
},
"stage": {
"browserTarget": "browser:build:stage"
}
}
},
"extract-i18n": {
"builder": "@angular-devkit/build-angular:extract-i18n",
"options": {
"browserTarget": "browser:build"
}
},
"test": {
"builder": "@angular-devkit/build-angular:karma",
"options": {
"main": "src/test.ts",
"karmaConfig": "./karma.conf.js",
"polyfills": "src/polyfills.ts",
"tsConfig": "src/tsconfig.spec.json",
"scripts": [
"node_modules/sweetalert2/dist/sweetalert2.min.js",
"node_modules/lodash/lodash.min.js",
"node_modules/jquery-watch/jquery-watch.min.js",
"node_modules/bootstrap/dist/js/bootstrap.min.js",
"node_modules/socket.io-client/dist/socket.io.js"
],
"styles": [
"src/styles.scss",
"node_modules/froala-editor/css/froala_editor.pkgd.min.css",
"node_modules/froala-editor/css/froala_style.min.css",
"node_modules/sweetalert2/dist/sweetalert2.min.css",
"node_modules/pnotify/dist/pnotify.css",
"node_modules/@ng-select/ng-select/themes/default.theme.css",
"node_modules/angular2-busy/build/style/busy.css"
],
"assets": [
"src/assets"
]
}
},
"lint": {
"builder": "@angular-devkit/build-angular:tslint",
"options": {
"tsConfig": [
"src/tsconfig.app.json",
"src/tsconfig.spec.json"
],
"exclude": [
"**/node_modules/**"
]
}
}
}
},
"browser-e2e": {
"root": "",
"sourceRoot": "",
"projectType": "application",
"architect": {
"server": {
"builder": "@angular-devkit/build-angular:server",
"options": {
"outputPath": "dist/server",
"main": "src/main.server.ts",
"tsConfig": "src/tsconfig.server.json"
}
},
"e2e": {
"builder": "@angular-devkit/build-angular:protractor",
"options": {
"protractorConfig": "./protractor.conf.js",
"devServerTarget": "browser:serve"
}
},
"lint": {
"builder": "@angular-devkit/build-angular:tslint",
"options": {
"tsConfig": [
"e2e/tsconfig.e2e.json"
],
"exclude": [
"**/node_modules/**"
]
}
}
}
}
},
"defaultProject": "browser",
"schematics": {
"@schematics/angular:component": {
"prefix": "app",
"styleext": "scss"
},
"@schematics/angular:directive": {
"prefix": "app"
}
}
}
package.json
{
"name": "myapp",
"version": "3.0.0",
"description": "myapp",
"main": "server.js",
"scripts": {
"build:ssr": "npm run build:client-and-server-bundles && npm run webpack:server",
"serve:ssr": "node server",
"build:client-and-server-bundles": "ng build --prod && ng run browser:server",
"webpack:server": "webpack --config webpack.server.config.js --progress --colors"
},
"engines": {
"node": "8.11.2",
"npm": "6.0.1"
},
"dependencies": {
"@angular/animations": "6.0.2",
"@angular/common": "6.0.2",
"@angular/compiler": "6.0.2",
"@angular/core": "6.0.2",
"@angular/forms": "6.0.2",
"@angular/http": "6.0.2",
"@angular/platform-browser": "6.0.2",
"@angular/platform-browser-dynamic": "6.0.2",
"@angular/platform-server": "6.0.2",
"@angular/router": "6.0.2",
"@angular/upgrade": "6.0.2",
"@angularclass/bootloader": "^1.0.1",
"@ng-bootstrap/ng-bootstrap": "^1.0.0",
"@ng-select/ng-select": "^0.32.0",
"@ngtools/webpack": "^6.0.3",
"@nguniversal/express-engine": "^6.0.0",
"@nguniversal/module-map-ngfactory-loader": "^6.0.0",
"@types/async": "^2.0.47",
"@types/moment": "^2.13.0",
"@types/uuid": "^3.4.3",
"@uirouter/angular": "^1.0.1",
"angular-elastic": "^2.5.1",
"angular-filesize-filter": "^0.1.3",
"angular-froala-wysiwyg": "^2.7.5",
"angular-pipes": "^7.1.0",
"angular-sortablejs": "^2.5.2",
"angular2-busy": "^2.0.4",
"angular2-cookie": "^1.2.6",
"angular2-moment": "^1.8.0",
"animate.css": "~3.5.2",
"async": "~2.0.0-rc.5",
"aws-sdk": "^2.235.1",
"body-parser": "^1.18.2",
"bootstrap": "^3.3.7",
"chai": "^4.1.2",
"chai-as-promised": "^7.1.1",
"chai-jquery": "^2.0.0",
"compression": "~1.6.2",
"connect-gzip-static": "~1.0.0",
"core-js": "^2.4.1",
"domino": "^1.0.30",
"dotenv": "^5.0.0",
"ejs": "^2.6.1",
"envify": "~3.4.1",
"es6-promise": "^4.2.2",
"express": "~4.14.0",
"express-history-api-fallback": "^2.2.1",
"express-sslify": "^1.2.0",
"git-rev": "^0.2.1",
"heroku-ssl-redirect": "0.0.4",
"http-server": "~0.9.0",
"httpbackend": "^2.0.0",
"is-utf8": "^0.2.1",
"jquery": "~3.1.1",
"jquery-watch": "^1.21.0",
"lodash": "~4.11.1",
"lodash-move": "^1.1.1",
"moment": "~2.15.1",
"nconf": "~0.8.4",
"ng-elastic": "^1.0.0-beta.5",
"ng-file-upload": "~12.0.4",
"ng-img-crop": "~0.2.0",
"ng2-file-upload": "^1.3.0",
"ng2-pnotify": "0.0.3",
"ng2-search-filter": "^0.4.7",
"ng4-validators": "^5.1.0",
"ngx-color-picker": "^5.3.3",
"ngx-cookie": "^3.0.1",
"ngx-pagination": "^3.1.0",
"ngx-textarea-autosize": "^1.1.1",
"pnotify": "~3.0.0",
"popup-tools": "^1.0.2",
"protractor": "~5.3.0",
"protractor-cucumber-framework": "^4.2.0",
"request": "^2.85.0",
"require-globify": "~1.4.1",
"rxjs": "^6.1.0",
"rxjs-compat": "^6.1.0",
"sass-thematic": "^2.0.4",
"selectize": "~0.12.4",
"socket.io-client": "^2.0.4",
"sortablejs": "^1.7.0",
"striptags": "^3.1.1",
"sweetalert2": "^7.10.0",
"textangular": "~1.5.1",
"thematic": "0.0.1",
"ts-loader": "^4.1.0",
"ts-node": "^4.1.0",
"typescript": "2.7.2",
"ui-select": "~0.18.1",
"uuid": "^3.2.1",
"zone.js": "^0.8.26"
},
"devDependencies": {
"@angular-devkit/build-angular": "~0.6.3",
"@angular/cli": "6.0.3",
"@angular/compiler-cli": "6.0.2",
"@angular/language-service": "6.0.2",
"@types/jasmine": "~2.8.3",
"@types/jasminewd2": "~2.0.2",
"@types/node": "~6.0.60",
"codelyzer": "^4.0.1",
"jasmine-core": "~2.8.0",
"jasmine-spec-reporter": "~4.2.1",
"karma": "~2.0.0",
"karma-chrome-launcher": "~2.2.0",
"karma-coverage-istanbul-reporter": "^1.2.1",
"karma-jasmine": "~1.1.0",
"karma-jasmine-html-reporter": "^0.2.2",
"protractor": "~5.1.2",
"ts-node": "~4.1.0",
"tslint": "~5.9.1",
"typescript": "2.7.2",
"webpack-cli": "^2.1.3"
}
}
You need to remove "bundleDependencies": "all"
from angular.json file (under server -> options). Not sure why it's breaking, but it sure does fix the issue.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With