diff --git a/InertiaCore/Models/InertiaOptions.cs b/InertiaCore/Models/InertiaOptions.cs index cc5de44..6f19803 100644 --- a/InertiaCore/Models/InertiaOptions.cs +++ b/InertiaCore/Models/InertiaOptions.cs @@ -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; } diff --git a/InertiaCore/ResponseFactory.cs b/InertiaCore/ResponseFactory.cs index 534b7ba..aff4f9f 100644 --- a/InertiaCore/ResponseFactory.cs +++ b/InertiaCore/ResponseFactory.cs @@ -59,7 +59,7 @@ public Response Render(string component, object? props = null) public async Task Head(dynamic model) { - if (!_options.Value.SsrEnabled) return new HtmlString(""); + if (!_options.Value.SsrEnabled || !_gateway.ShouldDispatch()) return new HtmlString(""); var context = _contextAccessor.HttpContext!; @@ -74,7 +74,7 @@ public async Task Head(dynamic model) public async Task Html(dynamic model) { - if (_options.Value.SsrEnabled) + if (_options.Value.SsrEnabled && _gateway.ShouldDispatch()) { var context = _contextAccessor.HttpContext!; diff --git a/InertiaCore/Ssr/Gateway.cs b/InertiaCore/Ssr/Gateway.cs index 1878c52..b898a86 100644 --- a/InertiaCore/Ssr/Gateway.cs +++ b/InertiaCore/Ssr/Gateway.cs @@ -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 { public Task Dispatch(object model, string url); + public bool ShouldDispatch(); } internal class Gateway : IGateway { private readonly IHttpClientFactory _httpClientFactory; + private readonly IOptions _options; + private readonly IWebHostEnvironment _environment; - public Gateway(IHttpClientFactory httpClientFactory) => _httpClientFactory = httpClientFactory; + public Gateway(IHttpClientFactory httpClientFactory, IOptions options, IWebHostEnvironment environment) => + (_httpClientFactory, _options, _environment) = (httpClientFactory, options, environment); public async Task Dispatch(dynamic model, string url) { @@ -30,4 +37,42 @@ internal class Gateway : IGateway var response = await client.PostAsync(url, content); return await response.Content.ReadFromJsonAsync(); } + + 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); + } } diff --git a/InertiaCoreTests/Setup.cs b/InertiaCoreTests/Setup.cs index 5942c2b..fc730ae 100644 --- a/InertiaCoreTests/Setup.cs +++ b/InertiaCoreTests/Setup.cs @@ -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; @@ -21,11 +22,14 @@ public void Setup() { var contextAccessor = new Mock(); var httpClientFactory = new Mock(); + var environment = new Mock(); + environment.SetupGet(x => x.ContentRootPath).Returns(Path.GetTempPath()); - var gateway = new Gateway(httpClientFactory.Object); var options = new Mock>(); 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); } diff --git a/InertiaCoreTests/UnitTestSsrDispatch.cs b/InertiaCoreTests/UnitTestSsrDispatch.cs new file mode 100644 index 0000000..66b5922 --- /dev/null +++ b/InertiaCoreTests/UnitTestSsrDispatch.cs @@ -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(); + var environment = new Mock(); + environment.SetupGet(x => x.ContentRootPath).Returns(Path.GetTempPath()); + + var options = new Mock>(); + 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(); + var environment = new Mock(); + environment.SetupGet(x => x.ContentRootPath).Returns(Path.GetTempPath()); + + var options = new Mock>(); + 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(); + var environment = new Mock(); + environment.SetupGet(x => x.ContentRootPath).Returns(tempDir); + + var options = new Mock>(); + 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(); + var environment = new Mock(); + environment.SetupGet(x => x.ContentRootPath).Returns(tempDir); + + var options = new Mock>(); + 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(); + var environment = new Mock(); + environment.SetupGet(x => x.ContentRootPath).Returns(tempDir); + + var options = new Mock>(); + 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); + } + } +} \ No newline at end of file