FsHotWatch 0.3.0-alpha.1
dotnet add package FsHotWatch --version 0.3.0-alpha.1
NuGet\Install-Package FsHotWatch -Version 0.3.0-alpha.1
<PackageReference Include="FsHotWatch" Version="0.3.0-alpha.1" />
<PackageVersion Include="FsHotWatch" Version="0.3.0-alpha.1" />
<PackageReference Include="FsHotWatch" />
paket add FsHotWatch --version 0.3.0-alpha.1
#r "nuget: FsHotWatch, 0.3.0-alpha.1"
#:package FsHotWatch@0.3.0-alpha.1
#addin nuget:?package=FsHotWatch&version=0.3.0-alpha.1&prerelease
#tool nuget:?package=FsHotWatch&version=0.3.0-alpha.1&prerelease
FsHotWatch
Speed up your F# development feedback loop.
FsHotWatch is a background daemon that watches your source files and keeps the F# compiler warm. When you save a file, it instantly re-checks it and tells your tools (linters, analyzers, test runners) what changed — without restarting the compiler from scratch each time.
The problem
F# tools are slow because they each start their own compiler from zero. A 15-project solution takes ~2 minutes to analyze. Every time you save a file, your linter restarts, your analyzer restarts, your test runner restarts — all parsing and type-checking the same 500 files again.
How FsHotWatch helps
FsHotWatch runs one compiler in the background and shares it with all your tools:
- You save a file — FsHotWatch notices the change
- It re-checks just that file using the compiler that's already warm (milliseconds, not minutes)
- Plugins get the results instantly — your linter, analyzer, and test runner all see the updated check results without re-parsing anything
- You query the results —
fs-hot-watch statusshows what each tool found
Changes are debounced — if you save 10 files in quick succession (like a formatter running), FsHotWatch waits for things to settle, then processes them all in one batch.
Quick start
# Install the CLI tool
dotnet tool install -g FsHotWatch.Cli
# Start the daemon in your repo (runs in foreground, Ctrl+C to stop)
fs-hot-watch start
# From another terminal, check what's happening
fs-hot-watch status
Packages
FsHotWatch is split into small packages so you only install what you need:
| Package | What it does |
|---|---|
FsHotWatch |
Core library — the daemon, file watcher, plugin system, IPC |
FsHotWatch.Cli |
CLI tool — fs-hot-watch start/stop/status |
FsHotWatch.TestPrune |
Plugin: figures out which tests to run when code changes |
FsHotWatch.Analyzers |
Plugin: runs F# analyzers (like G-Research or your own) |
FsHotWatch.Lint |
Plugin: runs FSharpLint using the warm compiler's results |
FsHotWatch.Fantomas |
Plugin: checks if your files are formatted with Fantomas |
FsHotWatch.Build |
Plugin: runs dotnet build and emits BuildCompleted events |
FsHotWatch.Coverage |
Plugin: checks coverage thresholds after tests complete |
FsHotWatch.FileCommand |
Plugin: runs custom commands when specific files change |
Writing your own plugin
Plugins use a declarative framework: you define an update function that receives events and returns new state. The framework manages the agent, status tracking, and IPC command registration.
open FsHotWatch.Events
open FsHotWatch.PluginFramework
type MyState = { FilesChecked: int }
let myPlugin: PluginHandler<MyState, unit> =
{ Name = "my-plugin"
Init = { FilesChecked = 0 }
Update =
fun ctx state event ->
async {
match event with
| FileChecked result ->
// result.ParseResults and result.CheckResults come from
// the warm FSharpChecker — no re-parsing needed.
printfn "Checked: %s" result.File
ctx.ReportStatus(Completed(System.DateTime.UtcNow))
return { FilesChecked = state.FilesChecked + 1 }
| _ -> return state
}
Commands =
[ "my-status",
fun state _args ->
async { return $"checked %d{state.FilesChecked} files" } ]
Subscriptions =
{ PluginSubscriptions.none with
FileChecked = true }
CacheKey = None }
// Register with the daemon:
daemon.RegisterHandler(myPlugin)
Available events (subscribe via Subscriptions):
FileChanged— a source file was saved (you get the file paths)BuildCompleted—dotnet buildfinished (success or failure)FileChecked— a file was type-checked (you get parse + check results)TestCompleted— tests finished running (you get per-project results)
What you can do in Update via ctx:
ctx.Checker— the warm FSharpChecker (reuse it for your own analysis)ctx.RepoRoot— path to the repository rootctx.ReportStatus(status)— tell the daemon your current statusctx.ReportErrors(file, entries)— report diagnostics to the error ledgerctx.EmitBuildCompleted(result)— emit events to other pluginsctx.Post(msg)— send a custom message back to your own agent
Configuration
Create .fs-hot-watch.json in your repo root. All fields are optional — sensible defaults are used when omitted.
{
"build": {
"command": "dotnet",
"args": "build"
},
"format": true,
"lint": true,
"cache": "jj",
"tests": {
"beforeRun": "dotnet build",
"projects": [
{
"project": "MyProject.Tests",
"command": "dotnet",
"args": "run --project tests/MyProject.Tests --no-build --",
"filterTemplate": "--filter-class {classes}",
"classJoin": " ",
"group": "unit"
}
]
},
"coverage": {
"directory": "./coverage",
"thresholdsFile": "coverage-thresholds.json"
},
"analyzers": {
"paths": ["analyzers/"]
},
"fileCommands": [
{
"pattern": "*.fsx",
"command": "dotnet",
"args": "fsi --typecheck-only"
}
]
}
Configuration reference
| Field | Type | Default | Description |
|---|---|---|---|
build |
object \| bool |
{"command": "dotnet", "args": "build"} |
Build command. false disables. |
format |
bool |
true |
Enable Fantomas format-on-save preprocessor. |
lint |
bool |
true |
Enable FSharpLint plugin. Uses fsharplint.json if found. |
cache |
string \| bool |
auto ("jj" if .jj/ exists, else "file") |
Cache strategy: "none", "memory", "file", or "jj". |
tests |
object |
— | Test runner config. See below. |
coverage |
object |
— | Coverage threshold checking. |
analyzers |
object |
— | F# Analyzers SDK integration. |
fileCommands |
array |
[] |
Custom commands triggered by file patterns. |
build fields:
| Field | Type | Default | Description |
|---|---|---|---|
command |
string |
"dotnet" |
Build command. |
args |
string |
"build" |
Arguments to the build command. |
buildTemplate |
string |
— | Template for incremental builds. {projects} is replaced with changed project paths. |
tests fields:
| Field | Type | Default | Description |
|---|---|---|---|
beforeRun |
string |
— | Command to run before each test run (e.g. "dotnet build"). |
projects |
array |
[] |
List of test project configurations. |
tests.projects[] fields:
| Field | Type | Default | Description |
|---|---|---|---|
project |
string |
"unknown" |
Project name (used for filtering and display). |
command |
string |
"dotnet" |
Test runner command. |
args |
string |
"test --project <project>" |
Arguments to the test runner. |
group |
string |
"default" |
Group name (for running subsets). |
environment |
object |
{} |
Extra environment variables as "KEY": "VALUE" pairs. |
filterTemplate |
string |
— | Template for class-based filtering. {classes} is replaced with affected test class names. |
classJoin |
string |
" " |
Separator for joining class names in the filter. |
coverage fields:
| Field | Type | Default | Description |
|---|---|---|---|
directory |
string |
"./coverage" |
Directory containing Cobertura XML coverage reports. |
thresholdsFile |
string |
— | Path to JSON file with per-project coverage thresholds. |
analyzers fields:
| Field | Type | Default | Description |
|---|---|---|---|
paths |
string[] |
— | Directories containing analyzer DLLs. Relative paths resolved from repo root. |
fileCommands[] fields:
| Field | Type | Default | Description |
|---|---|---|---|
pattern |
string |
"*.fsx" |
File extension pattern to match (e.g. "*.fsx", "*.sql"). |
command |
string |
"echo" |
Command to run when a matching file changes. |
args |
string |
"" |
Arguments to the command. |
Cache directory
FsHotWatch stores check result caches and the TestPrune database in .fshw/ at the repository root. Add this to your .gitignore:
.fshw/
The cache directory contains:
cache/— Cached FCS check results for faster cold startstest-impact.db— TestPrune dependency analysis database
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net10.0 is compatible. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
-
net10.0
- FSharp.Compiler.Service (>= 43.12.201)
- FSharp.Core (>= 10.1.201)
- FSharp.Data.Adaptive (>= 1.2.26)
- Ionide.ProjInfo (>= 0.74.2)
- Ionide.ProjInfo.FCS (>= 0.74.2)
- StreamJsonRpc (>= 2.24.84)
- TestPrune.Core (>= 1.0.1)
NuGet packages (7)
Showing the top 5 NuGet packages that depend on FsHotWatch:
| Package | Downloads |
|---|---|
|
FsHotWatch.TestPrune
FsHotWatch plugin for TestPrune test impact analysis |
|
|
FsHotWatch.Coverage
FsHotWatch plugin that checks coverage thresholds after tests complete |
|
|
FsHotWatch.Analyzers
FsHotWatch plugin for F# Analyzers SDK integration |
|
|
FsHotWatch.Fantomas
FsHotWatch plugin for Fantomas format checking |
|
|
FsHotWatch.Lint
FsHotWatch plugin for FSharpLint integration |
GitHub repositories
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated |
|---|---|---|
| 0.3.0-alpha.1 | 15 | 4/8/2026 |
| 0.2.0-alpha.1 | 24 | 4/8/2026 |
| 0.1.0-alpha.1 | 57 | 4/3/2026 |