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
23 changes: 23 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,29 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Added

- npm-style lock files. `Update-PSDependLock` resolves supported Dependencies
and their transitive dependencies to exact versions in `<name>.lock.json`.
`Invoke-PSDepend` and `Get-Dependency` honor locks automatically, install
transitive packages first, preserve separate root installation contexts,
reject malformed or unsafe lock data, and detect changes to Dependencies,
versions, resolution sources, and DependencyScript parameters. `-IgnoreLock`
opts out. Resolution is greedy and does not backtrack to older parent versions.
- New `Resolve` PSDependAction for `PSGalleryModule`, `PSResourceGet`,
`PSGalleryNuget`, `Nuget`, `Chocolatey` and `Npm`: query the source and return
an exact version without installing. `Npm` accepts npm semver ranges and pins
only the declared package; its subtree remains under npm's `package-lock.json`.

### Fixed

- `Invoke-DependencyScript -PSDependTypePath` is now passed through to the
type/script lookup instead of always reading the module's `PSDependMap.psd1`.
- Lock resolution now exhausts paged NuGet v2 feeds, honors exact prerelease
requests, rejects ambiguous framework-specific dependency constraints, and
re-resolves whenever a combined constraint changes so the highest matching
version remains locked.

## [0.6.0] - 2026-09-30

