Best way to integrate webpack builds with ASP.NET Core 3.0?

You mention VS. My solution is good for Visual Studio, but not VS Code.

I use WebPack Task Runner: https://marketplace.visualstudio.com/items?itemName=MadsKristensen.WebPackTaskRunner

This adds webpack.config.js tasks into the "Task Runner Explorer" in Visual Studio, and you can then bind those tasks to events like "Before Build" or "After Build"


In my opinion, Kram's answer should be marked as accepted as it really gives what's needed. I've spent some time setting up a .NET Core/React/Webpack project recently, and I could not get the spa.UseReactDevelopmentServer to work, but the spa.UseProxyToSpaDevelopmentServer works like a charm. Thanks Kram!

Here are my few gotchas they might help someone:

webpack.config.js (excerpt):

const path = require('path');
const webpack = require('webpack');

output: {
    path: path.resolve(__dirname, 'dist'),
    filename: 'build.[hash].js',
},

devServer: {
    contentBase: path.resolve(__dirname, 'public'),
    publicPath: '/dist',
    open: false,
    hot: true
},

plugins: [
    new webpack.HotModuleReplacementPlugin()
]
  1. DevServer sets its root by publicPath property and completely ignores the output.path property in the root. So even though your output files (for prod) will go to dist folder, under webpack server they will be served under a root by default. If you want to preserve the same url, you have to set publicPath: '/dist'. This means that your default page will be under http://localhost:8083/dist. I could not figure out a way to place assets under subfolder while keeping index in the root (other than hardcode the path for assets).

  2. You need HotModuleReplacementPlugin in order to watch mode to work and also hot: true setting in the devServer configuration (or pass it as a parameter upon start).

  3. If you (like me) copy some dev server configuration with open: true (default is false), you will finish up with two browsers opened upon application start as another one is opened by the launchSettings "launchBrowser": true. It was a bit of a surprise at the first moment.

Also, you will have to enable CORS for your project otherwise your backend calls will get blocked:

Startup.cs (excerpt):

public void ConfigureServices(IServiceCollection services)
{
    services.AddCors(options => 
    {
        options.AddPolicy(name: "developOrigins",
            builder =>
            {
                builder.WithOrigins("http://localhost:8083");
            });
    });
}

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    if (env.IsDevelopment())
        app.UseCors("developOrigins");
}

If anything else will come to my mind I will update the answer, hope this helps to someone.

Webpack 5 update

webpack-dev-server command doesn't work anymore. Use:

"build:hotdev": webpack serve --config webpack.config.development.js

You might also need to add target: 'web' to your webpack.config.js in Webpack 5 to enable hot module reload to work:

module.exports = {
    target: 'web'
}

Alternatively you might need to create a browserlist file. Hope this issue gets fixed soon.


Extension is here : AspNetCore.SpaServices.Extensions

You could find examples here : https://github.com/RimuTec/AspNetCore.SpaServices.Extensions

With core 3.1 or 5.0 React with webpack

using :spaBuilder.UseWebpackDevelopmentServer(npmScriptName: "start");


So, I was using UseWebpackDevMiddleware for HMR for a much smoother dev process - In the end I reverted to using webpack-dev-server

Steps:

1) Add package to package.json: "webpack-dev-server": "3.8.2", 2) Add webpack.development.js

const merge = require('webpack-merge');
const common = require('./webpack.config.js');
const ExtractCssPlugin = require('mini-css-extract-plugin');

const webpackDevServerPort = 8083;
const proxyTarget = "http://localhost:8492";

module.exports = merge(common(), {
    output: {
        filename: "[name].js",
        publicPath: '/dist/'
    },
    mode: 'development',
    devtool: 'inline-source-map',
    devServer: {
        compress: true,
        proxy: {
            '*': {
                target: proxyTarget
            }
        },
        port: webpackDevServerPort
    },
    plugins: [
        new ExtractCssPlugin({
            filename: "[name].css",
            chunkFilename: "[id].css"
        })
    ]
});

Note that the proxy setting here will be used to proxy through to ASP.Net core for API calls

Modify launchSettings.json to point to webpack-dev-server:

"profiles": {
    "VisualStudio: Connect to HotDev proxy": {
      "commandName": "Project",
      "launchBrowser": true,
      "launchUrl": "http://localhost:8083/",
      "environmentVariables": {
        "ASPNETCORE_ENVIRONMENT": "Development"
      },
      "applicationUrl": "http://localhost:8492/"
    }
  }

(also I had some problem with configuring the right locations in webpack, and found this useful

Also, will need to start webpack-dev-server which can be done via a npm script:

  "scripts": {
    "build:hotdev": "webpack-dev-server --config webpack.development.js --hot --inline",

And then this is bootstrapped

            app.UseSpa(spa =>
            {
                spa.Options.SourcePath = "ClientApp";

                if (env.IsDevelopment())
                {
                    spa.UseReactDevelopmentServer(npmScript: "build:hotdev");
                }
            });

(or you can install the npm task runner extension and:

  "-vs-binding": {
    "ProjectOpened": [
      "build:hotdev"
    ]
  }

Alternatively I realise you can proxy the other way using the following - this way any request to under "dist" will be pushed through to the proxy webpack-dev-server

            app.UseSpa(spa =>
            {
                spa.Options.SourcePath = "dist";

                if (env.IsDevelopment())
                {
                    // Ensure that you start webpack-dev-server - run "build:hotdev" npm script
                    // Also if you install the npm task runner extension then the webpack-dev-server script will run when the solution loads
                    spa.UseProxyToSpaDevelopmentServer("http://localhost:8083");
                }
            });

And then you don't need to proxy though from that back any more and can just serve /dist/ content - both hot and vendor-precompiled using as web.config.js like this:

module.exports = merge(common(), {
    output: {
        filename: "[name].js",
        publicPath: '/dist/',
    },
    mode: 'development',
    devtool: 'inline-source-map',
    devServer: {
        compress: true,
        port: 8083,
        contentBase: path.resolve(__dirname,"wwwroot"),
    },

    plugins: [
        new ExtractCssPlugin({
            filename: "[name].css",
            chunkFilename: "[id].css"
        })
    ]
});