diff --git a/CHANGELOG.md b/CHANGELOG.md
index 7e4e64169..dc92ff7d1 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -28,6 +28,14 @@ Conventions for contributors:
### Added
+- **Getting Started documents the single-file path.** A Reactor app does not need
+ a `.csproj`: .NET 10 runs a lone `.cs` file whose `#:package` / `#:property`
+ header supplies what a project file otherwise would. The header also references
+ `Microsoft.Windows.SDK.BuildTools.WinApp`, so `dotnet run counter.cs` launches
+ the app packaged, just like `winapp run counter.cs`; `WindowsPackageType=None`
+ runs it unpackaged (winappCli#794 / #874, shipped in winapp 0.7.0). The
+ agent-kit skills use the same header.
+
- **Validation now works on any control, not only inside `FormField`** — the
Forms guide's "Validation Context" example needed new surface to work as
written (issue #1262):
@@ -149,6 +157,16 @@ Conventions for contributors:
### Fixed
+- **Apps that reference only `Microsoft.UI.Reactor` are framework-dependent
+ again** (regression from #822). The package now depends on
+ `Microsoft.WindowsAppSDK.Runtime`; without it the Windows App SDK silently
+ built such apps self-contained, so a plain `dotnet run` failed with
+ *"WindowsAppSDKSelfContained requires a supported Windows architecture"*. To
+ keep bundling the Windows App Runtime, set `WindowsAppSDKSelfContained=true`.
+ To move to a newer WinUI, bump the full `Microsoft.WindowsAppSDK` package;
+ bumping `Microsoft.WindowsAppSDK.WinUI` on its own now fails the Windows App
+ SDK's version check.
+
- **The Forms guide's "Validation Context" example now works as written**
(issue #1262). Clicking **Register** on an empty form submitted successfully
with no errors, because five defects compounded: `.Validate()` was inert
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index d0e8e50e0..47935e61f 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -28,7 +28,7 @@ This project has adopted the [Microsoft Open Source Code of Conduct](https://ope
`Directory.Build.props`)
- Visual Studio 2022 (17.8+) or VS Code with C# Dev Kit
-> **Windows App SDK packages:** The right package is injected centrally by `Directory.Build.targets` based on each project's shape: framework-dependent **libraries** (including the shipped `Microsoft.UI.Reactor`) reference the lean `Microsoft.WindowsAppSDK.WinUI` sub-package; framework-dependent **apps** additionally reference `Microsoft.WindowsAppSDK.Runtime`; **self-contained / MSIX** projects keep the full `Microsoft.WindowsAppSDK` metapackage (it bundles the runtime + DWriteCore redist). The metapackage's AI/ML/Widgets/DWrite slices are otherwise unused. Versions are pinned in `Directory.Build.props` (`WindowsAppSDKWinUIVersion` and `WindowsAppSDKVersion`) — update them there to change the version for every project at once.
+> **Windows App SDK packages:** The right package is injected centrally by `Directory.Build.targets` based on each project's shape: framework-dependent projects — **libraries and apps alike**, including the shipped `Microsoft.UI.Reactor` — reference the lean `Microsoft.WindowsAppSDK.WinUI` sub-package plus `Microsoft.WindowsAppSDK.Runtime` (a library's dependencies become its consumers', and without Runtime the SDK silently builds a consumer app self-contained); **self-contained / MSIX** projects keep the full `Microsoft.WindowsAppSDK` metapackage (it bundles the runtime + DWriteCore redist). The metapackage's AI/ML/Widgets/DWrite slices are otherwise unused. Versions are pinned in `Directory.Build.props` (`WindowsAppSDKWinUIVersion` and `WindowsAppSDKVersion`) — update them there to change the version for every project at once.
> **Clone to a short path (Windows):** Clone into a short root directory such as
> `C:\src\` (for example `C:\src\microsoft-ui-reactor`) rather than a deeply
diff --git a/Directory.Build.targets b/Directory.Build.targets
index 5ec9801ce..61eb474e0 100644
--- a/Directory.Build.targets
+++ b/Directory.Build.targets
@@ -52,17 +52,24 @@
which transitively brings Base + Foundation + InteractiveExperiences + WebView2
(the bootstrapper, deployment manager, MRT Core and app-lifecycle MSBuild
targets). The metapackage also aggregates AI / ML / Widgets / DWrite, which the
- repo never uses — so framework-dependent projects reference only
- Microsoft.WindowsAppSDK.WinUI and let the machine-wide installed
- Microsoft.WindowsAppRuntime.2 supply native bits (DWriteCore, etc.) at runtime.
+ repo never uses — so framework-dependent projects reference
+ Microsoft.WindowsAppSDK.WinUI + Microsoft.WindowsAppSDK.Runtime rather than the
+ metapackage, and let the machine-wide installed Microsoft.WindowsAppRuntime.2
+ supply native bits (DWriteCore, etc.) at runtime.
+
+ Runtime is what makes an app framework-dependent: it sets
+ MicrosoftWindowsAppSDKPackageDir, and without it the SDK's
+ Microsoft.WindowsAppSDK.Base.targets silently defaults WindowsAppSDKSelfContained
+ to true (it only errors when a project sets that property to something other
+ than true explicitly). Libraries take it too, even though they don't produce an
+ app, because a library's dependencies become its consumers' dependencies: when
+ the published Microsoft.UI.Reactor carried WinUI alone, every app that
+ referenced only Reactor was silently built self-contained, which also demands a
+ RuntimeIdentifier.
Projects that BUNDLE the native runtime keep the full metapackage, which carries
the Runtime redist + DWriteCore: WindowsAppSDKSelfContained=true, or
- framework-dependent MSIX (WindowsPackageType=MSIX). A framework-dependent WinUI
- *executable* additionally needs Microsoft.WindowsAppSDK.Runtime — the SDK's
- Microsoft.WindowsAppSDK.Base.targets rejects a framework-dependent app that
- references neither Runtime nor WindowsAppSDKSelfContained. Libraries don't produce
- an app, so they take the lean WinUI reference alone.
+ framework-dependent MSIX (WindowsPackageType=MSIX).
Evaluating the conditions here in .targets (a late import) means per-project
OutputType / WindowsAppSDKSelfContained / WindowsPackageType values are already
@@ -80,14 +87,12 @@
<_ReactorWinAppSDKMetapackage>false
<_ReactorWinAppSDKMetapackage Condition="'$(WindowsAppSDKSelfContained)' == 'true' or '$(WindowsPackageType)' == 'MSIX'">true
- <_ReactorWinAppSDKFrameworkDependentApp>false
- <_ReactorWinAppSDKFrameworkDependentApp Condition="'$(_ReactorWinAppSDKMetapackage)' != 'true' and ('$(OutputType)' == 'Exe' or '$(OutputType)' == 'WinExe')">true
-
+
diff --git a/SKILL.md b/SKILL.md
index 86ae6220d..217fb33e4 100644
--- a/SKILL.md
+++ b/SKILL.md
@@ -541,11 +541,10 @@ For lightweight demos, skip the `.csproj` entirely. Add a file-level header:
```csharp
#:package Microsoft.UI.Reactor@0.0.0-local
-#:package Microsoft.WindowsAppSDK@2.2.0
+#:package Microsoft.Windows.SDK.BuildTools.WinApp@0.7.0
#:property OutputType=WinExe
#:property TargetFramework=net10.0-windows10.0.22621.0
#:property UseWinUI=true
-#:property WindowsPackageType=None
using Microsoft.UI.Reactor;
using static Microsoft.UI.Reactor.Factories;
@@ -557,9 +556,19 @@ ReactorApp.Run("Hello", ctx =>
});
```
-Run with `dotnet run MyApp.cs -p:Platform=ARM64` (or `x64`). In selfhost
-the version is `0.0.0-local` — run `mur pack-local` first if the package
-isn't found. Outside the clone, replace the version with the published
+Run with `dotnet run MyApp.cs` — no architecture flag. This is the header the
+[Getting Started guide](https://microsoft.github.io/microsoft-ui-reactor/getting-started/#one-file-no-project)
+documents: `Microsoft.Windows.SDK.BuildTools.WinApp` makes `dotnet run` launch
+the app packaged, with package identity, which needs Developer Mode. If
+Developer Mode is off, or you don't need identity, add
+`#:property WindowsPackageType=None` to run it as a plain unpackaged `.exe`.
+With neither, the app builds, then dies at startup with `REGDB_E_CLASSNOTREG`.
+Either way the app uses the Windows App Runtime installed on the machine. To
+bundle it instead, add `#:property WindowsAppSDKSelfContained=true`; that mode
+also needs an architecture (`-r win-x64` or `-r win-arm64`), and `-r` alone
+bundles nothing.
+In selfhost the version is `0.0.0-local` — run `mur pack-local` first if the
+package isn't found. Outside the clone, replace the version with the published
release you depend on.
> **Always capture `dotnet run` output.** Build errors exit with code 1.
diff --git a/docs/_pipeline/templates/getting-started.md.dt b/docs/_pipeline/templates/getting-started.md.dt
index 41b354b8d..253cfa5a8 100644
--- a/docs/_pipeline/templates/getting-started.md.dt
+++ b/docs/_pipeline/templates/getting-started.md.dt
@@ -82,6 +82,86 @@ identity — the same thing F5 does in Visual Studio. That requires **Developer
Mode** (Settings → System → For developers). See [Packaging](packaging.md) for
the unpackaged alternative.
+## One file, no project
+
+A Reactor app does not need a `.csproj`. .NET 10 runs a single `.cs` file
+directly, and file-level `#:` directives supply the configuration a project file
+otherwise would:
+
+```csharp
+#:package Microsoft.UI.Reactor@{{reactorVersion}}
+#:package Microsoft.Windows.SDK.BuildTools.WinApp@0.7.0
+#:property OutputType=WinExe
+#:property TargetFramework=net10.0-windows10.0.22621.0
+#:property UseWinUI=true
+
+using Microsoft.UI.Reactor;
+using Microsoft.UI.Reactor.Core;
+using static Microsoft.UI.Reactor.Factories;
+
+ReactorApp.Run("Hello Reactor");
+
+class MyApp : Component
+{
+ public override Element Render()
+ {
+ var (count, setCount) = UseState(0);
+
+ return VStack(
+ Heading($"Count: {count}"),
+ HStack(8,
+ Button("-", () => setCount(count - 1)),
+ Button("+", () => setCount(count + 1))
+ )
+ );
+ }
+}
+```
+
+Save it as `counter.cs` and run it:
+
+```powershell
+dotnet run counter.cs
+```
+
+That is the whole app — one file, no scaffolding, no build output to manage.
+It is the fastest way to try an idea, reproduce a bug, or paste a complete
+runnable example into an issue. Like a scaffolded app, it runs packaged:
+`Microsoft.Windows.SDK.BuildTools.WinApp` makes `dotnet run` synthesize an
+appxmanifest from the `#:property` values, register the app, and launch it with
+package identity, which needs **Developer Mode**. If you have the
+[winapp CLI](https://github.com/microsoft/winappCli) 0.7.0 or later,
+`winapp run counter.cs` does the same.
+
+`Microsoft.UI.Reactor` brings the Windows App SDK in transitively, exactly as in
+the [minimal `.csproj`](index.md#minimal-setup). The rest of the header:
+
+| Directive | Why it is required |
+|---|---|
+| `Microsoft.Windows.SDK.BuildTools.WinApp` | Lets `dotnet run` package the app. It carries its own copy of the winapp CLI, so that does not need installing. Without it, plain `dotnet run` cannot package the app, and it dies at startup with `COMException … REGDB_E_CLASSNOTREG` — an error that says nothing about the missing package — unless you run it unpackaged (below). |
+| `OutputType=WinExe` | Links the app for the Windows GUI subsystem. Without it the build still succeeds, but the `.exe` is a console binary, so Windows opens a console window alongside your UI for as long as the app runs. |
+| `TargetFramework=net10.0-windows10.0.22621.0` | Reactor ships `net10.0-windows10.0.22621`. A lower Windows version compiles against nothing and you get `CS0234: the namespace 'Reactor' does not exist`, as if the package were missing. |
+| `UseWinUI=true` | Brings in the WinUI 3 targets. |
+
+### Running it unpackaged
+
+`WindowsPackageType` decides whether the app runs packaged, and `dotnet run` and
+`winapp run` both obey it. Declare the app unpackaged to run it as a plain `.exe`:
+
+```csharp
+#:property WindowsPackageType=None
+```
+
+An unpackaged app has no package identity, so `Package.Current` and `ms-appx:`
+URIs are unavailable.
+
+Either way the app is framework-dependent: it uses the Windows App Runtime
+installed on the machine. To carry it with the app instead, add
+`#:property WindowsAppSDKSelfContained=true`. That mode also needs the
+architecture named with `-r win-x64` or `-r win-arm64`, because the runtime it
+copies is native code built separately for each CPU — `-r` on its own does not
+bundle anything.
+
## Contributor setup (one-time)
Working *on* Reactor rather than with it? Clone and bootstrap:
diff --git a/docs/_pipeline/templates/index.md.dt b/docs/_pipeline/templates/index.md.dt
index 771d6a682..2a13b7538 100644
--- a/docs/_pipeline/templates/index.md.dt
+++ b/docs/_pipeline/templates/index.md.dt
@@ -214,7 +214,9 @@ project and edit the `.csproj`:
One package reference is enough — `Microsoft.UI.Reactor` brings the Windows App
SDK in transitively, so you only add `Microsoft.WindowsAppSDK` yourself when you
want to pin a specific WinUI patch or bundle the runtime with
-`WindowsAppSDKSelfContained`. See [Packaging](packaging.md) for the
+`WindowsAppSDKSelfContained`. Pin WinUI through that full package, not
+`Microsoft.WindowsAppSDK.WinUI` on its own, which fails the Windows App SDK's
+version check. See [Packaging](packaging.md) for the
``, ARM64, and self-contained shapes the project template uses.
Replace `App.cs` with a component and a `ReactorApp.Run()` call, and run with
diff --git a/docs/_pipeline/templates/packaging.md.dt b/docs/_pipeline/templates/packaging.md.dt
index 5fac5b182..ff40bd60b 100644
--- a/docs/_pipeline/templates/packaging.md.dt
+++ b/docs/_pipeline/templates/packaging.md.dt
@@ -71,9 +71,10 @@ machines, with ARM64 second for Snapdragon X). The
sub-package brings the WinUI 3 SDK — reference assemblies plus the MSBuild
build/props/targets — while the native WinUI runtime is supplied by the
machine-wide Windows App Runtime install (or bundled into the publish output
-when `WindowsAppSDKSelfContained=true`). That sub-package is what a consumer
-gets transitively from `Microsoft.UI.Reactor`; the scaffolded template adds the
-full
+when `WindowsAppSDKSelfContained=true`). That sub-package, together with
+`Microsoft.WindowsAppSDK.Runtime` (which keeps the app framework-dependent), is
+what a consumer gets transitively from `Microsoft.UI.Reactor`; the scaffolded
+template adds the full
[`Microsoft.WindowsAppSDK`](https://www.nuget.org/packages/Microsoft.WindowsAppSDK)
metapackage on top at scaffold time (see below), because the self-contained
shape needs the Runtime redist the metapackage carries. Inside this repo the
@@ -295,9 +296,15 @@ frozen when the template shipped. The two references then sit
side-by-side in your CSPROJ, so the SDK version stays an obvious knob —
edit it there when you need a specific WinUI patch. If you skip the
template and reference `Microsoft.UI.Reactor` by hand, the SDK still
-arrives transitively as `Microsoft.WindowsAppSDK.WinUI`; add the full
+arrives transitively as `Microsoft.WindowsAppSDK.WinUI` plus
+`Microsoft.WindowsAppSDK.Runtime`, so the app builds framework-dependent; add the full
metapackage yourself when you want `WindowsAppSDKSelfContained=true` or
-an MSIX, both of which need the Runtime redist. The repo-internal
+an MSIX, both of which need the Runtime redist. The metapackage is also
+how you move to a newer WinUI, because it moves Runtime with it: a newer
+`Microsoft.WindowsAppSDK.WinUI` on its own leaves Runtime behind, and the
+build fails with *"One or more referenced Windows App SDK components are
+newer than the versions expected by the Microsoft.WindowsAppSDK.Runtime
+package"*. The repo-internal
`WindowsAppSDKVersion` MSBuild property only governs projects under this
clone (`Directory.Build.props`); consumer projects pick their version
directly.
diff --git a/docs/guide/README.md b/docs/guide/README.md
index e0bd7f939..17769014d 100644
--- a/docs/guide/README.md
+++ b/docs/guide/README.md
@@ -240,7 +240,9 @@ project and edit the `.csproj`:
One package reference is enough — `Microsoft.UI.Reactor` brings the Windows App
SDK in transitively, so you only add `Microsoft.WindowsAppSDK` yourself when you
want to pin a specific WinUI patch or bundle the runtime with
-`WindowsAppSDKSelfContained`. See [Packaging](packaging.md) for the
+`WindowsAppSDKSelfContained`. Pin WinUI through that full package, not
+`Microsoft.WindowsAppSDK.WinUI` on its own, which fails the Windows App SDK's
+version check. See [Packaging](packaging.md) for the
``, ARM64, and self-contained shapes the project template uses.
Replace `App.cs` with a component and a `ReactorApp.Run()` call, and run with
diff --git a/docs/guide/getting-started.md b/docs/guide/getting-started.md
index 4cc6ee722..6858a45f1 100644
--- a/docs/guide/getting-started.md
+++ b/docs/guide/getting-started.md
@@ -70,6 +70,86 @@ identity — the same thing F5 does in Visual Studio. That requires **Developer
Mode** (Settings → System → For developers). See [Packaging](packaging.md) for
the unpackaged alternative.
+## One file, no project
+
+A Reactor app does not need a `.csproj`. .NET 10 runs a single `.cs` file
+directly, and file-level `#:` directives supply the configuration a project file
+otherwise would:
+
+```csharp
+#:package Microsoft.UI.Reactor@0.1.0-preview.16
+#:package Microsoft.Windows.SDK.BuildTools.WinApp@0.7.0
+#:property OutputType=WinExe
+#:property TargetFramework=net10.0-windows10.0.22621.0
+#:property UseWinUI=true
+
+using Microsoft.UI.Reactor;
+using Microsoft.UI.Reactor.Core;
+using static Microsoft.UI.Reactor.Factories;
+
+ReactorApp.Run("Hello Reactor");
+
+class MyApp : Component
+{
+ public override Element Render()
+ {
+ var (count, setCount) = UseState(0);
+
+ return VStack(
+ Heading($"Count: {count}"),
+ HStack(8,
+ Button("-", () => setCount(count - 1)),
+ Button("+", () => setCount(count + 1))
+ )
+ );
+ }
+}
+```
+
+Save it as `counter.cs` and run it:
+
+```powershell
+dotnet run counter.cs
+```
+
+That is the whole app — one file, no scaffolding, no build output to manage.
+It is the fastest way to try an idea, reproduce a bug, or paste a complete
+runnable example into an issue. Like a scaffolded app, it runs packaged:
+`Microsoft.Windows.SDK.BuildTools.WinApp` makes `dotnet run` synthesize an
+appxmanifest from the `#:property` values, register the app, and launch it with
+package identity, which needs **Developer Mode**. If you have the
+[winapp CLI](https://github.com/microsoft/winappCli) 0.7.0 or later,
+`winapp run counter.cs` does the same.
+
+`Microsoft.UI.Reactor` brings the Windows App SDK in transitively, exactly as in
+the [minimal `.csproj`](index.md#minimal-setup). The rest of the header:
+
+| Directive | Why it is required |
+|---|---|
+| `Microsoft.Windows.SDK.BuildTools.WinApp` | Lets `dotnet run` package the app. It carries its own copy of the winapp CLI, so that does not need installing. Without it, plain `dotnet run` cannot package the app, and it dies at startup with `COMException … REGDB_E_CLASSNOTREG` — an error that says nothing about the missing package — unless you run it unpackaged (below). |
+| `OutputType=WinExe` | Links the app for the Windows GUI subsystem. Without it the build still succeeds, but the `.exe` is a console binary, so Windows opens a console window alongside your UI for as long as the app runs. |
+| `TargetFramework=net10.0-windows10.0.22621.0` | Reactor ships `net10.0-windows10.0.22621`. A lower Windows version compiles against nothing and you get `CS0234: the namespace 'Reactor' does not exist`, as if the package were missing. |
+| `UseWinUI=true` | Brings in the WinUI 3 targets. |
+
+### Running it unpackaged
+
+`WindowsPackageType` decides whether the app runs packaged, and `dotnet run` and
+`winapp run` both obey it. Declare the app unpackaged to run it as a plain `.exe`:
+
+```csharp
+#:property WindowsPackageType=None
+```
+
+An unpackaged app has no package identity, so `Package.Current` and `ms-appx:`
+URIs are unavailable.
+
+Either way the app is framework-dependent: it uses the Windows App Runtime
+installed on the machine. To carry it with the app instead, add
+`#:property WindowsAppSDKSelfContained=true`. That mode also needs the
+architecture named with `-r win-x64` or `-r win-arm64`, because the runtime it
+copies is native code built separately for each CPU — `-r` on its own does not
+bundle anything.
+
## Contributor setup (one-time)
Working *on* Reactor rather than with it? Clone and bootstrap:
diff --git a/docs/guide/index.md b/docs/guide/index.md
index e0bd7f939..17769014d 100644
--- a/docs/guide/index.md
+++ b/docs/guide/index.md
@@ -240,7 +240,9 @@ project and edit the `.csproj`:
One package reference is enough — `Microsoft.UI.Reactor` brings the Windows App
SDK in transitively, so you only add `Microsoft.WindowsAppSDK` yourself when you
want to pin a specific WinUI patch or bundle the runtime with
-`WindowsAppSDKSelfContained`. See [Packaging](packaging.md) for the
+`WindowsAppSDKSelfContained`. Pin WinUI through that full package, not
+`Microsoft.WindowsAppSDK.WinUI` on its own, which fails the Windows App SDK's
+version check. See [Packaging](packaging.md) for the
``, ARM64, and self-contained shapes the project template uses.
Replace `App.cs` with a component and a `ReactorApp.Run()` call, and run with
diff --git a/docs/guide/packaging.md b/docs/guide/packaging.md
index d3d89de3d..8543af81d 100644
--- a/docs/guide/packaging.md
+++ b/docs/guide/packaging.md
@@ -65,9 +65,10 @@ machines, with ARM64 second for Snapdragon X). The
sub-package brings the WinUI 3 SDK — reference assemblies plus the MSBuild
build/props/targets — while the native WinUI runtime is supplied by the
machine-wide Windows App Runtime install (or bundled into the publish output
-when `WindowsAppSDKSelfContained=true`). That sub-package is what a consumer
-gets transitively from `Microsoft.UI.Reactor`; the scaffolded template adds the
-full
+when `WindowsAppSDKSelfContained=true`). That sub-package, together with
+`Microsoft.WindowsAppSDK.Runtime` (which keeps the app framework-dependent), is
+what a consumer gets transitively from `Microsoft.UI.Reactor`; the scaffolded
+template adds the full
[`Microsoft.WindowsAppSDK`](https://www.nuget.org/packages/Microsoft.WindowsAppSDK)
metapackage on top at scaffold time (see below), because the self-contained
shape needs the Runtime redist the metapackage carries. Inside this repo the
@@ -313,9 +314,15 @@ frozen when the template shipped. The two references then sit
side-by-side in your CSPROJ, so the SDK version stays an obvious knob —
edit it there when you need a specific WinUI patch. If you skip the
template and reference `Microsoft.UI.Reactor` by hand, the SDK still
-arrives transitively as `Microsoft.WindowsAppSDK.WinUI`; add the full
+arrives transitively as `Microsoft.WindowsAppSDK.WinUI` plus
+`Microsoft.WindowsAppSDK.Runtime`, so the app builds framework-dependent; add the full
metapackage yourself when you want `WindowsAppSDKSelfContained=true` or
-an MSIX, both of which need the Runtime redist. The repo-internal
+an MSIX, both of which need the Runtime redist. The metapackage is also
+how you move to a newer WinUI, because it moves Runtime with it: a newer
+`Microsoft.WindowsAppSDK.WinUI` on its own leaves Runtime behind, and the
+build fails with *"One or more referenced Windows App SDK components are
+newer than the versions expected by the Microsoft.WindowsAppSDK.Runtime
+package"*. The repo-internal
`WindowsAppSDKVersion` MSBuild property only governs projects under this
clone (`Directory.Build.props`); consumer projects pick their version
directly.
diff --git a/docs/specs/022-packaging-and-distribution.md b/docs/specs/022-packaging-and-distribution.md
index e6958a8ce..625385f9a 100644
--- a/docs/specs/022-packaging-and-distribution.md
+++ b/docs/specs/022-packaging-and-distribution.md
@@ -430,7 +430,7 @@ Consumer gets: framework, analyzers, source generator, and WinUI SDK (transitive
- **Internal feed ownership.** Which Azure Artifacts organization hosts the P1 feed? Creating one takes ~a day plus approvals.
- **Signing prerequisite for P1.** Does the chosen internal feed enforce signed packages? If yes, P1 needs ESRP too, not just P2.
- **Package ID.** `Microsoft.UI.Reactor` assumes we stay in the `Microsoft.UI.*` namespace (see spec 018 for the namespace rename). If that namespace decision changes, the package ID follows.
-- **WinUI SDK version.** The right Windows App SDK package is injected centrally per project shape (`Directory.Build.targets`): framework-dependent libraries — including the shipped `Microsoft.UI.Reactor` — reference `Microsoft.WindowsAppSDK.WinUI` (pinned via `WindowsAppSDKWinUIVersion`), so the published package flows only WinUI to consumers rather than the full metapackage (AI/ML/Widgets/DWrite); framework-dependent apps additionally reference `Microsoft.WindowsAppSDK.Runtime`; self-contained/MSIX projects keep the `Microsoft.WindowsAppSDK` metapackage (`WindowsAppSDKVersion`). Consumers who want a different WinUI version can reference their own `Microsoft.WindowsAppSDK*` version — NuGet unifies it transitively with the library's.
+- **WinUI SDK version.** The right Windows App SDK package is injected centrally per project shape (`Directory.Build.targets`): framework-dependent projects — libraries and apps alike, including the shipped `Microsoft.UI.Reactor` — reference `Microsoft.WindowsAppSDK.WinUI` (pinned via `WindowsAppSDKWinUIVersion`) plus `Microsoft.WindowsAppSDK.Runtime` rather than the full metapackage (AI/ML/Widgets/DWrite), so the published package flows WinUI + Runtime to consumers — without Runtime the SDK silently builds a consumer app self-contained; self-contained/MSIX projects keep the `Microsoft.WindowsAppSDK` metapackage (`WindowsAppSDKVersion`). Consumers who want a newer WinUI reference the `Microsoft.WindowsAppSDK` metapackage at that version, which moves Runtime and the components together. Referencing `Microsoft.WindowsAppSDK.WinUI` alone leaves Runtime at the library's version, and Runtime's component-version check fails the build.
- **`mur` install-script trust boundary.** `iwr | iex` from GitHub Releases works for P1 but will concern P3 users. Document the signed-binary fallback (direct download + verify signature) before public launch.
## 14. Implementation Phases
diff --git a/plugins/reactor/skills/reactor-build-and-check/SKILL.md b/plugins/reactor/skills/reactor-build-and-check/SKILL.md
index 80c1e3f51..17c05b52c 100644
--- a/plugins/reactor/skills/reactor-build-and-check/SKILL.md
+++ b/plugins/reactor/skills/reactor-build-and-check/SKILL.md
@@ -10,9 +10,18 @@ Run after every non-trivial edit. **Read the output** — `dotnet run` exits wit
### Single-file `.cs` (default for new apps)
```powershell
-dotnet run App.cs -p:Platform=ARM64 # or -p:Platform=x64
+dotnet run App.cs
```
+The file header needs `#:package Microsoft.Windows.SDK.BuildTools.WinApp@0.7.0`,
+which launches the app packaged (needs Developer Mode), or
+`#:property WindowsPackageType=None`, which runs it unpackaged. With neither,
+the app builds, then dies at startup with `REGDB_E_CLASSNOTREG`. No
+`-p:Platform` is needed — only a self-contained app
+(`#:property WindowsAppSDKSelfContained=true`) needs an architecture. The
+[Getting Started guide](https://microsoft.github.io/microsoft-ui-reactor/getting-started/#one-file-no-project)
+documents the packaged header.
+
Single-file builds **do not load analyzers**. You'll catch CS errors but not the Reactor-specific `REACTOR_*` warnings.
### `.csproj` (multi-file, analyzer coverage)
@@ -136,8 +145,9 @@ Additional flags:
| `CS1061` | error | "'X' does not contain a definition for 'Y'" | A type-specific modifier called on an element type that does not declare it — `.Bold()` / `.FontSize()` / `.TextWrapping()` are `TextBlockElement`-only, so `Button("Go").Bold()` fails. This is **not** an ordering problem: modifiers are generic (` where T : Element`) and preserve the concrete type, so `.Margin(16).Bold()` and `.Bold().Margin(16)` compile alike. Check the receiver's element type against `reactor-dsl/references/reactor.api.txt`. |
| `CS0117` | error | "'Element' does not contain a definition for X" | You're calling a factory or static member that doesn't exist — confirm the name against `reactor-dsl/references/reactor.api.txt`. (For a missing *instance* modifier see `CS1061` above.) |
| `CS1955` | error | "Non-invocable member 'Element.Margin' cannot be used like a method" | Missing `using Microsoft.UI.Reactor;`. The element records expose same-named *properties* (`Element.Margin`, `Element.Padding`, plus `CornerRadius`/`BorderThickness` on derived records), and C# only falls back to extension methods when none is in scope — so an absent import reports the property as non-invocable rather than saying the fluent modifier isn't imported. It reads like the API doesn't exist; it does. Add the `using`. (Distinct from `REACTOR_DYM_001` above, which is a genuine property invoked with parens, e.g. `GridSize.Auto()`.) |
-| `MSB4025` | error | "The project file could not be loaded" | Single-file `.cs` build attempted without `-p:Platform=...` on a WinUI project. Add `-p:Platform=ARM64` (or x64). |
+| `MSB4025` | error | "The project file could not be loaded" | Self-contained (`WindowsAppSDKSelfContained=true`) single-file `.cs` build attempted without an architecture. Add `-p:Platform=ARM64` (or x64), or `-r win-arm64` / `-r win-x64`. |
| `NETSDK1136` | error | "platform required" | Same fix — pass `-p:Platform=ARM64` or `x64`. |
+| — | error | "One or more referenced Windows App SDK components are newer than the versions expected by the Microsoft.WindowsAppSDK.Runtime package" | A newer `Microsoft.WindowsAppSDK.WinUI` (or another Windows App SDK component) was referenced on its own, leaving `Microsoft.WindowsAppSDK.Runtime` at Reactor's version. Reference the full `Microsoft.WindowsAppSDK` package at the version you want instead; it moves Runtime with the components. Don't switch to `WindowsAppSDKSelfContained=true` just to silence it. |
If a `REACTOR_*` ID isn't in this table, the bundled analyzer DLL has more docs. The descriptions ship in the warnings themselves.
diff --git a/tests/Reactor.DocPipeline.Tests/InlineSnippetLedgerTests.cs b/tests/Reactor.DocPipeline.Tests/InlineSnippetLedgerTests.cs
index 5a19dbe24..dc65c78de 100644
--- a/tests/Reactor.DocPipeline.Tests/InlineSnippetLedgerTests.cs
+++ b/tests/Reactor.DocPipeline.Tests/InlineSnippetLedgerTests.cs
@@ -183,6 +183,68 @@ private static bool IsParameterDeclaration(string parameter)
private static readonly Dictionary> AllowedInlineExamples =
new(StringComparer.OrdinalIgnoreCase)
{
+ ["getting-started"] = new(StringComparer.Ordinal)
+ {
+ // A .NET file-based app is *defined* by having no .csproj, so it cannot be a doc
+ // app — the snippet mechanism extracts from projects. It stays one contiguous
+ // block rather than a ledgered header plus a snippet-backed body because the
+ // section's whole claim is "this is the entire file"; splitting it would show the
+ // reader two fragments to reassemble. It is also kept verbatim in step with the
+ // published blog's quick-start, so the two cannot drift.
+ //
+ // What that costs is smaller than it looks. Every construct in the body is
+ // exercised by a compiled snippet on this same page — ReactorApp.Run(string),
+ // Component/Render, UseState, VStack, Heading and HStack(spacing, …) all appear
+ // in getting-started/hello-world and getting-started/layout-basics, and
+ // UseState + Button(string, Action) in getting-started/usestate-counter. An API
+ // rename that broke this block would redden those first.
+ //
+ // The genuinely unverified part is the #: directives, and they are covered
+ // separately: the Reactor version is the {{reactorVersion}} token the doc
+ // compiler substitutes, and SingleFileGuideHeaderTests pins the TargetFramework
+ // to src/Reactor/Reactor.csproj and asserts each remaining directive sits in the
+ // block that needs it. That TFM check matters because a stale Windows version
+ // surfaces as CS0234 "the namespace 'Reactor' does not exist", which reads like
+ // a missing package rather than a wrong TFM.
+ ["""
+ #:package Microsoft.UI.Reactor@{{reactorVersion}}
+ #:package Microsoft.Windows.SDK.BuildTools.WinApp@0.7.0
+ #:property OutputType=WinExe
+ #:property TargetFramework=net10.0-windows10.0.22621.0
+ #:property UseWinUI=true
+
+ using Microsoft.UI.Reactor;
+ using Microsoft.UI.Reactor.Core;
+ using static Microsoft.UI.Reactor.Factories;
+
+ ReactorApp.Run("Hello Reactor");
+
+ class MyApp : Component
+ {
+ public override Element Render()
+ {
+ var (count, setCount) = UseState(0);
+
+ return VStack(
+ Heading($"Count: {count}"),
+ HStack(8,
+ Button("-", () => setCount(count - 1)),
+ Button("+", () => setCount(count + 1))
+ )
+ );
+ }
+ }
+ """] =
+ "File-based app: no .csproj by definition, so it cannot be a doc app. The published blog's quick-start app, plus the BuildTools package that lets dotnet run launch it packaged. Its DSL is covered by the compiled hello-world/usestate-counter/layout-basics snippets on the same page; its header by SingleFileGuideHeaderTests.",
+
+ // The one directive that switches the same file to an unpackaged launch. A
+ // fragment by design: the point is the delta, not a second copy of the app.
+ ["""
+ #:property WindowsPackageType=None
+ """] =
+ "One-line delta that runs the same file unpackaged; placement is guarded by SingleFileGuideHeaderTests.",
+ },
+
["source-mapping"] = new(StringComparer.Ordinal)
{
// The "before" half of the [ReactorSourceTransparent] explanation: a helper
diff --git a/tests/Reactor.DocPipeline.Tests/SingleFileGuideHeaderTests.cs b/tests/Reactor.DocPipeline.Tests/SingleFileGuideHeaderTests.cs
new file mode 100644
index 000000000..df6afd9ed
--- /dev/null
+++ b/tests/Reactor.DocPipeline.Tests/SingleFileGuideHeaderTests.cs
@@ -0,0 +1,242 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Text.RegularExpressions;
+using Xunit;
+
+namespace Microsoft.UI.Reactor.Cli.Docs.Tests;
+
+///
+/// Ties the file-based-app header documented in the Getting Started guide to the framework's
+/// own target framework.
+///
+///
+///
+/// Why this needs a test at all. A .NET file-based app is defined by having no
+/// .csproj, so the guide's #: header cannot be a compiled doc-app snippet the way
+/// every other C# block on that page is — it is ledgered in
+/// InlineSnippetLedgerTests.AllowedInlineExamples instead. That buys a hole: nothing
+/// compiles those six lines, and the page renders them identically to the verified blocks around
+/// them.
+///
+///
+/// The values that actually rot. The Reactor version is the {{reactorVersion}}
+/// token the doc compiler substitutes, so it cannot go stale. Every other directive is asserted
+/// here, because nothing else looks at them: the target framework against the framework project,
+/// and the rest for presence — plus, for WindowsPackageType, placement, since which block
+/// carries it is what makes one example packaged and the other not. The target framework is the
+/// worst to get wrong: a reader who copies a stale Windows version compiles against nothing and
+/// gets CS0234: the type or namespace name 'Reactor' does not exist, which reads like a
+/// missing package rather than a wrong TFM. Measured, not assumed — that is the exact error
+/// net10.0-windows10.0.19041.0 produces against this package.
+///
+///
+/// The Microsoft.Windows.SDK.BuildTools.WinApp pin in the header is deliberately
+/// not version-guarded. WinAppSDKReferenceGuardTests does sweep #:package
+/// headers, but only for Microsoft.WindowsAppSDK…, and this repo has no central pin for
+/// the build-tools package to check a doc against — it is a consumer-side tool rather than a
+/// framework dependency. Its presence is asserted below; its version is not.
+///
+///
+public class SingleFileGuideHeaderTests
+{
+ private const string GuideTemplate = "docs/_pipeline/templates/getting-started.md.dt";
+ private const string FrameworkProject = "src/Reactor/Reactor.csproj";
+
+ ///
+ /// Every #:property TargetFramework=… in the guide must name the TFM
+ /// src/Reactor/Reactor.csproj builds, because that is what the published package's
+ /// lib/ folder is keyed on.
+ ///
+ [Fact]
+ public void DocumentedTargetFramework_MatchesTheFrameworkProject()
+ {
+ var root = RepoRoot();
+ var expected = FrameworkTfm(root);
+
+ var guide = File.ReadAllText(Path.Join(root, GuideTemplate));
+ var documented = Regex.Matches(guide, @"^#:property\s+TargetFramework=(?\S+)\s*$", RegexOptions.Multiline)
+ .Select(m => m.Groups["tfm"].Value)
+ .ToArray();
+
+ // A rename or a restructure that drops the header entirely would otherwise leave this
+ // test passing over an empty set — the classic vacuous green.
+ Assert.NotEmpty(documented);
+
+ foreach (var tfm in documented)
+ {
+ Assert.True(
+ string.Equals(tfm, expected, StringComparison.Ordinal),
+ $"{GuideTemplate} documents '#:property TargetFramework={tfm}' but {FrameworkProject} "
+ + $"builds '{expected}'. A reader copying the stale value compiles against no Reactor "
+ + "assembly and sees CS0234 \"the namespace 'Reactor' does not exist\", which looks "
+ + "like a missing package. Update the guide header (both the unpackaged and packaged "
+ + "blocks) to match.");
+ }
+ }
+
+ ///
+ /// The primary example carries the full header; the second block is the unpackaged delta and
+ /// must be exactly the one directive that delta exists to add.
+ ///
+ ///
+ ///
+ /// The header runs packaged under both runners — Microsoft.Windows.SDK.BuildTools.WinApp
+ /// makes dotnet run package the app the way winapp run does — and
+ /// WindowsPackageType=None is the switch both obey to run it unpackaged instead. For a
+ /// Reactor-only app it is also what lets that plain exe start: the app is framework-dependent,
+ /// because the package flows Microsoft.WindowsAppSDK.Runtime (guarded by
+ /// WinAppSDKReferenceGuardTests.Reactor_library_flows_WinUI_and_Runtime_not_the_metapackage),
+ /// and the directive turns on the Windows App SDK bootstrapper. No architecture is needed: only
+ /// a self-contained app needs one, and the section offers that as an opt-in rather than a step.
+ /// Equality rather than Contains, so a RuntimeIdentifier line creeping back into
+ /// the delta fails here.
+ ///
+ ///
+ /// Placement matters as much as presence. Both runners obey the directive — measured: each
+ /// launched the app with no package identity — so in the primary block it would make every
+ /// launch unpackaged and silently cost the reader the identity that block exists to give them.
+ ///
+ ///
+ [Fact]
+ public void DotnetDeltaDirective_SitsInTheSecondBlockOnly()
+ {
+ var guide = File.ReadAllText(Path.Join(RepoRoot(), GuideTemplate));
+ var blocks = HeaderBlocks(guide);
+
+ // Two header blocks: the complete file, then the unpackaged delta. If the section is
+ // restructured into a different number, this needs rereading rather than silently
+ // passing over whichever blocks happen to remain.
+ Assert.Equal(2, blocks.Count);
+
+ const string directive = "#:property WindowsPackageType=None";
+ Assert.Equal(directive, blocks[1]);
+
+ Assert.False(
+ blocks[0].Contains(directive, StringComparison.Ordinal),
+ $"The first #: block is the complete single-file app, packaged by default, and must NOT "
+ + $"declare '{directive}': dotnet run and winapp run both obey it and launch the app "
+ + "unpackaged, so the reader would lose the package identity that block is there to give them.");
+ }
+
+ ///
+ /// The primary header is exactly the five directives the page documents.
+ ///
+ ///
+ /// Compared as an order-insensitive set of directive names: the values are pinned by the other
+ /// tests, and this one is about which directives the reader has to write at all. It is what
+ /// catches an architecture or packaging directive being added to the file everyone copies —
+ /// every such directive is either unnecessary or, like WindowsPackageType, changes how
+ /// the app runs.
+ ///
+ [Fact]
+ public void PrimaryHeader_DeclaresExactlyTheDocumentedDirectives()
+ {
+ var guide = File.ReadAllText(Path.Join(RepoRoot(), GuideTemplate));
+ var blocks = HeaderBlocks(guide);
+
+ Assert.Equal(2, blocks.Count);
+
+ var names = blocks[0].Split('\n')
+ .Select(d => Regex.Match(d, @"^#:(?:package\s+(?[^@\s]+)|property\s+(?[^=\s]+))").Groups["name"].Value)
+ .Order(StringComparer.Ordinal)
+ .ToArray();
+
+ Assert.Equal(
+ new[] { "Microsoft.UI.Reactor", "Microsoft.Windows.SDK.BuildTools.WinApp", "OutputType", "TargetFramework", "UseWinUI" },
+ names);
+ }
+
+ ///
+ /// Every directive the primary header's table calls load-bearing must appear in it.
+ ///
+ ///
+ /// These share a failure mode: the block is exempt from compilation, so dropping any one of
+ /// them leaves the whole suite green while a reader's run fails. Each row of the table is a
+ /// claim, and each claim is checked.
+ ///
+ /// - Microsoft.Windows.SDK.BuildTools.WinApp — without it plain dotnet run
+ /// cannot package the app, and it dies at startup with REGDB_E_CLASSNOTREG. Measured:
+ /// the same header with WindowsPackageType unset, or even set to MSIX, crashes
+ /// that way under plain dotnet run.
+ /// - OutputType=WinExe — does not fail the build at all; it links the app for the
+ /// console subsystem instead. Measured on this tree: the PE subsystem byte reads 3 (console)
+ /// without it and 2 (Windows GUI) with it, so a console window sits behind the UI.
+ /// - TargetFramework — also checked against the framework project by
+ /// ; this pins its presence
+ /// in the primary block specifically.
+ /// - UseWinUI=true — brings in the WinUI 3 targets.
+ ///
+ ///
+ [Theory]
+ [InlineData("#:package Microsoft.Windows.SDK.BuildTools.WinApp@")]
+ [InlineData("#:property OutputType=WinExe")]
+ [InlineData("#:property TargetFramework=")]
+ [InlineData("#:property UseWinUI=true")]
+ public void PrimaryHeader_DeclaresTheLoadBearingDirectives(string directive)
+ {
+ var guide = File.ReadAllText(Path.Join(RepoRoot(), GuideTemplate));
+ var blocks = HeaderBlocks(guide);
+
+ Assert.Equal(2, blocks.Count);
+ Assert.True(
+ blocks[0].Contains(directive, StringComparison.Ordinal),
+ $"The primary single-file header in {GuideTemplate} is missing '{directive}'. The "
+ + "guide's directive table documents it as required, and this block is exempt from "
+ + "compilation, so nothing else would catch its removal.");
+ }
+
+ ///
+ /// The contiguous runs of #: directives in the guide, in document order.
+ ///
+ private static List HeaderBlocks(string guide)
+ {
+ var blocks = new List();
+ var current = new List();
+
+ foreach (var line in guide.Split('\n').Select(l => l.TrimEnd('\r')))
+ {
+ if (line.StartsWith("#:", StringComparison.Ordinal))
+ {
+ current.Add(line);
+ }
+ else if (current.Count > 0)
+ {
+ blocks.Add(string.Join('\n', current));
+ current.Clear();
+ }
+ }
+
+ if (current.Count > 0)
+ blocks.Add(string.Join('\n', current));
+
+ return blocks;
+ }
+
+ private static string FrameworkTfm(string root)
+ {
+ var csproj = File.ReadAllText(Path.Join(root, FrameworkProject));
+ var match = Regex.Match(csproj, @"(?[^<]+)");
+
+ Assert.True(match.Success, $"No found in {FrameworkProject}.");
+ return match.Groups["tfm"].Value.Trim();
+ }
+
+ private static string RepoRoot()
+ {
+ // Anchor on the solution file, not on .git: in a git worktree .git is a *file*, so a
+ // Directory.Exists probe walks past the root and returns null. Mirrors FindRepoRoot in
+ // InlineSnippetLedgerTests.
+ var dir = AppContext.BaseDirectory;
+ while (dir is not null
+ && !File.Exists(Path.Join(dir, "Reactor.slnx"))
+ && !Directory.Exists(Path.Join(dir, ".git")))
+ {
+ dir = Path.GetDirectoryName(dir);
+ }
+
+ Assert.NotNull(dir);
+ return dir!;
+ }
+}
diff --git a/tests/Reactor.Tests/WinAppSDKReferenceGuardTests.cs b/tests/Reactor.Tests/WinAppSDKReferenceGuardTests.cs
index 2dddf07df..e5ac3968b 100644
--- a/tests/Reactor.Tests/WinAppSDKReferenceGuardTests.cs
+++ b/tests/Reactor.Tests/WinAppSDKReferenceGuardTests.cs
@@ -14,8 +14,9 @@ namespace Microsoft.UI.Reactor.Tests;
/// Guards the Windows App SDK dependency policy (see Directory.Build.targets).
/// Repo projects must not reference the full Microsoft.WindowsAppSDK
/// metapackage directly: the correct package is injected centrally —
-/// Microsoft.WindowsAppSDK.WinUI for framework-dependent libraries, WinUI +
-/// Microsoft.WindowsAppSDK.Runtime for framework-dependent apps, and the full
+/// Microsoft.WindowsAppSDK.WinUI + Microsoft.WindowsAppSDK.Runtime for
+/// framework-dependent projects (libraries included, so Runtime flows to consumer
+/// apps), and the full
/// metapackage only for self-contained / MSIX projects. A stray direct reference
/// bypasses that rule and re-drags the unused AI / ML / Widgets / DWrite slices back
/// into the dependency graph.
@@ -86,21 +87,31 @@ public void No_repo_project_references_the_WindowsAppSDK_metapackage_directly()
offenders.Count == 0,
"These projects reference the Microsoft.WindowsAppSDK metapackage directly. "
+ "Remove the reference — Directory.Build.targets injects "
- + "Microsoft.WindowsAppSDK.WinUI (+ .Runtime for apps) or the metapackage "
+ + "Microsoft.WindowsAppSDK.WinUI + .Runtime or the metapackage "
+ "(self-contained / MSIX) centrally. Offenders:\n "
+ string.Join("\n ", offenders.OrderBy(x => x, StringComparer.Ordinal)));
}
///
/// Outcome-level guard: the shipped Microsoft.UI.Reactor library must
- /// resolve the lean WinUI sub-package and must NOT flow the full metapackage to
- /// consumers. Reads Reactor's restore graph (present because this test project
- /// references Reactor), so it catches a central-rule regression wherever it is
- /// introduced — including in Directory.Build.*, which the source scan above
- /// does not cover.
+ /// resolve the lean WinUI sub-package plus Microsoft.WindowsAppSDK.Runtime,
+ /// and must NOT flow the full metapackage to consumers. Reads Reactor's restore
+ /// graph (present because this test project references Reactor), so it catches a
+ /// central-rule regression wherever it is introduced — including in
+ /// Directory.Build.*, which the source scan above does not cover.
///
+ ///
+ /// Runtime is what keeps a consumer app framework-dependent: it sets
+ /// MicrosoftWindowsAppSDKPackageDir, and an app whose graph lacks it is
+ /// silently built self-contained by the SDK's Base.targets. That is what
+ /// happened to every app referencing only Reactor between #822 and this guard,
+ /// surfacing as "WindowsAppSDKSelfContained requires a supported Windows
+ /// architecture". Being in Reactor's own graph is not enough, so the edge is also
+ /// checked for flowing: a PrivateAssets="all" reference would pass the graph
+ /// check while never reaching the nuspec.
+ ///
[Fact]
- public void Reactor_library_resolves_WinUI_subpackage_not_the_metapackage()
+ public void Reactor_library_flows_WinUI_and_Runtime_not_the_metapackage()
{
var root = RepoRootFinder.FindRepoRoot();
Assert.NotNull(root);
@@ -116,7 +127,24 @@ public void Reactor_library_resolves_WinUI_subpackage_not_the_metapackage()
.ToList();
Assert.Contains(winAppSdk, k => k.StartsWith("Microsoft.WindowsAppSDK.WinUI/", StringComparison.Ordinal));
+ Assert.Contains(winAppSdk, k => k.StartsWith("Microsoft.WindowsAppSDK.Runtime/", StringComparison.Ordinal));
Assert.DoesNotContain(winAppSdk, k => k.StartsWith("Microsoft.WindowsAppSDK/", StringComparison.Ordinal));
+
+ foreach (var framework in doc.RootElement.GetProperty("project").GetProperty("frameworks").EnumerateObject())
+ {
+ Assert.True(
+ framework.Value.GetProperty("dependencies").TryGetProperty("Microsoft.WindowsAppSDK.Runtime", out var runtime),
+ $"Microsoft.WindowsAppSDK.Runtime is not a direct dependency of Reactor for {framework.Name}, so "
+ + "nothing guarantees it reaches the nuspec. The Windows App SDK injection in "
+ + "Directory.Build.targets must give framework-dependent libraries Runtime as well as WinUI.");
+
+ var suppressParent = runtime.TryGetProperty("suppressParent", out var value) ? value.GetString() : null;
+ Assert.False(
+ string.Equals(suppressParent, "All", StringComparison.OrdinalIgnoreCase),
+ $"Microsoft.WindowsAppSDK.Runtime is a private asset of Reactor for {framework.Name} "
+ + "(suppressParent=All), so it never reaches consumer apps and they fall back to "
+ + "self-contained. Drop PrivateAssets from that reference.");
+ }
}
///