I recently upgraded my Angular 8 application to be an Universal application (SSR). I had it deployed to Firebase before it was a SSR application, but then I figured out that deploying a SSR application on Firebase doesn't work by using regular Firebase Hosting. I did some research and discovered that I have to use Firebase Cloud Functions.
I created a SSR application using:
ng add @nguniversal/express-engine --clientProject PROJECT_NAME
PROJECT_NAME
can be found in angular.json
file below "projects"
section.
Can anyone help me with this?
Important note: This solution was taken from Q&A section of the Angular course on Udemy (here). I tried it out and with some modifications managed to make it work.
So, first make sure SSR actually works by running npm run build:ssr
and npm run serve:ssr
.
Then install Firebase Tools and initialize the project:
- If you didn't install the Firebase Command Line Tools already before, run
npm install -g firebase-tools
- Run
firebase login
, providing your Firebase credentials (email/password) if requested.
- Run
firebase init
Answer some questions ...
"Are you ready to proceed?"
Type y
and hit ENTER.
"Which firebase CLI features do you want to setup?"
Choose ...
(*) Functions
(*) Hosting
... , selecting both with the SPACE key, and hitting ENTER.
"Select a default Firebase project for this directory?"
Select one with the ARROW keys and hit ENTER.
"What language would you like to use to write Cloud Functions?"
Select TypeScript
with the ARROW keys and hit ENTER.
"Do you want to use TSLint?"
Type y
and hit ENTER.
"Do you want to install dependencies with npm now?"
Type y
and hit ENTER.
"What do you want to use as your public directory?"
Type dist/browser
and hit ENTER (Please note: this is different from deploying an app without Universal!).
"Configure as a single page app?"
Type y
and hit ENTER.
File index.html already exists. Overwrite?
Type n
(important!) and hit ENTER.
Modify some files ...
In firebase.json replace "destination": "/index.html"
by "function": "ssr"
(ssr
points to this export const ssr = functions.https.onRequest(universal);
variable, you will find it below).
In server.ts add export
to the app
initialization: export const app = express();
instead of const app = express();
In server.ts either comment out the last three lines (app.listen(...)
) or replace them with these:
// If we're not in the Cloud Functions environment, spin up a Node server
if (!process.env.FUNCTION_NAME) {
// Start up the Node server
app.listen(PORT, () => {
console.log(`Node Express server listening on http://localhost:${PORT}`);
});
}
You can remove them when deploying to Firebase but you need them when running npm run serve:ssr
to be able to host your app on localhost
.
- In webpack.server.config.js modify the
output
like this:
output: {
// Puts the output at the root of the dist folder
path: path.join(__dirname, 'dist'),
// Export a UMD of the webpacked server.ts & dependencies for rendering in Cloud Functions
library: 'app',
libraryTarget: 'umd',
filename: '[name].js',
},
and modify externals
like this:
externals: [
// Firebase has some troubles being webpacked when it's in the Node environment, so we will skip it.
/^firebase/
],
This will fix an error:
Cannot find module 'require("./server/main")'
when running npm run serve:ssr
or firebase serve
commands.
Rebuild your app by running npm run build:ssr
.
Using the terminal move to the functions folder: cd functions
Install a npm package for filesystem access: npm i fs-extra
Inside the functions folder create a new file named copy-angular-app.js, with this content:
const fs = require('fs-extra');
fs.copy('../dist', './dist').then(() => {
// We should remove the original "index.html" since Firebase will use it when SSR is enabled (instead of calling SSR functions),
// and because of that, SSR won't work for the initial page.
fs.remove('../dist/browser/index.html').catch(e => console.error('REMOVE ERROR: ', e));
}).catch(err => {
console.error('COPY ERROR: ', err)
});
This fixes initial page not loaded as a SSR (instead of showing content for the initial page it's still showing <app-root></app-root>
).
Note: Since we removed index.html
file, running npm run serve:ssr
won't work unless you first rebuild your app (by running npm run build:ssr
-> this will recreate index.html
file).
- In functions/package.json (not in the project's package.json!) change the build entry like this:
"build": "node copy-angular-app && tsc",
- In functions/src/index.ts replace the content with this:
import * as functions from 'firebase-functions';
const universal = require(`${process.cwd()}/dist/server`).app;
export const ssr = functions.https.onRequest(universal);
- In the terminal make sure that you are in the functions directory, and there run
npm run build
to copy the dist
folder into the functions
folder.
Additional note: To make building for Firebase easier you can create a script in the main project's package.json
file:
"build:ssr": "npm run build:client-and-server-bundles && npm run compile:server", // this one should already exist
"build:ssr-firebase": "npm run build:ssr && npm --prefix functions/ run build",
This script will first build your Angular SSR application (npm run build:ssr
) and then it will run npm run build
inside your functions folder (that way it will copy the project's dist
folder to your functions dist
folder and will remove the project's index.html
file).
Deploy your app ...
You can serve your app locally before deployment, on localhost:5000, by running firebase serve
(if you want).
Stop the server (Ctrl + C).
Then you can deploy the app by running firebase deploy
and visit it via the url which is displayed in the terminal.
This way I managed to deploy my Angular SSR app on Firebase.
Hope this helps...