Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 0 additions & 3 deletions Directory.Build.props
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@
</PropertyGroup>

<Import Project="Sdk.props" Sdk="Microsoft.DotNet.Arcade.Sdk" />
<Import Project="$(RepositoryEngineeringDir)Analyzers.props" />

<PropertyGroup>
<IsLinux Condition="$([MSBuild]::IsOSPlatform('LINUX'))">true</IsLinux>
Expand Down Expand Up @@ -77,9 +76,7 @@
</PropertyGroup>

<PropertyGroup Condition="'$(IsTestProject)' == 'true'">
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
<GenerateProgramFile>false</GenerateProgramFile>
<!-- <TestRunnerAdditionalArguments>-parallel none</TestRunnerAdditionalArguments> -->
</PropertyGroup>

<PropertyGroup>
Expand Down
5 changes: 0 additions & 5 deletions eng/Analyzers.props

This file was deleted.

1 change: 0 additions & 1 deletion eng/Versions.props
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,6 @@
<NETStandardLibraryVersion>2.0.3</NETStandardLibraryVersion>
<NewtonsoftJsonPackageVersion>13.0.3</NewtonsoftJsonPackageVersion>
<SystemDataSqlClientPackageVersion>4.8.6</SystemDataSqlClientPackageVersion>
<StyleCopAnalyzersPackageVersion>1.2.0-beta.435</StyleCopAnalyzersPackageVersion>
<WebDeploymentPackageVersion>4.0.5</WebDeploymentPackageVersion>
<SystemCommandLineNamingConventionBinderVersion>2.0.0-beta5.25279.2</SystemCommandLineNamingConventionBinderVersion>
<MicrosoftCodeAnalysisAnalyzerTestingVersion>1.1.2</MicrosoftCodeAnalysisAnalyzerTestingVersion>
Expand Down
23 changes: 6 additions & 17 deletions src/BuiltInTools/HotReloadClient/HotReloadClients.cs
Original file line number Diff line number Diff line change
Expand Up @@ -187,39 +187,28 @@ public async ValueTask InitialUpdatesAppliedAsync(CancellationToken cancellation
}

/// <param name="cancellationToken">Cancellation token. The cancellation should trigger on process terminatation.</param>
public async Task ApplyStaticAssetUpdatesAsync(IEnumerable<(string filePath, string relativeUrl, string assemblyName, bool isApplicationProject)> assets, CancellationToken cancellationToken)
public async Task ApplyStaticAssetUpdatesAsync(IEnumerable<StaticWebAsset> assets, CancellationToken cancellationToken)
{
if (browserRefreshServer != null)
{
await browserRefreshServer.UpdateStaticAssetsAsync(assets.Select(static a => a.relativeUrl), cancellationToken);
await browserRefreshServer.UpdateStaticAssetsAsync(assets.Select(static a => a.RelativeUrl), cancellationToken);
}
else
{
var updates = new List<HotReloadStaticAssetUpdate>();

foreach (var (filePath, relativeUrl, assemblyName, isApplicationProject) in assets)
foreach (var asset in assets)
{
ImmutableArray<byte> content;
try
{
#if NET
var blob = await File.ReadAllBytesAsync(filePath, cancellationToken);
#else
var blob = File.ReadAllBytes(filePath);
#endif
content = ImmutableCollectionsMarshal.AsImmutableArray(blob);
ClientLogger.LogDebug("Loading asset '{Url}' from '{Path}'.", asset.RelativeUrl, asset.FilePath);
updates.Add(await HotReloadStaticAssetUpdate.CreateAsync(asset, cancellationToken));
}
catch (Exception e) when (e is not OperationCanceledException)
{
ClientLogger.LogError("Failed to read file {FilePath}: {Message}", filePath, e.Message);
ClientLogger.LogError("Failed to read file {FilePath}: {Message}", asset.FilePath, e.Message);
continue;
}

updates.Add(new HotReloadStaticAssetUpdate(
assemblyName: assemblyName,
relativePath: relativeUrl,
content: content,
isApplicationProject));
}

await ApplyStaticAssetUpdatesAsync([.. updates], isProcessSuspended: false, cancellationToken);
Expand Down
18 changes: 18 additions & 0 deletions src/BuiltInTools/HotReloadClient/HotReloadStaticAssetUpdate.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@
#nullable enable

using System.Collections.Immutable;
using System.IO;
using System.Runtime.InteropServices;
using System.Threading;
using System.Threading.Tasks;

namespace Microsoft.DotNet.HotReload;

Expand All @@ -13,4 +17,18 @@ internal readonly struct HotReloadStaticAssetUpdate(string assemblyName, string
public string AssemblyName { get; } = assemblyName;
public ImmutableArray<byte> Content { get; } = content;
public bool IsApplicationProject { get; } = isApplicationProject;

public static async ValueTask<HotReloadStaticAssetUpdate> CreateAsync(StaticWebAsset asset, CancellationToken cancellationToken)
{
#if NET
var blob = await File.ReadAllBytesAsync(asset.FilePath, cancellationToken);
#else
var blob = File.ReadAllBytes(asset.FilePath);
#endif
return new HotReloadStaticAssetUpdate(
assemblyName: asset.AssemblyName,
relativePath: asset.RelativeUrl,
content: ImmutableCollectionsMarshal.AsImmutableArray(blob),
asset.IsApplicationProject);
}
}
116 changes: 116 additions & 0 deletions src/BuiltInTools/HotReloadClient/Utilities/PathExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.

#nullable enable

using System.Diagnostics;
using System.Runtime.CompilerServices;
using System.Text;

namespace System.IO;

internal static partial class PathExtensions
{
#if NET // binary compatibility
public static bool IsPathFullyQualified(string path)
=> Path.IsPathFullyQualified(path);

public static string Join(string? path1, string? path2)
=> Path.Join(path1, path2);
#else
extension(Path)
{
public static bool IsPathFullyQualified(string path)
=> Path.DirectorySeparatorChar == '\\'
? !IsPartiallyQualified(path.AsSpan())
: Path.IsPathRooted(path);
}

// Copied from https://github.com/dotnet/runtime/blob/a6c5ba30aab998555e36aec7c04311935e1797ab/src/libraries/Common/src/System/IO/PathInternal.Windows.cs#L250

/// <summary>
/// Returns true if the path specified is relative to the current drive or working directory.
/// Returns false if the path is fixed to a specific drive or UNC path. This method does no
/// validation of the path (URIs will be returned as relative as a result).
/// </summary>
/// <remarks>
/// Handles paths that use the alternate directory separator. It is a frequent mistake to
/// assume that rooted paths (Path.IsPathRooted) are not relative. This isn't the case.
/// "C:a" is drive relative- meaning that it will be resolved against the current directory
/// for C: (rooted, but relative). "C:\a" is rooted and not relative (the current directory
/// will not be used to modify the path).
/// </remarks>
private static bool IsPartiallyQualified(ReadOnlySpan<char> path)
{
if (path.Length < 2)
{
// It isn't fixed, it must be relative. There is no way to specify a fixed
// path with one character (or less).
return true;
}

if (IsDirectorySeparator(path[0]))
{
// There is no valid way to specify a relative path with two initial slashes or
// \? as ? isn't valid for drive relative paths and \??\ is equivalent to \\?\
return !(path[1] == '?' || IsDirectorySeparator(path[1]));
}

// The only way to specify a fixed path that doesn't begin with two slashes
// is the drive, colon, slash format- i.e. C:\
return !((path.Length >= 3)
&& (path[1] == Path.VolumeSeparatorChar)
&& IsDirectorySeparator(path[2])
// To match old behavior we'll check the drive character for validity as the path is technically
// not qualified if you don't have a valid drive. "=:\" is the "=" file's default data stream.
&& IsValidDriveChar(path[0]));
}

/// <summary>
/// True if the given character is a directory separator.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal static bool IsDirectorySeparator(char c)
{
return c == Path.DirectorySeparatorChar || c == Path.AltDirectorySeparatorChar;
}

/// <summary>
/// Returns true if the given character is a valid drive letter
/// </summary>
internal static bool IsValidDriveChar(char value)
{
return (uint)((value | 0x20) - 'a') <= (uint)('z' - 'a');
}

// Copied from https://github.com/dotnet/runtime/blob/main/src/libraries/System.Private.CoreLib/src/System/IO/Path.cs

private static readonly string s_directorySeparatorCharAsString = Path.DirectorySeparatorChar.ToString();

extension(Path)
{
public static string Join(string? path1, string? path2)
{
if (string.IsNullOrEmpty(path1))
return path2 ?? string.Empty;

if (string.IsNullOrEmpty(path2))
return path1;

return JoinInternal(path1, path2);
}
}

private static string JoinInternal(string first, string second)
{
Debug.Assert(first.Length > 0 && second.Length > 0, "should have dealt with empty paths");

bool hasSeparator = IsDirectorySeparator(first[^1]) || IsDirectorySeparator(second[0]);

return hasSeparator ?
string.Concat(first, second) :
string.Concat(first, s_directorySeparatorCharAsString, second);
}
#endif
}
63 changes: 63 additions & 0 deletions src/BuiltInTools/HotReloadClient/Web/StaticWebAsset.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

#nullable enable

using System;
using System.Collections.Immutable;
using System.IO;
using System.Threading.Tasks;

namespace Microsoft.DotNet.HotReload;

internal readonly struct StaticWebAsset(string filePath, string relativeUrl, string assemblyName, bool isApplicationProject)
{
public string FilePath => filePath;
public string RelativeUrl => relativeUrl;
public string AssemblyName => assemblyName;
public bool IsApplicationProject => isApplicationProject;

public const string WebRoot = "wwwroot";
public const string ManifestFileName = "staticwebassets.development.json";

public static bool IsScopedCssFile(string filePath)
=> filePath.EndsWith(".razor.css", StringComparison.Ordinal) ||
filePath.EndsWith(".cshtml.css", StringComparison.Ordinal);

public static string GetScopedCssRelativeUrl(string applicationProjectFilePath, string containingProjectFilePath)
=> WebRoot + "/" + GetScopedCssBundleFileName(applicationProjectFilePath, containingProjectFilePath);

public static string GetScopedCssBundleFileName(string applicationProjectFilePath, string containingProjectFilePath)
{
var sourceProjectName = Path.GetFileNameWithoutExtension(containingProjectFilePath);

return string.Equals(containingProjectFilePath, applicationProjectFilePath, StringComparison.OrdinalIgnoreCase)
? $"{sourceProjectName}.styles.css"
: $"{sourceProjectName}.bundle.scp.css";
}

public static bool IsScopedCssBundleFile(string filePath)
=> filePath.EndsWith(".bundle.scp.css", StringComparison.Ordinal) ||
filePath.EndsWith(".styles.css", StringComparison.Ordinal) ||
filePath.EndsWith(".bundle.scp.css.gz", StringComparison.Ordinal) ||
filePath.EndsWith(".styles.css.gz", StringComparison.Ordinal);

public static string? GetRelativeUrl(string applicationProjectFilePath, string containingProjectFilePath, string assetFilePath)
=> IsScopedCssFile(assetFilePath)
? GetScopedCssRelativeUrl(applicationProjectFilePath, containingProjectFilePath)
: GetAppRelativeUrlFomDiskPath(containingProjectFilePath, assetFilePath);

/// <summary>
/// For non scoped css, the only static files which apply are the ones under the wwwroot folder in that project. The relative path
/// will always start with wwwroot. eg: "wwwroot/css/styles.css"
/// </summary>
public static string? GetAppRelativeUrlFomDiskPath(string containingProjectFilePath, string assetFilePath)
{
var webRoot = "wwwroot" + Path.DirectorySeparatorChar;
var webRootDir = Path.Combine(Path.GetDirectoryName(containingProjectFilePath)!, webRoot);

return assetFilePath.StartsWith(webRootDir, StringComparison.OrdinalIgnoreCase)
? assetFilePath.Substring(webRootDir.Length - webRoot.Length).Replace("\\", "/")
: null;
}
}
13 changes: 13 additions & 0 deletions src/BuiltInTools/HotReloadClient/Web/StaticWebAssetPattern.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

#nullable enable

namespace Microsoft.DotNet.HotReload;

internal sealed partial class StaticWebAssetPattern(string directory, string pattern, string baseUrl)
{
public string Directory { get; } = directory;
public string Pattern { get; } = pattern;
public string BaseUrl { get; } = baseUrl;
}
Loading
Loading