pf-module.ps1

$ErrorActionPreference = 'stop'

function Import-Module_Ensure ($name) {
    if (Get-Module -name $name ) { return }

    if (-not (Get-Module -name $name -ListAvailable )) {
        Set-PSRepository -name PsGallery -InstallationPolicy Trusted
        Install-Module -Name $name -Repository PsGallery
    }
    Import-Module $name
}

function Initialize-File_InNotPresent($path) {
    if (-not (Test-Path -Path $path)) {
        Set-Content -Path $path -Value ""
    }
}

function Get-ScriptFolder {
    $scriptPath = Get-PSCallStack | Where-Object ScriptName | Select-Object -first 1
    $scriptPath = Split-Path $scriptPath.ScriptName -Parent
    return $scriptPath
}

function Get-ScriptList([switch]$NoExcludeThisScript, $path) {
    $fileExtension = "ps1"
    if (-not $path) {
        $path = Get-ScriptFolder
    }
    $result = Get-ChildItem -Path $path -Recurse -Filter "*.$fileExtension" |
         Where-Object Extension -eq ".$fileExtension" |
         Sort-Object FullName
    if (-not $NoExcludeThisScript) {
        $result = $result | Where-Object FullName -NE $MyInvocation.ScriptName 
    }
    return $result
}

function Remove-Alias {
    Param(
        [Parameter(ValueFromPipeline=$true)]$name,
        [Switch]$NoResolvedCommand
    )
    process {
        If ($name -and ( Test-Path Alias:$name)) {Remove-Item Alias:$name}
    }
    end {
        if ($NoResolvedCommand) {
            $toRemove = Get-Alias | Where-Object { -not $_.ResolvedCommand }
            $toRemove.name | Where-Object {$_} | Where-Object { Test-Path Alias:$_ } | 
                ForEach-Object { Remove-Item Alias:$_ -Verbose }
        }
    }
}
function Remove-Alias:::Example {
    Remove-Alias -NoResolvedCommand
}

function Get-Functions {
    param( 
        [Parameter(ValueFromPipeline=$true)]$path
    ) 
    process {
        $path = $path.FullName ?? $path
        . $path
        return Get-Command | Where-Object ScriptBlock |
            Where-Object { $_.ScriptBlock.File -eq $path } 
    }
}

function Get-ContentToInclude($functionName) {
    if ($functionName) {
        $header = get-command DefineModuleHeader
        $body = $header.ScriptBlock.ToString()
        $result = "#Include-Start $functionName`n$body`n#Include-end $functionName`n"
    }
    return $result
}

