I’ve been working on my side project on and off and I finally decided to get with the times and upgrade my gulp kludges to modern Webpack.

Previously I had simply used the TypeScript compiler to do a 1:1 compile from TypeScript files to ES2016 JavaScript files - no bundling! - and System.JS to load it all. This worked well for developing on my machine but it lacked features like hot module replacement. My day job also introduced me to CSS modules and dynamic typings for them.

My technology stack is .NET Core, TypeScript, SASS and Bootstrap. You could also adapt this for LESS or plain CSS easily by swapping or dropping the sass-loader. I also use React so I used a React library for hot module replacement, but I’m sure the Angular one will work just as well.

After all this I have a setup with hot module replacement in development but cache-busting filenames in production. Not that I have a production.

This information is all correct for webpack 3.5.6 and ASP.NET Core 2.0. Newer versions of ASP.NET will probably still work, but who knows with webpack?

If you want the full config file just skip to the end.

Update

I’ve added version numbers to the npm packages. This should help you reproduce this setup.

You should use whatever versions are current, but they may require changes.

Basic Webpack setup for TypeScript

You’ll need these npm packages:

  • webpack 3.5.6
  • awesome-typescript-loader 3.2.3
  • source-map-loader 0.2.1

First I adapted the basic webpack.config.js for the ASP.NET core layout. Since webpack.config.js lives in the webapp root I wanted it to export to wwwroot. The reason for prepending js/ to the filename will become clearer later.

Right now this will generate bundle.js but there’s no CSS modules or reloading. Simply add ~/js/bundle.js to your Razor _Layout.cshtml to get started.

module.exports = {
    output: {
        publicPath: "/",
        path: __dirname + './wwwroot/',
        filename: "js/bundle.js",
    },

    entry: {
        bundle: {
            "./Scripts/src/index.ts"
        }
    },

    devtool: "source-map",

    resolve: {
        extensions: [".ts", ".tsx", ".js"]
    },

    module: {
        rules: [
            { test: /\.tsx?$/, use: "awesome-typescript-loader" },
            { enforce: "pre", test: /\.js$/, use: "source-map-loader" }
        ]
    },

    /* Include these if you're loading React separately from webpack */
    externals: {
         "react": "React",
         "react-dom": "ReactDOM"
    }
};

Adding CSS modules

The main reason I did all this was for CSS modules. Encapsulation of CSS is really useful when your CSS goes over more than a few pages.

You’ll need to add these npm packages:

  • sass-loader 6.0.6
  • css-loader 0.28.7
  • typings-for-css-modules-loader 1.5.0

First add .css and .sass to the extensions webpack handles:

    resolve: {
        extensions: [".ts", ".tsx", ".js", ".css", ".scss"]
    },

Next I added the CSS module loader. This combines three different loaders, sass-loader does the SASS to CSS conversion, typings-for-css-modules-loader outputs a .d.ts file so you can reference the css module from TypeScript and finally style-loader loads the styles via bundle.js so there’s no need for a separate style tag. Later on I extract this to a separate cssfile for production, but in dev loading through JavaScript is much easier.

module: {
    rules: [
        ...
        {
            test: /\.scss$/,
            use: [
                "style-loader",
                {
                    loader: 'typings-for-css-modules-loader',
                    query: {
                        modules: true,
                        importLoaders: 1,
                        camelCase: true,
                        namedExport: true,
                        localIdentName: '[name]_[local]_[hash:base64:5]'
                    }
                },
                "sass-loader"]
        }
    ]
}

Now you can “import” a SASS file directly from TypeScript and Webpack will take care of creating a unique name.

import { mainDiv } from "./mystyles.css";

export function render() {
    return `<div class=${mainDiv}>Lorem ipsum</div>`;
}

There is one caveat though, as Webpack only calls typings-for-css-modules-loader on files it sees the .ts file must be imported somewhere, and the first build will fail due to missing types. I advise committing .d.ts files. It’s a bit annoying to start without types but it’s not as bad as not having any!

Adding hot module replacement

You’ll need to add these npm packages

  • react-hot-loader 3.0.0-beta.7 (or whatever your framework uses)
  • webpack-hot-middleware 2.19.1

This was surprisingly easy. I added the react-hot-loader npm module and added two lines, first to the entry

entry: {
    bundle: [
        "./Scripts/src/index.ts",
        "react-hot-loader/patch"
    ]
}

And the second to the TypeScript rule:

module: {
    rules: [
        { 
            test: /\.tsx?$/, 
            use: [
                "react-hot-loader/webpack",
                "awesome-typescript-loader"
            ]
        },
    ]
}

But right now nothing actually check for changes. Most tutorials have you run webpack-dev-server as a proxy, but I can do better! Microsoft have created Microsoft.AspNetCore.SpaServices for this, so dotnet add package Microsoft.AspNetCore.SpaServices.

Next make a small change to your Startup.cs.

using Microsoft.AspNetCore.SpaServices.Webpack;

...

