Skip to content
Merged
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
42 changes: 42 additions & 0 deletions .github/workflows/nonstop-template.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
name: NonStop generated template
on:
pull_request:
paths: ['packages/nonstop_cli/**', '.github/workflows/nonstop-template.yml']
push:
branches: [main]
paths: ['packages/nonstop_cli/**', '.github/workflows/nonstop-template.yml']
permissions:
contents: read
jobs:
contracts:
runs-on: ubuntu-latest
defaults:
run:
working-directory: packages/nonstop_cli
steps:
- uses: actions/checkout@v4
- uses: subosito/flutter-action@v2
with:
flutter-version: '3.44.0'
cache: true
- run: dart pub get
- run: dart analyze --fatal-infos lib test tool
- run: dart test --exclude-tags version-verify
generated:
needs: contracts
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
profile: [full, default, minimal, auth-only, firestore-only, network-only]
defaults:
run:
working-directory: packages/nonstop_cli
steps:
- uses: actions/checkout@v4
- uses: subosito/flutter-action@v2
with:
flutter-version: '3.44.0'
cache: true
- run: dart pub get
- run: dart run tool/verify_template.dart ${{ matrix.profile }}
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,12 @@

A place where Flutter packages are crafted and built.

[NonStop CLI](packages/nonstop_cli/README.md) generates a Melos-managed Flutter
starter with SOLID-oriented boundaries, offline unit/widget tests, a strict
100% handwritten-runtime line-coverage gate and generated CI. Its template is
verified by generating and building real applications, including minimal and
optional-module configurations.

- All shared [packages](packages/README.md) are in the `/packages` directory
- All [plugins](plugins/README.md) are in the `/plugins` directory

Expand Down
13 changes: 12 additions & 1 deletion packages/nonstop_cli/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,15 @@
## Unreleased

- Refactor the mono-repo template around injectable SDK adapters, explicit app
composition and owned asynchronous lifecycles.
- Fix authenticated request retries, response contracts, token refresh races,
remote-config defaults, notification registration and fail-closed guards.
- Generate offline unit/widget tests, a strict 100% handwritten-runtime line
coverage gate, portable quality commands, architecture docs and GitHub CI.
- Verify every module selection structurally and representative fresh projects
through analysis, tests, coverage and web builds.
- Generate from the shipped bundle and use workspace-local Melos for bootstrap.

## 0.0.8+2

- **FIX**(nonstop_cli): boot apps before flutterfire configure.
Expand Down Expand Up @@ -98,4 +110,3 @@

- **FIX**(nonstop_cli): rename nonstop_cli.dart to nonstop.dart.
- **FEAT**(nonstop_cli): initial release of core commands.

