diff --git a/tools/brick_generator/README.md b/tools/brick_generator/README.md index be78415..3ca9087 100644 --- a/tools/brick_generator/README.md +++ b/tools/brick_generator/README.md @@ -2,42 +2,365 @@ A simple command-line application which allows to generate the template of a brick from a project which compiles. -With an entrypoint in `bin/`, library code in `lib/`. +With an entrypoint in `bin/` , library code in `lib/` . ## How to use - -- Add your app in `apps/`. -- Add `brick_config.yaml` in you app folder and add this fields: - -> Here we have created `wyatt_feature_brick` app in `apps/` +* Add your app in `apps/`. +* Add `brickgen.yaml` in you app folder and add this fields: ```yaml -name: wyatt_feature_brick -description: New feature brick including state mananement -path_to_brickify: lib/feature_name +name: brick_name +description: An awesome brick -version: 0.1.1 +version: 0.1.0 vars: - feature_name: + bundle_id: + compilable: io.wyattapp.new type: string - name: feature_name - description: Name of the feature - default: Dash - prompt: What is the name of your new feature + description: The bundle id used in Android and iOS + default: io.wyattapp.new + prompt: "What is the bundle id?" + + flutter: + type: boolean + description: If this app is a Flutter or Dart project. + default: false + prompt: "Is it Flutter app ?" + +brickgen: + path_to_brickify: brick_folder + ignore: + - .env + - node_modules/ + hooks: true + boolean_file_system: + flutter: + folders: + on_true: + - android + - ios + on_false: + - bin + files: + on_true: + - l10n.yaml + - trapeze.yaml + ``` -then run command with project path +Project structure must be like: + +``` +apps +├── brick_name +│ ├── brickgen.yaml +│ ├── hooks +│ │ └── pre_gen.dart +│ ├── brick_folder +│ │ └── ... compilable project +``` + +Then run command with project path ```sh -dart tools/brick_generator/bin/brick_generator.dart ./apps/wyatt_feature_brick +dart ./tools/brick_generator/bin/brickgen.dart ./apps/brick_name ``` ## TODO -- [ ] bool variables -- [ ] enum variables -- [ ] array variables -- [ ] pre hooks -- [ ] post hooks \ No newline at end of file +* [ ] hooks + - [x] post hooks + - [x] pre hooks + - [ ] add partial hooks +* [ ] bool variables + - [x] boolean file system + - [ ] boolean mapping +* [ ] enum variables +* [ ] array variables + +## Current specification + +### File Structure + +``` +apps +├── +│ ├── brickgen.yaml +│ ├── hooks +│ │ └── pre_gen.dart +│ ├── +│ │ └── ... compilable project +``` + +### Brick (Mason) + +```yaml +name: +description: + +version: 0.1.0 + +vars: + ... +``` + +- name: the brick name (will be in `brick.yaml`) +- description: the brick description (will be in `brick.yaml`) +- version: the brick version (will be in `brick.yaml`) +- vars: the brick variables (will be in `brick.yaml`) + +#### String variable + +```yaml +name: + compilable: + type: string + description: + default: + prompt: +``` + +- name: the variable name (will be in `brick.yaml`) + - compilable: the variable compilable name used in brickgen project + - type: variable type, here string, (will be in `brick.yaml`) + - default: default value (will be in `brick.yaml`) + - prompt: the displayed prompt at the brick generation (will be in `brick.yaml`) + +#### Boolean variable + +```yaml +name: + type: boolean + description: + default: + prompt: +``` + +- name: the variable name (will be in `brick.yaml`) + - type: variable type, here string, (will be in `brick.yaml`) + - default: default value (will be in `brick.yaml`) + - prompt: the displayed prompt at the brick generation (will be in `brick.yaml`) + +> Identical to Mason + +### Brickgen + +```yaml +brickgen: + path_to_brickify: + ignore: + - ... + hooks: true + boolean_file_system: + ... +``` + +- path_to_brickify: the path of the compilable project that will be brickified +- ignore: list of the ignored files and folders. + - be sure to add an `/` at the end of the folders name. +- hooks: copy or not `hooks` folder +- boolean_file_system: list of the boolean file system variables + +#### Boolean File System + +```yaml +boolean_file_system: + boolean: + folders: + on_true: + - ... + on_false: + - ... + files: + on_true: + - ... + on_false: + - ... +``` + +- boolean: name of the `boolean` variable used. +- folders + - on_true: list of the folders includes if the variable is `true` + - on_true: list of the folders includes if the variable is `false` +- files + - on_true: list of the files includes if the variable is `true` + - on_true: list of the files includes if the variable is `false` + +> `on_true` and `on_false` are optionnal. (You are allowed to set one or two of them). + +## Specifications (WIP) + +File structure: + +``` +apps +├── +│ ├── brickgen.yaml +│ ├── hooks +│ │ └── pre_gen.dart +│ ├── +│ │ └── ... compilable project +``` + +Configuration: `brickgen.yaml` + +```yaml +name: +description: + +version: 0.1.0 + +vars: + display_name: + compilable: Display Name + type: string + description: The display name + default: Display Name + prompt: "What is the display name?" + + project_name: + compilable: wyatt_app_template + type: string + description: The project name + default: wyatt_app + prompt: "What is the project name?" + + bundle_id: + compilable: io.wyattapp.new + type: string + description: The bundle id used in Android and iOS + default: io.wyattapp.new + prompt: "What is the bundle id?" + + flutter: + type: boolean + description: If this app is a Flutter or Dart project. + default: false + prompt: "Is it Flutter app ?" + +brickgen: + path_to_brickify: + ignore: + - .env + - node_modules/ + hooks: true + boolean_mapping: + flutter: + folders: + - "test": + on_true: "test" + on_false: "test_dart" + files: + - "pubspec.yaml": + on_true: pubspec.yaml + on_false: pubspec.dart.yaml + boolean_file_system: + flutter: + folders: + on_true: + - android + - ios + on_false: + - bin + files: + on_true: + - l10n.yaml + - trapeze.yaml +``` + +Will generate: + +``` +bricks +├── +│ ├── brick.yaml +│ ├── hooks +│ │ └── pre_gen.dart +│ ├── __brick__ +│ │ └── +``` + +> It creates a sub folder in __brick__ + + +### How copy works + +- Case 1 + +``` +apps +├── brick_1 +│ ├── brickgen.yaml +│ ├── hooks +│ │ └── pre_gen.dart +│ ├── brick_1 +│ │ ├── lib/ +│ │ └── pubspec.yaml +``` + +```yaml +... +name: awesome_brick_1 +brickgen: + path_to_brickify: brick_1 + hooks: true +... +``` + +```sh +dart ./tools/brick_generator/bin/brickgen.dart ./apps/brick_1 +``` + +Will generate: + +``` +bricks +├── awesome_brick_1 +│ ├── brick.yaml +│ ├── hooks +│ │ └── pre_gen.dart +│ ├── __brick__ +│ │ ├── lib/ +│ │ └── pubspec.yaml +``` + +*** + +- Case 2 + +``` +apps +├── brick_2 +│ ├── brickgen.yaml +│ ├── hooks +│ │ └── pre_gen.dart +│ ├── brick_2 +│ │ ├── lib +│ │ │ └── widget.dart +│ │ └── pubspec.yaml +``` + +```yaml +... +name: awesome_brick_2 +brickgen: + path_to_brickify: brick_2/lib + hooks: false +... +``` + +```sh +dart ./tools/brick_generator/bin/brickgen.dart ./apps/brick_2 +``` + +Will generate: + +``` +bricks +├── awesome_brick_2 +│ ├── brick.yaml +│ ├── __brick__ +│ │ └── widget.dart +``` \ No newline at end of file diff --git a/tools/brick_generator/bin/brick.yaml b/tools/brick_generator/bin/brick.yaml deleted file mode 100644 index 0d107b1..0000000 --- a/tools/brick_generator/bin/brick.yaml +++ /dev/null @@ -1,30 +0,0 @@ -name: -description: - -version: 0.1.0 - -vars: - display_name: - type: string - description: The display name - default: Display Name - prompt: What is the display name? - - project_name: - type: string - description: The project name - default: wyatt_app - prompt: What is the project name? - - bundle_id: - type: string - description: The bundle id used in Android and iOS - default: io.wyattapp.new - prompt: What is the bundle id? - - flutter: - type: boolean - description: If this app is a Flutter or Dart project. - default: false - prompt: Is it Flutter app ? - diff --git a/tools/brick_generator/bin/brick_generator.dart b/tools/brick_generator/bin/brick_generator.dart deleted file mode 100644 index ceecce0..0000000 --- a/tools/brick_generator/bin/brick_generator.dart +++ /dev/null @@ -1,93 +0,0 @@ -import 'dart:io'; - -import 'package:brick_generator/core/logger.dart'; -import 'package:brick_generator/core/shell.dart'; -import 'package:brick_generator/file_system_utils.dart'; -import 'package:brick_generator/models/brick_config.dart'; -import 'package:brick_generator/yaml_reader.dart'; -import 'package:path/path.dart' as path; - -// Constants -const _bricks = 'bricks'; -const _brickFolderLabel = '__brick__'; -const _yamlFileName = 'brick_config.yaml'; -const _cfgFileName = 'brick.yaml'; - -Future main(List arguments) async { - try { - if (arguments.length != 1) { - Logger.error('Please provide project path.'); - exit(1); - } - - final projectPath = arguments[0]; - - // Store options from yaml file - final configs = - YamlReader.readYamlFile(path.join(projectPath, _yamlFileName)); - final brickConfig = BrickConfig.from(configs); - Logger.info('Config retrieved.'); - - brickConfig?.checkFormat(); - - // Define paths - final sourcePath = path.join( - projectPath, - brickConfig!.pathToBrickify, - ); - final cfgTarget = path.join( - _bricks, - brickConfig.name, - _cfgFileName, - ); - final targetPath = path.join( - _bricks, - brickConfig.name, - _brickFolderLabel, - ); - Logger.info('Path defined.'); - - // Remove previously generated files - final targetDir = Directory(targetPath); - if (targetDir.existsSync()) { - await targetDir.delete(recursive: true); - } - - final cfgTargetFile = File(cfgTarget); - if (cfgTargetFile.existsSync()) { - await cfgTargetFile.delete(); - } - - // create target folder - await Shell.mkdir(targetPath); - - // Copy project files - await Shell.cp(sourcePath, targetPath); - Logger.info('Files copied.'); - - // Remove ignored folders - final brickPath = path.join(targetPath, brickConfig.pathToBrickify); - await FileSystemUtils.deleteIgnoredFolders(brickConfig, brickPath); - - // Convert values to variables - await FileSystemUtils.convertValuesToVariablesInFolder( - brickConfig, - targetPath, - ); - Logger.info('Values converted into variables.'); - - // Rename files and folders - await FileSystemUtils.renamePathsInFolder(brickConfig, targetPath); - Logger.info('Folders and files renamed.'); - - // Create config file - cfgTargetFile.writeAsStringSync(brickConfig.toBrickYaml()); - Logger.info('brick.yml added.'); - - await FileSystemUtils.deleteEmptyFolders(targetPath); - Logger.info('Empty folders removed'); - Logger.success('Brick template available at $targetPath'); - } catch (e) { - Logger.error(e); - } -} diff --git a/tools/brick_generator/bin/brickgen.dart b/tools/brick_generator/bin/brickgen.dart index fc162b7..80bd337 100644 --- a/tools/brick_generator/bin/brickgen.dart +++ b/tools/brick_generator/bin/brickgen.dart @@ -26,6 +26,7 @@ import 'package:path/path.dart'; const _configurationFileName = 'brickgen.yaml'; const _masonConfigurationFileName = 'brick.yaml'; const _brickFolderName = '__brick__'; +const _hooksFolderName = 'hooks'; const _helpOption = 'help'; const _verboseOption = 'verbose'; @@ -37,10 +38,12 @@ class Brickgen { final bool deleteEmptyFolders; late String configPath; + late String hooksInputPath; String? toBrickifyPath; String? masonConfigPath; String? targetPath; + String? hooksOutputPath; Brickgen({ required this.brickPath, @@ -48,6 +51,7 @@ class Brickgen { required this.deleteEmptyFolders, }) { configPath = join(brickPath, _configurationFileName); + hooksInputPath = join(brickPath, _hooksFolderName); } Future run() async { @@ -70,6 +74,13 @@ class Brickgen { ); Logger.debug('Define `masonConfigPath`: $masonConfigPath'); + hooksOutputPath = FileSystem.joinRecreate( + outputPath, + config.name, + _hooksFolderName, + ); + Logger.debug('Define `hooksOutputPath`: $hooksOutputPath'); + targetPath = FileSystem.joinRecreate( outputPath, config.name, @@ -78,7 +89,8 @@ class Brickgen { Logger.debug('Define `targetPath`: $targetPath'); // Check paths - if (toBrickifyPath == null || targetPath == null) { + if (toBrickifyPath == null || targetPath == null + || masonConfigPath == null || hooksOutputPath == null) { throw Exception('An error occures during path definition.'); } Logger.info('Paths defined.'); @@ -90,6 +102,11 @@ class Brickgen { ignoreList: config.brickgenConfig.ignore, ); Logger.info('Project copied.'); + + // Take care of boolean mapping and boolean fs + FileSystem.applyBooleanFileSystem(config, targetPath!); + Logger.info('Boolean file system applied.'); + // TODO(wyatt): generate hook to handle boolean mapping. // Convert compilable values -> variables (in files) FileSystem.convertCompilableVariablesInFolder(config, targetPath!); @@ -109,17 +126,16 @@ class Brickgen { FileSystem.renameCompilableFilesInFolder(config, targetPath!); Logger.info('Files renamed with variables.'); - // Take care of boolean mapping and boolean fs // Create Mason config FileSystem.writeFile(masonConfigPath!, config.toMason()); Logger.info('Mason `brick.yaml` generated.'); - // Create gitkeep hook - - // Copy custom hooks + // Copy hooks + await FileSystem.copyHooks(config, hooksInputPath, hooksOutputPath!); // Success! + Logger.success('Brick template available at $targetPath'); } } diff --git a/tools/brick_generator/lib/core/file_system.dart b/tools/brick_generator/lib/core/file_system.dart index 60e77fd..11d43b6 100644 --- a/tools/brick_generator/lib/core/file_system.dart +++ b/tools/brick_generator/lib/core/file_system.dart @@ -341,7 +341,7 @@ abstract class FileSystem { if (toReplace != null && file.path.contains(toReplace)) { final newPath = file.path.replaceAll( toReplace, - '{{${variable.name}.${syntax.id}}}', + '{{${variable.name}.${syntax.id}()}}', ); File(newPath).createSync(recursive: true); @@ -354,4 +354,124 @@ abstract class FileSystem { }); }); } + + static Future copyHooks( + BrickConfig config, + String source, + String targetPath, + ) async { + if (!config.brickgenConfig.hooks) { + Logger.debug('Ignore hooks'); + return; + } + + if (!FileSystemEntity.isDirectorySync(source)) { + throw ArgumentError('Source must be a directory', 'source'); + } + + if (!FileSystemEntity.isDirectorySync(targetPath)) { + throw ArgumentError('Target must be a directory', 'targetPath'); + } + + await copyFolder(source, targetPath); + Logger.info('Hooks copied.'); + } + + /// Apply boolean file system: create or not folders/files depending of + /// a boolean variable. + /// + /// Example + /// ```yaml + /// boolean_file_system: + /// flutter: + /// folders: + /// on_true: + /// - android + /// - ios + /// - assets + /// on_false: + /// - bin + /// files: + /// on_true: + /// - l10n.yaml + /// - trapeze.yaml + /// ``` + /// + /// Creates some folders and files depending of `flutter` variable. + static void applyBooleanFileSystem( + BrickConfig config, + String targetPath, + ) { + for (final variable in config.brickgenConfig.booleanFileSystem) { + final boolean = variable.booleanName; + + String rename( + String boolean, + FileSystemEntity entity, { + bool? onValue, + }) { + final entityPath = entity.path; + final entityPathSplitted = + (entityPath.split('/')..removeWhere((element) => element.isEmpty)); + final entityRenamed = + '{{${(onValue ?? true) ? "#" : "^"}$boolean}}${entityPathSplitted.last}{{/$boolean}}'; + final entityNewPath = + ((entityPathSplitted..removeLast())..add(entityRenamed)).join('/'); + + Logger.debug('Apply boolean file system on $entityPath'); + + return entityNewPath; + } + + // Rename folders + for (final folder in variable.folders.onTrueNames) { + final directory = Directory(join(targetPath, folder)); + final newPath = rename( + boolean, + directory, + onValue: true, + ); + + Directory(newPath).createSync(recursive: true); + directory.renameSync(newPath); + } + + for (final folder in variable.folders.onFalseNames) { + final directory = Directory(join(targetPath, folder)); + final newPath = rename( + boolean, + directory, + onValue: false, + ); + + Directory(newPath).createSync(recursive: true); + directory.renameSync(newPath); + } + + // Rename files + for (final path in variable.files.onTrueNames) { + final file = File(join(targetPath, path)); + final newPath = rename( + boolean, + file, + onValue: true, + ); + + File(newPath).createSync(recursive: true); + file.renameSync(newPath); + } + + for (final path in variable.files.onFalseNames) { + final file = File(join(targetPath, path)); + final newPath = rename( + boolean, + file, + onValue: false, + ); + + File(newPath).createSync(recursive: true); + file.renameSync(newPath); + } + } + } } diff --git a/tools/brick_generator/lib/yaml_reader.dart b/tools/brick_generator/lib/core/reader.dart similarity index 88% rename from tools/brick_generator/lib/yaml_reader.dart rename to tools/brick_generator/lib/core/reader.dart index b154f02..a91d5a6 100644 --- a/tools/brick_generator/lib/yaml_reader.dart +++ b/tools/brick_generator/lib/core/reader.dart @@ -1,23 +1,26 @@ // Copyright (C) 2023 WYATT GROUP // Please see the AUTHORS file for details. -// +// // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // any later version. -// +// // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. -// +// // You should have received a copy of the GNU General Public License // along with this program. If not, see . import 'dart:io'; + import 'package:yaml/yaml.dart'; -class YamlReader { - static YamlMap readYamlFile(String path) => +abstract class Reader {} + +class YamlReader extends Reader { + static YamlMap read(String path) => loadYaml(File(path).readAsStringSync()) as YamlMap; } diff --git a/tools/brick_generator/lib/file_system_utils.dart b/tools/brick_generator/lib/file_system_utils.dart deleted file mode 100644 index b499f55..0000000 --- a/tools/brick_generator/lib/file_system_utils.dart +++ /dev/null @@ -1,136 +0,0 @@ -// Copyright (C) 2023 WYATT GROUP -// Please see the AUTHORS file for details. -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with this program. If not, see . - -import 'dart:io'; - -import 'package:brick_generator/core/logger.dart'; -import 'package:brick_generator/core/shell.dart'; -import 'package:brick_generator/models/brick_config.dart'; -import 'package:brick_generator/models/variable_string_syntax.dart'; -import 'package:brick_generator/models/variable_type.dart'; -import 'package:path/path.dart'; - -class FileSystemUtils { - static Future convertValuesToVariablesInFolder( - BrickConfig brickConfig, - String path, - ) async { - await Future.wait( - Directory(path) - .listSync(recursive: true) - .whereType() - .map((f) async { - var file = f; - - try { - var contents = await file.readAsString(); - - // Transform all values in variables - if (brickConfig.variables != null) { - for (final variable in brickConfig.variables!) { - // Replace all string variables - if (variable?.type == VariableType.string) { - for (final syntax in VariableStringSyntax.values) { - final toReplace = variable?.syntax?[syntax.mapKey]; - if (toReplace != null) { - contents = contents.replaceAll( - toReplace, - '{{#${syntax.id}}}{{${variable?.name}}}{{/${syntax.id}}}', - ); - } - } - } - Logger.debug( - 'Variable `${variable?.name}` replaced in ${file.path}', - ); - } - } - - // Replace content - file = await file.writeAsString(contents); - } catch (e) { - Logger.error(e); - } - }), - ); - } - - static Future renamePathsInFolder( - BrickConfig brickConfig, - String path, - ) async { - await Future.wait( - Directory(path) - .listSync(recursive: true) - .whereType() - .map((_) async { - final file = _; - - // Rename file if needed - if (brickConfig.variables != null) { - for (final variable in brickConfig.variables!) { - if (variable?.type == VariableType.string && - variable?.syntax?[VariableStringSyntax.snakeCase.mapKey] != - null) { - final snake = - variable!.syntax![VariableStringSyntax.snakeCase.mapKey]; - if (snake == null) { - const err = 'Invalid snake_case syntax'; - Logger.throwError(ArgumentError(err), err); - } - final newPath = file.path.replaceAll( - snake!, - '{{${variable.name}.snakeCase()}}', - ); - - await File(newPath).create(recursive: true); - await file.rename(newPath); - Logger.debug( - '${file.path} renamed with variable `${variable.name}`', - ); - } - } - } - }), - ); - } - - static Future deleteEmptyFolders(String path) async { - for (final dir in Directory(path).listSync(recursive: true).reversed) { - if (dir is Directory) { - if (dir.existsSync() && await dir.list().isEmpty) { - await dir.delete(recursive: true); - } - } - } - } - - // For the moment ignored files are copied, then deleted... - static Future deleteIgnoredFolders( - BrickConfig brickConfig, - String brickPath, - ) async { - for (final String ignore in brickConfig.brickgenConfig.ignore.ignored) { - final String toDelete = join(brickPath, ignore); - if (FileSystemEntity.isDirectorySync(toDelete)) { - await Shell.rm(toDelete, recursive: true); - } else { - await Shell.rm(toDelete); - } - Logger.debug('$toDelete ignored.'); - } - } -} diff --git a/tools/brick_generator/lib/models/brick_config.dart b/tools/brick_generator/lib/models/brick_config.dart index a514dcf..6711a93 100644 --- a/tools/brick_generator/lib/models/brick_config.dart +++ b/tools/brick_generator/lib/models/brick_config.dart @@ -14,9 +14,9 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . +import 'package:brick_generator/core/reader.dart'; import 'package:brick_generator/models/brick_variable.dart'; import 'package:brick_generator/models/brickgen_config.dart'; -import 'package:brick_generator/yaml_reader.dart'; import 'package:yaml/yaml.dart'; const _nameKey = 'name'; @@ -62,7 +62,7 @@ class BrickConfig { throw ArgumentError.notNull('path'); } - final config = YamlReader.readYamlFile(path); + final config = YamlReader.read(path); return BrickConfig.parse(config); }