Skip to content

Commit 581b46b

Browse files
authored
feat(dart_frog_cli): update prompt and command (#348)
1 parent 40fec06 commit 581b46b

File tree

6 files changed

+247
-5
lines changed

6 files changed

+247
-5
lines changed

packages/dart_frog_cli/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ Available commands:
2727
build Create a production build.
2828
create Creates a new Dart Frog app.
2929
dev Run a local development server.
30+
update Update the Dart Frog CLI.
3031
3132
Run "dart_frog help <command>" for more information about a command.
3233
```

packages/dart_frog_cli/lib/src/command_runner.dart

Lines changed: 41 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
import 'package:args/args.dart';
22
import 'package:args/command_runner.dart';
33
import 'package:dart_frog_cli/src/commands/commands.dart';
4+
import 'package:dart_frog_cli/src/commands/update/update.dart';
45
import 'package:dart_frog_cli/src/version.dart';
56
import 'package:mason/mason.dart' hide packageVersion;
7+
import 'package:pub_updater/pub_updater.dart';
68

79
/// The package name.
810
const packageName = 'dart_frog_cli';
@@ -21,24 +23,61 @@ class DartFrogCommandRunner extends CommandRunner<int> {
2123
/// {@macro dart_frog_command_runner}
2224
DartFrogCommandRunner({
2325
Logger? logger,
26+
PubUpdater? pubUpdater,
2427
}) : _logger = logger ?? Logger(),
28+
_pubUpdater = pubUpdater ?? PubUpdater(),
2529
super(executableName, executableDescription) {
2630
argParser.addFlags();
2731
addCommand(BuildCommand(logger: _logger));
2832
addCommand(CreateCommand(logger: _logger));
2933
addCommand(DevCommand(logger: _logger));
34+
addCommand(UpdateCommand(logger: _logger));
3035
}
3136

3237
final Logger _logger;
38+
final PubUpdater _pubUpdater;
3339

3440
@override
3541
Future<int> run(Iterable<String> args) async {
42+
final argResults = parse(args);
43+
late final int exitCode;
44+
3645
try {
37-
return await runCommand(parse(args)) ?? ExitCode.success.code;
46+
exitCode = await runCommand(argResults) ?? ExitCode.success.code;
3847
} catch (error) {
3948
_logger.err('$error');
40-
return ExitCode.software.code;
49+
exitCode = ExitCode.software.code;
4150
}
51+
52+
if (argResults.command?.name != 'update') await _checkForUpdates();
53+
54+
return exitCode;
55+
}
56+
57+
Future<void> _checkForUpdates() async {
58+
try {
59+
final latestVersion = await _pubUpdater.getLatestVersion(packageName);
60+
final isUpToDate = packageVersion == latestVersion;
61+
if (!isUpToDate) {
62+
final changelogLink = lightCyan.wrap(
63+
styleUnderlined.wrap(
64+
link(
65+
uri: Uri.parse(
66+
'https://github.com/verygoodopensource/dart_frog/releases/tag/dart_frog_cli-v$latestVersion',
67+
),
68+
),
69+
),
70+
);
71+
_logger
72+
..info('')
73+
..info(
74+
'''
75+
${lightYellow.wrap('Update available!')} ${lightCyan.wrap(packageVersion)} \u2192 ${lightCyan.wrap(latestVersion)}
76+
${lightYellow.wrap('Changelog:')} $changelogLink
77+
Run ${lightCyan.wrap('$executableName update')} to update''',
78+
);
79+
}
80+
} catch (_) {}
4281
}
4382

4483
@override
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import 'package:args/command_runner.dart';
2+
import 'package:dart_frog_cli/src/command_runner.dart';
3+
import 'package:dart_frog_cli/src/version.dart';
4+
import 'package:mason/mason.dart' hide packageVersion;
5+
import 'package:pub_updater/pub_updater.dart';
6+
7+
/// {@template update_command}
8+
/// `dart_frog update` command which updates the dart_frog_cli.
9+
/// {@endtemplate}
10+
class UpdateCommand extends Command<int> {
11+
/// {@macro update_command}
12+
UpdateCommand({
13+
required Logger logger,
14+
PubUpdater? pubUpdater,
15+
}) : _logger = logger,
16+
_pubUpdater = pubUpdater ?? PubUpdater();
17+
18+
final Logger _logger;
19+
final PubUpdater _pubUpdater;
20+
21+
@override
22+
String get description => 'Update the Dart Frog CLI.';
23+
24+
@override
25+
String get name => 'update';
26+
27+
@override
28+
Future<int> run() async {
29+
final updateCheckProgress = _logger.progress('Checking for updates');
30+
late final String latestVersion;
31+
try {
32+
latestVersion = await _pubUpdater.getLatestVersion(packageName);
33+
} catch (error) {
34+
updateCheckProgress.fail();
35+
_logger.err('$error');
36+
return ExitCode.software.code;
37+
}
38+
updateCheckProgress.complete('Checked for updates');
39+
40+
final isUpToDate = packageVersion == latestVersion;
41+
if (isUpToDate) {
42+
_logger.info('$packageName is already at the latest version.');
43+
return ExitCode.success.code;
44+
}
45+
46+
final updateProgress = _logger.progress('Updating to $latestVersion');
47+
try {
48+
await _pubUpdater.update(packageName: packageName);
49+
} catch (error) {
50+
updateProgress.fail();
51+
_logger.err('$error');
52+
return ExitCode.software.code;
53+
}
54+
updateProgress.complete('Updated to $latestVersion');
55+
56+
return ExitCode.success.code;
57+
}
58+
}

packages/dart_frog_cli/pubspec.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ dependencies:
1414
mason: ^0.1.0-dev.33
1515
meta: ^1.7.0
1616
path: ^1.8.1
17+
pub_updater: ^0.2.2
1718
stream_transform: ^2.0.0
1819
watcher: ^1.0.1
1920

packages/dart_frog_cli/test/src/command_runner_test.dart

Lines changed: 49 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,12 @@ import 'package:dart_frog_cli/src/command_runner.dart';
55
import 'package:dart_frog_cli/src/version.dart';
66
import 'package:mason/mason.dart' hide packageVersion;
77
import 'package:mocktail/mocktail.dart';
8+
import 'package:pub_updater/pub_updater.dart';
89
import 'package:test/test.dart';
910

10-
class MockLogger extends Mock implements Logger {}
11+
class _MockLogger extends Mock implements Logger {}
12+
13+
class _MockPubUpdater extends Mock implements PubUpdater {}
1114

1215
const expectedUsage = [
1316
'A fast, minimalistic backend framework for Dart.\n'
@@ -23,29 +26,72 @@ const expectedUsage = [
2326
' build Create a production build.\n'
2427
' create Creates a new Dart Frog app.\n'
2528
' dev Run a local development server.\n'
29+
' update Update the Dart Frog CLI.\n'
2630
'\n'
2731
'Run "dart_frog help <command>" for more information about a command.'
2832
];
2933

34+
const latestVersion = '0.0.0';
35+
final changelogLink = lightCyan.wrap(
36+
styleUnderlined.wrap(
37+
link(
38+
uri: Uri.parse(
39+
'https://github.com/verygoodopensource/dart_frog/releases/tag/dart_frog_cli-v$latestVersion',
40+
),
41+
),
42+
),
43+
);
44+
final updateMessage = '''
45+
${lightYellow.wrap('Update available!')} ${lightCyan.wrap(packageVersion)} \u2192 ${lightCyan.wrap(latestVersion)}
46+
${lightYellow.wrap('Changelog:')} $changelogLink
47+
Run ${lightCyan.wrap('$executableName update')} to update''';
48+
3049
void main() {
3150
group('DartFrogCommandRunner', () {
3251
late Logger logger;
52+
late PubUpdater pubUpdater;
3353
late DartFrogCommandRunner commandRunner;
3454

3555
setUp(() {
3656
printLogs = [];
37-
logger = MockLogger();
57+
logger = _MockLogger();
58+
pubUpdater = _MockPubUpdater();
59+
60+
when(
61+
() => pubUpdater.getLatestVersion(any()),
62+
).thenAnswer((_) async => packageVersion);
63+
3864
commandRunner = DartFrogCommandRunner(
3965
logger: logger,
66+
pubUpdater: pubUpdater,
4067
);
4168
});
4269

43-
test('can be instantiated without an explicit logger instance', () {
70+
test('can be instantiated without any explicit parameters', () {
4471
final commandRunner = DartFrogCommandRunner();
4572
expect(commandRunner, isNotNull);
4673
});
4774

4875
group('run', () {
76+
test('prompts for update when newer version exists', () async {
77+
when(
78+
() => pubUpdater.getLatestVersion(any()),
79+
).thenAnswer((_) async => latestVersion);
80+
final result = await commandRunner.run(['--version']);
81+
expect(result, equals(ExitCode.success.code));
82+
verify(() => logger.info(updateMessage)).called(1);
83+
});
84+
85+
test('handles pub update errors gracefully', () async {
86+
when(
87+
() => pubUpdater.getLatestVersion(any()),
88+
).thenThrow(Exception('oops'));
89+
90+
final result = await commandRunner.run(['--version']);
91+
expect(result, equals(ExitCode.success.code));
92+
verifyNever(() => logger.info(updateMessage));
93+
});
94+
4995
test('handles Exception', () async {
5096
final exception = Exception('oops!');
5197
var isFirstInvocation = true;
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
import 'dart:io';
2+
3+
import 'package:dart_frog_cli/src/command_runner.dart';
4+
import 'package:dart_frog_cli/src/commands/update/update.dart';
5+
import 'package:dart_frog_cli/src/version.dart';
6+
import 'package:mason/mason.dart' hide packageVersion;
7+
import 'package:mocktail/mocktail.dart';
8+
import 'package:pub_updater/pub_updater.dart';
9+
import 'package:test/test.dart';
10+
11+
class MockLogger extends Mock implements Logger {}
12+
13+
class MockPubUpdater extends Mock implements PubUpdater {}
14+
15+
class FakeProcessResult extends Fake implements ProcessResult {}
16+
17+
class MockProgress extends Mock implements Progress {}
18+
19+
void main() {
20+
const latestVersion = '0.0.0';
21+
22+
group('dart_frog update', () {
23+
late Logger logger;
24+
late PubUpdater pubUpdater;
25+
late UpdateCommand command;
26+
27+
setUp(() {
28+
logger = MockLogger();
29+
pubUpdater = MockPubUpdater();
30+
31+
when(() => logger.progress(any())).thenReturn(MockProgress());
32+
when(
33+
() => pubUpdater.getLatestVersion(any()),
34+
).thenAnswer((_) async => packageVersion);
35+
when(
36+
() => pubUpdater.update(packageName: packageName),
37+
).thenAnswer((_) => Future.value(FakeProcessResult()));
38+
39+
command = UpdateCommand(logger: logger, pubUpdater: pubUpdater);
40+
});
41+
42+
test('handles pub latest version query errors', () async {
43+
when(
44+
() => pubUpdater.getLatestVersion(any()),
45+
).thenThrow(Exception('oops'));
46+
final result = await command.run();
47+
expect(result, equals(ExitCode.software.code));
48+
verify(() => logger.progress('Checking for updates')).called(1);
49+
verify(() => logger.err('Exception: oops'));
50+
verifyNever(
51+
() => pubUpdater.update(packageName: any(named: 'packageName')),
52+
);
53+
});
54+
55+
test('handles pub update errors', () async {
56+
when(
57+
() => pubUpdater.getLatestVersion(any()),
58+
).thenAnswer((_) async => latestVersion);
59+
when(
60+
() => pubUpdater.update(packageName: any(named: 'packageName')),
61+
).thenThrow(Exception('oops'));
62+
final result = await command.run();
63+
expect(result, equals(ExitCode.software.code));
64+
verify(() => logger.progress('Checking for updates')).called(1);
65+
verify(() => logger.err('Exception: oops'));
66+
verify(
67+
() => pubUpdater.update(packageName: any(named: 'packageName')),
68+
).called(1);
69+
});
70+
71+
test('updates when newer version exists', () async {
72+
when(
73+
() => pubUpdater.getLatestVersion(any()),
74+
).thenAnswer((_) async => latestVersion);
75+
when(() => logger.progress(any())).thenReturn(MockProgress());
76+
final result = await command.run();
77+
expect(result, equals(ExitCode.success.code));
78+
verify(() => logger.progress('Checking for updates')).called(1);
79+
verify(() => logger.progress('Updating to $latestVersion')).called(1);
80+
verify(() => pubUpdater.update(packageName: packageName)).called(1);
81+
});
82+
83+
test('does not update when already on latest version', () async {
84+
when(
85+
() => pubUpdater.getLatestVersion(any()),
86+
).thenAnswer((_) async => packageVersion);
87+
when(() => logger.progress(any())).thenReturn(MockProgress());
88+
final result = await command.run();
89+
expect(result, equals(ExitCode.success.code));
90+
verify(
91+
() => logger.info('$packageName is already at the latest version.'),
92+
).called(1);
93+
verifyNever(() => logger.progress('Updating to $latestVersion'));
94+
verifyNever(() => pubUpdater.update(packageName: packageName));
95+
});
96+
});
97+
}

0 commit comments

Comments
 (0)