diff --git a/.gitignore b/.gitignore index 4c4ddfd..76c4bfa 100644 --- a/.gitignore +++ b/.gitignore @@ -48,3 +48,6 @@ lcov.info .firebase .fvmrc .supermaven + +# skill-creator run_loop / eval workspaces (ephemeral artifacts) +solid-workspace/ diff --git a/analysis_options.yaml b/analysis_options.yaml index 23a838e..6c98d8a 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -1,5 +1,10 @@ include: package:very_good_analysis/analysis_options.yaml analyzer: + exclude: + # Solid skill eval fixtures: template project skeletons used by + # skill-creator's eval runner. They reference flutter_lints, which the + # workspace doesn't resolve, and they aren't real source to be analyzed. + - skills/solid/evals/files/** errors: always_put_required_named_parameters_first: ignore package_names: ignore diff --git a/docs/src/content/docs/guides/getting-started.mdx b/docs/src/content/docs/guides/getting-started.mdx index 10a3fad..ae9d971 100644 --- a/docs/src/content/docs/guides/getting-started.mdx +++ b/docs/src/content/docs/guides/getting-started.mdx @@ -70,11 +70,39 @@ import { Steps } from '@astrojs/starlight/components'; ``` - +If you're using an AI coding assistant (Claude Code, Cursor, Codex, GitHub Copilot, Amp, OpenHands, etc.), these three steps teach it the inverted `source/`-vs-`lib/` rule and Solid's annotation contract — so it stops trying to edit `lib/` and silently losing your work. + + + +1. Drop `AGENTS.md` at your app root. + + Most AI coding tools auto-load `AGENTS.md` at session start, so the rule applies to every interaction — even short or routine-looking ones — without depending on skill triggering. + + ```bash + curl -o AGENTS.md https://raw.githubusercontent.com/nank1ro/solid/main/skills/solid/assets/AGENTS.md + ``` + + If your tool only looks for `CLAUDE.md`, symlink it: + + ```bash + ln -s AGENTS.md CLAUDE.md + ``` + +2. Install the Solid skill. + + ```bash + npx skills add nank1ro/solid + ``` + + Or copy `skills/solid/` from the [Solid repo](https://github.com/nank1ro/solid) into your editor's skill location. The skill gives the agent deeper guidance (full annotation contract, scripts, troubleshooting) when it triggers on Solid-related work. `AGENTS.md` is the always-loaded baseline; the skill is the on-demand reference. + +3. Point HTTP-docs tools at the LLM-friendly bundle (optional). + + If your tool fetches documentation over HTTP (Cursor `@docs`, ChatGPT custom GPTs, claude.ai web search, …), point it at [`/llms-full.txt`](https://solid.mariuti.com/llms-full.txt) — the full docs as a single LLM-friendly file. A short index lives at [`/llms.txt`](https://solid.mariuti.com/llms.txt). + + ## How it works @@ -136,6 +164,25 @@ dart run build_runner watch --delete-conflicting-outputs - Press `r` in the `flutter run` terminal after `build_runner` emits. - Use [`dashmonx`](https://pub.dev/packages/dashmonx), which wraps `flutter run` and triggers hot reload automatically when files under `lib/` change. Any `flutter run` flag passes through, e.g. `dashmonx -d chrome` for a web target. +### Clean up after generation + +Solid prioritises producing correct, runnable code over polishing it. The generated `lib/` output may miss some `const` opportunities, leave unused imports, or pick a non-preferred import form. Run `dart fix` after `build_runner` to apply the lint-driven fixes (`prefer_const_constructors`, `unnecessary_import`, `prefer_relative_imports`, …): + +```bash +dart fix --apply +``` + +In CI, chain the two commands so the generated output is always lint-clean: + +```bash +dart run build_runner build --delete-conflicting-outputs +dart fix --apply +``` + + + diff --git a/example/analysis_options.yaml b/example/analysis_options.yaml index edf66c2..a395964 100644 --- a/example/analysis_options.yaml +++ b/example/analysis_options.yaml @@ -6,6 +6,7 @@ analyzer: invalid_annotation_target: ignore avoid_print: ignore unnecessary_statements: ignore + always_use_package_imports: ignore linter: rules: public_member_api_docs: false diff --git a/example/lib/main.dart b/example/lib/main.dart index 3b90314..f570eb3 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -1,7 +1,8 @@ -import 'package:example/counter.dart'; import 'package:flutter/material.dart'; import 'package:flutter_solidart/flutter_solidart.dart'; +import 'counter.dart'; + void main() { SolidartConfig.autoDispose = false; runApp(const MaterialApp(home: CounterPage())); diff --git a/example/source/main.dart b/example/source/main.dart index 3b90314..bb233b9 100644 --- a/example/source/main.dart +++ b/example/source/main.dart @@ -1,8 +1,9 @@ -import 'package:example/counter.dart'; import 'package:flutter/material.dart'; import 'package:flutter_solidart/flutter_solidart.dart'; +import 'counter.dart'; + void main() { SolidartConfig.autoDispose = false; - runApp(const MaterialApp(home: CounterPage())); + runApp(MaterialApp(home: CounterPage())); } diff --git a/skills/solid/SKILL.md b/skills/solid/SKILL.md index aba602f..2a24862 100644 --- a/skills/solid/SKILL.md +++ b/skills/solid/SKILL.md @@ -1,59 +1,127 @@ --- name: solid -description: Use when writing Flutter code that uses the Solid framework — annotations like @SolidState, @SolidEffect, @SolidQuery, @SolidEnvironment in source/ files. Solid is a codegen layer where source/ is the source of truth and lib/ is generated. +description: > + PRIORITY — read this skill FIRST before writing Dart code when `pubspec.yaml` + declares `solid_annotations` or `solid_generator`. In these projects Flutter + conventions are inverted: `lib/` is build_runner output from `source/`; + editing `lib/` is destroyed on next build. Without this skill you WRITE TO + THE WRONG DIRECTORY and silently lose work. Use whenever ANY of these appear: + `solid_annotations`/`solid_generator` in pubspec; + `@SolidState`/`@SolidEffect`/`@SolidQuery`/`@SolidEnvironment`/`.untracked` + in the user's message or code; "my lib/ edits keep disappearing"; + "add/scaffold a widget/page/route/model/controller" in such a project; + reactive patterns (debounce, "when X changes fetch Y", "make reactive"); or + installing pub packages (go_router, freezed, riverpod, drift, + json_serializable) whose docs say `lib/` — substitute `source/`. Annotation + contract (`@SolidQuery` no params, `@SolidEnvironment` needs `late`) is + non-obvious. NOT for SolidJS/Solid.js or Flutter projects without those + packages. --- # Solid (Flutter) -Solid is a tiny framework on top of Flutter. You write reactive state directly on `StatelessWidget` in `source/`; the `solid_generator` `build_runner` builder transpiles each `source/.dart` to `lib/.dart`. Inspired by SwiftUI (`@Environment`) and SolidJS (fine-grained reactivity). Backed by [`flutter_solidart`](https://pub.dev/packages/flutter_solidart). +Solid is a tiny framework on top of Flutter. You write reactive state directly on a `StatelessWidget` in `source/`; the `solid_generator` build_runner builder transpiles each `source/.dart` into `lib/.dart`, turning your class into a `StatefulWidget` with `Signal`/`Computed`/`Effect`/`Resource` plumbing from [`flutter_solidart`](https://pub.dev/packages/flutter_solidart). Inspired by SwiftUI (`@Environment`) and SolidJS (fine-grained reactivity). + +User-facing docs: . + +## Step 0 — Is this project Solid? + +Before doing anything, decide if the project actually uses Solid. The user may not say "Solid" anywhere — and an existing `lib/` directory plus `solid_annotations`/`solid_generator` not being in pubspec means this is *not* a Solid project. + +Run this check first: + +```bash +grep -E '^\s*(solid_annotations|solid_generator):' pubspec.yaml +``` + +- **Match found** → this is a Solid project. Follow this skill. +- **No match** → not a Solid project. Don't apply Solid conventions. Skip this skill. + +The same check via Read tool works too: read `pubspec.yaml` and look for either package name under `dependencies:` or `dev_dependencies:`. A Solid setup typically has `solid_annotations` (runtime) under `dependencies` and `solid_generator` plus `build_runner` under `dev_dependencies`. ## Cardinal rule -**Always edit `source/`. Never edit `lib/` by hand.** `lib/` is regenerated by `dart run build_runner build` / `watch` and any manual edits are lost. If you need to add a widget, create the file in `source/`, then run `build_runner`. The user-facing app entry point is `source/main.dart`; `lib/main.dart` is generated. +**Edit `source/.dart`. Never edit `lib/.dart`.** + +In Flutter, `lib/` is where you write code. In Solid, `lib/` is generated output — every time build_runner runs, it overwrites `lib/.dart` from `source/.dart`. Hand-edits to `lib/` are lost on the next build. The user-facing entry point is `source/main.dart`; `lib/main.dart` is generated. + +This inverts Flutter muscle memory. The rest of the Flutter ecosystem (`flutter run`, pub packages, IDE templates, every tutorial on the internet) assumes `lib/` is the source of truth. In this project it isn't. Apply the substitution everywhere. + +## Decision shortcuts + +| Situation | What to do | +| --- | --- | +| About to write to a file under `lib/` | **Stop.** Find or create the matching `source/.dart` and write there. | +| User asks to "add a widget / page / button / form" | Create `source/.dart`. Use `@SolidState` for fields you'll mutate. | +| User asks to "fetch X when Y changes" | `@SolidState` for the input Y, `@SolidQuery` (no parameters) for the fetch. Body reads Y to register the dependency. | +| User asks to install a pub package whose README says "create `lib/.dart`" | Substitute `source/` for `lib/` in every file-creation step. See `references/third-party-packages.md`. | +| User says "I edited `lib/foo.dart` and the change disappeared" | The `lib/` write is the bug, not build_runner. Migrate the change to `source/foo.dart`, then regenerate. | +| build_runner output looks unpolished (no `const`, unused imports) | Run `scripts/verify.sh`, which chains `dart fix --apply` after build_runner. | +| build_runner fails | Run `scripts/verify.sh` from the package root — it surfaces the first `[SEVERE]` line. | + +## Third-party packages: substitute `source/` for `lib/` + +When the user (or another AI) installs a new pub package, the package's README, examples, and any AI-generated setup instructions will all assume `lib/` is the source of truth. In a Solid project that assumption is wrong. -**Same-package imports must be relative.** Inside a `source/` file, reference other source files via relative paths (`../controllers/foo.dart`), never via `package:/foo.dart`. The `package:` form resolves to `lib/` (the generated realm), pointing a source file at the lowered Signal types — and the generator now rejects it at build time. Cross-package imports (`flutter`, `solid_annotations`, `provider`, third-party) keep the `package:` form as usual. +**Rule of thumb**: the package itself stays in `pubspec.yaml` as the docs describe. Only the *files you write that import it* move from `lib/` to `source/`. Examples: + +- `go_router` README says "create `lib/router.dart`" → create `source/router.dart` instead, and import it from `source/main.dart` via a relative path. +- `freezed` says "create `lib/models/user.dart`" → create `source/models/user.dart`. Freezed's own `*.freezed.dart` generated output still lands wherever `build.yaml` puts it (typically next to the source file under `lib/`, since freezed reads from `lib/`); **but** in a Solid project you want freezed to read from `source/` too. Add `source/**` to freezed's `build.yaml` `sources` list (Solid's setup already does this — keep it). +- `riverpod` generator says "create `lib/providers/...`" → create `source/providers/...`. +- `drift` says "create `lib/database.dart`" → create `source/database.dart`. + +For the full list and per-package gotchas, read `references/third-party-packages.md`. + +**The key thing to tell yourself**: "the docs say `lib/`. In this project, that means `source/`." + +## Same-package imports must be relative + +Inside a `source/` file, reference other source files via relative paths (`../controllers/foo.dart`), never via `package:/foo.dart`. The `package:` form resolves to `lib/` (the *generated* realm), pointing your source file at lowered Signal types — the generator now rejects it at build time. Cross-package imports (`flutter`, `solid_annotations`, `provider`, third-party) keep the `package:` form as usual. ## Annotation cheat sheet Each annotation goes on a class member of a `StatelessWidget` (or any class — `@SolidEnvironment` also works in `State`). The generator turns the widget into a `StatefulWidget` under the hood. - **`@SolidState()`** — reactive state. Docs: . - - Valid on: instance field with initializer, `late` non-nullable instance field, instance getter (derived state). + - Valid on: instance field with initializer, `late` non-nullable instance field, nullable instance field, instance getter (derived state). - Invalid: `final`, `const`, `static`, setter, method, top-level. - - Example: `@SolidState() int counter = 0;` or `@SolidState() int get doubleCounter => counter * 2;` + - Example: `@SolidState() int counter = 0;` or `@SolidState() int get doubleCounter => counter * 2;`. - **`@SolidEffect()`** — side effect that re-runs whenever its tracked dependencies change. Docs: . - Valid on: instance method returning `void`. - - Example: `@SolidEffect() void logCounter() { print('Counter: $counter'); }` + - Example: `@SolidEffect() void logCounter() { print('Counter: $counter'); }`. - **`@SolidQuery()`** — reactive async/stream resource. Docs: . - Valid on: instance method returning `Future` or `Stream`. **No parameters.** - Call site `fetchData()` returns a `Resource` exposing `.when(ready:, loading:, error:)`, `.maybeWhen(...)`, `.isRefreshing`, `.refresh()`. - Options: `debounce: Duration(...)`, `useRefreshing: false`. - - Example: `@SolidQuery() Future fetchData() async { ... }` then `fetchData().when(ready: Text.new, loading: CircularProgressIndicator.new, error: (e, _) => Text('$e'))`. + - Read `@SolidState` fields from the body to make the query react to them. -- **`@SolidEnvironment()`** — inject a value from the widget tree (SwiftUI `@Environment`-style). Docs: . +- **`@SolidEnvironment()`** — inject a value from the widget tree (SwiftUI-style). Docs: . - Valid on: `late` field on a `StatelessWidget` or `State`. - - Bound on first access to the nearest ancestor `Provider`. Reactive — `@SolidState` fields on the injected type stay reactive. - - Provide via `.environment()` extension or `Provider` from `package:provider`. - - Example: `@SolidEnvironment() late Counter counter;` then `child: CounterDisplay().environment((_) => Counter())`. + - Bound on first access to the nearest ancestor `Provider`. + - Provide via `.environment()` extension shipped by `solid_annotations`, or `Provider` from `package:provider`. + +For full target rules per annotation, read `references/annotation-contract.md`. For canonical idioms, read `references/patterns.md`. ## Untracked reads -By default every read of a `@SolidState` field inside `build`, `@SolidEffect`, or `@SolidQuery` registers a dependency on the surrounding computation — `build` re-renders the read site, effects re-fire, queries re-execute. Two ways to read without registering a dependency: +By default every read of a `@SolidState` field inside `build`, `@SolidEffect`, or `@SolidQuery` registers a dependency. Two opt-outs: + +- **Automatic**: reads inside callback parameters whose name starts with `on` (`onPressed`, `onTap`, `onChanged`, …) are untracked — Solid recognizes user-interaction handlers and doesn't subscribe. +- **Manual**: append `.untracked` to the field. Common use: `key: ValueKey(counter.untracked)`, or reading the same signal you're writing to inside an effect to avoid a self-dependency loop. -- **Automatic**: reads inside callback parameters whose name starts with `on` (`onPressed`, `onTap`, `onChanged`, …) are untracked — Solid treats them as gesture handlers. You don't write anything special. -- **Manual**: append `.untracked` to the field for a one-off untracked read. Common case is `key: ValueKey(counter.untracked)` (build the key once, don't rebuild on later changes), or reading the same signal you're writing to inside an effect to avoid a self-dependency loop: `history = [...history.untracked, counter];`. +In string interpolations, only the long form works: `'${counter.untracked}'`. The short form `'$counter.untracked'` parses as `${counter}` followed by a literal suffix (still tracked). -In string interpolations use the long form `'${counter.untracked}'` — the short form `'$counter.untracked'` parses as `${counter}` followed by a literal suffix (still tracked). Docs: . +Docs: . -For full target rules (every valid/invalid form with rationale) see `references/annotation-contract.md`. For canonical idioms see `references/patterns.md`. For error messages and fixes see `references/troubleshooting.md`. +## Setup checklist (fresh project) -## Setup checklist +If `pubspec.yaml` doesn't yet declare Solid, install it: 1. `flutter pub add solid_annotations flutter_solidart provider` 2. `dart pub add --dev solid_generator build_runner` -3. Add `source/**` to `build.yaml`: +3. Create `build.yaml` at the project root (or extend the existing one): ```yaml targets: $default: @@ -62,32 +130,46 @@ For full target rules (every valid/invalid form with rationale) see `references/ - lib/** - $package$ ``` -4. In `source/main.dart`, set `SolidartConfig.autoDispose = false` before `runApp(...)`. (Temporary — will become the default in a future `flutter_solidart` major release.) -5. Run `dart run build_runner watch --delete-conflicting-outputs` during development. +4. In `source/main.dart`, set `SolidartConfig.autoDispose = false;` before `runApp(...)`. (Temporary — will become the default in a future `flutter_solidart` major release.) +5. In `analysis_options.yaml`, add `analyzer.errors.must_be_immutable: ignore` (your source widgets are mutable; the generated ones are immutable). +6. Run `dart run build_runner watch --delete-conflicting-outputs` during development. + +## Verify your changes + +After writing or editing `source/`, regenerate `lib/` and apply lint fixes: + +- **`scripts/verify.sh`** — run from any package root. Runs `dart run build_runner build --delete-conflicting-outputs`, then `dart fix --apply` on the package (adds `const`, removes unused imports, applies relative-import lints). Prints PASS/FAIL plus the first `[SEVERE]` error on failure. Exit code reflects build_runner success; `dart fix` failure is non-fatal. + +Why `dart fix --apply` matters: the generator prioritises correct, runnable code over polish. The emitted `lib/` may miss `const` opportunities, leave unused imports, or pick a non-preferred import form. `dart fix --apply` cleans this up using the project's lint rules (`prefer_const_constructors`, `unnecessary_import`, `prefer_relative_imports`, …). Always run it after generation — in CI too. ## Hot reload -`flutter run` does not auto-reload when `build_runner` rewrites `lib/`. Two workflows: -- Press `r` in the `flutter run` terminal after `build_runner` emits. -- Use [`dashmonx`](https://pub.dev/packages/dashmonx) — it wraps `flutter run` and triggers reload on `lib/` changes. +`flutter run` does not auto-reload when build_runner rewrites `lib/` (no IDE save event fires for filesystem changes). Two workflows: + +- Press `r` in the `flutter run` terminal after build_runner emits. +- Use [`dashmonx`](https://pub.dev/packages/dashmonx) — wraps `flutter run` and triggers hot reload on `lib/` changes. Any `flutter run` flag passes through, e.g. `dashmonx -d chrome`. ## Common mistakes -- Editing `lib/` files. They are regenerated; your edits are lost. +- Writing to a file under `lib/`. Always under `source/`. (The single hardest rule to internalise.) +- Following a pub-package README literally when it says `lib/`. Substitute `source/`. See `references/third-party-packages.md`. - Adding `final` or `static` to a `@SolidState` field. The generator rejects it. -- Forgetting `SolidartConfig.autoDispose = false` — atoms are not disposed and tests / long sessions leak. -- Giving `@SolidQuery` parameters. Use `@SolidState` fields as inputs instead — the query re-runs when they change. -- Expecting `flutter run` to pick up `build_runner` output without `r` or `dashmonx`. -- Treating `must_be_immutable` lint as a real error. The widgets you write are mutable; the generated ones are immutable. Set `must_be_immutable: ignore` in `analysis_options.yaml`. +- Giving `@SolidQuery` parameters. Use `@SolidState` fields as inputs — the query re-runs when they change. +- Forgetting `SolidartConfig.autoDispose = false` in `source/main.dart` — atoms leak in tests and long sessions. +- Importing same-package files via `package:/...` from inside `source/`. Use relative paths. +- Expecting `flutter run` to pick up build_runner output without `r` or `dashmonx`. +- Treating `must_be_immutable` lint as a real error — your widgets are mutable; the generated ones are immutable. Set `must_be_immutable: ignore`. ## Helper scripts -The skill ships two scripts under `scripts/`: - -- **`scripts/verify.sh`** — run from any package root. Runs `dart run build_runner build --delete-conflicting-outputs`, prints PASS/FAIL plus the first error. Exit code reflects success. -- **`scripts/scaffold-widget.sh [--state|--query|--env]`** — writes a starter `source/.dart` with the right boilerplate for the chosen annotation. Refuses to overwrite existing files. +- **`scripts/verify.sh`** — described above. +- **`scripts/scaffold-widget.sh [--state|--query|--env]`** — writes a starter `source/.dart` with the right boilerplate. Refuses to overwrite. ## Where to read more - Canonical docs: -- Working example on GitHub: +- Annotation valid/invalid targets: `references/annotation-contract.md` +- Canonical idioms (counter, computed, effect, query, environment, untracked): `references/patterns.md` +- Error symptom → cause → fix: `references/troubleshooting.md` +- Third-party package redirect catalogue: `references/third-party-packages.md` +- Working example: diff --git a/skills/solid/assets/AGENTS.md b/skills/solid/assets/AGENTS.md new file mode 100644 index 0000000..64418c1 --- /dev/null +++ b/skills/solid/assets/AGENTS.md @@ -0,0 +1,91 @@ +# AGENTS.md — Solid (Flutter) project + +> Copy this file to the root of your Flutter app when you install Solid. +> Most AI coding tools (Claude Code, Cursor, Codex, GitHub Copilot, Amp, etc.) auto-load `AGENTS.md` at session start. +> Tools that look for `CLAUDE.md` instead can symlink: `ln -s AGENTS.md CLAUDE.md`. + +This Flutter app uses [Solid](https://solid.mariuti.com), a reactive framework built on `flutter_solidart`. Flutter conventions are **inverted** in this project. Read this file carefully before editing. + +## Cardinal rule + +**Edit `source/.dart`. Never edit `lib/.dart`.** + +- `source/` is the code you write. +- `lib/` is regenerated by `dart run build_runner build` from `source/`. Any hand-edit to `lib/` is silently destroyed on the next build. +- The app entry point is `source/main.dart`. `lib/main.dart` is generated. + +If you catch yourself about to write to `lib/`, stop and write to the matching `source/.dart` instead. + +## Reactive annotations + +You write a `StatelessWidget` with these annotations on members; the generator rewrites it into a `StatefulWidget` with `Signal`/`Computed`/`Effect`/`Resource` plumbing. + +- `@SolidState()` — reactive field, getter (derived state). **Not** valid on `final`, `static`, or methods. +- `@SolidEffect()` — `void` method that re-runs when its tracked reads change. +- `@SolidQuery()` — `Future` / `Stream` method. **No parameters** — read `@SolidState` fields from the body to make it react. Options: `debounce: Duration(...)` waits N after the last input change before re-running; `useRefreshing: true` (default) keeps the resource on its previous value while refetching with `.isRefreshing == true` for smoother UX, `useRefreshing: false` drops back to the `loading` state on every re-execution. +- `@SolidEnvironment()` — `late` field bound to the nearest ancestor `Provider`. **`late` is required.** +- `.untracked` — read a `@SolidState` field without registering a dependency. In string interpolation, only `'${x.untracked}'` works (not `'$x.untracked'`). + +## Same-package imports inside `source/` must be relative + +`import 'package:/foo.dart'` inside a `source/` file resolves to `lib/` (the generated realm) and is rejected by the generator. Use relative paths: `import '../path/to/foo.dart'`. + +Cross-package imports (`package:flutter/...`, `package:solid_annotations/...`, `package:provider/...`, third-party) stay normal. + +## Third-party packages + +When you install a pub package whose docs say "create `lib/.dart`" (go_router, freezed, riverpod, drift, json_serializable, get_it, flutter_bloc, …), substitute `source/.dart`. The package itself stays in `pubspec.yaml` as the docs describe; only the files **you write that import it** move from `lib/` to `source/`. If the package is itself a code generator, ensure `source/**` is in `build.yaml` `sources` (Solid's setup checklist puts it there). + +## Workflow + +After editing anything under `source/`: + +```bash +dart run build_runner build --delete-conflicting-outputs +dart fix --apply +``` + +The first regenerates `lib/`. The second cleans up `const` placement, unused imports, and prefers relative imports. Both are mandatory; the [Solid skill](https://github.com/nank1ro/solid/tree/main/skills/solid) ships a `scripts/verify.sh` that chains them. + +`flutter run` doesn't auto-pick-up build_runner output — press `r` in the terminal after the generator emits, or use [`dashmonx`](https://pub.dev/packages/dashmonx). + +## Required `analysis_options.yaml` rules + +```yaml +analyzer: + errors: + must_be_immutable: ignore # source/ widgets are mutable by design +linter: + rules: + public_member_api_docs: false + always_use_package_imports: false + prefer_relative_imports: true +``` + +## Required `source/main.dart` setup + +```dart +import 'package:flutter/material.dart'; +import 'package:flutter_solidart/flutter_solidart.dart'; + +void main() { + SolidartConfig.autoDispose = false; // Solid manages disposal manually + runApp(/* ... */); +} +``` + +## Common bugs you might be asked about + +| Symptom | Diagnosis | +| --- | --- | +| "my edits to `lib/.dart` keep disappearing" | They are. Move the change to `source/.dart` and regenerate. | +| build_runner: "requires assignable target" on `@SolidState() final int x = 0` | Drop `final`. `@SolidState` rewrites reads through a setter; the field must be assignable. | +| `@SolidQuery()` rejected with parameter list | Queries cannot have parameters. Move inputs to `@SolidState` fields. | +| `@SolidEnvironment` field is null at first read | Mark it `late`. The lookup is lazy and `late` defers initialization. | +| `must_be_immutable` lint everywhere | Add `must_be_immutable: ignore` to `analysis_options.yaml`. | + +## Where to read more + +- User docs: +- Skill for AI assistants (deeper guidance, scripts, evals): +- LLM-friendly full docs (for tools that fetch over HTTP): diff --git a/skills/solid/evals/evals.json b/skills/solid/evals/evals.json new file mode 100644 index 0000000..7589830 --- /dev/null +++ b/skills/solid/evals/evals.json @@ -0,0 +1,86 @@ +{ + "skill_name": "solid", + "evals": [ + { + "id": 1, + "prompt": "Add a counter widget to my Flutter app — tap + and the number bumps. Make it a fresh page.", + "expected_output": "A new widget file created under source/counter.dart (or source/counter_page.dart) using @SolidState for the counter field. No lib/ files created or edited by the agent. The user is told to run build_runner (or scripts/verify.sh) afterward.", + "files": [ + "evals/files/eval-1/pubspec.yaml", + "evals/files/eval-1/build.yaml", + "evals/files/eval-1/analysis_options.yaml", + "evals/files/eval-1/source/main.dart" + ], + "expectations": [ + "The agent created a new file under source/ (path matches source/*.dart) — not under lib/.", + "The new file uses @SolidState on at least one mutable field (e.g. `@SolidState() int counter = 0;`).", + "The new file imports 'package:solid_annotations/solid_annotations.dart'.", + "The new file does NOT define an empty `void dispose() {}` override on the StatelessWidget (the generator synthesizes dispose for reactive fields).", + "No file under lib/ was created or modified by the agent.", + "The agent recommends running `dart run build_runner build` (or `scripts/verify.sh`, or `build_runner watch`) so the new widget is transpiled into lib/." + ] + }, + { + "id": 2, + "prompt": "Fetch posts whenever the selected userId changes. Debounce 500ms.", + "expected_output": "A widget (or modification to an existing widget) that has a @SolidState field for the userId and a @SolidQuery method (no parameters) that fetches based on that field, with debounce: Duration(milliseconds: 500). Rendered with .when(ready:, loading:, error:). All edits under source/, none under lib/.", + "files": [ + "evals/files/eval-2/pubspec.yaml", + "evals/files/eval-2/build.yaml", + "evals/files/eval-2/analysis_options.yaml", + "evals/files/eval-2/source/main.dart", + "evals/files/eval-2/source/posts_page.dart" + ], + "expectations": [ + "The agent extends source/posts_page.dart (or replaces it) so the displayed posts react to the dropdown's selected userId.", + "The widget declares a @SolidState field (typically String? userId or similar) that the query reads from its body.", + "The query method is annotated @SolidQuery(...) with debounce: Duration(milliseconds: 500).", + "The @SolidQuery method takes no parameters (the body reads @SolidState fields directly).", + "The query is rendered with a `.when(ready:, loading:, error:)` block (or `.maybeWhen`).", + "No lib/ file was created or modified by the agent." + ] + }, + { + "id": 3, + "prompt": "I keep editing lib/counter.dart to change the AppBar title from 'Counter' to 'My Counter' but the change keeps disappearing on save. What's going on, and how do I fix it?", + "expected_output": "The agent diagnoses that lib/counter.dart is generated from source/counter.dart and explains why edits to lib/ are overwritten. The agent then makes the change in source/counter.dart (changing the AppBar title string from 'Counter' to 'My Counter') and tells the user to re-run build_runner. The agent must NOT edit lib/counter.dart.", + "files": [ + "evals/files/eval-3/pubspec.yaml", + "evals/files/eval-3/build.yaml", + "evals/files/eval-3/analysis_options.yaml", + "evals/files/eval-3/source/main.dart", + "evals/files/eval-3/source/counter.dart", + "evals/files/eval-3/lib/main.dart", + "evals/files/eval-3/lib/counter.dart" + ], + "expectations": [ + "The agent explains that lib/ is generated from source/ and that build_runner overwrites lib/ edits — using language the user can act on, not jargon.", + "The agent edits source/counter.dart to change the AppBar title from 'Counter' to 'My Counter'.", + "The agent does NOT modify or write to lib/counter.dart.", + "The agent recommends re-running `dart run build_runner build` (or `scripts/verify.sh`) to regenerate lib/counter.dart from the updated source." + ] + }, + { + "id": 4, + "prompt": "Add go_router and set up two routes: `/` → HomePage, `/settings` → SettingsPage. Keep my existing app structure.", + "expected_output": "go_router added to pubspec.yaml. A new source/router.dart created with the GoRouter configuration referencing the existing source/home_page.dart and source/settings_page.dart. source/main.dart edited to use MaterialApp.router with the new router config. NO lib/ files created or edited by the agent — even though the go_router README would normally direct the agent to create lib/router.dart.", + "files": [ + "evals/files/eval-4/pubspec.yaml", + "evals/files/eval-4/build.yaml", + "evals/files/eval-4/analysis_options.yaml", + "evals/files/eval-4/source/main.dart", + "evals/files/eval-4/source/home_page.dart", + "evals/files/eval-4/source/settings_page.dart" + ], + "expectations": [ + "go_router was added as a dependency in pubspec.yaml.", + "A new file `source/router.dart` (or similar under source/) exists and contains a `GoRouter` configuration declaring the `/` and `/settings` routes that point to the existing HomePage and SettingsPage classes.", + "source/main.dart was edited to use `MaterialApp.router` (or equivalent) wired to the GoRouter config.", + "Any imports between source/ files use relative paths (e.g. `import 'router.dart';`), NOT `package:/router.dart`.", + "The agent did NOT recreate or overwrite source/home_page.dart or source/settings_page.dart — they already existed.", + "No file under lib/ was created or modified by the agent — even though go_router's README normally instructs creating lib/router.dart.", + "The agent recommends running build_runner (or `scripts/verify.sh`) after the source/ edits so lib/ is regenerated." + ] + } + ] +} diff --git a/skills/solid/evals/files/eval-1/analysis_options.yaml b/skills/solid/evals/files/eval-1/analysis_options.yaml new file mode 100644 index 0000000..1659294 --- /dev/null +++ b/skills/solid/evals/files/eval-1/analysis_options.yaml @@ -0,0 +1,9 @@ +include: package:flutter_lints/flutter.yaml +analyzer: + errors: + must_be_immutable: ignore +linter: + rules: + public_member_api_docs: false + always_use_package_imports: false + prefer_relative_imports: true diff --git a/skills/solid/evals/files/eval-1/build.yaml b/skills/solid/evals/files/eval-1/build.yaml new file mode 100644 index 0000000..673bd1d --- /dev/null +++ b/skills/solid/evals/files/eval-1/build.yaml @@ -0,0 +1,6 @@ +targets: + $default: + sources: + - source/** + - lib/** + - $package$ diff --git a/skills/solid/evals/files/eval-1/pubspec.yaml b/skills/solid/evals/files/eval-1/pubspec.yaml new file mode 100644 index 0000000..ab77fbb --- /dev/null +++ b/skills/solid/evals/files/eval-1/pubspec.yaml @@ -0,0 +1,23 @@ +name: my_app +description: Sample Flutter app using Solid. +publish_to: none +version: 0.1.0 + +environment: + sdk: ^3.5.0 + +dependencies: + flutter: + sdk: flutter + flutter_solidart: ^2.0.0 + solid_annotations: ^2.0.0 + provider: ^6.1.0 + +dev_dependencies: + flutter_test: + sdk: flutter + build_runner: ^2.4.0 + solid_generator: ^2.0.0 + +flutter: + uses-material-design: true diff --git a/skills/solid/evals/files/eval-1/source/main.dart b/skills/solid/evals/files/eval-1/source/main.dart new file mode 100644 index 0000000..b593477 --- /dev/null +++ b/skills/solid/evals/files/eval-1/source/main.dart @@ -0,0 +1,22 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_solidart/flutter_solidart.dart'; + +void main() { + SolidartConfig.autoDispose = false; + runApp(const MyApp()); +} + +class MyApp extends StatelessWidget { + const MyApp({super.key}); + + @override + Widget build(BuildContext context) { + return MaterialApp( + title: 'My App', + home: Scaffold( + appBar: AppBar(title: const Text('My App')), + body: const Center(child: Text('Hello, world!')), + ), + ); + } +} diff --git a/skills/solid/evals/files/eval-2/analysis_options.yaml b/skills/solid/evals/files/eval-2/analysis_options.yaml new file mode 100644 index 0000000..1659294 --- /dev/null +++ b/skills/solid/evals/files/eval-2/analysis_options.yaml @@ -0,0 +1,9 @@ +include: package:flutter_lints/flutter.yaml +analyzer: + errors: + must_be_immutable: ignore +linter: + rules: + public_member_api_docs: false + always_use_package_imports: false + prefer_relative_imports: true diff --git a/skills/solid/evals/files/eval-2/build.yaml b/skills/solid/evals/files/eval-2/build.yaml new file mode 100644 index 0000000..673bd1d --- /dev/null +++ b/skills/solid/evals/files/eval-2/build.yaml @@ -0,0 +1,6 @@ +targets: + $default: + sources: + - source/** + - lib/** + - $package$ diff --git a/skills/solid/evals/files/eval-2/pubspec.yaml b/skills/solid/evals/files/eval-2/pubspec.yaml new file mode 100644 index 0000000..ab77fbb --- /dev/null +++ b/skills/solid/evals/files/eval-2/pubspec.yaml @@ -0,0 +1,23 @@ +name: my_app +description: Sample Flutter app using Solid. +publish_to: none +version: 0.1.0 + +environment: + sdk: ^3.5.0 + +dependencies: + flutter: + sdk: flutter + flutter_solidart: ^2.0.0 + solid_annotations: ^2.0.0 + provider: ^6.1.0 + +dev_dependencies: + flutter_test: + sdk: flutter + build_runner: ^2.4.0 + solid_generator: ^2.0.0 + +flutter: + uses-material-design: true diff --git a/skills/solid/evals/files/eval-2/source/main.dart b/skills/solid/evals/files/eval-2/source/main.dart new file mode 100644 index 0000000..101ce34 --- /dev/null +++ b/skills/solid/evals/files/eval-2/source/main.dart @@ -0,0 +1,18 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_solidart/flutter_solidart.dart'; + +import 'posts_page.dart'; + +void main() { + SolidartConfig.autoDispose = false; + runApp(MyApp()); +} + +class MyApp extends StatelessWidget { + MyApp({super.key}); + + @override + Widget build(BuildContext context) { + return MaterialApp(title: 'My App', home: PostsPage()); + } +} diff --git a/skills/solid/evals/files/eval-2/source/posts_page.dart b/skills/solid/evals/files/eval-2/source/posts_page.dart new file mode 100644 index 0000000..17aaaf8 --- /dev/null +++ b/skills/solid/evals/files/eval-2/source/posts_page.dart @@ -0,0 +1,36 @@ +import 'package:flutter/material.dart'; + +/// Placeholder posts page. The user picks a `userId` via the dropdown, +/// then the body is supposed to show that user's posts — but right now +/// it just shows a static "No user selected" message. +/// +/// Extend this widget so that picking a user fetches and displays their +/// posts. Debounce 500ms. +class PostsPage extends StatelessWidget { + PostsPage({super.key}); + + // Available users for the dropdown. Hard-coded for now. + static const List userIds = ['alice', 'bob', 'charlie']; + + String? selectedUserId; + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar(title: const Text('Posts')), + body: Column( + children: [ + DropdownButton( + value: selectedUserId, + hint: const Text('Pick a user'), + items: userIds + .map((id) => DropdownMenuItem(value: id, child: Text(id))) + .toList(), + onChanged: (id) => selectedUserId = id, + ), + const Expanded(child: Center(child: Text('No user selected'))), + ], + ), + ); + } +} diff --git a/skills/solid/evals/files/eval-3/analysis_options.yaml b/skills/solid/evals/files/eval-3/analysis_options.yaml new file mode 100644 index 0000000..1659294 --- /dev/null +++ b/skills/solid/evals/files/eval-3/analysis_options.yaml @@ -0,0 +1,9 @@ +include: package:flutter_lints/flutter.yaml +analyzer: + errors: + must_be_immutable: ignore +linter: + rules: + public_member_api_docs: false + always_use_package_imports: false + prefer_relative_imports: true diff --git a/skills/solid/evals/files/eval-3/build.yaml b/skills/solid/evals/files/eval-3/build.yaml new file mode 100644 index 0000000..673bd1d --- /dev/null +++ b/skills/solid/evals/files/eval-3/build.yaml @@ -0,0 +1,6 @@ +targets: + $default: + sources: + - source/** + - lib/** + - $package$ diff --git a/skills/solid/evals/files/eval-3/lib/counter.dart b/skills/solid/evals/files/eval-3/lib/counter.dart new file mode 100644 index 0000000..87984d5 --- /dev/null +++ b/skills/solid/evals/files/eval-3/lib/counter.dart @@ -0,0 +1,41 @@ +// GENERATED FILE — DO NOT EDIT BY HAND. +// Source: source/counter.dart +// The user attempted to change the AppBar title from 'Counter' to 'My Counter' +// directly in this file. build_runner's next run will overwrite their change. +import 'package:flutter/material.dart'; +import 'package:flutter_solidart/flutter_solidart.dart'; + +class CounterPage extends StatefulWidget { + const CounterPage({super.key}); + + @override + State createState() => _CounterPageState(); +} + +class _CounterPageState extends State { + final counter = Signal(0, name: 'counter'); + + @override + void dispose() { + counter.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text('My Counter'), + ), // user's mis-edit, will be overwritten + body: Center( + child: SignalBuilder( + builder: (context, _) => Text('Counter is ${counter.value}'), + ), + ), + floatingActionButton: FloatingActionButton( + onPressed: () => counter.value++, + child: const Icon(Icons.add), + ), + ); + } +} diff --git a/skills/solid/evals/files/eval-3/lib/main.dart b/skills/solid/evals/files/eval-3/lib/main.dart new file mode 100644 index 0000000..8026c8b --- /dev/null +++ b/skills/solid/evals/files/eval-3/lib/main.dart @@ -0,0 +1,24 @@ +// GENERATED FILE — DO NOT EDIT BY HAND. +// Source: source/main.dart +import 'package:flutter/material.dart'; +import 'package:flutter_solidart/flutter_solidart.dart'; + +void main() { + SolidartConfig.autoDispose = false; + runApp(const MyApp()); +} + +class MyApp extends StatelessWidget { + const MyApp({super.key}); + + @override + Widget build(BuildContext context) { + return MaterialApp( + title: 'My App', + home: Scaffold( + appBar: AppBar(title: const Text('My App')), + body: const Center(child: Text('Hello, world!')), + ), + ); + } +} diff --git a/skills/solid/evals/files/eval-3/pubspec.yaml b/skills/solid/evals/files/eval-3/pubspec.yaml new file mode 100644 index 0000000..ab77fbb --- /dev/null +++ b/skills/solid/evals/files/eval-3/pubspec.yaml @@ -0,0 +1,23 @@ +name: my_app +description: Sample Flutter app using Solid. +publish_to: none +version: 0.1.0 + +environment: + sdk: ^3.5.0 + +dependencies: + flutter: + sdk: flutter + flutter_solidart: ^2.0.0 + solid_annotations: ^2.0.0 + provider: ^6.1.0 + +dev_dependencies: + flutter_test: + sdk: flutter + build_runner: ^2.4.0 + solid_generator: ^2.0.0 + +flutter: + uses-material-design: true diff --git a/skills/solid/evals/files/eval-3/source/counter.dart b/skills/solid/evals/files/eval-3/source/counter.dart new file mode 100644 index 0000000..55f4462 --- /dev/null +++ b/skills/solid/evals/files/eval-3/source/counter.dart @@ -0,0 +1,21 @@ +import 'package:flutter/material.dart'; +import 'package:solid_annotations/solid_annotations.dart'; + +class CounterPage extends StatelessWidget { + CounterPage({super.key}); + + @SolidState() + int counter = 0; + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar(title: const Text('Counter')), + body: Center(child: Text('Counter is $counter')), + floatingActionButton: FloatingActionButton( + onPressed: () => counter++, + child: const Icon(Icons.add), + ), + ); + } +} diff --git a/skills/solid/evals/files/eval-3/source/main.dart b/skills/solid/evals/files/eval-3/source/main.dart new file mode 100644 index 0000000..b593477 --- /dev/null +++ b/skills/solid/evals/files/eval-3/source/main.dart @@ -0,0 +1,22 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_solidart/flutter_solidart.dart'; + +void main() { + SolidartConfig.autoDispose = false; + runApp(const MyApp()); +} + +class MyApp extends StatelessWidget { + const MyApp({super.key}); + + @override + Widget build(BuildContext context) { + return MaterialApp( + title: 'My App', + home: Scaffold( + appBar: AppBar(title: const Text('My App')), + body: const Center(child: Text('Hello, world!')), + ), + ); + } +} diff --git a/skills/solid/evals/files/eval-4/analysis_options.yaml b/skills/solid/evals/files/eval-4/analysis_options.yaml new file mode 100644 index 0000000..1659294 --- /dev/null +++ b/skills/solid/evals/files/eval-4/analysis_options.yaml @@ -0,0 +1,9 @@ +include: package:flutter_lints/flutter.yaml +analyzer: + errors: + must_be_immutable: ignore +linter: + rules: + public_member_api_docs: false + always_use_package_imports: false + prefer_relative_imports: true diff --git a/skills/solid/evals/files/eval-4/build.yaml b/skills/solid/evals/files/eval-4/build.yaml new file mode 100644 index 0000000..673bd1d --- /dev/null +++ b/skills/solid/evals/files/eval-4/build.yaml @@ -0,0 +1,6 @@ +targets: + $default: + sources: + - source/** + - lib/** + - $package$ diff --git a/skills/solid/evals/files/eval-4/pubspec.yaml b/skills/solid/evals/files/eval-4/pubspec.yaml new file mode 100644 index 0000000..ab77fbb --- /dev/null +++ b/skills/solid/evals/files/eval-4/pubspec.yaml @@ -0,0 +1,23 @@ +name: my_app +description: Sample Flutter app using Solid. +publish_to: none +version: 0.1.0 + +environment: + sdk: ^3.5.0 + +dependencies: + flutter: + sdk: flutter + flutter_solidart: ^2.0.0 + solid_annotations: ^2.0.0 + provider: ^6.1.0 + +dev_dependencies: + flutter_test: + sdk: flutter + build_runner: ^2.4.0 + solid_generator: ^2.0.0 + +flutter: + uses-material-design: true diff --git a/skills/solid/evals/files/eval-4/source/home_page.dart b/skills/solid/evals/files/eval-4/source/home_page.dart new file mode 100644 index 0000000..7d43d88 --- /dev/null +++ b/skills/solid/evals/files/eval-4/source/home_page.dart @@ -0,0 +1,13 @@ +import 'package:flutter/material.dart'; + +class HomePage extends StatelessWidget { + const HomePage({super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar(title: const Text('Home')), + body: const Center(child: Text('Welcome home')), + ); + } +} diff --git a/skills/solid/evals/files/eval-4/source/main.dart b/skills/solid/evals/files/eval-4/source/main.dart new file mode 100644 index 0000000..632b2c2 --- /dev/null +++ b/skills/solid/evals/files/eval-4/source/main.dart @@ -0,0 +1,18 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_solidart/flutter_solidart.dart'; + +import 'home_page.dart'; + +void main() { + SolidartConfig.autoDispose = false; + runApp(const MyApp()); +} + +class MyApp extends StatelessWidget { + const MyApp({super.key}); + + @override + Widget build(BuildContext context) { + return const MaterialApp(title: 'My App', home: HomePage()); + } +} diff --git a/skills/solid/evals/files/eval-4/source/settings_page.dart b/skills/solid/evals/files/eval-4/source/settings_page.dart new file mode 100644 index 0000000..b3ec9b6 --- /dev/null +++ b/skills/solid/evals/files/eval-4/source/settings_page.dart @@ -0,0 +1,13 @@ +import 'package:flutter/material.dart'; + +class SettingsPage extends StatelessWidget { + const SettingsPage({super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar(title: const Text('Settings')), + body: const Center(child: Text('Settings go here')), + ); + } +} diff --git a/skills/solid/evals/trigger_queries.json b/skills/solid/evals/trigger_queries.json new file mode 100644 index 0000000..d2bfd1c --- /dev/null +++ b/skills/solid/evals/trigger_queries.json @@ -0,0 +1,82 @@ +[ + { + "query": "In my flutter app i need to add a counter widget — tap + and it counts up. I have solid_annotations in my pubspec.yaml. give me a fresh page for it", + "should_trigger": true + }, + { + "query": "ok so im trying to fetch user posts whenever the selected user id changes, with a 500ms debounce so we dont hammer the api. this is the flutter app where i'm already using @SolidQuery elsewhere", + "should_trigger": true + }, + { + "query": "weird bug — i keep editing lib/counter.dart to change a string and it keeps reverting after a few seconds. why?? my pubspec has build_runner and solid_generator listed", + "should_trigger": true + }, + { + "query": "i want to add go_router to this flutter app and set up 2 routes (/ and /settings). just looked at pubspec and we already use solid_annotations so i'm not sure where router.dart should go", + "should_trigger": true + }, + { + "query": "Add a SolidEffect that prints to console whenever the counter changes. The widget is in source/counter_page.dart and the counter is an @SolidState int field", + "should_trigger": true + }, + { + "query": "the build_runner is failing with 'requires assignable target' on my @SolidState field. heres the snippet: @SolidState() final int counter = 0;", + "should_trigger": true + }, + { + "query": "Make this widget reactive — when toggle is true the colour changes, when it's false the colour reverts. the file is at source/colour_picker.dart in a flutter project with solid_generator in dev_deps", + "should_trigger": true + }, + { + "query": "scaffold a new page that fetches `/api/profile` and shows a loading spinner while it resolves. flutter project, solid_annotations is in pubspec already", + "should_trigger": true + }, + { + "query": "I want to inject an AuthService into a bunch of widgets via @SolidEnvironment. how do I provide it from main()? this is a Flutter app already set up with the Solid build_runner", + "should_trigger": true + }, + { + "query": "Help me wire up freezed in this flutter project — i need a User model with email, name, age. pubspec already has solid_generator so i'm not sure if i write the model under source/ or lib/", + "should_trigger": true + }, + { + "query": "fix this Solid.js component — when the signal changes the JSX doesn't re-render properly. it's a vite + typescript project", + "should_trigger": false + }, + { + "query": "explain how Riverpod scoping works in flutter. when does a Provider get re-created? this is a pure riverpod app, no codegen", + "should_trigger": false + }, + { + "query": "im setting up GitHub Actions for my Flutter project. how do i make `flutter test` run on PRs? pubspec is a vanilla flutter app, no solid", + "should_trigger": false + }, + { + "query": "json_serializable is throwing 'no JsonKey for field email'. where do i put the @JsonKey annotation? this is a standard flutter project, no solid_annotations", + "should_trigger": false + }, + { + "query": "in vanilla flutter, when should I use ChangeNotifier vs ValueListenable for state management? we're not using any codegen libs", + "should_trigger": false + }, + { + "query": "what's the difference between StatefulWidget and StatelessWidget? im new to flutter and starting a fresh project, pubspec is empty besides flutter", + "should_trigger": false + }, + { + "query": "convert this React class component to React hooks. its a Next.js page using class component lifecycle methods", + "should_trigger": false + }, + { + "query": "explain how to use setState properly to avoid extra rebuilds in flutter. our codebase is standard flutter, no codegen libs in pubspec", + "should_trigger": false + }, + { + "query": "I keep getting 'Provider not found' in my flutter app — pubspec has `provider: ^6.0` and i wrapped in Provider. what am i missing? no other state libs", + "should_trigger": false + }, + { + "query": "configure my flutter project's launch.json in VS Code to start with --device web-server. flutter app, no solid packages installed", + "should_trigger": false + } +] diff --git a/skills/solid/references/annotation-contract.md b/skills/solid/references/annotation-contract.md index 469d22d..00eb002 100644 --- a/skills/solid/references/annotation-contract.md +++ b/skills/solid/references/annotation-contract.md @@ -1,6 +1,6 @@ # Solid annotation contract -Per-annotation valid/invalid targets. Distilled from the user docs at . +Per-annotation valid/invalid targets and rationale. Distilled from the user docs at . ## `@SolidState()` — reactive state @@ -23,7 +23,7 @@ Docs: . | `static` field | State is per-widget-instance, not per-class. | | Setter | A `@SolidState` write goes through the generated setter; you don't write your own. | | Method | Use `@SolidEffect` for side effects or `@SolidQuery` for async values. | -| Top-level / library variable | Annotation only applies to class members. | +| Top-level / library variable | The annotation only applies to class members. | ## `@SolidEffect()` — side effect @@ -43,7 +43,7 @@ The body's reads of `@SolidState` fields are tracked automatically. The effect r - Methods with parameters. - Static or top-level functions. -## `@SolidQuery()` — async/stream resource +## `@SolidQuery()` — async / stream resource Docs: . @@ -60,7 +60,7 @@ Docs: . - Return type must be `Future` or `Stream`. - The call site `fetchData()` does **not** return a `Future`/`Stream` — it returns a `Resource` exposing: - `.when(ready: ..., loading: ..., error: ...)` - - `.maybeWhen(...orElse: ...)` + - `.maybeWhen(..., orElse: ...)` - `.isRefreshing` (true while a re-execution is in flight) - `.refresh()` to manually re-run @@ -69,7 +69,7 @@ Docs: . | Option | Effect | | --- | --- | | `debounce: Duration(...)` | Wait this long after the last input change before re-running. | -| `useRefreshing` (default `true`) | On re-execution from a dependency change, the resource stays on the current value while refetching, and `.isRefreshing` becomes `true` (smoother UX, no loading flash). Pass `useRefreshing: false` to drop back into the `loading` state on each re-execution instead. | +| `useRefreshing` (default `true`) | On re-execution from a dependency change, the resource stays on the current value while refetching, and `.isRefreshing` becomes `true` (smoother UX, no loading flash). Pass `useRefreshing: false` to drop back into the `loading` state on each re-execution. | ## `@SolidEnvironment()` — inject from widget tree @@ -84,13 +84,13 @@ Docs: . ### Behavior - Lookup is lazy: the field initializer runs the first time the field is read. -- Resolves the nearest ancestor `Provider` where `T` is the field's declared type. +- Resolves the nearest ancestor `Provider` where `T` is the declared type. - Reading a `@SolidState` member of the injected instance stays reactive — fine-grained reactivity is preserved across the boundary. - Works inside `build`, `@SolidEffect`, `@SolidQuery` bodies, or any other context. ### Providing the instance -Two equivalent ways: +Two equivalent forms: ```dart // 1. .environment() extension shipped by solid_annotations @@ -100,7 +100,7 @@ home: CounterDisplay().environment((_) => Counter()), home: Provider(create: (_) => Counter(), child: CounterDisplay()), ``` -For multiple providers chain `.environment(...)` calls or use `MultiProvider` from `package:provider`. +For multiple providers, chain `.environment(...)` calls or use `MultiProvider` from `package:provider`. The type argument is inferred from the closure's return type. Pass it explicitly only when consumers should read by a supertype: `.environment((_) => RealAuthService())`. @@ -121,7 +121,7 @@ extension UntrackedExtension on T { } ``` -`counter.untracked` typechecks identically to `counter` and is a no-op at runtime. The generator detects the pattern at source level and rewrites it to the underlying `untrackedValue` primitive and excludes the read from the dependency set — `SignalBuilder` doesn't wrap it inside `build`, and an enclosing `@SolidEffect` / `@SolidQuery` won't re-fire on changes to it. +`counter.untracked` typechecks identically to `counter` and is a no-op at runtime. The generator detects the pattern at source level and rewrites it to the underlying `untrackedValue` primitive, excluding the read from the dependency set — `SignalBuilder` doesn't wrap it inside `build`, and an enclosing `@SolidEffect` / `@SolidQuery` won't re-fire on changes to it. ### Two ways reads become untracked @@ -132,9 +132,9 @@ extension UntrackedExtension on T { ### Hard rules -- **String interpolation form**: only `'${counter.untracked}'` works. The short form `'$counter.untracked'` parses as `${counter}` followed by a literal `.untracked` suffix and is still a tracked read. +- **String interpolation form**: only `'${counter.untracked}'` works. The short form `'$counter.untracked'` parses as `${counter}` followed by a literal `.untracked` suffix (still tracked). - **Shadowing**: a local variable that shadows the field disables the rewrite for that scope (the analyzer's identifier resolution wins). -- **No-op on non-reactive types**: applied to a non-`@SolidState` value the extension is identity at compile time *and* runtime — safe to leave in code that may or may not target a reactive field. +- **No-op on non-reactive types**: applied to a non-`@SolidState` value, the extension is identity at compile time and at runtime — safe to leave in code that may or may not target a reactive field. ### When to reach for it diff --git a/skills/solid/references/patterns.md b/skills/solid/references/patterns.md index 5c7eb7b..e45cdb3 100644 --- a/skills/solid/references/patterns.md +++ b/skills/solid/references/patterns.md @@ -1,6 +1,6 @@ # Solid patterns -Six canonical idioms, each with a complete `source/` snippet. All examples lifted from the user docs at . +Canonical idioms, each with a complete `source/` snippet. All examples lifted from the user docs at . ## 1. Counter with `@SolidState` field @@ -211,7 +211,7 @@ import 'package:provider/provider.dart'; home: Provider(create: (_) => Counter(), child: CounterDisplay()), ``` -For multiple providers chain `.environment(...)` calls or use `MultiProvider`: +For multiple providers, chain `.environment(...)` calls or use `MultiProvider`: ```dart HomePage() @@ -251,8 +251,8 @@ List history = []; @SolidEffect() void recordHistory() { - history = [...history.untracked, counter]; // counter is tracked, history is not + history = [...history.untracked, counter]; // counter tracked, history not } ``` -Reads inside `on*` callback parameters (`onPressed`, `onTap`, `onChanged`, …) are auto-untracked — no `.untracked` needed. In string interpolations only the long form `'${counter.untracked}'` works; `'$counter.untracked'` is still tracked. +Reads inside `on*` callback parameters (`onPressed`, `onTap`, `onChanged`, …) are auto-untracked — no `.untracked` needed. In string interpolations, only the long form `'${counter.untracked}'` works; `'$counter.untracked'` is still tracked. diff --git a/skills/solid/references/third-party-packages.md b/skills/solid/references/third-party-packages.md new file mode 100644 index 0000000..240548c --- /dev/null +++ b/skills/solid/references/third-party-packages.md @@ -0,0 +1,116 @@ +# Third-party packages in a Solid project + +Every pub package's README, every Stack Overflow answer, every AI-generated setup instruction assumes `lib/` is where you write code. In a Solid project that assumption is inverted: `source/` is where you write code, `lib/` is generated. + +This reference catalogues the redirect pattern for common packages. + +## The general rule + +When a package's docs say: + +- *"Create `lib/.dart` and put this in it..."* → create `source/.dart` instead. +- *"Register this in `lib/main.dart`..."* → edit `source/main.dart` instead. +- *"Import via `package:/.dart`..."* → from inside `source/`, use a **relative** import (`../path/to/x.dart`). The `package:/...` form resolves to `lib/` (the generated realm) and the generator rejects it. + +The package itself stays in `pubspec.yaml` exactly as the docs describe. Only the *Dart files you write that import it* move from `lib/` to `source/`. + +## When a new code generator is added + +If the new package is itself a `build_runner` builder (freezed, json_serializable, drift, riverpod_generator, isar_generator, mockito with `@GenerateMocks`, …), it reads from `lib/` by default. To make it read from `source/` too, ensure your `build.yaml` `targets.$default.sources` includes `source/**`: + +```yaml +targets: + $default: + sources: + - source/** + - lib/** + - $package$ +``` + +The Solid setup already puts `source/**` first — keep it there and other generators will pick up your source files automatically. + +## Per-package gotchas + +### `go_router` + +The README says: *"Create `lib/router.dart` with your `GoRouter` config and import it from `lib/main.dart`."* + +In a Solid project: +- Create `source/router.dart` with the `GoRouter` config. +- Import it from `source/main.dart` via a relative path: `import 'router.dart';`. +- No `lib/` files created or edited by you. build_runner produces `lib/router.dart` and `lib/main.dart` from your source. + +```dart title="source/router.dart" +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; + +import 'home_page.dart'; +import 'settings_page.dart'; + +final appRouter = GoRouter( + routes: [ + GoRoute(path: '/', builder: (_, __) => const HomePage()), + GoRoute(path: '/settings', builder: (_, __) => const SettingsPage()), + ], +); +``` + +```dart title="source/main.dart" +import 'package:flutter/material.dart'; +import 'package:flutter_solidart/flutter_solidart.dart'; + +import 'router.dart'; + +void main() { + SolidartConfig.autoDispose = false; + runApp(MaterialApp.router(routerConfig: appRouter)); +} +``` + +### `freezed` + +The README tells you to create `lib/models/user.dart` and run `build_runner`. In a Solid project: +- Create `source/models/user.dart`. +- Freezed generates `*.freezed.dart` siblings. Where those land depends on `build.yaml`. If `source/**` is in your sources list (Solid's setup ensures this), freezed reads your `source/models/user.dart` and emits `source/models/user.freezed.dart`. Solid then transpiles both `source/` files into `lib/`. +- Inside `source/`, import the freezed-generated file via a relative `.freezed.dart` path: `part 'user.freezed.dart';`. + +### `json_serializable` + +Same redirect as freezed — create the annotated class under `source/`, let the generator produce the `.g.dart` sibling under `source/`, and Solid copies the whole thing to `lib/` on build. + +### `riverpod` / `flutter_riverpod` + `riverpod_generator` + +Riverpod docs say: *"Define providers in `lib/providers/`."* In Solid: +- Create `source/providers/.dart`. +- `riverpod_generator` emits `*.g.dart` siblings — same deal as freezed. +- Providers read in widgets via `ref.watch(...)` work without modification, because by the time the code runs, it's the generated `lib/` version. + +That said: if you're using Riverpod, you may not need Solid at all (both are reactive state libraries). Mixing them in one project is unusual but supported — Solid handles the widget-local state, Riverpod handles app-global. + +### `drift` + +Drift docs say create `lib/database.dart`. In Solid: `source/database.dart`. Drift's generated `*.g.dart` lands as a sibling. + +### `isar` + +Same — `source/.dart` for the annotated classes. + +### `get_it` + +`get_it` doesn't generate code; it's runtime DI. The README still tells you to call `GetIt.I.registerSingleton(...)` in `lib/main.dart`. Substitute `source/main.dart`. + +### `flutter_bloc` + +The pattern guides write `lib/blocs/_bloc.dart` and `lib/blocs/_event.dart` etc. Substitute `source/blocs/_bloc.dart` etc. + +### `provider` + +`provider` is already a Solid dependency (it backs `@SolidEnvironment`). Use it directly when you need multiple providers or when the `.environment(...)` chain gets long. + +### `dio` / `http` / `chopper` / API client packages + +These are runtime libraries with no code generation — `lib/`-vs-`source/` doesn't apply to the package itself. But the *Dart files you write to consume them* (your `ApiClient` class, your DTOs) go under `source/`. + +## What if the new package isn't listed here? + +Apply the rule of thumb: anywhere the docs say `lib/`, substitute `source/`. The package itself stays normal in `pubspec.yaml`. If it's a code generator, make sure `source/**` is in `build.yaml` sources. diff --git a/skills/solid/references/troubleshooting.md b/skills/solid/references/troubleshooting.md index 9d5f487..e7bb41f 100644 --- a/skills/solid/references/troubleshooting.md +++ b/skills/solid/references/troubleshooting.md @@ -8,9 +8,10 @@ Common errors and fixes. Source: plus the "commo | --- | --- | --- | | Edits to a file under `lib/` keep disappearing on save | `lib/` is generated; `build_runner watch` rewrites it from `source/`. | Move the change to the matching `source/.dart`. | | `lib/.dart` not produced | `source/**` not in `build.yaml` `targets.$default.sources`. | Add `- source/**` to the sources list. | -| Generator errors complain about stale outputs | Old `.g.dart` / `lib/` files conflict with regeneration. | `dart run build_runner build --delete-conflicting-outputs` (or `watch ...`). | +| Generator errors complain about stale outputs | Old `.g.dart` / `lib/` files conflict with regeneration. | `dart run build_runner build --delete-conflicting-outputs` (or `watch ...`). Or run `scripts/verify.sh`. | | `flutter run` doesn't pick up a `build_runner` rewrite | No IDE save event fires for filesystem changes. | Press `r` in the `flutter run` terminal, or use [`dashmonx`](https://pub.dev/packages/dashmonx) (`dashmonx -d chrome` etc.). | | Working with another generator (`freezed`, `json_serializable`, …) and only Solid runs | `build.yaml` `sources` list missing `lib/**` or `$package$`. | Use the full block: `- lib/**`, `- $package$`, `- source/**`. Run `dart run build_runner watch --delete-conflicting-outputs` once. | +| Generated `lib/` files are missing `const`, have unused imports, or use absolute `package:` imports | The generator prioritises correctness over polish; lint-driven fixes aren't applied. | Run `dart fix --apply` after `build_runner` — or use `scripts/verify.sh` which chains them. Apply in CI too. | ## Annotation rejections @@ -21,6 +22,7 @@ Common errors and fixes. Source: plus the "commo | `late` field with `@SolidState` never gets a value | A `late` `@SolidState` field needs an initializer site (or to be assigned before first read). | Either initialize at declaration or assign before read; or make the type nullable. | | `@SolidQuery() Future fetchData(int id) async {...}` rejected | Queries cannot have parameters. | Move the input into a `@SolidState` field; the query auto-re-runs when it changes. See `references/patterns.md` §5. | | `@SolidEnvironment() Counter counter;` errors at first read | Lookup is lazy and needs `late`. | Mark the field `late`: `@SolidEnvironment() late Counter counter;`. | +| Generator rejects `import 'package:/foo.dart';` from a `source/` file | Same-package imports inside `source/` must be relative — `package:` resolves to the generated `lib/` realm. | Use a relative import: `import '../path/to/foo.dart';`. | ## Runtime @@ -37,3 +39,4 @@ Common errors and fixes. Source: plus the "commo | --- | --- | --- | | `must_be_immutable` lint fires on every Solid widget | The `StatelessWidget` you write holds mutable fields. The generated widget is immutable. | In `analysis_options.yaml`: `analyzer.errors.must_be_immutable: ignore`. | | `public_member_api_docs` lint fires everywhere in `source/` | Solid's recommended setup disables it. | In `analysis_options.yaml`: `linter.rules.public_member_api_docs: false`. | +| Lints complain about `package:/...` imports inside `source/` | Same-package imports must be relative. | Set `linter.rules.always_use_package_imports: false` and `linter.rules.prefer_relative_imports: true`. | diff --git a/skills/solid/scripts/scaffold-widget.sh b/skills/solid/scripts/scaffold-widget.sh index eb38248..35c06d4 100755 --- a/skills/solid/scripts/scaffold-widget.sh +++ b/skills/solid/scripts/scaffold-widget.sh @@ -1,6 +1,8 @@ #!/usr/bin/env bash # Scaffold a starter Solid widget under source/.dart. +# # Usage: scaffold-widget.sh [--state|--query|--env] +# # Run from the package root. Refuses to overwrite an existing file. set -eu @@ -34,7 +36,7 @@ fi mkdir -p source -# PascalCase -> snake_case (e.g. CounterPage -> counter_page) +# PascalCase -> snake_case (CounterPage -> counter_page). snake="$(printf '%s' "$name" \ | sed -E 's/([a-z0-9])([A-Z])/\1_\2/g; s/([A-Z]+)([A-Z][a-z])/\1_\2/g' \ | tr '[:upper:]' '[:lower:]')" diff --git a/skills/solid/scripts/verify.sh b/skills/solid/scripts/verify.sh index c798fb0..0401168 100755 --- a/skills/solid/scripts/verify.sh +++ b/skills/solid/scripts/verify.sh @@ -1,8 +1,14 @@ #!/usr/bin/env bash -# Run the Solid (build_runner) code generator and report PASS/FAIL. -# Run from the package root (the directory with pubspec.yaml). -# Exits 0 on success, non-zero on failure. On failure, prints the first -# [SEVERE] line so the agent can act on it without parsing the full log. +# Verify a Solid package: regenerate lib/ from source/, then apply lint-driven +# fixes. Run from the package root (the directory with pubspec.yaml). +# +# Exit code: +# 0 = build_runner succeeded (dart fix failure is non-fatal, reported as WARN) +# 1 = build_runner failed +# 2 = misuse (not a package root) +# +# On build_runner failure, prints the first [SEVERE] line so the caller can act +# on it without parsing the full log. set -u @@ -14,17 +20,26 @@ fi log="$(mktemp -t solid-verify.XXXXXX)" trap 'rm -f "$log"' EXIT -if dart run build_runner build --delete-conflicting-outputs >"$log" 2>&1; then - echo "PASS: build_runner generated lib/ from source/." - exit 0 +# Step 1: build_runner. This is the hard requirement. +if ! dart run build_runner build --delete-conflicting-outputs >"$log" 2>&1; then + echo "FAIL: build_runner returned non-zero." >&2 + first_severe="$(grep -m1 '\[SEVERE\]' "$log" || true)" + if [[ -n "$first_severe" ]]; then + echo "First error: $first_severe" >&2 + else + echo "Last 20 lines of output:" >&2 + tail -n 20 "$log" >&2 + fi + exit 1 fi -echo "FAIL: build_runner returned non-zero." >&2 -first_severe="$(grep -m1 '\[SEVERE\]' "$log" || true)" -if [[ -n "$first_severe" ]]; then - echo "First error: $first_severe" >&2 -else - echo "Last 20 lines of output:" >&2 - tail -n 20 "$log" >&2 +# Step 2: dart fix. This is polish; failure here doesn't fail the script. +if dart fix --apply >>"$log" 2>&1; then + echo "PASS: build_runner generated lib/ from source/, dart fix applied." + exit 0 fi -exit 1 + +echo "PASS: build_runner generated lib/ from source/." +echo "WARN: dart fix --apply failed (non-fatal). Last 10 lines of log:" >&2 +tail -n 10 "$log" >&2 +exit 0