Falco 3.1.14

There is a newer version of this package available.
See the version list below for details.
dotnet add package Falco --version 3.1.14                
NuGet\Install-Package Falco -Version 3.1.14                
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="Falco" Version="3.1.14" />                
For projects that support PackageReference, copy this XML node into the project file to reference the package.
paket add Falco --version 3.1.14                
#r "nuget: Falco, 3.1.14"                
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
// Install Falco as a Cake Addin
#addin nuget:?package=Falco&version=3.1.14

// Install Falco as a Cake Tool
#tool nuget:?package=Falco&version=3.1.14                

Falco

NuGet Version Build Status

open Falco
open Falco.Routing
open Falco.HostBuilder

webHost [||] {
    endpoints [                    
        get "/" (Response.ofPlainText "Hello World")
    ]
}

Falco is a toolkit for building fast, functional-first and fault-tolerant web applications using F#.

  • Built upon the high-performance primitives of ASP.NET Core.
  • Optimized for building HTTP applications quickly.
  • Seamlessly integrates with existing .NET Core middleware and frameworks.

Key Features

Design Goals

  • Provide a toolset to build a working full-stack web application.
  • Can be easily learned.
  • Should be extensible.

Table of Contents

  1. Getting Started
  2. Sample Applications
  3. Request Handling
  4. Routing
  5. Model Binding
  6. JSON
  7. Markup
  8. Host Builder
  9. Authentication
  10. Security
  11. Why "Falco"?
  12. Find a bug?
  13. License

Getting Started

Using dotnet new

The easiest way to get started with Falco is by installing the Falco.Template package, which adds a new template to your dotnet new command line tool:

dotnet new -i "Falco.Template::*"

Afterwards you can create a new Falco application by running:

dotnet new falco -o HelloWorldApp

Manually installing

Create a new F# web project:

dotnet new web -lang F# -o HelloWorldApp

Install the nuget package:

dotnet add package Falco

Remove the Startup.fs file and save the following in Program.fs (if following the manual install path):

module HelloWorld.Program

open Falco
open Falco.Routing
open Falco.HostBuilder

[<EntryPoint>]
let main args =
    webHost args {
        endpoints [ 
            get "/" (Response.ofPlainText "Hello World")
        ]
    }
    0

Run the application:

dotnet run

There you have it, an industrial-strength Hello World web app, achieved using only base ASP.NET Core libraries. Pretty sweet!

Sample Applications

Code is always worth a thousand words, so for the most up-to-date usage, the /samples directory contains a few sample applications.

Sample Description
Hello World A basic hello world app
Configure Host Demonstrating how to configure the IHost instance using the webHost computation expression
Blog A basic markdown (with YAML frontmatter) blog
Third-part View Engine Demonstrating how to render with an external view engine, specifically Scriban
Falco Journal A bullet journal built using Falco

Request Handling

The HttpHandler type is used to represent the processing of a request. It can be thought of as the eventual (i.e. asynchronous) completion and processing of an HTTP request, defined in F# as: HttpContext -> Task. Handlers will typically involve some combination of: route inspection, form/query binding, business logic and finally response writing. With access to the HttpContext you are able to inspect all components of the request, and manipulate the response in any way you choose.

Basic request/response handling is divided between the aptly named Request and Response modules, which offer a suite of continuation-passing style (CPS) HttpHandler functions for common scenarios.

Plain Text responses

let textHandler : HttpHandler =
    Response.ofPlainText "hello world"

HTML responses

let htmlHandler : HttpHandler =
    let html =
        Elem.html [ Attr.lang "en" ] [
            Elem.head [] []
            Elem.body [] [
                Elem.h1 [] [ Text.raw "Sample App" ]                
            ]
        ]

    Response.ofHtml html

Alternatively, if you're using an external view engine and want to return an HTML response from a string literal, then you can use Response.ofHtmlString.

let htmlHandler : HttpHandler = 
    Response.ofHtmlString "<html>...</html>"

JSON responses

IMPORTANT: This handler uses the default System.Text.Json.JsonSerializer. See JSON section below for further information.

type Person =
    { First : string
      Last  : string }

let jsonHandler : HttpHandler =
    let name = { First = "John"; Last = "Doe" }
    Response.ofJson name

Redirect (301/302) Response

let oldUrlHandler : HttpHandler =
    Response.redirect "/new-url" true

Note: The trailing bool value is used to indicate permanency (i.e., true = 301 / false = 302)

Accessing Request Data

Falco exposes a uniform API to obtain typed values from the various sources of request data. Note, the similarity in the various binders below.

Route Collection

let helloHandler : HttpHandler =
    let routeMap (route : RouteCollectionReader) =
        let name = route.GetString "name" "World" 
        sprintf "Hello %s" name
        
    Request.mapRoute routeMap Response.ofPlainText

Query Parameters

let helloHandler : HttpHandler =
    let queryMap (query : QueryCollectionReader) =
        let name = query.GetString "name" "World" 
        sprintf "Hello %s" name
        
    Request.mapQuery queryMap Response.ofPlainText

Form Data

let helloHandler : HttpHandler =
    let formMap (query : FormCollectionReader) =
        let name = query.GetString "name" "World" 
        sprintf "Hello %s" name
        
    Request.mapForm formMap Response.ofPlainText

To prevent XSS attacks it is often advisable to use a CSRF token during form submissions. In these situations, you'll want to validate the token before processing the form input using Request.mapFormSecure, which will automatically validate the token for you before consuming input.

let secureHelloHandler : HttpHandler =
    let formMap (query : FormCollectionReader) =
        let name = query.GetString "name" "World" 
        sprintf "Hello %s" name

    let invalidTokenHandler : HttpHandler =
        Response.withStatusCode 403
        >> Resposne.ofEmpty
        
    Request.mapFormSecure formMap Response.ofPlainText invalidTokenHandler

Response Modifiers

Response modifiers can be thought of as the in-and-out modification of the HttpResponse. A preamble to writing and returning. Since these functions receive the Httpcontext as input and return it as the only output, they can take advantage of function composition.

Set the status code of the response

let notFoundHandler : HttpHandler =
    Response.withStatusCode 404
    >> Response.ofPlainText "Not found"

Add a header to the response

let handlerWithHeader : HttpHandler =
    Response.withHeader "Content-Language" "en-us"
    >> Response.ofPlainText "Hello world"
let handlerWithHeader : HttpHandler =
    Response.withCookie "greeted" "1"
    >> Response.ofPlainText "Hello world"

IMPORTANT: Do not use this for authentication. Instead use the Auth.signIn and Auth.signOut functions found in the Authentication module.

Routing

The breakdown of Endpoint Routing is simple. Associate a specific route pattern (and optionally an HTTP verb) to an HttpHandler which represents the ongoing processing (and eventual return) of a request.

Bearing this in mind, routing can practically be represented by a list of these "mappings" known in Falco as an HttpEndpoint which bind together: a route, verb and handler.

let helloHandler : HttpHandler =
    let getMessage (route : RouteCollectionReader) =
        route.GetString "name" "World" 
        |> sprintf "Hello %s"
        
    Request.mapRoute getMessage Response.ofPlainText

let loginHandler : HttpHandler = // ...

let loginSubmitHandler : HttpHandler = // ...  

let endpoints : HttpEndpoint list =
  [
    // a basic GET handler
    get "/hello/{name:alpha}" helloHandler

    // multi-method endpoint
    all "/login"
        [ POST, loginSubmitHandler
          GET,  loginHandler ]
  ]

Model Binding

Reflection-based approaches to binding at IO boundaries work well for simple use cases. But as the complexity of the input rises it becomes error-prone and often involves tedious workarounds. This is especially true for an expressive, algebraic type system like F#. As such, it is often advisable to take back control of this process from the runtime. An added bonus of doing this is that it all but eliminates the need for [<CLIMutable>] attributes.

We can make this simpler by creating a succinct API to obtain typed values from IFormCollection, IQueryCollection, RouteValueDictionary and IHeaderCollection. Readers for all four exist as derivatives of StringCollectionReader which is an abstraction intended to make it easier to work with the string-based key/value collections.

Route Binding

let mapRouteHandler : HttpHandler =
    let routeMap (r : RouteCollectionReader) = 
        r.GetString "Name" "John Doe"
    
    Request.mapRoute routeMap Response.ofJson

let manualRouteHandler : HttpHandler = fun ctx ->
    let r : RouteCollectionReader = Request.getRoute ctx
    let name = r.GetString "Name" "John Doe"  

    Response.ofJson name ctx

Query Binding

type Person = { FirstName : string; LastName : string }

let mapQueryHandler : HttpHandler =    
    let queryMap (q : QueryCollectionReader) =
        let first = q.GetString "FirstName" "John" // Get value or return default value
        let last = q.GetString "LastName" "Doe"
        { FirstName = first; LastName = last }

    Request.mapQuery queryMap Response.ofJson 

let manualQueryHandler : HttpHandler = fun ctx ->
    let q : QueryCollectionReader = Request.getQuery ctx
    
    let person = 
        { FirstName = q.GetString "FirstName" "John" // Get value or return default value
          LastName  = q.GetString "LastName" "Doe" }

    Response.ofJson person ctx

Form Binding

The FormCollectionReader has full access to the IFormFilesCollection via the _.Files member.

Note the addition of Request.mapFormSecure, which will automatically validate CSRF token for you.

type Person = { FirstName : string; LastName : string }

let mapFormHandler : HttpHandler =   
    let formMap (f : FormCollectionReader) =
        let first = f.GetString "FirstName" "John" // Get value or return default value
        let last = f.GetString "LastName" "Doe"        
        { FirstName = first; LastName = last }

    Request.mapForm formMap Response.ofJson 

let mapFormSecureHandler : HttpHandler =    
    let formMap (f : FormCollectionReader) =
        let first = f.GetString "FirstName" "John" // Get value or return default value
        let last = f.GetString "LastName" "Doe"        
        { FirstName = first; LastName = last }

    let handleInvalidCsrf : HttpHandler = 
        Response.withStatusCode 400 >> Response.ofEmpty

    Request.mapFormSecure formMap Response.ofJson handleInvalidCsrf

let manualFormHandler : HttpHandler = fun ctx -> task {
    let! f : FormCollectionReader = Request.getForm ctx
    
    let person = 
        { FirstName = f.GetString "FirstName" "John" // Get value or return default value
          LastName = f.GetString "LastName" "Doe" }

    return! Response.ofJson person ctx
}        
multipart/form-data Binding (handling large uploads)

Microsoft defines large uploads as anything > 64KB, which well... is most uploads. Anything beyond this size and they recommend streaming the multipart data to avoid excess memory consumption.

To make this process a lot easier Falco provides a set of four HttpHandler's analogous to the form handlers above, which utilize an HttpContext extension method called TryStreamFormAsync() that will attempt to stream multipart form data, or return an error message indicating the likely problem.

Below is an example demonstrating the insecure map variant:

let imageUploadHandler : HttpHandler =
    let formBinder (f : FormCollectionReader) : IFormFile option =
        f.TryGetFormFile "profile_image"
    
    let uploadImage (profileImage : IFormFile option) : HttpHandler = 
        // Process the uploaded file ...

    // Safely buffer the multipart form submission
    Request.mapFormStream formBinder uploadImage

JSON

Included in Falco are basic JSON in/out handlers, Request.mapJson and Response.ofJson respectively. Both rely on System.Text.Json and thus have minimal support for F#'s algebraic types.

type Person = { FirstName : string; LastName : string }

let jsonHandler : HttpHandler =
    { FirstName = "John"; LastName = "Doe" }
    |> Response.ofJson

let mapJsonHandler : HttpHandler =    
    let handleOk person : HttpHandler = 
        let message = sprintf "hello %s %s" person.First person.Last
        Response.ofPlainText message

    Request.mapJson handleOk

Markup

A core feature of Falco is the XML markup module. It can be used to produce any form of angle-bracket markup (i.e. HTML, SVG, XML etc.).

For example, the module is easily extended since creating new tags is simple. An example to render <svg>'s:

let svg (width : float) (height : float) =
    Elem.tag "svg" [
        Attr.create "version" "1.0"
        Attr.create "xmlns" "http://www.w3.org/2000/svg"
        Attr.create "viewBox" (sprintf "0 0 %f %f" width height)
    ]

let path d = Elem.tag "path" [ Attr.create "d" d ] []

let bars =
    svg 384.0 384.0 [
        path "M368 154.668H16c-8.832 0-16-7.168-16-16s7.168-16 16-16h352c8.832 0 16 7.168 16 16s-7.168 16-16 16zm0 0M368 32H16C7.168 32 0 24.832 0 16S7.168 0 16 0h352c8.832 0 16 7.168 16 16s-7.168 16-16 16zm0 0M368 277.332H16c-8.832 0-16-7.168-16-16s7.168-16 16-16h352c8.832 0 16 7.168 16 16s-7.168 16-16 16zm0 0"
    ]

HTML View Engine

Most of the standard HTML tags & attributes have been built into the markup module and produce objects to represent the HTML node. Nodes are either:

  • Text which represents string values. (Ex: Text.raw "hello", Text.rawf "hello %s" "world")
  • SelfClosingNode which represent self-closing tags (Ex: <br />).
  • ParentNode which represent typical tags with, optionally, other tags within it (Ex: <div>...</div>).

The benefits of using the Falco markup module as an HTML engine include:

  • Writing your views in plain F#, directly in your assembly.
  • Markup is compiled alongside the rest of your code, leading to improved performance and ultimately simpler deployments.
// Create an HTML5 document using built-in template
let doc = 
    Templates.html5 "en"
        [ Elem.title [] [ Text.raw "Sample App" ] ] // <head></head>
        [ Elem.h1 [] [ Text.raw "Sample App" ] ]    // <body></body>

Since views are plain F# they can easily be made strongly-typed:

type Person = { FirstName : string; LastName : string }

let doc (person : Person) = 
    Elem.html [ Attr.lang "en" ] [
        Elem.head [] [                    
            Elem.title [] [ Text.raw "Sample App" ]                                                            
        ]
        Elem.body [] [                     
            Elem.main [] [
                Elem.h1 [] [ Text.raw "Sample App" ]
                Elem.p  [] [ Text.rawf "%s %s" person.First person.Last ]
            ]
        ]
    ]

Views can also be combined to create more complex views and share output:

let master (title : string) (content : XmlNode list) =
    Elem.html [ Attr.lang "en" ] [
        Elem.head [] [                    
            Elem.title [] [ Text.raw "Sample App" ]                                                            
        ]
        Elem.body [] content
    ]