51 changes: 49 additions & 2 deletions packages/nonstop_cli/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -152,14 +152,61 @@ youtube/
└── 📄 pubspec.yaml
```

The generated workspace is bootstrapped, formatted and passes its own
`melos lint` and `melos test` before you touch it. If you picked any Firebase
The generated workspace is bootstrapped and formatted, with offline unit and
widget tests and a strict 100% executable-line coverage gate. Run its checks:

```bash
dart run melos run lint
dart run melos run coverage
cd apps/youtube
flutter run
```

Melos is a workspace dependency; no global Melos installation is needed.
If you picked any Firebase
module, run `flutterfire configure` in the app directory when you want those
features live. Until then the app still runs; Firebase-backed modules are
skipped at startup.

</details>

### Template quality and architecture

The default `mono` template separates app composition, feature behavior and
reusable adapters. SDK clients receive explicit dependencies, asynchronous
startup is awaited, and disposable services own their subscriptions and error
handlers. The generated `docs/architecture.md` explains the SOLID boundaries
and how to extend them without coupling UI to infrastructure.

Every runtime package includes tests. `dart run melos run coverage` runs them,
imports otherwise untested libraries, merges cross-package coverage and fails
below **100% executable-line coverage of handwritten runtime Dart**. Only
generated `.g.dart`, `.freezed.dart` and `.i69n.dart` files are excluded; test,
tooling and native platform code are outside this metric. Coverage does not
prove every branch, platform integration or security property. Live Firebase,
push delivery and backend authorization still need project-specific integration
tests. The generated GitHub workflow checks formatting, analysis, coverage and
a web build on Flutter 3.44.0.

Templates are bundled with the CLI release, not fetched from the registry at
generation time. To change the default template, generate an app, implement and
verify the change there, then port it into the brick and rebuild the bundle:

```bash
# From packages/nonstop_cli; requires Flutter 3.44.0 and Mason CLI.
dart pub get
mason bundle bricks/flutter_project_with_mono_repo -t dart -o lib/commands/create
dart format lib/commands/create/flutter_project_with_mono_repo_bundle.dart
dart test --exclude-tags version-verify
dart run tool/verify_template.dart full
dart run tool/verify_template.dart minimal
```

The contract suite checks the source/bundle match and parses all 512 module
selections. End-to-end CI generates fresh `full`, `default`, `minimal`,
`auth-only`, `firestore-only` and `network-only` workspaces and runs their real
checks. The verification tool retains each temporary workspace for diagnosis.

<details>
<summary><strong>📦 Create a Flutter package for a mono-repo</strong></summary>

Expand Down
Original file line number Diff line number Diff line change
@@ -1 +1,15 @@
# flutter_project_with_mono_repo
# Flutter mono-repo template

The default NonStop CLI template: a Melos-managed application, reusable packages
and optional feature/SDK modules. See the [CLI guide](../../README.md) for module
flags and the generate → improve → verify → port → regenerate workflow.

The generated README and `docs/architecture.md` describe the architecture,
configuration and testing contract. All handwritten runtime Dart is subject to
a 100% executable-line coverage gate; generated code, tools and native code are
outside that scope.

After editing `__brick__` or hooks, rebuild the embedded bundle from the CLI
package directory and run its tests and `tool/verify_template.dart`. Do not edit
the encoded bundle directly. Contract tests reject stale bundled source and
invalid Dart across all 512 module selections; CI also exercises fresh projects.
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
name: Template quality
on: [push, pull_request]
permissions:
contents: read
jobs:
quality:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: subosito/flutter-action@v2
with:
flutter-version: '3.44.0'
cache: true
- run: dart pub get
- run: dart run melos bootstrap
- run: dart run melos run lint
- run: dart run melos run coverage
- run: flutter build web
working-directory: apps/{{name.snakeCase()}}
- uses: actions/upload-artifact@v4
if: always()
with:
name: coverage
path: coverage/lcov.info
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,8 @@ packages, packages depend on nothing above them.
## Getting started

```sh
dart pub global activate melos
melos bootstrap
dart pub get
dart run melos bootstrap
```
{{#firebase}}
### Firebase
Expand Down Expand Up @@ -71,25 +71,78 @@ To run against the Firebase emulator suite, add
## Day-to-day

```sh
melos lint # format + analyze everything
melos test # run every package's tests
melos run generate # build_runner where it is needed
melos run generate:i69n # regenerate strings after editing messages.i69n.yaml
dart run melos run lint # format-check and analyze
dart run melos run test # run every package's tests
dart run melos run coverage # tests + strict 100% line-coverage gate
dart run melos run generate # rebuild serializers
dart run melos run generate:i69n # rebuild localization
```

## Adding to the monorepo

```sh
nonstop create package my_feature -o features
nonstop create package my_package -o packages
nonstop create app my_second_app -o apps
nonstop create plugin my_plugin -o plugins
nonstop create my_feature --template package -o features
nonstop create my_package --template package -o packages
nonstop create my_second_app --template app -o apps
nonstop create my_plugin --template plugin -o plugins
```

## Where to start

1. Replace the placeholder tabs in `features/dashboard`.
1. Replace the {{#dashboard}}placeholder tabs in `features/dashboard`{{/dashboard}}{{^dashboard}}placeholder home screen in the app router{{/dashboard}}.
2. Put your product strings in `packages/localization/lib/messages.i69n.yaml`.
3. Set the palette in `packages/design_system/lib/generated/theme.dart`.
4. Add feature modules and spread their routes into
`apps/{{name.snakeCase()}}/lib/router/router.dart`.

## Architecture and testing

The starter separates the app view, routing, startup and foreground lifecycle.
Clients accept injected SDKs, loggers and transports; feature code consumes small
interfaces rather than subclassing platform SDKs. See [architecture](docs/architecture.md)
for the SOLID boundaries and an example of adding a tested feature.

Every generated package has tests. Unit and widget tests run without a Firebase
account or network service; Firebase integration tests use offline platform doubles.
The strict gate imports otherwise-unloaded libraries, runs all suites, merges their
LCOV records and requires **100% executable-line coverage of workspace `lib/` code**.
It includes entrypoints, startup, theme and error paths. Only compiler-generated
`.g.dart`, `.freezed.dart` and `.i69n.dart` files are excluded.

```sh
dart run tool/coverage.dart
# Inspect existing reports without rerunning tests (not a validation run):
dart run tool/coverage.dart --report-only
```

The gate fails for failed tests, missing tests/reports, or uncovered lines.
The merged report is `coverage/lcov.info`. Do not run two coverage jobs in the
same checkout: each owns a temporary `test/coverage_imports_test.dart` fixture,
removed on normal completion. After an interrupted run, inspect and remove only
that generated fixture before retrying.

Coverage is not a proof of correctness, branch completeness, security, or native
plugin compatibility. Keep assertions about behavior, and add device integration
tests for your Firebase project, native permissions, provider sign-in and backend.
The generated GitHub Actions workflow checks analysis, tests, coverage and a web build.

{{#auth}}### Authentication and demo mode

`GoAuthRoute` denies access when authentication is missing or signed out.
{{#dashboard}}The starter dashboard explicitly allows **unconfigured demo mode** so
you can explore it before `flutterfire configure`. It contains no protected data.
Do not use `allowUnconfigured: true` for real protected routes.
{{/dashboard}}Client-side guards are navigation helpers; enforce authorization in
backend endpoints and Firebase security rules.
{{/auth}}
{{#notifications}}### Notifications

The SDK client owns subscriptions, not navigation or toast widgets. The app
supplies callbacks, queues cold-start routes until the router is ready, and owns
foreground lifecycle handling. Device IDs are random per installation and
persisted locally; they are not hardware fingerprints.

Implement the `POST /device-tokens/me` and `DELETE /device-tokens/me/:deviceId`
backend endpoints before enabling token registration in production. Configure
native capabilities, Firebase messaging and your app's permission explanation.
{{/notifications}}
Original file line number Diff line number Diff line change
@@ -1,55 +1,15 @@
import 'package:core/core.dart';
import 'package:design_system/design_system.dart';
{{#notifications}}import 'package:di/di.dart';
{{/notifications}}import 'package:flutter/material.dart';
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:localization/localization.dart';
{{#notifications}}import 'package:notifications/notifications.dart';
{{/notifications}}import 'package:{{name.snakeCase()}}/router/router.dart';

class App extends StatefulWidget {
const App({super.key});
/// The application view. Its owner creates and disposes the router.
class App extends StatelessWidget {
const App({required this.router, super.key});

@override
State<App> createState() => _AppState();
}
final GoRouter router;

class _AppState extends State<App> {
{{#notifications}} late final AppLifecycleListener _lifecycleListener;

{{/notifications}} @override
void initState() {
super.initState();
{{#notifications}} // Asking for notification permission needs a foreground surface, so this
// runs from the first frame rather than from bootstrap. Move it behind
// sign-in once you would rather ask a user who has committed to the app.
_lifecycleListener = AppLifecycleListener(onResume: _clearBadge);
WidgetsBinding.instance.addPostFrameCallback((_) async {
if (!di.has<NotificationClient>()) return;
final logger = di.get<Logger>();
try {
await di.get<NotificationClient>().init();
} catch (e, s) {
logger.e('Failed to initialize NotificationClient', e, s);
}
});
{{/notifications}} }
{{#notifications}}
void _clearBadge() {
if (!di.has<NotificationClient>()) return;
final logger = di.get<Logger>();
try {
di.get<NotificationClient>().clearBadge();
} catch (e, s) {
logger.e('Failed to clear badge on app resume', e, s);
}
}

@override
void dispose() {
_lifecycleListener.dispose();
super.dispose();
}
{{/notifications}}
@override
Widget build(BuildContext context) {
return GlobalEventChannelProvider(
Expand All @@ -58,9 +18,7 @@ class _AppState extends State<App> {
debugShowCheckedModeBanner: false,
title: strings.app.name,
theme: theme,
routeInformationParser: AppRouter.routeInformationParser,
routeInformationProvider: AppRouter.routeInformationProvider,
routerDelegate: AppRouter.routerDelegate,
routerConfig: router,
),
),
);
Expand Down
Loading
Loading