Predictable PowerShell Modules with Solid Manifests: Clean Loads, Clear Exports
When you ship a PowerShell module, your users expect it to import cleanly, expose only supported commands, and behave consistently across environments. The key to that predictability is a solid module manifest (.psd1) paired with a focused module script (.psm1). In this guide, you’ll create a minimal, production-ready module, explicitly declare your exports, set minimum PowerShell versions and compatible editions, and verify your public surface. You’ll also see how to version, test, and automate releases so consuming your module is effortless and reliable.
Build a Minimal, Predictable Module
A PowerShell module has two essential pieces:
- .psm1: The module implementation (functions, private helpers, initialization).
- .psd1: The manifest that declares metadata and what the module publicly exports.
Start with a simple module that exports exactly one function. Import from the manifest (not the .psm1) so all metadata, compatibility, and exports apply consistently.
# Create a minimal module with clear exports
$root = Join-Path -Path (Get-Location) -ChildPath 'MyTools'
New-Item -ItemType Directory -Path $root -Force | Out-Null
$psm1 = Join-Path -Path $root -ChildPath 'MyTools.psm1'
Set-Content -Path $psm1 -Encoding utf8 -Value 'function Get-Greeting { param([string]$Name) "Hello, $Name" }'
$psd1 = Join-Path -Path $root -ChildPath 'MyTools.psd1'
New-ModuleManifest -Path $psd1 `
-RootModule 'MyTools.psm1' `
-ModuleVersion '1.0.0' `
-Author 'Morten Elmstrøm Hansen' `
-Description 'Utility commands with clear exports.' `
-FunctionsToExport @('Get-Greeting') `
-CompatiblePSEditions @('Core','Desktop') `
-PowerShellVersion '5.1'
# Always import from the manifest so metadata/exports apply
Import-Module -Name $psd1 -Force
# Verify the public surface area
Get-Command -Module MyToolsWhy import via manifest? The manifest is the source of truth for exports, minimum engine version, and compatible editions. Users get a predictable load no matter how the environment is configured.
Files and folders that scale
For real-world usage, install your module in a versioned folder on a module path the shell knows about, such as:
# Windows PowerShell (Desktop):
$env:USERPROFILE\Documents\WindowsPowerShell\Modules\MyTools\1.0.0\
# PowerShell (Core):
$env:USERPROFILE\Documents\PowerShell\Modules\MyTools\1.0.0\Place MyTools.psd1 and MyTools.psm1 in that versioned directory. Users can then simply run Import-Module MyTools or specify versions precisely.
Lock Down Your Public Surface and Compatibility
Declare exports explicitly
Replace wildcard exports with explicit lists. This avoids accidental leakage of helper functions and keeps your API surface stable. Use the manifest keys FunctionsToExport, CmdletsToExport, VariablesToExport, and AliasesToExport.
# Inside MyTools.psm1
function Get-Greeting {
[CmdletBinding()]
param([Parameter(Mandatory)][string]$Name)
"Hello, $Name"
}
# Private helper not exported
function Write-Trace {
param([string]$Message)
Write-Verbose "[trace] $Message"
}# Inside MyTools.psd1 (excerpt)
@{
RootModule = 'MyTools.psm1'
ModuleVersion = '1.0.0'
GUID = '00000000-0000-0000-0000-000000000001'
Author = 'Your Name'
Description = 'Utility commands with clear exports.'
# Explicitly exported members
FunctionsToExport = @('Get-Greeting')
CmdletsToExport = @() # none
VariablesToExport = @() # none
AliasesToExport = @() # none
# Dependencies
RequiredModules = @() # add module specs if needed
RequiredAssemblies = @()
# Compatibility controls
PowerShellVersion = '5.1'
CompatiblePSEditions = @('Core', 'Desktop')
}With explicit exports, a new helper function won’t suddenly become public and break users who rely on your module’s stable surface.
Set minimum versions and editions
Use PowerShellVersion to set the minimum engine version and CompatiblePSEditions to restrict to Desktop (Windows PowerShell) and/or Core (cross-platform). This prevents your module from loading in unsupported environments, saving your users from confusing runtime errors.
- PowerShellVersion: Gate features that require newer engines (e.g., PS 7+ pipeline binding behavior or APIs).
- CompatiblePSEditions: Avoid platform pitfalls (e.g., Windows-only APIs) by explicitly supporting only
Desktopwhen necessary.
Complement the manifest with #requires in your .psm1 for critical checks:
#requires -Version 5.1
#requires -PSEdition Core#requires provides immediate feedback during dot-sourcing or development; the manifest enforces at import time for users.
Validate your public surface
Ensure only expected commands are exported and supported:
# Confirm exports match the manifest
Get-Command -Module MyTools | Select-Object Name, CommandType
# Validate manifest
Test-ModuleManifest -Path $psd1
# Try a real call
Get-Greeting -Name 'Ada'For automated enforcement, add a small Pester test to your CI pipeline:
# In tests\PublicSurface.Tests.ps1
Describe 'Public surface' {
BeforeAll {
Import-Module MyTools -Force
}
It 'exports the expected functions' {
$expected = @('Get-Greeting')
$actual = (Get-Command -Module MyTools -CommandType Function).Name | Sort-Object
$actual | Should -BeExactly $expected
}
}This test will fail if a helper leaks into the public API or a public function goes missing.
Versioning, Distribution, and Automation
Use SemVer and versioned folders
Adopt semantic versioning (MAJOR.MINOR.PATCH):
- Increment PATCH for bug fixes that don’t change the API.
- Increment MINOR for new backward-compatible features.
- Increment MAJOR for breaking changes to the public API.
Install modules under MyTools\1.0.0\ to support side-by-side installs. Consumers can pin precisely:
# Pin to an exact version
Import-Module -FullyQualifiedName @{ ModuleName = 'MyTools'; RequiredVersion = '1.0.0' }
# Or specify minimum/maximum when using RequiredModules in your own manifest
# RequiredModules = @(@{ ModuleName = 'MyTools'; ModuleVersion = '1.0.0' })Package and publish with confidence
When you're ready to share:
- Local install: Copy to your user or system module path (
$env:PSModulePath). - Private feeds: Use an internal NuGet feed or Artifactory.
- PowerShell Gallery: Use
Publish-Modulefor public distribution.
# Prepare and publish
Update-ModuleManifest -Path .\MyTools\1.0.0\MyTools.psd1 -ModuleVersion '1.0.1'
Publish-Module -Path .\MyTools\1.0.0 -NuGetApiKey $env:PSGALLERY_KEYConsumers can install or cache versions predictably:
# End-users
Install-Module MyTools -Scope CurrentUser
# CI environments that pin versions
Save-Module -Name MyTools -RequiredVersion 1.0.1 -Path .\Modules
Import-Module -Name .\Modules\MyTools\1.0.1\MyTools.psd1Automate quality gates
Build a small but effective pipeline that enforces module hygiene:
- Static analysis:
Invoke-ScriptAnalyzerto catch style and correctness issues. - Unit tests: Pester tests for function behavior and exported surface.
- Manifest validation:
Test-ModuleManifeston every build. - Signing (optional but recommended): Code-sign the
.psm1and any scripts for constrained environments. - Version bump + changelog: Automate SemVer increments and release notes.
# Example CI step (PowerShell 7)
$ErrorActionPreference = 'Stop'
Invoke-ScriptAnalyzer -Path . -Recurse -Severity Warning, Error
Invoke-Pester -CI
Test-ModuleManifest -Path .\MyTools\1.0.0\MyTools.psd1 | Out-NullReal-world tips
- Prefer manifest imports: Always import via
.psd1to apply exports and metadata. - Keep helpers private: Prefix helpers with
_or place them in aPrivatefolder and dot-source inside the.psm1. - Don’t export everything: Set
*ToExportlists explicitly; avoid'*'. - Set clear compatibility:
PowerShellVersionandCompatiblePSEditionsprevent unsupported loads. - Lock dependencies: Use
RequiredModuleswith version bounds to stabilize transitive behavior. - Use
-Forcejudiciously: It’s fine in dev/test; avoid in production automation unless you know why you need it.
Putting it together
With a disciplined manifest and explicit exports, your modules load predictably, keep public APIs tight, and support reliable automation. Versioned releases and CI checks ensure your consumers get consistent behavior—no surprises from accidental exports or environment differences.
Strengthen your module hygiene further with deep dives, patterns, and advanced techniques in the PowerShell Advanced CookBook: https://www.amazon.com/PowerShell-Advanced-Cookbook-scripting-advanced-ebook/dp/B0D5CPP2CQ/
What you get: clear exports, versioned releases, easier installs, and predictable loads.