Skip to content

Commit 30bfb17

Browse files
committed
Manually inject live reload script, the default was causing issues on larger chucked Results.Content
1 parent 95a3780 commit 30bfb17

File tree

5 files changed

+131
-99
lines changed

5 files changed

+131
-99
lines changed

src/Elastic.ApiExplorer/OpenApiGenerator.cs

Lines changed: 25 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -62,15 +62,17 @@ public LandingNavigationItem CreateNavigation(string apiUrlSuffix, OpenApiDocume
6262
? anyApi.Node.GetValue<string>()
6363
: null;
6464
var tag = op.Value.Tags?.FirstOrDefault()?.Reference.Id;
65-
var classification = openApiDocument.Info.Title == "Elasticsearch Request & Response Specification"
66-
? ClassifyElasticsearchTag(tag ?? "unknown")
67-
: "unknown";
65+
var tagClassification = (extensions?.TryGetValue("x-tag-group", out var g) ?? false) && g is OpenApiAny anyTagGroup
66+
? anyTagGroup.Node.GetValue<string>()
67+
: openApiDocument.Info.Title == "Elasticsearch Request & Response Specification"
68+
? ClassifyElasticsearchTag(tag ?? "unknown")
69+
: "unknown";
6870

6971
var apiString = ns is null
7072
? api ?? op.Value.Summary ?? Guid.NewGuid().ToString("N") : $"{ns}.{api}";
7173
return new
7274
{
73-
Classification = classification,
75+
Classification = tagClassification,
7476
Api = apiString,
7577
Tag = tag,
7678
pair.Path,
@@ -158,7 +160,7 @@ group tagGroup by classificationGroup.Key
158160
var hasClassifications = classifications.Count > 1;
159161
foreach (var classification in classifications)
160162
{
161-
if (hasClassifications)
163+
if (hasClassifications && classification.Name != "common")
162164
{
163165
var classificationNavigationItem = new ClassificationNavigationItem(classification, rootNavigation, rootNavigation);
164166
var tagNavigationItems = new List<IApiGroupingNavigationItem<IApiGroupingModel, INavigationItem>>();
@@ -264,23 +266,25 @@ public async Task Generate(Cancel ctx = default)
264266
MarkdownRenderer = markdownStringRenderer
265267
};
266268
_ = await Render(prefix, navigation, navigation.Index, renderContext, navigationRenderer, ctx);
267-
await RenderNavigationItems(navigation);
268-
async Task RenderNavigationItems(INavigationItem currentNavigation)
269-
{
270-
if (currentNavigation is INodeNavigationItem<IApiModel, INavigationItem> node)
271-
{
272-
_ = await Render(prefix, node, node.Index, renderContext, navigationRenderer, ctx);
273-
foreach (var child in node.NavigationItems)
274-
await RenderNavigationItems(child);
275-
}
269+
await RenderNavigationItems(prefix, renderContext, navigationRenderer, navigation, ctx);
276270

277-
else
278-
{
279-
_ = currentNavigation is ILeafNavigationItem<IApiModel> leaf
280-
? await Render(prefix, leaf, leaf.Model, renderContext, navigationRenderer, ctx)
281-
: throw new Exception($"Unknown navigation item type {currentNavigation.GetType()}");
282-
}
283-
}
271+
}
272+
}
273+
274+
private async Task RenderNavigationItems(string prefix, ApiRenderContext renderContext, IsolatedBuildNavigationHtmlWriter navigationRenderer, INavigationItem currentNavigation, Cancel ctx)
275+
{
276+
if (currentNavigation is INodeNavigationItem<IApiModel, INavigationItem> node)
277+
{
278+
_ = await Render(prefix, node, node.Index, renderContext, navigationRenderer, ctx);
279+
foreach (var child in node.NavigationItems)
280+
await RenderNavigationItems(prefix, renderContext, navigationRenderer, child, ctx);
281+
}
282+
283+
else
284+
{
285+
_ = currentNavigation is ILeafNavigationItem<IApiModel> leaf
286+
? await Render(prefix, leaf, leaf.Model, renderContext, navigationRenderer, ctx)
287+
: throw new Exception($"Unknown navigation item type {currentNavigation.GetType()}");
284288
}
285289
}
286290

src/Elastic.ApiExplorer/Operations/OperationNavigationItem.cs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,19 @@
1212

1313
namespace Elastic.ApiExplorer.Operations;
1414

15+
public interface IApiProperty
16+
{
17+
18+
}
19+
20+
public record ApiObject
21+
{
22+
public required string Name { get; init; }
23+
public IReadOnlyCollection<IApiProperty> Properties { get; init; } = [];
24+
}
25+
26+
27+
1528
public record ApiOperation(OperationType OperationType, OpenApiOperation Operation, string Route, IOpenApiPathItem Path, string ApiName) : IApiModel
1629
{
1730
public async Task RenderAsync(FileSystemStream stream, ApiRenderContext context, Cancel ctx = default)

src/tooling/docs-builder/Http/DocumentationWebHost.cs

Lines changed: 26 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,13 @@
55
using System.IO.Abstractions;
66
using System.Net;
77
using System.Runtime.InteropServices;
8+
using System.Text;
89
using Documentation.Builder.Diagnostics.LiveMode;
910
using Elastic.Documentation.Configuration;
1011
using Elastic.Documentation.Configuration.Versions;
1112
using Elastic.Documentation.Site.FileProviders;
1213
using Elastic.Documentation.Tooling;
13-
using Elastic.Markdown.Exporters;
1414
using Elastic.Markdown.IO;
15-
using Elastic.Markdown.Myst.Renderers;
16-
using Markdig.Syntax;
1715
using Microsoft.AspNetCore.Builder;
1816
using Microsoft.AspNetCore.Hosting;
1917
using Microsoft.AspNetCore.Http;
@@ -92,60 +90,9 @@ public async Task StopAsync(Cancel ctx)
9290
private void SetUpRoutes()
9391
{
9492
_ = _webApplication
95-
.UseExceptionHandler(options =>
96-
{
97-
options.Run(async context =>
98-
{
99-
try
100-
{
101-
var exception = context.Features.Get<Microsoft.AspNetCore.Diagnostics.IExceptionHandlerFeature>();
102-
if (exception != null)
103-
{
104-
var logger = context.RequestServices.GetRequiredService<ILogger<DocumentationWebHost>>();
105-
logger.LogError(
106-
exception.Error,
107-
"Unhandled exception processing request {Path}. Error: {Error}\nStack Trace: {StackTrace}\nInner Exception: {InnerException}",
108-
context.Request.Path,
109-
exception.Error.Message,
110-
exception.Error.StackTrace,
111-
exception.Error.InnerException?.ToString() ?? "None"
112-
);
113-
logger.LogError(
114-
"Request Details - Method: {Method}, Path: {Path}, QueryString: {QueryString}",
115-
context.Request.Method,
116-
context.Request.Path,
117-
context.Request.QueryString
118-
);
119-
120-
context.Response.StatusCode = 500;
121-
context.Response.ContentType = "text/html";
122-
await context.Response.WriteAsync(@"
123-
<html>
124-
<head><title>Error</title></head>
125-
<body>
126-
<h1>Internal Server Error</h1>
127-
<p>An error occurred while processing your request.</p>
128-
<p>Please check the application logs for more details.</p>
129-
</body>
130-
</html>");
131-
}
132-
}
133-
catch (Exception handlerEx)
134-
{
135-
var logger = context.RequestServices.GetRequiredService<ILogger<DocumentationWebHost>>();
136-
logger.LogCritical(
137-
handlerEx,
138-
"Error handler failed to process exception. Handler Error: {Error}\nStack Trace: {StackTrace}",
139-
handlerEx.Message,
140-
handlerEx.StackTrace
141-
);
142-
context.Response.StatusCode = 500;
143-
context.Response.ContentType = "text/plain";
144-
await context.Response.WriteAsync("A critical error occurred.");
145-
}
146-
});
147-
})
148-
.UseLiveReload()
93+
.UseLiveReloadWithManualScriptInjection(_webApplication.Lifetime)
94+
.UseDeveloperExceptionPage(new DeveloperExceptionPageOptions())
95+
//.UseMiddleware<NoInjectLiveReloadMiddleware>()
14996
.UseStaticFiles(
15097
new StaticFileOptions
15198
{
@@ -169,37 +116,34 @@ await context.Response.WriteAsync(@"
169116

170117
private async Task<IResult> ServeApiFile(ReloadableGeneratorState holder, string slug, Cancel ctx)
171118
{
172-
#if DEBUG
173-
// only reload when actually debugging
174-
if (System.Diagnostics.Debugger.IsAttached)
175-
await holder.ReloadApiReferences(ctx);
176-
#endif
119+
var x = LiveReloadConfiguration.Current.LiveReloadScriptUrl;
120+
177121
var path = Path.Combine(holder.ApiPath.FullName, slug.Trim('/'), "index.html");
178122
var info = _writeFileSystem.FileInfo.New(path);
179123
if (info.Exists)
180124
{
181125
//TODO STREAM
182126
var contents = await _writeFileSystem.File.ReadAllTextAsync(info.FullName, ctx);
183-
return Results.Content(contents, "text/html");
127+
return LiveReloadHtml(contents, Encoding.UTF8, 200);
184128
}
185129

186130
return Results.NotFound();
187131
}
188132

189133
private static async Task<IResult> ServeDocumentationFile(ReloadableGeneratorState holder, string slug, Cancel ctx)
190134
{
135+
if (slug == ".well-known/appspecific/com.chrome.devtools.json")
136+
return Results.NotFound();
137+
191138
var generator = holder.Generator;
192139
const string navPartialSuffix = ".nav.html";
193140

194141
// Check if the original request is asking for LLM-rendered markdown
195142
var requestLlmMarkdown = slug.EndsWith(".md");
196-
var originalSlug = slug;
197143

198144
// If requesting .md output, remove the .md extension to find the source file
199145
if (requestLlmMarkdown)
200-
{
201146
slug = slug[..^3]; // Remove ".md" extension
202-
}
203147

204148
if (slug.EndsWith(navPartialSuffix))
205149
{
@@ -238,12 +182,9 @@ private static async Task<IResult> ServeDocumentationFile(ReloadableGeneratorSta
238182
var llmRendered = await generator.RenderLlmMarkdown(markdown, ctx);
239183
return Results.Content(llmRendered, "text/markdown; charset=utf-8");
240184
}
241-
else
242-
{
243-
// Regular HTML rendering
244-
var rendered = await generator.RenderLayout(markdown, ctx);
245-
return Results.Content(rendered.Html, "text/html");
246-
}
185+
// Regular HTML rendering
186+
var rendered = await generator.RenderLayout(markdown, ctx);
187+
return LiveReloadHtml(rendered.Html);
247188

248189
case ImageFile image:
249190
return Results.File(image.SourceFile.FullName, image.MimeType);
@@ -261,4 +202,17 @@ private static async Task<IResult> ServeDocumentationFile(ReloadableGeneratorSta
261202
return Results.Content(renderedNotFound.Html, "text/html", null, (int)HttpStatusCode.NotFound);
262203
}
263204
}
205+
206+
private static IResult LiveReloadHtml(string content, Encoding? encoding = null, int? statusCode = null)
207+
{
208+
if (LiveReloadConfiguration.Current.LiveReloadEnabled)
209+
{
210+
//var script = WebsocketScriptInjectionHelper.GetWebSocketClientJavaScript(context, true);
211+
//var html = $"<script>\n{script}\n</script>";
212+
var html = "\n" + $@"<script src=""{LiveReloadConfiguration.Current.LiveReloadScriptUrl}"" defer></script>";
213+
content += html;
214+
}
215+
216+
return Results.Content(content, "text/html", encoding, statusCode);
217+
}
264218
}

src/tooling/docs-builder/Http/LiveReload.cs

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,13 @@
33
// See the LICENSE file in the project root for more information
44

55
using System.Diagnostics.CodeAnalysis;
6+
using System.Reflection;
7+
using Microsoft.AspNetCore.Builder;
68
using Microsoft.AspNetCore.Hosting;
9+
using Microsoft.AspNetCore.Http;
710
using Microsoft.Extensions.Configuration;
811
using Microsoft.Extensions.DependencyInjection;
12+
using Microsoft.Extensions.Hosting;
913

1014
// ReSharper disable once CheckNamespace
1115
#pragma warning disable IDE0130
@@ -57,4 +61,61 @@ public static IServiceCollection AddAotLiveReload(this IServiceCollection servic
5761

5862
return services;
5963
}
64+
65+
public static IApplicationBuilder UseLiveReloadWithManualScriptInjection(this IApplicationBuilder builder, IHostApplicationLifetime webApplicationLifetime)
66+
{
67+
var config = LiveReloadConfiguration.Current;
68+
69+
if (config.LiveReloadEnabled)
70+
{
71+
var webSocketOptions = new WebSocketOptions()
72+
{
73+
KeepAliveInterval = TimeSpan.FromSeconds(300),
74+
};
75+
_ = builder.UseWebSockets(webSocketOptions);
76+
77+
_ = builder
78+
.Use((context, next) =>
79+
{
80+
var middleWare = new NoInjectLiveReloadMiddleware(next, webApplicationLifetime);
81+
return middleWare.InvokeAsync(context);
82+
});
83+
LiveReloadFileWatcher.StartFileWatcher();
84+
85+
// always refresh when the server restarts...
86+
_ = LiveReloadMiddleware.RefreshWebSocketRequest();
87+
}
88+
89+
return builder;
90+
}
91+
}
92+
93+
94+
/// <inheritdoc />
95+
public class NoInjectLiveReloadMiddleware(RequestDelegate next, IHostApplicationLifetime lifeTime) : LiveReloadMiddleware(next, lifeTime)
96+
{
97+
private readonly MethodInfo _handleWebSocketRequest =
98+
typeof(LiveReloadMiddleware).GetMethod("HandleWebSocketRequest", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.InvokeMethod)!;
99+
100+
private readonly RequestDelegate _next = next;
101+
102+
public new async Task InvokeAsync(HttpContext context)
103+
{
104+
var config = LiveReloadConfiguration.Current;
105+
if (!config.LiveReloadEnabled)
106+
{
107+
await _next(context);
108+
return;
109+
}
110+
111+
if (await HandleServeLiveReloadScript(context))
112+
return;
113+
114+
// See if we have a WebSocket request. True means we handled
115+
var invoked = await (Task<bool>)_handleWebSocketRequest.Invoke(this, [context])!;
116+
if (invoked)
117+
return;
118+
119+
await _next(context);
120+
}
60121
}

src/tooling/docs-builder/Http/ReloadGeneratorService.cs

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -26,12 +26,12 @@ public async Task StartAsync(Cancel cancellationToken)
2626
var watcher = new FileSystemWatcher(ReloadableGenerator.Generator.DocumentationSet.SourceDirectory.FullName)
2727
{
2828
NotifyFilter = NotifyFilters.Attributes
29-
| NotifyFilters.CreationTime
30-
| NotifyFilters.DirectoryName
31-
| NotifyFilters.FileName
32-
| NotifyFilters.LastWrite
33-
| NotifyFilters.Security
34-
| NotifyFilters.Size
29+
| NotifyFilters.CreationTime
30+
| NotifyFilters.DirectoryName
31+
| NotifyFilters.FileName
32+
| NotifyFilters.LastWrite
33+
| NotifyFilters.Security
34+
| NotifyFilters.Size
3535
};
3636

3737
watcher.Changed += OnChanged;

0 commit comments

Comments
 (0)