Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions InertiaCore/Models/InertiaOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,6 @@ public class InertiaOptions

public bool SsrEnabled { get; set; } = false;
public string SsrUrl { get; set; } = "http://127.0.0.1:13714/render";
public bool SsrEnsureBundleExists { get; set; } = true;
public bool EncryptHistory { get; set; } = false;
}
4 changes: 2 additions & 2 deletions InertiaCore/ResponseFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ public Response Render(string component, object? props = null)

public async Task<IHtmlContent> Head(dynamic model)
{
if (!_options.Value.SsrEnabled) return new HtmlString("");
if (!_options.Value.SsrEnabled || !_gateway.ShouldDispatch()) return new HtmlString("");

var context = _contextAccessor.HttpContext!;

Expand All @@ -74,7 +74,7 @@ public async Task<IHtmlContent> Head(dynamic model)

public async Task<IHtmlContent> Html(dynamic model)
{
if (_options.Value.SsrEnabled)
if (_options.Value.SsrEnabled && _gateway.ShouldDispatch())
{
var context = _contextAccessor.HttpContext!;

Expand Down
71 changes: 69 additions & 2 deletions InertiaCore/Ssr/Gateway.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,26 @@
using System.Text;
using System.Text.Json;
using System.Text.Json.Serialization;
using InertiaCore.Models;
using Microsoft.Extensions.Options;
using Microsoft.AspNetCore.Hosting;

namespace InertiaCore.Ssr;

internal interface IGateway
internal interface IGateway : IHasHealthCheck
{
public Task<SsrResponse?> Dispatch(object model, string url);
public bool ShouldDispatch();
}

internal class Gateway : IGateway
{
private readonly IHttpClientFactory _httpClientFactory;
private readonly IOptions<InertiaOptions> _options;
private readonly IWebHostEnvironment _environment;

public Gateway(IHttpClientFactory httpClientFactory) => _httpClientFactory = httpClientFactory;
public Gateway(IHttpClientFactory httpClientFactory, IOptions<InertiaOptions> options, IWebHostEnvironment environment) =>
(_httpClientFactory, _options, _environment) = (httpClientFactory, options, environment);

public async Task<SsrResponse?> Dispatch(dynamic model, string url)
{
Expand All @@ -30,4 +37,64 @@ internal class Gateway : IGateway
var response = await client.PostAsync(url, content);
return await response.Content.ReadFromJsonAsync<SsrResponse>();
}

public bool ShouldDispatch()
{
return !_options.Value.SsrEnsureBundleExists || BundleExists();
}

private bool BundleExists()
{
var commonBundlePaths = new[]
{
"~/public/js/ssr.js",
"~/public/build/ssr.js",
"~/wwwroot/js/ssr.js",
"~/wwwroot/build/ssr.js",
"~/dist/ssr.js",
"~/build/ssr.js"
};

foreach (var path in commonBundlePaths)
{
var resolvedPath = ResolvePath(path);
if (!string.IsNullOrEmpty(resolvedPath) && File.Exists(resolvedPath))
{
return true;
}
}

return false;
}

private string? ResolvePath(string path)
{
if (path.StartsWith("~/"))
{
return Path.Combine(_environment.ContentRootPath, path[2..]);
}
return Path.IsPathRooted(path) ? path : Path.Combine(_environment.ContentRootPath, path);
}

public async Task<bool> IsHealthy()
{
try
{
var healthUrl = GetUrl("/health");
var client = _httpClientFactory.CreateClient();

using var response = await client.GetAsync(healthUrl);
return response.IsSuccessStatusCode;
}
catch
{
return false;
}
}

private string GetUrl(string endpoint)
{
var baseUrl = _options.Value.SsrUrl.TrimEnd('/');
return $"{baseUrl}{endpoint}";
}
}
6 changes: 6 additions & 0 deletions InertiaCore/Ssr/IHasHealthCheck.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
namespace InertiaCore.Ssr;

internal interface IHasHealthCheck
{
public Task<bool> IsHealthy();
}
6 changes: 5 additions & 1 deletion InertiaCoreTests/Setup.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
using Microsoft.AspNetCore.Mvc.Abstractions;
using Microsoft.AspNetCore.Routing;
using Microsoft.Extensions.Options;
using Microsoft.AspNetCore.Hosting;
using Moq;

namespace InertiaCoreTests;
Expand All @@ -21,11 +22,14 @@ public void Setup()
{
var contextAccessor = new Mock<IHttpContextAccessor>();
var httpClientFactory = new Mock<IHttpClientFactory>();
var environment = new Mock<IWebHostEnvironment>();
environment.SetupGet(x => x.ContentRootPath).Returns(Path.GetTempPath());

var gateway = new Gateway(httpClientFactory.Object);
var options = new Mock<IOptions<InertiaOptions>>();
options.SetupGet(x => x.Value).Returns(new InertiaOptions());

var gateway = new Gateway(httpClientFactory.Object, options.Object, environment.Object);

_factory = new ResponseFactory(contextAccessor.Object, gateway, options.Object);
}

Expand Down
141 changes: 141 additions & 0 deletions InertiaCoreTests/UnitTestSsrDispatch.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
using InertiaCore.Models;
using InertiaCore.Ssr;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Options;
using Moq;

namespace InertiaCoreTests;

public partial class Tests
{
[Test]
[Description("Test SSR dispatch should not dispatch by default when no bundle exists and bundle is required")]
public void TestSsrDispatchDefaultBehaviorWithoutBundle()
{
var httpClientFactory = new Mock<IHttpClientFactory>();
var environment = new Mock<IWebHostEnvironment>();
environment.SetupGet(x => x.ContentRootPath).Returns(Path.GetTempPath());

var options = new Mock<IOptions<InertiaOptions>>();
options.SetupGet(x => x.Value).Returns(new InertiaOptions { SsrEnsureBundleExists = true });

var gateway = new Gateway(httpClientFactory.Object, options.Object, environment.Object);

Assert.That(gateway.ShouldDispatch(), Is.False);
}

[Test]
[Description("Test SSR dispatch should dispatch when SsrEnsureBundleExists is disabled")]
public void TestSsrDispatchWithoutBundleEnabled()
{
var httpClientFactory = new Mock<IHttpClientFactory>();
var environment = new Mock<IWebHostEnvironment>();
environment.SetupGet(x => x.ContentRootPath).Returns(Path.GetTempPath());

var options = new Mock<IOptions<InertiaOptions>>();
options.SetupGet(x => x.Value).Returns(new InertiaOptions { SsrEnsureBundleExists = false });

var gateway = new Gateway(httpClientFactory.Object, options.Object, environment.Object);

Assert.That(gateway.ShouldDispatch(), Is.True);
}

[Test]
[Description("Test SSR dispatch should dispatch when bundle exists")]
public void TestSsrDispatchWithBundleExists()
{
var tempDir = Path.GetTempPath();
var bundleDir = Path.Combine(tempDir, "wwwroot", "js");
Directory.CreateDirectory(bundleDir);

var bundlePath = Path.Combine(bundleDir, "ssr.js");
File.WriteAllText(bundlePath, "// SSR bundle");

try
{
var httpClientFactory = new Mock<IHttpClientFactory>();
var environment = new Mock<IWebHostEnvironment>();
environment.SetupGet(x => x.ContentRootPath).Returns(tempDir);

var options = new Mock<IOptions<InertiaOptions>>();
options.SetupGet(x => x.Value).Returns(new InertiaOptions { SsrEnsureBundleExists = true });

var gateway = new Gateway(httpClientFactory.Object, options.Object, environment.Object);

Assert.That(gateway.ShouldDispatch(), Is.True);
}
finally
{
if (File.Exists(bundlePath))
File.Delete(bundlePath);
if (Directory.Exists(bundleDir))
Directory.Delete(bundleDir, true);
}
}

[Test]
[Description("Test SSR dispatch should dispatch when either bundle exists or SsrEnsureBundleExists is disabled")]
public void TestSsrDispatchWithBundleAndDispatchWithoutBundleEnabled()
{
var tempDir = Path.GetTempPath();
var bundleDir = Path.Combine(tempDir, "build");
Directory.CreateDirectory(bundleDir);

var bundlePath = Path.Combine(bundleDir, "ssr.js");
File.WriteAllText(bundlePath, "// SSR bundle");

try
{
var httpClientFactory = new Mock<IHttpClientFactory>();
var environment = new Mock<IWebHostEnvironment>();
environment.SetupGet(x => x.ContentRootPath).Returns(tempDir);

var options = new Mock<IOptions<InertiaOptions>>();
options.SetupGet(x => x.Value).Returns(new InertiaOptions { SsrEnsureBundleExists = false });

var gateway = new Gateway(httpClientFactory.Object, options.Object, environment.Object);

Assert.That(gateway.ShouldDispatch(), Is.True);
}
finally
{
if (File.Exists(bundlePath))
File.Delete(bundlePath);
if (Directory.Exists(bundleDir))
Directory.Delete(bundleDir, true);
}
}

[Test]
[Description("Test SSR dispatch checks multiple common bundle paths")]
public void TestSsrDispatchChecksMultipleBundlePaths()
{
var tempDir = Path.GetTempPath();
var bundleDir = Path.Combine(tempDir, "dist");
Directory.CreateDirectory(bundleDir);

var bundlePath = Path.Combine(bundleDir, "ssr.js");
File.WriteAllText(bundlePath, "// SSR bundle in dist");

try
{
var httpClientFactory = new Mock<IHttpClientFactory>();
var environment = new Mock<IWebHostEnvironment>();
environment.SetupGet(x => x.ContentRootPath).Returns(tempDir);

var options = new Mock<IOptions<InertiaOptions>>();
options.SetupGet(x => x.Value).Returns(new InertiaOptions { SsrEnsureBundleExists = true });

var gateway = new Gateway(httpClientFactory.Object, options.Object, environment.Object);

Assert.That(gateway.ShouldDispatch(), Is.True);
}
finally
{
if (File.Exists(bundlePath))
File.Delete(bundlePath);
if (Directory.Exists(bundleDir))
Directory.Delete(bundleDir, true);
}
}
}
Loading