PSSharp.ModuleFactory.psm1

$script:ExportModuleTemplatJsonSerializerOptions = [System.Text.Json.JsonSerializerOptions]::new([System.Text.Json.JsonSerializerDefaults]::General)

function Export-ModuleTemplate {
    [CmdletBinding(RemotingCapability = 'PowerShell', DefaultParameterSetName = 'Name')]
    param(
        [Parameter(Mandatory, ParameterSetName = 'Name', Position = 0)]
        [PSSharp.ModuleFactory.ModuleTemplateCompletion()]
        [SupportsWildcards()]
        [string]
        $Name,

        [Parameter(Mandatory, ParameterSetName = 'TemplateId', ValueFromPipelineByPropertyName)]
        [PSSharp.ModuleFactory.ModuleTemplateCompletion()]
        [Guid]
        $TemplateId,

        [Parameter(Mandatory)]
        [string]
        $OutputPath
    )
    process {
        $GetTemplateParameters = @{}
        if ($PSCmdlet.ParameterSetName -eq 'Name') {
            $GetTemplateParameters['Name'] = $Name
        }
        else {
            $GetTemplateParameters['TemplateId'] = $TemplateId
        }
        $Template = Get-ModuleTemplate @GetTemplateParameters

        if ($Template.Count -gt 1) {
            $ex = [System.Reflection.AmbiguousMatchException]::new('The module template name is ambiguous.')
            $er = [System.Management.Automation.ErrorRecord]::new(
                $ex,
                'AmbiguousModuleTemplate',
                [System.Management.Automation.ErrorCategory]::InvalidResult,
                $Name ?? $TemplateId
            )
            $PSCmdlet.WriteError($er)
            return
        }

        if (!$Template) { return; }

        $TemplateContents = Get-ModuleTemplateContents -Template $Template


        try {
            $TempArchive = New-TemporaryFile

            $CompressArchiveParameters = @{
                LiteralPath      = $TemplateContents.TemplateDirectory
                DestinationPath  = $TempArchive.FullName
                PassThru         = $true
                CompressionLevel = 'Optimal'
                Force            = $true
            }
            [void](Compress-Archive @CompressArchiveParameters)

            Use-DisposableObject ($FileStream = [System.IO.FileStream]::new($OutputPath, 'Create', 'Write')) {
                Use-DisposableObject ($ArchiveStream = [System.IO.FileStream]::new($TempArchive.FullName, 'Open', 'Read')) {
                    $MetadataBytes = [System.Text.Json.JsonSerializer]::SerializeToUtf8Bytes($Template, [PSSharp.ModuleFactory.ModuleTemplateMetadata], $script:ExportModuleTemplatJsonSerializerOptions)
                    $MetadataByteSize = $MetadataBytes.Count
                    $MetadataByteSizeBytes = [BitConverter]::GetBytes($MetadataByteSize)

                    $FileStream.Write($MetadataByteSizeBytes, 0, $MetadataByteSizeBytes.Count)
                    $FileStream.Write($MetadataBytes, 0, $MetadataBytes.Count)
                    $ArchiveStream.CopyTo($FileStream)
                }
            }
        }
        finally {
            if (Test-Path $TempArchive.FullName) {
                Remove-Item $TempArchive.FullName
            }
        }
    }
}
function Get-ModuleFingerprint {
    [CmdletBinding(
        DefaultParameterSetName = 'DefaultSet',
        RemotingCapability = [System.Management.Automation.RemotingCapability]::PowerShell
    )]
    [OutputType([PSSharp.ModuleFactory.ModuleFingerprint])]
    param(
        [Parameter(
            Position = 0,
            Mandatory,
            ValueFromPipelineByPropertyName,
            ParameterSetName = 'DefaultSet')]
        [PSSharp.ModuleFactory.ModuleNameCompletion()]
        [SupportsWildcards()]
        [string[]]
        $Name,

        [Parameter(
            Position = 0,
            Mandatory,
            ValueFromPipelineByPropertyName,
            ValueFromPipeline,
            ParameterSetName = 'FullyQualifiedNameSet'
        )]
        [Microsoft.PowerShell.Commands.ModuleSpecification[]]
        [PSSharp.ModuleFactory.NoCompletion()]
        [Alias('ModuleSpecification')]
        $FullyQualifiedName,

        [Parameter(
            Position = 0,
            Mandatory,
            ValueFromPipeline,
            ParameterSetName = 'ModuleInfoSet')]
        [PSSharp.ModuleFactory.NoCompletion()]
        [psmoduleinfo[]]
        $Module
    )
    begin {
        $PSModuleAutoLoadingPreference = [System.Management.Automation.PSModuleAutoLoadingPreference]::None
    }
    process {
        # Identify the module
        if ($PSCmdlet.ParameterSetName -eq 'DefaultSet') {
            $InvokeAsModule = $PSCmdlet.SessionState.Module
            if (-not $InvokeAsModule) {
                $InvokeAsModule = [psmoduleinfo]::new($false)
                $InvokeAsModule.SessionState = $PSCmdlet.SessionState
            }

            $Module = & ($InvokeAsModule) { Get-Module -Name $args[0] } $Name
        }
        elseif ($PSCmdlet.ParameterSetName -eq 'FullyQualifiedNameSet') {
            $InvokeAsModule = $PSCmdlet.SessionState.Module
            if (-not $InvokeAsModule) {
                $InvokeAsModule = [psmoduleinfo]::new($false)
                $InvokeAsModule.SessionState = $PSCmdlet.SessionState
            }

            $Module = & ($InvokeAsModule) { Get-Module -FullyQualifiedName $args[0] } $FullyQualifiedName
        }
        elseif ($PSCmdlet.ParameterSetName -ne 'ModuleInfoSet') {
            $ex = [System.NotImplementedException]::new()
            $er = [ErrorRecord]::new(
                $ex,
                'ParameterSetNotImplemented',
                [System.Management.Automation.ErrorCategory]::NotImplemented,
                $PSCmdlet.ParameterSetName
            )
            $er.ErrorDetails = Get-PSSharpModuleFactoryResourceString ParameterSetNotImplemented $PSCmdlet.ParameterSetName
            $er.ErrorDetails.RecommendedAction = "Contact the module author for support. $(PSCmdlet.SessionState.Module.RepositorySourceLocation)"
            $PSCmdlet.WriteError($er)
            return
        }

        # If the module is imported, we can easily create a new fingerprint from the module metadata. However, if
        # the module is not imported we do not get descriptive CommandInfo objects so the fingerprint will be
        # inaccurate.
        $ImportedModules = @(Get-Module)

        foreach ($m in $Module) {
            $ModuleIsImported = $ImportedModules -contains $m
            if ($ModuleIsImported) {
                $Fingerprint = [PSSharp.ModuleFactory.ModuleFingerprint]::new($m)
                $Fingerprint.PSObject.Properties.Add([psnoteproperty]::new('PSModuleName', $m.Name))
                $Fingerprint
            }
            else {
                $ex = [System.InvalidOperationException]::new((Get-PSSharpModuleFactoryResourceString ModuleNotImported))
                $er = [System.Management.Automation.ErrorRecord]::new(
                    $ex,
                    'ModuleNotImported',
                    [System.Management.Automation.ErrorCategory]::InvalidArgument,
                    $m
                )
                $er.ErrorDetails = Get-PSSharpModuleFactoryResourceString ModuleNotImportedInterpolated $m.Name
                $er.ErrorDetails.RecommendedAction = Get-PSSharpModuleFactoryResourceString ModuleNotImportedRecommendedAction
                $PSCmdlet.WriteError($er)
                continue;
            }
        }
    }
}
$script:ImportModuleTemplateTempDirId = 0

