diff --git a/packages/cli/.DS_Store b/packages/cli/.DS_Store new file mode 100644 index 00000000..5d196f03 Binary files /dev/null and b/packages/cli/.DS_Store differ diff --git a/packages/cli/.gitignore b/packages/cli/.gitignore new file mode 100644 index 00000000..b0dcebde --- /dev/null +++ b/packages/cli/.gitignore @@ -0,0 +1,80 @@ +# Created by .ignore support plugin (hsz.mobi) +### JetBrains template +# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm +# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 + +# User-specific stuff: +.idea/workspace.xml +.idea/tasks.xml +.idea/dictionaries +.idea/vcs.xml +.idea/jsLibraryMappings.xml + +# Sensitive or high-churn files: +.idea/dataSources.ids +.idea/dataSources.xml +.idea/dataSources.local.xml +.idea/sqlDataSources.xml +.idea/dynamic.xml +.idea/uiDesigner.xml + +# Gradle: +.idea/gradle.xml +.idea/libraries + +# Mongo Explorer plugin: +.idea/mongoSettings.xml + +## File-based project format: +*.iws + +## Plugin-specific files: + +# IntelliJ +/out/ + +# mpeltonen/sbt-idea plugin +.idea_modules/ + +# JIRA plugin +atlassian-ide-plugin.xml + +# Crashlytics plugin (for Android Studio and IntelliJ) +com_crashlytics_export_strings.xml +crashlytics.properties +crashlytics-build.properties +fabric.properties +### Dart template +# See https://www.dartlang.org/tools/private-files.html + +# Files and directories created by pub +.buildlog +.packages +.project +.pub/ +build/ +**/packages/ + +# Files created by dart2js +# (Most Dart developers will use pub build to compile Dart, use/modify these +# rules if you intend to use dart2js directly +# Convention is to use extension '.dart.js' for Dart compiled to Javascript to +# differentiate from explicit Javascript files) +*.dart.js +*.part.js +*.js.deps +*.js.map +*.info.json + +# Directory created by dartdoc +doc/api/ + +# Don't commit pubspec lock file +# (Library packages only! Remove pattern if developing an application package) +pubspec.lock +/sample_project/lib/src/services/ +/sample_project/test/services/ +/sample_project/ +sample_project/ +sample-project +.dart_tool \ No newline at end of file diff --git a/packages/cli/.idea/angel_cli.iml b/packages/cli/.idea/angel_cli.iml new file mode 100644 index 00000000..6118a7ae --- /dev/null +++ b/packages/cli/.idea/angel_cli.iml @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/packages/cli/.idea/modules.xml b/packages/cli/.idea/modules.xml new file mode 100644 index 00000000..5f91449b --- /dev/null +++ b/packages/cli/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/packages/cli/.idea/runConfigurations/Controller.xml b/packages/cli/.idea/runConfigurations/Controller.xml new file mode 100644 index 00000000..38315109 --- /dev/null +++ b/packages/cli/.idea/runConfigurations/Controller.xml @@ -0,0 +1,8 @@ + + + + \ No newline at end of file diff --git a/packages/cli/.idea/runConfigurations/Doctor.xml b/packages/cli/.idea/runConfigurations/Doctor.xml new file mode 100644 index 00000000..bd640ddb --- /dev/null +++ b/packages/cli/.idea/runConfigurations/Doctor.xml @@ -0,0 +1,7 @@ + + + + \ No newline at end of file diff --git a/packages/cli/.idea/runConfigurations/Init.xml b/packages/cli/.idea/runConfigurations/Init.xml new file mode 100644 index 00000000..c0740078 --- /dev/null +++ b/packages/cli/.idea/runConfigurations/Init.xml @@ -0,0 +1,8 @@ + + + + \ No newline at end of file diff --git a/packages/cli/.idea/runConfigurations/Show_Help.xml b/packages/cli/.idea/runConfigurations/Show_Help.xml new file mode 100644 index 00000000..009fef31 --- /dev/null +++ b/packages/cli/.idea/runConfigurations/Show_Help.xml @@ -0,0 +1,7 @@ + + + + \ No newline at end of file diff --git a/packages/cli/.idea/runConfigurations/Update.xml b/packages/cli/.idea/runConfigurations/Update.xml new file mode 100644 index 00000000..75cab2d3 --- /dev/null +++ b/packages/cli/.idea/runConfigurations/Update.xml @@ -0,0 +1,8 @@ + + + + \ No newline at end of file diff --git a/packages/cli/.idea/runConfigurations/Version.xml b/packages/cli/.idea/runConfigurations/Version.xml new file mode 100644 index 00000000..cd70052b --- /dev/null +++ b/packages/cli/.idea/runConfigurations/Version.xml @@ -0,0 +1,7 @@ + + + + \ No newline at end of file diff --git a/packages/cli/.travis.yml b/packages/cli/.travis.yml new file mode 100644 index 00000000..3939d628 --- /dev/null +++ b/packages/cli/.travis.yml @@ -0,0 +1 @@ +language: dart diff --git a/packages/cli/CHANGELOG.md b/packages/cli/CHANGELOG.md new file mode 100644 index 00000000..7e3bf35f --- /dev/null +++ b/packages/cli/CHANGELOG.md @@ -0,0 +1,89 @@ +# 2.1.7+1 +* Fix a bug where new directories were not being created in +`init`. + +# 2.1.7 +* Fix a bug where `ArgResults.arguments` was used in `init` instead of the +intended `ArgResults.rest`. +* Stop including `package:angel_model` imports in `make model`. +* Update dependencies in `make` commands. +* Fix `make model` to generate ORM + migration by default. +* Fix `MakerDependency` logic to print missing dependencies. + +# 2.1.6 +* Fix a bug where models always defaulted to ORM. +* Add GraphQL boilerplate. +* Automatically restore terminal colors on shutdown. + +# 2.1.5+1 +* Update to `inflection2`. + +# 2.1.5 +* Add `shared` boilerplates. +* Remove uncecessary `angel_model` imports. + +# 2.1.4+1 +* Patch `part of 'path'` renames. + +# 2.1.4 +* The `migration` argument to `model` just emits an annotation now. +* Add the ORM boilerplate. + +# 2.1.3 +* Fix generation of ORM models. +* A `--project-name` to `init` command. + +# 2.1.2 +* No migrations-by-default. + +# 2.1.1 +* Edit the way `rename` runs, leaving no corner unturned. + +# 2.1.0 +* Deprecate `angel install`. +* Rename projects using `snake_case`. +* `init` now fetches from `master`. +* Remove the `1.x` option. +* Add `make migration` command. +* Replace `{{oldName}}` in the `rename` command. +* `pub get` now runs with `inheritStdio`. + +# 2.0.1 +* `deploy systemd` now has an `--install` option, where you can immediately +spawn the service. + +# 2.0.0 +* `init` can now produce either 1.x or 2.x projects. +* Fixed deps for compatibility with Dart2 stable. + +# 1.3.4 +* Fix another typo. + +# 1.3.3 +* Fix a small typo in the model generator. + +# 1.3.2 +* Restore `part` directives in generated models. + +# 1.3.1 +* Add `deploy nginx` and `deploy systemd`. + +# 1.3.0 +* Focus on Dart2 from here on out. +* Update `code_builder`. +* More changes... + +# 1.1.5 +Deprecated several commands, in favor of the `make` +command: +* `controller` +* `plugin` +* `service` +* `test` + +The `rename` command will now replace *all* occurrences +of the old project names with the new one in `config/` +YAML files, and also operates on the glob `config/**/*.yaml`. + +Changed the call to run `angel start` to run `dart bin/server.dart` instead, after an +`init` command. diff --git a/packages/cli/LICENSE b/packages/cli/LICENSE new file mode 100644 index 00000000..63b4b681 --- /dev/null +++ b/packages/cli/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) [year] [fullname] + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/packages/cli/README.md b/packages/cli/README.md new file mode 100644 index 00000000..3b96938f --- /dev/null +++ b/packages/cli/README.md @@ -0,0 +1,22 @@ +# angel_cli + +![Screenshot of Terminal](screenshots/screenshot.png) + +Command-line tools for the Angel framework. +Includes functionality such as: +* Project scaffolding +* Generating service models, plugins, tests and more +* Renaming projects +* Much more... + +To install: + +```bash +$ pub global activate angel_cli +``` + +And then, for information on each command: + +```bash +$ angel help +``` diff --git a/packages/cli/TODO.md b/packages/cli/TODO.md new file mode 100644 index 00000000..74835954 --- /dev/null +++ b/packages/cli/TODO.md @@ -0,0 +1,7 @@ +# Todo + +* `service` + * Add tests +* `migration` +* `deploy` +* Call these from Grinder script :) \ No newline at end of file diff --git a/packages/cli/analysis_options.yaml b/packages/cli/analysis_options.yaml new file mode 100644 index 00000000..eae1e42a --- /dev/null +++ b/packages/cli/analysis_options.yaml @@ -0,0 +1,3 @@ +analyzer: + strong-mode: + implicit-casts: false \ No newline at end of file diff --git a/packages/cli/bin/angel.dart b/packages/cli/bin/angel.dart new file mode 100644 index 00000000..43c62d82 --- /dev/null +++ b/packages/cli/bin/angel.dart @@ -0,0 +1,55 @@ +#!/usr/bin/env dart +library angel_cli.tool; + +import "dart:io"; +import "package:args/command_runner.dart"; +import 'package:angel_cli/angel_cli.dart'; +import 'package:io/ansi.dart'; + +final String DOCTOR = "doctor"; + +main(List args) async { + var runner = new CommandRunner( + "angel", + asciiArt.trim() + + '\n\n' + + "Command-line tools for the Angel framework." + + '\n\n' + + 'https://angel-dart.github.io'); + + runner.argParser + .addFlag('verbose', help: 'Print verbose output.', negatable: false); + + runner + ..addCommand(new DeployCommand()) + ..addCommand(new DoctorCommand()) + ..addCommand(new KeyCommand()) + ..addCommand(new InitCommand()) + ..addCommand(new InstallCommand()) + ..addCommand(new RenameCommand()) + ..addCommand(new MakeCommand()); + + return await runner.run(args).catchError((exc, st) { + if (exc is String) { + stdout.writeln(exc); + } else { + stderr.writeln("Oops, something went wrong: $exc"); + if (args.contains('--verbose')) { + stderr.writeln(st); + } + } + + exitCode = 1; + }).whenComplete(() { + stdout.write(resetAll.wrap('')); + }); +} + +const String asciiArt = ''' +____________ ________________________ +___ |__ | / /_ ____/__ ____/__ / +__ /| |_ |/ /_ / __ __ __/ __ / +_ ___ | /| / / /_/ / _ /___ _ /___ +/_/ |_/_/ |_/ \____/ /_____/ /_____/ + +'''; diff --git a/packages/cli/example/main.dart b/packages/cli/example/main.dart new file mode 100644 index 00000000..ffecc0d0 --- /dev/null +++ b/packages/cli/example/main.dart @@ -0,0 +1,3 @@ +void main() { + // This package isn't usable from code. +} diff --git a/packages/cli/lib/angel_cli.dart b/packages/cli/lib/angel_cli.dart new file mode 100644 index 00000000..843a6734 --- /dev/null +++ b/packages/cli/lib/angel_cli.dart @@ -0,0 +1,3 @@ +library angel_cli; + +export 'src/commands/commands.dart'; diff --git a/packages/cli/lib/src/commands/commands.dart b/packages/cli/lib/src/commands/commands.dart new file mode 100644 index 00000000..a6db230d --- /dev/null +++ b/packages/cli/lib/src/commands/commands.dart @@ -0,0 +1,9 @@ +library angel_cli.commands; + +export "deploy.dart"; +export "doctor.dart"; +export "key.dart"; +export "init.dart"; +export "install.dart"; +export "make.dart"; +export "rename.dart"; diff --git a/packages/cli/lib/src/commands/deploy.dart b/packages/cli/lib/src/commands/deploy.dart new file mode 100644 index 00000000..92b5f82e --- /dev/null +++ b/packages/cli/lib/src/commands/deploy.dart @@ -0,0 +1,17 @@ +import 'package:args/command_runner.dart'; +import 'deploy/nginx.dart'; +import 'deploy/systemd.dart'; + +class DeployCommand extends Command { + @override + String get name => 'deploy'; + + @override + String get description => + 'Generates scaffolding + helper functionality for deploying servers. Run this in your project root.'; + + DeployCommand() { + addSubcommand(new NginxCommand()); + addSubcommand(new SystemdCommand()); + } +} diff --git a/packages/cli/lib/src/commands/deploy/nginx.dart b/packages/cli/lib/src/commands/deploy/nginx.dart new file mode 100644 index 00000000..638f70ac --- /dev/null +++ b/packages/cli/lib/src/commands/deploy/nginx.dart @@ -0,0 +1,52 @@ +import 'dart:io'; +import 'package:args/command_runner.dart'; +import 'package:io/ansi.dart'; +import 'package:path/path.dart' as p; +import '../../util.dart'; + +class NginxCommand extends Command { + @override + String get name => 'nginx'; + + @override + String get description => + 'Generates a NGINX configuration for a reverse proxy + static server.'; + + NginxCommand() { + argParser.addOption('out', + abbr: 'o', + help: + 'An optional output file to write to; otherwise prints to stdout.'); + } + + @override + run() async { + var webPath = p.join(p.current, 'web'); + var nginxText = ''' +server { + listen 80 default_server; + root ${p.absolute(webPath)}; # Set to your static files directory + + location / { + try_files \$uri @proxy; # Try to serve static files; fallback to proxied Angel server + } + + location @proxy { + proxy_pass http://127.0.0.1:3000; + proxy_http_version 1.1; # Important, do not omit + } +} + ''' + .trim(); + + if (!argResults.wasParsed('out')) { + print(nginxText); + } else { + var file = new File(argResults['out'] as String); + await file.create(recursive: true); + await file.writeAsString(nginxText); + print(green.wrap( + "$checkmark Successfully generated nginx configuration in '${file.path}'.")); + } + } +} diff --git a/packages/cli/lib/src/commands/deploy/systemd.dart b/packages/cli/lib/src/commands/deploy/systemd.dart new file mode 100644 index 00000000..28c496a8 --- /dev/null +++ b/packages/cli/lib/src/commands/deploy/systemd.dart @@ -0,0 +1,86 @@ +import 'dart:io'; +import 'package:args/command_runner.dart'; +import 'package:io/ansi.dart'; +import 'package:path/path.dart' as p; +import '../../util.dart'; + +class SystemdCommand extends Command { + @override + String get name => 'systemd'; + + @override + String get description => + 'Generates a systemd service to continuously run your server.'; + + SystemdCommand() { + argParser + ..addOption('install', + abbr: 'i', help: 'A name to install this service as on the system.') + ..addOption('user', + abbr: 'u', + defaultsTo: 'web', + help: 'The name of the unprivileged account to run the server as.') + ..addOption('out', + abbr: 'o', + help: + 'An optional output file to write to; otherwise prints to stdout.'); + } + + @override + run() async { + var projectPath = p.absolute(p.current); + var pubspec = await loadPubspec(); + var user = argResults['user']; + var systemdText = ''' +[Unit] +Description=`${pubspec.name}` server + +[Service] +Environment=ANGEL_ENV=production +User=$user # Name of unprivileged `$user` user +WorkingDirectory=$projectPath # Path to `${pubspec.name}` project +ExecStart=${Platform.resolvedExecutable} bin/prod.dart +Restart=always # Restart process on crash + +[Install] +WantedBy=multi-user.target + ''' + .trim(); + + if (!argResults.wasParsed('out') && !argResults.wasParsed('install')) { + print(systemdText); + } else if (argResults.wasParsed('install')) { + var systemdPath = argResults.wasParsed('out') + ? argResults['out'] as String + : p.join('etc', 'systemd', 'system'); + var serviceFilename = p.join(systemdPath, + p.setExtension(argResults['install'] as String, '.service')); + var file = new File(serviceFilename); + await file.create(recursive: true); + await file.writeAsString(systemdText); + print(green.wrap( + "$checkmark Successfully generated systemd service in '${file.path}'.")); + + // sudo systemctl daemon-reload + if (await runCommand('sudo', ['systemctl', 'daemon-reload'])) { + // sudo service start + if (await runCommand('sudo', [ + 'service', + p.basenameWithoutExtension(serviceFilename), + 'start' + ])) { + } else { + print(red.wrap('$ballot Failed to install service system-wide.')); + } + } else { + print(red.wrap('$ballot Failed to install service system-wide.')); + } + } else { + var file = new File(argResults['out'] as String); + await file.create(recursive: true); + await file.writeAsString(systemdText); + print(green.wrap( + "$checkmark Successfully generated systemd service in '${file.path}'.")); + } + } +} diff --git a/packages/cli/lib/src/commands/doctor.dart b/packages/cli/lib/src/commands/doctor.dart new file mode 100644 index 00000000..b435b6f0 --- /dev/null +++ b/packages/cli/lib/src/commands/doctor.dart @@ -0,0 +1,34 @@ +import "dart:convert"; +import "dart:io"; +import "package:args/command_runner.dart"; +import 'package:io/ansi.dart'; +import '../util.dart'; + +class DoctorCommand extends Command { + @override + String get name => "doctor"; + + @override + String get description => + "Ensures that the current system is capable of running Angel."; + + @override + run() async { + print("Checking your system for dependencies..."); + await _checkForGit(); + } + + _checkForGit() async { + try { + var git = await Process.start("git", ["--version"]); + if (await git.exitCode == 0) { + var version = await git.stdout.transform(utf8.decoder).join(); + print(green.wrap( + "$checkmark Git executable found: v${version.replaceAll('git version', '').trim()}")); + } else + throw new Exception("Git executable exit code not 0"); + } catch (exc) { + print(red.wrap("$ballot Git executable not found")); + } + } +} diff --git a/packages/cli/lib/src/commands/init.dart b/packages/cli/lib/src/commands/init.dart new file mode 100644 index 00000000..7dafb866 --- /dev/null +++ b/packages/cli/lib/src/commands/init.dart @@ -0,0 +1,322 @@ +import 'dart:async'; +import "dart:io"; +import "package:args/command_runner.dart"; +import 'package:io/ansi.dart'; +import 'package:path/path.dart' as p; +import 'package:prompts/prompts.dart' as prompts; +import 'package:recase/recase.dart'; +import '../random_string.dart' as rs; +import '../util.dart'; +import 'key.dart'; +import 'pub.dart'; +import 'rename.dart'; + +class InitCommand extends Command { + final KeyCommand _key = new KeyCommand(); + + @override + String get name => "init"; + + @override + String get description => + "Initializes a new Angel project in the current directory."; + + InitCommand() { + argParser + ..addFlag('offline', + help: + 'Disable online fetching of boilerplates. Also disables `pub-get`.', + negatable: false) + ..addFlag('pub-get', defaultsTo: true) + ..addOption('project-name', + abbr: 'n', help: 'The name for this project.'); + } + + @override + run() async { + Directory projectDir = + new Directory(argResults.rest.isEmpty ? "." : argResults.rest[0]); + print("Creating new Angel project in ${projectDir.absolute.path}..."); + await _cloneRepo(projectDir); + // await preBuild(projectDir); + var secret = rs.randomAlphaNumeric(32); + print('Generated new development JWT secret: $secret'); + await _key.changeSecret( + new File.fromUri(projectDir.uri.resolve('config/default.yaml')), + secret); + + secret = rs.randomAlphaNumeric(32); + print('Generated new production JWT secret: $secret'); + await _key.changeSecret( + new File.fromUri(projectDir.uri.resolve('config/production.yaml')), + secret); + + var name = argResults.wasParsed('project-name') + ? argResults['project-name'] as String + : p.basenameWithoutExtension( + projectDir.absolute.uri.normalizePath().toFilePath()); + + name = ReCase(name).snakeCase; + print('Renaming project from "angel" to "$name"...'); + await renamePubspec(projectDir, 'angel', name); + await renameDartFiles(projectDir, 'angel', name); + + if (argResults['pub-get'] != false && argResults['offline'] == false) { + print('Now running pub get...'); + await _pubGet(projectDir); + } + + print(green.wrap("$checkmark Successfully initialized Angel project.")); + + stdout + ..writeln() + ..writeln( + 'Congratulations! You are ready to start developing with Angel!') + ..write('To start the server (with ') + ..write(cyan.wrap('hot-reloading')) + ..write('), run ') + ..write(magenta.wrap('`dart --observe bin/dev.dart`')) + ..writeln(' in your terminal.') + ..writeln() + ..writeln('Find more documentation about Angel:') + ..writeln(' * https://angel-dart.github.io') + ..writeln(' * https://github.com/angel-dart/angel/wiki') + ..writeln( + ' * https://www.youtube.com/playlist?list=PLl3P3tmiT-frEV50VdH_cIrA2YqIyHkkY') + ..writeln(' * https://medium.com/the-angel-framework') + ..writeln(' * https://dart.academy/tag/angel') + ..writeln() + ..writeln('Happy coding!'); + } + + _deleteRecursive(FileSystemEntity entity, [bool self = true]) async { + if (entity is Directory) { + await for (var entity in entity.list(recursive: true)) { + try { + await _deleteRecursive(entity); + } catch (e) {} + } + + try { + if (self != false) await entity.delete(recursive: true); + } catch (e) {} + } else if (entity is File) { + try { + await entity.delete(recursive: true); + } catch (e) {} + } else if (entity is Link) { + var path = await entity.resolveSymbolicLinks(); + var stat = await FileStat.stat(path); + + switch (stat.type) { + case FileSystemEntityType.directory: + return await _deleteRecursive(new Directory(path)); + case FileSystemEntityType.file: + return await _deleteRecursive(new File(path)); + default: + break; + } + } + } + + _cloneRepo(Directory projectDir) async { + Directory boilerplateDir; + + try { + if (await projectDir.exists()) { + var shouldDelete = prompts.getBool( + "Directory '${projectDir.absolute.path}' already exists. Overwrite it?"); + + if (!shouldDelete) + throw "Chose not to overwrite existing directory."; + else if (projectDir.absolute.uri.normalizePath().toFilePath() != + Directory.current.absolute.uri.normalizePath().toFilePath()) + await projectDir.delete(recursive: true); + else { + await _deleteRecursive(projectDir, false); + } + } + + // var boilerplate = basicBoilerplate; + print('Choose a project type before continuing:'); + var boilerplate = prompts.choose( + 'Choose a project type before continuing', boilerplates); + + // Ultimately, we want a clone of every boilerplate locally on the system. + var boilerplateRootDir = Directory(p.join(angelDir.path, 'boilerplates')); + var boilerplateBasename = p.basenameWithoutExtension(boilerplate.url); + if (boilerplate.ref != null) boilerplateBasename += '.${boilerplate.ref}'; + boilerplateDir = + Directory(p.join(boilerplateRootDir.path, boilerplateBasename)); + await boilerplateRootDir.create(recursive: true); + + var branch = boilerplate.ref ?? 'master'; + + // If there is no clone existing, clone it. + if (!await boilerplateDir.exists()) { + if (argResults['offline'] as bool) { + throw Exception( + '--offline was selected, but the "${boilerplate.name}" boilerplate has not yet been downloaded.'); + } + + print( + 'Cloning "${boilerplate.name}" boilerplate from "${boilerplate.url}"...'); + Process git; + + if (boilerplate.ref == null) { + print(darkGray.wrap( + '\$ git clone --depth 1 ${boilerplate.url} ${boilerplateDir.absolute.path}')); + git = await Process.start( + "git", + [ + "clone", + "--depth", + "1", + boilerplate.url, + boilerplateDir.absolute.path + ], + mode: ProcessStartMode.inheritStdio, + ); + } else { + // git clone --single-branch -b branch host:/dir.git + print(darkGray.wrap( + '\$ git clone --depth 1 --single-branch -b ${boilerplate.ref} ${boilerplate.url} ${boilerplateDir.absolute.path}')); + git = await Process.start( + "git", + [ + "clone", + "--depth", + "1", + "--single-branch", + "-b", + boilerplate.ref, + boilerplate.url, + boilerplateDir.absolute.path + ], + mode: ProcessStartMode.inheritStdio, + ); + } + + if (await git.exitCode != 0) { + throw new Exception("Could not clone repo."); + } + } + + // Otherwise, pull from git. + else if (!(argResults['offline'] as bool)) { + print(darkGray.wrap('\$ git pull origin $branch')); + var git = await Process.start("git", ['pull', 'origin', '$branch'], + mode: ProcessStartMode.inheritStdio, + workingDirectory: boilerplateDir.absolute.path); + if (await git.exitCode != 0) { + print(yellow.wrap( + "Update of $branch failed. Attempting to continue with existing contents.")); + } + } else { + print(darkGray.wrap( + 'Using existing contents of "${boilerplate.name}" boilerplate.')); + } + + // Next, just copy everything into the given directory. + await copyDirectory(boilerplateDir, projectDir); + + if (boilerplate.needsPrebuild) { + await preBuild(projectDir).catchError((_) => null); + } + + var gitDir = new Directory.fromUri(projectDir.uri.resolve(".git")); + if (await gitDir.exists()) await gitDir.delete(recursive: true); + } catch (e) { + await boilerplateDir.delete(recursive: true).catchError((_) => null); + + if (e is! String) { + print(red.wrap("$ballot Could not initialize Angel project.")); + } + rethrow; + } + } + + _pubGet(Directory projectDir) async { + var pubPath = resolvePub(); + print(darkGray.wrap('Running pub at "$pubPath"...')); + print(darkGray.wrap('\$ $pubPath get')); + var pub = await Process.start(pubPath, ["get"], + workingDirectory: projectDir.absolute.path, + mode: ProcessStartMode.inheritStdio); + var code = await pub.exitCode; + print("Pub process exited with code $code"); + } +} + +Future preBuild(Directory projectDir) async { + // Run build + // print('Running `pub run build_runner build`...'); + print(darkGray.wrap('\$ pub run build_runner build')); + + var build = await Process.start( + resolvePub(), ['run', 'build_runner', 'build'], + workingDirectory: projectDir.absolute.path, + mode: ProcessStartMode.inheritStdio); + + var buildCode = await build.exitCode; + + if (buildCode != 0) throw new Exception('Failed to pre-build resources.'); +} + +const BoilerplateInfo graphQLBoilerplate = const BoilerplateInfo( + 'GraphQL', + "A starting point for GraphQL API servers.", + 'https://github.com/angel-dart/angel.git', + ref: 'graphql', +); + +const BoilerplateInfo ormBoilerplate = const BoilerplateInfo( + 'ORM', + "A starting point for applications that use Angel's ORM.", + 'https://github.com/angel-dart/angel.git', + ref: 'orm', +); + +const BoilerplateInfo basicBoilerplate = const BoilerplateInfo( + 'Basic', + 'Minimal starting point for Angel 2.x - A simple server with only a few additional packages.', + 'https://github.com/angel-dart/angel.git'); + +const BoilerplateInfo legacyBoilerplate = const BoilerplateInfo( + 'Legacy', + 'Minimal starting point for applications running Angel 1.1.x.', + 'https://github.com/angel-dart/angel.git', + ref: '1.1.x', +); + +const BoilerplateInfo sharedBoilerplate = const BoilerplateInfo( + 'Shared', + 'Holds common models and files shared across multiple Dart projects.', + 'https://github.com/angel-dart/boilerplate_shared.git'); + +const BoilerplateInfo sharedOrmBoilerplate = const BoilerplateInfo( + 'Shared (ORM)', + 'Holds common models and files shared across multiple Dart projects.', + 'https://github.com/angel-dart/boilerplate_shared.git', + ref: 'orm', +); + +const List boilerplates = const [ + basicBoilerplate, + //legacyBoilerplate, + ormBoilerplate, + graphQLBoilerplate, + sharedBoilerplate, + sharedOrmBoilerplate, +]; + +class BoilerplateInfo { + final String name, description, url, ref; + final bool needsPrebuild; + + const BoilerplateInfo(this.name, this.description, this.url, + {this.ref, this.needsPrebuild: false}); + + @override + String toString() => '$name ($description)'; +} diff --git a/packages/cli/lib/src/commands/install.dart b/packages/cli/lib/src/commands/install.dart new file mode 100644 index 00000000..d7a9ec37 --- /dev/null +++ b/packages/cli/lib/src/commands/install.dart @@ -0,0 +1,248 @@ +import 'dart:async'; +import 'dart:io'; +import 'package:args/command_runner.dart'; +import 'package:glob/glob.dart'; +import 'package:io/ansi.dart'; +import 'package:mustache4dart/mustache4dart.dart' as mustache; +import 'package:path/path.dart' as p; +import 'package:prompts/prompts.dart' as prompts; +import 'package:pubspec_parse/pubspec_parse.dart'; +import 'package:yaml/yaml.dart' as yaml; +import '../util.dart'; +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 { + print(yellow.wrap( + 'WARNING: The `install` command is no longer considered necessary, and has been deprecated.\n' + 'Expect it to be removed in an upcoming release.\n\n' + 'See here: https://github.com/angel-dart/install.git\n\n' + 'To stop seeing this, downgrade to `package:angel_cli@<=2.0.0`.')); + + if (argResults['wipe'] as bool) { + if (await installRepo.exists()) await installRepo.delete(recursive: true); + } else if (argResults['list'] as bool) { + 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'] as bool) { + 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 loadPubspec(); + + 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`.'; + print('Installing $packageName...'); + + Map values = { + 'project_name': pubspec.name, + 'pubspec': pubspec, + }; + + List globs = []; + + var projectPubspec = await loadPubspec(packageDir); + var deps = projectPubspec.dependencies.keys + .map((k) { + var dep = projectPubspec.dependencies[k]; + if (dep is HostedDependency) + return new MakerDependency(k, dep.version.toString()); + return null; + }) + .where((d) => d != null) + .toList(); + + deps.addAll(projectPubspec.devDependencies.keys.map((k) { + var dep = projectPubspec.devDependencies[k]; + if (dep is HostedDependency) + return new MakerDependency(k, dep.version.toString(), dev: true); + return null; + }).where((d) => d != null)); + + await depend(deps); + + var promptFile = + new File.fromUri(packageDir.uri.resolve('angel_cli.yaml')); + + 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'] as List).map((p) => new Glob(p.toString()))); + } + + 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') { + values[key] = prompts.get(desc.toString(), + defaultsTo: val[key]['default']?.toString()); + } else if (val[key]['type'] == 'choice') { + values[key] = prompts.choose( + desc.toString(), val[key]['choices'] as Iterable); + } + } + } + } + + 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.'); + allClear = prompts.getBool('Overwrite the existing file?'); + 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.toString()); + } 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 loadPubspec(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/packages/cli/lib/src/commands/key.dart b/packages/cli/lib/src/commands/key.dart new file mode 100644 index 00000000..cab986f8 --- /dev/null +++ b/packages/cli/lib/src/commands/key.dart @@ -0,0 +1,30 @@ +import 'dart:io'; +import 'package:args/command_runner.dart'; +import '../random_string.dart' as rs; + +class KeyCommand extends Command { + @override + String get name => 'key'; + + @override + String get description => 'Generates a new `angel_auth` key.'; + + @override + run() async { + var secret = rs.randomAlphaNumeric(32); + print('Generated new development JWT secret: $secret'); + await changeSecret(new File('config/default.yaml'), secret); + + secret = rs.randomAlphaNumeric(32); + print('Generated new production JWT secret: $secret'); + await changeSecret(new File('config/production.yaml'), secret); + } + + changeSecret(File file, String secret) async { + if (await file.exists()) { + var contents = await file.readAsString(); + contents = contents.replaceAll(new RegExp(r'jwt_secret:[^\n]+\n?'), ''); + await file.writeAsString(contents.trim() + '\njwt_secret: "$secret"'); + } + } +} diff --git a/packages/cli/lib/src/commands/make.dart b/packages/cli/lib/src/commands/make.dart new file mode 100644 index 00000000..219fedf1 --- /dev/null +++ b/packages/cli/lib/src/commands/make.dart @@ -0,0 +1,25 @@ +import 'package:args/command_runner.dart'; +import 'make/controller.dart'; +import 'make/migration.dart'; +import 'make/model.dart'; +import 'make/plugin.dart'; +import 'make/service.dart'; +import 'make/test.dart'; + +class MakeCommand extends Command { + @override + String get name => 'make'; + + @override + String get description => + 'Generates common code for your project, such as projects and controllers.'; + + MakeCommand() { + addSubcommand(new ControllerCommand()); + addSubcommand(new MigrationCommand()); + addSubcommand(new ModelCommand()); + addSubcommand(new PluginCommand()); + addSubcommand(new TestCommand()); + addSubcommand(new ServiceCommand()); + } +} diff --git a/packages/cli/lib/src/commands/make/controller.dart b/packages/cli/lib/src/commands/make/controller.dart new file mode 100644 index 00000000..209aa401 --- /dev/null +++ b/packages/cli/lib/src/commands/make/controller.dart @@ -0,0 +1,123 @@ +import 'dart:io'; +import 'package:args/command_runner.dart'; +import 'package:code_builder/code_builder.dart'; +import 'package:dart_style/dart_style.dart'; +import 'package:io/ansi.dart'; +import 'package:prompts/prompts.dart' as prompts; +import 'package:recase/recase.dart'; +import '../../util.dart'; +import 'maker.dart'; + +class ControllerCommand extends Command { + @override + String get name => 'controller'; + + @override + String get description => 'Generates a controller class.'; + + ControllerCommand() { + argParser + ..addFlag('websocket', + abbr: 'w', + help: + 'Generates a WebSocketController, instead of an HTTP controller.', + negatable: false) + ..addOption('name', + abbr: 'n', help: 'Specifies a name for the model class.') + ..addOption('output-dir', + help: 'Specifies a directory to create the controller class in.', + defaultsTo: 'lib/src/routes/controllers'); + } + + @override + run() async { + var pubspec = await loadPubspec(); + String name; + if (argResults.wasParsed('name')) name = argResults['name'] as String; + + if (name?.isNotEmpty != true) { + name = prompts.get('Name of controller class'); + } + + List deps = [ + const MakerDependency('angel_framework', '^2.0.0') + ]; + + // ${pubspec.name}.src.models.${rc.snakeCase} + + var rc = new ReCase(name); + var controllerLib = new Library((controllerLib) { + if (argResults['websocket'] as bool) { + deps.add(const MakerDependency('angel_websocket', '^2.0.0')); + controllerLib.directives + .add(new Directive.import('package:angel_websocket/server.dart')); + } else { + controllerLib.directives.add(new Directive.import( + 'package:angel_framework/angel_framework.dart')); + } + + controllerLib.body.add(new Class((clazz) { + clazz + ..name = '${rc.pascalCase}Controller' + ..extend = refer(argResults['websocket'] as bool + ? 'WebSocketController' + : 'Controller'); + + if (argResults['websocket'] as bool) { + // XController(AngelWebSocket ws) : super(ws); + clazz.constructors.add(new Constructor((b) { + b + ..requiredParameters.add(new Parameter((b) => b + ..name = 'ws' + ..type = refer('AngelWebSocket'))) + ..initializers.add(new Code('super(ws)')); + })); + + clazz.methods.add(new Method((meth) { + meth + ..name = 'hello' + ..returns = refer('void') + ..annotations + .add(refer('ExposeWs').call([literal('get_${rc.snakeCase}')])) + ..requiredParameters.add(new Parameter((b) => b + ..name = 'socket' + ..type = refer('WebSocketContext'))) + ..body = new Block((block) { + block.addExpression(refer('socket').property('send').call([ + literal('got_${rc.snakeCase}'), + literalMap({'message': literal('Hello, world!')}), + ])); + }); + })); + } else { + clazz + ..annotations + .add(refer('Expose').call([literal('/${rc.snakeCase}')])) + ..methods.add(new Method((meth) { + meth + ..name = 'hello' + ..returns = refer('String') + ..body = literal('Hello, world').returned.statement + ..annotations.add(refer('Expose').call([ + literal('/'), + ])); + })); + } + })); + }); + + var outputDir = new Directory.fromUri( + Directory.current.uri.resolve(argResults['output-dir'] as String)); + var controllerFile = + new File.fromUri(outputDir.uri.resolve('${rc.snakeCase}.dart')); + if (!await controllerFile.exists()) + await controllerFile.create(recursive: true); + await controllerFile.writeAsString(new DartFormatter() + .format(controllerLib.accept(new DartEmitter()).toString())); + + print(green.wrap( + '$checkmark Created controller file "${controllerFile.absolute.path}"')); + + if (deps.isNotEmpty) await depend(deps); + } +} diff --git a/packages/cli/lib/src/commands/make/maker.dart b/packages/cli/lib/src/commands/make/maker.dart new file mode 100644 index 00000000..09bf1bd9 --- /dev/null +++ b/packages/cli/lib/src/commands/make/maker.dart @@ -0,0 +1,82 @@ +import 'dart:async'; +import 'package:io/ansi.dart'; +import '../../util.dart'; + +class MakerDependency implements Comparable { + final String name, version; + final bool dev; + + const MakerDependency(this.name, this.version, {this.dev: false}); + + @override + int compareTo(MakerDependency other) => name.compareTo(other.name); +} + +Future depend(Iterable deps) async { + var pubspec = await loadPubspec(); + var missing = []; + + for (var dep in deps) { + var isPresent = false; + if (dep.dev) + isPresent = pubspec.devDependencies.containsKey(dep.name); + else + isPresent = pubspec.dependencies.containsKey(dep.name); + + if (!isPresent) { + missing.add(dep); +// TODO: https://github.com/dart-lang/pubspec_parse/issues/17: +// print('Installing ${dep.name}@${dep.version}...'); +// +// if (dep.dev) { +// pubspec.devDependencies[dep.name] = new HostedDependency( +// version: new VersionConstraint.parse(dep.version), +// ); +// } else { +// pubspec.dependencies[dep.name] = new HostedDependency( +// version: new VersionConstraint.parse(dep.version), +// ); +// } + } + } + + var missingDeps = missing.where((d) => !d.dev).toList()..sort(); + var missingDevDeps = missing.where((d) => d.dev).toList()..sort(); + var totalCount = missingDeps.length + missingDevDeps.length; + + if (totalCount > 0) { + print(yellow.wrap(totalCount == 1 + ? 'You are missing one dependency.' + : 'You are missing $totalCount dependencies.')); + print(yellow.wrap( + 'Update your `pubspec.yaml` to add the following dependencies:\n')); + + void printMissing(String type, Iterable deps) { + if (deps.isNotEmpty) { + print(yellow.wrap(' $type:')); + for (var dep in deps) { + print(yellow.wrap(' ${dep.name}: ${dep.version}')); + } + } + } + + printMissing('dependencies', missingDeps); + printMissing('dev_dependencies', missingDevDeps); + print('\n'); + } + +// if (isPresent) { +// TODO: https://github.com/dart-lang/pubspec_parse/issues/17 +// await savePubspec(pubspec); +// var pubPath = resolvePub(); +// +// print('Now running `$pubPath get`...'); +// +// var pubGet = await Process.start(pubPath, ['get']); +// pubGet.stdout.listen(stdout.add); +// pubGet.stderr.listen(stderr.add); +// +// var code = await pubGet.exitCode; +// +// if (code != 0) throw 'pub get terminated with exit code $code'; +} diff --git a/packages/cli/lib/src/commands/make/migration.dart b/packages/cli/lib/src/commands/make/migration.dart new file mode 100644 index 00000000..a5792b0c --- /dev/null +++ b/packages/cli/lib/src/commands/make/migration.dart @@ -0,0 +1,133 @@ +import 'dart:async'; +import 'dart:io'; +import 'package:args/command_runner.dart'; +import 'package:code_builder/code_builder.dart'; +import 'package:dart_style/dart_style.dart'; +import 'package:inflection2/inflection2.dart'; +import 'package:io/ansi.dart'; +import 'package:prompts/prompts.dart' as prompts; +import 'package:recase/recase.dart'; +import '../../util.dart'; +import 'maker.dart'; + +class MigrationCommand extends Command { + @override + String get name => 'migration'; + + @override + String get description => 'Generates a migration class.'; + + MigrationCommand() { + argParser + ..addOption('name', + abbr: 'n', help: 'Specifies a name for the model class.') + ..addOption('output-dir', + help: 'Specifies a directory to create the migration class in.', + defaultsTo: 'tool/migrations'); + } + + @override + FutureOr run() async { + String name; + if (argResults.wasParsed('name')) name = argResults['name'] as String; + + if (name?.isNotEmpty != true) { + name = prompts.get('Name of model class'); + } + + var deps = [const MakerDependency('angel_migration', '^2.0.0')]; + var rc = new ReCase(name); + + var migrationLib = new Library((migrationLib) { + migrationLib + ..directives.add(new Directive.import( + 'package:angel_migration.dart/angel_migration.dart')) + ..body.add(new Class((migrationClazz) { + migrationClazz + ..name = '${rc.pascalCase}Migration' + ..extend = refer('Migration'); + + var tableName = pluralize(rc.snakeCase); + + // up() + migrationClazz.methods.add(new Method((up) { + up + ..name = 'up' + ..returns = refer('void') + ..annotations.add(refer('override')) + ..requiredParameters.add(new Parameter((b) => b + ..name = 'schema' + ..type = refer('Schema'))) + ..body = new Block((block) { + // (table) { ... } + var callback = new Method((callback) { + callback + ..requiredParameters + .add(new Parameter((b) => b..name = 'table')) + ..body = new Block((block) { + var table = refer('table'); + + block.addExpression( + (table.property('serial').call([literal('id')])) + .property('primaryKey') + .call([]), + ); + + block.addExpression( + table.property('date').call([ + literal('created_at'), + ]), + ); + + block.addExpression( + table.property('date').call([ + literal('updated_at'), + ]), + ); + }); + }); + + block.addExpression(refer('schema').property('create').call([ + literal(tableName), + callback.closure, + ])); + }); + })); + + // down() + migrationClazz.methods.add(new Method((down) { + down + ..name = 'down' + ..returns = refer('void') + ..annotations.add(refer('override')) + ..requiredParameters.add(new Parameter((b) => b + ..name = 'schema' + ..type = refer('Schema'))) + ..body = new Block((block) { + block.addExpression( + refer('schema').property('drop').call([ + literal(tableName), + ]), + ); + }); + })); + })); + }); + + // Save migration file + var migrationDir = new Directory.fromUri( + Directory.current.uri.resolve(argResults['output-dir'] as String)); + var migrationFile = + new File.fromUri(migrationDir.uri.resolve('${rc.snakeCase}.dart')); + if (!await migrationFile.exists()) + await migrationFile.create(recursive: true); + + await migrationFile.writeAsString(new DartFormatter() + .format(migrationLib.accept(new DartEmitter()).toString())); + + print(green.wrap( + '$checkmark Created migration file "${migrationFile.absolute.path}".')); + + await depend(deps); + } +} diff --git a/packages/cli/lib/src/commands/make/model.dart b/packages/cli/lib/src/commands/make/model.dart new file mode 100644 index 00000000..04e964c8 --- /dev/null +++ b/packages/cli/lib/src/commands/make/model.dart @@ -0,0 +1,122 @@ +import 'dart:io'; +import 'package:args/command_runner.dart'; +import 'package:code_builder/code_builder.dart'; +import 'package:dart_style/dart_style.dart'; +import 'package:io/ansi.dart'; +import 'package:prompts/prompts.dart' as prompts; +import 'package:recase/recase.dart'; +import '../../util.dart'; +import 'maker.dart'; + +class ModelCommand extends Command { + @override + String get name => 'model'; + + @override + String get description => 'Generates a model class.'; + + ModelCommand() { + argParser + ..addFlag('migration', + abbr: 'm', + help: 'Generate migrations when running `build_runner`.', + defaultsTo: true) + ..addFlag('orm', help: 'Generate angel_orm code.', negatable: false) + ..addFlag('serializable', + help: 'Generate angel_serialize annotations.', defaultsTo: true) + ..addOption('name', + abbr: 'n', help: 'Specifies a name for the model class.') + ..addOption('output-dir', + help: 'Specifies a directory to create the model class in.', + defaultsTo: 'lib/src/models'); + } + + @override + run() async { + String name; + if (argResults.wasParsed('name')) name = argResults['name'] as String; + + if (name?.isNotEmpty != true) { + name = prompts.get('Name of model class'); + } + + List deps = [ + const MakerDependency('angel_model', '^1.0.0'), + ]; + + var rc = new ReCase(name); + + var modelLib = new Library((modelLib) { + if (argResults['orm'] as bool && argResults['migration'] as bool) { + modelLib.directives.addAll([ + new Directive.import('package:angel_migration/angel_migration.dart'), + ]); + } + + var needsSerialize = + argResults['serializable'] as bool || argResults['orm'] as bool; + // argResults['migration'] as bool; + + if (needsSerialize) { + modelLib.directives.add(new Directive.import( + 'package:angel_serialize/angel_serialize.dart')); + deps.add(const MakerDependency('angel_serialize', '^2.0.0')); + deps.add(const MakerDependency('angel_serialize_generator', '^2.0.0')); + deps.add(const MakerDependency('build_runner', '^1.0.0')); + } + + // else { + // modelLib.directives + // .add(new Directive.import('package:angel_model/angel_model.dart')); + // deps.add(const MakerDependency('angel_model', '^1.0.0')); + // } + + if (argResults['orm'] as bool) { + modelLib.directives.addAll([ + new Directive.import('package:angel_orm/angel_orm.dart'), + ]); + deps.add(const MakerDependency('angel_orm', '^2.0.0')); + } + + modelLib.body.addAll([ + new Code("part '${rc.snakeCase}.g.dart';"), + ]); + + modelLib.body.add(new Class((modelClazz) { + modelClazz + ..abstract = true + ..name = needsSerialize ? '_${rc.pascalCase}' : rc.pascalCase + ..extend = refer('Model'); + + if (needsSerialize) { + // modelLib.addDirective(new PartBuilder('${rc.snakeCase}.g.dart')); + modelClazz.annotations.add(refer('serializable')); + } + + if (argResults['orm'] as bool) { + if (argResults['migration'] as bool) { + modelClazz.annotations.add(refer('orm')); + } else { + modelClazz.annotations.add( + refer('Orm').call([], {'generateMigration': literalFalse})); + } + } + })); + }); + + // Save model file + var outputDir = new Directory.fromUri( + Directory.current.uri.resolve(argResults['output-dir'] as String)); + var modelFile = + new File.fromUri(outputDir.uri.resolve('${rc.snakeCase}.dart')); + if (!await modelFile.exists()) await modelFile.create(recursive: true); + + await modelFile.writeAsString(new DartFormatter() + .format(modelLib.accept(new DartEmitter()).toString())); + + print(green + .wrap('$checkmark Created model file "${modelFile.absolute.path}".')); + + if (deps.isNotEmpty) await depend(deps); + } +} diff --git a/packages/cli/lib/src/commands/make/plugin.dart b/packages/cli/lib/src/commands/make/plugin.dart new file mode 100644 index 00000000..4f979777 --- /dev/null +++ b/packages/cli/lib/src/commands/make/plugin.dart @@ -0,0 +1,69 @@ +import 'dart:io'; +import 'package:args/command_runner.dart'; +import 'package:dart_style/dart_style.dart'; +import 'package:io/ansi.dart'; +import 'package:prompts/prompts.dart' as prompts; +import 'package:pubspec_parse/pubspec_parse.dart'; +import 'package:recase/recase.dart'; +import '../../util.dart'; +import 'maker.dart'; + +class PluginCommand extends Command { + @override + String get name => "plugin"; + + @override + String get description => "Creates a new plug-in within the given project."; + + PluginCommand() { + argParser + ..addOption('name', + abbr: 'n', help: 'Specifies a name for the plug-in class.') + ..addOption('output-dir', + help: 'Specifies a directory to create the plug-in class in.', + defaultsTo: 'lib/src/config/plugins'); + } + + @override + run() async { + var pubspec = await loadPubspec(); + String name; + if (argResults.wasParsed('name')) name = argResults['name'] as String; + + if (name?.isNotEmpty != true) { + name = prompts.get('Name of plug-in class'); + } + + List deps = [ + const MakerDependency('angel_framework', '^2.0.0') + ]; + + var rc = new ReCase(name); + final pluginDir = new Directory.fromUri( + Directory.current.uri.resolve(argResults['output-dir'] as String)); + final pluginFile = + new File.fromUri(pluginDir.uri.resolve("${rc.snakeCase}.dart")); + if (!await pluginFile.exists()) await pluginFile.create(recursive: true); + await pluginFile.writeAsString( + new DartFormatter().format(_generatePlugin(pubspec, rc))); + + if (deps.isNotEmpty) await depend(deps); + + print(green.wrap( + '$checkmark Successfully generated plug-in file "${pluginFile.absolute.path}".')); + } + + String _generatePlugin(Pubspec pubspec, ReCase rc) { + return ''' +library ${pubspec.name}.src.config.plugins.${rc.snakeCase}; + +import 'package:angel_framework/angel_framework.dart'; + +AngelConfigurer ${rc.camelCase}() { + return (Angel app) async { + // Work some magic... + }; +} + '''; + } +} diff --git a/packages/cli/lib/src/commands/make/service.dart b/packages/cli/lib/src/commands/make/service.dart new file mode 100644 index 00000000..a21d07f1 --- /dev/null +++ b/packages/cli/lib/src/commands/make/service.dart @@ -0,0 +1,136 @@ +import 'dart:io'; +import 'package:args/command_runner.dart'; +import 'package:code_builder/code_builder.dart'; +import 'package:dart_style/dart_style.dart'; +import 'package:inflection2/inflection2.dart'; +import 'package:io/ansi.dart'; +import 'package:prompts/prompts.dart' as prompts; +import 'package:pubspec_parse/pubspec_parse.dart'; +import 'package:recase/recase.dart'; +import '../service_generators/service_generators.dart'; +import '../../util.dart'; +import 'maker.dart'; + +class ServiceCommand extends Command { + @override + String get name => 'service'; + + @override + String get description => 'Generates an Angel service.'; + + ServiceCommand() { + argParser + ..addFlag('typed', + abbr: 't', + help: 'Wrap the generated service in a `TypedService` instance.', + negatable: false) + ..addOption('name', + abbr: 'n', help: 'Specifies a name for the service file.') + ..addOption('output-dir', + help: 'Specifies a directory to create the service file.', + defaultsTo: 'lib/src/services'); + } + + @override + run() async { + var pubspec = await loadPubspec(); + String name; + if (argResults.wasParsed('name')) name = argResults['name'] as String; + + if (name?.isNotEmpty != true) { + name = prompts.get('Name of service'); + } + + List deps = [ + const MakerDependency('angel_framework', '^2.0.0') + ]; + + // '${pubspec.name}.src.services.${rc.snakeCase}' + var rc = new ReCase(name); + var serviceLib = new Library((serviceLib) { + var generator = prompts.choose( + 'Choose which type of service to create', serviceGenerators); + +// if (generator == null) { +// _pen.red(); +// _pen('${Icon.BALLOT_X} \'$type\' services are not yet implemented. :('); +// _pen(); +// throw 'Unrecognized service type: "$type".'; +// } + + for (var dep in generator.dependencies) { + if (!deps.any((d) => d.name == dep.name)) deps.add(dep); + } + + if (generator.goesFirst) { + generator.applyToLibrary(serviceLib, name, rc.snakeCase); + serviceLib.directives.add(new Directive.import( + 'package:angel_framework/angel_framework.dart')); + } else { + serviceLib.directives.add(new Directive.import( + 'package:angel_framework/angel_framework.dart')); + generator.applyToLibrary(serviceLib, name, rc.snakeCase); + } + + if (argResults['typed'] as bool) { + serviceLib.directives + .add(new Directive.import('../models/${rc.snakeCase}.dart')); + } + + // configureServer() {} + serviceLib.body.add(new Method((configureServer) { + configureServer + ..name = 'configureServer' + ..returns = refer('AngelConfigurer'); + + configureServer.body = new Block((block) { + generator.applyToConfigureServer( + serviceLib, configureServer, block, name, rc.snakeCase); + + // return (Angel app) async {} + var closure = new Method((closure) { + closure + ..modifier = MethodModifier.async + ..requiredParameters.add(new Parameter((b) => b + ..name = 'app' + ..type = refer('Angel'))); + closure.body = new Block((block) { + generator.beforeService(serviceLib, block, name, rc.snakeCase); + + // app.use('/api/todos', new MapService()); + var service = generator.createInstance( + serviceLib, closure, name, rc.snakeCase); + + if (argResults['typed'] as bool) { + var tb = new TypeReference((b) => b + ..symbol = 'TypedService' + ..types.add(refer(rc.pascalCase))); + service = tb.newInstance([service]); + } + + block.addExpression(refer('app').property('use').call([ + literal('/api/${pluralize(rc.snakeCase)}'), + service, + ])); + }); + }); + + block.addExpression(closure.closure.returned); + }); + })); + }); + + final outputDir = new Directory.fromUri( + Directory.current.uri.resolve(argResults['output-dir'] as String)); + final serviceFile = + new File.fromUri(outputDir.uri.resolve("${rc.snakeCase}.dart")); + if (!await serviceFile.exists()) await serviceFile.create(recursive: true); + await serviceFile.writeAsString(new DartFormatter() + .format(serviceLib.accept(new DartEmitter()).toString())); + + print(green.wrap( + '$checkmark Successfully generated service file "${serviceFile.absolute.path}".')); + + if (deps.isNotEmpty) await depend(deps); + } +} diff --git a/packages/cli/lib/src/commands/make/test.dart b/packages/cli/lib/src/commands/make/test.dart new file mode 100644 index 00000000..32adcd7f --- /dev/null +++ b/packages/cli/lib/src/commands/make/test.dart @@ -0,0 +1,110 @@ +import 'dart:io'; +import 'package:args/command_runner.dart'; +import 'package:dart_style/dart_style.dart'; +import 'package:io/ansi.dart'; +import 'package:prompts/prompts.dart' as prompter; +import 'package:pubspec_parse/pubspec_parse.dart'; +import 'package:recase/recase.dart'; +import '../../util.dart'; +import 'maker.dart'; + +class TestCommand extends Command { + @override + String get name => "test"; + + @override + String get description => "Creates a new test within the given project."; + + TestCommand() { + argParser + ..addFlag('run-configuration', + help: 'Generate a run configuration for JetBrains IDE\'s.', + defaultsTo: true) + ..addOption('name', + abbr: 'n', help: 'Specifies a name for the plug-in class.') + ..addOption('output-dir', + help: 'Specifies a directory to create the plug-in class in.', + defaultsTo: 'test'); + } + + @override + run() async { + var pubspec = await loadPubspec(); + String name; + if (argResults.wasParsed('name')) name = argResults['name'] as String; + + if (name?.isNotEmpty != true) { + name = prompter.get('Name of test'); + } + + List deps = [ + const MakerDependency('angel_framework', '^2.0.0'), + const MakerDependency('angel_test', '^2.0.0', dev: true), + const MakerDependency('test', '^1.0.0', dev: true), + ]; + + var rc = new ReCase(name); + final testDir = new Directory.fromUri( + Directory.current.uri.resolve(argResults['output-dir'] as String)); + final testFile = + new File.fromUri(testDir.uri.resolve("${rc.snakeCase}_test.dart")); + if (!await testFile.exists()) await testFile.create(recursive: true); + await testFile + .writeAsString(new DartFormatter().format(_generateTest(pubspec, rc))); + + if (deps.isNotEmpty) await depend(deps); + + print(green.wrap( + '$checkmark Successfully generated test file "${testFile.absolute.path}".')); + + if (argResults['run-configuration'] as bool) { + final runConfig = new File.fromUri(Directory.current.uri + .resolve('.idea/runConfigurations/${name}_Tests.xml')); + + if (!await runConfig.exists()) await runConfig.create(recursive: true); + await runConfig.writeAsString(_generateRunConfiguration(name, rc)); + + print(green.wrap( + '$checkmark Successfully generated run configuration "$name Tests" at "${runConfig.absolute.path}".')); + } + } + + String _generateRunConfiguration(String name, ReCase rc) { + return ''' + + + + +''' + .trim(); + } + + String _generateTest(Pubspec pubspec, ReCase rc) { + return ''' +import 'dart:io'; +import 'package:${pubspec.name}/${pubspec.name}.dart' as ${pubspec.name}; +import 'package:angel_framework/angel_framework.dart'; +import 'package:angel_test/angel_test.dart'; +import 'package:test/test.dart'; + +main() async { + TestClient client; + + setUp(() async { + var app = new Angel(); + await app.configure(${pubspec.name}.configureServer); + client = await connectTo(app); + }); + + tearDown(() => client.close()); + + test('${rc.snakeCase}', () async { + final response = await client.get('/${rc.snakeCase}'); + expect(response, hasStatus(HttpStatus.ok)); + }); +} + '''; + } +} diff --git a/packages/cli/lib/src/commands/pub.dart b/packages/cli/lib/src/commands/pub.dart new file mode 100644 index 00000000..1f3a74ce --- /dev/null +++ b/packages/cli/lib/src/commands/pub.dart @@ -0,0 +1,12 @@ +import 'dart:io'; + +final RegExp _leadingSlashes = new RegExp(r'^/+'); + +String resolvePub() { + var exec = new File(Platform.resolvedExecutable); + var pubPath = exec.parent.uri.resolve('pub').path; + if (Platform.isWindows) + pubPath = pubPath.replaceAll(_leadingSlashes, '') + '.bat'; + pubPath = Uri.decodeFull(pubPath); + return pubPath; +} diff --git a/packages/cli/lib/src/commands/rename.dart b/packages/cli/lib/src/commands/rename.dart new file mode 100644 index 00000000..ae11b5e5 --- /dev/null +++ b/packages/cli/lib/src/commands/rename.dart @@ -0,0 +1,183 @@ +import 'dart:io'; +import 'package:analyzer/analyzer.dart'; +import 'package:args/command_runner.dart'; +import 'package:dart_style/dart_style.dart'; +import 'package:glob/glob.dart'; +import 'package:io/ansi.dart'; +import 'package:prompts/prompts.dart' as prompts; +import 'package:recase/recase.dart'; +import '../util.dart'; +import 'pub.dart'; + +class RenameCommand extends Command { + @override + String get name => 'rename'; + + @override + String get description => 'Renames the current project.'; + + @override + String get invocation => '$name '; + + @override + run() async { + String newName; + + if (argResults.rest.isNotEmpty) + newName = argResults.rest.first; + else { + newName = prompts.get('Rename project to'); + } + + newName = new ReCase(newName).snakeCase; + + var choice = prompts.getBool('Rename the project to `$newName`?'); + + if (choice) { + print('Renaming project to `$newName`...'); + var pubspecFile = + new File.fromUri(Directory.current.uri.resolve('pubspec.yaml')); + + if (!await pubspecFile.exists()) { + throw new Exception('No pubspec.yaml found in current directory.'); + } else { + var pubspec = await loadPubspec(); + var oldName = pubspec.name; + await renamePubspec(Directory.current, oldName, newName); + await renameDartFiles(Directory.current, oldName, newName); + print('Now running `pub get`...'); + var pubPath = resolvePub(); + print('Pub path: $pubPath'); + var pub = await Process.start(pubPath, ['get']); + stdout.addStream(pub.stdout); + stderr.addStream(pub.stderr); + await pub.exitCode; + } + } + } +} + +renamePubspec(Directory dir, String oldName, String newName) async { +// var pubspec = await loadPubspec(dir); + print(cyan.wrap('Renaming your project to `$newName.`')); + + var pubspecFile = new File.fromUri(dir.uri.resolve('pubspec.yaml')); + + if (await pubspecFile.exists()) { + var contents = await pubspecFile.readAsString(), oldContents = contents; + contents = + contents.replaceAll(new RegExp('name:\\s*$oldName'), 'name: $newName'); + + if (contents != oldContents) { + await pubspecFile.writeAsString(contents); + } + } + +// print(cyan +// .wrap('Note that this does not actually modify your `pubspec.yaml`.')); +// TODO: https://github.com/dart-lang/pubspec_parse/issues/17 +// var newPubspec = new Pubspec.fromJson(pubspec.toJson()..['name'] = newName); +// await newPubspec.save(dir); +} + +renameDartFiles(Directory dir, String oldName, String newName) async { + if (!await dir.exists()) return; + + // Try to replace MongoDB URL + var configGlob = new Glob('config/**/*.yaml'); + + try { + await for (var yamlFile in configGlob.list(root: dir.absolute.path)) { + if (yamlFile is File) { + print( + 'Replacing occurrences of "$oldName" with "$newName" in file "${yamlFile.absolute.path}"...'); + var contents = await yamlFile.readAsString(); + contents = contents.replaceAll(oldName, newName); + await yamlFile.writeAsString(contents); + } + } + } catch (_) {} + + var entry = new File.fromUri(dir.uri.resolve('lib/$oldName.dart')); + + if (await entry.exists()) { + await entry.rename(dir.uri.resolve('lib/$newName.dart').toFilePath()); + print('Renaming library file `${entry.absolute.path}`...'); + } + + var fmt = new DartFormatter(); + await for (FileSystemEntity file in dir.list(recursive: true)) { + if (file is File && file.path.endsWith('.dart')) { + var contents = await file.readAsString(); + var ast = parseCompilationUnit(contents); + var visitor = new RenamingVisitor(oldName, newName) + ..visitCompilationUnit(ast); + + if (visitor.replace.isNotEmpty) { + visitor.replace.forEach((range, replacement) { + if (range.first is int) { + contents = contents.replaceRange( + range.first as int, range.last as int, replacement); + } else if (range.first is String) { + contents = contents.replaceAll(range.first as String, replacement); + } + }); + + await file.writeAsString(fmt.format(contents)); + print('Updated file `${file.absolute.path}`.'); + } + } + } +} + +class RenamingVisitor extends RecursiveAstVisitor { + final String oldName, newName; + final Map replace = {}; + + RenamingVisitor(this.oldName, this.newName) { + replace[['{{$oldName}}']] = newName; + } + + String updateUri(String uri) { + if (uri == 'package:$oldName/$oldName.dart') { + return 'package:$newName/$newName.dart'; + } else if (uri.startsWith('package:$oldName/')) { + return 'package:$newName/' + uri.replaceFirst('package:$oldName/', ''); + } else + return uri; + } + + @override + visitExportDirective(ExportDirective ctx) { + var uri = ctx.uri.stringValue, updated = updateUri(uri); + if (uri != updated) replace[[uri]] = updated; + } + + @override + visitImportDirective(ImportDirective ctx) { + var uri = ctx.uri.stringValue, updated = updateUri(uri); + if (uri != updated) replace[[uri]] = updated; + } + + @override + visitLibraryDirective(LibraryDirective ctx) { + var name = ctx.name.name; + + if (name.startsWith(oldName)) { + replace[[ctx.offset, ctx.end]] = + 'library ' + name.replaceFirst(oldName, newName) + ';'; + } + } + + @override + visitPartOfDirective(PartOfDirective ctx) { + if (ctx.libraryName != null) { + var name = ctx.libraryName.name; + + if (name.startsWith(oldName)) { + replace[[ctx.offset, ctx.end]] = + 'part of ' + name.replaceFirst(oldName, newName) + ';'; + } + } + } +} diff --git a/packages/cli/lib/src/commands/service_generators/custom.dart b/packages/cli/lib/src/commands/service_generators/custom.dart new file mode 100644 index 00000000..d4134602 --- /dev/null +++ b/packages/cli/lib/src/commands/service_generators/custom.dart @@ -0,0 +1,27 @@ +import 'package:code_builder/code_builder.dart'; +import 'generator.dart'; + +class CustomServiceGenerator extends ServiceGenerator { + @override + bool get createsModel => false; + + @override + bool get createsValidator => false; + + const CustomServiceGenerator() : super('Custom'); + + @override + void applyToLibrary(LibraryBuilder library, String name, String lower) { + library.body.add(new Class((clazz) { + clazz + ..name = '${name}Service' + ..extend = refer('Service'); + })); + } + + @override + Expression createInstance(LibraryBuilder library, MethodBuilder methodBuilder, + String name, String lower) { + return refer('${name}Service').newInstance([]); + } +} diff --git a/packages/cli/lib/src/commands/service_generators/file_service.dart b/packages/cli/lib/src/commands/service_generators/file_service.dart new file mode 100644 index 00000000..39e1086e --- /dev/null +++ b/packages/cli/lib/src/commands/service_generators/file_service.dart @@ -0,0 +1,48 @@ +import 'generator.dart'; +import 'package:code_builder/code_builder.dart'; +import 'package:inflection2/inflection2.dart'; +import '../make/maker.dart'; + +class FileServiceGenerator extends ServiceGenerator { + const FileServiceGenerator() : super('Persistent JSON File'); + + @override + List get dependencies => + const [const MakerDependency('angel_file_service', '^2.0.0')]; + + @override + bool get goesFirst => true; + + @override + void applyToConfigureServer( + LibraryBuilder library, + MethodBuilder configureServer, + BlockBuilder block, + String name, + String lower) { + configureServer.requiredParameters.add(new Parameter((b) => b + ..name = 'dbDirectory' + ..type = refer('Directory'))); + } + + @override + void applyToLibrary(LibraryBuilder library, String name, String lower) { + library.directives.addAll([ + new Directive.import( + 'package:angel_file_service/angel_file_service.dart'), + ]); + } + + @override + Expression createInstance(LibraryBuilder library, MethodBuilder methodBuilder, + String name, String lower) { + library.directives.addAll([ + new Directive.import('package:file/file.dart'), + ]); + return refer('JsonFileService').newInstance([ + refer('dbDirectory') + .property('childFile') + .call([literal(pluralize(lower) + '_db.json')]) + ]); + } +} diff --git a/packages/cli/lib/src/commands/service_generators/generator.dart b/packages/cli/lib/src/commands/service_generators/generator.dart new file mode 100644 index 00000000..63289581 --- /dev/null +++ b/packages/cli/lib/src/commands/service_generators/generator.dart @@ -0,0 +1,46 @@ +import 'package:code_builder/code_builder.dart'; +import '../make/maker.dart'; + +class ServiceGenerator { + final String name; + + const ServiceGenerator(this.name); + + List get dependencies => []; + + @deprecated + bool get createsModel => true; + + @deprecated + bool get createsValidator => true; + + @deprecated + bool get exportedInServiceLibrary => true; + + @deprecated + bool get injectsSingleton => false; + + @deprecated + bool get shouldRunBuild => false; + + bool get goesFirst => false; + + void applyToLibrary(LibraryBuilder library, String name, String lower) {} + + void beforeService(LibraryBuilder library, BlockBuilder builder, String name, + String lower) {} + + void applyToConfigureServer( + LibraryBuilder library, + MethodBuilder configureServer, + BlockBuilder block, + String name, + String lower) {} + + Expression createInstance(LibraryBuilder library, MethodBuilder methodBuilder, + String name, String lower) => + literal(null); + + @override + String toString() => name; +} diff --git a/packages/cli/lib/src/commands/service_generators/map.dart b/packages/cli/lib/src/commands/service_generators/map.dart new file mode 100644 index 00000000..545961df --- /dev/null +++ b/packages/cli/lib/src/commands/service_generators/map.dart @@ -0,0 +1,15 @@ +import 'generator.dart'; +import 'package:code_builder/code_builder.dart'; + +class MapServiceGenerator extends ServiceGenerator { + const MapServiceGenerator() : super('In-Memory'); + + @override + bool get createsModel => false; + + @override + Expression createInstance(LibraryBuilder library, MethodBuilder methodBuilder, + String name, String lower) { + return refer('MapService').newInstance([]); + } +} diff --git a/packages/cli/lib/src/commands/service_generators/mongo.dart b/packages/cli/lib/src/commands/service_generators/mongo.dart new file mode 100644 index 00000000..d8d2f0d3 --- /dev/null +++ b/packages/cli/lib/src/commands/service_generators/mongo.dart @@ -0,0 +1,43 @@ +import 'generator.dart'; +import 'package:code_builder/code_builder.dart'; +import 'package:inflection2/inflection2.dart'; +import '../make/maker.dart'; + +class MongoServiceGenerator extends ServiceGenerator { + const MongoServiceGenerator() : super('MongoDB'); + + @override + List get dependencies => + const [const MakerDependency('angel_mongo', '^2.0.0')]; + + @override + bool get createsModel => false; + + @override + void applyToConfigureServer( + LibraryBuilder library, + MethodBuilder configureServer, + BlockBuilder block, + String name, + String lower) { + configureServer.requiredParameters.add(new Parameter((b) => b + ..name = 'db' + ..type = refer('Db'))); + } + + @override + void applyToLibrary(LibraryBuilder library, String name, String lower) { + library.directives.addAll([ + new Directive.import('package:angel_mongo/angel_mongo.dart'), + new Directive.import('package:mongo_dart/mongo_dart.dart'), + ]); + } + + @override + Expression createInstance(LibraryBuilder library, MethodBuilder methodBuilder, + String name, String lower) { + return refer('MongoService').newInstance([ + refer('db').property('collection').call([literal(pluralize(lower))]) + ]); + } +} diff --git a/packages/cli/lib/src/commands/service_generators/rethink.dart b/packages/cli/lib/src/commands/service_generators/rethink.dart new file mode 100644 index 00000000..57059844 --- /dev/null +++ b/packages/cli/lib/src/commands/service_generators/rethink.dart @@ -0,0 +1,49 @@ +import 'generator.dart'; +import 'package:code_builder/code_builder.dart'; +import 'package:inflection2/inflection2.dart'; +import '../make/maker.dart'; + +class RethinkServiceGenerator extends ServiceGenerator { + const RethinkServiceGenerator() : super('RethinkDB'); + + @override + List get dependencies => + const [const MakerDependency('angel_rethink', '^2.0.0')]; + + @override + bool get createsModel => false; + + @override + void applyToConfigureServer( + LibraryBuilder library, + MethodBuilder configureServer, + BlockBuilder block, + String name, + String lower) { + configureServer.requiredParameters.addAll([ + new Parameter((b) => b + ..name = 'connection' + ..type = refer('Connection')), + new Parameter((b) => b + ..name = 'r' + ..type = refer('Rethinkdb')), + ]); + } + + @override + void applyToLibrary(LibraryBuilder library, String name, String lower) { + library.directives.addAll([ + 'package:angel_rethink/angel_rethink.dart', + 'package:rethinkdb_driver/rethinkdb_driver.dart' + ].map((str) => new Directive.import(str))); + } + + @override + Expression createInstance(LibraryBuilder library, MethodBuilder methodBuilder, + String name, String lower) { + return refer('RethinkService').newInstance([ + refer('connection'), + refer('r').property('table').call([literal(pluralize(lower))]) + ]); + } +} diff --git a/packages/cli/lib/src/commands/service_generators/service_generators.dart b/packages/cli/lib/src/commands/service_generators/service_generators.dart new file mode 100644 index 00000000..813adc18 --- /dev/null +++ b/packages/cli/lib/src/commands/service_generators/service_generators.dart @@ -0,0 +1,15 @@ +import 'custom.dart'; +import 'file_service.dart'; +import 'generator.dart'; +import 'map.dart'; +import 'mongo.dart'; +import 'rethink.dart'; +export 'generator.dart'; + +const List serviceGenerators = const [ + const MapServiceGenerator(), + const FileServiceGenerator(), + const MongoServiceGenerator(), + const RethinkServiceGenerator(), + const CustomServiceGenerator() +]; diff --git a/packages/cli/lib/src/random_string.dart b/packages/cli/lib/src/random_string.dart new file mode 100644 index 00000000..c089e870 --- /dev/null +++ b/packages/cli/lib/src/random_string.dart @@ -0,0 +1,15 @@ +import 'dart:math'; + +const String _valid = + 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; +final Random _rnd = new Random.secure(); + +String randomAlphaNumeric(int length) { + var b = new StringBuffer(); + + for (int i = 0; i < length; i++) { + b.writeCharCode(_valid.codeUnitAt(_rnd.nextInt(_valid.length))); + } + + return b.toString(); +} diff --git a/packages/cli/lib/src/util.dart b/packages/cli/lib/src/util.dart new file mode 100644 index 00000000..a294d40c --- /dev/null +++ b/packages/cli/lib/src/util.dart @@ -0,0 +1,77 @@ +import 'dart:async'; +import 'dart:io'; +import 'package:io/ansi.dart'; +import 'package:path/path.dart' as p; +import 'package:pubspec_parse/pubspec_parse.dart'; +//import 'package:yamlicious/yamlicious.dart'; + +final String checkmark = ansiOutputEnabled ? '\u2714' : '[Success]'; + +final String ballot = ansiOutputEnabled ? '\u2717' : '[Failure]'; + +String get homeDirPath => + Platform.environment['HOME'] ?? Platform.environment['USERPROFILE']; + +Directory get homeDir => new Directory(homeDirPath); + +Directory get angelDir => Directory(p.join(homeDir.path, '.angel')); + +Future loadPubspec([Directory directory]) { + directory ??= Directory.current; + var file = new File.fromUri(directory.uri.resolve('pubspec.yaml')); + return file + .readAsString() + .then((yaml) => new Pubspec.parse(yaml, sourceUrl: file.uri)); +} + +// From: https://gist.github.com/tobischw/98dcd2563eec9a2a87bda8299055358a +Future copyDirectory(Directory source, Directory destination) async { + // if (!topLevel) stdout.write('\r'); + // print(darkGray + // .wrap('Copying dir "${source.path}" -> "${destination.path}..."')); + + await for (var entity in source.list(recursive: false)) { + if (p.basename(entity.path) == '.git') continue; + if (entity is Directory) { + var newDirectory = + Directory(p.join(destination.absolute.path, p.basename(entity.path))); + await newDirectory.create(recursive: true); + await copyDirectory(entity.absolute, newDirectory); + } else if (entity is File) { + var newPath = p.join(destination.path, p.basename(entity.path)); + // print(darkGray.wrap('\rCopying file "${entity.path}" -> "$newPath"')); + await File(newPath).create(recursive: true); + await entity.copy(newPath); + } + } + + // print('\rCopied "${source.path}" -> "${destination.path}.'); +} + +Future savePubspec(Pubspec pubspec) async { + // TODO: Save pubspec for real? + //var text = toYamlString(pubspec); +} + +Future runCommand(String exec, List args) async { + var s = '$exec ${args.join(' ')}'.trim(); + stdout.write(darkGray.wrap('Running `$s`... ')); + + try { + var p = await Process.start(exec, args); + var code = await p.exitCode; + + if (code == 0) { + print(green.wrap(checkmark)); + return true; + } else { + print(red.wrap(ballot)); + await stdout.addStream(p.stdout); + await stderr.addStream(p.stderr); + return false; + } + } catch (e) { + print(red.wrap('$ballot Failed to run process.')); + return false; + } +} diff --git a/packages/cli/pubspec.yaml b/packages/cli/pubspec.yaml new file mode 100644 index 00000000..34ab6e6c --- /dev/null +++ b/packages/cli/pubspec.yaml @@ -0,0 +1,28 @@ +author: Tobe O +description: Command-line tools for the Angel framework, including scaffolding. +homepage: https://github.com/angel-dart/angel_cli +name: angel_cli +version: 2.1.7+1 +dependencies: + analyzer: ">=0.32.0 <2.0.0" + args: ^1.0.0 + code_builder: ^3.0.0 + dart_style: ^1.0.0 + glob: ^1.1.0 + http: ^0.12.0 + io: ^0.3.2 + inflection2: ^0.4.2 + mustache4dart: ^3.0.0-dev.1.0 + path: ^1.0.0 + prompts: ^1.0.0 + pubspec_parse: ^0.1.2 + quiver: ^2.0.0 + recase: ^2.0.0 + shutdown: ^0.4.0 + watcher: ^0.9.7 + yaml: ^2.0.0 + #yamlicious: ^0.0.5 +environment: + sdk: ">=2.0.0-dev <3.0.0" +executables: + angel: angel diff --git a/packages/cli/screenshots/screenshot.png b/packages/cli/screenshots/screenshot.png new file mode 100644 index 00000000..fe8c9652 Binary files /dev/null and b/packages/cli/screenshots/screenshot.png differ