Fixed NNBD issues

This commit is contained in:
thomashii 2021-09-07 08:11:42 +08:00
parent 3bfdf05c0e
commit 422c92098a
20 changed files with 97 additions and 75 deletions

View file

@ -1,5 +1,9 @@
# Change Log # Change Log
## 3.1.1
* Fixed NNBD issues
## 3.1.0 ## 3.1.0
* Upgraded to support `analyzer` 2.0.0 major release * Upgraded to support `analyzer` 2.0.0 major release

View file

@ -2,7 +2,7 @@
[![Screenshot of Terminal](screenshots/angel3-screenshot.png)](https://github.com/dukefirehawk/angel3-cli) [![Screenshot of Terminal](screenshots/angel3-screenshot.png)](https://github.com/dukefirehawk/angel3-cli)
[![version](https://img.shields.io/badge/pub-v3.1.0-brightgreen)](https://pub.dartlang.org/angel3_cli) [![version](https://img.shields.io/badge/pub-v3.1.1-brightgreen)](https://pub.dartlang.org/angel3_cli)
[![Null Safety](https://img.shields.io/badge/null-safety-brightgreen)](https://dart.dev/null-safety) [![Null Safety](https://img.shields.io/badge/null-safety-brightgreen)](https://dart.dev/null-safety)
[![Gitter](https://img.shields.io/gitter/room/angel_dart/discussion)](https://gitter.im/angel_dart/discussion) [![Gitter](https://img.shields.io/gitter/room/angel_dart/discussion)](https://gitter.im/angel_dart/discussion)

View file

@ -30,7 +30,7 @@ class SystemdCommand extends Command {
Future run() async { Future run() async {
var projectPath = p.absolute(p.current); var projectPath = p.absolute(p.current);
var pubspec = await loadPubspec(); var pubspec = await loadPubspec();
var user = argResults!['user']; var user = argResults?['user'];
var systemdText = ''' var systemdText = '''
[Unit] [Unit]
Description=`${pubspec.name}` server Description=`${pubspec.name}` server
@ -47,14 +47,15 @@ WantedBy=multi-user.target
''' '''
.trim(); .trim();
if (!argResults!.wasParsed('out') && !argResults!.wasParsed('install')) { if (argResults?.wasParsed('out') != true &&
argResults?.wasParsed('install') != true) {
print(systemdText); print(systemdText);
} else if (argResults!.wasParsed('install')) { } else if (argResults?.wasParsed('install') == true) {
var systemdPath = argResults!.wasParsed('out') var systemdPath = argResults?.wasParsed('out') == true
? (argResults!['out'] as String?)! ? (argResults?['out'] as String)
: p.join('etc', 'systemd', 'system'); : p.join('etc', 'systemd', 'system');
var serviceFilename = p.join(systemdPath, var serviceFilename = p.join(systemdPath,
p.setExtension(argResults!['install'] as String, '.service')); p.setExtension(argResults?['install'] as String, '.service'));
var file = File(serviceFilename); var file = File(serviceFilename);
await file.create(recursive: true); await file.create(recursive: true);
await file.writeAsString(systemdText); await file.writeAsString(systemdText);
@ -76,7 +77,7 @@ WantedBy=multi-user.target
print(red.wrap('$ballot Failed to install service system-wide.')); print(red.wrap('$ballot Failed to install service system-wide.'));
} }
} else { } else {
var file = File(argResults!['out'] as String); var file = File(argResults?['out'] as String);
await file.create(recursive: true); await file.create(recursive: true);
await file.writeAsString(systemdText); await file.writeAsString(systemdText);
print(green.wrap( print(green.wrap(

View file

@ -55,16 +55,16 @@ class InstallCommand extends Command {
'See here: https://github.com/angel-dart/install.git\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`.')); 'To stop seeing this, downgrade to `package:angel_cli@<=2.0.0`.'));
if (argResults!['wipe'] as bool) { if (argResults?['wipe'] as bool) {
if (await installRepo.exists()) await installRepo.delete(recursive: true); if (await installRepo.exists()) await installRepo.delete(recursive: true);
} else if (argResults!['list'] as bool) { } else if (argResults?['list'] as bool) {
var addons = await list(); var addons = await list();
print('${addons.length} add-on(s) installed:'); print('${addons.length} add-on(s) installed:');
for (var addon in addons) { for (var addon in addons) {
print(' * ${addon.name}@${addon.version}: ${addon.description}'); print(' * ${addon.name}@${addon.version}: ${addon.description}');
} }
} else if (argResults!['update'] as bool) { } else if (argResults?['update'] as bool) {
await update(); await update();
} else if (argResults!.rest.isNotEmpty) { } else if (argResults!.rest.isNotEmpty) {
if (!await installRepo.exists()) { if (!await installRepo.exists()) {
@ -73,7 +73,7 @@ class InstallCommand extends Command {
var pubspec = await loadPubspec(); var pubspec = await loadPubspec();
for (var packageName in argResults!.rest) { for (var packageName in argResults?.rest ?? <String>[]) {
var packageDir = var packageDir =
Directory.fromUri(installRepo.uri.resolve(packageName)); Directory.fromUri(installRepo.uri.resolve(packageName));
@ -98,7 +98,7 @@ class InstallCommand extends Command {
} }
return null; return null;
}) })
.where((d) => d != null) .whereType<MakerDependency>()
.toList(); .toList();
deps.addAll(projectPubspec.devDependencies.keys.map((k) { deps.addAll(projectPubspec.devDependencies.keys.map((k) {
@ -107,7 +107,7 @@ class InstallCommand extends Command {
return MakerDependency(k, dep.version.toString(), dev: true); return MakerDependency(k, dep.version.toString(), dev: true);
} }
return null; return null;
}).where((d) => d != null)); }).whereType<MakerDependency>());
await depend(deps); await depend(deps);

View file

@ -32,7 +32,9 @@ class ControllerCommand extends Command {
@override @override
Future run() async { Future run() async {
String? name; String? name;
if (argResults!.wasParsed('name')) name = argResults!['name'] as String?; if (argResults?.wasParsed('name') == true) {
name = argResults!['name'] as String?;
}
if (name?.isNotEmpty != true) { if (name?.isNotEmpty != true) {
name = prompts.get('Name of controller class'); name = prompts.get('Name of controller class');
@ -46,7 +48,7 @@ class ControllerCommand extends Command {
var rc = ReCase(name!); var rc = ReCase(name!);
var controllerLib = Library((controllerLib) { var controllerLib = Library((controllerLib) {
if (argResults!['websocket'] as bool) { if (argResults?['websocket'] as bool) {
deps.add(const MakerDependency('angel3_websocket', '^4.0.0')); deps.add(const MakerDependency('angel3_websocket', '^4.0.0'));
controllerLib.directives controllerLib.directives
.add(Directive.import('package:angel3_websocket/server.dart')); .add(Directive.import('package:angel3_websocket/server.dart'));
@ -58,7 +60,7 @@ class ControllerCommand extends Command {
controllerLib.body.add(Class((clazz) { controllerLib.body.add(Class((clazz) {
clazz clazz
..name = '${rc.pascalCase}Controller' ..name = '${rc.pascalCase}Controller'
..extend = refer(argResults!['websocket'] as bool ..extend = refer(argResults?['websocket'] as bool
? 'WebSocketController' ? 'WebSocketController'
: 'Controller'); : 'Controller');
@ -106,7 +108,7 @@ class ControllerCommand extends Command {
}); });
var outputDir = Directory.fromUri( var outputDir = Directory.fromUri(
Directory.current.uri.resolve(argResults!['output-dir'] as String)); Directory.current.uri.resolve(argResults?['output-dir'] as String));
var controllerFile = var controllerFile =
File.fromUri(outputDir.uri.resolve('${rc.snakeCase}.dart')); File.fromUri(outputDir.uri.resolve('${rc.snakeCase}.dart'));
if (!await controllerFile.exists()) { if (!await controllerFile.exists()) {

View file

@ -12,13 +12,13 @@ class MakerDependency implements Comparable<MakerDependency> {
int compareTo(MakerDependency other) => name.compareTo(other.name); int compareTo(MakerDependency other) => name.compareTo(other.name);
} }
Future depend(Iterable<MakerDependency?> deps) async { Future depend(Iterable<MakerDependency> deps) async {
var pubspec = await loadPubspec(); var pubspec = await loadPubspec();
var missing = <MakerDependency?>[]; var missing = <MakerDependency>[];
for (var dep in deps) { for (var dep in deps) {
var isPresent = false; var isPresent = false;
if (dep!.dev) { if (dep.dev) {
isPresent = pubspec.devDependencies.containsKey(dep.name); isPresent = pubspec.devDependencies.containsKey(dep.name);
} else { } else {
isPresent = pubspec.dependencies.containsKey(dep.name); isPresent = pubspec.dependencies.containsKey(dep.name);
@ -41,8 +41,8 @@ Future depend(Iterable<MakerDependency?> deps) async {
} }
} }
var missingDeps = missing.where((d) => !d!.dev).toList()..sort(); var missingDeps = missing.where((d) => !d.dev).toList()..sort();
var missingDevDeps = missing.where((d) => d!.dev).toList()..sort(); var missingDevDeps = missing.where((d) => d.dev).toList()..sort();
var totalCount = missingDeps.length + missingDevDeps.length; var totalCount = missingDeps.length + missingDevDeps.length;
if (totalCount > 0) { if (totalCount > 0) {

View file

@ -29,7 +29,9 @@ class MigrationCommand extends Command {
@override @override
FutureOr run() async { FutureOr run() async {
String? name; String? name;
if (argResults!.wasParsed('name')) name = argResults!['name'] as String?; if (argResults?.wasParsed('name') == true) {
name = argResults?['name'] as String?;
}
if (name?.isNotEmpty != true) { if (name?.isNotEmpty != true) {
name = prompts.get('Name of model class'); name = prompts.get('Name of model class');

View file

@ -34,7 +34,9 @@ class ModelCommand extends Command {
@override @override
Future run() async { Future run() async {
String? name; String? name;
if (argResults!.wasParsed('name')) name = argResults!['name'] as String?; if (argResults?.wasParsed('name') == true) {
name = argResults?['name'] as String?;
}
if (name?.isNotEmpty != true) { if (name?.isNotEmpty != true) {
name = prompts.get('Name of model class'); name = prompts.get('Name of model class');
@ -47,14 +49,14 @@ class ModelCommand extends Command {
var rc = ReCase(name!); var rc = ReCase(name!);
var modelLib = Library((modelLib) { var modelLib = Library((modelLib) {
if (argResults!['orm'] as bool && argResults!['migration'] as bool) { if (argResults?['orm'] as bool && argResults?['migration'] as bool) {
modelLib.directives.addAll([ modelLib.directives.addAll([
Directive.import('package:angel3_migration/angel3_migration.dart'), Directive.import('package:angel3_migration/angel3_migration.dart'),
]); ]);
} }
var needsSerialize = var needsSerialize =
argResults!['serializable'] as bool || argResults!['orm'] as bool; argResults?['serializable'] as bool || argResults?['orm'] as bool;
// argResults['migration'] as bool; // argResults['migration'] as bool;
if (needsSerialize) { if (needsSerialize) {
@ -71,7 +73,7 @@ class ModelCommand extends Command {
// deps.add(const MakerDependency('angel_model', '^1.0.0')); // deps.add(const MakerDependency('angel_model', '^1.0.0'));
// } // }
if (argResults!['orm'] as bool) { if (argResults?['orm'] as bool) {
modelLib.directives.addAll([ modelLib.directives.addAll([
Directive.import('package:angel3_orm/angel3_orm.dart'), Directive.import('package:angel3_orm/angel3_orm.dart'),
]); ]);
@ -93,8 +95,8 @@ class ModelCommand extends Command {
modelClazz.annotations.add(refer('serializable')); modelClazz.annotations.add(refer('serializable'));
} }
if (argResults!['orm'] as bool) { if (argResults?['orm'] as bool) {
if (argResults!['migration'] as bool) { if (argResults?['migration'] as bool) {
modelClazz.annotations.add(refer('orm')); modelClazz.annotations.add(refer('orm'));
} else { } else {
modelClazz.annotations.add( modelClazz.annotations.add(
@ -106,7 +108,7 @@ class ModelCommand extends Command {
// Save model file // Save model file
var outputDir = Directory.fromUri( var outputDir = Directory.fromUri(
Directory.current.uri.resolve(argResults!['output-dir'] as String)); Directory.current.uri.resolve(argResults?['output-dir'] as String));
var modelFile = File.fromUri(outputDir.uri.resolve('${rc.snakeCase}.dart')); var modelFile = File.fromUri(outputDir.uri.resolve('${rc.snakeCase}.dart'));
if (!await modelFile.exists()) await modelFile.create(recursive: true); if (!await modelFile.exists()) await modelFile.create(recursive: true);

View file

@ -28,7 +28,9 @@ class PluginCommand extends Command {
Future run() async { Future run() async {
var pubspec = await loadPubspec(); var pubspec = await loadPubspec();
String? name; String? name;
if (argResults!.wasParsed('name')) name = argResults!['name'] as String?; if (argResults?.wasParsed('name') == true) {
name = argResults?['name'] as String?;
}
if (name?.isNotEmpty != true) { if (name?.isNotEmpty != true) {
name = prompts.get('Name of plug-in class'); name = prompts.get('Name of plug-in class');

View file

@ -34,7 +34,9 @@ class ServiceCommand extends Command {
void run() async { void run() async {
await loadPubspec(); await loadPubspec();
String? name; String? name;
if (argResults!.wasParsed('name')) name = argResults!['name'] as String?; if (argResults?.wasParsed('name') == true) {
name = argResults?['name'] as String?;
}
if (name?.isNotEmpty != true) { if (name?.isNotEmpty != true) {
name = prompts.get('Name of service'); name = prompts.get('Name of service');
@ -61,11 +63,11 @@ class ServiceCommand extends Command {
if (!deps.any((d) => d.name == dep.name)) deps.add(dep); if (!deps.any((d) => d.name == dep.name)) deps.add(dep);
} }
if (generator.goesFirst) { if (name != null && generator.goesFirst) {
generator.applyToLibrary(serviceLib, name, rc.snakeCase); generator.applyToLibrary(serviceLib, name, rc.snakeCase);
serviceLib.directives.add( serviceLib.directives.add(
Directive.import('package:angel3_framework/angel3_framework.dart')); Directive.import('package:angel3_framework/angel3_framework.dart'));
} else { } else if (name != null) {
serviceLib.directives.add( serviceLib.directives.add(
Directive.import('package:angel3_framework/angel3_framework.dart')); Directive.import('package:angel3_framework/angel3_framework.dart'));
generator.applyToLibrary(serviceLib, name, rc.snakeCase); generator.applyToLibrary(serviceLib, name, rc.snakeCase);
@ -83,8 +85,10 @@ class ServiceCommand extends Command {
..returns = refer('AngelConfigurer'); ..returns = refer('AngelConfigurer');
configureServer.body = Block((block) { configureServer.body = Block((block) {
if (name != null) {
generator.applyToConfigureServer( generator.applyToConfigureServer(
serviceLib, configureServer, block, name, rc.snakeCase); serviceLib, configureServer, block, name, rc.snakeCase);
}
// return (Angel app) async {} // return (Angel app) async {}
var closure = Method((closure) { var closure = Method((closure) {
@ -93,7 +97,9 @@ class ServiceCommand extends Command {
..requiredParameters.add(Parameter((b) => b ..requiredParameters.add(Parameter((b) => b
..name = 'app' ..name = 'app'
..type = refer('Angel'))); ..type = refer('Angel')));
closure.body = Block((block) { closure.body = Block((block) {
if (name != null) {
generator.beforeService(serviceLib, block, name, rc.snakeCase); generator.beforeService(serviceLib, block, name, rc.snakeCase);
// app.use('/api/todos', new MapService()); // app.use('/api/todos', new MapService());
@ -111,6 +117,7 @@ class ServiceCommand extends Command {
literal('/api/${pluralize(rc.snakeCase)}'), literal('/api/${pluralize(rc.snakeCase)}'),
service, service,
])); ]));
}
}); });
}); });

View file

@ -31,7 +31,9 @@ class TestCommand extends Command {
Future run() async { Future run() async {
var pubspec = await loadPubspec(); var pubspec = await loadPubspec();
String? name; String? name;
if (argResults!.wasParsed('name')) name = argResults!['name'] as String?; if (argResults?.wasParsed('name') == true) {
name = argResults?['name'] as String?;
}
if (name?.isNotEmpty != true) { if (name?.isNotEmpty != true) {
name = prompter.get('Name of test'); name = prompter.get('Name of test');
@ -45,7 +47,7 @@ class TestCommand extends Command {
var rc = ReCase(name!); var rc = ReCase(name!);
final testDir = Directory.fromUri( final testDir = Directory.fromUri(
Directory.current.uri.resolve(argResults!['output-dir'] as String)); Directory.current.uri.resolve(argResults?['output-dir'] as String));
final testFile = final testFile =
File.fromUri(testDir.uri.resolve('${rc.snakeCase}_test.dart')); File.fromUri(testDir.uri.resolve('${rc.snakeCase}_test.dart'));
if (!await testFile.exists()) await testFile.create(recursive: true); if (!await testFile.exists()) await testFile.create(recursive: true);
@ -69,7 +71,7 @@ class TestCommand extends Command {
} }
} }
String _generateRunConfiguration(String? name, ReCase rc) { String _generateRunConfiguration(String name, ReCase rc) {
return ''' return '''
<component name="ProjectRunConfigurationManager"> <component name="ProjectRunConfigurationManager">
<configuration default="false" name="$name Tests" type="DartTestRunConfigurationType" factoryName="Dart Test" singleton="true"> <configuration default="false" name="$name Tests" type="DartTestRunConfigurationType" factoryName="Dart Test" singleton="true">

View file

@ -21,15 +21,15 @@ class RenameCommand extends Command {
@override @override
Future run() async { Future run() async {
String newName; String? newName;
if (argResults!.rest.isNotEmpty) { if (argResults?.rest.isNotEmpty == true) {
newName = argResults!.rest.first; newName = argResults?.rest.first;
} else { } else {
newName = prompts.get('Rename project to'); newName = prompts.get('Rename project to');
} }
newName = ReCase(newName).snakeCase; newName = ReCase(newName!).snakeCase;
var choice = prompts.getBool('Rename the project to `$newName`?'); var choice = prompts.getBool('Rename the project to `$newName`?');

View file

@ -11,7 +11,7 @@ class CustomServiceGenerator extends ServiceGenerator {
const CustomServiceGenerator() : super('Custom'); const CustomServiceGenerator() : super('Custom');
@override @override
void applyToLibrary(LibraryBuilder library, String? name, String lower) { void applyToLibrary(LibraryBuilder library, String name, String lower) {
library.body.add(Class((clazz) { library.body.add(Class((clazz) {
clazz clazz
..name = '${name}Service' ..name = '${name}Service'
@ -21,7 +21,7 @@ class CustomServiceGenerator extends ServiceGenerator {
@override @override
Expression createInstance(LibraryBuilder library, MethodBuilder methodBuilder, Expression createInstance(LibraryBuilder library, MethodBuilder methodBuilder,
String? name, String lower) { String name, String lower) {
return refer('${name}Service').newInstance([]); return refer('${name}Service').newInstance([]);
} }
} }

View file

@ -26,7 +26,7 @@ class FileServiceGenerator extends ServiceGenerator {
} }
@override @override
void applyToLibrary(LibraryBuilder library, String? name, String lower) { void applyToLibrary(LibraryBuilder library, String name, String lower) {
library.directives.addAll([ library.directives.addAll([
Directive.import('package:angel3_file_service/angel3_file_service.dart'), Directive.import('package:angel3_file_service/angel3_file_service.dart'),
]); ]);
@ -34,7 +34,7 @@ class FileServiceGenerator extends ServiceGenerator {
@override @override
Expression createInstance(LibraryBuilder library, MethodBuilder methodBuilder, Expression createInstance(LibraryBuilder library, MethodBuilder methodBuilder,
String? name, String lower) { String name, String lower) {
library.directives.addAll([ library.directives.addAll([
Directive.import('package:file/file.dart'), Directive.import('package:file/file.dart'),
]); ]);

View file

@ -25,20 +25,20 @@ class ServiceGenerator {
bool get goesFirst => false; bool get goesFirst => false;
void applyToLibrary(LibraryBuilder library, String? name, String lower) {} void applyToLibrary(LibraryBuilder library, String name, String lower) {}
void beforeService(LibraryBuilder library, BlockBuilder builder, String? name, void beforeService(LibraryBuilder library, BlockBuilder builder, String name,
String lower) {} String lower) {}
void applyToConfigureServer( void applyToConfigureServer(
LibraryBuilder library, LibraryBuilder library,
MethodBuilder configureServer, MethodBuilder configureServer,
BlockBuilder block, BlockBuilder block,
String? name, String name,
String lower) {} String lower) {}
Expression createInstance(LibraryBuilder library, MethodBuilder methodBuilder, Expression createInstance(LibraryBuilder library, MethodBuilder methodBuilder,
String? name, String lower) => String name, String lower) =>
literal(null); literal(null);
@override @override

View file

@ -9,7 +9,7 @@ class MapServiceGenerator extends ServiceGenerator {
@override @override
Expression createInstance(LibraryBuilder library, MethodBuilder methodBuilder, Expression createInstance(LibraryBuilder library, MethodBuilder methodBuilder,
String? name, String lower) { String name, String lower) {
return refer('MapService').newInstance([]); return refer('MapService').newInstance([]);
} }
} }

View file

@ -26,7 +26,7 @@ class MongoServiceGenerator extends ServiceGenerator {
} }
@override @override
void applyToLibrary(LibraryBuilder library, String? name, String lower) { void applyToLibrary(LibraryBuilder library, String name, String lower) {
library.directives.addAll([ library.directives.addAll([
Directive.import('package:angel3_mongo/angel3_mongo.dart'), Directive.import('package:angel3_mongo/angel3_mongo.dart'),
Directive.import('package:mongo_dart/mongo_dart.dart'), Directive.import('package:mongo_dart/mongo_dart.dart'),
@ -35,7 +35,7 @@ class MongoServiceGenerator extends ServiceGenerator {
@override @override
Expression createInstance(LibraryBuilder library, MethodBuilder methodBuilder, Expression createInstance(LibraryBuilder library, MethodBuilder methodBuilder,
String? name, String lower) { String name, String lower) {
return refer('MongoService').newInstance([ return refer('MongoService').newInstance([
refer('db').property('collection').call([literal(pluralize(lower))]) refer('db').property('collection').call([literal(pluralize(lower))])
]); ]);

View file

@ -31,7 +31,7 @@ class RethinkServiceGenerator extends ServiceGenerator {
} }
@override @override
void applyToLibrary(LibraryBuilder library, String? name, String lower) { void applyToLibrary(LibraryBuilder library, String name, String lower) {
library.directives.addAll([ library.directives.addAll([
'package:angel3_rethink/angel3_rethink.dart', 'package:angel3_rethink/angel3_rethink.dart',
'package:rethinkdb_dart/rethinkdb_dart.dart' 'package:rethinkdb_dart/rethinkdb_dart.dart'
@ -40,7 +40,7 @@ class RethinkServiceGenerator extends ServiceGenerator {
@override @override
Expression createInstance(LibraryBuilder library, MethodBuilder methodBuilder, Expression createInstance(LibraryBuilder library, MethodBuilder methodBuilder,
String? name, String lower) { String name, String lower) {
return refer('RethinkService').newInstance([ return refer('RethinkService').newInstance([
refer('connection'), refer('connection'),
refer('r').property('table').call([literal(pluralize(lower))]) refer('r').property('table').call([literal(pluralize(lower))])

View file

@ -9,10 +9,10 @@ final String checkmark = ansiOutputEnabled ? '\u2714' : '[Success]';
final String ballot = ansiOutputEnabled ? '\u2717' : '[Failure]'; final String ballot = ansiOutputEnabled ? '\u2717' : '[Failure]';
String? get homeDirPath => String get homeDirPath =>
Platform.environment['HOME'] ?? Platform.environment['USERPROFILE']; Platform.environment['HOME'] ?? Platform.environment['USERPROFILE'] ?? '.';
Directory get homeDir => Directory(homeDirPath!); Directory get homeDir => Directory(homeDirPath);
Directory get angelDir => Directory(p.join(homeDir.path, '.angel')); Directory get angelDir => Directory(p.join(homeDir.path, '.angel'));

View file

@ -1,5 +1,5 @@
name: angel3_cli name: angel3_cli
version: 3.1.0 version: 3.1.1
description: Command line tools for the Angel3 framework, including scaffolding. description: Command line tools for the Angel3 framework, including scaffolding.
homepage: https://angel3-framework.web.app/ homepage: https://angel3-framework.web.app/
repository: https://github.com/dukefirehawk/angel3-cli repository: https://github.com/dukefirehawk/angel3-cli