Skip to content
Merged
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
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,16 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html) from version [0.1.0] moving forward.

## [0.4.0] - 2026-09-03

### Fixed

- An F# file that type-checks but that Fable cannot translate now fails `vite build` instead of passing for a successful compile. `Fable.Compiler` filled a compile's diagnostics from the F# type-check only and discarded Fable's own logs, so a call like `Async.RunSynchronously` produced a module that does nothing while the build printed nothing and exited 0, and the app broke in the browser. Fixed upstream in [fable-compiler/Fable#4923](https://github.com/fable-compiler/Fable/pull/4923), which this release picks up: the daemon now reports what Fable said alongside the F# diagnostics, for the first compile of the project as well as for every recompile after an edit, so these errors reach the terminal, the browser overlay and `/api/diagnostics`. They carry no error number, so they are named by their tag — `ERROR FABLE:` where an F# diagnostic reads `ERROR FS0025:`. Diagnostics on files under `fable_modules` are still dropped unless `fableModulesDiagnostics` is on. ([#67](https://github.com/fable-compiler/vite-plugin-fable/pull/67))

### Changed

- Updated Fable.Compiler to 5.15.0 and `@fable-org/fable-library-js` to 2.6.0. ([#67](https://github.com/fable-compiler/vite-plugin-fable/pull/67))

## [0.3.1] - 2026-08-29

### Changed
Expand Down
2 changes: 1 addition & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ else's contract:
hook-contract questions definitively. Several plugin bugs were misdiagnosed from assumptions
about Vite that the source contradicted, and several fixes in the git history cite line numbers
from it.
- `../Fable` — Fable 5.14. Notably `src/Fable.Compiler/Library.fs`, where
- `../Fable` — Fable 5.15. Notably `src/Fable.Compiler/Library.fs`, where
`FileWriter.AddSourceMapping` is a no-op, so `CliArgs.SourceMaps` does nothing.

## Seeing what the plugin actually emitted, without a browser
Expand Down
2 changes: 1 addition & 1 deletion Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
<PackageVersion Include="FSharp.Core" Version="10.1.203" />
<PackageVersion Include="FSharp.SystemTextJson" Version="1.4.36" />
<PackageVersion Include="StreamJsonRpc" Version="2.25.29" />
<PackageVersion Include="Fable.Compiler" Version="5.14.1" />
<PackageVersion Include="Fable.Compiler" Version="5.15.0" />
<PackageVersion Include="Suave" Version="2.6.2" />
<PackageVersion Include="protobuf-net" Version="3.2.30" />
<!-- lock transitive dependencies -->
Expand Down
6 changes: 2 additions & 4 deletions ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,12 @@ Nothing here is done. Items are deleted as they land, so what remains is open wo

Item 1 is blocked upstream and can only be tracked and item 2 is on hold. Item 3 is a project, items 4 and 5 are smaller ones, item 6 records a rejected decision and item 7 is loose ends.

References into `~/Projects/Fable` are against Fable 5.14, the version in the workspace catalog.
References into `~/Projects/Fable` are against Fable 5.15, the version in the workspace catalog.

## 1. Blocked on Fable

Neither of these can be fixed in this repo. Both need an upstream change first.
This cannot be fixed in this repo. It needs an upstream change first.

- **Fable's own errors never reach the plugin.** `CodeServices.compileMultipleFilesToJavaScript` fills `CompileResult.Diagnostics` from FCS's type-check results only, and discards the `CompilerImpl` holding `com.Logs` (`~/Projects/Fable/src/Fable.Compiler/Library.fs:223` upcasts it to the `Compiler` interface, where `Logs` does not exist). A file that type-checks but that Fable cannot translate therefore compiles to `return null` with no diagnostic at all: `vite build` prints nothing, exits 0, and the app breaks at runtime. Reproduced in `sample-project` with `Async.RunSynchronously`, and filed with a proposed direction as [fable-compiler/Fable#4922](https://github.com/fable-compiler/Fable/issues/4922).
There is a local half waiting on it: `FilesCompiledResult.Success` carries no diagnostics, so `tryCompileProject` has nowhere to put them and `failBuildOnErrors` would never see them.
- **Real F# source maps.** `FileWriter.AddSourceMapping` in `src/Fable.Compiler/Library.fs:84-90` is a no-op with the `SourceMapSharp` generator commented out, so `CliArgs.SourceMaps` does nothing. The plugin returns `{ mappings: '' }`, which is honest about having no mapping, but a real F#-to-JS one needs the Fable change first. Not filed.

## 2. A Vite DevTools panel (on hold)
Expand Down
4 changes: 2 additions & 2 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

14 changes: 13 additions & 1 deletion docs/how.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ sequenceDiagram
Plugin->>Daemon: fable/project-changed
Daemon-->>Plugin: source files, diagnostics, MSBuild inputs
Plugin->>Daemon: fable/initial-compile
Daemon-->>Plugin: JavaScript per F# file
Daemon-->>Plugin: JavaScript per F# file, and what Fable could not translate
Vite->>Plugin: load (per .fs request)
</div>

Expand All @@ -80,6 +80,18 @@ and type-checks the project. `fable/initial-compile` then transpiles every sourc
inside the daemon on [Fable.Compiler](https://github.com/fable-compiler/Fable), the same code
`dotnet fable` uses.

Both steps report problems, and they are not the same problems. The type-check reports what the F#
compiler found, `FS0025` and the like: code that does not compile. The compile reports what Fable
found, which is code that compiles as F# but that Fable cannot turn into JavaScript, such as
`Async.RunSynchronously`. Those carry no error number, so the plugin names them by their tag:

```text
9:42:26 PM [vite] [fable] ERROR FABLE: Microsoft.FSharp.Control.FSharpAsync.RunSynchronously (static) is not supported by Fable Math.fs (6,26) (6,48)
```

Either kind fails `vite build`. In dev the server stays up and the error goes to the browser
overlay, so the next edit can fix it.

The daemon also reports which MSBuild files the project depends on. The plugin watches those, so a
change to an `fsproj` or a `Directory.Build.props` triggers a full re-crack rather than an
incremental compile.
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
"sample-project"
],
"catalog": {
"@fable-org/fable-library-js": "2.5.1",
"@fable-org/fable-library-js": "2.6.0",
"vite": "^8.2.2",
"vite-plugin-inspect": "^12.0.2"
}
Expand Down
58 changes: 55 additions & 3 deletions packages/vite-plugin-fable/Fable.Daemon.Tests/DebugTests.fs
Original file line number Diff line number Diff line change
Expand Up @@ -439,6 +439,25 @@ module WireTests =
}
Severity = "Warning"
FileName = "/project/Math.fs"
Tag = "FSHARP"
}

/// What Fable reports about a file that type-checks but that it cannot translate. It carries no
/// error number, which is why `ErrorNumberText` is empty rather than absent.
let private fableDiagnostic : Diagnostic =
{
ErrorNumberText = ""
Message = "Microsoft.FSharp.Control.FSharpAsync.RunSynchronously (static) is not supported by Fable"
Range =
{
StartLine = 7
StartColumn = 12
EndLine = 7
EndColumn = 41
}
Severity = "Error"
FileName = "/project/Math.fs"
Tag = "FABLE"
}

[<Test>]
Expand All @@ -458,7 +477,10 @@ module WireTests =
[<Test>]
let ``fable/initial-compile matches its fixture`` () =
let response =
FilesCompiledResult.Success (Map.ofList [ "/project/Math.fs", "export const sum = 1;" ])
FilesCompiledResult.Success (
Map.ofList [ "/project/Math.fs", "export const sum = 1;" ],
[| fableDiagnostic |]
)

Assert.That (
normalize (serialize response),
Expand All @@ -468,7 +490,10 @@ module WireTests =
[<Test>]
let ``fable/compile matches its fixture`` () =
let response =
FileChangedResult.Success (Map.ofList [ "/project/Math.fs", "export const sum = 2;" ], [| diagnostic |])
FileChangedResult.Success (
Map.ofList [ "/project/Math.fs", "export const sum = 2;" ],
[| diagnostic ; fableDiagnostic |]
)

Assert.That (normalize (serialize response), Is.EqualTo (normalize (File.ReadAllText (fixture "compile.json"))))

Expand Down Expand Up @@ -529,6 +554,7 @@ module DebugServerTests =
EndLine = 3
EndColumn = 9
}
Tag = "FSHARP"
Severity = "Warning"
FileName = mathFs
}
Expand Down Expand Up @@ -651,7 +677,33 @@ module DebugServerTests =

