Skip to content

Commit af47b77

Browse files
kMutagenedsyme
andauthored
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 <[email protected]>
1 parent 6d11662 commit af47b77

File tree

7 files changed

+84
-26
lines changed

7 files changed

+84
-26
lines changed

docs/_template.html

+1-1
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525
<!-- BEGIN SEARCH BOX: this adds support for the search box -->
2626
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/JavaScript-autoComplete/1.0.4/auto-complete.css" />
2727
<!-- END SEARCH BOX: this adds support for the search box -->
28-
28+
{{fsdocs-watch-script}}
2929
</head>
3030

3131
<body>

docs/content.fsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ See [Styling](styling.html) for information about template parameters and stylin
8181
| `fsdocs-page-title` | First h1 heading in literate file. Generated for API docs |
8282
| `fsdocs-source` | Original literate script or markdown source |
8383
| `fsdocs-tooltips` | Generated hidden div elements for tooltips |
84-
84+
| `fsdocs-watch-script` | The websocket script used in watch mode to trigger hot reload |
8585
8686
The following substitutions are extracted from your project files and may or may not be used by the default
8787
template:

docs/styling.md

+6
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,12 @@ If you write a new theme by CSS styling please contribute it back to FSharp.Form
104104
You can do advanced styling by creating a new template. Add a file `docs/_template.html`, likely starting
105105
with the existing default template.
106106

107+
To enable hot reload during development with `fsdocs watch` in a custom `_template.html` file, make sure to add the following line to your `<head>` tag:
108+
109+
```
110+
{{fsdocs-watch-script}}
111+
```
112+
107113
> NOTE: There is no guarantee that your template will continue to work with future versions of F# Formatting.
108114
> If you do develop a good template please consider contributing it back to F# Formatting.
109115

docs/templates/leftside/_template.html

+1-1
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525
<!-- BEGIN SEARCH BOX: this adds support for the search box -->
2626
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/JavaScript-autoComplete/1.0.4/auto-complete.css" />
2727
<!-- END SEARCH BOX: this adds support for the search box -->
28-
28+
{{fsdocs-watch-script}}
2929
</head>
3030

3131
<body>

src/FSharp.Formatting.CommandTool/BuildCommand.fs

+69-21
Original file line numberDiff line numberDiff line change
@@ -218,16 +218,46 @@ type internal DocContent(outputDirectory, previous: Map<_,_>, lineNumbers, fsiEv
218218

219219
/// Processes and runs Suave server to host them on localhost
220220
module Serve =
221+
//not sure what this was needed for
222+
//let refreshEvent = new Event<_>()
223+
224+
/// generate the script to inject into html to enable hot reload during development
225+
let generateWatchScript (port:int) =
226+
let tag = """
227+
<script type="text/javascript">
228+
var wsUri = "ws://localhost:{{PORT}}/websocket";
229+
function init()
230+
{
231+
websocket = new WebSocket(wsUri);
232+
websocket.onclose = function(evt) { onClose(evt) };
233+
}
234+
function onClose(evt)
235+
{
236+
console.log('closing');
237+
websocket.close();
238+
document.location.reload();
239+
}
240+
window.addEventListener("load", init, false);
241+
</script>
242+
"""
243+
tag.Replace("{{PORT}}", string port)
244+
221245

222-
let refreshEvent = new Event<_>()
246+
let mutable signalHotReload = false
223247

224248
let socketHandler (webSocket : WebSocket) _ = socket {
225249
while true do
226-
do!
227-
refreshEvent.Publish
228-
|> Control.Async.AwaitEvent
229-
|> Suave.Sockets.SocketOp.ofAsync
230-
do! webSocket.send Text (ByteSegment (Encoding.UTF8.GetBytes "refreshed")) true
250+
let emptyResponse = [||] |> ByteSegment
251+
//not sure what this was needed for
252+
//do!
253+
// refreshEvent.Publish
254+
// |> Control.Async.AwaitEvent
255+
// |> Suave.Sockets.SocketOp.ofAsync
256+
//do! webSocket.send Text (ByteSegment (Encoding.UTF8.GetBytes "refreshed")) true
257+
if signalHotReload then
258+
printfn "Triggering hot reload on the client"
259+
do! webSocket.send Close emptyResponse true
260+
signalHotReload <- false
231261
}
232262

233263
let startWebServer outputDirectory localPort =
@@ -487,6 +517,16 @@ type CoreBuildOptions(watch) =
487517
let indxTxt = System.Text.Json.JsonSerializer.Serialize index
488518
File.WriteAllText(Path.Combine(output, "index.json"), indxTxt)
489519

520+
/// get the hot reload script if running in watch mode
521+
let getLatestWatchScript() =
522+
if watch then
523+
// if running in watch mode, inject hot reload script
524+
[ParamKeys.``fsdocs-watch-script``, Serve.generateWatchScript this.port_option]
525+
else
526+
// otherwise, inject empty replacement string
527+
[ParamKeys.``fsdocs-watch-script``, ""]
528+
529+
490530
// Incrementally convert content
491531
let runDocContentPhase1 () =
492532
protect (fun () ->
@@ -528,7 +568,7 @@ type CoreBuildOptions(watch) =
528568

529569
let runDocContentPhase2 () =
530570
protect (fun () ->
531-
let globals = getLatestGlobalParameters()
571+
let globals = getLatestWatchScript() @ getLatestGlobalParameters()
532572
latestDocContentPhase2 globals
533573
)
534574

@@ -581,7 +621,7 @@ type CoreBuildOptions(watch) =
581621
protect (fun () ->
582622
printfn ""
583623
printfn "Write API Docs:"
584-
let globals = getLatestGlobalParameters()
624+
let globals = getLatestWatchScript() @ getLatestGlobalParameters()
585625
latestApiDocPhase2 globals
586626
regenerateSearchIndex()
587627
)
@@ -643,33 +683,41 @@ type CoreBuildOptions(watch) =
643683
if not docsQueued then
644684
docsQueued <- true
645685
printfn "Detected change in '%s', scheduling rebuild of docs..." this.input
646-
Async.Start(async {
686+
async {
647687
do! Async.Sleep(300)
648688
lock monitor (fun () ->
649-
docsQueued <- false
650-
if runDocContentPhase1() then
651-
if runDocContentPhase2() then
652-
regenerateSearchIndex()
653-
) }) )
689+
docsQueued <- false
690+
if runDocContentPhase1() then
691+
if runDocContentPhase2() then
692+
regenerateSearchIndex()
693+
)
694+
Serve.signalHotReload <- true
695+
}
696+
|> Async.Start
697+
)
654698

