Skip to content

Instantly share code, notes, and snippets.

@michelepatrassi
Last active February 5, 2023 13:56
Show Gist options
  • Save michelepatrassi/242f1f0a867af99918977ea64787fcee to your computer and use it in GitHub Desktop.
Save michelepatrassi/242f1f0a867af99918977ea64787fcee to your computer and use it in GitHub Desktop.
Angular Universal Server Side Rendering battle tested server.ts and webpack.server.config.js files 🦁(includes Firebase, FirebaseUI, Angular i18n and log rotation)
import { RESPONSE, REQUEST } from '@nguniversal/express-engine/tokens';
import { renderModuleFactory } from '@angular/platform-server';
// These are important and needed before anything else
import 'zone.js/dist/zone-node';
import 'reflect-metadata';
import { enableProdMode, ValueProvider, FactoryProvider } from '@angular/core';
// Import module map for lazy loading
import {provideModuleMap} from '@nguniversal/module-map-ngfactory-loader';
import { registerLocaleData } from '@angular/common';
// DOM libs required for Firebase
(global as any).WebSocket = require('ws');
(global as any).XMLHttpRequest = require('xmlhttprequest').XMLHttpRequest;
import * as express from 'express';
import * as morgan from 'morgan';
import * as fs from 'fs';
import rfs from 'rotating-file-stream';
import { join } from 'path';
// Faster server renders w/ Prod mode (dev mode never needed)
enableProdMode();
// render icons on server side
import './resources/assets/ts/icons';
// workaround for https://github.com/angular/angular-cli/issues/9975
const languages = ['it'];
languages.forEach(lang => {
const locale = require(`@angular/common/locales/${lang}`).default;
registerLocaleData(locale, lang);
});
// Express server
const app = express();
const PORT = 8080;
const DIST_FOLDER = join(process.cwd(), 'dist');
const LOG_FOLDER = join(DIST_FOLDER, 'log');
const isCi = process.env.IS_CI || false;
// Our index.html we'll use as our template
const template = fs.readFileSync(join(DIST_FOLDER, 'app', 'index.html')).toString();
// firebaseui needs a mock window and document to be compiled
// https://github.com/angular/universal/issues/830#issuecomment-345228799
const domino = require('domino');
const win = domino.createWindow(template);
win.process = process; // protobufjs fix to confirm node env: https://github.com/protobufjs/protobuf.js/blob/master/src/util/minimal.js#L55
global['window'] = win;
global['document'] = win.document;
global['navigator'] = win.navigator;
global['requestAnimationFrame'] = function(callback, element) {
let lastTime = 0;
const currTime = new Date().getTime();
const timeToCall = Math.max(0, 16 - (currTime - lastTime));
const id = setTimeout(function() { callback(currTime + timeToCall); },
timeToCall);
lastTime = currTime + timeToCall;
return id;
};
global['cancelAnimationFrame'] = function(id) {
clearTimeout(id);
};
// firebaseui fix: componentHandler is expected in the global scope
global['componentHandler'] = {
register: () => {}
};
// * NOTE :: leave this as require() since this file is built Dynamically from webpack
const { AppServerModuleNgFactory, LAZY_MODULE_MAP } = require('./dist/universal/main');
// Our Universal express-engine (found @ https://github.com/angular/universal/tree/master/modules/express-engine)
// Provide response token to enable 404: https://www.thecodecampus.de/blog/angular-universal-handle-404-set-status-codes/
app.engine('html', (_, options, callback) => {
renderModuleFactory(AppServerModuleNgFactory, {
document: template,
url: options.req.url,
extraProviders: [
provideModuleMap(LAZY_MODULE_MAP),
<ValueProvider>{
provide: RESPONSE,
useValue: options.req.res,
},
<FactoryProvider>{
provide: REQUEST,
useFactory: () => options.req, deps: [],
},
]
}).then(html => {
callback(null, html);
});
});
app.set('view engine', 'html');
app.set('views', join(DIST_FOLDER, 'app'));
// Setup logging with rotation and the combined format (https://github.com/expressjs/morgan)
fs.existsSync(LOG_FOLDER) || fs.mkdirSync(LOG_FOLDER);
const accessLogStream = rfs('access.log', {
interval: '1d', // rotate daily
path: LOG_FOLDER
});
app.use(morgan('combined', {stream: accessLogStream}));
// Data request should always go to backend
app.get('/api/*', (req, res) => {
res.status(404).send('data requests are not supported');
});
// Server static files from /browser
app.get('*.*', express.static(join(DIST_FOLDER, 'app'), {
maxAge: '1y'
}));
// All regular routes use the Universal engine
app.get('*', (req, res) => {
res.render('index', { req });
});
if (!isCi) {
// Start up the Node server
app.listen(PORT, () => {
console.log(`Node server listening on http://localhost:${PORT}`);
});
}
const path = require('path');
const webpack = require('webpack');
const regex = /firebase\/(app|firestore)/;
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/, function(context, request, callback) {
// exclude firebase products from being bundled, so they will be loaded using require() at runtime.
// https://github.com/firebase/firebase-js-sdk/issues/1455#issuecomment-455712500
if(regex.test(request)) {
return callback(null, 'commonjs ' + request);
}
callback();
}],
output: {
path: path.join(__dirname, 'dist'),
filename: '[name].js'
},
module: {
rules: [
{ test: /\.ts$/,
loader: 'ts-loader',
options: {
configFile: "./resources/assets/ts/tsconfig.server.json"
}
}
]
},
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'),
{}
),
// workaround for https://github.com/angular/angular-cli/issues/9975
new webpack.ContextReplacementPlugin(
/(.+)?angular(\\|\/)common(\\|\/)locales/,
/(de-AT|de-CH|de|en-AU|en-CA|en-GB|en-IE|en-NZ|en|es-CL|es|es-MX|fr-BE|fr-CA|fr|it|nl-BE|nl|pl|pt)$/
)
]
}
@michelepatrassi
Copy link
Author

hey @Liudmyla01, looks like some typing issue. Based on this one, can you give it a try with

var lastTime = 0;
global['requestAnimationFrame'] = function(callback)
{
  const now = new Date().getTime();
  var nextTime = Math.max(lastTime + 16, now);
  return setTimeout(function() { callback(lastTime = nextTime); }, nextTime - now);
};

I'm not actively maintaining the script but maybe the above could help you based on your screenshot!

@Liudmyla01
Copy link

thanks, I will try it)

@mojtabasoha
Copy link

hey @Liudmyla01, looks like some typing issue. Based on this one, can you give it a try with

var lastTime = 0;
global['requestAnimationFrame'] = function(callback)
{
  const now = new Date().getTime();
  var nextTime = Math.max(lastTime + 16, now);
  return setTimeout(function() { callback(lastTime = nextTime); }, nextTime - now);
};

I'm not actively maintaining the script but maybe the above could help you based on your screenshot!
I tried it and now I'm getting this error
image

dont know how to solve it :|

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment