Skip to content
7 changes: 6 additions & 1 deletion lib/src/commands/create.dart
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,12 @@ const _defaultOrgName = 'com.example.verygoodcore';
const _defaultDescription = 'A Very Good Project created by Very Good CLI.';
final _defaultTemplate = CoreTemplate();

final _templates = [_defaultTemplate, DartPkgTemplate(), FlutterPkgTemplate()];
final _templates = [
_defaultTemplate,
DartPkgTemplate(),
FlutterPkgTemplate(),
FlutterPluginTemplate(),
];

// A valid Dart identifier that can be used for a package, i.e. no
// capital letters.
Expand Down
72 changes: 68 additions & 4 deletions lib/src/flutter_cli.dart
Original file line number Diff line number Diff line change
@@ -1,15 +1,42 @@
import 'package:path/path.dart' as p;
import 'package:universal_io/io.dart';

/// Thrown when `flutter packages get` or `flutter pub get`
/// is exectuted without a pubspec.yaml
class PubspecNotFound implements Exception {}

/// Flutter CLI
class Flutter {
/// Install flutter dependencies (`flutter packages get`).
static Future<void> packagesGet([String? cwd]) {
return _Cmd.run('flutter', ['packages', 'get'], workingDirectory: cwd);
static Future<void> packagesGet({
String cwd = '.',
bool recursive = false,
}) async {
await _installPackages(
cmd: (cwd) => _Cmd.run(
'flutter',
['packages', 'get'],
workingDirectory: cwd,
),
cwd: cwd,
recursive: recursive,
);
}

/// Install dart dependencies (`flutter pub get`).
static Future<void> pubGet([String? cwd]) {
return _Cmd.run('flutter', ['pub', 'get'], workingDirectory: cwd);
static Future<void> pubGet({
String cwd = '.',
bool recursive = false,
}) async {
await _installPackages(
cmd: (cwd) => _Cmd.run(
'flutter',
['pub', 'get'],
workingDirectory: cwd,
),
cwd: cwd,
recursive: recursive,
);
}

/// Determine whether flutter is installed
Expand All @@ -21,6 +48,38 @@ class Flutter {
return false;
}
}

static Future<void> _installPackages({
required Future<ProcessResult> Function(String cwd) cmd,
required String cwd,
required bool recursive,
}) async {
if (!recursive) {
final pubspec = File(p.join(cwd, 'pubspec.yaml'));
if (!pubspec.existsSync()) throw PubspecNotFound();

await cmd(cwd);
return;
}

final processes = _process(
run: (entity) => cmd(entity.parent.path),
where: _isPubspec,
cwd: cwd,
);

if (processes.isEmpty) throw PubspecNotFound();

await Future.wait(processes);
}

static Iterable<Future<ProcessResult>> _process({
required Future<ProcessResult> Function(FileSystemEntity) run,
required bool Function(FileSystemEntity) where,
String cwd = '.',
}) {
return Directory(cwd).listSync(recursive: true).where(where).map(run);
}
}

/// Abstraction for running commands via command-line.
Expand Down Expand Up @@ -61,3 +120,8 @@ class _Cmd {
}
}
}

bool _isPubspec(FileSystemEntity entity) {
if (entity is! File) return false;
return p.basename(entity.path) == 'pubspec.yaml';
}
705 changes: 705 additions & 0 deletions lib/src/templates/flutter_plugin_bundle.dart

Large diffs are not rendered by default.

39 changes: 36 additions & 3 deletions lib/src/templates/template.dart
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ class DartPkgTemplate extends Template {
final installDependenciesDone = logger.progress(
'Running "flutter pub get" in ${outputDir.path}',
);
await Flutter.pubGet(outputDir.path);
await Flutter.pubGet(cwd: outputDir.path);
installDependenciesDone();
}
_logSummary(logger);
Expand Down Expand Up @@ -81,7 +81,7 @@ class FlutterPkgTemplate extends Template {
final installDependenciesDone = logger.progress(
'Running "flutter packages get" in ${outputDir.path}',
);
await Flutter.packagesGet(outputDir.path);
await Flutter.packagesGet(cwd: outputDir.path);
installDependenciesDone();
}
_logSummary(logger);
Expand All @@ -95,6 +95,39 @@ class FlutterPkgTemplate extends Template {
}
}

