GetTransitiveProjects(IEnumerable new(projectNode.ProjectInstance.FullPath, projectNode.GetTargetFramework());
+ public static ProjectInstanceId GetId(this ProjectInstance project)
+ => new(project.FullPath, project.GetTargetFramework());
}
diff --git a/src/BuiltInTools/Watch/Build/StaticWebAssetPattern.MSBuild.cs b/src/BuiltInTools/Watch/Build/StaticWebAssetPattern.MSBuild.cs
new file mode 100644
index 000000000000..35931038b8ec
--- /dev/null
+++ b/src/BuiltInTools/Watch/Build/StaticWebAssetPattern.MSBuild.cs
@@ -0,0 +1,11 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using Microsoft.Build.Globbing;
+
+namespace Microsoft.DotNet.HotReload;
+
+internal sealed partial class StaticWebAssetPattern
+{
+ public MSBuildGlob Glob => field ??= MSBuildGlob.Parse(Directory, Pattern);
+}
diff --git a/src/BuiltInTools/Watch/Context/EnvironmentOptions.cs b/src/BuiltInTools/Watch/Context/EnvironmentOptions.cs
index 8eb705b17926..0569229e3233 100644
--- a/src/BuiltInTools/Watch/Context/EnvironmentOptions.cs
+++ b/src/BuiltInTools/Watch/Context/EnvironmentOptions.cs
@@ -30,7 +30,7 @@ internal sealed record EnvironmentOptions(
string MuxerPath,
TimeSpan? ProcessCleanupTimeout,
bool IsPollingEnabled = false,
- bool SuppressHandlingStaticContentFiles = false,
+ bool SuppressHandlingStaticWebAssets = false,
bool SuppressMSBuildIncrementalism = false,
bool SuppressLaunchBrowser = false,
bool SuppressBrowserRefresh = false,
@@ -49,7 +49,7 @@ internal sealed record EnvironmentOptions(
MuxerPath: ValidateMuxerPath(muxerPath),
ProcessCleanupTimeout: EnvironmentVariables.ProcessCleanupTimeout,
IsPollingEnabled: EnvironmentVariables.IsPollingEnabled,
- SuppressHandlingStaticContentFiles: EnvironmentVariables.SuppressHandlingStaticContentFiles,
+ SuppressHandlingStaticWebAssets: EnvironmentVariables.SuppressHandlingStaticWebAssets,
SuppressMSBuildIncrementalism: EnvironmentVariables.SuppressMSBuildIncrementalism,
SuppressLaunchBrowser: EnvironmentVariables.SuppressLaunchBrowser,
SuppressBrowserRefresh: EnvironmentVariables.SuppressBrowserRefresh,
diff --git a/src/BuiltInTools/Watch/Context/EnvironmentVariables.cs b/src/BuiltInTools/Watch/Context/EnvironmentVariables.cs
index 41ac7d88edea..6a89a52cdf88 100644
--- a/src/BuiltInTools/Watch/Context/EnvironmentVariables.cs
+++ b/src/BuiltInTools/Watch/Context/EnvironmentVariables.cs
@@ -13,6 +13,7 @@ public static class Names
public const string DotnetWatchIteration = "DOTNET_WATCH_ITERATION";
public const string DotnetLaunchProfile = "DOTNET_LAUNCH_PROFILE";
+ public const string DotnetHostPath = "DOTNET_HOST_PATH";
public const string DotNetWatchHotReloadNamedPipeName = HotReload.AgentEnvironmentVariables.DotNetWatchHotReloadNamedPipeName;
public const string DotNetStartupHooks = HotReload.AgentEnvironmentVariables.DotNetStartupHooks;
@@ -47,7 +48,7 @@ public static LogLevel? CliLogLevel
"";
#endif
- public static bool SuppressHandlingStaticContentFiles => ReadBool("DOTNET_WATCH_SUPPRESS_STATIC_FILE_HANDLING");
+ public static bool SuppressHandlingStaticWebAssets => ReadBool("DOTNET_WATCH_SUPPRESS_STATIC_FILE_HANDLING");
public static bool SuppressMSBuildIncrementalism => ReadBool("DOTNET_WATCH_SUPPRESS_MSBUILD_INCREMENTALISM");
public static bool SuppressLaunchBrowser => ReadBool("DOTNET_WATCH_SUPPRESS_LAUNCH_BROWSER");
public static bool SuppressBrowserRefresh => ReadBool(Names.SuppressBrowserRefresh);
diff --git a/src/BuiltInTools/Watch/FileWatcher/FileWatcher.cs b/src/BuiltInTools/Watch/FileWatcher/FileWatcher.cs
index 43d60c17aefe..3f5bff195e92 100644
--- a/src/BuiltInTools/Watch/FileWatcher/FileWatcher.cs
+++ b/src/BuiltInTools/Watch/FileWatcher/FileWatcher.cs
@@ -44,7 +44,7 @@ protected virtual DirectoryWatcher CreateDirectoryWatcher(string directory, Immu
var watcher = DirectoryWatcher.Create(directory, fileNames, environmentOptions.IsPollingEnabled, includeSubdirectories);
if (watcher is EventBasedDirectoryWatcher eventBasedWatcher)
{
- eventBasedWatcher.Logger = message => logger.LogDebug(message);
+ eventBasedWatcher.Logger = message => logger.LogTrace(message);
}
return watcher;
diff --git a/src/BuiltInTools/Watch/HotReload/CompilationHandler.cs b/src/BuiltInTools/Watch/HotReload/CompilationHandler.cs
index e95ccc1be34f..a8f73dbb48df 100644
--- a/src/BuiltInTools/Watch/HotReload/CompilationHandler.cs
+++ b/src/BuiltInTools/Watch/HotReload/CompilationHandler.cs
@@ -3,6 +3,7 @@
using System.Collections.Immutable;
using System.Diagnostics;
+using Microsoft.Build.Execution;
using Microsoft.Build.Graph;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
@@ -15,9 +16,8 @@ namespace Microsoft.DotNet.Watch
internal sealed class CompilationHandler : IDisposable
{
public readonly IncrementalMSBuildWorkspace Workspace;
- private readonly ILogger _logger;
+ private readonly DotNetWatchContext _context;
private readonly HotReloadService _hotReloadService;
- private readonly ProcessRunner _processRunner;
///
/// Lock to synchronize:
@@ -39,11 +39,10 @@ internal sealed class CompilationHandler : IDisposable
private bool _isDisposed;
- public CompilationHandler(ILogger logger, ProcessRunner processRunner)
+ public CompilationHandler(DotNetWatchContext context)
{
- _logger = logger;
- _processRunner = processRunner;
- Workspace = new IncrementalMSBuildWorkspace(logger);
+ _context = context;
+ Workspace = new IncrementalMSBuildWorkspace(context.Logger);
_hotReloadService = new HotReloadService(Workspace.CurrentSolution.Services, () => ValueTask.FromResult(GetAggregateCapabilities()));
}
@@ -53,9 +52,12 @@ public void Dispose()
Workspace?.Dispose();
}
+ private ILogger Logger
+ => _context.Logger;
+
public async ValueTask TerminateNonRootProcessesAndDispose(CancellationToken cancellationToken)
{
- _logger.LogDebug("Terminating remaining child processes.");
+ Logger.LogDebug("Terminating remaining child processes.");
await TerminateNonRootProcessesAsync(projectPaths: null, cancellationToken);
Dispose();
}
@@ -75,7 +77,7 @@ private void DiscardPreviousUpdates(ImmutableArray projectsToBeRebuil
}
public async ValueTask StartSessionAsync(CancellationToken cancellationToken)
{
- _logger.Log(MessageDescriptor.HotReloadSessionStarting);
+ Logger.Log(MessageDescriptor.HotReloadSessionStarting);
var solution = Workspace.CurrentSolution;
@@ -95,7 +97,7 @@ public async ValueTask StartSessionAsync(CancellationToken cancellationToken)
}
}
- _logger.Log(MessageDescriptor.HotReloadSessionStarted);
+ Logger.Log(MessageDescriptor.HotReloadSessionStarted);
}
public async Task TrackRunningProjectAsync(
@@ -140,7 +142,7 @@ public async ValueTask StartSessionAsync(CancellationToken cancellationToken)
};
var launchResult = new ProcessLaunchResult();
- var runningProcess = _processRunner.RunAsync(processSpec, clients.ClientLogger, launchResult, processTerminationSource.Token);
+ var runningProcess = _context.ProcessRunner.RunAsync(processSpec, clients.ClientLogger, launchResult, processTerminationSource.Token);
if (launchResult.ProcessId == null)
{
// error already reported
@@ -232,7 +234,7 @@ public async ValueTask StartSessionAsync(CancellationToken cancellationToken)
catch (OperationCanceledException) when (processExitedSource.IsCancellationRequested)
{
// Process exited during initialization. This should not happen since we control the process during this time.
- _logger.LogError("Failed to launch '{ProjectPath}'. Process {PID} exited during initialization.", projectPath, launchResult.ProcessId);
+ Logger.LogError("Failed to launch '{ProjectPath}'. Process {PID} exited during initialization.", projectPath, launchResult.ProcessId);
return null;
}
}
@@ -281,7 +283,7 @@ private ImmutableArray GetAggregateCapabilities()
.Order()
.ToImmutableArray();
- _logger.Log(MessageDescriptor.HotReloadCapabilities, string.Join(" ", capabilities));
+ Logger.Log(MessageDescriptor.HotReloadCapabilities, string.Join(" ", capabilities));
return capabilities;
}
@@ -341,7 +343,7 @@ private static void PrepareCompilations(Solution solution, string projectPath, C
{
_hotReloadService.DiscardUpdate();
- _logger.Log(MessageDescriptor.HotReloadSuspended);
+ Logger.Log(MessageDescriptor.HotReloadSuspended);
await Task.Delay(-1, cancellationToken);
return ([], [], [], []);
@@ -409,7 +411,7 @@ await ForEachProjectAsync(projectsToUpdate, async (runningProject, cancellationT
return projectsWithPath[0];
}
- return projectsWithPath.SingleOrDefault(p => string.Equals(p.ProjectNode.GetTargetFramework(), tfm, StringComparison.OrdinalIgnoreCase));
+ return projectsWithPath.SingleOrDefault(p => string.Equals(p.ProjectNode.ProjectInstance.GetTargetFramework(), tfm, StringComparison.OrdinalIgnoreCase));
}
private async ValueTask DisplayResultsAsync(HotReloadService.Updates updates, ImmutableDictionary runningProjectInfos, CancellationToken cancellationToken)
@@ -420,11 +422,11 @@ private async ValueTask DisplayResultsAsync(HotReloadService.Updates updates, Im
break;
case HotReloadService.Status.NoChangesToApply:
- _logger.Log(MessageDescriptor.NoCSharpChangesToApply);
+ Logger.Log(MessageDescriptor.NoCSharpChangesToApply);
break;
case HotReloadService.Status.Blocked:
- _logger.Log(MessageDescriptor.UnableToApplyChanges);
+ Logger.Log(MessageDescriptor.UnableToApplyChanges);
break;
default:
@@ -433,7 +435,7 @@ private async ValueTask DisplayResultsAsync(HotReloadService.Updates updates, Im
if (!updates.ProjectsToRestart.IsEmpty)
{
- _logger.Log(MessageDescriptor.RestartNeededToApplyChanges);
+ Logger.Log(MessageDescriptor.RestartNeededToApplyChanges);
}
var errorsToDisplayInApp = new List();
@@ -515,7 +517,7 @@ void ReportDiagnostic(Diagnostic diagnostic, MessageDescriptor descriptor, strin
var display = CSharpDiagnosticFormatter.Instance.Format(diagnostic);
var args = new[] { autoPrefix, display };
- _logger.Log(descriptor, args);
+ Logger.Log(descriptor, args);
if (autoPrefix != "")
{
@@ -551,19 +553,27 @@ static MessageDescriptor GetMessageDescriptor(Diagnostic diagnostic, bool verbos
}
}
- public async ValueTask HandleStaticAssetChangesAsync(IReadOnlyList files, ProjectNodeMap projectMap, CancellationToken cancellationToken)
- {
- var allFilesHandled = true;
+ private static readonly string[] s_targets = [TargetNames.GenerateComputedBuildStaticWebAssets, TargetNames.ResolveReferencedProjectsStaticWebAssets];
- var updates = new Dictionary>();
+ private static bool HasScopedCssTargets(ProjectInstance projectInstance)
+ => s_targets.All(t => projectInstance.Targets.ContainsKey(t));
+
+ public async ValueTask HandleStaticAssetChangesAsync(
+ IReadOnlyList files,
+ ProjectNodeMap projectMap,
+ IReadOnlyDictionary manifests,
+ CancellationToken cancellationToken)
+ {
+ var assets = new Dictionary>();
+ var projectInstancesToRegenerate = new HashSet();
foreach (var changedFile in files)
{
var file = changedFile.Item;
+ var isScopedCss = StaticWebAsset.IsScopedCssFile(file.FilePath);
- if (file.StaticWebAssetPath is null)
+ if (!isScopedCss && file.StaticWebAssetRelativeUrl is null)
{
- allFilesHandled = false;
continue;
}
@@ -572,48 +582,145 @@ public async ValueTask HandleStaticAssetChangesAsync(IReadOnlyList? failedApplicationProjectInstances = null;
+ if (projectInstancesToRegenerate.Count > 0)
+ {
+ var buildReporter = new BuildReporter(_context.BuildLogger, _context.Options, _context.EnvironmentOptions);
+
+ // Note: MSBuild only allows one build at a time in a process.
+ foreach (var projectInstance in projectInstancesToRegenerate)
+ {
+ Logger.LogDebug("[{Project}] Regenerating scoped CSS bundle.", projectInstance.GetDisplayName());
+
+ using var loggers = buildReporter.GetLoggers(projectInstance.FullPath, "ScopedCss");
+
+ // Deep copy so that we don't pollute the project graph:
+ if (!projectInstance.DeepCopy().Build(s_targets, loggers))
+ {
+ loggers.ReportOutput();
+
+ failedApplicationProjectInstances ??= [];
+ failedApplicationProjectInstances.Add(projectInstance);
+ }
+ }
}
- var tasks = updates.Select(async entry =>
+ var tasks = assets.Select(async entry =>
{
- var (runningProject, assets) = entry;
- using var processCommunicationCancellationSource = CancellationTokenSource.CreateLinkedTokenSource(runningProject.ProcessExitedCancellationToken, cancellationToken);
- await runningProject.Clients.ApplyStaticAssetUpdatesAsync(assets, processCommunicationCancellationSource.Token);
+ var (applicationProjectInstance, instanceAssets) = entry;
+
+ if (failedApplicationProjectInstances?.Contains(applicationProjectInstance) == true)
+ {
+ return;
+ }
+
+ if (!TryGetRunningProject(applicationProjectInstance.FullPath, out var runningProjects))
+ {
+ return;
+ }
+
+ foreach (var runningProject in runningProjects)
+ {
+ using var processCommunicationCancellationSource = CancellationTokenSource.CreateLinkedTokenSource(runningProject.ProcessExitedCancellationToken, cancellationToken);
+ await runningProject.Clients.ApplyStaticAssetUpdatesAsync(instanceAssets.Values, processCommunicationCancellationSource.Token);
+ }
});
await Task.WhenAll(tasks).WaitAsync(cancellationToken);
- _logger.Log(MessageDescriptor.HotReloadOfStaticAssetsSucceeded);
-
- return allFilesHandled;
+ Logger.Log(MessageDescriptor.StaticAssetsReloaded);
}
///
diff --git a/src/BuiltInTools/Watch/HotReload/HotReloadDotNetWatcher.cs b/src/BuiltInTools/Watch/HotReload/HotReloadDotNetWatcher.cs
index 52df0fa62829..ab85d94c08f0 100644
--- a/src/BuiltInTools/Watch/HotReload/HotReloadDotNetWatcher.cs
+++ b/src/BuiltInTools/Watch/HotReload/HotReloadDotNetWatcher.cs
@@ -3,6 +3,8 @@
using System.Collections.Immutable;
using System.Diagnostics;
+using System.Text.Encodings.Web;
+using Microsoft.Build.Execution;
using Microsoft.Build.Graph;
using Microsoft.CodeAnalysis;
using Microsoft.DotNet.HotReload;
@@ -113,8 +115,7 @@ public async Task WatchAsync(CancellationToken shutdownCancellationToken)
}
var projectMap = new ProjectNodeMap(evaluationResult.ProjectGraph, _context.Logger);
- compilationHandler = new CompilationHandler(_context.Logger, _context.ProcessRunner);
- var scopedCssFileHandler = new ScopedCssFileHandler(_context.Logger, _context.BuildLogger, projectMap, _context.BrowserRefreshServerFactory, _context.Options, _context.EnvironmentOptions);
+ compilationHandler = new CompilationHandler(_context);
var projectLauncher = new ProjectLauncher(_context, projectMap, compilationHandler, iteration);
evaluationResult.ItemExclusions.Report(_context.Logger);
@@ -260,13 +261,9 @@ void FileChangedCallback(ChangedPath change)
var stopwatch = Stopwatch.StartNew();
HotReloadEventSource.Log.HotReloadStart(HotReloadEventSource.StartType.StaticHandler);
- await compilationHandler.HandleStaticAssetChangesAsync(changedFiles, projectMap, iterationCancellationToken);
+ await compilationHandler.HandleStaticAssetChangesAsync(changedFiles, projectMap, evaluationResult.StaticWebAssetsManifests, iterationCancellationToken);
HotReloadEventSource.Log.HotReloadEnd(HotReloadEventSource.StartType.StaticHandler);
- HotReloadEventSource.Log.HotReloadStart(HotReloadEventSource.StartType.ScopedCssHandler);
- await scopedCssFileHandler.HandleFileChangesAsync(changedFiles, iterationCancellationToken);
- HotReloadEventSource.Log.HotReloadEnd(HotReloadEventSource.StartType.ScopedCssHandler);
-
HotReloadEventSource.Log.HotReloadStart(HotReloadEventSource.StartType.CompilationHandler);
var (managedCodeUpdates, projectsToRebuild, projectsToRedeploy, projectsToRestart) = await compilationHandler.HandleManagedCodeChangesAsync(
@@ -399,7 +396,7 @@ await Task.WhenAll(
_context.Logger.Log(MessageDescriptor.HotReloadChangeHandled, stopwatch.ElapsedMilliseconds);
- async Task> CaptureChangedFilesSnapshot(ImmutableArray rebuiltProjects)
+ async Task> CaptureChangedFilesSnapshot(ImmutableArray rebuiltProjects)
{
var changedPaths = Interlocked.Exchange(ref changedFilesAccumulator, []);
if (changedPaths is [])
@@ -432,22 +429,14 @@ async Task> CaptureChangedFilesSnapshot(ImmutableArra
new FileItem() { FilePath = changedPath.Path, ContainingProjectPaths = [] },
changedPath.Kind);
})
- .ToImmutableList();
+ .ToList();
ReportFileChanges(changedFiles);
- // When a new file is added we need to run design-time build to find out
- // what kind of the file it is and which project(s) does it belong to (can be linked, web asset, etc.).
- // We also need to re-evaluate the project if any project files have been modified.
- // We don't need to rebuild and restart the application though.
- var fileAdded = changedFiles.Any(f => f.Kind is ChangeKind.Add);
- var projectChanged = !fileAdded && changedFiles.Any(f => evaluationResult.BuildFiles.Contains(f.Item.FilePath));
- var evaluationRequired = fileAdded || projectChanged;
+ AnalyzeFileChanges(changedFiles, evaluationResult, out var evaluationRequired);
if (evaluationRequired)
{
- _context.Logger.Log(fileAdded ? MessageDescriptor.FileAdditionTriggeredReEvaluation : MessageDescriptor.ProjectChangeTriggeredReEvaluation);
-
// TODO: consider re-evaluating only affected projects instead of the whole graph.
evaluationResult = await EvaluateRootProjectAsync(restore: true, iterationCancellationToken);
@@ -463,9 +452,14 @@ async Task> CaptureChangedFilesSnapshot(ImmutableArra
}
// Update files in the change set with new evaluation info.
- changedFiles = [.. changedFiles
- .Select(f => evaluationResult.Files.TryGetValue(f.Item.FilePath, out var evaluatedFile) ? f with { Item = evaluatedFile } : f)
- ];
+ for (var i = 0; i < changedFiles.Count; i++)
+ {
+ var file = changedFiles[i];
+ if (evaluationResult.Files.TryGetValue(file.Item.FilePath, out var evaluatedFile))
+ {
+ changedFiles[i] = file with { Item = evaluatedFile };
+ }
+ }
_context.Logger.Log(MessageDescriptor.ReEvaluationCompleted);
}
@@ -478,13 +472,13 @@ async Task> CaptureChangedFilesSnapshot(ImmutableArra
var rebuiltProjectPaths = rebuiltProjects.ToHashSet();
var newAccumulator = ImmutableList.Empty;
- var newChangedFiles = ImmutableList.Empty;
+ var newChangedFiles = new List();
foreach (var file in changedFiles)
{
if (file.Item.ContainingProjectPaths.All(containingProjectPath => rebuiltProjectPaths.Contains(containingProjectPath)))
{
- newChangedFiles = newChangedFiles.Add(file);
+ newChangedFiles.Add(file);
}
else
{
@@ -504,7 +498,7 @@ async Task> CaptureChangedFilesSnapshot(ImmutableArra
await compilationHandler.Workspace.UpdateFileContentAsync(changedFiles, iterationCancellationToken);
}
- return changedFiles;
+ return [.. changedFiles];
}
}
}
@@ -560,6 +554,104 @@ async Task> CaptureChangedFilesSnapshot(ImmutableArra
}
}
+ private void AnalyzeFileChanges(
+ List changedFiles,
+ EvaluationResult evaluationResult,
+ out bool evaluationRequired)
+ {
+ // If any build file changed (project, props, targets) we need to re-evaluate the projects.
+ // Currently we re-evaluate the whole project graph even if only a single project file changed.
+ if (changedFiles.Select(f => f.Item.FilePath).FirstOrDefault(path => evaluationResult.BuildFiles.Contains(path) || MatchesBuildFile(path)) is { } firstBuildFilePath)
+ {
+ _context.Logger.Log(MessageDescriptor.ProjectChangeTriggeredReEvaluation, firstBuildFilePath);
+ evaluationRequired = true;
+ return;
+ }
+
+ for (var i = 0; i < changedFiles.Count; i++)
+ {
+ var changedFile = changedFiles[i];
+ var filePath = changedFile.Item.FilePath;
+
+ if (changedFile.Kind is ChangeKind.Add)
+ {
+ if (MatchesStaticWebAssetFilePattern(evaluationResult, filePath, out var staticWebAssetUrl))
+ {
+ changedFiles[i] = changedFile with
+ {
+ Item = changedFile.Item with { StaticWebAssetRelativeUrl = staticWebAssetUrl }
+ };
+ }
+ else
+ {
+ // TODO: https://github.com/dotnet/sdk/issues/52390
+ // Get patterns from evaluation that match Compile, AdditionalFile, AnalyzerConfigFile items.
+ // Avoid re-evaluating on addition of files that don't affect the project.
+
+ // project file or other file:
+ _context.Logger.Log(MessageDescriptor.FileAdditionTriggeredReEvaluation, filePath);
+ evaluationRequired = true;
+ return;
+ }
+ }
+ }
+
+ evaluationRequired = false;
+ }
+
+ ///
+ /// True if the file path looks like a file that might be imported by MSBuild.
+ ///
+ private static bool MatchesBuildFile(string filePath)
+ {
+ var extension = Path.GetExtension(filePath);
+ return extension.Equals(".props", PathUtilities.OSSpecificPathComparison)
+ || extension.Equals(".targets", PathUtilities.OSSpecificPathComparison)
+ || extension.EndsWith("proj", PathUtilities.OSSpecificPathComparison)
+ || extension.Equals("projitems", PathUtilities.OSSpecificPathComparison) // shared project items
+ || string.Equals(Path.GetFileName(filePath), "global.json", PathUtilities.OSSpecificPathComparison);
+ }
+
+ ///
+ /// Determines if the given file path is a static web asset file path based on
+ /// the discovery patterns.
+ ///
+ private static bool MatchesStaticWebAssetFilePattern(EvaluationResult evaluationResult, string filePath, out string? staticWebAssetUrl)
+ {
+ staticWebAssetUrl = null;
+
+ if (StaticWebAsset.IsScopedCssFile(filePath))
+ {
+ return true;
+ }
+
+ foreach (var (_, manifest) in evaluationResult.StaticWebAssetsManifests)
+ {
+ foreach (var pattern in manifest.DiscoveryPatterns)
+ {
+ var match = pattern.Glob.MatchInfo(filePath);
+ if (match.IsMatch)
+ {
+ var dirUrl = match.WildcardDirectoryPartMatchGroup.Replace(Path.DirectorySeparatorChar, '/');
+
+ Debug.Assert(!dirUrl.EndsWith('/'));
+ Debug.Assert(!pattern.BaseUrl.EndsWith('/'));
+
+ var url = UrlEncoder.Default.Encode(dirUrl + "/" + match.FilenamePartMatchGroup);
+ if (pattern.BaseUrl != "")
+ {
+ url = pattern.BaseUrl + "/" + url;
+ }
+
+ staticWebAssetUrl = url;
+ return true;
+ }
+ }
+ }
+
+ return false;
+ }
+
private void DeployProjectDependencies(ProjectGraph graph, ImmutableArray projectPaths, CancellationToken cancellationToken)
{
var projectPathSet = projectPaths.ToImmutableHashSet(PathUtilities.OSSpecificPathComparer);
diff --git a/src/BuiltInTools/Watch/HotReload/HotReloadEventSource.cs b/src/BuiltInTools/Watch/HotReload/HotReloadEventSource.cs
index 92af26786b5a..f14b000bb34c 100644
--- a/src/BuiltInTools/Watch/HotReload/HotReloadEventSource.cs
+++ b/src/BuiltInTools/Watch/HotReload/HotReloadEventSource.cs
@@ -14,7 +14,6 @@ public enum StartType
Main,
StaticHandler,
CompilationHandler,
- ScopedCssHandler
}
internal sealed class Keywords
diff --git a/src/BuiltInTools/Watch/HotReload/ScopedCssFileHandler.cs b/src/BuiltInTools/Watch/HotReload/ScopedCssFileHandler.cs
deleted file mode 100644
index 09e33759e7a4..000000000000
--- a/src/BuiltInTools/Watch/HotReload/ScopedCssFileHandler.cs
+++ /dev/null
@@ -1,107 +0,0 @@
-// Licensed to the .NET Foundation under one or more agreements.
-// The .NET Foundation licenses this file to you under the MIT license.
-
-
-using Microsoft.Build.Graph;
-using Microsoft.Extensions.Logging;
-
-namespace Microsoft.DotNet.Watch
-{
- internal sealed class ScopedCssFileHandler(ILogger logger, ILogger buildLogger, ProjectNodeMap projectMap, BrowserRefreshServerFactory browserConnector, GlobalOptions options, EnvironmentOptions environmentOptions)
- {
- private const string BuildTargetName = TargetNames.GenerateComputedBuildStaticWebAssets;
-
- public async ValueTask HandleFileChangesAsync(IReadOnlyList files, CancellationToken cancellationToken)
- {
- var projectsToRefresh = new HashSet();
- var hasApplicableFiles = false;
-
- for (int i = 0; i < files.Count; i++)
- {
- var file = files[i].Item;
-
- if (!file.FilePath.EndsWith(".razor.css", StringComparison.Ordinal) &&
- !file.FilePath.EndsWith(".cshtml.css", StringComparison.Ordinal))
- {
- continue;
- }
-
- hasApplicableFiles = true;
- logger.LogDebug("Handling file change event for scoped css file {FilePath}.", file.FilePath);
- foreach (var containingProjectPath in file.ContainingProjectPaths)
- {
- if (!projectMap.Map.TryGetValue(containingProjectPath, out var projectNodes))
- {
- // Shouldn't happen.
- logger.LogWarning("Project '{Path}' not found in the project graph.", containingProjectPath);
- continue;
- }
-
- // Build and refresh each instance (TFM) of the project.
- foreach (var projectNode in projectNodes)
- {
- // The outer build project instance (that specifies TargetFrameworks) won't have the target.
- if (projectNode.ProjectInstance.Targets.ContainsKey(BuildTargetName))
- {
- projectsToRefresh.Add(projectNode);
- }
- }
- }
- }
-
- if (!hasApplicableFiles)
- {
- return;
- }
-
- var buildReporter = new BuildReporter(buildLogger, options, environmentOptions);
-
- var buildTasks = projectsToRefresh.Select(projectNode => Task.Run(() =>
- {
- using var loggers = buildReporter.GetLoggers(projectNode.ProjectInstance.FullPath, BuildTargetName);
-
- // Deep copy so that we don't pollute the project graph:
- if (!projectNode.ProjectInstance.DeepCopy().Build(BuildTargetName, loggers))
- {
- loggers.ReportOutput();
- return null;
- }
-
- return projectNode;
- }));
-
- var buildResults = await Task.WhenAll(buildTasks).WaitAsync(cancellationToken);
-
- var browserRefreshTasks = buildResults.Where(p => p != null)!.GetAncestorsAndSelf().Select(async projectNode =>
- {
- if (browserConnector.TryGetRefreshServer(projectNode, out var browserRefreshServer))
- {
- // We'd like an accurate scoped css path, but this needs a lot of work to wire-up now.
- // We'll handle this as part of https://github.com/dotnet/aspnetcore/issues/31217.
- // For now, we'll make it look like some css file which would cause JS to update a
- // single file if it's from the current project, or all locally hosted css files if it's a file from
- // referenced project.
- var relativeUrl = Path.GetFileNameWithoutExtension(projectNode.ProjectInstance.FullPath) + ".css";
- await browserRefreshServer.UpdateStaticAssetsAsync([relativeUrl], cancellationToken);
- }
- });
-
- await Task.WhenAll(browserRefreshTasks).WaitAsync(cancellationToken);
-
- var successfulCount = buildResults.Sum(r => r != null ? 1 : 0);
-
- if (successfulCount == buildResults.Length)
- {
- logger.Log(MessageDescriptor.HotReloadOfScopedCssSucceeded);
- }
- else if (successfulCount > 0)
- {
- logger.Log(MessageDescriptor.HotReloadOfScopedCssPartiallySucceeded, successfulCount, buildResults.Length);
- }
- else
- {
- logger.Log(MessageDescriptor.HotReloadOfScopedCssFailed);
- }
- }
- }
-}
diff --git a/src/BuiltInTools/Watch/UI/IReporter.cs b/src/BuiltInTools/Watch/UI/IReporter.cs
index 82b60996e1ab..079ce3e46101 100644
--- a/src/BuiltInTools/Watch/UI/IReporter.cs
+++ b/src/BuiltInTools/Watch/UI/IReporter.cs
@@ -196,12 +196,12 @@ public MessageDescriptor WithLevelWhen(LogLevel level, bool condition)
public static readonly MessageDescriptor ConnectedToRefreshServer = Create(LogEvents.ConnectedToRefreshServer, Emoji.Default);
public static readonly MessageDescriptor RestartingApplicationToApplyChanges = Create("Restarting application to apply changes ...", Emoji.Default, LogLevel.Information);
public static readonly MessageDescriptor RestartingApplication = Create("Restarting application ...", Emoji.Default, LogLevel.Information);
- public static readonly MessageDescriptor IgnoringChangeInHiddenDirectory = Create("Ignoring change in hidden directory '{0}': {1} '{2}'", Emoji.Watch, LogLevel.Debug);
- public static readonly MessageDescriptor IgnoringChangeInOutputDirectory = Create("Ignoring change in output directory: {0} '{1}'", Emoji.Watch, LogLevel.Debug);
- public static readonly MessageDescriptor IgnoringChangeInExcludedFile = Create("Ignoring change in excluded file '{0}': {1}. Path matches {2} glob '{3}' set in '{4}'.", Emoji.Watch, LogLevel.Debug);
- public static readonly MessageDescriptor FileAdditionTriggeredReEvaluation = Create("File addition triggered re-evaluation.", Emoji.Watch, LogLevel.Debug);
+ public static readonly MessageDescriptor IgnoringChangeInHiddenDirectory = Create("Ignoring change in hidden directory '{0}': {1} '{2}'", Emoji.Watch, LogLevel.Trace);
+ public static readonly MessageDescriptor IgnoringChangeInOutputDirectory = Create("Ignoring change in output directory: {0} '{1}'", Emoji.Watch, LogLevel.Trace);
+ public static readonly MessageDescriptor IgnoringChangeInExcludedFile = Create("Ignoring change in excluded file '{0}': {1}. Path matches {2} glob '{3}' set in '{4}'.", Emoji.Watch, LogLevel.Trace);
+ public static readonly MessageDescriptor FileAdditionTriggeredReEvaluation = Create("File addition triggered re-evaluation: '{0}'.", Emoji.Watch, LogLevel.Debug);
+ public static readonly MessageDescriptor ProjectChangeTriggeredReEvaluation = Create("Project change triggered re-evaluation: '{0}'.", Emoji.Watch, LogLevel.Debug);
public static readonly MessageDescriptor ReEvaluationCompleted = Create("Re-evaluation completed.", Emoji.Watch, LogLevel.Debug);
- public static readonly MessageDescriptor ProjectChangeTriggeredReEvaluation = Create("Project change triggered re-evaluation.", Emoji.Watch, LogLevel.Debug);
public static readonly MessageDescriptor NoCSharpChangesToApply = Create("No C# changes to apply.", Emoji.Watch, LogLevel.Information);
public static readonly MessageDescriptor Exited = Create("Exited", Emoji.Watch, LogLevel.Information);
public static readonly MessageDescriptor ExitedWithUnknownErrorCode = Create("Exited with unknown error code", Emoji.Error, LogLevel.Error);
@@ -215,10 +215,7 @@ public MessageDescriptor WithLevelWhen(LogLevel level, bool condition)
public static readonly MessageDescriptor TerminatingProcess = Create("Terminating process {0} ({1}).", Emoji.Watch, LogLevel.Debug);
public static readonly MessageDescriptor FailedToSendSignalToProcess = Create("Failed to send {0} signal to process {1}: {2}", Emoji.Warning, LogLevel.Warning);
public static readonly MessageDescriptor ErrorReadingProcessOutput = Create("Error reading {0} of process {1}: {2}", Emoji.Watch, LogLevel.Debug);
- public static readonly MessageDescriptor HotReloadOfScopedCssSucceeded = Create("Hot reload of scoped css succeeded.", Emoji.HotReload, LogLevel.Information);
- public static readonly MessageDescriptor HotReloadOfScopedCssPartiallySucceeded = Create("Hot reload of scoped css partially succeeded: {0} project(s) out of {1} were updated.", Emoji.HotReload, LogLevel.Information);
- public static readonly MessageDescriptor HotReloadOfScopedCssFailed = Create("Hot reload of scoped css failed.", Emoji.Error, LogLevel.Error);
- public static readonly MessageDescriptor HotReloadOfStaticAssetsSucceeded = Create("Hot reload of static assets succeeded.", Emoji.HotReload, LogLevel.Information);
+ public static readonly MessageDescriptor StaticAssetsReloaded = Create("Static assets reloaded.", Emoji.HotReload, LogLevel.Information);
public static readonly MessageDescriptor SendingStaticAssetUpdateRequest = Create(LogEvents.SendingStaticAssetUpdateRequest, Emoji.Default);
public static readonly MessageDescriptor HotReloadCapabilities = Create("Hot reload capabilities: {0}.", Emoji.HotReload, LogLevel.Debug);
public static readonly MessageDescriptor HotReloadSuspended = Create("Hot reload suspended. To continue hot reload, press \"Ctrl + R\".", Emoji.HotReload, LogLevel.Information);
@@ -232,7 +229,7 @@ public MessageDescriptor WithLevelWhen(LogLevel level, bool condition)
public static readonly MessageDescriptor ApplicationKind_WebApplication = Create("Application kind: WebApplication.", Emoji.Default, LogLevel.Debug);
public static readonly MessageDescriptor ApplicationKind_Default = Create("Application kind: Default.", Emoji.Default, LogLevel.Debug);
public static readonly MessageDescriptor WatchingFilesForChanges = Create("Watching {0} file(s) for changes", Emoji.Watch, LogLevel.Debug);
- public static readonly MessageDescriptor WatchingFilesForChanges_FilePath = Create("> {0}", Emoji.Watch, LogLevel.Debug);
+ public static readonly MessageDescriptor WatchingFilesForChanges_FilePath = Create("> {0}", Emoji.Watch, LogLevel.Trace);
public static readonly MessageDescriptor Building = Create("Building {0} ...", Emoji.Default, LogLevel.Information);
public static readonly MessageDescriptor BuildSucceeded = Create("Build succeeded: {0}", Emoji.Default, LogLevel.Information);
public static readonly MessageDescriptor BuildFailed = Create("Build failed: {0}", Emoji.Default, LogLevel.Information);
diff --git a/src/BuiltInTools/dotnet-watch/CommandLine/CommandLineOptions.cs b/src/BuiltInTools/dotnet-watch/CommandLine/CommandLineOptions.cs
index 5a5306fe2ddf..7335284574b0 100644
--- a/src/BuiltInTools/dotnet-watch/CommandLine/CommandLineOptions.cs
+++ b/src/BuiltInTools/dotnet-watch/CommandLine/CommandLineOptions.cs
@@ -7,6 +7,7 @@
using System.Data;
using Microsoft.DotNet.Cli;
using Microsoft.DotNet.Cli.CommandLine;
+using Microsoft.DotNet.Cli.Commands;
using Microsoft.DotNet.Cli.Commands.Build;
using Microsoft.DotNet.Cli.Commands.Run;
using Microsoft.DotNet.Cli.Commands.Test;
@@ -16,8 +17,6 @@ namespace Microsoft.DotNet.Watch;
internal sealed class CommandLineOptions
{
- public const string DefaultCommand = "run";
-
private static readonly ImmutableArray s_binaryLogOptionNames = ["-bl", "/bl", "-binaryLogger", "--binaryLogger", "/binaryLogger"];
public bool List { get; init; }
@@ -38,9 +37,9 @@ internal sealed class CommandLineOptions
///
public required IReadOnlyList BuildArguments { get; init; }
- public string? ExplicitCommand { get; init; }
+ public required string Command { get; init; }
- public string Command => ExplicitCommand ?? DefaultCommand;
+ public required bool IsExplicitCommand { get; init; }
public static CommandLineOptions? Parse(IReadOnlyList args, ILogger logger, TextWriter output, out int errorCode)
{
@@ -67,8 +66,7 @@ internal sealed class CommandLineOptions
}
// determine subcommand:
- var explicitCommand = TryGetSubcommand(parseResult);
- var command = explicitCommand ?? RunCommandParser.GetCommand();
+ var command = GetSubcommand(parseResult, out bool isExplicitCommand);
var buildOptions = command.Options.Where(o => o.ForwardingFunction is not null);
foreach (var buildOption in buildOptions)
@@ -111,7 +109,7 @@ internal sealed class CommandLineOptions
var commandArguments = GetCommandArguments(
parseResult,
command,
- explicitCommand,
+ isExplicitCommand,
out var binLogToken,
out var binLogPath);
@@ -143,7 +141,8 @@ internal sealed class CommandLineOptions
},
CommandArguments = commandArguments,
- ExplicitCommand = explicitCommand?.Name,
+ Command = command.Name,
+ IsExplicitCommand = isExplicitCommand,
ProjectPath = projectValue,
LaunchProfileName = parseResult.GetValue(definition.LaunchProfileOption),
@@ -176,7 +175,7 @@ internal sealed class CommandLineOptions
private static IReadOnlyList GetCommandArguments(
ParseResult parseResult,
Command command,
- Command? explicitCommand,
+ bool isExplicitCommand,
out string? binLogToken,
out string? binLogPath)
{
@@ -250,7 +249,7 @@ private static IReadOnlyList GetCommandArguments(
if (i < unmatchedTokensBeforeDashDash)
{
- if (!seenCommand && token == explicitCommand?.Name)
+ if (!seenCommand && isExplicitCommand && token == command.Name)
{
seenCommand = true;
continue;
@@ -293,24 +292,27 @@ private static string GetOptionNameToForward(OptionResult optionResult)
// For those that do not, use the Option's Name instead.
=> optionResult.IdentifierToken?.Value ?? optionResult.Option.Name;
- private static Command? TryGetSubcommand(ParseResult parseResult)
+ private static Command GetSubcommand(ParseResult parseResult, out bool isExplicit)
{
// Assuming that all tokens after "--" are unmatched:
var dashDashIndex = IndexOf(parseResult.Tokens, t => t.Value == "--");
var unmatchedTokensBeforeDashDash = parseResult.UnmatchedTokens.Count - (dashDashIndex >= 0 ? parseResult.Tokens.Count - dashDashIndex - 1 : 0);
- var knownCommandsByName = Parser.Subcommands.ToDictionary(keySelector: c => c.Name, elementSelector: c => c);
+ var dotnetDefinition = new DotNetCommandDefinition();
+ var knownCommandsByName = dotnetDefinition.Subcommands.ToDictionary(keySelector: c => c.Name, elementSelector: c => c);
for (int i = 0; i < unmatchedTokensBeforeDashDash; i++)
{
// command token can't follow "--"
if (knownCommandsByName.TryGetValue(parseResult.UnmatchedTokens[i], out var explicitCommand))
{
+ isExplicit = true;
return explicitCommand;
}
}
- return null;
+ isExplicit = false;
+ return dotnetDefinition.RunCommand;
}
private static bool ReportErrors(ParseResult parseResult, ILogger logger)
diff --git a/src/BuiltInTools/dotnet-watch/Program.cs b/src/BuiltInTools/dotnet-watch/Program.cs
index 384df13ae5cc..476108f319c7 100644
--- a/src/BuiltInTools/dotnet-watch/Program.cs
+++ b/src/BuiltInTools/dotnet-watch/Program.cs
@@ -46,6 +46,9 @@ public static async Task Main(string[] args)
var environmentOptions = EnvironmentOptions.FromEnvironment(processPath);
+ // msbuild tasks depend on host path variable:
+ Environment.SetEnvironmentVariable(EnvironmentVariables.Names.DotnetHostPath, environmentOptions.MuxerPath);
+
var program = TryCreate(
args,
new PhysicalConsole(environmentOptions.TestFlags),
diff --git a/src/BuiltInTools/dotnet-watch/Properties/launchSettings.json b/src/BuiltInTools/dotnet-watch/Properties/launchSettings.json
index 9e28729eb807..b60b37dc0855 100644
--- a/src/BuiltInTools/dotnet-watch/Properties/launchSettings.json
+++ b/src/BuiltInTools/dotnet-watch/Properties/launchSettings.json
@@ -2,14 +2,15 @@
"profiles": {
"dotnet-watch": {
"commandName": "Project",
- "commandLineArgs": "--verbose -bl",
- "workingDirectory": "C:\\bugs\\9756\\aspire-watch-start-issue\\Aspire.AppHost",
+ "commandLineArgs": "-bl",
+ "workingDirectory": "$(RepoRoot)src\\Assets\\TestProjects\\BlazorWasmWithLibrary\\blazorwasm",
"environmentVariables": {
"DOTNET_WATCH_DEBUG_SDK_DIRECTORY": "$(RepoRoot)artifacts\\bin\\redist\\$(Configuration)\\dotnet\\sdk\\$(Version)",
"DCP_IDE_REQUEST_TIMEOUT_SECONDS": "100000",
"DCP_IDE_NOTIFICATION_TIMEOUT_SECONDS": "100000",
"DCP_IDE_NOTIFICATION_KEEPALIVE_SECONDS": "100000",
- "ASPIRE_ALLOW_UNSECURED_TRANSPORT": "1"
+ "ASPIRE_ALLOW_UNSECURED_TRANSPORT": "1",
+ "DOTNET_CLI_CONTEXT_VERBOSE": "true"
}
}
}
diff --git a/src/BuiltInTools/dotnet-watch/Watch/MsBuildFileSetFactory.cs b/src/BuiltInTools/dotnet-watch/Watch/MsBuildFileSetFactory.cs
index 32f8f2a0d58d..52f7f6d8985b 100644
--- a/src/BuiltInTools/dotnet-watch/Watch/MsBuildFileSetFactory.cs
+++ b/src/BuiltInTools/dotnet-watch/Watch/MsBuildFileSetFactory.cs
@@ -5,6 +5,7 @@
using System.Diagnostics;
using System.Text.Json;
using Microsoft.Build.Graph;
+using Microsoft.DotNet.HotReload;
using Microsoft.Extensions.Logging;
namespace Microsoft.DotNet.Watch
@@ -96,7 +97,8 @@ internal sealed class EvaluationResult(IReadOnlyDictionary fil
foreach (var staticFile in projectItems.StaticFiles)
{
- AddFile(staticFile.FilePath, staticFile.StaticWebAssetPath);
+ // that target adds items with "wwwroot/" prefix:
+ AddFile(staticFile.FilePath, staticFile.StaticWebAssetPath?["wwwroot/".Length..]);
}
void AddFile(string filePath, string? staticWebAssetPath)
@@ -107,7 +109,7 @@ void AddFile(string filePath, string? staticWebAssetPath)
{
FilePath = filePath,
ContainingProjectPaths = [projectPath],
- StaticWebAssetPath = staticWebAssetPath,
+ StaticWebAssetRelativeUrl = staticWebAssetPath,
});
}
else if (!existingFile.ContainingProjectPaths.Contains(projectPath))
@@ -161,7 +163,7 @@ private IReadOnlyList GetMSBuildArguments(string watchListFilePath)
// Set dotnet-watch reserved properties after the user specified propeties,
// so that the former take precedence.
- if (EnvironmentOptions.SuppressHandlingStaticContentFiles)
+ if (EnvironmentOptions.SuppressHandlingStaticWebAssets)
{
arguments.Add("/p:DotNetWatchContentFiles=false");
}
diff --git a/src/BuiltInTools/Watch/HotReload/StaticFileHandler.cs b/src/BuiltInTools/dotnet-watch/Watch/StaticFileHandler.cs
similarity index 92%
rename from src/BuiltInTools/Watch/HotReload/StaticFileHandler.cs
rename to src/BuiltInTools/dotnet-watch/Watch/StaticFileHandler.cs
index d20603095ab3..dddba6cb2d41 100644
--- a/src/BuiltInTools/Watch/HotReload/StaticFileHandler.cs
+++ b/src/BuiltInTools/dotnet-watch/Watch/StaticFileHandler.cs
@@ -19,7 +19,7 @@ public async ValueTask HandleFileChangesAsync(IReadOnlyList f
{
var file = files[i].Item;
- if (file.StaticWebAssetPath is null)
+ if (file.StaticWebAssetRelativeUrl is null)
{
allFilesHandled = false;
continue;
@@ -46,7 +46,7 @@ public async ValueTask HandleFileChangesAsync(IReadOnlyList f
refreshRequests.Add(refreshServer, filesPerServer = []);
}
- filesPerServer.Add(file.StaticWebAssetPath);
+ filesPerServer.Add(StaticWebAsset.WebRoot + "/" + file.StaticWebAssetRelativeUrl);
}
else if (projectsWithoutRefreshServer.Add(projectNode))
{
@@ -65,7 +65,7 @@ public async ValueTask HandleFileChangesAsync(IReadOnlyList f
await Task.WhenAll(tasks).WaitAsync(cancellationToken);
- logger.Log(MessageDescriptor.HotReloadOfStaticAssetsSucceeded);
+ logger.Log(MessageDescriptor.StaticAssetsReloaded);
return allFilesHandled;
}
diff --git a/src/BuiltInTools/dotnet-watch/dotnet-watch.csproj b/src/BuiltInTools/dotnet-watch/dotnet-watch.csproj
index 6ee14bf8ee71..5d5968b4f949 100644
--- a/src/BuiltInTools/dotnet-watch/dotnet-watch.csproj
+++ b/src/BuiltInTools/dotnet-watch/dotnet-watch.csproj
@@ -28,14 +28,12 @@