PowerShell Module Manifests: Version, Requirements, and Public Surface
Use a PowerShell module manifest to declare compatibility, dependencies, exports, and version metadata, then validate the packaged module.
A PowerShell module can be a script file, a folder of functions, a compiled assembly, or a combination of components. A module manifest is a .psd1 data file that describes how the module should be loaded and what it claims to support. It does not replace the implementation, but it creates a boundary between internal files and the module’s declared identity, version, prerequisites, and public surface.
Manifests are not required merely to load every module, but Microsoft requires a manifest for publishing a module to the PowerShell Gallery. Even for an internal module, the manifest gives automation a stable place to inspect requirements and exports before import. A small, reviewed manifest is preferable to relying on implicit export of every function currently present in a script module.
Keep the module layout explicit
A simple script module can keep public function definitions in a .psm1 and describe the module in a companion .psd1 file:
Acme.Inventory/
Acme.Inventory.psd1
Acme.Inventory.psm1
Public/
Get-InventoryItem.ps1
Private/
ConvertTo-InventoryRecord.ps1
The root module file is the manifest’s RootModule. If it is omitted, the manifest itself can be the primary file, creating a manifest module. When a root module is declared, the file extension influences the module type: script module (.psm1), binary module (.dll), or another supported component. Keep paths relative to the manifest so the module can be installed under different roots without hard-coded machine-specific locations.
@{
RootModule = 'Acme.Inventory.psm1'
ModuleVersion = '1.2.0'
GUID = '9a7cc327-6854-4d1f-8c94-a3acb0d9e188'
Author = 'Acme Engineering'
Description = 'Read-only inventory query functions.'
PowerShellVersion = '7.4'
FunctionsToExport = @('Get-InventoryItem')
CmdletsToExport = @()
AliasesToExport = @()
VariablesToExport = @()
}
This is an illustrative manifest, not a guarantee that the module implements every declared command. Every declared export should exist and be tested. A GUID identifies the module; generate one for your module rather than copying the sample value. ModuleVersion follows the manifest’s version syntax, and release processes should update it deliberately when compatibility or behavior changes.
Declare compatibility and dependencies conservatively
Use manifest metadata to state minimum PowerShell compatibility and required modules. A RequiredModules entry can identify a module by name and can constrain version or GUID. A manifest does not install missing dependencies or guarantee that a network repository is reachable. Import should fail in a way that lets deployment tooling report the missing prerequisite, while the package build should verify dependencies in the target environment.
@{
RootModule = 'Acme.Inventory.psm1'
ModuleVersion = '1.2.0'
GUID = '9a7cc327-6854-4d1f-8c94-a3acb0d9e188'
PowerShellVersion = '7.4'
RequiredModules = @(
@{
ModuleName = 'Microsoft.PowerShell.Utility'
ModuleVersion = '7.4.0'
}
)
FunctionsToExport = @('Get-InventoryItem')
}
Avoid declaring a dependency on a module merely because it happens to be present on the developer’s workstation. Test the module in a clean PowerShell process with only its documented dependencies installed. On Windows, PowerShell 7 and Windows PowerShell can have different module search roots and compatibility constraints; a module version available to one engine may not be loadable by the other.
The CompatiblePSEditions field can help describe edition compatibility, but metadata does not make an implementation portable. The code still needs tests on each supported operating system and PowerShell engine. For filesystem paths, native commands, registry access, WMI/CIM, and Windows-only APIs, either implement platform-aware behavior or declare the supported platform clearly in documentation.
Use export lists as an API boundary
Script modules export functions by default unless they restrict exports. That can make a newly added helper function unexpectedly callable by consumers. An explicit export list establishes the public interface and lets private implementation names change without silently becoming semver commitments.
# At the end of Acme.Inventory.psm1
Export-ModuleMember -Function Get-InventoryItem
Coordinate the manifest’s FunctionsToExport with Export-ModuleMember. A mismatch can make a command disappear from discovery or be exported differently than expected. Exported functions should have approved verb-noun names, useful parameter contracts, comment-based help, and tests. Keep private helper functions out of the public surface unless a real consumer need justifies exposing them.
The manifest can also list NestedModules, RequiredAssemblies, type or format data, and scripts to process during module import. These features affect loading behavior and can change the session beyond a function export. Type data and format data may have broad effects; keep import-time work predictable and avoid expensive initialization or network requests just because a user imported a module.
Validate the manifest and load path
Use Test-ModuleManifest before packaging. It can report an invalid manifest or requirements that the current session does not satisfy. Then import the module from a clean process and test its public commands, not only the manifest text.
$manifestPath = Resolve-Path './Acme.Inventory/Acme.Inventory.psd1'
$manifest = Test-ModuleManifest -Path $manifestPath -ErrorAction Stop
Import-Module -Name $manifestPath -Force -ErrorAction Stop
Get-Command -Module Acme.Inventory
Get-Help Get-InventoryItem -Full
The -Force switch during an isolated test helps reload after code changes, but should not hide state leakage in an already-running production session. Prefer a fresh pwsh -NoProfile process for repeatable module tests. Test required modules both present and absent, an incorrect manifest path, version metadata, and each public function’s ordinary and failure behavior.
Check the packaged layout itself. Relative paths must resolve from the manifest location after installation, not merely from the repository root. Ensure tests run against the built package rather than loading a source path whose neighboring files are not included in the release artifact. A useful release check copies the package to a clean temporary module directory, starts a fresh shell, imports by manifest path, and runs a short smoke test against the declared exports.
Version the public behavior, not just the file
Module version metadata should correspond to a release policy. If a parameter changes meaning, a previously exported function disappears, or a required PowerShell version increases, existing automation may break even if the module continues to import. Record changes in release notes and test representative callers. Do not publish by copying a folder over a live shared module directory without a rollback plan; a partial copy can leave a mixed package version.
Finally, manifests are PowerShell data files evaluated in a restricted language mode during import. Keep them declarative: use literal metadata and the supported manifest expressions, not arbitrary initialization logic. Validate them in CI and review dependency identifiers, version ranges, exports, and relative paths as part of the release. The manifest is executable loading metadata, so changes to it deserve the same code review as changes to the .psm1 itself.
Test the installed artifact, not just the source tree
Importing the module from the repository can hide packaging defects. A missing public script may be present in a developer checkout but absent from a built archive; a relative path may work only because the current directory happens to be the project root. Build the exact release artifact, extract it into a clean temporary module root, and run the manifest and command tests there.
$moduleRoot = Join-Path ([System.IO.Path]::GetTempPath()) 'AcmeModuleTest'
$null = New-Item -ItemType Directory -Path $moduleRoot -Force
# Copy the built package into $moduleRoot using the release pipeline's artifact.
$manifest = Join-Path $moduleRoot 'Acme.Inventory/Acme.Inventory.psd1'
Test-ModuleManifest -Path $manifest -ErrorAction Stop | Out-Null
pwsh -NoProfile -Command "Import-Module '$manifest' -ErrorAction Stop; Get-Command -Module Acme.Inventory"
In a real test harness, pass the manifest path as a structured argument rather than interpolating it into a command string, especially if the path can contain quotes. The sketch shows the isolation goal: the module should load from its packaged location in a fresh process. Assert the exported command names, module version, required PowerShell version, and a representative function result.
Also test an unsupported runtime and a missing required module. Those are expected deployment failures, not conditions to bypass by lowering the manifest requirement in CI. If the package is intended for both Windows PowerShell and PowerShell 7, use separate test jobs and separate module path diagnostics; sharing one installed module tree can mask edition-specific dependencies.
Review the manifest as part of the release diff. Check that RootModule resolves relative to the manifest, exported function names match the implementation, required-module constraints use the intended minimum or exact version semantics, and the declared PowerShell version matches the code’s actual syntax and APIs. A manifest that claims compatibility with an older runtime can allow installation and then fail only when a command is invoked, which is worse than a clear import-time refusal.
Keep package validation separate from command testing. Test-ModuleManifest catches manifest and requirement problems in the current session, but it does not prove every exported function works, that all data files are included in the archive, or that the module handles its supported operating systems. Follow it with a clean import, command discovery, help inspection, smoke tests, and a package-content check. For a module with optional dependencies, test both the supported optional and absent cases so that loading behavior stays predictable.
Related:
- PowerShell Splatting: Explicit Parameter Sets and Safe Forwarding
- PowerShell Module Autoloading: PSModulePath and Command Resolution
Sources: