Skip to content

Architecture

albertoodev edited this page Aug 21, 2026 · 8 revisions

Architecture

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.

Top-level 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

Features

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 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. 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, and a widget arrives with the base class it extends, since a broken supertype chain would change what analyze counts. Everything else, meaning models, services, constants and third-party symbols, 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. 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, prunes unused imports, and reports per-file diagnostics into the mapping JSONL. 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.

Core layer (lib/src/core/)

  • types.dart holds all shared type aliases. The key ones are Result<T> = Either<Failure, T>, AsyncResult<T>, StreamResult<T>, AsyncVoidResult = AsyncResult<void>, AsyncVoid = Future<void> (data layer, may throw), and JsonRecord = Map<String, dynamic>. Always use these aliases; never spell out the full types inline.
  • errors/failures.dart defines 20 typed Failure subclasses (e.g. InjectionFailure, RunAppFailure, FlutterAnalyzeFailure); CompoundFailure aggregates multiple failures.
  • injection/ holds static service locators built on lazy singleton getters (??=), with no external DI framework. Each DI class exposes a reset() to clear all singletons; call it in setUp/tearDown when testing through the DI layer.
  • loggor/logger.dart provides SpmLogger for stdout/stderr. Use it instead of print.

Entry points

  • lib/spm.dart is the supported public import. It exports SpmState; all other implementation libraries live under lib/src/.
  • bin/spm.dartlib/src/runner.dart (SpmRunner, a CommandRunner with the feature Command subclasses registered).
  • Each feature's presentation/ contains one Command subclass wiring CLI args to use cases via the feature's DI singleton.

Error handling

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.

Key dependencies

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.

Clone this wiki locally