PowerShell module assembler: assembles multiple source files into a single installable package. Performs dependency resolution and syntax checks to ensure the module source is valid.
Find a file
2026-05-25 15:54:43 -07:00
ModuleAssembler.ps1 ModuleAssembler: import snapshot of v1.1.0 2026-05-25 14:06:13 -07:00
ModuleMetadata.json ModuleAssembler: import snapshot of v1.1.0 2026-05-25 14:06:13 -07:00
README.md v1.1.0: sync README 2026-05-25 15:54:43 -07:00
Resolve-AssemblerSourceLocation.ps1 ModuleAssembler: import snapshot of v1.1.0 2026-05-25 14:06:13 -07:00

ModuleAssembler v1.1.0

ModuleAssembler is a generic PowerShell module build tool. It assembles a module from source files into a single .ps1 and accompanying .psd1 manifest, ready for installation or distribution.

In release mode (default), all source files are concatenated in dependency order into a single .ps1 file. A source map is generated alongside it to allow stack traces from the assembled file to be traced back to their original source locations.

In dev mode (-Dev), no assembly is performed. Instead a .psd1 manifest is written to the repository root using NestedModules to reference source files directly, allowing the module to be loaded in-tree without a full build.


Requirements

  • PowerShell 7.0 or later
  • Git available on $PATH
  • ModuleMetadata.json in the module repository root

Usage

# Preview a release build (dry run)
./ModuleAssembler.ps1

# Perform a release build
./ModuleAssembler.ps1 -DryRun $false

# Generate a dev manifest for in-tree loading
./ModuleAssembler.ps1 -Dev -DryRun $false

# Build with unstaged changes (development only)
./ModuleAssembler.ps1 -Dev -DryRun $false -IgnoreGitUnstaged

# Create a new ModuleMetadata.json template
./ModuleAssembler.ps1 -New

Parameters

Parameter Type Default Description
MetadataPath string ./ModuleMetadata.json Path to the metadata file
DryRun bool $true When true, prints all output without writing any files
Dev switch off Generates a dev manifest instead of a release build
New switch off Creates a new ModuleMetadata.json template. Will not overwrite
IgnoreGitUnstaged switch off Allows building with unstaged changes present
IgnoreGitUntracked switch off Includes untracked files in the build

Creating a New Module Manifest

To start a new module, run:

./ModuleAssembler.ps1 -New

This generates a ModuleMetadata.json in the current directory with all mandatory fields populated with placeholder values and a freshly generated GUID. The file will not be created if one already exists at the target path.

To create a manifest at a specific location:

./ModuleAssembler.ps1 -New -MetadataPath 'C:\Projects\MyModule\ModuleMetadata.json'

After creation, open the file and update at minimum:

  • Name - the module name
  • Author and CompanyName
  • Categories - adjust the glob patterns to match your source directory layout

The Guid is automatically generated and should not be changed after the module is first published. The Version can be left as 1.0.0 and will have an API component appended automatically at build time.

Optional fields (GitUnstagedIgnore, ScriptsToProcess, BundleFiles, FormatsToProcess, VersionHeader) are not included in the generated template - add them as needed, referring to the reference section below.


ModuleMetadata.json Reference

Mandatory fields

Name - The module name. Used as the base name for all generated files.

"Name": "ModuleName"

Guid - The module GUID. Must be unique and stable across all versions of the module. Generate once with [guid]::NewGuid() and never change it.

"Guid": "00000000-0000-0000-0000-000000000000"

Version - The module version. May be in Major.Minor.Patch or Major.Minor.Patch.APIVersion format.

  • Major.Minor.Patch - AutoVersion appends a date-based API component at build time: 1.1.0 -> 1.1.0.2026043001
  • Major.Minor.Patch.APIVersion - used as-is with a warning that the API component is already present
"Version": "1.0.0"

Author - Author name written into the generated .psd1.

"Author": "Module Developer Name"

CompanyName - Company name written into the generated .psd1.

"CompanyName": "Contoso Corporation"

Copyright - Copyright string written into the generated .psd1.

"Copyright": "Internal proprietary content of Contoso Corporation"

PowerShellVersion - Minimum required PowerShell version written into the generated .psd1.

"PowerShellVersion": "7.0"

OutputPath - Path relative to the repository root where release build outputs are written. The directory is created if it does not exist.

"OutputPath": "dist"