### Added
Expand Down
2 changes: 1 addition & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ No compilation; files are staged verbatim to `Output\` — do not edit files und

Two files must be updated together:

1. **`PSDepend/PSDependScripts/<Type>.ps1`** — handler script. Must include comment-based help and a `PSDependAction` parameter accepting `Install`, `Test`, and `Import` values.
1. **`PSDepend/PSDependScripts/<Type>.ps1`** — handler script. Must include comment-based help and a `PSDependAction` parameter accepting `Install`, `Test`, and `Import` values. Optionally accept `Resolve` (lock support): query the source only and emit one `PSDepend.ResolvedDependency` object (`Name`, exact `Version`, `Dependencies` hashtable of name → NuGet range or `'latest'`); see `PSGalleryModule.ps1`.
2. **`PSDepend/PSDependMap.psd1`** — registers the type, maps it to the script, and sets `Supports` to control platform filtering (`windows`, `core`, `macos`, `linux`).

See `Git.ps1` and `PSGalleryModule.ps1` as reference implementations.
Expand Down
13 changes: 11 additions & 2 deletions CONTEXT.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,16 @@ A label on a Dependency that controls inclusion when `Invoke-PSDepend` is called
_Avoid_: filter, category, label

**VersionRange**:
A constraint on which versions of a Dependency satisfy it, expressed in NuGet range syntax (e.g. `[2.2.3,3.0)`, `[2.0,)`) inside the Version field. A bare version (`3.2.1`) is not a range — it means exactly that version.
_Avoid_: version spec, version constraint, MinimumVersion/MaximumVersion
A constraint on which versions satisfy a Dependency, expressed in the DependencyScript's syntax inside Version. Gallery, NuGet, and Chocolatey Dependencies use NuGet range syntax (for example `[2.2.3,3.0)`); Npm uses npm semver. A bare version (`3.2.1`) means exactly that version.
_Avoid_: version spec, MinimumVersion/MaximumVersion

**Lock**:
A `<name>.lock.json` file next to a DependencyFile, written by `Update-PSDependLock`, that records one exact version per `DependencyType::Name` for every Dependency whose DependencyScript supports Resolve, including transitive dependencies. Root entries also fingerprint resolution Source and Parameters. Consumed automatically by `Get-Dependency`/`Invoke-PSDepend`; malformed, unsafe, or stale locks are rejected.
_Avoid_: lockfile (npm's), pin file, freeze

**Resolve**:
The PSDependAction that asks a DependencyScript for the highest version satisfying Version at its source, plus direct dependencies as ranges, without installing. Runs alone; only DependencyScripts that opt in support it. The graph engine is greedy and does not backtrack to older parent versions.
_Avoid_: lookup, query, find

## Relationships

Expand All @@ -53,6 +61,7 @@ _Avoid_: version spec, version constraint, MinimumVersion/MaximumVersion
- A **DependencyScript** receives a **Dependency** and a set of **PSDependAction** flags on each invocation
- **Target** is a field on a **Dependency** interpreted differently by each **DependencyScript**
- A **Dependency**'s Version field carries either an exact version or a **VersionRange**; the `PSGalleryModule` and `PSGalleryNuget` **DependencyScripts** resolve a **VersionRange** to a concrete version to install, while `PSResourceGet` passes the range to `Install-PSResource` and lets it resolve
- A **Lock** belongs to exactly one **DependencyFile**; it is produced by invoking **Resolve** on each **DependencyScript** that supports it and, when applied, pins each **Dependency**'s Version and adds locked transitive packages as **Prerequisites** of the **Dependency** that pulled them in

## Example dialogue

Expand Down
3 changes: 2 additions & 1 deletion PSDepend/PSDepend.psd1
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,8 @@
'Install-Dependency',
'Invoke-DependencyScript',
'Invoke-PSDepend',
'Test-Dependency'
'Test-Dependency',
'Update-PSDependLock'
)

# Cmdlets to export from this module
Expand Down
55 changes: 53 additions & 2 deletions PSDepend/PSDependScripts/Chocolatey.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,12 @@
Defaults to https://community.chocolatey.org/install.ps1

.PARAMETER PSDependAction
Test, or Install the package. Defaults to Install
Test, Install, or Resolve the package. Defaults to Install

Test: Return true or false on whether the dependency is in place
Install: Install the dependency
Resolve: Query the source for the highest version satisfying Version and report
its dependencies. Requires an HTTP(S) NuGet v2 feed URL and performs no installation.

.EXAMPLE
@{
Expand Down Expand Up @@ -76,7 +78,7 @@ param(

[string]$ChocoInstallScriptUrl = 'https://community.chocolatey.org/install.ps1',

[ValidateSet('Test', 'Install')]
[ValidateSet('Test', 'Install', 'Resolve')]
[string[]]$PSDependAction = @('Install')
)

Expand Down Expand Up @@ -237,6 +239,55 @@ if (-not $Dependency.Source -or $Source -eq '') {

$Credential = $Dependency.Credential

if ($PSDependAction -contains 'Resolve') {
# Chocolatey feeds are NuGet v2 OData; query the feed directly so Resolve never needs choco.exe.
if ($Source -notmatch '^https?://') {
Write-Error "Resolve for [$Name] requires a NuGet v2 feed URL as Source; got [$Source]"
return
}
if ($Credential -and $Source -notmatch '^https://') {
Write-Error "Resolve for [$Name] requires an HTTPS Source when Credential is supplied; got [$Source]"
return
}

$findParams = @{
Name = $Name
PackageSourceUrl = $Source
}
if ($Credential) {
$findParams.Credential = $Credential
}
# choco install/upgrade never picks a prerelease without --pre, so Resolve ignores them too.
$packages = @(Find-NugetPackage @findParams | Where-Object { $_.Version -and $_.Properties.IsPrerelease -ne 'true' })

$selected = $null
if ($Version -eq 'latest') {
foreach ($package in $packages) {
if ($null -eq $selected -or (Compare-Version -ReferenceVersion $package.Version -DifferenceVersion $selected.Version) -gt 0) {
$selected = $package
}
}
} else {
$resolvedVersion = Resolve-VersionInRange -Candidate $packages.Version -Required $Version
if ($resolvedVersion) {
$selected = $packages | Where-Object { $_.Version -eq $resolvedVersion } | Select-Object -First 1
}
}

if ($null -eq $selected) {
Write-Error "No version of [$Name] at [$Source] satisfies [$Version]"
return
}

[PSCustomObject]@{
PSTypeName = 'PSDepend.ResolvedDependency'
Name = $Name
Version = $selected.Version
Dependencies = ConvertFrom-NugetDependencyString -Dependencies ([string]$selected.Properties.Dependencies)
}
return
}

$versionRange = $null
if ($Version -ne 'latest') {
$versionRange = ConvertFrom-VersionRange -Version $Version
Expand Down
39 changes: 36 additions & 3 deletions PSDepend/PSDependScripts/Npm.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,14 @@

Note: We require npm in your path.

Lock behavior (Resolve): PSDepend's lock pins only the declared package to an
exact version. Transitive node dependencies are not resolved by PSDepend; npm's
own package-lock.json governs the package's subtree.

Relevant Dependency metadata:
DependencyName (Key): Node Package Name
Version: Version of the node package to install; defaults to latest.
Version: Exact version or npm semver range (for example, '^1.2.0' or
'>=1 <2'); defaults to latest. NuGet range syntax is not supported.
Target: Path to place the node_modules folder, and all relevant packages, in.
You can specify a full path, a UNC path, or a relative path from the
current directory. You can also specify the special keyword, 'Global',
Expand All @@ -23,10 +28,12 @@
If specified, the node package will be installed globally.

.PARAMETER PSDependAction
Test or Install the dependency. Defaults to Install
Test, Install or Resolve the dependency. Defaults to Install

Test: Return true or false on whether the dependency is in place
Install: Install the dependency
Resolve: Query npm for the highest version satisfying Version and report it.
NuGet range syntax is rejected. Performs no installation.

.EXAMPLE
@{
Expand Down Expand Up @@ -59,7 +66,7 @@ param (
[PSTypeName('PSDepend.Dependency')]
[PSObject[]]$Dependency,

[ValidateSet('Test', 'Install')]
[ValidateSet('Test', 'Install', 'Resolve')]
[string[]]$PSDependAction = @('Install'),
[switch]$Force,
[switch]$Global
Expand All @@ -81,6 +88,32 @@ If (-not [string]::IsNullOrEmpty($Target) -and $Target -ne 'global') {
}
}
#endregion Extract Dependency Data
#region Resolve Action
If ($PSDependAction -contains 'Resolve') {
if ($Version -match '[\[\]\(\),]') {
Write-Error "Npm dependency [$Name] uses NuGet range syntax [$Version]; use an npm semver range instead"
return
}
$Candidates = @(Find-NodeModule -PackageName $Name -Version $Version)
$Resolved = $null
foreach ($Candidate in $Candidates) {
if ($null -eq $Resolved -or (Compare-Version -ReferenceVersion $Candidate -DifferenceVersion $Resolved) -gt 0) {
$Resolved = $Candidate
}
}
if ($null -eq $Resolved) {
Write-Error "No version of [$Name] at [npm] satisfies [$Version]"
return
}
[PSCustomObject]@{
PSTypeName = 'PSDepend.ResolvedDependency'
Name = $Name
Version = $Resolved
Dependencies = @{}
}
return
}
#endregion Resolve Action
#region Test Action
If ($PSDependAction -contains 'Test') {
If ([string]::IsNullOrEmpty($Target)) {
Expand Down
44 changes: 40 additions & 4 deletions PSDepend/PSDependScripts/Nuget.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,12 @@
If specified and Target already exists, remove existing item before saving

.PARAMETER PSDependAction
Test, or Install the package. Defaults to Install
Test, Install, or Resolve the package. Defaults to Install

Test: Return true or false on whether the dependency is in place
Install: Install the dependency
Resolve: Query the source for the highest version satisfying Version and report
its dependencies. Used by Update-PSDependLock; performs no installation.

.EXAMPLE

Expand Down Expand Up @@ -73,7 +75,7 @@ param(

[switch]$Force,

[ValidateSet('Test', 'Install')]
[ValidateSet('Test', 'Install', 'Resolve')]
[string[]]$PSDependAction = @('Install'),

[Alias('DLLName')]
Expand All @@ -95,15 +97,49 @@ if (-not $Dependency.Source) {
$Source = 'https://www.nuget.org/api/v2/'
}

$Credential = $Dependency.Credential

if ($PSDependAction -contains 'Resolve') {
if ($Credential -and $Source -notmatch '^https://') {
Write-Error "Resolve for [$DependencyName] requires an HTTPS Source when Credential is supplied; got [$Source]"
return
}
$packages = @(Find-NugetPackage -Name $DependencyName -PackageSourceUrl $Source -Credential $Credential)
Comment thread
Copilot marked this conversation as resolved.
$stable = @($packages | Where-Object { $_.Properties.IsPrerelease -ne 'true' })
$resolvedVersion = $null
if ($Version -eq 'latest') {
foreach ($package in $stable) {
if (-not $resolvedVersion -or (Compare-Version -ReferenceVersion $package.Version -DifferenceVersion $resolvedVersion) -gt 0) {
$resolvedVersion = $package.Version
}
}
}
else {
$requestedRange = ConvertFrom-VersionRange -Version $Version
$candidates = if ($requestedRange.IsExact) { $packages } else { $stable }
$resolvedVersion = Resolve-VersionInRange -Candidate @($candidates.Version) -Required $Version
}
if (-not $resolvedVersion) {
Write-Error "No version of [$DependencyName] at [$Source] satisfies [$Version]"
return
}
$resolved = $packages | Where-Object { $_.Version -eq $resolvedVersion } | Select-Object -First 1
[PSCustomObject]@{
PSTypeName = 'PSDepend.ResolvedDependency'
Name = $DependencyName
Version = $resolvedVersion
Dependencies = ConvertFrom-NugetDependencyString -Dependencies ([string]$resolved.Properties.Dependencies)
}
return
}

# We use target as a proxy for Scope
$Target = $Dependency.Target
if (-not $Dependency.Target) {
Write-Error "Nuget requires a Dependency Target. Skipping [$DependencyName]"
return
}

$Credential = $Dependency.Credential

if (-not (Get-Command Nuget -ErrorAction SilentlyContinue)) {
if (Test-PlatformSupport -Type 'Nuget' -Support 'windows', 'core') {
BootStrap-Nuget -NugetPath $NuGetPath
Expand Down
Loading
Loading