Context
Process-PSModule already compiles module dependencies automatically: Build-PSModule aggregates every #Requires -Modules statement across the source files and writes them into the built manifest's RequiredModules (entries placed directly in src/manifest.psd1 are intentionally dropped — see #293). What is missing is a policy layer over how those dependencies are version-constrained, and an option to ship dependencies with the module.
Authors currently make ad-hoc choices, producing two failure modes:
- Too loose → non-reproducible builds and supply-chain exposure (a compromised, yanked, or unexpectedly-changed upstream version flows straight in).
- Too tight (exact
RequiredVersion pins) → staleness and breakage: modules fail to import when a newer good version is present, and security patches never flow. Not hypothetical — this broke CI across 8 modules on 2026-07-05 (Pester RequiredVersion = '5.7.1' vs installed 5.8.0), and previously broke Import-Module in NerdFonts#53 / Fonts#58.
This is a framework-level capability, not a per-module fix. It needs a documented standard (the why and the principles), a policy engine in the build (the how), and automation so module authors carry as little toil as possible. The work is broken down as the sub-issues of this epic.
Principles
- Take supply-chain risk seriously. Reproducible builds, known-good versions, provenance (SBOM), and the option to vendor dependencies so a build cannot be silently altered upstream.
- Avoid staleness from over-locking. Hard-locking everything is itself a risk (missed security patches, import conflicts). The default should track patches/minors, not freeze forever.
- Right tool for the artifact type. Scripts and modules have different mechanisms and different option sets.
- Automate the toil. Detection, transitive resolution, manifest updates, and drift/currency checks should be automated (Dependabot-style), not hand-maintained.
The hard parts
- Exact pins vs. floating: reproducibility and supply-chain safety pull toward pinning; freshness and patch-flow pull away from it.
- Transitive dependencies: a declared dep pulls its own deps, which the manifest does not capture by default.
- Session pollution and version conflicts:
RequiredModules load into the caller's session and can clash with what the user already has.
- Identity vs. name: a name can be squatted; only the
GUID pins the actual module identity.
- Keeping it current without manual toil, while still gating changes for review.
Scripts vs. modules
Scripts (.ps1) — fewer options. Dependencies are asserted via #Requires -Modules / #Requires -Version; the shell only checks presence, it does not install or isolate. Guidance: declare minimum versions (ModuleVersion), not exact pins.
Modules — richer option set:
| Mechanism |
Loaded into |
Gallery auto-install |
Use for |
RequiredModules (compiled from #Requires) |
caller session |
yes |
published external deps |
NestedModules |
module's private session |
yes (by name) |
splitting own code |
Vendored copy (Save-PSResource → modules/) |
module's private session (by path) |
no |
isolation / offline / unpublished deps |
Risk-appetite ladder (version-lock policy)
A configurable policy the build applies when compiling RequiredModules — a per-module default with per-dependency override. For a resolved dependency 2.4.7 / GUID G:
| Level |
Intent |
Compiled expression |
| Latest |
always newest |
ModuleVersion floor only |
| Lock Major |
minor + patch, block next major |
ModuleVersion='2.4.7' + MaximumVersion='2.*' |
| Lock Minor |
patch only |
ModuleVersion='2.4.7' + MaximumVersion='2.4.*' |
| Lock Patch |
exact version |
RequiredVersion='2.4.7' |
| Lock GUID |
exact version + identity |
RequiredVersion='2.4.7' + GUID='G' |
The manifest primitives (ModuleVersion / RequiredVersion / MaximumVersion / GUID) already exist — this is the policy layer that chooses among them.
Offline / vendored dependencies
An opt-in mode to package dependencies with the module: use Save-PSResource (which resolves the full transitive tree, including nested dependencies) to download deps into a modules/ subfolder, register them as NestedModules, and load them from the root module — isolated from the user session. When vendoring, the natural default is Lock Patch (ship the exact bits), optionally Lock GUID.
Related (motivation, not sub-tasks)
Context
Process-PSModulealready compiles module dependencies automatically:Build-PSModuleaggregates every#Requires -Modulesstatement across the source files and writes them into the built manifest'sRequiredModules(entries placed directly insrc/manifest.psd1are intentionally dropped — see #293). What is missing is a policy layer over how those dependencies are version-constrained, and an option to ship dependencies with the module.Authors currently make ad-hoc choices, producing two failure modes:
RequiredVersionpins) → staleness and breakage: modules fail to import when a newer good version is present, and security patches never flow. Not hypothetical — this broke CI across 8 modules on 2026-07-05 (PesterRequiredVersion = '5.7.1'vs installed5.8.0), and previously brokeImport-Modulein NerdFonts#53 / Fonts#58.This is a framework-level capability, not a per-module fix. It needs a documented standard (the why and the principles), a policy engine in the build (the how), and automation so module authors carry as little toil as possible. The work is broken down as the sub-issues of this epic.
Principles
The hard parts
RequiredModulesload into the caller's session and can clash with what the user already has.GUIDpins the actual module identity.Scripts vs. modules
Scripts (
.ps1) — fewer options. Dependencies are asserted via#Requires -Modules/#Requires -Version; the shell only checks presence, it does not install or isolate. Guidance: declare minimum versions (ModuleVersion), not exact pins.Modules — richer option set:
RequiredModules(compiled from#Requires)NestedModulesSave-PSResource→modules/)Risk-appetite ladder (version-lock policy)
A configurable policy the build applies when compiling
RequiredModules— a per-module default with per-dependency override. For a resolved dependency2.4.7/ GUIDG:ModuleVersionfloor onlyModuleVersion='2.4.7'+MaximumVersion='2.*'ModuleVersion='2.4.7'+MaximumVersion='2.4.*'RequiredVersion='2.4.7'RequiredVersion='2.4.7'+GUID='G'The manifest primitives (
ModuleVersion/RequiredVersion/MaximumVersion/GUID) already exist — this is the policy layer that chooses among them.Offline / vendored dependencies
An opt-in mode to package dependencies with the module: use
Save-PSResource(which resolves the full transitive tree, including nested dependencies) to download deps into amodules/subfolder, register them asNestedModules, and load them from the root module — isolated from the user session. When vendoring, the natural default is Lock Patch (ship the exact bits), optionally Lock GUID.Related (motivation, not sub-tasks)
Import-Module