5

I am using webpack 5 and I currently have the following setup:

  • webpack.prod.js - where I have some specific configs for production (e.g. image compression, devtool, CSS minification, specific meta tags values)
  • webpack.dev.js - where I have some specific configs for development (e.g. no image compression, no CSS minification, specific meta tags values)

The issue I am currently facing is that I am unable to get webpack dev server live reloading to work (this apply to all file types). I've been through the docs but no luck so far.

As far as I understand, when in development mode, webpack runs stuff in memory rather than in disk (which is supposed to be faster and that is great!). For some reason it appears that the watcher is not reacting to changes in the files specified in the devServer.watchFiles config. I was expecting webpack to detect changes on a typescript file, compile it and reload, but that's not happening.

You can find the contents of both files below.

webpack.prod.js:

const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const CssMinimizerPlugin = require("css-minimizer-webpack-plugin");
const TerserPlugin = require('terser-webpack-plugin');
const ImageMinimizerPlugin = require('image-minimizer-webpack-plugin');
const CopyPlugin = require('copy-webpack-plugin');
const buildPath = path.resolve(__dirname, 'dist');

module.exports = {
  //devtool: 'source-map',
  entry: {
    index: "./src/index/index.ts",
    error: "./src/error/error.ts",
  },
  output: {
    filename: "js/[name].[contenthash].js",
    path: buildPath,
    clean: true,
  },
  module: {
    rules: [{
        test: /\.ts$/i,
        exclude: /node_modules/,
        use: "ts-loader",
      },
      {
        test: /\.html$/i,
        exclude: /node_modules/,
        use: "html-loader",
      },
      {
        test: /\.css$/i,
        exclude: /node_modules/,
        use: [
          MiniCssExtractPlugin.loader,
          "css-loader",
        ]
      },
      {
        test: /\.png$/i,
        exclude: /node_modules/,
        type: "asset/resource",
        generator: {
          filename: "img/[name].[contenthash][ext]",
        },
      },
      {
        test: /\.(woff|woff2|ttf)$/i,
        exclude: /node_modules/,
        type: "asset/resource",
        generator: {
          filename: "fonts/[name].[contenthash][ext]",
        },
      },
      {
        test: /\.mp3$/i,
        exclude: /node_modules/,
        type: "asset/resource",
        generator: {
          filename: "[name].[contenthash][ext]",
        },
      },
    ],
  },
  plugins: [
    new HtmlWebpackPlugin({
      template: "./src/index/index.ejs",
      inject: "body",
      chunks: ["index"],
      filename: "index.html",
      meta: {
        "robots": {
          name: "robots",
          content: "index,follow"
        },
      },
    }),
    new HtmlWebpackPlugin({
      template: "./src/error/error.html",
      inject: "body",
      chunks: ["error"],
      filename: "error.html",
    }),
    new MiniCssExtractPlugin({
      filename: "css/[name].[contenthash].css",
      chunkFilename: "css/[id].[contenthash].css",
    }),
    new CopyPlugin({
      patterns: [{
        from: "src/robots.txt",
        to: "robots.txt",
      }, ],
    }),
  ],
  optimization: {
    minimize: true,
    minimizer: [
      new TerserPlugin({
        parallel: true,
      }),
      new CssMinimizerPlugin(),
      new ImageMinimizerPlugin({
        minimizer: {
          implementation: ImageMinimizerPlugin.imageminMinify,
          options: {
            plugins: [
              ["imagemin-pngquant", {
                quality: [0.5, 0.9]
              }],
            ],
          },
        },
      }),
    ],
  },
};

webpack.dev.js:

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