/// {@template flutter_plugin_template}
/// A Flutter plugin template.
/// {@endtemplate}
class FlutterPluginTemplate extends Template {
/// {@macro flutter_pkg_template}
FlutterPluginTemplate()
: super(
name: 'flutter_plugin',
bundle: flutterPluginBundle,
help: 'Generate a reusable Flutter plugin.',
);

@override
Future<void> onGenerateComplete(Logger logger, Directory outputDir) async {
final isFlutterInstalled = await Flutter.installed();
if (isFlutterInstalled) {
final installDependenciesDone = logger.progress(
'Running "flutter packages get" in ${outputDir.path}',
);
await Flutter.packagesGet(cwd: outputDir.path, recursive: true);
installDependenciesDone();
}
_logSummary(logger);
}

void _logSummary(Logger logger) {
logger
..info('\n')
..alert('Created a Very Good Flutter plugin! 🦄')
..info('\n');
}
}

/// {@template core_template}
/// A core Flutter app template.
/// {@endtemplate}
Expand All @@ -114,7 +147,7 @@ class CoreTemplate extends Template {
final installDependenciesDone = logger.progress(
'Running "flutter packages get" in ${outputDir.path}',
);
await Flutter.packagesGet(outputDir.path);
await Flutter.packagesGet(cwd: outputDir.path);
installDependenciesDone();
}
_logSummary(logger);
Expand Down
1 change: 1 addition & 0 deletions lib/src/templates/templates.dart
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export 'dart_package_bundle.dart';
export 'flutter_package_bundle.dart';
export 'flutter_plugin_bundle.dart';
export 'template.dart';
export 'very_good_core_bundle.dart';
77 changes: 75 additions & 2 deletions test/src/cmd_test.dart
Original file line number Diff line number Diff line change
@@ -1,33 +1,106 @@
import 'package:path/path.dart' as p;
import 'package:test/test.dart';
import 'package:universal_io/io.dart';
import 'package:very_good_cli/src/flutter_cli.dart';

const pubspec = '''
name: example
environment:
sdk: ">=2.13.0 <3.0.0"
''';

const invalidPubspec = '''
name: example
''';

void main() {
group('Flutter CLI', () {
group('packages get', () {
test('throws when there is no pubspec.yaml', () {
expectLater(
Flutter.packagesGet(Directory.systemTemp.path),
Flutter.packagesGet(cwd: Directory.systemTemp.path),
throwsException,
);
});

test('throws when process fails', () {
final directory = Directory.systemTemp.createTempSync();
File(p.join(directory.path, 'pubspec.yaml'))
.writeAsStringSync(invalidPubspec);

expectLater(
Flutter.packagesGet(cwd: directory.path),
throwsException,
);
});

test('completes when there is a pubspec.yaml', () {
expectLater(Flutter.packagesGet(), completes);
});

test('throws when there is no pubspec.yaml (recursive)', () {
final directory = Directory.systemTemp.createTempSync();
expectLater(
Flutter.packagesGet(cwd: directory.path, recursive: true),
throwsException,
);
});

test('completes when there is a pubspec.yaml (recursive)', () {
final directory = Directory.systemTemp.createTempSync();
final nestedDirectory = Directory(p.join(directory.path, 'test'))
..createSync();
File(p.join(nestedDirectory.path, 'pubspec.yaml'))
.writeAsStringSync(pubspec);
expectLater(
Flutter.packagesGet(cwd: directory.path, recursive: true),
completes,
);
});
});

group('pub get', () {
test('throws when there is no pubspec.yaml', () {
expectLater(
Flutter.pubGet(Directory.systemTemp.path),
Flutter.pubGet(cwd: Directory.systemTemp.path),
throwsException,
);
});

test('throws when process fails', () {
final directory = Directory.systemTemp.createTempSync();
File(p.join(directory.path, 'pubspec.yaml'))
.writeAsStringSync(invalidPubspec);

expectLater(
Flutter.pubGet(cwd: directory.path),
throwsException,
);
});

test('completes when there is a pubspec.yaml', () {
expectLater(Flutter.pubGet(), completes);
});

test('throws when there is no pubspec.yaml (recursive)', () {
final directory = Directory.systemTemp.createTempSync();
expectLater(
Flutter.pubGet(cwd: directory.path, recursive: true),
throwsException,
);
});

test('completes when there is a pubspec.yaml (recursive)', () {
final directory = Directory.systemTemp.createTempSync();
final nestedDirectory = Directory(p.join(directory.path, 'test'))
..createSync();
File(p.join(nestedDirectory.path, 'pubspec.yaml'))
.writeAsStringSync(pubspec);
expectLater(
Flutter.pubGet(cwd: directory.path, recursive: true),
completes,
);
});
});
});
}
39 changes: 35 additions & 4 deletions test/src/commands/create_test.dart
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
import 'dart:async';

