PSScriptModule.psm1

#Region './Private/ConvertToHumanReadableSize.ps1' -1

function ConvertToHumanReadableSize {
    <#
    .SYNOPSIS
        Converts byte size to human-readable format

    .DESCRIPTION
        Internal helper function that converts file sizes in bytes to
        human-readable format (KB, MB, GB, TB).

    .PARAMETER Bytes
        The size in bytes to convert

    .EXAMPLE
        ConvertToHumanReadableSize -Bytes 1024
        Returns: "1.00 KB"

    .EXAMPLE
        ConvertToHumanReadableSize -Bytes 1048576
        Returns: "1.00 MB"

    .OUTPUTS
        System.String

    .NOTES
        Private function - not exported from module
    #>

    [CmdletBinding()]
    [OutputType([string])]
    param (
        [Parameter(Mandatory)]
        [ValidateRange(0, [long]::MaxValue)]
        [long]
        $Bytes
    )

    # Fail fast: Handle zero bytes
    if ($Bytes -eq 0) {
        return '0 bytes'
    }

    # Fail fast: Validate non-negative
    if ($Bytes -lt 0) {
        throw "Bytes parameter must be non-negative. Received: $Bytes"
    }

    $sizes = @(
        @{ Name = 'TB'; Value = 1TB }
        @{ Name = 'GB'; Value = 1GB }
        @{ Name = 'MB'; Value = 1MB }
        @{ Name = 'KB'; Value = 1KB }
    )

    foreach ($size in $sizes) {
        if ($Bytes -ge $size.Value) {
            $value = $Bytes / $size.Value
            return '{0:N2} {1}' -f $value, $size.Name
        }
    }

    # Less than 1 KB
    return "$Bytes bytes"
}
#EndRegion './Private/ConvertToHumanReadableSize.ps1' 63
#Region './Public/Get-ModuleMetadata.ps1' -1

function Get-ModuleMetadata {
    <#
    .SYNOPSIS
        Retrieves metadata information from PowerShell module manifest files

    .DESCRIPTION
        Reads and extracts metadata from PowerShell module manifest (.psd1) files,
        including version, author, description, and exported functions.
        Supports pipeline input for processing multiple modules.

    .PARAMETER Path
        The path to the module manifest file (.psd1) or directory containing a manifest.
        Accepts wildcards for batch processing.

    .PARAMETER IncludeSize
        Include the file size of the module in the output

    .EXAMPLE
        Get-ModuleMetadata -Path ./MyModule/MyModule.psd1

        Retrieves metadata from the specified module manifest file.

    .EXAMPLE
        Get-ChildItem -Path ./modules -Filter *.psd1 -Recurse | Get-ModuleMetadata

        Processes multiple module manifests through the pipeline.

    .EXAMPLE
        Get-ModuleMetadata -Path ./MyModule -IncludeSize

        Retrieves metadata and includes formatted file size information.

    .INPUTS
        System.String
        System.IO.FileInfo

    .OUTPUTS
        PSCustomObject
        Returns objects with module metadata properties

    .NOTES
        Validates manifest files before processing to ensure data integrity.
        Uses fail-fast approach for invalid inputs.

    .LINK
        https://github.com/YourUsername/PSScriptModule
    #>

    [CmdletBinding()]
    [OutputType([PSCustomObject])]
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseSingularNouns', '', Justification = 'Metadata is conceptually singular')]
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseOutputTypeCorrectly', '', Justification = 'Function returns PSCustomObject items from internal List collection')]
    param (
        [Parameter(
            Mandatory,
            ValueFromPipeline,
            ValueFromPipelineByPropertyName,
            Position = 0
        )]
        [ValidateNotNullOrEmpty()]
        [Alias('FullName', 'PSPath')]
        [string[]]
        $Path,

        [Parameter()]
        [switch]
        $IncludeSize
    )

    begin {
        Write-Verbose "Starting $($MyInvocation.MyCommand)"
        $results = [System.Collections.Generic.List[PSCustomObject]]::new()
    }

    process {
        foreach ($itemPath in $Path) {
            try {
                Write-Verbose "Processing path: $itemPath"

                # Fail fast: Validate path exists
                if (-not (Test-Path -Path $itemPath)) {
                    throw "Path does not exist: $itemPath"
                }

                # Resolve to manifest file
                $manifestPath = if ((Get-Item -Path $itemPath).PSIsContainer) {
                    # If directory, look for .psd1 file
                    $psd1Files = Get-ChildItem -Path $itemPath -Filter '*.psd1' -File
                    if ($psd1Files.Count -eq 0) {
                        throw "No module manifest (.psd1) file found in: $itemPath"
                    }
                    if ($psd1Files.Count -gt 1) {
                        throw "Multiple module manifest files found in: $itemPath. Specify exact file path."
                    }
                    $psd1Files[0].FullName
                } else {
                    # Fail fast: Validate file extension
                    if ([System.IO.Path]::GetExtension($itemPath) -ne '.psd1') {
                        throw "File must be a PowerShell module manifest (.psd1): $itemPath"
                    }
                    $itemPath
                }

                Write-Verbose "Reading manifest: $manifestPath"

                # Import and validate manifest
                $manifest = Test-ModuleManifest -Path $manifestPath -ErrorAction Stop

                # Build result object
                $metadata = [PSCustomObject]@{
                    Name              = $manifest.Name
                    Version           = $manifest.Version.ToString()
                    Author            = $manifest.Author
                    Description       = $manifest.Description
                    CompanyName       = $manifest.CompanyName
                    Copyright         = $manifest.Copyright
                    PowerShellVersion = $manifest.PowerShellVersion.ToString()
                    ExportedFunctions = $manifest.ExportedFunctions.Keys.Count
                    ExportedCmdlets   = $manifest.ExportedCmdlets.Keys.Count
                    Path              = $manifestPath
                }
                $metadata.PSObject.TypeNames.Insert(0, 'PSScriptModule.ModuleMetadata')

                # Add size information if requested
                if ($IncludeSize) {
                    $fileInfo = Get-Item -Path $manifestPath
                    $metadata | Add-Member -NotePropertyName 'SizeBytes' -NotePropertyValue $fileInfo.Length
                    $metadata | Add-Member -NotePropertyName 'FormattedSize' -NotePropertyValue (ConvertToHumanReadableSize -Bytes $fileInfo.Length)
                }

                $results.Add($metadata)
                Write-Verbose "Successfully processed: $($manifest.Name)"
            } catch {
                Write-Verbose "$($MyInvocation.MyCommand) Failed to process '$itemPath': $_"
                Write-Verbose "StackTrace: $($_.ScriptStackTrace)"
                throw $_
            }
        }
    }

    end {
        Write-Verbose "Completed $($MyInvocation.MyCommand). Processed $($results.Count) module(s)."
        return $results
    }
}
#EndRegion './Public/Get-ModuleMetadata.ps1' 145