diff --git a/app/api/track/route.ts b/app/api/track/route.ts index 9d73a90..77aa1e9 100644 --- a/app/api/track/route.ts +++ b/app/api/track/route.ts @@ -1,4 +1,5 @@ import { NextRequest, NextResponse } from "next/server" +import { revalidatePath } from "next/cache" import { architectureSchema, navigationSchema, stateManagementSchema } from "@/app/lib/config/schema" import { createPublishableSupabaseClient } from "@/app/lib/supabase/server" @@ -86,6 +87,10 @@ export async function POST(req: NextRequest) { return NextResponse.json({ error: "track_failed" }, { status: 503 }) } + // Bust the landing page ISR cache so the next visitor sees updated stats + // immediately rather than waiting up to 60 seconds. + revalidatePath("/") + return NextResponse.json({ ok: true }, { status: 202 }) } catch { return NextResponse.json({ error: "track_failed" }, { status: 503 }) diff --git a/app/components/landing/StatsSection.tsx b/app/components/landing/StatsSection.tsx index f685bac..7e30e97 100644 --- a/app/components/landing/StatsSection.tsx +++ b/app/components/landing/StatsSection.tsx @@ -1,3 +1,4 @@ +import { createPublishableSupabaseClient } from "@/app/lib/supabase/server" import { StatsShowcase, StatsShowcaseSkeleton } from "@/app/components/landing/StatsShowcase" type StatsResponse = { @@ -14,17 +15,15 @@ type StatsResponse = { dark_mode_enabled?: number } - async function getStats(): Promise { try { - const res = await fetch(`${process.env.NEXT_PUBLIC_VERCEL_URL}/api/stats`, { - // next: { - // revalidate: 60, - // tags: ["generator-stats"], - // }, - }) - if (!res.ok) return null - return (await res.json()) as StatsResponse + const supabase = createPublishableSupabaseClient() + const { data, error } = await supabase + .from("stats_summary") + .select("*") + .single() + if (error) return null + return data as StatsResponse } catch { return null } diff --git a/app/components/landing/WhyFlutterInit.tsx b/app/components/landing/WhyFlutterInit.tsx index f36de90..3c31c13 100644 --- a/app/components/landing/WhyFlutterInit.tsx +++ b/app/components/landing/WhyFlutterInit.tsx @@ -9,6 +9,7 @@ import { } from "@/components/ui/card"; import { cn } from '@/lib/utils'; import { + AiBrain01Icon, Clock01Icon, CpuIcon, DashboardSquare01Icon, @@ -25,8 +26,9 @@ const bentoClasses = [ "md:col-span-2 md:row-span-1", // Row 2: Item 3 "md:col-span-2 md:row-span-1", // Row 2: Item 4 "md:col-span-2 md:row-span-1", // Row 2: Item 5 - "md:col-span-3 md:row-span-1", // Row 3: Item 6 - "md:col-span-3 md:row-span-1", // Row 3: Item 7 + "md:col-span-2 md:row-span-1", // Row 3: Item 6 + "md:col-span-2 md:row-span-1", // Row 3: Item 7 + "md:col-span-2 md:row-span-1", // Row 3: Item 8 ] export function WhyFlutterInit() { @@ -66,6 +68,11 @@ export function WhyFlutterInit() { glow: "from-cyan-500/10 to-transparent", chip: "bg-cyan-500/8 border-cyan-500/15", }, + violet: { + iconColor: "text-violet-500", + glow: "from-violet-500/10 to-transparent", + chip: "bg-violet-500/8 border-violet-500/15", + }, } as const; const features = [ @@ -117,6 +124,13 @@ export function WhyFlutterInit() { icon: Globe02Icon, accent: accents.cyan, label: "Localization" + }, + { + title: "AI-Ready Context", + description: "AGENTS.md, DESIGN.md, and Cursor rules ship with every project.", + icon: AiBrain01Icon, + accent: accents.violet, + label: "AI Assistants" } ]; @@ -151,8 +165,7 @@ export function WhyFlutterInit() {
{features.map((feature, index) => { - const isWideTile = index === 0 || index === 1 || index === 5 || index === 6; - const isCompactTile = index === 2 || index === 3 || index === 4; + const isWideTile = index === 0 || index === 1; return (
- FlutterInit Logo + + FlutterInit Logo 1.0 diff --git a/app/page.tsx b/app/page.tsx index b2c843b..7054a35 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -4,6 +4,11 @@ import { Footer } from "@/app/components/landing/Footer" import { StatsSection, StatsSectionSkeleton } from "@/app/components/landing/StatsSection" import { Suspense } from "react" +// Re-render this page (and re-fetch stats from Supabase) at most every 60 seconds. +// Without this, Next.js statically renders the page once at build time and the +// stats count would stay frozen forever regardless of new generations. +export const revalidate = 60 + export default function Page() { return (
diff --git a/docs/generated-output.md b/docs/generated-output.md index 6d6d099..78a1175 100644 --- a/docs/generated-output.md +++ b/docs/generated-output.md @@ -26,6 +26,9 @@ my_app/ │ └── utils/ # General utilities, logging, and error handling ├── test/ # Unit and Widget tests ├── pubspec.yaml # Flutter dependency management +├── AGENTS.md # AI/agent context (stack, architecture, safe zones) +├── DESIGN.md # Design system tokens and UI conventions +├── .cursor/rules/ # Cursor IDE rules (flutter-project.mdc) └── SETUP.md # Post-generation setup instructions ``` diff --git a/skills-lock.json b/skills-lock.json index 07cec29..caf1035 100644 --- a/skills-lock.json +++ b/skills-lock.json @@ -6,6 +6,12 @@ "sourceType": "github", "computedHash": "82588484b29316f3106185374cc6aedf409c38a0698d49f7034c9b5ec07a1aa7" }, + "hallmark": { + "source": "nutlope/hallmark", + "sourceType": "github", + "skillPath": "SKILL.md", + "computedHash": "a5c828f76d82801993fe9d9826720af459360720235e1a68e10290bcb1f6491e" + }, "markdown-to-html": { "source": "github/awesome-copilot", "sourceType": "github", diff --git a/templates/flutter/base/.cursor/rules/flutter-project.mdc.hbs b/templates/flutter/base/.cursor/rules/flutter-project.mdc.hbs new file mode 100644 index 0000000..dd0dc25 --- /dev/null +++ b/templates/flutter/base/.cursor/rules/flutter-project.mdc.hbs @@ -0,0 +1,132 @@ +--- +description: {{appName}} — FlutterInit stack, architecture, design conventions, and agent zones +alwaysApply: true +--- + +# {{appName}} — Cursor project context + +{{#if description}}{{description}}{{/if}} + +Generated by FlutterInit. Package: `{{packageId}}`. + +**Extended docs (read when changing setup or deep UI):** + +- **[AGENTS.md](AGENTS.md)** — full agent guide (duplicate of sections below + packages list) +- **[DESIGN.md](DESIGN.md)** — complete design system +- **[SETUP.md](SETUP.md)** — env, Firebase/Supabase/Appwrite, native config, permissions + +--- + +{{> llm/stack-summary}} + +--- + +{{> llm/architecture-rules}} + +--- + +{{> llm/state-management-rules}} + +--- + +{{> llm/navigation-rules}} + +--- + +{{#unless (eq backend.provider "none")}} +{{> llm/backend-rules}} + +--- +{{/unless}} + +{{> llm/networking-rules}} + +--- + +{{> llm/services-conventions}} + +--- + +{{> llm/design-quick-reference}} + +--- + +{{> llm/add-feature-workflow}} + +--- + +## File zones + +### Safe to modify + +| Path | Guidance | +|------|----------| +{{#if (or (eq architecture "clean") (eq architecture "feature-first"))}}| `lib/src/features/**` | Feature screens, widgets, domain/data/presentation |{{/if}} +{{#if (eq architecture "mvc")}}| `lib/src/views/**`, `lib/src/controllers/**` | UI and controllers |{{/if}} +{{#if (eq architecture "mvvm")}}| `lib/src/ui/**`, `lib/src/data/**` | UI and data for features |{{/if}} +{{#if (eq architecture "layer-first")}}| `lib/src/presentation/**`, `lib/src/domain/**`, `lib/src/data/**` | Layer slices |{{/if}} +| `lib/src/shared/widgets/**` | Shared UI components | +| `test/**` | Tests | + +### Modify with caution + +| Path | Why | +|------|-----| +| `lib/src/routing/app_router.dart` | All navigation | +| `lib/main.dart` | Init order (SDKs, dotenv, l10n) | +| `lib/src/config/app_config.dart` | Dio / SDK clients | +| `lib/src/theme/**` | Global design tokens | +| `lib/src/imports/**` | Barrel exports | +| `pubspec.yaml` | Dependencies | +{{#if flags.usesDotenv}}| `.env` | Secrets |{{/if}} + +### Do not touch + +| Path | Why | +|------|-----| +| `android/`, `ios/`, `web/`, desktop native trees | Platform config | +{{#if flags.usesDotenv}}| `.env` with production secrets | Security |{{/if}} +{{#if flags.usesFirebase}}| `google-services.json`, `GoogleService-Info.plist` | Firebase credentials |{{/if}} + +--- + +{{> llm/packages-list}} + +--- + +## Quick rules (always enforce) + +- Import: `package:{{flags.appSlug}}/src/imports/imports.dart` +- Services: `ClassName.instance` + `runTask()` → `FutureEither`; use `rootContext`, not `BuildContext` in services +- UI: `context.colors`, `context.textTheme`, `AppSpacing`, `AppBorders` — no hardcoded hex or magic padding +- Routes: only `lib/src/routing/app_router.dart` +- Network/backend: `lib/src/services/` + `app_config.dart` — never from widgets +- No second state-management or routing library +- No business logic in `build()`; no empty `catch` blocks +{{#if flags.isRiverpod}}- Riverpod: `ref.watch` in build only; `ref.read` in callbacks; no `@riverpod` codegen{{/if}} +{{#if flags.isBloc}}- Bloc: immutable state/events; delegate logic out of handlers{{/if}} +{{#if flags.isProvider}}- Provider: `watch` in build, `read` in callbacks{{/if}} +{{#if flags.isGetX}}- GetX: bindings; no GoRouter/AutoRoute mix{{/if}} +{{#if flags.isMobX}}- MobX: mutate via `@action`; run `build_runner` after store edits{{/if}} +{{#if flags.supportsLocalization}}- Copy: `easy_localization` + `assets/translations/*.json`{{/if}} + +--- + +## Verify after changes + +```bash +flutter pub get +flutter analyze +``` + +{{> llm/build-runner-note}} + +{{#if flags.supportsLocalization}} +```bash +flutter pub run easy_localization:generate -S assets/translations -O lib/src/core/i18n -o locale_keys.g.dart +``` +{{/if}} + +```bash +flutter test +``` diff --git a/templates/flutter/base/AGENTS.md.hbs b/templates/flutter/base/AGENTS.md.hbs new file mode 100644 index 0000000..d610f30 --- /dev/null +++ b/templates/flutter/base/AGENTS.md.hbs @@ -0,0 +1,108 @@ +# Agent guide — {{appName}} + +{{> llm/stack-summary}} + +--- + +{{> llm/architecture-rules}} + +--- + +{{> llm/state-management-rules}} + +--- + +{{> llm/navigation-rules}} + +--- + +{{#unless (eq backend.provider "none")}} +{{> llm/backend-rules}} + +--- +{{/unless}} + +{{> llm/networking-rules}} + +--- + +{{> llm/services-conventions}} + +--- + +{{> llm/packages-list}} + +--- + +{{> llm/add-feature-workflow}} + +--- + +## Safe to modify + +| Path | Guidance | +|------|----------| +{{#if (or (eq architecture "clean") (eq architecture "feature-first"))}}| `lib/src/features/**` | Feature screens, widgets, domain/data/presentation code |{{/if}} +{{#if (eq architecture "mvc")}}| `lib/src/views/**`, `lib/src/controllers/**` | UI and controllers for your features |{{/if}} +{{#if (eq architecture "mvvm")}}| `lib/src/ui/**`, `lib/src/data/**` | UI and data layer for your features |{{/if}} +{{#if (eq architecture "layer-first")}}| `lib/src/presentation/**`, `lib/src/domain/**`, `lib/src/data/**` | Layer slices for new behavior |{{/if}} +| `lib/src/shared/widgets/**` | Reusable UI components | +| `lib/src/shared/helpers/**` | App-wide helpers (when not generated-only) | +| `test/**` | Unit and widget tests | +| `README.md` | Project documentation | + +## Modify with caution + +| Path | Why | +|------|-----| +| `lib/src/routing/app_router.dart` | Central navigation — breaks deep links if wrong | +| `lib/main.dart` | Initialization order (Firebase, dotenv, localization, flavors) | +| `lib/src/config/app_config.dart` | SDK and HTTP client setup | +| `lib/src/theme/**` | Global visual system | +| `lib/src/imports/**` | Barrel exports — keep pattern consistent | +| `pubspec.yaml` | Dependency graph for the whole app | +{{#if flags.usesDotenv}}| `.env` | Secrets — never commit real values |{{/if}} + +## Do not touch + +| Path | Why | +|------|-----| +| `android/`, `ios/`, `web/`, desktop folders | Native project configuration | +{{#if flags.usesDotenv}}| `.env` with real secrets | Security |{{/if}} +{{#if flags.usesFirebase}}| `google-services.json`, `GoogleService-Info.plist` | Environment-specific credentials |{{/if}} + +--- + +## Verification after changes + +```bash +flutter pub get +flutter analyze +``` + +{{> llm/build-runner-note}} + +{{#if flags.supportsLocalization}} +### Localization + +After editing `assets/translations/*.json`: + +```bash +flutter pub run easy_localization:generate -S assets/translations -O lib/src/core/i18n -o locale_keys.g.dart +``` +{{/if}} + +```bash +flutter test +``` + +--- + +## Hard limits + +- Do not disable `flutter_lints` rules without an explanatory comment. +- Do not call backend or networking code directly from widgets. +- Do not commit `.env` files containing production secrets. +- Do not introduce a second state-management or routing library. +- For platform setup and API keys, use **[SETUP.md](SETUP.md)**. +- For UI tokens and spacing, use **[DESIGN.md](DESIGN.md)**. diff --git a/templates/flutter/base/DESIGN.md.hbs b/templates/flutter/base/DESIGN.md.hbs new file mode 100644 index 0000000..a9fe0aa --- /dev/null +++ b/templates/flutter/base/DESIGN.md.hbs @@ -0,0 +1,149 @@ +# Design system — {{appName}} + +This file documents the design conventions established at generation time. Consult it before changing UI code. + +--- + +## Theme overview + +- **Preset:** `{{theme.preset}}`{{#if flags.isCupertino}} (Cupertino-style widgets where applicable){{/if}} +- **Material 3:** {{#unless flags.isCupertino}}Primary app chrome uses Material 3 theming in `lib/src/theme/theme.dart`.{{else}}Cupertino preset — still uses shared tokens from `lib/src/theme/`.{{/unless}} +- **Dark mode:** {{#if flags.hasDarkMode}}Light and dark themes are supported{{#if theme.darkMode.system}}; app can follow system brightness{{/if}}. Use semantic colors — do not branch on brightness in widgets for basic surfaces.{{else}}Light mode only — avoid hardcoded colors that will not adapt if dark mode is added later.{{/if}} +- **Customization:** Global `ThemeData` lives in `lib/src/theme/theme.dart` — avoid one-off `ThemeData` overrides in feature widgets. + +{{#if theme.primaryColor}} +- **Seed color:** `{{theme.primaryColor}}` (used to derive `ColorScheme` via `ColorScheme.fromSeed` where applicable). +{{/if}} + +--- + +## Color system + +- Use `context.colors` (`ColorScheme`) for standard Material roles: `primary`, `onPrimary`, `secondary`, `surface`, `onSurface`, `error`, etc. +- Use `context.appColors` for semantic extensions: `success`, `warning`, `info`, and their `on*` / container variants (see `lib/src/theme/color_schemes.dart`). +- Definitions: `lib/src/theme/color_schemes.dart`, applied through `lib/src/theme/theme.dart`. +- **Do not** hardcode hex colors in widgets — use theme roles or `appColors`. + +--- + +## Typography + +- Use `context.textTheme` (alias `context.typography`) for all text styles. +- Roles follow Material 3: `display*`, `headline*`, `title*`, `body*`, `label*`. +{{#if flags.hasCustomFonts}} +### Custom fonts + +Primary family: **{{flags.primaryFontFamily}}** + +{{#each flags.fontFamilies}} +- **{{family}}** — {{fonts.length}} file(s) configured in `pubspec.yaml`{{#if @first}} (app-wide `fontFamily` when primary){{/if}} +{{/each}} + +Do not set raw `fontFamily:` strings in widgets — use `textTheme` roles or theme configuration in `lib/src/theme/text_theme.dart`. +{{else}} +- No custom fonts uploaded — platform default typography (Roboto / SF Pro) via Material/Cupertino theme. +{{/if}} + +--- + +## Spacing, borders, motion + +Import tokens via `package:{{flags.appSlug}}/src/theme/theme_constants.dart`. + +| Token class | Purpose | +|-------------|---------| +| `AppSpacing` | Padding, gaps (`xxs` … `xxxl`, plus `pagePadding`, `itemGap`, `cardPadding`) | +| `AppBorders` | Border radii (`xs` … `xl`, `button`, `card`, `input`, `dialog`, …) | +| `AppShadows` | Elevation shadows (`none`, `subtle`, `card`, `elevated`, `modal`) | +| `AppDurations` | Animation durations (`fast`, `normal`, `medium`, `slow`, …) | +| `AppCurves` | Standard curves (`standard`, `emphasized`, `pageEnter`, …) | + +**Rules** + +- Do not use magic numbers for padding — use `AppSpacing`. +- Do not use inline `BorderRadius.circular(n)` — use `AppBorders`. +- Prefer Material 3 tonal elevation on `Card` / `Surface` — custom shadows only via `AppShadows`. + +{{#if flags.usesScreenutil}} +--- + +## Responsive scaling (ScreenUtil) + +- `ScreenUtilInit` is already applied in the app wrapper — do not add another. +- Design size baseline: **390×844** (iPhone 14 class). +- In templates and new code, use the `res` convention: widths `.w`, heights `.h`, radius `.r`, font sizes `.sp` when ScreenUtil is enabled. +- ScreenUtil values are runtime — avoid `const` widgets that depend on `.w` / `.sp`. +{{else}} +--- + +## Responsive layout (no ScreenUtil) + +- Use `MediaQuery`, `LayoutBuilder`, `Flexible`, and `Expanded` for responsiveness. +- Fixed pixel sizes are acceptable only for elements that should not scale (e.g. icon touch targets defined by tokens). +{{/if}} + +--- + +## Context extensions + +Defined in `lib/src/extensions/context_extension.dart`: + +| Member | Use | +|--------|-----| +| `context.colors` | `ColorScheme` | +| `context.textTheme` / `context.typography` | Text styles | +| `context.appColors` | Semantic success/warning/info | +| `context.designTokens` | Theme extension tokens | +| `context.isDarkMode` | Brightness check | +| `context.width` / `context.height` | Screen size | +| `context.showAppDialog` | Dialog helper | +| `context.showTypedSnackBar` | Status snackbars | + +Prefer these shortcuts over repeating `Theme.of(context)` in widgets. + +--- + +## Component conventions + +- Reusable widgets belong in `lib/src/shared/widgets/`. +- Default style parameters to theme values, not hardcoded colors. +- Widget file names match the widget class (`primary_button.dart` → `PrimaryButton`). +- Do not read theme inside `const` constructors. + +{{#if flags.hasDarkMode}} +### Dark mode checklist + +- No raw `Colors.white` / `Colors.black` for surfaces — use `colorScheme` roles. +- Verify new screens in both light and dark theme. +{{/if}} + +{{#if flags.supportsLocalization}} +--- + +## Localization + +- User-visible strings use `easy_localization` — files in `assets/translations/`. +- Do not hardcode display strings in widgets. +- Add keys to JSON translation files, then run the generate command in **SETUP.md**. +- Use interpolation in JSON — do not concatenate translated strings in Dart. +{{/if}} + +--- + +## Do / Don't + +**Do** + +- `context.colors` / `context.appColors` for color +- `context.textTheme` for typography +- `AppSpacing`, `AppBorders`, `AppShadows`, `AppDurations`, `AppCurves` for layout and motion +- `showAppDialog` and shared widgets for consistent UX + +**Don't** + +- Hardcode hex colors or arbitrary font families in widgets +- Magic padding/radius numbers +- Duplicate theme definitions per screen +- Bypass design tokens for one-off “quick” UI + +For agent workflows and architecture boundaries, see **[AGENTS.md](AGENTS.md)**. diff --git a/templates/flutter/base/lib/src/imports/packages_imports.dart.hbs b/templates/flutter/base/lib/src/imports/packages_imports.dart.hbs index d803fcc..a2b0e06 100644 --- a/templates/flutter/base/lib/src/imports/packages_imports.dart.hbs +++ b/templates/flutter/base/lib/src/imports/packages_imports.dart.hbs @@ -32,7 +32,7 @@ export 'package:get/get_instance/get_instance.dart'; {{/if}} {{/if}} {{#if flags.isMobX}} -export 'package:mobx/mobx.dart' hide version, StringExtension, Action, Listener, Listenable; +export 'package:mobx/mobx.dart' hide version, StringExtension, Action, Listener, Listenable, Interceptor, Interceptors; export 'package:flutter_mobx/flutter_mobx.dart' hide version; {{/if}} {{#if (eq flags.routerPackage "go_router")}} diff --git a/templates/flutter/partials/llm/add-feature-workflow.hbs b/templates/flutter/partials/llm/add-feature-workflow.hbs new file mode 100644 index 0000000..6571e39 --- /dev/null +++ b/templates/flutter/partials/llm/add-feature-workflow.hbs @@ -0,0 +1,38 @@ +## How to add a new feature + +{{#if (eq architecture "clean")}} +1. Create domain entity and repository contract under `lib/src/features//domain/`. +2. Add use case(s) in `domain/usecases/` (single responsibility). +3. Add data model, datasource, and repository implementation under `data/`. +4. Wire state ({{stateManagement}}) under `presentation/`. +5. Build screens and widgets under `presentation/`. +6. Register the route in `lib/src/routing/app_router.dart`. +7. Export new public API only through existing barrel files if needed. +{{/if}} + +{{#if (eq architecture "feature-first")}} +1. Create `lib/src/features//` with model, service, state, and view folders as needed. +2. Keep all feature code inside that folder — use `shared/` only for truly global UI/utils. +3. Register the route in `lib/src/routing/app_router.dart`. +{{/if}} + +{{#if (eq architecture "mvc")}} +1. Add models under `lib/src/models/` (or feature subfolder if you introduce one). +2. Add or extend services in `lib/src/services/`. +3. Add a controller under `lib/src/controllers//`. +4. Add views under `lib/src/views//`. +5. Register the route in `lib/src/routing/app_router.dart`. +{{/if}} + +{{#if (eq architecture "mvvm")}} +1. Add data models and repository under `lib/src/data/`. +2. Add UI, view model / provider / bloc under `lib/src/ui//`. +3. Register the route in `lib/src/routing/app_router.dart`. +{{/if}} + +{{#if (eq architecture "layer-first")}} +1. Add domain entity and repository contract under `lib/src/domain/`. +2. Implement datasource/model/repository under `lib/src/data/`. +3. Add presentation (screens + state) under `lib/src/presentation/`. +4. Register the route in `lib/src/routing/app_router.dart`. +{{/if}} diff --git a/templates/flutter/partials/llm/architecture-rules.hbs b/templates/flutter/partials/llm/architecture-rules.hbs new file mode 100644 index 0000000..b595277 --- /dev/null +++ b/templates/flutter/partials/llm/architecture-rules.hbs @@ -0,0 +1,92 @@ +## Architecture (`{{architecture}}`) + +{{#if (eq architecture "clean")}} +```text +lib/src/ +├── features// +│ ├── data/ # datasources, models, repository impls +│ ├── domain/ # entities, repo contracts, use cases +│ └── presentation/ # screens, widgets, state ({{stateManagement}}) +├── routing/ +├── services/ +├── shared/ +└── theme/ +``` + +**Rules** + +- `domain/` is pure Dart — no Flutter imports, no imports from `data/`. +- `data/` implements contracts from `domain/` — never the reverse. +- Entities are not models — no `fromJson`/`toJson` on domain entities. +- Use cases expose a single responsibility (typically one public `call()`). +- `presentation/` talks to use cases or state layer — not datasources directly. +{{/if}} + +{{#if (eq architecture "feature-first")}} +```text +lib/src/ +├── features// # model, service, state, view per feature +├── shared/ # cross-feature widgets & utils only +├── routing/ +├── services/ +└── theme/ +``` + +**Rules** + +- Each feature is self-contained — no imports from another feature's internals. +- Shared code lives only under `lib/src/shared/`. +{{/if}} + +{{#if (eq architecture "mvc")}} +```text +lib/src/ +├── controllers/ # state & orchestration ({{stateManagement}}) +├── models/ +├── services/ # repositories & API access +├── views/ # screens & widgets +├── routing/ +└── theme/ +``` + +**Rules** + +- Views only talk to controllers — no direct service calls from widgets. +- Controllers orchestrate services — no Flutter widget imports in controllers. +- One controller per screen or cohesive flow. +{{/if}} + +{{#if (eq architecture "mvvm")}} +```text +lib/src/ +├── ui/ # screens, widgets, state ({{stateManagement}}) +├── data/ # models, repository impls +├── routing/ +├── services/ +└── theme/ +``` + +**Rules** + +- UI layer binds to view models / providers / controllers under `ui/`. +- Data access goes through repositories in `data/` or `services/`. +- No business logic in widget `build()` methods. +{{/if}} + +{{#if (eq architecture "layer-first")}} +```text +lib/src/ +├── presentation/ # screens, providers/blocs, widgets +├── domain/ # entities, repo contracts +├── data/ # models, datasources, repository impls +├── routing/ +├── services/ +└── theme/ +``` + +**Rules** + +- `domain/` has no Flutter or `data/` imports. +- `data/` implements `domain/` contracts only. +- `presentation/` depends on domain abstractions — not concrete datasources. +{{/if}} diff --git a/templates/flutter/partials/llm/backend-rules.hbs b/templates/flutter/partials/llm/backend-rules.hbs new file mode 100644 index 0000000..f4a0efa --- /dev/null +++ b/templates/flutter/partials/llm/backend-rules.hbs @@ -0,0 +1,30 @@ +## Backend ({{backend.provider}}) + +{{#if flags.usesFirebase}} +- All Firebase access goes through `lib/src/services/` and repository/datasource layers — never from widgets. +- Use `FirebaseAuth` / SDK instances via existing app initialization in `lib/main.dart` — do not re-initialize Firebase in features. +{{#if flags.usesFirebaseAuth}}- Auth: email/Google/phone options are pre-wired per generator config.{{/if}} +{{#if flags.usesFirebaseFirestore}}- Firestore: keep security rules enabled; map errors to domain failures in the data layer.{{/if}} +{{#if flags.usesFirebaseStorage}}- Storage: upload/download via repository or service abstractions.{{/if}} +- Native config: place `google-services.json` and `GoogleService-Info.plist` per **SETUP.md** — do not commit secrets. +{{/if}} + +{{#if flags.usesSupabase}} +- All Supabase calls go through services/repositories — not from UI. +- Assume Row Level Security on tables — never bypass RLS in client code. +- Session persistence is handled by the SDK — avoid manual token caching. +- Configure URL and anon key via `.env` / **SETUP.md** when `usesDotenv` is enabled. +{{/if}} + +{{#if flags.usesAppwrite}} +- Use the configured Appwrite client from app config/services — do not instantiate ad hoc clients in widgets. +- Auth and database flags follow generator options — extend services, not screens. +{{/if}} + +{{#if flags.usesCustomBackend}} +- HTTP/Dio client is configured in `lib/src/config/app_config.dart`. +- API calls belong in `lib/src/services/` (e.g. auth service) using the shared client. +- Base URL comes from environment / config — see **SETUP.md**. +{{/if}} + +For console setup (keys, native files, env), follow **[SETUP.md](SETUP.md)** — do not duplicate platform steps here. diff --git a/templates/flutter/partials/llm/build-runner-note.hbs b/templates/flutter/partials/llm/build-runner-note.hbs new file mode 100644 index 0000000..8c90e1c --- /dev/null +++ b/templates/flutter/partials/llm/build-runner-note.hbs @@ -0,0 +1,15 @@ +{{#if (or flags.isMobX (eq flags.routerPackage "auto_route") flags.usesHive flags.usesFirebase)}} +### Code generation + +This stack uses `build_runner` ({{#if flags.usesHive}}Hive, {{/if}}{{#if flags.isMobX}}MobX, {{/if}}{{#if (eq flags.routerPackage "auto_route")}}Auto Route, {{/if}}{{#if flags.usesFirebase}}Firebase, {{/if}}etc.): + +```bash +dart run build_runner build --delete-conflicting-outputs +``` + +Re-run after changing generated routes, MobX stores, Hive adapters, or Firebase-related generated code. +{{else}} +### Code generation + +No `build_runner` step is required for the selected stack. +{{/if}} diff --git a/templates/flutter/partials/llm/design-quick-ref.hbs b/templates/flutter/partials/llm/design-quick-ref.hbs new file mode 100644 index 0000000..8e6e27e --- /dev/null +++ b/templates/flutter/partials/llm/design-quick-ref.hbs @@ -0,0 +1,14 @@ +## Design system (summary) + +Full reference: **[DESIGN.md](DESIGN.md)** + +- **Theme:** `lib/src/theme/theme.dart` — global `ThemeData` only; no per-widget theme overrides. +- **Preset:** `{{theme.preset}}`{{#if flags.isCupertino}} (Cupertino){{/if}}{{#if theme.primaryColor}} · seed `{{theme.primaryColor}}`{{/if}} +- **Dark mode:** {{#if flags.hasDarkMode}}on{{#if theme.darkMode.system}} (system){{/if}}{{else}}off (light only){{/if}} +- **Colors:** `context.colors` (Material roles), `context.appColors` (`success`, `warning`, `info`) +- **Typography:** `context.textTheme` / `context.typography` — no raw `fontFamily` in widgets +{{#if flags.hasCustomFonts}}- **Fonts:** primary `{{flags.primaryFontFamily}}` (see `lib/src/theme/text_theme.dart`){{/if}} +- **Tokens:** `AppSpacing`, `AppBorders`, `AppShadows`, `AppDurations`, `AppCurves` from `lib/src/theme/theme_constants.dart` +- **Extensions:** `context_extension.dart` — `width`, `height`, `isDarkMode`, `showAppDialog`, `showTypedSnackBar` +{{#if flags.usesScreenutil}}- **ScreenUtil:** baseline 390×844; `.w` `.h` `.r` `.sp`; wrapper already in app — no second `ScreenUtilInit`{{else}}- **Layout:** `MediaQuery` / `LayoutBuilder` / `Flexible` — no ScreenUtil{{/if}} +- **Widgets:** reusable UI in `lib/src/shared/widgets/`; use tokens not magic numbers diff --git a/templates/flutter/partials/llm/design-quick-reference.hbs b/templates/flutter/partials/llm/design-quick-reference.hbs new file mode 100644 index 0000000..3f20fdb --- /dev/null +++ b/templates/flutter/partials/llm/design-quick-reference.hbs @@ -0,0 +1,14 @@ +## Design system (summary) + +Full reference: **[DESIGN.md](DESIGN.md)** + +- **Theme:** `lib/src/theme/theme.dart` — no per-widget `ThemeData` overrides +- **Tokens:** `package:{{flags.appSlug}}/src/theme/theme_constants.dart` → `AppSpacing`, `AppBorders`, `AppShadows`, `AppDurations`, `AppCurves` +- **Colors:** `context.colors` (Material roles); `context.appColors` (`success`, `warning`, `info` in `color_schemes.dart`) +- **Typography:** `context.textTheme` / `context.typography` — no raw `fontFamily` in widgets +{{#if theme.primaryColor}}- **Seed color:** `{{theme.primaryColor}}`{{/if}} +{{#if flags.hasCustomFonts}}- **Primary font:** {{flags.primaryFontFamily}}{{/if}} +{{#if flags.hasDarkMode}}- **Dark mode:** enabled{{#if theme.darkMode.system}} (system){{/if}} — use semantic colors, not `Colors.white`/`Colors.black`{{/if}} +{{#if flags.usesScreenutil}}- **ScreenUtil:** baseline 390×844; `.w` `.h` `.r` `.sp`; single `ScreenUtilInit` in app wrapper{{else}}- **Layout:** `MediaQuery` / `LayoutBuilder` / `Flexible` — no ScreenUtil{{/if}} +- **Extensions:** `lib/src/extensions/context_extension.dart` — `showAppDialog`, `showTypedSnackBar`, `width`/`height` +- **Widgets:** reusable UI in `lib/src/shared/widgets/` only diff --git a/templates/flutter/partials/llm/navigation-rules.hbs b/templates/flutter/partials/llm/navigation-rules.hbs new file mode 100644 index 0000000..278e6f6 --- /dev/null +++ b/templates/flutter/partials/llm/navigation-rules.hbs @@ -0,0 +1,27 @@ +## Navigation + +{{#if (eq flags.routerPackage "go_router")}} +- All routes are defined in `lib/src/routing/app_router.dart` — do not scatter route tables elsewhere. +- Prefer typed/named routes from the central config — avoid hard-coded path strings in widgets when a named route exists. +- `context.go()` replaces the stack; `context.push()` pushes on top. +- Redirects and guards belong in the router configuration, not inside individual screens. +{{/if}} + +{{#if (eq flags.routerPackage "auto_route")}} +- Annotate routable pages with `@RoutePage()`. +- Routes are code-generated — run `build_runner` after adding, renaming, or removing pages. +- Use `context.router.push()`, `context.router.replace()`, and `context.router.pop()` for navigation. +- Route definitions live in `lib/src/routing/app_router.dart` and generated `app_router.gr.dart`. +{{/if}} + +{{#if (eq flags.routerPackage "getx")}} +- Pages and bindings are registered via `AppRouter.getPages` in `lib/src/routing/app_router.dart`. +- Use GetX navigation APIs consistent with the existing router setup. +- Do not mix in a second routing package alongside GetX pages. +{{/if}} + +{{#unless flags.routerPackage}} +- Imperative navigation uses `AppRouter.onGenerateRoute` in `lib/src/routing/app_router.dart`. +- Use `Navigator` APIs / extension helpers on `BuildContext` from `context_extension.dart`. +- Register new screens in the central router — not ad hoc `MaterialPageRoute` factories in widgets. +{{/unless}} diff --git a/templates/flutter/partials/llm/networking-rules.hbs b/templates/flutter/partials/llm/networking-rules.hbs new file mode 100644 index 0000000..0646b0a --- /dev/null +++ b/templates/flutter/partials/llm/networking-rules.hbs @@ -0,0 +1,16 @@ +## Networking + +{{#if flags.usesDio}} +- Use the shared `Dio` instance from `lib/src/config/app_config.dart` — never `Dio()` inside a widget or screen. +- Extend or use existing services under `lib/src/services/` for HTTP calls. +- Map transport errors to `Failure` / `FutureEither` results via `runTask()` — do not leak raw `DioException` to UI. +{{/if}} + +{{#if (and flags.usesHttp (not flags.usesDio))}} +- Use the HTTP service exported from `lib/src/services/` — do not call `http.get` directly from presentation code. +- Handle timeouts and socket errors explicitly in the service layer. +{{/if}} + +{{#unless (or flags.usesDio flags.usesHttp)}} +- No HTTP client package is enabled — use mock/local data patterns already in the template or add Dio/HTTP via `pubspec.yaml` deliberately. +{{/unless}} diff --git a/templates/flutter/partials/llm/packages-list.hbs b/templates/flutter/partials/llm/packages-list.hbs new file mode 100644 index 0000000..e073876 --- /dev/null +++ b/templates/flutter/partials/llm/packages-list.hbs @@ -0,0 +1,26 @@ +## Key packages + +{{#if (eq flags.routerPackage "go_router")}}- `go_router` — declarative routing{{/if}} +{{#if (eq flags.routerPackage "auto_route")}}- `auto_route` — code-generated routing{{/if}} +{{#if flags.isGetX}}- `get` — state & navigation{{/if}} +{{#if flags.isRiverpod}}- `flutter_riverpod` — state management{{/if}} +{{#if flags.isProvider}}- `provider` — state management{{/if}} +{{#if flags.isBloc}}- `flutter_bloc` — state management{{/if}} +{{#if flags.isMobX}}- `mobx`, `flutter_mobx` — state management{{/if}} +{{#if flags.usesFirebase}}- `firebase_core`{{#if flags.usesFirebaseAuth}}, `firebase_auth`{{/if}}{{#if flags.usesFirebaseFirestore}}, `cloud_firestore`{{/if}}{{#if flags.usesFirebaseStorage}}, `firebase_storage`{{/if}}{{/if}} +{{#if flags.usesSupabase}}- `supabase_flutter`{{/if}} +{{#if flags.usesAppwrite}}- `appwrite`{{/if}} +{{#if flags.usesDio}}- `dio`{{/if}} +{{#if (and flags.usesHttp (not flags.usesDio))}}- `http`{{/if}} +{{#if flags.usesHive}}- `hive`, `hive_flutter`{{/if}} +{{#if flags.usesSharedPreferences}}- `shared_preferences`{{/if}} +{{#if flags.usesSecureStorage}}- `flutter_secure_storage`{{/if}} +{{#if flags.usesCachedNetworkImage}}- `cached_network_image`{{/if}} +{{#if flags.usesGeolocator}}- `geolocator`{{/if}} +{{#if flags.usesImagePicker}}- `image_picker`{{/if}} +{{#if flags.usesFilePicker}}- `file_picker`{{/if}} +{{#if flags.supportsLocalization}}- `easy_localization`{{/if}} +{{#if flags.usesScreenutil}}- `flutter_screenutil`{{/if}} +{{#if flags.usesFlutterHooks}}- `flutter_hooks`{{/if}} +{{#if flags.usesDotenv}}- `flutter_dotenv`{{/if}} +{{#if flags.usesSkeletonizer}}- `skeletonizer`{{/if}} diff --git a/templates/flutter/partials/llm/services-conventions.hbs b/templates/flutter/partials/llm/services-conventions.hbs new file mode 100644 index 0000000..1abc871 --- /dev/null +++ b/templates/flutter/partials/llm/services-conventions.hbs @@ -0,0 +1,20 @@ +## Services & shared conventions + +- Services are singletons with `ClassName.instance`, returning `FutureEither` via `runTask()`. +- Never pass `BuildContext` into services — use `rootContext` from the global navigator helper when UI is required. +- Logging: `AppLogger`; user feedback: `showGlobalToast()`; dialogs: `showAppDialog()` / `context.showAppDialog()`. +- Primary import barrel: `package:{{flags.appSlug}}/src/imports/imports.dart`. +- File names: `snake_case.dart`; classes: `PascalCase`; private members: `_camelCase`. +- Prefer `const` constructors where possible; avoid `dynamic` without justification. +- No empty `catch` blocks — log, map to failure, or rethrow. +- Do not add packages that duplicate existing stack choices (routing, state, backend). + +### Dart / Flutter anti-patterns + +- No business logic in widget `build()` methods. +- No direct backend or network calls from presentation widgets. +{{#if flags.isRiverpod}}- Do not use `ref.read` inside `build()`.{{/if}} +{{#if flags.isBloc}}- Do not put heavy logic inside bloc event handlers — delegate outward.{{/if}} +{{#if flags.isProvider}}- Do not use `context.watch` inside callbacks.{{/if}} +{{#if flags.isGetX}}- Do not mix GetX routing with GoRouter/AutoRoute in the same app.{{/if}} +{{#if flags.isMobX}}- Do not mutate `@observable` fields outside `@action` methods.{{/if}} diff --git a/templates/flutter/partials/llm/stack-summary.hbs b/templates/flutter/partials/llm/stack-summary.hbs new file mode 100644 index 0000000..7b7aadd --- /dev/null +++ b/templates/flutter/partials/llm/stack-summary.hbs @@ -0,0 +1,22 @@ +## Project + +**{{appName}}**{{#if description}} — {{description}}{{/if}} + +Generated by [FlutterInit](https://flutterinit.com). Package: `{{packageId}}`. + +### Stack + +| Area | Choice | +|------|--------| +| Architecture | `{{architecture}}` | +| State management | `{{stateManagement}}` | +| Navigation | `{{navigation}}` | +| Backend | `{{backend.provider}}` | +| Networking | {{#if flags.usesDio}}Dio{{else if flags.usesHttp}}HTTP{{else}}—{{/if}} | +| Local storage | {{#if flags.usesHive}}Hive{{/if}}{{#if (and flags.usesHive flags.usesSharedPreferences)}}, {{/if}}{{#if flags.usesSharedPreferences}}SharedPreferences{{/if}}{{#if (and (or flags.usesHive flags.usesSharedPreferences) flags.usesSecureStorage)}}, {{/if}}{{#if flags.usesSecureStorage}}Secure storage{{/if}}{{#unless (or flags.usesHive flags.usesSharedPreferences flags.usesSecureStorage)}}—{{/unless}} | +| Theme preset | `{{theme.preset}}` | +| Dark mode | {{#if flags.hasDarkMode}}enabled{{#if theme.darkMode.system}} (follows system){{/if}}{{else}}light only{{/if}} | +| ScreenUtil | {{#if flags.usesScreenutil}}yes{{else}}no{{/if}} | +| Localization | {{#if flags.supportsLocalization}}`easy_localization` ({{#each flags.supportedLocales}}{{this}}{{#unless @last}}, {{/unless}}{{/each}}){{else}}disabled{{/if}} | +| Flutter Hooks | {{#if flags.usesFlutterHooks}}enabled{{else}}disabled{{/if}} | +| Dotenv | {{#if flags.usesDotenv}}enabled{{else}}disabled{{/if}} | diff --git a/templates/flutter/partials/llm/state-management-rules.hbs b/templates/flutter/partials/llm/state-management-rules.hbs new file mode 100644 index 0000000..d2d680f --- /dev/null +++ b/templates/flutter/partials/llm/state-management-rules.hbs @@ -0,0 +1,40 @@ +## State management ({{stateManagement}}) + +{{#if flags.isRiverpod}} +- Use `ConsumerWidget` / `ConsumerStatefulWidget` where reactive state is needed. +- Providers live next to features (see overlay paths under `presentation/providers/` or equivalent). +- `ref.watch` inside `build()`; `ref.read` inside callbacks and one-off actions — never `ref.read` in `build()`. +- Do not nest a second `ProviderScope` — one at app root is already configured. +- This project does **not** use `@riverpod` / `riverpod_generator` — define providers manually. +{{/if}} + +{{#if flags.isBloc}} +- Events and states are immutable — prefer `const` constructors. +- `BlocBuilder` for UI rebuilds; `BlocListener` for side effects; `BlocConsumer` when both are needed. +- Keep handlers thin — delegate to use cases, repositories, or services. +- One bloc per feature flow — do not share blocs across unrelated features. +{{/if}} + +{{#if flags.isProvider}} +- Call `notifyListeners()` after every state change in `ChangeNotifier`s. +- `context.watch` in `build()`; `context.read` in callbacks — never reversed. +- Scope providers at the appropriate subtree — not everything belongs at app root. +{{/if}} + +{{#if flags.isGetX}} +- Controllers extend `GetxController`; reactive fields use `.obs` with `Obx()`. +- Register controllers via bindings / route setup — avoid inline `Get.put()` inside widgets. +- `GetMaterialApp` is configured when navigation uses GetX — do not add a second material app wrapper. +{{/if}} + +{{#if flags.isMobX}} +- Annotate state with `@observable`; mutations with `@action`; derived values with `@computed`. +- Wrap reactive UI in `Observer`. +- Run `dart run build_runner build --delete-conflicting-outputs` after changing any store. +{{/if}} + +{{#if flags.isNoneState}} +- Session and feature state use manager / `ChangeNotifier` classes in architecture overlay paths. +- Keep state classes free of widget imports. +- Prefer explicit loading/error fields over silent failures. +{{/if}} diff --git a/tests/unit/llm-context.spec.ts b/tests/unit/llm-context.spec.ts new file mode 100644 index 0000000..2930b13 --- /dev/null +++ b/tests/unit/llm-context.spec.ts @@ -0,0 +1,150 @@ +import { describe, expect, it } from "vitest" + +import type { ScaffoldConfig } from "@/app/lib/config/schema" +import { defaultBackendConfig } from "@/app/lib/config/schema" +import { + assertNoUnresolvedTokens, + assertRequiredFilesExist, +} from "../utils/assertions" +import { buildConfig } from "../utils/config-builder" +import { generateToMap, getFile } from "../utils/generate" +import type { PrimaryCombo } from "../utils/matrix.config" + +function buildLlmConfig(overrides: Partial = {}): ScaffoldConfig { + const base = buildConfig({ + architecture: "clean", + stateManagement: "riverpod", + navigation: "go_router", + backend: "supabase", + }) + return { ...base, ...overrides } +} + +describe("LLM context files", () => { + it("emits AGENTS.md, DESIGN.md, and Cursor rule on every generation", async () => { + const files = await generateToMap(buildLlmConfig()) + assertRequiredFilesExist(files) + assertNoUnresolvedTokens(files) + + expect(getFile(files, "AGENTS.md")).toBeDefined() + expect(getFile(files, "DESIGN.md")).toBeDefined() + expect(getFile(files, ".cursor/rules/flutter-project.mdc")).toBeDefined() + }) + + it("renders stack-specific content for clean + riverpod + supabase + go_router", async () => { + const agents = getFile( + await generateToMap(buildLlmConfig()), + "AGENTS.md" + )! + expect(agents).toContain("clean") + expect(agents).toContain("riverpod") + expect(agents).toContain("supabase") + expect(agents).toContain("go_router") + expect(agents).toContain("lib/src/features/") + expect(agents).toContain("ref.watch") + expect(agents).toContain("Supabase") + }) + + it("renders mvc + bloc + firebase content", async () => { + const config = buildLlmConfig({ + architecture: "mvc", + stateManagement: "bloc", + navigation: "auto_route", + backend: defaultBackendConfig("firebase"), + }) + const agents = getFile(await generateToMap(config), "AGENTS.md")! + expect(agents).toContain("mvc") + expect(agents).toContain("lib/src/controllers/") + expect(agents).toContain("BlocBuilder") + expect(agents).toContain("Firebase") + expect(agents).toContain("build_runner") + }) + + it("omits backend section when provider is none", async () => { + const config = buildLlmConfig({ + architecture: "feature-first", + stateManagement: "getx", + navigation: "getx", + backend: { provider: "none" }, + }) + const agents = getFile(await generateToMap(config), "AGENTS.md")! + expect(agents).toContain("feature-first") + expect(agents).not.toContain("## Backend (none)") + expect(agents).not.toContain("Row Level Security") + }) + + it("DESIGN.md includes ScreenUtil when enabled", async () => { + const withUtil = getFile( + await generateToMap( + buildLlmConfig({ + misc: { ...buildLlmConfig().misc, usesScreenutil: true }, + }) + ), + "DESIGN.md" + )! + expect(withUtil).toContain("ScreenUtil") + expect(withUtil).toContain("390×844") + + const withoutUtil = getFile( + await generateToMap( + buildLlmConfig({ + misc: { ...buildLlmConfig().misc, usesScreenutil: false }, + }) + ), + "DESIGN.md" + )! + expect(withoutUtil).not.toContain("ScreenUtilInit") + expect(withoutUtil).toContain("Responsive layout") + }) + + it("DESIGN.md includes primary color from config", async () => { + const design = getFile(await generateToMap(buildLlmConfig()), "DESIGN.md")! + expect(design).toContain("#6750A4") + }) + + it("Cursor rule has alwaysApply frontmatter and embedded stack details", async () => { + const mdc = getFile( + await generateToMap(buildLlmConfig()), + ".cursor/rules/flutter-project.mdc" + )! + expect(mdc).toMatch(/^---/) + expect(mdc).toContain("alwaysApply: true") + expect(mdc).toContain("AGENTS.md") + expect(mdc).toContain("DESIGN.md") + expect(mdc).toContain("SETUP.md") + // Embedded context (not only pointers) + expect(mdc).toContain("Architecture (`clean`)") + expect(mdc).toContain("lib/src/features/") + expect(mdc).toContain("State management (riverpod)") + expect(mdc).toContain("ref.watch") + expect(mdc).toContain("AppSpacing") + expect(mdc).toContain("Safe to modify") + expect(mdc).toContain("go_router") + }) + + it("does not mention build_runner when stack does not need codegen", async () => { + const config = buildLlmConfig({ + stateManagement: "provider", + navigation: "go_router", + backend: { provider: "none" }, + misc: { + ...buildLlmConfig().misc, + usesHive: false, + }, + }) + const agents = getFile(await generateToMap(config), "AGENTS.md")! + expect(agents).toContain("No `build_runner` step is required") + }) + + it("passes token check across critical architecture combos", async () => { + const combos: PrimaryCombo[] = [ + { architecture: "layer-first", stateManagement: "mobx", navigation: "auto_route", backend: "firebase" }, + { architecture: "mvvm", stateManagement: "none", navigation: "imperative", backend: "none" }, + ] + for (const combo of combos) { + const files = await generateToMap(buildConfig(combo)) + assertNoUnresolvedTokens(files) + expect(getFile(files, "AGENTS.md")).toBeDefined() + } + }) +}) diff --git a/tests/unit/state-management.spec.ts b/tests/unit/state-management.spec.ts index eec93f7..78b4944 100644 --- a/tests/unit/state-management.spec.ts +++ b/tests/unit/state-management.spec.ts @@ -93,6 +93,26 @@ describe("State Management", () => { expect(pubspec).toContain("build_runner:") expect(pubspec).toContain("mobx_codegen:") }) + + it("hides mobx Interceptor when dio is enabled (avoids ambiguous export)", async () => { + const mobxDioFiles = await generateToMap( + buildConfig( + { + architecture: "mvc", + stateManagement: "mobx", + backend: "custom", + navigation: "imperative", + }, + MISC_DEFAULT, + ), + ) + assertFileContains( + mobxDioFiles, + "lib/src/imports/packages_imports.dart", + "hide version, StringExtension, Action, Listener, Listenable, Interceptor, Interceptors", + ) + assertFileContains(mobxDioFiles, "lib/src/imports/packages_imports.dart", "export 'package:dio/dio.dart'") + }) }) // ── None (setState) ───────────────────────────────────────── diff --git a/tests/utils/assertions.ts b/tests/utils/assertions.ts index 3e0167d..1838aa8 100644 --- a/tests/utils/assertions.ts +++ b/tests/utils/assertions.ts @@ -128,6 +128,9 @@ const ALWAYS_REQUIRED_FILES = [ "pubspec.yaml", "lib/main.dart", "analysis_options.yaml", + "AGENTS.md", + "DESIGN.md", + ".cursor/rules/flutter-project.mdc", ] export function assertRequiredFilesExist(files: Map): void {