fluframe 1.9.0
fluframe: ^1.9.0 copied to clipboard
Generate production-ready Flutter apps from the fluFrame boilerplate — Riverpod 3, go_router, localization, theming, dio, and tests wired out of the box.
Changelog #
Unreleased #
1.9.0 #
Improvement cycle 6. Two of these change what a shipped app does; the rest close gaps that had no test or no owner.
Why a minor. Nothing here breaks the public contract in
docs/versioning.md — no CLI option is renamed or removed, no argument
value is refused, the metadata schema is unchanged, and template internals
are explicitly free to evolve. A patch would have satisfied the contract.
It is a minor because the authentication behaviour of a generated app
changes: an unconfigured backend that used to sign anyone in now refuses
to, and that is something an app owner should learn from the version
number rather than from the diff. Maintainer decision, 2026-09-11.
A backend you selected but never configured now refuses to sign anyone in #
--backend supabase/--backend firebasefail closed in release and in theprodflavor. The generated app falls back to the in-memory auth fake when its backend URL is empty, so a freshly generated app is usable before you have keys. That constant folds at compile time, so a release build made without--dart-define-from-filegot the same fake — which accepts any email with any six-character password — with no crash, no visible difference, and no log, because a release build has nodart:developersink to report into. The fallback is now gated onfailClosedWhenUnconfigured(kReleaseMode || isProdFlavor), and an unconfigured release refuses every sign-in with a message naming the missing build flag. Debug and profile builds are unchanged.
upgrade no longer records an unreadable conflict as resolved #
- A conflicted file that cannot be read counts as still conflicted. If
a file left carrying conflict markers became unreadable between runs — an
editor holding it open, a re-save in UTF-16 — the check that looks for
markers treated "cannot read" as "no markers", so
upgrade --applyreported "Conflicts resolved", recorded the new version, and the already-up-to-date short circuit then refused to merge again. The file kept its<<<<<<<markers with no way back except hand-editing.fluframe.json. The re-run now names the file and stays in progress. - The
--fromhelp and the no-metadata error say "before 1.0.0", which is when.fluframe.jsonactually started shipping. They said "0.14.0", a version that was never published, so following the advice literally could only fail at bundle fetch.
Template #
- HTTP timeouts are configurable.
API_TIMEOUT_SECONDS(default 10) joinsAPI_BASE_URLinenv/dev.jsonandenv/prod.json, andapiTimeoutincore/config/app_config.dartfeeds dio's connect, send and receive timeouts. Pointing an app at a slower backend no longer means editingapi_client.dart, whichupgradewould then report as a conflict on every later template change. - Wide viewports are documented. The 840 dp
ContentWidthcap that centres content on desktop and web is now named in the app README, with the constant to change.
Publishing #
- The published package no longer carries maintainer-only code.
template_sync.dart,bundle_hygiene.dartandexample_drift.dart(~34 KB) only run against a full monorepo checkout, and were uploaded in every release — and re-downloaded by everyupgrade, which uses the published archive as its merge base. - A symbolic link under
template/stops the bundle sync. Following one would copy its target's bytes into the archive under the link's own name, which both publish guards — a path filter and a filename scan — would pass.
Internal #
- New gates so these stop being re-audited by hand: parser-level tests for
every
createoption, aCreateCommandgenerator seam, ARB parity that compares placeholders rather than key sets, a per-key boot-failure isolation test,doctor's missing-Dart branch, the POSIX half of the shell boundary,.pubignorereach and anchoring, tracker citations in the addon sources, and the examples' own codegen drift and coverage floor. CI now also compiles the CLI at its declared SDK floor (3.12.0), and a nightly job checks the addon dependency pins against pub.flutter-io.cn. Upgrader.runis 355 lines instead of 596, split into metadata read, pending-upgrade resume, and apply-and-record. No behaviour change.
1.8.0 #
Two improvement passes (cycles 4 and 5), led by a behaviour change
every platform sees. create's output path now refuses seven
characters it used to accept, which under the versioning contract
(point 7, added this release) makes this a minor.
create refuses shell metacharacters in --output-directory — on every platform #
- The path may no longer contain
&|<>^"or%. Exit 64, and the message names the character it found. On Windows thefluttertool is a.batshim, socreatemust go throughcmd.exe— which splits the command line at a metacharacter before Flutter ever sees it.fluframe create my_app -o "C:\dev&tools\projects"— the user quoting their own path correctly — reached cmd as two commands: the first wasflutter create C:\dev, scaffolding over that directory if it existed, and the leftover fragment exited with the same code a missing Flutter returns, so the CLI reported "Flutter SDK not found" while a directory you never named was being rewritten. - The refusal is deliberately not gated on Windows. A POSIX path
containing
&worked before and is refused now — a command documented for a mixed-OS team must not scaffold cleanly for one developer and silently overwrite for another. Escaping was tried first and abandoned:^is honoured only outside quotes, dart:io adds quotes exactly when the argument also contains a space, and%cannot be escaped forcmd /cat all. --platformsvalues are now validated by the parser against the six platformscreatecan generate, instead of being passed through.gitno longer runs through the shell on Windows. Only theflutteranddart.batshims needcmd.exe; routinggitthrough it madePATHEXTapply, so agit.batsitting in the project directory could answer thegit status --porcelainthatupgraderuns before a destructive--apply. Now only a realgit.execan.
add feature rollback is fault-tolerant #
- A rollback that cannot remove one of its own half-written files (an editor or antivirus holding it open) no longer dies mid-rollback with a stack trace: it finishes the paths it can, then names exactly which paths could not be put back and how to restore them. Exit 74.
Failures on your files are reported as sentences, not stack traces #
- A successful
upgrade --applycan no longer end in "This is a bug". The one unguarded write in the apply loop was the.fluframe.jsonbookkeeping write at the very end: a locked or read-only file there crashed the run after your tree had been fully and correctly upgraded. It now says the merge succeeded, names what could not be written and why, and explains that a re-run will record the version. - An unreadable project file is named, not reported as a CLI bug.
.fluframe.json,pubspec.yaml,app_router.dartor an ARB that cannot be read — a UTF-16 save from an editor's encoding dropdown, a permission problem, an antivirus lock — used to surface as "This is a bug. Please report it" with a stack trace. Each site now answers with the file's path and the OS reason. create --backend supabasewrites the new dependency inside thedependencies:block instead of after its trailing blank line, so the generatedpubspec.yamlno longer reshuffles on the firstflutter pub add.
Template and examples #
- The example todo app no longer wraps its scrollable list in a width clamp (dead scroll gutters on wide windows), and a stored todo list that fails to decode no longer locks the user out of their own todos — the malformed blob is set aside and the list starts fresh.
- Two issue-tracker citations no longer ship into every generated app's source comments.
template/README.mdno longer claimsadd featurescaffolds adomain/layer (it writesdata/andpresentation/), and the layer-layout paragraph matches the tree that actually ships.- A settings change the store refuses is now reported. Theme mode, color preset and language changes used to be written fire-and-forget: a failed write showed nothing and the preference silently reverted at the next launch. The screen now shows the generic error snackbar and logs the failure, following the same pattern the todos and profile screens already used.
- The template's Dio client sets
sendTimeoutalongside its connect and receive timeouts — the error mapper always handled it; now the timeout actually exists, so a stalled upload fails in 10 seconds instead of hanging.
Dart library surface (not part of the versioned contract) #
fluframe's public contract is the executable (docs/versioning.md); the
package's Dart library surface was never documented or consumed, and two
internal refactors touched it:
package:fluframe/fluframe.dartno longer re-exports eightsrc/libraries — the file remains as the package-resolution anchor only. If you were importing the CLI as a library, open an issue describing the use case.- The bundle download/extract internals now return an ownership-aware
BundleCheckout(which is howupgradestopped leaking one extracted bundle directory into the system temp per run — dry runs included).
1.7.0 #
Two improvement passes over the CLI and the template, and 46 findings
worked between them. Nothing in the public contract moved: no command
or flag changed meaning, .fluframe.json is still "schema": 1, and a
generated app is still generated the same way — so this is not a major.
But upgrade now refuses three things it used to accept, and one
add feature failure exits on a different code, which is why it is not
a patch either. Every one of those refusals names a value fluframe
itself never writes: they are for a hand-edited or hand-carried
.fluframe.json, not for the one create left in your app.
upgrade refuses three more .fluframe.json values #
- A
cliVersionnow has to be a version, not merely contain one. The parser behind that check is shared withdoctor, which reads whole tool banners and so deliberately finds a version anywhere in its input — as a gate, that accepted anything with one inside. The recorded value is spliced into the pub.flutter-io.cn URL the old bundle is fetched from, so acliVersionof1.0.0/../../../other_pkg/versions/1.0.0passed on its leading1.0.0, and dot-segment removal turned the request into a different published package's archive. None of those bytes could reach your tree — writes are keyed on the local template — but the archive becomes the merge base, and a base equal to your own files makes every file report as unchanged:--applywould then record the app as upgraded having received nothing. Exit 64, the same code an unparseable version already gave. - A
namethat is not a package name is refused — including oneupgradeguessed. The recorded name is joined onto a scratch directory to rebuild the merge base, andpackage:path's join discards everything before an absolute part, so an absolute or climbing name aimed that write outside the scratch tree — on the default dry run. It now goes through the same validator your name goes through oncreate, and so do the two fallbacks, thepubspec.yamlname:and the directory basename, which are guesses that were never checked at all. Exit 65, and the message says which of the three it read. pendingConflictsentries have to be paths inside the app. They are joined onto your project root to look for conflict markers, so a climbing or absolute entry pointed that check at a file outside it. Both separators are rejected whatever the host is, so an entry written on Windows cannot decode cleanly on Linux and travel on from there. Exit 65.
The bundle upgrade downloads is bounded, and fails clearly #
- The download is capped as it arrives, not once it is held. 1.6.0 put an 8 MB ceiling on what an archive may inflate to — but that ceiling sat behind a read that accumulated the whole response body first, and the only bound on that was the 60s deadline, which on a fast link admits several hundred MB resident before the digest check or the ceiling gets a say. Both bodies now carry a transport cap — 8 MB for the archive, 1 MB for the version document — refused on the declared length when there is one, and on a running total when there is not, which is the case that matters: a chunked response declares nothing.
- The inflate ceiling is counted now, not believed. gzip states its uncompressed length modulo 2^32 (RFC 1952), so 4 GiB and 100 bytes of content declares 100 — which cleared the pre-inflate check, and then cleared the post-inflate comparison too, with the 4 GiB already resident. The declared size is now only a cheap early reject; what holds the line is a counter over the decoder's output that stops the moment it passes the ceiling, whatever the trailer claimed.
- A version document that is not a version document is named as
such. A 200 whose body is valid JSON but not an object — what a
proxy or a captive portal answers with — met a bare cast and raised a
TypeError, which is anErrorand so slid straight past theFormatExceptionhandler written for it: you got "This is a bug. Please report it", a stack trace and exit 70 for someone else's network. Same for anarchive_urlorarchive_sha256that arrives as something other than a string. Both are reported as data problems now. The document's redirect chain is origin-checked as well, the way the archive's already was — symmetry rather than a hole closed, since that first request is always https://pub.flutter-io.cn. - A malformed
addons.jsoninside a downloaded bundle falls back instead of crashing.upgradedocuments dropping back to this CLI's own addon definitions when a bundle's registry cannot be read, and it caughtFormatExceptionto do exactly that — but a missing key, a wrong-typed field or a non-object section raisedTypeErrorinstead, so for that whole class of inputs the documented fallback was unreachable. Every field is read typed now, and the fallback happens.
fluframe add feature #
- A failure that put every file back reads as one. The scaffold has two failure arms, and they were inverted: the incomplete rollback — the worse outcome — printed its sentence and exited 74, while the one that restored every file rethrew into the runner's "This is a bug" report, stack trace and exit 70. The better outcome was the one demanding a bug report for a full disk or a read-only file. It now exits 74 too, with a sentence saying nothing was changed. Anything that is genuinely not an exception still reaches the crash handler, because that one really is ours.
Generation #
sortImportsno longer splits a directive it should move whole. Every.dartfilecreatecopies out of the template,add featurescaffolds, andupgraderebuilds its merge base from goes through an import sort. It worked a line at a time, so a directivedart formathad wrapped onto a second line — which it does once a combinator pushes one past 80 columns — was sorted away from its own tail, and a directive carrying a trailing// ignore: ...comment read as unterminated and swallowed everything up to the next line ending in;, a statement out of the function below it, sorted in among the imports. Either one writes Dart that does not parse. Neither ever fired. No template file carries an import of either shape, and all 20 published bundles were downloaded and checked — 1,182.dartfiles, 4,888 import lines — before this was sized: it is a trigger being made impossible, not a break being repaired.
Generated apps #
All four reach an existing app through fluframe upgrade.
- A cache write that fails no longer fails the fetch. The offline
cache write shared the
tryaround the fetch it decorates, whose only handler is for a network failure — so a store that refused the write (a full disk, alocalStoragequota, a platform-channel error) threw after the posts had already arrived, and the controller turned posts it was holding into a full-screen error. Caching is additive: a write that did not happen is a future cache miss, not a present failure, and it is logged rather than raised. The offline read is guarded the same way, so a store that throws there leaves the real network error standing instead of replacing it. - Signing out clears the session even when the repository throws —
and says so. The session was published only after the repository
returned, so a
signOutthat failed left the app signed in with no way out of it. The clear now happens in afinally. The failure is no longer dropped either: the profile screen awaited nothing, so the error went to the zone handler, a release build showed nothing at all, and the button read as dead. It now shows the error message and logs the cause. - A blank stored locale tag follows the system locale. An empty
string became
Locale(''), whichdart:uiasserts against — caught two layers up, so the app booted on the default anyway, at the cost of a reported crash on every launch. Nothing in the app writes such a tag; the store is simply shared with whatever else you keep in it. --backend firebaseand--backend supabasemap their SDK's exception insignOut.AuthRepository.signOutdocuments throwing the app's ownAuthException, andsignInmapped onto it in both addons — butsignOutforwarded the SDK call bare, so Firebase's and Supabase's own exception types crossed the seam that exists to keep them out. The UI catches broadly enough that you still saw a message; what leaked was the contract, and with it any caller's ability to tell one sign-out failure from another.
1.6.0 #
Everything a downloaded bundle could do to you, and four crashes that
reported themselves as fluframe bugs. A full improvement pass over the
CLI and the template: 33 findings, the 13 highest-value fixed here. No CLI
flag changed meaning — but upgrade now refuses two things it used to
accept and one add feature failure exits on a different code, which is
why this is a minor and not a patch.
The bundle upgrade downloads is now verified #
- The download is checked against pub.flutter-io.cn's own digest. pub.flutter-io.cn
publishes
archive_sha256beside the archive URL; fluframe read past it and relied on the gzip CRC32, which anyone who rewrites the bytes recomputes for free. That archive is merged into your source tree, so it is now refused unless its SHA-256 is the one pub.flutter-io.cn published. - A hop off https is refused, not followed. The archive URL, and any redirect away from it, must be https — or the plain-http registry you named yourself, which is what a self-hosted mirror and the tests use.
- A bundle can no longer write outside your project. Addon patch
targets come out of the downloaded bundle's own
addons.jsonand were joined onto your project root unvalidated, so a traversal path could rewrite an existing file anywhere you can write. Refused when the registry is parsed, and again before the file is touched. - An oversized archive stops before it is inflated, instead of being decompressed into memory first and measured afterwards.
Adds crypto as a runtime dependency: Dart has no SHA-256 in core.
upgrade no longer leaves an app it cannot finish #
- A failed write is a sentence, and not a half-upgrade. One unwritable file — a lock, a read-only file, a full disk — used to escape mid-loop as "This is a bug", leaving some files upgraded, the recorded version untouched, and nothing anywhere to say so. The re-run then merged the same base against already-upgraded content and conflicted on files you never edited. Writes are now ordered so conflict markers land last, and a failure records no upgrade at all, which keeps the re-run a plain re-merge of exactly the files that never landed.
- The scratch tree is deleted. Every run, dry runs included, left two fully reconstructed app trees behind in your temp directory.
- A hand-broken
.fluframe.jsondies as a sentence.backend,errorReportingandanalyticswere still read with bare casts, so a wrong type in any of them printed "This is a bug" with a stack trace — the exact class the validation around them exists to prevent.
Crashes that reported themselves as fluframe bugs #
add featureprints its own recovery instructions. When a rollback cannot put every file back, the scaffold names those files and tells you to restore them before building. That message was being buried under a crash trace, at precisely the moment you needed to read it. The exit code moved with it: that path now exits 74 (EX_IOERR) instead of the runner's catch-all 70, so a script can tell a half-applied scaffold apart from a bug in fluframe.- An ARB that is not a JSON object is now named as such, instead of
raising a
TypeErrorthe CLI reports as a bug in itself.
Generated apps #
- The load-more spinner can no longer hang forever. A wrong-shaped
page-two response raises a
TypeError, which the controller'son Exceptionnever caught: the loading flag stayed set, and the guard at the top of the method turned every later attempt into a no-op — a spinner with no error and no retry. Both the paging and the refresh path now surface it, so the retry the list footer already had appears.
Repo #
- Restored
docs/adr/anddocs/design/, which ten shipped source files,docs/comparison.mdand this changelog already pointed at. - The Gradle cache key for the generated-app Android build hashed files that build never uses, so it could never change: written once, then an exact hit forever, quietly reintroducing the download it was added to avoid.
- Every one of the 20 secret-scan patterns that gates
dart pub publishnow has a fixture, so a pattern that stops matching fails the suite instead of shipping a key. - The settings fallbacks are pinned: theme preset, theme mode and locale are persisted by name, so renaming a value breaks stored settings.
1.5.0 #
Two features, and the audit of everything around them. Two repo-wide audits ran between 1.4.1 and this release; every defect they proved landed here. Backwards compatible — no CLI flag changed meaning.
New #
- Generated apps handle a desktop window. Body content is capped at
840dp — the Material 3 expanded-window floor — via a shared
ContentWidthwidget. Scrollables are capped by padding, not wrapping, so the mouse wheel keeps working across the whole window. Phone layout is untouched, down to widget offsets.add featureemits the cap for you (and skips it in apps generated before it existed, which would not compile otherwise). - CI compiles what generation produces. This repo now builds a real generated app for Android on every push and for iOS and Linux nightly — previously nothing ever ran Gradle, so the template's AGP/Kotlin/Gradle pins shipped untested.
The upgrade path, made honest #
- The merge base now matches your app.
createends withdart fix --apply, which re-sorts imports after the package rename; the upgrader's reconstructed base skipped that, so for most project names dozens of untouched files were reported as "your edits kept" — 25 files for a plainmy_app, more with a backend addon, whose spliced imports and missing pubspec entries diverged the same way. The overlay now sorts imports and writes addon dependencies into pubspec.yaml itself, on every path, so the base is byte-identical to whatcreateproduced. - Resolving a conflict no longer loops. Keeping any of your own code
used to make the re-run re-merge the same base and write markers back
into the file you just fixed. A conflicted
--applynow records the upgrade in progress in.fluframe.json; the re-run finishes it. Works for--fromapps too, which previously dead-ended on "No .fluframe.json found" — about the file fluframe had just written. - An older CLI refuses to downgrade a newer app instead of silently reverting template files and rewriting the recorded version — exit 0, content gone — which is what it did.
--fromreads the package name from pubspec.yaml, not the folder name. A checkout directory likecheckout-2024used to end up in imports as a package that does not exist (and, with a hyphen, could not).
fluframe add feature #
- Multi-word names produce lowerCamelCase identifiers
(
orderHistoryControllerProvider), not snake_case ones the analyzer rejects —flutter analyzewas failing on code the scaffold itself wrote. - Scaffolded files' imports are emitted sorted for YOUR package name.
The hardcoded order was only right for names sorting before
flutter, which most real names do not. - A locale you added (a fourth ARB) gets the new keys too, instead of being silently skipped while the app's own parity test goes red.
- A CRLF working tree stays CRLF: inserting a route no longer rewrites every line of the router and all ARBs to LF.
- The dry run prints the key names that will actually be written.
Sharper failures #
- On Korean/Japanese Windows consoles, a missing Flutter is reported as a missing Flutter — not as "Malformed JSON ... .fluframe.json", which is what a cp949 shell message crashing the strict UTF-8 decoder produced.
- Hand-edited
.fluframe.jsonwith wrong-typed values gets one sentence naming the key, instead of a TypeError labelled "This is a bug". - A
cliVersionthat is not a version ("abc") is named before any network I/O, instead of pub.flutter-io.cn's 400 being relayed as "usually temporary — try again in a minute". - The cleanup command printed after a failed create quotes its path;
unquoted,
rmdir /s /q C:\temp\my folder\appdeletesC:\temp\my. --no-pubwith a backend addon now yields an app whose pubspec already carries the pinned dependencies (carets intact — the oldflutter pub add "pkg:^x"next-step lost the caret on every shell but cmd.exe) and which passesflutter analyzeas generated.
Template #
- The CI workflow shipped into generated apps declares
permissions: contents: readinstead of inheriting the org default, which in legacy-settings organizations is read-write. PostDetailScreen/PostNotFoundScreenand the 404 branch are tested; no widget test can reach the network; a corrupt offline-cache blob is discarded instead of crashing the offline path (reaches existing apps viafluframe upgrade).
1.4.1 #
Six defects, five of them in code that only runs when something has already gone wrong. No CLI surface changed; nothing here needs anything from you but the upgrade.
-
Fixed:
fluframe upgradecould hang forever. The bundle download created a bareHttpClientwith no connect timeout and no deadline on any request, so a connection that is accepted and then goes nowhere — a captive portal, a proxy blackhole — leftFetching the fluframe X template bundle…on screen with no way to tell a hang from a slow link. It now gives up after 10s connecting, 30s on the version document and 60s on the archive, and says which URL and which limit. No retries: a second attempt down the same hole doubles the wait. -
Fixed: a half-downloaded bundle became a merge base. The
archivepackage's gzip decoder does not raise on a truncated stream — it inflates what it can and returns it. So a download that lost its tail turned into a partial template, andupgradethen reported a conflict on a file you never edited, and offered to--applyit. The archive's own CRC32 and length trailer are now checked, which catches a stream cut one byte short that aContent-Lengthcheck would pass. -
Fixed:
fluframe add featureleft your app non-compiling if a write failed. It wrote the feature files, thenapp_router.dart, then the three ARBs — and rolled back by deleting only the feature directories. A failure on any ARB (read-only file, full disk;app_ko.arbgoes last) left the router importing and routing to a class whose file had just been deleted, plus whichever ARBs had already taken the new keys — and the obvious next move, running it again, was refused withapp_en.arb already defines "…". The router and all three ARBs are now restored to their original bytes. -
Fixed: a corrupt offline cache crashed the generated app. The posts cache was decoded with no guard, from inside the handler that exists to serve you something when the network is gone. Malformed JSON surfaced as a parse error instead of "you are offline"; JSON of the wrong shape raised a
TypeError, which is anErrorrather than anException, so the controller never caught it at all. The blob now carries a schema version, every unreadable form is treated as "no cache", and a bad blob is deleted rather than re-parsed on every later read. This one is in the template, so it reaches an existing app throughfluframe upgrade. -
Fixed: the published bundle had no negative space.
sync_templatecopied every overlay entry verbatim, andenv/is one of them — the directorytemplate/.gitignoredocuments as where real secrets go. The package's own.gitignorehides the staged bundle fromgit status, so the local publish fallback could have uploaded a credential that appeared in no diff. The sync now filters againsttemplate/.gitignoreitself, and the finished bundle is rescanned for secret-shaped files before upload. -
Fixed: pub.flutter-io.cn scored 1.4.0 at 150/160.
dart analyzeexits 0 on infos while pana scores them, so oneINFOshipped and cost 10 points. CI now runs--fatal-infos.
Also: PostDetailScreen and PostNotFoundScreen — including the 404
branch with a documented regression history — now have tests in the
template your app is generated from.
1.4.0 #
The bugs a first arrival would actually hit. Nothing here is a new feature; it is the pass you make before telling people the project exists. Backwards compatible — no CLI surface changed.
-
Fixed:
fluframe add featurewrote a broken description into yourapp_en.arb.'Label for the $entry.key screen.'is$identifierinterpolation, so every scaffolded feature landed"description": "Label for the MapEntry(billingTitle: Billing).key screen."in the@-metadata block — the contract with translators. Shipped since 1.3.0. Regenerate the block by hand, or re-runadd featureon a fresh key; the fix does not rewrite what earlier versions already wrote. -
Fixed:
fluframe doctorsaid "All set" without checking your SDK. It printedflutter --versionand compared it to nothing, so a machine on an older stable channel was told it was fine and only found out a minute intocreate, in a raw pub solver error.doctornow reads the floor out of the template's ownenvironment: sdk:— one copy, so it cannot drift — and fails with the version it found, the version required, andflutter upgrade. A constraint it cannot bound is skipped rather than failed: a wrong floor rejects a machine that would have worked. -
Fixed: the generated app never mentioned the CLI that generated it.
README.mdandAGENTS.mdtaught only the manual way to add a feature, and nothing explained the.fluframe.jsonyou were asked to commit. Both now lead withfluframe add feature, and the README documentsfluframe upgradeproperly: dry-run by default,--applyrequires a clean git tree, conflicts come back as ordinary git markers, plus--restore-deletedand--from. -
Fixed: the auth guides reproduced a boot failure the addons exist to prevent. Both told you to initialize the SDK "right after
WidgetsFlutterBinding.ensureInitialized()" — before the error hooks, so a throwinginitialize()escaped into the root zone with no widget tree to show it. A black screen with the error nowhere. The guides now match what--backend supabase/--backend firebaseactually generate, including the configured-or-fallback guard they were missing entirely and the dependency pins the CLI uses. -
Fixed: the English language picker named Japanese in English. It read
System · English · 한국어 · Japanese;app_en.arbwas the only ARB not using the endonym. Now日本語, with a test asserting the literal in all three locales — "all three agree" is also satisfied by all three regressing. -
Fixed: both example apps showed fluFrame's name in Japanese. Their
app_ja.arbstill carried the unrewrittenFluFrame アプリwhile en and ko were correctly renamed. Found by the new value check below. -
Added:
check_example_drift --fixrepairs ARB keys. It used to re-sync every file it owned and report success while the ARBs were still missing the keys the template had just gained — twice in the 1.3.0 milestone alone. It now inserts missing keys with their@-metadata, preserves existing order, never removes a key the example owns, and compares shared values after the rename tokens. A differing value is reported and deliberately left alone, because overwriting a translation is worse than the drift. -
Added: an ARB locale-parity test. Nothing compared the three ARBs to each other.
gen-l10nfalls back to English for a missing key and only warns, so a locale could silently stop being translated with CI green. Six documents also said the template had two locales; all six now say three. -
Security: every GitHub Action is pinned to a commit SHA.
publish.ymlgranted the OIDC scope that mints a pub.flutter-io.cn credential in the same job that ran a third-party action on a mutable@v2tag. Both workflows now default topermissions: contents: read. AddsSECURITY.mdwith a private disclosure channel, and Dependabot for actions and both pubspecs. -
Docs: prerequisites and the
PATHstep that was missing everywhere. No document stated a minimum Flutter version (3.44 / Dart 3.12), and all four entry points saiddart pub global activate fluframethenfluframe ...with nothing about the pub cache not being onPATH— the first failure a newcomer can hit, and the onedoctorcannot diagnose becausedoctoris the command that will not run. The pub.flutter-io.cn usage block also ranupgradeabovecreate; it is now runnable top to bottom.
1.3.0 #
create was the only command you ever ran twice — by starting over.
This release adds the one you run every time the app grows a feature, and
makes the project something you can look at before installing anything.
Backwards compatible. fluframe add feature needs anchors that ship with
this version's template, so an app generated earlier needs
fluframe upgrade first — the command says so and changes nothing until
you do.
-
Added:
fluframe add feature <name> [--tab] [--dry-run]. Scaffolds a feature module into an existing app — repository, controller, screen and two tests — registers its route (or a bottom-navigation tab with--tab), and adds the strings to all three ARBs. It names the keys that still carry the English text rather than letting a silent English string sit inapp_ja.arb.Nothing is written until the whole change has been computed: the name, the app, an existing feature directory, the router anchors and every ARB are checked first, and a failed write removes the feature directory it had already created.
--dry-runprints the plan and writes nothing. Unlikeupgrade, it is not dry-run by default — it only creates new files and makes bounded insertions — and--helpsays so.The scaffold deliberately contains no
freezedmodel: a generated@freezedclass does not compile untilbuild_runnerhas run, and the app must analyze and test cleanly the moment the command exits. The screen points atfeatures/postsfor the real pattern. -
Added: a live demo of a generated app at https://jogyoungjun.github.io/fluFrame/ — the template with zero edits, built for web and redeployed from
mainafter every gate passes. Linked from all three READMEs. -
Template: the posts list is now paginated with infinite scroll, and is the reference implementation for the four things the first attempt gets wrong: appending instead of replacing, one request per frame near the bottom, no representation of "there is no more", and discarding the list when a later page fails.
Riverpod's
ref.invalidatereuses the notifier instance and leaves it mounted, so a page still in flight during a pull-to-refresh will silently append onto the freshly reloaded first one. A generation counter is what prevents that;ref.mounteddoes not.Consequence worth knowing: the offline fallback cache holds page one only, so an offline post-detail lookup now covers the first 20 posts rather than all of them.
-
Docs:
docs/comparison.md— fluFrame againstflutter create, Very Good CLI and cloning a boilerplate, with every claim about the other tools taken from their current sources and the check date stated. It names five cases where you should pick something else. -
Corrected locale claims that went stale when Japanese was added: the READMEs said "4 locales" (there are three, plus a System option) and four places still described the template as English + Korean.
1.2.0 #
Correctness release. A full audit of 1.1.0 found that the two things
fluframe promises — generate an app, then keep it up to date — could both
fail while reporting success. upgrade --apply destroyed every non-ASCII
character in a Korean or Japanese app and printed conflicts: 0; a
generated Firebase app opened to a black screen; doctor said "All set"
a minute before create died. Nothing below is a new feature.
Everything here is backwards compatible. Two behaviours changed on
purpose: upgrade --apply now requires a clean git working tree
(--force opts out), and it exits non-zero when conflicts remain.
-
Fixed: unexpected failures printed a raw stack trace and exited -1. Only
UsageExceptionwas handled, so a corrupt.fluframe.jsonor an unpublished--fromversion dumped a trace with no advice. Each now gets a sentence and a real exit code; genuine bugs still print the trace, after the message. -
Fixed: a malicious bundle could write outside the extraction directory.
templates/../../../probe.txtpassed thestartsWith('templates/')check. Entries are now resolved against the destination and rejected by name, and every failure path cleans up its temp directory. -
Fixed:
upgrade --applyrewrote every line ending on Windows. Line endings are normalized for comparison only; each file keeps the style it had. One non-UTF-8 file no longer aborts the whole run either — it is reported and skipped. -
Fixed:
upgraderan happily in a directory that was not an app. In an unrelated empty folder it reported "unchanged: 65 / added: 3" and exited 0. It now refuses without a.fluframe.jsonor apubspec.yaml, and theunchangedcount means what it says. -
Template: boot survives a storage failure. Four reads ran before
runAppwith no error handling, so one failure meant no widget tree at all. They fall back to defaults and report through the error seam. -
Template:
AsyncValueWidgetcan show a refresh over existing content. Retry looked dead (so users tapped again, firing duplicate requests) and a failed pull-to-refresh discarded the list being read. -
Template: the login button awaits its own work and no longer loses non-
AuthExceptionfailures;authControllerstops resurrecting a signed-out session when it is invalidated. -
Template:
ApiExceptioncarries the backend's response body, so a generated app can read the server's own error code;cancelandbadCertificateare no longer both "unknown". -
Generated apps now ship their own CI workflow and an
AGENTS.mddescribing the app's conventions to a coding agent. Both are rewritten to the project's name. -
Added: an example-drift gate.
examples/had lostcore/logging/error_handlers.dart— the template's crash-reporting seam — while the docs claimed CI meant they "cannot rot". Both are re-synced (and gained the Japanese locale they were missing), andtool/check_example_drift.dartnow fails CI on new drift. -
Coverage is measured for the CLI as well as the template, published to the job summary, uploaded as an artifact, and gated by a floor for the CLI. Addon sources are format-checked, since they ship into user apps.
-
Docs: ADR 0003 (Riverpod without provider codegen) and ADR 0004 (committed code generation) record two decisions the whole product rests on;
docs/architecture.mdno longer claims three rename tokens or an en+ko-only template. -
Fixed:
upgrade --applycorrupted non-ASCII source files. Child process output was decoded with the OS codepage (cp949 on Korean Windows, cp932 on Japanese), so a mergedfind.text('한국어')came back asfind.text('?쒓뎅??— closing quote and all. Every process the CLI launches now decodes as UTF-8, andgit merge-filewrites its result to a file instead of stdout so the merged bytes never cross a pipe. The damage was silent: the run reportedconflicts: 0and exited 0. -
Fixed: a failed merge could blank a file. A hard
git merge-fileerror (binary input, unreadable file) produced empty output that was written over the user's copy. Such files are now reported and left untouched. -
Fixed: unresolved conflicts sealed off the re-run.
--applybumped.fluframe.jsonto the new version even with conflict markers still in the tree, so the next run answerednothing to upgradeand the only way out was hand-editing the metadata. The recorded version now advances only on a clean result, and a run with conflicts exits non-zero. -
Added:
--applyrefuses when it could not be undone. It keeps no backup andflutter createdoes notgit init, so it now requires a git repository with a clean working tree.fluframe upgrade --apply --forceopts out. The check runs before the bundle download, not after. -
CLI unit tests also run on Windows and macOS in CI.
-
Fixed: a failed
createleft you unable to retry. Every failure path now prints where the half-written project is and the exact command to remove it — previously the obvious retry hitDirectory "./my_app" already exists. Aborting.with no hint..fluframe.jsonis written last, so a partial directory never looks upgradable. -
Fixed:
doctorsaid "All set" and thencreatefailed. It now checks that this machine can create symbolic links, which Flutter needs for thewindowsandlinuxplugins in the default platform set. On Windows the fix is Developer Mode; the message says so, and says how to scope the app instead.createrepeats the hint ifpub getstill fails that way. The README quick start now starts withfluframe doctor. -
Fixed: project names that could not work were accepted.
dio,intl,go_router,shared_preferences,firebase_coreand the rest of the generated app's own dependencies were let through, then failed withA package may not list itself as a dependency— after generation had written the whole project. Windows device names (con,aux,nul,com1…lpt9) and leading underscores are refused too, and the rejection message now names the actual reason. A test fails if the dependency list drifts fromtemplate/pubspec.yamlor the addons. -
Fixed: an incomplete template bundle produced a "successful" empty app. The overlay deletes the scaffold's
lib/before copying, so a bundle missinglib,testorpubspec.yamlyielded a project with no source — reported as a warning, exit 0. It is now a hard failure with a reinstall pointer. -
e2e now covers the shipped default platform set (all six), which no test had ever generated, and runs on Windows and macOS.
-
Fixed: generated apps still showed fluFrame's name. The home screen greeted users with "Welcome to fluFrame!" in every locale and the root widget was called
FluFrameApp, because the rewriter only replaces four exact tokens and none of those spellings is one. The greeting now uses theFluFrame App/FluFrame 앱/FluFrame アプリtokens, the widget isAppRoot, and a test walks the real template sources so this cannot come back. -
Fixed:
upgrade --applyresurrected files you deleted. A missing local file was treated as new, so deleted files came back — and a renamed one came back beside its copy, declaring the same class twice. They are now reported asdeleted locally - not restored;--restore-deletedopts in. -
Template: uncaught async errors no longer vanish in release builds.
onPlatformErrorreturnedtrue, suppressing Flutter's default log path, while the app's only sink wasdart:developer— a VM service channel that does not exist in release. -
Template: an unmatched deep link is no longer a dead end. There was no
errorBuilder, so go_router's default error page took over, and its only button navigates to/— a route the app did not define. There is now a localized not-found screen and a/→/homeredirect. -
Template: the offline cache no longer hides server errors. It caught every
ApiException, so a 404 or a 500 was answered with a stale copy andPostDetailScreen's 404 branch could never run. OnlyNetworkExceptionfalls back now. -
Fixed:
--backend firebaseopened to a black screen.Firebase.initializeAppran beforerunAppwith a placeholder that throws untilflutterfire configure— so the very first launch of every generated app died with no widget tree, no red error screen, and nothing in the log. It is now initialized after the error hooks, inside a try/catch, and the app runs on the in-memory auth fake until it is configured.--backend supabasebehaves the same way with an emptySUPABASE_URL, matching how the Sentry and Amplitude addons already stayed inert without their keys. -
Fixed:
--error-reporting sentrylost most of what Sentry does.SentryFlutter.initwas called withoutappRunner, so zone errors were never captured — and the template then overwrote both handlers the SDK had just installed. The app now runs insideappRunner:, the SDK's integrations chain onto the template's handlers, and the hand-rolledSentry.captureExceptioncalls are gone (they double-reported and marked everythinghandled: true). -
Fixed: addon dependencies were installed unpinned.
pub addwith no constraint resolves to whatever is latest, while the injected sources target one major — so an upstream major release would break newly generated apps on its release day. All four addons now pin^major.minor, and a test fails if any addon declares a dependency without a constraint. The constraint is written intopubspec.yamlrather than passed on the command line:flutteris a batch file on Windows, andcmd.exeeats the^. -
Fixed:
--no-pubstill resolved dependencies. It skippedpub getbut ran onepub addper addon anyway. It now installs nothing and prints theflutter pub addline to run by hand. -
Fixed: setup notes sent secrets into a committed file. All three addons told users to put keys in
env/dev.json, which the template's own.gitignoredocuments as committed safe defaults — secrets belong inenv/*.local.json. The notes and the committed placeholders now agree. -
Fixed: an addon could make an app permanently un-upgradable.
upgraderebuilt the merge base by replaying the current CLI's addon anchors against an archived bundle, so moving any anchored template line blocked the upgrade at exit 70 for every app generated with an addon. Bundles now ship their owntemplates/addons.json, and when the addons cannot be replayed at all both sides are rebuilt without them and the report says so. -
Fixed: the injected
firebase_optionsimport landed at an unsorted position (its anchor named an import that stopped being last), as did the Sentry SDK import.dart fix --applyhid both — except under--no-puband on the upgrade path. -
Template: screen-view analytics report the route pattern (
/home/posts/:id), not the resolved path — concrete paths explode dashboard cardinality and would ship path secrets to a third party. The router alsoreads the analytics provider instead of watching it, which would have rebuilt the router and lost the navigation stack.
1.1.0 #
- Template: the settings language picker no longer clips its labels on phone-width screens. Four segments plus a selected-state check icon overflowed a 390pt viewport, wrapping "English" mid-word; the picker now uses the same wrapping chips as the theme-colour section. Regression test pins the labels to a single line at phone width.
- Template:
go_routermoved to^17.4.0, and the committed lockfiles (template and both examples) were refreshed — this drops the retractedbuild_daemon4.1.3 that generated apps previously inherited, and picks upbuilt_value8.12.7. - Docs: README (English and Korean) and the pub.flutter-io.cn page now show screenshots of a generated app — home, the sample REST feature, and the settings tab in dark mode.
1.0.0 #
Stability declaration — fluframe now follows semantic versioning
against a documented public contract (see docs/versioning.md in the
repository): CLI surface, generation guarantees, rename tokens,
.fluframe.json schema, the stackable addon mechanism, and the upgrade
path.
fluframe upgrade [--apply] [--from]— pull template updates into an existing app via a per-file three-way merge (base reconstructed from the pub.flutter-io.cn archive of the version the app was generated with); dry-run by default, git conflict markers, removals reported but never auto-deleted.createnow records generation metadata (.fluframe.json, schema 1: version + addon combo) — the contractupgradereads.--analytics amplitude— wires the analytics seam to Amplitude with an API-key-guarded provider swap; stackable with--backendand--error-reporting.
0.12.0 #
- Template: analytics seam — an
AnalyticsServiceinterface with a debug-logging default, automatic screen-view tracking for every navigation (bottom tabs included), and a sample domain event. Swap one provider to wire a real product-analytics SDK;--analyticsaddons are next on the roadmap.
0.11.0 #
--error-reporting sentry: wires sentry_flutter into the template's error hooks with DSN-guarded initialization (emptySENTRY_DSNkeeps Sentry disabled, so fresh apps run untouched). Stackable with--backend, e.g.fluframe create my_app --backend supabase --error-reporting sentry.
0.10.0 #
- Template: Japanese localization (full
app_ja.arb+ language picker entry) alongside English and Korean;FluFrame アプリjoins the rename tokens so generated apps stay unbranded in every locale. - Template: sixth theme preset (teal).
0.9.0 #
- Template: global error handling hooks —
FlutterError.onErrorandplatformDispatcher.onErrorroute every uncaught error through one documented file (core/logging/error_handlers.dart), the exact seam for wiring Sentry/Crashlytics later.
0.8.0 #
- Template: offline fallback cache for the posts sample — the last successful response is persisted and served automatically when the network fails, demonstrating the repository-decorator pattern with zero new dependencies.
0.7.1 #
- Actually ships the theme color presets announced in 0.7.0 — a release-ordering mistake published 0.7.0 from a commit that did not yet contain them. No other changes.
0.7.0 #
⚠️ Published without the preset feature described below — use 0.7.1.
- Template: five selectable theme color presets (persisted) join the existing light/dark mode — pick Indigo/Emerald/Crimson/Amber/Violet in Settings, survives restarts.
0.6.0 #
fluframe doctor: one command to verify the machine can generate and run fluFrame apps — Flutter/Dart/git probes with actionable fixes and a template-bundle check.
0.5.0 #
--backend firebase: generate an app with Firebase Auth wired into the auth seam. Ships a compile-safeDefaultFirebaseOptionsstub that throws with clear guidance until you runflutterfire configure— honest about the one step that cannot be automated.
0.4.0 #
--backend supabase: generate an app with Supabase Auth already wired into the auth seam —SupabaseAuthRepository, env-based configuration (SUPABASE_URL/SUPABASE_PUBLISHABLE_KEYvia--dart-define-from-file), and setup notes. The generated app's test suite stays green and offline regardless of backend.- Backend addon mechanism (ADR 0001): dependencies via
flutter pub add, bundled addon files, and anchored patches that fail loudly if the template and addon ever drift apart.
0.3.0 #
- Backend-neutral auth scaffold in the generated app (zero new
dependencies): email/password login screen with validation, auth-gated
profile tab, GoRouter redirect with return-path, and a session that
survives restarts — all behind a single
AuthRepositoryseam. - Swap-in guides for Supabase and Firebase auth (one provider
override) in the repository's
docs/guides/.
0.2.0 #
--orgis now validated before anything runs: malformed identifiers (spaces, digit-leading or empty segments) produce a clear usage error instead of a downstreamflutter createfailure.- A missing Flutter SDK now prints a friendly message with the install
guide and exits 69, instead of dying with a raw
ProcessException— on both the direct-spawn and Windows shell paths.
0.1.0 #
- Initial release.
fluframe create <name>scaffolds a production-ready Flutter app:- Riverpod 3 (manual notifiers), go_router
StatefulShellRoutetabs - freezed 3 + json_serializable models, dio with typed error mapping
- Localization (en/ko) via
flutter gen-l10n, Material 3 light/dark themes - Persisted settings (
SharedPreferencesAsyncbehind aKeyValueStore) --dart-define-from-fileflavors, very_good_analysis, unit + widget tests
- Riverpod 3 (manual notifiers), go_router
- Options:
--org,--description,--output-directory,--platforms,--no-pub.