Skip to content

Commit b992b68

Browse files
authored
Update NuGet and npm dependencies and add Temporal flag (#69)
1 parent 3adc17b commit b992b68

17 files changed

Lines changed: 211 additions & 112 deletions

File tree

‎.config/dotnet-tools.json‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,12 @@
33
"isRoot": true,
44
"tools": {
55
"fantomas": {
6-
"version": "8.0.0-alpha-025",
6+
"version": "8.0.0-beta-002",
77
"commands": ["fantomas"],
88
"rollForward": false
99
},
1010
"fsdocs-tool": {
11-
"version": "22.1.0",
11+
"version": "23.0.0-alpha.1",
1212
"commands": ["fsdocs"],
1313
"rollForward": false
1414
},

‎CHANGELOG.md‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,16 @@ All notable changes to this project will be documented in this file.
55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html) from version [0.1.0] moving forward.
77

8+
## [0.5.0] - 2026-09-09
9+
10+
### Added
11+
12+
- `temporal` option. Compiles the .NET date and time types to the Temporal API instead of `Date`, with tick precision and .NET's arithmetic across DST changes. It is Fable's experimental `--test:js-temporal` flag passed through, and the plugin adds no polyfill. Off by default, so existing output is unchanged. See [Temporal](https://fable.io/vite-plugin-fable/recipes.html#Temporal).
13+
14+
### Changed
15+
16+
- Updated Fable.Compiler to 5.16.0 and `@fable-org/fable-library-js` to 2.7.0. Numeric formatting in the emitted JavaScript now matches .NET for the `G`, `E` and custom format strings, and `E` is accepted on `int64` and `uint64`.
17+
818
## [0.4.0] - 2026-09-03
919

1020
### Fixed

‎Directory.Packages.props‎

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -4,24 +4,24 @@
44
<CentralPackageTransitivePinningEnabled>true</CentralPackageTransitivePinningEnabled>
55
</PropertyGroup>
66
<ItemGroup>
7-
<PackageVersion Include="Fable.React" Version="7.4.3" />
8-
<PackageVersion Include="FSharp.Core" Version="10.1.203" />
7+
<PackageVersion Include="Fable.React" Version="9.4.0" />
8+
<PackageVersion Include="FSharp.Core" Version="10.1.401" />
99
<PackageVersion Include="FSharp.SystemTextJson" Version="1.4.36" />
1010
<PackageVersion Include="StreamJsonRpc" Version="2.25.29" />
11-
<PackageVersion Include="Fable.Compiler" Version="5.15.0" />
12-
<PackageVersion Include="Suave" Version="2.6.2" />
13-
<PackageVersion Include="protobuf-net" Version="3.2.30" />
11+
<PackageVersion Include="Fable.Compiler" Version="5.16.0" />
12+
<PackageVersion Include="Suave" Version="3.4.6" />
13+
<PackageVersion Include="protobuf-net" Version="3.4.21" />
1414
<!-- lock transitive dependencies -->
15-
<PackageVersion Include="Fable.AST" Version="5.0.0" />
15+
<PackageVersion Include="Fable.AST" Version="5.1.0" />
1616
<PackageVersion Include="System.Collections" Version="4.3.0" />
1717
<PackageVersion Include="System.Diagnostics.Debug" Version="4.3.0" />
1818
<PackageVersion Include="System.IO.FileSystem.Primitives" Version="4.3.0" />
1919
<PackageVersion Include="System.Runtime.Extensions" Version="4.3.1" />
2020
<PackageVersion Include="MessagePack" Version="2.5.302" />
2121
<!-- unit test project -->
22-
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.13.0" />
23-
<PackageVersion Include="Nerdbank.Streams" Version="2.13.31" />
24-
<PackageVersion Include="NUnit" Version="4.3.2" />
25-
<PackageVersion Include="NUnit3TestAdapter" Version="5.0.0" />
22+
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="18.9.0" />
23+
<PackageVersion Include="Nerdbank.Streams" Version="2.14.354" />
24+
<PackageVersion Include="NUnit" Version="4.6.1" />
25+
<PackageVersion Include="NUnit3TestAdapter" Version="6.3.0" />
2626
</ItemGroup>
27-
</Project>
27+
</Project>

‎bun.lock‎

Lines changed: 50 additions & 50 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎docs/recipes.md‎

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,20 +18,21 @@ Every option the plugin accepts. All of them are optional.
1818
| `configuration` | <code>"Debug" &#124; "Release"</code> | `Release` on build, `Debug` on dev | MSBuild configuration. See [Debug or Release](#Debug-or-Release). |
1919
| `jsx` | <code>"automatic" &#124; "transform" &#124; null</code> | `null` | Transform JSX that Fable emitted. See [Fable.Core.JSX](#Fable-Core-JSX). |
2020
| `noReflection` | `boolean` | `false` | Passed to Fable. Skips emitting reflection info, which produces smaller output. |
21+
| `temporal` | `boolean` | `false` | Passed to Fable. Compiles the .NET date and time types to the Temporal API instead of `Date`. See [Temporal](#Temporal). |
2122
| `exclude` | `string[]` | `[]` | Passed to Fable. Excludes assemblies from compilation, typically Fable plugins. |
2223
| `debug` | `boolean` | `false` | Print what the plugin is doing, and start the daemon's debug server. See [Seeing what the plugin is doing](#Seeing-what-the-plugin-is-doing). |
2324
| `fableModulesDiagnostics` | `boolean` | `false` | Report diagnostics for files under `fable_modules`. See [Diagnostics from restored packages](#Diagnostics-from-restored-packages). |
2425

25-
`noReflection` and `exclude` are handed to Fable.Compiler unchanged; they mean what they mean for
26-
the `dotnet fable` CLI. Changing either invalidates the plugin's build caches, so you do not need
27-
to clear `obj/` yourself.
26+
`noReflection`, `temporal` and `exclude` are handed to Fable.Compiler unchanged; they mean what
27+
they mean for the `dotnet fable` CLI. Changing any of them invalidates the plugin's build caches,
28+
so you do not need to clear `obj/` yourself.
2829

2930
Unknown or badly typed options are rejected when the config loads, so a misspelled one fails with
3031
a message rather than being quietly ignored:
3132

3233
```
3334
vite-plugin-fable: unknown option "noRefleciton". Did you mean "noReflection"?
34-
Known options: fsproj, jsx, noReflection, exclude, configuration, debug, fableModulesDiagnostics.
35+
Known options: fsproj, jsx, noReflection, temporal, exclude, configuration, debug, fableModulesDiagnostics.
3536
```
3637

3738
If you write your Vite config in TypeScript you get the same feedback in the editor. The plugin
@@ -59,6 +60,23 @@ takes the only signal with it: nothing is printed and `vite build` exits 0, even
5960
emitted nothing usable for that file. If a build succeeds and the app is broken in a way that
6061
points at a package, turn this on first.
6162

63+
## Temporal
64+
65+
Fable compiles `DateTime`, `DateTimeOffset`, `DateOnly`, `TimeOnly` and `TimeSpan` to JavaScript's
66+
`Date` by default, which is millisecond precise and does arithmetic across DST changes differently
67+
from .NET. Fable 5.16 can target the
68+
[Temporal API](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal)
69+
instead, which is tick precise and matches .NET's wall-clock semantics:
70+
71+
```js
72+
fable({ temporal: true });
73+
```
74+
75+
It is experimental in Fable, where it is the hidden `--test:js-temporal` flag, and the plugin only
76+
passes it through. It sets the `FABLE_COMPILER_JAVASCRIPT_TEMPORAL` define as `dotnet fable` does,
77+
so F# code can branch on it. The plugin adds no polyfill: check that the browsers and runtimes you
78+
target ship `Temporal`, and load one yourself if they do not.
79+
6280
## Seeing what the plugin is doing
6381

6482
By default the plugin prints one line per compile, plus any diagnostics and errors:

‎package.json‎

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
"sample-project"
88
],
99
"catalog": {
10-
"@fable-org/fable-library-js": "2.6.0",
10+
"@fable-org/fable-library-js": "2.7.0",
1111
"vite": "^8.2.2",
1212
"vite-plugin-inspect": "^12.0.2"
1313
}
@@ -33,12 +33,12 @@
3333
},
3434
"devDependencies": {
3535
"@nojaf/oxlint-plugin-annotate-non-primitives": "^0.1.1",
36-
"@types/bun": "^1.4.0",
36+
"@types/bun": "^1.4.2",
3737
"copy-to-clipboard": "^4.0.2",
38-
"keep-a-changelog": "^3.1.0",
38+
"keep-a-changelog": "^3.2.0",
3939
"lit": "^3.3.3",
40-
"oxfmt": "0.66.0",
41-
"oxlint": "^1.81.0",
40+
"oxfmt": "0.67.0",
41+
"oxlint": "^1.82.0",
4242
"typescript": "7.0.2"
4343
}
4444
}

‎packages/vite-plugin-fable/Fable.Daemon.Tests/DebugTests.fs‎

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ let sampleApp =
2222
Configuration = "Release"
2323
Exclude = Array.empty
2424
NoReflection = false
25+
Temporal = false
2526
}
2627

