Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions SharpSite.sln
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SharpSite.E2E", "e2e\SharpS
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SharpSite.Tests.Plugins", "tests\SharpSite.Tests.Plugins\SharpSite.Tests.Plugins.csproj", "{6B629CEE-5AAC-4885-89C6-7BED9DA7CF2C}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SharpSite.PluginPacker", "src\SharpSite.PluginPacker\SharpSite.PluginPacker.csproj", "{677B59E7-C4BA-4024-84D7-78CE6985F3F5}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "2. Tools", "2. Tools", "{78F974E0-8074-0543-93D5-DC2AAC8BF3DF}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -126,6 +130,10 @@ Global
{6B629CEE-5AAC-4885-89C6-7BED9DA7CF2C}.Debug|Any CPU.Build.0 = Debug|Any CPU
{6B629CEE-5AAC-4885-89C6-7BED9DA7CF2C}.Release|Any CPU.ActiveCfg = Release|Any CPU
{6B629CEE-5AAC-4885-89C6-7BED9DA7CF2C}.Release|Any CPU.Build.0 = Release|Any CPU
{677B59E7-C4BA-4024-84D7-78CE6985F3F5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{677B59E7-C4BA-4024-84D7-78CE6985F3F5}.Debug|Any CPU.Build.0 = Debug|Any CPU
{677B59E7-C4BA-4024-84D7-78CE6985F3F5}.Release|Any CPU.ActiveCfg = Release|Any CPU
{677B59E7-C4BA-4024-84D7-78CE6985F3F5}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand All @@ -147,6 +155,7 @@ Global
{BA24379C-40D5-5EDF-63BE-CE5BC727E45D} = {3266CA51-9816-4037-9715-701EB6C2928A}
{EFCFB571-6B0C-35CD-6664-160CA5B39244} = {8779454A-1F9C-4705-8EE0-5980C6B9C2A5}
{6B629CEE-5AAC-4885-89C6-7BED9DA7CF2C} = {3266CA51-9816-4037-9715-701EB6C2928A}
{677B59E7-C4BA-4024-84D7-78CE6985F3F5} = {78F974E0-8074-0543-93D5-DC2AAC8BF3DF}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {62A15C13-360B-4791-89E9-1FDDFE483970}
Expand Down
25 changes: 25 additions & 0 deletions src/SharpSite.PluginPacker/ArgumentParser.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
namespace SharpSite.PluginPacker;

public static class ArgumentParser
{
public static (string? inputPath, string? outputPath) ParseArguments(string[] args)
{
string? inputPath = null;
string? outputPath = null;
for (int i = 0; i < args.Length; i++)
{
switch (args[i])
{
case "-i":
case "--input":
if (i + 1 < args.Length) inputPath = args[++i];
break;
case "-o":
case "--output":
if (i + 1 < args.Length) outputPath = args[++i];
break;
}
}
return (inputPath, outputPath);
}
}
40 changes: 40 additions & 0 deletions src/SharpSite.PluginPacker/ManifestHandler.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
using System.Text.Json;
using System.Text.Json.Serialization;
using SharpSite.Plugins;

namespace SharpSite.PluginPacker;

public static class ManifestHandler
{
private static readonly JsonSerializerOptions _Opts = new()
{
WriteIndented = true,
Converters = { new JsonStringEnumConverter() }
};

public static PluginManifest? LoadOrCreateManifest(string inputPath)
{
string manifestPath = Path.Combine(inputPath, "manifest.json");
PluginManifest? manifest;
if (!File.Exists(manifestPath))
{
Console.WriteLine($"manifest.json not found in {inputPath}.");
Console.WriteLine("Let's create one interactively.");
manifest = ManifestPrompter.PromptForManifest();
var json = JsonSerializer.Serialize(manifest, _Opts);
File.WriteAllText(manifestPath, json);
Console.WriteLine($"Created manifest.json at {manifestPath}");
}
else
{
var json = File.ReadAllText(manifestPath);
manifest = JsonSerializer.Deserialize<PluginManifest>(json, _Opts);
if (manifest is null)
{
Console.WriteLine("Failed to parse manifest.json");
return null;
}
}
return manifest;
}
}
69 changes: 69 additions & 0 deletions src/SharpSite.PluginPacker/ManifestPrompter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
using SharpSite.Plugins;

namespace SharpSite.PluginPacker;

public static class ManifestPrompter
{
private static string PromptRequired(string label)
{
string? value;
do
{
Console.Write($"{label}: ");
value = Console.ReadLine()?.Trim();
if (string.IsNullOrWhiteSpace(value))
{
Console.WriteLine($"{label} is required.");
}
} while (string.IsNullOrWhiteSpace(value));
return value;
}

public static PluginManifest PromptForManifest()
{
var id = PromptRequired("Id");
var displayName = PromptRequired("DisplayName");
var description = PromptRequired("Description");
var version = PromptRequired("Version");
var published = PromptRequired("Published (yyyy-MM-dd)");
var supportedVersions = PromptRequired("SupportedVersions");
var author = PromptRequired("Author");
var contact = PromptRequired("Contact");
var contactEmail = PromptRequired("ContactEmail");
var authorWebsite = PromptRequired("AuthorWebsite");

// Optional fields
Console.Write("Icon (URL): ");
var icon = (Console.ReadLine() ?? "").Trim();
Console.Write("Source (repository URL): ");
var source = (Console.ReadLine() ?? "").Trim();
Console.Write("KnownLicense (e.g. MIT, Apache, LGPL): ");
var knownLicense = (Console.ReadLine() ?? "").Trim();
Console.Write("Tags (comma separated): ");
var tagsStr = (Console.ReadLine() ?? "").Trim();
var tags = tagsStr.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);

Console.Write("Features (comma separated, e.g. Theme,FileStorage): ");
var featuresStr = (Console.ReadLine() ?? "").Trim();
var features = featuresStr.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
var featureEnums = features.Length > 0 ? Array.ConvertAll(features, f => Enum.Parse<PluginFeatures>(f, true)) : [];
return new PluginManifest
{
Id = id,
DisplayName = displayName,
Description = description,
Version = version,
Icon = string.IsNullOrWhiteSpace(icon) ? null : icon,
Published = published,
SupportedVersions = supportedVersions,
Author = author,
Contact = contact,
ContactEmail = contactEmail,
AuthorWebsite = authorWebsite,
Source = string.IsNullOrWhiteSpace(source) ? null : source,
KnownLicense = string.IsNullOrWhiteSpace(knownLicense) ? null : knownLicense,
Tags = tags.Length > 0 ? tags : null,
Features = featureEnums
};
}
}
142 changes: 142 additions & 0 deletions src/SharpSite.PluginPacker/PluginPackager.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
using System.Diagnostics;
using System.IO.Compression;
using SharpSite.Plugins;

