diff --git a/packages/shorebird_cli/lib/src/commands/commands.dart b/packages/shorebird_cli/lib/src/commands/commands.dart index 5d9d1434..3f19752a 100644 --- a/packages/shorebird_cli/lib/src/commands/commands.dart +++ b/packages/shorebird_cli/lib/src/commands/commands.dart @@ -1,4 +1,5 @@ export 'cache/cache.dart'; +export 'create.dart'; export 'doctor_command.dart'; export 'flutter/flutter.dart'; export 'init_command.dart'; diff --git a/packages/shorebird_cli/lib/src/commands/create.dart b/packages/shorebird_cli/lib/src/commands/create.dart new file mode 100644 index 00000000..59743675 --- /dev/null +++ b/packages/shorebird_cli/lib/src/commands/create.dart @@ -0,0 +1,41 @@ +import 'dart:async'; + +import 'package:mason_logger/mason_logger.dart'; +import 'package:path/path.dart' as p; +import 'package:scoped_deps/scoped_deps.dart'; +import 'package:shorebird_cli/src/shorebird_command.dart'; +import 'package:shorebird_cli/src/shorebird_env.dart'; +import 'package:shorebird_cli/src/shorebird_process.dart'; + +/// {@template shorebird_create_command} +/// `shorebird create` +/// Create a new Flutter app with Shorebird. +/// {@endtemplate} +class CreateCommand extends ShorebirdProxyCommand { + @override + String get name => 'create'; + + @override + String get description => 'Create a new Flutter project with Shorebird.'; + + @override + Future run() async { + final createExitCode = await process.stream('flutter', [ + 'create', + ...results.rest, + ]); + if (createExitCode != ExitCode.success.code) return createExitCode; + return runScoped( + () => runner!.run(['init']), + values: { + shorebirdEnvRef.overrideWith( + () => ShorebirdEnv( + flutterProjectRootOverride: p.absolute( + p.normalize(results.rest.first), + ), + ), + ), + }, + ); + } +} diff --git a/packages/shorebird_cli/lib/src/shorebird_cli_command_runner.dart b/packages/shorebird_cli/lib/src/shorebird_cli_command_runner.dart index dd08dcb8..4b752e56 100644 --- a/packages/shorebird_cli/lib/src/shorebird_cli_command_runner.dart +++ b/packages/shorebird_cli/lib/src/shorebird_cli_command_runner.dart @@ -69,6 +69,7 @@ class ShorebirdCliCommandRunner extends CompletionCommandRunner { ); addCommand(CacheCommand()); + addCommand(CreateCommand()); addCommand(DoctorCommand()); addCommand(FlutterCommand()); addCommand(InitCommand()); diff --git a/packages/shorebird_cli/lib/src/shorebird_command.dart b/packages/shorebird_cli/lib/src/shorebird_command.dart index 0aa05f04..b88f2814 100644 --- a/packages/shorebird_cli/lib/src/shorebird_command.dart +++ b/packages/shorebird_cli/lib/src/shorebird_command.dart @@ -34,13 +34,17 @@ abstract class ShorebirdCommand extends Command { // coverage:ignore-start @override ShorebirdCliCommandRunner? get runner => - super.runner as ShorebirdCliCommandRunner?; + testRunner ?? super.runner as ShorebirdCliCommandRunner?; // coverage:ignore-end /// [ArgResults] used for testing purposes only. @visibleForTesting ArgResults? testArgResults; + /// The parent command runner used for testing purposes only. + @visibleForTesting + ShorebirdCliCommandRunner? testRunner; + /// [ArgResults] for the current command. ArgResults get results => testArgResults ?? argResults!; } diff --git a/packages/shorebird_cli/lib/src/shorebird_env.dart b/packages/shorebird_cli/lib/src/shorebird_env.dart index dcb742d7..84e6cd5e 100644 --- a/packages/shorebird_cli/lib/src/shorebird_env.dart +++ b/packages/shorebird_cli/lib/src/shorebird_env.dart @@ -22,8 +22,11 @@ ShorebirdEnv get shorebirdEnv => read(shorebirdEnvRef); /// {@endtemplate} class ShorebirdEnv { /// {@macro shorebird_env} - const ShorebirdEnv({String? flutterRevisionOverride}) - : _flutterRevisionOverride = flutterRevisionOverride; + const ShorebirdEnv({ + String? flutterRevisionOverride, + String? flutterProjectRootOverride, + }) : _flutterRevisionOverride = flutterRevisionOverride, + _flutterProjectRootOverride = flutterProjectRootOverride; /// Copy the [ShorebirdEnv] and optionally override the flutter revision. ShorebirdEnv copyWith({String? flutterRevisionOverride}) => ShorebirdEnv( @@ -32,6 +35,7 @@ class ShorebirdEnv { ); final String? _flutterRevisionOverride; + final String? _flutterProjectRootOverride; /// The application config directory for the Shorebird CLI. Directory get configDirectory { @@ -156,6 +160,9 @@ class ShorebirdEnv { /// Returns the root directory of the nearest Flutter project. Directory? getFlutterProjectRoot() { + if (_flutterProjectRootOverride != null) { + return Directory(_flutterProjectRootOverride); + } final file = findNearestAncestor( where: (path) => getPubspecYamlFile(cwd: Directory(path)), ); diff --git a/packages/shorebird_cli/test/src/commands/create_test.dart b/packages/shorebird_cli/test/src/commands/create_test.dart new file mode 100644 index 00000000..46efa250 --- /dev/null +++ b/packages/shorebird_cli/test/src/commands/create_test.dart @@ -0,0 +1,97 @@ +import 'package:args/args.dart'; +import 'package:mason_logger/mason_logger.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:path/path.dart' as p; +import 'package:scoped_deps/scoped_deps.dart'; +import 'package:shorebird_cli/src/commands/commands.dart'; +import 'package:shorebird_cli/src/shorebird_cli_command_runner.dart'; +import 'package:shorebird_cli/src/shorebird_env.dart'; +import 'package:shorebird_cli/src/shorebird_process.dart'; +import 'package:test/test.dart'; + +import '../mocks.dart'; + +void main() { + group(CreateCommand, () { + const args = ['my_app']; + late ShorebirdProcess process; + late ArgResults argResults; + late ShorebirdCliCommandRunner runner; + late CreateCommand command; + + R runWithOverrides(R Function() body) { + return runScoped(body, values: {processRef.overrideWith(() => process)}); + } + + setUp(() { + argResults = MockArgResults(); + process = MockShorebirdProcess(); + runner = MockShorebirdCliCommandRunner(); + command = runWithOverrides(CreateCommand.new) + ..testArgResults = argResults + ..testRunner = runner; + + when(() => argResults.rest).thenReturn(args); + when( + () => runner.run(any()), + ).thenAnswer((_) async => ExitCode.success.code); + when( + () => process.stream('flutter', ['create', ...args]), + ).thenAnswer((_) async => ExitCode.success.code); + }); + + test('has correct name and description', () { + expect(command.name, equals('create')); + expect( + command.description, + equals('Create a new Flutter project with Shorebird.'), + ); + }); + + test('runs the `flutter create` command', () async { + await expectLater( + runWithOverrides(command.run), + completion(equals(ExitCode.success.code)), + ); + + verify(() => process.stream('flutter', ['create', ...args])).called(1); + }); + + test('runs the shorebird init command', () async { + when(() => runner.run(any())).thenAnswer((invocation) async { + final runnerArgs = invocation.positionalArguments.first as List; + if (runnerArgs.first == 'init') { + expect( + p.basename(shorebirdEnv.getFlutterProjectRoot()!.path), + args.first, + ); + } + return ExitCode.success.code; + }); + await expectLater( + runWithOverrides(command.run), + completion(equals(ExitCode.success.code)), + ); + + verify(() => runner.run(['init'])).called(1); + }); + + group('when flutter create fails', () { + setUp(() { + when( + () => process.stream('flutter', ['create', ...args]), + ).thenAnswer((_) async => 1); + }); + + test('exits', () async { + await expectLater( + runWithOverrides(command.run), + completion(equals(1)), + ); + + verify(() => process.stream('flutter', ['create', ...args])).called(1); + verifyNever(() => runner.run(any())); + }); + }); + }); +} diff --git a/packages/shorebird_cli/test/src/mocks.dart b/packages/shorebird_cli/test/src/mocks.dart index 8a9f1f54..9521a199 100644 --- a/packages/shorebird_cli/test/src/mocks.dart +++ b/packages/shorebird_cli/test/src/mocks.dart @@ -33,6 +33,7 @@ import 'package:shorebird_cli/src/platform/platform.dart'; import 'package:shorebird_cli/src/pubspec_editor.dart'; import 'package:shorebird_cli/src/shorebird_android_artifacts.dart'; import 'package:shorebird_cli/src/shorebird_artifacts.dart'; +import 'package:shorebird_cli/src/shorebird_cli_command_runner.dart'; import 'package:shorebird_cli/src/shorebird_env.dart'; import 'package:shorebird_cli/src/shorebird_flutter.dart'; import 'package:shorebird_cli/src/shorebird_process.dart'; @@ -160,6 +161,9 @@ class MockShorebirdAndroidArtifacts extends Mock class MockShorebirdArtifacts extends Mock implements ShorebirdArtifacts {} +class MockShorebirdCliCommandRunner extends Mock + implements ShorebirdCliCommandRunner {} + class MockShorebirdEnv extends Mock implements ShorebirdEnv {} class MockShorebirdFlutter extends Mock implements ShorebirdFlutter {} diff --git a/packages/shorebird_cli/test/src/shorebird_env_test.dart b/packages/shorebird_cli/test/src/shorebird_env_test.dart index f60bb5e1..2b2de059 100644 --- a/packages/shorebird_cli/test/src/shorebird_env_test.dart +++ b/packages/shorebird_cli/test/src/shorebird_env_test.dart @@ -94,6 +94,23 @@ void main() { }); group('getFlutterProjectRoot', () { + test('uses override when provided', () { + final tempDir = Directory.systemTemp.createTempSync(); + final overridePubspec = File( + p.join(tempDir.path, 'override', 'pubspec.yaml'), + ); + final override = overridePubspec.parent.path; + File(p.join(tempDir.path, 'pubspec.yaml')).createSync(recursive: true); + expect( + runWithOverrides( + () => ShorebirdEnv( + flutterProjectRootOverride: override, + ).getFlutterProjectRoot(), + ), + isA().having((d) => d.path, 'absolute', override), + ); + }); + test('returns null when no Flutter project exists', () { final tempDir = Directory.systemTemp.createTempSync(); expect(