Skip to content
Back to archipelago.banuacoder.com

Shared Packages

app_lints Free ​

Tier
Freeversion 1.3.4
Command
archipelago generate app_lints
Generates
  • utilities/app_lints

Custom Dart analyzer plugin enforcing this monorepo's architecture conventions (layer direction, API-package isolation, impl-package isolation), two DI boundary rules for feature code, and a PII-in-telemetry guard, via the analysis_server_plugin system.

Variables ​

VariableTypeDefaultDescription
appNamestringMyAppThe name of your application
isForMonorepobooleantrueWhether this is being generated as part of a monorepo

Usage ​

Interactive ​

bash
archipelago generate app_lints

Non-interactive (CI) ​

bash
archipelago generate app_lints --config my_config.json

Generated Structure ​

text
utilities/
└── app_lints/
    ├── lib/
    │   ├── app_lints.dart                      # Barrel export
    │   ├── main.dart                           # AppLintsPlugin — registers all 7 rules
    │   └── src/
    │       ├── pii/
    │       │   ├── pii_key_matcher.dart        # Deny-list matcher for personal-data key names
    │       │   └── pii_telemetry_scanner.dart  # Finds track/log/breadcrumb/recordError/captureCustomEvent calls
    │       └── rules/
    │           ├── api_no_external_deps_rule.dart
    │           ├── no_impl_cross_import_rule.dart
    │           ├── no_upward_dependency_rule.dart
    │           ├── no_pii_in_telemetry_rule.dart
    │           ├── no_service_locator_outside_di_rule.dart
    │           ├── feature_global_module_allowlist_rule.dart
    │           ├── no_service_locator_in_manifest_rule.dart
    │           ├── di_types.dart               # GetIt / MicroPackageModule / FeatureSDK checks by element
    │           └── workspace_layer.dart        # Path-based layer lookup shared by the layer and DI rules
    ├── test/
    │   ├── di_rules_test.dart                  # Runs the 3 DI rules over a resolved in-memory workspace
    │   ├── layer_rules_test.dart               # Runs the 3 layer rules over a resolved in-memory workspace
    │   └── no_pii_in_telemetry_rule_test.dart
    ├── analysis_options.yaml
    └── pubspec.yaml

Rules ​

AppLintsPlugin.register registers exactly 7 AnalysisRules: the three layer rules, three DI rules and the PII rule.

  • api_no_external_deps — an *_api package file may only import dart:*, any workspace package in shared/ or utilities/, another workspace *_api package, or one of a fixed list of pub packages (flutter, meta, freezed_annotation, json_annotation, equatable, collection, injectable, auto_route). Anything else: "Move this dependency to the impl package, or extract a shared contract into an _api package."
  • no_impl_cross_import — an *_impl package must not import another workspace *_impl package directly; cross-feature communication must go through the corresponding *_api package. A pub package whose name ends in _impl is not affected.
  • no_upward_dependency — enforces the layer direction apps -> features -> infrastructure -> packages -> shared; a lower layer importing a higher one is flagged with both packages' layers. Pub and SDK packages are never flagged.

How the layer rules find a package's layer ​

The three layer rules never guess a layer from a package name. For each import they take the file the import resolves to and do this:

  1. Find the workspace root: the nearest folder above the importing file whose pubspec.yaml has a top-level workspace: key.
  2. If the resolved file is outside that root (the pub cache or the SDK), it is an external package. It has no layer.
  3. Otherwise the layer is the first folder under the root: apps/, features/, infrastructure/, packages/, or shared/ and utilities/ (one layer). Other folders, such as devtools/ and third_party/, have no layer and are not checked.

So a renamed package (for example acme_ui_kit in packages/) gets the layer of its folder, and a pub package named like a workspace package (for example a pub monitoring_api) is still external. The importing package's own layer comes from its folder in the same way. A package that is not in a pub workspace is not checked. This matches how graph validate classifies packages. There is no layer configuration to keep in sync.

  • no_pii_in_telemetry — flags deny-listed personal-data key names (via pii_key_matcher.dart) inside map literals passed to track, log, breadcrumb, recordError, or captureCustomEvent calls (via pii_telemetry_scanner.dart). "The runtime scrubber will redact it anyway — sending it is a bug, not a fallback."

DI rules ​

