Setup webpack for TypeScript and SASS Manually

25/02/2022 • 9 min read

Introduction

By the end of this guide, you will have a project that you can customize its inner workings anytime. Even if you don't want to customize it, you will learn how does the module bundling work in action.

What could this bring to you?

  • You won't have to eject to customize your bundling
  • You can make your features such as making the routing similar to Next.js
  • Broader debugging opportunities

Here is what we are going to do:


Cover of Setup TypeScript and SASS Manually with webpack

Final project repository on GitHub: BerkeKaragoz/berkekaragoz-webpack-post

Preparing the Initial Project

We create a default npm project in our folder: npm init -y

We will have a package.json file that looks like this:

{
  "name": "berkekaragoz-webpack-scss",
  "version": "1.0.0",
  "description": "",
  "main": "index.js", "comment": "<-delete the main"
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC"
}

We are deleting the main entry since webpack is going to tell that.

Now we will create the other project files according to the following tree:

dist/
node_modules/ //already created
src/
 ├───index.css
 ├───index.html
 ├───index.js
 └───sample.jpg // or any image

You can fill them as you wish, here are the contents of what I did.

index.css:

html {
   background-color: #333;
   color: aliceblue;
}

index.html, create one with the emmet snippet !:

<!DOCTYPE html>
<html lang="en">
   <head>
      <meta charset="UTF-8" />
      <meta http-equiv="X-UA-Compatible" content="IE=edge" />
      <meta name="viewport" content="width=device-width, initial-scale=1.0" />
      <title>Document</title>
   </head>
   <body></body>
</html>

index.js:

console.log("I didn't watch Star Wars.")

sample.jpg: A link of my recent photo

Configuring webpack For the Initial Project

Install webpack and its CLI as a dev dependency: npm i -D webpack webpack-cli

Create the webpack.config.js under the root of the project folder. And don't forget to read the comments.

const path = require("path")

ENTRY_PATH = path.resolve(__dirname, "src/index")
DIST_PATH = path.resolve(__dirname, "dist")

module.exports = {
   mode: "development", // or production
   entry: {
      // remember that we deleted the main in package.json
      // this is where the project starts
      main: ENTRY_PATH,
   },
   output: {
      // this is where the bundled packages go
      path: DIST_PATH,
      // [name] is the name of the file and
      // [contenthash] is the hash of the bundle
      filename: "[name].[contenthash].js",
      clean: true, // remove existing previous bundles
   },
   //later
   module: {},
   plugins: [],
}

Add the build command to the package.json under scripts:

{
   "scripts": {
      "test": "...",
      "build": "webpack"
   }
}

Run the build command: npm run build

And you will see that your code will be bundled under the dist/ folder: main.7487bec281371ef08592.js

You can run this with node: node ./dist/main.7487bec281371ef08592.js

The output: I didn't watch Star Wars.

This means that it works perfectly for what we have configured. It only bundles the JS at this time.

HTML, CSS and JS Together

Now we have to bundle HTML and CSS with that.

HTML

Install html-webpack-plugin to help us bundle HTML: npm i -D html-webpack-plugin

Import the plugin in webpack.config.js:

// const path = require("path");
const HtmlWebpackPlugin = require("html-webpack-plugin");

//... under module.exports
  plugins: [
    new HtmlWebpackPlugin({
      filename: "index.html", // output
      // the one which we have created
      template: path.resolve(__dirname, "src/index.html"),
    }),
  ],

Ensure that your project works by building it. When you open the bundled dist/index.html and open the console, you will see the JS output in there.

CSS

Now we have to setup loaders. These loaders import the assets such as CSS, SCSS as well as images and others things like TS. Those come in handy when dealing with parsing/compilation situations.

We will install two loaders: npm i -D style-loader css-loader

The css-loader will get the CSS file and passes it to the JS file. The style-loader will get the CSS file from the JS file and will pass that to the HTML.

We will set those up in the module in webpack.config.js:

module: {
  rules: [{ test: /\.css$/, use: ["style-loader", "css-loader"] }], // have to be inorder
},

Import the CSS file to index.js:

import "./index.css"
//console.log("I didn't watch Star Wars.");