function Update-ModulePsm1($path) {
    $folder = Split-Path $path -Parent
    $files = Get-ScriptList -path $folder -NoExcludeThisScript
    $fullnameList = $files.fullname | 
        ForEach-Object { ([string]$_).Substring($folder.Length + 1) }

    function DefineModuleHeader {
        $ErrorActionPreference = 'stop'
        $script:scriptFolder = Split-Path ( Get-PSCallStack )[0].ScriptName -Parent
    }

    $content =  $fullnameList | ForEach-Object { "`n" + '. $scriptFolder\' + $_  } 

    $DefineModuleHeader = Get-ContentToInclude -functionName DefineModuleHeader

    $content = $DefineModuleHeader + $content

    Set-Content -Value $content -Path $path
}
function Update-ModulePsm1:::Example {
    Update-ModulePsm1 -path C:\code\PowerFrame\pf-azFuncDeploy\pf-azFuncDeploy.psm1
}

function Import-Module_EnsureManifest ($Path, $FunctionsToExport,$Revision) {
    function NewModuleManifest_Default {
        $moduleName = Split-Path $Path -Leaf
        New-ModuleManifest -Path $Path -ModuleVersion "1.0.0.1"  -Description $moduleName
    }
    if (-not (Test-Path $Path) ) { NewModuleManifest_Default }
    $psd = Import-PowerShellDataFile  -Path $path -ErrorAction SilentlyContinue
    if (-not $psd) {
        # psd1 likely to be invalid so recreate one
        NewModuleManifest_Default
    }

    $description = if ($psd.Description) { $psd.Description } else { "$moduleName" }
    $ModuleVersion = if ($psd.ModuleVersion) { [Version]$psd.ModuleVersion } else { "$moduleName" }
    $Revision = if ($Revision) { $Revision } else { [Math]::Max($ModuleVersion.Revision, 1) }
    $ModuleVersion = [Version]::new([Math]::Max(1,$ModuleVersion.Major), 
        [Math]::Max(0,$ModuleVersion.Minor),
        [Math]::Max(0,$ModuleVersion.Build),
        $Revision)
    $functionsToExport = if ( $functionsToExport ) { $functionsToExport } else { $psd.functionsToExport }

    Update-ModuleManifest -Path $Path -FunctionsToExport $functionsToExport `
        -Description $description -ModuleVersion $ModuleVersion -RootModule "$moduleName.psm1"

    # Remove comments in order to avoid commit changes because of header containg dates
    Remove-PowershellComments -path $Path
}
function Import-Module_EnsureManifest:::Example {
    Import-Module_EnsureManifest -Path C:\code\PowerFrame\pf-git\pf-git.psd1 -Revision 20
}

function Remove-PowershellComments($path) {
    $content = Get-Content -Path $path -Raw
    $result = Remove-PowershellCommentsFromString -content $content
    Set-Content -Path $path -Value $result
}

function Remove-PowershellCommentsFromString($content) {
    $result = $content -replace '\s*#(?!\brequires\b).*', ''   
    $result = $result.Trim()
    return $result
}

function Remove-PowershellCommentsFromString:::Test($content) {
    Remove-PowershellCommentsFromString -content "#comment" | assert -eq ""
    Remove-PowershellCommentsFromString -content "#requires" | assert -eq "#requires"
    Remove-PowershellCommentsFromString -content " #comment abc" | assert -eq ""
    Remove-PowershellCommentsFromString -content "abc" | assert -eq " abc"
}

function Initialize-Module_Manifest_EnsureFiles {
    param(
        [Parameter(ValueFromPipeline=$true)]$module,
        $Revision
    )
    begin {
        $approvedVerbs = Get-Verb | ForEach-Object { $_.Verb  }
        # $approvedVerbs += "Ensure"
        $approvedVerbsRegEx = [string]::Join( '|', ( $approvedVerbs | ForEach-Object { "$_-" } ) ) 
        $approvedVerbsRegEx += "|Assert"
    }
    process {
        $modulePath = $module.DirectoryName ?? $module
        $moduleName = Split-Path $modulePath -Leaf
        $fullPathNoExtension = "$modulePath\$moduleName"

        function Update-ModuleManifest_FunctionsToExport {

            $scriptList = Get-ScriptList -NoExcludeThisScript -path $modulePath
            $functionList = $scriptList | Get-Functions
            $functionsToExport = $functionList |
                Where-Object name -NotLike '*:*' |
                Where-Object name -Match $approvedVerbsRegEx |
                Sort-Object name

            Import-Module_EnsureManifest -Path "$fullPathNoExtension.psd1" `
                -FunctionsToExport $functionsToExport.Name -Revision $Revision
        }

        Initialize-File_InNotPresent -path "$fullPathNoExtension.Format.ps1xml"

        Update-ModulePsm1 -path "$fullPathNoExtension.psm1"

        Update-ModuleManifest_FunctionsToExport
    }
}

function Add-Module_PSModulePath ([string]$ModulesFolder = 'PSModules', [switch]$notRequired ) {
    $p = split-path -parent ($current = ( Get-PSCallStack )[0].ScriptName )
    while ( $p -and -not ( Test-Path ( "$p\$ModulesFolder" ) ) ) { $p = Split-Path $p -Parent }
    if (-not $p -and -not $notRequired) { 
        throw "Modules folder '$ModulesFolder' cannot be found starting from '$current'" 
    } 
    $p = Resolve-Path "$p\$ModulesFolder"
    if (-not $env:PSModulePath.Contains("$p;")) { 
        $env:PSModulePath = "$p;$env:PSModulePath" 
    }
}

function Get-Modulefolders ($path) {
    $folders =  if ($path) { Get-ChildItem -Directory -path $path } 
                    else { Get-ChildItem -Directory }
    $folders |  Where-Object name -NotLike '.*'
}

function Initialize-Module_Manifest_EnsureFiles:::Example {
    $folderModules = Get-Modulefolders -path C:\code\PowerFrame   
    $folderModules.FullName | Initialize-Module_Manifest_EnsureFiles -Revision 3
}

function Sync-ModuleManifests {
    $commitCount =  Get-GitCommitCount
    Get-Modules_Local | Get-Path | Get-Git_Changed -folder 
        | Initialize-Module_Manifest_EnsureFiles -Revision $commitCount
}

function Publish-ModuleEx {
    param(
        [Parameter(ValueFromPipeline=$true)]$modulePath,
        [string]$NuGetApiKey,
        [string[]]$repositoryList = @('LocalRepo')
    ) 
    process {
        $moduleName = Split-Path $modulePath -Leaf
        $psd = Import-PowerShellDataFile  -Path "$modulePath\$moduleName.psd1" 
     
        foreach ($repository in $repositoryList) {
            $module = Find-Module  -Name $moduleName -RequiredVersion $psd.ModuleVersion `
            -ErrorAction SilentlyContinue -Repository $repository
            if  (-not $module) {
                $additionalParams = @{}
                if ($NuGetApiKey) {
                    $additionalParams.Add("NuGetApiKey",$NuGetApiKey)
                }
                Publish-Module @additionalParams -Path $modulePath -Repository $repository `
                    -Verbose -Force
            }
        }
        # $repository = $repositoryList[0]
        # $env:PSModulePath
        # Uninstall-Module -Name $moduleName -ErrorAction
        # Install-Module -Name $moduleName -Scope CurrentUser -Repository $repository `
        # -Force -AllowClobber -RequiredVersion $psd.ModuleVersion
    }
}