Assert.That (files.RootElement.GetProperty("files").[0].GetProperty("compiled").GetBoolean(), Is.False)

Debug.publishInitialCompile (Map.ofList [ mathFs, "export const sum = 1;" ]) Set.empty
// A file that type-checks but that Fable cannot translate reports nothing to the
// type-check, so the compile is the only place its error can come from.
let fableError : Diagnostic =
{
ErrorNumberText = ""
Message = "Microsoft.FSharp.Control.FSharpAsync.RunSynchronously (static) is not supported by Fable"
Range =
{
StartLine = 7
StartColumn = 12
EndLine = 7
EndColumn = 41
}
Severity = "Error"
FileName = mathFs
Tag = "FABLE"
}

Debug.publishInitialCompile (Map.ofList [ mathFs, "export const sum = 1;" ]) Set.empty [| fableError |]

use! compileErrors = getJson client $"{baseUrl}/api/diagnostics?severity=error"
Assert.That (compileErrors.RootElement.GetProperty("count").GetInt32(), Is.EqualTo 1)

Assert.That (
compileErrors.RootElement.GetProperty("diagnostics").[0].GetProperty("source").GetString(),
Is.EqualTo "compile"
)

use! compiled = getJson client $"{baseUrl}/api/files?path=Math.fs"