namespace SharpSite.PluginPacker;

public static class PluginPackager
{
public static bool PackagePlugin(string inputPath, string outputPath)
{
// Load manifest
var manifest = ManifestHandler.LoadOrCreateManifest(inputPath);
if (manifest is null)
{
Console.WriteLine("Manifest not found or invalid.");
return false;
}

// Create temp build output folder
string tempBuildDir = Path.Combine(Path.GetTempPath(), "SharpSitePluginBuild_" + Guid.NewGuid().ToString("N"));
Directory.CreateDirectory(tempBuildDir);

// Build the project in Release mode to temp build folder
if (!BuildProject(inputPath, tempBuildDir))
{
Console.WriteLine("Build failed.");
try { if (Directory.Exists(tempBuildDir)) Directory.Delete(tempBuildDir, true); } catch { }
return false;
}

// Create temp folder for packaging
string tempDir = Path.Combine(Path.GetTempPath(), "SharpSitePluginPack_" + Guid.NewGuid().ToString("N"));
Directory.CreateDirectory(tempDir);
try
{
// Copy DLL to lib/ and rename
CopyAndRenameDll(inputPath, tempBuildDir, tempDir, manifest);

// If Theme, copy .css from wwwroot/ to web/
if (manifest.Features.Contains(PluginFeatures.Theme))
{
CopyThemeCssFiles(inputPath, tempDir);
}
// Copy manifest.json and other required files
CopyRequiredFiles(inputPath, tempDir);
// Zip tempDir to outputPath - use proper naming convention ID@VERSION.sspkg
// outputPath is always a directory, generate the filename from manifest
string outFile = Path.Combine(outputPath, $"{manifest.IdVersionToString()}.sspkg");

// Ensure the output directory exists
if (!Directory.Exists(outputPath))
{
Directory.CreateDirectory(outputPath);
}

if (File.Exists(outFile)) File.Delete(outFile);
ZipFile.CreateFromDirectory(tempDir, outFile);
Console.WriteLine($"Plugin packaged successfully: {outFile}");
return true;
}
catch (Exception ex)
{
Console.WriteLine($"Packaging failed: {ex.Message}");
return false;
}
finally
{
// Clean up temp folder
try { if (Directory.Exists(tempDir)) Directory.Delete(tempDir, true); } catch { }
try { if (Directory.Exists(tempBuildDir)) Directory.Delete(tempBuildDir, true); } catch { }
}
}

private static void CopyAndRenameDll(string inputPath, string tempBuildDir, string tempDir, PluginManifest manifest)
{
string libDir = Path.Combine(tempDir, "lib");
Directory.CreateDirectory(libDir);
string projectName = new DirectoryInfo(inputPath).Name;
string dllSource = Path.Combine(tempBuildDir, projectName + ".dll");
string dllTarget = Path.Combine(libDir, manifest.Id + ".dll");
if (!File.Exists(dllSource))
{
throw new FileNotFoundException($"DLL not found: {dllSource}");
}
File.Copy(dllSource, dllTarget, overwrite: true);
}

private static void CopyThemeCssFiles(string inputPath, string tempDir)
{
string webSrc = Path.Combine(inputPath, "wwwroot");
string webDst = Path.Combine(tempDir, "web");
if (Directory.Exists(webSrc))
{
Directory.CreateDirectory(webDst);
foreach (var css in Directory.GetFiles(webSrc, "*.css", SearchOption.AllDirectories))
{
string dest = Path.Combine(webDst, Path.GetFileName(css));
File.Copy(css, dest, overwrite: true);
}
}
}

private static void CopyRequiredFiles(string inputPath, string tempDir)
{
string[] requiredFiles = ["manifest.json", "LICENSE", "README.md", "Changelog.txt"];
foreach (var file in requiredFiles)
{
string src = Path.Combine(inputPath, file);
if (File.Exists(src))
{
File.Copy(src, Path.Combine(tempDir, file), overwrite: true);
}
}
}

private static bool BuildProject(string inputPath, string outputPath)
{
var psi = new ProcessStartInfo
{
FileName = "dotnet",
Arguments = $"build --configuration Release --output \"{outputPath}\"",
WorkingDirectory = inputPath,
RedirectStandardOutput = true,
RedirectStandardError = true,
UseShellExecute = false,
CreateNoWindow = true
};
using var proc = Process.Start(psi);
if (proc is null)
{
Console.WriteLine("Failed to start build process.");
return false;
}
proc.WaitForExit();
if (proc.ExitCode != 0)
{
Console.WriteLine(proc.StandardError.ReadToEnd());
return false;
}
return true;
}
}
45 changes: 45 additions & 0 deletions src/SharpSite.PluginPacker/Program.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
using SharpSite.PluginPacker;