2728
let telplin =
@@ -31,6 +32,7 @@ let telplin =
3132
Configuration = "Debug"
3233
Exclude = Array.empty
3334
NoReflection = false
35+
Temporal = false
3436
}
3537

3638
let fantomasTools =
@@ -44,6 +46,7 @@ let fantomasTools =
4446
Configuration = "Debug"
4547
Exclude = Array.empty
4648
NoReflection = false
49+
Temporal = false
4750
}
4851

4952
// let ronnies =
@@ -53,6 +56,7 @@ let fantomasTools =
5356
// Configuration = "Debug"
5457
// Exclude = [| "Nojaf.Fable.React.Plugin" |]
5558
// NoReflection = true
59+
// Temporal = false
5660
// }
5761

5862
[<Test>]
@@ -147,7 +151,7 @@ let ``a failing dotnet msbuild call reports what MSBuild said`` () =
147151
let! finished = Task.WhenAny (run :> Task, Task.Delay (TimeSpan.FromMinutes 2.))
148152
Assert.That (finished, Is.SameAs (run :> Task), "dotnet msbuild never came back")
149153

150-
let error = Assert.Throws<AggregateException>(fun () -> run.Wait ())
154+
let error = Assert.Throws<AggregateException>(Action (fun () -> run.Wait ()))
151155

