PSResourceGet.Bootstrap.psm1

#Region '.\prefix.ps1' -1

using module .\Modules\DscResource.Base

$script:dscResourceCommonModulePath = Join-Path -Path $PSScriptRoot -ChildPath 'Modules/DscResource.Common'
Import-Module -Name $script:dscResourceCommonModulePath

$script:localizedData = Get-LocalizedData -DefaultUICulture 'en-US'
#EndRegion '.\prefix.ps1' 7
#Region '.\Enum\SingleInstance.ps1' -1

enum SingleInstance
{
    Yes
}
#EndRegion '.\Enum\SingleInstance.ps1' 5
#Region '.\Classes\001.PSResourceGetBootstrapReason.ps1' -1

<#
    .SYNOPSIS
        The reason a property of a DSC resource is not in desired state.
 
    .DESCRIPTION
        A DSC resource can have a read-only property `Reasons` that the compliance
        part (audit via Azure Policy) of Azure AutoManage Machine Configuration
        uses. The property Reasons holds an array of PSResourceGetBootstrapReason.
        Each PSResourceGetBootstrapReason explains why a property of a DSC resource
        is not in desired state.
#>


class PSResourceGetBootstrapReason
{
    [DscProperty()]
    [System.String]
    $Code

    [DscProperty()]
    [System.String]
    $Phrase
}
#EndRegion '.\Classes\001.PSResourceGetBootstrapReason.ps1' 23
#Region '.\Classes\020.BootstrapPSResourceGet.ps1' -1