module.exports = {
    mode: "development",
    devtool: "eval-cheap-module-source-map",
    entry: {
        index: "./src/index/index.ts",
        error: "./src/error/error.ts",
    },
    devServer: {
        watchFiles: [path.resolve(__dirname, "src/**/*")],
        open: true,
    },
    module: {
        rules: [
            {
                test: /\.ts$/i,
                exclude: /node_modules/,
                use: "ts-loader",
            },
            {
                test: /\.html$/i,
                exclude: /node_modules/,
                use: "html-loader",
            },
            {
                test: /\.css$/i,
                exclude: /node_modules/,
                use: ["style-loader", "css-loader"]
            },
            {
                test: /\.png$/i,
                exclude: /node_modules/,
                type: "asset/resource",
                generator: {
                    filename: "img/[name].[contenthash][ext]",
                },
            },
            {
                test: /\.(woff|woff2|ttf)$/i,
                exclude: /node_modules/,
                type: "asset/resource",
                generator: {
                    filename: "fonts/[name].[contenthash][ext]",
                },
            },
            {
                test: /\.mp3$/i,
                exclude: /node_modules/,
                type: "asset/resource",
                generator: {
                    filename: "[name].[contenthash][ext]",
                },
            },
        ],
    },
    plugins: [
        new HtmlWebpackPlugin({
            template: "./src/index/index.ejs",
            inject: "body",
            chunks: ["index"],
            filename: "index.html",
            meta: {
                "robots": { name: "robots", content: "noindex, nofollow" },
            },
        }),
        new HtmlWebpackPlugin({
            template: "./src/error/error.html",
            inject: "body",
            chunks: ["error"],
            filename: "error.html"
        }),
    ],
    optimization: {
        runtimeChunk: "single",
    },
};
nobitta
  • 195
  • 1
  • 11

3 Answers3

2

The issue was related with WSL. More specifically running webpack in WSL on Windows file system (e.g. at /mnt/c/Users/MyUser/Documents/MyProject).

After moving the project to WSL file system (e.g. at /home/MyUser/MyProject) I was able to get live reload to work.

Even though this question mentions Parcel and Webpack, it is similar. I found the answer provides good context around the issue: https://stackoverflow.com/a/72786450/3685587

nobitta
  • 195
  • 1
  • 11
1

There are a few spots where I am a little bit blind because you didn't add some description about your tsconfig.json file and your package.json, but I will try to do my best to explain you the key points here and afterwards explain the solution.

The ts-module

It is the module that uses the typescript module for compiling the files, it requires the typescript node_module installed in the app and a proper tsconfig.json.

For practical purposes I pasted bellow the configuration that I used in previous projects -- super basic you can improve it depending on your project --; There is nothing special about the config, it will not affect the HMR but it is required for the compiling.

{
  "compilerOptions": {
    "noImplicitAny": true,
    "removeComments": true,
    "allowSyntheticDefaultImports": true,
    "esModuleInterop": true,
    "outDir": "./dist/",
    "sourceMap": true,
    "module": "commonjs",
    "target": "es6",
    "allowJs": true,
  },
  "includes": ["/**/*"]
}

webpack-dev-server

The package for starting the dev server in your local environment, this is quite straight forward, this should be installed along webpack and webpack-cli.

Your package.json could have a start script pointing to your webpack.dev.js configuration:

"scripts" {
   "start": "webpack-dev-server --config ./webpack.dev.js",
}

In my understanding there are a few different ways to start the dev server, you can check the documentation

The Webpack configuration

There are a few missing things to consider before implementing a "Hot reload"

The resolver configuration

I pasted bellow a pre-define list that I did for one of my projects, you can modify it to accept less/more extensions, but the idea is to "tell webpack" which files will be able to compile.

This will allow you to imports files with those extensions.

resolve: {
      extensions: [
        ".js",
        ".jsx",
        ".ts",
        ".tsx",
        ".less",
        ".css",
        ".json",
        ".mjs",
      ],
    }

After applying that, Webpack should already be able to compile the files; typescript and the ts-loader should be able to watch your files properly. But, this doesn't mean "Hot Module Reload", this is just reloading your browser when something changes, the dev server will do "its magic" by its own.

I hope that the gif shows the proof that the configuration works

enter image description here

The full webpack config looks like:

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