import 'package:args/args.dart';
import 'package:io/io.dart';
import 'package:mason/mason.dart';
import 'package:mocktail/mocktail.dart';
import 'package:path/path.dart' as p;
import 'package:test/test.dart';
import 'package:universal_io/io.dart';
import 'package:usage/usage_io.dart';
import 'package:very_good_cli/src/command_runner.dart';
import 'package:very_good_cli/src/commands/create.dart';
Expand All @@ -25,10 +28,17 @@ const expectedUsage = [
''' [core] (default) Generate a Very Good Flutter application.\n'''
' [dart_pkg] Generate a reusable Dart package.\n'
' [flutter_pkg] Generate a reusable Flutter package.\n'
' [flutter_plugin] Generate a reusable Flutter plugin.\n'
'\n'
'Run "very_good help" to see global options.'
];

const pubspec = '''
name: example
environment:
sdk: ">=2.13.0 <3.0.0"
''';

class MockArgResults extends Mock implements ArgResults {}

class MockAnalytics extends Mock implements Analytics {}
Expand Down Expand Up @@ -153,7 +163,10 @@ void main() {
when(() => generator.description).thenReturn('generator description');
when(
() => generator.generate(any(), vars: any(named: 'vars')),
).thenAnswer((_) async => 62);
).thenAnswer((_) async {
File(p.join('.tmp', 'pubspec.yaml')).writeAsStringSync(pubspec);
return 62;
});
final result = await command.run();
expect(result, equals(ExitCode.success.code));
verify(() => logger.progress('Bootstrapping')).called(1);
Expand Down Expand Up @@ -211,7 +224,10 @@ void main() {
when(() => generator.description).thenReturn('generator description');
when(
() => generator.generate(any(), vars: any(named: 'vars')),
).thenAnswer((_) async => 62);
).thenAnswer((_) async {
File(p.join('.tmp', 'pubspec.yaml')).writeAsStringSync(pubspec);
return 62;
});
final result = await command.run();
expect(result, equals(ExitCode.success.code));
verify(
Expand Down Expand Up @@ -294,7 +310,10 @@ void main() {
when(() => generator.description).thenReturn('generator description');
when(
() => generator.generate(any(), vars: any(named: 'vars')),
).thenAnswer((_) async => 62);
).thenAnswer((_) async {
File(p.join('.tmp', 'pubspec.yaml')).writeAsStringSync(pubspec);
return 62;
});
final result = await command.run();
expect(result, equals(ExitCode.success.code));
verify(
Expand Down Expand Up @@ -411,7 +430,10 @@ void main() {
when(() => generator.description).thenReturn('generator description');
when(
() => generator.generate(any(), vars: any(named: 'vars')),
).thenAnswer((_) async => 62);
).thenAnswer((_) async {
File(p.join('.tmp', 'pubspec.yaml')).writeAsStringSync(pubspec);
return 62;
});
final result = await command.run();
expect(result, equals(ExitCode.success.code));
verify(() => logger.progress('Bootstrapping')).called(1);
Expand Down Expand Up @@ -479,6 +501,15 @@ void main() {
expectedLogSummary: 'Created a Very Good Flutter package! 🦄',
);
});

test('flutter plugin template', () async {
await expectValidTemplateName(
getPackagesMsg: 'Running "flutter packages get" in .tmp',
templateName: 'flutter_plugin',
expectedBundle: flutterPluginBundle,
expectedLogSummary: 'Created a Very Good Flutter plugin! 🦄',
);
});
});
});
});
Expand Down