<#
    .SYNOPSIS
        The `BootstrapPSResourceGet` DSC resource is used to bootstrap the module
        Microsoft.PowerShell.PSResourceGet to the specified location.
 
    .DESCRIPTION
        The `BootstrapPSResourceGet` DSC resource is used to bootstrap the module
        Microsoft.PowerShell.PSResourceGet to the specified location.
 
        It supports two parameter sets: 'Destination' and 'Scope'. The 'Destination'
        parameter set allows you to specify a specific location to save the module,
        while the 'ModuleScope' parameter set saves the module to the appropriate
        `$env:PSModulePath` location based on the specified scope ('CurrentUser'
        or 'AllUsers').
 
        The built-in parameter **PSDscRunAsCredential** can be used to run the resource
        as another user.
 
        ## Requirements
 
        * Target machine must be running a operating system supporting running
          class-based DSC resources.
        * Target machine must support running Microsoft.PowerShell.PSResourceGet.
 
        ## Known issues
 
        All issues are not listed here, see [here for all open issues](https://github.com/viscalyx/PSResourceGet.Bootstrap/issues?q=is%3Aissue+is%3Aopen+in%3Atitle+BootstrapPSResourceGet).
 
        ### Property **Reasons** does not work with **PSDscRunAsCredential**
 
        When using the built-in parameter **PSDscRunAsCredential** the read-only
        property **Reasons** will return empty values for the properties **Code**
        and **Phrase. The built-in property **PSDscRunAsCredential** does not work
        together with class-based resources that using advanced type like the parameter
        **Reasons** have.
 
    .PARAMETER IsSingleInstance
        Specifies that only a single instance of the resource can exist in one and
        the same configuration. Must always be set to the value `Yes`.
 
    .PARAMETER Destination
        Specifies the destination path where the module should be saved. This parameter
        is mandatory when using the 'Destination' parameter set. The path must be a valid
        container. This parameter may not be used at the same time as the parameter
        `ModuleScope`.
 
    .PARAMETER ModuleScope
        Specifies the scope for saving the module. This parameter is used when using the
        'ModuleScope' parameter set. The valid values are 'CurrentUser' and 'AllUsers'. The
        default value is 'CurrentUser'. This parameter may not be used at the same time
        as the parameter Destination.
 
    .PARAMETER Version
        Specifies the version of the Microsoft.PowerShell.PSResourceGet module to download.
        If not specified, the latest version will be downloaded.
 
    .EXAMPLE
        Invoke-DscResource -ModuleName PSResourceGet.Bootstrap -Name BootstrapPSResourceGet -Method Get -Property @{
            IsSingleInstance = 'Yes'
            ModuleScope = 'CurrentUser'
        }
 
        This example shows how to call the resource using Invoke-DscResource. This
        example bootstraps the Microsoft.PowerShell.PSResourceGet module, saving
        it to the appropriate location based on the scope `'CurrentUser'`.
 
    .EXAMPLE
        Invoke-DscResource -ModuleName PSResourceGet.Bootstrap -Name BootstrapPSResourceGet -Method Get -Property @{
            IsSingleInstance = 'Yes'
            ModuleScope = 'CurrentUser'
            Version = '1.0.2'
        }
 
        This example shows how to call the resource using Invoke-DscResource. This
        example bootstraps the Microsoft.PowerShell.PSResourceGet module with version
        1.0.2, saving it to the appropriate location based on the scope `'CurrentUser'`.
 
    .EXAMPLE
        Invoke-DscResource -ModuleName PSResourceGet.Bootstrap -Name BootstrapPSResourceGet -Method Get -Property @{
            IsSingleInstance = 'Yes'
            Destination = '/path/to/destination'
        }
 
        This example shows how to call the resource using Invoke-DscResource. This
        example bootstraps the Microsoft.PowerShell.PSResourceGet module, saving it
        to the path specified in the parameter `Destination`.
#>

[DscResource(RunAsCredential = 'Optional')]
class BootstrapPSResourceGet : ResourceBase
{
    [DscProperty(Key)]
    [SingleInstance]
    $IsSingleInstance

    # The Destination is evaluated if exist in AssertProperties().
    [DscProperty()]
    [System.String]
    $Destination

    <#
        The ModuleScope is evaluated in AssertProperties(). This parameter cannot
        use the ValidateSet() attribute since it is not possible to set a null value,
        unless it is set to [ValidateSet('CurrentUser', 'AllUsers', $null)] .
 
        The parameter name Scope could not be used as it is a reserved keyword in
        PowerShell DSC, if used it throws an error when parsing a configuration.
    #>

    [DscProperty()]
    [System.String]
    $ModuleScope

    # The Version is evaluated if exist in AssertProperties().
    [DscProperty()]
    [System.String]
    $Version

    [DscProperty(NotConfigurable)]
    [PSResourceGetBootstrapReason[]]
    $Reasons

    BootstrapPSResourceGet () : base ($PSScriptRoot)
    {
        # These properties will not be enforced.
        $this.ExcludeDscProperties = @(
            'IsSingleInstance'
        )
    }

    [BootstrapPSResourceGet] Get()
    {
        # Call the base method to return the properties.
        return ([ResourceBase] $this).Get()
    }

    [System.Boolean] Test()
    {
        # Call the base method to test all of the properties that should be enforced.
        return ([ResourceBase] $this).Test()
    }

    [void] Set()
    {
        # Call the base method to enforce the properties.
        ([ResourceBase] $this).Set()
    }

    <#
        Base method Get() call this method to get the current state as a hashtable.
        The parameter keyProperty will contain the key properties.
    #>

    hidden [System.Collections.Hashtable] GetCurrentState([System.Collections.Hashtable] $keyProperty)
    {
        Write-Debug -Message (
            'Enter GetCurrentState. Parameters: {0}' -f ($keyProperty | ConvertTo-Json -Compress)
        )

        Write-Verbose -Message $this.localizedData.EvaluateModule

        $currentState = @{
            IsSingleInstance = [SingleInstance]::Yes
        }

        # Need to find out how to evaluate state since there are no key properties for that.
        $assignedDscProperties = $this | Get-DscProperty -HasValue -Attribute @(
            'Mandatory'
            'Optional'
        )

        $testModuleExistParameters = @{
            Name = 'Microsoft.PowerShell.PSResourceGet'
        }

        if ($assignedDscProperties.Keys -contains 'Version')
        {
            $testModuleExistParameters.Version = $assignedDscProperties.Version

            $currentState.Version = $null
        }

        # If it is ModuleScope wasn't specified, then destination was specified.
        if ($assignedDscProperties.Keys -contains 'ModuleScope')
        {
            Write-Verbose -Message (
                $this.localizedData.EvaluatingScope -f $assignedDscProperties.ModuleScope
            )

            $currentState.ModuleScope = $null

            $testModuleExistParameters.Scope = $assignedDscProperties.ModuleScope

            if ((Test-ModuleExist @testModuleExistParameters -ErrorAction 'Stop'))
            {
                $currentState.ModuleScope = $assignedDscProperties.ModuleScope

                if ($assignedDscProperties.Keys -contains 'Version')
                {
                    $currentState.Version = $assignedDscProperties.Version
                }
            }
        }
        else
        {
            Write-Verbose -Message (
                $this.localizedData.EvaluatingDestination -f $assignedDscProperties.Destination
            )

            $currentState.Destination = $null

            $testModuleExistParameters.Path = $assignedDscProperties.Destination

            if ((Test-ModuleExist @testModuleExistParameters -ErrorAction 'Stop'))
            {
                $currentState.Destination = $assignedDscProperties.Destination

                if ($assignedDscProperties.Keys -contains 'Version')
                {
                    $currentState.Version = $assignedDscProperties.Version
                }
            }
        }

        Write-Debug -Message 'Exit GetCurrentState'

        return $currentState
    }

    <#
        Base method Set() call this method with the properties that should be
        enforced are not in desired state. It is not called if all properties
        are in desired state. The variable $property contain the properties
        that are not in desired state.
    #>

    hidden [void] Modify([System.Collections.Hashtable] $property)
    {
        Write-Debug -Message (
            'Enter Modify. Parameters: {0}' -f ($property | ConvertTo-Json -Compress)
        )

        Write-Verbose -Message $this.localizedData.Bootstrapping

        if ($property.Keys -contains 'ModuleScope')
        {
            $property.Scope = $property.ModuleScope

            $property.Remove('ModuleScope')
        }

        Write-Debug -Message "Start-PSResourceGetBootstrap Parameters:`n$($property | Out-String)"

        Start-PSResourceGetBootstrap @property -Force -ErrorAction 'Stop'

        Write-Debug -Message 'Exit Modify'
    }

    <#
        Base method Assert() call this method with the properties that was assigned
        a value.
    #>

    hidden [void] AssertProperties([System.Collections.Hashtable] $property)
    {
        Write-Debug -Message (
            'Enter AssertProperties. Parameters: {0}' -f ($property | ConvertTo-Json -Compress)
        )

        # The properties ModuleScope and Destination are mutually exclusive.
        $assertBoundParameterParameters = @{
            BoundParameterList     = $property
            MutuallyExclusiveList1 = @(
                'ModuleScope'
            )
            MutuallyExclusiveList2 = @(
                'Destination'
            )
        }

        Assert-BoundParameter @assertBoundParameterParameters

        if ($property.Keys -notcontains 'ModuleScope' -and $property.Keys -notcontains 'Destination')
        {
            $errorMessage = $this.localizedData.MissingRequiredParameter

            New-InvalidArgumentException -ArgumentName 'ModuleScope, Destination' -Message $errorMessage
        }

        if ($property.Keys -contains 'ModuleScope')
        {
            <#
                It is not possible to set a null value to the parameter ModuleScope
                when it has a [ValidateSet()] unless it would be set to
                [ValidateSet('CurrentUser', 'AllUsers', $null)]. But that would
                give a strange output if giving the wrong value to the parameter:
                E.g.
 
                    'The argument "CurrentUser2" does not belong to the set
                    "CurrentUser,AllUsers," specified by the ValidateSet
                    attribute.'
            #>

            if ($property.ModuleScope -notin ('CurrentUser', 'AllUsers'))
            {
                $errorMessage = $this.localizedData.ModuleScopeInvalid -f $property.ModuleScope

                New-InvalidArgumentException -ArgumentName 'ModuleScope' -Message $errorMessage
            }

            $scopeModulePath = Get-PSModulePath -Scope $property.ModuleScope

            if ([System.String]::IsNullOrEmpty($scopeModulePath) -or -not (Test-Path -Path $scopeModulePath))
            {
                $errorMessage = $this.localizedData.ScopePathInvalid -f $property.ModuleScope, $scopeModulePath

                New-InvalidArgumentException -ArgumentName 'ModuleScope' -Message $errorMessage
            }
        }

        if ($property.Keys -contains 'Destination')
        {
            if ([System.String]::IsNullOrEmpty($property.Destination) -or -not (Test-Path -Path $property.Destination))
            {
                $errorMessage = $this.localizedData.DestinationInvalid -f $property.Destination

                New-InvalidArgumentException -ArgumentName 'Destination' -Message $errorMessage
            }
        }

        if ($property.Keys -contains 'Version')
        {
            $isValidVersion = (
                # From https://semver.org/#is-there-a-suggested-regular-expression-regex-to-check-a-semver-string
                $property.Version  -match '^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$' -or
                # Need to support the Nuget range syntax as well.
                $property.Version  -match '^[\[(][0-9\.\,]*[\])]$'
            )

            if (-not $isValidVersion)
            {
                $errorMessage = $this.localizedData.VersionInvalid -f $property.Version

                New-InvalidArgumentException -ArgumentName 'Version' -Message $errorMessage
            }
        }

        Write-Debug -Message 'Exit AssertProperties'
    }
}
#EndRegion '.\Classes\020.BootstrapPSResourceGet.ps1' 345
#Region '.\Public\Start-PSResourceGetBootstrap.ps1' -1

<#
    .SYNOPSIS
        Bootstraps the Microsoft.PowerShell.PSResourceGet module to the specified location.
 
    .DESCRIPTION
        The command Start-PSResourceGetBootstrap is used to bootstrap the Microsoft.PowerShell.PSResourceGet
        module.
 
        It supports two parameter sets: 'Destination' and 'Scope'. The 'Destination'
        parameter set allows you to specify a specific location to save the module,
        while the 'Scope' parameter set saves the module to the appropriate `$env:PSModulePath`
        location based on the specified scope ('CurrentUser' or 'AllUsers').
 
    .PARAMETER Destination
        Specifies the destination path where the module should be saved. This parameter
        is mandatory when using the 'Destination' parameter set. The path must be a valid
        container.
 
    .PARAMETER Scope
        Specifies the scope for saving the module. This parameter is used when using the
        'Scope' parameter set. The valid values are 'CurrentUser' and 'AllUsers'. The
        default value is 'CurrentUser'.
 
    .PARAMETER Version
        Specifies the version of the Microsoft.PowerShell.PSResourceGet module to download.
        If not specified, the latest version will be downloaded.
 
    .PARAMETER UseCompatibilityModule
        Indicates whether to use the compatibility module. If this switch parameter is
        present, the compatibility module will be downloaded.
 
    .PARAMETER CompatibilityModuleVersion
        Specifies the version of the compatibility module to download. If not specified,
        it will default to a minimum required range that includes previews.
 
    .PARAMETER Force
        Forces the operation without prompting for confirmation. This is useful when
        running the script in non-interactive mode.
 
    .PARAMETER ImportModule
        Indicates whether to import the module after it has been downloaded.
 
    .OUTPUTS
        None.
 
    .EXAMPLE
        Start-PSResourceGetBootstrap -Destination 'C:\Modules'
 
        This example bootstraps the Microsoft.PowerShell.PSResourceGet module, saving
        it to the specified destination path "C:\Modules".
 
    .EXAMPLE
        Start-PSResourceGetBootstrap -Scope 'AllUsers'
 
        This example bootstraps the Microsoft.PowerShell.PSResourceGet module, saving
        it to the appropriate location based on the 'AllUsers' scope.
 
    .EXAMPLE
        Start-PSResourceGetBootstrap -UseCompatibilityModule
 
        This example bootstraps the Microsoft.PowerShell.PSResourceGet module, saving
        it to the appropriate location based on the default scope ('CurrentUser').
        It will also save the compatibility module to the same location.
#>

function Start-PSResourceGetBootstrap
{
    # TODO: Change impact to 'Medium' when the script is stable.
    [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'High', DefaultParameterSetName = 'Scope')]
    [OutputType()]
    param
    (
        [Parameter(Mandatory = $true, ParameterSetName = 'Destination')]
        [ValidateScript({
            Write-Verbose -Message "Destination folder is set to '$($_)'."
            Test-Path -Path $_ -PathType 'Container'
        })]
        [System.String]
        $Destination,

        [Parameter(ParameterSetName = 'Scope')]
        [ValidateSet('CurrentUser', 'AllUsers')]
        [System.String]
        $Scope = 'CurrentUser',

        [Parameter()]
        [ValidateScript({
            # From https://semver.org/#is-there-a-suggested-regular-expression-regex-to-check-a-semver-string
            $_ -match '^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$' -or
            # Need to support the Nuget range syntax as well.
            $_ -match '^[\[(][0-9\.\,]*[\])]$'
        })]
        [System.String]
        $Version,

        [Parameter()]
        [System.Management.Automation.SwitchParameter]
        $UseCompatibilityModule,

        [Parameter()]
        [ValidateScript({
            # From https://semver.org/#is-there-a-suggested-regular-expression-regex-to-check-a-semver-string
            $_ -match '^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$' -or
            # Need to support the Nuget range syntax as well.
            $_ -match '^[\[(][0-9\.\,]*[\])]$'
        })]
        [System.String]
        $CompatibilityModuleVersion = '[3.0.22,]',

        [Parameter()]
        [System.Management.Automation.SwitchParameter]
        $ImportModule,

        [Parameter()]
        [System.Management.Automation.SwitchParameter]
        $Force
    )

    if ($Force.IsPresent -and -not $Confirm)
    {
        $ConfirmPreference = 'None'
    }

    $name = 'Microsoft.PowerShell.PSResourceGet'

    switch ($PSCmdlet.ParameterSetName)
    {
        'Destination'
        {
            # Resolve relative path to absolute path.
            $Destination = Resolve-Path -Path $Destination -ErrorAction 'Stop'

            $verboseDescriptionMessage = $script:localizedData.Start_PSResourceGetBootstrap_Destination_ShouldProcessVerboseDescription -f $name, $Destination

            Write-Debug -Message ($script:localizedData.Start_PSResourceGetBootstrap_Destination_SaveModule -f $Destination)
        }

        'Scope'
        {
            $verboseDescriptionMessage = $script:localizedData.Start_PSResourceGetBootstrap_Scope_ShouldProcessVerboseDescription -f $name, $Scope

            $scopeModulePath = Get-PSModulePath -Scope $Scope

            if (-not (Test-Path -Path $scopeModulePath))
            {
                # cSpell: ignore SPSRGB
                $exception = New-Exception -Message ($script:localizedData.Start_PSResourceGetBootstrap_MissingScopePath -f $scopeModulePath, $Scope)
                $errorRecord = New-ErrorRecord -Exception $exception -ErrorId 'SPSRGB0004' -ErrorCategory 'InvalidOperation' -TargetObject $name

                $PSCmdlet.ThrowTerminatingError($errorRecord)
            }

            $Destination = $scopeModulePath

            Write-Debug -Message ($script:localizedData.Start_PSResourceGetBootstrap_Scope_SaveModule -f $Scope, $Destination)
        }
    }

    $loadedModule = Get-Module -Name $name

    if ($loadedModule -and $loadedModule.Path -match [System.Text.RegularExpressions.Regex]::Escape($Destination))
    {
        Write-Verbose -Message ($script:localizedData.Start_PSResourceGetBootstrap_AlreadyInUse -f $name)

        # Since it is loaded into the session, assume it is downloaded and working.
        $moduleAvailable = $true
    }
    else
    {
        $verboseWarningMessage = $script:localizedData.Start_PSResourceGetBootstrap_ShouldProcessVerboseWarning -f $name
        $captionMessage = $script:localizedData.Start_PSResourceGetBootstrap_ShouldProcessCaption -f $name

        if ($PSCmdlet.ShouldProcess($verboseDescriptionMessage, $verboseWarningMessage, $captionMessage))
        {
            $moduleAvailable = $false

            try
            {
                if (-not $Version)
                {
                    # Default to latest version if no version is passed in parameter or specified in configuration.
                    $psResourceGetUri = "https://www.powershellgallery.com/api/v2/package/$name"
                }
                else
                {
                    $psResourceGetUri = "https://www.powershellgallery.com/api/v2/package/$name/$Version"
                }

                $invokeWebRequestParameters = @{
                    # TODO: Should support proxy parameters passed to the command.
                    Uri         = $psResourceGetUri
                    OutFile     = "$Destination/$name.nupkg" # cSpell: ignore nupkg
                    ErrorAction = 'Stop'
                }

                Invoke-WebRequest @invokeWebRequestParameters

                $moduleAvailable = $true
            }
            catch
            {
                $exception = New-Exception -ErrorRecord $_ -Message ($script:localizedData.Start_PSResourceGetBootstrap_FailedDownload -f $name)
                $errorRecord = New-ErrorRecord -Exception $exception -ErrorId 'SPSRGB0001' -ErrorCategory 'InvalidOperation' -TargetObject $name

                $PSCmdlet.ThrowTerminatingError($errorRecord)
            }

            if ($moduleAvailable)
            {
                try
                {
                    # On Windows PowerShell the command Expand-Archive do not like .nupkg as a zip archive extension.
                    $zipFileName = ((Split-Path -Path $invokeWebRequestParameters.OutFile -Leaf) -replace 'nupkg', 'zip')

                    $renameItemParameters = @{
                        Path    = $invokeWebRequestParameters.OutFile
                        NewName = $zipFileName
                        Force   = $true
                    }

                    Rename-Item @renameItemParameters
                }
                catch
                {
                    # If the rename fails, we should remove the .nupkg file.
                    Remove-Item -Path $invokeWebRequestParameters.OutFile

                    $exception = New-Exception -ErrorRecord $_ -Message ($script:localizedData.Start_PSResourceGetBootstrap_RenamedFailed -f $invokeWebRequestParameters.OutFile, $invokeWebRequestParameters.NewName)
                    $errorRecord = New-ErrorRecord -Exception $exception -ErrorId 'SPSRGB0002' -ErrorCategory 'InvalidOperation' -TargetObject $invokeWebRequestParameters.OutFile

                    $PSCmdlet.ThrowTerminatingError($errorRecord)
                }

                try
                {
                    $zipArchivePath = Join-Path -Path (Split-Path -Path $invokeWebRequestParameters.OutFile -Parent) -ChildPath $zipFileName

                    $expandArchiveParameters = @{
                        Path            = $zipArchivePath
                        DestinationPath = "$Destination/$name"
                        Force           = $true
                    }

                    Expand-Archive @expandArchiveParameters
                }
                catch
                {
                    $exception = New-Exception -ErrorRecord $_ -Message ($script:localizedData.Start_PSResourceGetBootstrap_ExpandFailed -f $zipArchivePath)
                    $errorRecord = New-ErrorRecord -Exception $exception -ErrorId 'SPSRGB0003' -ErrorCategory 'InvalidOperation' -TargetObject $zipArchivePath

                    $PSCmdlet.ThrowTerminatingError($errorRecord)
                }
                finally
                {
                    # When the expand succeeds, we should remove the .zip file.
                    Remove-Item -Path $zipArchivePath
                }

                if ($ImportModule.IsPresent)
                {
                    Import-Module -Name $expandArchiveParameters.DestinationPath -Force
                }
            }
        }
        else
        {
            if ((Test-Path -Path (Join-Path -Path $Destination -ChildPath $name)))
            {
                # Since it is available in the destination, assume it is downloaded and can work.
                $moduleAvailable = $true

                Write-Debug -Message 'Did not bootstrap, but module is available in the destination.'
            }
        }
    }

    if ($moduleAvailable -and $UseCompatibilityModule.IsPresent)
    {
        $name = 'PowerShellGet'

        $loadedModule = Get-Module -Name $name

        if ($loadedModule -and $loadedModule.Path -match [System.Text.RegularExpressions.Regex]::Escape($Destination))
        {
            Write-Verbose -Message ($script:localizedData.Start_PSResourceGetBootstrap_AlreadyInUse -f $name)
        }
        else
        {
            $verboseDescriptionMessage = $script:localizedData.Start_PSResourceGetBootstrap_CompatibilityModule_ShouldProcessVerboseDescription -f $name, $Destination
            $verboseWarningMessage = $script:localizedData.Start_PSResourceGetBootstrap_CompatibilityModule_ShouldProcessVerboseWarning -f $name
            $captionMessage = $script:localizedData.Start_PSResourceGetBootstrap_CompatibilityModule_ShouldProcessCaption -f $name

            if ($PSCmdlet.ShouldProcess($verboseDescriptionMessage, $verboseWarningMessage, $captionMessage))
            {
                $savePowerShellGetParameters = @{
                    Name            = $name
                    Path            = $Destination
                    Repository      = 'PSGallery'
                    TrustRepository = $true

                    # If not specified, default to a minimum required range that includes previews.
                    Version = $CompatibilityModuleVersion
                    # TODO: Should probably be a switch parameter when there is a full release out.
                    Prerelease = $true
                }

                Save-PSResource @savePowerShellGetParameters

                if ($ImportModule.IsPresent)
                {
                    Import-Module -Name "$Destination/$name"
                }
            }
        }
    }
}
#EndRegion '.\Public\Start-PSResourceGetBootstrap.ps1' 316