The DI rules check package: libraries in features/ only. The layer comes from the folder, as for the layer rules. Tests, apps, infrastructure and shared code are not checked. The host composition root and its initializers live in apps/, so they are allowed by scope.

  • no_service_locator_outside_di — flags every use of a GetIt value outside lib/src/di/ and outside classes that extend MicroPackageModule. The type comes from the element model, so GetIt.I, GetIt.instance, a top-level getIt, a feature accessor such as authGetIt and a local final getIt = GetIt.instance are all caught. "Take the dependency as a constructor parameter, and resolve it in the feature's lib/src/di/ files or its MicroPackageModule."
  • feature_global_module_allowlist — flags each registration inside a feature MicroPackageModule.init: GetIt.register*, and the GetItHelper methods singleton*, lazySingleton* and factory*. Three registrations are allowed: a FeatureManifest; the feature's own SDK (a FeatureSDK subtype declared in the same features/<name>/ folder); and a line annotated // di-global: <reason>, or the line directly below a comment line with that annotation. The reason must not be empty.
  • no_service_locator_in_manifest — flags every use of a GetIt value in a FeatureManifest subtype's constructor (including non-late field initializers), isEnabled getter or registerRoutes method. The host reads every manifest during pre-launch, so a lookup there puts the feature's graph on the boot path. A closure written there is fine: it runs only when called (a claim's resolve, a route builder). FeatureSDK is a manifest, so eager SDKs are checked too. "Take a provider closure from the global module instead."
dart
getIt
  // di-global: port the network layer resolves.
  ..registerLazySingleton<AuthTokenProvider>(AuthSessionTokenProvider.new)
  ..registerLazySingleton<LoginRepository>(LoginRepositoryImpl.new); // LINT

Use di-global only for a type that code outside the feature resolves from the root. Everything else belongs in the feature's local DI.

Enabled by default? ​

Yes. The generated root analysis_options.yaml enables all three DI rules:

yaml
plugins:
  app_lints:
    path: utilities/app_lints
    diagnostics:
      feature_global_module_allowlist: true
      no_service_locator_outside_di: true
      no_service_locator_in_manifest: true

On template_base all three rules have 0 findings. Six real ports carry a di-global reason (four in auth_impl, MqttDebugLog in the MQTT debugger noop, and MiniAppRegistry in webview_advanced). The service-locator rule had 41 findings in 12 files before a later change removed them: pages get their dependencies from the shell (a container lease), a route builder or a typed function in the feature's DI files, state holders take them by constructor, and optional lookups became optional constructor parameters whose defaults live in a DI file. A project generated before that can set no_service_locator_outside_di: false until it has migrated.

Dependencies ​

  • analyzer: ">=13.1.0 <15.0.0" — floor set in 1.0.3 specifically because analyzer < 13.1.0 cannot parse the Dart 3.13 primary-constructor syntax (class const Foo(int x) {}) used across template_base and other bricks; a lower floor breaks build_runner build workspace-wide with a "primary-constructors language feature" error on every generator, not just this package.
  • analysis_server_plugin — the plugin host API this brick implements against.
  • No self-generation and no runtime archipelago: di_modules block — this is a static-analysis plugin, not app-runtime code, and has no hard dependency on another brick.

Known Limitations ​

  • Before 1.2.0 the layer rules guessed layers from package names, so every pub package and every renamed workspace package was misclassified. 1.2.0 reads the layer from the resolved file path instead. On template_base that removed 54 false no_upward_dependency findings and left 0.
  • *_api and *_impl still come from the package name: that suffix defines the two package kinds. Only workspace packages count.
  • An import that does not resolve (missing dependency, missing generated file) has no resolved path. The layer rules skip it, and the analyzer reports the broken import on its own.
  • Rules run through the Dart analyzer plugin system, so they apply during dart analyze / IDE analysis, not as a separate CLI command.
  • The DI rules have no CI gate like graph validate. They report in the IDE only.
  • Generated *.module.dart / *.config.dart files are excluded from analysis, so @injectable-annotated registrations are not checked by feature_global_module_allowlist. Only hand-written init bodies are.
  • feature_global_module_allowlist checks registrations written in init itself, including closures inside it. A registration in a helper method that init calls is not checked.
  • There is no baseline file yet. To adopt no_service_locator_outside_di in an existing project, suppress current sites with // ignore: app_lints/no_service_locator_outside_di (the app_lints/ prefix is required for plugin diagnostics) and fix them over time.

Changelog ​

Run archipelago brick info app_lints to see the latest published version. Enterprise customers can read the full CHANGELOG.md in the brick source repository.