1.0.1
This commit is contained in:
parent
f07211328f
commit
80a31575aa
9 changed files with 662 additions and 295 deletions
|
@ -1,19 +1,20 @@
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
import 'package:args/command_runner.dart';
|
import 'package:args/command_runner.dart';
|
||||||
|
import 'package:code_builder/code_builder.dart';
|
||||||
import 'package:console/console.dart';
|
import 'package:console/console.dart';
|
||||||
import 'package:id/id.dart';
|
import 'package:inflection/inflection.dart';
|
||||||
import 'package:recase/recase.dart';
|
import 'package:recase/recase.dart';
|
||||||
|
import 'service_generators/service_generators.dart';
|
||||||
import 'init.dart' show preBuild;
|
import 'init.dart' show preBuild;
|
||||||
|
|
||||||
|
const List<ServiceGenerator> GENERATORS = const [
|
||||||
|
const MapServiceGenerator(),
|
||||||
|
const MongoServiceGenerator(),
|
||||||
|
const RethinkServiceGenerator(),
|
||||||
|
const CustomServiceGenerator()
|
||||||
|
];
|
||||||
|
|
||||||
class ServiceCommand extends Command {
|
class ServiceCommand extends Command {
|
||||||
final String CUSTOM = 'Custom';
|
|
||||||
final String MEMORY = 'In-Memory';
|
|
||||||
final String MEMORY_JSON = 'In-Memory (serialized via `source_gen`)';
|
|
||||||
final String MONGO = 'MongoDB';
|
|
||||||
final String MONGO_TYPED = 'MongoDB (typed)';
|
|
||||||
final String MONGO_TYPED_JSON =
|
|
||||||
'MongoDB (typed, serialized via `source_gen`)';
|
|
||||||
final String TRESTLE = 'Trestle';
|
|
||||||
final TextPen _pen = new TextPen();
|
final TextPen _pen = new TextPen();
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
@ -22,130 +23,146 @@ class ServiceCommand extends Command {
|
||||||
@override
|
@override
|
||||||
String get description => 'Creates a new service within the given project.';
|
String get description => 'Creates a new service within the given project.';
|
||||||
|
|
||||||
String _snake(name) => idFromString(name).snake;
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
run() async {
|
run() async {
|
||||||
var name = await readInput('Name of Service (not plural): ');
|
var name = await readInput('Name of Service (not plural): ');
|
||||||
var chooser = new Chooser([MONGO, MONGO_TYPED, MEMORY, CUSTOM],
|
var chooser = new Chooser<String>(
|
||||||
|
GENERATORS.map<String>((g) => g.name).toList(),
|
||||||
message: 'What type of service would you like to create? ');
|
message: 'What type of service would you like to create? ');
|
||||||
var type = await chooser.choose();
|
var type = await chooser.choose();
|
||||||
|
|
||||||
fail() {
|
print('Wrap this service in a TypedService? (slight performance cost)');
|
||||||
_pen.red();
|
chooser = new Chooser<String>(['Yes', 'No']);
|
||||||
_pen('Could not successfully create service $name.');
|
var typed = (await chooser.choose()) == 'Yes';
|
||||||
_pen();
|
|
||||||
}
|
|
||||||
|
|
||||||
String serviceSource = '';
|
var generator =
|
||||||
|
GENERATORS.firstWhere((g) => g.name == type, orElse: () => null);
|
||||||
|
|
||||||
if (type == MONGO) {
|
if (generator == null) {
|
||||||
serviceSource = _generateMongoService(name);
|
|
||||||
} else if (type == MONGO_TYPED) {
|
|
||||||
serviceSource = _generateMongoTypedService(name);
|
|
||||||
await _generateMongoModel(name);
|
|
||||||
await _generateValidator(name);
|
|
||||||
} else if (type == MONGO_TYPED_JSON) {
|
|
||||||
serviceSource = _generateMongoTypedService(name);
|
|
||||||
await _generateMongoModelJson(name);
|
|
||||||
await _generateValidator(name);
|
|
||||||
} else if (type == MEMORY) {
|
|
||||||
serviceSource = _generateMemoryService(name);
|
|
||||||
await _generateMemoryModel(name);
|
|
||||||
await _generateValidator(name);
|
|
||||||
} else if (type == MEMORY_JSON) {
|
|
||||||
serviceSource = _generateMemoryService(name);
|
|
||||||
await _generateMemoryModelJson(name);
|
|
||||||
await _generateValidator(name);
|
|
||||||
} else if (type == CUSTOM) {
|
|
||||||
serviceSource = _generateCustomService(name);
|
|
||||||
} else if (type == TRESTLE) {
|
|
||||||
_pen.blue();
|
_pen.blue();
|
||||||
_pen('${Icon.STAR} Trestle services are not yet implemented. :(');
|
_pen('${Icon.STAR} \'$type\' services are not yet implemented. :(');
|
||||||
_pen();
|
_pen();
|
||||||
} else {
|
} else {
|
||||||
print('Code to generate a $type service is not yet written.');
|
var rc = new ReCase(name);
|
||||||
}
|
name = rc.pascalCase;
|
||||||
|
var lower = rc.snakeCase;
|
||||||
if (serviceSource.isEmpty) {
|
|
||||||
fail();
|
|
||||||
throw new Exception('Empty generated service code.');
|
|
||||||
}
|
|
||||||
|
|
||||||
var lower = _snake(name);
|
|
||||||
var servicesDir = new Directory('lib/src/services');
|
var servicesDir = new Directory('lib/src/services');
|
||||||
var serviceFile = new File.fromUri(servicesDir.uri.resolve('$lower.dart'));
|
var serviceFile =
|
||||||
|
new File.fromUri(servicesDir.uri.resolve('$lower.dart'));
|
||||||
var testDir = new Directory('test/services');
|
var testDir = new Directory('test/services');
|
||||||
var testFile = new File.fromUri(testDir.uri.resolve('${lower}_test.dart'));
|
var testFile =
|
||||||
|
new File.fromUri(testDir.uri.resolve('${lower}_test.dart'));
|
||||||
if (!await servicesDir.exists()) await servicesDir.create(recursive: true);
|
|
||||||
|
|
||||||
|
if (!await servicesDir.exists())
|
||||||
|
await servicesDir.create(recursive: true);
|
||||||
if (!await testDir.exists()) await testDir.create(recursive: true);
|
if (!await testDir.exists()) await testDir.create(recursive: true);
|
||||||
|
|
||||||
await serviceFile.writeAsString(serviceSource);
|
await serviceFile
|
||||||
|
.writeAsString(_generateService(generator, name, lower, typed));
|
||||||
if (type == MONGO_TYPED || type == MEMORY) {
|
await testFile.writeAsString(_generateTests(lower, type));
|
||||||
var serviceLibrary = new File('lib/src/models/models.dart');
|
|
||||||
await serviceLibrary.writeAsString("\nexport '$lower.dart';",
|
|
||||||
mode: FileMode.APPEND);
|
|
||||||
await preBuild(Directory.current);
|
|
||||||
}
|
|
||||||
|
|
||||||
await testFile.writeAsString(_generateTests(name, type));
|
|
||||||
|
|
||||||
var runConfig = new File('./.idea/runConfigurations/${name}_Tests.xml');
|
var runConfig = new File('./.idea/runConfigurations/${name}_Tests.xml');
|
||||||
|
|
||||||
if (!await runConfig.exists()) {
|
if (!await runConfig.exists()) {
|
||||||
await runConfig.create(recursive: true);
|
await runConfig.create(recursive: true);
|
||||||
await runConfig.writeAsString(_generateRunConfiguration(name));
|
await runConfig.writeAsString(_generateRunConfiguration(name, lower));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (generator.createsModel == true) {
|
||||||
|
await _generateModel(name, lower);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (generator.createsValidator == true) {
|
||||||
|
await _generateValidator(lower, rc.constantCase);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (generator.exportedInServiceLibrary == true) {
|
||||||
|
var serviceLibrary = new File('lib/src/models/models.dart');
|
||||||
|
await serviceLibrary.writeAsString("\nexport '$lower.dart';",
|
||||||
|
mode: FileMode.APPEND);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (generator.shouldRunBuild == true) {
|
||||||
|
await preBuild(Directory.current);
|
||||||
}
|
}
|
||||||
|
|
||||||
_pen.green();
|
_pen.green();
|
||||||
_pen('${Icon.CHECKMARK} Successfully generated service $name.');
|
_pen('${Icon.CHECKMARK} Successfully generated service $name.');
|
||||||
_pen();
|
_pen();
|
||||||
}
|
}
|
||||||
|
|
||||||
_generateValidator(String name) async {
|
|
||||||
var rc = new ReCase(name);
|
|
||||||
var file = new File('lib/src/validators/${rc.snakeCase}.dart');
|
|
||||||
|
|
||||||
if (!await file.exists()) await file.createSync(recursive: true);
|
|
||||||
|
|
||||||
await file.writeAsString('''
|
|
||||||
import 'package:angel_validate/angel_validate.dart';
|
|
||||||
|
|
||||||
final Validator CREATE_${rc.constantCase} =
|
|
||||||
new Validator({'name*': isString, 'desc*': isString});
|
|
||||||
'''
|
|
||||||
.trim());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
_generateCustomService(String name) {
|
String _generateService(
|
||||||
return '''
|
ServiceGenerator generator, String name, String lower, bool typed) {
|
||||||
|
var lib = new LibraryBuilder();
|
||||||
|
|
||||||
|
/*
|
||||||
import 'package:angel_framework/angel_framework.dart';
|
import 'package:angel_framework/angel_framework.dart';
|
||||||
|
import '../models/$lower.dart';
|
||||||
|
export '../models/$lower.dart';
|
||||||
|
*/
|
||||||
|
lib.addMember(
|
||||||
|
new ImportBuilder('package:angel_framework/angel_framework.dart'));
|
||||||
|
generator.applyToLibrary(lib, name, lower);
|
||||||
|
|
||||||
class ${name}Service extends Service {
|
if (generator.createsModel == true) {
|
||||||
${name}Service():super() {
|
lib
|
||||||
// Your logic here!
|
..addMember(new ImportBuilder('../models/$lower.dart'))
|
||||||
}
|
..addMember(new ExportBuilder('../models/$lower.dart'));
|
||||||
}
|
|
||||||
'''
|
|
||||||
.trim();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
_generateMemoryModel(String name) async {
|
// configureServer() {}
|
||||||
var lower = _snake(name);
|
var configureServer = new MethodBuilder('configureServer',
|
||||||
|
returnType: new TypeBuilder('AngelConfigurer'));
|
||||||
|
generator.applyToConfigureServer(configureServer, name, lower);
|
||||||
|
|
||||||
|
// return (Angel app) async {}
|
||||||
|
var closure = new MethodBuilder.closure(modifier: MethodModifier.asAsync)
|
||||||
|
..addPositional(parameter('app', [new TypeBuilder('Angel')]));
|
||||||
|
generator.beforeService(closure, name, lower);
|
||||||
|
|
||||||
|
// app.use('/api/todos', new MapService());
|
||||||
|
var service = generator.createInstance(closure, name, lower);
|
||||||
|
|
||||||
|
if (typed == true) {
|
||||||
|
service =
|
||||||
|
new TypeBuilder('TypedService', genericTypes: [new TypeBuilder(name)])
|
||||||
|
.newInstance([service]);
|
||||||
|
}
|
||||||
|
|
||||||
|
closure.addStatement(reference('app')
|
||||||
|
.invoke('use', [literal('/api/${pluralize(lower)}'), service]));
|
||||||
|
|
||||||
|
if (generator.injectsSingleton == true) {
|
||||||
|
closure.addStatement(varField('service',
|
||||||
|
value: reference('app')
|
||||||
|
.invoke('service', [literal('/api/${pluralize(lower)}')]).castAs(
|
||||||
|
new TypeBuilder('HookedService'))));
|
||||||
|
closure.addStatement(reference('app')
|
||||||
|
.property('container')
|
||||||
|
.invoke('singleton', [reference('service').property('inner')]));
|
||||||
|
}
|
||||||
|
|
||||||
|
configureServer.addStatement(closure.asReturn());
|
||||||
|
|
||||||
|
lib.addMember(configureServer);
|
||||||
|
|
||||||
|
return prettyToSource(lib.buildAst());
|
||||||
|
}
|
||||||
|
|
||||||
|
_generateModel(String name, String lower) async {
|
||||||
var file = new File('lib/src/models/$lower.dart');
|
var file = new File('lib/src/models/$lower.dart');
|
||||||
|
|
||||||
if (!await file.exists()) await file.createSync(recursive: true);
|
if (!await file.exists()) await file.createSync(recursive: true);
|
||||||
|
|
||||||
await file.writeAsString('''
|
await file.writeAsString('''
|
||||||
library angel.models.$lower;
|
|
||||||
|
|
||||||
import 'package:angel_framework/common.dart';
|
import 'package:angel_framework/common.dart';
|
||||||
|
|
||||||
class $name extends Model {
|
class $name extends Model {
|
||||||
String name, desc;
|
String name;
|
||||||
|
|
||||||
|
String desc;
|
||||||
|
|
||||||
$name({String id, this.name, this.desc}) {
|
$name({String id, this.name, this.desc}) {
|
||||||
this.id = id;
|
this.id = id;
|
||||||
|
@ -155,197 +172,21 @@ class $name extends Model {
|
||||||
.trim());
|
.trim());
|
||||||
}
|
}
|
||||||
|
|
||||||
_generateMemoryModelJson(String name) async {
|
_generateValidator(String lower, String constantCase) async {
|
||||||
var lower = _snake(name);
|
var file = new File('lib/src/validators/$lower.dart');
|
||||||
var file = new File('lib/src/models/$lower.dart');
|
|
||||||
|
|
||||||
if (!await file.exists()) await file.createSync(recursive: true);
|
if (!await file.exists()) await file.createSync(recursive: true);
|
||||||
|
|
||||||
await file.writeAsString('''
|
await file.writeAsString('''
|
||||||
library angel.models.$lower;
|
|
||||||
|
|
||||||
import 'package:angel_framework/common.dart';
|
|
||||||
import 'package:source_gen/generators/json_serializable.dart';
|
|
||||||
|
|
||||||
part '$lower.g.dart';
|
|
||||||
|
|
||||||
@JsonSerializable()
|
|
||||||
class $name extends Model with _\$${name}SerializerMixin {
|
|
||||||
@JsonKey('id')
|
|
||||||
@override
|
|
||||||
String id;
|
|
||||||
|
|
||||||
@JsonKey('name')
|
|
||||||
String name;
|
|
||||||
|
|
||||||
@JsonKey('desc')
|
|
||||||
String desc;
|
|
||||||
|
|
||||||
factory $name.fromJson(Map json) => _\$${name}FromJson(json);
|
|
||||||
|
|
||||||
$name({this.id, this.name, this.desc});
|
|
||||||
}
|
|
||||||
'''
|
|
||||||
.trim());
|
|
||||||
}
|
|
||||||
|
|
||||||
_generateMemoryService(String name) {
|
|
||||||
var rc = new ReCase(name);
|
|
||||||
var lower = rc.snakeCase;
|
|
||||||
|
|
||||||
return '''
|
|
||||||
import 'package:angel_framework/angel_framework.dart';
|
|
||||||
import '../models/$lower.dart';
|
|
||||||
export '../models/$lower.dart';
|
|
||||||
|
|
||||||
/// Manages [$name] in-memory.
|
|
||||||
class ${name}Service extends MemoryService<$name> {
|
|
||||||
${name}Service():super() {
|
|
||||||
// Your logic here!
|
|
||||||
}
|
|
||||||
}
|
|
||||||
'''
|
|
||||||
.trim();
|
|
||||||
}
|
|
||||||
|
|
||||||
_generateMongoModel(String name) async {
|
|
||||||
var lower = _snake(name);
|
|
||||||
var file = new File('lib/src/models/$lower.dart');
|
|
||||||
|
|
||||||
if (!await file.exists()) await file.createSync(recursive: true);
|
|
||||||
|
|
||||||
await file.writeAsString('''
|
|
||||||
library angel.models.$lower;
|
|
||||||
|
|
||||||
import 'package:angel_mongo/model.dart';
|
|
||||||
import 'package:source_gen/generators/json_serializable.dart';
|
|
||||||
|
|
||||||
part '$lower.g.dart';
|
|
||||||
|
|
||||||
@JsonSerializable()
|
|
||||||
class $name extends Model with _\$${name}SerializerMixin {
|
|
||||||
@JsonKey('id')
|
|
||||||
@override
|
|
||||||
String id;
|
|
||||||
|
|
||||||
@JsonKey('name')
|
|
||||||
String name;
|
|
||||||
|
|
||||||
@JsonKey('desc')
|
|
||||||
String desc;
|
|
||||||
|
|
||||||
factory $name.fromJson(Map json) => _\$${name}FromJson(json);
|
|
||||||
|
|
||||||
$name({this.id, this.name, this.desc});
|
|
||||||
}
|
|
||||||
'''
|
|
||||||
.trim());
|
|
||||||
}
|
|
||||||
|
|
||||||
_generateMongoModelJson(String name) async {
|
|
||||||
var lower = _snake(name);
|
|
||||||
var file = new File('lib/src/models/$lower.dart');
|
|
||||||
|
|
||||||
if (!await file.exists()) await file.createSync(recursive: true);
|
|
||||||
|
|
||||||
await file.writeAsString('''
|
|
||||||
library angel.models.$lower;
|
|
||||||
|
|
||||||
import 'package:angel_framework/common.dart';
|
|
||||||
import 'package:source_gen/generators/json_serializable.dart';
|
|
||||||
|
|
||||||
part '$lower.g.dart';
|
|
||||||
|
|
||||||
@JsonSerializable()
|
|
||||||
class $name extends Model with _\$${name}SerializerMixin {
|
|
||||||
@JsonKey('id')
|
|
||||||
@override
|
|
||||||
String id;
|
|
||||||
|
|
||||||
@JsonKey('name')
|
|
||||||
String name;
|
|
||||||
|
|
||||||
@JsonKey('desc')
|
|
||||||
String desc;
|
|
||||||
|
|
||||||
factory $name.fromJson(Map json) => _\$${name}FromJson(json);
|
|
||||||
|
|
||||||
$name({this.id, this.name, this.desc});
|
|
||||||
}
|
|
||||||
'''
|
|
||||||
.trim());
|
|
||||||
}
|
|
||||||
|
|
||||||
_generateMongoService(String name) {
|
|
||||||
var lower = _snake(name);
|
|
||||||
|
|
||||||
return '''
|
|
||||||
import 'package:angel_framework/angel_framework.dart';
|
|
||||||
import 'package:angel_mongo/angel_mongo.dart';
|
|
||||||
import 'package:angel_validate/angel_validate.dart';
|
import 'package:angel_validate/angel_validate.dart';
|
||||||
import 'package:angel_validate/server.dart';
|
|
||||||
import 'package:mongo_dart/mongo_dart.dart';
|
|
||||||
|
|
||||||
final Validator ${lower}Schema = new Validator({
|
final Validator CREATE_$constantCase =
|
||||||
'name*': [isString, isNotEmpty],
|
new Validator({'name*': isString, 'desc*': isString});
|
||||||
'desc*': [isString, isNotEmpty]
|
|
||||||
});
|
|
||||||
|
|
||||||
configureServer(Db db) {
|
|
||||||
return (Angel app) async {
|
|
||||||
app.use('/api/${lower}s', new ${name}Service(db.collection('${lower}s')));
|
|
||||||
|
|
||||||
HookedService service = app.service('api/${lower}s');
|
|
||||||
app.container.singleton(service.inner);
|
|
||||||
|
|
||||||
service
|
|
||||||
..beforeCreate.listen(validateEvent(${lower}Schema))
|
|
||||||
..beforeUpdate.listen(validateEvent(${lower}Schema));
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Manages [$name] in the database.
|
|
||||||
class ${name}Service extends MongoService {
|
|
||||||
${name}Service(collection):super(collection) {
|
|
||||||
// Your logic here!
|
|
||||||
}
|
|
||||||
}
|
|
||||||
'''
|
'''
|
||||||
.trim();
|
.trim());
|
||||||
}
|
}
|
||||||
|
|
||||||
_generateMongoTypedService(String name) {
|
_generateRunConfiguration(String name, String lower) {
|
||||||
var lower = _snake(name);
|
|
||||||
|
|
||||||
return '''
|
|
||||||
import 'package:angel_framework/angel_framework.dart';
|
|
||||||
import 'package:angel_mongo/angel_mongo.dart';
|
|
||||||
import 'package:mongo_dart/mongo_dart.dart';
|
|
||||||
import '../models/$lower.dart';
|
|
||||||
export '../models/$lower.dart';
|
|
||||||
|
|
||||||
configureServer(Db db) {
|
|
||||||
return (Angel app) async {
|
|
||||||
app.use('/api/${lower}s', new ${name}Service(db.collection('${lower}s')));
|
|
||||||
|
|
||||||
HookedService service = app.service('api/${lower}s');
|
|
||||||
app.container.singleton(service.inner);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Manages [$name] in the database.
|
|
||||||
class ${name}Service extends MongoTypedService<$name> {
|
|
||||||
${name}Service(collection):super(collection) {
|
|
||||||
// Your logic here!
|
|
||||||
}
|
|
||||||
}
|
|
||||||
'''
|
|
||||||
.trim();
|
|
||||||
}
|
|
||||||
|
|
||||||
_generateRunConfiguration(String name) {
|
|
||||||
var lower = _snake(name);
|
|
||||||
|
|
||||||
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">
|
||||||
|
@ -357,9 +198,7 @@ class ${name}Service extends MongoTypedService<$name> {
|
||||||
.trim();
|
.trim();
|
||||||
}
|
}
|
||||||
|
|
||||||
_generateTests(String name, String type) {
|
_generateTests(String lower, String type) {
|
||||||
var lower = _snake(name);
|
|
||||||
|
|
||||||
return '''
|
return '''
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
import 'package:angel/angel.dart';
|
import 'package:angel/angel.dart';
|
||||||
|
@ -382,13 +221,13 @@ main() async {
|
||||||
});
|
});
|
||||||
|
|
||||||
test('index via REST', () async {
|
test('index via REST', () async {
|
||||||
var response = await client.get('/api/${lower}s');
|
var response = await client.get('/api/${pluralize(lower)}');
|
||||||
expect(response, hasStatus(HttpStatus.OK));
|
expect(response, hasStatus(HttpStatus.OK));
|
||||||
});
|
});
|
||||||
|
|
||||||
test('Index ${lower}s', () async {
|
test('Index ${pluralize(lower)}', () async {
|
||||||
var ${lower}s = await client.service('api/${lower}s').index();
|
var ${pluralize(lower)} = await client.service('api/${pluralize(lower)}').index();
|
||||||
print(${lower}s);
|
print(${pluralize(lower)});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
24
lib/src/commands/service_generators/custom.dart
Normal file
24
lib/src/commands/service_generators/custom.dart
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
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) {
|
||||||
|
var clazz = new ClassBuilder('${name}Service', asExtends: new TypeBuilder('Service'));
|
||||||
|
library.addMember(clazz);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
ExpressionBuilder createInstance(
|
||||||
|
MethodBuilder methodBuilder, String name, String lower) {
|
||||||
|
return new TypeBuilder('${name}Service').newInstance([]);
|
||||||
|
}
|
||||||
|
}
|
24
lib/src/commands/service_generators/generator.dart
Normal file
24
lib/src/commands/service_generators/generator.dart
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
import 'package:code_builder/code_builder.dart';
|
||||||
|
|
||||||
|
class ServiceGenerator {
|
||||||
|
final String name;
|
||||||
|
|
||||||
|
const ServiceGenerator(this.name);
|
||||||
|
|
||||||
|
bool get createsModel => true;
|
||||||
|
bool get createsValidator => true;
|
||||||
|
bool get exportedInServiceLibrary => true;
|
||||||
|
bool get injectsSingleton => true;
|
||||||
|
bool get shouldRunBuild => false;
|
||||||
|
|
||||||
|
void applyToLibrary(LibraryBuilder library, String name, String lower) {}
|
||||||
|
|
||||||
|
void beforeService(MethodBuilder methodBuilder, String name, String lower) {}
|
||||||
|
|
||||||
|
void applyToConfigureServer(
|
||||||
|
MethodBuilder configureServer, String name, String lower) {}
|
||||||
|
|
||||||
|
ExpressionBuilder createInstance(
|
||||||
|
MethodBuilder methodBuilder, String name, String lower) =>
|
||||||
|
literal(null);
|
||||||
|
}
|
15
lib/src/commands/service_generators/map.dart
Normal file
15
lib/src/commands/service_generators/map.dart
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
import 'generator.dart';
|
||||||
|
import 'package:code_builder/code_builder.dart';
|
||||||
|
|
||||||
|
class MapServiceGenerator extends ServiceGenerator {
|
||||||
|
@override
|
||||||
|
bool get createsModel => false;
|
||||||
|
|
||||||
|
const MapServiceGenerator() : super('In-Memory');
|
||||||
|
|
||||||
|
@override
|
||||||
|
ExpressionBuilder createInstance(
|
||||||
|
MethodBuilder methodBuilder, String name, String lower) {
|
||||||
|
return new TypeBuilder('MapService').newInstance([]);
|
||||||
|
}
|
||||||
|
}
|
29
lib/src/commands/service_generators/mongo.dart
Normal file
29
lib/src/commands/service_generators/mongo.dart
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
import 'generator.dart';
|
||||||
|
import 'package:code_builder/code_builder.dart';
|
||||||
|
import 'package:inflection/inflection.dart';
|
||||||
|
|
||||||
|
class MongoServiceGenerator extends ServiceGenerator {
|
||||||
|
const MongoServiceGenerator() : super('MongoDB');
|
||||||
|
|
||||||
|
@override
|
||||||
|
void applyToConfigureServer(
|
||||||
|
MethodBuilder configureServer, String name, String lower) {
|
||||||
|
configureServer.addPositional(parameter('db', [new TypeBuilder('Db')]));
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void applyToLibrary(LibraryBuilder library, String name, String lower) {
|
||||||
|
library.addMembers([
|
||||||
|
'package:angel_mongo/angel_mongo.dart',
|
||||||
|
'package:mongo_dart/mongo_dart.dart'
|
||||||
|
].map((str) => new ImportBuilder(str)));
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
ExpressionBuilder createInstance(
|
||||||
|
MethodBuilder methodBuilder, String name, String lower) {
|
||||||
|
return new TypeBuilder('MongoService').newInstance([
|
||||||
|
reference('db').invoke('collection', [literal(pluralize(lower))])
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
31
lib/src/commands/service_generators/rethink.dart
Normal file
31
lib/src/commands/service_generators/rethink.dart
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
import 'generator.dart';
|
||||||
|
import 'package:code_builder/code_builder.dart';
|
||||||
|
import 'package:inflection/inflection.dart';
|
||||||
|
|
||||||
|
class RethinkServiceGenerator extends ServiceGenerator {
|
||||||
|
const RethinkServiceGenerator() : super('RethinkDB');
|
||||||
|
|
||||||
|
@override
|
||||||
|
void applyToConfigureServer(
|
||||||
|
MethodBuilder configureServer, String name, String lower) {
|
||||||
|
configureServer
|
||||||
|
..addPositional(parameter('connection', [new TypeBuilder('Connection')]))
|
||||||
|
..addPositional(parameter('r', [new TypeBuilder('Rethinkdb')]));
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void applyToLibrary(LibraryBuilder library, String name, String lower) {
|
||||||
|
library.addMembers([
|
||||||
|
'package:angel_rethink/angel_rethink.dart',
|
||||||
|
'package:rethinkdb_driver/rethinkdb_driver.dart'
|
||||||
|
].map((str) => new ImportBuilder(str)));
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
ExpressionBuilder createInstance(
|
||||||
|
MethodBuilder methodBuilder, String name, String lower) {
|
||||||
|
return new TypeBuilder('RethinkService').newInstance([
|
||||||
|
reference('r').invoke('table', [literal(pluralize(lower))])
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,5 @@
|
||||||
|
export 'custom.dart';
|
||||||
|
export 'generator.dart';
|
||||||
|
export 'map.dart';
|
||||||
|
export 'mongo.dart';
|
||||||
|
export 'rethink.dart';
|
398
lib/src/commands/service_old.dart
Normal file
398
lib/src/commands/service_old.dart
Normal file
|
@ -0,0 +1,398 @@
|
||||||
|
import 'dart:io';
|
||||||
|
import 'package:args/command_runner.dart';
|
||||||
|
import 'package:console/console.dart';
|
||||||
|
import 'package:id/id.dart';
|
||||||
|
import 'package:recase/recase.dart';
|
||||||
|
import 'init.dart' show preBuild;
|
||||||
|
|
||||||
|
class ServiceCommand extends Command {
|
||||||
|
final String CUSTOM = 'Custom';
|
||||||
|
final String MEMORY = 'In-Memory';
|
||||||
|
final String MEMORY_JSON = 'In-Memory (serialized via `source_gen`)';
|
||||||
|
final String MONGO = 'MongoDB';
|
||||||
|
final String MONGO_TYPED = 'MongoDB (typed)';
|
||||||
|
final String MONGO_TYPED_JSON =
|
||||||
|
'MongoDB (typed, serialized via `source_gen`)';
|
||||||
|
final String TRESTLE = 'Trestle';
|
||||||
|
final TextPen _pen = new TextPen();
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get name => 'service';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get description => 'Creates a new service within the given project.';
|
||||||
|
|
||||||
|
String _snake(name) => idFromString(name).snake;
|
||||||
|
|
||||||
|
@override
|
||||||
|
run() async {
|
||||||
|
var name = await readInput('Name of Service (not plural): ');
|
||||||
|
var chooser = new Chooser([MONGO, MONGO_TYPED, MEMORY, CUSTOM],
|
||||||
|
message: 'What type of service would you like to create? ');
|
||||||
|
var type = await chooser.choose();
|
||||||
|
|
||||||
|
fail() {
|
||||||
|
_pen.red();
|
||||||
|
_pen('Could not successfully create service $name.');
|
||||||
|
_pen();
|
||||||
|
}
|
||||||
|
|
||||||
|
String serviceSource = '';
|
||||||
|
|
||||||
|
if (type == MONGO) {
|
||||||
|
serviceSource = _generateMongoService(name);
|
||||||
|
} else if (type == MONGO_TYPED) {
|
||||||
|
serviceSource = _generateMongoTypedService(name);
|
||||||
|
await _generateMongoModel(name);
|
||||||
|
await _generateValidator(name);
|
||||||
|
} else if (type == MONGO_TYPED_JSON) {
|
||||||
|
serviceSource = _generateMongoTypedService(name);
|
||||||
|
await _generateMongoModelJson(name);
|
||||||
|
await _generateValidator(name);
|
||||||
|
} else if (type == MEMORY) {
|
||||||
|
serviceSource = _generateMemoryService(name);
|
||||||
|
await _generateMemoryModel(name);
|
||||||
|
await _generateValidator(name);
|
||||||
|
} else if (type == MEMORY_JSON) {
|
||||||
|
serviceSource = _generateMemoryService(name);
|
||||||
|
await _generateMemoryModelJson(name);
|
||||||
|
await _generateValidator(name);
|
||||||
|
} else if (type == CUSTOM) {
|
||||||
|
serviceSource = _generateCustomService(name);
|
||||||
|
} else if (type == TRESTLE) {
|
||||||
|
_pen.blue();
|
||||||
|
_pen('${Icon.STAR} Trestle services are not yet implemented. :(');
|
||||||
|
_pen();
|
||||||
|
} else {
|
||||||
|
print('Code to generate a $type service is not yet written.');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (serviceSource.isEmpty) {
|
||||||
|
fail();
|
||||||
|
throw new Exception('Empty generated service code.');
|
||||||
|
}
|
||||||
|
|
||||||
|
var lower = _snake(name);
|
||||||
|
var servicesDir = new Directory('lib/src/services');
|
||||||
|
var serviceFile = new File.fromUri(servicesDir.uri.resolve('$lower.dart'));
|
||||||
|
var testDir = new Directory('test/services');
|
||||||
|
var testFile = new File.fromUri(testDir.uri.resolve('${lower}_test.dart'));
|
||||||
|
|
||||||
|
if (!await servicesDir.exists()) await servicesDir.create(recursive: true);
|
||||||
|
|
||||||
|
if (!await testDir.exists()) await testDir.create(recursive: true);
|
||||||
|
|
||||||
|
await serviceFile.writeAsString(serviceSource);
|
||||||
|
|
||||||
|
if (type == MONGO_TYPED || type == MEMORY) {
|
||||||
|
var serviceLibrary = new File('lib/src/models/models.dart');
|
||||||
|
await serviceLibrary.writeAsString("\nexport '$lower.dart';",
|
||||||
|
mode: FileMode.APPEND);
|
||||||
|
await preBuild(Directory.current);
|
||||||
|
}
|
||||||
|
|
||||||
|
await testFile.writeAsString(_generateTests(name, type));
|
||||||
|
|
||||||
|
var runConfig = new File('./.idea/runConfigurations/${name}_Tests.xml');
|
||||||
|
|
||||||
|
if (!await runConfig.exists()) {
|
||||||
|
await runConfig.create(recursive: true);
|
||||||
|
await runConfig.writeAsString(_generateRunConfiguration(name));
|
||||||
|
}
|
||||||
|
|
||||||
|
_pen.green();
|
||||||
|
_pen('${Icon.CHECKMARK} Successfully generated service $name.');
|
||||||
|
_pen();
|
||||||
|
}
|
||||||
|
|
||||||
|
_generateValidator(String name) async {
|
||||||
|
var rc = new ReCase(name);
|
||||||
|
var file = new File('lib/src/validators/${rc.snakeCase}.dart');
|
||||||
|
|
||||||
|
if (!await file.exists()) await file.createSync(recursive: true);
|
||||||
|
|
||||||
|
await file.writeAsString('''
|
||||||
|
import 'package:angel_validate/angel_validate.dart';
|
||||||
|
|
||||||
|
final Validator CREATE_${rc.constantCase} =
|
||||||
|
new Validator({'name*': isString, 'desc*': isString});
|
||||||
|
'''
|
||||||
|
.trim());
|
||||||
|
}
|
||||||
|
|
||||||
|
_generateCustomService(String name) {
|
||||||
|
return '''
|
||||||
|
import 'package:angel_framework/angel_framework.dart';
|
||||||
|
|
||||||
|
class ${name}Service extends Service {
|
||||||
|
${name}Service():super() {
|
||||||
|
// Your logic here!
|
||||||
|
}
|
||||||
|
}
|
||||||
|
'''
|
||||||
|
.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
_generateMemoryModel(String name) async {
|
||||||
|
var lower = _snake(name);
|
||||||
|
var file = new File('lib/src/models/$lower.dart');
|
||||||
|
|
||||||
|
if (!await file.exists()) await file.createSync(recursive: true);
|
||||||
|
|
||||||
|
await file.writeAsString('''
|
||||||
|
library angel.models.$lower;
|
||||||
|
|
||||||
|
import 'package:angel_framework/common.dart';
|
||||||
|
|
||||||
|
class $name extends Model {
|
||||||
|
String name, desc;
|
||||||
|
|
||||||
|
$name({String id, this.name, this.desc}) {
|
||||||
|
this.id = id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
'''
|
||||||
|
.trim());
|
||||||
|
}
|
||||||
|
|
||||||
|
_generateMemoryModelJson(String name) async {
|
||||||
|
var lower = _snake(name);
|
||||||
|
var file = new File('lib/src/models/$lower.dart');
|
||||||
|
|
||||||
|
if (!await file.exists()) await file.createSync(recursive: true);
|
||||||
|
|
||||||
|
await file.writeAsString('''
|
||||||
|
library angel.models.$lower;
|
||||||
|
|
||||||
|
import 'package:angel_framework/common.dart';
|
||||||
|
import 'package:source_gen/generators/json_serializable.dart';
|
||||||
|
|
||||||
|
part '$lower.g.dart';
|
||||||
|
|
||||||
|
@JsonSerializable()
|
||||||
|
class $name extends Model with _\$${name}SerializerMixin {
|
||||||
|
@JsonKey('id')
|
||||||
|
@override
|
||||||
|
String id;
|
||||||
|
|
||||||
|
@JsonKey('name')
|
||||||
|
String name;
|
||||||
|
|
||||||
|
@JsonKey('desc')
|
||||||
|
String desc;
|
||||||
|
|
||||||
|
factory $name.fromJson(Map json) => _\$${name}FromJson(json);
|
||||||
|
|
||||||
|
$name({this.id, this.name, this.desc});
|
||||||
|
}
|
||||||
|
'''
|
||||||
|
.trim());
|
||||||
|
}
|
||||||
|
|
||||||
|
_generateMemoryService(String name) {
|
||||||
|
var rc = new ReCase(name);
|
||||||
|
var lower = rc.snakeCase;
|
||||||
|
|
||||||
|
return '''
|
||||||
|
import 'package:angel_framework/angel_framework.dart';
|
||||||
|
import '../models/$lower.dart';
|
||||||
|
export '../models/$lower.dart';
|
||||||
|
|
||||||
|
/// Manages [$name] in-memory.
|
||||||
|
class ${name}Service extends MemoryService<$name> {
|
||||||
|
${name}Service():super() {
|
||||||
|
// Your logic here!
|
||||||
|
}
|
||||||
|
}
|
||||||
|
'''
|
||||||
|
.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
_generateMongoModel(String name) async {
|
||||||
|
var lower = _snake(name);
|
||||||
|
var file = new File('lib/src/models/$lower.dart');
|
||||||
|
|
||||||
|
if (!await file.exists()) await file.createSync(recursive: true);
|
||||||
|
|
||||||
|
await file.writeAsString('''
|
||||||
|
library angel.models.$lower;
|
||||||
|
|
||||||
|
import 'package:angel_mongo/model.dart';
|
||||||
|
import 'package:source_gen/generators/json_serializable.dart';
|
||||||
|
|
||||||
|
part '$lower.g.dart';
|
||||||
|
|
||||||
|
@JsonSerializable()
|
||||||
|
class $name extends Model with _\$${name}SerializerMixin {
|
||||||
|
@JsonKey('id')
|
||||||
|
@override
|
||||||
|
String id;
|
||||||
|
|
||||||
|
@JsonKey('name')
|
||||||
|
String name;
|
||||||
|
|
||||||
|
@JsonKey('desc')
|
||||||
|
String desc;
|
||||||
|
|
||||||
|
factory $name.fromJson(Map json) => _\$${name}FromJson(json);
|
||||||
|
|
||||||
|
$name({this.id, this.name, this.desc});
|
||||||
|
}
|
||||||
|
'''
|
||||||
|
.trim());
|
||||||
|
}
|
||||||
|
|
||||||
|
_generateMongoModelJson(String name) async {
|
||||||
|
var lower = _snake(name);
|
||||||
|
var file = new File('lib/src/models/$lower.dart');
|
||||||
|
|
||||||
|
if (!await file.exists()) await file.createSync(recursive: true);
|
||||||
|
|
||||||
|
await file.writeAsString('''
|
||||||
|
library angel.models.$lower;
|
||||||
|
|
||||||
|
import 'package:angel_framework/common.dart';
|
||||||
|
import 'package:source_gen/generators/json_serializable.dart';
|
||||||
|
|
||||||
|
part '$lower.g.dart';
|
||||||
|
|
||||||
|
@JsonSerializable()
|
||||||
|
class $name extends Model with _\$${name}SerializerMixin {
|
||||||
|
@JsonKey('id')
|
||||||
|
@override
|
||||||
|
String id;
|
||||||
|
|
||||||
|
@JsonKey('name')
|
||||||
|
String name;
|
||||||
|
|
||||||
|
@JsonKey('desc')
|
||||||
|
String desc;
|
||||||
|
|
||||||
|
factory $name.fromJson(Map json) => _\$${name}FromJson(json);
|
||||||
|
|
||||||
|
$name({this.id, this.name, this.desc});
|
||||||
|
}
|
||||||
|
'''
|
||||||
|
.trim());
|
||||||
|
}
|
||||||
|
|
||||||
|
_generateMongoService(String name) {
|
||||||
|
var lower = _snake(name);
|
||||||
|
|
||||||
|
return '''
|
||||||
|
import 'package:angel_framework/angel_framework.dart';
|
||||||
|
import 'package:angel_mongo/angel_mongo.dart';
|
||||||
|
import 'package:angel_validate/angel_validate.dart';
|
||||||
|
import 'package:angel_validate/server.dart';
|
||||||
|
import 'package:mongo_dart/mongo_dart.dart';
|
||||||
|
|
||||||
|
final Validator ${lower}Schema = new Validator({
|
||||||
|
'name*': [isString, isNotEmpty],
|
||||||
|
'desc*': [isString, isNotEmpty]
|
||||||
|
});
|
||||||
|
|
||||||
|
configureServer(Db db) {
|
||||||
|
return (Angel app) async {
|
||||||
|
app.use('/api/${lower}s', new ${name}Service(db.collection('${lower}s')));
|
||||||
|
|
||||||
|
HookedService service = app.service('api/${lower}s');
|
||||||
|
app.container.singleton(service.inner);
|
||||||
|
|
||||||
|
service
|
||||||
|
..beforeCreate.listen(validateEvent(${lower}Schema))
|
||||||
|
..beforeUpdate.listen(validateEvent(${lower}Schema));
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Manages [$name] in the database.
|
||||||
|
class ${name}Service extends MongoService {
|
||||||
|
${name}Service(collection):super(collection) {
|
||||||
|
// Your logic here!
|
||||||
|
}
|
||||||
|
}
|
||||||
|
'''
|
||||||
|
.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
_generateMongoTypedService(String name) {
|
||||||
|
var lower = _snake(name);
|
||||||
|
|
||||||
|
return '''
|
||||||
|
import 'package:angel_framework/angel_framework.dart';
|
||||||
|
import 'package:angel_mongo/angel_mongo.dart';
|
||||||
|
import 'package:mongo_dart/mongo_dart.dart';
|
||||||
|
import '../models/$lower.dart';
|
||||||
|
export '../models/$lower.dart';
|
||||||
|
|
||||||
|
configureServer(Db db) {
|
||||||
|
return (Angel app) async {
|
||||||
|
app.use('/api/${lower}s', new ${name}Service(db.collection('${lower}s')));
|
||||||
|
|
||||||
|
HookedService service = app.service('api/${lower}s');
|
||||||
|
app.container.singleton(service.inner);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Manages [$name] in the database.
|
||||||
|
class ${name}Service extends MongoTypedService<$name> {
|
||||||
|
${name}Service(collection):super(collection) {
|
||||||
|
// Your logic here!
|
||||||
|
}
|
||||||
|
}
|
||||||
|
'''
|
||||||
|
.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
_generateRunConfiguration(String name) {
|
||||||
|
var lower = _snake(name);
|
||||||
|
|
||||||
|
return '''
|
||||||
|
<component name="ProjectRunConfigurationManager">
|
||||||
|
<configuration default="false" name="$name Tests" type="DartTestRunConfigurationType" factoryName="Dart Test" singleton="true">
|
||||||
|
<option name="filePath" value="\$PROJECT_DIR\$/test/services/${lower}_test.dart" />
|
||||||
|
<method />
|
||||||
|
</configuration>
|
||||||
|
</component>
|
||||||
|
'''
|
||||||
|
.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
_generateTests(String name, String type) {
|
||||||
|
var lower = _snake(name);
|
||||||
|
|
||||||
|
return '''
|
||||||
|
import 'dart:io';
|
||||||
|
import 'package:angel/angel.dart';
|
||||||
|
import 'package:angel_framework/angel_framework.dart';
|
||||||
|
import 'package:angel_test/angel_test.dart';
|
||||||
|
import 'package:test/test.dart';
|
||||||
|
|
||||||
|
main() async {
|
||||||
|
Angel app;
|
||||||
|
TestClient client;
|
||||||
|
|
||||||
|
setUp(() async {
|
||||||
|
app = await createServer();
|
||||||
|
client = await connectTo(app);
|
||||||
|
});
|
||||||
|
|
||||||
|
tearDown(() async {
|
||||||
|
await client.close();
|
||||||
|
app = null;
|
||||||
|
});
|
||||||
|
|
||||||
|
test('index via REST', () async {
|
||||||
|
var response = await client.get('/api/${lower}s');
|
||||||
|
expect(response, hasStatus(HttpStatus.OK));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Index ${lower}s', () async {
|
||||||
|
var ${lower}s = await client.service('api/${lower}s').index();
|
||||||
|
print(${lower}s);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
'''
|
||||||
|
.trim();
|
||||||
|
}
|
||||||
|
}
|
|
@ -2,13 +2,15 @@ author: "Tobe O <thosakwe@gmail.com>"
|
||||||
description: "Command-line tools for the Angel framework."
|
description: "Command-line tools for the Angel framework."
|
||||||
homepage: "https://github.com/angel-dart/angel_cli"
|
homepage: "https://github.com/angel-dart/angel_cli"
|
||||||
name: "angel_cli"
|
name: "angel_cli"
|
||||||
version: "1.0.0"
|
version: "1.0.1"
|
||||||
dependencies:
|
dependencies:
|
||||||
analyzer: "^0.29.0"
|
analyzer: "^0.29.0"
|
||||||
args: "^0.13.7"
|
args: "^0.13.7"
|
||||||
|
code_builder: "^1.0.0-alpha"
|
||||||
console: "^2.2.3"
|
console: "^2.2.3"
|
||||||
glob: "^1.1.0"
|
glob: "^1.1.0"
|
||||||
id: "^1.0.0"
|
id: "^1.0.0"
|
||||||
|
inflection: "^0.4.1"
|
||||||
pubspec: "^0.0.14"
|
pubspec: "^0.0.14"
|
||||||
random_string: "^0.0.1"
|
random_string: "^0.0.1"
|
||||||
recase: "^1.0.0"
|
recase: "^1.0.0"
|
||||||
|
|
Loading…
Reference in a new issue