diff --git a/samples/CG.Blazor.Plugins.QuickStart/CG.Blazor.Plugins.QuickStart.csproj b/samples/CG.Blazor.Plugins.QuickStart/CG.Blazor.Plugins.QuickStart.csproj index 9964db9..edaefbc 100644 --- a/samples/CG.Blazor.Plugins.QuickStart/CG.Blazor.Plugins.QuickStart.csproj +++ b/samples/CG.Blazor.Plugins.QuickStart/CG.Blazor.Plugins.QuickStart.csproj @@ -1,4 +1,4 @@ - + net6.0 diff --git a/samples/CG.Blazor.Plugins.QuickStart/appsettings.json b/samples/CG.Blazor.Plugins.QuickStart/appsettings.json index acb0a22..a11e123 100644 --- a/samples/CG.Blazor.Plugins.QuickStart/appsettings.json +++ b/samples/CG.Blazor.Plugins.QuickStart/appsettings.json @@ -7,17 +7,18 @@ } }, "AllowedHosts": "*", - "Plugins.1": { + "Plugins": { "Modules": [ { "AssemblyNameOrPath": "../CG.Blazor.Plugins.TestPlugin/bin/Debug/net6.0/CG.Blazor.Plugins.TestPlugin.dll", - "AssemblyNameOrPath.1": "CG.Blazor.Plugins.TestPlugin", + "AssemblyNameOrPath.1": "C:/CG.Blazor.Plugins/samples/CG.Blazor.Plugins.TestPlugin/bin/Debug/net6.0/CG.Blazor.Plugins.TestPlugin.dll", + "AssemblyNameOrPath.2": "CG.Blazor.Plugins.TestPlugin", "IsDisabled": false, - "IsRouted": true + "IsRouted": true, + "Scripts": [ + "/scripts/utility.js" + ] } ] - }, - "Plugins": { - "Modules": [] } } diff --git a/samples/CG.Blazor.Plugins.TestPlugin/CG.Blazor.Plugins.TestPlugin.csproj b/samples/CG.Blazor.Plugins.TestPlugin/CG.Blazor.Plugins.TestPlugin.csproj index 13e145b..54b8d5d 100644 --- a/samples/CG.Blazor.Plugins.TestPlugin/CG.Blazor.Plugins.TestPlugin.csproj +++ b/samples/CG.Blazor.Plugins.TestPlugin/CG.Blazor.Plugins.TestPlugin.csproj @@ -3,9 +3,14 @@ enable net6.0 + true + + + + @@ -17,10 +22,7 @@ - - - - + diff --git a/samples/CG.Blazor.Plugins.TestPlugin/Pages/Component1.razor b/samples/CG.Blazor.Plugins.TestPlugin/Pages/Component1.razor index 7337c37..a6bf895 100644 --- a/samples/CG.Blazor.Plugins.TestPlugin/Pages/Component1.razor +++ b/samples/CG.Blazor.Plugins.TestPlugin/Pages/Component1.razor @@ -2,7 +2,7 @@

This page is contained within a Blazor plugin!

-

Don't believe me? Go set IsEnabled property to false, in the Plugins:Modules section of the appsettings.json file, and restart the app. You won't be able to get to this page then ...

+

Don't believe me? Go set IsDisabled property to false, in the Plugins:Modules section of the appsettings.json file, and restart the app. You won't be able to get to this page then ...

Dependent Class: @typeof(CG.Blazor.Plugins.TestPlugin.Dependency.Class1).FullName was resolved by the plugin loader, even though the Assembly.Load method initially failed to find the assembly in the @typeof(CG.Blazor.Plugins.TestPlugin.Dependency.Class1).Assembly.Location folder.

