|
3 | 3 | using System.Linq; |
4 | 4 | using System.Collections.Generic; |
5 | 5 | using System.Text.Json; |
| 6 | +using System.Text.Json.Serialization; |
| 7 | +using System.Xml.Linq; |
6 | 8 | using Serilog; |
7 | 9 | using Serilog.Sinks.SystemConsole.Themes; |
8 | 10 | using Nuke.Common; |
@@ -181,11 +183,175 @@ private static void LogHeader(string message) |
181 | 183 | // Application Build Targets |
182 | 184 | // ============================================================ |
183 | 185 |
|
| 186 | + /// <summary> |
| 187 | + /// Generate bundled-modules.json from extension.vsixmanifest files. |
| 188 | + /// This target scans all modules with IsBundled="true" and generates a JSON manifest |
| 189 | + /// that will be embedded in host applications for runtime seeding. |
| 190 | + /// </summary> |
| 191 | + Target GenerateBundledModules => _ => _ |
| 192 | + .DependsOn(Restore) |
| 193 | + .Description("Generate bundled-modules.json from extension.vsixmanifest files") |
| 194 | + .Executes(() => |
| 195 | + { |
| 196 | + LogHeader("Generating Bundled Modules Manifest"); |
| 197 | + |
| 198 | + var vsixNs = XNamespace.Get("http://schemas.microsoft.com/developer/vsx-schema/2011"); |
| 199 | + var modules = new List<BundledModuleJsonDto>(); |
| 200 | + |
| 201 | + // Scan all modules |
| 202 | + var moduleDirectories = Directory.GetDirectories(ModulesDirectory); |
| 203 | + foreach (var moduleDir in moduleDirectories) |
| 204 | + { |
| 205 | + var manifestPath = Path.Combine(moduleDir, "extension.vsixmanifest"); |
| 206 | + if (!File.Exists(manifestPath)) |
| 207 | + { |
| 208 | + LogWarning($"No extension.vsixmanifest in {Path.GetFileName(moduleDir)}, skipping"); |
| 209 | + continue; |
| 210 | + } |
| 211 | + |
| 212 | + try |
| 213 | + { |
| 214 | + var doc = XDocument.Load(manifestPath); |
| 215 | + var root = doc.Root; |
| 216 | + if (root == null) continue; |
| 217 | + |
| 218 | + // Parse Identity |
| 219 | + var metadata = root.Element(vsixNs + "Metadata") ?? root.Element("Metadata"); |
| 220 | + var identity = metadata?.Element(vsixNs + "Identity") ?? metadata?.Element("Identity"); |
| 221 | + |
| 222 | + if (identity == null) |
| 223 | + { |
| 224 | + LogWarning($"No Identity element in {Path.GetFileName(moduleDir)}/extension.vsixmanifest"); |
| 225 | + continue; |
| 226 | + } |
| 227 | + |
| 228 | + // Check IsBundled attribute |
| 229 | + var isBundled = (string?)identity.Attribute("IsBundled"); |
| 230 | + if (!string.Equals(isBundled, "true", StringComparison.OrdinalIgnoreCase)) |
| 231 | + { |
| 232 | + LogNormal($"Module {Path.GetFileName(moduleDir)} is not bundled, skipping"); |
| 233 | + continue; |
| 234 | + } |
| 235 | + |
| 236 | + var moduleName = Path.GetFileName(moduleDir); |
| 237 | + LogHighlight($"Processing bundled module: {moduleName}"); |
| 238 | + |
| 239 | + // Parse module info |
| 240 | + var module = new BundledModuleJsonDto |
| 241 | + { |
| 242 | + Id = (string?)identity.Attribute("Id") ?? moduleName, |
| 243 | + Version = (string?)identity.Attribute("Version") ?? "1.0.0", |
| 244 | + Language = (string?)identity.Attribute("Language"), |
| 245 | + Publisher = (string?)identity.Attribute("Publisher"), |
| 246 | + DisplayName = metadata?.Element(vsixNs + "DisplayName")?.Value |
| 247 | + ?? metadata?.Element("DisplayName")?.Value |
| 248 | + ?? moduleName, |
| 249 | + Description = metadata?.Element(vsixNs + "Description")?.Value |
| 250 | + ?? metadata?.Element("Description")?.Value, |
| 251 | + Tags = metadata?.Element(vsixNs + "Tags")?.Value |
| 252 | + ?? metadata?.Element("Tags")?.Value, |
| 253 | + Website = metadata?.Element(vsixNs + "Website")?.Value |
| 254 | + ?? metadata?.Element("Website")?.Value, |
| 255 | + Path = $"Modules/{moduleName}", |
| 256 | + IsBundled = true |
| 257 | + }; |
| 258 | + |
| 259 | + // Parse Installation targets |
| 260 | + var installation = root.Element(vsixNs + "Installation") ?? root.Element("Installation"); |
| 261 | + if (installation != null) |
| 262 | + { |
| 263 | + var targets = installation.Elements(vsixNs + "InstallationTarget") |
| 264 | + .Concat(installation.Elements("InstallationTarget")); |
| 265 | + foreach (var target in targets) |
| 266 | + { |
| 267 | + var hostId = (string?)target.Attribute("Id"); |
| 268 | + if (!string.IsNullOrEmpty(hostId)) |
| 269 | + module.SupportedHosts.Add(hostId); |
| 270 | + } |
| 271 | + } |
| 272 | + |
| 273 | + // Parse Assets (menus) |
| 274 | + var assets = root.Element(vsixNs + "Assets") ?? root.Element("Assets"); |
| 275 | + if (assets != null) |
| 276 | + { |
| 277 | + var menuAssets = assets.Elements(vsixNs + "Asset") |
| 278 | + .Concat(assets.Elements("Asset")) |
| 279 | + .Where(a => (string?)a.Attribute("Type") == "Modulus.Menu"); |
| 280 | + |
| 281 | + foreach (var menuAsset in menuAssets) |
| 282 | + { |
| 283 | + var targetHost = (string?)menuAsset.Attribute("TargetHost") ?? "Modulus.Host.Avalonia"; |
| 284 | + var menu = new MenuJsonDto |
| 285 | + { |
| 286 | + Id = (string?)menuAsset.Attribute("Id") ?? "", |
| 287 | + DisplayName = (string?)menuAsset.Attribute("DisplayName") ?? "", |
| 288 | + Icon = (string?)menuAsset.Attribute("Icon") ?? "Folder", |
| 289 | + Route = (string?)menuAsset.Attribute("Route") ?? "", |
| 290 | + Location = (string?)menuAsset.Attribute("Location") ?? "Main", |
| 291 | + Order = int.TryParse((string?)menuAsset.Attribute("Order"), out var order) ? order : 0 |
| 292 | + }; |
| 293 | + |
| 294 | + if (!module.Menus.ContainsKey(targetHost)) |
| 295 | + module.Menus[targetHost] = new List<MenuJsonDto>(); |
| 296 | + |
| 297 | + module.Menus[targetHost].Add(menu); |
| 298 | + } |
| 299 | + } |
| 300 | + |
| 301 | + modules.Add(module); |
| 302 | + LogSuccess($"Added bundled module: {module.DisplayName} ({module.Id})"); |
| 303 | + } |
| 304 | + catch (Exception ex) |
| 305 | + { |
| 306 | + LogError($"Failed to parse {Path.GetFileName(moduleDir)}/extension.vsixmanifest: {ex.Message}"); |
| 307 | + } |
| 308 | + } |
| 309 | + |
| 310 | + // Generate JSON manifest |
| 311 | + var manifest = new BundledModulesManifestDto |
| 312 | + { |
| 313 | + GeneratedAt = DateTime.UtcNow, |
| 314 | + Configuration = Configuration.ToString(), |
| 315 | + Modules = modules |
| 316 | + }; |
| 317 | + |
| 318 | + var jsonOptions = new JsonSerializerOptions |
| 319 | + { |
| 320 | + WriteIndented = true, |
| 321 | + PropertyNamingPolicy = JsonNamingPolicy.CamelCase, |
| 322 | + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull |
| 323 | + }; |
| 324 | + |
| 325 | + var json = JsonSerializer.Serialize(manifest, jsonOptions); |
| 326 | + |
| 327 | + // Output to host projects' Resources directories |
| 328 | + var hostProjects = new[] |
| 329 | + { |
| 330 | + RootDirectory / "src" / "Hosts" / "Modulus.Host.Avalonia" / "Resources", |
| 331 | + RootDirectory / "src" / "Hosts" / "Modulus.Host.Blazor" / "Resources" |
| 332 | + }; |
| 333 | + |
| 334 | + foreach (var resourceDir in hostProjects) |
| 335 | + { |
| 336 | + Directory.CreateDirectory(resourceDir); |
| 337 | + var outputPath = resourceDir / "bundled-modules.json"; |
| 338 | + File.WriteAllText(outputPath, json); |
| 339 | + LogSuccess($"Generated: {outputPath}"); |
| 340 | + } |
| 341 | + |
| 342 | + LogHeader("BUNDLED MODULES SUMMARY"); |
| 343 | + LogNormal($"Total bundled modules: {modules.Count}"); |
| 344 | + foreach (var module in modules) |
| 345 | + { |
| 346 | + LogSuccess($" ✓ {module.DisplayName} ({module.Id}) - {module.Menus.Count} host(s)"); |
| 347 | + } |
| 348 | + }); |
| 349 | + |
184 | 350 | /// <summary> |
185 | 351 | /// Just compile the solution (default bin/Debug output) |
186 | 352 | /// </summary> |
187 | 353 | Target Compile => _ => _ |
188 | | - .DependsOn(Restore) |
| 354 | + .DependsOn(Restore, GenerateBundledModules) |
189 | 355 | .Description("Compile the solution") |
190 | 356 | .Executes(() => |
191 | 357 | { |
@@ -837,4 +1003,41 @@ private bool PackSinglePlugin(string pluginName) |
837 | 1003 | return false; |
838 | 1004 | } |
839 | 1005 | } |
| 1006 | + |
| 1007 | + // ============================================================ |
| 1008 | + // DTO classes for bundled-modules.json generation |
| 1009 | + // ============================================================ |
| 1010 | + |
| 1011 | + class BundledModulesManifestDto |
| 1012 | + { |
| 1013 | + public DateTime GeneratedAt { get; set; } |
| 1014 | + public string? Configuration { get; set; } |
| 1015 | + public List<BundledModuleJsonDto> Modules { get; set; } = new(); |
| 1016 | + } |
| 1017 | + |
| 1018 | + class BundledModuleJsonDto |
| 1019 | + { |
| 1020 | + public string Id { get; set; } = ""; |
| 1021 | + public string Version { get; set; } = "1.0.0"; |
| 1022 | + public string? Language { get; set; } |
| 1023 | + public string? Publisher { get; set; } |
| 1024 | + public string DisplayName { get; set; } = ""; |
| 1025 | + public string? Description { get; set; } |
| 1026 | + public string? Tags { get; set; } |
| 1027 | + public string? Website { get; set; } |
| 1028 | + public List<string> SupportedHosts { get; set; } = new(); |
| 1029 | + public string Path { get; set; } = ""; |
| 1030 | + public bool IsBundled { get; set; } = true; |
| 1031 | + public Dictionary<string, List<MenuJsonDto>> Menus { get; set; } = new(); |
| 1032 | + } |
| 1033 | + |
| 1034 | + class MenuJsonDto |
| 1035 | + { |
| 1036 | + public string Id { get; set; } = ""; |
| 1037 | + public string DisplayName { get; set; } = ""; |
| 1038 | + public string Icon { get; set; } = "Folder"; |
| 1039 | + public string Route { get; set; } = ""; |
| 1040 | + public string Location { get; set; } = "Main"; |
| 1041 | + public int Order { get; set; } |
| 1042 | + } |
840 | 1043 | } |
0 commit comments