Dependency Trees With Fable and Sutil


Motivation ¶

One of my colleagues has recently mentioned they can hardly follow the dependency tree of YAML-configured systems like kustomize or GitLab CI. Both mentioned technologies enable configuration reuse via composition and inheritance. Obviously the reuse comes with an increased complexity which may be overwhelming for a newcomer. To lower the learning curve I wanted to create a tool that would allow an ad-hoc tree traversal and visualisation. I am excited as it requires reaching beyond my comfort zone. Straight to the realm of front-end.

The plan ¶

FROM THE FUTURE: It has actually evolved into a one-person weekend mini-hackathon


After a short research I decided I’d use a typical web front-end tech (obviously with a twist). That way I can easily embed this later into a VS Code extension (via the WebView API). Being an F# fan I wanted to give Fable a go. I have never used it so it should be fun. Also I decided I’d write as I go - the thing I’d like to do more and turn it into a habit. As for the tree rendering itself I thought I could use Vue.js and vnodes sutil.

Getting started ¶

Let’s first make a new public GitHub repo and find some Fable template to start with. I named the repo yplainer being a yaml explainer. Ok so that was the easy part. Now, the typical problem of a newcomer to the front-end world is which tools I should pick (from the plethora available out there).

  • Yarn - package manager
  • Vitejs - build + dev server
  • Fable - because I want to do it in F#
  • Elmish - an MVU framework
  • Vue.js - to build the UI

FROM THE FUTURE: replacing Vue.js by sutil

I only have a vague idea of how it will fit together so it all may change as we go. Ok, I’ve just learnt about Femto and it looks like it makes sense to use it.

FROM THE FUTURE: This step is redundant as Femto is already a part of the template I use below


I want to install it as local dotnet tool so:

git clone git@github.com:queil/yplainr.git
cd yplainer && mkdir src && cd src
dotnet new tool-manifest
dotnet tool install femto

FROM THE FUTURE: Follow Fable’s getting started


Now it’s the time to generate some scaffolding. To do that I’ll install the fable template and execute it:

dotnet new --install Fable.Template
dotnet new fable --force # adding force as I already have some files in my repo

This is how my tree looks like now:

.
├── LICENSE
├── Nuget.Config
├── README.md
├── package-lock.json
├── package.json
├── public
│   ├── fable.ico
│   └── index.html
├── src
│   ├── App.fs
│   └── App.fsproj
└── webpack.config.js

I can already see it uses npm and webpack. I do not want it so I’ll try to get it replaced by yarn and vite respectively. Also I should’ve held my horses with femto - it is already included in the fable template.

Why would you?


BTW I already have Yarn installed.

The following steps will make you npm-free.

rm package-lock.json
rm -fr node_modules
yarn
yarn set version latest

Added the following to .gitignore as per this answer.

