Introducing Femto
In this blog post, we will introduce a new tool called Femto that will hopefully make the lives of Fable users a lot easier when it comes to the npm packages they depend upon. First things first: let's go through the problem context to understand what Femto is trying to solve.
Fable Packages
As you all know, Fable packages are normal dotnet packages that are distributed to the nuget package registry. A special type of these nuget packages are Fable bindings which provide idiomatic and type-safe wrappers around native Javascript APIs. These native Javascript APIs can be either available directly in the environment where your compiled F# code is running (Web APIs in browsers, system modules in node.js), or these APIs can be those of third-party Javascript libraries distributed to the npm registry and this is where things get complicated.
In order to use a Fable binding for a third-party Javascript library you as a user have to install both libraries in your project:
- The Fable binding from nuget
- The actual Javascript third-party library from npm
For example when you install Fable.React
in your Fable project you also have to install react
and react-dom
into your npm dependencies. Users usually have to look up the the exact versions of the npm package they need from the documentation page of the Fable binding itself, in case of Fable.React
you need to install react@16.8.0
and react-dom@16.8.0
.
This is of course doable if you only have a couple of packages with direct npm dependencies such as Fable.React
but the problem gets even more complicated when you install a nuget package that itself depends on a Fable binding such as Fable.Elmish.React
which depends both on Fable.Elmish
and Fable.React
:
Fable.Elmish.React (nuget)
|
| -- Fable.Elmish (nuget)
| -- Fable.React (nuget)
|
| -- react@16.8.0 (npm)
| -- react-dom@16.8.0 (npm)
Here, when you install Fable.Elmish.React
, you also install Fable.React
because it is a transitive dependency. This means now you have to install react
and react-dom
yourself because you installed Fable.React
.
This is something you just have to know and applies to every Fable binding you use. Wouldn't it be nice if you didn't have to do this manually, having a tool that does this for you correctly and automatically? Here comes Femto into play.
What is Femto
Femto is a dotnet CLI tool that primarily does two things:
Project dependency analysis: going through all your project's dependencies, the direct and the transitive, extracting information about which npm dependencies are needed, which versions they should have and how they should be installed (dependency vs development dependency). Next this information is compared against what you already have installed to decide whether a package is missing or if a version of an installed npm package has an invalid version (falls outside the required version constraint specified by the Fable binding). Femto will log the commands that are required for full package resolution.
Automatic package resolution: Logging the actions required for package resolution is nice, you can manually execute them one by one and have all the packages you need in the right place. However why do it manually when Femto can do it for you? Run Femto with the
--resolve
flag and let it do the magic. Femto finds versions that satisfy the version requirements as specified by the author of the Fable binding (see below). Femto even solves resolution conflicts when multiple libraries require the same package using different version requirements. In that case Femto will find a version that satisfies all of the constraints when possible, if Femto cannot find a proper version that you are using incompatible libraries and Femto will log which library is specifying a version constraint that cannot be resolved.
Today, Femto is released as beta and we would love to hear from you what you think about it, ready to take it for a spin?
Installing and using Femto
Install Femto as a global dotnet tool as follows:
dotnet tool install femto --global
Alternatively, you can install it locally into your projects directory:
dotnet tool install femto --tool-path ./femto
then you can cd
your way to where you have your Fable project and run project analysis:
# navigate to your project directory
cd fable-minimal
cd src
# run femto for project analysis
femto
You can also specify the directory of your Fable project or specify the project path itself, the following will find the same project.
femto .
femto ./src
femto ./src/App.fsproj
The default command of femto
will only run project analysis against the project in subject and logs the required actions for package resolution, for example if you install these packages in your project:
dotnet add package Fable.DateFunctions
dotnet add package Elmish.SweetAlert
dotnet add package Elmish.AnimatedTree
then run femto
in the project directory, you get the following logs:
[20:13:28 INF] Analyzing project C:/projects/elmish-getting-started/src/App.fsproj
[20:13:34 INF] Found package.json in C:\projects\elmish-getting-started
[20:13:36 INF] Using npm for package management
[20:13:43 INF] Fable.DateFunctions requires npm package date-fns
[20:13:43 INF] | -- Required range >= 1.30 < 2.0 found in project file
[20:13:43 INF] | -- Missing date-fns in package.json
[20:13:43 INF] | -- Resolve manually using 'npm install date-fns@1.30.1 --save'
[20:13:43 INF] Elmish.SweetAlert requires npm package sweetalert2
[20:13:43 INF] | -- Required range >= 8.5 found in project file
[20:13:43 INF] | -- Missing sweetalert2 in package.json
[20:13:43 INF] | -- Resolve manually using 'npm install sweetalert2@8.5.0 --save'
[20:13:43 INF] Elmish.AnimatedTree requires npm package react-spring
[20:13:43 INF] | -- Required range >= 8.0.0 found in project file
[20:13:43 INF] | -- Missing react-spring in package.json
[20:13:43 INF] | -- Resolve manually using 'npm install react-spring@8.0.1 --save'
Notice how Femto decided to use npm
for package management. In case I had the yarn.lock
file next to package.json
then Femto will use yarn
instead and the install commands would be yarn specific:
npm install sweetalert2@8.5.0 --save
# becomes
yarn add sweetalert2@8.5.0
Now you can either run the commands that Femto logged or let it do the work simply as follows:
femto --resolve
This logs the following:
[20:15:39 INF] Analyzing project C:\projects\elmish-getting-started\src\App.fsproj
[20:15:42 INF] Found package.json in C:\projects\elmish-getting-started
[20:15:42 INF] Using npm for package management
[20:15:47 INF] Executing required actions for package resolution
[20:15:47 INF] Installing dependencies [date-fns@1.30.1, sweetalert2@8.5.0, react-spring@8.0.1]
[20:15:55 INF] √ Package resolution complete
That's it!
You can try to confuse Femto by installing an old version of a package, for example install date-fns@1.0.0
in the project and re-run the dependency analysis, you will get this:
[20:22:06 INF] Analyzing project C:/projects/elmish-getting-started/src/App.fsproj
[20:22:09 INF] Found package.json in C:\projects\elmish-getting-started
[20:22:09 INF] Using npm for package management
[20:22:11 INF] Elmish.SweetAlert requires npm package sweetalert2
[20:22:11 INF] | -- Required range >= 8.5 found in project file
[20:22:11 INF] | -- Used range ^8.5.0 in package.json
[20:22:11 INF] | -- √ Installed version 8.5.0 satisfies required range >= 8.5
[20:22:11 INF] Elmish.AnimatedTree requires npm package react-spring
[20:22:11 INF] | -- Required range >= 8.0.0 found in project file
[20:22:11 INF] | -- Used range ^8.0.1 in package.json
[20:22:11 INF] | -- √ Installed version 8.0.1 satisfies required range >= 8.0.0
[20:22:11 INF] Fable.DateFunctions requires npm package date-fns
[20:22:11 INF] | -- Required range >= 1.30 < 2.0 found in project file
[20:22:11 INF] | -- Used range ^1.0.0 in package.json
[20:22:11 INF] | -- Found installed version 1.0.0
[20:22:11 INF] | -- Installed version 1.0.0 does not satisfy [>= 1.30 < 2.0]
[20:22:11 INF] | -- Resolve manually using 'npm uninstall date-fns' then 'npm install date-fns@1.30.1 --save'
Notice how it tells you need to uninstall the current version 1.0.0
and re-install a version that specifies the required version constraint >= 1.30 < 2.0
. Automatic package resolution understands this as well and is able to solve it correctly.
Another fun attempt to experiment with is installing a required package with a proper version but installed into "devDependencies" instead of "dependencies" in package.json, this too can be detected by Femto and correctly resolved:
[20:28:01 INF] Analyzing project C:\projects\elmish-getting-started\src\App.fsproj
[20:28:04 INF] Found package.json in C:\projects\elmish-getting-started
[20:28:04 INF] Using npm for package management
[20:28:04 INF] Elmish.SweetAlert requires npm package sweetalert2
[20:28:04 INF] | -- Required range >= 8.5 found in project file
[20:28:04 INF] | -- Used range ^8.5.0 in package.json
[20:28:04 INF] | -- √ Installed version 8.5.0 satisfies required range >= 8.5
[20:28:04 INF] Elmish.AnimatedTree requires npm package react-spring
[20:28:04 INF] | -- Required range >= 8.0.0 found in project file
[20:28:04 INF] | -- Used range ^8.0.1 in package.json
[20:28:04 INF] | -- √ Installed version 8.0.1 satisfies required range >= 8.0.0
[20:28:04 INF] Fable.DateFunctions requires npm package date-fns
[20:28:04 INF] | -- Required range >= 1.30 < 2.0 found in project file
[20:28:04 INF] | -- Used range ^1.30.1 in package.json
[20:28:04 INF] | -- Found installed version 1.30.1
[20:28:04 INF] | -- date-fns was installed into "devDependencies" instead of "dependencies"
[20:28:04 INF] | -- Re-install as a production dependency
[20:28:04 INF] | -- Resolve manually using 'npm uninstall date-fns' then 'npm install date-fns@1.30.1 --save'
I hope you are just as excited about Femto as much I am now! However, there is a caveat: in order for Femto to be useful, it needs information about the required npm packages from Fable bindings. Fable library authors of these package will need to include this information about the npm dependencies they require in the project file of their binding. In case you are a Fable library author, please read on!
Fable library authors
In order for Femto to pick up the information about the npm dependency that your library requires, you need to release a new version of your Fable binding with this information included in the project file. Simply add a section in your project file like this:
<PropertyGroup>
<NpmDependencies>
<NpmPackage Name="sweetalert2" Version=">= 8.5" />
</NpmDependencies>
</PropertyGroup>
You have a NpmDependencies
section and it can have a bunch of NpmPackage
tags. Each NpmPackage
tag can have the following attributes:
Name
: the name of the npm package you depend uponVersion
: specify the version constraint your library needs (not necessarily an exact version)- Optional
ResolutionStrategy
: with valuesMin
orMax
, specifies how we determine a valid version for the package.Min
is the default value if you don't specify this attribute. - Optional
DevDependency
: with valuestrue
orfalse
, specifies whether the npm package is a development dependency or a production dependency (devDependency
vsdependency
).false
is the default value if you don't specify the attribute.
See project files of Fable.DateFunctions, Elmish.SweetAlert or Elmish.AnimatedTree for working examples.
After you have added your npm dependency information, you can let Femto
validate them to check if they are well formatted and whether a valid version can be resolved based on your specification using the --validate
flag
femto --validate
femto --validate ./src/MyLibrary.fsproj
History and Credits
A while ago, sometime after FableConf 2018, I proposed the concept and thought of adding the current functionality of Femto into Fable and have the compiler do the work internally but the idea wasn't welcomed because it was thought of as introducing a new package manager. This is of course not the case, Femto
is only orchestrating what an existing package manager, npm or yarn, should do. At that time, I was working on the stuff that I couldn't prototype something concrete. The next time the problem was mentioned here, I re-iterated my proposition but this time Alfonso had a more positive feeling about it so I started working on it and when the first prototype came out, only including a primitive of way of dependency analysis, Maxime joined in on the party. In a single week of intense work, Maxime and I were able to put together what I have demonstrated above. Maxime would fix issues himself or would tell me that I did something stupid (which I definitely did) so I had lots of fun working together.
Want to contribute?
At this point, we have the most crucial features added to Femto but there are a couple more good first issues for those who want to contribute to Femto. Please take a look at the issues pages and feel free to send pull requests, they are very much appreciated and are always welcome!