Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,23 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Added

- `Add-PSDepend` appends a dependency to a DependencyFile, creating
`requirements.psd1` if none exists. It parses the target file with the
PowerShell AST and splices the new entry in as text, leaving every other
entry and any comments untouched. By default it re-runs
`Update-PSDependLock` afterward, rolling back the edit if resolution
fails; `-NoLock` opts out. `-Force` overwrites an existing entry for the
same Name, replacing it entirely.
- `PSGalleryModule` now warns when `Version` is an exact prerelease version
(e.g. `0.7.0-beta1`, including SemVer build metadata like `1.0.0-beta+build.5`)
but `AllowPrerelease` is not set, since `Install-Module` will otherwise fail
to find the package. The detection heuristic lives in the new
`Test-PSDependPrereleaseVersion` private helper (a regex, not
`SemanticVersion`, since that type isn't available on the inbox Windows
PowerShell 5.1 assembly this module targets).

## [0.7.0-beta1] - 2026-10-07

### Added
Expand Down
1 change: 1 addition & 0 deletions PSDepend/PSDepend.psd1
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@
# Functions to export from this module
# Generated by the build process
FunctionsToExport = @(
'Add-PSDepend',
'Get-Dependency',
'Get-PSDependScript',
'Get-PSDependType',
Expand Down
5 changes: 5 additions & 0 deletions PSDepend/PSDependScripts/PSGalleryModule.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
Name: The name for this module
Version: Used to identify existing installs meeting this criteria, and as RequiredVersion for installation. Defaults to 'latest'
Also accepts a NuGet version range (e.g. '[2.2.3,3.0)', '[2.0,)', '(,3.0)'). A bare version (e.g. '3.2.1') still means that exact version. When a range is given, the highest available version that satisfies it is installed.
A bare exact version that looks like a prerelease (e.g. '0.7.0-beta1') triggers a warning unless AllowPrerelease is also specified.
Target: Used as 'Scope' for Install-Module. If this is a path, we use Save-Module with this path. On reruns, PSDepend checks existing modules first and skips reinstalling when the requested version is already present. Defaults to 'AllUsers'
AddToPath: If target is used as a path, prepend that path to ENV:PSModulePath
Credential: The username and password used to authenticate against the private repository
Expand Down Expand Up @@ -217,6 +218,10 @@ if ($Version -and $Version -ne 'latest') {
}
if ($versionRange.IsExact) {
$params.Add('RequiredVersion', $versionRange.Exact)

if ((Test-PSDependPrereleaseVersion -Version $versionRange.Exact) -and -not $AllowPrerelease) {
Write-Warning "Version [$($versionRange.Exact)] for [$Name] looks like a prerelease version, but AllowPrerelease was not specified. Add 'AllowPrerelease = `$true' to Parameters, or the module may not be found."
}
}
}

