Last update: 2024-01-18 for Vite 5 compatibility.
In this article we’re going to explore how to integrate Vite 5 for building (and bundling) JavaScript/CSS files in an ASP.NET Core application.
The following is what we’re going to achieve in brief. If the setup you have in mind is different, you can probably still take inspiration from this.
- We’re creating a Multi-Page Application (MPA), not an SPA. This means we’ll use Razor templates for HTML generation and Vite for JavaScript and CSS. This setup was tested with ASP.NET Core MVC (8.0) but it should work with Razor Pages too.
- We will be using the
Vite.AspNetCore
library, which does a lot of things for us. Thanks to the author for making our lives simpler. - The solution will work differently depending on whether we’re in a production environment:
- In the development environment, we’ll be proxying the requests for static files to the Vite dev server, which will be running in background in “watch” mode
- In production, the assets will be built at build time and served from the web root, without the Vite CLI playing any role after the build
- We will make sure to not lose ASP.NET Core features like referencing images and other assets from
wwwroot
(with IDE auto-complete) or assets versioning (asp-append-version="true"
). You’ll still be able to use the Vitepublic
directory if you want, but I think usingwwwroot
is a better experience during development. - We will use Tailwind CSS to demonstrate how to use CSS in this setup, but you’re free to use anything else that is compatible with Vite.
- For the sake of the example we won’t be using TypeScript.
File structure
The following is the file structure we’ll get to at the end of the article.
As a starting point we’re using an empty ASP.NET Core MVC project (of which you will recognize folders like Views
and Controllers
).
ViteTest/
├─ Properties/...
├─ Controllers/...
├─ Views/...
├─ node_modules/...
├─ wwwroot/
├─ Assets/
│ ├─ main.js
│ ├─ main.css
├─ dist/
│ ├─ main.js
│ ├─ main.css
│ ├─ manifest.json
├─ ViteTest.csproj
├─ package.json
├─ package-lock.json
├─ vite.config.js
├─ postcss.config.js
├─ tailwind.config.js
ViteTest.sln
Here are some important things to notice:
- All the configuration files, like
package.json
andvite.config.js
, will be placed in the root of the ASP.NET Core project (and not of the solution). - The source JavaScript/CSS files are going to be in the
Assets
directory - In production, the bundled output will be generated at build time and placed in the
dist
directory, together with the.vite/manifest.json
that tells how to map each entry point to its output bundle. You don’t need to create thedist
directory manually.- Note that we’re not including the file hash in the output file names, since we’ll use the fingerprinting feature provided by ASP.NET Core (which adds
?v=
as a query string parameter).
- Note that we’re not including the file hash in the output file names, since we’ll use the fingerprinting feature provided by ASP.NET Core (which adds
- The classic web root
wwwroot
is still there and it’s the place where you’re going to put other static assets (images, icons, etc.) that are not processed by Vite.- Note that both
dist
andwwwroot
are going to be web roots in our implementation.
- Note that both
postcss.config.js
andtailwind.config.js
are Tailwind CSS-related configuration files.
Vite configuration
Let’s start from the Vite configuration. This is the content of the vite.config.js
file:
export default {
appType: 'custom',
root: 'Assets',
build: {
manifest: true,
outDir: '../dist',
emptyOutDir: true,
assetsDir: '',
rollupOptions: {
input: {
main: 'Assets/main.js',
},
output: {
entryFileNames: '[name].js',
chunkFileNames: '[name].js',
assetFileNames: '[name].[ext]'
}
},
},
server: {
port: 5173,
strictPort: true,
hmr: {
clientPort: 5173
}
}
}
Let’s go through it:
root
defines the root directory for the Vite application. In our case it’sAssets
, where the source JS/CSS files are located.manifest
enables the creation of themanifest.json
file, which contains the mapping between the inputs and the outputs. This will be used later in the .NET application to discover the output file names. In our case the setup is simple enough that input file names correspond to output file names, but in general this is the correct way of doing it. You can learn more about this in Backend integration in the Vite docs.outDir
is the directory where Vite will put the bundled files. In our case it’s../dist
, relative to theAssets
directory. Since thedist
directory is outside the Vite root, we need to explicitly allow Vite to clean the directory before build, with theemptyOutDir
option.assetsDir
is set to an empty string so that output files are placed directly indist
and not indist/assets
(the default).- in
rollupOptions.input
we specify the entry points of the application. Here we have a singlemain
entry point corresponding to theAssets/main.js
file. Theoutput
overrides the output file names to remove the hashes. If you want hashes, just remove theoutput
key. - the
server
options enforce the use of port5173
and makes sure that the WebSockets client for Hot Module Replacement (HMR) connects directly to the development server (instead of going through the proxy, as we will see).
You’ve probably noticed that there’s no mention of CSS. That’s because in Vite we import CSS files through JavaScript. Here’s the content of the main.js
file:
import './main.css';
For the sake of the example, the main.css
contains:
body {
font-size: 16px;
}
Note that this is not CSS-in-JS: the processed CSS will be output as a “normal” CSS file.
At this point you can create the package.json
and install Vite 5:
# don't forget to cd to the project directory before running these commands
npm init -y
npm install -D vite
Modify the package.json
and add the dev
and build
npm scripts:
{
"name": "vitetest",
"scripts": {
"dev": "vite",
"build": "vite build"
},
"devDependencies": {
"vite": "^5.0.11"
}
}
You can test that the build is working correctly (without ASP.NET Core being involved, for now) by running npm run build
.
The output should tell you which output files were generated:
❯ npm run build
> vite5@1.0.0 build
> vite build
vite v5.0.11 building for production...
✓ 2 modules transformed.
../dist/.vite/manifest.json 0.12 kB │ gzip: 0.09 kB
../dist/main.css 0.02 kB │ gzip: 0.04 kB
../dist/main.js 0.00 kB │ gzip: 0.02 kB
✓ built in 22ms
The manifest.json
file contained in the dist/.vite
directory will look like this:
{
"main.js": {
"css": [
"main.css"
],
"file": "main.js",
"isEntry": true,
"src": "main.js"
}
}
You can also check that npm run dev
starts correctly without errors.
Test your email setup for free on DMARCwise, then start monitoring SPF, DKIM and DMARC with a powerful dashboard, weekly email digests and more to ensure your emails land in the inbox.
Integrating Vite with ASP.NET Core
The next step is to integrate the Vite build with the ASP.NET Core application.
First, install the Vite.AspNetCore
package with Nuget.
We now have to prepare for two different situations: development and production. Let’s start by making things work in the development environment.
Development
In the services section of Program.cs
, add Vite:
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllersWithViews();
builder.Services.AddViteServices(); // <--
Then add the middleware for the development server:
if (app.Environment.IsDevelopment())
{
app.UseViteDevelopmentServer(true);
}
According to this issue it’s best to add the middleware as the last middleware, but in my experience it seems to work just fine even if you put it somewhere above.
Now, modify your _ViewImports.cshtml
and enable the Vite tag helpers:
@addTagHelper *, Vite.AspNetCore
Next, modify _Layout.cshtml
and add the following lines:
<link rel="stylesheet" vite-href="~/main.js" asp-append-version="true" />
<script type="module" vite-src="~/main.js" asp-append-version="true"></script>
When rendering the Razor templates these lines will be replaced with different stuff depending on whether we’re in development or production. The README of the project explains how this works in greater detail.
At this point, if you run the Vite development server:
npm run dev
and start the ASP.NET Core application, things should already work.
If you look at the generated HTML the two lines above should have been replaced by something like:
<script type="module" src="/@vite/client"></script>
<script type="module" src="/main.js"></script>
Which is the standard way of integrating Vite with custom backends in development.
If you want the development server to be run automatically when you start the application, Vite.AspNetCore
has an option for that:
builder.Services.AddViteServices(options =>
{
options.Server.AutoRun = true;
});
One further thing you can verify is that we’re still able to refer static files from the wwwroot
directory. For example, this line of code would work correctly and we’d still have the IDE autocomplete available:
<img src="~/dotnet.svg" asp-append-version="true" width="50" height="50" />
The generated HTML would be:
<img src="/dotnet.svg?v=ckU8b02NWzo9KQihdtOs-DgVSfR6kALDD7yUZ2NU584" alt="dotnet" width="50" height="50" />
Note that the development server doesn’t use the dist
folder.
Production
For production, what you typically want is to generate the output files at build time and then have them served statically.
Run npm run build
manually so that the dist
directory is populated with JS/CSS bundles. We’ll see how to automate this step later.
To make ASP.NET Core read static files from the dist
directory, we need to change the web root path. At the same time, we’d like to retain the ability of using the “old” wwwroot
directory to embed additional static files from there.
To achieve this, we need to replace the default web root file provider with a composite file provider which reads static files from both dist
and wwwroot
. We also change the default web root path to dist
since Vite.AspNetCore
uses that to find the files.
Here’s how you can do it:
if (!app.Environment.IsDevelopment())
{
var webRootProvider = new PhysicalFileProvider(Path.Combine(builder.Environment.ContentRootPath, "wwwroot"));
var distProvider = new PhysicalFileProvider(Path.Combine(builder.Environment.ContentRootPath, "dist"));
var compositeProvider = new CompositeFileProvider(webRootProvider, distProvider);
app.Environment.WebRootFileProvider = compositeProvider;
app.Environment.WebRootPath = distProvider.Root;
}
app.UseStaticFiles();
If you run the application with the production environment you’ll notice that the <head>
now contains:
<link rel="stylesheet" href="/main.css?v=1i5SKik2NbXlYrQ_wIJhSUNplJr5ZIvLEm7y0bCmFeg" />
<script type="module" src="/main.js?v=-URFENx0A-QQSd6xM_aJKqamPAVZGytZ5O5bI017vZk"></script>
Which are the CSS and JavaScript bundles, respectively. The Vite library uses the manifest.json
file to map the source files to the output files (in this case they have the same names, but in more complex setups they probably won’t).
Note that the manifest.json
will be served publicly along with the other output files. If that’s a problem for you, you can implement a middleware to stop those requests, or simply change the manifest file name to something less discoverable.
Finally, you can automate the execution of the npm run build
command by adding a pre-build task to your csproj
file. A basic example follows but you can think of more complex configurations.
<Target Name="ViteBuild" BeforeTargets="BeforeBuild" Condition=" '$(Configuration)' == 'Release' ">
<Exec Command="npm run build" />
</Target>
To make sure that both wwwroot
and dist
are copied to the publish directory when publishing the project, add the following configuration to your csproj
file:
<Project Sdk="Microsoft.NET.Sdk.Web">
<!-- ...omitted... -->
<!-- Copy `wwwroot` and `dist` -->
<ItemGroup>
<None Include="wwwroot/**" CopyToPublishDirectory="PreserveNewest" />
<None Include="dist/**" CopyToPublishDirectory="PreserveNewest" />
</ItemGroup>
<!-- Don't copy npm files -->
<ItemGroup>
<Content Update="package-lock.json" CopyToPublishDirectory="Never" />
<Content Update="package.json" CopyToPublishDirectory="Never" />
</ItemGroup>
</Project>
Also, if you use git make sure to add the dist
and node_modules
folders to your .gitignore
.
Adding Tailwind CSS
Just to demonstrate that this setup works with typical frontend development workflows, we can add Tailwind CSS to the mix.
Follow the official installation instructions for Vite and make sure to change tailwind.config.js
so that Tailwind knows about the Razor views:
/** @type {import('tailwindcss').Config} */
module.exports = {
content: [
'./Views/**/*.cshtml',
],
theme: {
extend: {},
},
plugins: [],
}
You can then easily test that Tailwind CSS works correctly in both development and production.
The development server will recreate the pruned/trimmed CSS file based on your Razor views as soon as you make changes to them. In this way, you don’t need to restart the application nor rebuild anything manually.
Note that for this experience to work you probably need to enable Razor runtime compilation during development.
Conclusion
In this article we explored how to integrate Vite with ASP.NET Core in a multi-page application (MPA). This is only one of the possible ways of using Vite in a web application, but I hope it was useful in some way.
If you have questions or feedback, feel free to leave a comment below.