HtmlWebpackPlugin + ASP.NET Core 3

The idea

My goal with this experiment was to set up a Razor page that was generated from HtmlWebpackPlugin in ASP.NET Core 3.

In other words, I want webpack to generate my controller view (.cshtml file) because it has a better understanding of what the client code should look like. At the same time, I wanted the full power of ASP.NET Core in my Razor files.

I spent far too many hours trying to get this to work to not share my solution with the world.

The full code for this experiment is at https://github.com/alexdresko/htmlwebpackplugin-aspnetcore3

webpack config

The webpack.config.js file is simple enough. Note the template, inject, and filename options for HtmlWebpackPlugin. template is relative to webpack.config.js, and filename is relative to the output folder.

const path = require('path');
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
const HtmlWebpackPlugin = require('html-webpack-plugin');

module.exports = {
    entry: { index: './src/index.js' },
    mode: 'development',
    output: {
        filename: '[name].[hash].bundle.js',
        path: path.resolve(__dirname, 'wwwroot/dist'),
        publicPath: '/dist'
    },
    plugins: [
        new CleanWebpackPlugin(),
        new HtmlWebpackPlugin({
            template: './Views/Home/Index.ejs',
            filename: '../../Views/Home/Index.cshtml',
            inject: false,
            minify: false,
        })
    ]
};

The html-webpack-plugin template

The index.ejs file is pretty simple. Notice how I’m using both Razor functionality (setting the layout at the top of the page), and ejs functionality (definitely see the plugin documentation for more on that)

@using System.IO

@{
Layout = "";
}
<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <title><%= htmlWebpackPlugin.options.title || 'Webpack App'%></title>

    <% for (var css in htmlWebpackPlugin.files.css) { %>
    <link href="<%= htmlWebpackPlugin.files.css[css] %>" rel="stylesheet">
    <% } %>
</head>

<body>
    <h1>TADA!</h1>
	<p>If you didn't see a popup, it didn't work</p>
	<h2>Expected</h2>
	<ol>
	<% for (var chunk in htmlWebpackPlugin.files.chunks) { %>
	    <li><%= htmlWebpackPlugin.files.chunks[chunk].entry %></li>
    <% } %>
    </ol>

	<h2>Actual</h2>
	<ol>
	    @{
	        foreach (var file in Directory.GetFiles("./wwwroot/dist"))
	        {
	            <li>@file</li>
	        }
	    }
    </ol>

	<% for (var chunk in htmlWebpackPlugin.files.chunks) { %>
    <script src="<%= htmlWebpackPlugin.files.chunks[chunk].entry %>"></script>
    <% } %>

    <% if (htmlWebpackPlugin.options.devServer) { %>
    <script src="<%= htmlWebpackPlugin.options.devServer%>/webpack-dev-server.js"></script>
    <% } %>
</body>

</html>

On the server side

This page has some very important information. Namely:

Runtime compilation is enabled using the Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation package. To enable runtime compilation, apps must:

Install the Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation NuGet package.

Update the project’s Startup.ConfigureServices method to include a call to AddRazorRuntimeCompilation

With that information in hand, I added AddRazorRuntimeCompilation()to startup.cs:

services.AddRazorPages()
                .AddRazorRuntimeCompilation();

Justification

To prove why all of this is good, consider the typical approach for including webpack files into a Razor page:

<script type="text/javascript" asp-src-include="~/dist/app*.chunk.js"
                                  asp-src-exclude="~/dist/app*.async*.chunk.js"></script>

Then remember that webpack is going to put new files into the dist folder every time you build your application. By default, webpack will not clean your dist folder, and we’ve configured webpack to output our files as “[name].[hash].bundle.js”. If you’ve built your application 100 times, the <script> tag above is going to insert your javascript files 100 times!

Granted, you could just install and configure clean-webpack-plugin to clean your dist folder as part of the build, but that’s not the point. The point again is that html-webpack-plugin is smarter than ASP.NET about how the client code should look. There’s a rich ecosystem of plugins and options available for html-webpack-plugin, and many front end frameworks go to great lengths to ensure their compatibility.

It works!

Just for fun, the demo in the repo writes to the page the file(s) that webpack created, as well as the files that are in the dist folder.

Caveats

The only negative side effect I can think of with this approach is that it might cause an extra Razor compilation — Basically, when the page first loads, I think it gets compiled first. But before the page fully loads, the ASP.NET UseWebpackDevMiddleware... code kicks in and runs webpack. Our webpack process, however, rewrites the very Razor file that we were trying to load in the first place. My hunch is that this causes the Razor file to be recompiled before being displayed. I’m not sure if any of that is correct, but it’s worth a consideration. If it’s true, the performance hit is minimal.

Am I crazy?

I hope that’s helpful to someone. Please let me know what you think!

Published by alexdresko

To learn more about me, check the "About Me" page on this site.

Scroll Up