Skip to content

Commit dbbc7a1

Browse files
authored
Merge pull request #9 from s97712/feature/assets
add assets management
2 parents ddbab6a + 40d02ab commit dbbc7a1

8 files changed

Lines changed: 299 additions & 0 deletions

File tree

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
using System.Text.Json.Serialization;
2+
3+
namespace BlazorTS.Assets;
4+
5+
/// <summary>
6+
/// Represents an entry in the asset manifest.
7+
/// </summary>
8+
public class AssetManifestEntry
9+
{
10+
/// <summary>
11+
/// Gets or sets the file path.
12+
/// </summary>
13+
[JsonPropertyName("file")]
14+
public string File { get; set; } = string.Empty;
15+
16+
/// <summary>
17+
/// Gets or sets the source file path.
18+
/// </summary>
19+
[JsonPropertyName("src")]
20+
public string? Src { get; set; }
21+
22+
/// <summary>
23+
/// Gets or sets a value indicating whether this is an entry point.
24+
/// </summary>
25+
[JsonPropertyName("isEntry")]
26+
public bool? IsEntry { get; set; }
27+
28+
/// <summary>
29+
/// Gets or sets the CSS files associated with this entry.
30+
/// </summary>
31+
[JsonPropertyName("css")]
32+
public string[]? Css { get; set; }
33+
}

BlazorTS.Assets/AssetOptions.cs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
namespace BlazorTS.Assets;
2+
3+
/// <summary>
4+
/// Asset service configuration options
5+
/// </summary>
6+
public class AssetOptions
7+
{
8+
/// <summary>
9+
/// Gets or sets the manifest file path
10+
/// </summary>
11+
public string ManifestPath { get; set; } = ".vite/manifest.json";
12+
13+
/// <summary>
14+
/// Gets or sets the development server URL
15+
/// </summary>
16+
public string DevServerUrl { get; set; } = "http://localhost:5173";
17+
}

