Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions RELEASE_NOTES.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,12 @@

## [Unreleased]

### Fixed
* Fix `fsdocs watch` WebSocket hot-reload URL to use `window.location.host` instead of hardcoded `localhost`, enabling hot reload when served via GitHub Codespaces, reverse proxies, or other non-localhost environments.

### Added
* Add `--root` option to `fsdocs watch` for overriding the root URL, enabling use with GitHub Codespaces port-forwarding, reverse proxies, and other non-localhost environments. [#924](https://github.com/fsprojects/FSharp.Formatting/issues/924)

## [22.0.0] - 2026-04-03

### Fixed
Expand Down
35 changes: 25 additions & 10 deletions src/fsdocs-tool/BuildCommand.fs
Original file line number Diff line number Diff line change
Expand Up @@ -827,11 +827,10 @@ module Serve =
let refreshEvent = FSharp.Control.Event<string>()

/// generate the script to inject into html to enable hot reload during development
let generateWatchScript (port: int) =
let tag =
"""
let generateWatchScript () =
"""
<script type="text/javascript">
var wsUri = "ws://localhost:{{PORT}}/websocket";
var wsUri = "ws://" + window.location.host + "/websocket";
function init()
{
websocket = new WebSocket(wsUri);
Expand All @@ -858,8 +857,6 @@ module Serve =
</script>
"""

tag.Replace("{{PORT}}", string<int> port)

let connectedClients = ConcurrentDictionary<WebSocket, unit>()

let socketHandler (webSocket: WebSocket) (context: HttpContext) =
Expand Down Expand Up @@ -1568,9 +1565,15 @@ type CoreBuildOptions(watch) =
// Adjust the user substitutions for 'watch' mode root
let userRoot, userParameters =
if watch then
let userRoot = sprintf "http://localhost:%d/" this.port_option

if userParametersDict.ContainsKey(ParamKeys.root) then
let userRoot =
match this.root_override_option with
| Some r -> r
| None -> sprintf "http://localhost:%d/" this.port_option

if
userParametersDict.ContainsKey(ParamKeys.root)
&& this.root_override_option.IsNone
then
printfn "ignoring user-specified root since in watch mode, root = %s" userRoot

let userParameters =
Expand Down Expand Up @@ -1881,7 +1884,7 @@ type CoreBuildOptions(watch) =
let getLatestWatchScript () =
if watch then
// if running in watch mode, inject hot reload script
[ ParamKeys.``fsdocs-watch-script``, Serve.generateWatchScript this.port_option ]
[ ParamKeys.``fsdocs-watch-script``, Serve.generateWatchScript () ]
else
// otherwise, inject empty replacement string
[ ParamKeys.``fsdocs-watch-script``, "" ]
Expand Down Expand Up @@ -2330,6 +2333,9 @@ type CoreBuildOptions(watch) =
abstract port_option: int
default x.port_option = 0

abstract root_override_option: string option
default x.root_override_option = None

/// Helpers for the <c>fsdocs convert</c> command.
module private ConvertHelpers =

Expand Down Expand Up @@ -2746,3 +2752,12 @@ type WatchCommand() =

[<Option("port", Required = false, Default = 8901, HelpText = "Port to serve content for http://localhost serving.")>]
member val port = 8901 with get, set

override x.root_override_option = if x.root = "" then None else Some x.root

[<Option("root",
Required = false,
Default = "",
HelpText =
"Override the root URL for generated pages. Useful for reverse proxies or GitHub Codespaces. E.g. --root / or --root https://example.com/docs/. When not set, defaults to http://localhost:<port>/.")>]
member val root = "" with get, set