Skip to content

Development ​

text
cli/                 the generator (Rust): scan → resolve/check → emit
cli/templates/       minijinja templates for app.g.dart and `fsp new`
editors/vscode/      the VS Code extension (TypeScript): fsp diagnostics in the Problems panel
editors/intellij/    the IntelliJ / Android Studio plugin (Kotlin): fsp diagnostics in the editor
docs/                the user documentation, one page per topic
scripts/             packaging.py renders the Homebrew formula and Scoop manifest for a release;
                     pin_checksums.py writes the release's checksums into the Dart package
scripts/telemetry/   the dashboards' one spec (dashboards.toml) and build_dashboards.py, which writes
                     the OpenObserve and Grafana JSON; seed.py and smoke.py run the stack in Docker
cli/templates/telemetry/   the stack `fsp telemetry` writes (compose file, collector, importer,
                     dashboards); runs as it is with `docker compose up -d`
packages/fespalier/  the runtime app.g.dart imports (DataView, segment parsing, TypedLocation),
                     testing.dart, and bin/fespalier.dart, the `dart run fespalier` launcher for `fsp`
packages/fespalier_transport/   HttpTransport, the seam the networking packages share, with HttpCredentials, HttpWrites and WriteGuardTransport; pure Dart (since 0.21.0)
packages/fespalier_auth/   signed-in routes: session provider, guards, SessionTransport; protocol-agnostic since 0.21.0
packages/fespalier_oidc/   OpenID Connect and Keycloak for fespalier_auth: OidcBackend, bearer tokens (since 0.21.0)
packages/fespalier_keypair/   device keys that sign (Secure Enclave, AndroidKeyStore, a browser's non-extractable key, or memory); Flutter 3.44, outside the workspace (since 0.21.0)
packages/fespalier_keypair_dpop/   DPoP proofs for fespalier_oidc, signed by a fespalier_keypair key; Flutter 3.44, outside the workspace (since 0.21.0)
packages/fespalier_sign_keypair/   token-free device-signed sessions for fespalier_auth; Flutter 3.44, outside the workspace
packages/fespalier_flags/   feature flags: FlagSource, flag() providers that guards watch, flagGuard (since 0.9.0)
packages/fespalier_storage/   dataCache storages on shared_preferences and Hive, with a size budget (since 0.9.0)
packages/fespalier_connectivity/   reconnectSignal from connectivity_plus, and hasNetwork for offline banners (since 0.9.0)
packages/fespalier_adaptive/   nav.dart menus as a bar, a rail or a drawer by window width
packages/fespalier_image/   responsive CDN images (ResponsiveImage, the URL builders), with FakeImages for tests
packages/fespalier_http/  package:http (since 0.15.0): requests aborted with their page, server field errors, writes never retried; HttpClientTransport and TransportClient (since 0.21.0)
packages/fespalier_dio/   Dio: requests cancelled with their page, server field errors, writes never retried; DioTransport and TransportAdapter (since 0.21.0)
packages/fespalier_sentry/   Sentry: errors tagged with the route and the file, page breadcrumbs, optional screen-load transactions
packages/fespalier_tolgee/   translations from Tolgee's CDN with a bundled fallback and route locales (since 0.10.0)
packages/fespalier_forms/   the form of an action.dart: ActionForm, typed fields, useActionForm (since 0.11.0)
packages/fespalier_maps/   a MapLibre pin picker that returns a place, with Geocoder and PositionSource seams (since 0.13.0)
packages/fespalier_download/   downloads: DownloadRequest, DownloadStatus and the backend ports, with fakes (since 0.15.0)
packages/fespalier_download_background/   the background download backend over background_downloader, Flutter 3.47 (since 0.15.0)
packages/fespalier_push/   notification taps open typed routes: PushSource, linkRoute, an adapter (since 0.13.0)
packages/fespalier_analytics/   screen views by route pattern, with consent: AnalyticsBackend, AnalyticsSink, an adapter (since 0.13.0)
packages/fespalier_devtools/   the DevTools extension's source (a Flutter web app, tested on the VM)
packages/fespalier/extension/devtools/   what DevTools loads: config.yaml (its version is release-please's)
                     and build/, the extension's release build, committed
examples/minimal/    the smallest app: `flutter create` + `fsp init` + three pages, with widget tests
examples/shop/       end-to-end example; its lib/app.g.dart is committed
examples/features/   every binding rule, section data and nested not_found.dart, with widget tests
examples/tabs/       a tab layout (StatefulShellRoute), with widget tests
examples/telemetry/  the route lifecycle, OpenTelemetry and Sentry, with widget tests
examples/auth/       fespalier_auth: sign-in, guards, refresh and Keycloak, with widget tests
examples/device/     fespalier_sign_keypair: enrol a device, token-free signed requests, a user-present action, with widget tests
examples/plugins/    fespalier_push and fespalier_analytics through adapters, with widget tests
examples/downloads/  fespalier_download: a list of downloads, a detail page a notification tap opens, one button per device-test case, with widget tests
examples/cose/       a full stack: a fespalier_cratestack app with a COSE_Sign1 transport, and server/, the Rust
                     CrateStack server it talks to (its own Cargo.lock); `just cose` runs both
examples/cratestack/  CrateStack, a client generated with --preset fespalier and device-signed sessions: the app, server/ (Rust,
                     its own Cargo.lock) and client/ (generated); `just cratestack` runs them all
examples/frb-cose/   (since 0.21.1) a Rust core behind flutter_rust_bridge that is CrateStack's generated client, sealing every
                     call as COSE_Sign1 (rust/, its generated bindings committed), server/, a copy of cose's server, and the app
examples/i18n/       fespalier_tolgee: a $lang segment, bundled catalogs, a language switch, with widget tests
examples/maps/       fespalier_maps: a pin picker and one offline pack, with fakes and widget tests
examples/adopt/      a go_router app half-way through adopting fespalier (AppRoutes.mount, ready/attach), with a before/after parity test
examples/offline/    fespalier_cratestack offline-first: reads, intents and owned rows on a demo server, with widget tests
examples/live/       fespalier_stream: event streams, a live change feed, a streaming action and a WebSocket chat, on a
                     demo server or tool/server.dart, with widget tests
skills/              agent skills: how to write lib/app/ and read fsp's errors (skills/README.md);
                     scripts/skills/ checks them against the code
sh
just ci          # everything CI runs on the code, locally (needs Flutter, Node, just, cargo-deny)
just --list      # the individual steps: fmt, lint, test, deny, examples, flutter, devtools, packaging, telemetry, skills
just telemetry-dashboards   # regenerate the dashboards after editing scripts/telemetry/dashboards.toml
just telemetry-smoke        # run the telemetry stack in Docker and check every dashboard query (needs Docker)
just devtools-build   # rebuild the DevTools extension after touching its source (see below)
just web-routes  # the shop's Maestro flows open their routes in Chromium (needs Flutter and Node; not in `just ci`)
just examples-web  # every example's routes open in Chromium (needs Flutter and Node; not in `just ci`)
just example-screenshots  # the examples' README screenshots, from a release web build (needs Flutter, Node and Docker; not in `just ci`)
just example-images       # the README screenshots match examples.json and the READMEs (in `just ci`)
just dev-e2e     # fsp dev against the real flutter in headless Chrome (needs Flutter and Chrome; CI's scaffold job runs it)
just cose        # examples/cose: its Rust server's gates, then the app's tests against the binary it built (needs Rust, cargo-deny and Flutter; CI's cose job runs it)
just cratestack-cli && just cratestack  # examples/cratestack: the CLI built from the commit the script pins, the server's gates, the generated client compared with a fresh one, then the app's tests against the binary (needs Rust, cargo-deny and Flutter; CI's cratestack job runs it)
just frb-cose          # examples/frb-cose: the server's and the core's gates, the bindings check, then the app's tests against the server (needs Rust with the targets rust/ lists, cargo-deny, the codegen and Flutter; CI's frb-cose job runs it)
just frb-cose-core     # examples/frb-cose's Rust core on its own: fmt, clippy, the tests (in process, against its server), a locked build and cargo-deny
just frb-cose-server   # examples/frb-cose's server on its own: the same gates on its own crate
just frb-cose-bindings # regenerate the flutter_rust_bridge bindings with the pinned codegen (flutter_rust_bridge_codegen 2.13.0 and cargo-expand 1.0.127 on PATH) and fail on a diff
just floor <sdk dir>   # the declared floor: Flutter 3.35 in copies outside the workspace, then each package alone and all of them as an app at their lowest versions (needs that SDK; CI's floor and floor-app jobs run it)
dart run melos run deps:outdated   # what is newer than the constraints allow, workspace and outside it
dart run melos run deps:upgrade    # raise the constraints to the newest majors, every pubspec at once (then the checks it prints)

just ci needs rustup, with network, on its first run (since 0.21.1). The frb-cose example's flutter test builds its Rust core through a build hook, and rustup installs the toolchain and the twelve targets that examples/frb-cose/rust/rust-toolchain.toml lists: about 2 GB, once per machine. The core then builds in about two minutes cold (measured on a shared 4-CPU machine), and in seconds after that. A contributor without rustup or network fails just ci at that example; the justfile's header says so too.

AGENTS.md is the contributor and agent guide: the layout, the gate commands, how to run each suite, and the conventions (Conventional Commit PR titles, squash merges, SHA-pinned actions, regenerating the examples).

Building fsp. cd cli && cargo build --release writes cli/target/release/fsp. After changing the emitter or a template, regenerate with cargo run -- gen --project ../examples/<name>; a test fails if a committed app.g.dart is stale. The code of fsp size is cli/src/size.rs and its tests cli/src/size_tests.rs, over an excerpt of a real build in cli/tests/fixtures/shop-build/main.dart.js.

Benchmarks. The numbers of Performance come from synthetic apps in cli/src/bench.rs; re-run them with cd cli && cargo test --release bench -- --ignored --nocapture --test-threads=1. At 5,000 routes, cold, walking the folders and reading the files takes 82 ms, parsing 208 (spread over the cores), resolving 29, emitting 153 (the model 50, minijinja 105) and writing 6; dart format, when on, takes 1.4 s at 500 routes, 5.3 s at 2,000 and 14 s at 5,000.

The pub workspace. The packages that claim Flutter 3.35 are one pub workspace (the root pubspec.yaml): flutter pub get in any of them resolves all of them together, and their git dependencies on each other resolve to the checkout. The DevTools extension, fespalier_keypair, fespalier_keypair_dpop, fespalier_sign_keypair (all three claim Flutter 3.44), fespalier_download_background and the examples are outside it and resolve on their own. Melos runs the scripts: dart run melos run check (what CI runs in each package), dart run melos run deps:outdated and dart run melos run deps:upgrade (every pubspec moves together; cli/tests/versions.rs then checks that each third-party dependency is spelled one way everywhere). The floor job copies each package out of the workspace first (scripts/floor_copy.py), so its flutter pub downgrade is that package's own. The floor job then writes each package as an app of its own (scripts/floor_app.py --only), and the floor-app job writes one app with every package (scripts/floor_app.py, from the same list), and in each: flutter pub downgrade, flutter test of a file that imports every public library (each lib/*.dart) and of one that imports the Dart code of every plugin the app resolved for Android, iOS, Linux, macOS and Windows (what a build's plugin registrant compiles), and a web build. It does not run flutter analyze. Why an app, and not the package's own floor: a package's own floor also resolves its dev_dependencies (storage's platform dev dependency hid platform 3.0.0), and its tests compile only what they import; an app resolves only the regular dependencies, and a build compiles the plugin registrant and, on the web, every import. And why each package alone as well as all together: one package's constraints steer the whole union, so it hides the next one's regression. That is why fespalier_storage, fespalier_auth and fespalier_download_background list platform and file, fespalier_dio, fespalier_auth and fespalier_cratestack list dio_web_adapter, fespalier_sentry lists win32 and fespalier_connectivity lists web, none of them imported: at the lowest versions an app would pick platform 3.0.0 (it reads Platform.packageRoot, gone from Dart 3), file before 6.1.3 (File.create has no exclusive: on Dart 3.9), dio_web_adapter 1.0.0 (it calls a debugLog that dio 5.7.0 lacks), win32 before 5.5.3 (it uses UnmodifiableUint8ListView, gone from dart:typed_data) and web 0.3.0 (JSObject cannot be a supertype), which do not compile. cli/tests/versions.rs keeps a package that depends on one of those plugins or on dio from forgetting its constraint. just floor skips the web build when the SDK has no web support; CI sets FSP_REQUIRE_WEB=1, which fails instead.

What CI runs. CI (.github/workflows/ci.yml) runs just ci's steps:

  • cargo fmt --check, clippy and the tests, cargo deny check, and fsp check on the examples.
  • dart format, flutter analyze and flutter test on the package, the DevTools extension and every example. The packages that claim Flutter 3.35 are one pub workspace: each package's job runs flutter pub get in its folder, which resolves the whole workspace at the root (one resolution), and a workspace job runs dart run melos bootstrap and dart run melos list; the floor job runs each package in a copy outside the workspace, and each alone as an app, and the floor-app job runs the app with every package (see above).
  • A devtools job builds the extension again and fails when the committed build in packages/fespalier/extension/devtools/build is not what its source builds to, then runs devtools_extensions validate (scripts/build-devtools-extension.sh --check). After touching packages/fespalier_devtools, lib/src/devtools/protocol.dart or the Flutter version in ci.yml, run just devtools-build and commit the result.
  • A scaffold job writes every file kind with fsp new and fsp init and checks the result with flutter analyze and dart format. It gives that app a guarded route with a data.dart and an action.dart, builds it for profile and for release, and checks that the release build holds none of the DevTools code (the traceGuard, traceData and watchData wrappers included). It also runs dart run fespalier against a freshly built fsp. It also runs types_tests::follows_typedefs_as_the_analyzer_does with FSP_REQUIRE_DART=1, so the check of fsp's typedef following against dart analyze cannot skip.
  • A create job (and create-cases, which asks the binary for the cases with fsp create --list-features --json) makes the base app and each optional feature with fsp create, with this checkout as the packages, and runs dart format, flutter analyze and flutter test in each. just create-check runs the same script (scripts/check-create.sh, from one case to all of them: just create-check base); just ci does not, because it needs the network. The hidden flag --local-packages <checkout> is what makes the apps depend on the checkout (path: dependencies and the dependency_overrides the companions need) instead of the release tag; it is for CI and contributors, and not part of the documented interface. The feature table is cli/src/create/recipes.rs; cli/src/create_tests.rs checks it against packages/ (every package is a feature's companion or says why it is not) and the plan goldens are under cli/tests/golden/create/ (FSP_UPDATE_GOLDEN=1 cargo test create_tests::). pubspec.yaml, lib/app/startup.dart, lib/app/app.dart and lib/main.dart are written by one composer, cli/src/create/compose.rs, from the features' fragments (a feature's Startup in the table); a feature never has a template for them (a feature that changes MaterialApp.router, as i18n does, names a whole app_template, at most one per app; assets, smoke_skip and android_min_sdk fill the pubspec's assets: and test: skip: and the Gradle edit of create/android.rs). The auth case needs the keychain in an in-memory store wherever a test starts the whole app (flutter_secure_storage is a dev dependency for setMockInitialValues), which storage_test.dart does. The all case is --features all, the telemetry case is otel and sentry together (the pair that has to compose: one sink slot, one zone), and the adaptive case is made with --template tabs (create_tests.rs pins that the two are the same app). A create-floor job makes the base app, all, otel and telemetry on the floor Flutter (FSP_CREATE_SKIP_FORMAT=1 leaves out dart format, whose output depends on the SDK): all is every feature that Flutter runs, and every feature runs on the floor since 0.16.0 (otel was the one that needed a newer Flutter, 3.35, because otel_zone needs Dart 3.9). The http and download cases need no network and no server: their tests use FakeHttpClient and FakeDownloadBackend, and --local-packages overrides fespalier_http for download though no feature names it (COMPANION_DEPS). A recipe's flutter_floor is what fsp create refuses below, and a new feature above the floor (3.35) needs its own step there.
  • A cose job builds the Rust server of examples/cose (a crate of its own: cargo fmt, clippy, tests, a locked build and cargo deny with examples/cose/server/deny.toml, on the toolchain in examples/cose/server/rust-toolchain.toml, which a test keeps equal to cli/rust-toolchain.toml) and runs the app's tests against the binary with FSP_REQUIRE_COSE_SERVER=1, so the end-to-end test cannot skip. just cose runs the same steps; just ci does not (a cold build compiles CrateStack). After changing server/schema.cstack, FSP_UPDATE_GOLDEN=1 cargo test --test contracts in examples/cose/server rewrites contracts.json and lib/src/contracts.g.dart.
  • A cratestack job builds the Rust server of examples/cratestack (a crate of its own, on the toolchain in examples/cratestack/server/rust-toolchain.toml, which a test keeps equal to cli/rust-toolchain.toml: cargo fmt, clippy, the verifier's tests and the router's, a locked build and cargo deny), installs the cratestack CLI from the CrateStack commit scripts/cratestack-client.sh names (the commit of the v0.17.2 tag, the release the server pins), fails when the committed examples/cratestack/client differs from what it generates, and runs the app's tests against the binary with FSP_REQUIRE_ORDERS_SERVER=1, so the end-to-end test (Dart signs, Rust verifies) cannot skip.
  • A frb-cose job (since 0.21.1) builds the example's Rust server and its Rust core, each with the gates of its own crate (cargo fmt, clippy, tests, a locked build and cargo deny, on the toolchain its rust-toolchain.toml names: the two must name the same channel). It regenerates the flutter_rust_bridge bindings with the pinned codegen and fails on any difference, then runs the app's tests against the server's binary with FSP_REQUIRE_FRB_COSE_SERVER=1, so the end-to-end test cannot skip. The dart job runs the app's tests too, with the Rust toolchain installed for the build hook. Both run on Linux only.
  • The VS Code extension compiles and its tests run.
  • post-publish-smoke.yml runs after a release is published, not on pull requests: it installs the released fsp with install.sh and makes the base app and --features all with it, against the release's tag (no --local-packages). Not a gate; see Releasing.
  • The Homebrew and Scoop rendering, checksum pinning, release staging and the manual run of Release (the recovery for a release PR merged before its pins; its validation steps run out of release.yml in a throwaway repository) are tested (python3 scripts/test_packaging.py, python3 scripts/test_pin_checksums.py, python3 scripts/test_verify_staged.py, python3 scripts/test_release_assets.py, python3 scripts/test_release_dispatch.py).
  • The telemetry stack's files and dashboards are checked (python3 scripts/test_telemetry.py): the generated dashboards are fresh, every query uses only the telemetry conventions, compose.yaml pins its images, and the dashboard importer runs against a fake OpenObserve.
    • The telemetry-smoke job (just telemetry-smoke) runs the whole stack in Docker, sends a seeded session and runs every panel's query in OpenObserve and Grafana.
    • After touching scripts/telemetry/dashboards.toml, run just telemetry-dashboards.
    • To bump an image pin, edit the tag, resolve the digest with docker buildx imagetools inspect <image>:<tag> and run just telemetry-smoke.
  • The agent skills in skills/ cover every README and docs/ section, file kind, config key and fsp command (node scripts/skills/verify-coverage.mjs, which also checks every link and anchor of the docs; see skills/README.md).
  • The version agrees everywhere it is spelled out (cli/tests/versions.rs):
    • cli/Cargo.toml, packages/fespalier/pubspec.yaml, packages/fespalier/extension/devtools/config.yaml, .release-please-manifest.json, the ref: that fsp init prints, and the READMEs', the docs pages' and the skills' ref:, --tag and FSP_VERSION;
    • each of them is annotated for release-please and listed in release-please-config.json;
    • the release workflows' own version readers, scripts/read-version.sh, still find each one;
    • release_checksums.dart pins nothing, or a version no newer than the package's.

You do not bump any version: release-please does (see Releasing).

Three more jobs build examples for the web in a throwaway copy (scripts/web-copy.sh), outside just ci:

  • web checks that each deferred page is a chunk of its own (just web-chunks): it builds examples/shop, runs fsp size --check against the budgets in its pubspec, and cross-checks the attribution with strings that only each deferred page contains.
  • web-routes replays the committed Maestro flows (since 0.8.1) in a pinned Chromium, with every request that is not to the local server blocked (just web-routes; ci/web-routes/). It builds the shop as a release build with --no-web-resources-cdn (the examples have no web/ folder), serves it, and reads the very same YAML: launchApp, openLink, then it waits for the element whose flt-semantics-identifier is the flow's id:, within the flow's timeout. That is not Maestro (the browser is the Chromium that a pinned Playwright installs), so it gates every pull request: it is a required check on main (Releasing, "Repository settings"). What it proves is what fespalier answers for: the identifier reaches the web DOM, ensureWebSemantics() ran, the link opens the route, and its guards, data and deferred chunk let the page build in time. It needs Flutter and Node and takes about two minutes. scripts/check-web-routes.sh <example> with ci/web-routes/ is a template for an app's own CI.
  • examples-web opens every route without parameters of every example (just examples-web; ci/web-routes/examples.json, one job per example). Each builds its example as a release build with --no-web-resources-cdn, serves it, and opens each URL of its entry: the URL's text must show, with no uncaught error and no console error (a broken widget is a grey box in a release build, and its exception is what the console shows), and no request to anything but the local server. What a browser cannot show here (a map drawing, a device plugin, a real service) is in the entry's skip or allow with its reason, and an allow that matches nothing fails the run. cli/tests/examples_web.rs fails when a route without parameters is neither opened there nor skipped. It needs Flutter and Node and takes two to five minutes per example. The screenshots the READMEs show are shots of the same spec, made by just example-screenshots (Chromium and Docker, not in CI: the pixels depend on the Chromium build, so no CI job compares them); just example-images checks in just ci that each one is there, of its size, within the byte budget, shown by its README, and made from the current spec and not edited since (ci/web-routes/example-shots.lock.json, written by just example-screenshots). It cannot see a page that changed with its spec unchanged: run just example-screenshots <name> after changing what an example shows.

One more browser job builds no example: fetch-adapter (since 0.25.0; just fetch-adapter) compiles packages/fespalier_dio/test/browser/harness.dart with dart compile js and dart compile wasm (it imports no Flutter), serves it with packages/fespalier_dio/tool/stream_server.dart, and runs it in Playwright's Chromium, Firefox and WebKit (ci/web-routes/fetch.mjs): over Dio's fetch adapter and over BrowserClient, an event stream must arrive as it is written, a cancel must reach the server, and headers, statuses, bodies and credentials must come through (HTTP clients). It needs Flutter and Node and takes about three minutes.

maestro-web.yml runs real Maestro 2.11.0 on the same build weekly and on demand; a red run there is not a failed pull request, and it is not a required check. Maestro's web driver follows Chrome and has broken on Chrome upgrades (its changelog has Chrome-version fixes in 2.1.0, 2.2.0 and 2.9.0).

Built on ​

It's built on existing libraries rather than hand-rolled parts:

What the tests cover ​

The generator tests cover:

  • Parsing and binding: every binding rule and contract error, query parameters, (group) folders and route order, tab layouts, navigators and shells, transitions, all three data forms, section data, nested not_found.dart, the typed helpers, guards and redirects, extra for pages, layouts and guards and extra_codec.dart.
  • Output: scaffolding, the generated main() (which files make it, every shape of lib/app.main.g.dart, every diagnostic of the three root files), the route manifest, meta.dart (and meta_unique) and restoration ids, match / dataAt.
  • Paths: typed catch-alls, enum segments, per-folder case, localized paths (spellings, non-ASCII, collisions, and route.dart paths edits in the incremental test), routes that leave the page above (nest = false).
  • Deferred routes: the route.dart switch and what it inherits, the deferred as imports and views, preload, the type rule.
  • Checks: string paths that match no route (the lint, its matching, mount point and ignore comments), fsp size (dart2js's table of deferred parts read from a real build's main.dart.js, own and shared bytes, the stale-build checks and the size: budgets), that the committed outputs are up to date, and that watch's incremental runs equal a from-scratch gen after random edits (enum files outside the app folder included).

Clippy is clean. The Flutter tests cover the package, the DevTools extension, the OpenTelemetry adapter and the examples shop, features, tabs, minimal and telemetry; the example tests drive the generated router through every file kind.

Released under the MIT License.