The modular CLI, and exactly what it does when you run it.
modular ships from its own repo,
modular_cli — a
self-contained Dart package with its own resolution and its own
tests, versioned separately from the template. Four commands, no code generation
magic: it reads and writes plain text at known anchor points.
new feature <name>
Generates an _api/_impl pair and registers it — in the workspace, the DI config, and the router.
doctor
Five architecture checks the Dart analyzer has no way to run.
gen assets
Regenerates AppVectorAssets / AppRasterAssets from what's on disk.
rename
Rewrites the bundle id and app name across every platform folder.
modular new feature <name>
Generating packages is easy. The wiring is the point.
A feature that compiles but was never registered fails at runtime, the first time something resolves its facade. So the command doesn't stop at writing files — it edits three other files for you, at anchors that already exist in a fresh checkout.
What happens, in order
Validate the name
Naming.fromFeatureName requires lower_snake_case (^[a-z][a-z0-9]*(_[a-z0-9]+)*$) and rejects Dart reserved words — rejecting rather than normalizing keeps the package name, directory and generated class names in agreement.
Locate the workspace
Project.locate() walks up from the working directory looking for a pubspec.yaml that declares workspace: — so the CLI behaves the same from any subdirectory.
Verify the anchors exist first
Before writing a single file, it checks all three anchor comments are present. A missing anchor fails loudly here — never as a half-generated feature on disk.
Render the templates
{{name}} / {{Name}} / {{camelName}} are substituted, then the package: import block is re-sorted — whether {{name}}_api sorts before router_api depends on the name, so it can never be hardcoded in a template.
Write features/<name>/
13 files: two pubspecs, two barrels, the API facade + launcher, the impl facade + launcher, the module, the controller, the screen, the route info and the module router.
Wire it in
Three edits — see the anchor system below — to the root pubspec.yaml, dependency_injection_configuration.dart and (unless --route=false) router_configuration.dart.
The anchor system
Comments that survive reformatting, and fail loudly if removed.
Rather than parsing Dart or YAML, the wiring step looks for a plain
text marker and inserts a line above it, matching that line's
indentation. Simple enough to never break on a hand edit — and if you
delete the anchor, the next new feature tells you exactly
why it stopped.
pubspec.yaml
# feature modules - features/counter/* + - features/user_profile/* # <generated:feature-packages>
dependency_injection_configuration.dart
+ import 'package:user_profile_impl/ + user_profile_impl.dart'; ... CounterModule(), + UserProfileModule(), // <generated:feature-modules>
router_configuration.dart
+ import 'package:user_profile_impl/ + user_profile_impl.dart'; ... CounterModuleRouter(), + UserProfileModuleRouter(), // <generated:feature-routers>
AnchoredFile.insertImport does the same for the import line
itself — it walks the existing import 'package:...' block and
inserts alphabetically, so the result satisfies directives_ordering
without a manual pass.
modular doctor
Five checks the compiler has no way to run.
implementation_imports: error already stops an _impl
internal from being imported directly. What it can't see is a feature
that was never registered, or an _api package that quietly grew
a dependency on an implementation — both fail at runtime, or not until
someone tries to reuse the package.
api-depends-on-impl
An _api package must not depend on any _impl package
— that would make the contract require its own implementation to
even compile.
impl-to-impl
Only app may depend on an _impl package. Everything
else — including another feature — must go through the _api
contract.
not-in-workspace
A package that declares resolution: workspace but is
missing from the root pubspec.yaml's workspace: list
resolves separately and silently drifts from the rest of the repo.
module-not-registered
The failure this command exists for: a DependencyModule
implementation that's never passed to DependencyInjectionConfiguration
— compiles fine, throws the first time something resolves it.
route-info-exported
A _route_info.dart file exported from an _impl barrel
lets other modules navigate by hardcoded path, bypassing the
launcher entirely.
modular gen assets
Typed accessors, generated from what's actually on disk.
Drop a file into core/design_system/assets/{vectors,rasters}/,
run the command, and it's available as
AppVectorAssets.yourFile — never a raw string path. The
file name is turned into a Dart identifier
(arrow-right.svg → arrowRight), and a name that
can't become one — a Dart reserved word, an empty stem — fails the
command instead of emitting code that won't compile.
CI runs gen assets --check and fails if the committed
output doesn't match what the generator would produce — the
generated files are committed, and this is what keeps them honest.
generated by `modular gen assets`
// GENERATED — DO NOT EDIT BY HAND. abstract class AppVectorAssets { static const arrowRight = AppVectorAsset( path: 'vectors/arrow-right.svg', packageName: 'assets', ); }
modular rename
One command, every platform folder.
Renaming a Flutter app by hand means touching the Android
applicationId, the iOS bundle identifier, both
platforms' display names, and every leftover reference to the
template's own name. modular rename does the pass once,
from the CLI, right after cloning.
terminal
modular rename \
--bundle-id com.acme.myapp \
--name "My App"
dart pub get, its
own analysis_options.yaml and its own tests — deliberately
isolated so the CLI's dependencies (args, path) never
leak into the app's resolution, and so it can be released and versioned on
its own schedule, activated globally
(dart pub global activate --source git
https://github.com/Yusubov-Engineering/modular_cli.git --git-ref v1.0.0)
independent of whichever project it's pointed at.