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 } |