Skip to content

Commit 18fd905

Browse files
authored
feat(install): add broadcasting support (#6)
* feat(install): add broadcasting support with conditional env vars Add --without-broadcasting flag to install command, broadcasting config stub with Reverb connection, and conditional BROADCAST_CONNECTION/REVERB_* env vars that are only generated when broadcasting is enabled. * chore(release): 0.0.1-alpha.4 * fix(stubs): add missing magic import and fix docs URL in broadcasting config
1 parent 15f604d commit 18fd905

7 files changed

Lines changed: 219 additions & 73 deletions

File tree

CHANGELOG.md

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,18 @@ This project follows [Semantic Versioning 2.0.0](https://semver.org/spec/v2.0.0.
88

99
## [Unreleased]
1010

11-
### 📝 Documentation
11+
---
12+
13+
## [0.0.1-alpha.4] - 2026-04-06
14+
15+
### ✨ New Features
16+
17+
- **Broadcasting Support**: Add `--without-broadcasting` flag to `install` command with `config/broadcasting.dart` stub and Reverb connection config
18+
- **Conditional Env Vars**: Broadcasting env vars (`BROADCAST_CONNECTION`, `REVERB_*`) only generated when broadcasting is enabled
19+
20+
### 🔧 Improvements
1221

13-
- Update homepage URL to Magic website package page (`https://magic.fluttersdk.com/packages/magic-cli`)
14-
- Add "Website" link to README navigation bar
15-
- Update CLI Commands link in welcome view stub to point to package page
16-
- Update documentation issue template placeholder URL
22+
- **Documentation**: Update homepage URL to Magic website package page, add "Website" link to README, update CLI Commands link in welcome view stub
1723

1824
---
1925

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,7 @@ magic install --without-database --without-auth
138138
| `--without-events` | Skip events setup |
139139
| `--without-localization` | Skip localization setup |
140140
| `--without-logging` | Skip logging setup |
141+
| `--without-broadcasting` | Skip broadcasting setup |
141142

142143
<details>
143144
<summary><strong>Generated Structure</strong></summary>
@@ -147,6 +148,7 @@ lib/
147148
├── config/
148149
│ ├── app.dart
149150
│ ├── auth.dart
151+
│ ├── broadcasting.dart
150152
│ ├── cache.dart
151153
│ ├── database.dart
152154
│ ├── logging.dart
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import 'package:magic/magic.dart';
2+
3+
/// Broadcasting configuration.
4+
///
5+
/// Defines the default broadcasting connection and available connections.
6+
/// See: https://magic.fluttersdk.com/docs/broadcasting
7+
Map<String, dynamic> get broadcastingConfig => {
8+
'broadcasting': {
9+
'default': env('BROADCAST_CONNECTION', 'null'),
10+
'connections': {
11+
'reverb': {
12+
'driver': 'reverb',
13+
'host': env('REVERB_HOST', 'localhost'),
14+
'port': int.tryParse(env('REVERB_PORT', '8080')) ?? 8080,
15+
'scheme': env('REVERB_SCHEME', 'ws'),
16+
'app_key': env('REVERB_APP_KEY', ''),
17+
'auth_endpoint': '/broadcasting/auth',
18+
'reconnect': true,
19+
'max_reconnect_delay': 30000,
20+
'activity_timeout': 120,
21+
'dedup_buffer_size': 100,
22+
},
23+
'null': {
24+
'driver': 'null',
25+
},
26+
},
27+
},
28+
};

lib/src/commands/install_command.dart

Lines changed: 39 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ import '../stubs/install_stubs.dart';
3232
/// | `--without-events` | Skip events setup |
3333
/// | `--without-localization` | Skip `assets/lang/` directory |
3434
/// | `--without-logging` | Skip `config/logging.dart` |
35+
/// | `--without-broadcasting` | Skip broadcasting setup |
3536
class InstallCommand extends Command {
3637
@override
3738
String get name => 'install';
@@ -48,11 +49,7 @@ class InstallCommand extends Command {
4849

4950
@override
5051
void configure(ArgParser parser) {
51-
parser.addFlag(
52-
'without-auth',
53-
help: 'Skip auth setup',
54-
negatable: false,
55-
);
52+
parser.addFlag('without-auth', help: 'Skip auth setup', negatable: false);
5653
parser.addFlag(
5754
'without-database',
5855
help: 'Skip database setup',
@@ -63,11 +60,7 @@ class InstallCommand extends Command {
6360
help: 'Skip network setup',
6461
negatable: false,
6562
);
66-
parser.addFlag(
67-
'without-cache',
68-
help: 'Skip cache setup',
69-
negatable: false,
70-
);
63+
parser.addFlag('without-cache', help: 'Skip cache setup', negatable: false);
7164
parser.addFlag(
7265
'without-events',
7366
help: 'Skip events setup',
@@ -83,6 +76,11 @@ class InstallCommand extends Command {
8376
help: 'Skip logging setup',
8477
negatable: false,
8578
);
79+
parser.addFlag(
80+
'without-broadcasting',
81+
help: 'Skip broadcasting setup',
82+
negatable: false,
83+
);
8684
}
8785

8886
@override
@@ -96,6 +94,7 @@ class InstallCommand extends Command {
9694
final withoutEvents = arguments['without-events'] as bool;
9795
final withoutLocalization = arguments['without-localization'] as bool;
9896
final withoutLogging = arguments['without-logging'] as bool;
97+
final withoutBroadcasting = arguments['without-broadcasting'] as bool;
9998

10099
_createDirectories(
101100
root,
@@ -112,6 +111,7 @@ class InstallCommand extends Command {
112111
withoutCache: withoutCache,
113112
withoutLogging: withoutLogging,
114113
withoutLocalization: withoutLocalization,
114+
withoutBroadcasting: withoutBroadcasting,
115115
);
116116

117117
_createStarterFiles(root);
@@ -123,10 +123,11 @@ class InstallCommand extends Command {
123123
withoutNetwork: withoutNetwork,
124124
withoutCache: withoutCache,
125125
withoutLogging: withoutLogging,
126+
withoutBroadcasting: withoutBroadcasting,
126127
);
127128

128129
_patchDefaultWidgetTest(root);
129-
_createEnvFiles(root);
130+
_createEnvFiles(root, withoutBroadcasting: withoutBroadcasting);
130131

131132
_registerEnvAsset(root);
132133

@@ -167,10 +168,7 @@ class InstallCommand extends Command {
167168
];
168169

169170
if (!withoutEvents) {
170-
appDirs.addAll([
171-
'lib/app/listeners',
172-
'lib/app/events',
173-
]);
171+
appDirs.addAll(['lib/app/listeners', 'lib/app/events']);
174172
}
175173

176174
for (final dir in appDirs) {
@@ -209,6 +207,7 @@ class InstallCommand extends Command {
209207
required bool withoutCache,
210208
required bool withoutLogging,
211209
required bool withoutLocalization,
210+
required bool withoutBroadcasting,
212211
}) {
213212
final providerImports = <String>[];
214213
final providerEntries = <String>[];
@@ -239,6 +238,9 @@ class InstallCommand extends Command {
239238
if (!withoutAuth) {
240239
providerEntries.add('(app) => VaultServiceProvider(app),');
241240
}
241+
if (!withoutBroadcasting) {
242+
providerEntries.add('(app) => BroadcastServiceProvider(app),');
243+
}
242244

243245
// Auth providers boot AFTER AppServiceProvider (which registers
244246
// userFactory via setUserFactory). AuthServiceProvider.boot()
@@ -291,6 +293,12 @@ class InstallCommand extends Command {
291293
InstallStubs.loggingConfigContent(),
292294
);
293295
}
296+
if (!withoutBroadcasting) {
297+
_writeIfNotExists(
298+
path.join(root, 'lib/config/broadcasting.dart'),
299+
InstallStubs.broadcastingConfigContent(),
300+
);
301+
}
294302
}
295303

296304
/// Writes the framework starter files that are always created:
@@ -342,6 +350,7 @@ class InstallCommand extends Command {
342350
required bool withoutNetwork,
343351
required bool withoutCache,
344352
required bool withoutLogging,
353+
required bool withoutBroadcasting,
345354
}) {
346355
final mainPath = path.join(root, 'lib/main.dart');
347356

@@ -358,10 +367,7 @@ class InstallCommand extends Command {
358367
"import 'config/view.dart';",
359368
];
360369

361-
final configFactories = <String>[
362-
'() => appConfig',
363-
'() => viewConfig',
364-
];
370+
final configFactories = <String>['() => appConfig', '() => viewConfig'];
365371

366372
if (!withoutAuth) {
367373
configImports.add("import 'config/auth.dart';");
@@ -383,6 +389,10 @@ class InstallCommand extends Command {
383389
configImports.add("import 'config/logging.dart';");
384390
configFactories.add('() => loggingConfig');
385391
}
392+
if (!withoutBroadcasting) {
393+
configImports.add("import 'config/broadcasting.dart';");
394+
configFactories.add('() => broadcastingConfig');
395+
}
386396

387397
final appName = _getAppName(root);
388398

@@ -420,26 +430,29 @@ class InstallCommand extends Command {
420430
return;
421431
}
422432

423-
FileHelper.writeFile(
424-
widgetTestPath,
425-
InstallStubs.widgetTestContent(),
426-
);
433+
FileHelper.writeFile(widgetTestPath, InstallStubs.widgetTestContent());
427434
}
428435

429436
/// Writes `.env` and `.env.example` to [root] if they do not already exist.
430437
///
431438
/// [root] — absolute path to the Flutter project root.
432-
void _createEnvFiles(String root) {
439+
/// [withoutBroadcasting] — when `true`, omits broadcasting env vars.
440+
void _createEnvFiles(String root, {required bool withoutBroadcasting}) {
433441
final appName = _getAppName(root);
434442

435443
_writeIfNotExists(
436444
path.join(root, '.env'),
437-
InstallStubs.envContent(appName: appName),
445+
InstallStubs.envContent(
446+
appName: appName,
447+
withoutBroadcasting: withoutBroadcasting,
448+
),
438449
);
439450

440451
_writeIfNotExists(
441452
path.join(root, '.env.example'),
442-
InstallStubs.envExampleContent(),
453+
InstallStubs.envExampleContent(
454+
withoutBroadcasting: withoutBroadcasting,
455+
),
443456
);
444457
}
445458

lib/src/stubs/install_stubs.dart

Lines changed: 50 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -34,14 +34,11 @@ class InstallStubs {
3434
final imports = configImports.join('\n');
3535
final factories = configFactories.map((f) => ' $f,').join('\n');
3636

37-
return StubLoader.replace(
38-
StubLoader.load('install/main'),
39-
{
40-
'configImports': imports,
41-
'configFactories': factories,
42-
'appName': appName,
43-
},
44-
);
37+
return StubLoader.replace(StubLoader.load('install/main'), {
38+
'configImports': imports,
39+
'configFactories': factories,
40+
'appName': appName,
41+
});
4542
}
4643

4744
// ---------------------------------------------------------------------------
@@ -80,13 +77,10 @@ class InstallStubs {
8077
...authProviderEntries.map((e) => ' $e'),
8178
].join('\n');
8279

83-
return StubLoader.replace(
84-
StubLoader.load('install/app_config'),
85-
{
86-
'allImports': allImports,
87-
'allProviders': allProviders,
88-
},
89-
);
80+
return StubLoader.replace(StubLoader.load('install/app_config'), {
81+
'allImports': allImports,
82+
'allProviders': allProviders,
83+
});
9084
}
9185

9286
/// Generates `lib/config/auth.dart` matching the Uptizm production pattern.
@@ -124,6 +118,11 @@ class InstallStubs {
124118
return StubLoader.load('install/logging_config');
125119
}
126120

121+
/// Generates `lib/config/broadcasting.dart` with Reverb and null connections.
122+
static String broadcastingConfigContent() {
123+
return StubLoader.load('install/broadcasting_config');
124+
}
125+
127126
// ---------------------------------------------------------------------------
128127
// Service Providers
129128
// ---------------------------------------------------------------------------
@@ -180,12 +179,9 @@ class InstallStubs {
180179
///
181180
/// [appName] — the human-readable application name shown in the hero section.
182181
static String welcomeViewContent({required String appName}) {
183-
return StubLoader.replace(
184-
StubLoader.load('install/welcome_view'),
185-
{
186-
'appName': appName,
187-
},
188-
);
182+
return StubLoader.replace(StubLoader.load('install/welcome_view'), {
183+
'appName': appName,
184+
});
189185
}
190186

191187
// ---------------------------------------------------------------------------
@@ -195,20 +191,44 @@ class InstallStubs {
195191
/// Generates a `.env` template file with sensible defaults.
196192
///
197193
/// [appName] — written as the default value for `APP_NAME`.
198-
static String envContent({required String appName}) {
199-
return StubLoader.replace(
200-
StubLoader.load('install/env'),
201-
{
202-
'appName': appName,
203-
},
204-
);
194+
/// [withoutBroadcasting] — when `true`, omits the `BROADCAST_CONNECTION`
195+
/// and `REVERB_*` environment variables.
196+
static String envContent({
197+
required String appName,
198+
bool withoutBroadcasting = false,
199+
}) {
200+
var content = StubLoader.replace(StubLoader.load('install/env'), {
201+
'appName': appName,
202+
});
203+
204+
if (!withoutBroadcasting) {
205+
content += '\nBROADCAST_CONNECTION=null\n'
206+
'REVERB_HOST=localhost\n'
207+
'REVERB_PORT=8080\n'
208+
'REVERB_SCHEME=ws\n'
209+
'REVERB_APP_KEY=\n';
210+
}
211+
212+
return content;
205213
}
206214

207215
/// Generates a `.env.example` template file with empty values.
208216
///
209217
/// Safe to commit — contains keys but no secrets.
210-
static String envExampleContent() {
211-
return StubLoader.load('install/env_example');
218+
/// [withoutBroadcasting] — when `true`, omits the `BROADCAST_CONNECTION`
219+
/// and `REVERB_*` environment variable keys.
220+
static String envExampleContent({bool withoutBroadcasting = false}) {
221+
var content = StubLoader.load('install/env_example');
222+
223+
if (!withoutBroadcasting) {
224+
content += '\nBROADCAST_CONNECTION=\n'
225+
'REVERB_HOST=\n'
226+
'REVERB_PORT=\n'
227+
'REVERB_SCHEME=\n'
228+
'REVERB_APP_KEY=\n';
229+
}
230+
231+
return content;
212232
}
213233

214234
/// Generates a Magic-compatible smoke test for `test/widget_test.dart`.

pubspec.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
name: magic_cli
22
description: "Command-line tools for Magic Framework. Scaffolding, code generation, and project management."
3-
version: 0.0.1-alpha.3
3+
version: 0.0.1-alpha.4
44
homepage: https://magic.fluttersdk.com/packages/magic-cli
55
repository: https://github.com/fluttersdk/magic_cli
66
issue_tracker: https://github.com/fluttersdk/magic_cli/issues

0 commit comments

Comments
 (0)