diff --git a/samples/CG.Blazor.Plugins.TestPlugin/wwwroot/scripts/utility.js b/samples/CG.Blazor.Plugins.TestPlugin/wwwroot/scripts/utility.js new file mode 100644 index 0000000..153271c --- /dev/null +++ b/samples/CG.Blazor.Plugins.TestPlugin/wwwroot/scripts/utility.js @@ -0,0 +1,124 @@ +// Generate a random number within a specified range +const getRandomNumberInRange = (lower = 0, upper = 10) => { + if (isNaN(lower) || isNaN(upper)) { + console.error("lower and upper must be valid numbers") + return + } + lower = Math.ceil(lower) + upper = Math.floor(upper) + return Math.floor(Math.random() * (upper - lower + 1)) + lower +} + +// Do something x times. e.g. times(3, () => console.log("hello")) +const times = (times, func) => { + if (isNaN(times)) { + console.error("times to run must be specified") + return + } + if (typeof func !== "function") { + console.error(`func must be a valid function, ${typeof func} provided`) + return + } + Array.from(Array(times)).forEach(() => { + func() + }) +} + +// Shorten a string with ellipsis. e.g. shorten("I am some text", 4, 2) => I am.. +const shorten = (text, length = 10, ellipsisCount = 3) => { + if (!(typeof text === "string" || text instanceof String)) { + console.error(`expecting a string, ${typeof text} provided`) + return "" + } + if (isNaN(length) || isNaN(ellipsisCount)) { + console.error("length and ellipsisCount must be valid numbers") + return + } + + if (text.length <= length) { + return text + } + const ellipsis = Array.from(Array(ellipsisCount)).map(() => ".").join("") + return `${text.substr(0, length)}${ellipsis}` +} + +// Remove duplicates from an array. e.g. removeDuplicates(["Tom", "Simon", "Tom", "Sarah"]) => ["Tom", "Simon", "Sarah"] +const removeDuplicates = (arr) => { + if (!Array.isArray(arr)) { + console.error(`array expected, ${typeof arr} provided`) + return + } + return [...new Set(arr)] +} + +// Debounce (or delay) a function +const debounce = (func, timeout = 2500) => { + if (typeof func !== "function") { + console.error(`func must be a valid function, ${typeof func} provided`) + return + } + let timer + return (...args) => { + clearTimeout(timer) + timer = setTimeout(() => { + func.apply(this, args) + }, timeout) + } +} + +// Measure the performance/time taken of a function. e.g. measureTime("findPeople", someExpensiveFindPeopleFunction) => findPeople: 13426.336181640625ms +const measureTime = (func, label = "default") => { + if (typeof func !== "function") { + console.error(`func must be a valid function, ${typeof func} provided`) + return + } + console.time(label) + func() + console.timeEnd(label) +} + +// Slugify a string. e.g. slugify("Hello, everyone!") => hello-everyone +const slugify = (text) => { + if (!(typeof text === "string" || text instanceof String)) { + console.error(`string expected, ${typeof str} provided`) + return str + } + return text.toLowerCase() + .replace(/ /g, "-") + .replace(/[^\w-]+/g, "") +} + +// Camel case to snake case. e.g. camelToSnakeCase("camelCaseToSnakeCase") => camel_case_to_snake_case +const camelToSnakeCase = (text) => { + if (!(typeof text === "string" || text instanceof String)) { + console.error(`string expected, ${typeof text} provided`) + return text + } + return text.replace(/[A-Z]/g, (letter) => `_${letter.toLowerCase()}`) +} + +// Snake case to camel case. e.g. snakeToCamelCase("snake_case_to_camel_case") => snakeCaseToCamelCase +const snakeToCamelCase = (text) => { + if (!(typeof text === "string" || text instanceof String)) { + console.error(`string expected, ${typeof text} provided`) + return text + } + text + .toLowerCase() + .replace(/([-_][a-z])/g, group => + group + .toUpperCase() + .replace("-", "") + .replace("_", "") + ) +} + +// Validate an email address. e.g. emailIsValid("somebody@somewhere.com") => true | emailIsValid("nobody@nowhere") => false +const emailIsValid = (email) => { + if (!(typeof email === "string" || email instanceof String)) { + console.error(`string expected, ${typeof email} provided`) + return false + } + const expression = /\S+@\S+\.\S+/ + return expression.test(email) +} \ No newline at end of file diff --git a/src/CG.Blazor.Plugins/CG.Blazor.Plugins.csproj b/src/CG.Blazor.Plugins/CG.Blazor.Plugins.csproj index 3fd389e..6146dd8 100644 --- a/src/CG.Blazor.Plugins/CG.Blazor.Plugins.csproj +++ b/src/CG.Blazor.Plugins/CG.Blazor.Plugins.csproj @@ -8,10 +8,7 @@ true Martin Cook CodeGator - This package contains server side Blazor plugin extensions used by other CodeGator packages. - -Platforms supported: - .NET 6.x or above + This package contains server side Blazor plugin extensions used by other CodeGator packages. Platforms supported: .NET 6.x or above Copyright © 2018 - 2023 by CodeGator. All rights reserved. MIT https://github.com/CodeGator/CG.Blazor.Plugins diff --git a/src/CG.Blazor.Plugins/Extensions/WebApplicationBuilderExtensions.cs b/src/CG.Blazor.Plugins/Extensions/WebApplicationBuilderExtensions.cs index 6d8ddb4..6f94fdb 100644 --- a/src/CG.Blazor.Plugins/Extensions/WebApplicationBuilderExtensions.cs +++ b/src/CG.Blazor.Plugins/Extensions/WebApplicationBuilderExtensions.cs @@ -1,4 +1,6 @@  +using System.IO; + namespace Microsoft.Extensions.DependencyInjection; /// @@ -115,68 +117,54 @@ out var pluginOptions // If the AssemblyNameOrPath ends with a .dll then we'll assume the // property contains a path and treat it as such. - if (module.AssemblyNameOrPath.EndsWith(".dll")) + if (module.AssemblyNameOrPath.EndsWith(".dll", true, null)) { - // Log what we are about to do. - bootstrapLogger?.LogDebug( - "Deciding whether the assembly path is rooted, or " + - "not, for the plugin loader." - ); - - // Check for relative paths. - if (false == Path.IsPathRooted(module.AssemblyNameOrPath)) + try { // Log what we are about to do. bootstrapLogger?.LogDebug( - "Building a complete path to a plugin assembly, " + - "for the plugin loader" - ); - - // Expand the path (the load expects a rooted path). - var completePath = Path.GetFullPath( - module.AssemblyNameOrPath - ); - - // Log what we are about to do. - bootstrapLogger?.LogDebug( - "Loading assembly by path: {path}, for the plugin " + - "loader", - completePath - ); + "Deciding whether the assembly path is rooted, or " + + "not, for the plugin loader." + ); - // Load the assembly from the path. - asm = Assembly.LoadFile( - completePath - ); - } - else - { - try + // Check for relative paths. + if (false == Path.IsPathRooted(module.AssemblyNameOrPath) || Path.IsPathFullyQualified(module.AssemblyNameOrPath)) { // Log what we are about to do. bootstrapLogger?.LogDebug( - "Loading assembly by name: {name}, for the " + - "plugin loader", + "Building a complete path to a plugin assembly, " + + "for the plugin loader" + ); + + // Expand the path (the load expects a rooted path). + var completePath = Path.GetFullPath( module.AssemblyNameOrPath ); - // Load the assembly from the path. - asm = Assembly.Load( - new AssemblyName(module.AssemblyNameOrPath) + // Log what we are about to do. + bootstrapLogger?.LogDebug( + "Loading assembly by path: {path}, for the plugin " + + "loader", + completePath ); - } - catch (FileNotFoundException ex) - { - // Provide better context for the error. - throw new BlazorPluginException( - innerException: ex, - message: "When loading from an assembly name, remember that the " + - "assembly itself must have been loaded through a project reference. " + - "To dynamically load a plugin assembly, use a path to the assembly, " + - "instead of a name." + + // Load the assembly from the path. + asm = Assembly.LoadFile( + completePath ); } } + catch (FileNotFoundException ex) + { + // Provide better context for the error. + throw new BlazorPluginException( + innerException: ex, + message: "When loading from an assembly name, remember that the " + + "assembly itself must have been loaded through a project reference. " + + "To dynamically load a plugin assembly, use a path to the assembly, " + + "instead of a name." + ); + } } else { diff --git a/src/CG.Blazor.Plugins/Extensions/WebApplicationExtensions.cs b/src/CG.Blazor.Plugins/Extensions/WebApplicationExtensions.cs index a164c1d..8e32b97 100644 --- a/src/CG.Blazor.Plugins/Extensions/WebApplicationExtensions.cs +++ b/src/CG.Blazor.Plugins/Extensions/WebApplicationExtensions.cs @@ -56,6 +56,11 @@ this WebApplication webApplication var asmNameSet = new HashSet(); + // Get the plugin options. + var options = webApplication.Services.GetRequiredService< + IOptions + >(); + // Log what we are about to do. webApplication.Logger.LogDebug( "Building style sheet support for the plugin loader." @@ -63,6 +68,7 @@ this WebApplication webApplication // Add providers for any embedded style sheets. BuildStyleSheetProviders( + options, asmNameSet, allProviders, webApplication.Logger @@ -75,6 +81,7 @@ this WebApplication webApplication // Add providers for any embedded scripts. BuildScriptProviders( + options, asmNameSet, allProviders, webApplication.Logger @@ -83,12 +90,7 @@ this WebApplication webApplication // Log what we are about to do. webApplication.Logger.LogDebug( "Fetching the plugin options for the plugin loader." - ); - - // Get the plugin options. - var options = webApplication.Services.GetRequiredService< - IOptions - >(); + ); // Log what we are about to do. webApplication.Logger.LogDebug( @@ -181,12 +183,14 @@ this WebApplication webApplication /// collection, to read the static /// resource at runtime. /// + /// Plugin configuration /// The set of all previously processed plugin /// assemblies. /// The list of all previously added file /// providers. /// The logger to use for the operation. private static void BuildScriptProviders( + IOptions options, HashSet asmNameSet, List allProviders, ILogger logger @@ -259,10 +263,21 @@ ILogger logger "Fetching assembly reference, for the plugin loader." ); - // Get the assembly reference. - asm = Assembly.Load( - new AssemblyName(asmName) - ); + foreach (var module in options.Value.Modules) + { + if (module.AssemblyNameOrPath.Contains(asmName)) + { + if (module.AssemblyNameOrPath.EndsWith(".dll", true, null)) + { + var completePath = Path.GetFullPath(module.AssemblyNameOrPath); + asm = Assembly.LoadFile(completePath); + } + else + asm = Assembly.Load(new AssemblyName(asmName)); + + break; + } + } } catch (FileNotFoundException ex) { @@ -306,12 +321,14 @@ ILogger logger /// collection, to read the static /// resource at runtime. /// + /// Plugin configuration /// The set of all previously processed plugin /// assemblies. /// The list of all previously added file /// providers. /// The logger to use for the operation. private static void BuildStyleSheetProviders( + IOptions options, HashSet asmNameSet, List allProviders, ILogger logger @@ -384,10 +401,21 @@ ILogger logger "Fetching assembly reference, for the plugin loader." ); - // Get the assembly reference. - asm = Assembly.Load( - new AssemblyName(asmName) - ); + foreach (var module in options.Value.Modules) + { + if (module.AssemblyNameOrPath.Contains(asmName)) + { + if (module.AssemblyNameOrPath.EndsWith(".dll", true, null)) + { + var completePath = Path.GetFullPath(module.AssemblyNameOrPath); + asm = Assembly.LoadFile(completePath); + } + else + asm = Assembly.Load(new AssemblyName(asmName)); + + break; + } + } } catch (FileNotFoundException ex) { @@ -473,7 +501,7 @@ ILogger logger } // Is this module configured with a path? - if (module.AssemblyNameOrPath.EndsWith(".dll")) + if (module.AssemblyNameOrPath.EndsWith(".dll", true, null)) { // Strip out just the assembly file name. var fileName = Path.GetFileNameWithoutExtension( @@ -487,7 +515,7 @@ ILogger logger } // Check for relative paths. - if (false == Path.IsPathRooted(module.AssemblyNameOrPath)) + if (false == Path.IsPathRooted(module.AssemblyNameOrPath) || Path.IsPathFullyQualified(module.AssemblyNameOrPath)) { // Log what we are about to do. logger.LogDebug(