Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Chapter 1 "Understanding Fable" updates for Fable v4 #176

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 9 additions & 11 deletions chapters/fable/development-mode.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,29 +5,27 @@ Before we dive any further, we have to talk about the development workflow. So f
### Webpack development server

Along with the *full build* configuration in file `webpack.config.js` there is a section called `devServer`:
```js {highlight: [6, 7, 8]}
```js {highlight: [10,11,12]}
var path = require("path");

module.exports = {
mode: "none",
entry: "./src/App.fsproj",
devServer: {
contentBase: path.join(__dirname, "./dist")
entry: "./src/App.fs.js",
output: {
path: path.join(__dirname, "./dist"),
filename: "main.js"
},
module: {
rules: [{
test: /\.fs(x|proj)?$/,
use: "fable-loader"
}]
devServer: {
static: path.join(__dirname, "./dist")
}
}
```
Within the `devServer` section, you see the `contentBase` options pointing to the `dist` directory. These options are the configuration for the development server of webpack. This setup says, "Start a local server that serves files from the `dist` directory." The development server runs on port 8080 by default. To use the development server, run the following commands:
Within the `devServer` section, you see the `static` option pointing to the `dist` directory. These options are the configuration for the development server of webpack. This setup says, "Start a local server that serves files from the `dist` directory." The development server runs on port 8080 by default. To use the development server, run the following commands:
```bash
npm install
npm start
```
The command `npm start` will start the webpack development server, compile the project *only once*, and keep watching the project files for any changes. You can then navigate to `http://localhost:8080` to see your project running.
The command `npm start` will start both the compilation process by Fable and start the webpack development server at the same time. Fable will watch for changes in F# source files and recompile only the files affected, then webpack dev server will pick up the newly generated JS files and refresh the page. You can then navigate to `http://localhost:8080` to see your project running.

> Learn how `npm start` relates to webpack development server in the paragraph [Npm Scripts](node-packages#npm-scripts) of section [Node.js Packages](node-packages).

Expand Down
4 changes: 3 additions & 1 deletion chapters/fable/fable-bindings.md
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,8 @@ This may sound worrisome since you have to know which versions of npm packages g

### Authoring Fable libraries and bindings

This was just a glimpse at how Fable packages can integrate native JavaScript modules into Fable applications. In chapter 5, we will be looking into interoperability in great detail and the different ways of authoring a Fable package.
This was just a glimpse at how Fable packages can integrate native JavaScript modules into Fable applications.

In chapter 5 <strong>[TODO]</strong>, we will be looking into interoperability in great detail and the different ways of authoring a Fable package.

If you would like to find out more about how to write bindings, here is a write-up of my own on Medium. It is a little out of date, e.g. Pojo attributes, but the ideas are pretty much the same. [F# Interop with Javascript in Fable: The Complete Guide](https://medium.com/@zaid.naom/f-interop-with-javascript-in-fable-the-complete-guide-ccc5b896a59f).
6 changes: 3 additions & 3 deletions chapters/fable/fable-packages.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,13 @@ From the template we started within the [Hello World](hello-world.md) section, w
```xml {highlight:[9]}
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<TargetFramework>net6.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<Compile Include="App.fs" />
<Compile Include="App.fs" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Fable.Browser.Dom" Version="1.0.0" />
<PackageReference Include="Fable.Browser.Dom" Version="2.10.0" />
</ItemGroup>
</Project>
```
Expand Down
49 changes: 30 additions & 19 deletions chapters/fable/hello-world.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
# Hello World

To get started with Fable, it is easier to use a template instead of building your application from scratch, so I have set up a simple hello world fable application in the [fable-getting-started](https://github.com/Zaid-Ajaj/fable-getting-started) repository. Clone it locally on your machine as follows:
To get started with Fable, it is easier to use a template instead of building your application from scratch. I have set up a simple hello world Fable application in the [fable-getting-started](https://github.com/Zaid-Ajaj/fable-getting-started) repository. We will use this repository to demonstrate Fable features. Keep in mind that this template is not suitable for production use.

Now, to get started, clone the repository locally on your machine as follows:

```bash
git clone https://github.com/Zaid-Ajaj/fable-getting-started.git
Expand All @@ -11,11 +13,13 @@ fable-getting-started
├─── .gitattributes
├─── .gitignore
├─── LICENSE
├─── Nuget.Config
├─── package-lock.json
├─── package.json
├─── README.md
├─── webpack.config.js
├─── App.sln
├─── .config
│ ├─── dotnet-tools.json
├─── dist
│ ├─── fable.ico
│ ├─── index.html
Expand All @@ -29,7 +33,9 @@ The most important parts of the template are these directories:
- `src` is where your F# source code lives
- `dist` is the output directory when you compile F# to JavaScript
- `package.json` is used by Node.js to give information to the Node.js package manager (npm for short) that allows it to identify the project as well as handle the project's *dependencies*
- `webpack.config.js` will contain our "compiler configuration" with [webpack](https://webpack.js.org/). We will talk about Webpack in great detail in a later chapter because it is an advanced topic.
- `webpack.config.js` will contain our development server configuration as well as instructions for bundling the generated code by Fable using [webpack](https://webpack.js.org/). We will talk about Webpack in great detail in a later chapter because it is an advanced topic.
- `App.sln` is the solution file for the project
- `.config/dotnet-tools.json` specifies dotnet CLI tools that are used in this project. The Fable compiler itself is a dotnet CLI tool which is specified here.

The only F# source file in the project is `App.fs` and it contains the following code:
```fsharp
Expand Down Expand Up @@ -59,27 +65,30 @@ var path = require("path");

module.exports = {
mode: "none",
entry: "./src/App.fsproj",
devServer: {
contentBase: path.join(__dirname, "./dist")
entry: "./src/App.fs.js",
output: {
path: path.join(__dirname, "./dist"),
filename: "main.js"
},
module: {
rules: [{
test: /\.fs(x|proj)?$/,
use: "fable-loader"
}]
devServer: {
static: path.join(__dirname, "./dist")
}
}
```
The `entry` option specifies the path to the project that should be compiled. The other options will be discussed in a later chapter.
- The `entry` option specifies the path to entry _JavaScript_ file. This entry file defined here is `./App.fs.js` which is compiled from the F# entry file `App.fs`.
- The `output` option defines where the bundled JavaScript code should be emitted. Here we say that the bundled code should go in the `./dist` directory in a file named `main.js`. This is actually the default configuration that webpack uses when we don't specify `output` so we can omit this option entirely but I've kept here to show what webpack is doing.
- The `devServer` option specifies further options for the development server. More about that discussed in [Development Mode](development-mode.md)

### Compiling the project
To get your F# code to run in the browser, you will first need to compile the project and then open `index.html` in your browser. However, before being able to compile the project, there are a couple of requirements that you need to have installed on your machine:

- [.NET Core](https://www.microsoft.com/net/download) 2.2 or later, both SDK and runtime
- [Node.js](https://nodejs.org/en/) 10.0 or later
- [dotnet SDK](https://dotnet.microsoft.com/en-us/download) v6.0 or a more recent version
- [Node.js](https://nodejs.org/en/) 18.0 or later

Of course, having a code editor is not a requirement for building the project but rather for development. To edit F# code, it is highly recommended to have [VS Code](https://code.visualstudio.com/) installed (along with the [Ionide](http://ionide.io/) extension).
Of course, having a code editor is not a requirement for building the project but rather for development. To edit F# code, you can use:
- [Visual Studio](https://visualstudio.microsoft.com/)
- [Rider from JetBrains](https://www.jetbrains.com/rider/)
- [Visual Studio Code](https://code.visualstudio.com/) along with the [Ionide](http://ionide.io/) extension.

Once you have installed both .NET and Node.js, you can verify that you have the correct versions by running these commands in your terminal:
```bash
Expand All @@ -89,9 +98,11 @@ node --version
After you have checked the versions, you can clone the [fable-getting-started](https://github.com/Zaid-Ajaj/fable-getting-started) repository from GitHub and compile the whole project
```bash
cd fable-getting-started
dotnet tool restore
npm install
npm run build
```

I use Windows, so the compilation looks as follows on my machine

<resolved-image source='/images/fable/compile.gif' />
Expand Down Expand Up @@ -139,10 +150,10 @@ Of course, printing out a message to the console is boring. We can try something
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="shortcut icon" href="fable.ico" />
</head>
<body>
<button id="printMsg">Print Message</button>
<script src="main.js"></script>
</body>
<body>
<button id="printMsg">Print Message</button>
<script src="main.js"></script>
</body>
</html>
```
Here, we have added a `button` tag to the page with identity attribute called `"printMsg"`. We will use this id to reference the button from the F# code. Modify the contents of `App.fs` to the following:
Expand Down
10 changes: 3 additions & 7 deletions chapters/fable/how-fable-works.md
Original file line number Diff line number Diff line change
@@ -1,11 +1,7 @@
# Understanding how Fable works

The compiler operates on F# source code directly, using the [F# Compiler Services](https://fsharp.github.io/FSharp.Compiler.Service/) for parsing and type-checking the code. Once the code is parsed, a representation of the F# code's structure is obtained, also known as an abstract syntax tree (AST). This structure is then transformed into a specialized simpler Fable AST that is easier to work with. Afterwards, the actual translation begins by transforming the Fable AST into [Babel](https://babeljs.io/) AST representing the structure of the JavaScript code that will be generated.
The compiler operates on F# source code directly, using the [F# Compiler Services](https://www.nuget.org/packages/FSharp.Compiler.Service) being part of [dotnet/fsharp](https://github.com/dotnet/fsharp) for parsing and type-checking the code. Once the code is parsed, a representation of the F# code's structure is obtained, also known as an abstract syntax tree (AST). This structure is then transformed into a specialized simpler Fable AST that is easier to work with. Then using a specialized printer, Fable translates the AST into JavaScript modules. By default, every F# file ends up being a single JavaScript file/module even if the F# source file has multiple modules defined.

The `Fable -> Babel` transformation is implemented as a set of what is called *replacements*. Parts of the F# code are replaced with parts of JavaScript code. These parts could be different kinds of declarations, function calls, loop constructs etc. These are translated to their counterpart in JavaScript. The resulting Babel AST is then handed off to the Babel compiler that will actually generate the final output.
The `F# AST -> Fable AST` transformation is implemented as a set of what is called *replacements*. Parts of the F# code are replaced with parts of JavaScript code that is implemented in an internal library called `fable-library`. Users of Fable don't typically interact with it but it is fun to poke around and see what Fable implements as an alternative for the corresponding .NET constructs. The code is very readable actually most of the time. That is until you start writing active patterns and use nested pattern matching. That is alright, though. Generating _idiomatice_ JavaScript is something that Fable tries its best at but it doesn't always work out. Mainly because writing a pretty piece of F# code can't always be expressed with the same conciseness in JavaScript.

This diagram below shows an overview of the process.

<resolved-image source="/images/fable/fable.png" />

The generated JavaScript doesn't have to be a minified JavaScript file. This is only the convention when compiling F# source code for the browser. When building F# projects for Node.js environments, the output can be multiple JavaScript files referencing (i.e. `requiring`) each other while preserving the same input F# project structure that Fable compiled.
The fact that we use a bundler to bundle the generated JavaScript into a single JS file is actually optional and has to do with the fact that we want to create a website. When building F# projects for Node.js environments, the output can be multiple JavaScript files referencing (i.e. `requiring`) each other while preserving the same input F# project structure that Fable compiled.
48 changes: 33 additions & 15 deletions chapters/fable/node-packages.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,16 +8,18 @@ Understanding the hybrid nature of Fable is vital to being productive with Fable
You have already seen the F# project in the [Hello World](hello-world) template inside the `src` directory, and it is what you are usually familiar with from .NET. This is, however only half of the story because the repository itself is actually a Node.js project.

Let's have another look at the structure of the repository, notice the highlighted file called `package.json`:
```bash {highlight: [7]}
```bash {highlight: [6]}
fable-getting-started
├─── .gitattributes
├─── .gitignore
├─── LICENSE
├─── Nuget.Config
├─── package-lock.json
├─── package.json
├─── README.md
├─── webpack.config.js
├─── App.sln
├─── .config
│ ├─── dotnet-tools.json
├─── dist
│ ├─── fable.ico
│ ├─── index.html
Expand All @@ -26,35 +28,51 @@ fable-getting-started
├─── App.fs
├─── App.fsproj
```
The location of the `package.json` file *implies* that this directory is indeed a Node.js project and `package.json` is at the *root* of such project. Within `package.json`, you can specify the dependencies that the project uses. These dependencies can either be libraries or command-line interface (cli) programs. Let's have a look:
The location of the `package.json` file *implies* that this directory is indeed a Node.js project and `package.json` is at the *root* of such project.

Inside `package.json`, you can specify the dependencies that the project uses. These dependencies can either be libraries or development tools such as command-line interface (CLI) programs. Let's have a look:
```json
{
"private": true,
"scripts": {
"build": "webpack",
"start": "webpack-dev-server"
"build": "dotnet fable ./src && webpack",
"start": "dotnet fable watch ./src --runFast webpack-dev-server"
},
"devDependencies": {
"@babel/core": "^7.1.2",
"fable-compiler": "^2.3.24",
"fable-loader": "^2.1.8",
"webpack": "^4.38.0",
"webpack-cli": "^3.3.6",
"webpack-dev-server": "^3.7.2"
"webpack": "^5.74.0",
"webpack-cli": "^4.10.0",
"webpack-dev-server": "^4.10.0"
}
}

```
### Development dependencies
This `package.json` has two very important sections: the `devDependencies` and `scripts`. The former section defines *development* dependencies which are libraries and cli programs used during development. When you run `npm install` in the directory where `package.json` lives, all these dependencies are downloaded into a directory called `node_modules` next to `package.json`.

This means your development dependencies are installed on a *per-project* basis which is really great because first, you do not need to install any program to compile your project and second because the versions of these programs are maintained inside `package.json` allowing you to work on multiple projects on the same machine that uses different versions of the same programs. For example, you can work on a project A that uses Fable `1.x` and another project B that uses Fable `2.x` without the two interfering with each other.
This means your development dependencies are installed on a *per-project* basis which is really great because first, you do not need to install any machine-wise installations and second because the versions of these programs are maintained inside `package.json` allowing you to easily update them.

You might have wondered: "How did Fable compile the project if I didn't install it anywhere?" Well, there you have it. You installed it as part of your development dependencies with the package `fable-compiler`. The template uses version `2.3.24` of Fable, which is the most recent version at the time of writing. I intend to keep the project template up-to-date with latest Fable versions.
The development dependencies we have here are all related to `webpack`, our choice for bundling and serving the application during development. Note that there are other options to work with Fable projects such as [Vite](https://vitejs.dev/) which has gained a lot of popularity among Fable/F# devs recently.

### Npm scripts

The latter section of `package.json` is the `scripts` sections, also known as npm scripts. This section provides shortcuts to *running* the cli programs that were installed as development dependencies or any shell command from the system. For example, we installed `webpack` and `webpack-dev-server` which are programs that work with Fable to turn the F# project into a nicely bundled JavaScript file. We provide shortcuts to run these programs using the npm scripts `start` and `build`. To run such a script, you run the command:
The latter section of `package.json` is the `scripts` sections, also known as npm scripts. This section provides shortcuts to *running* the cli programs that were installed as development dependencies or any shell command from the system. You can run these scripts using the command
```bash
npm run <script name>
```
So when we run `npm start` (short for `npm run start`), we are invoking `webpack-dev-server` which will start the development server. The same goes for `npm run build`. This command will invoke `webpack` to start a full build of the project.
In our projects, we have the two scripts `start` and `build`.

Let us first talk about `build` which is a short hand command that invokes the following
```
dotnet fable ./src && webpack
```
This script invokes the Fable compiler with `dotnet fable` and asks it to compile the project inside the `./src` directory. Then, once that has finished it invokes `webpack` which takes care of the bundling business.

The `start` script on the other hand is slighly more complicated
```
dotnet fable watch ./src --runFast webpack-dev-server
```
It invokes the Fable compiler in _watch_ mode and immediately starts `webpack-dev-server` without waiting for the compilation to finish (what `--runFast` does) so that both processes run in parallel: Fable watching F# source files and recompiling them as you edit them and webpack development server rebundling and refreshing the page as soon as new JS files are generated.

Note that these two commands assume you have already restored the dotnet tools using `dotnet tool restore`.

Finally it is worth mentioning that at the time of writing we are using Fable v4.
Loading