Part 5: Getting ready for deployment & deploy
Tags: javascript, webdev, tutorial, vue
Published: 2022-05-18 22:13:54

> The whole code for this part can be found [here](https://github.com/Evertvdw/creating-a-embeddable-chat-widget/tree/part-5)

Let's get into the exciting stuff this part! I always find that deploying an app you are working on makes it more 'real'. It's also a good check to see if your setup works, as deployment will most likely require some tweaks. (Spoiler alert, it will😅)

## Getting ready for deployment
> Section git diff (as my commits were a mess 😅) [here](https://github.com/Evertvdw/creating-a-embeddable-chat-widget/compare/21a08fa..e904dcc?diff=split)

Deploying the app means that we need to build every part and get the build files somewhere on a server and run the main process there. In my case the main file that will be run is the `packages/server/index.ts` file. This means that we have to serve up our `portal` and `widget` package from there.

### Serving local files
To do this we have to add some code to this file:
```ts
// At the top:
import serveStatic from 'serve-static';
import history from 'connect-history-api-fallback';

// After app.use(cookieParser()):
app.use(history());
app.use(serveStatic('./../../dist/widget'));
app.use(serveStatic('./../../dist/portal'));
```

Also add the dependencies necessary for this:
```bash
yarn workspace server add connect-history-api-fallback
yarn workspace server add -D @types/connect-history-api-fallback
```

The `history()` function is needed to run our Vue app in history mode, meaning that you can navigate directly to `/clients` and get served the entry `index.html` no matter the initial url.

> This will be refined later on. Also we're introducing a bug when we add it like this, which we will tackle later. Can you spot what will go wrong? Hint: the order of code is important 😇

### Next, finding out your types sharing solution does not work well
Always fun to find out that some solution you chose is not really a solution at all, but hey, that happens! To me at least but I figure to all developers 🙂

Turns out that by specifying the project `rootDir` in the `tsconfig.json` will also affect where the files will be placed when building the project. I did some fiddling around with this and eventually came to the conclusion that moving the types to a separate 4th package in the project should work. This however was unknown territory for me, but I managed to get it to work.

So let's get to it! First off we create a `packages/types/package.json` file:
```json
{
  "name": "types",
  "version": "0.0.0",
  "license": "MIT",
  "scripts": {
    "build": "tsc --build",
    "start": "tsc -w"
  },
  "main": "dist/index.js",
  "types": "dist/index.d.ts",
  "dependencies": {},
  "devDependencies": {
    "typescript": "^4.6.4"
  }
}
```
and a `packages/types/tsconfig.json`:
```json
{
  "compilerOptions": {
    /* Basic Options */
    "declaration": true,
    "declarationMap": true,
    "sourceMap": true,
    "composite": true,
    "outDir": "./dist",
    "rootDir": "./src",
    "target": "esnext",
    "module": "esnext"
  },
  "include": ["./src"]
}
```
And adding `{ "path": "./packages/types" }` to the references in the root `tsconfig.json`.

The `types.ts` file that was initially at the root of our project will move to `packages/types/src/index.ts`. That is basically it. 

What we setup now is a separate package that will export some types that we can import in other projects by importing from `types` where this name is taken from the `name` key inside the `package.json` of that package. To make this work we do have to make sure that our types package is build, otherwise our IDE will complain. 

To do that we are going to add and change some scripts in our root `package.json`:
```json
// add
"types": "cd ./packages/types && yarn start && cd ../..",
"types:build": "cd ./packages/types && yarn build && cd ../.."

// change
"dev": "npm-run-all --parallel types portal server widget",
"build": "npm-run-all types:build portal:build widget:build
```

### Updating all types imports
Next we have to update our project everywhere we import from `<relative_path>/types`, this is needed in the following files:
- packages/portal/src/components/ClientChat.vue
- packages/portal/src/stores/client.ts
- packages/server/types.ts
- packages/widget/src/App.vue
- packages/widget/src/stores/socket.ts

Also update the `tsconfig.json` of the other packages to remove the `rootDir` property and add `"references": [{ "path": "../types" }]` as a new property after the `include` array. Finally remove `../../types.ts` from the `include` array in each file.

### Checking if we can build
Let's run `yarn run build` to see what happens when all packages are build. You should see that a `dist` directory is created with 3 folders and a packages.json. If this is the first time you build the `types` packages you will see that some files inside a `packages/types/dist` folder are created. We need to commit those to the repository as well. But we do want to ignore those when linting, so in our `.eslintignore` we change `/dist` to `dist`. To ignore `dist` folders anywhere, not just at the root level.

We can run our server now by running:
```bash
node dist/server/index.js
```

Which we will add as a script inside the root `package.json` as well for convenience: `"start": "node dist/server/index.js",`.

## Getting ready for deployment - environment variables
> Git diff for this section [here](https://github.com/Evertvdw/creating-a-embeddable-chat-widget/compare/e904dcc..522b44c?diff=split)

Our build server should run now but going to `localhost:5000` will return `Cannot GET /` as our paths defined inside `packages/server/index.ts` are only correct for development 🤷. In fact it would make sense to only add this when we are running a build app, so a good use case to add environment variables to make some thing configurable based on development versus production, where with production I mean running the `dist/server/index.js` file produced by `yarn run build`.

### Setting up environment variables
Two of our projects are Vite projects which will pick up .env files by default as documented [here](https://vitejs.dev/guide/env-and-mode.html#env-variables-and-modes). I found out about this figuring out the best way to add environment variables, so I learned something new this part🎉.

We can create `.env.<production|development>` files which will be picked up by vite automatically at either build or development. 

We will create the variable `VITE_SOCKET_URL` as that will not be the same during development and production. 

Inside `packages/portal/src/boot/socket.ts` remove the URL declaration and instead do:
```ts
const socket = io(import.meta.env.VITE_SOCKET_URL, {
  autoConnect: false,
}); 
```
Do the same for `packages/widget/src/App.vue`. 

At this point typescript will complain so we have to inform it that we will supply this variable by adding to `packages/widget/src/env.d.ts` and `packages/portal/src/env.d.ts`:
```ts
interface ImportMetaEnv {
  readonly VITE_SOCKET_URL: string;
  // more env variables...
}

interface ImportMeta {
  readonly env: ImportMetaEnv;
}
```

And also add `/// <reference types="vite/client" />` at the top of `packages/portal/src/env.d.ts`.

### Providing the variables for widget and portal
Vite will pickup .env.development files when in development mode, so lets create `packages/portal/.env.development` and `packages/widget/.env.development`:
```env
VITE_SOCKET_URL=http://localhost:5000
```

To make VSCode highlight the files a bit better, add to `.vscode/settings.json`:
```json
"files.associations": {
  "*.env.*": "env"
}
```

### Small improvement to package.json scripts
Along the way trying stuff out I found out that you can pass a `cwd` argument to yarn commands that will execute them in a specific working directory, eliminating the need to do `cd <path>` and `cd ../..` in every script. So instead of:

```bash
cd ./packages/server && yarn start && cd ../..
```
We can do:
```
yarn --cwd ./packages/server start
```

Much better in my opinion so I changed all the scripts to use this pattern. Also I updated every script to call `start` when in development and `build` for building. This means changing the scripts inside the package.json of two packages.

In `packages/widget/package.json` rename the `dev` script to `start`, and update `packages/portal/package.json` scripts to contain:

```json
"start": "quasar dev",
"build": "quasar build"
```

### Environment variables for the server
There is an important distinction between environment variables in the server compared to the widget and portal. The portal and the widget will run client side (in the browser) and any environment variables used there are read when the project is build, so they are compiled to static variables by rollup in our case. The server will run in nodeJS, which means that the variables mentioned there are not compiled at build time. They will need to be present at runtime. So at the place we start our `index.js` the environment variables have to be present.

For the server we will have three variables:
1. APP_ENV - to signal to our code if we run in production or development
2. PORT - the port our server will listen at
3. JWT_SECRET - the secret that is used to create our jwt tokens

Define them for typescript inside `packages/server/env.d.ts`:
```ts
declare namespace NodeJS {
  interface ProcessEnv {
    PORT: string;
    JWT_SECRET: string;
    APP_ENV: 'development' | 'production';
  }
}
```

For development we can use defaults (in the code) for these variables, so that means we only will have to define them when we are deploying the app.

Let's set defaults, inside `packages/server/index.ts` we read and use the PORT variable:
```ts
// add these lines
import path from 'path';

const port = process.env.PORT || 5000;

// change
server.listen(port, () => {
  console.log(
    `Server started on port ${port} at ${new Date().toLocaleString()}`
  );
});
```

We also serve the portal and widget only when APP_ENV is equal to `production`:
```ts
if (process.env.APP_ENV === 'production') {
  app.use(serveStatic(path.join(__dirname, './../../dist/widget')));
  app.use(serveStatic(path.join(__dirname, './../../dist/portal')));
}
```

Finally we want to prevent that we run in production with the default JWT_SECRET if we somehow fail to provide it, so lets add a check for it, inside the try-catch before we call `server.listen`:
```ts
if (process.env.APP_ENV === 'production' && !process.env.JWT_SECRET) {
  throw new Error('Should provide JWT_SECRET env variable');
}
```

Next update the `packages/server/middleware/socket.ts` and `packages/server/routes/auth.ts` to use the JWT_SECRET if present by inserting `process.env.JWT_SECRET ||` after `secret = `.

## Deploying an Heroku app
If you do not have an account at Heroku, create one [here](https://www.heroku.com/). Also install the Heroku [CLI](https://devcenter.heroku.com/articles/heroku-cli), which we will use to deploy our app.

In your Heroku dashboard create a new app. Go to the Settings tab and to `Config vars`, in here we will create two variables for now:
1. JWT_SECRET - set this one to some long string
2. APP_ENV - set this to `production`

### Doing the deploy
Deploying to Heroku is done by pushing code from a certain branch to a repository that comes with your heroku app. First login with the Heroku CLI if you have not done so yet:

```bash
heroku login
```

After that we need to add our heroku app as an extra remote in git we can push to. We can do that by running:
```
heroku git:remote -a <name-of-your-app>
```
Fill in the name of your app that you have chosen upon creating it, in my case that was `embeddable-chat-widget-part-5`. Once that is run you can check that a remote was added by running `git remote -v`, and you should see a remote called `origin` and a remote called `heroku`. 

To push our code to heroku and start the deploy you need to run:
```bash
git push heroku main
// or
git push heroku <other-local-branch>:main
```
and that will start the deploy, which will output in the command line.

> Heroku will only deploy stuff you push to it's main branch or master branch. That is why you have to do `:main` if you want to push a different branch to a certain heroku app.

### Fixes and stuff
If you have coded along and pushed the branch so far to heroku you will probably have seen a build error, and if not atleast things don't work as expected when opening the app. There are a couple of fixes needed, which I will highlight in the next sections.

### Production .env file
When we were setting up environment variables we skipped defining them for production. We need to create two files `packages/portal/.env.production` and `packages/widget/.env.production` with the following content:
```env
VITE_SOCKET_URL=https://<your-app-name>.herokuapp.com
```
Where the URL should be the url of your heroku app.

### Node engine
We currently specify in our root `packages.json` inside the `engines` property: `"node": ">= 14"` and Heroku will look at this to determine which node version to use when building our app. This will cause it to take the latest version available which is a non-lts version, which for some reason did not work for me. So change this to `"node": "16.x"`, which will take the last version of version 16.

### Using absolute path when serving portal and widget 
Inside `packages/server/index.ts` we have to update the lines that use `serveStatic`
```ts
// Add at top
import path from 'path';

// Update
app.use(serveStatic(path.join(__dirname, './../../dist/widget')));
app.use(serveStatic(path.join(__dirname, './../../dist/portal')));
```

### Don't hardcode the login URL
Inside `packages/portal/src/stores/auth.ts` I forgot to update the login urls, which still harcode to `localhost:5000`, which will not work once deployed of course. We created an environment variable called `VITE_SOCKET_URL` for this.
```
// Replace login url to
`${import.meta.env.VITE_SOCKET_URL}/auth/login`

// Replace refresh_token url to
`${import.meta.env.VITE_SOCKET_URL}/auth/refresh_token`
```

### Widget package missing headers
When we get the widget package to use on a different site we have to send some headers along to allow different origins to use this package, so in `packages/server/index.ts` update:
```ts
app.use(serveStatic(path.join(__dirname, './../../dist/widget')));
// becomes
app.use(
  serveStatic(path.join(__dirname, './../../dist/widget'), {
    setHeaders: (res) => {
      res.header('Cross-Origin-Resource-Policy', 'cross-origin');
    },
  })
);
```

### Allow codepen origins
I want to demonstrate our setup later by importing the widget inside a codepen and using it there, to make that work we have to add `'https://cdpn.io'` to our allowed cors origins inside `packages/server/index.ts`. Add it to both `origin: [...]` arrays in that file.

### Fixing the bug mentioned earlier
Before I mentioned that by serving the portal and the widget caused a bug, and it has to do with the order of the code. When setting up express routes like `/auth/<something>` the order of setup matters. By using history mode and calling `app.use(history())` it sets up a catch all listener for GET requests that will serve up the index.html. By placing this before the `app.use('/auth')` call, the GET routes inside of it will be intercepted by the history catch all listener.

So we have to move our serveStatic lines after the `app.use('/auth')`, in order to make it work as expected. I also placed the `history()` call inside the if statement, as that is only necessary when deploying.
```ts
// Move this
if (process.env.APP_ENV === 'production') {
  app.use(history());
  app.use(
    serveStatic(path.join(__dirname, './../../dist/widget'), {
      setHeaders: (res) => {
        res.header('Cross-Origin-Resource-Policy', 'cross-origin');
      },
    })
  );
```

## Wrapping up
After these changes you can push the changes to the heroku branch as before and it will redeploy.

Here is a video of it in action:

![Demo of the app](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/e1eh3ocg18g34bwmyu7y.gif)
 
You can check out my deployed app [here](https://embeddable-chat-widget-part-5.herokuapp.com). I made a test user account that you can login with:
- email: admin@admin.nl
- password: admin

There is also a codepen [here](https://codepen.io/Evertvdw/full/BaYjMmy) which loads in the widget and displays it. This is done by including a script on the page with the source `https://embeddable-chat-widget-part-5.herokuapp.com/widget.umd.js` and then placing a `<chat-widget/>` element in the HTML, easy peasy👌

See you in the next part!