Private/Merge-PSDependLock.ps1

function Merge-PSDependLock {
    <#
    .SYNOPSIS
    Apply a lock to the Dependencies parsed from its DependencyFile.
 
    .DESCRIPTION
    First verifies the lock still describes the DependencyFile: every
    Dependency in the file must appear in the lock with the same
    DependencyType, Name and requested Version, and the lock must not list
    Dependencies the file no longer has. Any drift throws, mirroring npm ci,
    so a stale lock is never silently installed.
 
    Then, for each locked Dependency, the requested Version is replaced by the
    exact locked version and the locked transitive packages are materialized as
    additional Dependency objects cloned from the Dependency that pulled them
    in. DependsOn edges ensure children install before parents. A package is
    materialized once per root so roots with different installation contexts
    each receive their transitive dependencies. Dependencies whose type could
    not be locked pass through untouched.
 
    .PARAMETER Dependency
    The Dependencies parsed from one DependencyFile.
 
    .PARAMETER Lock
    The lock object from Import-PSDependLock.
 
    .PARAMETER LockPath
    Path of the lock file, used in error messages.
 
    .PARAMETER DependencyFile
    Path of the DependencyFile represented by Dependency. Required even when
    the file contains no Dependencies so stale locks can still be rejected.
 
    .EXAMPLE
    Merge-PSDependLock -Dependency $deps -Lock (Import-PSDependLock $lockPath) -LockPath $lockPath
    #>

    [Diagnostics.CodeAnalysis.SuppressMessageAttribute(
        'PSUseShouldProcessForStateChangingFunctions',
        '',
        Justification = 'Transforms in-memory Dependency objects; it does not change external state.'
    )]
    [CmdletBinding()]
    [OutputType([PSObject[]])]
    param(
        [AllowEmptyCollection()]
        [PSTypeName('PSDepend.Dependency')]
        [PSObject[]]$Dependency,

        [Parameter(Mandatory)]
        [System.Collections.IDictionary]$Lock,

        [Parameter(Mandatory)]
        [string]$LockPath,

        [Parameter(Mandatory)]
        [string]$DependencyFile
    )

    # Stale check: the lock's view of the DependencyFile must match what was parsed
    $problems = New-Object System.Collections.ArrayList
    $seen = @{}
    foreach ($root in $Dependency) {
        $seen[$root.DependencyName] = $true
        $name = if ($root.Name) { $root.Name } else { $root.DependencyName }
        $requested = Get-PSDependRequestedVersion -Version $root.Version
        $contextHash = Get-PSDependResolutionContext -Dependency $root
        if (-not $Lock.dependencies.Contains($root.DependencyName)) {
            $null = $problems.Add("[$($root.DependencyName)] is not in the lock")
            continue
        }
        $entry = $Lock.dependencies[$root.DependencyName]
        if ($entry.dependencyType -ne $root.DependencyType -or $entry.name -ne $name -or $entry.requested -ne $requested) {
            $null = $problems.Add(
                "[$($root.DependencyName)] changed: lock has [$($entry.dependencyType)] [$($entry.name)] [$($entry.requested)], " +
                "file has [$($root.DependencyType)] [$name] [$requested]"
            )
        }
        if ($entry.contextHash -ne $contextHash) {
            $null = $problems.Add("[$($root.DependencyName)] resolution source or parameters changed")
        }
    }
    foreach ($lockedName in $Lock.dependencies.Keys) {
        if (-not $seen.ContainsKey($lockedName)) {
            $null = $problems.Add("[$lockedName] is in the lock but not in the DependencyFile")
        }
    }
    if ($problems.Count -gt 0) {
        $message = "Lock file [$LockPath] is out of date with [$DependencyFile]:`n - $($problems -join "`n - ")"
        $message += "`nRun Update-PSDependLock -Path '$DependencyFile' to refresh it, or use -IgnoreLock to resolve without it"
        throw $message
    }


    $synthesized = [ordered]@{}
    $usedDependencyNames = @{}
    foreach ($root in $Dependency) {
        $usedDependencyNames[$root.DependencyName] = $true
    }

    function New-LockedDependencyName {
        param([string]$Name, [string]$Version, [string]$ContextName)

        $baseName = "$Name@$Version"
        $candidate = $baseName
        if ($usedDependencyNames.ContainsKey($candidate)) {
            $candidate = "$baseName#$ContextName"
        }
        $suffix = 2
        while ($usedDependencyNames.ContainsKey($candidate)) {
            $candidate = "$baseName#$ContextName-$suffix"
            $suffix++
        }
        $usedDependencyNames[$candidate] = $true
        $candidate
    }

    function Get-LockedPackage {
        param([string]$Key)
        if (-not $Lock.packages.Contains($Key)) {
            throw "Lock file [$LockPath] is corrupt: package [$Key] is referenced but not locked. Run Update-PSDependLock -Path '$dependencyFile' to regenerate it."
        }
        $Lock.packages[$Key]
    }

    # Materialization is scoped to a root Dependency. The same locked package
    # may need installing more than once when roots use different Targets,
    # Sources, Parameters or Tags.
    function Resolve-LockedChild {
        param(
            [string]$Key,
            [PSObject]$Template,
            [string]$RootKey,
            [string]$ContextName
        )

        if ($Key -eq $RootKey) {
            return $Template.DependencyName
        }
        $materializationKey = "$ContextName`0$Key"
        if ($synthesized.Contains($materializationKey)) {
            return $synthesized[$materializationKey].DependencyName
        }
        $package = Get-LockedPackage -Key $Key
        $name = $Key.Substring($Key.IndexOf('::') + 2)
        $child = [PSCustomObject]@{
            PSTypeName      = 'PSDepend.Dependency'
            DependencyFile  = $Template.DependencyFile
            DependencyName  = (New-LockedDependencyName -Name $name -Version $package.version -ContextName $ContextName)
            DependencyType  = $Template.DependencyType
            Name            = $name
            Version         = $package.version
            Parameters      = $Template.Parameters
            Source          = $Template.Source
            Target          = $Template.Target
            AddToPath       = $Template.AddToPath
            Tags            = $Template.Tags
            DependsOn       = $null
            PreScripts      = $null
            PostScripts     = $null
            Credential      = $Template.Credential
            PSDependOptions = $Template.PSDependOptions
            Raw             = $null
        }
        # Register before recursion so a cycle terminates at the existing node.
        $synthesized[$materializationKey] = $child
        $child.DependsOn = Get-LockedChildList -Package $package -DependencyType $Template.DependencyType `
            -Template $Template -RootKey $RootKey -ContextName $ContextName
        $child.DependencyName
    }

    function Get-LockedChildList {
        param(
            $Package,
            [string]$DependencyType,
            [PSObject]$Template,
            [string]$RootKey,
            [string]$ContextName
        )

        $names = foreach ($childName in $Package.dependencies.Keys) {
            Resolve-LockedChild -Key "${DependencyType}::$childName" -Template $Template `
                -RootKey $RootKey -ContextName $ContextName
        }
        if ($names) { @($names) } else { $null }
    }

    foreach ($root in $Dependency) {
        $entry = $Lock.dependencies[$root.DependencyName]
        if (-not $entry.Contains('resolved')) {
            continue
        }
        $package = Get-LockedPackage -Key $entry.resolved
        Write-Verbose "Lock pins [$($root.DependencyName)] to [$($package.version)] (requested [$($entry.requested)])"
        $root.Version = $package.version
        $children = Get-LockedChildList -Package $package -DependencyType $root.DependencyType `
            -Template $root -RootKey $entry.resolved -ContextName $root.DependencyName
        if ($children) {
            $dependencies = @($root.DependsOn) + $children
            $root.DependsOn = @($dependencies | Where-Object { $_ } | Select-Object -Unique)
        }
    }

    $Dependency
    $synthesized.Values
}