MvcFrontendKit is a Node.js-free frontend bundling toolkit for ASP.NET Core MVC / Razor applications.
It wraps esbuild behind a simple .NET + YAML workflow:
- Dev: Serve raw JS/CSS from
wwwrootwith cache-busting query strings. - Prod: Build fingerprinted bundles (
/dist/js,/dist/css) viaesbuild, driven by a singlefrontend.config.yaml. - No Node / npm required: Uses a native
esbuildbinary under the hood. - Razor-friendly: Provides HTML helpers / tag helpers for layouts, views, partials, and components.
- Spec-driven: Behavior is fully defined in
SPEC.md.
This is for ASP.NET devs who want modern bundling without committing to the full Node toolchain.
Status: v1.0 - Production-ready.
dotnet add package MvcFrontendKitThis is the only package required for both runtime and production builds. It includes:
- Razor HTML helpers for your views
- MSBuild targets that automatically run esbuild during
dotnet publish -c Release - Platform-specific esbuild binaries (no Node.js required)
The CLI provides commands for development compilation, production builds, and diagnostics (dev, watch, build, init, check):
# Global install (available everywhere)
dotnet tool install --global MvcFrontendKit.Cli
# Or local install (per-project, tracked in .config/dotnet-tools.json)
dotnet new tool-manifest # if you don't have one yet
dotnet tool install MvcFrontendKit.CliNote: The CLI is not required for production builds. Production bundling is handled automatically by MSBuild targets during
dotnet publish -c Release. However, the CLI is useful for:
- Development: Compile TypeScript/SCSS with
dotnet frontend devordotnet frontend watch- Standalone builds: Build bundles without running MSBuild with
dotnet frontend build(useful for CDN workflows)- Diagnostics: Validate configuration and assets with
dotnet frontend check
# Creates frontend.config.yaml with sensible defaults
dotnet frontend initIf you don't have the CLI installed, you can copy the template from the SPEC.md or let the MSBuild target auto-generate a default config on first build.
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllersWithViews();
// Add MvcFrontendKit services
builder.Services.AddMvcFrontendKit();
var app = builder.Build();
// ... rest of your app configurationAdd helpers to your _Layout.cshtml or equivalent:
<head>
<meta charset="utf-8" />
<title>@ViewData["Title"] - MyApp</title>
@* Dev: import map for bare imports, Prod: bundled *@
@Html.FrontendImportMap()
@* Global + view-specific CSS *@
@Html.FrontendGlobalStyles()
@Html.FrontendViewStyles()
</head>
<body>
@RenderBody()
@* Global + view-specific JS *@
@Html.FrontendGlobalScripts()
@Html.FrontendViewScripts()
@RenderSection("Scripts", required: false)
</body>-
Development (with TypeScript/SCSS):
# Compile TS/SCSS once dotnet frontend dev # Or watch for changes (recommended) dotnet frontend watch
Then run your app with
dotnet runordotnet watch run. -
Development (plain JS/CSS only): Just run
dotnet run- no compilation needed. -
Production: Run
dotnet publish -c Release. Bundles built automatically towwwroot/distwithfrontend.manifest.json.
-
Dev vs Prod flow
- Dev:
- Raw JS/CSS from
wwwroot/jsandwwwroot/css type="module"for JS (usewindow.myFunc = myFuncto expose globals)?v={File.GetLastWriteTimeUtc(path).Tickscache-busting
- Raw JS/CSS from
- Prod:
- Bundled + minified JS/CSS into
/dist/jsand/dist/css - Fingerprinted filenames and a JSON manifest
- Helpers emit
<script>/<link>tags pointing at bundles
- Bundled + minified JS/CSS into
- Dev:
-
Modes
singlemode — one global JS + one global CSS bundleareasmode — one global bundle plus one bundle per Area (intentionally minimal in v1)viewsmode — per-view bundles driven by conventions + overrides (recommended)
-
Config-driven (YAML)
frontend.config.yamlcontrols:mode,webRoot,appBasePathglobal.js/global.cssviews.conventionsandviews.overridescomponents(named reusable JS/CSS chunks)cssUrlPolicy(relative vs root-relative URLs,@importhandling)importMap(Dev import map, Prod strategy:bundlevsexternal)cleanDistOnBuild, bundle size warning thresholds, etc.
-
Components
- Named components (e.g.
datepicker) with optional JS and/or CSS - Dependency graph with cycle detection
- Per-request deduplication — a component used multiple times renders its tags only once
- Named components (e.g.
-
Import maps for Dev
- Support for
import { ref } from 'vue'style bare imports during Development - Production strategy is explicit (
prodStrategy: bundleorexternal)
- Support for
-
CSS handling
- Global CSS bundle built via a virtual entry file with ordered
@importstatements - Default policy enforces safe, root-relative URLs like
/img/foo.png - Optional
allowRelativemode for advanced layouts @importresolution (and failure) handled viaesbuild
- Global CSS bundle built via a virtual entry file with ordered
-
Error-first behavior
- Invalid YAML → build fails with line/column info
- Missing JS/CSS declared in config → build fails
- Invalid or missing manifest in Prod → app startup fails (no silent fallback to Dev mode)
-
TypeScript & SCSS Support (zero-config)
- TypeScript (
.ts,.tsx) compiled automatically via esbuild - SCSS/Sass (
.scss,.sass) compiled automatically via bundled Dart Sass - Just use the file extensions - no configuration needed
- TypeScript (
See SPEC.md for the full formal specification.
MvcFrontendKit provides HTML helpers for rendering script and link tags in your Razor views.
Use these in your _Layout.cshtml:
@* Import map for bare module imports (Dev only) *@
@Html.FrontendImportMap()
@* Global CSS (from global.css in config) *@
@Html.FrontendGlobalStyles()
@* View-specific CSS (convention or override) *@
@Html.FrontendViewStyles()
@* Global JS (from global.js in config) *@
@Html.FrontendGlobalScripts()
@* View-specific JS (convention or override) *@
@Html.FrontendViewScripts()
@* Debug panel (renders only in Development) *@
@Html.FrontendDebugInfo()Use in views to load named components:
@* Load a component defined in frontend.config.yaml *@
@Html.FrontendComponent("datepicker")
@* Load multiple components *@
@Html.FrontendComponent("calendar")
@Html.FrontendComponent("modal")Components are deduplicated per-request - calling @Html.FrontendComponent("datepicker") multiple times only renders the tags once.
For area-specific bundles (when using areas mode):
@* In Areas/Admin/_ViewStart.cshtml or layout *@
@Html.FrontendAreaScripts("Admin")
@Html.FrontendAreaStyles("Admin")Development:
<script type="module" src="/js/site.js?v=638123456789"></script>
<link rel="stylesheet" href="/css/site.css?v=638123456789">Production:
<script src="/dist/js/global.a1b2c3d4.js"></script>
<link rel="stylesheet" href="/dist/css/global.e5f6g7h8.css">MvcFrontendKit automatically compiles TypeScript and SCSS files - no configuration required.
Place .ts or .tsx files anywhere you would normally place .js files:
wwwroot/
js/
site.ts # Global TypeScript entry
Home/
Index.ts # Per-view TypeScript
components/
calendar.tsx # Component with JSX
The tool automatically:
- Detects
.ts/.tsxextensions - Applies esbuild's native TypeScript loader
- Compiles to JavaScript during bundling
Example config:
global:
js:
- wwwroot/js/site.ts # TypeScript works directly
views:
overrides:
"Views/Home/Index":
js:
- wwwroot/js/Home/Index.tsPlace .scss or .sass files anywhere you would normally place .css files:
wwwroot/
css/
site.scss # Global SCSS entry
Home/
Index.scss # Per-view SCSS
components/
calendar.scss # Component SCSS
The tool automatically:
- Detects
.scss/.sassextensions - Compiles to CSS using bundled Dart Sass (no Node.js required)
- Passes the compiled CSS to esbuild for bundling and minification
Example config:
global:
css:
- wwwroot/css/site.scss # SCSS works directly
views:
overrides:
"Areas/Admin/Settings/Index":
css:
- wwwroot/css/custom/admin-settings.scssYou can mix JavaScript/TypeScript and CSS/SCSS freely:
global:
js:
- wwwroot/js/vendor.js # Plain JavaScript
- wwwroot/js/app.ts # TypeScript
css:
- wwwroot/css/reset.css # Plain CSS
- wwwroot/css/theme.scss # SCSSFor development, use the dev or watch command to compile TypeScript and SCSS files:
# One-time compilation
dotnet frontend dev
# Watch mode (recommended)
dotnet frontend watchThis compiles your source files to .js and .css next to the originals, which are then served by the development helpers with cache-busting.
- Development vs Production: Use
dotnet frontend devduring development for fast compilation. Production builds (dotnet publish -c Release) handle bundling, minification, and fingerprinting automatically. - Compilation only: This provides TS→JS and SCSS→CSS compilation. For editor features like IntelliSense and type checking, install appropriate tools (TypeScript language server, Sass extension, etc.)
- No tsconfig.json required: esbuild handles TypeScript compilation without a tsconfig. For strict type checking during development, you can add a tsconfig and run
tsc --noEmitseparately. - SCSS imports:
@importand@usestatements in SCSS are resolved relative to the file, and thecssRootdirectory is added to the load path.
-
Add MvcFrontendKit to your web project (NuGet package) and the CLI tool.
-
Generate a default config:
dotnet frontend init
This creates a commented
frontend.config.yamlat the project root. -
Minimal example config:
mode: single webRoot: wwwroot appBasePath: "/" global: js: - wwwroot/js/site.js css: - wwwroot/css/site.css
-
Use helpers in your layout:
<head> <meta charset="utf-8" /> <title>@ViewData["Title"] - MyApp</title> @* Global CSS (Dev: /css, Prod: /dist/css) *@ @Html.FrontendGlobalStyles() </head> <body> @RenderBody() @* Global JS (Dev: /js, Prod: /dist/js) *@ @Html.FrontendGlobalScripts() @RenderSection("Scripts", required: false) </body>
-
In Development:
- Helpers emit tags for raw files:
/css/site.css?v=.../js/site.js?v=...
- Helpers emit tags for raw files:
-
In Production:
dotnet frontend check # diagnostic dotnet publish -c Releaseesbuildruns under the hood and builds/dist/js/...and/dist/css/...- A
frontend.manifest.jsonis generated - Helpers read the manifest and switch to bundle URLs.
The CLI tool provides diagnostics, build utilities, and development compilation:
# Initialize configuration
dotnet frontend init
dotnet frontend init --force # Overwrite existing
# Compile TypeScript/SCSS for development
dotnet frontend dev # One-time compilation
dotnet frontend dev --verbose # Show detailed output
# Watch for changes and recompile
dotnet frontend watch # Continuous watch mode
dotnet frontend watch --verbose # Show detailed output
# Validate configuration and assets
dotnet frontend check # Basic check
dotnet frontend check --verbose # Detailed output
dotnet frontend check --all # Check all discoverable views
dotnet frontend check --view "Areas/Admin/Settings/Index" # Diagnose specific view
# Build bundles (standalone, useful for CDN workflows)
dotnet frontend build # Build to wwwroot/dist/
dotnet frontend build --dry-run # Preview bundles without building
dotnet frontend build --verbose # Show detailed build outputThe dev command compiles TypeScript and SCSS files to JavaScript and CSS for development (one-time).
# Compile all TypeScript/SCSS files from frontend.config.yaml
dotnet frontend dev
# Show compilation details
dotnet frontend dev --verboseHow it works:
- Reads
frontend.config.yamlto find all TypeScript (.ts,.tsx) and SCSS (.scss,.sass) files - Compiles TypeScript to JavaScript using esbuild (fast, native compilation)
- Compiles SCSS to CSS using Dart Sass (bundled, no Node.js required)
- Output files are placed next to source files (e.g.,
site.ts→site.js) - Source maps are generated for debugging
The watch command compiles and then monitors for changes:
# Start watching for changes
dotnet frontend watch
# With detailed output
dotnet frontend watch --verbose- Monitors your
jsRootandcssRootdirectories for changes - Automatically recompiles when
.ts,.tsx,.scss, or.sassfiles change - Shows compilation results in real-time
- Press
Ctrl+Cto stop watching
Example workflow:
# Terminal 1: Start your ASP.NET app
dotnet watch run
# Terminal 2: Watch and compile frontend assets
dotnet frontend watchThis gives you:
- Hot reload for C# code (via
dotnet watch) - Auto-compilation for TypeScript/SCSS (via
frontend watch) - Browser refresh to see changes
When a view's JS/CSS isn't loading, use diagnostics to understand why:
dotnet frontend check --view "Areas/Admin/Settings/Index"Output shows:
- Resolution method (explicit override vs convention)
- Matched convention pattern
- Files found/expected
- Import validation results
- What would be bundled in production
The check command automatically validates relative imports in JS files:
// These imports are validated:
import { helper } from './utils.js';
import shared from '../shared/common.js';
// Broken imports are reported:
// ✗ Broken import in index.js: ./missing-file.jsUse --skip-imports to disable import validation.
Preview what will be bundled without actually building:
dotnet frontend build --dry-runShows:
- All bundles that would be created
- Input files and sizes
- Estimated output sizes after minification
The frontend.config.yaml file controls all bundling behavior. Here's a complete reference:
# Schema version (for future migrations)
configVersion: 1
# Bundling mode: "single", "areas", or "views" (recommended)
mode: views
# Base path for URL generation (for sub-path deployments)
appBasePath: "/"
# Directory paths
webRoot: wwwroot # Web root directory
jsRoot: wwwroot/js # JavaScript source directory
cssRoot: wwwroot/css # CSS source directory
libRoot: wwwroot/lib # Third-party libraries
# Production output paths
distUrlRoot: /dist # URL prefix for bundles
distJsSubPath: js # Subdirectory for JS bundles
distCssSubPath: css # Subdirectory for CSS bundlesglobal:
js:
- wwwroot/js/site.ts # Global JS (TypeScript supported)
- wwwroot/js/vendor.js # Plain JS also works
css:
- wwwroot/css/site.scss # Global CSS (SCSS supported)
- wwwroot/css/reset.css # Plain CSS also worksviews:
# Auto-discovery by convention
jsAutoLinkByConvention: true
cssAutoLinkByConvention: true
# JS file conventions (tried in order)
conventions:
- viewPattern: "Views/{controller}/{action}"
scriptBasePattern: "wwwroot/js/{controller}/{action}"
# CSS file conventions
cssConventions:
- viewPattern: "Views/{controller}/{action}"
cssPattern: "wwwroot/css/{controller}/{action}"
# Explicit overrides (bypass conventions)
overrides:
"Views/Home/Index":
js:
- wwwroot/js/home/custom-index.ts
css:
- wwwroot/css/home/custom-index.scsscomponents:
datepicker:
js:
- wwwroot/js/components/datepicker.ts
css:
- wwwroot/css/components/datepicker.scss
depends:
- calendar # Load calendar component first
calendar:
js:
- wwwroot/js/components/calendar.ts
css:
- wwwroot/css/components/calendar.scssUse components in views with @Html.FrontendComponent("datepicker").
areas:
Admin:
js:
- wwwroot/js/Areas/Admin/admin.ts
css:
- wwwroot/css/Areas/Admin/admin.scss
isolate: false # When true, global JS/CSS is NOT emitted for this areaArea Isolation:
When isolate: true is set for an area, @Html.FrontendGlobalStyles() and @Html.FrontendGlobalScripts() will return empty for views in that area. This is useful for areas with completely different styling or JavaScript frameworks (e.g., a separate admin panel with its own design system).
areas:
Admin:
isolate: true # Global assets skipped for Admin area
js:
- wwwroot/js/Areas/Admin/admin-framework.ts
css:
- wwwroot/css/Areas/Admin/admin-design-system.scssimportMap:
enabled: true
prodStrategy: bundle # "bundle" (default) or "external"
entries:
vue: /lib/vue/vue.esm-browser.js
lodash: /lib/lodash-es/lodash.jsAllows bare imports in development:
import { ref } from 'vue';cssUrlPolicy:
# Fail build if relative URLs (../img) found in CSS
allowRelative: false
# Resolve @import statements
resolveImports: trueoutput:
cleanDistOnBuild: true # Remove dist folder before buildesbuild:
jsTarget: es2020 # JavaScript target version
jsFormat: iife # "iife" (default) or "esm"
jsSourcemap: true # Generate source maps
cssSourcemap: true # Generate CSS source mapsjsFormat options:
iife(default): Wraps bundle in(function(){...})();- works with regular<script>tagsesm: Preserves ES module syntax - requirestype="module"on script tags
cdn:
baseUrl: "https://cdn.example.com/assets" # CDN base URL for asset URLs
enableSri: false # Enable SRI hashes (future)When cdn.baseUrl is set:
- Manifest URLs are prefixed with the CDN base URL
- Example:
/dist/js/global.abc123.jsbecomeshttps://cdn.example.com/assets/dist/js/global.abc123.js
CDN Workflow:
- Run
dotnet frontend buildto generate bundles inwwwroot/dist/ - Upload the
dist/folder contents to your CDN - Set
cdn.baseUrlin config to match your CDN URL - Rebuild to update manifest with CDN URLs
- Deploy your app - HTML helpers will emit CDN URLs
MvcFrontendKit provides two debugging mechanisms to help troubleshoot asset resolution issues during development.
In the Development environment, all HTML helpers automatically emit HTML comments showing resolution details:
<!-- MvcFrontendKit:FrontendViewScripts - Development mode | View: Views/Home/Index | Resolution: Convention | 1 file(s) -->
<!-- wwwroot/js/Home/Index.js -->
<script type="module" src="/js/Home/Index.js?v=638123456789"></script>These comments show:
- Helper name: Which helper generated the output
- Mode: Development (raw files) or Production (manifest)
- View key: The resolved view key (e.g.,
Views/Home/Index) - Resolution method: Override (from config) or Convention (auto-discovered)
- File list: All files being loaded
Note: Debug comments are automatically suppressed in Production—no configuration needed.
For a visual debug overlay, add @Html.FrontendDebugInfo() to your layout:
<body>
@RenderBody()
@Html.FrontendGlobalScripts()
@Html.FrontendViewScripts()
@* Shows debug panel in Development environment only *@
@Html.FrontendDebugInfo()
</body>The debug panel displays:
- Current view key
- Manifest key
- Resolved JS/CSS files
- Whether using production manifest or development mode
Note: The helper renders nothing in Production environment, so it's safe to leave in your layout.
MvcFrontendKit automatically handles most upgrade scenarios. When you update to a new version:
-
Version marker detection: The tool writes a
.mvcfrontendkit-versionfile towwwroot/dist/. On each build, it checks if the version has changed and automatically performs a clean build if needed. -
SDK cache cleanup: The build process automatically cleans the ASP.NET SDK's static web assets compression cache (
obj/**/compressed/) to prevent stale reference errors.
In rare cases where you encounter build errors about missing fingerprinted files, run:
# Full clean
dotnet clean -c Release
rm -rf wwwroot/dist
rm -rf obj/*/compressed
# Then rebuild
dotnet publish -c ReleaseAdd the version marker to your .gitignore:
wwwroot/dist/
wwwroot/frontend.manifest.json
MvcFrontendKit/
LICENSE
README.md
SPEC.md # this repo’s internal design spec
.gitignore
src/
MvcFrontendKit/ # core library (config + manifest + helpers)
MvcFrontendKit.Cli/ # CLI: 'dotnet frontend'
tests/
MvcFrontendKit.Tests/
The behavior of this project is defined in SPEC.md.
- Please read
SPEC.mdbefore proposing changes to core behavior. - For new features, open an issue and describe:
- Your scenario
- How it fits into existing modes (
single,areas,views) - Any config changes you propose
Pull requests should:
- Keep public APIs consistent with the spec
- Include tests where it makes sense
MvcFrontendKit is built on the shoulders of these excellent open-source projects:
- esbuild by Evan Wallace (GitHub) - An extremely fast JavaScript/TypeScript bundler
- Dart Sass by Google and the Sass team (GitHub) - The reference implementation of Sass
Both tools are bundled as native binaries, requiring no Node.js installation.
See THIRD-PARTY-NOTICES.md for full license information.
This project is licensed under the MIT License - see the LICENSE.md file for details