Skip to content

How to Serve a Single Page Application (SPA) using Rollup.js and Web Dev Server

This article was written over 18 months ago and may contain information that is out of date. Some content may be relevant but please refer to the relevant official documentation or available resources for the latest information.

In a previous post, I explained How to setup a TypeScript project using Rollup.js. In that article, I covered the necessary steps to have a Project Setup 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:

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, 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

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 too!

Project Setup

Prerequisites

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

  • Node.js. Preferably the latest LTS version.
  • 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 the project seed before adding the new configurations and tools:

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:

|- 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
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 initiative, you'll understand that this project migrated to Modern Web 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
npm install --save-dev concurrently

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

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:

{
  "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.

Rollup Configuration

The project is configured to use Rollup 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:

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:

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 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:

<!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:

<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:

...
"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:

... 
"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.
    • 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:

// 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:

... 
"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:

npm run start

You should see the following output:

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

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:

npm run build

This will generate a dist folder with the following content:

|- 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):

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:

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 installed before running the latest command.

Source Code of the Project

Find the complete project in this GitHub repository: typescript-rollup. Do not forget to give it a star ⭐️ and play around with the code.

Feel free to reach out on Twitter if you have any questions. Follow me on GitHub to see more about my work.

This Dot is a consultancy dedicated to guiding companies through their modernization and digital transformation journeys. Specializing in replatforming, modernizing, and launching new initiatives, we stand out by taking true ownership of your engineering projects.

We love helping teams with projects that have missed their deadlines or helping keep your strategic digital initiatives on course. Check out our case studies and our clients that trust us with their engineering.

Let's innovate together!

We're ready to be your trusted technical partners in your digital innovation journey.

Whether it's modernization or custom software solutions, our team of experts can guide you through best practices and how to build scalable, performant software that lasts.

Prefer email? hi@thisdot.co