BlazorTS.Assets/AssetService.cs

Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
using Microsoft.AspNetCore.Components;
2+
using Microsoft.AspNetCore.Components.Web;
3+
using Microsoft.AspNetCore.Hosting;
4+
using Microsoft.AspNetCore.Http;
5+
using Microsoft.Extensions.Hosting;
6+
using Microsoft.Extensions.Options;
7+
using System.Text.Json;
8+
9+
namespace BlazorTS.Assets;
10+
11+
/// <summary>
12+
/// Service for managing assets, handling asset path resolution in development and production environments.
13+
/// </summary>
14+
public class AssetService : IAssetService
15+
{
16+
private readonly IWebHostEnvironment _environment;
17+
private readonly IHttpContextAccessor _httpContextAccessor;
18+
private readonly AssetOptions _options;
19+
private readonly Lazy<Dictionary<string, AssetManifestEntry>> _manifestCache;
20+
private readonly Lazy<ImportMapDefinition?> _importMapCache;
21+
22+
/// <summary>
23+
/// Gets a value indicating whether the current environment is development.
24+
/// </summary>
25+
public bool IsDevelopment => _environment.IsDevelopment();
26+
27+
/// <summary>
28+
/// Gets the import map definition (only available in production).
29+
/// </summary>
30+
public ImportMapDefinition? ImportMap => IsDevelopment ? null : _importMapCache.Value;
31+
32+
/// <summary>
33+
/// Initializes a new instance of the <see cref="AssetService"/> class.
34+
/// </summary>
35+
/// <param name="environment">The web host environment.</param>
36+
/// <param name="httpContextAccessor">The HTTP context accessor.</param>
37+
/// <param name="options">The asset options.</param>
38+
public AssetService(
39+
IWebHostEnvironment environment,
40+
IHttpContextAccessor httpContextAccessor,
41+
IOptions<AssetOptions> options)
42+
{
43+
_environment = environment;
44+
_httpContextAccessor = httpContextAccessor;
45+
_options = options.Value;
46+
47+
// Lazily load the manifest and import map to improve startup performance.
48+
_manifestCache = new Lazy<Dictionary<string, AssetManifestEntry>>(LoadManifest);
49+
_importMapCache = new Lazy<ImportMapDefinition?>(CreateImportMap);
50+
}
51+
52+
/// <summary>
53+
/// Gets the URL path for the specified asset key.
54+
/// </summary>
55+
/// <param name="assetKey">The asset key.</param>
56+
/// <returns>The full URL path of the asset.</returns>
57+
/// <exception cref="KeyNotFoundException">Thrown when the asset is not found in the manifest in a production environment.</exception>
58+
public string this[string assetKey]
59+
{
60+
get
61+
{
62+
return IsDevelopment
63+
? GetDevelopmentAssetUrl(assetKey)
64+
: GetProductionAssetUrl(assetKey);
65+
}
66+
}
67+
68+
private string GetDevelopmentAssetUrl(string assetKey)
69+
{
70+
return $"{_options.DevServerUrl}/{assetKey}";
71+
}
72+
73+
private string GetProductionAssetUrl(string assetKey)
74+
{
75+
if (!_manifestCache.Value.TryGetValue(assetKey, out var manifestEntry))
76+
{
77+
throw new KeyNotFoundException($"Asset not found in manifest: {assetKey}");
78+
}
79+
80+
return NormalizeAssetPath(manifestEntry.File);
81+
}
82+
83+
private static string NormalizeAssetPath(string filePath)
84+
{
85+
return filePath.StartsWith('/') ? filePath : $"/{filePath}";
86+
}
87+
88+
private Dictionary<string, AssetManifestEntry> LoadManifest()
89+
{
90+
if (IsDevelopment)
91+
{
92+
return new Dictionary<string, AssetManifestEntry>();
93+
}
94+
95+
var manifestPath = Path.Combine(_environment.WebRootPath, _options.ManifestPath);
96+
97+
if (!File.Exists(manifestPath))
98+
{
99+
throw new FileNotFoundException($"Vite manifest file not found: {manifestPath}");
100+
}
101+
102+
var manifestJson = File.ReadAllText(manifestPath);
103+
var deserializeOptions = new JsonSerializerOptions
104+
{
105+
PropertyNameCaseInsensitive = true
106+
};
107+
108+
return JsonSerializer.Deserialize<Dictionary<string, AssetManifestEntry>>(manifestJson, deserializeOptions)
109+
?? new Dictionary<string, AssetManifestEntry>();
110+
}
111+
112+
private ImportMapDefinition? CreateImportMap()
113+
{
114+
if (IsDevelopment)
115+
{
116+
return null;
117+
}
118+
119+
var manifestImports = ExtractManifestImports();
120+
var endpointImports = GetEndpointImportMap();
121+
122+
if (manifestImports.Any() || endpointImports is not null)
123+
{
124+
return CombineImportMaps(manifestImports, endpointImports);
125+
}
126+
127+
return null;
128+
}
129+
130+
private Dictionary<string, string> ExtractManifestImports()
131+
{
132+
return _manifestCache.Value
133+
.Where(entry => entry.Value.IsEntry == true && !string.IsNullOrEmpty(entry.Value.Src))
134+
.ToDictionary(
135+
entry => entry.Value.Src!,
136+
entry => NormalizeAssetPath(entry.Value.File)
137+
);
138+
}
139+
140+
private ImportMapDefinition? GetEndpointImportMap()
141+
{
142+
return _httpContextAccessor.HttpContext?
143+
.GetEndpoint()?
144+
.Metadata
145+
.GetMetadata<ImportMapDefinition>();
146+
}
147+
148+
private static ImportMapDefinition CombineImportMaps(
149+
Dictionary<string, string> manifestImports,
150+
ImportMapDefinition? endpointImports)
151+
{
152+
var manifestImportMap = new ImportMapDefinition(manifestImports, null, null);
153+
var fallbackImportMap = endpointImports ?? new ImportMapDefinition(null, null, null);
154+
155+
return ImportMapDefinition.Combine(manifestImportMap, fallbackImportMap);
156+
}
157+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
3+
<PropertyGroup>
4+
<TargetFramework>net9.0</TargetFramework>
5+
<ImplicitUsings>enable</ImplicitUsings>
6+
<Nullable>enable</Nullable>
7+
<PackageReadmeFile>README.md</PackageReadmeFile>
8+
</PropertyGroup>
9+
10+
<ItemGroup>
11+
<FrameworkReference Include="Microsoft.AspNetCore.App" />
12+
</ItemGroup>
13+
14+
<ItemGroup>
15+
<PackageReference Include="Microsoft.AspNetCore.Components.Web" Version="9.0.9" />
16+
</ItemGroup>
17+
18+
<ItemGroup>
19+
<None Include="README.md" Pack="true" PackagePath="\"/>
20+
</ItemGroup>
21+
22+
</Project>

BlazorTS.Assets/IAssetService.cs

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
using Microsoft.AspNetCore.Components;
2+
3+
namespace BlazorTS.Assets;
4+
5+
/// <summary>
6+
/// Defines the contract for a service that provides access to assets.
7+
/// </summary>
8+
public interface IAssetService
9+
{
10+
/// <summary>
11+
/// Gets the URL for the specified asset key.
12+
/// </summary>
13+
/// <param name="key">The asset key.</param>
14+
/// <returns>The URL of the asset.</returns>
15+
string this[string key] { get; }
16+
17+
/// <summary>
18+
/// Gets the import map.
19+
/// </summary>
20+
ImportMapDefinition? ImportMap { get; }
21+
22+
/// <summary>
23+
/// Gets a value indicating whether the application is in the development environment.
24+
/// </summary>
25+
bool IsDevelopment { get; }
26+
}

BlazorTS.Assets/README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# BlazorTS.Assets
2+
3+
This project provides asset management services for BlazorTS applications.
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
using Microsoft.AspNetCore.Http;
2+
using Microsoft.Extensions.DependencyInjection;
3+
4+
namespace BlazorTS.Assets;
5+
6+
/// <summary>
7+
/// Extension methods for setting up asset services.
8+
/// </summary>
9+
public static class ServiceExtensions
10+
{
11+
/// <summary>
12+
/// Adds the asset integration services to the specified <see cref="IServiceCollection"/>.
13+
/// </summary>
14+
/// <param name="services">The <see cref="IServiceCollection"/> to add the services to.</param>
15+
/// <param name="configure">An <see cref="Action{AssetOptions}"/> to configure the provided <see cref="AssetOptions"/>.</param>
16+
/// <returns>The <see cref="IServiceCollection"/> so that additional calls can be chained.</returns>
17+
public static IServiceCollection AddAssetIntegration(
18+
this IServiceCollection services,
19+
Action<AssetOptions> configure)
20+
{
21+
services.AddHttpContextAccessor();
22+
services.Configure(configure);
23+
24+
services.AddScoped<IAssetService, AssetService>();
25+
return services;
26+
}
27+
}

blazorts.sln

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BlazorTS", "BlazorTS\Blazor
77
EndProject
88
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BlazorTS.SourceGenerator", "BlazorTS.SourceGenerator\BlazorTS.SourceGenerator.csproj", "{532F3D25-FD70-48C6-AE33-861504E1CC27}"
99
EndProject
10+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BlazorTS.Assets", "BlazorTS.Assets\BlazorTS.Assets.csproj", "{B959B5A1-EC75-4D21-BE36-ACB14BC69634}"
11+
EndProject
1012
Global
1113
GlobalSection(SolutionConfigurationPlatforms) = preSolution
1214
Debug|Any CPU = Debug|Any CPU
@@ -41,6 +43,18 @@ Global
4143
{532F3D25-FD70-48C6-AE33-861504E1CC27}.Release|x64.Build.0 = Release|Any CPU
4244
{532F3D25-FD70-48C6-AE33-861504E1CC27}.Release|x86.ActiveCfg = Release|Any CPU
4345
{532F3D25-FD70-48C6-AE33-861504E1CC27}.Release|x86.Build.0 = Release|Any CPU
46+
{B959B5A1-EC75-4D21-BE36-ACB14BC69634}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
47+
{B959B5A1-EC75-4D21-BE36-ACB14BC69634}.Debug|Any CPU.Build.0 = Debug|Any CPU
48+
{B959B5A1-EC75-4D21-BE36-ACB14BC69634}.Debug|x64.ActiveCfg = Debug|Any CPU
49+
{B959B5A1-EC75-4D21-BE36-ACB14BC69634}.Debug|x64.Build.0 = Debug|Any CPU
50+
{B959B5A1-EC75-4D21-BE36-ACB14BC69634}.Debug|x86.ActiveCfg = Debug|Any CPU
51+
{B959B5A1-EC75-4D21-BE36-ACB14BC69634}.Debug|x86.Build.0 = Debug|Any CPU
52+
{B959B5A1-EC75-4D21-BE36-ACB14BC69634}.Release|Any CPU.ActiveCfg = Release|Any CPU
53+
{B959B5A1-EC75-4D21-BE36-ACB14BC69634}.Release|Any CPU.Build.0 = Release|Any CPU
54+
{B959B5A1-EC75-4D21-BE36-ACB14BC69634}.Release|x64.ActiveCfg = Release|Any CPU
55+
{B959B5A1-EC75-4D21-BE36-ACB14BC69634}.Release|x64.Build.0 = Release|Any CPU
56+
{B959B5A1-EC75-4D21-BE36-ACB14BC69634}.Release|x86.ActiveCfg = Release|Any CPU
57+
{B959B5A1-EC75-4D21-BE36-ACB14BC69634}.Release|x86.Build.0 = Release|Any CPU
4458
EndGlobalSection
4559
GlobalSection(SolutionProperties) = preSolution
4660
HideSolutionNode = FALSE

0 commit comments

Comments
 (0)