feat: add boolean file system support

This commit is contained in:
Hugo Pointcheval 2023-01-26 18:57:07 +01:00
parent 18aeef9e35
commit 77e60213f9
Signed by: hugo
GPG Key ID: 3AAC487E131E00BC
8 changed files with 497 additions and 294 deletions

View File

@ -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
* [ ] 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
├── <brick_name>
│ ├── brickgen.yaml
│ ├── hooks
│ │ └── pre_gen.dart
│ ├── <brick_folder>
│ │ └── ... compilable project
```
### Brick (Mason)
```yaml
name: <brick_name>
description: <brick_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: <compilable>
type: string
description: <description>
default: <default>
prompt: <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: <description>
default: <default>
prompt: <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: <brick_folder>
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
├── <brick_name>
│ ├── brickgen.yaml
│ ├── hooks
│ │ └── pre_gen.dart
│ ├── <brick_folder>
│ │ └── ... compilable project
```
Configuration: `brickgen.yaml`
```yaml
name: <brick_name>
description: <brick_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: <brick_folder>
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_name>
│ ├── brick.yaml
│ ├── hooks
│ │ └── pre_gen.dart
│ ├── __brick__
│ │ └── <brick_folder>
```
> 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
```

View File

@ -1,30 +0,0 @@
name: <brick_name>
description: <brick_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 ?

View File

@ -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<void> main(List<String> 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);
}
}

View File

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

View File

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

View File

@ -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 <https://www.gnu.org/licenses/>.
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;
}

View File

@ -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 <https://www.gnu.org/licenses/>.
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<void> convertValuesToVariablesInFolder(
BrickConfig brickConfig,
String path,
) async {
await Future.wait(
Directory(path)
.listSync(recursive: true)
.whereType<File>()
.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<void> renamePathsInFolder(
BrickConfig brickConfig,
String path,
) async {
await Future.wait(
Directory(path)
.listSync(recursive: true)
.whereType<File>()
.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<void> 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<void> 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.');
}
}
}

View File

@ -14,9 +14,9 @@
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
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);
}