And when you build and open the bundled HTML file, you will see that the background will be gray (#333). This means that the setup is successful.

Assets

Now we will add resources such as images. For this, we need to add another rule in the webpack.config.js:

rules: [
//...
  { test: /\.(jpg|jpeg|png|svg|ico|webp|gif)$/, type: "asset/resource" },
]
// And if you want to provide a name template
// add the following under the `output`:
  assetModuleFilename: "[name]-[contenthash][ext]",

We will test this later on, but it's probably okay. This is for using assets directly in our code.

Most of the time you will be using your public/ folder to access resources. This way, you will be able to switch to a CDN in production.

We make a public/ folder and copy its contents into the build folder dist/. To do this we will install a copying plugin: npm i -D copy-webpack-plugin

The configuration is pretty straightforward, webpack.config.js:

const CopyPlugin = require("copy-webpack-plugin");
// Create the path of the static folder as public
PUBLIC_PATH = path.resolve(__dirname, "public");
// Under module.exports:
  plugins: [ //...
    new CopyPlugin({
      patterns: [{ from: PUBLIC_PATH, to: DIST_PATH }],
    }),
  ],

Now if you put the sample.jpg into the public/ folder, you will be able to access it with the /sample.jpg url. You will see the image in the dist/ if you build your application.

Configuring webpack for Development

Now we are successfully bundling our code. We have to make the development easier. In the end, we wouldn't want to build every time we want to test our code.

We will install the development server: npm i -D webpack-dev-server

Add the script to run the server to the package.json:

{
   "scripts": {
      "dev": "webpack serve --open",
      "build": "..."
   }
}

The --open option is if you want to open your default browser when the server is ready to serve.

With this, we will be accessing our project from the memory instead of the disk.

Now we can configure more options in webpack.config.js:

  plugins: [
    //...
  ],
  devtool: "inline-source-map", //for tracing
  devServer: {
    // so that it can watch for other changes
    static: DIST_PATH,
    hot: true, // hot reload, save to refresh
  },

Notice the devtool, that is how we can know where the bundles come from. This way we can trace and debug possible errors or bugs.

Now you can run npm run dev to develop properly.

Setup TypeScript

First, we install TypeScript and its loader: npm i -D typescipt ts-loader

Create the tsconfig.json file:

{
  "compilerOptions": {
    "outDir": "./dist/",
    "noImplicitAny": true,
    "module": "es6",
    "target": "es5",
    "jsx": "react",
    "sourceMap": true,
    "allowJs": true,
    "moduleResolution": "node",
    // generate default imports if they don't exist
    "allowSyntheticDefaultImports": true,
    "esModuleInterop": true
  },
  // our project shouldn't touch these
  "exclude": ["node_modules", "**/*.spec.ts", "**/*.test.ts"],
  // look only for the following
  "include": ["src", "index.d.ts"]
}

Configure ts-loader in webpack.config.js:

  rules: [ //...
    {
      test: /\.tsx?$/,
      use: 'ts-loader',
      exclude: /node_modules/,
    },
  ],
  //...
  module: {/*...*/},
  resolve: { // works like autocomplete for imports
    extensions: ['.tsx', '.ts', '.js'],
  },

Change src/index.js to src/index.ts and test it out:

import "./index.css"

let myVar: number
myVar = 1
console.log("myVar is", myVar)

You should see the output.

There is one more step to complete the TS setup. The resources will not work anymore in our code. Because we have to declare modules for them. Let's do that after configuring React so that we can see that in action.

Configuring React

Let's get into rendering components with React. You can also use a different library.

Install React: npm i react react-dom @types/react @types/react-dom

Firstly, let's create our component at src/App.tsx:

import React from "react"
//import sampleImg from "./sample.jpg";
import "./index.scss"

export const App: React.FC = () => {
   return (
      <div>
         <h1>BerkeKaragoz webpack Post</h1>
      </div>
   )
}

export default App

Now we have to render it somehow. But React requires a div to start on. So we modify our src/index.html file:

<body>
   <!-- div#__react -->
   <div id="__react"></div>
</body>

We will select that div with its id and mount the App.tsx to it in index.ts:

import React from "react"
import ReactDOM from "react-dom"
import App from "./App"

ReactDOM.render(React.createElement(App), document.getElementById("__react"))

Notice that you cannot call the App with JSX notation. Because index file has .ts extension instead of .tsx. You can configure that on your own if you want to. But for this article, I will continue.

When you run your app, you will see the heading in the document with the proper colors in the index.css file.

Assets, again

Now while you can access the resources under public/ folder, you will get an error if you try to import your asset to your code directly. That happens because we have to declare them as modules for TypeScript at the root as index.d.ts:

declare module "*.jpg"
declare module "*.jpeg"
declare module "*.png"
declare module "*.svg"
declare module "*.ico"
declare module "*.webp"
declare module "*.gif"

Remember that we had included this file in tsconfig.json before. That's how it gets into our project.

Test it out in App.tsx:

import sampleImg from "./sample.jpg";
//...
  <img src={"/sample.jpg"} alt="sample" />
  <img src={sampleImg} alt="sample" />

Now both of the images should be in your document. If it works and your IDE shows an error, restart it.

Configuring SASS

First, we install SASS and its loader like we did before. Since SASS works like TS, it compiles its files to CSS, we will also install them as dev dependencies: npm i -D sass sass-loader

Setup the loader process in webpack.config.js:

  rules: [ //...
    {
      test: /\.s[ac]ss$/,
      use: ["style-loader", "css-loader", "sass-loader"],
    },
  ],

As I have explained, sass-loader passes its output to the css-loader. The rest is the same with CSS.

And since they are also seen as resources, we have to declare them as modules too in index.d.ts:

//...
declare module "*.scss"
declare module "*.sass"

We can test it out with a normal file, but let's use CS modules. Lets create App.module.scss:

$maxWidth: 512px;

.image {
   max-width: $maxWidth;
   height: $maxWidth * 0.5;
   object-fit: cover;
}

And apply the class to our images in App.tsx:

import style from "./App.module.scss";
//...
<img src={"/sample.jpg"} alt="sample" className={style.image} />
<img src={sampleImg} alt="sample" className={style.image} />

You will see that the images will be resized.

The project output screenshot

Final

Now you have built a project from scratch with webpack that you can use TypeScript and SCSS in it!

Where to?

You should optimize this process by making use of caching and optimization options.

"Setup webpack for TypeScript and SASS Manually", 25/02/2022, 10:11:00

#guide, #react, #sass, #typescript, #web-development, #webpack