Skip to content

Instantly share code, notes, and snippets.

@navix
Last active September 22, 2021 11:04
Show Gist options
  • Star 5 You must be signed in to star a gist
  • Fork 4 You must be signed in to fork a gist
  • Save navix/7cbdac0e5337cc869613cdb0dae7929f to your computer and use it in GitHub Desktop.
Save navix/7cbdac0e5337cc869613cdb0dae7929f to your computer and use it in GitHub Desktop.

Angular Universal (Webpack bundle + Docker deploy)

Tested with angular@5.

Install deps

npm i @angular/platform-server @nguniversal/express-engine @nguniversal/module-map-ngfactory-loader express @types/express ts-loader@3 rimraf --save-dev

Update src/app/app.module.ts

@NgModule({
  imports: [
    BrowserModule.withServerTransition({appId: '_app'}),
    ...

Add src/app/app-server.module.ts

import { NgModule } from '@angular/core';
import { ServerModule } from '@angular/platform-server';
import { ModuleMapLoaderModule } from '@nguniversal/module-map-ngfactory-loader';
import { AppComponent } from './app.component';
import { AppModule } from './app.module';

@NgModule({
  imports: [
    AppModule,
    ServerModule,
    ModuleMapLoaderModule,
  ],
  bootstrap: [
    AppComponent,
  ],
})
export class AppServerModule {
}

Add src/main.server.ts

export { AppServerModule } from './app/app-server.module';

Add src/server.ts

// These are important and needed before anything else
import 'zone.js/dist/zone-node';
import 'reflect-metadata';

import { renderModuleFactory } from '@angular/platform-server';
import { enableProdMode } from '@angular/core';

import * as express from 'express';
import { join } from 'path';
import { readFileSync } from 'fs';

// Faster server renders w/ Prod mode (dev mode never needed)
enableProdMode();

// Express server
const app = express();

const PORT = process.env.PORT || 8000;
const DIST_FOLDER = join(process.cwd(), 'dist');

// Our index.html we'll use as our template
const template = readFileSync(join(DIST_FOLDER, 'index.html')).toString();

// * NOTE :: leave this as require() since this file is built Dynamically from webpack
const { AppServerModuleNgFactory, LAZY_MODULE_MAP } = require('../dist-server/main.bundle');

// Express Engine
const {ngExpressEngine} = require('@nguniversal/express-engine');
const { provideModuleMap } = require('@nguniversal/module-map-ngfactory-loader');

// Our Universal express-engine (found @ https://github.com/angular/universal/tree/master/modules/express-engine)
app.engine('html', ngExpressEngine({
  bootstrap: AppServerModuleNgFactory,
  providers: [
    provideModuleMap(LAZY_MODULE_MAP)
  ]
}));

app.set('view engine', 'html');
app.set('views', join(DIST_FOLDER));

// Server static files from /browser
app.get('*.*', express.static(join(DIST_FOLDER)));

// All regular routes use the Universal engine
app.get('*', (req: any, res: any) => {
  req.readFileSync = readFileSync;
  res.render(join(DIST_FOLDER, 'index.html'), { req });
});

// Start up the Node server
app.listen(PORT, () => {
  console.log(`Node server listening on http://localhost:${PORT}`);
});

Add src/tsconfig.server.json

{
  "extends": "../tsconfig.json",
  "compilerOptions": {
    "outDir": "../out-tsc/app",
    "module": "commonjs",
    "baseUrl": "./",
  },
  "exclude": [
    "test.ts",
    "server.ts",
    "**/*.spec.ts"
  ],
  "angularCompilerOptions": {
    "entryModule": "app/app-server.module#AppServerModule"
  }
}

Add src/webpack.server.config.js

const path = require('path');
const webpack = require('webpack');

module.exports = {
  entry: {server: './src/server.ts'},
  resolve: {extensions: ['.js', '.ts']},
  target: 'node',
  // this makes sure we include node_modules and other 3rd party libraries
  externals: [/(node_modules|main\..*\.js)/],
  output: {
    path: path.join(__dirname, '../dist'),
    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'),
      {}
    )
  ]
};

Update .angular-cli.json

Add app:

{
  "name": "server",
  "platform": "server",
  "root": "src",
  "outDir": "dist-server",
  "assets": [
    "assets",
    "favicon.ico"
  ],
  "index": "index.html",
  "main": "main.server.ts",
  "test": "test.ts",
  "tsconfig": "tsconfig.server.json",
  "prefix": "app",
  "styles": [
    "styles.css"
  ],
  "environmentSource": "environments/environment.ts",
  "environments": {
    "dev": "environments/environment.ts",
    "prod": "environments/environment.prod.ts"
  }
}

Add Dockerfile

FROM node:latest

MAINTAINER Sasha Novik <alex@nvx.me>

ARG env

RUN npm install pm2 -g

RUN mkdir -p /var/app

WORKDIR /var/app

COPY ./dist/ dist/

EXPOSE 8000

CMD ["pm2-docker", "./dist/server.js", "--name='uni'"]

Add scripts to package.json

"build": "npm run clean && npm run build:app && npm run build:ssr:app && npm run build:ssr:bundle",
"build:app": "ng build --prod --output-hashing=all --build-optimizer=false",
"build:ssr:app": "ng build -a=server --prod --output-hashing=none",
"build:ssr:bundle": "webpack --config ./src/webpack.server.config.js --progress --colors",
"ssr:serve": "node ./dist/server",
"docker:build": "docker build -t PATH .",
"docker:run": "docker run -d -p 8002:8000 PATH",
"docker:push": "docker push PATH"
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment