Fable.io from Scratch — Part 2

In part 1 we took a look at how to use Paket to install Fable.Core and Fable.Import.Browser. In part 2 we are going to look at getting up and running with webpack and babel to output usable JavaScript. While we are using webpack in this article, there are other options like the fable-rollup loader. Rollup is an alternative to webpack that is not as popular as webpack but comes with some advantages over webpack for some scenarios.

DISCLAIMER: This is the hard way of starting a fable project. It is intended for people to learn more about the stack Fable is built on. For a better quick start experience using a template with all of this configured for you, please check out the Getting Started with Fable and Fable Elmish article.

So far all we have set-up is the Fable dependencies and associated them with our project. In this article we will set up our JavaScript dependencies and setup our build pipeline with webpack.

Yarn is to npm as Paket is to NuGet. Yarn makes npm better through the use of lock files. The same is true for Paket.

You should have already downloaded Yarn in part 1 of this article, if you haven’t done this yet, please do so now.

Installing JavaScript Dependencies

There are 3 main JavaScript dependencies that we will be leveraging in our project.

We will be installing these dependencies with Yarn. From our fable-from-scratch folder, run the following commands:

yarn init -y

The command above creates a default package.json file. This file is used by yarn to resolve all of the JavaScript dependencies in the project.

yarn add babel-runtime
yarn add -D babel-core babel-loader babel-plugin-transform-runtime babel-preset-es2015 fable-loader webpack webpack-dev-server

It is nice to keep your package.json file organized between run time and development time dependencies. In the JavaScript ecosystem this is a best practice as there can often be a large number of development side dependencies. The -D in the command above tells yarn to add those as development dependencies.

At this point your package.json file should like this:

{
"name": "fable-from-scratch",
"version": "1.0.0",
"main": "index.js",
"license": "MIT",
"dependencies": {
"babel-runtime": "^6.23.0"
},
"devDependencies": {
"babel-core": "^6.25.0",
"babel-loader": "^7.1.1",
"babel-plugin-transform-runtime": "^6.23.0",
"babel-preset-es2015": "^6.24.1",
"fable-loader": "^1.0.5",
"webpack": "^3.0.0",
"webpack-dev-server": "^2.5.0"
}}

At this point all of our dependencies are installed both on the .NET side and the JavaScript side.

Installing the Fable-Cli

To install the Fable-Cli, we need to modify the .fsproj file, but before we do that let’s create a “src” directory to store our source files.

mkdir src

In the root of your project a file was created called Program.fs. Go ahead and copy this file to the “src” directory and remove all of the contents from the file.

Now in the .fsproj file, change the path to the correct location for the Program.fs file since we moved it into the “src” folder.

Before

<ItemGroup>
<Compile Include="Program.fs" />
</ItemGroup>

After

<ItemGroup>
<Compile Include="src/Program.fs" />
</ItemGroup>

Next we need to add the package reference for the .NET Cli. You can put this directly below the line we changed above so that the .fsproj now looks like this:

<Project Sdk="FSharp.NET.Sdk;Microsoft.NET.Sdk">
 <PropertyGroup>
 <OutputType>Exe</OutputType>
 <TargetFramework>netcoreapp2.0</TargetFramework>
 </PropertyGroup>
 <ItemGroup>
 <Compile Include="src/Program.fs" />
 <DotNetCliToolReference Include="dotnet-fable" Version="1.1.*" />
 </ItemGroup>
 <Import Project=".paket\Paket.Restore.targets" />
</Project>

After this we need to run a dotnet restore so we pull down and reference the Fable Cli.

dotnet restore

To confirm the Cli was installed successfully, type the following command:

dotnet fable --help

This should output the following:

Fable F# to JS compiler (1.1.5)
Usage: dotnet fable [command] [script] [fable arguments] [-- [script arguments]]
Commands:
-h|--help Show help
--version Print version
start Start Fable daemon
npm-run Run Fable while an npm script is running
yarn-run Run Fable while a yarn script is running
node-run Run Fable while a node script is running
shell-run Run Fable while a shell script is running
webpack Start Fable daemon, invoke Webpack and shut it down
webpack-dev-server Run Fable while Webpack development server is running
Fable arguments:
--timeout Stop the daemon if timeout (ms) is reached
--port Port number (default 61225) or "free" to choose a free port
--verbose Print more info during execution
To pass arguments to the script, write them after `--`. Example:
dotnet fable npm-run build --port free -- -p --config webpack.production.js
You can use shortcuts for npm and yarn scripts in the following way:
dotnet fable yarn-start       # Same as `dotnet fable yarn-run start`
It is a beautiful thing that works amazingly well.

Take a second and review the above output.

One of the more complex things about Fable compared to other web stacks is that Fable requires a Fable Compiler Service to be running while code is being built and reloaded. Don’t worry, the way we are going to set this project up will make this mostly invisible.

This server translates your beautiful F# code into an Abstract Syntax Tree (AST) that Babel understands and can turn into beautiful JavaScript. It is a beautiful thing that works amazingly well.

The default port is 61225 and the fable-loader JavaScript dependency that we downloaded earlier knows how to talk to this server, so it needs to be running any time webpack or webpack-dev-server is running.

Speaking of webpack, let’s get that set-up next!

Webpack Config

The webpack.config.js file is the only file webpack needs to instruct it how to build and bundle our application. I am going to provide a minimal file for you to copy and paste into your editor. Place this file in the root of your project.

webpack.config.js

var path = require("path");
var webpack = require("webpack");
function resolve(filePath) {
return path.join(__dirname, filePath)
}
// This is configuration used by both Babel and 
// the fable-loader to generate ES2015

var babelOptions = {
presets: [["es2015", { "modules": false }]],
plugins: ["transform-runtime"]
};
var isProduction = process.argv.indexOf("-p") >= 0;
console.log("Bundling for " + (isProduction ? "production" : "development") + "...");
module.exports = {
devtool: "source-map", // Generates source maps
entry: resolve('./fable-from-scratch.fsproj'), // Entry point for webpack
output: { // The file to output and the directory to place it in
filename: 'bundle.js',
path: resolve('./public'),
},
resolve: {
modules: [
"node_modules", resolve("./node_modules/")
]
},
devServer: {
contentBase: resolve('./public'), // Dev server root directory. public becomes http://localhost:8080/
port: 8080 // Port to run dev server on
},
module: {
rules: [
{
test: /\.fs(x|proj)?$/, // regex test for which files will be processed by fable-loader
use: {
loader: "fable-loader",
options: {
babel: babelOptions,
define: isProduction ? [] : ["DEBUG"]
}
}
},
{
test: /\.js$/,
exclude: /node_modules/,
use: {
loader: 'babel-loader',
options: babelOptions
},
}
]
}
};

I added comments to the more important parts of the file. Please take a minute and read them unless you are already familiar with webpack.

Build Scripts

Update your package.json to add the scripts section below.

package.json

{
"name": "fable-from-scratch",
"version": "1.0.0",
"main": "index.js",
"license": "MIT",
"private": true,
"scripts": {
"build": "webpack -p",
"start": "webpack-dev-server",
"test": "echo \"Error: no test specified\" && exit 1"
},

"dependencies": {
"babel-runtime": "^6.23.0"
},
"devDependencies": {
"babel-core": "^6.25.0",
"babel-loader": "^7.1.1",
"babel-plugin-transform-runtime": "^6.23.0",
"babel-preset-es2015": "^6.24.1",
"fable-loader": "^1.0.5",
"webpack": "^3.0.0",
"webpack-dev-server": "^2.5.0"
}
}

Bringing it all together

At this point we have everything setup that we need to build our application, except for some code to run. Here is a bare minimum hello world application so we can test that everything works!

src/Program.fs

module Program
open Fable.Core
printfn "Hello World"

Now execute the following and make sure it works.

dotnet fable yarn-build
node ./public/bundle.js

You should see the following output in your console window.

Hello World

Congratulations! You have successfully built a fable application from an empty directory!

To learn more about Fable visit their site at http://fable.io/ or come chat with us on Gitter at https://gitter.im/fable-compiler/Fable