Categories - Ordered array of source file groups. Files are assembled into the .ps1 in the order the categories appear, with class files topologically sorted within their category. Each entry requires a Glob and a Type.

"Categories": [
    { "Glob": "Headers/Constants/*.ps1", "Type": "Header" },
    { "Glob": "Headers/Classes/*.ps1",   "Type": "Class"  },
    { "Glob": "Private/**/*.ps1",        "Type": "Source" },
    { "Glob": "Public/**/*.ps1",         "Type": "Source" }
]

Category types:

Type Description
Header Constants, enums, and other definitions. Included first, in glob order
Class Class and enum definitions. Topologically sorted by cross-file inheritance and type reference dependencies
Source Functions and other code. Included after headers and classes, in glob order

Optional per-category fields:

  • Exclude - array of glob patterns to exclude from the category's glob results
  • Comment - human-readable description, ignored by the assembler
{ "Glob": "Core/*.ps1", "Type": "Source", "Exclude": ["Core/Constants.ps1", "Core/Classes.ps1"], "Comment": "Core source files" }

RequiredModules - Array of module names written into the RequiredModules block of the generated .psd1. Use an empty array if there are no dependencies.

"RequiredModules": ["ActiveDirectory", "AWS.Tools.Route53"]

Optional fields

GitUnstagedIgnore - Array of file glob patterns that are exempt from the unstaged changes check. Files matching these patterns will not block the build even if they have unstaged modifications. Useful for generated files such as the manifest itself, version headers, and build outputs.

"GitUnstagedIgnore": [
    "ModuleMetadata.json",
    "ModuleName.psd1",
    "Headers/Constants/Library.ps1",
    "dist/*"
]

ScriptsToProcess - Array of source file paths (relative to the repository root) to be concatenated into Preload.ps1 and listed in the .psd1 ScriptsToProcess block. These files are dot-sourced into the caller's session before the module is parsed.

Use this for custom validation attribute classes and other types that must be available at module parse time. Files listed here are still included in the assembled .ps1 - for most types, duplicate definitions do not cause problems.

"ScriptsToProcess": [
    "Headers/Classes/ValidateTypes.ps1"
]

FormatsToProcess - Array of .ps1xml format file paths (relative to the repository root) to be listed in the .psd1 FormatsToProcess block. In release mode, these files are copied into OutputPath. In dev mode, the paths are referenced verbatim.

"FormatsToProcess": [
    "ModuleName.Format.ps1xml"
]

BundleFiles - Array of glob patterns for files to be copied verbatim into OutputPath, preserving their relative directory structure. Not processed by the assembler. Useful for bundling companion tools, SQL schemas, or other supporting files alongside the module.

"BundleFiles": [
    "Tools/*",
    "Schema/**/*.sql"
]

VersionHeader - Writes a generated version header file from a template before assembly begins. Useful for modules that expose their version number as a runtime-readable variable. The file is written to Path (relative to the repository root) and should be listed in GitUnstagedIgnore since it is overwritten on every build.

The Template array is joined with newlines. Available placeholders:

Placeholder Value
{Major} Major version component
{Minor} Minor version component
{Patch} Patch version component
{API} API version component (date+counter, e.g. 2026043001)
{Version} Full version string (e.g. 1.1.0.2026043001)
"VersionHeader": {
    "Path": "Headers/Constants/Library.ps1",
    "Template": [
        "# ModuleAssembler:ExportHeader",
        "# This header is automatically generated by ModuleAssembler - do not modify!",
        "Set-Variable -Force Library_Version       -Option ReadOnly -Value {API}",
        "Set-Variable -Force Library_Version_Major -Option ReadOnly -Value {Major}",
        "Set-Variable -Force Library_Version_Minor -Option ReadOnly -Value {Minor}",
        "Set-Variable -Force Library_Version_Patch -Option ReadOnly -Value {Patch}"
    ]
}

Export Pragmas

ModuleAssembler scans source files for two pragma comments that control the generated .psd1 export lists and Exports.ps1 content. Pragma lines are stripped from the assembled .ps1 output.

# ModuleAssembler:Export - Place on the line immediately before a function definition to include that function in FunctionsToExport.

# ModuleAssembler:Export
function Get-DatabaseHandle {
    ...
}

# ModuleAssembler:ExportHeader - Place anywhere in a file to include that file's content in Exports.ps1, which is dot-sourced into the caller's session before the module is parsed. Use this to export enum and class types for interactive use.