655699
let apiDocsDependenciesChanged = Event<_>()
656700
apiDocsDependenciesChanged.Publish.Add(fun () ->
657701
if not generateQueued then
658702
generateQueued <- true
659703
printfn "Detected change in built outputs, scheduling rebuild of API docs..."
660-
Async.Start(async {
704+
async {
661705
do! Async.Sleep(300)
662706
lock monitor (fun () ->
663-
generateQueued <- false
664-
if runGeneratePhase1() then
665-
if runGeneratePhase2() then
666-
regenerateSearchIndex()) }))
667-
707+
generateQueued <- false
708+
if runGeneratePhase1() then
709+
if runGeneratePhase2() then
710+
regenerateSearchIndex()
711+
)
712+
Serve.signalHotReload <- true
713+
}
714+
|> Async.Start
715+
)
668716

669717
// Listen to changes in any input under docs
670718
docsWatcher.IncludeSubdirectories <- true
671719
docsWatcher.NotifyFilter <- NotifyFilters.LastWrite
672-
docsWatcher.Changed.Add (fun _ ->docsDependenciesChanged.Trigger())
720+
docsWatcher.Changed.Add (fun _ -> docsDependenciesChanged.Trigger())
673721

674722
// When _template.* change rebuild everything
675723
templateWatcher.IncludeSubdirectories <- true

src/FSharp.Formatting.Common/Templating.fs

+3
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,9 @@ module ParamKeys =
102102
/// A parameter key known to FSharp.Formatting
103103
let ``fsdocs-tooltips`` = ParamKey "fsdocs-tooltips"
104104

105+
/// A parameter key known to FSharp.Formatting
106+
let ``fsdocs-watch-script`` = ParamKey "fsdocs-watch-script"
107+
105108
module internal SimpleTemplating =
106109

107110
#if NETSTANDARD2_0

tests/FSharp.Literate.Tests/files/template.html

+3-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
<!DOCTYPE html>
22
<html lang="en">
3-
<head>
3+
<head>
44
<meta charset="utf-8">
55
<!--
66
The {page-title} parameters will be replaced with the
@@ -18,7 +18,8 @@
1818
<!--[if lt IE 9]>
1919
<script src="http://html5shim.googlecode.com/svn/trunk/html5.js"></script>
2020
<![endif]-->
21-
</head>
21+
{{fsdocs-watch-script}}
22+
</head>
2223
<body>
2324
<div class="container">
2425
<div class="row" style="margin-top:30px">

0 commit comments

Comments
 (0)