diff --git a/.ai/prompts/create-plugin.md b/.ai/prompts/create-plugin.md index 886e9b043..c1632b860 100644 --- a/.ai/prompts/create-plugin.md +++ b/.ai/prompts/create-plugin.md @@ -41,7 +41,7 @@ Scaffold a new installable GrandNode plugin end to end, with the correct project 1. `SystemName` in `Manifest.cs` must equal the value in `{Feature}Defaults` and the output folder name. 2. `Group` must be one of the existing group names in `.ai/knowledge/plugin-types.md`. -3. All GrandNode project references must be `Private="false"`. +3. Import `src/Build/Grand.Plugin.props` for the shared host references; do not repeat them in the plugin. Anything referenced beyond that set must be `Private="false"`. 4. Use `Microsoft.NET.Sdk.Razor` when the plugin contains `.cshtml` files, `Microsoft.NET.Sdk` otherwise. 5. `Install()` saves default settings and adds localization resources, then calls `base.Install()` last. 6. `Uninstall()` deletes settings and removes localization resources, then calls `base.Uninstall()` last. diff --git a/.ai/standards/dependencies.md b/.ai/standards/dependencies.md index b03c9ab24..9736693f6 100644 --- a/.ai/standards/dependencies.md +++ b/.ai/standards/dependencies.md @@ -50,7 +50,8 @@ Do not override `TargetFramework` or `LangVersion` in an individual project. ## Project references -- Reference GrandNode projects with `false` in plugins and modules — the host already loads those assemblies. +- Plugins import `src/Build/Grand.Plugin.props`, which carries the host projects every plugin compiles against, each already `Private="false"`. Do not restate that list in a plugin — a new core project goes in the props file, so it reaches all plugins at once. +- Reference GrandNode projects with `false` in modules, and in a plugin for anything beyond the shared set — the host already loads those assemblies. `Private="false"` does not carry to a reference's own dependencies, so each project has to be named. - Use `all` (plugins referencing `Grand.Web` / `Grand.Web.Common`) or `runtime` (modules) following the nearest existing project of the same kind. - Never reference a plugin from core, business, or web projects. Dependencies point inward only. diff --git a/.ai/templates/plugin/base-plugin.md b/.ai/templates/plugin/base-plugin.md index 75aa3dd42..317bd58cc 100644 --- a/.ai/templates/plugin/base-plugin.md +++ b/.ai/templates/plugin/base-plugin.md @@ -13,6 +13,7 @@ Placeholders: `{SystemName}` = `{Group}.{Name}`, `{Feature}` = type-name prefix. ```xml + true false @@ -28,31 +29,6 @@ Placeholders: `{SystemName}` = `{Group}.{Name}`, `{Feature}` = type-name prefix. $(OutputPath) - - - false - - - false - - - false - - - false - - - false - - - false - - - false - all - - - Always @@ -61,7 +37,11 @@ Placeholders: `{SystemName}` = `{Group}.{Name}`, `{Feature}` = type-name prefix. ``` -Use `Microsoft.NET.Sdk` and drop the Razor properties when the plugin ships no `.cshtml`. Trim project references down to what the plugin actually uses. +Use `Microsoft.NET.Sdk` and drop the Razor properties when the plugin ships no `.cshtml`. + +`Grand.Plugin.props` brings in the host projects every plugin compiles against — `Grand.SharedKernel`, `Grand.Domain`, `Grand.Data`, `Grand.Mapping`, `Grand.Mediator`, `Grand.Infrastructure`, `Grand.Business.Core` and `Grand.Web.Common` — each with `Private=false` so nothing is copied into the plugin folder. Do not restate them in the plugin: they were duplicated across all 16 plugins until `Grand.Mediator` was added to the solution, added to none of them, and copied into every plugin's output. Add a new core project to `src/Build/Grand.Plugin.props`, not here. + +The unused references cost nothing at runtime, so there is no need to trim the list. A plugin needing something beyond that set — a NuGet package, or `Grand.Web` as `Theme.Modern` does — declares it in its own `ItemGroup`. ## 2. `Manifest.cs` diff --git a/src/Build/Grand.Plugin.props b/src/Build/Grand.Plugin.props new file mode 100644 index 000000000..d0ff5d048 --- /dev/null +++ b/src/Build/Grand.Plugin.props @@ -0,0 +1,50 @@ + + + + + true + + + + + + + + + + + + + + + + + diff --git a/src/Core/Grand.Infrastructure/Plugins/PluginInfoAttribute.cs b/src/Core/Grand.Infrastructure/Plugins/PluginInfoAttribute.cs index 4511ed7a3..aba23d772 100644 --- a/src/Core/Grand.Infrastructure/Plugins/PluginInfoAttribute.cs +++ b/src/Core/Grand.Infrastructure/Plugins/PluginInfoAttribute.cs @@ -1,22 +1,18 @@ -using System.Reflection; - -namespace Grand.Infrastructure.Plugins; +namespace Grand.Infrastructure.Plugins; [AttributeUsage(AttributeTargets.Assembly)] public class PluginInfoAttribute : Attribute { - public PluginInfoAttribute() - { - var assembly = Assembly.GetExecutingAssembly(); - var fullVersion = assembly.GetName().Version; - SupportedVersion = $"{fullVersion?.Major}.{fullVersion?.Minor}"; - } - public string Group { get; set; } = string.Empty; public string FriendlyName { get; set; } = string.Empty; public string SystemName { get; set; } = string.Empty; public string Author { get; set; } = string.Empty; + /// + /// The GrandNode version this plugin supports, as "Major.Minor". Left unset by convention - + /// it is then resolved from the assembly's Grand.Infrastructure reference by + /// . + /// public string SupportedVersion { get; set; } public string Version { get; set; } diff --git a/src/Core/Grand.Infrastructure/Plugins/PluginManager.cs b/src/Core/Grand.Infrastructure/Plugins/PluginManager.cs index 278806195..6265f94d9 100644 --- a/src/Core/Grand.Infrastructure/Plugins/PluginManager.cs +++ b/src/Core/Grand.Infrastructure/Plugins/PluginManager.cs @@ -254,7 +254,7 @@ private static PluginInfo PreparePluginInfo(FileInfo pluginFile) Group = pluginInfo.Group, SystemName = pluginInfo.SystemName, Version = pluginInfo.Version, - SupportedVersion = pluginInfo.SupportedVersion, + SupportedVersion = PluginVersionResolver.ResolveSupportedVersion(assembly, pluginInfo.SupportedVersion), Author = pluginInfo.Author, PluginFileName = plug.Name, OriginalAssemblyFile = pluginFile, diff --git a/src/Core/Grand.Infrastructure/Plugins/PluginVersionResolver.cs b/src/Core/Grand.Infrastructure/Plugins/PluginVersionResolver.cs new file mode 100644 index 000000000..c525b0285 --- /dev/null +++ b/src/Core/Grand.Infrastructure/Plugins/PluginVersionResolver.cs @@ -0,0 +1,35 @@ +using System.Reflection; + +namespace Grand.Infrastructure.Plugins; + +/// +/// Resolves which GrandNode version a plugin assembly was built against. +/// +public static class PluginVersionResolver +{ + private const string CoreAssemblyName = "Grand.Infrastructure"; + + /// + /// Returns the GrandNode version the plugin supports, as "Major.Minor". + /// + /// The plugin assembly the info attribute was read from + /// The version declared on , if any + /// + /// The declared version when the plugin states one, otherwise the version of the + /// reference it was compiled against. Null when neither is available - + /// such a plugin cannot be matched against and is + /// therefore treated as incompatible. + /// + public static string ResolveSupportedVersion(Assembly pluginAssembly, string declaredVersion) + { + if (!string.IsNullOrWhiteSpace(declaredVersion)) + return declaredVersion.Trim(); + + var coreReference = pluginAssembly?.GetReferencedAssemblies() + .FirstOrDefault(x => string.Equals(x.Name, CoreAssemblyName, StringComparison.OrdinalIgnoreCase)); + + return coreReference?.Version == null + ? null + : $"{coreReference.Version.Major}.{coreReference.Version.Minor}"; + } +} diff --git a/src/Modules/Grand.Module.Api/Grand.Module.Api.csproj b/src/Modules/Grand.Module.Api/Grand.Module.Api.csproj index 5bc94f45a..6abfa6983 100644 --- a/src/Modules/Grand.Module.Api/Grand.Module.Api.csproj +++ b/src/Modules/Grand.Module.Api/Grand.Module.Api.csproj @@ -16,6 +16,10 @@ False runtime + + False + runtime + False runtime diff --git a/src/Modules/Grand.Module.Installer/Grand.Module.Installer.csproj b/src/Modules/Grand.Module.Installer/Grand.Module.Installer.csproj index c23d05e9e..58b290707 100644 --- a/src/Modules/Grand.Module.Installer/Grand.Module.Installer.csproj +++ b/src/Modules/Grand.Module.Installer/Grand.Module.Installer.csproj @@ -24,6 +24,9 @@ False + + False + diff --git a/src/Modules/Grand.Module.Migration/Grand.Module.Migration.csproj b/src/Modules/Grand.Module.Migration/Grand.Module.Migration.csproj index be1148574..380b06646 100644 --- a/src/Modules/Grand.Module.Migration/Grand.Module.Migration.csproj +++ b/src/Modules/Grand.Module.Migration/Grand.Module.Migration.csproj @@ -20,6 +20,9 @@ False + + False + diff --git a/src/Modules/Grand.Module.ScheduledTasks/Grand.Module.ScheduledTasks.csproj b/src/Modules/Grand.Module.ScheduledTasks/Grand.Module.ScheduledTasks.csproj index 6eae10918..5018356d4 100644 --- a/src/Modules/Grand.Module.ScheduledTasks/Grand.Module.ScheduledTasks.csproj +++ b/src/Modules/Grand.Module.ScheduledTasks/Grand.Module.ScheduledTasks.csproj @@ -13,6 +13,10 @@ False runtime + + False + runtime + False runtime diff --git a/src/Plugins/Authentication.Facebook/Authentication.Facebook.csproj b/src/Plugins/Authentication.Facebook/Authentication.Facebook.csproj index d58e2f3ec..12a2a36e8 100644 --- a/src/Plugins/Authentication.Facebook/Authentication.Facebook.csproj +++ b/src/Plugins/Authentication.Facebook/Authentication.Facebook.csproj @@ -1,5 +1,6 @@  + true false @@ -19,37 +20,7 @@ - - - - - - - - - false - - - false - - - false - - - false - - - false - - - false - all - - - false - - diff --git a/src/Plugins/Authentication.Google/Authentication.Google.csproj b/src/Plugins/Authentication.Google/Authentication.Google.csproj index cf6d6ec07..072a9ef93 100644 --- a/src/Plugins/Authentication.Google/Authentication.Google.csproj +++ b/src/Plugins/Authentication.Google/Authentication.Google.csproj @@ -1,5 +1,6 @@  + true false @@ -18,37 +19,7 @@ - - - - - - - - - false - - - false - - - false - - - false - - - false - - - false - all - - - false - - diff --git a/src/Plugins/DiscountRules.Standard/DiscountRules.Standard.csproj b/src/Plugins/DiscountRules.Standard/DiscountRules.Standard.csproj index d99106c54..fd8214640 100644 --- a/src/Plugins/DiscountRules.Standard/DiscountRules.Standard.csproj +++ b/src/Plugins/DiscountRules.Standard/DiscountRules.Standard.csproj @@ -1,5 +1,6 @@  + true false @@ -15,30 +16,6 @@ $(OutputPath) - - - false - - - false - - - false - - - false - - - false - - - false - all - - - false - - Always diff --git a/src/Plugins/ExchangeRate.McExchange/ExchangeRate.McExchange.csproj b/src/Plugins/ExchangeRate.McExchange/ExchangeRate.McExchange.csproj index b7a9486b8..23e8be1ca 100644 --- a/src/Plugins/ExchangeRate.McExchange/ExchangeRate.McExchange.csproj +++ b/src/Plugins/ExchangeRate.McExchange/ExchangeRate.McExchange.csproj @@ -1,5 +1,6 @@  + ..\..\Web\Grand.Web\Plugins\ExchangeRate.McExchange\ @@ -11,30 +12,6 @@ $(OutputPath) - - - false - - - false - - - false - - - false - - - false - - - false - - - false - all - - diff --git a/src/Plugins/Payments.BrainTree/Payments.BrainTree.csproj b/src/Plugins/Payments.BrainTree/Payments.BrainTree.csproj index 96795784c..2b8789b69 100644 --- a/src/Plugins/Payments.BrainTree/Payments.BrainTree.csproj +++ b/src/Plugins/Payments.BrainTree/Payments.BrainTree.csproj @@ -1,5 +1,6 @@  + true false @@ -20,38 +21,7 @@ - - - - - - - - - - false - - - false - - - false - - - false - - - false - - - false - all - - - false - - diff --git a/src/Plugins/Payments.CashOnDelivery/Payments.CashOnDelivery.csproj b/src/Plugins/Payments.CashOnDelivery/Payments.CashOnDelivery.csproj index 4476dc110..fdc6f6863 100644 --- a/src/Plugins/Payments.CashOnDelivery/Payments.CashOnDelivery.csproj +++ b/src/Plugins/Payments.CashOnDelivery/Payments.CashOnDelivery.csproj @@ -1,5 +1,6 @@  + true false @@ -15,30 +16,6 @@ $(OutputPath) - - - false - - - false - - - false - - - false - - - false - - - false - all - - - false - - diff --git a/src/Plugins/Payments.StripeCheckout/Payments.StripeCheckout.csproj b/src/Plugins/Payments.StripeCheckout/Payments.StripeCheckout.csproj index cd4f0e44b..533e643d1 100644 --- a/src/Plugins/Payments.StripeCheckout/Payments.StripeCheckout.csproj +++ b/src/Plugins/Payments.StripeCheckout/Payments.StripeCheckout.csproj @@ -1,5 +1,6 @@ + true false @@ -19,38 +20,7 @@ - - - - - - - - - - false - - - false - - - false - - - false - - - false - - - false - - - false - all - - diff --git a/src/Plugins/Shipping.ByWeight/Shipping.ByWeight.csproj b/src/Plugins/Shipping.ByWeight/Shipping.ByWeight.csproj index e6e6277bc..7f1f31cc8 100644 --- a/src/Plugins/Shipping.ByWeight/Shipping.ByWeight.csproj +++ b/src/Plugins/Shipping.ByWeight/Shipping.ByWeight.csproj @@ -1,5 +1,6 @@  + true false @@ -15,30 +16,6 @@ $(OutputPath) - - - false - - - false - - - false - - - false - - - false - - - false - - - false - all - - diff --git a/src/Plugins/Shipping.FixedRateShipping/Shipping.FixedRateShipping.csproj b/src/Plugins/Shipping.FixedRateShipping/Shipping.FixedRateShipping.csproj index e0eb36d3b..b6586751e 100644 --- a/src/Plugins/Shipping.FixedRateShipping/Shipping.FixedRateShipping.csproj +++ b/src/Plugins/Shipping.FixedRateShipping/Shipping.FixedRateShipping.csproj @@ -1,5 +1,6 @@  + true false @@ -15,30 +16,6 @@ $(OutputPath) - - - false - - - false - - - false - - - false - - - false - - - false - - - false - all - - diff --git a/src/Plugins/Shipping.ShippingPoint/Shipping.ShippingPoint.csproj b/src/Plugins/Shipping.ShippingPoint/Shipping.ShippingPoint.csproj index 164635d1d..142598e9b 100644 --- a/src/Plugins/Shipping.ShippingPoint/Shipping.ShippingPoint.csproj +++ b/src/Plugins/Shipping.ShippingPoint/Shipping.ShippingPoint.csproj @@ -1,5 +1,6 @@  + true false @@ -15,30 +16,6 @@ $(OutputPath) - - - false - - - false - - - false - - - false - - - false - - - false - - - false - all - - diff --git a/src/Plugins/Tax.CountryStateZip/Tax.CountryStateZip.csproj b/src/Plugins/Tax.CountryStateZip/Tax.CountryStateZip.csproj index 1bcf0371c..e4944e7d8 100644 --- a/src/Plugins/Tax.CountryStateZip/Tax.CountryStateZip.csproj +++ b/src/Plugins/Tax.CountryStateZip/Tax.CountryStateZip.csproj @@ -1,5 +1,6 @@  + true false @@ -15,30 +16,6 @@ $(OutputPath) - - - false - - - false - - - false - - - false - - - false - - - false - - - false - all - - diff --git a/src/Plugins/Tax.FixedRate/Tax.FixedRate.csproj b/src/Plugins/Tax.FixedRate/Tax.FixedRate.csproj index 253057629..2316dea02 100644 --- a/src/Plugins/Tax.FixedRate/Tax.FixedRate.csproj +++ b/src/Plugins/Tax.FixedRate/Tax.FixedRate.csproj @@ -1,5 +1,6 @@  + true false @@ -15,30 +16,6 @@ $(OutputPath) - - - false - - - false - - - false - - - false - - - false - - - false - - - false - all - - diff --git a/src/Plugins/Theme.Modern/Theme.Modern.csproj b/src/Plugins/Theme.Modern/Theme.Modern.csproj index 4763b7070..043c5e1e0 100644 --- a/src/Plugins/Theme.Modern/Theme.Modern.csproj +++ b/src/Plugins/Theme.Modern/Theme.Modern.csproj @@ -1,5 +1,6 @@ + true false @@ -16,28 +17,6 @@ - - false - - - false - - - false - - - false - - - false - - - false - - - false - all - false all diff --git a/src/Plugins/Widgets.FacebookPixel/Widgets.FacebookPixel.csproj b/src/Plugins/Widgets.FacebookPixel/Widgets.FacebookPixel.csproj index ba5b7120b..ac023b1c0 100644 --- a/src/Plugins/Widgets.FacebookPixel/Widgets.FacebookPixel.csproj +++ b/src/Plugins/Widgets.FacebookPixel/Widgets.FacebookPixel.csproj @@ -1,5 +1,6 @@  + true false @@ -15,30 +16,6 @@ $(OutputPath) - - - false - - - false - - - false - - - false - - - false - - - false - - - false - all - - diff --git a/src/Plugins/Widgets.GoogleAnalytics/Widgets.GoogleAnalytics.csproj b/src/Plugins/Widgets.GoogleAnalytics/Widgets.GoogleAnalytics.csproj index f7c53f599..8fc0f29eb 100644 --- a/src/Plugins/Widgets.GoogleAnalytics/Widgets.GoogleAnalytics.csproj +++ b/src/Plugins/Widgets.GoogleAnalytics/Widgets.GoogleAnalytics.csproj @@ -1,5 +1,6 @@  + true false @@ -15,30 +16,6 @@ $(OutputPath) - - - false - - - false - - - false - - - false - - - false - - - false - - - false - all - - diff --git a/src/Plugins/Widgets.Slider/Widgets.Slider.csproj b/src/Plugins/Widgets.Slider/Widgets.Slider.csproj index c416b24c4..e9b6bdb28 100644 --- a/src/Plugins/Widgets.Slider/Widgets.Slider.csproj +++ b/src/Plugins/Widgets.Slider/Widgets.Slider.csproj @@ -1,5 +1,6 @@  + true false @@ -15,30 +16,6 @@ $(OutputPath) - - - false - - - false - - - false - - - false - - - false - - - false - - - false - all - - PreserveNewest diff --git a/src/Tests/Grand.Infrastructure.Tests/Plugins/PluginVersionResolverTests.cs b/src/Tests/Grand.Infrastructure.Tests/Plugins/PluginVersionResolverTests.cs new file mode 100644 index 000000000..bcf6a85c9 --- /dev/null +++ b/src/Tests/Grand.Infrastructure.Tests/Plugins/PluginVersionResolverTests.cs @@ -0,0 +1,79 @@ +using Grand.Infrastructure.Plugins; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using System.Reflection; + +namespace Grand.Infrastructure.Tests.Plugins; + +[TestClass] +public class PluginVersionResolverTests +{ + /// + /// Stands in for a plugin assembly: it is compiled against the same Grand.Infrastructure + /// as any real plugin, so its reference version is the one a plugin would carry. + /// + private static readonly Assembly PluginLikeAssembly = typeof(PluginVersionResolverTests).Assembly; + + [TestMethod] + public void ResolveSupportedVersion_AssemblyBuiltAgainstCurrentCore_MatchesSupportedPluginVersion() + { + var result = PluginVersionResolver.ResolveSupportedVersion(PluginLikeAssembly, null); + + Assert.AreEqual(GrandVersion.SupportedPluginVersion, result); + } + + [TestMethod] + public void ResolveSupportedVersion_DeclaredVersion_WinsOverAssemblyReference() + { + var result = PluginVersionResolver.ResolveSupportedVersion(PluginLikeAssembly, "1.0"); + + Assert.AreEqual("1.0", result); + Assert.AreNotEqual(GrandVersion.SupportedPluginVersion, result, + "A plugin declaring an old version must stay distinguishable from the current one - " + + "this is the comparison the compatibility gate relies on."); + } + + [TestMethod] + public void ResolveSupportedVersion_DeclaredVersionIsWhitespace_FallsBackToAssemblyReference() + { + var result = PluginVersionResolver.ResolveSupportedVersion(PluginLikeAssembly, " "); + + Assert.AreEqual(GrandVersion.SupportedPluginVersion, result); + } + + [TestMethod] + public void ResolveSupportedVersion_DeclaredVersionIsPadded_IsTrimmed() + { + var result = PluginVersionResolver.ResolveSupportedVersion(PluginLikeAssembly, " 2.3 "); + + Assert.AreEqual("2.3", result); + } + + [TestMethod] + public void ResolveSupportedVersion_AssemblyWithoutCoreReference_ReturnsNull() + { + //System.Private.CoreLib does not reference Grand.Infrastructure + var result = PluginVersionResolver.ResolveSupportedVersion(typeof(object).Assembly, null); + + Assert.IsNull(result, + "A plugin that cannot be tied to a core version must not be reported as compatible."); + } + + [TestMethod] + public void ResolveSupportedVersion_NoAssembly_ReturnsNull() + { + var result = PluginVersionResolver.ResolveSupportedVersion(null, null); + + Assert.IsNull(result); + } + + [TestMethod] + public void PluginInfoAttribute_DoesNotSelfAssignSupportedVersion() + { + //The attribute used to compute SupportedVersion in its constructor from + //Assembly.GetExecutingAssembly() - always Grand.Infrastructure, never the plugin - + //which made every plugin compare equal to GrandVersion.SupportedPluginVersion. + var attribute = new PluginInfoAttribute(); + + Assert.IsNull(attribute.SupportedVersion); + } +}