There is no “one” design pattern when it comes to writing modules. The choices you make when building one largely depend on the size, complexity and use case. Rather than describe “How to build a module”, I have instead created a template module where I can throw in different ideas and test new styles. This can be found here and is well commented:
This serves as a starting point for me. Below are some of the features I have used. These are all optional allowing me to pick and choose what I want or need depending on the modules:
- Using InvokeBuild to Compile From Source
- CI Engine (Bamboo, AppVeyor)
- Pester Tests
- Generate Markdown Docs
- Format Files
Using InvokeBuild to Compile From Source
Simply, the idea is to write all your code in a neat maintainable way but then compile the source into a performant, only-what-you-need powershell module. I do this by keeping functions and classes in separate files contain in each folder:
- .\PSModuleTemplate\Classes – Contain each PS Class
- .\PSModuleTemplate\Public – Contain functions that will not be exported
- .\PSModuleTemplate\Private – Contain functions that will be exported
- .\PSModuleTemplate\PSModuleTemplate.psd1 – The module manifest
- .\PSModuleTemplate\PSModuleTemplate.psm1 – Temporary module loader
The module loader pulls together classes, private and public functions by dot sourcing (or using the using statement for classes). However this is only used to facilitate development. dot sourcing is expensive and can cause massive delays when loading modules with lots of functions.
Instead when I am happy with the source files I call Invoke-Build. This will action the steps found in .\PSModuleTemplate.build.ps1. This will compile each constituent script into a single psm1 file and update the module manifest accordingly straight into the PSModulePath. It is this compiled version of the module that you will want to deploy.
CI Engine (Bamboo, AppVeyor)
InvokeBuild works well locally to develop and test a module. But its true purpose is to facilitate Continous Integration. There are a few such CI Engines, I have only included AppVeyor and Bamboo out of preferences but in theory the PSModuleTemplate.build.ps1 can be used with any other CI.
I have configured GitHub with a commit web hook that will trigger AppVeyor for example to start a new build.
AppVeyor will now spin up a new virtual environment for me as configured in appveyor.yml. Here the same steps will be followed as when I compile, analyze and test the module locally with the addition of AppVeyor tracking and updating the build number in the ModuleManifest. Logically if the build suceeds you would want extra steps for deploying the module into production (or into PSGallery for the community) . At time of writing I have not had a chance to play with this.
…are a must. These are also called by InvokeBuild after the module is compiled to check for errors. Pester is a big topic that I intend to make a separate blog post on.
This is also called from Invoke-Build in order to check the standards of code are as high as expected. This is especially important when multiple people are working on the module. The level to which PSScriptAnalyzer checks your code is defined in .\ScriptAnalyzerSettings.ps1. Here you can specify what checks to enable/disable which will again depend on the project
Generate Markdown Docs
I find text based help written into each function itself is both help for to me and the end user (obviously) which is why i prefer this method. To get all that text based help out of the source code and into a pretty format for people to view is done by using PlatyPS. This is again called via Invoke-Build but is a separate standalone task that I will do before committing new work. At time of writing I have yet to decide on a way to automate this better but its serves the purpose of producing up to date documentation on demmand.
Format files are great for creating user friendly views. Create a file in the module folder called <ModuleName>.format.ps1xml. For info on all the controls you can use see: Writing a Windows Powershell Format File.
Instead of reloading the module you can force an update of any changes to this file by using
Update-formatdata –prependpath PathToFormattingFile
Below are a few examples of a simple formats.
<?xml version="1.0" encoding="utf-8" ?> <Configuration> <ViewDefinitions> <View> <Name>ComputerGroup</Name> <ViewSelectedBy> <TypeName>WSUS.ComputerGroup</TypeName> </ViewSelectedBy> <TableControl> <TableRowEntries> <TableRowEntry> <TableColumnItems> <TableColumnItem> <PropertyName>Name</PropertyName> </TableColumnItem> <TableColumnItem> <PropertyName>Id</PropertyName> </TableColumnItem> </TableColumnItems> </TableRowEntry> </TableRowEntries> </TableControl> </View> </ViewDefinitions> </Configuration>
<?xml version="1.0" encoding="utf-8" ?> <Configuration> <ViewDefinitions> <View> <Name>Provider</Name> <ViewSelectedBy> <TypeName>Provider</TypeName> </ViewSelectedBy> <ListControl> <ListEntries> <ListEntry> <ListItems> <ListItem> <PropertyName>Name</PropertyName> </ListItem> <ListItem> <PropertyName>Provider</PropertyName> </ListItem> </ListItems> </ListEntry> </ListEntries> </ListControl> </View> </ViewDefinitions> </Configuration>