Building & Publishing Your Own Modules
PowerShell Intermediate/Advanced
Chapter 8 · Building & Publishing Your Own Modules
Fundamentals 11 covered consuming modules — Get-Module, Import-Module, installing one from the Gallery. This chapter is about being on the other side of that: turning a folder of your own functions into something someone else could genuinely Install-Module, complete with the one file most people skip when they're just experimenting, and the reason skipping it eventually causes real problems.
Two Files: .psm1 (Code) and .psd1 (Manifest)
A script module is just a .ps1 file renamed to .psm1, containing function definitions — nothing more is technically required to import and use one. A real, publishable module adds a manifest, a .psd1 file: pure metadata (version, author, which functions are actually meant to be public, required PowerShell version, dependencies) that PowerShell can read without ever running any of the module's own code.
Export-ModuleMember: Controlling What's Actually Public
Export-ModuleMember entirely, and every function defined in the .psm1 — including Get-InternalHelper, which was never meant to be part of any public API — becomes fully visible and callable by anyone who imports the module, discoverable through the exact same Get-Command Fundamentals 3 already covered. Explicitly listing only the functions actually meant to be public is what keeps a module's real interface honest.
Module Folder Structure & PSModulePath
A module named MyTools that doesn't sit inside a MyTools\ folder, on one of the paths $env:PSModulePath lists, simply won't be found by Import-Module or the auto-loading Fundamentals 11 already relied on.
New-ModuleManifest: Generating a Real Manifest
New-ModuleManifest generates a correctly structured .psd1 with all the right fields — far more reliable than hand-writing one from scratch and risking a typo in a key PowerShell's own tooling actually checks.
Publishing to the PowerShell Gallery
Publishing requires an account and a NuGet API key from powershellgallery.com — the same PSGallery Fundamentals 11 already covered from the consuming side, via Find-Module/Install-Module.
Publish-Module again with the identical ModuleVersion still set in the manifest fails outright, even for a genuinely tiny fix. The version has to be bumped first:
Major.Minor.Patch) is the expected convention — a patch bump like 1.0.0 → 1.0.1 for a small fix, a minor bump for a genuinely new feature, a major bump for a breaking change.
FunctionsToExport vs. Export-ModuleMember: Why Explicit Beats Wildcard
Both the manifest's own FunctionsToExport and the .psm1's Export-ModuleMember can restrict what's public — but leaving FunctionsToExport as the default wildcard ('*') carries a real, system-wide performance cost, not just a style preference:
FunctionsToExport list is pure, static metadata — PowerShell can read exactly what a module exports directly from the .psd1 file, without ever loading or running a single line of the module's own code. FunctionsToExport = '*' throws that advantage away: PowerShell has no choice but to actually load the module and introspect it to discover what it exports, every time module auto-discovery needs an answer. Multiply that across every module on a system using a wildcard, and Fundamentals 11's own "most modules just auto-load, no explicit Import-Module needed" convenience gets measurably slower for everyone — auto-loading only stays fast because well-behaved modules declare their exports explicitly, rather than making the shell do the work of finding out.
New-ModuleManifest -FunctionsToExport match exactly the same list already passed to Export-ModuleMember in the .psm1 — keeping both declarations in sync avoids a confusing mismatch between what the manifest promises and what the code actually exposes.
Hands-On Exercises
Explain what happens to Get-InternalHelper if Export-ModuleMember -Function Get-Greeting is never added to MyTools.psm1 at all, referencing this chapter's own default-export behavior.
Explain why running Publish-Module a second time with the manifest's ModuleVersion left unchanged fails, and what you'd need to do first before it can succeed.
Explain why FunctionsToExport @('Get-Greeting') in a manifest is genuinely better for system-wide performance than leaving it as FunctionsToExport = '*', referencing this chapter's own explanation of module auto-discovery.
Chapter 8 Quick Reference
.psm1— the module's actual code;.psd1— its manifest, pure metadata read without running any codeExport-ModuleMember -Function— restricts what's public; without it, every function in the.psm1is exported by default- Folder name must match the module's base name, and sit on a path listed in
$env:PSModulePath New-ModuleManifest— generates a correctly structured.psd1rather than hand-writing onePublish-Module -NuGetApiKey— publishes to the PowerShell Gallery; requires an account and API key- Published versions are immutable — republishing the same
ModuleVersionfails; bump it withUpdate-ModuleManifestfirst - Explicit
FunctionsToExportbeats a wildcard — lets PowerShell read exports as static metadata instead of loading the whole module just to find out, keeping system-wide auto-discovery fast - Next chapter: Introduction to Desired State Configuration (DSC)