public void Configure(IApplicationBuilder app, IHostingEnvironment env,
{
...
    if (env.IsDevelopment())
    {
        app.UseWebpackDevMiddleware(new WebpackDevMiddlewareOptions
        {
            HotModuleReplacement = true,
        });
...

This will now run webpack in a node process in the background checking for changes and push them through to your browser, and you only need to run the .NET Core process!

Production-ifying this

So far this is a great dev setup, but we don’t want webpack running in production. Also I want production filenames to be unique so I don’t end up getting an older cached version.

I needed two more npm packages:

  • extract-text-webpack-plugin 3.0.0
  • webpack-manifest-plugin 1.3.2

The first problem I faced is that the production and development configs are almost but not quite identical. I took advantage of the fact that webpack.config.js is a plain old JavaScript file and extracted webpack.common.js without all the development-specific parts. For this reason I named the rules in the common file.

module.exports = {
    entry: {
        bundle: [
            "./Scripts/src/index.ts"
        ]
    },
    output: {
        publicPath: "/",
        path: __dirname + '/wwwroot/',
        filename: "js/[name].js"
    },
    extensions:[".ts", ".tsx", ".js", ".css", ".scss"],
    rules: {
        typescript: {
            test: /\.tsx?$/,
            use: [
                "awesome-typescript-loader"
            ]
        },
        moduleCss: {
            test: /\.scss$/,
            use: [
                "style-loader",
                {
                    loader: 'typings-for-css-modules-loader',
                    query: {
                        modules: true,
                        importLoaders: 1,
                        camelCase: true,
                        namedExport: true,
                        localIdentName: '[name]_[local]_[hash:base64:5]'
                    }
                },
                "sass-loader"]
        }
    },
    plugins: [],
    externals: {
        /* List all modules you're loading outside Webpack */
        "react": "React",
        "react-dom": "ReactDOM"
    }
}

then updated webpack.config.js to use this common base, but add the development dependencies back in, which involved pushing the hot module replacement libraries back in to the appropriate places.

const common = require('./webpack.common');

const typescript = common.rules.typescript;
typescript.use.unshift("react-hot-loader/webpack");

const entry = common.entry;
entry.bundle.push("react-hot-loader/patch");

module.exports = {
    output: common.output,
    entry: entry,
    devtool: "source-map",
    resolve: { extensions: common.extensions },
    module: {
        rules: [
            typescript,
            {
                enforce: "pre", test: /\.js$/, use: "source-map-loader"
            },
            common.rules.moduleCss
        ]
    },
    plugins: common.plugins,
    externals: common.externals
};

Production config

I created webpack.production.config.js and started configuring the extract text and manifest plugins. First I configure the extract text plugin to output to css/app.[chunkhash].css. Webpack will replace [chunkhash] with the hash of the file contents (more or less), which creates a unique file name. You can also just use [hash] which will use a build-unique hash.

Then I added that to the plugins array along with a ManifestPlugin which outputs a json file containing a map of output name to final filename.

I also overrode output.filename = "js/[name].[chunkhash].js" so the JavaScript bundle would have a proper name.

const common = require('./webpack.common');
const ExtractTextPlugin = require("extract-text-webpack-plugin");
const ManifestPlugin = require('webpack-manifest-plugin');

const extractAppStyles = new ExtractTextPlugin({
    allChunks: true,
    filename: 'css/app.[chunkhash].css'
})

const plugins = common.plugins.concat([
    extractAppStyles,
    new ManifestPlugin({
        fileName: "../manifest.json"
    })
]);

const moduleCss = common.rules.moduleCss;
moduleCss.use = extractAppStyles.extract({
    fallback: "style-loader",
    use: [{
        loader: 'css-loader',
        options: {
            importLoaders: 1,
            modules: true,
            localIdentName: 'cssm-[name]_[local]_[hash:base64:5]',
        }
    }, 'sass-loader']
});

const output = common.output;
output.filename = "js/[name].[chunkhash].js"

module.exports = {
    output: common.output,
    entry: common.entry,
    resolve: { extensions: common.extensions },
    module: {
        rules: [
            common.rules.typescript,
            applicationCss,
            moduleCss,
            common.rules.files
        ]
    },
    plugins: plugins,
    externals: common.externals
};

Now all I had to do is load manifest.json in to .NET. Fortunately that’s pretty easy in .NET core. First I created a class to hold the details

using Newtonsoft.Json;

public class WebpackOptions
{
    [JsonProperty("bundle.css")]
    public string BundleCss { get; set; }

    [JsonProperty("bundle.js")]
    public string BundleJs { get; set; }
}

and in Startup.cs I loaded the file inside Startup() and bound it to my dependency injection container (Autofac) inside ConfigureServices(). Your DI container may work differently, but if you use the standard .NET Core container you can bind a singleton to a factory method.

public WebpackOptions WebpackConfiguration { get; }
public Startup(IHostingEnvironment env)
{
    ...
    using (var f = File.Open(Path.Combine(env.ContentRootPath, "manifest.json"), FileMode.Open))
    using (var reader = new StreamReader(f))
    {
        WebpackConfiguration = JsonConvert.DeserializeObject<WebpackOptions>(reader.ReadToEnd());
    }
}

public IServiceProvider ConfigureServices(IServiceCollection services)
{
    ...
    builder.RegisterInstance(WebpackConfiguration).As<WebpackOptions>().ExternallyOwned();
}

And finally in my _Layout.cshtml I added the production styles and javascript as a conditional include

@inject WebpackOptions WebpackOptions
<!DOCTYPE html>
<html>
<head>
    <environment names="Staging,Production">
        <link rel="stylesheet" href="@WebpackOptions.BundleCss" />
    </environment>
</head>
<body>
    [Your app here]
    <environment names="Development">
        <script async src="~/js/bundle.js"></script>
    </environment>
    <environment names="Staging,Production">
        <script src="@WebpackOptions.BundleJS"></script>
    </environment>
</body>
</html>

And that’s it! A Webpack configuration for TypeScript and SASS-based CSS modules integrated with ASP.NET Core for hot module replacement in development and cache-busting filenames in production.

It should be easy to extend this with more Webpack features, I later added a second CSS output to customise Bootstrap based off bootstrap-sass.

Send any comments to me on Twitter. I’ll collate anything interesting here, or in a separate blog post.