Files
Sergey Penkovsky a3648209b9 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
2025-07-15 12:07:23 +03:00

8.4 KiB

Cherrypick Generator

Cherrypick Generator is a Dart code generation library for automating dependency injection (DI) boilerplate. It processes classes and fields annotated with cherrypick_annotations and generates registration code for services, modules, and field injection for classes marked as @injectable. It supports advanced DI features such as scopes, named bindings, parameters, and asynchronous dependencies.


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:

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:

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:
    Detects classes annotated with @injectable(), and generates mixins to inject all fields annotated with @inject(), supporting scope and named qualifiers.

  • Module and Service Registration:
    For classes annotated with @module(), generates service registration code for methods using annotations such as @provide, @instance, @singleton, @named, and @params.

  • Scope & Named Qualifier Support:
    Supports advanced DI features:
      • Field-level scoping with @scope('scopename')
      • Named dependencies via @named('value')

  • Synchronous & Asynchronous Support:
    Handles both synchronous and asynchronous services (including Future<T>) for both field injection and module registration.

  • Parameters and Runtime Arguments:
    Recognizes and wires both injected dependencies and runtime parameters using @params.

  • Error Handling:
    Validates annotations at generation time. Provides helpful errors for incorrect usage (e.g., using @injectable on non-class elements).


How It Works

1. Annotate your code

Use annotations from cherrypick_annotations:

  • @injectable() — on classes to enable field injection
  • @inject() — on fields to specify they should be injected
  • @scope(), @named() — on fields or parameters for advanced wiring
  • @module() — on classes to mark as DI modules
  • @provide, @instance, @singleton, @params — on methods and parameters for module-based DI

2. Run the generator

Use build_runner to process your code and generate .module.cherrypick.g.dart and .inject.cherrypick.g.dart files.

3. Use the output in your application

  • For modules: Register DI providers using the generated $YourModule class.
  • For services: Enable field injection on classes using the generated mixin.

Field Injection Example

Given the following:

import 'package:cherrypick_annotations/cherrypick_annotations.dart';

@injectable()
class MyWidget with _$MyWidget {
  @inject()
  late final AuthService auth;

  @inject()
  @scope('profile')
  late final ProfileManager manager;

  @inject()
  @named('special')
  late final ApiClient specialApi;
}

The generator will output (simplified):

mixin _$MyWidget {
  void _inject(MyWidget instance) {
    instance.auth = CherryPick.openRootScope().resolve<AuthService>();
    instance.manager = CherryPick.openScope(scopeName: 'profile').resolve<ProfileManager>();
    instance.specialApi = CherryPick.openRootScope().resolve<ApiClient>(named: 'special');
  }
}

You can then mix this into your widget to enable automatic DI at runtime.


Module Registration Example

Given:

import 'package:cherrypick_annotations/cherrypick_annotations.dart';

@module()
class MyModule {
  @singleton
  @instance
  AuthService provideAuth(Api api);

  @provide
  @named('logging')
  Future<Logger> provideLogger(@params Map<String, dynamic> args);
}

The generator will output (simplified):

final class $MyModule extends MyModule {
  @override
  void builder(Scope currentScope) {
    bind<AuthService>()
      .toInstance(provideAuth(currentScope.resolve<Api>()))
      .singleton();

    bind<Logger>()
      .toProvideAsyncWithParams((args) => provideLogger(args))
      .withName('logging');
  }
}

Key Points

  • Rich Annotation Support: Mix and match field, parameter, and method annotations for maximum flexibility.
  • Scope and Named Resolution: Use @scope('...') and @named('...') to precisely control where and how dependencies are wired.
  • Async/Synchronous:
    The generator distinguishes between sync (resolve<T>) and async (resolveAsync<T>) dependencies.
  • Automatic Mixins:
    For classes with @injectable(), a mixin is generated that injects all relevant fields (using constructor or setter).
  • Comprehensive Error Checking:
    Misapplied annotations (e.g., @injectable() on non-class) produce clear build-time errors.

Usage

  1. Add dependencies

    dependencies:
      cherrypick_annotations: ^latest
    
    dev_dependencies:
      cherrypick_generator: ^latest
      build_runner: ^2.1.0
    
  2. Annotate your classes and modules as above

  3. Run the generator

    dart run build_runner build
    # or, if using Flutter:
    flutter pub run build_runner build
    
  4. Use generated code

    • Import the generated .inject.cherrypick.g.dart or .cherrypick.g.dart files where needed

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:

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:
    Use @named on both provider and parameter for named registration and lookup; use @params to pass runtime arguments.
  • Async Factories:
    Methods returning Future generate async bindings and async field resolution logic.

Developer Notes

  • The generator relies on the Dart analyzer, source_gen, and build packages.
  • All classes and methods are parsed for annotations.
  • Improper annotation usage will result in generator errors.

License

Licensed under the Apache License, Version 2.0

Contribution

Pull requests and issues are welcome! Please open GitHub issues or submit improvements.