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
9 changes: 7 additions & 2 deletions .github/workflows/dart.yml
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ jobs:


# Get packages that have changes using melos diff
packages=$(melos list --diff="$BASE_REF" --json | jq -c "[.[] | select(.name != \"biomedict_workspace\") | .name]" 2>/dev/null || echo '[]')
packages=$(melos list --diff="$BASE_REF" --json | jq -c "[.[] | select(.name != \"flutter_forge_workspace\") | .name]" 2>/dev/null || echo '[]')
if [ "$packages" = "" ] || [ "$packages" = "null" ]; then
packages="[]"
fi
Expand Down Expand Up @@ -66,7 +66,12 @@ jobs:
melos bootstrap --scope="${{ matrix.package }}"

- name: Format package
run: melos exec --scope="${{ matrix.package }}" -- dart format . --set-exit-if-changed
# Brick templates are mustache, not parseable Dart, and `dart format`
# has no exclude of its own - so filter the paths before it sees them.
run: |
melos exec --scope="${{ matrix.package }}" -- \
"find . -name '*.dart' -not -path '*/__brick__/*' -not -path '*/.dart_tool/*' \
-exec dart format --set-exit-if-changed {} +"

- name: Analyze package
run: melos exec --scope="${{ matrix.package }}" -- dart analyze --fatal-infos
Expand Down
5 changes: 4 additions & 1 deletion melos.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,11 @@ command:
scripts:
lint:
description: "Run format and analyze all packages"
# Brick sources are mustache templates, not valid Dart, so the formatter
# has to skip them - `dart format` has no exclude of its own.
run: |
dart format --set-exit-if-changed . &&
find . -name '*.dart' -not -path '*/__brick__/*' -not -path '*/.dart_tool/*' \
-exec dart format --set-exit-if-changed {} + &&
dart analyze --fatal-infos .
exec:
concurrency: 5
Expand Down
55 changes: 53 additions & 2 deletions packages/nonstop_cli/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,14 +68,15 @@ nonstop create <project-name> [arguments]
| `--output-directory` | `-o` | Output directory for the new project | Current directory |
| `--description` | `--desc` | Description for the new project | "A Melos-managed project for mono-repo, created using NonStop CLI." |
| `--org-name` | `--org` | Organization name for the new project | `com.example` |
| `--defaults` | `-y` | Skip the module picker and take the recommended set | Prompt |

#### 📦 Template Options

<div align="center">

| Template | 🎯 Description | 📁 Structure Created |
|-----------|------------------------------------------------------------------|-------------------------------------------------------------------------|
| `mono` | 🏢 Generate a Flutter application along with mono-repo (default) | Complete mono-repo structure with apps, features, packages, and plugins |
| `mono` | 🏢 Generate a full app skeleton in a mono-repo (default) | App shell, design system, localization and the modules you pick |
| `package` | 📦 Generate a Flutter package for a Melos-managed mono-repo | Flutter package compatible with mono-repo structure |
| `app` | 📱 Generate a Flutter application for a Melos-managed mono-repo | Flutter application configured for mono-repo structure |
| `plugin` | 🔌 Generate a Flutter plugin for a Melos-managed mono-repo | Flutter plugin compatible with mono-repo structure |
Expand All @@ -84,6 +85,38 @@ nonstop create <project-name> [arguments]

#### 💡 Examples

#### 🧩 Modules

`mono` does not stop at empty folders. It asks which platform modules the app
needs and generates them wired together - bootstrap order, DI registration,
route table and all.

```
? Which modules should youtube start with?
❯ ◉ Network - Dio client, auth + logging interceptors, typed errors
◉ Authentication - Firebase Auth (email, Google, Apple) with route guards
◯ Cloud Firestore - Firestore + Cloud Functions wiring and timestamp converters
◉ Push notifications
◉ Analytics
◉ Crash reporting
◉ Feature flags
◉ Developer tools
◉ Dashboard shell
```

Answer non-interactively with `-y` (recommended set) or per module:

```bash
nonstop create youtube -y --no-notifications --firestore
```

Two rules are applied for you, because the code will not compile otherwise:
push notifications pull in `network`, and developer tools pull in
`feature_flags`.

Every project also gets `core`, `di`, `design_system`, `localization` and
`utils` - the pieces every app needs.

<details>
<summary><strong>🏢 Create a mono-repo project</strong></summary>

Expand All @@ -96,16 +129,34 @@ This creates the following structure:
```
youtube/
├── 📱 apps/
│ └── youtube/
│ └── youtube/ # bootstrap, router, splash - no business logic
├── 🧩 features/
│ ├── auth/ # Firebase Auth + route guards
│ └── dashboard/ # bottom-navigation shell
├── 📦 packages/
│ ├── core/ # logger, DI bootstrap, observers, error types
│ ├── design_system/ # Material 3 theme, components, toasts
│ ├── di/ # service locator
│ ├── localization/ # type-safe strings (i69n)
│ ├── network/ # Dio client and interceptors
│ ├── analytics/
│ ├── crashlytics/
│ ├── feature_flags/
│ ├── notifications/
│ ├── developer/
│ └── utils/
├── 🔌 plugins/
├── 📋 analysis_options.yaml
├── 📖 README.md
├── ⚙️ melos.yaml
└── 📄 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
module, run `flutterfire configure` in the app directory - the generated
`firebase_options.dart` throws until you do.

</details>

<details>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,5 +12,7 @@ pubspec_overrides.yaml
.flutter-plugins-dependencies
*.iml
*.lock
# Apps ship a binary; pin their resolution.
!apps/*/pubspec.lock
mason-lock.json
.mason
Original file line number Diff line number Diff line change
@@ -1,20 +1,95 @@
# {{ name.titleCase() }}

{{ description }}
{{{description}}}

[![nonstop_cli](https://img.shields.io/badge/started%20with-nonstop_cli-166C4E.svg?style=flat-square)](https://pub.dev/packages/nonstop_cli)
[![melos](https://img.shields.io/badge/maintained%20with-melos-f700ff.svg?style=flat-square)](https://github.com/invertase/melos)

## Layout

## Technologies at play
```
apps/{{name.snakeCase()}} the application shell: bootstrap, router, splash
features/ vertical slices (ui + state + data)
packages/ shared capabilities
plugins/ native integrations
```

- Dart
- Flutter
- Melos
Dependencies point inward: apps depend on features, features depend on
packages, packages depend on nothing above them.

## Getting Started
### What is already wired

- All apps are in the `/apps` directory.
- All features are in the `/features` directory
- All shared packages are in the `/packages` directory
- All plugins are in the `/plugins` directory
| Module | Role |
| --- | --- |
| `packages/core` | logger, DI bootstrap, route observers, error types, BLoC observer |
| `packages/di` | service locator (GetIt) behind a small interface |
| `packages/design_system` | Material 3 theme, shared components, toasts, loaders |
| `packages/localization` | type-safe strings generated from YAML (i69n) |
| `packages/utils` | dependency-light helpers |
{{#network}}| `packages/network` | Dio client, auth + logging interceptors, typed errors |
{{/network}}{{#analytics}}| `packages/analytics` | event tracking behind a swappable client |
{{/analytics}}{{#crashlytics}}| `packages/crashlytics` | crash and non-fatal reporting |
{{/crashlytics}}{{#notifications}}| `packages/notifications` | FCM, permissions, device-token registration |
{{/notifications}}{{#feature_flags}}| `packages/feature_flags` | Remote Config flags with a widget wrapper |
{{/feature_flags}}{{#developer}}| `packages/developer` | in-app dev tools, reachable by a hidden 5-tap gesture |
{{/developer}}{{#auth}}| `features/auth` | Firebase Auth (email, Google, Apple) + route guards |
{{/auth}}{{#dashboard}}| `features/dashboard` | bottom-navigation shell with starter tabs |
{{/dashboard}}

## Getting started

```sh
dart pub global activate melos
melos bootstrap
```
{{#firebase}}
### Firebase

This project uses Firebase, so configure it before the first run - the
generated `lib/firebase_options.dart` deliberately throws until you do:

```sh
dart pub global activate flutterfire_cli
cd apps/{{name.snakeCase()}}
flutterfire configure
```
{{/firebase}}
### Run

```sh
cd apps/{{name.snakeCase()}}
flutter run --dart-define=BASE_URL=https://api.example.com
```

Every environment value is a `--dart-define` read in
`packages/core/lib/constants/environment.dart`. Add new ones there rather than
scattering `String.fromEnvironment` around the codebase.
{{#firebase}}
To run against the Firebase emulator suite, add
`--dart-define=USE_EMULATORS=true`.
{{/firebase}}
## 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
```

## 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
```

## Where to start

1. Replace the placeholder tabs in `features/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`.
Original file line number Diff line number Diff line change
@@ -1,5 +1,16 @@
include: package:flutter_lints/flutter.yaml

analyzer:
exclude:
# Generated code - regenerate it, do not lint it.
- "**/*.g.dart"
- "**/*.freezed.dart"
- "**/*.i69n.dart"
# Build output and the plugin sources CocoaPods symlinks into the app.
- "**/build/**"
- "**/ios/.symlinks/**"
- "**/macos/.symlinks/**"

linter:
rules:
require_trailing_commas: true
require_trailing_commas: true
Original file line number Diff line number Diff line change
@@ -1 +1,11 @@
# Put all your apps in this folder
# Apps

Runnable Flutter applications. An app wires modules together in
`lib/bootstrap.dart` and composes feature routes in `lib/router/router.dart` -
it should hold almost no business logic of its own.

Add another (an admin build, a white-label variant) with:

```sh
nonstop create app my_other_app -o apps
```
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
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: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});

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

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 {
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() {
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(
child: DesignSystemWrapper(
builder: (context, theme) => MaterialApp.router(
debugShowCheckedModeBanner: false,
title: strings.app.name,
theme: theme,
routeInformationParser: AppRouter.routeInformationParser,
routeInformationProvider: AppRouter.routeInformationProvider,
routerDelegate: AppRouter.routerDelegate,
),
),
);
}
}
Loading
Loading