0.4.0 — Adoption of `ToolUp.Elmish.*` packages (consumer-side sweep)
0.4.0 — Adoption of ToolUp.Elmish.* packages (consumer-side sweep)
0.4.3 update — Elmish runtime folded into
ToolUp.Platform.Client. The three published packagesToolUp.Elmish,ToolUp.Elmish.React, andToolUp.Elmish.HMRhave been merged intoToolUp.Platform.Client. Thenamespace Elmishis preserved — everyopen Elmish,open Elmish.React, andopen Elmish.HMRcall site continues to compile unchanged — but the three separatePackageReferences +PackageVersionentries are gone. Consumers already onToolUp.Elmish.*0.4.0–0.4.2 update by dropping those PackageReference / PackageVersion lines and verifying they still take a PackageReference onToolUp.Platform.Client; the runtime arrives transitively. Consumers still onFable.Elmish.*skip the intermediate ToolUp.Elmish.* hop and follow the diff in § Diff to apply (per consumer) — 0.4.3 path below. The rationale below explains the 0.4.0 swap; the 0.4.3 update is purely structural — the runtime stays the same.
toolup-forge 0.4.0 swaps the upstream Elmish family for the forked ToolUp.Elmish.* family. The runtime / wire shape is preserved unchanged from upstream Fable.Elmish v5.x — existing open Elmish call sites compile against the new packages with no F# source edits.
What changes
Three <PackageReference> entries:
- <PackageReference Include="Fable.Elmish" />
- <PackageReference Include="Fable.Elmish.React" />
- <PackageReference Include="Fable.Elmish.HMR" />
+ <PackageReference Include="ToolUp.Elmish" />
+ <PackageReference Include="ToolUp.Elmish.React" />
+ <PackageReference Include="ToolUp.Elmish.HMR" />
And three corresponding <PackageVersion> entries in the consumer's Directory.Packages.props (point to 0.4.0).
Why
Upstream Elmish is a healthy 8-year-old project, but the ToolUp surface has accumulated patterns that the upstream record doesn't carry:
- Every multi-module Elmish shell re-invents the
let mutable shellDispatch : (Msg -> unit) option = None+Cmd.ofEffect (fun d -> shellDispatch <- Some d)capture pattern.ToolUp.ElmishshipsIDispatcher<'msg>+Program.withDispatcherHandleso the dance is captured in one primitive — andIDispatcher.IsActiveflips tofalseon termination so background callbacks no-op on hot-reload rather than dispatching against a torn-down loop. - Boot-time multi-source prefetch gating ("load Configs in parallel with Flags, fire
ReinitActiveModulewhen the last one resolves") needs ad-hocIsConfigsPending/IsFlagsPendingbookkeeping fields.Prefetch<'a>+Prefetch.onAllReadycodify the pattern. - The upstream
(string * exn) -> unitonErrorshape loses every piece of context that matters — which phase raised, which module, which correlation id.ErrorContext+Program.withErrorReportergive the reporter all of it. - Background subscriptions (SSE, notifications, navigation requests) wired via
Cmd.ofEffectare never torn down — they leak across hot reloads.EffectHandle<'msg>+Program.withEffectcarry an explicitLifetime(Program/Module of moduleId/Manual) so the runtime + HMR can dispose them cleanly. - The dominant RPC pattern (
Cmd.OfAsync.either api.Method arg OkMsg ErrMsg) deserves an intent-named name.Cmd.OfRemoting.{call, callWithRetry}provides it, with aRetryPolicyknob for transient transport failures. - The unused-by-ToolUp surface (
Cmd.OfFunc,Cmd.OfPromise,Cmd.OfTask,Cmd.OfValueTask,Cmd.OfAsyncWith,Cmd.OfAsyncImmediate, WebSharper paths,cmd.obsolete.fsv3.x shims) is dropped. The migration cost is zero — no observed call sites across forge or any downstream consumer in our audit set.
Diff to apply (per consumer)
Step 1 — Directory.Packages.props
- <PackageVersion Include="Fable.Elmish" Version="5.0.2" />
- <PackageVersion Include="Fable.Elmish.HMR" Version="9.0" />
- <PackageVersion Include="Fable.Elmish.React" Version="5.6" />
+ <PackageVersion Include="ToolUp.Elmish" Version="0.4.0" />
+ <PackageVersion Include="ToolUp.Elmish.HMR" Version="0.4.0" />
+ <PackageVersion Include="ToolUp.Elmish.React" Version="0.4.0" />
Step 2 — every .fsproj referencing the upstream packages
- <PackageReference Include="Fable.Elmish" />
- <PackageReference Include="Fable.Elmish.HMR" />
- <PackageReference Include="Fable.Elmish.React" />
+ <PackageReference Include="ToolUp.Elmish" />
+ <PackageReference Include="ToolUp.Elmish.HMR" />
+ <PackageReference Include="ToolUp.Elmish.React" />
Step 3 — source code
No source edits required for the swap itself. open Elmish, Cmd.batch, Cmd.OfAsync.either, Program.mkProgram, Program.withReactSynchronous, Program.withSubscription, Cmd.map, Cmd.ofMsg — all unchanged.
Compile errors that would indicate non-trivial migration work:
error FS0039: ... 'OfFunc' is not defined→ replaceCmd.OfFunc.either f arg ok errwithCmd.ofEffect (fun dispatch -> try dispatch (ok (f arg)) with ex -> dispatch (err ex)).Cmd.OfFunc.performandCmd.OfFunc.attemptmap to the same shape minus theok/errbranch.error FS0039: ... 'OfPromise' is not defined→Cmd.OfAsync.*viaAsync.AwaitPromise.error FS0039: ... 'OfTask' is not defined→Cmd.OfAsync.*viaAsync.AwaitTask.error FS0039: ... 'OfValueTask' is not defined→ asOfTask.error FS0039: ... 'OfAsyncWith' / 'OfAsyncImmediate' is not defined→ drop thestartparameter; the defaultAsync.Start(Fable:Async.StartImmediate) is correct for every observed call site.warning FS0044: ... 'withConsoleTrace' is deprecated→ ignore for now; the function still works as a compat shim. For new code useProgram.withErrorReporter+ a per-update interceptor.
Verification
dotnet build <consumer>.sln # must succeed clean
# Fable side (catches issues `dotnet build` misses):
cd <client-project> && dotnet fable -o output --noCache
Both pass clean against forge's samples/MinimalClient after the swap; the swap demonstrably preserves runtime-shape compatibility.
Optional follow-up — adopt the new primitives
The swap is the load-bearing step. Adopting the new primitives is incremental and each migration is small. forge has worked one example already (the IDispatcher adoption in src/ToolUp.Platform.Client/Client/SDK.Client.fs); the others are documented as forge follow-up phases:
IDispatcheradoption — deletelet mutable shellDispatch; install viaProgram.withDispatcherHandleat the run point; useIDispatcher.IsActivein background callbacks. Landed in forge SDK 0.4.0 (SDK.Client.fs:376,462,605,2014).Prefetchadoption — replaceIsConfigsPending/IsFlagsPendingbookkeeping withPrefetch<Map<...>>typed fields; usePrefetch.onAllReadyin theConfigsLoaded/FlagsLoadedhandlers. Forge follow-up phase —SDK.Client.fs:487-505(boot) +:772-808(team switch).- Structured
ErrorContext— wireProgram.withErrorReporter; populateModuleIdviaErrorContext.withModulein the per-module boundary. Forge follow-up phase —ClientConfig.OnError+Components.ModuleBoundary.wrap. EffectHandlelifetimes — convert SSE / Notifications / NavigationRequest fromCmd.ofEffect (fun d -> ...)toProgram.withEffect (EffectHandle.programLifetime "<id>" (fun d -> ...)). Forge follow-up phase —SDK.Client.fs:619-643.Cmd.OfRemoting.call— sweepCmd.OfAsync.either api.Method→Cmd.OfRemoting.call api.Method. Pure rename for the common case; opens the door to per-call retry policies and tracing interceptor recognition. Forge follow-up phase — every module's API call sites.
Each follow-up is bounded and can ship as its own incremental commit; the SDK.Client.fs build remains green between them. Downstream consumer apps should sequence their adoption similarly: swap first, then adopt primitives one at a time.
Rollback
The package swap is reversible by reverting the <PackageReference> and <PackageVersion> edits — all consumer code that calls the upstream-shape API continues to work against upstream Fable.Elmish. Any code that has adopted the new primitives (IDispatcher, Prefetch, structured ErrorContext, EffectHandle, Cmd.OfRemoting) needs to be reverted to the pre-adoption shape, since those primitives don't exist in upstream Elmish.
0.4.3 path (consumers on Fable.Elmish.* still)
A consumer on upstream Fable.Elmish.* who has not yet adopted the 0.4.0 swap can skip the intermediate ToolUp.Elmish.* hop and jump straight to 0.4.3. The end state is the same: open Elmish continues to compile, the runtime ships as part of ToolUp.Platform.Client, and the new primitives (IDispatcher, Prefetch, structured ErrorContext, EffectHandle, Cmd.OfRemoting) are available.
Step 1 — Directory.Packages.props
Drop the three upstream PackageVersion entries; do NOT add ToolUp.Elmish.* entries:
- <PackageVersion Include="Fable.Elmish" Version="5.0.2" />
- <PackageVersion Include="Fable.Elmish.HMR" Version="9.0" />
- <PackageVersion Include="Fable.Elmish.React" Version="5.6" />
Step 2 — every .fsproj referencing the upstream packages
Drop the upstream PackageReferences. Verify the project has a PackageReference (or ProjectReference) on ToolUp.Platform.Client — that's where the elmish runtime arrives from in 0.4.3:
- <PackageReference Include="Fable.Elmish" />
- <PackageReference Include="Fable.Elmish.HMR" />
- <PackageReference Include="Fable.Elmish.React" />
<PackageReference Include="ToolUp.Platform.Client" /> <!-- existing, unchanged -->
If a project depended on Fable.Elmish.* for the Elmish runtime but did NOT depend on ToolUp.Platform.Client, that's a structural smell — the elmish runtime is a forge implementation detail now, not a standalone library. Either add a PackageReference on ToolUp.Platform.Client, or extract the relevant Elmish source into the project directly. There are no remaining published ToolUp.Elmish.* packages to take a direct PackageReference on.
Step 3 — source code
Same as the 0.4.0 swap: no source edits required. open Elmish, open Elmish.React, open Elmish.HMR, Cmd.*, Program.*, all unchanged. The compile-time error signals listed in the 0.4.0 Step 3 above all still apply — they indicate non-trivial migration work from APIs upstream Fable.Elmish supported that this fork dropped (Cmd.OfFunc, Cmd.OfPromise, Cmd.OfTask, Cmd.OfValueTask, Cmd.OfAsyncWith, Cmd.OfAsyncImmediate).
Step 4 — verify
dotnet build <consumer>.sln # must succeed clean
# Fable side:
cd <client-project> && dotnet fable -o output --noCache
Same verification cadence as the 0.4.0 swap.
0.4.3 consumers already on ToolUp.Elmish.* 0.4.0–0.4.2
Drop the three ToolUp.Elmish.* PackageReference + PackageVersion entries. Confirm a PackageReference on ToolUp.Platform.Client exists. Build. The end state is identical to the 0.4.3 path above.
- <PackageVersion Include="ToolUp.Elmish" Version="0.4.2" />
- <PackageVersion Include="ToolUp.Elmish.HMR" Version="0.4.2" />
- <PackageVersion Include="ToolUp.Elmish.React" Version="0.4.2" />
- <PackageReference Include="ToolUp.Elmish" />
- <PackageReference Include="ToolUp.Elmish.HMR" />
- <PackageReference Include="ToolUp.Elmish.React" />
No source-code changes are required.