scout_router 0.1.0
scout_router: ^0.1.0 copied to clipboard
Scout Router — statically analyse your go_router configuration and emit a structured route manifest (YAML, JSON, or Markdown) at compile time. Part of the Scout Application Capability Intelligence Pla [...]
scout_router #
Part of the Scout Application Capability Intelligence Platform
A build_runner plugin that statically analyses your go_router configuration and
emits a structured route manifest at compile time — no reflection, zero runtime overhead.
Run dart run build_runner build and get a route_map.yaml (or .json or .md) that
shows every route, its access level, path/query parameters, and the Dart type of
state.extra — all resolved from source, no app execution needed.
Why #
go_router gives you a declarative routing tree at runtime, but there is no built-in
way to inspect that tree statically — for documentation, security audits, or generating
navigation helpers. This plugin fills that gap.
Typical uses:
- Security review: confirm every sensitive route is behind the correct guard.
- Documentation: commit a
route_map.mdthat reviewers can read without running the app. - Onboarding: new team members can see the entire navigation graph in one file.
- Navigation codegen: downstream tools can read
route_map.jsonto generate typed navigator wrappers.
Quick start #
1. Add to dev_dependencies #
# pubspec.yaml
dev_dependencies:
scout_router: ^0.1.0
build_runner: ^2.4.0
2. Configure build.yaml #
# build.yaml
targets:
$default:
builders:
scout_router:
options:
router_file: lib/router.dart # path to your GoRouter file
router_class: AppRouter # class containing GoRoute definitions
output_format: yaml # yaml | json | markdown
3. Run #
dart run build_runner build
# or watch mode:
dart run build_runner watch
Output: route_map.yaml (or .json / .md) in your package root.
Access levels #
The inspector can tag each route with an access-level label derived from static list membership or path prefixes — fully configurable, not hardcoded to any naming convention.
# build.yaml
options:
access_levels:
# Map a static List<String> field name in [router_class] → access label.
publicRoutes: public
adminRoutes: admin
path_prefix_rules:
# Fallback: match by path prefix → access label.
/admin: admin
default_access: authenticated # used when no rule matches
In your router class, just maintain the lists you already have:
class AppRouter {
static const publicRoutes = ['/login', '/register', '/'];
static const adminRoutes = ['/admin', '/admin/users'];
}
Any naming scheme works — map your field names to labels in build.yaml.
Output formats #
YAML (default) #
routes:
- path: /
name: home
access: public
- path: /products/:id
access: authenticated
pathParams: [id]
- path: /checkout
access: authenticated
queryParams: [coupon]
extra:
type: CheckoutArgs
fields:
- cartId: String
- expressShipping: bool
- path: /admin
access: admin
children:
- path: /admin/users
access: admin
queryParams: [page, q]
JSON #
{
"routes": [
{
"path": "/",
"access": "public",
"name": "home"
},
{
"path": "/checkout",
"access": "authenticated",
"queryParams": ["coupon"],
"extra": {
"type": "CheckoutArgs",
"fields": [
{ "name": "cartId", "type": "String", "required": true },
{ "name": "expressShipping", "type": "bool", "required": true }
]
}
}
]
}
Markdown #
Produces a document with a route tree, a summary table, and per-access-level route tables — ready to commit alongside your code.
# Route Map
## Summary
| Access Level | Count |
|---|---|
| `public` | 3 |
| `authenticated` | 8 |
| `admin` | 4 |
## Route Tree
├── / [public]
├── /products [public]
│ └── /products/:id [public]
├── /checkout [authenticated]
└── /admin [admin]
├── /admin/users [admin]
└── /admin/orders [admin]
Extra type resolution #
If your builder passes a typed object via state.extra, the inspector resolves it:
GoRoute(
path: '/checkout',
builder: (context, state) {
final args = state.extra as CheckoutArgs; // ← extracted
return CheckoutPage(args: args);
},
)
The output includes the class name and its public fields, scanned from source:
extra:
type: CheckoutArgs
fields:
- cartId: String
- expressShipping: bool
Point the model_scan_path glob at where your arg classes live:
options:
model_scan_path: lib/navigation/**/*.dart
ShellRoute and StatefulShellRoute #
Shell routes are transparent wrappers — the inspector recurses into their routes:
(or branches:) and promotes the children to the parent level, so they appear at the
correct path depth without a spurious wrapper node in the output.
All build.yaml options #
| Option | Default | Description |
|---|---|---|
router_file |
lib/router.dart |
Path to the file containing your router class |
router_class |
AppRouter |
Class name that holds GoRoute definitions |
output_format |
yaml |
yaml, json, or markdown |
output_path |
(derived from format) | Override the output file path |
access_levels |
{} |
Map of list-field-name → access label |
path_prefix_rules |
{} |
Map of path prefix → access label |
default_access |
authenticated |
Label when no rule matches |
model_scan_path |
lib/**/*.dart |
Glob for scanning extra-type model classes |
private_routes |
[] |
Paths to exclude from the output |
Committing the output #
The generated file is deterministic — commit it to your repo and diff it in PRs.
A changed route_map.yaml in a PR is an immediately visible signal that routing
or access-level logic changed, making security reviews much easier.
What gets scanned #
Route definitions are discovered from the router_file whether they live in:
- a
Listfield on the configured router class (static, instance,const, orfinal); - a top-level
Listvariable; - a
GoRouter(routes: [...])constructor — inline, in a field, or returned from a getter/function; - across files:
...featureARoutesspreads and bare-identifier route lists (GoRouter(routes: appRoutes),routes: childRoutes) are followed by name into any file undermodel_scan_path. This lets you split the router by feature module:// app_router.dart final routes = [...authRoutes, ...shopRoutes, ...adminRoutes]; // features/shop/routes.dart final shopRoutes = [GoRoute(path: '/shop', ...)];
Limitations #
- Pure static analysis: dynamic string interpolation in paths (e.g.
'/$prefix/detail'whereprefixis a runtime variable) cannot be resolved. - Cross-file references are resolved by name (parse-only), not through the import graph. Two top-level symbols with the same name in different libraries would collide; in practice route-list names are unique. Full import-aware resolution is a planned enhancement.
Part of Scout #
scout_router is one of the foundation packages of the
Scout Application Capability Intelligence Platform.
Scout runs scout_router's route analysis as one of three passes to produce
scout.manifest.json — a machine-readable capability graph consumed by AI systems,
enforced in CI, and maintained at compile time with zero runtime overhead.
License #
MIT