diff --git a/src/Bicep.Core.UnitTests/Assertions/CachedModuleExtensions.cs b/src/Bicep.Core.UnitTests/Assertions/CachedModuleExtensions.cs index 0170e1bf5fc..c323d747d16 100644 --- a/src/Bicep.Core.UnitTests/Assertions/CachedModuleExtensions.cs +++ b/src/Bicep.Core.UnitTests/Assertions/CachedModuleExtensions.cs @@ -1,6 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +using System; using System.Collections.Generic; using System.Collections.Immutable; using System.IO; @@ -10,6 +11,7 @@ using Bicep.Core.Modules; using Bicep.Core.Registry; using Bicep.Core.Registry.Oci; +using Bicep.Core.SourceCode; using Bicep.Core.UnitTests.Registry; using FluentAssertions; using FluentAssertions.Primitives; @@ -31,19 +33,21 @@ public CachedModuleAssertions(CachedModule CachedModule) : base(CachedModule) protected override string Identifier => $"CachedModule at {Subject.ModuleCacheFolder}"; - public AndConstraint HaveSource(bool f = true) + public AndConstraint HaveSource(bool shouldHaveSource = true) { Subject.Should().BeValid(); - if (f) + if (shouldHaveSource) { Subject.HasSourceLayer.Should().BeTrue(); - Subject.TryGetSource().Should().NotBeNull(); + Subject.TryGetSource().IsSuccess().Should().BeTrue(); } else { Subject.HasSourceLayer.Should().BeFalse(); - Subject.TryGetSource().Should().BeNull(); + Subject.TryGetSource().IsSuccess().Should().BeFalse(); + Subject.TryGetSource().IsSuccess(out _, out var ex); + ex.Should().BeOfType(); } return new(this); diff --git a/src/Bicep.Core.UnitTests/Assertions/MockRegistryAssertions.cs b/src/Bicep.Core.UnitTests/Assertions/MockRegistryAssertions.cs index 7afc22e14b0..4fb992b5296 100644 --- a/src/Bicep.Core.UnitTests/Assertions/MockRegistryAssertions.cs +++ b/src/Bicep.Core.UnitTests/Assertions/MockRegistryAssertions.cs @@ -120,6 +120,7 @@ private AndConstraint HaveModuleCore(string tag, Stream expectedSourceContent!.Position = 0; byte[] expectedSourceBytes = new byte[expectedSourceContent.Length]; expectedSourceContent.Read(expectedSourceBytes, 0, expectedSourceBytes.Length); + expectedSourceContent.Position = 0; actualSourcesBytes.Should().Equal(expectedSourceBytes, "module sources should match"); } diff --git a/src/Bicep.Core.UnitTests/Registry/CachedModules.cs b/src/Bicep.Core.UnitTests/Registry/CachedModules.cs index 233b5017b66..5cc9515c319 100644 --- a/src/Bicep.Core.UnitTests/Registry/CachedModules.cs +++ b/src/Bicep.Core.UnitTests/Registry/CachedModules.cs @@ -8,6 +8,7 @@ using System.Text.Json; using System.Text.Json.Nodes; using System.Text.Json.Serialization; +using Bicep.Core.Diagnostics; using Bicep.Core.Registry; using Bicep.Core.SourceCode; using FluentAssertions; @@ -85,14 +86,14 @@ public string[] LayerMediaTypes public bool HasSourceLayer => LayerMediaTypes.Contains("application/vnd.ms.bicep.module.source.v1.tar+gzip"); - public SourceArchive? TryGetSource() + public ResultWithException TryGetSource() { var sourceArchivePath = Path.Combine(ModuleCacheFolder, $"source.tar.gz"); if (File.Exists(sourceArchivePath)) { - return SourceArchive.FromStream(File.OpenRead(sourceArchivePath)); + return SourceArchive.UnpackFromStream(File.OpenRead(sourceArchivePath)); } - return null; + return new(new SourceNotAvailableException()); } } diff --git a/src/Bicep.Core.UnitTests/Registry/OciModuleRegistryTests.cs b/src/Bicep.Core.UnitTests/Registry/OciModuleRegistryTests.cs index 3a53f184911..ede7e9d8891 100644 --- a/src/Bicep.Core.UnitTests/Registry/OciModuleRegistryTests.cs +++ b/src/Bicep.Core.UnitTests/Registry/OciModuleRegistryTests.cs @@ -678,16 +678,16 @@ public async Task RestoreModuleWithSource_ShouldRestoreSourceToDisk(bool publish await RestoreModule(ociRegistry, moduleReference); ociRegistry.Should().HaveValidCachedModules(withSource: publishSource); - var actualSource = ociRegistry.TryGetSource(moduleReference); + var actualSourceResult = ociRegistry.TryGetSource(moduleReference); if (sourceStream is { }) { - actualSource.Should().NotBeNull(); - actualSource.Should().BeEquivalentTo(SourceArchive.FromStream(sourceStream)); + actualSourceResult.TryUnwrap().Should().NotBeNull(); + actualSourceResult.Unwrap().Should().BeEquivalentTo(SourceArchive.UnpackFromStream(sourceStream).Unwrap()); } else { - actualSource.Should().BeNull(); + actualSourceResult.IsSuccess().Should().BeFalse(); } } diff --git a/src/Bicep.Core.UnitTests/Registry/SourceArchiveTests.cs b/src/Bicep.Core.UnitTests/Registry/SourceArchiveTests.cs index 2b358db4fb2..73cf15fac50 100644 --- a/src/Bicep.Core.UnitTests/Registry/SourceArchiveTests.cs +++ b/src/Bicep.Core.UnitTests/Registry/SourceArchiveTests.cs @@ -147,8 +147,9 @@ public void CanPackAndUnpackSourceFiles() using var stream = SourceArchive.PackSourcesIntoStream(mainBicep.FileUri, mainBicep, mainJson, standaloneJson, templateSpecMainJson, localModuleJson); stream.Length.Should().BeGreaterThan(0); - SourceArchive sourceArchive = SourceArchive.FromStream(stream); - sourceArchive.EntrypointRelativePath.Should().Be("main.bicep"); + SourceArchive? sourceArchive = SourceArchive.UnpackFromStream(stream).TryUnwrap(); + sourceArchive.Should().NotBeNull(); + sourceArchive!.EntrypointRelativePath.Should().Be("main.bicep"); var archivedFiles = sourceArchive.SourceFiles.ToArray(); @@ -210,9 +211,10 @@ public void HandlesPathsCorrectly( using var stream = SourceArchive.PackSourcesIntoStream(mainBicep.FileUri, mainBicep, testFile); - SourceArchive sourceArchive = SourceArchive.FromStream(stream); + SourceArchive? sourceArchive = SourceArchive.UnpackFromStream(stream).TryUnwrap(); - sourceArchive.EntrypointRelativePath.Should().Be("my main.bicep"); + sourceArchive.Should().NotBeNull(); + sourceArchive!.EntrypointRelativePath.Should().Be("my main.bicep"); var archivedTestFile = sourceArchive.SourceFiles.Single(f => f.Path != "my main.bicep"); archivedTestFile.Path.Should().Be(expecteArchivedUri); @@ -246,7 +248,7 @@ public void GetSourceFiles_ForwardsCompat_ShouldIgnoreUnrecognizedPropertiesInMe ) ); - var sut = SourceArchive.FromStream(zip); + var sut = SourceArchive.UnpackFromStream(zip).Unwrap(); var file = sut.SourceFiles.Single(); file.Kind.Should().Be("bicep"); @@ -280,7 +282,7 @@ public void GetSourceFiles_BackwardsCompat_ShouldBeAbleToReadOldFormats() ) ); - var sut = SourceArchive.FromStream(zip); + var sut = SourceArchive.UnpackFromStream(zip).Unwrap(); var file = sut.SourceFiles.Single(); file.Kind.Should().Be("bicep"); @@ -318,7 +320,7 @@ public void GetSourceFiles_ForwardsCompat_ShouldIgnoreFileEntriesNotInMetadata() ) ); - var sut = SourceArchive.FromStream(zip); + var sut = SourceArchive.UnpackFromStream(zip).Unwrap(); var file = sut.SourceFiles.Single(); file.Kind.Should().Be("bicep"); @@ -326,6 +328,70 @@ public void GetSourceFiles_ForwardsCompat_ShouldIgnoreFileEntriesNotInMetadata() file.Path.Should().Contain("main.bicep"); } + [TestMethod] + public void GetSourceFiles_ShouldGiveError_ForIncompatibleOlderVersion() + { + var zip = CreateGzippedTarredFileStream( + ( + "__metadata.json", + @" + { + ""entryPoint"": ""file:///main.bicep"", + ""metadataVersion"": , + ""bicepVersion"": ""0.whatever.0"", + ""sourceFiles"": [ + { + ""path"": ""file:///main.bicep"", + ""archivePath"": ""main.bicep"", + ""kind"": ""bicep"" + } + ] + }".Replace("", (SourceArchive.CurrentMetadataVersion - 1).ToString()) + ), + ( + "main.bicep", + @"bicep contents" + ) + ); + + SourceArchive.UnpackFromStream(zip).IsSuccess(out var sourceArchive, out var ex); + sourceArchive.Should().BeNull(); + ex.Should().NotBeNull(); + ex!.Message.Should().StartWith("This source code was published with an older, incompatible version of Bicep (0.whatever.0). You are using version "); + } + + [TestMethod] + public void GetSourceFiles_ShouldGiveError_ForIncompatibleNewerVersion() + { + var zip = CreateGzippedTarredFileStream( + ( + "__metadata.json", + @" + { + ""entryPoint"": ""file:///main.bicep"", + ""metadataVersion"": , + ""bicepVersion"": ""0.whatever.0"", + ""sourceFiles"": [ + { + ""path"": ""file:///main.bicep"", + ""archivePath"": ""main.bicep"", + ""kind"": ""bicep"" + } + ] + }".Replace("", (SourceArchive.CurrentMetadataVersion + 1).ToString()) + ), + ( + "main.bicep", + @"bicep contents" + ) + ); + + var success = SourceArchive.UnpackFromStream(zip).IsSuccess(out _, out var ex); + success.Should().BeFalse(); + ex.Should().NotBeNull(); + ex!.Message.Should().StartWith("This source code was published with a newer, incompatible version of Bicep (0.whatever.0). You are using version "); + } + private Stream CreateGzippedTarredFileStream(params (string relativePath, string contents)[] files) { var outFolder = FileHelper.GetUniqueTestOutputPath(TestContext!); diff --git a/src/Bicep.Core/Diagnostics/ResultWithException.cs b/src/Bicep.Core/Diagnostics/ResultWithException.cs new file mode 100644 index 00000000000..2dd826066ac --- /dev/null +++ b/src/Bicep.Core/Diagnostics/ResultWithException.cs @@ -0,0 +1,15 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using Bicep.Core.Utils; + +namespace Bicep.Core.Diagnostics; + +public class ResultWithException : Result + where TSuccess : class +{ + public ResultWithException(TSuccess success) : base(success) { } + + public ResultWithException(Exception exception) : base(exception) { } +} diff --git a/src/Bicep.Core/Registry/ArtifactRegistry.cs b/src/Bicep.Core/Registry/ArtifactRegistry.cs index 6b6f9aa7047..3068b99ae2e 100644 --- a/src/Bicep.Core/Registry/ArtifactRegistry.cs +++ b/src/Bicep.Core/Registry/ArtifactRegistry.cs @@ -8,6 +8,7 @@ using System.Threading.Tasks; using Bicep.Core.Diagnostics; using Bicep.Core.SourceCode; +using Bicep.Core.Utils; namespace Bicep.Core.Registry { @@ -37,7 +38,7 @@ public abstract class ArtifactRegistry : IArtifactRegistry where T : Artifact public abstract Task TryGetDescription(T reference); - public abstract SourceArchive? TryGetSource(T reference); + public abstract ResultWithException TryGetSource(T reference); public bool IsArtifactRestoreRequired(ArtifactReference reference) => this.IsArtifactRestoreRequired(ConvertReference(reference)); @@ -62,7 +63,7 @@ public ResultWithDiagnostic TryGetLocalArtifactEntryPointUri(ArtifactRefere public async Task TryGetDescription(ArtifactReference reference) => await this.TryGetDescription(ConvertReference(reference)); - public SourceArchive? TryGetSource(ArtifactReference reference) => this.TryGetSource(ConvertReference(reference)); + public ResultWithException TryGetSource(ArtifactReference reference) => this.TryGetSource(ConvertReference(reference)); public abstract RegistryCapabilities GetCapabilities(T reference); diff --git a/src/Bicep.Core/Registry/IArtifactDispatcher.cs b/src/Bicep.Core/Registry/IArtifactDispatcher.cs index 3154169612b..e1baf13234f 100644 --- a/src/Bicep.Core/Registry/IArtifactDispatcher.cs +++ b/src/Bicep.Core/Registry/IArtifactDispatcher.cs @@ -8,6 +8,7 @@ using System.Threading.Tasks; using Bicep.Core.Diagnostics; using Bicep.Core.SourceCode; +using Bicep.Core.Utils; namespace Bicep.Core.Registry { @@ -32,6 +33,6 @@ public interface IModuleDispatcher : IArtifactReferenceFactory void PruneRestoreStatuses(); // Retrieves the sources that have been restored along with the module into the cache (if available) - SourceArchive? TryGetModuleSources(ArtifactReference reference); + ResultWithException TryGetModuleSources(ArtifactReference reference); } } diff --git a/src/Bicep.Core/Registry/IArtifactRegistry.cs b/src/Bicep.Core/Registry/IArtifactRegistry.cs index fa72c3e15ab..e60303cb486 100644 --- a/src/Bicep.Core/Registry/IArtifactRegistry.cs +++ b/src/Bicep.Core/Registry/IArtifactRegistry.cs @@ -10,6 +10,7 @@ using Bicep.Core.Modules; using Bicep.Core.Registry.Providers; using Bicep.Core.SourceCode; +using Bicep.Core.Utils; namespace Bicep.Core.Registry { @@ -102,6 +103,6 @@ public interface IArtifactRegistry /// /// The module reference /// A source archive - SourceArchive? TryGetSource(ArtifactReference reference); + ResultWithException TryGetSource(ArtifactReference reference); } } diff --git a/src/Bicep.Core/Registry/LocalModuleRegistry.cs b/src/Bicep.Core/Registry/LocalModuleRegistry.cs index 0a031c86d0a..b5e738a60d2 100644 --- a/src/Bicep.Core/Registry/LocalModuleRegistry.cs +++ b/src/Bicep.Core/Registry/LocalModuleRegistry.cs @@ -11,6 +11,7 @@ using Bicep.Core.Modules; using Bicep.Core.Semantics; using Bicep.Core.SourceCode; +using Bicep.Core.Utils; namespace Bicep.Core.Registry { @@ -106,9 +107,9 @@ public override Task PublishProvider(LocalModuleReference reference, Stream type return null; } - public override SourceArchive? TryGetSource(LocalModuleReference reference) + public override ResultWithException TryGetSource(LocalModuleReference reference) { - return null; + return new(new SourceNotAvailableException()); } } } diff --git a/src/Bicep.Core/Registry/ModuleDispatcher.cs b/src/Bicep.Core/Registry/ModuleDispatcher.cs index 05a98813de0..875aea8cdc9 100644 --- a/src/Bicep.Core/Registry/ModuleDispatcher.cs +++ b/src/Bicep.Core/Registry/ModuleDispatcher.cs @@ -16,6 +16,7 @@ using Bicep.Core.Semantics; using Bicep.Core.SourceCode; using Bicep.Core.Syntax; +using Bicep.Core.Utils; namespace Bicep.Core.Registry { @@ -228,7 +229,7 @@ public void PruneRestoreStatuses() private IArtifactRegistry GetRegistry(ArtifactReference reference) => Registries(reference.ParentModuleUri).TryGetValue(reference.Scheme, out var registry) ? registry : throw new InvalidOperationException($"Unexpected artifactDeclaration reference scheme '{reference.Scheme}'."); - public SourceArchive? TryGetModuleSources(ArtifactReference reference) + public ResultWithException TryGetModuleSources(ArtifactReference reference) { var registry = this.GetRegistry(reference); return registry.TryGetSource(reference); diff --git a/src/Bicep.Core/Registry/OciArtifactRegistry.cs b/src/Bicep.Core/Registry/OciArtifactRegistry.cs index 8924a03b015..91d21e3a6c3 100644 --- a/src/Bicep.Core/Registry/OciArtifactRegistry.cs +++ b/src/Bicep.Core/Registry/OciArtifactRegistry.cs @@ -21,6 +21,7 @@ using Bicep.Core.Semantics; using Bicep.Core.SourceCode; using Bicep.Core.Tracing; +using Bicep.Core.Utils; using Newtonsoft.Json; namespace Bicep.Core.Registry @@ -506,15 +507,16 @@ private string GetArtifactFilePath(OciArtifactReference reference, ArtifactFileT return fileSystem.Path.Combine(this.GetArtifactDirectoryPath(reference), fileName); } - public override SourceArchive? TryGetSource(OciArtifactReference reference) + public override ResultWithException TryGetSource(OciArtifactReference reference) { var zipPath = GetArtifactFilePath(reference, ArtifactFileType.Source); if (File.Exists(zipPath)) { - return SourceArchive.FromStream(File.OpenRead(zipPath)); + return SourceArchive.UnpackFromStream(File.OpenRead(zipPath)); } - return null; + // No sources available (presumably they weren't published) + return new(new SourceNotAvailableException()); } private enum ArtifactFileType diff --git a/src/Bicep.Core/Registry/TemplateSpecModuleRegistry.cs b/src/Bicep.Core/Registry/TemplateSpecModuleRegistry.cs index 5ec3f2394f0..02c29a26eb3 100644 --- a/src/Bicep.Core/Registry/TemplateSpecModuleRegistry.cs +++ b/src/Bicep.Core/Registry/TemplateSpecModuleRegistry.cs @@ -14,6 +14,7 @@ using Bicep.Core.Semantics; using Bicep.Core.SourceCode; using Bicep.Core.Tracing; +using Bicep.Core.Utils; namespace Bicep.Core.Registry { @@ -153,9 +154,9 @@ protected override string GetArtifactDirectoryPath(TemplateSpecModuleReference r return Task.FromResult(null); } - public override SourceArchive? TryGetSource(TemplateSpecModuleReference reference) + public override ResultWithException TryGetSource(TemplateSpecModuleReference reference) { - return null; + return new(new SourceNotAvailableException()); } } } diff --git a/src/Bicep.Core/SourceCode/SourceArchive.cs b/src/Bicep.Core/SourceCode/SourceArchive.cs index c9bf540111e..9ac99cfa755 100644 --- a/src/Bicep.Core/SourceCode/SourceArchive.cs +++ b/src/Bicep.Core/SourceCode/SourceArchive.cs @@ -14,31 +14,53 @@ using System.Text.Json.Serialization; using System.Text.Json.Serialization.Metadata; using System.Threading.Tasks; +using Bicep.Core.Diagnostics; using Bicep.Core.Exceptions; using Bicep.Core.Navigation; using Bicep.Core.Registry.Oci; using Bicep.Core.Semantics; +using Bicep.Core.Utils; using Bicep.Core.Workspaces; using static Bicep.Core.SourceCode.SourceArchive; namespace Bicep.Core.SourceCode { + public class SourceNotAvailableException : Exception + { + public SourceNotAvailableException() + : base("(Experimental) No source code is available for this module") + { } + } + // Contains the individual source code files for a Bicep file and all of its dependencies. - public partial class SourceArchive + public partial class SourceArchive // Partial required for serialization { + // Attributes of this archive instance + + private ArchiveMetadata InstanceMetadata { get; init; } + public ImmutableArray SourceFiles { get; init; } - public string EntrypointRelativePath { get; init; } + + public string EntrypointRelativePath => InstanceMetadata.EntryPoint; + + // The version of Bicep which created this deserialized archive instance. + public string? BicepVersion => InstanceMetadata.BicepVersion; + public string FriendlyBicepVersion => InstanceMetadata.BicepVersion ?? "unknown"; + + // The version of the metadata file format used by this archive instance. + public int MetadataVersion => InstanceMetadata.MetadataVersion; + + // Constants public const string SourceKind_Bicep = "bicep"; public const string SourceKind_ArmTemplate = "armTemplate"; public const string SourceKind_TemplateSpec = "templateSpec"; - private const string MetadataArchivedFileName = "__metadata.json"; - - private bool isDisposed = false;//asfdg remove + private const string MetadataFileName = "__metadata.json"; - // WARNING: Only change this value if there is a breaking change such that old versions of Bicep should fail on reading this source archive - private const int CurrentMetadataVersion = 0; // TODO: Change to 1 when remove experimental flag + // NOTE: Only change this value if there is a breaking change such that old versions of Bicep should fail on reading new source archives + public const int CurrentMetadataVersion = 0; + private static readonly string CurrentBicepVersion = ThisAssembly.AssemblyVersion; public partial record SourceFileInfo( string Path, // the location, relative to the main.bicep file's folder, for the file that will be shown to the end user (required in all Bicep versions) @@ -47,15 +69,16 @@ public partial record SourceFileInfo( string Contents ); - [JsonSerializable(typeof(MetadataEntry))] + [JsonSerializable(typeof(ArchiveMetadata))] [JsonSourceGenerationOptions(PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase)] private partial class MetadataSerializationContext : JsonSerializerContext { } - [JsonSerializable(typeof(MetadataEntry))] - private record MetadataEntry( + [JsonSerializable(typeof(ArchiveMetadata))] + private record ArchiveMetadata( int MetadataVersion, string EntryPoint, // Path of the entrypoint file - IEnumerable SourceFiles + IEnumerable SourceFiles, + string? BicepVersion = null ); [JsonSerializable(typeof(SourceFileInfoEntry))] @@ -68,9 +91,32 @@ private partial record SourceFileInfoEntry( string Kind // kind of source ); - public static SourceArchive FromStream(Stream stream) + private string? GetRequiredBicepVersionMessage() + { + if (MetadataVersion < CurrentMetadataVersion) + { + return $"This source code was published with an older, incompatible version of Bicep ({FriendlyBicepVersion}). You are using version {ThisAssembly.AssemblyVersion}."; + } + + if (MetadataVersion > CurrentMetadataVersion) + { + return $"This source code was published with a newer, incompatible version of Bicep ({FriendlyBicepVersion}). You are using version {ThisAssembly.AssemblyVersion}. You need a newer version in order to view the module source."; + } + + return null; + } + + public static ResultWithException UnpackFromStream(Stream stream) { - return new SourceArchive(stream); + var archive = new SourceArchive(stream); + if (archive.GetRequiredBicepVersionMessage() is string message) + { + return new(new Exception(message)); + } + else + { + return new(archive); + } } /// @@ -133,7 +179,7 @@ public static Stream PackSourcesIntoStream(Uri entrypointFileUri, params ISource // Add the metadata file var metadataContents = CreateMetadataFileContents(entryPointPath, filesMetadata); - WriteNewFileEntry(tarWriter, MetadataArchivedFileName, metadataContents); + WriteNewFileEntry(tarWriter, MetadataFileName, metadataContents); } } @@ -150,14 +196,8 @@ public static Stream PackSourcesIntoStream(Uri entrypointFileUri, params ISource private SourceArchive(Stream stream) { - if (isDisposed) - { - throw new ObjectDisposedException(nameof(SourceArchive)); - } - var filesBuilder = ImmutableDictionary.CreateBuilder(); - stream.Position = 0; var gz = new GZipStream(stream, CompressionMode.Decompress); using var tarReader = new TarReader(gz); @@ -169,9 +209,9 @@ private SourceArchive(Stream stream) var dictionary = filesBuilder.ToImmutableDictionary(); - var metadataJson = dictionary[MetadataArchivedFileName] + var metadataJson = dictionary[MetadataFileName] ?? throw new BicepException("Incorrectly formatted source file: No {MetadataArchivedFileName} entry"); - var metadata = JsonSerializer.Deserialize(metadataJson, MetadataSerializationContext.Default.MetadataEntry) + var metadata = JsonSerializer.Deserialize(metadataJson, MetadataSerializationContext.Default.ArchiveMetadata) ?? throw new BicepException("Source archive has invalid metadata entry"); var infos = new List(); @@ -182,15 +222,15 @@ private SourceArchive(Stream stream) infos.Add(new SourceFileInfo(info.Path, info.ArchivePath, info.Kind, contents)); } - this.EntrypointRelativePath = metadata.EntryPoint; + this.InstanceMetadata = metadata; this.SourceFiles = infos.ToImmutableArray(); } private static string CreateMetadataFileContents(string entrypointPath, IEnumerable files) { // Add the __metadata.json file - var metadata = new MetadataEntry(CurrentMetadataVersion, entrypointPath, files); - return JsonSerializer.Serialize(metadata, MetadataSerializationContext.Default.MetadataEntry); + var metadata = new ArchiveMetadata(CurrentMetadataVersion, entrypointPath, files, CurrentBicepVersion); + return JsonSerializer.Serialize(metadata, MetadataSerializationContext.Default.ArchiveMetadata); } private static void WriteNewFileEntry(TarWriter tarWriter, string archivePath, string contents) diff --git a/src/Bicep.LangServer.IntegrationTests/CodeLensTests.cs b/src/Bicep.LangServer.IntegrationTests/CodeLensTests.cs index 62eb9388f9d..4967177fc7a 100644 --- a/src/Bicep.LangServer.IntegrationTests/CodeLensTests.cs +++ b/src/Bicep.LangServer.IntegrationTests/CodeLensTests.cs @@ -10,6 +10,7 @@ using System.Reactive.Linq; using System.Reflection; using System.Threading.Tasks; +using Bicep.Core.Diagnostics; using Bicep.Core.Registry; using Bicep.Core.Samples; using Bicep.Core.SourceCode; @@ -49,16 +50,16 @@ public static string GetDisplayName(MethodInfo info, object[] row) } // If entrypointSource is not null, then a source archive will be created with the given entrypointSource, otherwise no source archive will be created. - private SharedLanguageHelperManager CreateServer(Uri? bicepModuleEntrypoint, string? entrypointSource) + private SharedLanguageHelperManager CreateServer(Uri? bicepModuleEntrypoint, string? entrypointSource, ResultWithException? sourceArchiveResult = null) { var moduleRegistry = StrictMock.Of(); - SourceArchive? sourceArchive = null; if (bicepModuleEntrypoint is not null && entrypointSource is not null) { BicepFile moduleEntrypointFile = SourceFileFactory.CreateBicepFile(bicepModuleEntrypoint, entrypointSource); - sourceArchive = SourceArchive.FromStream(SourceArchive.PackSourcesIntoStream(moduleEntrypointFile.FileUri, moduleEntrypointFile)); + sourceArchiveResult ??= SourceArchive.UnpackFromStream(SourceArchive.PackSourcesIntoStream(moduleEntrypointFile.FileUri, moduleEntrypointFile)); } - moduleRegistry.Setup(m => m.TryGetSource(It.IsAny())).Returns(sourceArchive); + sourceArchiveResult ??= new(new SourceNotAvailableException()); + moduleRegistry.Setup(m => m.TryGetSource(It.IsAny())).Returns(sourceArchiveResult); var moduleDispatcher = StrictMock.Of(); moduleDispatcher.Setup(x => x.RestoreModules(It.IsAny>(), It.IsAny())). @@ -71,7 +72,7 @@ private SharedLanguageHelperManager CreateServer(Uri? bicepModuleEntrypoint, str var artifactRegistries = moduleRegistry.Object.AsArray(); moduleDispatcher.Setup(m => m.TryGetModuleSources(It.IsAny())).Returns((ArtifactReference reference) => - artifactRegistries.Select(r => r.TryGetSource(reference)).FirstOrDefault(s => s is not null)); + artifactRegistries.Select(r => r.TryGetSource(reference)).FirstOrDefault(s => s is not null) ?? new(new SourceNotAvailableException())); var defaultServer = new SharedLanguageHelperManager(); defaultServer.Initialize( @@ -186,7 +187,7 @@ public async Task DisplayingModuleCompiledJsonFile_AndSourceIsAvailable_ShouldHa var lens = lenses.First(); lens.Should().HaveRange(new Range(0, 0, 0, 0)); lens.Should().HaveCommandName("bicep.internal.showModuleSourceFile"); - lens.Should().HaveCommandTitle("Show Bicep source"); + lens.Should().HaveCommandTitle("Show Bicep source (experimental)"); var target = new ExternalSourceReference(lens.CommandArguments().Single()); target.IsRequestingCompiledJson.Should().BeFalse(); target.RequestedFile.Should().Be(Path.GetFileName(moduleEntrypointUri.Path)); @@ -209,7 +210,7 @@ public async Task DisplayingModuleCompiledJsonFile_AndSourceNotAvailable_ShouldH var lens = lenses.First(); lens.Should().HaveRange(new Range(0, 0, 0, 0)); lens.Should().HaveCommandName(""); - lens.Should().HaveCommandTitle("No source code is available for this module"); + lens.Should().HaveCommandTitle("(Experimental) No source code is available for this module"); lens.Should().HaveNoCommandArguments(); } @@ -230,7 +231,29 @@ public async Task HasBadUri_ShouldHaveCodeLens_ToExplainError() var lens = lenses.First(); lens.Should().HaveRange(new Range(0, 0, 0, 0)); lens.Should().HaveCommandName(""); - lens.Should().HaveCommandTitle("There was an error retrieving source code for this module: Invalid module reference 'br:myregistry.azurecr.io/myrepo/bicep/module1:'. The specified OCI artifact reference \"br:myregistry.azurecr.io/myrepo/bicep/module1:\" is not valid. The module tag or digest is missing. (Parameter 'fullyQualifiedModuleReference')"); + lens.Should().HaveCommandTitle("(Experimental) There was an error retrieving source code for this module: Invalid module reference 'br:myregistry.azurecr.io/myrepo/bicep/module1:'. The specified OCI artifact reference \"br:myregistry.azurecr.io/myrepo/bicep/module1:\" is not valid. The module tag or digest is missing. (Parameter 'fullyQualifiedModuleReference')"); + lens.Should().HaveNoCommandArguments(); + } + + [TestMethod] + public async Task SourceArchiveHasError_ShouldHaveCodeLensWithError() + { + var uri = DocumentUri.From($"/{this.TestContext.TestName}"); + var moduleEntrypointUri = DocumentUri.From($"/module entrypoint.bicep"); + + var sourceArchiveResult = new ResultWithException(new Exception("Source archive is incompatible with this version of Bicep.")); + await using var server = CreateServer(moduleEntrypointUri.ToUriEncoded(), "// module entrypoint", sourceArchiveResult); + var helper = await server.GetAsync(); + await helper.OpenFileOnceAsync(this.TestContext, string.Empty, uri); + + var documentUri = new ExternalSourceReference("title", "br:myregistry.azurecr.io/myrepo/bicep/module1:v1", null /* main.json */).ToUri(); + var lenses = await GetExternalSourceCodeLenses(helper, documentUri); + + lenses.Should().HaveCount(1); + var lens = lenses.First(); + lens.Should().HaveRange(new Range(0, 0, 0, 0)); + lens.Should().HaveCommandName(""); + lens.Should().HaveCommandTitle("(Experimental) Cannot display source code for this module. Source archive is incompatible with this version of Bicep."); lens.Should().HaveNoCommandArguments(); } diff --git a/src/Bicep.LangServer.IntegrationTests/Registry/ModuleRestoreSchedulerTests.cs b/src/Bicep.LangServer.IntegrationTests/Registry/ModuleRestoreSchedulerTests.cs index 12d9499b6ab..5505b93f04e 100644 --- a/src/Bicep.LangServer.IntegrationTests/Registry/ModuleRestoreSchedulerTests.cs +++ b/src/Bicep.LangServer.IntegrationTests/Registry/ModuleRestoreSchedulerTests.cs @@ -210,7 +210,7 @@ public ResultWithDiagnostic TryParseArtifactReference(Artifac return new(new MockArtifactRef(reference, PathHelper.FilePathToFileUrl(Path.GetTempFileName()))); } - public SourceArchive? TryGetSource(ArtifactReference artifactReference) => null; + public ResultWithException TryGetSource(ArtifactReference artifactReference) => new(new SourceNotAvailableException()); } private class MockArtifactRef : ArtifactReference diff --git a/src/Bicep.LangServer.UnitTests/BicepExternalSourceRequestHandlerTests.cs b/src/Bicep.LangServer.UnitTests/BicepExternalSourceRequestHandlerTests.cs index 70a41729211..617170ba703 100644 --- a/src/Bicep.LangServer.UnitTests/BicepExternalSourceRequestHandlerTests.cs +++ b/src/Bicep.LangServer.UnitTests/BicepExternalSourceRequestHandlerTests.cs @@ -168,8 +168,7 @@ public async Task FailureToReadEntryPointShouldThrow() dispatcher.Setup(m => m.GetArtifactRestoreStatus(moduleReference!, out nullBuilder)).Returns(ArtifactRestoreStatus.Succeeded); dispatcher.Setup(m => m.TryGetLocalArtifactEntryPointUri(moduleReference!)).Returns(ResultHelper.Create(compiledJsonUri, null)); - SourceArchive? sourceArchive = null; - dispatcher.Setup(m => m.TryGetModuleSources(moduleReference!)).Returns(sourceArchive); + dispatcher.Setup(m => m.TryGetModuleSources(moduleReference!)).Returns(new ResultWithException(new SourceNotAvailableException())); var resolver = StrictMock.Of(); resolver.Setup(m => m.TryRead(compiledJsonUri)).Returns(ResultHelper.Create((string?)null, readFailureBuilder)); @@ -208,8 +207,7 @@ public async Task RestoredValidModule_WithNoSources_ShouldReturnJsonContents() dispatcher.Setup(m => m.GetArtifactRestoreStatus(moduleReference!, out nullBuilder)).Returns(ArtifactRestoreStatus.Succeeded); dispatcher.Setup(m => m.TryGetLocalArtifactEntryPointUri(moduleReference!)).Returns(ResultHelper.Create(compiledJsonUri, null)); - SourceArchive? sourceArchive = null; - dispatcher.Setup(m => m.TryGetModuleSources(moduleReference!)).Returns(sourceArchive); + dispatcher.Setup(m => m.TryGetModuleSources(moduleReference!)).Returns(new ResultWithException(new SourceNotAvailableException())); var resolver = StrictMock.Of(); resolver.Setup(m => m.TryRead(compiledJsonUri)).Returns(ResultHelper.Create(compiledJsonContents, nullBuilder)); @@ -249,7 +247,7 @@ public async Task RestoredValidModule_WithSource_RequestingBicepFile_ShouldRetur var bicepSource = "metadata hi = 'This is the bicep source file'"; var bicepUri = new Uri("file:///foo/bar/entrypoint.bicep"); - var sourceArchive = SourceArchive.FromStream(SourceArchive.PackSourcesIntoStream(bicepUri, new Core.Workspaces.ISourceFile[] { + var sourceArchive = SourceArchive.UnpackFromStream(SourceArchive.PackSourcesIntoStream(bicepUri, new Core.Workspaces.ISourceFile[] { SourceFileFactory.CreateBicepFile(bicepUri, bicepSource)})); dispatcher.Setup(m => m.TryGetModuleSources(moduleReference!)).Returns(sourceArchive); @@ -291,7 +289,7 @@ public async Task RestoredValidModule_WithSource_RequestingCompiledJson_ShouldRe var bicepSource = "metadata hi = 'This is the bicep source file'"; var bicepUri = new Uri("file:///foo/bar/entrypoint.bicep"); - var sourceArchive = SourceArchive.FromStream(SourceArchive.PackSourcesIntoStream(bicepUri, new Core.Workspaces.ISourceFile[] { + var sourceArchive = SourceArchive.UnpackFromStream(SourceArchive.PackSourcesIntoStream(bicepUri, new Core.Workspaces.ISourceFile[] { SourceFileFactory.CreateBicepFile(bicepUri, bicepSource)})); dispatcher.Setup(m => m.TryGetModuleSources(moduleReference!)).Returns(sourceArchive); @@ -309,6 +307,18 @@ public async Task RestoredValidModule_WithSource_RequestingCompiledJson_ShouldRe #region GetExternalSourceLinkUri tests + [TestMethod] + public void GetExternalSourceLinkUri_DefaultToBicepIsFalse_WithoutOrWithoutSource_ShouldRequestMainJson() + { + Uri resultWithSource = GetExternalSourceLinkUri(new ExternalSourceLinkTestData(), defaultToDisplayingBicep: false); + DecodeExternalSourceUri(resultWithSource).IsRequestingCompiledJson.Should().BeTrue(); + DecodeExternalSourceUri(resultWithSource).Title.Should().Contain("main.json"); + + Uri resultWithoutSource = GetExternalSourceLinkUri(new ExternalSourceLinkTestData(), defaultToDisplayingBicep: false); + DecodeExternalSourceUri(resultWithoutSource).IsRequestingCompiledJson.Should().BeTrue(); + DecodeExternalSourceUri(resultWithoutSource).Title.Should().Contain("main.json"); + } + [TestMethod] public void GetExternalSourceLinkUri_FullLink_WithSource() { @@ -385,7 +395,7 @@ public void GetExternalSourceLinkUri_RequestedFilenameShouldBeBicepOrJson(Extern (decoded.RequestedFile ?? "main.json").Should().MatchRegex(".+\\.(bicep|json)$", "requested source file should end with .json or .bicep"); } - private Uri GetExternalSourceLinkUri(ExternalSourceLinkTestData testData) + private Uri GetExternalSourceLinkUri(ExternalSourceLinkTestData testData, bool defaultToDisplayingBicep = true) { Uri? entrypointUri = testData.sourceEntrypoint is { } ? new($"file:///{testData.sourceEntrypoint}") : null; OciArtifactReference reference = new( @@ -396,23 +406,28 @@ private Uri GetExternalSourceLinkUri(ExternalSourceLinkTestData testData) testData.tagOrDigest[0] == '@' ? testData.tagOrDigest[1..] : null, new Uri("file:///parent.bicep", UriKind.Absolute)); - var sourceArchive = entrypointUri is { } ? - SourceArchive.FromStream(SourceArchive.PackSourcesIntoStream( + SourceArchive? sourceArchive = entrypointUri is { } ? + SourceArchive.UnpackFromStream(SourceArchive.PackSourcesIntoStream( entrypointUri, new Core.Workspaces.ISourceFile[] { SourceFileFactory.CreateBicepFile(entrypointUri, "metadata description = 'bicep module'") - })) + })).TryUnwrap() : null; - return BicepExternalSourceRequestHandler.GetExternalSourceLinkUri(reference, sourceArchive); + return BicepExternalSourceRequestHandler.GetExternalSourceLinkUri(reference, sourceArchive, defaultToDisplayingBicep); + } + + private string TrimFirstCharacter(string s) + { + return s.Length > 2 ? s[1..] : s; } - public ExternalSourceReference DecodeExternalSourceUri(Uri uri) + private ExternalSourceReference DecodeExternalSourceUri(Uri uri) { // NOTE: This code should match src\vscode-bicep\src\language\bicepExternalSourceContentProvider.ts string title = Uri.UnescapeDataString(uri.AbsolutePath); - string moduleReference = Uri.UnescapeDataString(uri.Query[1..]); // skip '?' - string? requestedSourceFile = Uri.UnescapeDataString(uri.Fragment[1..]); // skip '#' + string moduleReference = Uri.UnescapeDataString(TrimFirstCharacter(uri.Query)); // skip '?' + string? requestedSourceFile = Uri.UnescapeDataString(TrimFirstCharacter(uri.Fragment)); // skip '#' return new ExternalSourceReference(title, moduleReference, requestedSourceFile); } diff --git a/src/Bicep.LangServer/Handlers/BicepDefinitionHandler.cs b/src/Bicep.LangServer/Handlers/BicepDefinitionHandler.cs index 6d69fb4f0d3..3f0afca3339 100644 --- a/src/Bicep.LangServer/Handlers/BicepDefinitionHandler.cs +++ b/src/Bicep.LangServer/Handlers/BicepDefinitionHandler.cs @@ -198,7 +198,8 @@ private Uri GetModuleSourceLinkUri(ISourceFile sourceFile, ArtifactReference ref return sourceFile.FileUri; } - return BicepExternalSourceRequestHandler.GetExternalSourceLinkUri(ociReference, moduleDispatcher.TryGetModuleSources(reference)); + //TODO(#12811): set defaultToDisplayingBicep back to default of true when removing experimental flag for publishing source + return BicepExternalSourceRequestHandler.GetExternalSourceLinkUri(ociReference, moduleDispatcher?.TryGetModuleSources(reference).TryUnwrap(), defaultToDisplayingBicep: false); } private LocationOrLocationLinks HandleWildcardImportDeclaration(CompilationContext context, DefinitionParams request, SymbolResolutionResult result, WildcardImportSymbol wildcardImport) diff --git a/src/Bicep.LangServer/Handlers/BicepExternalSourceRequestHandler.cs b/src/Bicep.LangServer/Handlers/BicepExternalSourceRequestHandler.cs index b388b407886..79237e442d3 100644 --- a/src/Bicep.LangServer/Handlers/BicepExternalSourceRequestHandler.cs +++ b/src/Bicep.LangServer/Handlers/BicepExternalSourceRequestHandler.cs @@ -23,7 +23,7 @@ public record BicepExternalSourceParams( string? requestedSourceFile = null // The relative source path of the file in the module to get source for (main.json if null)) ) : IRequest; - public record BicepExternalSourceResponse(string Content); + public record BicepExternalSourceResponse(string? Content, string? Error = null); /// /// Handles textDocument/bicepExternalSource LSP requests. These are sent by clients that are resolving contents of document URIs using the bicep-extsrc: scheme. @@ -72,19 +72,26 @@ public Task Handle(BicepExternalSourceParams reques $"Unable to obtain the entry point URI for module '{moduleReference.FullyQualifiedReference}'."); } - if (moduleDispatcher.TryGetModuleSources(moduleReference) is SourceArchive sourceArchive && request.requestedSourceFile is { }) + if (request.requestedSourceFile is { }) { - var requestedFile = sourceArchive.SourceFiles.FirstOrDefault(f => f.Path == request.requestedSourceFile); - if (requestedFile is null) + if (moduleDispatcher.TryGetModuleSources(moduleReference).IsSuccess(out var sourceArchive, out var ex)) { - throw new InvalidOperationException($"Could not find source file \"{request.requestedSourceFile}\" in the sources for module \"{moduleReference.FullyQualifiedReference}\""); - } + var requestedFile = sourceArchive.SourceFiles.FirstOrDefault(f => f.Path == request.requestedSourceFile); + if (requestedFile is null) + { + throw new InvalidOperationException($"Could not find source file \"{request.requestedSourceFile}\" in the sources for module \"{moduleReference.FullyQualifiedReference}\""); + } - return Task.FromResult(new BicepExternalSourceResponse(requestedFile.Contents)); + return Task.FromResult(new BicepExternalSourceResponse(requestedFile.Contents)); + } + else if (ex is not SourceNotAvailableException) + { + return Task.FromResult(new BicepExternalSourceResponse(null, ex.Message)); + } } - // No sources available, or specifically requesting the compiled main.json. - // Retrieve the JSON source + // No sources available, or specifically requesting the compiled main.json (requestedSourceFile=null), or there was an error retrieving sources. + // Just show the compiled JSON if (!this.fileResolver.TryRead(compiledJsonUri).IsSuccess(out var contents, out var failureBuilder)) { var message = failureBuilder(DiagnosticBuilder.ForDocumentStart()).Message; @@ -101,9 +108,9 @@ public Task Handle(BicepExternalSourceParams reques /// The module reference /// The source archive for the module, if sources are available /// A bicep-extsrc: URI - public static Uri GetExternalSourceLinkUri(OciArtifactReference reference, SourceArchive? sourceArchive) + public static Uri GetExternalSourceLinkUri(OciArtifactReference reference, SourceArchive? sourceArchive, bool defaultToDisplayingBicep = true) { - return new ExternalSourceReference(reference, sourceArchive).ToUri(); + return new ExternalSourceReference(reference, sourceArchive, defaultToDisplayingBicep: defaultToDisplayingBicep).ToUri(); } } } diff --git a/src/Bicep.LangServer/Handlers/ExternalSourceCodeLensProvider.cs b/src/Bicep.LangServer/Handlers/ExternalSourceCodeLensProvider.cs index 923a3dcf0cf..629a6cd6752 100644 --- a/src/Bicep.LangServer/Handlers/ExternalSourceCodeLensProvider.cs +++ b/src/Bicep.LangServer/Handlers/ExternalSourceCodeLensProvider.cs @@ -6,6 +6,7 @@ using System.Diagnostics; using System.Threading; using Bicep.Core.Registry; +using Bicep.Core.SourceCode; using Newtonsoft.Json.Linq; using OmniSharp.Extensions.LanguageServer.Protocol.Models; using Range = OmniSharp.Extensions.LanguageServer.Protocol.Models.Range; @@ -23,7 +24,7 @@ public static IEnumerable GetCodeLenses(IModuleDispatcher moduleDispat if (request.TextDocument.Uri.Scheme == LangServerConstants.ExternalSourceFileScheme) { - string? error = null; + string? message = null; ExternalSourceReference? externalReference = null; try @@ -32,38 +33,44 @@ public static IEnumerable GetCodeLenses(IModuleDispatcher moduleDispat } catch (Exception ex) { - error = ex.Message; + message = $"(Experimental) There was an error retrieving source code for this module: {ex.Message}"; } if (externalReference is not null) { - Debug.Assert(error is null); + Debug.Assert(message is null); var isDisplayingCompiledJson = externalReference.IsRequestingCompiledJson; - if (externalReference.ToArtifactReference().IsSuccess(out var artifactReference, out error)) + if (externalReference.ToArtifactReference().IsSuccess(out var artifactReference, out message)) { - var sourceArchive = artifactReference is { } ? moduleDispatcher.TryGetModuleSources(artifactReference) : null; + var sourceArchiveResult = moduleDispatcher.TryGetModuleSources(artifactReference); if (isDisplayingCompiledJson) { - if (sourceArchive is { }) + // Displaying main.json + + if (sourceArchiveResult.IsSuccess(out var sourceArchive, out var ex)) { yield return CreateCodeLens( DocumentStart, - "Show Bicep source", + "Show Bicep source (experimental)", "bicep.internal.showModuleSourceFile", new ExternalSourceReference(request.TextDocument.Uri).WithRequestForSourceFile(sourceArchive.EntrypointRelativePath).ToUri().ToString()); } + else if (ex is SourceNotAvailableException) + { + message = ex.Message; + } else { - yield return CreateCodeLens( - DocumentStart, - "No source code is available for this module"); + message = $"(Experimental) Cannot display source code for this module. {ex.Message}"; } } else { - if (sourceArchive is { }) + // Displaying a bicep file + + if (sourceArchiveResult.IsSuccess(out var _, out var ex)) { // We're displaying some source file other than the compiled JSON for the module. Allow user to switch to the compiled JSON. yield return CreateCodeLens( @@ -74,16 +81,17 @@ public static IEnumerable GetCodeLenses(IModuleDispatcher moduleDispat } else { - // This can happen if the user has a source file open in the editor and then restores to a version of the module that doesn't have source code available. - error = "Could not find the expected source archive in the module registry"; + message = ex.Message ?? + // This can happen if the user has a source file open in the editor and then restores to a version of the module that doesn't have source code available. + "Could not find the expected source archive in the module registry"; } } } } - if (error is not null) + if (message is not null) { - yield return CreateErrorLens($"There was an error retrieving source code for this module: {error}"); + yield return CreateErrorLens(message); } } diff --git a/src/Bicep.LangServer/Handlers/ExternalSourceReference.cs b/src/Bicep.LangServer/Handlers/ExternalSourceReference.cs index fa5cb402a17..a4c8e597257 100644 --- a/src/Bicep.LangServer/Handlers/ExternalSourceReference.cs +++ b/src/Bicep.LangServer/Handlers/ExternalSourceReference.cs @@ -77,12 +77,12 @@ public ExternalSourceReference WithRequestForSourceFile(string? requestedSourceF return new ExternalSourceReference(Components, requestedSourceFile); // recalculate title } - public ExternalSourceReference(OciArtifactReference moduleReference, SourceArchive? sourceArchive) + public ExternalSourceReference(OciArtifactReference moduleReference, SourceArchive? sourceArchive, bool defaultToDisplayingBicep = true) { Debug.Assert(moduleReference.Type == ArtifactType.Module && moduleReference.Scheme == OciArtifactReferenceFacts.Scheme, "Expecting a module reference, not a provider reference"); Components = moduleReference.AddressComponents; - if (sourceArchive is { }) + if (sourceArchive is { } && defaultToDisplayingBicep) { // We have Bicep source code available RequestedFile = sourceArchive.EntrypointRelativePath; diff --git a/src/vscode-bicep/src/language/bicepExternalSourceContentProvider.ts b/src/vscode-bicep/src/language/bicepExternalSourceContentProvider.ts index 9ec1d9282f8..83356910bcf 100644 --- a/src/vscode-bicep/src/language/bicepExternalSourceContentProvider.ts +++ b/src/vscode-bicep/src/language/bicepExternalSourceContentProvider.ts @@ -48,7 +48,7 @@ export class BicepExternalSourceContentProvider token, ); - return response.content; + return response.error ? `// ${response.error}` : response.content ?? ""; } private bicepExternalSourceRequest( diff --git a/src/vscode-bicep/src/language/protocol.ts b/src/vscode-bicep/src/language/protocol.ts index db9778a0117..e5ac7fed072 100644 --- a/src/vscode-bicep/src/language/protocol.ts +++ b/src/vscode-bicep/src/language/protocol.ts @@ -148,7 +148,8 @@ export interface BicepExternalSourceParams { } export interface BicepExternalSourceResponse { - content: string; + content: string | undefined; + error: string | undefined; } export const bicepExternalSourceRequestType = new ProtocolRequestType<