From a3648209b9145061b3b46f77bc14b5ab7409995b Mon Sep 17 00:00:00 2001 From: Sergey Penkovsky Date: Tue, 15 Jul 2025 12:07:23 +0300 Subject: [PATCH] feat(generator): support output_dir and build_extensions config for generated files Now the code generator supports specifying a custom output directory and extension/name template for generated DI files via build.yaml ( and ). This allows placing all generated code in custom folders and using flexible naming schemes. docs: update all user docs and tutorials to explain new output_dir/build_extensions config - Added detailed usage and YAML examples to cherrypick_generator/README.md - Synced full_tutorial_en.md and full_tutorial_ru.md (advanced codegen section) with explanation of new configuration and impact on imports - Updated quick_start_en.md and quick_start_ru.md to mention advanced customization and point to tutorials - Added troubleshooting and tips for custom output/imports in docs --- .gitignore | 2 +- cherrypick_generator/README.md | 64 ++++++++++++++++ cherrypick_generator/build.yaml | 20 +++-- .../lib/cherrypick_custom_builders.dart | 76 +++++++++++++++++++ .../lib/inject_generator.dart | 3 +- .../lib/module_generator.dart | 8 +- cherrypick_generator/pubspec.yaml | 1 + doc/full_tutorial_en.md | 39 ++++++++++ doc/full_tutorial_ru.md | 39 ++++++++++ doc/quick_start_en.md | 24 +++++- doc/quick_start_ru.md | 24 +++++- examples/postly/build.yaml | 27 +++++++ examples/postly/lib/app.dart | 2 +- examples/postly/lib/di/app_module.dart | 2 +- 14 files changed, 311 insertions(+), 20 deletions(-) create mode 100644 cherrypick_generator/lib/cherrypick_custom_builders.dart create mode 100644 examples/postly/build.yaml diff --git a/.gitignore b/.gitignore index 65de10d..36c2da2 100644 --- a/.gitignore +++ b/.gitignore @@ -7,7 +7,7 @@ .idea/ .vscode/ - +**/generated **/*.g.dart **/*.gr.dart **/*.freezed.dart diff --git a/cherrypick_generator/README.md b/cherrypick_generator/README.md index 4522fe4..68d9b9f 100644 --- a/cherrypick_generator/README.md +++ b/cherrypick_generator/README.md @@ -4,6 +4,50 @@ --- +### Advanced: Customizing Generated File Paths (`build_extensions`) + +You can further control the filenames and subdirectory structure of generated files using the `build_extensions` option in `build.yaml`. This is especially useful in large apps for keeping DI artifacts organized under `lib/generated/` or any custom location. + +**Example advanced build.yaml:** + +```yaml +targets: + $default: + builders: + cherrypick_generator|inject_generator: + options: + build_extensions: + '^lib/app.dart': ['lib/generated/app.inject.cherrypick.g.dart'] + output_dir: lib/generated + generate_for: + - lib/**.dart + cherrypick_generator|module_generator: + options: + build_extensions: + '^lib/di/{{}}.dart': ['lib/generated/di/{{}}.module.cherrypick.g.dart'] + output_dir: lib/generated + generate_for: + - lib/**.dart +``` + +- **output_dir**: Path where all generated files are placed (e.g., `lib/generated`) +- **build_extensions**: Allows templating of generated filenames and locations. You can use wildcards like `{{}}` to keep directory structure or group related files. + +**If you use these options, be sure to update your imports accordingly, for example:** + +```dart +import 'package:your_package/generated/app.inject.cherrypick.g.dart'; +import 'package:your_package/generated/di/app_module.module.cherrypick.g.dart'; +``` + +### FAQ / Troubleshooting + +- If files are missing or located in unexpected directories, double-check your `output_dir` and `build_extensions` configuration. +- If you change generation paths, always update your imports in the codebase. +- These options are backward compatible: omitting them preserves pre-existing (side-by-source) output behavior. + +--- + ## Features - **Automatic Field Injection:** @@ -170,6 +214,26 @@ final class $MyModule extends MyModule { ## Advanced Usage +### Custom output directory for generated code (output_dir) + +You can control the directory where the generated files (`*.inject.cherrypick.g.dart`, `*.module.cherrypick.g.dart`) are placed using the `output_dir` option in your `build.yaml`: + +```yaml +targets: + $default: + builders: + cherrypick_generator|injectBuilder: + options: + output_dir: lib/generated + cherrypick_generator|moduleBuilder: + options: + output_dir: lib/generated +``` + +**If `output_dir` is omitted, generated files are placed next to the original sources (default behavior).** + +After running code generation, you will find files like `lib/generated/app.inject.cherrypick.g.dart` and `lib/generated/your_module.module.cherrypick.g.dart`. You can import them as needed from that directory. + - **Combining Modules and Field Injection:** It's possible to mix both style of DI — modules for binding, and field injection for consuming services. - **Parameter and Named Injection:** diff --git a/cherrypick_generator/build.yaml b/cherrypick_generator/build.yaml index 0a1e0b6..46a628d 100644 --- a/cherrypick_generator/build.yaml +++ b/cherrypick_generator/build.yaml @@ -1,20 +1,18 @@ builders: - module_generator: - import: "package:cherrypick_generator/module_generator.dart" - builder_factories: ["moduleBuilder"] - build_extensions: {".dart": [".module.cherrypick.g.dart"]} - auto_apply: dependents - required_inputs: ["lib/**"] - runs_before: [] - build_to: source inject_generator: import: "package:cherrypick_generator/inject_generator.dart" builder_factories: ["injectBuilder"] build_extensions: {".dart": [".inject.cherrypick.g.dart"]} auto_apply: dependents - required_inputs: ["lib/**"] - runs_before: [] build_to: source + applies_builders: ["source_gen|combining_builder"] + module_generator: + import: "package:cherrypick_generator/module_generator.dart" + builder_factories: ["moduleBuilder"] + build_extensions: {".dart": [".module.cherrypick.g.dart"]} + auto_apply: dependents + build_to: source + applies_builders: ["source_gen|combining_builder"] targets: $default: @@ -24,4 +22,4 @@ targets: - lib/**.dart cherrypick_generator|inject_generator: generate_for: - - lib/**.dart \ No newline at end of file + - lib/**.dart diff --git a/cherrypick_generator/lib/cherrypick_custom_builders.dart b/cherrypick_generator/lib/cherrypick_custom_builders.dart new file mode 100644 index 0000000..db2b398 --- /dev/null +++ b/cherrypick_generator/lib/cherrypick_custom_builders.dart @@ -0,0 +1,76 @@ +import 'dart:async'; +import 'package:build/build.dart'; +import 'package:path/path.dart' as p; +import 'package:source_gen/source_gen.dart'; +import 'inject_generator.dart'; +import 'module_generator.dart'; + +/// Универсальный Builder для генераторов Cherrypick с поддержкой кастомного output_dir +/// (указывает директорию для складывания сгенерированных файлов через build.yaml) +class CustomOutputBuilder extends Builder { + final Generator generator; + final String extension; + final String outputDir; + final Map> customBuildExtensions; + + CustomOutputBuilder(this.generator, this.extension, this.outputDir, this.customBuildExtensions); + + @override + Map> get buildExtensions { + if (customBuildExtensions.isNotEmpty) { + return customBuildExtensions; + } + // Дефолт: рядом с исходником, как PartBuilder + return { + '.dart': [extension], + }; + } + + @override + Future build(BuildStep buildStep) async { + final inputId = buildStep.inputId; + print('[CustomOutputBuilder] build() called for input: \\${inputId.path}'); + final library = await buildStep.resolver.libraryFor(inputId); + print('[CustomOutputBuilder] resolved library for: \\${inputId.path}'); + final generated = await generator.generate(LibraryReader(library), buildStep); + print('[CustomOutputBuilder] gen result for input: \\${inputId.path}, isNull: \\${generated == null}, isEmpty: \\${generated?.isEmpty}'); + if (generated == null || generated.isEmpty) return; + String outputPath; + if (customBuildExtensions.isNotEmpty) { + // Кастомная директория/шаблон + final inputPath = inputId.path; + final relativeInput = p.relative(inputPath, from: 'lib/'); + final parts = p.split(relativeInput); + String subdir = ''; + String baseName = parts.last.replaceAll('.dart', ''); + if (parts.length > 1) { + subdir = parts.first; // Например, 'di' + } + outputPath = subdir.isEmpty + ? p.join('lib', 'generated', '$baseName$extension') + : p.join('lib', 'generated', subdir, '$baseName$extension'); + } else { + // Дефолт: рядом с исходником + outputPath = p.setExtension(inputId.path, extension); + } + final outputId = AssetId(inputId.package, outputPath); + // part of - всегда авто! + final partOfPath = p.relative(inputId.path, from: p.dirname(outputPath)); + final codeWithPartOf = "part of '$partOfPath';\n\n$generated"; + print('[CustomOutputBuilder] writing to output: \\${outputId.path}'); + await buildStep.writeAsString(outputId, codeWithPartOf); + print('[CustomOutputBuilder] successfully written for input: \\${inputId.path}'); + } +} + +Builder injectCustomBuilder(BuilderOptions options) { + final outputDir = options.config['output_dir'] as String? ?? ''; + final buildExtensions = (options.config['build_extensions'] as Map?)?.map((k,v)=>MapEntry(k.toString(), (v as List).map((item)=>item.toString()).toList())) ?? {}; + return CustomOutputBuilder(InjectGenerator(), '.inject.cherrypick.g.dart', outputDir, buildExtensions); +} + +Builder moduleCustomBuilder(BuilderOptions options) { + final outputDir = options.config['output_dir'] as String? ?? ''; + final buildExtensions = (options.config['build_extensions'] as Map?)?.map((k,v)=>MapEntry(k.toString(), (v as List).map((item)=>item.toString()).toList())) ?? {}; + return CustomOutputBuilder(ModuleGenerator(), '.module.cherrypick.g.dart', outputDir, buildExtensions); +} diff --git a/cherrypick_generator/lib/inject_generator.dart b/cherrypick_generator/lib/inject_generator.dart index afcf1d3..ce20105 100644 --- a/cherrypick_generator/lib/inject_generator.dart +++ b/cherrypick_generator/lib/inject_generator.dart @@ -19,6 +19,7 @@ import 'package:build/build.dart'; import 'package:source_gen/source_gen.dart'; import 'package:analyzer/dart/element/element.dart'; import 'package:cherrypick_annotations/cherrypick_annotations.dart' as ann; +import 'cherrypick_custom_builders.dart' as custom; /// InjectGenerator generates a mixin for a class marked with @injectable() /// and injects all fields annotated with @inject(), using CherryPick DI. @@ -204,4 +205,4 @@ class _ParsedInjectField { /// /// Фабрика билдера. Используется build_runner. Builder injectBuilder(BuilderOptions options) => - PartBuilder([InjectGenerator()], '.inject.cherrypick.g.dart'); + custom.injectCustomBuilder(options); diff --git a/cherrypick_generator/lib/module_generator.dart b/cherrypick_generator/lib/module_generator.dart index 54bcc3b..9e1bb88 100644 --- a/cherrypick_generator/lib/module_generator.dart +++ b/cherrypick_generator/lib/module_generator.dart @@ -15,9 +15,8 @@ import 'package:analyzer/dart/element/element.dart'; import 'package:build/build.dart'; import 'package:source_gen/source_gen.dart'; import 'package:cherrypick_annotations/cherrypick_annotations.dart' as ann; - import 'src/generated_class.dart'; - +import 'cherrypick_custom_builders.dart' as custom; /// --------------------------------------------------------------------------- /// ModuleGenerator for code generation of dependency-injected modules. /// @@ -89,5 +88,8 @@ class ModuleGenerator extends GeneratorForAnnotation { /// Возвращает Builder, используемый build_runner для генерации кода для всех /// файлов, где встречается @module(). /// --------------------------------------------------------------------------- + + + Builder moduleBuilder(BuilderOptions options) => - PartBuilder([ModuleGenerator()], '.module.cherrypick.g.dart'); + custom.moduleCustomBuilder(options); \ No newline at end of file diff --git a/cherrypick_generator/pubspec.yaml b/cherrypick_generator/pubspec.yaml index fb359ce..2236603 100644 --- a/cherrypick_generator/pubspec.yaml +++ b/cherrypick_generator/pubspec.yaml @@ -18,6 +18,7 @@ dependencies: build: ^2.4.1 source_gen: ^2.0.0 collection: ^1.18.0 + path: ^1.9.1 dev_dependencies: lints: ^4.0.0 diff --git a/doc/full_tutorial_en.md b/doc/full_tutorial_en.md index 7b2e4b6..a3d6a4f 100644 --- a/doc/full_tutorial_en.md +++ b/doc/full_tutorial_en.md @@ -379,6 +379,45 @@ You can use CherryPick in Dart CLI, server apps, and microservices. All major fe --- +### Advanced: Customizing Generated Code Location + +CherryPick's code generator now supports flexible output configuration via `build.yaml`. + +You can control both the output directory (using `output_dir`) and filename templates (using `build_extensions`): + +```yaml +targets: + $default: + builders: + cherrypick_generator|inject_generator: + options: + build_extensions: + '^lib/app.dart': ['lib/generated/app.inject.cherrypick.g.dart'] + output_dir: lib/generated + generate_for: + - lib/**.dart + cherrypick_generator|module_generator: + options: + build_extensions: + '^lib/di/{{}}.dart': ['lib/generated/di/{{}}.module.cherrypick.g.dart'] + output_dir: lib/generated + generate_for: + - lib/**.dart +``` + +- **output_dir**: Folder where all generated files will be placed. +- **build_extensions**: Allows full customization of generated file names and subfolders. + +If you use these, be sure to update your imports accordingly, e.g.: +```dart +import 'package:your_project/generated/app.inject.cherrypick.g.dart'; +``` +If not specified, generated files will appear next to your source files, as before. + +--- + +--- + ## Conclusion **CherryPick** is a modern DI solution for Dart and Flutter, combining a concise API and advanced annotation/codegen features. Scopes, parameterized providers, named bindings, and field-injection make it great for both small and large-scale projects. diff --git a/doc/full_tutorial_ru.md b/doc/full_tutorial_ru.md index ec06205..3f739be 100644 --- a/doc/full_tutorial_ru.md +++ b/doc/full_tutorial_ru.md @@ -382,6 +382,45 @@ class MyApp extends StatelessWidget { --- +### Продвинутая настройка путей генерации кода + +В последних версиях генератора CherryPick добавлена поддержка гибкой настройки директорий и шаблонов имён файлов через `build.yaml`. + +Вы можете управлять и папкой назначения (через `output_dir`), и шаблоном имён (через `build_extensions`): + +```yaml +targets: + $default: + builders: + cherrypick_generator|inject_generator: + options: + build_extensions: + '^lib/app.dart': ['lib/generated/app.inject.cherrypick.g.dart'] + output_dir: lib/generated + generate_for: + - lib/**.dart + cherrypick_generator|module_generator: + options: + build_extensions: + '^lib/di/{{}}.dart': ['lib/generated/di/{{}}.module.cherrypick.g.dart'] + output_dir: lib/generated + generate_for: + - lib/**.dart +``` + +- **output_dir**: Папка, куда будут складываться все сгенерированные файлы. +- **build_extensions**: Полный контроль над именами итоговых файлов и подпапками. + +Если вы это используете, обязательно обновляйте импорты, например: +```dart +import 'package:your_project/generated/app.inject.cherrypick.g.dart'; +``` +Если не задать параметры, файлы будут сгенерированы рядом с исходными — как и раньше. + +--- + +--- + ## Заключение **CherryPick** — это современное DI-решение для Dart и Flutter, сочетающее лаконичный API и расширенные возможности аннотирования и генерации кода. Гибкость Scopes, параметрические провайдеры, именованные биндинги и field-injection делают его особенно мощным как для небольших, так и для масштабных проектов. diff --git a/doc/quick_start_en.md b/doc/quick_start_en.md index 486ce6c..d8904f1 100644 --- a/doc/quick_start_en.md +++ b/doc/quick_start_en.md @@ -19,7 +19,29 @@ There are two main methods for initializing a custom instance `toInstance ()` an Example: -```dart +``` + +--- + +## Advanced: Customizing Code Generation Output + +You can configure where generated files will be placed by updating your `build.yaml` (supports `output_dir` and `build_extensions`): + +```yaml +targets: + $default: + builders: + cherrypick_generator|inject_generator: + options: + output_dir: lib/generated + cherrypick_generator|module_generator: + options: + output_dir: lib/generated +``` + +For full control and more examples, see the "Full Tutorial" or documentation on `build_extensions`. + +--- // initializing a text string instance through a method toInstance() Binding().toInstance("hello world"); diff --git a/doc/quick_start_ru.md b/doc/quick_start_ru.md index 94301f0..6232253 100644 --- a/doc/quick_start_ru.md +++ b/doc/quick_start_ru.md @@ -19,7 +19,29 @@ Binding - по сути это конфигуратор для пользов Пример: -```dart +``` + +--- + +## Продвинутая настройка генерации кода + +В файле `build.yaml` можно задать папку для сгенерированных файлов через параметр `output_dir` (а также использовать шаблон `build_extensions`): + +```yaml +targets: + $default: + builders: + cherrypick_generator|inject_generator: + options: + output_dir: lib/generated + cherrypick_generator|module_generator: + options: + output_dir: lib/generated +``` + +Для полной настройки и шаблонов см. раздел “Полный гайд” или документацию по `build_extensions`. + +--- // инициализация экземпляра текстовой строки через метод toInstance() Binding().toInstance("hello world"); diff --git a/examples/postly/build.yaml b/examples/postly/build.yaml new file mode 100644 index 0000000..a225cbe --- /dev/null +++ b/examples/postly/build.yaml @@ -0,0 +1,27 @@ +targets: + $default: + builders: + cherrypick_generator|inject_generator: + options: + build_extensions: + '^lib/app.dart': ['lib/generated/app.inject.cherrypick.g.dart'] + output_dir: lib/generated + generate_for: + - lib/**.dart + cherrypick_generator|module_generator: + options: + build_extensions: + '^lib/di/{{}}.dart': ['lib/generated/di/{{}}.module.cherrypick.g.dart'] + output_dir: lib/generated + generate_for: + - lib/**.dart + +#targets: +# $default: +# builders: +# cherrypick_generator|module_generator: +# generate_for: +# - lib/**.dart +# cherrypick_generator|inject_generator: +# generate_for: +# - lib/**.dart \ No newline at end of file diff --git a/examples/postly/lib/app.dart b/examples/postly/lib/app.dart index 260850c..a84144a 100644 --- a/examples/postly/lib/app.dart +++ b/examples/postly/lib/app.dart @@ -7,7 +7,7 @@ import 'domain/repository/post_repository.dart'; import 'presentation/bloc/post_bloc.dart'; import 'router/app_router.dart'; -part 'app.inject.cherrypick.g.dart'; +part 'generated/app.inject.cherrypick.g.dart'; @injectable() class MyApp extends StatelessWidget with _$MyApp { diff --git a/examples/postly/lib/di/app_module.dart b/examples/postly/lib/di/app_module.dart index 27cf10e..871acf0 100644 --- a/examples/postly/lib/di/app_module.dart +++ b/examples/postly/lib/di/app_module.dart @@ -5,7 +5,7 @@ import '../data/network/json_placeholder_api.dart'; import '../data/post_repository_impl.dart'; import '../domain/repository/post_repository.dart'; -part 'app_module.module.cherrypick.g.dart'; +part '../generated/di/app_module.module.cherrypick.g.dart'; @module() abstract class AppModule extends Module {