diff --git a/.idea/angel_cli.iml b/.idea/angel_cli.iml index 0f3fa76d..78848ff9 100644 --- a/.idea/angel_cli.iml +++ b/.idea/angel_cli.iml @@ -4,21 +4,11 @@ - - - - - - - - - - diff --git a/bin/angel.dart b/bin/angel.dart index 90aa8cd0..ccb0b146 100644 --- a/bin/angel.dart +++ b/bin/angel.dart @@ -17,6 +17,7 @@ main(List args) async { ..addCommand(new KeyCommand()) ..addCommand(new ServiceCommand()) ..addCommand(new InitCommand()) + ..addCommand(new InstallCommand()) ..addCommand(new TestCommand()) ..addCommand(new PluginCommand()) ..addCommand(new StartCommand()) diff --git a/lib/src/commands/commands.dart b/lib/src/commands/commands.dart index d5360558..28534228 100644 --- a/lib/src/commands/commands.dart +++ b/lib/src/commands/commands.dart @@ -4,6 +4,7 @@ export 'controller.dart'; export "doctor.dart"; export "key.dart"; export "init.dart"; +export "install.dart"; export "make.dart"; export "plugin.dart"; export "rename.dart"; diff --git a/lib/src/commands/init.dart b/lib/src/commands/init.dart index c854f249..cb5480a9 100644 --- a/lib/src/commands/init.dart +++ b/lib/src/commands/init.dart @@ -19,7 +19,14 @@ class InitCommand extends Command { "Initializes a new Angel project in the current directory."; InitCommand() { - argParser.addFlag('pub-get', defaultsTo: true); + argParser + ..addFlag('pub-get', defaultsTo: true) + ..addFlag( + 'legacy', + help: + 'Generate a project using Angel 1.0.x boilerplates, rather than 1.1.x+.', + negatable: false, + ); } @override @@ -60,7 +67,7 @@ class InitCommand extends Command { ..text('\nCongratulations! You are ready to start developing with Angel!') ..text('\nTo start the server (with file watching), run ') ..magenta() - ..text('`dart bin/server.dart`') + ..text(argResults['legacy'] ? '`dart bin/server.dart`' : '`dart bin/dev.dart`') ..normal() ..text(' in your terminal.') ..text('\n\nFind more documentation about Angel:') @@ -122,7 +129,9 @@ class InitCommand extends Command { } print('Choose a project type before continuing:'); - var boilerplateChooser = new Chooser(allBoilerplates); + var boilerplateChooser = new Chooser( + argResults['legacy'] ? legacyBoilerplates : boilerplates, + ); var boilerplate = await boilerplateChooser.choose(); print( @@ -137,6 +146,17 @@ class InitCommand extends Command { throw new Exception("Could not clone repo."); } + if (boilerplate.ref != null) { + git = await Process.start("git", ["checkout", boilerplate.ref]); + + stdout.addStream(git.stdout); + stderr.addStream(git.stderr); + + if (await git.exitCode != 0) { + throw new Exception("Could not checkout branch ${boilerplate.ref}."); + } + } + var gitDir = new Directory.fromUri(projectDir.uri.resolve(".git")); if (await gitDir.exists()) await gitDir.delete(recursive: true); } catch (e) { @@ -176,30 +196,45 @@ preBuild(Directory projectDir) async { } const BoilerplateInfo fullApplicationBoilerplate = const BoilerplateInfo( - 'Full Application', - 'A complete project including authentication, multi-threading, and more.', - 'https://github.com/angel-dart/angel.git'); + 'Full Application', + 'A complete project including authentication, multi-threading, and more.', + 'https://github.com/angel-dart/angel.git', + ref: '1.0.x', +); const BoilerplateInfo lightBoilerplate = const BoilerplateInfo( - 'Light', - 'Minimal starting point for new users.', - 'https://github.com/angel-dart/boilerplate_light.git'); + 'Light', + 'Minimal starting point for new users.', + 'https://github.com/angel-dart/boilerplate_light.git', +); const BoilerplateInfo ormBoilerplate = const BoilerplateInfo( - 'ORM', - "A starting point for applications that use Angel's ORM.", - 'https://github.com/angel-dart/boilerplate_orm.git'); + 'ORM', + "A starting point for applications that use Angel's ORM.", + 'https://github.com/angel-dart/boilerplate_orm.git', +); -const List allBoilerplates = const [ +const List legacyBoilerplates = const [ fullApplicationBoilerplate, lightBoilerplate, ormBoilerplate ]; -class BoilerplateInfo { - final String name, description, url; +const BoilerplateInfo basicBoilerplate = const BoilerplateInfo( + 'Basic', + 'Minimal starting point - A simple server with only a few additional packages.', + 'https://github.com/angel-dart/angel.git', +); - const BoilerplateInfo(this.name, this.description, this.url); +const List boilerplates = const [ + basicBoilerplate, + ormBoilerplate, +]; + +class BoilerplateInfo { + final String name, description, url, ref; + + const BoilerplateInfo(this.name, this.description, this.url, {this.ref}); @override String toString() => '$name ($description)'; diff --git a/lib/src/commands/install.dart b/lib/src/commands/install.dart new file mode 100644 index 00000000..3f2721cd --- /dev/null +++ b/lib/src/commands/install.dart @@ -0,0 +1,245 @@ +import 'dart:async'; +import 'dart:io'; +import 'package:args/command_runner.dart'; +import 'package:console/console.dart'; +import 'package:glob/glob.dart'; +import 'package:homedir/homedir.dart'; +import 'package:mustache4dart/mustache4dart.dart' as mustache; +import 'package:path/path.dart' as p; +import 'package:pubspec/pubspec.dart'; +import 'package:yaml/yaml.dart' as yaml; +import 'make/maker.dart'; + +class InstallCommand extends Command { + static const String repo = 'https://github.com/angel-dart/install.git'; + static final Directory installRepo = + new Directory.fromUri(homeDir.uri.resolve('./.angel/addons')); + + @override + String get name => 'install'; + + @override + String get description => + 'Installs additional add-ons to minimize boilerplate.'; + + InstallCommand() { + argParser + ..addFlag( + 'list', + help: 'List all currently-installed add-ons.', + negatable: false, + defaultsTo: false, + ) + ..addFlag( + 'update', + help: 'Update the local add-on repository.', + negatable: false, + defaultsTo: false, + ) + ..addFlag( + 'wipe', + help: 'Wipe the local add-on repository.', + negatable: false, + defaultsTo: false, + ); + } + + @override + run() async { + if (argResults['wipe']) { + if (await installRepo.exists()) await installRepo.delete(); + } else if (argResults['list']) { + var addons = await list(); + print('${addons.length} add-on(s) installed:'); + + for (var addon in addons) { + print(' * ${addon.name}@${addon.version}: ${addon.description}'); + } + } else if (argResults['update']) { + await update(); + } else if (argResults.rest.isNotEmpty) { + if (!await installRepo.exists()) + throw 'No local add-on database exists. Run `angel install --update` first.'; + + var pubspec = await PubSpec.load(Directory.current); + + for (var packageName in argResults.rest) { + var packageDir = + new Directory.fromUri(installRepo.uri.resolve(packageName)); + + if (!await packageDir.exists()) + throw 'No add-on named "$packageName" is installed. You might need to run `angel install --update`.'; + + Map values = { + 'project_name': pubspec.name, + 'pubspec': pubspec, + }; + + List globs = []; + + var promptFile = + new File.fromUri(packageDir.uri.resolve('angel_cli.yaml')); + var projectPubspec = await PubSpec.load(packageDir); + + if (await promptFile.exists()) { + var contents = await promptFile.readAsString(); + var y = yaml.loadYamlDocument(contents); + var cfg = y.contents.value as Map; + + // Loads globs + if (cfg['templates'] is List) { + globs.addAll(cfg['templates'].map((p) => new Glob(p))); + } + + if (cfg['values'] is Map) { + var val = cfg['values'] as Map; + + for (var key in val.keys) { + var desc = val[key]['description'] ?? key; + + if (val[key]['type'] == 'prompt') { + Prompter prompt; + + if (val[key]['default'] != null) { + prompt = new Prompter('$desc (${val[key]['default']}): '); + } else { + prompt = new Prompter('$desc: '); + } + + if (val[key]['default'] != null) { + var v = await prompt.prompt(); + v = v.isNotEmpty ? v : val[key]['default']; + values[key] = v; + } else + values[key] = + await prompt.prompt(checker: (s) => s.isNotEmpty); + } else if (val[key]['type'] == 'choice') { + var chooser = + new Chooser(val[key]['choices'], message: '$desc: '); + values[key] = await chooser.choose(); + } + } + } + + var deps = projectPubspec.dependencies.keys.map((k) { + var dep = projectPubspec.dependencies[k]; + if (dep is HostedReference) + return new MakerDependency(k, dep.versionConstraint.toString()); + return null; + }).where((d) => d != null); + await depend(deps); + } + + Future merge(Directory src, Directory dst, String prefix) async { + if (!await src.exists()) return; + print('Copying ${src.absolute.path} into ${dst.absolute.path}...'); + if (!await dst.exists()) await dst.create(recursive: true); + + await for (var entity in src.list()) { + if (entity is Directory) { + var name = p.basename(entity.path); + var newDir = new Directory.fromUri(dst.uri.resolve(name)); + await merge( + entity, newDir, prefix.isEmpty ? name : '$prefix/$name'); + } else if (entity is File && + !entity.path.endsWith('angel_cli.yaml')) { + var name = p.basename(entity.path); + var target = dst.uri.resolve(name); + var targetFile = new File.fromUri(target); + bool allClear = !await targetFile.exists(); + + if (!allClear) { + print('The file ${entity.absolute.path} already exists.'); + var p = new Prompter('Overwrite the existing file? [y/N]'); + var answer = await p.prompt( + checker: (s) => s.trim() == 'y' || s.trim() == 'N'); + allClear = answer == 'y'; + if (allClear) await targetFile.delete(); + } + + if (allClear) { + try { + var path = prefix.isEmpty ? name : '$prefix/$name'; + + if (globs.any((g) => g.matches(path))) { + print('Rendering Mustache template from ${entity.absolute + .path} to ${targetFile.absolute.path}...'); + var contents = await entity.readAsString(); + var renderer = mustache.compile(contents); + var generated = renderer(values); + await targetFile.writeAsString(generated); + } else { + print( + 'Copying ${entity.absolute.path} to ${targetFile.absolute.path}...'); + await targetFile.parent.create(recursive: true); + await entity.copy(targetFile.absolute.path); + } + } catch (_) { + print('Failed to copy.'); + } + } else { + print('Skipped ${entity.absolute.path}.'); + } + } + } + } + + await merge(new Directory.fromUri(packageDir.uri.resolve('files')), + Directory.current, ''); + print('Successfully installed $packageName@${projectPubspec.version}.'); + } + } else { + print('No add-ons were specified to be installed.'); + } + } + + Future> list() async { + if (!await installRepo.exists()) { + throw 'No local add-on database exists. Run `angel install --update` first.'; + } else { + List repos = []; + + await for (var entity in installRepo.list()) { + if (entity is Directory) { + try { + repos.add(await PubSpec.load(entity)); + } catch (_) { + // Ignore failures... + } + } + } + + return repos; + } + } + + Future update() async { + Process git; + + if (!await installRepo.exists()) { + git = await Process.start('git', [ + 'clone', + repo, + installRepo.absolute.path, + ]); + } else { + git = await Process.start( + 'git', + [ + 'pull', + 'origin', + 'master', + ], + workingDirectory: installRepo.absolute.path, + ); + } + + git..stdout.listen(stdout.add)..stderr.listen(stderr.add); + + var code = await git.exitCode; + + if (code != 0) { + throw 'git exited with code $code.'; + } + } +} diff --git a/lib/src/commands/pubspec.update.g.dart b/lib/src/commands/pubspec.update.g.dart index 7cf40e16..89067c01 100644 --- a/lib/src/commands/pubspec.update.g.dart +++ b/lib/src/commands/pubspec.update.g.dart @@ -3,7 +3,7 @@ import 'dart:convert'; import 'package:http/src/base_client.dart' as http; import 'package:pub_semver/pub_semver.dart'; -final Version PACKAGE_VERSION = new Version(1, 1, 5, build: '2'); +final Version PACKAGE_VERSION = new Version(1, 2, 0); Future fetchCurrentVersion(http.BaseClient client) async { var response = await client.get('https://pub.dartlang.org/api/packages/angel_cli'); diff --git a/lib/src/commands/service_generators/mongo.dart b/lib/src/commands/service_generators/mongo.dart index c81153ef..eb956d42 100644 --- a/lib/src/commands/service_generators/mongo.dart +++ b/lib/src/commands/service_generators/mongo.dart @@ -21,7 +21,10 @@ class MongoServiceGenerator extends ServiceGenerator { @override void applyToLibrary(LibraryBuilder library, String name, String lower) { - library.addMember(new ImportBuilder('package:mongo_dart/mongo_dart.dart')); + library.addMembers([ + new ImportBuilder('package:angel_mongo/angel_mongo.dart'), + new ImportBuilder('package:mongo_dart/mongo_dart.dart'), + ]); } @override diff --git a/pubspec.yaml b/pubspec.yaml index 8c31aa70..3da27681 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -2,7 +2,7 @@ author: "Tobe O " description: "Command-line tools for the Angel framework." homepage: "https://github.com/angel-dart/angel_cli" name: "angel_cli" -version: 1.1.5+2 +version: 1.2.0 dependencies: # analyzer: "^0.29.0" args: ^0.13.4 @@ -10,9 +10,11 @@ dependencies: console: "^2.2.3" dart_style: ^1.0.0 glob: "^1.1.0" + homedir: ^0.0.4 http: ^0.11.3 id: "^1.0.0" inflection: "^0.4.1" + mustache4dart: ^2.1.0 pubspec: "^0.0.14" random_string: "^0.0.1" recase: "^1.0.0"