A wrapper around `dotnet build` earns nothing, so build.ps1/build.sh are the full local gate instead — tool restore, package restore, format check, build, test — in the order that fails cheapest first, with a non-zero exit on failure. run.ps1/run.sh default to Debug and pass extra arguments through to the app. Both flavours ship because the Desktop head targets Windows, Linux and macOS. Writing them immediately paid for itself: the very first run failed restore on Avalonia.Diagnostics 12.1.1, which does not exist — the package stops at 11.3.x because Avalonia 12 moved the inspector into a separate tool with its own installation. The reference had survived because it sat behind Condition="'$(Configuration)' == 'Debug'", and `dotnet restore` evaluates with the default configuration while every build so far had passed -c Release. So `dotnet build -c Release` worked and a bare `dotnet restore` did not. Reference removed rather than replaced: AvaloniaUI.DiagnosticsSupport pulls in a separately installed tool, which is not a dependency to add to a skeleton without asking. README and CLAUDE.md no longer promise F12, and both traps are written down where the next person will hit them. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
130 lines
8.2 KiB
Markdown
130 lines
8.2 KiB
Markdown
# CLAUDE.md
|
||
|
||
Конвенции этого репозитория и грабли, на которые здесь уже наступили. Читать до правок.
|
||
|
||
## Команды
|
||
|
||
Полный локальный гейт (формат → сборка → тесты) — то, что нужно прогнать перед коммитом:
|
||
|
||
```bash
|
||
./build.ps1
|
||
```
|
||
|
||
Отдельные шаги, если нужен только один из них:
|
||
|
||
```bash
|
||
dotnet build AvParser.slnx -c Release
|
||
```
|
||
```bash
|
||
dotnet test AvParser.slnx -c Release
|
||
```
|
||
```bash
|
||
dotnet csharpier check .
|
||
```
|
||
```bash
|
||
./run.ps1
|
||
```
|
||
|
||
На Linux/macOS — `./build.sh` и `./run.sh` с теми же шагами.
|
||
|
||
## Слои
|
||
|
||
`Core ← Infrastructure ← UI ← Desktop`, строго в одну сторону.
|
||
|
||
- **`AvParser.Core` не ссылается на Avalonia.** Это единственное ограничение, которое здесь
|
||
по-настоящему несущее: домен должен запускаться из CLI, worker-сервиса или бенчмарка. Как
|
||
только Avalonia станет доступна из домена, кто-нибудь потянется к `Dispatcher.UIThread` или
|
||
`IStorageProvider` внутри парсера.
|
||
- **`AvParser.UI` — библиотека, а не exe.** Headless-тесты собирают настоящие View, не
|
||
подтягивая `Program.cs`, Serilog и контейнер.
|
||
- **`AvParser.Desktop` — тонкий composition root.** Логика туда не переезжает.
|
||
|
||
## Добавить парсер
|
||
|
||
1. Реализовать `ITextParser` в `Core/Parsing/`.
|
||
2. Одна строка в `CoreServiceCollectionExtensions.AddAvParserCore()`.
|
||
|
||
Всё. `IParserCatalog`, страница Parse и выпадающий список подхватят его сами.
|
||
|
||
## Добавить страницу
|
||
|
||
1. Наследник `PageViewModel` в `UI/ViewModels/XxxViewModel.cs` (`Title`, `IconKey`).
|
||
2. `UI/Views/XxxView.axaml` — имя обязано соответствовать конвенции `ViewLocator`:
|
||
`...ViewModels.XxxViewModel` → `...Views.XxxView`.
|
||
3. Регистрация в `AddAvParserUI()`: конкретным типом **и** как `PageViewModel` — порядок этих
|
||
регистраций и есть порядок пунктов в рельсе навигации.
|
||
|
||
## ReactiveUI 24 (дистрибутив Primitives)
|
||
|
||
Это не классический ReactiveUI. `System.Reactive` не используется:
|
||
|
||
| Классика | Здесь |
|
||
|---|---|
|
||
| `Unit` | `RxVoid` |
|
||
| `IScheduler` | `ISequencer` (`ReactiveUI.Primitives.Concurrency`) |
|
||
| `Subject<T>` / `BehaviorSubject<T>` | `Signal<T>` / `BehaviorSignal<T>` |
|
||
| `RxApp.MainThreadScheduler` | `RxSchedulers.MainThreadScheduler` |
|
||
| `TestScheduler` | `VirtualClock`, `ImmediateSequencer.Instance` |
|
||
|
||
Привычные имена операторов (`Select`, `Where`, `Throttle`, `DistinctUntilChanged`,
|
||
`CombineLatest`) **работают** — Primitives отдаёт оба набора. `using ReactiveUI.Primitives;`
|
||
нужен ради `Subscribe(Action<T>)`.
|
||
|
||
**ReactiveUI 24 не инициализируется сама.** Первый `WhenAnyValue` бросит
|
||
`InvalidOperationException`, пока не отработал builder. В приложении это делает
|
||
`AppBuilder.UseReactiveUI(...)`; в проекте VM-тестов — module initializer
|
||
`ReactiveUiBootstrap`. Новый тестовый проект без Avalonia обязан сделать то же самое.
|
||
|
||
## Конвенции ViewModel
|
||
|
||
- **Каждая VM принимает `ISequencer? mainThread = null`** и использует его в `outputScheduler:`
|
||
и `ToProperty(..., scheduler)`. Именно это делает тесты синхронными: они передают
|
||
`ImmediateSequencer.Instance`. Без этого пришлось бы гонять диспетчер.
|
||
- У VM с необязательным `ISequencer` регистрация в DI — явная фабрика, а не по типу: иначе
|
||
выбор конструктора контейнером зависит от порядка регистраций.
|
||
- `[Reactive]` из `ReactiveUI.SourceGenerators` на partial-свойствах; класс — `partial`.
|
||
|
||
## Грабли, уже оплаченные
|
||
|
||
- **Селектор типа в Avalonia матчит точный тип.** `UserControl.shell` не матчит `ShellView`
|
||
(наследник `ReactiveUserControl<T>`) и молча не делает ничего. Использовать
|
||
`:is(UserControl).shell`. Голый `.shell` тоже матчит, но тогда XAML-компилятор не может
|
||
вывести тип для `Setter` и падает с AVLN2200.
|
||
- **Style-сеттер навсегда проигрывает локальному значению.** Не стилизовать `IsPaneOpen` и
|
||
`DisplayMode` — они биндятся во ViewModel.
|
||
- **`IPseudoClasses.Set` требует ведущего `:`**.
|
||
- **`InvariantGlobalization` обязан быть `false`**: Semi.Avalonia строит `CultureInfo` в
|
||
статическом конструкторе и падает целиком.
|
||
- **Превьюер рефлексирует безпараметровый статический `BuildAvaloniaApp()`.** Необязательный
|
||
параметр ломает его вызов, вторая перегрузка — `AmbiguousMatchException`.
|
||
- **Compiled bindings включены по умолчанию** (Avalonia 12): `x:DataType` нужен на каждом
|
||
`UserControl` и каждом `DataTemplate`.
|
||
- **`Avalonia.Headless.XUnit` 12.x — это xUnit v3**, а не v2. Тестовые проекты — `Exe`.
|
||
- **Headless: ручной `Measure`/`Arrange` внутри окна бесполезен** — следующий проход layout
|
||
окна вернёт свой размер. Задавать ширину самому `Window`. Но и без окна нельзя: у
|
||
открепленного контрола не строится визуальное дерево.
|
||
- **csharpier — единственный владелец форматирования** (включая `.axaml` и `.csproj`).
|
||
`IDE0055` понижен до suggestion: два форматтера с `TreatWarningsAsErrors` дерутся насмерть.
|
||
- **`Condition="'$(Configuration)' == 'Debug'"` на `PackageReference` — ловушка.**
|
||
`dotnet restore` вычисляется с конфигурацией по умолчанию, поэтому пакет обязан
|
||
резолвиться, даже если эту конфигурацию никто не собирает. Так тут проехал мёртвый
|
||
`Avalonia.Diagnostics` (его нет под Avalonia 12): `dotnet build -c Release` работал,
|
||
а голый `dotnet restore` падал.
|
||
- **Инспектора в Avalonia 12 нет из коробки.** `Avalonia.Diagnostics` закончился на 11.3.x;
|
||
DevTools живут отдельно (`AvaloniaUI.DiagnosticsSupport` + `.WithDeveloperTools()`), со своей
|
||
установкой. Зависимость намеренно не добавлена.
|
||
|
||
## Качество
|
||
|
||
- Центральные версии пакетов — `Directory.Packages.props`. **Никаких `Version=` в csproj**
|
||
(иначе NU1008 на restore).
|
||
- `TreatWarningsAsErrors` включён; NuGet-advisory (`NU19xx`) выведены из ошибок, чтобы
|
||
свежая CVE не роняла сборку кода, который никто не трогал.
|
||
- Тестовые послабления анализаторов — в `tests/Directory.Build.props`, не в самих тестах.
|
||
|
||
## Что осталось абстрактным
|
||
|
||
Домен — заглушка. `IParser<TInput, TOutput>` + `DelimitedTextParser` + `KeyValueTextParser`
|
||
существуют, чтобы каркас проверялся end-to-end. Когда появится настоящая доменная логика,
|
||
демо-парсеры удаляются вместе с их тестами и `SampleFor`/`LargeSampleFor` в `ParseViewModel`.
|