Skip to content

Commit

Permalink
Add hot reload for the watch command (#627) (#629)
Browse files Browse the repository at this point in the history
* 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]>
  • Loading branch information
kMutagene and dsyme authored Jan 13, 2021
1 parent 6d11662 commit af47b77
Show file tree
Hide file tree
Showing 7 changed files with 84 additions and 26 deletions.
2 changes: 1 addition & 1 deletion docs/_template.html
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
<!-- BEGIN SEARCH BOX: this adds support for the search box -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/JavaScript-autoComplete/1.0.4/auto-complete.css" />
<!-- END SEARCH BOX: this adds support for the search box -->

{{fsdocs-watch-script}}
</head>

<body>
Expand Down
2 changes: 1 addition & 1 deletion docs/content.fsx
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
6 changes: 6 additions & 0 deletions docs/styling.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 `<head>` 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.
Expand Down
2 changes: 1 addition & 1 deletion docs/templates/leftside/_template.html
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
<!-- BEGIN SEARCH BOX: this adds support for the search box -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/JavaScript-autoComplete/1.0.4/auto-complete.css" />
<!-- END SEARCH BOX: this adds support for the search box -->

{{fsdocs-watch-script}}
</head>

<body>
Expand Down
90 changes: 69 additions & 21 deletions src/FSharp.Formatting.CommandTool/BuildCommand.fs
Original file line number Diff line number Diff line change
Expand Up @@ -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 = """
<script type="text/javascript">
var wsUri = "ws://localhost:{{PORT}}/websocket";
function init()
{
websocket = new WebSocket(wsUri);
websocket.onclose = function(evt) { onClose(evt) };
}
function onClose(evt)
{
console.log('closing');
websocket.close();
document.location.reload();
}
window.addEventListener("load", init, false);
</script>
"""
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 =
Expand Down Expand Up @@ -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 () ->
Expand Down Expand Up @@ -528,7 +568,7 @@ type CoreBuildOptions(watch) =

let runDocContentPhase2 () =
protect (fun () ->
let globals = getLatestGlobalParameters()
let globals = getLatestWatchScript() @ getLatestGlobalParameters()
latestDocContentPhase2 globals
)

Expand Down Expand Up @@ -581,7 +621,7 @@ type CoreBuildOptions(watch) =
protect (fun () ->
printfn ""
printfn "Write API Docs:"
let globals = getLatestGlobalParameters()
let globals = getLatestWatchScript() @ getLatestGlobalParameters()
latestApiDocPhase2 globals
regenerateSearchIndex()
)
Expand Down Expand Up @@ -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
Expand Down
3 changes: 3 additions & 0 deletions src/FSharp.Formatting.Common/Templating.fs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
5 changes: 3 additions & 2 deletions tests/FSharp.Literate.Tests/files/template.html
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<!DOCTYPE html>
<html lang="en">
<head>
<head>
<meta charset="utf-8">
<!--
The {page-title} parameters will be replaced with the
Expand All @@ -18,7 +18,8 @@
<!--[if lt IE 9]>
<script src="http://html5shim.googlecode.com/svn/trunk/html5.js"></script>
<![endif]-->
</head>
{{fsdocs-watch-script}}
</head>
<body>
<div class="container">
<div class="row" style="margin-top:30px">
Expand Down

0 comments on commit af47b77

Please sign in to comment.