functions/_getExtensionFromPSRepository.ps1

# <copyright file="_getExtensionFromPSRepository.ps1" company="Endjin Limited">
# Copyright (c) Endjin Limited. All rights reserved.
# </copyright>

function _getExtensionFromPSRepository {
    [CmdletBinding()]
    [OutputType([hashtable])]
    param(
        [Parameter(Mandatory=$true)]
        [string] $Name,

        [Parameter(Mandatory=$true)]
        [string] $PSRepository,

        [Parameter(Mandatory=$true)]
        [string] $TargetPath,

        [Parameter()]
        [string] $Version,

        [Parameter()]
        [string] $MinimumVersion,

        [Parameter()]
        [switch] $PreRelease,

        [Parameter()]
        [string] $LockedVersion
    )

    # Setup some objects we'll use for splatting
    $extension = @{
        Name = $Name
        Repository = $PSRepository
        Enabled = $true
    }
    if ($Version) {
        $extension.Add("Version", $Version)
    }
    if ($MinimumVersion) {
        $extension.Add("MinimumVersion", $MinimumVersion)
    }

    # The lock file records the exact version to install, which takes precedence over the requested version. Whether
    # pre-release versions are allowed also comes from the locked version, so that an installed pre-release (e.g.
    # '1.0.0-beta1') is not used in place of a locked version without a pre-release tag (e.g. '1.0.0').
    if ($LockedVersion) {
        $Version = $LockedVersion
        $MinimumVersion = $null
        $PreRelease = $LockedVersion.Contains('-')
    }

    # Compare a version with fewer than 3 numeric parts (e.g. '1.4') as its full version (e.g. '1.4.0'), which is the
    # version that is installed. The requested version is still returned unchanged.
    if ($Version) {
        $completeVersion = _getCompleteExtensionVersion -Version $Version
        if ($completeVersion) {
            $Version = $completeVersion
        }
    }

    $psResourceArgs = @{
        Name = $Name
        PreRelease = $PreRelease
        Verbose = $false
    }
    if ($Version) {
        $psResourceArgs.Add("Version", $Version)
    }

    # A minimum version accepts any version in its range, up to the next major version
    $installedArgs = @{}
    if ($MinimumVersion) {
        $range = _getExtensionVersionRange -MinimumVersion $MinimumVersion
        $installedArgs.Add("MinimumVersion", $MinimumVersion)
    }
    $requestedVersion = $MinimumVersion ? $range.Text : "v$Version"

    # Records which repository an installed extension came from
    $sourceInfoFileName = '.zf-source.json'

    # Whether an installed copy came from another repository. Copies installed by earlier versions of ZeroFailed have no
    # record of their repository, so they are used as they are.
    $isFromOtherRepository = {
        param ($path)
        $sourceInfoPath = Join-Path $path $sourceInfoFileName
        if (Test-Path $sourceInfoPath) {
            $installedRepository = (Get-Content -Raw -Path $sourceInfoPath | ConvertFrom-Json -AsHashtable)['repository']
            if ($installedRepository -and $installedRepository -ne $PSRepository) {
                Write-Verbose "Extension '$Name' found locally but it was installed from a different repository. Found: '$installedRepository'; Required: '$PSRepository'"
                return $true
            }
        }
        return $false
    }

    # Check whether module is already installed. Remove a copy from another repository, as a module with the same name
    # and version can have different content there, then check for another installed version that can be used.
    $existingExtensionPath,$existingExtensionVersion = Get-InstalledExtensionDetails @psResourceArgs @installedArgs -TargetPath $TargetPath
    while ($existingExtensionPath -and (& $isFromOtherRepository $existingExtensionPath)) {
        # Stop if the copy cannot be removed (e.g. a file is in use), as it would be found again, or a partly removed
        # copy could be used
        try {
            Remove-Item -Path $existingExtensionPath -Recurse -Force -ErrorAction Stop
        }
        catch {
            throw ("Extension $Name ($existingExtensionVersion) was installed from another repository, but it could not be removed: " +
                   "$($_.Exception.Message) Delete '$existingExtensionPath', then run again.")
        }
        $existingExtensionPath,$existingExtensionVersion = Get-InstalledExtensionDetails @psResourceArgs @installedArgs -TargetPath $TargetPath
    }

    # Handle getting the module from the repository. An installed copy always matches the requested version, including
    # a pre-release (e.g. '1.0.0-beta1') of a requested version when pre-release versions are allowed (e.g. '1.0.0').
    if (!$existingExtensionPath) {
        Write-Verbose "Extension '$Name' not found locally, checking repository"

        # Only search the configured repository, as the lock file records it as the extension's source
        if ($MinimumVersion) {
            # The repository returns every version in the range, so install the newest one
            $availableModule = Find-PSResource @psResourceArgs -Repository $PSRepository -Version $range.Text -ErrorAction Ignore |
                                    ForEach-Object {
                                        $resourceVersion = [semver]::new($_.Version.Major, $_.Version.Minor, [Math]::Max($_.Version.Build, 0), $_.Prerelease)
                                        if (_testExtensionVersionInRange -Version $resourceVersion -Range $range -PreRelease:$PreRelease) {
                                            [pscustomobject]@{ Version = $resourceVersion; Resource = $_ }
                                        }
                                    } |
                                    Sort-Object -Property Version -Descending |
                                    Select-Object -First 1 -ExpandProperty Resource
        }
        else {
            $availableModule = Find-PSResource @psResourceArgs -Repository $PSRepository -ErrorAction Ignore
        }
        if ($availableModule) {
            Write-Host "Installing extension $Name from $PSRepository" -f Cyan
            $availableModule | Save-PSResource -Path $TargetPath -TrustRepository

            $existingExtensionPath,$existingExtensionVersion = Get-InstalledExtensionDetails @psResourceArgs @installedArgs -TargetPath $TargetPath
            if (!$existingExtensionPath) {
                throw "Failed to install extension $Name ($requestedVersion) from $PSRepository repository"
            }
            [ordered]@{
                repository = $PSRepository
                version = $existingExtensionVersion
            } | ConvertTo-Json | Set-Content -Path (Join-Path $existingExtensionPath $sourceInfoFileName)
            Write-Host "INSTALLED MODULE: $Name (v$existingExtensionVersion)" -f Cyan
        }
        else {
            Write-Warning "SKIPPED: Extension $Name not found in $PSRepository repository"
            $extension.Enabled = $false
        }
    }
    else {
        Write-Host "FOUND MODULE: $Name (v$existingExtensionVersion)" -f Cyan
    }

    # Return the additional extension metadata that this function has populated
    $additionalMetadata = @{
        Path = $existingExtensionPath
        Enabled = $extension.Enabled
        ResolvedVersion = $extension.Enabled ? $existingExtensionVersion : $null
    }

    # Only report the installed version when no version was requested, so the requested version is not overwritten
    if (!$extension.ContainsKey("Version") -and !$extension.ContainsKey("MinimumVersion")) {
        $additionalMetadata += @{ Version = $existingExtensionVersion }
    }

    return $additionalMetadata
}