function Import-ModuleTemplate {
    [CmdletBinding(RemotingCapability = 'PowerShell', DefaultParameterSetName = 'Path')]
    [System.Diagnostics.CodeAnalysis.SuppressMessage('PSUseDeclaredVarsMoreThanAssignments', '', Justification = 'Variable referenced out of analyzed scope.')]
    param(
        [Parameter(Mandatory, ParameterSetName = 'Path', Position = 0)]
        [Alias('FilePath')]
        [string]
        $Path,

        [Parameter(Mandatory, ParameterSetName = 'LiteralPath', ValueFromPipelineByPropertyName)]
        [string]
        [Alias('PSPath')]
        $LiteralPath
    )
    process {
        do {
            $TempDirId = $script:ImportModuleTemplateTempDirId++
            $TempDirPath = [System.IO.Path]::Combine([System.IO.Path]::GetTempPath(), "ImportModuleTemplate_$TempDirId")
        }
        while ( Test-Path $TempDirPath )


        $TempArchivePath = New-TemporaryFile

        $ExpandArchiveParameters = @{
            DestinationPath = $TempDirPath
            LiteralPath     = $TempArchivePath.FullName
            Force           = $true
        }

        $ResolvePathParameters = @{}
        if ($PSCmdlet.ParameterSetName -eq 'Path') { 
            $ResolvedPaths = @( Resolve-Path -Path $Path )
            if ($ResolvedPaths.Count -ne 1) {
                Write-Debug 'Could not resolve single path.'
                return;
            }
            else{
                $ResolvedPath = $ResolvedPaths[0].Path
            }
        }
        else {
            $ResolvedPath = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($LiteralPath)
        }
        if (!(Test-Path $ResolvedPath)) {
            $exMessage = Get-PSSharpModuleFactoryResourceString -Name 'FileNotFound'
            $ex = [System.Management.Automation.ItemNotFoundException]::new($exMessage)
            $er = [System.Management.Automation.ErrorRecord]::new(
                $ex,
                'FileNotFound',
                [System.Management.Automation.ErrorCategory]::ObjectNotFound,
                $LiteralPath
            )
            $er.ErrorDetails = Get-PSSharpModuleFactoryResourceString 'FileNotFoundInterpolated' $LiteralPath
            $PSCmdlet.WriteError($er)
            return
        }

        Write-Debug "Importing template from path [$(${ResolvedPath}?.GetType() ?? '(null)')] '$ResolvedPath'."
        try {
            Use-DisposableObject ($TemplateStream = [System.IO.FileStream]::new($ResolvedPath, 'Open', 'Read')) {
                Use-DisposableObject ($ArchiveStream = [System.IO.FileStream]::new($TempArchivePath.FullName, 'Create', 'ReadWrite')) {
                    $SizeOfMetadataBytes = [byte[]]::new(4)
                    [void]$TemplateStream.Read($SizeOfMetadataBytes, 0, $SizeOfMetadataBytes.Count)
                    $SizeOfMetadata = [System.BitConverter]::ToInt32($SizeOfMetadataBytes)
                    $MetadataBytes = [byte[]]::new($SizeOfMetadata)
                    [void]$TemplateStream.Read($MetadataBytes, 0, $SizeOfMetadata)
                    $Metadata = [System.Text.Json.JsonSerializer]::Deserialize($MetadataBytes, [PSSharp.ModuleFactory.ModuleTemplateMetadata])
                    # The rest of the file is the archive
                    $TemplateStream.CopyTo($ArchiveStream)
                }
            }
            Expand-Archive @ExpandArchiveParameters

            Register-ModuleTemplate -LiteralPath $TempDirPath -Metadata $Metadata
        }
        finally {
            if (Test-Path $TempArchivePath.FullName) {
                Remove-Item $TempArchivePath.FullName
            }
            if (Test-Path $TempDirPath) {
                Remove-Item $TempDirPath -Force -Recurse
            }
        }
    }
}
Import-Module "$PSScriptRoot\PSSharp.ModuleFactory.dll"

$Templates = @(Get-ModuleTemplate)
if ($Templates.Count -eq 0) {
    Write-Verbose "Imoprting default initial module templates."
    Get-ChildItem -Path "$PSScriptRoot\DefaultTemplates" | Import-ModuleTemplate
}

$ExecutionContext.SessionState.Module.OnRemove = {
    Set-ModuleTemplateRepository -TemplateRepository $null
}