152156
Assert.That (
153157
error.InnerException.Message,
@@ -389,6 +393,22 @@ module CacheKeyTests =
389393
| Ok _ -> Assert.Pass ()
390394
| Error reason -> Assert.Fail $"expected the cache to be reusable, got %A{reason}"
391395

396+
[<Test>]
397+
let ``turning on temporal invalidates the cache`` () =
398+
// The option has no field in the cache; it rides on the define `dotnet fable` sets for it.
399+
Caching.writeDesignTimeBuild (mkKey [] false) emptyResponse
400+
401+
let withTemporal =
402+
{ mkKey [] false with
403+
Defines = Set.ofList [ "FABLE_COMPILER" ; "FABLE_COMPILER_JAVASCRIPT_TEMPORAL" ]
404+
}
405+
406+
match Caching.canReuseDesignTimeBuildCache withTemporal with
407+
| Error (Caching.InvalidCacheReason.DefinesMismatch (cached, current)) ->
408+
Assert.That (current, Does.Contain "FABLE_COMPILER_JAVASCRIPT_TEMPORAL")
409+
Assert.That (cached, Does.Not.Contain "FABLE_COMPILER_JAVASCRIPT_TEMPORAL")
410+
| other -> Assert.Fail $"expected a defines mismatch, got %A{other}"
411+
392412
[<Test>]
393413
let ``changing noReflection invalidates the cache`` () =
394414
Caching.writeDesignTimeBuild (mkKey [] false) emptyResponse
@@ -536,6 +556,7 @@ module DebugServerTests =
536556
FableLibrary = fableLibrary
537557
Exclude = []
538558
NoReflection = false
559+
Temporal = false
539560
SourceFiles = [| mathFs ; libraryFs |]
540561
DependentFiles = [| Path.CombineNormalize (sampleProjectDir, "App.fsproj") |]
541562
TargetFramework = Some "net10.0"

‎packages/vite-plugin-fable/Fable.Daemon/Debug.fs‎

Lines changed: 37 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -8,26 +8,16 @@ open System.Collections.Concurrent
88
open System.IO
99
open System.Net
1010
open System.Threading
11+
open System.Threading.Tasks
1112
open Suave
1213
open Suave.Filters
1314
open Suave.Operators
1415
open Suave.Successful
15-
open Suave.Logging
16-
open Suave.Sockets
17-
open Suave.Sockets.Control
1816
open Suave.WebSocket
1917
open Microsoft.Extensions.Logging
2018

2119
let defaultPort = 9014us
2220

23-
/// We can't log anything to the stdout!
24-
let zeroSuaveLogger : Logger =
25-
{ new Logger with
26-
member x.log level _ = ()
27-
member x.logWithAck _ _ = async.Zero ()
28-
member x.name = [| "vite-plugin-fable" |]
29-
}
30-
3121
/// Next to the assembly rather than `__SOURCE_DIRECTORY__`, which bakes in the path the daemon was
3222
/// compiled in. That was the same folder while the daemon was built on the machine that ran it, and
3323
/// is a stranger's home directory now that it ships prebuilt.
@@ -76,11 +66,11 @@ type InMemoryLogger() =
7666
|> Seq.skip currentCount
7767
|> HTML.mapLogEntriesToListItems
7868
|> Encoding.UTF8.GetBytes
79-
|> ByteSegment
69+
|> Memory<byte>
8070

81-
client.send Text messages true //
82-
|> Async.Ignore
83-
|> Async.RunSynchronously
71+
// Suave 3 hands back a ValueTask<Result<_, _>>; a failed send only means the client is
72+
// gone, and the read loop in `ws` drops it on its next turn.
73+
(client.send Text messages true).AsTask().GetAwaiter().GetResult() |> ignore
8474

8575
connectedClients.[client] <- entries.Count
8676

@@ -131,6 +121,7 @@ type ProjectState =
131121
FableLibrary : FullPath
132122
Exclude : string list
133123
NoReflection : bool
124+
Temporal : bool
134125
SourceFiles : FullPath array
135126
DependentFiles : FullPath array
136127
TargetFramework : string option
@@ -437,6 +428,7 @@ let projectPayload (includes : string) (project : ProjectState) =
437428
fableLibrary = project.FableLibrary
438429
exclude = project.Exclude
439430
noReflection = project.NoReflection
431+
temporal = project.Temporal
440432
targetFramework = Option.toObj project.TargetFramework
441433
outputType = Option.toObj project.OutputType
442434
sourceFiles =
@@ -714,25 +706,40 @@ let api (logger : InMemoryLogger) (port : uint16) : WebPart =
714706
)
715707
]
716708

