This project shows how to build with:
- Node.js 14 (with ESM)
- Webpack 5 (with hot reload)
- TypeScript 4
- React 17
in the mid of 2021.
- No Babel in this project.
- No Jest support until this issue been solved.
- May install beta/rc stage packages due to ESM is still new comparing to CommonJS.
- Node.js 14 will print warning for some experimental features. I left it untouched so you will know what should be noticed in the future.
- Use
ts-node
to run webpack configuration in TypeScript. - Use
ts-loader
to compile.tsx
and.ts
files.
We use yarn here because npm sucks.
yarn init -y
-y
will use default values for package.json
fields, instead of asking
questions.
First we need a bundler, i.e. Webpack. Webpack can process your sources and bundle them to HTML/JavaScript/CSS files. Webpack itself is just a library, so we also need a CLI tool to run it.
yarn add -D webpack webpack-cli
You can invoke webpack CLI by yarn:
yarn webpack
Webpack configuration can be written in TypeScript. Let's start it with a simple file.
import webpack from 'webpack';
export default function factory () {
const config: webpack.Configuration = {
};
return config;
};
Webpack accepts a configuration object as the default export, but it also accepts a factory function which returns a configuration object. We export a function here because it is more flexable.
Webpack will detect file extension of the configuration file. If Node.js cannot
interpret it, Webpack will try to find a plugin to load the file.
There are several packages can do this, @babel/register
and ts-node
probably
are the most well known.
Since we are not going to use Babel here, we will go with ts-node
.
ts-node
is simply a wrapper application which let Node.js work with
TypeScript, so we still need tsc
for transpilation.
To do this we need to install ts-node
and typescript
first.
yarn add -D ts-node typescript @types/nodes@14
@types/nodes@14
is a peer dependency of ts-node
, which is not required.
I just add it here to prevent warnings.
Specifing 14
because I'm targeting on Node.js 14.
We need to create a tsconfig.json
for tsc
.
ts-node
, tsc
and even your editor need this file to understand how to
process your typescripts.
Let's just create a template first.
yarn tsc --init
ts-node
already provides some predefined options, we can simply extends from
it.
{
"extends": "ts-node/node14/tsconfig.json",
"compilerOptions": {
}
}
By default TypeScript use it's own module resolution system, we need to change it to support Node.js.
{
"compilerOptions": {
"moduleResolution": "node"
}
}
TypeScript also provides DOM library type information, it would be helpful to turn it on.
{
"compilerOptions": {
"lib": ["dom"]
}
}
To Enable ESM, these are essential.
Add a field in package.json
to declare this is an ESM package.
{
"type": "module"
}
TypeScript needs to know it should resolve/generate code in ESM instead of
CommonJS. So we need to change some fields in tsconfig.json
.
{
"compilerOptions": {
"target": "ESNEXT",
"module": "ESNext"
}
}
Now we are ready to load the webpack configuration with this command.
NODE_OPTIONS='--loader=ts-node/esm' yarn webpack --node-env=production
--node-env=production
is an option for Webpack itself, which will set
NODE_ENV
to production
(which is used in many libraries), and set mode
to
production
(which will used by any webpack plugins).
NODE_OPTIONS='--loader=ts-node/esm'
is a temporary solution for ts-node
to
setup some experimental features for Node.js. We probably won't need this in the
future, but for now it is required.
This option will also cause Node.js prints some warnings, all about experimental
features are turned on. In the future we probably won't see those again.
It would be handy to have a simpler shortcut to run the above command, so add a
build script to package.json
like this.
{
"scripts": {
"build": "NODE_OPTIONS='--loader=ts-node/esm' webpack --node-env=production"
}
}
Note that we DO NOT need to call yarn
in the script. yarn
will find the
internal executable by itself.
We need react
for virtual DOM, and react-dom
for browser environment.
yarn add react react-dom
Note we actually need these packages in runtime, so they are dependencies
instead of devDependencies
.
It is also a great idea to have type informations.
yarn add -D @types/react @types/react-dom
tsc
also supports JSX syntax, we just need to enable it in tsconfig.json
.
{
"compilerOptions": {
"jsx": "react"
}
}
Now we are ready to compile TypeScript code. We will deal with HTML/CSS later soon.
src/index.tsx
import React from 'react';
import ReactDOM from 'react-dom';
function Application () {
return <h1>Hello, world!</h1>;
}
ReactDOM.render(<Application />, document.querySelector('.body'));
This is really simple, we defined a component Application
, and render to an
element which has class body
.
Note that it is not recommended to render a component as a immediate child to
document.body
.
Now we need to add ts-loader
to let Webpack to understand how to transpile
TypeScript to JavaScript.
Do not confuse this with ts-node
.
ts-node
is used to allow Webpack read configuration files
(i.e. webpack.config.*
) which written in TypeScript.
Where ts-loader
is used to allow Webpack to process source files which
written in TypeScript.
So the pipeline is more or less like this.
node
usests-node
loader to gain TypeScript compatibility.node
executeswebpack-cli
which useswebpack
API.webpack
readswebpack.config.ts
(thanks tots-node
).webpack
usests-loader
to process*.ts
to*.js
.
yarn add -D ts-loader
Modify webpack.config.ts
like this.
export default function factory () {
const config: webpack.Configuration = {
entry: './src/index.tsx',
module: {
rules: [
{
test: /\.tsx$/,
exclude: /node_modules/,
use: {
loader: 'ts-loader',
},
},
],
},
};
return config;
};
entry
is the start point(s) of your project. Webpack will find all used
sources/resources from the script, and bundle them together.
module
describes how to process resources.
We have a rule inside rules
which describes how we want to do with .tsx
files. We test for file extension and exclude ones in node_modules
, let them
pass through ts-loader
.
Now you should be able to run this to build the source.
yarn build
Because we did not specify output path and output name, by default it is
dist/main.js
.
We at least need a HTML file to be a complete web page. Here we use
html-webpack-plugin
to generate the HTML page.
yarn add -D html-webpack-plugin
html-webpack-plugin
will generate a default HTML file for you. But since we
want to render our component into an element matches .body
, which the default
page does not have, we have to create our own template.
By default html-webpack-plugin
will use src/index.ejs
as the template, let's
just use this file name.
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Webpack App</title>
<meta name="viewport" content="width=device-width,initial-scale=1">
</head>
<body>
<div class="body"></div>
</body>
</html>
By using the default settings in html-webpack-plugin
, we just need few
modification to webpack.config.ts
.
import HtmlWebpackPlugin from 'html-webpack-plugin';
const config: webpack.Configuration = {
entry: './src/index.tsx',
module: { /* ... */ },
plugins: [
new HtmlWebpackPlugin(),
],
};
During development, we don't want to run yarn build
everytime. Instead we will
start a development server which will watch file changes, auto rebuild project,
and notify the browser to reload pages.
To do so we use webpack-dev-server
here.
As this guide was written, webpack-dev-server
was not ready for Webpack 5 yet.
So we need to install beta version.
yarn add -D webpack-dev-server@next
Then we simply add a new command to package.json
to call it.
{
"scripts": {
"start": "NODE_OPTIONS='--loader ts-node/esm' webpack-dev-server --node-env=development"
}
}
Use yarn start
will start the server.
While auto reload is handy, every reload will reset states on the page, which is still inconvenient while developing. Hot reload is a feature which allow webpack hot patch changed parts in the page, so there is no need to reload the whole page.
We need react-refresh
for this, and react-refresh-typescript
for TypeScript
support, finally @pmmmwh/react-refresh-webpack-plugin
to setup it in Webpack.
yarn add -D react-refresh react-refresh-typescript @pmmmwh/react-refresh-webpack-plugin@next
@pmmmwh/react-refresh-webpack-plugin
has some problem with React 17, so we
install RC version here.
Modify webpack.config.ts
.
import process from 'process';
import webpack from 'webpack';
import HtmlWebpackPlugin from 'html-webpack-plugin';
import ReactRefreshWebpackPlugin from '@pmmmwh/react-refresh-webpack-plugin';
import ReactRefreshTypeScript from 'react-refresh-typescript';
export default function factory () {
const isDevelopment = process.env.NODE_ENV !== 'production';
const config: webpack.Configuration = {
entry: './src/index.tsx',
module: {
rules: [
{
test: /\.tsx$/,
exclude: /node_modules/,
use: {
loader: 'ts-loader',
options: {
getCustomTransformers() {
return {
before: isDevelopment ? [ReactRefreshTypeScript()] : [],
};
},
},
},
},
],
},
plugins: [
new HtmlWebpackPlugin(),
],
};
if (config.plugins && isDevelopment) {
config.plugins.push(new ReactRefreshWebpackPlugin());
}
return config;
};
Modify package.json
.
{
"scripts": {
"start": "NODE_OPTIONS='--loader ts-node/esm' webpack-dev-server --node-env=development --hot"
}
}