module.exports = {
  mode: "development",
  devtool: "eval-cheap-module-source-map",
  entry: {
    index: "./src/index/index.ts",
    error: "./src/error/error.ts",
  },
  resolve: {
    extensions: [
      ".js",
      ".jsx",
      ".ts",
      ".tsx",
      ".less",
      ".css",
      ".json",
      ".mjs",
    ],
  },
  module: {
    rules: [
      {
        test: /\.ts$/i,
        exclude: /node_modules/,
        use: [
          {
            loader: "ts-loader",
          },
        ],
      },
      {
        test: /\.html$/i,
        exclude: /node_modules/,
        use: "html-loader",
      },
      {
        test: /\.css$/i,
        exclude: /node_modules/,
        use: ["style-loader", "css-loader"],
      },
      {
        test: /\.png$/i,
        exclude: /node_modules/,
        type: "asset/resource",
        generator: {
          filename: "img/[name].[contenthash][ext]",
        },
      },
      {
        test: /\.(woff|woff2|ttf)$/i,
        exclude: /node_modules/,
        type: "asset/resource",
        generator: {
          filename: "fonts/[name].[contenthash][ext]",
        },
      },
      {
        test: /\.mp3$/i,
        exclude: /node_modules/,
        type: "asset/resource",
        generator: {
          filename: "[name].[contenthash][ext]",
        },
      },
    ],
  },
  plugins: [
    new HtmlWebpackPlugin({
        template: "./src/index/index.ejs",
        inject: "body",
        chunks: ["index"],
        filename: "index.html",
        meta: {
            "robots": { name: "robots", content: "noindex, nofollow" },
        },
    }),
    new HtmlWebpackPlugin({
        template: "./src/error/error.html",
        inject: "body",
        chunks: ["error"],
        filename: "error.html"
    })
  ],
  optimization: {
    runtimeChunk: "single",
  },
};

For proper Hot Module Reload -- stands for just update pieces of your changed code without reloading the whole browser --

A few more adjustments are required, there are a lot of resources in the internet about this that I will try to list at the end, but since you are not using babel -- that works differently -- you need to provide a few more configurations in your webpack and also remove other properties.

Update ts-loader configuration

First check the ts-loader docs about HMR https://github.com/TypeStrong/ts-loader#hot-module-replacement and the implications.

They advice to add a configuration called transpileOnly, but if you check the specifications about that option you will see that you loose some type checking in the next compilation, for that reason they advice to install another package

It's advisable to use transpileOnly alongside the fork-ts-checker-webpack-plugin to get full type checking again. To see what this looks like in practice then either take a look at our example.

Your ts-loader rule should look like:

