From 733c1db68ea3336c554caa905fb4e7aa71e08fff Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 4 Dec 2025 19:04:34 +0000 Subject: [PATCH 1/4] Initial plan From 6966c3678b5f662a36e62e369941477b0b50c659 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 4 Dec 2025 19:32:16 +0000 Subject: [PATCH 2/4] Add AdditionalStaticWebAssetsBasePath property for external content roots - Add ParseAdditionalStaticWebAssetsBasePaths task to parse and discover files from additional content roots - Add ResolveAdditionalStaticWebAssets target that runs as part of ResolveCoreStaticWebAssets - Add UsingTask registration for the new task - Add E2E tests for build and publish scenarios with additional content roots - Support semicolon-separated list of content-root,base-path pairs Co-authored-by: javiercn <6995051+javiercn@users.noreply.github.com> --- .../Microsoft.NET.Sdk.StaticWebAssets.targets | 51 ++++++ ...ParseAdditionalStaticWebAssetsBasePaths.cs | 136 ++++++++++++++++ .../StaticWebAssetsIntegrationTest.cs | 147 ++++++++++++++++++ 3 files changed, 334 insertions(+) create mode 100644 src/StaticWebAssetsSdk/Tasks/ParseAdditionalStaticWebAssetsBasePaths.cs diff --git a/src/StaticWebAssetsSdk/Targets/Microsoft.NET.Sdk.StaticWebAssets.targets b/src/StaticWebAssetsSdk/Targets/Microsoft.NET.Sdk.StaticWebAssets.targets index 0fb3afa2fbc2..419e315cf425 100644 --- a/src/StaticWebAssetsSdk/Targets/Microsoft.NET.Sdk.StaticWebAssets.targets +++ b/src/StaticWebAssetsSdk/Targets/Microsoft.NET.Sdk.StaticWebAssets.targets @@ -122,6 +122,11 @@ Copyright (c) .NET Foundation. All rights reserved. AssemblyFile="$(StaticWebAssetsSdkBuildTasksAssembly)" Condition="'$(StaticWebAssetsSdkBuildTasksAssembly)' != ''" /> + + + + + + + + + + + + + + + + + + + + + $(PackageId) + %(_ParsedAdditionalContentRoot.BasePath) + %(_ParsedAdditionalContentRoot.ContentRoot) + ** + + + + + diff --git a/src/StaticWebAssetsSdk/Tasks/ParseAdditionalStaticWebAssetsBasePaths.cs b/src/StaticWebAssetsSdk/Tasks/ParseAdditionalStaticWebAssetsBasePaths.cs new file mode 100644 index 000000000000..8c309351b8b1 --- /dev/null +++ b/src/StaticWebAssetsSdk/Tasks/ParseAdditionalStaticWebAssetsBasePaths.cs @@ -0,0 +1,136 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#nullable disable + +using Microsoft.Build.Framework; +using Microsoft.Build.Utilities; + +namespace Microsoft.AspNetCore.StaticWebAssets.Tasks; + +/// +/// Parses the AdditionalStaticWebAssetsBasePath property which is a semicolon-separated list of +/// content-root,base-path pairs and discovers the files in each content root. +/// +public class ParseAdditionalStaticWebAssetsBasePaths : Task +{ + /// + /// The semicolon-separated list of content-root,base-path pairs. + /// Format: content-root-1,base-path-1;content-root-2,base-path-2 + /// + [Required] + public string AdditionalStaticWebAssetsBasePaths { get; set; } + + /// + /// The parsed content root and base path pairs as items with ContentRoot and BasePath metadata. + /// + [Output] + public ITaskItem[] ParsedBasePaths { get; set; } + + /// + /// The discovered files from all content roots with appropriate metadata for DefineStaticWebAssets. + /// + [Output] + public ITaskItem[] DiscoveredFiles { get; set; } + + public override bool Execute() + { + var parsedPaths = new List(); + var discoveredFiles = new List(); + + if (string.IsNullOrEmpty(AdditionalStaticWebAssetsBasePaths)) + { + ParsedBasePaths = []; + DiscoveredFiles = []; + return true; + } + + var pairs = AdditionalStaticWebAssetsBasePaths.Split(new[] { ';' }, StringSplitOptions.RemoveEmptyEntries); + + foreach (var pair in pairs) + { + var parts = pair.Split(new[] { ',' }, 2, StringSplitOptions.None); + + if (parts.Length < 2) + { + Log.LogError( + "Invalid format for AdditionalStaticWebAssetsBasePath entry '{0}'. Expected format: 'content-root,base-path'.", + pair); + continue; + } + + var contentRoot = parts[0].Trim(); + var basePath = parts[1].Trim(); + + if (string.IsNullOrEmpty(contentRoot)) + { + Log.LogError( + "Content root is empty in AdditionalStaticWebAssetsBasePath entry '{0}'.", + pair); + continue; + } + + // Normalize content root path to end with directory separator + if (!contentRoot.EndsWith(Path.DirectorySeparatorChar.ToString()) && + !contentRoot.EndsWith(Path.AltDirectorySeparatorChar.ToString())) + { + contentRoot += Path.DirectorySeparatorChar; + } + + // Make content root path absolute if it's relative + if (!Path.IsPathRooted(contentRoot)) + { + contentRoot = Path.GetFullPath(contentRoot); + } + + var pathItem = new TaskItem(parsedPaths.Count.ToString(System.Globalization.CultureInfo.InvariantCulture)); + pathItem.SetMetadata("ContentRoot", contentRoot); + pathItem.SetMetadata("BasePath", basePath); + + Log.LogMessage(MessageImportance.Low, + "Parsed additional static web asset base path: ContentRoot='{0}', BasePath='{1}'", + contentRoot, + basePath); + + parsedPaths.Add(pathItem); + + // Discover files from this content root + if (Directory.Exists(contentRoot)) + { + var files = Directory.GetFiles(contentRoot, "*", SearchOption.AllDirectories); + foreach (var file in files) + { + var fullPath = Path.GetFullPath(file); + var relativePath = fullPath.Substring(contentRoot.Length); + + // Normalize path separators + relativePath = relativePath.Replace(Path.DirectorySeparatorChar, '/'); + + var fileItem = new TaskItem(fullPath); + fileItem.SetMetadata("ContentRoot", contentRoot); + fileItem.SetMetadata("BasePath", basePath); + fileItem.SetMetadata("RelativePath", relativePath); + + Log.LogMessage(MessageImportance.Low, + "Discovered file '{0}' with relative path '{1}' in content root '{2}'", + fullPath, + relativePath, + contentRoot); + + discoveredFiles.Add(fileItem); + } + } + else + { + Log.LogWarning( + "Content root directory '{0}' does not exist.", + contentRoot); + } + } + + ParsedBasePaths = [.. parsedPaths]; + DiscoveredFiles = [.. discoveredFiles]; + + return !Log.HasLoggedErrors; + } +} diff --git a/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssetsIntegrationTest.cs b/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssetsIntegrationTest.cs index c13c8b2d5c6b..45a33ccbd683 100644 --- a/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssetsIntegrationTest.cs +++ b/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssetsIntegrationTest.cs @@ -617,6 +617,153 @@ public void Build_DoesNotFailToCompress_TwoAssetsWith_TheSameContent() AssertManifest(manifest1, expectedManifest); AssertBuildAssets(manifest1, outputPath, intermediateOutputPath); } + + [Fact] + public void Build_AdditionalStaticWebAssetsBasePath_IncludesExternalContentRoots() + { + var testAsset = "RazorComponentApp"; + ProjectDirectory = CreateAspNetSdkTestAsset(testAsset); + + // Create an additional folder outside wwwroot to serve as additional content root + var additionalContentRoot = Path.Combine(ProjectDirectory.Path, "AdditionalAssets"); + Directory.CreateDirectory(additionalContentRoot); + File.WriteAllText(Path.Combine(additionalContentRoot, "extra.js"), "console.log('extra');"); + File.WriteAllText(Path.Combine(additionalContentRoot, "extra.css"), ".extra { color: red; }"); + + // Create a subdirectory with more assets + var subDir = Path.Combine(additionalContentRoot, "sub"); + Directory.CreateDirectory(subDir); + File.WriteAllText(Path.Combine(subDir, "nested.txt"), "nested content"); + + var build = CreateBuildCommand(ProjectDirectory); + + // Pass the AdditionalStaticWebAssetsBasePath property with format: content-root,base-path + // Quote the entire value to prevent MSBuild from interpreting the comma as a separator + var additionalAssetsProperty = $"\"{additionalContentRoot},_content/AdditionalAssets\""; + ExecuteCommand(build, $"/p:AdditionalStaticWebAssetsBasePath={additionalAssetsProperty}").Should().Pass(); + + var intermediateOutputPath = build.GetIntermediateDirectory(DefaultTfm, "Debug").ToString(); + var outputPath = build.GetOutputDirectory(DefaultTfm, "Debug").ToString(); + + // Verify the manifest file exists + var path = Path.Combine(intermediateOutputPath, "staticwebassets.build.json"); + new FileInfo(path).Should().Exist(); + + var manifest = StaticWebAssetsManifest.FromJsonBytes(File.ReadAllBytes(path)); + + // Verify the additional assets are in the manifest (only count Primary assets, not compressed alternatives) + var additionalAssets = manifest.Assets.Where(a => + a.AssetRole == "Primary" && + (a.RelativePath.Contains("extra.js") || + a.RelativePath.Contains("extra.css") || + a.RelativePath.Contains("nested.txt"))).ToArray(); + + additionalAssets.Should().HaveCount(3, "All additional assets should be included in the manifest"); + + // Verify the base path is correct + foreach (var asset in additionalAssets) + { + asset.BasePath.Should().Be("_content/AdditionalAssets"); + } + + // Verify discovery patterns include the additional content root + var discoveryPattern = manifest.DiscoveryPatterns.FirstOrDefault(p => + p.BasePath == "_content/AdditionalAssets"); + discoveryPattern.Should().NotBeNull("Discovery pattern for additional content root should exist"); + } + + [Fact] + public void Build_AdditionalStaticWebAssetsBasePath_MultipleContentRoots() + { + var testAsset = "RazorComponentApp"; + ProjectDirectory = CreateAspNetSdkTestAsset(testAsset); + + // Create first additional folder + var additionalContentRoot1 = Path.Combine(ProjectDirectory.Path, "AdditionalAssets1"); + Directory.CreateDirectory(additionalContentRoot1); + File.WriteAllText(Path.Combine(additionalContentRoot1, "file1.js"), "console.log('file1');"); + + // Create second additional folder + var additionalContentRoot2 = Path.Combine(ProjectDirectory.Path, "AdditionalAssets2"); + Directory.CreateDirectory(additionalContentRoot2); + File.WriteAllText(Path.Combine(additionalContentRoot2, "file2.js"), "console.log('file2');"); + + var build = CreateBuildCommand(ProjectDirectory); + + // Pass multiple content roots separated by semicolons + // Quote the entire value to prevent MSBuild from interpreting commas/semicolons as separators + var additionalAssetsProperty = $"\"{additionalContentRoot1},_content/Assets1;{additionalContentRoot2},_content/Assets2\""; + ExecuteCommand(build, $"/p:AdditionalStaticWebAssetsBasePath={additionalAssetsProperty}").Should().Pass(); + + var intermediateOutputPath = build.GetIntermediateDirectory(DefaultTfm, "Debug").ToString(); + + var path = Path.Combine(intermediateOutputPath, "staticwebassets.build.json"); + new FileInfo(path).Should().Exist(); + + var manifest = StaticWebAssetsManifest.FromJsonBytes(File.ReadAllBytes(path)); + + // Verify assets from both content roots are in the manifest (only count Primary assets) + var asset1 = manifest.Assets.FirstOrDefault(a => a.AssetRole == "Primary" && a.RelativePath.Contains("file1.js")); + var asset2 = manifest.Assets.FirstOrDefault(a => a.AssetRole == "Primary" && a.RelativePath.Contains("file2.js")); + + asset1.Should().NotBeNull("Asset from first additional content root should exist"); + asset1!.BasePath.Should().Be("_content/Assets1"); + + asset2.Should().NotBeNull("Asset from second additional content root should exist"); + asset2!.BasePath.Should().Be("_content/Assets2"); + + // Verify discovery patterns for both content roots + manifest.DiscoveryPatterns.Should().Contain(p => p.BasePath == "_content/Assets1"); + manifest.DiscoveryPatterns.Should().Contain(p => p.BasePath == "_content/Assets2"); + } + + [Fact] + public void Publish_AdditionalStaticWebAssetsBasePath_IncludesExternalContentRoots() + { + var testAsset = "RazorComponentApp"; + ProjectDirectory = CreateAspNetSdkTestAsset(testAsset); + + // Create an additional folder outside wwwroot to serve as additional content root + var additionalContentRoot = Path.Combine(ProjectDirectory.Path, "AdditionalAssets"); + Directory.CreateDirectory(additionalContentRoot); + File.WriteAllText(Path.Combine(additionalContentRoot, "publish-extra.js"), "console.log('publish-extra');"); + + var publish = CreatePublishCommand(ProjectDirectory); + + // Pass the AdditionalStaticWebAssetsBasePath property + // Quote the entire value to prevent MSBuild from interpreting the comma as a separator + var additionalAssetsProperty = $"\"{additionalContentRoot},_content/AdditionalAssets\""; + ExecuteCommand(publish, $"/p:AdditionalStaticWebAssetsBasePath={additionalAssetsProperty}").Should().Pass(); + + var intermediateOutputPath = publish.GetIntermediateDirectory(DefaultTfm, "Debug").ToString(); + var publishPath = publish.GetOutputDirectory(DefaultTfm, "Debug").ToString(); + + // Verify the build manifest file exists and includes the additional assets + var buildPath = Path.Combine(intermediateOutputPath, "staticwebassets.build.json"); + new FileInfo(buildPath).Should().Exist(); + + var buildManifest = StaticWebAssetsManifest.FromJsonBytes(File.ReadAllBytes(buildPath)); + + // Verify the additional asset is in the build manifest (only count Primary assets) + var additionalAsset = buildManifest.Assets.FirstOrDefault(a => + a.AssetRole == "Primary" && a.RelativePath.Contains("publish-extra.js")); + + additionalAsset.Should().NotBeNull("Additional asset should be included in the build manifest"); + additionalAsset!.BasePath.Should().Be("_content/AdditionalAssets"); + + // Verify the publish manifest file exists + var publishManifestPath = Path.Combine(intermediateOutputPath, "staticwebassets.publish.json"); + new FileInfo(publishManifestPath).Should().Exist(); + + var publishManifest = StaticWebAssetsManifest.FromJsonBytes(File.ReadAllBytes(publishManifestPath)); + + // Verify the additional asset is in the publish manifest + var publishAsset = publishManifest.Assets.FirstOrDefault(a => + a.AssetRole == "Primary" && a.RelativePath.Contains("publish-extra.js")); + + publishAsset.Should().NotBeNull("Additional asset should be included in the publish manifest"); + publishAsset!.BasePath.Should().Be("_content/AdditionalAssets"); + } } public class StaticWebAssetsAppWithPackagesIntegrationTest(ITestOutputHelper log) From 7900320dc2aceb2894d7f06642acec7de9c46d21 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 4 Dec 2025 19:38:10 +0000 Subject: [PATCH 3/4] Address code review comments for cross-framework compatibility - Use conditional compilation for EndsWith(char) and Path.GetRelativePath - Add fallback implementations for .NET Framework 4.7.2 Co-authored-by: javiercn <6995051+javiercn@users.noreply.github.com> --- .../ParseAdditionalStaticWebAssetsBasePaths.cs | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/src/StaticWebAssetsSdk/Tasks/ParseAdditionalStaticWebAssetsBasePaths.cs b/src/StaticWebAssetsSdk/Tasks/ParseAdditionalStaticWebAssetsBasePaths.cs index 8c309351b8b1..8f6616b9562c 100644 --- a/src/StaticWebAssetsSdk/Tasks/ParseAdditionalStaticWebAssetsBasePaths.cs +++ b/src/StaticWebAssetsSdk/Tasks/ParseAdditionalStaticWebAssetsBasePaths.cs @@ -71,8 +71,13 @@ public override bool Execute() } // Normalize content root path to end with directory separator +#if NET + if (!contentRoot.EndsWith(Path.DirectorySeparatorChar) && + !contentRoot.EndsWith(Path.AltDirectorySeparatorChar)) +#else if (!contentRoot.EndsWith(Path.DirectorySeparatorChar.ToString()) && !contentRoot.EndsWith(Path.AltDirectorySeparatorChar.ToString())) +#endif { contentRoot += Path.DirectorySeparatorChar; } @@ -101,7 +106,14 @@ public override bool Execute() foreach (var file in files) { var fullPath = Path.GetFullPath(file); - var relativePath = fullPath.Substring(contentRoot.Length); +#if NET + var relativePath = Path.GetRelativePath(contentRoot, fullPath); +#else + // For .NET Framework, manually compute relative path + var relativePath = fullPath.StartsWith(contentRoot, StringComparison.OrdinalIgnoreCase) + ? fullPath.Substring(contentRoot.Length) + : fullPath; +#endif // Normalize path separators relativePath = relativePath.Replace(Path.DirectorySeparatorChar, '/'); From 76775fbe600dcf80b4859dd5c0a9fd307e8dd13f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 4 Dec 2025 19:52:58 +0000 Subject: [PATCH 4/4] Replace custom task with MSBuild ItemGroup transforms - Remove ParseAdditionalStaticWebAssetsBasePaths custom task - Use MSBuild property functions to parse content-root,base-path pairs - Use ItemGroup transforms to split and discover files from content roots Co-authored-by: javiercn <6995051+javiercn@users.noreply.github.com> --- .../Microsoft.NET.Sdk.StaticWebAssets.targets | 27 ++-- ...ParseAdditionalStaticWebAssetsBasePaths.cs | 148 ------------------ 2 files changed, 16 insertions(+), 159 deletions(-) delete mode 100644 src/StaticWebAssetsSdk/Tasks/ParseAdditionalStaticWebAssetsBasePaths.cs diff --git a/src/StaticWebAssetsSdk/Targets/Microsoft.NET.Sdk.StaticWebAssets.targets b/src/StaticWebAssetsSdk/Targets/Microsoft.NET.Sdk.StaticWebAssets.targets index 419e315cf425..8de4f6806131 100644 --- a/src/StaticWebAssetsSdk/Targets/Microsoft.NET.Sdk.StaticWebAssets.targets +++ b/src/StaticWebAssetsSdk/Targets/Microsoft.NET.Sdk.StaticWebAssets.targets @@ -122,11 +122,6 @@ Copyright (c) .NET Foundation. All rights reserved. AssemblyFile="$(StaticWebAssetsSdkBuildTasksAssembly)" Condition="'$(StaticWebAssetsSdkBuildTasksAssembly)' != ''" /> - - + + <_AdditionalStaticWebAssetsBasePathEntry Include="$(AdditionalStaticWebAssetsBasePath)" /> + <_ParsedAdditionalContentRoot Include="@(_AdditionalStaticWebAssetsBasePathEntry)"> + $([System.String]::Copy('%(Identity)').Split(',')[0].Trim())$([System.IO.Path]::DirectorySeparatorChar) + $([System.String]::Copy('%(Identity)').Split(',')[1].Trim()) + + + + + + <_AdditionalContentRootCandidates Include="%(_ParsedAdditionalContentRoot.ContentRoot)**"> + %(_ParsedAdditionalContentRoot.ContentRoot) + %(_ParsedAdditionalContentRoot.BasePath) + + -/// Parses the AdditionalStaticWebAssetsBasePath property which is a semicolon-separated list of -/// content-root,base-path pairs and discovers the files in each content root. -/// -public class ParseAdditionalStaticWebAssetsBasePaths : Task -{ - /// - /// The semicolon-separated list of content-root,base-path pairs. - /// Format: content-root-1,base-path-1;content-root-2,base-path-2 - /// - [Required] - public string AdditionalStaticWebAssetsBasePaths { get; set; } - - /// - /// The parsed content root and base path pairs as items with ContentRoot and BasePath metadata. - /// - [Output] - public ITaskItem[] ParsedBasePaths { get; set; } - - /// - /// The discovered files from all content roots with appropriate metadata for DefineStaticWebAssets. - /// - [Output] - public ITaskItem[] DiscoveredFiles { get; set; } - - public override bool Execute() - { - var parsedPaths = new List(); - var discoveredFiles = new List(); - - if (string.IsNullOrEmpty(AdditionalStaticWebAssetsBasePaths)) - { - ParsedBasePaths = []; - DiscoveredFiles = []; - return true; - } - - var pairs = AdditionalStaticWebAssetsBasePaths.Split(new[] { ';' }, StringSplitOptions.RemoveEmptyEntries); - - foreach (var pair in pairs) - { - var parts = pair.Split(new[] { ',' }, 2, StringSplitOptions.None); - - if (parts.Length < 2) - { - Log.LogError( - "Invalid format for AdditionalStaticWebAssetsBasePath entry '{0}'. Expected format: 'content-root,base-path'.", - pair); - continue; - } - - var contentRoot = parts[0].Trim(); - var basePath = parts[1].Trim(); - - if (string.IsNullOrEmpty(contentRoot)) - { - Log.LogError( - "Content root is empty in AdditionalStaticWebAssetsBasePath entry '{0}'.", - pair); - continue; - } - - // Normalize content root path to end with directory separator -#if NET - if (!contentRoot.EndsWith(Path.DirectorySeparatorChar) && - !contentRoot.EndsWith(Path.AltDirectorySeparatorChar)) -#else - if (!contentRoot.EndsWith(Path.DirectorySeparatorChar.ToString()) && - !contentRoot.EndsWith(Path.AltDirectorySeparatorChar.ToString())) -#endif - { - contentRoot += Path.DirectorySeparatorChar; - } - - // Make content root path absolute if it's relative - if (!Path.IsPathRooted(contentRoot)) - { - contentRoot = Path.GetFullPath(contentRoot); - } - - var pathItem = new TaskItem(parsedPaths.Count.ToString(System.Globalization.CultureInfo.InvariantCulture)); - pathItem.SetMetadata("ContentRoot", contentRoot); - pathItem.SetMetadata("BasePath", basePath); - - Log.LogMessage(MessageImportance.Low, - "Parsed additional static web asset base path: ContentRoot='{0}', BasePath='{1}'", - contentRoot, - basePath); - - parsedPaths.Add(pathItem); - - // Discover files from this content root - if (Directory.Exists(contentRoot)) - { - var files = Directory.GetFiles(contentRoot, "*", SearchOption.AllDirectories); - foreach (var file in files) - { - var fullPath = Path.GetFullPath(file); -#if NET - var relativePath = Path.GetRelativePath(contentRoot, fullPath); -#else - // For .NET Framework, manually compute relative path - var relativePath = fullPath.StartsWith(contentRoot, StringComparison.OrdinalIgnoreCase) - ? fullPath.Substring(contentRoot.Length) - : fullPath; -#endif - - // Normalize path separators - relativePath = relativePath.Replace(Path.DirectorySeparatorChar, '/'); - - var fileItem = new TaskItem(fullPath); - fileItem.SetMetadata("ContentRoot", contentRoot); - fileItem.SetMetadata("BasePath", basePath); - fileItem.SetMetadata("RelativePath", relativePath); - - Log.LogMessage(MessageImportance.Low, - "Discovered file '{0}' with relative path '{1}' in content root '{2}'", - fullPath, - relativePath, - contentRoot); - - discoveredFiles.Add(fileItem); - } - } - else - { - Log.LogWarning( - "Content root directory '{0}' does not exist.", - contentRoot); - } - } - - ParsedBasePaths = [.. parsedPaths]; - DiscoveredFiles = [.. discoveredFiles]; - - return !Log.HasLoggedErrors; - } -}