-
-
Notifications
You must be signed in to change notification settings - Fork 0
Architecture
albertoodev edited this page Aug 24, 2026
·
8 revisions
SPM exposes one public library, package:spm/spm.dart, for Flutter code that uses SpmState. The
CLI remains internal under lib/src/. Its five feature modules separate data access, domain logic,
and command wiring with a data/ → domain/ → presentation/ layout.
lib/
├── spm.dart # public Flutter integration API; exports SpmState
└── src/
├── core/
│ ├── constants/
│ ├── errors/ # failures.dart: 20 typed Failure subclasses + CompoundFailure
│ ├── injection/ # static service locators (AnalysisDI, IsolationDI, InjectionDI, ValidationDI…)
│ ├── loggor/ # SpmLogger (use instead of print)
│ └── types.dart # shared type aliases (Result<T>, AsyncResult<T>, JsonRecord, …)
│
└── features/
├── analysis/ # static AST analysis → analyze
├── injection/ # code transformation + app execution → inject / run
├── isolation/ # rebuild-scope extraction → isolate
├── profiler/ # runtime-side API imported by the instrumented app
└── validation/ # structural-mutation gate → validate
Each feature follows the same three-layer layout:
<feature>/
├── data/
│ ├── data_sources/
│ ├── models/
│ └── repositories/
├── domain/
│ ├── entities/
│ ├── repositories/
│ └── use_cases/
└── presentation/ # one Command subclass, wiring CLI args → use cases via the feature's DI
| Feature | Command | Responsibility |
|---|---|---|
| analysis | analyze | Walks Dart files, finds every rebuild scope (the same kinds isolation detects), extracts build-tree metrics from each scope's body, skipping the callbacks a rebuild cannot run (core/rebuild_path.dart, shared with isolation so both commands prune the same slots), via feature sets → specialized extractors and visitors. TreeExtractor returns an ExtractionSet, pairing the feature set with the closure of files it read, so each row can report its dependencies and any it could not resolve. A library outside the analyzed directories is read too: AnalysisContextCollection.contextFor throws for a pub-cache path, so TreeExtractor builds one collection per package root against the project's own package_config.json, which --package-config pins across repeated runs over the same project. That parameter lives on AnalysisContextCollectionImpl rather than on the public factory, and analyzer_package_config_smoke_test.dart fails if it moves. A library that resolves while carrying an error is refused rather than read, since its types come back null and its widgets would classify as value objects. Streams AnalysisResultEntity records as JSONL. --scope-types narrows the emitted kinds. |
| isolation | isolate | Finds rebuild scopes (State, ConsumerWidget/HookConsumerWidget, builder patterns like BlocBuilder/Consumer/Selector/Obx/GetX/Observer) and transplants each into a self-contained .dart file, grouped by type, lifting the bindings the scope closed over onto the generated State and formatting the output directory. Dependencies that build UI are inlined whole, repo-local and third-party alike, and a widget arrives with the base class it extends and its companion State, since a broken supertype chain would change what analyze counts. Third-party UI is inlined because a stand-in widget has an empty build, so a file full of them describes a tree the app never built, and since 0.7.0 analyze reads a package library in place, so the two rows describe the same tree rather than diverging. helpers/ui_surface.dart holds the predicate both the AST gate and the element-model gate ask, helpers/inline_budget.dart counts how much third-party source one scope carried, and helpers/flutter_namespace.dart names the declarations that would shadow a Flutter export, which visitors/namespace_renamer.dart then carries under a mangled name instead of standing in for. Everything else, meaning models, services, constants and the third-party symbols that build no UI, gets a declaration-only stand-in from data/data_sources/emitters/shim_emitter.dart, which mirrors the original's supertype so widget-versus-value-object classification survives, passes a child, children or body argument through to its build, and hands back real values through helpers/default_values.dart and emitters/stub_emitter.dart rather than throwing. --no-inline-third-party restores the older behaviour outright, where every third-party symbol was stood in for. Before any of that runs, --prune-non-rebuild drops what a rebuild cannot reach: visitors/non_rebuild_body_eraser.dart empties the body of a handler closure while keeping its signature and its place in the tree, and the transplant drops the members of the scope's class build() cannot reach, so the dependency crawl never enters them. References the analyzer resolved to nothing, an extension behind a refused import or anything at all in a project whose pub get failed, are rebuilt from their call sites by emitters/synthetic_shim_emitter.dart. Imports are gathered by emitters/import_collector.dart, keyed by URI and prefix so an as clause survives, and gated to dart: and package:flutter/ by helpers/sdk_uris.dart. verifier/output_verifier.dart then analyses the written files in process and reports per-file diagnostics into the mapping JSONL; it reads and never writes. Those diagnostics also decide whether the inlining paid: a scope that carried third-party source and still does not analyse is extracted again with it stood in for, and the version with fewer errors is kept, so inlining can never leave a row worse than shimming would. DI class: IsolationDI. The scope-kind lists and predicates live in AppConstants.rebuildScopeTypes and analysis/.../extensions/state_class_detector.dart, shared with the analysis feature. |
| injection | inject, run | Reads the analysis JSONL, skips rows whose scopeType is not State, and rewrites each target State class to extends SpmState<T> (adding an instanceId getter and the SpmState import); modes inject / remove. run then launches the app and connects to its Dart VM Service over WebSocket to collect ext.spm.profiler events. |
| profiler | (imported by the app) | Runtime-side API. SpmState.setState picks SpmProfiler.monitor() under --profile or SpmProfiler.monitorDataFlow() under --debug, measuring frame timings and rebuild counts and emitting events back to SPM over the VM Service. |
| validation | validate | Static gate deciding whether a mutation is a clean structural variant of a base file. DI class: ValidationDI. See AST Internals. |
-
types.dartholds all shared type aliases. The key ones areResult<T> = Either<Failure, T>,AsyncResult<T>,StreamResult<T>,AsyncVoidResult = AsyncResult<void>,AsyncVoid = Future<void>(data layer, may throw), andJsonRecord = Map<String, dynamic>. Always use these aliases; never spell out the full types inline. -
errors/failures.dartdefines 20 typedFailuresubclasses (e.g.InjectionFailure,RunAppFailure,FlutterAnalyzeFailure);CompoundFailureaggregates multiple failures. -
injection/holds static service locators built on lazy singleton getters (??=), with no external DI framework. Each DI class exposes areset()to clear all singletons; call it insetUp/tearDownwhen testing through the DI layer. -
loggor/logger.dartprovidesSpmLoggerfor stdout/stderr. Use it instead ofprint.
-
lib/spm.dartis the supported public import. It exportsSpmState; all other implementation libraries live underlib/src/. -
bin/spm.dart→lib/src/runner.dart(SpmRunner, aCommandRunnerwith the featureCommandsubclasses registered). - Each feature's
presentation/contains oneCommandsubclass wiring CLI args to use cases via the feature's DI singleton.
Either<Failure, T> is used throughout the data and domain layers (Left = error, Right =
success). Data sources may throw domain-specific exceptions; repositories translate those into
typed failures. Presentation commands fold results and return a non-zero status on failure, and
bin/spm.dart terminates the process with that returned status.
| Package | Role |
|---|---|
analyzer |
Dart AST parsing and semantic resolution. |
dartz |
Either type for functional error handling. |
vm_service |
Connect to the Dart VM to collect profiler events. |
web_socket_channel |
WebSocket transport to the VM Service. |
args |
CLI argument parsing. |
path |
Cross-platform path utilities. |
Commands
Reference
Internals
Contributing