Expand Down
9 changes: 7 additions & 2 deletions packages/vite-plugin-fable/Fable.Daemon/Debug.fs
Original file line number Diff line number Diff line change
Expand Up @@ -212,15 +212,20 @@ let publishProject (project : ProjectState) : unit =
}
)

let publishInitialCompile (compiled : Map<FullPath, JavaScript>) (fromCache : Set<FullPath>) : unit =
let publishInitialCompile
(compiled : Map<FullPath, JavaScript>)
(fromCache : Set<FullPath>)
(diagnostics : Diagnostic array)
: unit
=
advance (fun s ->
{ s with
Compile =
Some
{
CompiledFiles = compiled
FromCache = fromCache
Diagnostics = Array.empty
Diagnostics = diagnostics
LastRequested = Array.empty
LastCompiledAt = DateTime.Now
}
Expand Down
6 changes: 4 additions & 2 deletions packages/vite-plugin-fable/Fable.Daemon/Debug.fsi
Original file line number Diff line number Diff line change
Expand Up @@ -51,8 +51,10 @@ val isEnabled : unit -> bool
/// decides which files the project even has.
val publishProject : project : ProjectState -> unit

/// Record what a full project compile produced.
val publishInitialCompile : compiled : Map<FullPath, JavaScript> -> fromCache : Set<FullPath> -> unit
/// Record what a full project compile produced. The diagnostics are Fable's own; the F# ones
/// belong to the crack that came before and are reported there.
val publishInitialCompile :
compiled : Map<FullPath, JavaScript> -> fromCache : Set<FullPath> -> diagnostics : Diagnostic array -> unit

/// Record what compiling a set of changed files produced, merged over what was compiled before,
/// the way the plugin merges it into its own map.
Expand Down
78 changes: 69 additions & 9 deletions packages/vite-plugin-fable/Fable.Daemon/Program.fs
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,9 @@ let tryTypeCheckProject
type CompiledProjectData =
{
CompiledFSharpFiles : Map<string, string>
/// What Fable reported while translating. The F# diagnostics are not repeated here: the
/// project was type-checked by the crack that came before, which already reported them.
Diagnostics : Diagnostic array
/// The files answered from the `fable_modules` cache rather than compiled just now.
FromCache : Set<FullPath>
}
Expand All @@ -232,6 +235,52 @@ let private mapDiagnostics (ds : FSharpDiagnostic array) =
Range = mapRange d.Range
Severity = string d.Severity
FileName = d.FileName
Tag = "FSHARP"
}
)