717-
let ws (logger : InMemoryLogger) (webSocket : WebSocket) (context : HttpContext) =
718-
context.runtime.logger.info (Message.eventX $"New websocket connection")
709+
let ws
710+
(logger : InMemoryLogger)
711+
(webSocket : WebSocket)
712+
(_context : HttpContext)
713+
: ValueTask<Result<unit, Suave.Sockets.Error>>
714+
=
719715
connectedClients.TryAdd (webSocket, logger.Count) |> ignore
720716

721-
socket {
722-
let mutable loop = true
717+
let loop =
718+
task {
719+
let mutable result = Result.Ok ()
720+
let mutable loop = true
723721

724-
while loop do
725-
let! msg = webSocket.read ()
722+
while loop do
723+
let! msg = webSocket.read ()
726724

727-
match msg with
728-
| Close, _, _ ->
729-
connectedClients.TryRemove webSocket |> ignore
730-
let emptyResponse = [||] |> ByteSegment
731-
do! webSocket.send Close emptyResponse true
732-
loop <- false
725+
match msg with
726+
| Result.Ok (Close, _, _) ->
727+
connectedClients.TryRemove webSocket |> ignore
728+
let! closed = webSocket.send Close Memory<byte>.Empty true
729+
result <- closed
730+
loop <- false
733731

734-
| _ -> ()
735-
}
732+
| Result.Ok _ -> ()
733+
734+
| Result.Error err ->
735+
connectedClients.TryRemove webSocket |> ignore
736+
result <- Result.Error err
737+
loop <- false
738+
739+
return result
740+
}
741+
742+
ValueTask<Result<unit, Suave.Sockets.Error>> loop
736743

