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_pluginsystem.
Variables
| Variable | Type | Default | Description |
|---|---|---|---|
| appName | string | MyApp | The name of your application |
| isForMonorepo | boolean | true | Whether this is being generated as part of a monorepo |
Usage
Interactive
archipelago generate app_lintsNon-interactive (CI)
archipelago generate app_lints --config my_config.jsonGenerated Structure
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.yamlRules
AppLintsPlugin.register registers exactly 7 AnalysisRules: the three layer rules, three DI rules and the PII rule.
api_no_external_deps— an*_apipackage file may only importdart:*, any workspace package inshared/orutilities/, another workspace*_apipackage, 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_apipackage."no_impl_cross_import— an*_implpackage must not import another workspace*_implpackage directly; cross-feature communication must go through the corresponding*_apipackage. A pub package whose name ends in_implis not affected.no_upward_dependency— enforces the layer directionapps -> 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:
- Find the workspace root: the nearest folder above the importing file whose
pubspec.yamlhas a top-levelworkspace:key. - If the resolved file is outside that root (the pub cache or the SDK), it is an external package. It has no layer.
- Otherwise the layer is the first folder under the root:
apps/,features/,infrastructure/,packages/, orshared/andutilities/(one layer). Other folders, such asdevtools/andthird_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 (viapii_key_matcher.dart) inside map literals passed totrack,log,breadcrumb,recordError, orcaptureCustomEventcalls (viapii_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 aGetItvalue outsidelib/src/di/and outside classes that extendMicroPackageModule. The type comes from the element model, soGetIt.I,GetIt.instance, a top-levelgetIt, a feature accessor such asauthGetItand a localfinal getIt = GetIt.instanceare 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 featureMicroPackageModule.init:GetIt.register*, and theGetItHelpermethodssingleton*,lazySingleton*andfactory*. Three registrations are allowed: aFeatureManifest; the feature's own SDK (aFeatureSDKsubtype declared in the samefeatures/<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 aGetItvalue in aFeatureManifestsubtype's constructor (including non-latefield initializers),isEnabledgetter orregisterRoutesmethod. 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'sresolve, a route builder).FeatureSDKis a manifest, so eager SDKs are checked too. "Take a provider closure from the global module instead."
getIt
// di-global: port the network layer resolves.
..registerLazySingleton<AuthTokenProvider>(AuthSessionTokenProvider.new)
..registerLazySingleton<LoginRepository>(LoginRepositoryImpl.new); // LINTUse 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:
plugins:
app_lints:
path: utilities/app_lints
diagnostics:
feature_global_module_allowlist: true
no_service_locator_outside_di: true
no_service_locator_in_manifest: trueOn 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 breaksbuild_runner buildworkspace-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_dependencyfindings and left 0. *_apiand*_implstill 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.dartfiles are excluded from analysis, so@injectable-annotated registrations are not checked byfeature_global_module_allowlist. Only hand-writteninitbodies are. feature_global_module_allowlistchecks registrations written ininititself, including closures inside it. A registration in a helper method thatinitcalls is not checked.- There is no baseline file yet. To adopt
no_service_locator_outside_diin an existing project, suppress current sites with// ignore: app_lints/no_service_locator_outside_di(theapp_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.