diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index c38bd546..705eefab 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -8,51 +8,30 @@ on: jobs: build: - runs-on: windows-latest + runs-on: ubuntu-latest steps: - name: Setup .NET (With cache) uses: actions/setup-dotnet@v5.2.0 with: - dotnet-version: | - 8.0.x - 9.0.x - 10.0.x - dotnet-quality: 'preview' + dotnet-version: 10.0.x + dotnet-quality: 'preview' - - name: 'Setup Java JDK 11' - uses: actions/setup-java@v5.2.0 - with: - distribution: 'microsoft' - java-version: '11' - - - name: 'Add MSBuild to PATH' - uses: microsoft/setup-msbuild@v2.0.0 - with: - vs-prerelease: true - - - name: 'Install DotNet workloads' - shell: bash - run: | - dotnet workload install android ios tvos macos maui maccatalyst - - - name: Checkout + - name: Checkout uses: actions/checkout@v6 with: fetch-depth: 0 - - name: 'Cache: .nuke/temp, ~/.nuget/packages' + - name: 'Cache: .nuke/temp, NuGet packages' uses: actions/cache@v5 with: path: | .nuke/temp ~/.nuget/packages - key: ${{ runner.os }}-${{ hashFiles('**/global.json', '**/*.csproj', '**/Directory.Packages.props') }} + reactiveui/api/cache + key: ${{ runner.os }}-${{ hashFiles('**/global.json', 'nuget-packages.json') }} - - name: 'Restore and Compile ReactiveUI Projects' - run: ./build.cmd Compile - - name: 'Build Website' - run: ./build.cmd BuildWebsite + run: ./build.sh BuildWebsite - name: 'Deploy netlify' if: ${{ github.event_name != 'pull_request' }} diff --git a/.gitignore b/.gitignore index 2078f48d..6a9b5eeb 100644 --- a/.gitignore +++ b/.gitignore @@ -349,5 +349,12 @@ MigrationBackup/ # Ionide (cross platform F# VS Code tools) working folder .ionide/ +reactiveui/api/lib/ +reactiveui/api/cache/ +reactiveui/api/refs/ reactiveui/api/reactiveui/ reactiveui/api/reactivemarbles/ +reactiveui/api-android/ +reactiveui/api-test/ +reactiveui/api-windows/ +reactiveui/_site/ diff --git a/.nuke/build.schema.json b/.nuke/build.schema.json index fd63df01..aef44719 100644 --- a/.nuke/build.schema.json +++ b/.nuke/build.schema.json @@ -26,8 +26,8 @@ "enum": [ "BuildWebsite", "Clean", - "Compile", - "Restore" + "FetchPackages", + "Serve" ] }, "Verbosity": { @@ -108,6 +108,11 @@ "Debug", "Release" ] + }, + "Port": { + "type": "integer", + "description": "Port for the preview server (default: 8080)", + "format": "int32" } } }, diff --git a/build.sh b/build.sh old mode 100644 new mode 100755 diff --git a/build/Build.cs b/build/Build.cs index 3eded580..79c83d95 100644 --- a/build/Build.cs +++ b/build/Build.cs @@ -1,149 +1,70 @@ using Nuke.Common; using Nuke.Common.IO; -using Nuke.Common.ProjectModel; using Nuke.Common.Tooling; -using Nuke.Common.Tools.DotNet; -using Nuke.Common.Tools.MSBuild; using ReactiveUI.Web; using System; -using System.IO; -using System.Linq; -using static Nuke.Common.Tools.DotNet.DotNetTasks; class Build : NukeBuild { - /// Support plugins are available for: - /// - JetBrains ReSharper https://nuke.build/resharper - /// - JetBrains Rider https://nuke.build/rider - /// - Microsoft VisualStudio https://nuke.build/visualstudio - /// - Microsoft VSCode https://nuke.build/vscode - - public static int Main() => Execute(x => x.Compile); + public static int Main() => Execute(x => x.BuildWebsite); [Parameter("Configuration to build - Default is 'Debug' (local) or 'Release' (server)")] readonly Configuration Configuration = IsLocalBuild ? Configuration.Debug : Configuration.Release; private static readonly string reactiveui = nameof(reactiveui); - private static readonly string akavache = nameof(akavache); - private static readonly string fusillade = nameof(fusillade); - private static readonly string punchclock = nameof(punchclock); - private static readonly string splat = nameof(splat); - private static readonly string DynamicData = nameof(DynamicData); - private static readonly string reactivemarbles = nameof(reactivemarbles); - private static readonly string extensions = nameof(extensions); - private static readonly string[] RxUIProjects = [akavache, fusillade, punchclock, splat, extensions]; ////, reactiveui, "ReactiveUI.Validation", "ReactiveUI.Avalonia", "Maui.Plugins.Popup"]; - - private AbsolutePath RxUIAPIDirectory => RootDirectory / reactiveui / "api" / reactiveui; - private AbsolutePath RxMAPIDirectory => RootDirectory / reactiveui / "api" / reactivemarbles; - + private static readonly string api = nameof(api); + private AbsolutePath WebRootPath => RootDirectory / reactiveui; + private AbsolutePath ApiPath => WebRootPath / api; + private AbsolutePath ApiLibDirectory => ApiPath / "lib"; + private AbsolutePath ApiRefsDirectory => ApiPath / "refs"; + private AbsolutePath ApiCacheDirectory => ApiPath / "cache"; + private AbsolutePath DocfxConfigPath => WebRootPath / "docfx.json"; + private AbsolutePath SiteOutputPath => WebRootPath / "_site"; Target Clean => _ => _ - .Before(Restore) + .Before(FetchPackages) .Executes(() => { - RxUIAPIDirectory.DeleteDirectory(); - RxMAPIDirectory.DeleteDirectory(); + ApiLibDirectory.DeleteDirectory(); + ApiRefsDirectory.DeleteDirectory(); // Install docfx ProcessTasks.StartShell("dotnet tool update -g docfx").AssertZeroExitCode(); }); - Target Restore => _ => _ + Target FetchPackages => _ => _ .DependsOn(Clean) .Executes(() => { - // Restore ReactiveUI Projects - RxUIAPIDirectory.GetSources(RootDirectory, reactiveui, RxUIProjects); - - // Restore Reactive Marbles Projects - RxMAPIDirectory.GetSources(RootDirectory, reactivemarbles, DynamicData); + NuGetFetcher.FetchPackages(RootDirectory, ApiPath); }); - Target Compile => _ => _ - .DependsOn(Restore) + Target BuildWebsite => _ => _ + .DependsOn(FetchPackages) + .Produces(SiteOutputPath) .Executes(() => { - foreach (var project in RxUIProjects) - { - try - { - var dirRx = RxUIAPIDirectory / "external" / project / $"{project}-main" / "src"; - File.Copy(RootDirectory / "global.json", dirRx / "global.json", true); - var solutionFile = dirRx / $"{project}.sln"; - - // Find any .sln or .slnx file as a fallback - var solutionFileSearch = Directory.EnumerateFiles(dirRx, "*.sln*").FirstOrDefault(); - if (File.Exists(solutionFile) == false) - { - solutionFile += "x"; - if (File.Exists(solutionFile) == false) - { - if (File.Exists(solutionFileSearch)) - { - solutionFile = solutionFileSearch; - } - else - { - SourceFetcher.LogRepositoryError(reactiveui, project, $"Solution file not found: {dirRx / $"{project}.sln(x)"}"); - continue; - } - } - } - - SourceFetcher.LogInfo($"Restoring {solutionFile}..."); - DotNetRestore(s => s.SetProjectFile(solutionFile)); - SourceFetcher.LogInfo($"Building {solutionFile}..."); - DotNetBuild(s => s - .SetProjectFile(solutionFile) - .SetConfiguration(Configuration) - .EnableNoRestore()); - SourceFetcher.LogInfo($"{project} build complete"); - } - catch (Exception ex) - { - SourceFetcher.LogRepositoryError(reactiveui, project, ex.ToString()); - } - } - try { - var dirDd = RxMAPIDirectory / "external" / DynamicData / $"{DynamicData}-main" / "src"; - File.Copy(RootDirectory / "global.json", dirDd / "global.json", true); - var solutionFile = dirDd / $"{DynamicData}.sln"; - if (File.Exists(solutionFile) == false) - { - solutionFile += "x"; - if (File.Exists(solutionFile) == false) - { - SourceFetcher.LogRepositoryError(reactivemarbles, DynamicData, $"Solution file not found: {dirDd / $"{DynamicData}.sln(x)"}"); - return; - } - } - - SourceFetcher.LogInfo($"Building {solutionFile}..."); - MSBuildTasks.MSBuild(s => s - .SetProjectFile(solutionFile) - .SetConfiguration(Configuration) - .SetRestore(true)); - SourceFetcher.LogInfo($"{DynamicData} build complete"); + NuGetFetcher.PatchDocfxJson(RootDirectory); + ProcessTasks.StartProcess("docfx", $"\"{DocfxConfigPath}\"", workingDirectory: RootDirectory) + .AssertZeroExitCode(); + NuGetFetcher.LogInfo("Web Site build complete"); } catch (Exception ex) { - SourceFetcher.LogRepositoryError(reactivemarbles, DynamicData, ex.ToString()); + NuGetFetcher.LogError(ex.ToString()); } }); - Target BuildWebsite => _ => _ - .Produces(RootDirectory / reactiveui / "_site") - .Executes(() => - { - try - { - ProcessTasks.StartShell("docfx reactiveui/docfx.json").AssertZeroExitCode(); - SourceFetcher.LogInfo("Web Site build complete"); - } - catch (Exception ex) + [Parameter("Port for the preview server (default: 8080)")] + readonly int Port = 8080; + + Target Serve => _ => _ + .DependsOn(BuildWebsite) + .Executes(() => { - SourceFetcher.LogError(ex.ToString()); - } - }); + NuGetFetcher.LogInfo($"Serving website at http://localhost:{Port}"); + ProcessTasks.StartProcess("docfx", $"serve \"{SiteOutputPath}\" -p {Port}", workingDirectory: RootDirectory) + .AssertZeroExitCode(); + }); } diff --git a/build/NuGetFetcher.cs b/build/NuGetFetcher.cs new file mode 100644 index 00000000..9067a5cd --- /dev/null +++ b/build/NuGetFetcher.cs @@ -0,0 +1,814 @@ +#nullable enable + +using System; +using System.Collections.Generic; +using System.IO; +using System.IO.Compression; +using System.Linq; +using System.Net.Http; +using System.Reflection.PortableExecutable; +using System.Text.Json; +using System.Text.Json.Serialization; +using System.Threading; +using System.Threading.Tasks; +using Nuke.Common.IO; +using Polly; + +namespace ReactiveUI.Web; + +internal static class NuGetFetcher +{ + private static readonly object _lockConsoleObject = new(); + + private static readonly JsonSerializerOptions _jsonOptions = new() + { + PropertyNameCaseInsensitive = true + }; + + private sealed class PackageConfig + { + public string[] NugetPackageOwners { get; set; } = []; + public string[] TfmPreference { get; set; } = []; + public AdditionalPackage[] AdditionalPackages { get; set; } = []; + public string[] ExcludePackages { get; set; } = []; + public ReferencePackage[] ReferencePackages { get; set; } = []; + public Dictionary TfmOverrides { get; set; } = new(); + } + + private sealed class AdditionalPackage + { + public string Id { get; set; } = ""; + public string? Version { get; set; } + } + + private sealed class ReferencePackage + { + public string Id { get; set; } = ""; + public string? Version { get; set; } + public string TargetTfm { get; set; } = ""; + public string PathPrefix { get; set; } = "ref"; + } + + public static void FetchPackages(AbsolutePath rootDirectory, AbsolutePath apiPath) + { + var manifestPath = rootDirectory / "nuget-packages.json"; + var json = File.ReadAllText(manifestPath); + var config = JsonSerializer.Deserialize(json, _jsonOptions) + ?? throw new InvalidOperationException("Failed to deserialize nuget-packages.json"); + + // DLLs go into per-TFM subdirectories under lib/ for docfx to resolve cross-package references + var libDir = apiPath / "lib"; + var cacheDir = apiPath / "cache"; + var refsDir = apiPath / "refs"; + + libDir.CreateDirectory(); + cacheDir.CreateDirectory(); + refsDir.CreateDirectory(); + + // Fetch reference assembly packages (support libraries for docfx resolution) + if (config.ReferencePackages.Length > 0) + FetchReferencePackages(config.ReferencePackages, refsDir, cacheDir); + + // Discover packages from NuGet owners + var discoveredIds = DiscoverAllPackages(config); + + // Merge additional packages + foreach (var additional in config.AdditionalPackages) + { + if (!discoveredIds.Any(d => string.Equals(d.Id, additional.Id, StringComparison.OrdinalIgnoreCase))) + { + discoveredIds.Add((additional.Id, additional.Version)); + } + } + + // Apply excludes + var excludeSet = new HashSet(config.ExcludePackages, StringComparer.OrdinalIgnoreCase); + discoveredIds.RemoveAll(d => excludeSet.Contains(d.Id)); + + // Build final package list with TFM overrides + var allPackages = discoveredIds.Select(d => + { + config.TfmOverrides.TryGetValue(d.Id, out var tfm); + return (d.Id, d.Version, Tfm: tfm); + }).ToArray(); + + LogInfo($"Fetching {allPackages.Length} packages..."); + FetchGroup(libDir, cacheDir, allPackages, config.TfmPreference); + + // Copy reference assemblies into each lib/ TFM directory so docfx's + // UniversalAssemblyResolver can find them (it searches the assembly's + // own directory first, before the references section which is unreliable) + CopyRefsIntoLibDirs(libDir, refsDir); + } + + private static void CopyRefsIntoLibDirs(AbsolutePath libDir, AbsolutePath refsDir) + { + if (!Directory.Exists(libDir) || !Directory.Exists(refsDir)) + return; + + var refsTfms = Directory.GetDirectories(refsDir) + .Select(d => Path.GetFileName(d)!) + .ToList(); + + foreach (var libTfmDir in Directory.GetDirectories(libDir)) + { + var libTfm = Path.GetFileName(libTfmDir)!; + var bestRef = FindBestRefsTfm(libTfm, refsTfms); + if (bestRef == null) continue; + + var refDir = Path.Combine(refsDir, bestRef); + var count = 0; + foreach (var refDll in Directory.GetFiles(refDir, "*.dll")) + { + var destPath = Path.Combine(libTfmDir, Path.GetFileName(refDll)); + if (!File.Exists(destPath)) + { + File.Copy(refDll, destPath); + count++; + } + } + if (count > 0) + LogInfo($"Copied {count} reference assemblies into lib/{libTfm} (from refs/{bestRef})"); + } + } + + private static List<(string Id, string? Version)> DiscoverAllPackages(PackageConfig config) + { + using var client = new HttpClient(); + var retryPolicy = Policy.Handle() + .WaitAndRetryAsync( + 6, + attempt => TimeSpan.FromSeconds(0.1 * Math.Pow(2, attempt))); + + var searchEndpoint = ResolveSearchEndpoint(client, retryPolicy).GetAwaiter().GetResult(); + LogInfo($"Using NuGet search endpoint: {searchEndpoint}"); + + var allIds = new List<(string Id, string? Version)>(); + + foreach (var owner in config.NugetPackageOwners) + { + var ids = DiscoverPackagesByOwner(client, retryPolicy, searchEndpoint, owner) + .GetAwaiter().GetResult(); + LogInfo($"Discovered {ids.Count} packages for owner '{owner}'"); + allIds.AddRange(ids.Select(id => (id, (string?)null))); + } + + return allIds; + } + + private static void FetchReferencePackages( + ReferencePackage[] packages, + AbsolutePath refsDir, + AbsolutePath cacheDir) + { + using var client = new HttpClient(); + var retryPolicy = Policy.Handle() + .WaitAndRetryAsync( + 6, + attempt => TimeSpan.FromSeconds(0.1 * Math.Pow(2, attempt))); + + foreach (var pkg in packages) + { + try + { + var idLower = pkg.Id.ToLowerInvariant(); + + var version = pkg.Version; + if (version == null) + { + LogInfo($"Resolving latest version for reference package {pkg.Id}..."); + version = ResolveLatestStableVersion(client, retryPolicy, idLower) + .GetAwaiter().GetResult(); + if (version == null) + { + LogError($"Could not resolve version for reference package {pkg.Id}, skipping"); + continue; + } + } + + LogInfo($"Using reference package {pkg.Id} v{version}"); + + var versionLower = version.ToLowerInvariant(); + var nupkgPath = cacheDir / $"{idLower}.{versionLower}.nupkg"; + if (!File.Exists(nupkgPath)) + { + LogInfo($"Downloading {pkg.Id} v{version}..."); + DownloadNupkg(client, retryPolicy, idLower, versionLower, nupkgPath) + .GetAwaiter().GetResult(); + } + else + { + LogInfo($"Using cached {pkg.Id} v{version}"); + } + + var tfmRefsDir = refsDir / pkg.TargetTfm; + tfmRefsDir.CreateDirectory(); + ExtractReferenceAssemblies(nupkgPath, tfmRefsDir, pkg.Id, pkg.PathPrefix); + } + catch (Exception ex) + { + LogError($"Failed to fetch reference package {pkg.Id}: {ex.Message}"); + } + } + } + + private static bool IsManagedAssembly(Stream stream) + { + try + { + using var peReader = new PEReader(stream, PEStreamOptions.LeaveOpen); + // HasMetadata passes mixed-mode assemblies (native+managed) like + // System.EnterpriseServices.Wrapper.dll — checking ILOnly ensures we + // only extract pure managed assemblies that Roslyn can reference + return peReader.HasMetadata + && peReader.PEHeaders.CorHeader != null + && peReader.PEHeaders.CorHeader.Flags.HasFlag(CorFlags.ILOnly); + } + catch + { + return false; + } + } + + private static void ExtractReferenceAssemblies( + AbsolutePath nupkgPath, + AbsolutePath tfmRefsDir, + string packageId, + string pathPrefix) + { + using var archive = ZipFile.OpenRead(nupkgPath); + var prefix = pathPrefix.TrimEnd('/') + "/"; + var count = 0; + + foreach (var entry in archive.Entries) + { + if (!entry.FullName.StartsWith(prefix, StringComparison.OrdinalIgnoreCase)) + continue; + if (string.IsNullOrEmpty(entry.Name)) + continue; + + var ext = Path.GetExtension(entry.Name).ToLowerInvariant(); + if (ext is ".dll") + { + using var entryStream = entry.Open(); + using var memStream = new MemoryStream(); + entryStream.CopyTo(memStream); + + // Skip native DLLs — only extract managed .NET assemblies + memStream.Position = 0; + if (!IsManagedAssembly(memStream)) + { + LogInfo($" Skipping native DLL: {entry.Name}"); + continue; + } + + memStream.Position = 0; + var destPath = Path.Combine(tfmRefsDir, entry.Name); + using var fileStream = new FileStream(destPath, FileMode.Create); + memStream.CopyTo(fileStream); + count++; + } + } + + LogInfo($"Extracted {count} reference assemblies from {packageId} ({pathPrefix})"); + } + + private static async Task ResolveSearchEndpoint( + HttpClient client, + IAsyncPolicy retryPolicy) + { + string? endpoint = null; + + await retryPolicy.ExecuteAsync(async () => + { + var response = await client.GetAsync("https://api.nuget.org/v3/index.json"); + response.EnsureSuccessStatusCode(); + + using var stream = await response.Content.ReadAsStreamAsync(); + using var doc = await JsonDocument.ParseAsync(stream); + + foreach (var resource in doc.RootElement.GetProperty("resources").EnumerateArray()) + { + var type = resource.GetProperty("@type").GetString(); + if (type == "SearchQueryService/3.5.0") + { + endpoint = resource.GetProperty("@id").GetString(); + break; + } + } + }); + + return endpoint ?? throw new InvalidOperationException( + "Could not find SearchQueryService/3.5.0 in NuGet service index"); + } + + private static async Task> DiscoverPackagesByOwner( + HttpClient client, + IAsyncPolicy retryPolicy, + string searchEndpoint, + string owner) + { + var packageIds = new List(); + var skip = 0; + const int take = 100; + + while (true) + { + var url = $"{searchEndpoint}?q=owner:{owner}&take={take}&skip={skip}&semVerLevel=2.0.0"; + int totalHits = 0; + + await retryPolicy.ExecuteAsync(async () => + { + var response = await client.GetAsync(url); + response.EnsureSuccessStatusCode(); + + using var stream = await response.Content.ReadAsStreamAsync(); + using var doc = await JsonDocument.ParseAsync(stream); + + totalHits = doc.RootElement.GetProperty("totalHits").GetInt32(); + + foreach (var result in doc.RootElement.GetProperty("data").EnumerateArray()) + { + // Skip deprecated packages + if (result.TryGetProperty("deprecation", out _)) + continue; + + var id = result.GetProperty("id").GetString(); + if (id != null) + packageIds.Add(id); + } + }); + + skip += take; + if (skip >= totalHits) + break; + } + + return packageIds; + } + + private static void FetchGroup( + AbsolutePath libDir, + AbsolutePath cacheDir, + (string Id, string? Version, string? Tfm)[] packages, + string[] tfmPreference) + { + using var client = new HttpClient(); + using var semaphore = new SemaphoreSlim(3, 3); + + var retryPolicy = Policy.Handle() + .WaitAndRetryAsync( + 6, + attempt => TimeSpan.FromSeconds(0.1 * Math.Pow(2, attempt))); + + Task.WaitAll(packages.Select(pkg => Task.Run(async () => + { + await semaphore.WaitAsync(); + try + { + var id = pkg.Id; + var idLower = id.ToLowerInvariant(); + + // Resolve version + var version = pkg.Version; + if (version == null) + { + LogInfo($"Resolving latest version for {id}..."); + version = await ResolveLatestStableVersion(client, retryPolicy, idLower); + if (version == null) + { + LogError($"Could not resolve version for {id}"); + return; + } + } + + LogInfo($"Using {id} v{version}"); + + // Download .nupkg (with cache) + var versionLower = version.ToLowerInvariant(); + var nupkgPath = cacheDir / $"{idLower}.{versionLower}.nupkg"; + if (!File.Exists(nupkgPath)) + { + LogInfo($"Downloading {id} v{version}..."); + await DownloadNupkg(client, retryPolicy, idLower, versionLower, nupkgPath); + } + else + { + LogInfo($"Using cached {id} v{version}"); + } + + // Extract DLL + XML + ExtractAssemblies(nupkgPath, libDir, id, pkg.Tfm, tfmPreference); + LogInfo($"Extracted {id} v{version}"); + } + catch (Exception ex) + { + LogError($"Failed to process {pkg.Id}: {ex.Message}"); + } + finally + { + semaphore.Release(); + } + })).ToArray()); + } + + private static async Task ResolveLatestStableVersion( + HttpClient client, + IAsyncPolicy retryPolicy, + string idLower) + { + string? result = null; + var url = $"https://api.nuget.org/v3-flatcontainer/{idLower}/index.json"; + + await retryPolicy.ExecuteAsync(async () => + { + var response = await client.GetAsync(url); + response.EnsureSuccessStatusCode(); + + using var stream = await response.Content.ReadAsStreamAsync(); + using var doc = await JsonDocument.ParseAsync(stream); + + var versions = doc.RootElement.GetProperty("versions") + .EnumerateArray() + .Select(v => v.GetString()!) + .Where(v => !v.Contains('-')) // Exclude prerelease + .ToArray(); + + result = versions.LastOrDefault(); // Versions are sorted ascending by the API + }); + + return result; + } + + private static async Task DownloadNupkg( + HttpClient client, + IAsyncPolicy retryPolicy, + string idLower, + string versionLower, + AbsolutePath outputPath) + { + var url = $"https://api.nuget.org/v3-flatcontainer/{idLower}/{versionLower}/{idLower}.{versionLower}.nupkg"; + + await retryPolicy.ExecuteAsync(async () => + { + var response = await client.GetAsync(url); + response.EnsureSuccessStatusCode(); + + using var contentStream = await response.Content.ReadAsStreamAsync(); + using var fileStream = new FileStream(outputPath, FileMode.Create); + await contentStream.CopyToAsync(fileStream); + }); + } + + private static void ExtractAssemblies( + AbsolutePath nupkgPath, + AbsolutePath libDir, + string packageId, + string? tfmOverride, + string[] tfmPreference) + { + using var archive = ZipFile.OpenRead(nupkgPath); + + // Find all lib/ entries grouped by TFM + var libEntries = archive.Entries + .Where(e => e.FullName.StartsWith("lib/", StringComparison.OrdinalIgnoreCase) + && !string.IsNullOrEmpty(e.Name)) + .GroupBy(e => + { + var parts = e.FullName.Split('/'); + return parts.Length >= 2 ? parts[1] : ""; + }) + .Where(g => g.Key != "") + .ToDictionary(g => g.Key, g => g.ToArray(), StringComparer.OrdinalIgnoreCase); + + if (libEntries.Count == 0) + { + LogInfo($"No lib/ entries found in {packageId}, skipping (source generator or meta-package)"); + return; + } + + // Select TFM + var selectedTfm = SelectTfm(libEntries.Keys, tfmOverride, tfmPreference); + if (selectedTfm == null) + { + LogError($"No matching TFM for {packageId}. Available: {string.Join(", ", libEntries.Keys)}"); + return; + } + + LogInfo($" {packageId}: selected TFM '{selectedTfm}'"); + + // Extract DLL and XML files into per-TFM subdirectory + var tfmLibDir = Path.Combine(libDir, selectedTfm); + Directory.CreateDirectory(tfmLibDir); + + foreach (var entry in libEntries[selectedTfm]) + { + var ext = Path.GetExtension(entry.Name).ToLowerInvariant(); + if (ext is ".dll" or ".xml") + { + var destPath = Path.Combine(tfmLibDir, entry.Name); + entry.ExtractToFile(destPath, overwrite: true); + } + } + } + + private static string? SelectTfm( + ICollection availableTfms, + string? tfmOverride, + string[] tfmPreference) + { + // 1. Per-package override — exact match + if (tfmOverride != null) + { + var exact = availableTfms.FirstOrDefault(t => + string.Equals(t, tfmOverride, StringComparison.OrdinalIgnoreCase)); + if (exact != null) return exact; + + // Per-package override — prefix match (e.g. "net10.0-android" matches "net10.0-android35.0") + var prefix = availableTfms.FirstOrDefault(t => + t.StartsWith(tfmOverride, StringComparison.OrdinalIgnoreCase)); + if (prefix != null) return prefix; + } + + // 2. Global preference — exact match + foreach (var pref in tfmPreference) + { + var exact = availableTfms.FirstOrDefault(t => + string.Equals(t, pref, StringComparison.OrdinalIgnoreCase)); + if (exact != null) return exact; + } + + // 3. Global preference — prefix match + foreach (var pref in tfmPreference) + { + var prefix = availableTfms.FirstOrDefault(t => + t.StartsWith(pref, StringComparison.OrdinalIgnoreCase)); + if (prefix != null) return prefix; + } + + // 4. Fallback: first available + return availableTfms.FirstOrDefault(); + } + + internal static void LogInfo(string message) + { + lock (_lockConsoleObject) + { + Console.ForegroundColor = ConsoleColor.Green; + Console.Write("[INFO] "); + Console.ResetColor(); + Console.WriteLine(message); + } + } + + internal static void LogError(string message) + { + lock (_lockConsoleObject) + { + Console.ForegroundColor = ConsoleColor.Red; + Console.Write("[ERROR] "); + Console.ResetColor(); + Console.WriteLine(message); + } + } + + /// + /// Dynamically patches docfx.json metadata entries based on discovered lib/ and refs/ TFM subdirectories. + /// Groups TFMs into core (non-platform) and platform-specific sections. + /// + public static void PatchDocfxJson(AbsolutePath rootDirectory) + { + var apiDir = rootDirectory / "reactiveui" / "api"; + var libDir = apiDir / "lib"; + var refsDir = apiDir / "refs"; + var docfxPath = rootDirectory / "reactiveui" / "docfx.json"; + + if (!Directory.Exists(libDir)) + { + LogInfo("No lib/ directory found, skipping docfx.json patching"); + return; + } + + // Find lib/ TFMs that contain DLLs + var libTfms = Directory.GetDirectories(libDir) + .Where(d => Directory.GetFiles(d, "*.dll").Length > 0) + .Select(d => Path.GetFileName(d)!) + .OrderBy(t => t) + .ToList(); + + if (libTfms.Count == 0) + { + LogInfo("No TFM directories with DLLs found in lib/, skipping docfx.json patching"); + return; + } + + // Find available refs/ TFMs + var refsTfms = Directory.Exists(refsDir) + ? Directory.GetDirectories(refsDir) + .Where(d => Directory.GetFiles(d, "*.dll").Length > 0) + .Select(d => Path.GetFileName(d)!) + .ToList() + : new List(); + + LogInfo($"Discovered lib/ TFMs: {string.Join(", ", libTfms)}"); + LogInfo($"Discovered refs/ TFMs: {string.Join(", ", refsTfms)}"); + + // Read existing docfx.json + var docfxJson = File.ReadAllText(docfxPath); + using var doc = JsonDocument.Parse(docfxJson); + var root = doc.RootElement; + + // Extract shared metadata settings from the first existing metadata entry + var existingMetadata = root.GetProperty("metadata")[0]; + var sharedSettings = new Dictionary(); + foreach (var prop in existingMetadata.EnumerateObject()) + { + if (prop.Name is not "src" and not "references" and not "dest") + sharedSettings[prop.Name] = prop.Value; + } + + // Build one metadata entry per lib TFM — each TFM gets its own compilation + // with only its matching refs to avoid type system conflicts between + // .NET Framework (mscorlib) and modern .NET (System.Runtime) + var metadataEntries = new List(); + var platformLabels = new HashSet(StringComparer.OrdinalIgnoreCase); + + foreach (var tfm in libTfms) + { + var bestRef = FindBestRefsTfm(tfm, refsTfms); + if (bestRef == null) + { + LogInfo($"No matching refs for lib/{tfm}, skipping metadata entry"); + continue; + } + + var platformLabel = GetPlatformLabel(tfm); + var dest = platformLabel != null ? $"api-{platformLabel}" : "api"; + if (platformLabel != null) + platformLabels.Add(platformLabel); + + // Only list package DLLs in src.files (not ref DLLs that were copied in + // for resolver support). Ref DLLs are in the same dir but docfx should + // only generate docs for the actual packages. + var refDir = Path.Combine(refsDir, bestRef); + var refDllNames = Directory.Exists(refDir) + ? new HashSet( + Directory.GetFiles(refDir, "*.dll").Select(f => Path.GetFileName(f)), + StringComparer.OrdinalIgnoreCase) + : new HashSet(StringComparer.OrdinalIgnoreCase); + + var libTfmDir = Path.Combine(libDir, tfm); + var packageDlls = Directory.GetFiles(libTfmDir, "*.dll") + .Select(f => Path.GetFileName(f)) + .Where(f => !refDllNames.Contains(f!)) + .OrderBy(f => f) + .ToArray(); + + if (packageDlls.Length == 0) + { + LogInfo($"No package DLLs in lib/{tfm} (only refs), skipping"); + continue; + } + + // No references section — ref DLLs are co-located in the lib/ dir + // so docfx's UniversalAssemblyResolver finds them automatically + var srcs = new[] { new { src = $"api/lib/{tfm}", files = packageDlls } }; + + metadataEntries.Add(BuildMetadataEntry(srcs, Array.Empty(), dest, sharedSettings)); + LogInfo($"Metadata entry: lib/{tfm} ({packageDlls.Length} DLLs, refs from {bestRef}) -> {dest}"); + } + + // Build new docfx.json + var buildSection = root.GetProperty("build"); + var newDocfx = new Dictionary + { + ["metadata"] = metadataEntries, + ["build"] = PatchBuildSection(buildSection, platformLabels.ToList()) + }; + + var writeOptions = new JsonSerializerOptions + { + WriteIndented = true, + PropertyNamingPolicy = JsonNamingPolicy.CamelCase, + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull + }; + + var newJson = JsonSerializer.Serialize(newDocfx, writeOptions); + File.WriteAllText(docfxPath, newJson); + LogInfo($"Patched docfx.json with {metadataEntries.Count} metadata entries"); + } + + private static string? GetPlatformLabel(string tfm) + { + // Modern platform TFMs (net10.0-android, net10.0-ios, etc.) + if (tfm.Contains("-android", StringComparison.OrdinalIgnoreCase)) return "android"; + if (tfm.Contains("-ios", StringComparison.OrdinalIgnoreCase)) return "ios"; + if (tfm.Contains("-maccatalyst", StringComparison.OrdinalIgnoreCase)) return "maccatalyst"; + if (tfm.Contains("-windows", StringComparison.OrdinalIgnoreCase)) return "windows"; + // Legacy platform TFMs + if (tfm.StartsWith("monoandroid", StringComparison.OrdinalIgnoreCase)) return "android"; + if (tfm.StartsWith("xamarinios", StringComparison.OrdinalIgnoreCase)) return "ios"; + if (tfm.StartsWith("xamarinmac", StringComparison.OrdinalIgnoreCase)) return "maccatalyst"; + if (tfm.StartsWith("uap", StringComparison.OrdinalIgnoreCase)) return "windows"; + return null; + } + + private static string? FindBestRefsTfm(string libTfm, List refsTfms) + { + // Strip platform suffix for matching (e.g. net10.0-android -> net10.0) + var baseTfm = libTfm.Contains('-') ? libTfm.Substring(0, libTfm.IndexOf('-')) : libTfm; + + // Exact match first + var exact = refsTfms.FirstOrDefault(r => + string.Equals(r, baseTfm, StringComparison.OrdinalIgnoreCase)); + if (exact != null) return exact; + + // Prefix match (e.g. net48 matches net481 refs, net10.0 matches net10.0 refs) + var prefix = refsTfms.FirstOrDefault(r => + baseTfm.StartsWith(r, StringComparison.OrdinalIgnoreCase)); + if (prefix != null) return prefix; + + // For netstandard, use highest modern .NET refs + if (baseTfm.StartsWith("netstandard", StringComparison.OrdinalIgnoreCase)) + { + return refsTfms + .Where(r => r.StartsWith("net", StringComparison.OrdinalIgnoreCase) + && !r.StartsWith("net4", StringComparison.OrdinalIgnoreCase) + && !r.StartsWith("netstandard", StringComparison.OrdinalIgnoreCase)) + .OrderByDescending(r => r) + .FirstOrDefault(); + } + + // No match — legacy TFMs (monoandroid, uap) with no available refs + return null; + } + + private static Dictionary BuildMetadataEntry( + object[] srcs, + object[] refs, + string dest, + Dictionary sharedSettings) + { + var entry = new Dictionary + { + ["src"] = srcs + }; + + if (refs.Length > 0) + entry["references"] = refs; + + entry["dest"] = dest; + + foreach (var (key, value) in sharedSettings) + entry[key] = value; + + return entry; + } + + private static Dictionary PatchBuildSection( + JsonElement buildSection, + List platformLabels) + { + var result = new Dictionary(); + + foreach (var prop in buildSection.EnumerateObject()) + { + if (prop.Name == "content") + { + var contentItems = new List(); + + // Add existing content items, filtering out previously-injected platform entries + foreach (var item in prop.Value.EnumerateArray()) + { + var isInjected = false; + if (item.TryGetProperty("files", out var files) && files.GetArrayLength() > 0) + { + var firstFile = files[0].GetString() ?? ""; + if (firstFile.StartsWith("api-", StringComparison.Ordinal) + && !firstFile.StartsWith("api/", StringComparison.Ordinal)) + isInjected = true; + } + if (!isInjected) + contentItems.Add(item); + } + + // Add platform API content entries + foreach (var label in platformLabels.OrderBy(l => l)) + { + contentItems.Add(new Dictionary + { + ["files"] = new[] { $"api-{label}/**.yml", $"api-{label}/index.md" } + }); + } + + result[prop.Name] = contentItems; + } + else + { + result[prop.Name] = prop.Value; + } + } + + return result; + } +} diff --git a/build/SourceFetcher.cs b/build/SourceFetcher.cs deleted file mode 100644 index 2b544063..00000000 --- a/build/SourceFetcher.cs +++ /dev/null @@ -1,205 +0,0 @@ -using System; -using System.Diagnostics; -using System.IO; -using System.IO.Compression; -using System.Linq; -using System.Net.Http; -using System.Threading; -using System.Threading.Tasks; -using Nuke.Common.IO; -using Polly; - -namespace ReactiveUI.Web; - -internal static class SourceFetcher -{ - private static readonly object _lockConsoleObject = new(); - private static readonly object _lockWorkloadObject = new(); - - public static void GetSources(this AbsolutePath fileSystem, AbsolutePath rootDirectory, string owner, params string[] repositories) - { - FetchGitHubZip(fileSystem, rootDirectory, owner, repositories, "external", true, true); - } - - internal static void LogInfo(string message) - { - lock (_lockConsoleObject) - { - Console.ForegroundColor = ConsoleColor.Green; - Console.Write("[INFO] "); - Console.ResetColor(); - Console.Write($"{message}"); - Console.WriteLine(); - } - } - - internal static void LogRepositoryInfo(string owner, string repository, string message) => - LogInfo($"{message} {owner}/{repository}..."); - - internal static void LogRepositoryError(string owner, string repository, string message) - { - LogError($"{message} {owner}/{repository}..."); - } - - internal static void LogError(string message) - { - lock (_lockConsoleObject) - { - Console.ForegroundColor = ConsoleColor.Red; - Console.Write("[ERROR] "); - Console.ResetColor(); - Console.Write(message); - Console.WriteLine(); - } - } - - private static void FetchGitHubZip(AbsolutePath fileSystem, AbsolutePath rootDirectory, string owner, string[] repositories, string outputFolder, bool fetchNuGet, bool useSrc) - { - var zipCache = fileSystem / "zip"; - zipCache.CreateDirectory(); - (fileSystem / outputFolder).CreateDirectory(); - - using var client = new HttpClient(); - - // Create the semaphore. - using var semaphore = new SemaphoreSlim(3, 3); - - var waitAndRetry = Policy.Handle() - .WaitAndRetryAsync( - 6, // We can also do this with WaitAndRetryForever... but chose WaitAndRetry this time. - attempt => TimeSpan.FromSeconds(0.1 * Math.Pow(2, attempt))); // Back off! 2, 4, 8, 16 etc times 1/4-second - - Task.WaitAll([.. repositories.Select(repository => Task.Run(async () => - { - await semaphore.WaitAsync(); - try - { - LogRepositoryInfo(owner, repository, "Downloading"); - - var url = $"https://codeload.github.com/{owner}/{repository}/zip/main"; - - var zipFilePath = zipCache / $"{owner}-{repository}.zip"; - zipFilePath.DeleteFile(); - var extractZipPath = zipCache / $"{owner}-{repository}-extract"; - var finalPath = fileSystem / outputFolder / repository; - - extractZipPath.DeleteDirectory(); - extractZipPath.CreateDirectory(); - - // Retry the following call according to the policy - await waitAndRetry.ExecuteAsync(async () => - { - var response = await client.GetAsync(url); - - if (!response.IsSuccessStatusCode) - { - LogRepositoryInfo(owner, repository, "Could not find a valid document at: " + url + " " + response.StatusCode); - throw new HttpRequestException("Could not find a valid document at: " + url, default, response.StatusCode); - } - - using (Stream contentStream = await response.Content.ReadAsStreamAsync(), - stream = new FileStream(zipFilePath, FileMode.OpenOrCreate)) - { - await contentStream.CopyToAsync(stream); - } - - LogRepositoryInfo(owner, repository, "Extracting Files"); - ZipFile.ExtractToDirectory(zipFilePath, extractZipPath, true); - - try - { - var zipInternalPath = extractZipPath / repository + "-main"; - finalPath.DeleteDirectory(); - finalPath.CreateDirectory(); - zipInternalPath.MoveToDirectory(finalPath); - } - catch (Exception ex) - { - LogRepositoryError(owner, repository, "Failed to move: " + ex); - } - - if (fetchNuGet) - { - var directory = useSrc ? finalPath / $"{repository}-main" / "src" : finalPath; - File.Copy(rootDirectory / "global.json", directory / "global.json", true); - WorkflowRestore(owner, repository, finalPath, useSrc); - FetchNuGet(owner, repository, finalPath, useSrc); - } - - zipFilePath.DeleteDirectory(); - - LogRepositoryInfo(owner, repository, "Downloaded"); - }); - } - catch (Exception ex) - { - LogRepositoryError(owner, repository, "Failed to download: " + ex); - } - finally - { - semaphore.Release(); - } - }))]); - } - - private static void FetchNuGet(string owner, string repository, AbsolutePath finalPath, bool useSrc) - { - LogRepositoryInfo(owner, repository, "Restoring Packages for "); - - var directory = useSrc ? finalPath / $"{repository}-main" / "src" : finalPath; - - RunDotNetOnSolution(owner, repository, directory, "restore"); - } - - private static void WorkflowRestore(string owner, string repository, AbsolutePath finalPath, bool useSrc) - { - lock (_lockWorkloadObject) - { - LogRepositoryInfo(owner, repository, "Restoring workload for "); - - var directory = useSrc ? finalPath / $"{repository}-main" / "src" : finalPath; - - RunDotNetOnSolution(owner, repository, directory, "workload restore"); - } - } - - private static void RunDotNetOnSolution(string owner, string repository, AbsolutePath directory, string command) - { - // Find any .sln or .slnx file and run dotnet restore on it - var solutionFile = Directory.EnumerateFiles(directory.ToString(), "*.sln*").FirstOrDefault(); - if (File.Exists(directory / $"{repository}.sln")) - { - RunDotNet(directory, $"{command} {repository}.sln"); - } - else if (File.Exists(directory / $"{repository}.slnx")) - { - RunDotNet(directory, $"{command} {repository}.slnx"); - } - else if (solutionFile != null) - { - RunDotNet(directory, $"{command} {Path.GetFileName(solutionFile)}"); - } - else - { - LogRepositoryError(owner, repository, $"No solution file found to {command}."); - } - } - - private static void RunDotNet(AbsolutePath finalPath, string parameters) - { - ProcessStartInfo startInfo = new() - { - WindowStyle = ProcessWindowStyle.Hidden, - FileName = "cmd.exe", - Arguments = $"/C dotnet {parameters}", - WorkingDirectory = finalPath.ToString() - }; - Process process = new() - { - StartInfo = startInfo - }; - process.Start(); - process.WaitForExit(); - process.Dispose(); - } -} diff --git a/build/_build.csproj b/build/_build.csproj index f9514706..c1346c1f 100644 --- a/build/_build.csproj +++ b/build/_build.csproj @@ -12,7 +12,7 @@ - + diff --git a/nuget-packages.json b/nuget-packages.json new file mode 100644 index 00000000..0b555581 --- /dev/null +++ b/nuget-packages.json @@ -0,0 +1,45 @@ +{ + "nugetPackageOwners": ["reactiveui", "reactivemarbles"], + "tfmPreference": [ + "net10.0", + "net9.0", + "net8.0", + "net481", + "net471", + "net462", + "netstandard2.1", + "netstandard2.0", + "net10.0-windows10.0.19041.0", + "net9.0-windows10.0.19041.0", + "net10.0-android", + "net10.0-ios", + "net10.0-maccatalyst", + "net9.0-android", + "net9.0-ios", + "net9.0-maccatalyst" + ], + "additionalPackages": [ + { "id": "System.Reactive" }, + { "id": "DynamicData" } + ], + "excludePackages": [], + "referencePackages": [ + { "id": "Microsoft.NETCore.App.Ref", "version": "10.0.0", "targetTfm": "net10.0", "pathPrefix": "ref/net10.0" }, + { "id": "Microsoft.NETCore.App.Ref", "version": "9.0.0", "targetTfm": "net9.0", "pathPrefix": "ref/net9.0" }, + { "id": "Microsoft.NETCore.App.Ref", "version": "8.0.0", "targetTfm": "net8.0", "pathPrefix": "ref/net8.0" }, + { "id": "Microsoft.NETFramework.ReferenceAssemblies.net461", "targetTfm": "net461", "pathPrefix": "build/.NETFramework/v4.6.1" }, + { "id": "Microsoft.NETFramework.ReferenceAssemblies.net462", "targetTfm": "net462", "pathPrefix": "build/.NETFramework/v4.6.2" }, + { "id": "Microsoft.NETFramework.ReferenceAssemblies.net471", "targetTfm": "net471", "pathPrefix": "build/.NETFramework/v4.7.1" }, + { "id": "Microsoft.NETFramework.ReferenceAssemblies.net472", "targetTfm": "net472", "pathPrefix": "build/.NETFramework/v4.7.2" }, + { "id": "Microsoft.NETFramework.ReferenceAssemblies.net48", "targetTfm": "net48", "pathPrefix": "build/.NETFramework/v4.8" }, + { "id": "Microsoft.NETFramework.ReferenceAssemblies.net481", "targetTfm": "net481", "pathPrefix": "build/.NETFramework/v4.8.1" } + ], + "tfmOverrides": { + "ReactiveUI.AndroidX": "net10.0-android", + "ReactiveUI.Validation.AndroidX": "net10.0-android", + "ReactiveUI.Blend": "net10.0-windows10.0.19041.0", + "ReactiveUI.Winforms": "net10.0-windows10.0.19041.0", + "ReactiveUI.WinUI": "net10.0-windows10.0.19041.0", + "ReactiveUI.Wpf": "net10.0-windows10.0.19041.0" + } +} diff --git a/reactiveui/api/index.md b/reactiveui/api/index.md index b9040ad8..a46c7fb6 100644 --- a/reactiveui/api/index.md +++ b/reactiveui/api/index.md @@ -1,6 +1,6 @@ # ReactiveUI API Documentation -Browse the latest API documentation generated from the current ReactiveUI source code. This section provides reference material for all major ReactiveUI packages and related projects. +Browse the latest API documentation for all major ReactiveUI packages and related projects. This section is generated from the published NuGet packages and reflects the latest stable releases. - [Akavache API](akavache.html) - [DynamicData API](dynamicdata.html) @@ -38,4 +38,4 @@ Browse the latest API documentation generated from the current ReactiveUI source - [Fusillade API](fusillade.html) - [Punchclock API](punchclock.html) -> The API documentation is generated from the latest source and reflects .NET 8 compatibility and current best practices. +> The API documentation is generated from the latest stable NuGet package releases and reflects current best practices. diff --git a/reactiveui/docfx.json b/reactiveui/docfx.json index 6d8fdcee..6361748f 100644 --- a/reactiveui/docfx.json +++ b/reactiveui/docfx.json @@ -3,50 +3,313 @@ { "src": [ { + "src": "api/lib/net10.0", "files": [ - "/**/Akavache.Core/bin/*/net10.0/Akavache.dll", - "/**/Akavache.SystemTextJson/bin/*/net10.0/Akavache.SystemTextJson.dll", - "/**/Akavache.NewtonsoftJson/bin/*/net10.0/Akavache.NewtonsoftJson.dll", - "/**/Akavache.Sqlite3/bin/*/net10.0/Akavache.Sqlite3.dll", - "/**/Akavache.EncryptedSqlite3/bin/*/net10.0/Akavache.EncryptedSqlite3.dll", - "/**/Akavache.Drawing/bin/*/net10.0/Akavache.Drawing.dll", - "/**/Akavache.Settings/bin/*/net10.0/Akavache.Settings", - "/**/Fusillade/bin/*/net10.0/Fusillade.dll", - "/**/Punchclock/bin/*/netstandard2.0/Punchclock.dll", - "/**/ReactiveUI/bin/*/net10.0/ReactiveUI.dll", - "/**/ReactiveUI.AndroidX/bin/*/net10.0-android/ReactiveUI.AndroidX.dll", - "/**/ReactiveUI.Blazor/bin/*/net10.0/ReactiveUI.Blazor.dll", - "/**/ReactiveUI.Blend/bin/*/net10.0-windows10.0.19041.0/ReactiveUI.Blend.dll", - "/**/ReactiveUI.Drawing/bin/*/net10.0/ReactiveUI.Drawing.dll", - "/**/ReactiveUI.Maui/bin/*/net10.0/ReactiveUI.Maui.dll", - "/**/ReactiveUI.Testing/bin/*/net10.0/ReactiveUI.Testing.dll", - "/**/ReactiveUI.Winforms/bin/*/net10.0-windows10.0.19041.0/ReactiveUI.Winforms.dll", - "/**/ReactiveUI.WinUI/bin/*/net10.0-windows10.0.19041.0/ReactiveUI.WinUI.dll", - "/**/ReactiveUI.Wpf/bin/*/net10.0-windows10.0.19041.0/ReactiveUI.Wpf.dll", - "/**/ReactiveUI.Validation/bin/*/net10.0/ReactiveUI.Validation.dll", - "/**/ReactiveUI.Validation.AndroidX/bin/*/net10.0-android/ReactiveUI.Validation.AndroidX.dll", - "/**/ReactiveUI.Maui.Plugins.Popup/bin/*/net10.0/ReactiveUI.Maui.Plugins.Popup.dll", - "/**/Splat.Core/bin/*/net10.0/Splat.Core.dll", - "/**/Splat.Builder/bin/*/net10.0/Splat.Builder.dll", - "/**/Splat/bin/*/net10.0/Splat.dll", - "/**/Splat.AppCenter/bin/*/net10.0/Splat.AppCenter.dll", - "/**/Splat.ApplicationInsights/bin/*/net10.0/Splat.ApplicationInsights.dll", - "/**/Splat.Autofac/bin/*/net10.0/Splat.Autofac.dll", - "/**/Splat.Drawing/bin/*/net10.0/Splat.Drawing.dll", - "/**/Splat.DryIoc/bin/*/net10.0/Splat.DryIoc.dll", - "/**/Splat.Exceptionless/bin/*/net10.0/Splat.Exceptionless.dll", - "/**/Splat.Log4Net/bin/*/net10.0/Splat.Log4Net.dll", - "/**/Splat.Microsoft.Extensions.DependencyInjection/bin/*/net10.0/Splat.Microsoft.Extensions.DependencyInjection.dll", - "/**/Splat.Microsoft.Extensions.Logging/bin/*/net10.0/Splat.Microsoft.Extensions.Logging.dll", - "/**/Splat.Ninject/bin/*/net10.0/Splat.Ninject.dll", - "/**/Splat.NLog/bin/*/net10.0/Splat.NLog.dll", - "/**/Splat.Prism/bin/*/net10.0/Splat.Prism.dll", - "/**/Splat.Prism.Forms/bin/*/net10.0/Splat.Prism.Forms.dll", - "/**/Splat.Raygun/bin/*/net10.0/Splat.Raygun.dll", - "/**/Splat.Serilog/bin/*/net10.0/Splat.Serilog.dll", - "/**/Splat.SimpleInjector/bin/*/net10.0/Splat.SimpleInjector.dll", - "/**/DynamicData/bin/*/net9.0/DynamicData.dll", - "/**/ReactiveUI.Extensions/bin/*/net10.0/ReactiveUI.Extensions.dll" + "Akavache.dll", + "Akavache.Drawing.dll", + "Akavache.EncryptedSqlite3.dll", + "Akavache.NewtonsoftJson.dll", + "Akavache.Sqlite3.dll", + "Akavache.SystemTextJson.dll", + "CrissCross.Avalonia.dll", + "CrissCross.Avalonia.UI.dll", + "CrissCross.dll", + "CrissCross.MAUI.dll", + "DynamicData.dll", + "Extensions.Hosting.Avalonia.dll", + "Extensions.Hosting.Identity.EntityFrameworkCore.Sqlite.dll", + "Extensions.Hosting.Identity.EntityFrameworkCore.SqlServer.dll", + "Extensions.Hosting.MainUIThread.dll", + "Extensions.Hosting.Maui.dll", + "Extensions.Hosting.Plugins.dll", + "Extensions.Hosting.ReactiveUI.Avalonia.dll", + "Extensions.Hosting.ReactiveUI.Maui.dll", + "Extensions.Hosting.SingleInstance.dll", + "Fusillade.dll", + "Punchclock.dll", + "ReactiveUI.Binding.dll", + "ReactiveUI.Binding.Maui.dll", + "ReactiveUI.Binding.Reactive.dll", + "ReactiveUI.Blazor.dll", + "ReactiveUI.dll", + "ReactiveUI.Drawing.dll", + "ReactiveUI.Extensions.dll", + "ReactiveUI.Maui.dll", + "ReactiveUI.Maui.Plugins.Popup.dll", + "ReactiveUI.Testing.dll", + "ReactiveUI.Testing.Reactive.dll", + "ReactiveUI.Uno.dll", + "ReactiveUI.Validation.dll", + "Refit.dll", + "Refit.HttpClientFactory.dll", + "Refit.Newtonsoft.Json.dll", + "Refit.Xml.dll", + "Sextant.Avalonia.dll", + "Sextant.dll", + "Sextant.Maui.dll", + "Splat.AppCenter.dll", + "Splat.ApplicationInsights.dll", + "Splat.Autofac.dll", + "Splat.Builder.dll", + "Splat.Common.Test.dll", + "Splat.Core.dll", + "Splat.dll", + "Splat.Drawing.dll", + "Splat.DryIoc.dll", + "Splat.Exceptionless.dll", + "Splat.Log4Net.dll", + "Splat.Logging.dll", + "Splat.Microsoft.Extensions.DependencyInjection.dll", + "Splat.Microsoft.Extensions.Logging.dll", + "Splat.Ninject.dll", + "Splat.NLog.dll", + "Splat.Prism.dll", + "Splat.Prism.Forms.dll", + "Splat.Raygun.dll", + "Splat.Serilog.dll", + "Splat.SimpleInjector.dll" + ] + } + ], + "dest": "api", + "outputFormat": "mref", + "includePrivateMembers": false, + "disableGitFeatures": false, + "disableDefaultFilter": false, + "noRestore": false, + "namespaceLayout": "flattened", + "memberLayout": "samePage", + "enumSortOrder": "alphabetic", + "allowCompilationErrors": false, + "filter": "filter.yml" + }, + { + "src": [ + { + "src": "api/lib/net10.0-android36.0", + "files": [ + "ReactiveUI.AndroidX.dll", + "ReactiveUI.Validation.AndroidX.dll" + ] + } + ], + "dest": "api-android", + "outputFormat": "mref", + "includePrivateMembers": false, + "disableGitFeatures": false, + "disableDefaultFilter": false, + "noRestore": false, + "namespaceLayout": "flattened", + "memberLayout": "samePage", + "enumSortOrder": "alphabetic", + "allowCompilationErrors": false, + "filter": "filter.yml" + }, + { + "src": [ + { + "src": "api/lib/net10.0-windows10.0.19041", + "files": [ + "Extensions.Hosting.ReactiveUI.WinUI.dll", + "Extensions.Hosting.WinUI.dll", + "ReactiveUI.WinUI.dll" + ] + } + ], + "dest": "api-windows", + "outputFormat": "mref", + "includePrivateMembers": false, + "disableGitFeatures": false, + "disableDefaultFilter": false, + "noRestore": false, + "namespaceLayout": "flattened", + "memberLayout": "samePage", + "enumSortOrder": "alphabetic", + "allowCompilationErrors": false, + "filter": "filter.yml" + }, + { + "src": [ + { + "src": "api/lib/net462", + "files": [ + "ReactiveMarbles.PropertyChanged.dll" + ] + } + ], + "dest": "api", + "outputFormat": "mref", + "includePrivateMembers": false, + "disableGitFeatures": false, + "disableDefaultFilter": false, + "noRestore": false, + "namespaceLayout": "flattened", + "memberLayout": "samePage", + "enumSortOrder": "alphabetic", + "allowCompilationErrors": false, + "filter": "filter.yml" + }, + { + "src": [ + { + "src": "api/lib/net481", + "files": [ + "CrissCross.WinForms.dll", + "CrissCross.WPF.dll", + "CrissCross.WPF.Plot.dll", + "CrissCross.WPF.UI.dll", + "CrissCross.WPF.UI.resources.dll", + "CrissCross.WPF.WebView2.dll", + "Extensions.Hosting.PluginService.dll", + "Extensions.Hosting.ReactiveUI.WinForms.dll", + "Extensions.Hosting.ReactiveUI.Wpf.dll", + "Extensions.Hosting.WinForms.dll", + "Extensions.Hosting.Wpf.dll", + "ReactiveUI.Binding.WinForms.dll", + "ReactiveUI.Binding.Wpf.dll", + "ReactiveUI.Blend.dll", + "ReactiveUI.Winforms.dll", + "ReactiveUI.Wpf.dll" + ] + } + ], + "dest": "api", + "outputFormat": "mref", + "includePrivateMembers": false, + "disableGitFeatures": false, + "disableDefaultFilter": false, + "noRestore": false, + "namespaceLayout": "flattened", + "memberLayout": "samePage", + "enumSortOrder": "alphabetic", + "allowCompilationErrors": false, + "filter": "filter.yml" + }, + { + "src": [ + { + "src": "api/lib/net8.0", + "files": [ + "Akavache.Settings.dll", + "ReactiveMarbles.Locator.dll", + "ReactiveMarbles.Mvvm.dll", + "ReactiveMarbles.ViewModel.Core.dll", + "ReactiveUI.Uno.WinUI.dll", + "Sextant.Plugins.Popup.dll", + "Sextant.XamForms.dll" + ] + } + ], + "dest": "api", + "outputFormat": "mref", + "includePrivateMembers": false, + "disableGitFeatures": false, + "disableDefaultFilter": false, + "noRestore": false, + "namespaceLayout": "flattened", + "memberLayout": "samePage", + "enumSortOrder": "alphabetic", + "allowCompilationErrors": false, + "filter": "filter.yml" + }, + { + "src": [ + { + "src": "api/lib/net8.0-android34.0", + "files": [ + "ReactiveMarbles.ViewModel.MAUI.dll" + ] + } + ], + "dest": "api-android", + "outputFormat": "mref", + "includePrivateMembers": false, + "disableGitFeatures": false, + "disableDefaultFilter": false, + "noRestore": false, + "namespaceLayout": "flattened", + "memberLayout": "samePage", + "enumSortOrder": "alphabetic", + "allowCompilationErrors": false, + "filter": "filter.yml" + }, + { + "src": [ + { + "src": "api/lib/net8.0-windows10.0.19041", + "files": [ + "ReactiveMarbles.ViewModel.WinForms.dll", + "ReactiveMarbles.ViewModel.Wpf.dll" + ] + } + ], + "dest": "api-windows", + "outputFormat": "mref", + "includePrivateMembers": false, + "disableGitFeatures": false, + "disableDefaultFilter": false, + "noRestore": false, + "namespaceLayout": "flattened", + "memberLayout": "samePage", + "enumSortOrder": "alphabetic", + "allowCompilationErrors": false, + "filter": "filter.yml" + }, + { + "src": [ + { + "src": "api/lib/net9.0", + "files": [ + "Akavache.Mobile.dll", + "CrissCross.XamForms.dll", + "Splat.Avalonia.Autofac.dll", + "Splat.Avalonia.DryIoc.dll", + "Splat.Avalonia.Microsoft.Extensions.DependencyInjection.dll", + "Splat.Avalonia.Ninject.dll", + "System.Reactive.Wasm.dll" + ] + } + ], + "dest": "api", + "outputFormat": "mref", + "includePrivateMembers": false, + "disableGitFeatures": false, + "disableDefaultFilter": false, + "noRestore": false, + "namespaceLayout": "flattened", + "memberLayout": "samePage", + "enumSortOrder": "alphabetic", + "allowCompilationErrors": false, + "filter": "filter.yml" + }, + { + "src": [ + { + "src": "api/lib/netstandard2.0", + "files": [ + "Cake.PinNuGetDependency.dll", + "Minimalist.Reactive.dll", + "Pharmacist.Common.dll", + "Pharmacist.Core.dll", + "ReactiveMarbles.CacheDatabase.Core.dll", + "ReactiveMarbles.CacheDatabase.EncryptedSettings.dll", + "ReactiveMarbles.CacheDatabase.EncryptedSqlite3.dll", + "ReactiveMarbles.CacheDatabase.NewtonsoftJson.dll", + "ReactiveMarbles.CacheDatabase.Settings.dll", + "ReactiveMarbles.CacheDatabase.Sqlite3.dll", + "ReactiveMarbles.CacheDatabase.SystemTextJson.dll", + "ReactiveMarbles.Command.dll", + "ReactiveMarbles.NuGet.Helpers.dll", + "ReactiveMarbles.PropertyChanged.SourceGenerator.dll", + "ReactiveMarbles.SourceGenerator.TestNuGetHelper.dll", + "ReactiveUI.Avalonia.Autofac.dll", + "ReactiveUI.Avalonia.dll", + "ReactiveUI.Avalonia.DryIoc.dll", + "ReactiveUI.Avalonia.Microsoft.Extensions.DependencyInjection.dll", + "ReactiveUI.Avalonia.Ninject.dll", + "ReactiveUI.XamForms.dll", + "SextantSample.ViewModels.dll", + "System.Reactive.dll" ] } ], @@ -100,6 +363,18 @@ "toc.yml", "*.md" ] + }, + { + "files": [ + "api-android/**.yml", + "api-android/index.md" + ] + }, + { + "files": [ + "api-windows/**.yml", + "api-windows/index.md" + ] } ], "resource": [ @@ -126,4 +401,4 @@ "changefreq": "monthly" } } -} +} \ No newline at end of file diff --git a/reactiveui/docs/getting-started/installation/maui.md b/reactiveui/docs/getting-started/installation/maui.md index cb7125b2..b61a7ddc 100644 --- a/reactiveui/docs/getting-started/installation/maui.md +++ b/reactiveui/docs/getting-started/installation/maui.md @@ -30,23 +30,19 @@ public static class MauiProgram var builder = MauiApp.CreateBuilder(); builder .UseMauiApp() - .ConfigureReactiveUI(); - - // Initialize ReactiveUI with RxAppBuilder - var app = RxAppBuilder.CreateReactiveUIBuilder() - .WithMaui() - .WithViewsFromAssembly(typeof(App).Assembly) - .WithRegistration(locator => - { - // Register your services - locator.RegisterLazySingleton(() => new NavigationService()); - locator.RegisterLazySingleton(() => new DataService()); - }) - .BuildApp(); - - // Optional: Register additional MAUI-specific services - builder.Services.AddSingleton(app.MainThreadScheduler); - builder.Services.AddSingleton(app.TaskpoolScheduler); + // Initialize ReactiveUI with RxAppBuilder + .UseReactiveUI(rxAppBuilder => + { + rxAppBuilder + .WithMaui() + .WithViewsFromAssembly(typeof(App).Assembly) + .WithRegistration(locator => + { + // Register your services here + locator.RegisterLazySingleton(() => new NavigationService()); + locator.RegisterLazySingleton(() => new DataService()); + }); + }); return builder.Build(); } diff --git a/reactiveui/docs/getting-started/installation/windows-forms.md b/reactiveui/docs/getting-started/installation/windows-forms.md index 027982d0..544a40a9 100644 --- a/reactiveui/docs/getting-started/installation/windows-forms.md +++ b/reactiveui/docs/getting-started/installation/windows-forms.md @@ -28,12 +28,12 @@ Install the following packages for ReactiveUI with Windows Forms: ### Framework Requirements -Ensure you are targeting at least .NET 8.0 with Windows 10.0.17763.0: +Ensure you are targeting at least .NET 8.0 with Windows 10.0.19041.0: ```xml -net8.0-windows10.0.17763.0 +net8.0-windows10.0.19041.0 -net10.0-windows10.0.17763.0 +net10.0-windows10.0.19041.0 ``` ## Getting Started with RxAppBuilder (Recommended) diff --git a/reactiveui/docs/guidelines/platform/windows-forms.md b/reactiveui/docs/guidelines/platform/windows-forms.md index 09419cc7..66e55031 100644 --- a/reactiveui/docs/guidelines/platform/windows-forms.md +++ b/reactiveui/docs/guidelines/platform/windows-forms.md @@ -3,9 +3,9 @@ Ensure that you install `ReactiveUI.WinForms` into your application. -Please ensure that you are targeting at least windows10.0.17763.0 +Please ensure that you are targeting at least windows10.0.19041.0 -i.e `net8.0-windows10.0.17763.0` in your csproj file. +i.e `net8.0-windows10.0.19041.0` in your csproj file. Your viewmodels should inherit from `ReactiveObject` diff --git a/reactiveui/toc.yml b/reactiveui/toc.yml index d5282011..e04df709 100644 --- a/reactiveui/toc.yml +++ b/reactiveui/toc.yml @@ -4,6 +4,14 @@ - name: Api href: api/ homepage: api/index.md +- name: Api - Android + href: api-android/ +- name: Api - iOS + href: api-ios/ +- name: Api - macOS + href: api-maccatalyst/ +- name: Api - Windows + href: api-windows/ - name: Contribute href: contribute/ homepage: contribute/index.md