(string? inputPath, string? outputPath) = ArgumentParser.ParseArguments(args);

if (string.IsNullOrWhiteSpace(inputPath))
{
Console.WriteLine("Usage: SharpSite.PluginPacker -i <input-folder> [-o <output-folder>]");
Console.WriteLine(" -i, --input Input folder containing the plugin project");
Console.WriteLine(" -o, --output Output directory (optional, defaults to current directory)");
Console.WriteLine();
Console.WriteLine("The output filename will be automatically generated as: ID@VERSION.sspkg");
return 1;
}

// Default to current directory if no output path specified
outputPath = string.IsNullOrWhiteSpace(outputPath) ? Directory.GetCurrentDirectory() : outputPath;

if (!Directory.Exists(inputPath))
{
Console.WriteLine($"Input directory '{inputPath}' does not exist.");
return 1;
}

// Validate that output path is a directory, not a file
if (File.Exists(outputPath))
{
Console.WriteLine($"Error: Output path '{outputPath}' points to a file. Please specify a directory.");
return 1;
}

var manifest = ManifestHandler.LoadOrCreateManifest(inputPath);
if (manifest is null)
{
Console.WriteLine("Failed to load or create manifest.");
return 1;
}
Console.WriteLine($"Loaded manifest for {manifest.DisplayName} ({manifest.Id})");

if (!PluginPackager.PackagePlugin(inputPath, outputPath))
{
Console.WriteLine("Packaging failed.");
return 1;
}

return 0;
14 changes: 14 additions & 0 deletions src/SharpSite.PluginPacker/SharpSite.PluginPacker.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<Project Sdk="Microsoft.NET.Sdk">

<ItemGroup>
<ProjectReference Include="..\SharpSite.Plugins\SharpSite.Plugins.csproj" />
</ItemGroup>

<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net9.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>

</Project>