737744
let webApp (logger : InMemoryLogger) (port : uint16) : WebPart =
738745
let allLogs ctx =
@@ -816,7 +823,6 @@ let startWebserver (logger : InMemoryLogger) (port : uint16) (cancellationToken
816823
{ defaultConfig with
817824
cancellationToken = cancellationToken
818825
homeFolder = Some homeFolder
819-
logger = zeroSuaveLogger
820826
bindings = [ HttpBinding.create HTTP IPAddress.Loopback port ]
821827
}
822828

@@ -826,4 +832,4 @@ let startWebserver (logger : InMemoryLogger) (port : uint16) (cancellationToken
826832
serverEnabled <- true
827833
writeDiscoveryFile port cancellationToken
828834
let _listening, server = startWebServerAsync conf (webApp logger port)
829-
server
835+
Async.AwaitTask server

‎packages/vite-plugin-fable/Fable.Daemon/Debug.fsi‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ type ProjectState =
2323
FableLibrary : FullPath
2424
Exclude : string list
2525
NoReflection : bool
26+
Temporal : bool
2627
/// Every source file, in compilation order.
2728
SourceFiles : FullPath array
2829
/// The MSBuild inputs a change to which forces a re-crack.

‎packages/vite-plugin-fable/Fable.Daemon/Program.fs‎

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -143,14 +143,25 @@ let tryTypeCheckProject
143143
TypedArrays = false
144144
ClampByteArrays = false
145145
Language = Language.JavaScript
146-
Define = [ "FABLE_COMPILER" ; "FABLE_COMPILER_4" ; "FABLE_COMPILER_JAVASCRIPT" ]
146+
Define =
147+
[
148+
"FABLE_COMPILER"
149+
"FABLE_COMPILER_4"
150+
"FABLE_COMPILER_JAVASCRIPT"
151+
// The define is what `dotnet fable` sets alongside the flag, and it is
152+
// part of the design time build cache key, so flipping the option
153+
// invalidates the cache without a field of its own.
154+
if payload.Temporal then
155+
"FABLE_COMPILER_JAVASCRIPT_TEMPORAL"
156+
]
147157
DebugMode = false
148158
OptimizeFSharpAst = false
149159
Verbosity = Verbosity.Verbose
150160
// We keep using `.fs` for the compiled FSharp file, even though the contents will be JavaScript.
151161
FileExtension = ".fs"
152162
TriggeredByDependency = false
153163
NoReflection = payload.NoReflection
164+
JsTemporal = payload.Temporal
154165
}
155166
RunProcess = None
156167
Verbosity = Verbosity.Verbose
@@ -310,6 +321,7 @@ let private describeProject
310321
FableLibrary = payload.FableLibrary
311322
Exclude = List.ofArray payload.Exclude
312323
NoReflection = payload.NoReflection
324+
Temporal = payload.Temporal
313325
SourceFiles = result.CrackerResponse.ProjectOptions.SourceFiles
314326
DependentFiles = result.DependentFiles
315327
TargetFramework = result.CrackerResponse.TargetFramework

0 commit comments

Comments
 (0)