Skip to content

Commit c2b89a0

Browse files
adrumkapi2289
andauthored
Add a Vite helper (#8)
* added a vite helper * simply css tag create add support for including just css files * added css check * support setting various variables * fix is css check * reorganize file for parity * dont return values on setter * convert to singleton+facade * added tests * use file system abstraction * added getters and setters * update tests to use new testing helper for fs * use getters * change method name for clarity * remove test method * remove classic assertions * added tests for getters and setters * use asset calls * increase code coverage * mark singleton instance var as private * fix interface * implement builder pattern * fix hotfile * added tests for facade * latest version for io * expose internals for tests * whitespace * Implement ViteBuilder configuration * added description attributes to Vite tests * added documentation comments to Vite helper * remove some comment formats * added vite docs and examples * Change script tag type to be always "module" --------- Co-authored-by: kapi2289 <kacper@ziubryniewicz.pl>
1 parent b38af98 commit c2b89a0

File tree

7 files changed

+641
-4
lines changed

7 files changed

+641
-4
lines changed

InertiaCore/Extensions/Configure.cs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
using System.IO.Abstractions;
12
using System.Net;
23
using InertiaCore.Models;
34
using InertiaCore.Ssr;
@@ -17,6 +18,9 @@ public static IApplicationBuilder UseInertia(this IApplicationBuilder app)
1718
var factory = app.ApplicationServices.GetRequiredService<IResponseFactory>();
1819
Inertia.UseFactory(factory);
1920

21+
var viteBuilder = app.ApplicationServices.GetService<IViteBuilder>();
22+
if (viteBuilder != null) Vite.UseBuilder(viteBuilder);
23+
2024
app.Use(async (context, next) =>
2125
{
2226
if (context.IsInertiaRequest()
@@ -49,6 +53,15 @@ public static IServiceCollection AddInertia(this IServiceCollection services,
4953
return services;
5054
}
5155

56+
public static IServiceCollection AddViteHelper(this IServiceCollection services,
57+
Action<ViteOptions>? options = null)
58+
{
59+
services.AddSingleton<IViteBuilder, ViteBuilder>();
60+
if (options != null) services.Configure(options);
61+
62+
return services;
63+
}
64+
5265
private static async Task OnVersionChange(HttpContext context, IApplicationBuilder app)
5366
{
5467
var tempData = app.ApplicationServices.GetRequiredService<TempDataDictionaryFactory>()

InertiaCore/InertiaCore.csproj

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,15 +17,20 @@
1717
</PropertyGroup>
1818

1919
<ItemGroup>
20-
<FrameworkReference Include="Microsoft.AspNetCore.App"/>
20+
<FrameworkReference Include="Microsoft.AspNetCore.App" />
2121
</ItemGroup>
2222

2323
<ItemGroup>
24-
<PackageReference Include="TypeMerger" Version="2.1.1"/>
24+
<PackageReference Include="System.IO.Abstractions" Version="19.2.16" />
25+
<PackageReference Include="TypeMerger" Version="2.1.1" />
2526
</ItemGroup>
2627

2728
<ItemGroup>
28-
<None Include="../LICENSE" Pack="true" PackagePath=""/>
29-
<None Include="../README.md" Pack="true" PackagePath=""/>
29+
<None Include="../LICENSE" Pack="true" PackagePath="" />
30+
<None Include="../README.md" Pack="true" PackagePath="" />
31+
</ItemGroup>
32+
33+
<ItemGroup>
34+
<InternalsVisibleTo Include="DynamicProxyGenAssembly2" />
3035
</ItemGroup>
3136
</Project>

InertiaCore/Models/ViteOptions.cs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
namespace InertiaCore.Models;
2+
3+
public class ViteOptions
4+
{
5+
// The path to the "hot" file.
6+
public string HotFile { get; set; } = "hot";
7+
8+
// The path to the build directory.
9+
public string? BuildDirectory { get; set; } = "build";
10+
11+
// The name of the manifest file.
12+
public string ManifestFilename { get; set; } = "manifest.json";
13+
14+
// The path to the public directory.
15+
public string PublicDirectory { get; set; } = "wwwroot";
16+
}

InertiaCore/Utils/Vite.cs

Lines changed: 225 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,225 @@
1+
using System.Text.Encodings.Web;
2+
using System.Text.Json;
3+
using Microsoft.AspNetCore.Html;
4+
using Microsoft.AspNetCore.Mvc.Rendering;
5+
using System.Text.RegularExpressions;
6+
using System.IO.Abstractions;
7+
using InertiaCore.Models;
8+
using Microsoft.Extensions.Options;
9+
10+
namespace InertiaCore.Utils;
11+
12+
public interface IViteBuilder
13+
{
14+
HtmlString ReactRefresh();
15+
HtmlString Input(string path);
16+
}
17+
18+
internal class ViteBuilder : IViteBuilder
19+
{
20+
private IFileSystem _fileSystem;
21+
private readonly IOptions<ViteOptions> _options;
22+
23+
public ViteBuilder(IOptions<ViteOptions> options) => (_fileSystem, _options) = (new FileSystem(), options);
24+
25+
protected internal void UseFileSystem(IFileSystem fileSystem)
26+
{
27+
_fileSystem = fileSystem;
28+
}
29+
30+
/// <summary>
31+
/// Get the public directory and build path.
32+
/// </summary>
33+
private string GetPublicPathForFile(string path)
34+
{
35+
var pieces = new List<string> { _options.Value.PublicDirectory };
36+
if (!string.IsNullOrEmpty(_options.Value.BuildDirectory))
37+
{
38+
pieces.Add(_options.Value.BuildDirectory);
39+
}
40+
41+
pieces.Add(path);
42+
return string.Join("/", pieces);
43+
}
44+
45+
/// <summary>
46+
/// Generates various tags from a given input file path.
47+
/// </summary>
48+
public HtmlString Input(string path)
49+
{
50+
if (IsRunningHot())
51+
{
52+
return new HtmlString(MakeModuleTag("@vite/client").Value + MakeModuleTag(path).Value);
53+
}
54+
55+
if (!_fileSystem.File.Exists(GetPublicPathForFile(_options.Value.ManifestFilename)))
56+
{
57+
throw new Exception("Vite Manifest is missing. Run `npm run build` and try again.");
58+
}
59+
60+
var manifest = _fileSystem.File.ReadAllText(GetPublicPathForFile(_options.Value.ManifestFilename));
61+
var manifestJson = JsonSerializer.Deserialize<Dictionary<string, JsonElement>>(manifest);
62+
63+
if (manifestJson == null)
64+
{
65+
throw new Exception("Vite Manifest is invalid. Run `npm run build` and try again.");
66+
}
67+
68+
if (!manifestJson.ContainsKey(path))
69+
{
70+
throw new Exception("Asset not found in manifest: " + path);
71+
}
72+
73+
var obj = manifestJson[path];
74+
var filePath = obj.GetProperty("file");
75+
76+
if (IsCssPath(filePath.ToString()))
77+
{
78+
return MakeTag(filePath.ToString());
79+
}
80+
81+
var html = MakeTag(filePath.ToString());
82+
83+
try
84+
{
85+
var css = obj.GetProperty("css");
86+
return css.EnumerateArray().Aggregate(html,
87+
(current, item) => new HtmlString(current.Value + MakeTag(item.ToString()).Value));
88+
}
89+
catch (Exception)
90+
{
91+
// ignored
92+
}
93+
94+
return html;
95+
}
96+
97+
/// <summary>
98+
/// Generate script tag with type="module"
99+
/// </summary>
100+
private HtmlString MakeModuleTag(string path)
101+
{
102+
var builder = new TagBuilder("script");
103+
builder.Attributes.Add("type", "module");
104+
builder.Attributes.Add("src", Asset(path));
105+
106+
return new HtmlString(GetString(builder) + "\n\t");
107+
}
108+
109+
/// <summary>
110+
/// Generate an appropriate tag for the given URL in HMR mode.
111+
/// </summary>
112+
private HtmlString MakeTag(string url)
113+
{
114+
return IsCssPath(url) ? MakeStylesheetTag(url) : MakeModuleTag(url);
115+
}
116+
117+
/// <summary>
118+
/// Generate a stylesheet tag for the given URL in HMR mode.
119+
/// </summary>
120+
private HtmlString MakeStylesheetTag(string filePath)
121+
{
122+
var builder = new TagBuilder("link");
123+
builder.Attributes.Add("rel", "stylesheet");
124+
builder.Attributes.Add("href", Asset(filePath));
125+
return new HtmlString(GetString(builder).Replace("></link>", " />") + "\n\t");
126+
}
127+
128+
/// <summary>
129+
/// Determine whether the given path is a CSS file.
130+
/// </summary>
131+
private static bool IsCssPath(string path)
132+
{
133+
return Regex.IsMatch(path, @".\.(css|less|sass|scss|styl|stylus|pcss|postcss)", RegexOptions.IgnoreCase);
134+
}
135+
136+
/// <summary>
137+
/// Generate React refresh runtime script.
138+
/// </summary>
139+
public HtmlString ReactRefresh()
140+
{
141+
if (!IsRunningHot())
142+
{
143+
return new HtmlString("<!-- no hot -->");
144+
}
145+
146+
var builder = new TagBuilder("script");
147+
builder.Attributes.Add("type", "module");
148+
149+
var inner = $"import RefreshRuntime from '{Asset("@react-refresh")}';" +
150+
"RefreshRuntime.injectIntoGlobalHook(window);" +
151+
"window.$RefreshReg$ = () => { };" +
152+
"window.$RefreshSig$ = () => (type) => type;" +
153+
"window.__vite_plugin_react_preamble_installed__ = true;";
154+
155+
builder.InnerHtml.AppendHtml(inner);
156+
157+
return new HtmlString(GetString(builder));
158+
}
159+
160+
/// <summary>
161+
/// Get the URL to a given asset when running in HMR mode.
162+
/// </summary>
163+
private string HotAsset(string path)
164+
{
165+
var hotFilePath = GetPublicPathForFile(_options.Value.HotFile);
166+
var hotContents = _fileSystem.File.ReadAllText(hotFilePath);
167+
168+
return hotContents + "/" + path;
169+
}
170+
171+
/// <summary>
172+
/// Get the URL for an asset.
173+
/// </summary>
174+
private string Asset(string path)
175+
{
176+
if (IsRunningHot())
177+
{
178+
return HotAsset(path);
179+
}
180+
181+
var pieces = new List<string>();
182+
if (!string.IsNullOrEmpty(_options.Value.BuildDirectory))
183+
{
184+
pieces.Add(_options.Value.BuildDirectory);
185+
}
186+
187+
pieces.Add(path);
188+
return "/" + string.Join("/", pieces);
189+
}
190+
191+
/// <summary>
192+
/// Determine if Vite is running in HMR mode.
193+
/// </summary>
194+
private bool IsRunningHot()
195+
{
196+
return _fileSystem.File.Exists(GetPublicPathForFile(_options.Value.HotFile));
197+
}
198+
199+
/// <summary>
200+
/// Convert an IHtmlContent to a string.
201+
/// </summary>
202+
private static string GetString(IHtmlContent content)
203+
{
204+
var writer = new StringWriter();
205+
content.WriteTo(writer, HtmlEncoder.Default);
206+
return writer.ToString();
207+
}
208+
}
209+
210+
public static class Vite
211+
{
212+
private static IViteBuilder _instance = default!;
213+
214+
internal static void UseBuilder(IViteBuilder instance) => _instance = instance;
215+
216+
/// <summary>
217+
/// Generates various tags from a given input file path.
218+
/// </summary>
219+
public static HtmlString Input(string path) => _instance.Input(path);
220+
221+
/// <summary>
222+
/// Generate React refresh runtime script.
223+
/// </summary>
224+
public static HtmlString ReactRefresh() => _instance.ReactRefresh();
225+
}

InertiaCoreTests/InertiaCoreTests.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
<PackageReference Include="NUnit3TestAdapter" Version="4.2.1"/>
1616
<PackageReference Include="NUnit.Analyzers" Version="3.3.0"/>
1717
<PackageReference Include="coverlet.collector" Version="3.1.2"/>
18+
<PackageReference Include="TestableIO.System.IO.Abstractions.TestingHelpers" Version="19.2.16" />
1819
</ItemGroup>
1920

2021
<ItemGroup>

0 commit comments

Comments
 (0)