This chapter describes how to create web apps via TypeScript and webpack. We will only be using the DOM API, not a particular frontend framework.
GitHub repository: ts-demo-webpack
The repository ts-demo-webpack
that we are working with in this chapter, can be downloaded from GitHub.
You should be roughly familiar with:
In this chapter, we stick with what is best supported by TypeScript: CommonJS modules, bundled as script files.
ts-demo-webpack
This is how the repository ts-demo-webpack
is structured:
ts-demo-webpack/
build/ (created on demand)
html/
index.html
package.json
ts/
src/
main.ts
tsconfig.json
webpack.config.js
The web app is built as follows:
ts/
html/
build/
with the complete web app:
build/main-bundle.js
. This process is called bundling and main-bundle.js
is a bundle file.build/
.Both output tasks are handled by webpack:
Copying the files in html/
to build/
is done via the webpack plugin copy-webpack-plugin
.
This chapter explores two different workflows for bundling:
ts-loader
.dist/
(like we did in the previous chpater). Then webpack doesn’t need a loader and only bundles JavaScript files.Most of this chapter is about using webpack with ts-loader
. At the end, we briefly look at the other workflow.
package.json
package.json
contains metadata for the project:
{
"private": true,
"scripts": {
"tsc": "tsc",
"tscw": "tsc --watch",
"wp": "webpack",
"wpw": "webpack --watch",
"serve": "http-server build"
},
"dependencies": {
"@types/lodash": "···",
"copy-webpack-plugin": "···",
"http-server": "···",
"lodash": "···",
"ts-loader": "···",
"typescript": "···",
"webpack": "···",
"webpack-cli": "···"
}
}
The properties work as follows:
"private": true
means that npm doesn’t complain if we don’t provide a package name and a package version.tsc, tscw
: These scripts invoke the TypeScript compiler directly. We don’t need them if we use webpack with ts-loader
. However, they are useful if we use webpack without ts-loader
(as demonstrated at the end of this chapter).wp
: runs webpack once, compile everything.wpw
: runs webpack in watch mode, where it watches the input files and only compiles files that change.serve
: runs the server http-server
and serves the directory build/
with the fully assembled web app.webpack
: the core of webpackwebpack-cli
: a command line interface for the corets-loader
: a loader for .ts
files that compiles them to JavaScriptcopy-webpack-plugin
: a plugin that copies files from one location to another onets-loader
: typescript
http-server
lodash
, @types/lodash
webpack.config.js
This is how we configure webpack:
const path = require('path');
const CopyWebpackPlugin = require('copy-webpack-plugin');
.exports = {
module
···entry: {
main: "./ts/src/main.ts",
,
}output: {
path: path.resolve(__dirname, 'build'),
filename: "[name]-bundle.js",
,
}resolve: {
// Add ".ts" and ".tsx" as resolvable extensions.
extensions: [".ts", ".tsx", ".js"],
,
}module: {
rules: [
// all files with a `.ts` or `.tsx` extension will be handled by `ts-loader`
test: /\.tsx?$/, loader: "ts-loader" },
{ ,
],
}plugins: [
new CopyWebpackPlugin([
{from: './html',
},
]),
]; }
Properties:
entry
: An entry point is the file where webpack starts collecting the data for an output bundle. First it adds the entry point file to the bundle, then the imports of the entry point, then the imports of the imports, etc. The value of property entry
is an object whose property keys specify names of entry points and whose property values specify paths of entry points.
output
specifies the path of the output bundle. [name]
is mainly useful when there are multiple entry points (and therefore multiple output bundles). It is replaced with the name of the entry point when assembling the path.
resolve
configures how webpack converts specifiers (IDs) of modules to locations of files.
module
configures loaders (plugins that process files) and more.
plugins
configures plugins which can change and augment webpack’s behavior in a variety of ways.
For more information on configuring webpack, see the webpack website.
tsconfig.json
This file configures the TypeScript compiler:
{
"compilerOptions": {
"rootDir": "ts",
"outDir": "dist",
"target": "es2019",
"lib": [
"es2019",
"dom"
],
"module": "commonjs",
"esModuleInterop": true,
"strict": true,
"sourceMap": true
}
}
The option outDir
is not needed if we use webpack with ts-loader
. However, we’ll need it if we use webpack without a loader (as explained later in this chapter).
index.html
This is the HTML page of the web app:
<!doctype html>
<html>
<head>
<meta charset="UTF-8">
<title>ts-demo-webpack</title>
</head>
<body>
<div id="output"></div>
<script src="main-bundle.js"></script>
</body>
</html>
The <div>
with the ID "output"
is where the web app displays its output. main-bundle.js
contains the bundled code.
main.ts
This is the TypeScript code of the web app:
import template from 'lodash/template';
= document.getElementById('output');
const outputElement if (outputElement) {
= template(`
const compiled <h1><%- heading %></h1>
Current date and time: <%- dateTimeString %>
`.trim());
.innerHTML = compiled({
outputElement: 'ts-demo-webpack',
heading: new Date().toISOString(),
dateTimeString;
}) }
template()
to turn a string with custom template syntax into a function compiled()
that maps data to HTML. The string defines two blanks to be filled in via data:
<%- heading %>
<%- dateTimeString %>
compiled()
to the data (an object with two properties) to generate HTML.First we need to install all npm packages that our web app depends on:
npm install
Then we need to run webpack (which was installed during the previous step) via a script in package.json
:
npm run wpw
From now on, webpack watches the files in the repository for changes and rebuilds the web app whenever it detects any.
In a different command line, we can now start a web server that serves the contents of build/
on localhost:
npm run serve
If we go to the URL printed out by the web server, we can see the web app in action.
Note that simple reloading may not be enough to see the results after changes – due to caching. You may have to force-reload by pressing shift when reloading.
Instead of building from a command line, we can also do that from within Visual Studio Code, via a so-called build task:
Execute “Configure Default Build Task…” from the “Terminal” menu.
Choose “npm: wpw”.
A problem matcher handles the conversion of tool output into lists of problems (infos, warning, and errors). The default works well in this case. If you want to be explicit, you can specify a value in .vscode/tasks.json
:
"problemMatcher": ["$tsc-watch"],
We can now start webpack via “Run Build Task…” from the “Terminal” menu.
webpack-no-loader.config.js
Instead of using on ts-loader
, we can also first compile our TypeScript files to JavaScript files and then bundle those via webpack. How the first of those two steps works, is described in the previous chapter.
We now don’t have to configure ts-loader
and our webpack configuration file is simpler:
const path = require('path');
.exports = {
moduleentry: {
main: "./dist/src/main.js",
,
}output: {
path: path.join(__dirname, 'build'),
filename: '[name]-bundle.js',
,
}plugins: [
new CopyWebpackPlugin([
{from: './html',
},
]),
]; }
Note that entry.main
is different. In the other config file, it is:
"./ts/src/main.ts"
Why would we want to produce intermediate files before bundling them? One benefit is that we can use Node.js to run unit tests for some of the TypeScript code.