Starter App Walkthrough

This page is the shortest explanation of how the examples/nextfs-starter app is meant to work.

Goal

The starter is a reference App Router project where:

  • handwritten F# lives under src/**
  • Fable emits JavaScript into .fable/**
  • nextfs.entries.json maps compiled modules to Next.js entry files
  • generated wrappers live under app/** plus the root proxy.js and instrumentation entries

It is not a separate framework. The goal is to keep the filesystem shape recognizable to a Next.js developer while letting the implementation live in F#.

File Ownership

Edit by hand:

  • src/**
  • next.config.mjs

Generated files (do not edit directly):

  • nextfs.entries.json — regenerated from [<NextFs.NextFsEntry>] attributes by npm run scan
  • .fable/** — emitted by Fable
  • app/**, proxy.js, instrumentation.js, instrumentation-client.js — emitted by gen:wrappers

If a generated file looks wrong, fix the F# source or its [<NextFsEntry>] annotation and re-run npm run sync:app. Do not patch generated files directly.

Command Loop

From the repository root:

dotnet tool restore

From examples/nextfs-starter:

npm install
npm run sync:app
npm run dev

For iterative work:

npm run watch:fable
npm run dev

Command meaning:

  • build:fsharp: compile-only .NET check for the F# project
  • build:fable: emit .fable/** JavaScript from F#
  • scan: regenerate nextfs.entries.json from [<NextFs.NextFsEntry>] attributes in the .fsproj
  • gen:wrappers: rewrite app/** and root wrapper files from nextfs.entries.json
  • sync:app: run build:fable, then scan, then gen:wrappers

Mapping Example

Typical flow:

  1. src/App/Page.fs carries a [<NextFs.NextFsEntry("app/page.js", ...)>] attribute
  2. npm run scan reads the .fsproj, finds the attribute, and writes nextfs.entries.json
  3. npm run build:fable compiles src/App/Page.fs into .fable/App/Page.js
  4. tools/nextfs-entry.mjs reads nextfs.entries.json and writes a wrapper like:
'use client'

// Generated by NextFs. Do not edit by hand.
export { default } from '../.fable/App/Page.js'

Next.js consumes app/page.js, but the implementation logic still lives in F#.

Why The Wrapper Layer Exists

Next.js needs file-level directives such as 'use client' and 'use server' to appear before imports.

Fable-generated JavaScript starts with imports, so App Router entry modules often need a separate wrapper file even when the implementation itself is already correct.

That is why the starter keeps two generated layers:

  • .fable/** for the compiled implementation
  • app/** for the Next.js-facing entrypoints

Current Upstream Note

As of March 21, 2026, some environments still hit an upstream Fable CLI hang during dotnet fable runs. The current tracker is fable-compiler/Fable#4326.

As of March 21, 2026, the starter is validated end-to-end with a real:

npm run sync:app
npm run build

The starter therefore serves as both:

  • a real folder-layout reference for Next.js + F#
  • a checked-in, CI-validated example that completes a real next build

Where To Go Next