function Publish-ModuleEx:::Example {
    $folderModules = Get-Modulefolders -path C:\code\PowerFrame 
    $folderModules.FullName | Initialize-Module_Manifest_EnsureFiles
    $folderModules.FullName | Publish-ModuleEx 
}

function Get-ScriptDependencies {
    # based on https://mikefrobbins.com/2019/02/21/powershell-tokenizer-more-accurate-than-ast-in-certain-scenarios/
    # see also https://mikefrobbins.com/2019/05/17/using-the-ast-to-find-module-dependencies-in-powershell-functions-and-scripts/
    param (
        [Parameter(ValueFromPipeline=$true)]
        $file,
        [Switch]$IgnoreMissingCommand 
    )
    begin {
        $getCommandErrorAction =  ($IgnoreMissingCommand) ? 'SilentlyContinue' : 'Stop'
    }
    process {
        $file = $file.fullname ?? $file
        $Token = $null
        $ignoredOutput = [System.Management.Automation.Language.Parser]::ParseFile($File, [ref]$Token, [ref]$null)
        Write-Verbose $ignoredOutput
        $commandTokenList =  $Token | Where-Object {$_.TokenFlags -eq 'CommandName'}
        $commandNameList = $commandTokenList.Value | Select-Object -Unique | Sort-Object
        $commandList = $commandNameList | ForEach-Object { Get-Command -name $_ -ErrorAction $getCommandErrorAction }
        $commandList.Module | Select-Object -Unique 
    }
}
function Get-ScriptDependencies:::Examples{
    $moduleList = Get-ChildItem -filter pf-git.ps1 -Recurse
        | Get-ScriptDependencies -IgnoreMissingCommand
    Write-Host ( $moduleList | Format-Table | Out-String )
}

function Get-ScriptFunctions {
    param (
        [Parameter(ValueFromPipeline=$true)]
        $file
    )
    process {
        $file = $file.fullname ?? $file
        $Token = $null
        $ignoredOutput = [System.Management.Automation.Language.Parser]::ParseFile($File, [ref]$Token, [ref]$null)
        Write-Verbose $ignoredOutput
        $funcBegin = $false 
        $Token | ForEach-Object {
            if ($funcBegin) {
                $_.Text
            }
            $funcBegin = ($_.Kind -like "function") 
        }
    }
}
function Get-ScriptFunctions:::Example {
    Get-ChildItem pf-* -Recurse -Filter *.ps1 | Select-Object -First 1 | Get-ScriptFunctions
}

function Get-ScriptFunctions_WithInvalidChars {
    Get-ChildItem pf-* -Recurse -Filter *.ps1 | Get-ScriptFunctions
      | Where-Object { $_ -match '\w+-\w+-' }
}

function Get-Modules_Local {
    $folders = Get-ChildItem -file -Recurse -Filter *.* -include @('*.psd1', '*.psm1', "*.ps1")
        | Where-Object { $_.FullName -notlike "*pending*" }      
        | Where-Object { $_.BaseName -eq $_.Directory.BaseName } 
        | Select-Object DirectoryName -Unique
        
    $folders.DirectoryName | 
        ForEach-Object { [PSCustomObject]@{ 
            Name = (Split-Path $_ -LeafBase)
            DirectoryName = $_ }        
        }
}

function Uninstall-Module_WhenDuplicated {
    [CmdletBinding()]
    param(
        [Parameter(ValueFromPipeline=$true)]
        $Module,
        [switch]$PassThru
    )
    process {
       $mm = Get-module $Module.Name -ListAvailable 
       if ( $mm -and -not $mm.Path.StartsWith($Module.DirectoryName)) {
           Uninstall-Module $Module.Name -Force -Verbose
       }
       if ($PassThru) {
           return $Module
       }
    }
}

function Import-Module_Reload {
    [CmdletBinding()]
    param(
        [Parameter(ValueFromPipeline=$true)]
        $Module
    )
    process {
        if ($Module) {
            if (get-module $Module.Name ) {
                remove-module $Module.Name
            }

            $modulePath =  Join-Path -Path $Module.DirectoryName -ChildPath ($Module.Name + ".psm1") 
            if (Test-Path $modulePath) {
                Import-Module $Module.DirectoryName -Global
            }
        }
    }
}

function Import-PSSnapin ($name) {
    if (-not (Get-PSSnapin | Where-Object Name -eq $name))     {
        if ( Get-PSSnapin  -Registered  | Where-Object Name -eq $name ) {
            Add-PsSnapin $name
        }
        else {
            Write-Warning "SNAPIN not available : '$name'"
        }
    }
}
function Import-PSSnapin:::Example {
    Import-PSSnapin Microsoft.SharePoint.PowerShell
}