File: RazorProjectBuilder.cs
Web Access
Project: src\src\Razor\src\Razor\test\Microsoft.AspNetCore.Razor.Test.Common.Cohosting\Microsoft.AspNetCore.Razor.Test.Common.Cohosting.csproj (Microsoft.AspNetCore.Razor.Test.Common.Cohosting)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
using Microsoft.AspNetCore.Razor;
using Microsoft.AspNetCore.Razor.Language;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.Razor.ProjectSystem;
using Microsoft.CodeAnalysis.Text;
using Microsoft.NET.Sdk.Razor.SourceGenerators;
using Roslyn.Test.Utilities;
 
namespace Microsoft.VisualStudio.Razor.LanguageClient.Cohost;
 
internal class RazorProjectBuilder(ProjectId? id = null)
{
    public ProjectId Id { get; } = id ?? ProjectId.CreateNewId();
 
    public string? ProjectName { get; set; }
 
    public string? ProjectFilePath
    {
        get;
        set
        {
            ProjectName ??= Path.GetFileNameWithoutExtension(value);
 
            field = value;
        }
    }
 
    public string? RootNamespace { get; set; } = "ASP";
 
    public bool ReferenceRazorSourceGenerator { get; set; } = true;
    public bool GenerateGlobalConfigFile { get; set; } = true;
    public bool GenerateMSBuildProjectDirectory { get; set; } = true;
    public bool GenerateAdditionalDocumentMetadata { get; set; } = true;
 
    public RazorLanguageVersion RazorLanguageVersion { get; set; } = RazorLanguageVersion.Preview;
 
    private readonly List<PortableExecutableReference> _references = [];
    private readonly List<(DocumentId id, string name, SourceText text, string filePath)> _documents = [];
    private readonly List<(DocumentId id, string name, SourceText text, string filePath)> _additionalDocuments = [];
    private readonly List<(string name, SourceText text, string filePath)> _analyzerConfigDocuments = [];
 
    internal void AddReferences(IEnumerable<PortableExecutableReference> enumerable)
    {
        _references.AddRange(enumerable);
    }
 
    internal DocumentId AddDocument(string filePath, SourceText text)
    {
        var name = Path.GetFileName(filePath);
        var id = DocumentId.CreateNewId(Id, name);
        _documents.Add((id, name, text, filePath));
        return id;
    }
 
    internal DocumentId AddAdditionalDocument(string filePath, SourceText text)
    {
        var name = Path.GetFileName(filePath);
        var id = DocumentId.CreateNewId(Id, name);
        AddAdditionalDocument(id, filePath, text);
        return id;
    }
 
    internal void AddAdditionalDocument(DocumentId id, string filePath, SourceText text)
    {
        var name = Path.GetFileName(filePath);
        _additionalDocuments.Add((id, name, text, filePath));
    }
 
    internal void AddAnalyzerConfigDocument(string filePath, SourceText text)
    {
        var name = Path.GetFileName(filePath);
        _analyzerConfigDocuments.Add((name, text, filePath));
    }
 
    public Solution Build(Solution solution)
    {
        var sgAssembly = typeof(RazorSourceGenerator).Assembly;
 
        var projectInfo = ProjectInfo
            .Create(
                Id,
                VersionStamp.Create(),
                name: ProjectName ?? "",
                assemblyName: ProjectName ?? "",
                LanguageNames.CSharp,
                ProjectFilePath,
                parseOptions: CSharpParseOptions.Default.WithFeatures([new("use-roslyn-tokenizer", "true")]),
                compilationOptions: new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary))
            .WithMetadataReferences(_references)
            .WithDefaultNamespace(RootNamespace);
 
        if (ReferenceRazorSourceGenerator)
        {
            projectInfo = projectInfo.WithAnalyzerReferences([new AnalyzerFileReference(sgAssembly.Location, TestAnalyzerAssemblyLoader.LoadFromFile)]);
        }
 
        solution = solution.AddProject(projectInfo);
 
        foreach (var document in _documents)
        {
            solution = solution.AddDocument(document.id, document.name, document.text, filePath: document.filePath);
        }
 
        foreach (var additionalDocument in _additionalDocuments)
        {
            solution = solution.AddAdditionalDocument(additionalDocument.id, additionalDocument.name, additionalDocument.text, filePath: additionalDocument.filePath);
        }
 
        if (GenerateGlobalConfigFile)
        {
            var globalConfigContent = new StringBuilder();
 
            globalConfigContent.AppendLine($"""
                is_global = true
 
                build_property.RazorLangVersion = {RazorLanguageVersion}
                build_property.RazorConfiguration = {FallbackRazorConfiguration.Latest.ConfigurationName}
                build_property.RootNamespace = {RootNamespace}
 
                # This might suprise you, but by suppressing the source generator here, we're mirroring what happens in the Razor SDK
                build_property.SuppressRazorSourceGenerator = true
                """);
 
            if (GenerateMSBuildProjectDirectory)
            {
                globalConfigContent.AppendLine($"""
                    build_property.MSBuildProjectDirectory = {Path.GetDirectoryName(ProjectFilePath).AssumeNotNull()}
                    """);
            }
 
            var projectBasePath = Path.GetDirectoryName(ProjectFilePath).AssumeNotNull();
 
            if (GenerateAdditionalDocumentMetadata)
            {
                foreach (var additionalDocument in _additionalDocuments)
                {
                    if (additionalDocument.filePath is not null &&
                        additionalDocument.filePath.StartsWith(projectBasePath))
                    {
                        var relativePath = additionalDocument.filePath[(projectBasePath.Length + 1)..];
                        globalConfigContent.AppendLine($"""

                            [{additionalDocument.filePath.AssumeNotNull().Replace('\\', '/')}]
                            build_metadata.AdditionalFiles.TargetPath = {Convert.ToBase64String(Encoding.UTF8.GetBytes(relativePath))}
                            """);
                    }
                }
            }
 
            solution = solution.AddAnalyzerConfigDocument(
                DocumentId.CreateNewId(Id),
                name: ".globalconfig",
                text: SourceText.From(globalConfigContent.ToString()),
                filePath: Path.Combine(projectBasePath, ".globalconfig"));
        }
 
        foreach (var analyzerConfigDocument in _analyzerConfigDocuments)
        {
            solution = solution.AddAnalyzerConfigDocument(
                DocumentId.CreateNewId(Id),
                name: analyzerConfigDocument.name,
                text: analyzerConfigDocument.text,
                filePath: analyzerConfigDocument.filePath);
        }
 
        return solution;
    }
}