From 2cb3b6dd07b46e2c333d7daf9e8fdb35449c9378 Mon Sep 17 00:00:00 2001 From: Austin Drummond Date: Sat, 20 Sep 2025 20:12:47 -0400 Subject: [PATCH 1/2] [2.x] Ability to perform a SSR request without a bundle https://github.com/inertiajs/inertia-laravel/pull/751 --- InertiaCore/Models/InertiaOptions.cs | 1 + InertiaCore/ResponseFactory.cs | 4 +- InertiaCore/Ssr/Gateway.cs | 52 ++++++++- InertiaCoreTests/Setup.cs | 6 +- InertiaCoreTests/UnitTestSsrDispatch.cs | 141 ++++++++++++++++++++++++ 5 files changed, 200 insertions(+), 4 deletions(-) create mode 100644 InertiaCoreTests/UnitTestSsrDispatch.cs diff --git a/InertiaCore/Models/InertiaOptions.cs b/InertiaCore/Models/InertiaOptions.cs index cc5de44..9079104 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 SsrDispatchWithoutBundle { get; set; } = false; 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..b57f655 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,47 @@ internal class Gateway : IGateway var response = await client.PostAsync(url, content); return await response.Content.ReadFromJsonAsync(); } + + public bool ShouldDispatch() + { + return ShouldDispatchWithoutBundle() || BundleExists(); + } + + private bool ShouldDispatchWithoutBundle() + { + return _options.Value.SsrDispatchWithoutBundle; + } + + 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..f2a3204 --- /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")] + 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 { SsrDispatchWithoutBundle = false }); + + var gateway = new Gateway(httpClientFactory.Object, options.Object, environment.Object); + + Assert.That(gateway.ShouldDispatch(), Is.False); + } + + [Test] + [Description("Test SSR dispatch should dispatch when SsrDispatchWithoutBundle is enabled")] + 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 { SsrDispatchWithoutBundle = true }); + + 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 { SsrDispatchWithoutBundle = 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 should dispatch when either bundle exists or dispatch without bundle is enabled")] + 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 { SsrDispatchWithoutBundle = 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 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 { SsrDispatchWithoutBundle = 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); + } + } +} \ No newline at end of file From 8c4c92942de96ec3d1930e94488988fb785e7391 Mon Sep 17 00:00:00 2001 From: Austin Drummond Date: Sat, 20 Sep 2025 20:25:33 -0400 Subject: [PATCH 2/2] Update config for ssr bundle --- InertiaCore/Models/InertiaOptions.cs | 2 +- InertiaCore/Ssr/Gateway.cs | 7 +------ InertiaCoreTests/UnitTestSsrDispatch.cs | 16 ++++++++-------- 3 files changed, 10 insertions(+), 15 deletions(-) diff --git a/InertiaCore/Models/InertiaOptions.cs b/InertiaCore/Models/InertiaOptions.cs index 9079104..6f19803 100644 --- a/InertiaCore/Models/InertiaOptions.cs +++ b/InertiaCore/Models/InertiaOptions.cs @@ -6,6 +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 SsrDispatchWithoutBundle { get; set; } = false; + public bool SsrEnsureBundleExists { get; set; } = true; public bool EncryptHistory { get; set; } = false; } diff --git a/InertiaCore/Ssr/Gateway.cs b/InertiaCore/Ssr/Gateway.cs index b57f655..b898a86 100644 --- a/InertiaCore/Ssr/Gateway.cs +++ b/InertiaCore/Ssr/Gateway.cs @@ -40,12 +40,7 @@ public Gateway(IHttpClientFactory httpClientFactory, IOptions op public bool ShouldDispatch() { - return ShouldDispatchWithoutBundle() || BundleExists(); - } - - private bool ShouldDispatchWithoutBundle() - { - return _options.Value.SsrDispatchWithoutBundle; + return !_options.Value.SsrEnsureBundleExists || BundleExists(); } private bool BundleExists() diff --git a/InertiaCoreTests/UnitTestSsrDispatch.cs b/InertiaCoreTests/UnitTestSsrDispatch.cs index f2a3204..66b5922 100644 --- a/InertiaCoreTests/UnitTestSsrDispatch.cs +++ b/InertiaCoreTests/UnitTestSsrDispatch.cs @@ -9,7 +9,7 @@ namespace InertiaCoreTests; public partial class Tests { [Test] - [Description("Test SSR dispatch should not dispatch by default when no bundle exists")] + [Description("Test SSR dispatch should not dispatch by default when no bundle exists and bundle is required")] public void TestSsrDispatchDefaultBehaviorWithoutBundle() { var httpClientFactory = new Mock(); @@ -17,7 +17,7 @@ public void TestSsrDispatchDefaultBehaviorWithoutBundle() environment.SetupGet(x => x.ContentRootPath).Returns(Path.GetTempPath()); var options = new Mock>(); - options.SetupGet(x => x.Value).Returns(new InertiaOptions { SsrDispatchWithoutBundle = false }); + options.SetupGet(x => x.Value).Returns(new InertiaOptions { SsrEnsureBundleExists = true }); var gateway = new Gateway(httpClientFactory.Object, options.Object, environment.Object); @@ -25,7 +25,7 @@ public void TestSsrDispatchDefaultBehaviorWithoutBundle() } [Test] - [Description("Test SSR dispatch should dispatch when SsrDispatchWithoutBundle is enabled")] + [Description("Test SSR dispatch should dispatch when SsrEnsureBundleExists is disabled")] public void TestSsrDispatchWithoutBundleEnabled() { var httpClientFactory = new Mock(); @@ -33,7 +33,7 @@ public void TestSsrDispatchWithoutBundleEnabled() environment.SetupGet(x => x.ContentRootPath).Returns(Path.GetTempPath()); var options = new Mock>(); - options.SetupGet(x => x.Value).Returns(new InertiaOptions { SsrDispatchWithoutBundle = true }); + options.SetupGet(x => x.Value).Returns(new InertiaOptions { SsrEnsureBundleExists = false }); var gateway = new Gateway(httpClientFactory.Object, options.Object, environment.Object); @@ -58,7 +58,7 @@ public void TestSsrDispatchWithBundleExists() environment.SetupGet(x => x.ContentRootPath).Returns(tempDir); var options = new Mock>(); - options.SetupGet(x => x.Value).Returns(new InertiaOptions { SsrDispatchWithoutBundle = false }); + options.SetupGet(x => x.Value).Returns(new InertiaOptions { SsrEnsureBundleExists = true }); var gateway = new Gateway(httpClientFactory.Object, options.Object, environment.Object); @@ -74,7 +74,7 @@ public void TestSsrDispatchWithBundleExists() } [Test] - [Description("Test SSR dispatch should dispatch when either bundle exists or dispatch without bundle is enabled")] + [Description("Test SSR dispatch should dispatch when either bundle exists or SsrEnsureBundleExists is disabled")] public void TestSsrDispatchWithBundleAndDispatchWithoutBundleEnabled() { var tempDir = Path.GetTempPath(); @@ -91,7 +91,7 @@ public void TestSsrDispatchWithBundleAndDispatchWithoutBundleEnabled() environment.SetupGet(x => x.ContentRootPath).Returns(tempDir); var options = new Mock>(); - options.SetupGet(x => x.Value).Returns(new InertiaOptions { SsrDispatchWithoutBundle = true }); + options.SetupGet(x => x.Value).Returns(new InertiaOptions { SsrEnsureBundleExists = false }); var gateway = new Gateway(httpClientFactory.Object, options.Object, environment.Object); @@ -124,7 +124,7 @@ public void TestSsrDispatchChecksMultipleBundlePaths() environment.SetupGet(x => x.ContentRootPath).Returns(tempDir); var options = new Mock>(); - options.SetupGet(x => x.Value).Returns(new InertiaOptions { SsrDispatchWithoutBundle = false }); + options.SetupGet(x => x.Value).Returns(new InertiaOptions { SsrEnsureBundleExists = true }); var gateway = new Gateway(httpClientFactory.Object, options.Object, environment.Object);