# ModuleAssembler:ExportHeader

enum DmarcPolicy {
    none       = 0
    quarantine = 1
    reject     = 2
}

[Alias()] attributes on functions are automatically detected and included in AliasesToExport without any pragma.


Output Files

Release build (dist/)

File Description
<Name>.ps1 Assembled module source
<Name>.psd1 Module manifest, references <Name>.ps1 as RootModule
<Name>.sourcemap.json Maps assembled .ps1 line numbers back to source file and line
Preload.ps1 Concatenated ScriptsToProcess files. Only present if ScriptsToProcess is defined
Exports.ps1 Concatenated ExportHeader-tagged files. Only present if any files are tagged
Bundle files Verbatim copies of BundleFiles entries, preserving directory structure
Format files Copies of FormatsToProcess entries

Dev build (repository root)

File Description
<Name>.psd1 Dev manifest, lists source files in NestedModules
Preload.ps1 As above. Only present if ScriptsToProcess is defined
Exports.ps1 As above. Only present if any files are tagged

Source Map

The source map (<Name>.sourcemap.json) records the pre-strip line count of each source file alongside its position in the assembled .ps1. Use Resolve-AssemblerSourceLocation.ps1 to translate a line number from a stack trace back to its original source file and line:

Resolve-AssemblerSourceLocation -SourceMapPath 'dist/ModuleName.sourcemap.json' -Line 1847
SourceFile              SourceLine  Psm1Line
----------              ----------  --------
Public/Database/Get.ps1 42          1847

AutoVersion

When Version in ModuleMetadata.json is in Major.Minor.Patch format, ModuleAssembler automatically computes a full Major.Minor.Patch.APIVersion at build time. The API component is yyyyMMddnn where nn is a daily counter derived by querying existing git tags matching the current date:

  • First build of the day: nn = 01
  • Subsequent builds: nn increments from the highest existing tag for that day

fetch-depth: 0 is required in CI checkouts for the tag query to work correctly.


Developer Workflow

# Generate dev manifest
.\ModuleAssembler.ps1 -Dev -DryRun $false

# Load module in-tree
Import-Module .\ModuleName.psd1

# After making changes, reload
Import-Module -Force .\ModuleName.psd1

If you add, remove, or rename source files, re-run the assembler to update the dev manifest before reloading.


Exit Codes

Various error conditions can be identified based on the script's exit code:

Code Description
0 New manifest written successfully, or module assembled successfully.
1 ModuleMetadata.json was not found
2 ModuleMetadata.json already exists
3 Failed to load or parse ModuleMetadata.json
4 ModuleMetadata.json contains a malformed ManifestCompatVersion tag
5 ModuleMetadata.json Version tag is not in Major.Minor.Patch or Major.Minor.Patch.Hotfix format
6 Git repository contains unstaged files
7 ModuleMetadata.json was written by a newer, incompatible version of ModuleAssembler

Future Enhancements

ModuleAssembler is already a fully functional build system and a core part of the build pipeline for several PowerShell modules, but it is not yet feature-complete. Planned future enhancements include but are not limited to the following:

  • Module signing (Authenticode)

Right now, assembled modules are primarily distributed as build artifacts in CI pipelines. The current environment does not strictly require module signing, but for consistency and verifiable release chains, module signing is a high priority feature. The assembled module, export and preload .ps1 files as well as the manifest .psd1 will be signed. Signing will occur as a separate stage post-assembly.

  • Reproducible builds

ModuleAssembler-built modules already provides fairly consistent builds, with only the headers of generated files (such as manifests, exports, version headers and preloads) changing during build processes. Assembled file format is currently dependent on both filesystem directory listing order (Get-ChildItem) as well as class definition/reference order (when classes are in use). This provides a consistent ordering when executed under similar OS and filesystem environments, but differing filesystems may potentially lead to varied file load order.

  • Dependency graph generation

A non-critical convenience feature - as we already parse source files and check validity, extending build to generate a flowchart of type dependencies (Graphviz?) would be fairly easy.

  • Improved source validation

We currently perform basic syntax validity checks, but certain types of errors (such as missing types) are ignored due to the behavior of parsing each source file in isolation. Improved validation would be a welcome feature to identify things such as duplicate function names, type mismatches, and scope violations.