Add 'packages/cli/' from commit '63e5c99c96809070c18af0c3803141d0436dd992'

git-subtree-dir: packages/cli
git-subtree-mainline: cf8e83552a
git-subtree-split: 63e5c99c96
This commit is contained in:
Tobe O 2020-02-15 18:28:18 -05:00
commit 0aab437ca1
48 changed files with 2512 additions and 0 deletions

BIN
packages/cli/.DS_Store vendored Normal file

Binary file not shown.

80
packages/cli/.gitignore vendored Normal file
View file

@ -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

View file

@ -0,0 +1,19 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="WEB_MODULE" version="4">
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$">
<excludeFolder url="file://$MODULE_DIR$/.dart_tool" />
<excludeFolder url="file://$MODULE_DIR$/.pub" />
<excludeFolder url="file://$MODULE_DIR$/.tmp" />
<excludeFolder url="file://$MODULE_DIR$/build" />
<excludeFolder url="file://$MODULE_DIR$/sample_project/.pub" />
<excludeFolder url="file://$MODULE_DIR$/sample_project/build" />
<excludeFolder url="file://$MODULE_DIR$/temp" />
<excludeFolder url="file://$MODULE_DIR$/tmp" />
</content>
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
<orderEntry type="library" name="Dart SDK" level="project" />
<orderEntry type="library" name="Dart Packages" level="project" />
</component>
</module>

View file

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/angel_cli.iml" filepath="$PROJECT_DIR$/.idea/angel_cli.iml" />
</modules>
</component>
</project>

View file

@ -0,0 +1,8 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Controller" type="DartCommandLineRunConfigurationType" factoryName="Dart Command Line Application" singleton="true">
<option name="arguments" value="controller" />
<option name="filePath" value="$PROJECT_DIR$/bin/angel.dart" />
<option name="workingDirectory" value="$PROJECT_DIR$/sample_project" />
<method />
</configuration>
</component>

View file

@ -0,0 +1,7 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Doctor" type="DartCommandLineRunConfigurationType" factoryName="Dart Command Line Application" singleton="true">
<option name="arguments" value="doctor" />
<option name="filePath" value="$PROJECT_DIR$/bin/angel.dart" />
<method />
</configuration>
</component>

View file

@ -0,0 +1,8 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Init" type="DartCommandLineRunConfigurationType" factoryName="Dart Command Line Application" singleton="true">
<option name="arguments" value="init sample_project" />
<option name="filePath" value="$PROJECT_DIR$/bin/angel.dart" />
<option name="workingDirectory" value="$PROJECT_DIR$" />
<method />
</configuration>
</component>

View file

@ -0,0 +1,7 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Show Help" type="DartCommandLineRunConfigurationType" factoryName="Dart Command Line Application" singleton="true">
<option name="arguments" value="--help" />
<option name="filePath" value="$PROJECT_DIR$/bin/angel.dart" />
<method />
</configuration>
</component>

View file

@ -0,0 +1,8 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Update" type="DartCommandLineRunConfigurationType" factoryName="Dart Command Line Application" singleton="true">
<option name="arguments" value="update" />
<option name="filePath" value="$PROJECT_DIR$/bin/angel.dart" />
<option name="workingDirectory" value="$PROJECT_DIR$" />
<method />
</configuration>
</component>

View file

@ -0,0 +1,7 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Version" type="DartCommandLineRunConfigurationType" factoryName="Dart Command Line Application" singleton="true">
<option name="arguments" value="version" />
<option name="filePath" value="$PROJECT_DIR$/bin/angel.dart" />
<method />
</configuration>
</component>

1
packages/cli/.travis.yml Normal file
View file

@ -0,0 +1 @@
language: dart

89
packages/cli/CHANGELOG.md Normal file
View file

@ -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.

21
packages/cli/LICENSE Normal file
View file

@ -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.

22
packages/cli/README.md Normal file
View file

@ -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
```

7
packages/cli/TODO.md Normal file
View file

@ -0,0 +1,7 @@
# Todo
* `service`
* Add tests
* `migration`
* `deploy`
* Call these from Grinder script :)

View file

@ -0,0 +1,3 @@
analyzer:
strong-mode:
implicit-casts: false

View file

@ -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<String> 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 = '''
____________ ________________________
___ |__ | / /_ ____/__ ____/__ /
__ /| |_ |/ /_ / __ __ __/ __ /
_ ___ | /| / / /_/ / _ /___ _ /___
/_/ |_/_/ |_/ \____/ /_____/ /_____/
''';

View file

@ -0,0 +1,3 @@
void main() {
// This package isn't usable from code.
}

View file

@ -0,0 +1,3 @@
library angel_cli;
export 'src/commands/commands.dart';

View file

@ -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";

View file

@ -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());
}
}

View file

@ -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}'."));
}
}
}

View file

@ -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 <name> 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}'."));
}
}
}

View file

@ -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"));
}
}
}

View file

@ -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<BoilerplateInfo> 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)';
}

View file

@ -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<Glob> 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<Pubspec>> list() async {
if (!await installRepo.exists()) {
throw 'No local add-on database exists. Run `angel install --update` first.';
} else {
List<Pubspec> 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.';
}
}
}

View file

@ -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"');
}
}
}

View file

@ -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());
}
}

View file

@ -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<MakerDependency> 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);
}
}

View file

@ -0,0 +1,82 @@
import 'dart:async';
import 'package:io/ansi.dart';
import '../../util.dart';
class MakerDependency implements Comparable<MakerDependency> {
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<MakerDependency> deps) async {
var pubspec = await loadPubspec();
var missing = <MakerDependency>[];
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<MakerDependency> 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';
}

View file

@ -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);
}
}

View file

@ -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<MakerDependency> 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);
}
}

View file

@ -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<MakerDependency> 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...
};
}
''';
}
}

View file

@ -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<MakerDependency> 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);
}
}

View file

@ -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<MakerDependency> 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 '''
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="$name Tests" type="DartTestRunConfigurationType" factoryName="Dart Test" singleton="true">
<option name="filePath" value="\$PROJECT_DIR\$/test/${rc.snakeCase}_test.dart" />
<method />
</configuration>
</component>
'''
.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));
});
}
''';
}
}

View file

@ -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;
}

View file

@ -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 <new 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<List, String> 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) + ';';
}
}
}
}

View file

@ -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([]);
}
}

View file

@ -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<MakerDependency> 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')])
]);
}
}

View file

@ -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<MakerDependency> 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;
}

View file

@ -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([]);
}
}

View file

@ -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<MakerDependency> 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))])
]);
}
}

View file

@ -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<MakerDependency> 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))])
]);
}
}

View file

@ -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<ServiceGenerator> serviceGenerators = const [
const MapServiceGenerator(),
const FileServiceGenerator(),
const MongoServiceGenerator(),
const RethinkServiceGenerator(),
const CustomServiceGenerator()
];

View file

@ -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();
}

View file

@ -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<Pubspec> 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<void> 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<bool> runCommand(String exec, List<String> 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;
}
}

28
packages/cli/pubspec.yaml Normal file
View file

@ -0,0 +1,28 @@
author: Tobe O <thosakwe@gmail.com>
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

Binary file not shown.

After

Width:  |  Height:  |  Size: 83 KiB