/// What Fable itself reported while translating, dropping the F# diagnostics it reports alongside.
///
/// Since Fable 5.15 a compile answers with `Logs` rather than the type-check's diagnostics: the F#
/// half tagged `FSHARP` with its error number folded into the message, and everything Fable raised
/// tagged `FABLE`. The F# half is dropped here and taken from `FSharpDiagnostic` instead, which
/// still carries the error number as a field of its own.
///
/// This is what a file that type-checks but that Fable cannot translate reports. Before 5.15 those
/// logs were discarded inside `Fable.Compiler`, so such a file compiled to `return null` and the
/// build said nothing.
let private mapFableLogs (logs : Fable.Transforms.State.LogEntry array) : Diagnostic array =
logs
|> Array.filter (fun log -> log.Tag <> "FSHARP")
|> Array.map (fun log ->
{
ErrorNumberText = ""
Message = log.Message
Range =
match log.Range with
| Some range ->
{
StartLine = range.start.line
StartColumn = range.start.column
EndLine = range.``end``.line
EndColumn = range.``end``.column
}
| None ->
// What `Fable.Cli` prints for a log without a range, so the message still
// points at the file it is about.
{
StartLine = 1
StartColumn = 1
EndLine = 1
EndColumn = 1
}
Severity =
match log.Severity with
| Severity.Error -> "Error"
| Severity.Warning -> "Warning"
| Severity.Info -> "Info"
FileName = Option.defaultValue "" log.FileName
Tag = log.Tag
}
)

Expand Down Expand Up @@ -327,6 +376,7 @@ let tryCompileProject (logger : ILogger) (model : Model) : Async<Result<Compiled
Ok
{
CompiledFSharpFiles = compiledFiles
Diagnostics = mapFableLogs initialCompileResponse.Logs
FromCache = cachedFableModuleFiles.Keys |> Set.ofSeq
}
with ex ->
Expand All @@ -337,7 +387,9 @@ let tryCompileProject (logger : ILogger) (model : Model) : Async<Result<Compiled
type CompiledFileData =
{
CompiledFiles : Map<string, string>
Diagnostics : FSharpDiagnostic array
/// The type-check of the project up to the last file compiled, plus whatever Fable
/// reported while translating.
Diagnostics : Diagnostic array
}

/// Find all the dependent files as efficient as possible.
Expand Down Expand Up @@ -436,7 +488,10 @@ let tryCompileFiles
Ok
{
CompiledFiles = compiledFileResponse.CompiledFiles
Diagnostics = compiledFileResponse.Diagnostics
Diagnostics =
Array.append
(mapDiagnostics checkProjectResult.Diagnostics)
(mapFableLogs compiledFileResponse.Logs)
}
with ex ->
logger.LogCritical ("tryCompileFile threw exception {ex}", ex)
Expand Down Expand Up @@ -544,8 +599,11 @@ type FableServer(sender : Stream, reader : Stream, logger : ILogger) as this =
match result with
| Error error -> replyChannel.Reply (FilesCompiledResult.Error error)
| Ok result ->
replyChannel.Reply (FilesCompiledResult.Success result.CompiledFSharpFiles)
Debug.publishInitialCompile result.CompiledFSharpFiles result.FromCache
replyChannel.Reply (
FilesCompiledResult.Success (result.CompiledFSharpFiles, result.Diagnostics)
)

Debug.publishInitialCompile result.CompiledFSharpFiles result.FromCache result.Diagnostics

return Some model

Expand All @@ -555,9 +613,9 @@ type FableServer(sender : Stream, reader : Stream, logger : ILogger) as this =
match result with
| Error error -> replyChannel.Reply (FileChangedResult.Error error)
| Ok result ->
let diagnostics = mapDiagnostics result.Diagnostics
replyChannel.Reply (FileChangedResult.Success (result.CompiledFiles, diagnostics))
Debug.publishFileCompile result.CompiledFiles diagnostics (Array.ofList fileNames)
replyChannel.Reply (FileChangedResult.Success (result.CompiledFiles, result.Diagnostics))

Debug.publishFileCompile result.CompiledFiles result.Diagnostics (Array.ofList fileNames)

return Some model

Expand Down Expand Up @@ -664,13 +722,15 @@ type FableServer(sender : Stream, reader : Stream, logger : ILogger) as this =
""
sw.Elapsed.TotalMilliseconds
(match response with
| FilesCompiledResult.Success compiled -> $"success: %i{compiled.Count} files"
| FilesCompiledResult.Success (compiled, _) -> $"success: %i{compiled.Count} files"
| FilesCompiledResult.Error error -> $"error: %s{error}")

let logResponse =
match response with
| FilesCompiledResult.Error e -> box e
| FilesCompiledResult.Success result -> result.Keys |> String.concat "\n" |> sprintf "\n%s" |> box
| FilesCompiledResult.Success (result, diagnostics) ->
let keys = result.Keys |> String.concat "\n" |> sprintf "\n%s"
box (keys, diagnostics)

logger.LogDebug ("exit \"fable/initial-compile\" with {logResponse}", logResponse)
return response
Expand Down
Loading