Expand Down
60 changes: 60 additions & 0 deletions PSDepend/Private/ConvertTo-PSDependLiteral.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
function ConvertTo-PSDependLiteral {
<#
.SYNOPSIS
Render a value as PowerShell source text suitable for a DependencyFile entry.

.DESCRIPTION
Used by Add-PSDepend to serialize field values (Tags, Parameters, ...) when
writing a new hashtable-form entry. Handles strings, booleans, numbers,
arrays, and nested hashtables.

.PARAMETER Value
The value to render.

.PARAMETER IndentLevel
Indentation depth, in 4-space units, for a nested hashtable's members and
its closing brace.

.EXAMPLE
ConvertTo-PSDependLiteral -Value @{ SkipPublisherCheck = $true }

Returns "@{`r`n SkipPublisherCheck = `$true`r`n }"
#>
[CmdletBinding()]
[OutputType([string])]
param(
$Value,

[int]$IndentLevel = 2
)

if ($null -eq $Value) {
return '$null'
}
if ($Value -is [bool]) {
return $(if ($Value) { '$true' } else { '$false' })
}
if ($Value -is [System.Collections.IDictionary]) {
$ChildIndent = ' ' * ($IndentLevel + 1)
$ClosingIndent = ' ' * $IndentLevel
$Entries = foreach ($Key in $Value.Keys) {
$QuotedKey = "'$($Key.ToString() -replace "'", "''")'"
"$ChildIndent$QuotedKey = $(ConvertTo-PSDependLiteral -Value $Value[$Key] -IndentLevel ($IndentLevel + 1))"
}
return "@{`r`n$($Entries -join "`r`n")`r`n$ClosingIndent}"
}
if ($Value -is [string]) {
return "'$($Value -replace "'", "''")'"
}
if ($Value -is [int] -or $Value -is [long] -or $Value -is [double]) {
return "$Value"
}
if ($Value -is [System.Collections.IEnumerable]) {
$Items = @($Value | ForEach-Object { ConvertTo-PSDependLiteral -Value $_ -IndentLevel $IndentLevel })
if ($Items.Count -eq 0) {
return '@()'
}
return ($Items -join ', ')
}
return "'$($Value.ToString() -replace "'", "''")'"
}
61 changes: 61 additions & 0 deletions PSDepend/Private/Resolve-PSDependFileTarget.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
function Resolve-PSDependFileTarget {
<#
.SYNOPSIS
Resolve the DependencyFile that Add-PSDepend should write to.

.DESCRIPTION
If Path points to an existing file, it is used as-is. If Path points to a
nonexistent path with a .psd1 extension, it is treated as the file to
create. Otherwise Path is treated as a directory: it is searched
(optionally recursively) for *.depend.psd1 and requirements.psd1 files,
the same way Invoke-PSDepend and Update-PSDependLock discover
DependencyFiles. Exactly one match is returned as-is. No matches falls
back to <Path>\requirements.psd1, to be created by the caller. More than
one match is ambiguous and the caller must narrow it with -Path.

.PARAMETER Path
A DependencyFile, a new DependencyFile to create, or a directory to search.

.PARAMETER Recurse
Whether to recurse into subdirectories when Path is a directory.

.EXAMPLE
Resolve-PSDependFileTarget -Path . -Recurse $true

Finds the single DependencyFile under the current directory tree, or
falls back to .\requirements.psd1 if none exist yet.
#>
[CmdletBinding()]
[OutputType([string])]
param(
[Parameter(Mandatory)]
[string]$Path,

[bool]$Recurse = $true
)

if (Test-Path -LiteralPath $Path -PathType Leaf) {
$Resolved = (Resolve-Path -LiteralPath $Path).ProviderPath
if ([System.IO.Path]::GetExtension($Resolved) -ine '.psd1') {
throw "Path '$Resolved' is not a .psd1 file"
}
return $Resolved
}

if (-not (Test-Path -LiteralPath $Path)) {
if ([System.IO.Path]::GetExtension($Path) -eq '.psd1') {
return $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($Path)
}
throw "Path '$Path' does not exist"
}

$DirectoryPath = (Resolve-Path -LiteralPath $Path).ProviderPath
$Found = @(Resolve-DependScripts -Path $DirectoryPath -Recurse $Recurse)
switch ($Found.Count) {
0 { Join-Path $DirectoryPath 'requirements.psd1' }
1 { $Found[0] }
default {
throw "Multiple DependencyFiles found under '$DirectoryPath': $($Found -join ', '). Specify -Path to target one of them."
}
}
}
50 changes: 50 additions & 0 deletions PSDepend/Private/Test-PSDependPrereleaseVersion.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
function Test-PSDependPrereleaseVersion {
<#
.SYNOPSIS
Heuristically test whether an exact version string looks like a prerelease.

.DESCRIPTION
Returns $true when Version has a dash-delimited suffix after 1-4 numeric
dot-separated components (e.g. '0.7.0-beta1', '2.5.0-alpha', or with SemVer
2.0 build metadata like '1.0.0-beta+build.5'). Used to warn callers who
pinned a prerelease version but didn't pass AllowPrerelease.

This is a heuristic, not a strict validity check: it is intentionally
looser than SemVer (it accepts 4 numeric components, and doesn't enforce
per-identifier SemVer grammar such as "no leading zero"), because a false
positive here only produces an extra warning, while a false negative
silently lets Install-Module fail to find the module. See
docs/PSDependScripts-PrereleaseVersionDetection.md for the full version
matrix this was validated against.

Deliberately does not use [System.Management.Automation.SemanticVersion]:
that type isn't available in the inbox Windows PowerShell 5.1 assembly
(confirmed via PSScriptAnalyzer's UseCompatibleTypes rule, which uses this
exact type as its worked example of a type missing from the 5.1 profile),
and PSDepend's manifest declares PowerShellVersion = '5.1'. A regex has no
such runtime-availability risk.

.PARAMETER Version
An exact (non-range) version string to test.

.EXAMPLE
Test-PSDependPrereleaseVersion -Version '0.7.0-beta1'

Returns $true.

.EXAMPLE
Test-PSDependPrereleaseVersion -Version '1.2.3'

Returns $false - no prerelease suffix.
#>
[CmdletBinding()]
[OutputType([bool])]
param(
[AllowNull()]
[string]$Version
)

if ([string]::IsNullOrWhiteSpace($Version)) { return $false }

$Version -match '^\d+(\.\d+){1,3}-[0-9A-Za-z][0-9A-Za-z.+-]*$'
}
Loading
Loading