Adding webpack to a HTML+Typescript project
This article chronicles a small part of my journey towards creating a full website that can be rendered in three parallel frameworks and three parallel design systems (React + Material UI; Angular + Clarity; “Vanilla” JS + Carbon).
← Previous article: Refactoring an HTML site into Typescript, part 2of 2
This article starts at this commit in my “Parallel Frameworks Project”. At this point, the project is a simple single page application (SPA) with source code written and built with Typescript, and loaded into a single HTML template.
This setup worked well to get the project off the ground; however, as I outlined in my previous article, even this simple setup quickly accumulated technical debt. In brief, the initial setup left my code exposed to:
- Zero test coverage
- Inconsistent formatting
- Potential syntax errors
- Reliance on dependencies outside the project
- Manual import handling
- … and more
To address these formatting and syntax errors, I’ll implement Prettier and ESLint, respectively, in future articles. For now, I want to set the stage to address test coverage, minimizing external dependencies, automatic import handling, and more, by implementing webpack to parse, bundle, and, while developing, even serve my code.
At a basic level, webpack is already an excellent tool for “bundling” Javascript code to improve how it is served in the browser and, therefore, to improve user exerpience. But it also excels at integrating and choreographing other tools — a benefit I’ll cover in greater depth in future articles.
For now, it’s enough to just set up webpack
, itself.
npm i -D webpack webpack-cli
The above command installs webpack
and its command-line interpreter. I also installed babel-loader
(and some of its sub-tools), which webpack
cna use to parse code, including transpiling from Typescript and preparing for the browser:
npm i -D babel-loader @babel/core @babel/preset-env
@babel/preset-typescript
Given this is a Typescript project, I could have used ts-loader
; however, babel
can also coordinate with other tools, such as Istanbul
, which I plan to implement for code coverage, so it made more sense to implement babel
from the beginning.
The key to webpack
is its configuration file, webpack.config.js
. At the time of writing, webpack
has an excellent setup tutorial, which I drew on to implement my own configuration. Given the quality of their tutorial, I won’t attempt to create another step-by-step configuration guide; instead, I’ll highlight some of the key configuration decisions I made. The full configuration file resulting from this article is available on Github.
The core to a webpack configuration are its entry
and output
specifications:
Webpack offers sensible defaults, but also extensive customization options. For my setup, I set entry
to my source code index file, and bundled it into a single output file, written to ./dist/index.js
. Down the line, I anticipate splitting the output into multiple files, but I have a little while to go before that becomes necessary.
The next key piece is module.rules
, which specifies how you want each different filetype to be handled. In a simple project like mine, all the file extensions mentioned in resolve
can be handled with a single rule. As the project grows, additional rules will likely become necessary:
The above rule tells webpack
to handle all files ending in .(j/t)s(x)
files using babel-loader
, which has been pre-configured to support converting modern Javascript to browser-friendly code (@babel/preset-env
), and to support parsing and transpiling Typescript (@babel/preset-typescript
).
Another key piece is resolve
, which gives wepback
and ordered list of file extensions to try and use when resolving imports. This is very helpful, as it solves the “manual imports” issue I faced, where I was writing code in .ts
files but importing as .js
files, because that’s what the transpiled code would expect. Three cheers for improved Developer Experience (DX)! In anticipation of eventually implementing React
, I went ahead and added .*sx
files from the get-go:
From now, I can list an import as ../some-file
, and webpack
will initially try and find ../some-file.ts
; failing that, it will look for ../some-file.tsc
, ../some-file.js
, and so forth. If it can’t find any of those, then I’ve likely done something wrong.
There’s one other quick configuration I wanted to implement, before adapting my project and running the build. At this point, I still had a <script .../>
tag in my public/index.html
file, which pointed towards the transpiled dist/src/index.js
. I didn’t want this to be hard-coded, however, because a) webpack
was configured to output the code to dist/index.js
, which could later be changed, and b) when I get to code-splitting, I would not know ahead of time the names of the multiple Javascript output files.
To solve this, I installed the HtmlWebpackPlugin
which, among other things, inserts the necessary <script .../>
tags based on webpack's
output into the HTML template of your choice. Add the plugin to the configuration, specify the desired template, and all is well. webpack
will even output the HTML template into the output directory (dist
), so I’ll be able to remove that step from my build script.
With the configurations in place, I was finally able to make the few adjustments to my project necessary to support the new setup! The first two changes were straightfoward — remove the file extensions from my import
commands, and remove the <script .../>
tag from my HTML template. Much cleaner!
I then needed to update my build
command in package.json
. I also took this opportunity to install two helper packages, rimraf
for clearing directories and npm-run-all
for running multiple npm
commands, either in sequence (run-s
, as seen below) or in parallel (run-p
). Here’s a quick before & after:
build:cp-public
is no longer necessary because HtmlWebpackPlugin
is moving the template into dist
. I still want to maintain type-checking but no longer need to emit any output files — just catching the errors is enough. And I want to clear the ./dist
folder before each install, to make sure no old files hang around. The core build step is now… webpack
!
There’s ONE final step before I considered webpack
fully set-up, and that was installing webpack-dev-server
— an very useful tool that builds a version of your project but holds it in memory rather than on disk, serves the project on your localhost
, watches for edits you make during development, and reloads your project in the browser every time you save a file. It is a valuable tool for improving DX, and it also happens to solve the “external dependency” I had on the VSCode Live Server Plugin. Plugin, you served me well, but forced me to rely on external tooling.
npm i -D webpack-dev-server
As you can probably guess, webpack-dev-server
is highly configurable. But for now, I just needed to tell it from where to serve it’s dev build:
webpack.config.js
=================...
devServer: {
contentBase: './dist',
},
...
Add a new command to package.json
…
"start": "webpack serve --open"
.. and I was done! Going forward, I will leave npm run start
running in a terminal window while developing, and be able to see my changes appear in the browser in near real time. Exciting, and a good place to end this article!