Skip to content

Migrate to .NET 10 - #147

Merged
0101 merged 3 commits into
mainfrom
net10
Jul 27, 2026
Merged

0101 merged 3 commits into
mainfrom
net10

Conversation

@0101

@0101 0101 commented Jul 27, 2026

Copy link
Copy Markdown
Owner

Problem

Treemon was pinned to net9.0 across all five projects. The spec recorded why:

net9.0 (not net10.0): Fable 4.28.0 FCS hangs with .NET 10 preview SDK

That blocker is real and reproducible — but the fix is not a simple TFM bump. Moving the client to net10.0 walks into a chain of three separate toolchain incompatibilities, each of which silently breaks something different.

Changes

Baseline

  • All five projects (Shared, Client, Server, Cli, Tests) -> net10.0
  • global.json -> SDK 10.0.110, plus allowPrerelease: false. With rollForward: latestMajor and the default allowPrerelease: true, dotnet silently selected an installed 10.0.400-preview SDK — exactly the preview SDK the original spec note blamed.
  • .github/workflows/ci.yml -> dotnet-version: 10.0.x
  • Microsoft.Data.Sqlite 9.0.11 -> 10.0.10

The Fable version squeeze

There is exactly one viable Fable release. Each candidate was verified empirically, not assumed:

Version Outcome
4.28.0 / 4.29.0 Deadlocks when the compiled project targets net10.0. Confirmed locally: the Fable process burned ~5s CPU over 10 minutes at a flat 59 MB — a hang, not slow compilation. Upstream fable-compiler/Fable#4326, closed with "use Fable 5". No fix will land on fable4.
5.1.0+ Fable#4529 made option_type() carry None/Some union cases, so FSharpType.IsUnion(typeof<'a option>) now returns true. Fable.SimpleJson matches its UnionType pattern before OptionType, so every option field in a remoting response fails to deserialize. Upstream Fable.Remoting#394 (open); the two-line fix is Fable.SimpleJson#111 (open, unmerged).
5.5.0+ Does the same to list_type (Fable#4698), and adds a hard error for inline functions referencing private values (Fable#4701) that rejects Fable.Remoting.MsgPack's write64bitNumber. Tracked as Fable.Remoting#396 (open, no maintainer response).
5.0.0 Predates all three. Stock Fable.Remoting works with no vendored patches.

So dotnet-tools.json pins fable to 5.0.0 (rollForward: false was already set). The reasoning is recorded in docs/spec/worktree-monitor.md with a revisit note.

This is the failure mode that mattered most: with Fable 5.4.0 the solution built, Fable compiled, Vite bundled, and the server started — but the dashboard rendered only skeleton placeholders. The API returned HTTP 200 with valid JSON, and there were zero console errors, zero page errors, and zero unhandled rejections. The exception was being swallowed into Elmish's DataFailed arm.

Feliz 3 migration

Fable 5 requires Feliz 3 — the Feliz 2.x compiler plugin targets the Fable 4 AST and throws Method not found: Fable.AST.Fable.Ident..ctor(...). Feliz 2.* -> 3.*, with the API changes it forces:

  • React.fragment -> React.Fragment (App.fs, CanvasPane.fs, CardViews.fs, Components.fs, OverviewBand.fs)
  • Interop.createElement -> HtmlHelper.createElement (CardViews.fs)
  • React.createDisposable removed -> plain unit -> unit cleanup lambdas (Components.fs, OverviewChart.fs). This also resolved a useEffect overload ambiguity. Note ignore does not typecheck here (it is 'a -> unit), hence fun () -> ().
  • svg.viewBox now infers the string overload rather than the (int, int, int, int) tuple one, so the icon helper in CardViews.fs takes an annotated viewBox: string and call sites pass "0 0 24 24" / "0 0 18 18".

Housekeeping

  • .gitignore: *.fable-temp.csproj (generated by Fable on every run)
  • AGENTS.md and review/rules/modern-fsharp-syntax.md: the "Fable 4.28 supports this syntax" notes now read "Fable 5.0"

Tests

dotnet build treemon.slnx — succeeds, 0 errors. dotnet fable and npm run build both succeed (122 modules, 453 kB bundle).

Suite Result
Full (dotnet test) 2011 passed, 1 failed, 10 skipped (2022 total, 10m13s)
Category=Fast 1671 passed, 0 failed
Category=E2E 446 passed, 1 failed, 1 skipped (448 total, 5m43s)

The single failure is warm 250-path summary appears within one second (DiffViewerTests.fs), a server-side perf assertion with a 1000 ms budget. It is pre-existing, not a regression — I stashed this branch and measured unmodified main, which fails the same assertion worse:

responseMs / displayMs
main (net9, Fable 4.28) 1073/1141, 1728/1837, 1618/1686
this branch (net10, Fable 5.0) 1183/1246, 1127/1196, 1163/1232

.NET 10 is in fact slightly faster on this path. The budget is simply too tight for this machine, so I left the threshold alone rather than silently relaxing a perf guard.

Runtime behaviour was verified separately with a Playwright probe against a server + Vite pair on isolated ports (60777/60778/60779) — the dashboard renders live data (11 cards, 11 branch names) with no console errors. Production and port 5000 were never touched.

Notes

  • NU1903 for SQLitePCLRaw.lib.e_sqlite3 2.1.11 appears in the build. It is pre-existing: the advisory range is <= 2.1.11, and Microsoft.Data.Sqlite 9.0.11 already pulled the equally-affected 2.1.10. Left alone as unrelated scope.
  • Fable should be un-pinned once Fable.SimpleJson ships the option/list ordering fix and Fable.Remoting addresses the inline private error. Both upstream issues are linked above and in the spec.

Move all projects to net10.0, SDK 10.0.110, and CI to 10.0.x.

The client toolchain gates this. Fable 4.x deadlocks when the compiled
project targets net10.0, so the client needs Fable 5, which in turn needs
Feliz 3 (the Feliz 2 compiler plugin targets the Fable 4 AST). Every later
Fable 5 release breaks the client in a different way: 5.1.0 made F#
reflection report `option` as a union, so Fable.SimpleJson classifies it
before its option case and every `option` field in a remoting response
fails to deserialize; 5.5.0 does the same for `list` and additionally
rejects Fable.Remoting.MsgPack's `inline private` helpers. Fable is
therefore pinned to 5.0.0, which predates all three.

global.json gains allowPrerelease: false so rollForward cannot silently
select an installed 10.0.400-preview SDK.

Feliz 3 API migration: React.fragment -> React.Fragment,
Interop.createElement -> HtmlHelper.createElement, React.createDisposable
-> plain cleanup lambdas, and svg.viewBox as a string (the tuple overload
is no longer inferred).

Copilot-Session: d46c75a1-198e-414a-b712-9d90152a7aea
Copilot AI review requested due to automatic review settings July 27, 2026 11:16

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Migrates Treemon to .NET 10 and the compatible Fable/Feliz toolchain.

Changes:

  • Targets .NET 10 across all projects and CI.
  • Pins Fable 5.0.0, upgrades Feliz 3, and updates affected client APIs.
  • Updates SQLite, documentation, and generated-file exclusions.
Show a summary per file
File Description
.github/workflows/ci.yml Uses .NET 10 in CI.
.gitignore Ignores Fable temporary projects.
AGENTS.md Updates Fable compatibility guidance.
docs/spec/worktree-monitor.md Documents the toolchain constraints.
dotnet-tools.json Pins Fable 5.0.0.
global.json Selects a stable .NET 10 SDK.
review/rules/modern-fsharp-syntax.md Updates Fable syntax guidance.
src/Cli/Cli.fsproj Targets .NET 10.
src/Client/App.fs Migrates fragment API.
src/Client/CanvasPane.fs Migrates fragment API.
src/Client/CardViews.fs Migrates element, fragment, and SVG APIs.
src/Client/Client.fsproj Targets .NET 10 and Feliz 3.
src/Client/Components.fs Migrates fragments and effect cleanup.
src/Client/OverviewBand.fs Migrates fragment API.
src/Client/OverviewChart.fs Migrates effect cleanup.
src/Server/Server.fsproj Targets .NET 10 and updates SQLite.
src/Shared/Shared.fsproj Targets .NET 10.
src/Tests/Tests.fsproj Runs tests on .NET 10.

Review details

  • Files reviewed: 17/18 changed files
  • Comments generated: 1
  • Review effort level: Medium

Comment thread src/Client/Client.fsproj
0101 added 2 commits July 27, 2026 13:23
Feliz 3 removes React.createDisposable, so the mandatory hook example in
the elmish-mvu-discipline rule no longer compiles against this project.
Return a plain unit -> unit cleanup instead, matching the shape used by
useFrameCoalescedHover in OverviewChart.fs.

Copilot-Session: d46c75a1-198e-414a-b712-9d90152a7aea
@0101
0101 enabled auto-merge (squash) July 27, 2026 11:49
@0101
0101 merged commit d2ad98b into main Jul 27, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants