-
Notifications
You must be signed in to change notification settings - Fork 25
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add public ISassCompiler interface to call dart-sass executable directly
- Loading branch information
Showing
10 changed files
with
399 additions
and
92 deletions.
There are no files selected for viewing
43 changes: 43 additions & 0 deletions
43
AspNetCore.SassCompiler.Tests/AspNetCore.SassCompiler.Tests.csproj
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,43 @@ | ||
<Project Sdk="Microsoft.NET.Sdk"> | ||
|
||
<PropertyGroup> | ||
<SassCompilerIncludeRuntime>true</SassCompilerIncludeRuntime> | ||
</PropertyGroup> | ||
|
||
<!-- Only needed because we're using a ProjectReference, this is done implicitly for PackageReference's --> | ||
<Import Project="..\AspNetCore.SassCompiler\build\AspNetCore.SassCompiler.props" /> | ||
|
||
<PropertyGroup> | ||
<TargetFramework>net6.0</TargetFramework> | ||
<Nullable>enable</Nullable> | ||
<ImplicitUsings>enable</ImplicitUsings> | ||
|
||
<IsPackable>false</IsPackable> | ||
</PropertyGroup> | ||
|
||
<PropertyGroup> | ||
<SassCompilerTasksAssembly Condition=" '$(Configuration)' != '' ">$(MSBuildThisFileDirectory)..\AspNetCore.SassCompiler.Tasks\bin\$(Configuration)\netstandard2.0\AspNetCore.SassCompiler.Tasks.dll</SassCompilerTasksAssembly> | ||
<SassCompilerTasksAssembly Condition=" '$(Configuration)' == '' ">$(MSBuildThisFileDirectory)..\AspNetCore.SassCompiler.Tasks\bin\Debug\netstandard2.0\AspNetCore.SassCompiler.Tasks.dll</SassCompilerTasksAssembly> | ||
</PropertyGroup> | ||
|
||
<ItemGroup> | ||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.9.0" /> | ||
<PackageReference Include="xunit" Version="2.7.0" /> | ||
<PackageReference Include="xunit.runner.visualstudio" Version="2.5.7"> | ||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> | ||
<PrivateAssets>all</PrivateAssets> | ||
</PackageReference> | ||
<PackageReference Include="coverlet.collector" Version="6.0.1"> | ||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> | ||
<PrivateAssets>all</PrivateAssets> | ||
</PackageReference> | ||
</ItemGroup> | ||
|
||
<ItemGroup> | ||
<ProjectReference Include="..\AspNetCore.SassCompiler\AspNetCore.SassCompiler.csproj" /> | ||
</ItemGroup> | ||
|
||
<!-- Only needed because we're using a ProjectReference, this is done implicitly for PackageReference's --> | ||
<Import Project="..\AspNetCore.SassCompiler\build\AspNetCore.SassCompiler.targets" /> | ||
|
||
</Project> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,117 @@ | ||
using System.Text; | ||
using Xunit; | ||
|
||
namespace AspNetCore.SassCompiler.Tests; | ||
|
||
public class SassCompilerTests | ||
{ | ||
[Fact] | ||
public async Task CompileAsync_WithoutStreams_Success() | ||
{ | ||
// Arrange | ||
var sassCompiler = new SassCompiler(); | ||
|
||
var tempDirectory = Path.Join(Path.GetTempPath(), Path.GetRandomFileName()); | ||
Directory.CreateDirectory(tempDirectory); | ||
|
||
try | ||
{ | ||
await File.WriteAllTextAsync(Path.Join(tempDirectory, "input"), "body { color: black; }"); | ||
|
||
// Act | ||
await sassCompiler.CompileAsync(new[] { Path.Join(tempDirectory, "input"), Path.Join(tempDirectory, "output"), "--no-source-map" }); | ||
var result = await File.ReadAllTextAsync(Path.Join(tempDirectory, "output")); | ||
|
||
// Assert | ||
Assert.Equal("body {\n color: black;\n}\n", result); | ||
} | ||
finally | ||
{ | ||
Directory.Delete(tempDirectory, true); | ||
} | ||
} | ||
|
||
[Theory] | ||
[InlineData("--watch")] | ||
[InlineData("--interactive")] | ||
public async Task CompileAsync_ThrowsWithInvalidArguments(string argument) | ||
{ | ||
// Arrange | ||
var sassCompiler = new SassCompiler(); | ||
|
||
var input = new MemoryStream(Encoding.UTF8.GetBytes("body { color: black; }")); | ||
|
||
// Act | ||
async Task Act() => await sassCompiler.CompileAsync(input, new[] { argument }); | ||
|
||
// Assert | ||
var exception = await Assert.ThrowsAsync<SassCompilerException>(Act); | ||
Assert.Equal($"The sass {argument} option is not supported.", exception.Message); | ||
Assert.Null(exception.ErrorOutput); | ||
} | ||
|
||
[Fact] | ||
public async Task CompileAsync_ThrowsWithInvalidInput() | ||
{ | ||
// Arrange | ||
var sassCompiler = new SassCompiler(); | ||
|
||
var input = new MemoryStream(Encoding.UTF8.GetBytes("body { color: black;")); | ||
|
||
// Act | ||
async Task Act() => await sassCompiler.CompileAsync(input, Array.Empty<string>()); | ||
|
||
// Assert | ||
var exception = await Assert.ThrowsAsync<SassCompilerException>(Act); | ||
Assert.Equal("Sass process exited with non-zero exit code: 65.", exception.Message); | ||
Assert.StartsWith("Error: expected \"}\".", exception.ErrorOutput); | ||
} | ||
|
||
[Fact] | ||
public async Task CompileAsync_ReturningOutputStream_Success() | ||
{ | ||
// Arrange | ||
var sassCompiler = new SassCompiler(); | ||
|
||
var input = new MemoryStream(Encoding.UTF8.GetBytes("body { color: black; }")); | ||
|
||
// Act | ||
var output = await sassCompiler.CompileAsync(input, Array.Empty<string>()); | ||
var result = await new StreamReader(output).ReadToEndAsync(); | ||
|
||
// Assert | ||
Assert.Equal("body {\n color: black;\n}\n", result); | ||
} | ||
|
||
[Fact] | ||
public async Task CompileAsync_WithInputAndOutputStream_Success() | ||
{ | ||
// Arrange | ||
var sassCompiler = new SassCompiler(); | ||
|
||
var input = new MemoryStream(Encoding.UTF8.GetBytes("body { color: black; }")); | ||
var output = new MemoryStream(); | ||
|
||
// Act | ||
await sassCompiler.CompileAsync(input, output, Array.Empty<string>()); | ||
var result = Encoding.UTF8.GetString(output.ToArray()); | ||
|
||
// Assert | ||
Assert.Equal("body {\n color: black;\n}\n", result); | ||
} | ||
|
||
[Fact] | ||
public async Task CompileToStringAsync_Success() | ||
{ | ||
// Arrange | ||
var sassCompiler = new SassCompiler(); | ||
|
||
var input = new MemoryStream(Encoding.UTF8.GetBytes("body { color: black; }")); | ||
|
||
// Act | ||
var result = await sassCompiler.CompileToStringAsync(input, Array.Empty<string>()); | ||
|
||
// Assert | ||
Assert.Equal("body {\n color: black;\n}\n", result); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,183 @@ | ||
using System; | ||
using System.Diagnostics; | ||
using System.IO; | ||
using System.Linq; | ||
using System.Reflection; | ||
using System.Runtime.InteropServices; | ||
using System.Text; | ||
using System.Threading.Tasks; | ||
|
||
namespace AspNetCore.SassCompiler; | ||
|
||
public interface ISassCompiler | ||
{ | ||
Task CompileAsync(string[] args); | ||
|
||
Task<Stream> CompileAsync(Stream input, string[] args); | ||
|
||
Task CompileAsync(Stream input, Stream output, string[] args); | ||
|
||
Task<string> CompileToStringAsync(Stream input, string[] args); | ||
} | ||
|
||
internal class SassCompiler : ISassCompiler | ||
{ | ||
public async Task CompileAsync(string[] args) | ||
{ | ||
var escapedArgs = args.Length > 0 ? '"' + string.Join("\" \"", args) + '"' : ""; | ||
ValidateArgs(escapedArgs); | ||
var process = CreateSassProcess(escapedArgs); | ||
if (process == null) | ||
throw new SassCompilerException("Sass executable not found"); | ||
|
||
var errorOutput = new MemoryStream(); | ||
|
||
process.Start(); | ||
ChildProcessTracker.AddProcess(process); | ||
await process.StandardOutput.BaseStream.CopyToAsync(Stream.Null); | ||
await process.StandardError.BaseStream.CopyToAsync(errorOutput); | ||
await process.WaitForExitAsync(); | ||
|
||
if (process.ExitCode != 0) | ||
{ | ||
var errorOutputText = Encoding.UTF8.GetString(errorOutput.ToArray()); | ||
throw new SassCompilerException($"Sass process exited with non-zero exit code: {process.ExitCode}.", errorOutputText); | ||
} | ||
|
||
process.Dispose(); | ||
} | ||
|
||
public async Task<Stream> CompileAsync(Stream input, string[] args) | ||
{ | ||
var output = new MemoryStream(); | ||
await CompileAsync(input, output, args); | ||
|
||
output.Position = 0; | ||
return output; | ||
} | ||
|
||
public async Task CompileAsync(Stream input, Stream output, string[] args) | ||
{ | ||
var escapedArgs = args.Length > 0 ? '"' + string.Join("\" \"", args) + '"' : ""; | ||
ValidateArgs(escapedArgs); | ||
var process = CreateSassProcess(escapedArgs + " --stdin"); | ||
if (process == null) | ||
throw new SassCompilerException("Sass executable not found"); | ||
|
||
process.StartInfo.RedirectStandardInput = true; | ||
|
||
var errorOutput = new MemoryStream(); | ||
|
||
process.Start(); | ||
ChildProcessTracker.AddProcess(process); | ||
await input.CopyToAsync(process.StandardInput.BaseStream); | ||
await process.StandardInput.DisposeAsync(); | ||
await process.StandardOutput.BaseStream.CopyToAsync(output); | ||
await process.StandardError.BaseStream.CopyToAsync(errorOutput); | ||
await process.WaitForExitAsync(); | ||
|
||
if (process.ExitCode != 0) | ||
{ | ||
var errorOutputText = Encoding.UTF8.GetString(errorOutput.ToArray()); | ||
throw new SassCompilerException($"Sass process exited with non-zero exit code: {process.ExitCode}.", errorOutputText); | ||
} | ||
|
||
process.Dispose(); | ||
} | ||
|
||
public async Task<string> CompileToStringAsync(Stream input, string[] args) | ||
{ | ||
using var output = new MemoryStream(); | ||
await CompileAsync(input, output, args); | ||
return Encoding.UTF8.GetString(output.ToArray()); | ||
} | ||
|
||
private static void ValidateArgs(string args) | ||
{ | ||
if (args.Contains("--watch")) | ||
throw new SassCompilerException("The sass --watch option is not supported."); | ||
|
||
if (args.Contains("--interactive")) | ||
throw new SassCompilerException("The sass --interactive option is not supported."); | ||
} | ||
|
||
internal static Process CreateSassProcess(string arguments) | ||
{ | ||
var command = GetSassCommand(); | ||
if (command.Filename == null) | ||
return null; | ||
|
||
if (!string.IsNullOrEmpty(command.Snapshot)) | ||
arguments = $"{command.Snapshot} {arguments}"; | ||
|
||
var process = new Process(); | ||
process.StartInfo.FileName = command.Filename; | ||
process.StartInfo.Arguments = arguments; | ||
process.StartInfo.WindowStyle = ProcessWindowStyle.Hidden; | ||
process.StartInfo.CreateNoWindow = true; | ||
process.StartInfo.UseShellExecute = false; | ||
process.StartInfo.RedirectStandardOutput = true; | ||
process.StartInfo.RedirectStandardError = true; | ||
process.StartInfo.WorkingDirectory = Directory.GetCurrentDirectory(); | ||
|
||
return process; | ||
} | ||
|
||
internal static (string Filename, string Snapshot) GetSassCommand() | ||
{ | ||
var attribute = Assembly.GetEntryAssembly()?.GetCustomAttributes<SassCompilerAttribute>().FirstOrDefault(); | ||
|
||
if (attribute != null) | ||
return (attribute.SassBinary, string.IsNullOrWhiteSpace(attribute.SassSnapshot) ? "" : $"\"{attribute.SassSnapshot}\""); | ||
|
||
var assemblyLocation = typeof(SassCompilerHostedService).Assembly.Location; | ||
|
||
var (exePath, snapshotPath) = GetExeAndSnapshotPath(); | ||
if (exePath == null) | ||
return (null, null); | ||
|
||
var directory = Path.GetDirectoryName(assemblyLocation); | ||
while (!string.IsNullOrEmpty(directory) && directory != "/") | ||
{ | ||
if (File.Exists(Path.Join(directory, exePath))) | ||
return (Path.Join(directory, exePath), snapshotPath == null ? null : "\"" + Path.Join(directory, snapshotPath) + "\""); | ||
|
||
directory = Path.GetDirectoryName(directory); | ||
} | ||
|
||
return (null, null); | ||
} | ||
|
||
internal static (string ExePath, string SnapshotPath) GetExeAndSnapshotPath() | ||
{ | ||
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) | ||
{ | ||
return RuntimeInformation.OSArchitecture switch | ||
{ | ||
Architecture.X64 => ("runtimes\\win-x64\\src\\dart.exe", "runtimes\\win-x64\\src\\sass.snapshot"), | ||
Architecture.Arm64 => ("runtimes\\win-x64\\src\\dart.exe", "runtimes\\win-x64\\src\\sass.snapshot"), | ||
_ => (null, null), | ||
}; | ||
} | ||
else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) | ||
{ | ||
return RuntimeInformation.OSArchitecture switch | ||
{ | ||
Architecture.X64 => ("runtimes/linux-x64/src/dart", "runtimes/linux-x64/src/sass.snapshot"), | ||
Architecture.Arm64 => ("runtimes/linux-arm64/src/dart", "runtimes/linux-x64/src/sass.snapshot"), | ||
_ => (null, null), | ||
}; | ||
} | ||
else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) | ||
{ | ||
return RuntimeInformation.OSArchitecture switch | ||
{ | ||
Architecture.X64 => ("runtimes/osx-x64/src/dart", "runtimes/osx-x64/src/sass.snapshot"), | ||
Architecture.Arm64 => ("runtimes/osx-arm64/src/dart", "runtimes/osx-arm64/src/sass.snapshot"), | ||
_ => (null, null), | ||
}; | ||
} | ||
|
||
return (null, null); | ||
} | ||
} |
Oops, something went wrong.