Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,50 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Added

- `MagicDevtools` umbrella wiring: a new `package:magic_devtools/magic_devtools.dart`
barrel exposing `MagicDevtools.installPre()` / `MagicDevtools.installPost()`.
`installPre` boots both tool plugins and registers telescope's opt-in
`ExceptionWatcher` + `DumpWatcher` (call before `Magic.init()`); `installPost`
wires `MagicTelescopeIntegration` + `MagicDuskIntegration` (call after
`Magic.init()`). Collapses the previous four `kDebugMode` blocks in a host's
`lib/main.dart` into two, while preserving the load-bearing pre/post ordering
and the call-site `kDebugMode` guard (moving the guard inside would defeat the
release tree-shake).
- `MagicPreview` framework: a dev-only component preview catalog hosted via two
plain pages (`/preview` and `/preview/:component`). New
`package:magic_devtools/preview.dart` barrel exports
the `PreviewEntry` contract (`label`, `slug`, `builder`), the
`MagicPreviewCatalog` widget (a scrollable sidebar next to a SINGLE active
preview pane — tapping a sidebar item, or deep-linking `/preview/<slug>`,
swaps the pane to that entry; only the selected preview is mounted, so a
large screen-heavy catalog stays responsive — plus a global light/dark toggle
bound to wind's `WindTheme.of(context).toggleTheme()`),
and the `MagicPreview` registration entrypoint (`register` plus `registerRoutes`).
The route, catalog, and every registered `PreviewEntry` are reachable only
through `MagicPreview.registerRoutes`, which is guarded by `kReleaseMode` plus
`const bool.fromEnvironment('PREVIEW_ENABLED', defaultValue: kDebugMode)`, so
the whole surface const-folds dead and tree-shakes out of release builds.
Entries are held in a function-scoped list (never a top-level const, the
dart-lang/sdk#33920 foot-gun). The generated `_previews.g.dart` (Step 18) feeds
a `List<PreviewEntry>` into `MagicPreview.register`. Consumers must call
`MagicPreview.registerRoutes()` from a provider `boot()` BEFORE the router locks
on first `routerConfig` access, else `/preview` silently never registers.
- `fluttersdk_wind` is now a direct dependency (the catalog renders on
`WDiv`/`WText`/`WAnchor` and binds the theme toggle to `WindThemeController`).
- `MagicDuskIntegration.install()` now registers a navigate adapter via
`DuskPlugin.registerNavigateAdapter` so `ext.dusk.navigate --route <path>`
drives GoRouter through `MagicRouter.instance.to(path)` instead of falling
back to the `SystemNavigator` platform broadcast. Returns `true` on success
and `false` when the router is not yet initialised (catches `StateError`).
`resetForTesting()` clears the adapter with `DuskPlugin.registerNavigateAdapter(null)`.

### Fixed

- **`/preview/<slug>` deep links now select the right entry**: the catalog moved off a persistent ShellRoute (which did not rebuild when only the child route swapped, leaving every deep link stuck on the first entry) to two plain pages; the `/preview/:component` builder receives the slug and rebuilds on navigation. Known dev-only limitation: feature-SCREEN previews (full controller-backed `MagicStatefulView`s) emit a couple of non-fatal `setState() during build` warnings because the catalog mounts the same screen in both the light and dark panes sharing a singleton controller; the screens render correctly and the real app routes are clean (the catalog is stripped from release).
- **`/preview` route no longer crashes the app**: the catalog group's index child path was `/` which composed to `/preview/`, tripping go_router's `route path may not end with '/'` assertion and blanking the entire app on every route. Changed the index child path to `''` so the composed path is exactly `/preview`.
- **Catalog previews now inherit the host theme**: each light/dark pane copied a bare `WindThemeData` that carried no aliases, so component semantic tokens (`text-fg`, `bg-surface`, ...) resolved to no-ops and every preview rendered Flutter's red unstyled-text fallback. Panes now `copyWith(brightness:)` the ambient app theme, preserving aliases and brand colors.
- **Catalog overflow**: the preview surface now scrolls vertically and each pane scrolls horizontally, so wide variant matrices no longer trigger RenderFlex overflows in the side-by-side light/dark layout.

## [0.0.1] - 2026-06-17

### Added
Expand Down
55 changes: 46 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

<p align="center">
<strong>Magic adapters for the FlutterSDK dev-tooling ecosystem.</strong><br/>
Wire Magic's runtime into <a href="https://pub.dev/packages/fluttersdk_dusk">fluttersdk_dusk</a> (E2E driver) and <a href="https://pub.dev/packages/fluttersdk_telescope">fluttersdk_telescope</a> (runtime inspector)debug-only, zero release cost.
Wire Magic's runtime into <a href="https://pub.dev/packages/fluttersdk_dusk">fluttersdk_dusk</a> (E2E driver) and <a href="https://pub.dev/packages/fluttersdk_telescope">fluttersdk_telescope</a> (runtime inspector), debug-only with zero release cost.
</p>

<p align="center">
Expand All @@ -21,22 +21,24 @@

---

> **Alpha Release** part of the Magic ecosystem, under active development. APIs may change before stable. [Star the repo](https://github.com/fluttersdk/magic_devtools) to follow progress.
> **Alpha Release**: part of the Magic ecosystem, under active development. APIs may change before stable. [Star the repo](https://github.com/fluttersdk/magic_devtools) to follow progress.

## What is magic_devtools?

`magic_devtools` is the Magic adapter layer for [`fluttersdk_dusk`](https://pub.dev/packages/fluttersdk_dusk) and [`fluttersdk_telescope`](https://pub.dev/packages/fluttersdk_telescope). It enriches dusk snapshots and telescope records with Magic-aware context (forms, navigation, controllers, gates, auth, broadcasting, HTTP) so an LLM agent or CI driver sees your app the way Magic sees it.

It is **debug-only**: you install and wire it under `kDebugMode`, so release builds tree-shake it entirely and it carries no runtime cost in production. This is exactly why it lives outside `magic` core the framework keeps no dev-tooling production dependencies.
It is **debug-only**: you install and wire it under `kDebugMode`, so release builds tree-shake it entirely and it carries no runtime cost in production. This is exactly why it lives outside `magic` core; the framework keeps no dev-tooling production dependencies.

Two import barrels:
Four import barrels:

- `package:magic_devtools/dusk.dart` — `MagicDuskIntegration` registers 14 Magic-aware enrichers into fluttersdk_dusk's snapshot pipeline.
- `package:magic_devtools/telescope.dart` — `MagicTelescopeIntegration` registers 5 Magic watchers and `MagicHttpFacadeAdapter` into fluttersdk_telescope.
- `package:magic_devtools/magic_devtools.dart`: `MagicDevtools` is the umbrella one-call wiring: `installPre()` boots both tool plugins (plus telescope's opt-in exception/dump watchers) before `Magic.init()`, `installPost()` wires both Magic integrations after it.
- `package:magic_devtools/dusk.dart`: `MagicDuskIntegration` registers 14 Magic-aware enrichers into fluttersdk_dusk's snapshot pipeline.
- `package:magic_devtools/telescope.dart`: `MagicTelescopeIntegration` registers 5 Magic watchers and `MagicHttpFacadeAdapter` into fluttersdk_telescope.
- `package:magic_devtools/preview.dart`: `MagicPreview` hosts a dev-only component preview catalog via two plain pages (`/preview` and `/preview/:component`), tree-shaken from release builds.

## Install

`magic_devtools` and the tooling packages are imported in `lib/main.dart` (under `kDebugMode`), so they are regular `dependencies`, not `dev_dependencies` `kDebugMode` tree-shakes them out of release builds, and because `lib/` imports them a `dev_dependencies` entry would trip the `depend_on_referenced_packages` lint. This matches how `fluttersdk_dusk` and `fluttersdk_telescope` are installed on their own.
`magic_devtools` and the tooling packages are imported in `lib/main.dart` (under `kDebugMode`), so they are regular `dependencies`, not `dev_dependencies`; `kDebugMode` tree-shakes them out of release builds, and because `lib/` imports them a `dev_dependencies` entry would trip the `depend_on_referenced_packages` lint. This matches how `fluttersdk_dusk` and `fluttersdk_telescope` are installed on their own.

```yaml
dependencies:
Expand All @@ -51,6 +53,18 @@ dependencies:

Both integrations are debug-only and run in `lib/main.dart`. The ordering is load-bearing: the dusk/telescope plugin installs **before** `Magic.init()` (so the snapshot pipeline is live during Magic boot and the exception watcher catches boot errors), and the Magic integration installs **after** `Magic.init()` (its enrichers and adapter resolve Magic primitives through the IoC container).

### Both tools at once (recommended)

`MagicDevtools` collapses the four blocks below into the two halves of that ordering. Keep the `kDebugMode` guard at the call site: moving it inside the methods would make the call live in release and defeat the tree-shake.

```dart
if (kDebugMode) MagicDevtools.installPre(); // dusk + telescope plugins + exception/dump watchers
await Magic.init(configFactories: [...]);
if (kDebugMode) MagicDevtools.installPost(); // MagicTelescopeIntegration + MagicDuskIntegration
```

Reach for the individual barrels below when you need only one tool, or a non-standard telescope watcher set (register extra watchers with `TelescopePlugin.registerWatcher` after `installPre`).

### Dusk

```dart
Expand All @@ -77,6 +91,29 @@ if (kDebugMode) {

You can wire either integration on its own, or both together: install each plugin before `Magic.init()` and each Magic integration after it. The `dusk:install` and `telescope:install` Artisan commands wire these blocks into `lib/main.dart` automatically when `magic_devtools` is a dependency.

### Preview catalog

`MagicPreview` hosts a dev-only component preview catalog: a sidebar of registered components next to each preview rendered in BOTH light and dark, with a global theme toggle bound to wind's `WindThemeController`. It is reachable only through `MagicPreview.registerRoutes()`, guarded by `kReleaseMode` plus `const bool.fromEnvironment('PREVIEW_ENABLED', defaultValue: kDebugMode)`, so the route, the catalog, and every registered `PreviewEntry` const-fold dead and tree-shake out of release builds.

The router-lock timing is load-bearing: `MagicRouter` locks its route table on the first `routerConfig` access, so registration MUST happen in a provider `boot()` (which runs during the Magic bootstrap lifecycle, before `MaterialApp` reads `routerConfig`). Register too late and `/preview` silently never appears.

```dart
class RouteServiceProvider extends ServiceProvider {
RouteServiceProvider(super.app);

@override
Future<void> boot() async {
registerAppRoutes();
if (kDebugMode) {
MagicPreview.register(previewEntries()); // from the generated _previews.g.dart
MagicPreview.registerRoutes();
}
}
}
```

The `previews:refresh` Artisan command scans `*.preview.dart` files and regenerates `previewEntries()` returning a `List<PreviewEntry>` from a function (never a top-level const, the dart-lang/sdk#33920 retention foot-gun).

## Ecosystem

| Package | |
Expand Down Expand Up @@ -110,11 +147,11 @@ dependency_overrides:

## License

MIT see [LICENSE](LICENSE) for details.
MIT, see [LICENSE](LICENSE) for details.

---

<p align="center">
<sub>Built with care by <a href="https://github.com/fluttersdk">FlutterSDK</a></sub><br/>
<sub>If magic_devtools helps you, <a href="https://github.com/fluttersdk/magic_devtools">give it a star</a> it helps others discover it.</sub>
<sub>If magic_devtools helps you, <a href="https://github.com/fluttersdk/magic_devtools">give it a star</a>; it helps others discover it.</sub>
</p>
12 changes: 12 additions & 0 deletions lib/magic_devtools.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
/// magic_devtools umbrella barrel.
///
/// Exposes [MagicDevtools], the one-call `installPre` / `installPost` wiring
/// for fluttersdk_dusk and fluttersdk_telescope plus their Magic
/// integrations, installed around [Magic.init] under `kDebugMode`.
///
/// See the finer-grained `dusk.dart`, `telescope.dart`, and `preview.dart`
/// barrels when you need direct access to a single integration, a
/// non-standard watcher set, or the preview catalog.
library;

export 'src/magic_devtools.dart';
31 changes: 31 additions & 0 deletions lib/preview.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
/// Magic dev-only component preview catalog barrel.
///
/// Import this file to host the auto-discovered component previews behind two
/// plain pages (`/preview` and `/preview/:component`). The whole surface is
/// dev-only: it is reachable only
/// through [MagicPreview.registerRoutes], which is guarded by `kReleaseMode` +
/// `bool.fromEnvironment('PREVIEW_ENABLED')` and tree-shaken from release
/// builds.
///
/// Wiring (in the consumer's `RouteServiceProvider.boot()`, which runs BEFORE
/// `MagicRouter.instance.routerConfig` is first accessed — the router locks its
/// route table on that first access):
///
/// ```dart
/// @override
/// Future<void> boot() async {
/// registerAppRoutes();
/// if (kDebugMode) {
/// MagicPreview.register(previewEntries()); // from _previews.g.dart
/// MagicPreview.registerRoutes();
/// }
/// }
/// ```
///
/// See `src/preview/magic_preview.dart` for the [PreviewEntry] contract and the
/// [MagicPreviewCatalog] widget, and `src/preview/preview_routes.dart` for the
/// [MagicPreview] registration entrypoint and the release boundary.
library;

export 'src/preview/magic_preview.dart';
export 'src/preview/preview_routes.dart';
84 changes: 84 additions & 0 deletions lib/src/magic_devtools.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import 'package:fluttersdk_dusk/dusk.dart';
import 'package:fluttersdk_telescope/telescope.dart';

import 'dusk_integration.dart';
import 'telescope_integration.dart';

/// One-call wiring for the Magic dev-tooling bundle: fluttersdk_dusk +
/// fluttersdk_telescope and their Magic integrations, installed in the two
/// phases that straddle [Magic.init].
///
/// The two tools cannot install in a single call because half of the work
/// has to happen BEFORE `Magic.init()` and half AFTER:
///
/// - [installPre] boots both plugins (VM Service extensions, capture hooks,
/// the dusk snapshot pipeline) and registers telescope's opt-in
/// [ExceptionWatcher] + [DumpWatcher]. It MUST run before `Magic.init()`
/// so the snapshot pipeline and ExceptionWatcher are already live while
/// Magic boots (they capture boot-time errors and the first route resolve).
/// - [installPost] wires Magic's runtime into both tools
/// ([MagicTelescopeIntegration] + [MagicDuskIntegration]). It MUST run
/// after `Magic.init()` because the watchers, the HTTP adapter, and the
/// snapshot enrichers resolve dependencies through the IoC container
/// (`Magic.find` / `Magic.bound`).
///
/// Keep both calls inside a `kDebugMode` guard AT THE CALL SITE. Do not move
/// the guard inside these methods: a live (unguarded) call defeats the
/// release tree-shake and pulls dusk + telescope into the production bundle,
/// which is the whole reason this wiring lives outside `magic` core.
///
/// ```dart
/// void main() async {
/// WidgetsFlutterBinding.ensureInitialized();
///
/// if (kDebugMode) MagicDevtools.installPre();
///
/// await Magic.init(configFactories: [...]);
///
/// if (kDebugMode) MagicDevtools.installPost();
///
/// runApp(const MyApp());
/// }
/// ```
///
/// This is the umbrella entry point; reach for the finer-grained
/// `dusk.dart`, `telescope.dart`, or `preview.dart` barrels only when you
/// need a non-standard watcher set or want to wire one tool without the
/// other.
class MagicDevtools {
MagicDevtools._();

/// Pre-`Magic.init()` half: boot both tool plugins and register the
/// standard opt-in telescope watchers.
///
/// Installs [DuskPlugin] (Semantics tree, `ext.dusk.*` extensions,
/// error/log capture) and [TelescopePlugin] (`ext.telescope.*`
/// extensions, auto [LogWatcher]), then registers [ExceptionWatcher] and
/// [DumpWatcher]: the two watchers telescope leaves opt-in but every Magic
/// dev session wants (uncaught exceptions and `debugPrint` dumps).
///
/// Each underlying install is idempotent, so a second call in the same
/// isolate is safe. Register additional watchers after this call via
/// [TelescopePlugin.registerWatcher].
static void installPre() {
DuskPlugin.install();

TelescopePlugin.install();
TelescopePlugin.registerWatcher(ExceptionWatcher());
TelescopePlugin.registerWatcher(DumpWatcher());
}

/// Post-`Magic.init()` half: wire Magic's runtime into both tools.
///
/// Installs [MagicTelescopeIntegration] (5 Magic watchers +
/// `MagicHttpFacadeAdapter` + the dusk-to-telescope bridge readers) and
/// [MagicDuskIntegration] (14 Magic-aware snapshot enrichers + the
/// [MagicRouter] navigate adapter). Both resolve their dependencies
/// through the IoC container, so [Magic.init] must have completed first.
///
/// Both integrations are idempotent.
static void installPost() {
MagicTelescopeIntegration.install();
MagicDuskIntegration.install();
}
}
Loading