-
Notifications
You must be signed in to change notification settings - Fork 837
feat: DevServer solution discovery #22437
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. Weβll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull request overview
This PR introduces centralized solution discovery functionality for the DevServer by creating a new SolutionDiscovery utility class. The implementation enables DevServer to automatically discover solution files in subdirectories (up to 2 levels deep), with prioritization for common source directories like "src", "source", and "app". This allows developers to start DevServer from the repository root even when solution files are in subdirectories.
Changes:
- Created new
SolutionDiscoveryhelper class with intelligent directory traversal and prioritization - Refactored two existing solution discovery implementations to use the centralized utility
- Linked the new helper class into
Uno.UI.RemoteControl.Hostproject
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 1 comment.
| File | Description |
|---|---|
| src/Uno.UI.DevServer.Cli/Helpers/SolutionDiscovery.cs | New utility class that discovers .sln and .slnx files in current directory and subdirectories with configurable search depth, prioritization, and directory skipping |
| src/Uno.UI.DevServer.Cli/Mcp/DevServerMonitor.cs | Refactored to use SolutionDiscovery.DiscoverSolutions() instead of inline directory enumeration |
| src/Uno.UI.RemoteControl.Host/Program.Command.cs | Refactored to use SolutionDiscovery.DiscoverFirstSolution() instead of inline directory enumeration |
| src/Uno.UI.RemoteControl.Host/Uno.UI.RemoteControl.Host.csproj | Added file link to include SolutionDiscovery.cs from the CLI project |
π‘ Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| using System; | ||
| using System.Collections.Generic; | ||
| using System.IO; | ||
| using System.Linq; | ||
| using Microsoft.Extensions.Logging; | ||
|
|
||
| namespace Uno.UI.DevServer.Cli.Helpers; | ||
|
|
||
| /// <summary> | ||
| /// Provides methods to discover solution files in the current directory and subdirectories. | ||
| /// This allows the DevServer to be started from a parent directory (e.g., repository root) | ||
| /// when solution files are located in subdirectories like 'src/'. | ||
| /// </summary> | ||
| internal static class SolutionDiscovery | ||
| { | ||
| /// <summary> | ||
| /// Prioritized subdirectory names to search first. These are common source directory names. | ||
| /// </summary> | ||
| private static readonly string[] PrioritizedSubdirectories = ["src", "source", "app"]; | ||
|
|
||
| /// <summary> | ||
| /// Directory names to skip during search. These typically contain build artifacts or dependencies. | ||
| /// </summary> | ||
| private static readonly HashSet<string> SkippedDirectories = new(StringComparer.OrdinalIgnoreCase) | ||
| { | ||
| "bin", "obj", "node_modules", "packages", ".git", ".vs", ".idea" | ||
| }; | ||
|
|
||
| /// <summary> | ||
| /// Maximum depth to search for solution files in subdirectories. | ||
| /// </summary> | ||
| private const int MaxSearchDepth = 2; | ||
|
|
||
| /// <summary> | ||
| /// Discovers all solution files starting from the specified directory. | ||
| /// First searches the current directory, then subdirectories up to MaxSearchDepth. | ||
| /// Results are ordered by depth (shallower first), then alphabetically. | ||
| /// </summary> | ||
| /// <param name="startDirectory">The directory to start searching from.</param> | ||
| /// <param name="logger">Optional logger for diagnostic output.</param> | ||
| /// <returns>Array of full paths to discovered solution files, or empty array if none found.</returns> | ||
| public static string[] DiscoverSolutions(string startDirectory, ILogger? logger = null) | ||
| { | ||
| var solutions = new List<(string path, int depth)>(); | ||
|
|
||
| // First, check current directory (depth 0) | ||
| var currentDirSolutions = GetSolutionFilesInDirectory(startDirectory); | ||
| if (currentDirSolutions.Length > 0) | ||
| { | ||
| logger?.LogDebug("Found {Count} solution(s) in current directory: {Directory}", currentDirSolutions.Length, startDirectory); | ||
| return currentDirSolutions.OrderBy(s => s, StringComparer.OrdinalIgnoreCase).ToArray(); | ||
| } | ||
|
|
||
| // Search subdirectories up to MaxSearchDepth | ||
| logger?.LogDebug("No solutions found in current directory, searching subdirectories..."); | ||
| SearchSubdirectories(startDirectory, 1, solutions, logger); | ||
|
|
||
| if (solutions.Count == 0) | ||
| { | ||
| logger?.LogDebug("No solution files found in {Directory} or its subdirectories", startDirectory); | ||
| return []; | ||
| } | ||
|
|
||
| // Order by depth first, then alphabetically | ||
| var result = solutions | ||
| .OrderBy(s => s.depth) | ||
| .ThenBy(s => s.path, StringComparer.OrdinalIgnoreCase) | ||
| .Select(s => s.path) | ||
| .ToArray(); | ||
|
|
||
| logger?.LogDebug("Found {Count} solution(s) in subdirectories", result.Length); | ||
| return result; | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Discovers the first solution file starting from the specified directory. | ||
| /// Convenience method that returns the first result from DiscoverSolutions. | ||
| /// </summary> | ||
| /// <param name="startDirectory">The directory to start searching from.</param> | ||
| /// <param name="logger">Optional logger for diagnostic output.</param> | ||
| /// <returns>Full path to the first discovered solution file, or null if none found.</returns> | ||
| public static string? DiscoverFirstSolution(string startDirectory, ILogger? logger = null) | ||
| { | ||
| return DiscoverSolutions(startDirectory, logger).FirstOrDefault(); | ||
| } | ||
|
|
||
| private static void SearchSubdirectories(string directory, int currentDepth, List<(string path, int depth)> results, ILogger? logger) | ||
| { | ||
| if (currentDepth > MaxSearchDepth) | ||
| { | ||
| return; | ||
| } | ||
|
|
||
| IEnumerable<string> subdirectories; | ||
| try | ||
| { | ||
| subdirectories = Directory.EnumerateDirectories(directory); | ||
| } | ||
| catch (UnauthorizedAccessException) | ||
| { | ||
| return; | ||
| } | ||
| catch (DirectoryNotFoundException) | ||
| { | ||
| return; | ||
| } | ||
|
|
||
| // Sort subdirectories: prioritized ones first, then alphabetically | ||
| var sortedSubdirs = subdirectories | ||
| .Select(d => new { Path = d, Name = Path.GetFileName(d) }) | ||
| .Where(d => !ShouldSkipDirectory(d.Name)) | ||
| .OrderBy(d => GetDirectoryPriority(d.Name)) | ||
| .ThenBy(d => d.Name, StringComparer.OrdinalIgnoreCase) | ||
| .Select(d => d.Path) | ||
| .ToList(); | ||
|
|
||
| foreach (var subdir in sortedSubdirs) | ||
| { | ||
| var solutions = GetSolutionFilesInDirectory(subdir); | ||
| foreach (var solution in solutions) | ||
| { | ||
| results.Add((solution, currentDepth)); | ||
| logger?.LogDebug("Found solution at depth {Depth}: {Path}", currentDepth, solution); | ||
| } | ||
|
|
||
| // Recurse into subdirectories | ||
| SearchSubdirectories(subdir, currentDepth + 1, results, logger); | ||
| } | ||
| } | ||
|
|
||
| private static string[] GetSolutionFilesInDirectory(string directory) | ||
| { | ||
| try | ||
| { | ||
| return Directory | ||
| .EnumerateFiles(directory, "*.sln") | ||
| .Concat(Directory.EnumerateFiles(directory, "*.slnx")) | ||
| .ToArray(); | ||
| } | ||
| catch (UnauthorizedAccessException) | ||
| { | ||
| return []; | ||
| } | ||
| catch (DirectoryNotFoundException) | ||
| { | ||
| return []; | ||
| } | ||
| } | ||
|
|
||
| private static bool ShouldSkipDirectory(string directoryName) | ||
| { | ||
| // Skip hidden directories (starting with . or _) | ||
| if (directoryName.StartsWith('.') || directoryName.StartsWith('_')) | ||
| { | ||
| return true; | ||
| } | ||
|
|
||
| return SkippedDirectories.Contains(directoryName); | ||
| } | ||
|
|
||
| private static int GetDirectoryPriority(string directoryName) | ||
| { | ||
| var index = Array.FindIndex(PrioritizedSubdirectories, p => p.Equals(directoryName, StringComparison.OrdinalIgnoreCase)); | ||
| return index >= 0 ? index : int.MaxValue; | ||
| } | ||
| } |
Copilot
AI
Jan 22, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The new SolutionDiscovery utility class lacks test coverage. Since this repository has comprehensive test coverage (including the Uno.UI.RemoteControl.DevServer.Tests project), and this class contains non-trivial logic including directory traversal, prioritization, error handling, and depth limits, it should have unit tests to ensure correctness. Consider adding tests that verify:
- Discovery of solutions in the current directory
- Discovery in prioritized subdirectories (src, source, app)
- Respect for MaxSearchDepth limit
- Proper skipping of excluded directories (bin, obj, node_modules, etc.)
- Handling of both .sln and .slnx extensions
- Error handling for UnauthorizedAccessException and DirectoryNotFoundException
- Ordering of results (depth first, then alphabetically)
|
π€ Your WebAssembly Skia Sample App stage site is ready! Visit it here: https://unowasmprstaging.z20.web.core.windows.net/pr-22437/wasm-skia-net9/index.html |
|
π€ Your Docs stage site is ready! Visit it here: https://unodocsprstaging.z13.web.core.windows.net/pr-22437/docs/index.html |
|
|
GitHub Issue: closes #22436
PR Type:
What is the current behavior? π€
What is the new behavior? π
PR Checklist β
Please check if your PR fulfills the following requirements:
Screenshots Compare Test Runresults.Other information βΉοΈ