Recipes
There are a few things you can configure in the plugin configuration.
Plugin options
Every option the plugin accepts. All of them are optional.
Option |
Type |
Default |
What it does |
|---|---|---|---|
|
|
the single |
The entry project. See Alternative fsproj. |
|
"Debug" | "Release" |
|
MSBuild configuration. See Debug or Release. |
|
"automatic" | "transform" | null |
|
Transform JSX that Fable emitted. See Fable.Core.JSX. |
|
|
|
Passed to Fable. Skips emitting reflection info, which produces smaller output. |
|
|
|
Passed to Fable. Excludes assemblies from compilation, typically Fable plugins. |
|
|
|
Print what the plugin is doing, and start the daemon's debug server. See Seeing what the plugin is doing. |
|
|
|
Report diagnostics for files under |
noReflection and exclude are handed to Fable.Compiler unchanged; they mean what they mean for
the dotnet fable CLI. Changing either invalidates the plugin's build caches, so you do not need
to clear obj/ yourself.
Unknown or badly typed options are rejected when the config loads, so a misspelled one fails with a message rather than being quietly ignored:
vite-plugin-fable: unknown option "noRefleciton". Did you mean "noReflection"?
Known options: fsproj, jsx, noReflection, exclude, configuration, debug, fableModulesDiagnostics.
If you write your Vite config in TypeScript you get the same feedback in the editor. The plugin
exports PluginOptions and FableConfiguration for when you want to name them:
|
Diagnostics from restored packages
Fable restores the sources of the packages your project depends on into fable_modules and
compiles them along with your own files, so their warnings arrive with yours. They are about code
you did not write and cannot edit, so the plugin drops them.
fableModulesDiagnostics: true reports them again. It is a debugging aid, for when a package
itself is what looks broken:
|
The option covers errors as well as warnings. With it off, a package whose sources fail to compile
takes the only signal with it: nothing is printed and vite build exits 0, even though Fable
emitted nothing usable for that file. If a build succeeds and the app is broken in a way that
points at a package, turn this on first.
Seeing what the plugin is doing
By default the plugin prints one line per compile, plus any diagnostics and errors:
|
When that is not enough, turn on debug:
|
That adds every hook the plugin runs, every file it transforms, where it resolved fable-library,
the cracking and type-checking timings, and whatever the daemon writes to stderr. Paths stay
relative to the Vite root, so they are readable at a glance.
VITE_PLUGIN_FABLE_DEBUG=1 does the same without touching the config, and additionally starts the
daemon's own log viewer on http://localhost:9014. That is the one thing the debug option cannot
do, because the viewer runs inside the compiler process rather than the plugin.
Alternative fsproj
By default, the plugin will look for a single .fsproj file inside your Vite root, which is the project folder unless you changed it.
If you deviate from this setup you can specify the entry fsproj file:
|
Debug or Release
The plugin compiles your F# in Release for vite build and Debug for vite dev. That follows
the command, not --mode, so vite build --mode staging still compiles Release.
Override it when you need the other one — a production bundle with assertions left in, say:
|
Using React
There are a couple of ways to deal with React and JSX in Fable.
⚠️ When using the vite-plugin-fable in combination with @vitejs/plugin-react, you do want to specify the fable plugin first! ⚠️
Feliz.CompilerPlugins
If you are using Feliz.CompilerPlugins, Fable output React Classic Runtime code.
Stuff like React.createElement. You will need to tailor your @vitejs/plugin-react accordingly:
|
Note that the react plugin will only apply the fast-refresh wrapper when you specify the fs extension in the include.
Fable.Core.JSX
Fable can also produce JSX (see blog). Tell
the fable plugin to transform it, and tell @vitejs/plugin-react that .fs counts as a React
file:
|
The two options do different jobs, and it is worth knowing which is which.
The JSX transform is fable({ jsx }). It has to be the plugin that does it. Vite's
built-in vite:oxc forces lang: "js" for any id whose extension is not a JavaScript one, which
disables JSX parsing — so JSX left inside a .fs module is a parse error there, not something Vite
can pick up. That is also why there is no preserve value: with @vitejs/plugin-react in the
config the module fails to parse, and without it Vite's import analysis rejects it instead. The
plugin refuses preserve when the config loads rather than letting it fail later.
Fast Refresh is react({ include: /\.fs$/ }). It does nothing for the JSX
transform. Without it an F# component still renders, but every edit reloads the page instead of
updating in place. The plugin warns when it spots this combination, so you do not have to notice it
yourself:
|
React Compiler
@vitejs/plugin-react 6 can run the React Compiler through oxc-transform-react, and it works on
Fable's output. Install the optional oxc-transform-react package and turn it on:
|
The compiler memoizes F# components the same way it does JavaScript ones — a Component.fs picks
up react/compiler-runtime and a _c(n) cache — and Fast Refresh keeps working. It is also the
more robust setup of the two: with compiler: true the refresh transform is applied explicitly to
everything the include matches, whereas otherwise Vite only applies it to modules whose emitted
code imports react/jsx-runtime. A component that compiles to no JSX at all therefore keeps Fast
Refresh under compiler: true and loses it without.
Plain Fable.React
If you are for some reason using Fable.React without Feliz.CompilerPlugins, there is one gotcha to get fast refresh working.
Fable.React will use the old JSX output.
The @vitejs/plugin-react needs to respect that in the configuration:
|
However, this is not enough for the fast refresh wrapper to be added.
⚠️ The React plugin will specifically look for a import React from "react" statement.
module Component
open Fable.React
open Fable.React.Props
// Super important for fast refresh to work in "classic" mode.
// The [<ReactComponent>] attribute from Feliz.CompilerPlugin will add for you.
// But here, we are not using that and we need to add this ourselves.
Fable.Core.JsInterop.emitJsStatement () "import React from \"react\""
let App () =
let counterHook = Hooks.useState(0)
div [] [
h1 [] [ str "Hey you!" ]
p [] [
ofInt counterHook.current
]
button [ OnClick (fun _ -> counterHook.update(fun c -> c + 1))] [
str "Increase"
]
]
vite-plugin-fable