rules: [
      {
        test: /\.ts$/i,
        exclude: /node_modules/,
        use: [
          {
            loader: "ts-loader",
            options: {
              transpileOnly: true,
            },
          },
        ],
      },

And your plugins:

plugins: [
    ..., // Your HTML plugins
    new ForkTsCheckerWebpackPlugin(),
  ],

Enabling Hot Module Reload

This is easier than it looks, you need to consider two key points:

  1. Let webpack know that you want HMR
  2. Let your code know that will be Hot reloaded -- sound like funny but yes --

The first point, is super easy, just add to your webpack configuration the following configuration:

devServer: {
    hot: true,
},

But for the second point, it is a little bit tricky, with a simple tweaks you can do it; The easiest way is via creating a new file per entry point, the file can be named as you wish, but in this example I will call it hot.ts, and will look like the code pasted bellow:

// Inside of your index folder
require("./index");

if (module.hot) {
  module.hot.accept("./index.ts", function () {
    console.log("Hot reloading index");
    require("./index");
  });
}
// Inside of your error folder
require("./error");

if (module.hot) {
  module.hot.accept("./error.ts", function () {
    console.log("Hot reloading error");
    require("./error");
  });
}

Side Note: You will find yourself with type errors with the module global variable, to solve that you need to install the following types npm i --save -D @types/node @types/webpack-env

And in your dev configuration mode -- usually the HRM is intended for development not for production -- you need to adjust the entry points:

entry: {
    index: "./src/index/hot.ts",
    error: "./src/error/hot.ts",
},

Don't override the HMR watcher

I didn't find a clear answer from the documentation, but seems like your watchFiles inside of your devServer overrides the HMR watcher, if you remove that -- even from the build dev, it doesn't makes a difference in this configuration -- the HMR should work smoothly.

After following the previous steps your webpack file should look like the following code:

const HtmlWebpackPlugin = require("html-webpack-plugin");
const ForkTsCheckerWebpackPlugin = require('fork-ts-checker-webpack-plugin');


module.exports = {
  mode: "development",
  devtool: "eval-cheap-module-source-map",
  entry: {
    index: "./src/index/hot.ts",
    error: "./src/error/hot.ts",
  },
  devServer: {
    hot: true,
  },
  resolve: {
    extensions: [
      ".js",
      ".jsx",
      ".ts",
      ".tsx",
      ".less",
      ".css",
      ".json",
      ".mjs",
    ],
  },
  module: {
    rules: [
      {
        test: /\.ts$/i,
        exclude: /node_modules/,
        use: [
          {
            loader: "ts-loader",
            options: {
              transpileOnly: true,
            },
          },
        ],
      },
      {
        test: /\.html$/i,
        exclude: /node_modules/,
        use: "html-loader",
      },
      {
        test: /\.css$/i,
        exclude: /node_modules/,
        use: ["style-loader", "css-loader"],
      },
      {
        test: /\.png$/i,
        exclude: /node_modules/,
        type: "asset/resource",
        generator: {
          filename: "img/[name].[contenthash][ext]",
        },
      },
      {
        test: /\.(woff|woff2|ttf)$/i,
        exclude: /node_modules/,
        type: "asset/resource",
        generator: {
          filename: "fonts/[name].[contenthash][ext]",
        },
      },
      {
        test: /\.mp3$/i,
        exclude: /node_modules/,
        type: "asset/resource",
        generator: {
          filename: "[name].[contenthash][ext]",
        },
      },
    ],
  },
  plugins: [
    new HtmlWebpackPlugin({
        template: "./src/index/index.ejs",
        inject: "body",
        chunks: ["index"],
        filename: "index.html",
        meta: {
            "robots": { name: "robots", content: "noindex, nofollow" },
        },
    }),
    new HtmlWebpackPlugin({
        template: "./src/error/error.html",
        inject: "body",
        chunks: ["error"],
        filename: "error.html"
    }),
    new ForkTsCheckerWebpackPlugin(),
  ],
  optimization: {
    runtimeChunk: "single",
  },
};

Repository with the working example: https://github.com/nicolasjuarezn/webpack-ts-example

Example of configuration working: enter image description here

Useful links:

I hope that this helps you!

  • I didn't realize but I was too focused in the dev server mode, for the production configuration the only difference is that you don't use the Hot Module Reload, but the rest is the same, you need the resolver configuration well done and remove the `watchFiles`. Maybe you will want to bundle the code in a different way, I recommend you to experiment more with the output configuration or the optimization conf (https://webpack.js.org/configuration/optimization/) or use Terser (https://webpack.js.org/plugins/terser-webpack-plugin/) and choose a good server library. From there the sky is the limit. – Nicolás Juárez Nuño Aug 29 '22 at 20:07
  • Hello, thanks for reaching out and for the detailed explanation! I tried everything you said and it still didn't work. That's when I noticed a weird pattern happening in the console. I was keep getting console log messages about webpack disconnecting. I researched a bit and found out that this was happening because I was using WSL 2 in windows file system. After moving the project to WSL file system, everything started working just fine. It now works with both, mine and your configuration :) – nobitta Aug 29 '22 at 22:39
  • There is still one thing happening regarding the images, even when I use your configuration. When I refresh the page some images fail to load with a 404. Any clue why this might be happening? Changing the `png` module rule type from `asset/resource` to `asset/inline` is enough to workaround it. – nobitta Aug 29 '22 at 22:44
  • Hey, tbh, its a bit hard to know the reason, but my guess is that perhaps you are not indicating properly the URL to the image. In the case that you are adding the image in the index.ejs or error.html with a simple html tag, it should look like: `` And the `/` represents the public path that usually aims to a folder called `public` in your root. You can modify that path with https://webpack.js.org/guides/public-path/ . I have just tried it and worked for me. Refreshing the page and everything. – Nicolás Juárez Nuño Aug 29 '22 at 23:06
  • Because of the cache busting I had to do it this way: ``, so I am not sure if the public path plays a role here. Because on the first load the images load just fine but any subsequent refresh causes a `404`. – nobitta Aug 29 '22 at 23:15
  • If you imports the images in ts, the other thing that I am thinking perhaps the hashing of the image makes the things a little bit more complicated for the second refresh to find the image? Did you try removing the `[contenthash]` instead of changing `asset/resource`? But as I said, it is hard to know the reason in my env works in both ways, with public path static asset and without public path importe asset. Hope that you find the solution! – Nicolás Juárez Nuño Aug 29 '22 at 23:15
  • Hmm, I just tried your code and also works on my end, maybe there is something else in your config that I can't see... if you find something else please post it here. – Nicolás Juárez Nuño Aug 29 '22 at 23:19
0

For webpack 5 use

  devServer: {
    watchFiles: ['src/**/*.php', 'public/**/*'],
  },

See details here https://webpack.js.org/configuration/dev-server/#devserverwatchfiles