ModuleBuilder: splitting a PowerShell module into many files and shipping one
A PowerShell Module to help scripters write, version, sign, package, and publish.
At a glance
- What is it?
- ModuleBuilder is a PowerShell module that lets you author a script module as one file per function in Public and Private folders and then build it into the single .psm1 file PowerShell actually wants, with incremental builds, a manifest that gets its export lists filled in for you, and helpers that translate line numbers between source and output. It suits a script author who finds one enormous file unmaintainable, and the trade is that the tool is opinionated about layout.
- Who is it for?
- Adopt ModuleBuilder if you are maintaining a PowerShell module that has outgrown one file and you still want to ship a single psm1 for performance and signing reasons.
- Can I use it commercially?
- Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 132 days ago.
- What is it written in?
- Mainly PowerShell, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 20, 2026, and from our analysis. They are not legal advice.
Editorial analysis
The problem it solves
This module makes it easier to break up your module source into several files for organization, even though you need to ship it as one big psm1 file.
That is the whole premise, and it is a genuine tension in PowerShell development. The language wants a script module to be a single file, and a project that has grown past a couple of hundred functions is not manageable as one file. So you end up in a bad position: your editor is slow on a 3000 line file, your diffs are unreadable, two people cannot work on it at once, and yet the thing has to be one file when it ships.
The README is emphatic about the shipping side. You should ship your module as one big file. PowerShell expects script modules to be all in one file, and a module in a single .psm1 script file results in the best performance, natural script scope, and full support for classes and using statements.
Three reasons are given for why that matters, and they are worth separating because two of them are not about performance at all.
The performance reason is specific. The single file option is particularly important if you are signing your module, or if your end users want to be able to code-sign it, because each file's signature must be checked and each certificate must be checked against CRLs. So a many-file module multiplies the cost of the signature verification that happens on load.
The class reason is a hard constraint. It is critical if you are using PowerShell classes, because the `using` statement only supports classes defined in the root psm1 file. That is the language refusing to let you do the thing you came to do, and no amount of clever packaging works around it.
The scope reason is about behaviour. It is basically required if you want to use module-scope variables to share state between functions in your module, because each file in a many-file module gets its own scope.
So the design is a build step that takes the well-organised source tree you want to work in and produces the single file you have to ship. The tooling exists because the language's requirements and a maintainable codebase pull in opposite directions.
The project is MIT licensed, the primary language is PowerShell, the default branch is main, and the last push was on 2026-06-12. The listed release is v1.0.0-beta0005, described as the first PowerShell Gallery pre-release, from October 2018, and there is no newer one listed, which suggests the project is now built and consumed from source rather than from the gallery.
The commands, and what each one is for
The module's main output is the ModuleBuilder module itself, consisting of one primary command and a few helpers to translate input and output line numbers so you can trouble-shoot error messages from your module against the source files.
`Build-Module` is the primary one. It builds a script module from a source project containing one file per function in Public and Private folders, and it is a build task for PowerShell script modules that supports incremental builds.
Incremental builds are the feature that makes this usable at scale. Without them, every build would concatenate every file every time, and for a module with hundreds of functions that is slow enough to make you skip the build. With them, only the changed files are reprocessed, so the build is fast enough to run on every save.
`Convert-CodeCoverage` takes the output from `Invoke-Pester -Passthru` run against the build output and converts the code coverage report back to the source lines. This one solves a problem that only exists because of the build step. Coverage is measured against the built psm1, where every function has a line number in one concatenated file, and a coverage report pointing at line 847 of an output file is useless to a developer whose function lives in a different file at line 12. This command maps it back.
`ConvertFrom-SourceLineNumber` converts a line number from a source file to the corresponding line number in the built output, and `ConvertTo-SourceLineNumber` does the reverse, converting a line number from the built output to the file and line number in the source.
Those two are the debugging equivalent of the coverage command, and they are what make error messages actionable. A stack trace saying the error is at line 847 of the built module tells you nothing when the module has 200 source files. With the mapping, your debugging tools can be pointed at the source file and the line inside it.
`Convert-Breakpoint` converts any breakpoints on source files to module files and vice-versa. That is the third application of the same idea: setting a breakpoint in the source tree, debugging the built module, and having the breakpoint land in the right place.
So the line-number helpers come in three forms, covering coverage reports, error messages and breakpoints. That is a coherent set, and it is the part of the tool that shows the author understood that a build step which changes the shape of the code has to give something back to the developer working in the source.
The conventions, and how to override them
The organising section is explicit that the module is opinionated, and that if you follow its conventions it should feel wonderfully automatic.
The setup has three steps. Create a `source` or `src` folder with a `build.psd1` file and your module manifest in it. In the `build.psd1` specify the relative Path to your module's manifest, for example `@{ Path = "ModuleBuilder.psd1" }`. And in your manifest, make sure a few values are not commented out, which you can leave empty because they will be overwritten.
Those three manifest values are the interesting part, because they show the tool writing into your manifest rather than only reading it. `FunctionsToExport` gets updated with the file names that match the `PublicFilter`. `AliasesToExport` gets updated with the values from `[Alias()]` attributes on commands. And `Prerelease` and `ReleaseNotes` in the `PSData` hash table inside `PrivateData` are managed too.
The first of those is the clever bit. The exported function list is derived from the file names in the Public folder rather than maintained by hand, which means adding a function to the Public folder and naming the file after it is enough to export it. Nobody has to remember to edit the manifest, and the manifest cannot drift out of sync with the code.
The second one is even better, because it inverts a manual process. Rather than maintaining a separate aliases list in the manifest, the aliases are read from the `[Alias()]` attributes that are already on the commands. The attribute is the source of truth and the manifest is generated from it.
Then the file layout conventions. You create sub-folders in source and put script files in them with only one function in each file, and you name the files with the same name as the function in them, especially in the `source\public` folder where the file names determine the exported functions. By convention, the source directories are named Classes, and optionally Enum, then Private, then Public. The PublicFilter is all of the functions in the Public directory. And to force classes into a certain order, you prefix their file names with numbers, like `01-User.ps1`.
That last convention is a small thing that solves a real problem. PowerShell classes have a load order, and the numeric prefix is the mechanism for controlling it, in the same way that ordering matters in other languages.
And the escape hatch is stated clearly. There are a lot of conventions in Build-Module, expressed as default values for its parameters. These defaults are documented in the help for Build-Module, and you can override any parameter defaults by adding keys to the `build.psd1` file with your preferences, or by passing the values to the Build-Module command directly. So the conventions are defaults, not rules, and an override can be either persistent in the build file or one-off on the command line.
Where it sits among the PowerShell build frameworks
There is a section on build tools that is unusually candid about what ModuleBuilder is and is not.
There are several PowerShell build frameworks available. The build task in ModuleBuilder doesn't particularly endorse or interoperate with any of them, but it does accomplish a particular task that is needed by all of them.
So the project is explicitly a component rather than a competitor. That is a sensible position, because the build framework question and the module assembly question are separate, and a build framework that also does the module assembly is doing two jobs.
The section then describes what a good build framework needs, which is effectively a specification for the ecosystem this tool lives in. It needs to support incremental builds and have a way to define build targets with dependencies on other targets, so it can infer the target build order. It should also include pre-defined tasks for most common build targets, including restoring dependencies, cleaning old output, building and assembling a module from source, testing that module, and publishing the module for public consumption.
And then the point about scope: the `Build-Module` command is just one task of several which would be needed for a build target for a PowerShell script module.
That is the honest self-description. ModuleBuilder does the assembling and nothing else, and the ecosystem it belongs to does the orchestration. It is the kind of boundary that makes a tool survive the churn in the layer above it.
There is also an acknowledgement that the problem is not entirely solved. There are still some issues in Visual Studio Code and PSScriptAnalyzer when authoring modules as multiple files, but if you want to break up your module into multiple files for organization and maintainability, and still need to ship it as one big file for performance and compatibility reasons, this module is for you.
That caveat is worth weighing. The multi-file authoring model is not what the editor and the analyser were built for, so a developer using this will hit friction in their editor and their linting, and the README says so instead of claiming the experience is seamless.
Building the project itself
The build instructions describe two routes, and the contrast between them says something about the project's tooling choices.
The easiest and fastest build uses Earthbuild. Earthbuilds use containers to ensure tools are available, to parallelize steps, and to cache output. On Windows it requires WSL2, Docker Desktop and the Earthbuild CLI, and if you already have those installed you can check out the repository and run `earth +test` to build and run the tests.
git clone https://github.com/PoshCode/ModuleBuilder.git
cd ModuleBuilder
earth +testThe container approach is a good fit for a PowerShell project, because the dependency situation is otherwise awkward. PowerShell modules have a habit of depending on other modules, and installing a specific version of each on a developer machine is the kind of setup that goes stale.
The alternative is building without Earthbuild. The full ModuleBuilder build depends on the dotnet SDK and has a few other build-time dependencies: Invoke-Build for orchestrating the build steps, Pester to test it, and Install-ModuleFast to install the rest of the dependencies. All of those, including dotnet, are handled for you in the Earthbuild build, but without it you need to install dotnet separately.
The dependency list is a good snapshot of the current state of PowerShell tooling. Invoke-Build is the incumbent for orchestrating builds in the language, Pester is the test framework everyone uses, and Install-ModuleFast is the tool for solving the module installation problem fast enough to be usable in a build.
You also need to clone the shared Tasks repository alongside it, which is the pattern of splitting the common build scaffolding into its own repository so that several projects can share it.
The repository layout reflects that. There is a `Source/` directory for the module source, `Templates/` for the project templates, `Tests/` for the test suite, `resources/` for assets, and a `PotentialContribution/` directory. The build files are `build.build.ps1`, `build.psd1` and `build.requires.psd1`, plus `build.earth` for the Earthbuild definition and `test.ps1` for the test entry point.
Two other files are worth noting. `GitVersion.yml` configures GitVersion, the tool that derives a version number from the git history, which is how a module gets a semantic version without anyone editing a version string. And `ScriptAnalyzerSettings.psd1` is the configuration for PSScriptAnalyzer, so the project's own linting rules are checked in and apply to everyone working on it.
Editorial conclusion
Adopt ModuleBuilder if you are maintaining a PowerShell module that has outgrown one file and you still want to ship a single psm1 for performance and signing reasons. Do not adopt it if your module is small enough to read in one sitting, because the conventions it imposes are not worth the machinery, and check first how the tooling handles multiple files, since the README notes there are still issues in Visual Studio Code and PSScriptAnalyzer when authoring a module this way.
Frequently asked questions
What does ModuleBuilder do for PowerShell developers?
It lets you author a PowerShell script module as one file per function in Public and Private folders and then build it into the single psm1 file that PowerShell expects, because a single file gives the best performance, natural script scope and full support for classes and using statements.
Why should a PowerShell module ship as one file?
PowerShell expects script modules to be all in one file. A single psm1 gives the best performance and natural script scope, and it is particularly important when signing because each file's signature must be checked and each certificate checked against CRLs. It is also required for PowerShell classes, since the using statement only supports classes defined in the root psm1 file, and for sharing state through module-scope variables.
What commands does the ModuleBuilder module provide?
Build-Module builds the script module from the source project and supports incremental builds. Convert-CodeCoverage maps a coverage report from Invoke-Pester back to source lines. ConvertFrom-SourceLineNumber and ConvertTo-SourceLineNumber map line numbers between source files and the built output. Convert-Breakpoint converts breakpoints on source files to module files and vice-versa.
How does ModuleBuilder decide what a module exports?
FunctionsToExport in the manifest is updated with the file names that match the PublicFilter, which by convention is everything in the Public directory, so naming a file after its function is enough to export it. AliasesToExport is updated from the [Alias()] attributes on the commands, and Prerelease and ReleaseNotes in the PSData hash table are managed too.
Can I change the ModuleBuilder conventions?
Yes. There are a lot of conventions in Build-Module, expressed as default values for its parameters and documented in the help for Build-Module. You can override any parameter default by adding keys to the build.psd1 file with your preferences, or by passing the values to the Build-Module command directly, so the conventions are defaults rather than rules.
How do I build ModuleBuilder from source?
The easiest route is Earthbuild, which uses containers to ensure tools are available, parallelize steps and cache output. It needs WSL2, Docker Desktop and the Earthbuild CLI on Windows, after which you clone the repository and run `earth +test`. Building without it means installing the dotnet SDK yourself, along with Invoke-Build for orchestration, Pester for testing and Install-ModuleFast for the rest of the dependencies.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/poshcode-modulebuilder)Was this useful?