ASP.NET Core + Vite
Table of Contents
-
- 3.1. Install Vite
- 3.2. Install TypeScript
- 3.3. Create vite.config.ts
- 3.4. Specify the output folder
- 3.5. Specify the output filename
- 3.6. Specify the ts file folder
- 3.7. Support multiple file outputs
- 3.8. Target all index.ts files
-
- 4.1. Add npm scripts
- 4.2. Startup
- 4.3. Verify operation
- 4.4. Root settings
- 4.5. Automatically start the Vite development server
-
- 6.1. package.json
- 6.2. azure-pipelines.yml
Introduction
When you want to compile TypeScript in ASP.NET Core (Visual Studio), you have several options.
Method | Overview / Notes |
---|---|
TypeScript SDK | Included with Visual Studio. Now deprecated. 1 |
Microsoft.TypeScript.MSBuild | Official Microsoft NuGet package |
tsc (Node.js) | Compiles TypeScript using the tsc command |
Webpack + tsc | Bundling is standard. HMR can be configured. |
Vite + tsc | Uses ESM for HMR, allowing fast hot-swapping during development. |
For Visual Studio, Microsoft.TypeScript.MSBuild
is the standard choice. Visual Studio supports the compileOnSave
option in tsconfig.json
, which automatically compiles ts
files when saved. This is a simple and good option for straightforward 1:1 conversion of ts
files to js
files. However, when you want to do more, such as import
other files or split a long ts
file, it can be difficult due to various limitations (though not impossible 2).
Furthermore, since ts
files are frequently updated during development, I started wanting to use HMR (Hot Module Replacement). Given its mechanism, Vite, which hot-swaps modules using ESM (ECMAScript Modules), seemed faster than Webpack, so I decided to adopt a configuration using Vite
.
Below are the steps to use Vite in an ASP.NET Core project.
Prerequisites
- An ASP.NET Core project is already created.
- Node.js is installed.
- Visual Studio 2022 or later.
Setting up the Vite build environment
Work in the project folder.
1Install Vite
npm install vite --save-dev
2Install TypeScript
npm install typescript --save-dev
The package.json file will look like this:
{
"devDependencies": {
"typescript": "^5.9.3",
"vite": "^7.1.7"
}
}
3Create vite.config.ts
Let's try building with npx vite build
.
> npx vite build
>>
vite v7.1.7 building for production...
✓ 0 modules transformed.
✗ Build failed in 9ms
error during build:
Could not resolve entry module "index.html".
We got an error indicating index.html
is missing.
Vite defaults to index.html
directly under the project root as its entry point.
Create the configuration file vite.config.ts
to specify the entry point file. vite.config.ts
should be created directly under the project root.
// vite.config.ts
import { defineConfig } from 'vite'
export default defineConfig({
build: {
rollupOptions: {
input: {
hoge: 'index.ts',
}
}
}
})
Create an index.ts
file directly under the project root and try npx vite build
again.
// index.ts
console.log('hoge');
> npx vite build
>>
vite v7.1.7 building for production...
✓ 1 modules transformed.
dist/assets/hoge-BBL_kPP-.js 0.02 kB │ gzip: 0.04 kB
✓ built in 40ms
It completed successfully. The compiled JavaScript is output to the dist/assets
folder.
// dist\assets\hoge-BBL_kPP-.js
console.log("hoge");
The -BBL_kPP-
part of the filename is a hash value generated by Vite. If the output filename is not explicitly specified, such a hash value is appended by default.
Next, let's specify the output location.
4Specify the output folder
Since this is an ASP.NET Core app, we want to output to the wwwroot
folder.
The following settings allow output to the wwwroot/js
folder.
// vite.config.ts
import { defineConfig } from 'vite'
export default defineConfig({
build: {
outDir: 'wwwroot/js', // Output directory
emptyOutDir: false, // Do not empty the directory
rollupOptions: {
input: {
hoge: 'index.ts',
}
}
}
})
emptyOutDir
is a setting to empty the output folder before compilation. The default value is true
, so we set it to false
here, considering cases where other JavaScript files might be used together.
> npx vite build
>>
vite v7.1.7 building for production...
✓ 1 modules transformed.
wwwroot/js/assets/hoge-BBL_kPP-.js 0.02 kB │ gzip: 0.04 kB
✓ built in 35ms
It was output to the specified output folder.
Next, we will change the output filename.
5Specify the output filename
Adding a hash value to the filename is a means of cache busting, ensuring the browser loads the latest js
file when the code changes.
However, in ASP.NET Core, asp-append-version
can handle cache busting, so there's no need to include hash values in the filename. Let's use a fixed filename.
// vite.config.ts
import { defineConfig } from 'vite'
export default defineConfig({
build: {
outDir: 'wwwroot/js', // Output directory
emptyOutDir: false, // Do not empty the directory
rollupOptions: {
input: {
hoge: 'index.ts',
},
output: {
entryFileNames: `[name].js` // Output file name
}
}
}
})
> npx vite build
>>
vite v7.1.7 building for production...
✓ 1 modules transformed.
wwwroot/js/hoge.js 0.02 kB │ gzip: 0.04 kB
✓ built in 36ms
It now outputs with a fixed filename.
You can reference it from cshtml
as follows:
// index.cshtml
<script src="~/js/hoge.js" asp-append-version="true"></script>
6Specify the ts file folder
We were placing TypeScript files directly under the project root, but now we'll create a ClientApp
folder and place them there.
// vite.config.ts
import { defineConfig } from 'vite'
export default defineConfig({
build: {
outDir: 'wwwroot/js', // Output directory
emptyOutDir: false, // Do not empty the directory
rollupOptions: {
input: {
hoge: 'ClientApp/index.ts',
},
output: {
entryFileNames: `[name].js` // Output file name
}
}
}
})
7Support multiple file outputs
If you want to output js
files from multiple entry points, write it as follows.
// vite.config.ts
import { defineConfig } from 'vite'
export default defineConfig({
build: {
outDir: 'wwwroot/js', // Output directory
emptyOutDir: false, // Do not empty the directory
rollupOptions: {
input: {
hoge: 'ClientApp/index.ts',
fuga: 'ClientApp/index2.ts',
},
output: {
entryFileNames: `[name].js` // Output file name
}
}
}
})
> npx vite build
>>
vite v7.1.7 building for production...
✓ 2 modules transformed.
wwwroot/js/hoge.js 0.02 kB │ gzip: 0.04 kB
wwwroot/js/fuga.js 0.02 kB │ gzip: 0.04 kB
✓ built in 42ms
hoge
and fuga
specified in input
are applied to [name]
in the output files.
This is sufficient, but we want to avoid modifying vite.config.ts
every time we add an entry point. Let's make it a bit more generic.
8Target all index.ts files
We will use the following rules:
- All
index.ts
files within theClientApp
folder are entry points. - Output
js
files in the same hierarchical structure as the folders.
Example)
ClientApp/pages/
├─ hoge/
│ └─ index.ts
└─ fuga/
└─ piyo/
└─ index.ts
wwwroot/js/pages/
├─ hoge.js
└─ fuga/piyo.js
The goal is to align with the folder structure of Razor Pages.
vite.config.ts
will be as follows:
// vite.config.ts
import { defineConfig } from 'vite';
import { relative, dirname, resolve } from 'path';
import fs from 'fs';
function collectEntries(rootDir: string): Record<string, string> {
const entries: Record<string, string> = {};
function walk(dir: string) {
for (const item of fs.readdirSync(dir, { withFileTypes: true })) {
const fullPath = resolve(dir, item.name);
if (item.isDirectory()) {
walk(fullPath);
} else if (item.isFile() && item.name === 'index.ts') {
const outputName = relative(rootDir, dirname(fullPath)).replace(/\\/g, '/');
entries[outputName] = fullPath;
}
}
}
walk(rootDir);
return entries;
}
export default defineConfig({
build: {
outDir: 'wwwroot/js',
emptyOutDir: false,
rollupOptions: {
input: collectEntries('ClientApp'),
output: {
entryFileNames: '[name].js'
}
}
}
});
The type of rollupOptions.input
is Record<string, string>
.
- Key: Output JS filename (relative path) ➜ becomes
[name]
inoutput
- Value: Absolute path to the corresponding TypeScript file
collectEntries
recursively traverses the ClientApp
folder. When it finds an index.ts
, it sets the folder name as the js
filename. This way, if you create index.ts
files within the ClientApp
folder, js
files will be created with filenames reflecting the folder structure.
Running the Vite development server
So far, we've looked at how to use Vite as a build tool. Next, we'll examine how to use it as a development server. For ASP.NET Core + Visual Studio, IIS Express
or Kestrel
typically serve as the development web server. We will keep these as they are and use Vite as a server for js
/ ts
files.
1Add npm scripts
Add npm scripts to package.json.
{
"scripts": {
"dev": "vite"
},
"devDependencies": {
"typescript": "^5.9.3",
"vite": "^7.1.7"
}
}
2Startup
Let's run it.
npm run dev
If you see the following, the Vite development server has started successfully:
VITE v7.1.7 ready in 196 ms
➜ Local: http://localhost:5173/
➜ Network: use --host to expose
➜ press h + enter to show help
The Vite server uses port number 5173
.
3Verify operation
Let's check the operation.
Create the following TypeScript file:
// ClientApp/pages/hoge/index.ts
console.log('hoge');
The cshtml should be written as follows:
<script type="module" src="http://localhost:5173/@@vite/client"></script>
<script type="module" src="http://localhost:5173/ClientApp/pages/hoge/index.ts"></script>
Run the project and display the page. If hoge
is displayed in the browser console, it's working correctly.
Next, let's change the TypeScript code:
// ClientApp/pages/hoge/index.ts
console.log('hoge');
alert('fuga');
The alert message fuga
appeared without needing to reload the page. This is HMR (Hot Module Replacement).
4Root settings
The reference to the ts
file in the cshtml
file was written as follows:
<script type="module" src="http://localhost:5173/@@vite/client"></script>
<script type="module" src="http://localhost:5173/ClientApp/pages/hoge/index.ts"></script>
Unless otherwise specified, Vite uses relative paths based on the project's root folder.
You can specify the base folder using root
.
// vite.config.ts
・・・
export default defineConfig({
root: 'ClientApp',
build: {
outDir: '../wwwroot/js',
emptyOutDir: false,
rollupOptions: {
input: collectEntries('ClientApp'),
output: {
entryFileNames: '[name].js'
}
}
}
});
This way, you can write it a little more compactly:
<script type="module" src="http://localhost:5173/@@vite/client"></script>
<script type="module" src="http://localhost:5173/pages/hoge/index.ts"></script>
Note that @vite/client
is the client module that enables HMR. (The @@
with two @
signs is for escaping because @
is a special character in Razor.)
5Automatically start the Vite development server
We start the Vite development server with npm run dev
, but it's tedious to enter the command every time. We'll configure it to automatically start the Vite development server when the web application starts. First, terminate the running Vite development server with Ctrl + C
.
Add the following code to Program.cs:
// Program.cs
var app = builder.Build();
// Start Vite development server (development environment only)
if (app.Environment.IsDevelopment())
{
// Check if port 5173 is in use
var listeners = IPGlobalProperties.GetIPGlobalProperties().GetActiveTcpListeners();
var is5173PortInUse = listeners.Any(ep => ep.Port == 5173);
// If port 5173 is not in use, start the Vite server
if (!is5173PortInUse)
{
var viteDevServer = new ProcessStartInfo
{
FileName = @"C:\Program Files\nodejs\npm.cmd",
Arguments = "run dev",
WorkingDirectory = Directory.GetCurrentDirectory(),
UseShellExecute = true,
};
var viteProcess = Process.Start(viteDevServer);
// Kill Vite on exit
app.Lifetime.ApplicationStopping.Register(() =>
{
if (viteProcess != null && !viteProcess.HasExited)
{
viteProcess.Kill(true);
}
});
}
}
Let's run the web application.
The Vite development server has started in a command prompt window (Figure 1).
Figure 1.
Referencing built files in cshtml
In a development environment, you reference ts
files, but in a production environment, you will reference built and bundled js
files.
How should this be written in the cshtml
file?
@inject IWebHostEnvironment Env
@if (Env.IsDevelopment())
{
// Development environment: Reference ts files via Vite development server
<script type="module" src="http://localhost:5173/@@vite/client"></script>
<script type="module" src="http://localhost:5173/pages/hoge/index.ts"></script>
}
else
{
// Production environment: Reference built js files
<script type="module" src="~/js/pages/hoge.js" asp-append-version="true"></script>
}
This way, we branch using the IsDevelopment
extension method.
We don't want to write this code in every cshtml
file, so let's create a tag helper.
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.AspNetCore.Mvc.ViewFeatures;
using Microsoft.AspNetCore.Razor.TagHelpers;
using Microsoft.Extensions.Hosting;
namespace korochin.Core.Web.TagHelpers;
[HtmlTargetElement("vite-script", Attributes = "entry")]
public class ViteScriptTagHelper(IWebHostEnvironment env, IFileVersionProvider fileVersionProvider) : TagHelper
{
[ViewContext]
[HtmlAttributeNotBound]
public required ViewContext ViewContext { get; set; }
[HtmlAttributeName("entry")]
public string Entry { get; set; } = "";
public override void Process(TagHelperContext context, TagHelperOutput output)
{
output.TagName = null;
if (env.IsDevelopment())
{
// Development environment: Reference ts files via Vite development server
output.Content.SetHtmlContent($@"
<script type=""module"" src=""http://localhost:5173/@@vite/client""></script>
<script type=""module"" src=""http://localhost:5173/{Entry}/index.ts""></script>");
}
else
{
// Production environment: Reference built js files
var jsPath = $"/js/{Entry}.js";
var versionedPath = fileVersionProvider.AddFileVersionToPath(ViewContext.HttpContext.Request.PathBase, jsPath);
output.Content.SetHtmlContent($@"<script src=""{versionedPath}""></script>");
}
}
}
Add to _ViewImports.cshtml
to register the tag helper.
//_ViewImports.cshtml
@using WebApplication1
@namespace WebApplication1.Pages
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
@addTagHelper *, WebApplication1
The cshtml
file can be written as follows, making it compact.
<vite-script entry="pages/hoge" />
Integration into CI/CD
Here's how to integrate Vite's build process into your CI/CD pipeline.
This section describes how to add Vite's build process to a deployment using Azure Pipelines.
1package.json
Add an npm script to package.json
so that npm run build
executes vite build
.
{
"scripts": {
"dev": "vite",
"build": "vite build"
},
"devDependencies": {
"typescript": "^5.9.3",
"vite": "^7.1.7"
}
}
2azure-pipelines.yml
Add the following processes to azure-pipelines.yml
:
- Install Node.js
- Install dependencies
- Execute
npm run build
Since Vite's settings are based on the web application's project folder, you need to set the workingDirectory
to the web application's project folder. The following example assumes the web application's folder name is "korochin.Hoge".
package.json
trigger:
- main
pool:
vmImage: ubuntu-latest
steps:
# Install Node.js
- task: NodeTool@0
inputs:
versionSpec: '22.x'
displayName: 'Install Node.js'
# Install dependencies
- script: |
npm ci
displayName: 'Install dependencies'
workingDirectory: korochin.Hoge
# Execute npm run build
- script: |
npm run build
displayName: 'npm run build'
workingDirectory: korochin.Hoge
- task: DotNetCoreCLI@2
inputs:
command: 'publish'
publishWebProjects: true
arguments: '--configuration Release'
- task: AzureWebApp@1
inputs:
azureSubscription: 'Service Connection'
appType: 'webAppLinux'
appName: 'korochin-hoge'
package: '$(System.DefaultWorkingDirectory)/**/*.zip'
With this, ts
files will be automatically compiled when pushed to the repository.
Footnotes
-
JavaScript and TypeScript in Visual Studio
The TypeScript SDK is deprecated in Visual Studio 2022. Existing projects that rely on the SDK must be upgraded to use the NuGet package.
-
For example, Bundler & Minifier allows you to bundle multiple compiled
js
files in arbitrary units.↩