.yarn/*
!.yarn/patches
!.yarn/releases
!.yarn/plugins
!.yarn/sdks
!.yarn/versions
.pnp.*

Run yarn again and it will do some updates.

Hopefully, we’re good to move to the next stage. In the meantime I tried to run it according to the Fable’s guide but it doesn’t seem to work:

npm start

> start
> dotnet fable watch src --run webpack-dev-server

Fable: F# to JS compiler 3.2.9
Thanks to the contributor! @zanaptak
src> dotnet restore App.fsproj
  Determining projects to restore...
  Restored /home/kris/github/yplainr/src/App.fsproj (in 3.33 sec).
Parsing src/App.fsproj...
Initializing F# compiler...
Compiling src/App.fsproj...
F# compilation finished in 3039ms
Fable compilation finished in 399ms
.> webpack-dev-server
Cannot run: No such file or directory
Watching src

Well, no surprise here - I am far away from the happy path.

I am back after a few hours. It’s time to replace webpack with allegedly blazingly-fast Vitejs. This time I’ll follow the guide.

yarn create vite

Well, actually this is not what I am after as I have my project already. Let’s try webpack-to-vite then:

npx @originjs/webpack-to-vite .

It seems to have worked:

******************* Webpack to Vite *******************
Project path: .
conversion items successful converted:
╔════════╤═══════════════════════════╤══════════════════╗
║ Number │ Conversion item           │ Conversion count ║
╟────────┼───────────────────────────┼──────────────────╢
║  V06   │ client-side env variables │        1         ║
║  B01   │ add package.json          │        1         ║
║  B04   │ required plugins          │        1         ║
║  V01   │ base public path          │        1         ║
║  V02   │ css options               │        1         ║
║  V03   │ build options             │        1         ║
║  V05   │ resolve.alias options     │        1         ║
║  B03   │ add vite.config.js        │        1         ║
║  B02   │ add index.html            │        1         ║
╚════════╧═══════════════════════════╧══════════════════╝

Conversion finished in 86ms.
The report output path is ./conversion.log
************************ Done ! ************************
Now please run:

yarn
yarn serve-vite

Then I follow the above instructions and I’ve got something partially working. Although, in Chrome Dev Tools I am seeing the site cannot load http://localhost:3000/bundle.js returning 404.

It seems it is probably a good time for integrating it with Fable better.

I’ve finally got it working. Luckily, I have found this excellent work of aaronpowell. Thanks!

yarn start now gives me a working website on http://localhost:3000/.

So now the time has come to integrate my solution with Vuejs. I’ll start by looking into this template by alfonsogarciacaro.

Given I wanted to use Elmish too, I am now looking at this repo by ed-ilyin. Let’s try to integrate it into the project.

It feels like all the above things got abandoned. So let’s move out from this dead end. It looks like the cool guys in the F# front-end area have gone sutil. Therefore, I am changing my initial decision of using Vuejs.

That wasn’t too difficult to be fair - PR. I’ve got a working counter app which is great.

Now, having finished my crash course in F# front-end apps, it’s the time to actually implement some code. I’ll try to combine this D3 tree example D3 Hierarchy (docs) and Sutil’s bar chart example.

In the meantime I realised my production build is broken because of this seemingly unimportant difference.

After pasting the bar chart example into App.fs I started getting the following error:

[vite]: Rollup failed to resolve import "d3-scale" from "src/App.fs.js".
This is most likely unintended because it can break your application at runtime.
If you do want to externalize this module explicitly add it to
`build.rollupOptions.external`

I solved it by adding the following to vite.config.js (under build):

rolloutOptions: {
  external: [
    'd3-scale'
  ]
}

Voila! I’ve got the bar chart example up and running.

yarn add d3-hierarchy

I’ve quickly realised that to use an external library I need Fable bindings for it. So there is the ts2fable tool one can generate it with. I have created some scripts and generated bindings for d3-hierarchy here. The next step is to use it to render some tree.

Resuming the project a week later. To use a generated Fable binding we need to add an import directive like (more details):

[<ImportAll("d3-hierarchy")>]
let D3Hierarchy : D3Hierarchy.IExports = jsNative

Then it is possible to invoke it like:

let layout = D3Hierarchy.tree()

I realised the D3Hierarchy is not necessarily compatible with IObservable model used by Sutil. Another realisation is that I likely want my tree display reactive. I.e. whenever a change is made to the observed directory I’d like it to be reflected in the display (essentially a hot-reload like functionality). To do that I’d need a backend component be able to monitor file system. It can push changes to the frontend over web-sockets (e.g. Fable.SignalR). It should be trivial to make an IObservable and pass ti to the Bind.el Sutil function.

I’ve decided to give Saturn a go. Before I can generate a scaffolding from template I need to restructure my directory structure. I.e. move the front-end code to the src/client dir so then I can put the backend code in src/server.

dotnet new -i Saturn.Template
dotnet new Saturn --name Yplainr --output src/server

The Saturn template assumes we create a project from scratch so I need to adjust the output to my needs. I am removing Paket, Fake, and migrations. NuGet works just fine for my uncomplicated needs and the build process is also trivial so I do not need those tools in this particular case. I’ve got it building. The next step is to make the server serve the client SPA.

TBC - watch this space