diff --git a/.melos/template.yaml b/.melos/template.yaml new file mode 100644 index 0000000..d05c996 --- /dev/null +++ b/.melos/template.yaml @@ -0,0 +1,13 @@ +scripts: + _: &template_scripts + template: + name: Create from template + description: | + Creates a new project from a template in the templates directory. + + Usage: melos run template template_name:name type:dart|flutter name:project_name + + Example: + melos run template template_name:bloc_app type:flutter name:my_new_app + melos run template template_name:core_package type:dart name:core_utils + run: dart run helpers/create_from_template.dart $MELOS_ARGS diff --git a/helpers/create_from_template.dart b/helpers/create_from_template.dart new file mode 100644 index 0000000..946757f --- /dev/null +++ b/helpers/create_from_template.dart @@ -0,0 +1,182 @@ +import 'dart:io'; +import 'package:path/path.dart' as path; +import 'package:yaml/yaml.dart'; + +void main(List args) async { + // Parse command line arguments + String? templateName; + String? projectType; + String? name; + + for (var arg in args) { + final parts = arg.split(':'); + if (parts.length == 2) { + switch (parts[0]) { + case 'template_name': + templateName = parts[1]; + break; + case 'type': + projectType = parts[1]; + break; + case 'name': + name = parts[1]; + break; + } + } + } + + // Print received arguments for debugging + print('Received arguments:'); + print('Template Name: $templateName'); + print('Project Type: $projectType'); + print('Name: $name'); + + // Validate inputs + if (templateName == null || projectType == null || name == null) { + print('Error: Missing required arguments'); + print( + 'Usage: melos run template template_name:name type:dart|flutter name:project_name'); + exit(1); + } + + if (projectType != 'dart' && projectType != 'flutter') { + print('Error: type must be either "dart" or "flutter"'); + exit(1); + } + + // Convert name to snake_case + final snakeCaseName = name + .replaceAllMapped( + RegExp(r'[A-Z]'), + (match) => '_${match.group(0)?.toLowerCase()}', + ) + .toLowerCase() + .replaceFirst(RegExp(r'^_'), ''); + + // Determine directories + final templateDir = Directory('templates/$templateName'); + final targetBaseDir = (projectType == 'flutter') ? 'apps' : 'packages'; + final targetDir = Directory('$targetBaseDir/$snakeCaseName'); + + // Validate template exists + if (!await templateDir.exists()) { + print('Error: Template "$templateName" not found in templates directory'); + print('Available templates:'); + await for (var entity in Directory('templates').list()) { + if (entity is Directory) { + print(' - ${path.basename(entity.path)}'); + } + } + exit(1); + } + + // Check if target directory already exists + if (await targetDir.exists()) { + print('Error: Target directory already exists at ${targetDir.path}'); + exit(1); + } + + try { + // Create target directory + await targetDir.create(recursive: true); + + // Copy template files + await _copyDirectory(templateDir, targetDir); + + // Process template files + await _processTemplateFiles(targetDir, { + 'PROJECT_NAME': name, + 'PROJECT_NAME_SNAKE_CASE': snakeCaseName, + 'PROJECT_NAME_PASCAL_CASE': _toPascalCase(name), + 'PROJECT_NAME_CAMEL_CASE': _toCamelCase(name), + 'CREATION_TIMESTAMP': DateTime.now().toIso8601String(), + }); + + // Update pubspec.yaml if it exists + final pubspecFile = File('${targetDir.path}/pubspec.yaml'); + if (await pubspecFile.exists()) { + await _updatePubspec(pubspecFile, name); + } + + print( + 'Successfully created project from template "$templateName" at ${targetDir.path}'); + print('Done! 🎉'); + print('To get started, cd into ${targetDir.path}'); + } catch (e) { + print('Error: $e'); + // Cleanup on error + if (await targetDir.exists()) { + await targetDir.delete(recursive: true); + } + exit(1); + } +} + +Future _copyDirectory(Directory source, Directory target) async { + await for (var entity in source.list(recursive: false)) { + final targetPath = + path.join(target.path, path.relative(entity.path, from: source.path)); + + if (entity is Directory) { + await Directory(targetPath).create(recursive: true); + await _copyDirectory(entity, Directory(targetPath)); + } else if (entity is File) { + await entity.copy(targetPath); + } + } +} + +Future _processTemplateFiles( + Directory directory, Map replacements) async { + await for (var entity in directory.list(recursive: true)) { + if (entity is File) { + if (path.extension(entity.path) == '.tmpl') { + // Process template file + String content = await entity.readAsString(); + for (var entry in replacements.entries) { + content = content.replaceAll('{{${entry.key}}}', entry.value); + } + + // Write processed content to new file without .tmpl extension + final newPath = entity.path.replaceAll('.tmpl', ''); + await File(newPath).writeAsString(content); + await entity.delete(); // Remove template file + } else { + // Process regular file (only process certain file types) + final ext = path.extension(entity.path); + if (['.dart', '.yaml', '.md', '.json'].contains(ext)) { + String content = await entity.readAsString(); + for (var entry in replacements.entries) { + content = content.replaceAll('{{${entry.key}}}', entry.value); + } + await entity.writeAsString(content); + } + } + } + } +} + +Future _updatePubspec(File pubspecFile, String projectName) async { + final content = await pubspecFile.readAsString(); + final yaml = loadYaml(content); + + // Create new pubspec content with updated name + final newContent = content.replaceFirst( + RegExp(r'name:.*'), + 'name: $projectName', + ); + + await pubspecFile.writeAsString(newContent); +} + +String _toPascalCase(String input) { + return input + .split(RegExp(r'[_\- ]')) + .map((word) => word[0].toUpperCase() + word.substring(1).toLowerCase()) + .join(''); +} + +String _toCamelCase(String input) { + final pascal = _toPascalCase(input); + return pascal[0].toLowerCase() + pascal.substring(1); +} diff --git a/melos.yaml b/melos.yaml index 9735f51..2f84a21 100644 --- a/melos.yaml +++ b/melos.yaml @@ -75,6 +75,10 @@ scripts: configure: run: "melos bootstrap && MELOS_SCOPE=\"platform_container_generator\" melos run generate:custom && MELOS_SCOPE=\"platform_model, platform_exceptions, platform_mocking\" melos run generate:dummy:test && MELOS_SCOPE=\"platform_container_generator\" melos run debug:reflectable && melos run test && melos run coverage && melos run coverage_report && melos run docs:generate\n" description: "Configure the development environment, generate code and dummy tests, run tests, generate coverage, and create API documentation" + template: + name: Create from template + description: "Creates a new project from a template in the templates directory.\n\nUsage: melos run template template_name:name type:dart|flutter name:project_name\n\nExample:\n melos run template template_name:bloc_app type:flutter name:my_new_app\n melos run template template_name:core_package type:dart name:core_utils\n" + run: dart run helpers/create_from_template.dart $MELOS_ARGS test: run: melos exec -c 1 --fail-fast -- "dart test" description: Run tests for all packages diff --git a/s b/s new file mode 100644 index 0000000..e69de29