From af47b774dea3a6db6619930dffb9090bb2425535 Mon Sep 17 00:00:00 2001 From: Kevin Schneider Date: Wed, 13 Jan 2021 20:34:56 +0100 Subject: [PATCH] Add hot reload for the watch command (#627) (#629) * Add server side hot reload message cycling for the watch command * Add substitution-based injection of hot reload script into templates * add note for watch script to styling docs * Incorporate review-requested changes Co-authored-by: Don Syme --- docs/_template.html | 2 +- docs/content.fsx | 2 +- docs/styling.md | 6 ++ docs/templates/leftside/_template.html | 2 +- .../BuildCommand.fs | 90 ++++++++++++++----- src/FSharp.Formatting.Common/Templating.fs | 3 + .../FSharp.Literate.Tests/files/template.html | 5 +- 7 files changed, 84 insertions(+), 26 deletions(-) diff --git a/docs/_template.html b/docs/_template.html index 5a47fccd1..b2dc7d006 100644 --- a/docs/_template.html +++ b/docs/_template.html @@ -25,7 +25,7 @@ - + {{fsdocs-watch-script}} diff --git a/docs/content.fsx b/docs/content.fsx index 724b3a488..a6b85ed1d 100644 --- a/docs/content.fsx +++ b/docs/content.fsx @@ -81,7 +81,7 @@ See [Styling](styling.html) for information about template parameters and stylin | `fsdocs-page-title` | First h1 heading in literate file. Generated for API docs | | `fsdocs-source` | Original literate script or markdown source | | `fsdocs-tooltips` | Generated hidden div elements for tooltips | - +| `fsdocs-watch-script` | The websocket script used in watch mode to trigger hot reload | The following substitutions are extracted from your project files and may or may not be used by the default template: diff --git a/docs/styling.md b/docs/styling.md index 8dd887377..b4ab82ac8 100644 --- a/docs/styling.md +++ b/docs/styling.md @@ -104,6 +104,12 @@ If you write a new theme by CSS styling please contribute it back to FSharp.Form You can do advanced styling by creating a new template. Add a file `docs/_template.html`, likely starting with the existing default template. +To enable hot reload during development with `fsdocs watch` in a custom `_template.html` file, make sure to add the following line to your `` tag: + +``` +{{fsdocs-watch-script}} +``` + > NOTE: There is no guarantee that your template will continue to work with future versions of F# Formatting. > If you do develop a good template please consider contributing it back to F# Formatting. diff --git a/docs/templates/leftside/_template.html b/docs/templates/leftside/_template.html index 3e79c9c45..8bde951de 100644 --- a/docs/templates/leftside/_template.html +++ b/docs/templates/leftside/_template.html @@ -25,7 +25,7 @@ - + {{fsdocs-watch-script}} diff --git a/src/FSharp.Formatting.CommandTool/BuildCommand.fs b/src/FSharp.Formatting.CommandTool/BuildCommand.fs index 574db0e67..1008ddf2a 100644 --- a/src/FSharp.Formatting.CommandTool/BuildCommand.fs +++ b/src/FSharp.Formatting.CommandTool/BuildCommand.fs @@ -218,16 +218,46 @@ type internal DocContent(outputDirectory, previous: Map<_,_>, lineNumbers, fsiEv /// Processes and runs Suave server to host them on localhost module Serve = + //not sure what this was needed for + //let refreshEvent = new Event<_>() + + /// generate the script to inject into html to enable hot reload during development + let generateWatchScript (port:int) = + let tag = """ + +""" + tag.Replace("{{PORT}}", string port) + - let refreshEvent = new Event<_>() + let mutable signalHotReload = false let socketHandler (webSocket : WebSocket) _ = socket { while true do - do! - refreshEvent.Publish - |> Control.Async.AwaitEvent - |> Suave.Sockets.SocketOp.ofAsync - do! webSocket.send Text (ByteSegment (Encoding.UTF8.GetBytes "refreshed")) true + let emptyResponse = [||] |> ByteSegment + //not sure what this was needed for + //do! + // refreshEvent.Publish + // |> Control.Async.AwaitEvent + // |> Suave.Sockets.SocketOp.ofAsync + //do! webSocket.send Text (ByteSegment (Encoding.UTF8.GetBytes "refreshed")) true + if signalHotReload then + printfn "Triggering hot reload on the client" + do! webSocket.send Close emptyResponse true + signalHotReload <- false } let startWebServer outputDirectory localPort = @@ -487,6 +517,16 @@ type CoreBuildOptions(watch) = let indxTxt = System.Text.Json.JsonSerializer.Serialize index File.WriteAllText(Path.Combine(output, "index.json"), indxTxt) + /// get the hot reload script if running in watch mode + let getLatestWatchScript() = + if watch then + // if running in watch mode, inject hot reload script + [ParamKeys.``fsdocs-watch-script``, Serve.generateWatchScript this.port_option] + else + // otherwise, inject empty replacement string + [ParamKeys.``fsdocs-watch-script``, ""] + + // Incrementally convert content let runDocContentPhase1 () = protect (fun () -> @@ -528,7 +568,7 @@ type CoreBuildOptions(watch) = let runDocContentPhase2 () = protect (fun () -> - let globals = getLatestGlobalParameters() + let globals = getLatestWatchScript() @ getLatestGlobalParameters() latestDocContentPhase2 globals ) @@ -581,7 +621,7 @@ type CoreBuildOptions(watch) = protect (fun () -> printfn "" printfn "Write API Docs:" - let globals = getLatestGlobalParameters() + let globals = getLatestWatchScript() @ getLatestGlobalParameters() latestApiDocPhase2 globals regenerateSearchIndex() ) @@ -643,33 +683,41 @@ type CoreBuildOptions(watch) = if not docsQueued then docsQueued <- true printfn "Detected change in '%s', scheduling rebuild of docs..." this.input - Async.Start(async { + async { do! Async.Sleep(300) lock monitor (fun () -> - docsQueued <- false - if runDocContentPhase1() then - if runDocContentPhase2() then - regenerateSearchIndex() - ) }) ) + docsQueued <- false + if runDocContentPhase1() then + if runDocContentPhase2() then + regenerateSearchIndex() + ) + Serve.signalHotReload <- true + } + |> Async.Start + ) let apiDocsDependenciesChanged = Event<_>() apiDocsDependenciesChanged.Publish.Add(fun () -> if not generateQueued then generateQueued <- true printfn "Detected change in built outputs, scheduling rebuild of API docs..." - Async.Start(async { + async { do! Async.Sleep(300) lock monitor (fun () -> - generateQueued <- false - if runGeneratePhase1() then - if runGeneratePhase2() then - regenerateSearchIndex()) })) - + generateQueued <- false + if runGeneratePhase1() then + if runGeneratePhase2() then + regenerateSearchIndex() + ) + Serve.signalHotReload <- true + } + |> Async.Start + ) // Listen to changes in any input under docs docsWatcher.IncludeSubdirectories <- true docsWatcher.NotifyFilter <- NotifyFilters.LastWrite - docsWatcher.Changed.Add (fun _ ->docsDependenciesChanged.Trigger()) + docsWatcher.Changed.Add (fun _ -> docsDependenciesChanged.Trigger()) // When _template.* change rebuild everything templateWatcher.IncludeSubdirectories <- true diff --git a/src/FSharp.Formatting.Common/Templating.fs b/src/FSharp.Formatting.Common/Templating.fs index 537050b38..de31e4dee 100644 --- a/src/FSharp.Formatting.Common/Templating.fs +++ b/src/FSharp.Formatting.Common/Templating.fs @@ -102,6 +102,9 @@ module ParamKeys = /// A parameter key known to FSharp.Formatting let ``fsdocs-tooltips`` = ParamKey "fsdocs-tooltips" + /// A parameter key known to FSharp.Formatting + let ``fsdocs-watch-script`` = ParamKey "fsdocs-watch-script" + module internal SimpleTemplating = #if NETSTANDARD2_0 diff --git a/tests/FSharp.Literate.Tests/files/template.html b/tests/FSharp.Literate.Tests/files/template.html index 43abe29b0..cefc0960f 100644 --- a/tests/FSharp.Literate.Tests/files/template.html +++ b/tests/FSharp.Literate.Tests/files/template.html @@ -1,6 +1,6 @@  - + - + {{fsdocs-watch-script}} +