How to Serve a Single Page Application (SPA) using Rollup.js and Web Dev Server
Tags: typescript, rollup, webdevserver, webdev
Published: 2021-01-20 04:44:06

In a previous post, I explained [How to setup a TypeScript project using Rollup.js](https://labs.thisdot.co/blog/how-to-setup-a-typescript-project-using-rollup-js). In that article, I covered the necessary steps to have a [Project Setup](https://github.com/luixaviles/typescript-rollup/tree/01-setup-ts-rollup) ready to generate a build using TypeScript and Rollup configurations.

So let's take that project as a starting point to serve a Single-page Application using modern web tools.

## What is a SPA (Single-page Application)?

According to [MDN Documentation](https://developer.mozilla.org/en-US/docs/Glossary/SPA):

> An SPA (single-page application) is a web app implementation that loads only a single web document, and then updates the body content of that single document via JavaScript APIs such as XMLHttpRequest and Fetch when different content is to be shown.

A single-page application allows updating only some portions of the current page instead of doing a page refresh. Think about some applications you use every day: 

* Gmail
* Facebook
* Twitter
* LinkedIn

All of these applications have been built with the user experience in mind along with a good loading speed and navigation between pages.

There are very good reasons to keep building applications following the SPAs approach.

## What is Web Dev Server?

[Web Dev Server](https://www.npmjs.com/package/@web/dev-server), as its name states, it's a web server for development. It helps development using native browser features like ES modules. It has a plugin architecture for code transformations.

> Web Dev Server is the successor of [es-dev-server](https://www.npmjs.com/package/es-dev-server)

It's worth mentioning that Web Dev Server allows configuring auto-reload on file changes along with efficient browser caching for faster reloads. It's configurable and supports [rollup plugins](https://modern-web.dev/docs/dev-server/plugins/rollup/) too!

## Project Setup

### Prerequisites
You'll need to have installed the following tools in your local environment:

* **Node.js**. Preferably the latest [LTS version](https://nodejs.org/en/about/releases/).
* **A package manager**. You can use either NPM or Yarn. This tutorial will use NPM.

### Initialize the Project
Let's create a clone or [download](https://github.com/luixaviles/typescript-rollup/archive/01-setup-ts-rollup.zip) the project seed before adding the new configurations and tools:

```bash
git clone https://github.com/luixaviles/typescript-rollup.git
cd typescript-rollup/
git checkout tags/01-setup-ts-rollup -b 02-serve-spa 
```

The previous commands will download the project and create a new branch `02-serve-spa` to get started.

### Source Code Files
Once you have the project ready, open it with your favorite code editor and pay attention to the current project structure:

```text
|- typescript-rollup
    |- src/
        |- math/
            |- math.ts
            |- index.ts
        |- string/
            |- string.ts
            |- index.ts
        |- app.ts
    |- package.json
    |- rollup.config.js
    |- tsconfig.json
```

### Installing Web Dev Server and Concurrently
Let's add some development dependencies to the project.

* Install Web Dev Server

```bash
npm install --save-dev @web/dev-server
```

As I mentioned before, Web Sev Server will be the main tool to configure and run our project this time.

Also, if you already know `es-dev-server` tool, used actively on the [Open Web Components](https://open-wc.org/) initiative, you'll understand that this project migrated to [Modern Web](https://modern-web.dev/docs/dev-server/overview/) website. That does not mean anything other than this new tool continues to be actively developed and can be used in any JavaScript project (TypeScript included ;-)).

* Install Concurrently tool

```bash
npm install --save-dev concurrently
```

If you are familiar with any command-line tool, you may have wanted to run commands in parallel. 

[Concurrently](https://www.npmjs.com/package/concurrently) has the same goal. However, it's hard to keep track of every command output and `concurrently` allows you to run any command you want. It's possible to kill all of them if anyone fails.

### TypeScript Configuration
In this case, the `tsconfig.json` file will remain the same, without any changes:

```json
{
  "compilerOptions": {
    "target": "es2018",                          
    "module": "esnext",
    "moduleResolution": "node",                     
    "noEmitOnError": true,
    "lib": ["es2017"],                            
    "strict": true,  
    "esModuleInterop": false,                 
    "outDir": "out-tsc",
    "rootDir": "./"
  }
  ,
  "include": ["./src/**/*.ts"]
}
```

Remember, this file is required to be present in the root of any [TypeScript project](https://www.typescriptlang.org/docs/handbook/tsconfig-json.html).

### Rollup Configuration
The project is configured to use [Rollup](https://rollupjs.org/guide/en/) as the module bundler. It's possible to run it through command-line parameters. However, if you're looking for advanced functionality, you can consider a `rollup.config.js` file:

```js
import merge from 'deepmerge';
import { createBasicConfig } from '@open-wc/building-rollup';

const baseConfig = createBasicConfig();

export default merge(baseConfig, {
  input: './out-tsc/src/app.js',
  output: {
      dir: 'dist',
  }
});
```

This file contains a basic configuration. I's needed to set the `input` files and the `output` directory for the build.

Since we intend to compile a Single-page application now, it will be necessary to apply some changes:

```js
import merge from 'deepmerge';
import { createSpaConfig } from '@open-wc/building-rollup';

const baseConfig = createSpaConfig({
  developmentMode: process.env.ROLLUP_WATCH === 'true',
  injectServiceWorker: false
});

export default merge(baseConfig, {
  // any <script type="module"> inside will be bundled by Rollup
  input: './index.html'
});
```

The `baseConfig` content will be generated from `createSpaConfig`, which is defined in [@open-wc/building-rollup](https://github.com/open-wc/open-wc/tree/master/packages/building-rollup) package. 

When `createSpaConfig` is used, a service worker is generated using Workbox. However, the service worker is injected into the `index.html` file when `injectServiceWorker` is enabled.

#### HTML file as an Input

Once the `input` parameter is set using an `index.html` file, any module defined as part of it will be bundled by Rollup. This content will be injected into the HTML file result.

### The index.html File

Let's create an `index.html` file into the root folder with the following content:

```html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>My Single Page Application</title>
</head>
<body>
    <h1>Welcome to your Single Page Application</h1>
    <span>This App uses:</span>
    <ul>
        <li>TypeScript</li>
        <li>Rollup.js</li>
        <li>es-dev-server</li>
    </ul>
    <script type="module" src="./out-tsc/src/app.js"></script>
</body>
</html>
```

We can say the content of this file is common. However, the most important line would be the reference to the `app.js` file:

```html
<script type="module" src="./out-tsc/src/app.js"></script>
```

Let's explain what is happening there:

* As you will remember, the `tsconfig.json` file sets `"outDir": "out-tsc"` and that means the TypeScript compiler will generate the output files in `./out-tsc` folder.
* Then, the `index.html` file does reference to the entry point of the source code, the compiled version of the `./src/app.ts` file.
* When the Rollup configuration starts to build the app, it will take the _module_ defined in `index.html` file to generate a bundle ready for it.

### Adding Serve and Build Scripts
If you pay attention to the `package.json` file, it already defines a script to generate the build:

```json
...
"scripts": {
    "build": "rimraf dist && tsc && rollup -c rollup.config.js"
},
...
```

The change we introduced into the `rollup.config.json` file doesn't affect the build process. 

Now let's move forward with the addition of some scripts to "serve" the application:

```json
... 
"scripts": {
    "tsc:watch": "tsc --watch",
    "start": "concurrently --kill-others --names tsc,web-dev-server \"npm run tsc:watch\" \"web-dev-server --app-index index.html --node-resolve --open --watch\"",
    "build": "rimraf dist && tsc && rollup -c rollup.config.js"
},
...
```

* **"tsc:watch"** is the script to start with the compilation process through `tsc`, which is the TypeScript compiler. The `--watch` param stands for a compiler option to run the compiler in _watch mode_(it triggers recompilation on file changes).
* **"start"** is the script to add the execution of some commands in parallel: `npm run tsc:watch` and `web-dev-server` that includes some [CLI flags](https://modern-web.dev/docs/dev-server/cli-and-configuration/).
  * You already noted that `concurrently` command is called first! 
  * The `--kill-others` parameter will kill all the invoked commands if one dies (either `tsc` or `web-dev-server`).

Think for a while how the "start" command is going to look if we're planning to add more configurations in the future: a script that is hard to read and maintain, of course.

#### Adding the web-dev-server.config.js File

The alternative option to include the CLI flags as parameters is the creation of the `web-dev-server.config.js` file. 

> The file extension can be .js, .cjs or .mjs. A .js file will be loaded as an es module or common js module based on your version of node, and the package type of your project.

By default, the Web Dev Server looks at the configuration file in the current working directory.

Let's create the `web-dev-server.config.js` file in the root folder:

```js
// web-dev-server.config.js

module.exports = {
    port: 8000,
    nodeResolve: true,
    open: true,
    watch: true,
    appIndex: 'index.html',
};
```

This file now contains the same options we used in the **start** script with the addition of a custom port number: `port: 8000`, which can be configured now.

For the sake of simplicity, let's update the **start** script with a reference to the new configuration file:

```json
... 
"scripts": {
    ...
    "start": "concurrently --kill-others --names tsc,web-dev-server \"npm run tsc:watch\" \"web-dev-server --config web-dev-server.config.js\"",
    ...
},
...
```

That's it! You're ready to serve your first application using:

```bash
npm run start
```

You should see the following output:

```bash
Web Dev Server started...
[web-dev-server] 
[web-dev-server]   Root dir: /Users/luixaviles/projects/typescript-rollup
[web-dev-server]   Local:    http://localhost:8000/
[web-dev-server]   Network:  http://192.168.1.2:8000/
[web-dev-server] 
```

Also, your default browser will be opened to display the `index.html` content.

![single-page-application rollup web-dev-server](//images.ctfassets.net/zojzzdop0fzx/6az24Qh6PoJjqnPKKuQRHU/46d942da414f09cc0de6af1edeb967ee/single-page-application_rollup_web-dev-server.png)

On the right side (when you open the Developer Tools), you'll see the output of the main script file. That is proof that the module is running correctly when the SPA is loaded.

#### Running the Build

The script is ready and doesn't need an update. Execute the following command:

```bash
npm run build
```

This will generate a `dist` folder with the following content:

```text
|- dist/
    |- 7b857f5b.js
    |- index.html
    |- ... other scripts
```

A simple way to serve these files locally would be to use the `http-server` tool (a command-line http server):

```bash
http-server dist/ -o
```

The output of this command will show you the host and the port you need to use to access the build version of the app:

```bash
Starting up http-server, serving dist/
Available on:
  http://127.0.0.1:8080
  http://192.168.1.2:8080
Hit CTRL-C to stop the server
open: http://127.0.0.1:8080
```

I suggest you review the content of `dist/` files and make sure you have [http-server](https://www.npmjs.com/package/http-server) installed before running the latest command.

## Source Code of the Project

Find the complete project in this GitHub repository: [typescript-rollup](https://github.com/luixaviles/typescript-rollup/tree/02-serve-spa). Do not forget to give it a star ⭐️ and play around with the code.

Feel free to reach out on [Twitter](https://twitter.com/luixaviles) if you have any questions. Follow me on [GitHub](https://github.com/luixaviles) to see more about my work.

---
*This Dot Labs is a modern web consultancy focused on helping companies realize their digital transformation efforts. For expert architectural guidance, training, or consulting in React, Angular, Vue, Web Components, GraphQL, Node, Bazel, or Polymer, visit [thisdotlabs.com](https://www.thisdotlabs.com).*

*This Dot Media is focused on creating an inclusive and educational web for all.  We keep you up to date with advancements in the modern web through events, podcasts, and free content. To learn, visit [thisdot.co](https://www.thisdot.co).*