diff --git a/CHANGELOG.md b/CHANGELOG.md index 384f480..1b29a40 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,12 +8,18 @@ This project follows [Semantic Versioning 2.0.0](https://semver.org/spec/v2.0.0. ## [Unreleased] -### 📝 Documentation +--- + +## [0.0.1-alpha.4] - 2026-04-06 + +### ✨ New Features + +- **Broadcasting Support**: Add `--without-broadcasting` flag to `install` command with `config/broadcasting.dart` stub and Reverb connection config +- **Conditional Env Vars**: Broadcasting env vars (`BROADCAST_CONNECTION`, `REVERB_*`) only generated when broadcasting is enabled + +### 🔧 Improvements -- Update homepage URL to Magic website package page (`https://magic.fluttersdk.com/packages/magic-cli`) -- Add "Website" link to README navigation bar -- Update CLI Commands link in welcome view stub to point to package page -- Update documentation issue template placeholder URL +- **Documentation**: Update homepage URL to Magic website package page, add "Website" link to README, update CLI Commands link in welcome view stub --- diff --git a/README.md b/README.md index 7b14fe2..840248e 100644 --- a/README.md +++ b/README.md @@ -138,6 +138,7 @@ magic install --without-database --without-auth | `--without-events` | Skip events setup | | `--without-localization` | Skip localization setup | | `--without-logging` | Skip logging setup | +| `--without-broadcasting` | Skip broadcasting setup |
Generated Structure @@ -147,6 +148,7 @@ lib/ ├── config/ │ ├── app.dart │ ├── auth.dart +│ ├── broadcasting.dart │ ├── cache.dart │ ├── database.dart │ ├── logging.dart diff --git a/assets/stubs/install/broadcasting_config.stub b/assets/stubs/install/broadcasting_config.stub new file mode 100644 index 0000000..cde3ddb --- /dev/null +++ b/assets/stubs/install/broadcasting_config.stub @@ -0,0 +1,28 @@ +import 'package:magic/magic.dart'; + +/// Broadcasting configuration. +/// +/// Defines the default broadcasting connection and available connections. +/// See: https://magic.fluttersdk.com/docs/broadcasting +Map get broadcastingConfig => { + 'broadcasting': { + 'default': env('BROADCAST_CONNECTION', 'null'), + 'connections': { + 'reverb': { + 'driver': 'reverb', + 'host': env('REVERB_HOST', 'localhost'), + 'port': int.tryParse(env('REVERB_PORT', '8080')) ?? 8080, + 'scheme': env('REVERB_SCHEME', 'ws'), + 'app_key': env('REVERB_APP_KEY', ''), + 'auth_endpoint': '/broadcasting/auth', + 'reconnect': true, + 'max_reconnect_delay': 30000, + 'activity_timeout': 120, + 'dedup_buffer_size': 100, + }, + 'null': { + 'driver': 'null', + }, + }, + }, +}; diff --git a/lib/src/commands/install_command.dart b/lib/src/commands/install_command.dart index 1ab8b1a..1a41d22 100644 --- a/lib/src/commands/install_command.dart +++ b/lib/src/commands/install_command.dart @@ -32,6 +32,7 @@ import '../stubs/install_stubs.dart'; /// | `--without-events` | Skip events setup | /// | `--without-localization` | Skip `assets/lang/` directory | /// | `--without-logging` | Skip `config/logging.dart` | +/// | `--without-broadcasting` | Skip broadcasting setup | class InstallCommand extends Command { @override String get name => 'install'; @@ -48,11 +49,7 @@ class InstallCommand extends Command { @override void configure(ArgParser parser) { - parser.addFlag( - 'without-auth', - help: 'Skip auth setup', - negatable: false, - ); + parser.addFlag('without-auth', help: 'Skip auth setup', negatable: false); parser.addFlag( 'without-database', help: 'Skip database setup', @@ -63,11 +60,7 @@ class InstallCommand extends Command { help: 'Skip network setup', negatable: false, ); - parser.addFlag( - 'without-cache', - help: 'Skip cache setup', - negatable: false, - ); + parser.addFlag('without-cache', help: 'Skip cache setup', negatable: false); parser.addFlag( 'without-events', help: 'Skip events setup', @@ -83,6 +76,11 @@ class InstallCommand extends Command { help: 'Skip logging setup', negatable: false, ); + parser.addFlag( + 'without-broadcasting', + help: 'Skip broadcasting setup', + negatable: false, + ); } @override @@ -96,6 +94,7 @@ class InstallCommand extends Command { final withoutEvents = arguments['without-events'] as bool; final withoutLocalization = arguments['without-localization'] as bool; final withoutLogging = arguments['without-logging'] as bool; + final withoutBroadcasting = arguments['without-broadcasting'] as bool; _createDirectories( root, @@ -112,6 +111,7 @@ class InstallCommand extends Command { withoutCache: withoutCache, withoutLogging: withoutLogging, withoutLocalization: withoutLocalization, + withoutBroadcasting: withoutBroadcasting, ); _createStarterFiles(root); @@ -123,10 +123,11 @@ class InstallCommand extends Command { withoutNetwork: withoutNetwork, withoutCache: withoutCache, withoutLogging: withoutLogging, + withoutBroadcasting: withoutBroadcasting, ); _patchDefaultWidgetTest(root); - _createEnvFiles(root); + _createEnvFiles(root, withoutBroadcasting: withoutBroadcasting); _registerEnvAsset(root); @@ -167,10 +168,7 @@ class InstallCommand extends Command { ]; if (!withoutEvents) { - appDirs.addAll([ - 'lib/app/listeners', - 'lib/app/events', - ]); + appDirs.addAll(['lib/app/listeners', 'lib/app/events']); } for (final dir in appDirs) { @@ -209,6 +207,7 @@ class InstallCommand extends Command { required bool withoutCache, required bool withoutLogging, required bool withoutLocalization, + required bool withoutBroadcasting, }) { final providerImports = []; final providerEntries = []; @@ -239,6 +238,9 @@ class InstallCommand extends Command { if (!withoutAuth) { providerEntries.add('(app) => VaultServiceProvider(app),'); } + if (!withoutBroadcasting) { + providerEntries.add('(app) => BroadcastServiceProvider(app),'); + } // Auth providers boot AFTER AppServiceProvider (which registers // userFactory via setUserFactory). AuthServiceProvider.boot() @@ -291,6 +293,12 @@ class InstallCommand extends Command { InstallStubs.loggingConfigContent(), ); } + if (!withoutBroadcasting) { + _writeIfNotExists( + path.join(root, 'lib/config/broadcasting.dart'), + InstallStubs.broadcastingConfigContent(), + ); + } } /// Writes the framework starter files that are always created: @@ -342,6 +350,7 @@ class InstallCommand extends Command { required bool withoutNetwork, required bool withoutCache, required bool withoutLogging, + required bool withoutBroadcasting, }) { final mainPath = path.join(root, 'lib/main.dart'); @@ -358,10 +367,7 @@ class InstallCommand extends Command { "import 'config/view.dart';", ]; - final configFactories = [ - '() => appConfig', - '() => viewConfig', - ]; + final configFactories = ['() => appConfig', '() => viewConfig']; if (!withoutAuth) { configImports.add("import 'config/auth.dart';"); @@ -383,6 +389,10 @@ class InstallCommand extends Command { configImports.add("import 'config/logging.dart';"); configFactories.add('() => loggingConfig'); } + if (!withoutBroadcasting) { + configImports.add("import 'config/broadcasting.dart';"); + configFactories.add('() => broadcastingConfig'); + } final appName = _getAppName(root); @@ -420,26 +430,29 @@ class InstallCommand extends Command { return; } - FileHelper.writeFile( - widgetTestPath, - InstallStubs.widgetTestContent(), - ); + FileHelper.writeFile(widgetTestPath, InstallStubs.widgetTestContent()); } /// Writes `.env` and `.env.example` to [root] if they do not already exist. /// /// [root] — absolute path to the Flutter project root. - void _createEnvFiles(String root) { + /// [withoutBroadcasting] — when `true`, omits broadcasting env vars. + void _createEnvFiles(String root, {required bool withoutBroadcasting}) { final appName = _getAppName(root); _writeIfNotExists( path.join(root, '.env'), - InstallStubs.envContent(appName: appName), + InstallStubs.envContent( + appName: appName, + withoutBroadcasting: withoutBroadcasting, + ), ); _writeIfNotExists( path.join(root, '.env.example'), - InstallStubs.envExampleContent(), + InstallStubs.envExampleContent( + withoutBroadcasting: withoutBroadcasting, + ), ); } diff --git a/lib/src/stubs/install_stubs.dart b/lib/src/stubs/install_stubs.dart index f540237..09e02e6 100644 --- a/lib/src/stubs/install_stubs.dart +++ b/lib/src/stubs/install_stubs.dart @@ -34,14 +34,11 @@ class InstallStubs { final imports = configImports.join('\n'); final factories = configFactories.map((f) => ' $f,').join('\n'); - return StubLoader.replace( - StubLoader.load('install/main'), - { - 'configImports': imports, - 'configFactories': factories, - 'appName': appName, - }, - ); + return StubLoader.replace(StubLoader.load('install/main'), { + 'configImports': imports, + 'configFactories': factories, + 'appName': appName, + }); } // --------------------------------------------------------------------------- @@ -80,13 +77,10 @@ class InstallStubs { ...authProviderEntries.map((e) => ' $e'), ].join('\n'); - return StubLoader.replace( - StubLoader.load('install/app_config'), - { - 'allImports': allImports, - 'allProviders': allProviders, - }, - ); + return StubLoader.replace(StubLoader.load('install/app_config'), { + 'allImports': allImports, + 'allProviders': allProviders, + }); } /// Generates `lib/config/auth.dart` matching the Uptizm production pattern. @@ -124,6 +118,11 @@ class InstallStubs { return StubLoader.load('install/logging_config'); } + /// Generates `lib/config/broadcasting.dart` with Reverb and null connections. + static String broadcastingConfigContent() { + return StubLoader.load('install/broadcasting_config'); + } + // --------------------------------------------------------------------------- // Service Providers // --------------------------------------------------------------------------- @@ -180,12 +179,9 @@ class InstallStubs { /// /// [appName] — the human-readable application name shown in the hero section. static String welcomeViewContent({required String appName}) { - return StubLoader.replace( - StubLoader.load('install/welcome_view'), - { - 'appName': appName, - }, - ); + return StubLoader.replace(StubLoader.load('install/welcome_view'), { + 'appName': appName, + }); } // --------------------------------------------------------------------------- @@ -195,20 +191,44 @@ class InstallStubs { /// Generates a `.env` template file with sensible defaults. /// /// [appName] — written as the default value for `APP_NAME`. - static String envContent({required String appName}) { - return StubLoader.replace( - StubLoader.load('install/env'), - { - 'appName': appName, - }, - ); + /// [withoutBroadcasting] — when `true`, omits the `BROADCAST_CONNECTION` + /// and `REVERB_*` environment variables. + static String envContent({ + required String appName, + bool withoutBroadcasting = false, + }) { + var content = StubLoader.replace(StubLoader.load('install/env'), { + 'appName': appName, + }); + + if (!withoutBroadcasting) { + content += '\nBROADCAST_CONNECTION=null\n' + 'REVERB_HOST=localhost\n' + 'REVERB_PORT=8080\n' + 'REVERB_SCHEME=ws\n' + 'REVERB_APP_KEY=\n'; + } + + return content; } /// Generates a `.env.example` template file with empty values. /// /// Safe to commit — contains keys but no secrets. - static String envExampleContent() { - return StubLoader.load('install/env_example'); + /// [withoutBroadcasting] — when `true`, omits the `BROADCAST_CONNECTION` + /// and `REVERB_*` environment variable keys. + static String envExampleContent({bool withoutBroadcasting = false}) { + var content = StubLoader.load('install/env_example'); + + if (!withoutBroadcasting) { + content += '\nBROADCAST_CONNECTION=\n' + 'REVERB_HOST=\n' + 'REVERB_PORT=\n' + 'REVERB_SCHEME=\n' + 'REVERB_APP_KEY=\n'; + } + + return content; } /// Generates a Magic-compatible smoke test for `test/widget_test.dart`. diff --git a/pubspec.yaml b/pubspec.yaml index 2b16434..a75c459 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: magic_cli description: "Command-line tools for Magic Framework. Scaffolding, code generation, and project management." -version: 0.0.1-alpha.3 +version: 0.0.1-alpha.4 homepage: https://magic.fluttersdk.com/packages/magic-cli repository: https://github.com/fluttersdk/magic_cli issue_tracker: https://github.com/fluttersdk/magic_cli/issues diff --git a/test/commands/install_command_test.dart b/test/commands/install_command_test.dart index ea5e5fe..6617c38 100644 --- a/test/commands/install_command_test.dart +++ b/test/commands/install_command_test.dart @@ -181,7 +181,7 @@ void main() { ); }); - test('creates all 7 config files', () { + test('creates all 8 config files', () { final configs = [ 'app.dart', 'auth.dart', @@ -190,6 +190,7 @@ void main() { 'view.dart', 'cache.dart', 'logging.dart', + 'broadcasting.dart', ]; for (final config in configs) { @@ -253,6 +254,21 @@ void main() { reason: '.env.example should exist', ); }); + + test('includes broadcasting env vars in .env', () { + final content = File('${tempDir.path}/.env').readAsStringSync(); + + expect( + content, + contains('BROADCAST_CONNECTION'), + reason: '.env should contain BROADCAST_CONNECTION', + ); + expect( + content, + contains('REVERB_HOST'), + reason: '.env should contain REVERB_HOST', + ); + }); }); // ----------------------------------------------------------------------- @@ -354,7 +370,7 @@ void main() { ); }); - test('imports all 7 configs by default', () async { + test('imports all 8 configs by default', () async { cmd.arguments = parser.parse([]); await cmd.handle(); @@ -369,6 +385,7 @@ void main() { "import 'config/view.dart'", "import 'config/cache.dart'", "import 'config/logging.dart'", + "import 'config/broadcasting.dart'", ]; for (final imp in expectedImports) { @@ -689,7 +706,67 @@ void main() { }); // ----------------------------------------------------------------------- - // Group 10: Combined flags (--without-auth --without-database) + // Group 10: --without-broadcasting flag + // ----------------------------------------------------------------------- + group('--without-broadcasting flag', () { + setUp(() async { + cmd.arguments = parser.parse(['--without-broadcasting']); + await cmd.handle(); + }); + + test('skips broadcasting.dart', () { + expect( + File('${tempDir.path}/lib/config/broadcasting.dart').existsSync(), + isFalse, + reason: 'broadcasting.dart should not be created', + ); + }); + + test('excludes BroadcastServiceProvider from app.dart', () { + final content = + File('${tempDir.path}/lib/config/app.dart').readAsStringSync(); + + expect( + content, + isNot(contains('BroadcastServiceProvider')), + reason: 'app.dart should not contain BroadcastServiceProvider', + ); + }); + + test('excludes broadcasting config import from main.dart', () { + final content = + File('${tempDir.path}/lib/main.dart').readAsStringSync(); + + expect( + content, + isNot(contains("import 'config/broadcasting.dart'")), + reason: 'main.dart should not import broadcasting config', + ); + expect( + content, + isNot(contains('broadcastingConfig')), + reason: 'main.dart should not use broadcastingConfig factory', + ); + }); + + test('excludes broadcasting env vars from .env', () { + final content = File('${tempDir.path}/.env').readAsStringSync(); + + expect( + content, + isNot(contains('BROADCAST_CONNECTION')), + reason: '.env should not contain BROADCAST_CONNECTION', + ); + expect( + content, + isNot(contains('REVERB_HOST')), + reason: '.env should not contain REVERB_HOST', + ); + }); + }); + + // ----------------------------------------------------------------------- + // Group 11: Combined flags (--without-auth --without-database) // ----------------------------------------------------------------------- group('Combined flags (--without-auth --without-database)', () { setUp(() async { @@ -713,21 +790,21 @@ void main() { expect(content, isNot(contains('DatabaseServiceProvider'))); }); - test('creates only 5 config files', () { + test('creates only 6 config files', () { final dir = Directory('${tempDir.path}/lib/config'); final files = dir.listSync().whereType().toList(); expect( files.length, - 5, + 6, reason: - 'Should create exactly 5 config files (app, network, view, cache, logging)', + 'Should create exactly 6 config files (app, network, view, cache, logging, broadcasting)', ); }); }); // ----------------------------------------------------------------------- - // Group 11: .env content + // Group 12: .env content // ----------------------------------------------------------------------- group('.env content', () { setUp(() async { @@ -755,7 +832,7 @@ void main() { }); // ----------------------------------------------------------------------- - // Group 12: Content verification + // Group 13: Content verification // ----------------------------------------------------------------------- group('Content verification', () { setUp(() async { @@ -801,7 +878,7 @@ void main() { }); // ----------------------------------------------------------------------- - // Group 13: Cache config uses FileStore instance + // Group 14: Cache config uses FileStore instance // ----------------------------------------------------------------------- group('Cache config uses FileStore instance', () { setUp(() async { @@ -838,7 +915,7 @@ void main() { }); // ----------------------------------------------------------------------- - // Group 14: .env registered as Flutter asset + // Group 15: .env registered as Flutter asset // ----------------------------------------------------------------------- group('.env registered as Flutter asset', () { test('adds .env to flutter assets in pubspec.yaml', () async { @@ -900,7 +977,7 @@ flutter: }); // ----------------------------------------------------------------------- - // Group 15: Web support (sqlite3.wasm) + // Group 16: Web support (sqlite3.wasm) // ----------------------------------------------------------------------- group('Web support (sqlite3.wasm)', () { late _TestInstallCommandWithDownload downloadCmd;