singleton_manager_generator 2.3.2
singleton_manager_generator: ^2.3.2 copied to clipboard
CLI tool that generates dependencyInjectionFactory() constructors for @dependencyInjectable classes.
singleton_manager_generator #
A CLI tool that generates a dependencyInjectionFactory() constructor for classes annotated with @dependencyInjectable, writing it directly into the annotated class itself.
Features #
- Scans Dart source files for classes annotated with
@dependencyInjectable - Reads the class's default (unnamed) constructor and generates a matching
factory <ClassName>.dependencyInjectionFactory({String key = 'default', String subkey = 'default'}) - Each constructor parameter is resolved from the
RegistryManager—getInstance<T>(key: key)for non-nullable types,getInstanceNullable<T>(key: key)for nullable ones - Parameters tagged
@Subkey('...')are resolved with that subkey instead of'default', disambiguating parameters that share an interface type - Parameters tagged
@Subkey.inherited()are resolved with the factory's ownsubkeyargument instead of a literal, so a class connected under multiple subkeys can have each variant transitively pull its own matching nested dependencies - Preserves the original parameter shape: positional-required, positional-optional (
[...]), or named (optionallyrequired) - Resolves the type of field-formal parameters (
this._field) with no explicit type from the matching field declaration - Writes the factory right after the default constructor, directly in the source file — no separate
_di.dartfile, and both constructors stay grouped before any field/method (satisfying thesort_constructors_firstlint) - Every generated line carries a trailing
// GENERATED CODE - DO NOT MODIFY BY HANDcomment — not just a header above the block, each line individually - Idempotent: running the generator again fully replaces the previously generated block (factory + trailing members) — whether the constructor's parameters changed shape or the factory had been left in the wrong place, no duplication
- Automatically inserts the
singleton_managerimport if it's missing - Minimal overhead — uses Dart's
analyzerpackage for lightweight AST parsing
Installation #
Add the packages to your pubspec.yaml:
dependencies:
singleton_manager: ^2.2.0
dev_dependencies:
singleton_manager_generator: ^2.2.0
Global Installation #
To install singleton_manager_generator globally and use it across multiple projects:
dart pub global activate singleton_manager_generator
Then use it directly from anywhere:
singleton_manager_generator --input lib
To update the global installation:
dart pub global activate singleton_manager_generator
To deactivate the global installation:
dart pub global deactivate singleton_manager_generator
Usage #
1. Annotate your class #
import 'package:singleton_manager/singleton_manager.dart';
abstract class IPluto {}
abstract class IPaperino {}
abstract class ITopolino {}
@dependencyInjectable
class Pippo {
IPluto _pluto;
IPaperino? _paperino;
ITopolino? _topolino;
Pippo(
this._pluto, {
IPaperino? paperino,
ITopolino? topolino,
}) {
_paperino = paperino;
_topolino = topolino;
}
}
2. Run the generator #
dart run singleton_manager_generator --input lib
Or use melos if set up in your workspace:
melos run generate
3. The generated factory is written directly into your class #
@dependencyInjectable
class Pippo {
IPluto _pluto;
IPaperino? _paperino;
ITopolino? _topolino;
Pippo(
this._pluto, {
IPaperino? paperino,
ITopolino? topolino,
}) {
_paperino = paperino;
_topolino = topolino;
}
factory Pippo.dependencyInjectionFactory({String key = 'default', String subkey = 'default'}) { // GENERATED CODE - DO NOT MODIFY BY HAND
final pluto = RegistryManager.instance.getInstance<IPluto>(key: key); // GENERATED CODE - DO NOT MODIFY BY HAND
final paperino = RegistryManager.instance.getInstanceNullable<IPaperino>(key: key); // GENERATED CODE - DO NOT MODIFY BY HAND
final topolino = RegistryManager.instance.getInstanceNullable<ITopolino>(key: key); // GENERATED CODE - DO NOT MODIFY BY HAND
return Pippo( // GENERATED CODE - DO NOT MODIFY BY HAND
pluto, // GENERATED CODE - DO NOT MODIFY BY HAND
paperino: paperino, // GENERATED CODE - DO NOT MODIFY BY HAND
topolino: topolino, // GENERATED CODE - DO NOT MODIFY BY HAND
); // GENERATED CODE - DO NOT MODIFY BY HAND
} // GENERATED CODE - DO NOT MODIFY BY HAND
}
The factory is always inserted (or, on a re-run, fully replaced) right
after the default constructor — before any field or method declared after
it, which get shifted down right after the factory, unchanged. Every
non-blank generated line carries the trailing // GENERATED CODE - DO NOT MODIFY BY HAND marker shown above.
Use it once the dependencies are connected:
final registry = RegistryManager.instance;
registry.connectInstance<IPluto, Pluto>(Pluto.dependencyInjectionFactory);
registry.connectInstance<IPaperino, Paperino>(Paperino.dependencyInjectionFactory);
registry.connectInstance<ITopolino, Topolino>(Topolino.dependencyInjectionFactory);
final pippo = Pippo.dependencyInjectionFactory();
How parameters map to the factory #
| Constructor parameter shape | Generated factory behavior |
|---|---|
Non-nullable type (IPluto _pluto) |
RegistryManager.instance.getInstance<IPluto>(key: key) |
Nullable type (IPaperino? paperino) |
RegistryManager.instance.getInstanceNullable<IPaperino>(key: key) |
Tagged @Subkey('...') |
Adds subkey: '...' to the getInstance/getInstanceNullable call |
Tagged @Subkey.inherited() |
Adds subkey: subkey — forwards the factory's own subkey argument instead of a literal |
| Positional-required | Passed positionally |
Positional-optional ([...]) |
Passed positionally |
Named ({...}), optionally required |
Passed by name: name: localVar |
Field-formal param with no explicit type (this._field) |
Type looked up from the matching field declaration |
{String key = 'default', String subkey = 'default'} is always added to the
generated factory's own signature, and key is threaded through to every
getInstance/getInstanceNullable call inside it — so
MyClass.dependencyInjectionFactory(key: 'prod') resolves the whole
dependency subtree under 'prod'. subkey is only read inside the body for
parameters tagged @Subkey.inherited(); a class connected multiple times
under different subkeys (via a hand-written connectInstance call per
subkey — the generator can't infer this) can pass that same subkey into its
own factory call, so @Subkey.inherited() parameters transitively follow it.
See the "Propagating a subkey through nested dependencies"
section of the singleton_manager README for the full pattern.
CLI Options #
--input, -i Input directory containing source Dart files (default: lib)
--output, -o Output directory to write the rewritten files to (default: same as input — in-place rewrite)
--registry-output, -r Write a .dart file at this path that registers every discovered singleton into RegistryManager (skipped if omitted)
--verbose, -v Enable verbose logging
--help, -h Show help message
By default (no --output, or --output equal to --input), the generator rewrites the actual source files in place.
Generating a registry file #
Pass --registry-output to also generate a .dart file that wires every discovered @dependencyInjectable class into RegistryManager, so you don't have to hand-write the connectInstance calls:
dart run singleton_manager_generator --input lib --registry-output lib/singleton_registry.g.dart
<ProjectName> (PascalCase) is auto-detected from the nearest pubspec.yaml's
name: field, walking up from --input — e.g. my_app becomes MyApp. For
the Pippo/IPluto/IPaperino/ITopolino example above, in a package named
my_app, this produces:
// GENERATED CODE - DO NOT MODIFY BY HAND
import 'package:singleton_manager/singleton_manager.dart'; // GENERATED CODE - DO NOT MODIFY BY HAND
import 'paperino.dart'; // GENERATED CODE - DO NOT MODIFY BY HAND
import 'pippo.dart'; // GENERATED CODE - DO NOT MODIFY BY HAND
import 'pluto.dart'; // GENERATED CODE - DO NOT MODIFY BY HAND
import 'topolino.dart'; // GENERATED CODE - DO NOT MODIFY BY HAND
/// ... doc comment ...
mixin MainInjectionMyAppMixin { // GENERATED CODE - DO NOT MODIFY BY HAND
/// Called by [registerAllSingletonsMyApp] right before it connects anything.
/// Override to customize. // GENERATED CODE - DO NOT MODIFY BY HAND
void beforeRegisterAllSingletonsMyApp({String key = 'default'}) {} // GENERATED CODE - DO NOT MODIFY BY HAND
/// Connects every discovered singleton under [key]. // GENERATED CODE - DO NOT MODIFY BY HAND
void registerAllSingletonsMyApp({String key = 'default'}) { // GENERATED CODE - DO NOT MODIFY BY HAND
beforeRegisterAllSingletonsMyApp(key: key); // GENERATED CODE - DO NOT MODIFY BY HAND
RegistryManager.instance // GENERATED CODE - DO NOT MODIFY BY HAND
..connectInstance<IPluto, Pluto>(() => Pluto.dependencyInjectionFactory(key: key), key: key) // GENERATED CODE - DO NOT MODIFY BY HAND
..connectInstance<IPaperino, Paperino>(() => Paperino.dependencyInjectionFactory(key: key), key: key) // GENERATED CODE - DO NOT MODIFY BY HAND
..connectInstance<ITopolino, Topolino>(() => Topolino.dependencyInjectionFactory(key: key), key: key) // GENERATED CODE - DO NOT MODIFY BY HAND
..connectInstance<Pippo, Pippo>(() => Pippo.dependencyInjectionFactory(key: key), key: key); // GENERATED CODE - DO NOT MODIFY BY HAND
afterRegisterAllSingletonsMyApp(key: key); // GENERATED CODE - DO NOT MODIFY BY HAND
} // GENERATED CODE - DO NOT MODIFY BY HAND
/// Called by [registerAllSingletonsMyApp] right after it finishes connecting
/// everything. Override to customize. // GENERATED CODE - DO NOT MODIFY BY HAND
void afterRegisterAllSingletonsMyApp({String key = 'default'}) {} // GENERATED CODE - DO NOT MODIFY BY HAND
/// Called by [registerAllSingletonsMyAppAsync] right before it connects anything.
/// Override to customize. // GENERATED CODE - DO NOT MODIFY BY HAND
Future<void> beforeRegisterAllSingletonsMyAppAsync({String key = 'default'}) async {} // GENERATED CODE - DO NOT MODIFY BY HAND
/// Async twin of [registerAllSingletonsMyApp] — use this when [beforeRegisterAllSingletonsMyAppAsync]
/// or [afterRegisterAllSingletonsMyAppAsync] need to await work (e.g. loading remote
/// config) before or after connecting. // GENERATED CODE - DO NOT MODIFY BY HAND
Future<void> registerAllSingletonsMyAppAsync({String key = 'default'}) async { // GENERATED CODE - DO NOT MODIFY BY HAND
await beforeRegisterAllSingletonsMyAppAsync(key: key); // GENERATED CODE - DO NOT MODIFY BY HAND
registerAllSingletonsMyApp(key: key); // GENERATED CODE - DO NOT MODIFY BY HAND
await afterRegisterAllSingletonsMyAppAsync(key: key); // GENERATED CODE - DO NOT MODIFY BY HAND
} // GENERATED CODE - DO NOT MODIFY BY HAND
/// Called by [registerAllSingletonsMyAppAsync] right after it finishes connecting
/// everything. Override to customize. // GENERATED CODE - DO NOT MODIFY BY HAND
Future<void> afterRegisterAllSingletonsMyAppAsync({String key = 'default'}) async {} // GENERATED CODE - DO NOT MODIFY BY HAND
} // GENERATED CODE - DO NOT MODIFY BY HAND
class MainInjectionMyApp with MainInjectionMyAppMixin { // GENERATED CODE - DO NOT MODIFY BY HAND
const MainInjectionMyApp(); // GENERATED CODE - DO NOT MODIFY BY HAND
} // GENERATED CODE - DO NOT MODIFY BY HAND
(imports are sorted alphabetically and deduped; when no classes are
discovered, registerAllSingletons<ProjectName>'s body is just a // No @dependencyInjectable classes were discovered. comment — no unused variable
is emitted.)
Call const MainInjectionMyApp().registerAllSingletonsMyApp() once at
startup instead of connecting each type by hand. Every method — before-hook,
register, after-hook — is a regular, overridable instance method: subclass
MainInjectionMyApp (or mix MainInjectionMyAppMixin into your own class)
to hook into the registration lifecycle, e.g. to hand-wire a dependency the
generator can't discover on its own (a plain class that isn't
@dependencyInjectable):
class AppMainInjection extends MainInjectionMyApp {
@override
void beforeRegisterAllSingletonsMyApp({String key = 'default'}) {
RegistryManager.instance.connectInstance<IExternalConfig, EnvConfig>(
() => EnvConfig.fromEnvironment(),
key: key,
);
}
}
void main() {
AppMainInjection().registerAllSingletonsMyApp();
// ...
}
Each call is independent per key — calling
registerAllSingletonsMyApp(key: 'a') and registerAllSingletonsMyApp(key: 'b') sets up two separate singleton graphs side by side, without one
overwriting the other.
An async twin — registerAllSingletonsMyAppAsync, with its own
beforeRegisterAllSingletonsMyAppAsync / afterRegisterAllSingletonsMyAppAsync
hooks — is generated alongside the sync API. Use it when a before/after hook
needs to await something (e.g. loading remote config) before or after
singletons are connected; the sync API is unaffected either way.
The type each class is registered under is the first interface in its
implements clause; a class implementing nothing is registered under its
own type. A class annotated with @DependencyInjectable(subkey: '...')
instead of the plain @dependencyInjectable shorthand is connected under
that subkey — use this when two @dependencyInjectable classes implement
the same interface and both need to be auto-wired without one overwriting
the other. The generated connectInstance call also forwards that subkey
into the class's own factory call (ClassName.dependencyInjectionFactory(key: key, subkey: '...')), so any of its own @Subkey.inherited() parameters see
it too. Import paths are computed relative to the registry file's own
location, so it can be placed anywhere.
Requirements #
- Dart SDK >= 3.11.0
package:singleton_manager^2.2.0 (for@dependencyInjectable,@Subkey, andRegistryManager)package:analyzerfor AST parsingpackage:argsfor CLI argument handlingpackage:pathfor path utilities
How it works #
- Parsing: Scans Dart files using the
analyzerpackage to find classes annotated with@dependencyInjectableand their default constructor's parameters - Generation: Builds a
factory <ClassName>.dependencyInjectionFactory()body that resolves each parameter fromRegistryManager.instanceand forwards it to the constructor - Rewriting: Inserts (or fully replaces, on a re-run) the factory right after the default constructor, and ensures the
singleton_managerimport is present - Output: Writes the rewritten file back to disk — in place by default, or under
--outputif given
Example Project #
See example/my_service.dart and the test/ directory for further examples.