let divider = 
    Elem.hr [ Attr.class' "divider" ]

let homeView =
    [
        Elem.h1 [] [ Text.raw "Homepage" ]
        divider
        Elem.p  [] [ Text.raw "Lorem ipsum dolor sit amet, consectetur adipiscing."]
    ]
    |> master "Homepage" 

let aboutView =
    [
        Elem.h1 [] [ Text.raw "About" ]
        divider
        Elem.p  [] [ Text.raw "Lorem ipsum dolor sit amet, consectetur adipiscing."]
    ]
    |> master "About Us"

Host Builder

Kestrel is the web server at the heart of ASP.NET. It's performant, secure, and maintained by incredibly smart people. Getting it up and running is usually done using Host.CreateDefaultBuilder(args), but it can grow verbose quickly.

To make things more expressive, Falco exposes an optional computation expression. Below is an example using the builder taken from the Configure Host sample.

[<EntryPoint>]
let main args = 
    webHost args {
        use_ifnot FalcoExtensions.IsDevelopment HstsBuilderExtensions.UseHsts
        use_https
        use_compression
        use_static_files

        use_if    FalcoExtensions.IsDevelopment DeveloperExceptionPageExtensions.UseDeveloperExceptionPage
        use_ifnot FalcoExtensions.IsDevelopment (FalcoExtensions.UseFalcoExceptionHandler exceptionHandler)

        endpoints [            
            get "/greet/{name:alpha}" 
                handleGreeting

            get "/json" 
                handleJson

            get "/html" 
                handleHtml
                
            get "/" 
                handlePlainText
        ]
    }
    0

Fully Customizing the Host

To assume full control over configuring your IHost use the configure custom operation. It expects a function with the signature of HttpEndpoint list -> IWebHostBuilder -> IWebHostBuilder and assumes you will register and activate Falco (i.e., AddFalco() and UseFalco(endpoints)).

[<EntryPoint>]
let main args = 
    let configureServices : IServiceCollection -> unit = 
      fun services -> services.AddFalco() |> ignore
    
    let configureApp : HttpEndpoint list -> IApplicationBuilder -> unit =
       fun endpoints app -> app.UseFalco(endpoints) |> ignore

    let configureWebHost : HttpEndpoint list -> IWebHostBuilder =
      fun endpoints webHost ->
          webHost.ConfigureLogging(configureLogging)
                 .ConfigureServices(configureServices)
                 .Configure(configureApp endpoints)

    webHost args {
      configure configureWebHost
      endpoints []
    }

Authentication

ASP.NET Core has amazing built-in support for authentication. Review the docs for specific implementation details. Falco includes some authentication utilities.

To use the authentication helpers, ensure the service has been registered (AddAuthentication()) with the IServiceCollection and activated (UseAuthentication()) using the IApplicationBuilder.

Prevent user from accessing secure endpoint:

open Falco.Security

let secureResourceHandler : HttpHandler =
    let handleAuth : HttpHandler = 
        "hello authenticated user"
        |> Response.ofPlainText 

    let handleInvalid : HttpHandler =
        Response.withStatusCode 403 
        >> Response.ofPlainText "Forbidden"

    Request.ifAuthenticated handleAuth handleInvalid

Prevent authenticated user from accessing anonymous-only end-point:

let anonResourceOnlyHandler : HttpHandler =
    let handleAnon : HttpHandler = 
        Response.ofPlainText "hello anonymous"

    let handleInvalid : HttpHandler = 
        Response.withStatusCode 403 
        >> Response.ofPlainText "Forbidden"

    Request.ifNotAuthenticated handleAnon handleInvalid

Allow only user's from a certain group to access endpoint"

let secureResourceHandler : HttpHandler =
    let handleAuthInRole : HttpHandler = 
        Response.ofPlainText "hello admin"

    let handleInvalid : HttpHandler = 
        Response.withStatusCode 403 
        >> Response.ofPlainText "Forbidden"

    let rolesAllowed = [ "Admin" ]

    Request.ifAuthenticatedInRole rolesAllowed handleAuthInRole handleInvalid

Allow only user's with a certain scope to access endpoint"

let secureResourceHandler : HttpHandler =
    let handleAuthHasScope : HttpHandler = 
        Response.ofPlainText "user1, user2, user3"

    let handleInvalid : HttpHandler = 
        Response.withStatusCode 403 
        >> Response.ofPlainText "Forbidden"

    let issuer = "https://oauth2issuer.com"
    let scope = "read:users"

    Request.ifAuthenticatedWithScope issuer scope handleAuthHasScope handleInvalid

End user session (sign out):

let logOut : HttpHandler =         
    let authScheme = "..."
    let redirectTo = "/login"

    Response.signOutAndRedirect authScheme redirectTo

Security

Cross-site scripting attacks are extremely common since they are quite simple to carry out. Fortunately, protecting against them is as easy as performing them.

The Microsoft.AspNetCore.Antiforgery package provides the required utilities to easily protect yourself against such attacks.

Falco provides a few handlers via Falco.Security.Xss:

To use the Xss helpers, ensure the service has been registered (AddAntiforgery()) with the IServiceCollection and activated (UseAntiforgery()) using the IApplicationBuilder.

open Falco.Markup
open Falco.Security 

let formView token =     
    Elem.html [] [
        Elem.body [] [
            Elem.form [ Attr.method "post" ] [
                Elem.input [ Attr.name "first_name" ]

                Elem.input [ Attr.name "last_name" ]

                // using the CSRF HTML helper
                Xss.antiforgeryInput token

                Elem.input [ Attr.type' "submit"; Attr.value "Submit" ]
            ]                                
        ]
    ]
    
// A handler that demonstrates obtaining a
// CSRF token and applying it to a view
let csrfViewHandler : HttpHandler = 
    formView
    |> Response.ofHtmlCsrf
    
// A handler that demonstrates validating
// the request's CSRF token
let mapFormSecureHandler : HttpHandler =    
    let mapPerson (form : FormCollectionReader) =
        { FirstName = form.GetString "first_name" "John" // Get value or return default value
          LastName = form.GetString "first_name" "Doe" }

    let handleInvalid : HttpHandler = 
        Response.withStatusCode 400 
        >> Response.ofEmpty

    Request.mapFormSecure mapPerson Response.ofJson handleInvalid

Crytography

Many sites have the requirement of a secure log in and sign up (i.e. registering and maintaining a user's database). Thus, generating strong hashes and random salts is important.

Falco helpers are accessed by importing Falco.Auth.Crypto.

open Falco.Security

// Generating salt,
// using System.Security.Cryptography.RandomNumberGenerator,
// create a random 16 byte salt and base 64 encode
let salt = Crypto.createSalt 16 

// Generate random int for iterations
let iterations = Crypto.randomInt 10000 50000

// Pbkdf2 Key derivation using HMAC algorithm with SHA256 hashing function
let password = "5upe45ecure"
let hashedPassword = password |> Crypto.sha256 iterations 32 salt

Why "Falco"?

Kestrel has been a game changer for the .NET web stack. In the animal kingdom, "Kestrel" is a name given to several members of the falcon genus. Also known as "Falco".

Find a bug?

There's an issue for that.

License

Built with ♥ by Pim Brouwers in Toronto, ON. Licensed under Apache License 2.0.

Product Compatible and additional computed target framework versions.
.NET net5.0 is compatible.  net5.0-windows was computed.  net6.0 is compatible.  net6.0-android was computed.  net6.0-ios was computed.  net6.0-maccatalyst was computed.  net6.0-macos was computed.  net6.0-tvos was computed.  net6.0-windows was computed.  net7.0 was computed.  net7.0-android was computed.  net7.0-ios was computed.  net7.0-maccatalyst was computed.  net7.0-macos was computed.  net7.0-tvos was computed.  net7.0-windows was computed.  net8.0 was computed.  net8.0-android was computed.  net8.0-browser was computed.  net8.0-ios was computed.  net8.0-maccatalyst was computed.  net8.0-macos was computed.  net8.0-tvos was computed.  net8.0-windows was computed. 
.NET Core netcoreapp3.1 is compatible. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages (3)

Showing the top 3 NuGet packages that depend on Falco:

Package Downloads
Falco.Htmx

HTMX Bindings for the Falco web toolkit.

Falco.Bulma

Package Description

Falco.OpenApi

Open API support for Falco.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last updated
5.0.0-beta1 80 11/1/2024
5.0.0-alpha5 168 10/21/2024
5.0.0-alpha4 115 10/18/2024
5.0.0-alpha3 69 10/17/2024
5.0.0-alpha2 79 9/29/2024
5.0.0-alpha1 61 9/22/2024
4.0.6 2,491 12/12/2023
4.0.5 430 11/16/2023
4.0.4 4,276 3/13/2023
4.0.3 883 1/1/2023
4.0.2 427 11/30/2022
4.0.1 353 11/23/2022
4.0.0 613 11/7/2022
4.0.0-rc1 146 11/2/2022
4.0.0-beta4 153 10/26/2022
4.0.0-beta3 144 10/25/2022
4.0.0-beta2 584 9/23/2022
4.0.0-beta1 142 9/13/2022
4.0.0-alpha2 138 9/11/2022
4.0.0-alpha1 174 8/29/2022
3.1.14 1,801 8/29/2022
3.1.13 674 8/11/2022
3.1.12 1,840 5/20/2022
3.1.11 1,142 2/8/2022
3.1.10 811 12/14/2021
3.1.9 432 12/6/2021
3.1.8 399 12/3/2021
3.1.7 798 9/24/2021
3.1.6 364 9/24/2021
3.1.5 377 9/24/2021
3.1.4 504 8/24/2021
3.1.3 538 8/4/2021
3.1.2 468 7/30/2021
3.1.1 453 7/27/2021
3.1.0 431 7/27/2021
3.1.0-beta8 258 7/7/2021
3.1.0-beta7 253 7/6/2021
3.1.0-beta6 270 7/6/2021
3.1.0-beta5 266 7/5/2021
3.1.0-beta4 222 7/5/2021
3.1.0-beta3 312 7/1/2021
3.1.0-beta2 257 7/1/2021
3.1.0-beta1 248 6/15/2021
3.0.5 1,427 6/14/2021
3.0.4 560 5/5/2021
3.0.3 556 4/10/2021
3.0.2 813 12/8/2020
3.0.1 526 12/1/2020
3.0.0 567 11/27/2020
3.0.0-alpha9 299 11/26/2020
3.0.0-alpha8 285 11/25/2020
3.0.0-alpha7 314 11/25/2020
3.0.0-alpha6 310 11/22/2020
3.0.0-alpha5 272 11/20/2020
3.0.0-alpha4 295 11/20/2020
3.0.0-alpha3 276 11/20/2020
3.0.0-alpha2 470 11/11/2020
3.0.0-alpha1 275 11/11/2020
2.1.0 667 11/11/2020
2.0.4 1,842 11/9/2020
2.0.3 578 10/31/2020
2.0.2 1,013 7/31/2020
2.0.1 548 7/20/2020
2.0.0 730 7/12/2020
2.0.0-alpha 332 7/7/2020
1.2.3 555 7/2/2020
1.2.2 532 6/29/2020
1.2.1 603 6/28/2020
1.2.0 604 6/23/2020
1.1.0 771 6/6/2020
1.0.10-alpha 341 5/13/2020
1.0.9-alpha 394 4/29/2020
1.0.8-alpha 340 4/29/2020
1.0.7-alpha 321 4/27/2020
1.0.6-alpha 330 4/24/2020
1.0.5-alpha 330 4/23/2020
1.0.4-alpha 342 4/22/2020
1.0.3-alpha 353 4/19/2020
1.0.2-alpha 353 4/19/2020
1.0.1-alpha 351 4/7/2020
1.0.0-alpha 345 4/6/2020