Last active
June 10, 2024 23:00
-
-
Save austins/c260c9e233411610f8f32c58ddd9f8ad to your computer and use it in GitHub Desktop.
ASP.NET Core Vite Manifest to Razor TagHelper
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<link rel="stylesheet" vite-asset="main.css"/> | |
<script type="module" vite-asset="main.js"></script> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
{ | |
"Vite": { | |
"AssetsDirectoryName": "Assets", | |
"AssetPaths": [ | |
"main.css", | |
"main.js" | |
], | |
"ManifestFileName": "manifest.json" | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
{ | |
"private": true, | |
"type": "module", | |
"scripts": { | |
"dev": "vite build --watch", | |
"build": "vite build" | |
}, | |
"devDependencies": | |
"is-subdir": "^1.2.0", | |
"jsonc-parser": "^3.2.1", | |
"vite": "^5.2.12" | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { defineConfig } from "vite"; | |
import { parse as parseJSONC } from "jsonc-parser"; | |
import { readFileSync } from "node:fs"; | |
import { resolve } from "node:path"; | |
import isSubdir from "is-subdir"; | |
const appsettings = parseJSONC(readFileSync("./appsettings.json", "utf8")); | |
// Validate config. | |
if (!isSubdir(process.cwd(), resolve(appsettings.Vite.AssetsDirectoryName)) | |
|| appsettings.Vite.ManifestFileName.includes("/")) { | |
throw new Error("Invalid Vite config."); | |
} | |
export default defineConfig({ | |
build: { | |
outDir: `./wwwroot/${appsettings.Vite.AssetsDirectoryName.toLowerCase()}`, | |
emptyOutDir: true, | |
manifest: appsettings.Vite.ManifestFileName, | |
rollupOptions: { | |
input: appsettings.Vite.AssetPaths.map(asset => `./${appsettings.Vite.AssetsDirectoryName}/${asset}`), | |
output: { | |
assetFileNames: "[name]-[hash][extname]", | |
chunkFileNames: "[name]-[hash].js", | |
entryFileNames: "[name]-[hash].js" | |
} | |
} | |
} | |
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using System.Text.Json; | |
using System.Text.Json.Serialization; | |
using Microsoft.AspNetCore.Mvc.Rendering; | |
using Microsoft.AspNetCore.Mvc.ViewFeatures; | |
using Microsoft.AspNetCore.Razor.TagHelpers; | |
using Microsoft.Extensions.Caching.Memory; | |
using Microsoft.Extensions.FileProviders; | |
using Microsoft.Extensions.Options; | |
/// <summary> | |
/// Tag helper to get hashed asset file name URL from Vite manifest. | |
/// </summary> | |
[HtmlTargetElement("link", Attributes = ViteAssetAttributeName, TagStructure = TagStructure.WithoutEndTag)] | |
[HtmlTargetElement("script", Attributes = ViteAssetAttributeName)] | |
public sealed class ViteAssetTagHelper : TagHelper | |
{ | |
private const string ViteAssetAttributeName = "vite-asset"; | |
private const string CacheKey = "vite:assets"; | |
private readonly IMemoryCache _cache; | |
private readonly IFileProvider _fileProvider; | |
private readonly string _publicAssetsDirectoryName; | |
private readonly IOptions<ViteOptions> _viteOptions; | |
public ViteAssetTagHelper(IMemoryCache cache, IWebHostEnvironment environment, IOptions<ViteOptions> viteOptions) | |
{ | |
_cache = cache; | |
_fileProvider = environment.WebRootFileProvider; | |
_viteOptions = viteOptions; | |
#pragma warning disable CA1308 | |
_publicAssetsDirectoryName = _viteOptions.Value.AssetsDirectoryName.ToLowerInvariant(); | |
#pragma warning restore CA1308 | |
} | |
[HtmlAttributeName(ViteAssetAttributeName)] | |
public required string ViteAsset { get; set; } | |
[ViewContext] | |
public required ViewContext ViewContext { get; set; } | |
public override async Task ProcessAsync(TagHelperContext context, TagHelperOutput output) | |
{ | |
var assets = await _cache.GetOrCreateAsync<IDictionary<string, Asset>>( | |
CacheKey, | |
async entry => | |
{ | |
var filePath = Path.Combine(_publicAssetsDirectoryName, _viteOptions.Value.ManifestFileName); | |
var fileInfo = _fileProvider.GetFileInfo(filePath); | |
if (!fileInfo.Exists || fileInfo.IsDirectory) | |
{ | |
throw new FileNotFoundException($"Vite manifest file not found at path [{filePath}]."); | |
} | |
entry.AddExpirationToken(_fileProvider.Watch(filePath)); | |
await using var fileStream = fileInfo.CreateReadStream(); | |
var result = await JsonSerializer.DeserializeAsync<IDictionary<string, Asset>>( | |
fileStream, | |
cancellationToken: ViewContext.HttpContext.RequestAborted); | |
return result!; | |
}); | |
if (!assets!.TryGetValue($"{_viteOptions.Value.AssetsDirectoryName}/{ViteAsset}", out var asset)) | |
{ | |
throw new InvalidOperationException($"Asset [{ViteAsset}] not found."); | |
} | |
var attributeName = output.TagName == "link" ? "href" : "src"; | |
var assetPath = $"/{_publicAssetsDirectoryName}/{asset.FilePath}"; | |
output.Attributes.SetAttribute(attributeName, assetPath); | |
} | |
private sealed record Asset( | |
[property: JsonPropertyName("file")] | |
string FilePath); | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using System.ComponentModel.DataAnnotations; | |
public sealed class ViteOptions : IValidatableObject | |
{ | |
public const string SectionName = "Vite"; | |
/// <summary> | |
/// The directory for the raw assets relative to the root of the project. | |
/// </summary> | |
[Required] | |
public required string AssetsDirectoryName { get; init; } | |
/// <summary> | |
/// The paths for the raw assets relative to the <see cref="AssetsDirectoryName" />. | |
/// </summary> | |
[Required] | |
[MinLength(1)] | |
public required ISet<string> AssetPaths { get; init; } | |
/// <summary> | |
/// The Vite manifest file name. | |
/// </summary> | |
[Required] | |
public required string ManifestFileName { get; init; } | |
public IEnumerable<ValidationResult> Validate(ValidationContext validationContext) | |
{ | |
if (AssetsDirectoryName.Contains(Path.DirectorySeparatorChar, StringComparison.Ordinal) | |
|| AssetsDirectoryName.Contains(Path.AltDirectorySeparatorChar, StringComparison.Ordinal)) | |
{ | |
yield return new ValidationResult( | |
$"{nameof(AssetsDirectoryName)} must be a name of a directory in the root of the project.", | |
[nameof(AssetsDirectoryName)]); | |
} | |
if (AssetPaths.Any(x => x.Contains("..", StringComparison.Ordinal))) | |
{ | |
yield return new ValidationResult( | |
$"{nameof(AssetPaths)} must not go up to parent directories.", | |
[nameof(AssetPaths)]); | |
} | |
if (ManifestFileName.Contains(Path.DirectorySeparatorChar, StringComparison.Ordinal) | |
|| ManifestFileName.Contains(Path.AltDirectorySeparatorChar, StringComparison.Ordinal)) | |
{ | |
yield return new ValidationResult( | |
$"{nameof(ManifestFileName)} must be a file name and not be a path.", | |
[nameof(ManifestFileName)]); | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment