feat(shorebird_cli): shorebird create command (#3175)
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
export 'cache/cache.dart';
|
||||
export 'create.dart';
|
||||
export 'doctor_command.dart';
|
||||
export 'flutter/flutter.dart';
|
||||
export 'init_command.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<int> 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),
|
||||
),
|
||||
),
|
||||
),
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -69,6 +69,7 @@ class ShorebirdCliCommandRunner extends CompletionCommandRunner<int> {
|
||||
);
|
||||
|
||||
addCommand(CacheCommand());
|
||||
addCommand(CreateCommand());
|
||||
addCommand(DoctorCommand());
|
||||
addCommand(FlutterCommand());
|
||||
addCommand(InitCommand());
|
||||
|
||||
@@ -34,13 +34,17 @@ abstract class ShorebirdCommand extends Command<int> {
|
||||
// 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!;
|
||||
}
|
||||
|
||||
@@ -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)),
|
||||
);
|
||||
|
||||
@@ -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>(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()));
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -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 {}
|
||||
|
||||
@@ -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<Directory>().having((d) => d.path, 'absolute', override),
|
||||
);
|
||||
});
|
||||
|
||||
test('returns null when no Flutter project exists', () {
|
||||
final tempDir = Directory.systemTemp.createTempSync();
|
||||
expect(
|
||||
|
||||
Reference in New Issue
Block a user