mirror of
https://github.com/microsoft/PowerToys.git
synced 2026-08-29 10:09:43 +02:00
Move JS manifest capabilities removal and publisher/author fallback to phase 2
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 762976e0-2453-40ca-8034-978a04a19d1e
This commit is contained in:
@@ -47,10 +47,4 @@ public sealed record JSCmdPalSection
|
||||
/// </summary>
|
||||
[JsonPropertyName("debugPort")]
|
||||
public int? DebugPort { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the capabilities declared by the extension (for example, "commands").
|
||||
/// </summary>
|
||||
[JsonPropertyName("capabilities")]
|
||||
public string[]? Capabilities { get; init; }
|
||||
}
|
||||
|
||||
@@ -69,11 +69,6 @@ public sealed record JSExtensionManifest
|
||||
/// </summary>
|
||||
public JSExtensionEngines? Engines { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the capabilities declared by the extension (cmdpal.capabilities).
|
||||
/// </summary>
|
||||
public string[]? Capabilities { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the effective display name, falling back to <see cref="Name"/> when no display name is set.
|
||||
/// </summary>
|
||||
@@ -178,18 +173,73 @@ public sealed record JSExtensionManifest
|
||||
Version = package.Version,
|
||||
Description = package.Description,
|
||||
Icon = package.CmdPal.Icon,
|
||||
Publisher = package.CmdPal.Publisher,
|
||||
Publisher = ResolvePublisher(package),
|
||||
Main = entryPoint,
|
||||
EntryPointPath = resolvedEntryPoint,
|
||||
Debug = package.CmdPal.Debug,
|
||||
DebugPort = package.CmdPal.DebugPort,
|
||||
Engines = package.Engines,
|
||||
Capabilities = package.CmdPal.Capabilities,
|
||||
};
|
||||
|
||||
return JSExtensionManifestParseResult.Success(manifest);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Resolves the publisher name. The explicit cmdpal.publisher value wins. When it is
|
||||
/// absent or whitespace, the name portion of the top-level npm "author" field is used.
|
||||
/// Returns null when neither source provides a name.
|
||||
/// </summary>
|
||||
private static string? ResolvePublisher(JSPackageJson package)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(package.CmdPal?.Publisher))
|
||||
{
|
||||
return package.CmdPal.Publisher;
|
||||
}
|
||||
|
||||
return ExtractAuthorName(package.Author);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Extracts the author name from the npm "author" field. The field is either a string
|
||||
/// such as "Jane Doe <jane@example.com> (https://example.com)" or an object with a
|
||||
/// "name" property. For the string form, the substring before the first '<' or '('
|
||||
/// delimiter is taken. Returns null when no usable name can be determined.
|
||||
/// </summary>
|
||||
private static string? ExtractAuthorName(JsonElement? author)
|
||||
{
|
||||
if (author is not { } authorElement)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
switch (authorElement.ValueKind)
|
||||
{
|
||||
case JsonValueKind.String:
|
||||
var raw = authorElement.GetString();
|
||||
if (string.IsNullOrWhiteSpace(raw))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var end = raw.AsSpan().IndexOfAny('<', '(');
|
||||
var name = (end >= 0 ? raw[..end] : raw).Trim();
|
||||
return string.IsNullOrEmpty(name) ? null : name;
|
||||
|
||||
case JsonValueKind.Object:
|
||||
if (authorElement.TryGetProperty("name", out var nameElement) &&
|
||||
nameElement.ValueKind == JsonValueKind.String)
|
||||
{
|
||||
var objectName = nameElement.GetString();
|
||||
return string.IsNullOrWhiteSpace(objectName) ? null : objectName.Trim();
|
||||
}
|
||||
|
||||
return null;
|
||||
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static string? ResolveEntryPoint(string extensionDirectory, string entryPoint, out string? error)
|
||||
{
|
||||
error = null;
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Microsoft.CmdPal.UI.ViewModels.Models;
|
||||
@@ -30,6 +31,15 @@ public sealed record JSPackageJson
|
||||
[JsonPropertyName("description")]
|
||||
public string? Description { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the raw npm "author" field. It can be either a string
|
||||
/// (for example, "Jane Doe <jane@example.com> (https://example.com)")
|
||||
/// or an object with "name", "email", and "url" properties. Only the name is
|
||||
/// used, and only when cmdpal.publisher is absent.
|
||||
/// </summary>
|
||||
[JsonPropertyName("author")]
|
||||
public JsonElement? Author { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the relative path to the entry point JavaScript file.
|
||||
/// </summary>
|
||||
|
||||
@@ -12,8 +12,6 @@ namespace Microsoft.CmdPal.UI.ViewModels.UnitTests;
|
||||
[TestClass]
|
||||
public class JSExtensionManifestTests
|
||||
{
|
||||
private static readonly string[] ExpectedCapabilities = ["commands"];
|
||||
|
||||
private string _testDirectory = null!;
|
||||
|
||||
[TestInitialize]
|
||||
@@ -47,7 +45,6 @@ public class JSExtensionManifestTests
|
||||
"displayName": "Sample Extension",
|
||||
"icon": "icon.png",
|
||||
"publisher": "sample-publisher",
|
||||
"capabilities": ["commands"],
|
||||
"debug": true,
|
||||
"debugPort": 9230
|
||||
}
|
||||
@@ -65,8 +62,6 @@ public class JSExtensionManifestTests
|
||||
Assert.AreEqual("Sample Extension", manifest.DisplayName);
|
||||
Assert.AreEqual("icon.png", manifest.Icon);
|
||||
Assert.AreEqual("sample-publisher", manifest.Publisher);
|
||||
Assert.IsNotNull(manifest.Capabilities);
|
||||
CollectionAssert.AreEqual(ExpectedCapabilities, manifest.Capabilities);
|
||||
Assert.IsTrue(manifest.Debug);
|
||||
Assert.AreEqual(9230, manifest.DebugPort);
|
||||
Assert.AreEqual(">=18", manifest.Engines!.Node);
|
||||
@@ -317,6 +312,81 @@ public class JSExtensionManifestTests
|
||||
}
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void TryParse_PublisherFromAuthorString_WhenNoCmdPalPublisher()
|
||||
{
|
||||
CreateEntryPoint("index.js");
|
||||
const string Json = """
|
||||
{
|
||||
"name": "author-string",
|
||||
"main": "index.js",
|
||||
"author": "Jane Doe <jane@example.com> (https://example.com)",
|
||||
"cmdpal": {}
|
||||
}
|
||||
""";
|
||||
|
||||
var result = JSExtensionManifest.TryParse(Json, _testDirectory);
|
||||
|
||||
Assert.IsTrue(result.IsValid, result.FailureReason);
|
||||
Assert.AreEqual("Jane Doe", result.Manifest!.Publisher);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void TryParse_PublisherFromAuthorObject_WhenNoCmdPalPublisher()
|
||||
{
|
||||
CreateEntryPoint("index.js");
|
||||
const string Json = """
|
||||
{
|
||||
"name": "author-object",
|
||||
"main": "index.js",
|
||||
"author": { "name": "Acme Corp", "email": "dev@acme.example", "url": "https://acme.example" },
|
||||
"cmdpal": {}
|
||||
}
|
||||
""";
|
||||
|
||||
var result = JSExtensionManifest.TryParse(Json, _testDirectory);
|
||||
|
||||
Assert.IsTrue(result.IsValid, result.FailureReason);
|
||||
Assert.AreEqual("Acme Corp", result.Manifest!.Publisher);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void TryParse_CmdPalPublisher_TakesPrecedenceOverAuthor()
|
||||
{
|
||||
CreateEntryPoint("index.js");
|
||||
const string Json = """
|
||||
{
|
||||
"name": "precedence",
|
||||
"main": "index.js",
|
||||
"author": "Author Name",
|
||||
"cmdpal": { "publisher": "cmdpal-publisher" }
|
||||
}
|
||||
""";
|
||||
|
||||
var result = JSExtensionManifest.TryParse(Json, _testDirectory);
|
||||
|
||||
Assert.IsTrue(result.IsValid, result.FailureReason);
|
||||
Assert.AreEqual("cmdpal-publisher", result.Manifest!.Publisher);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void TryParse_NoPublisherAndNoAuthor_LeavesPublisherNull()
|
||||
{
|
||||
CreateEntryPoint("index.js");
|
||||
const string Json = """
|
||||
{
|
||||
"name": "no-publisher",
|
||||
"main": "index.js",
|
||||
"cmdpal": {}
|
||||
}
|
||||
""";
|
||||
|
||||
var result = JSExtensionManifest.TryParse(Json, _testDirectory);
|
||||
|
||||
Assert.IsTrue(result.IsValid, result.FailureReason);
|
||||
Assert.IsNull(result.Manifest!.Publisher);
|
||||
}
|
||||
|
||||
private void CreateEntryPoint(string relativePath)
|
||||
{
|
||||
var fullPath = Path.Combine(_testDirectory, relativePath.Replace('/', Path.DirectorySeparatorChar));
|
||||
|
||||
Reference in New Issue
Block a user