Invoke-PSBuild.psm1

# https://riptutorial.com/powershell/example/27242/exporting-a-variable-from-a-module

function Get-Ast {
    [CmdletBinding(
        DefaultParameterSetName = "File"
    )]
    Param(

        [Parameter(
            ParameterSetName = "File",
            Mandatory,
            ValueFromPipeline,
            Position=0
        )]
        [string]
        $File,

        [Parameter(
            ParameterSetName = "Code",
            Mandatory,
            ValueFromPipeline,
            Position=0
        )]
        [string]
        $Code,

        [ArgumentCompleter({

            param(
                $commandName,
                $parameterName,
                $wordToComplete,
                $commandAst,
                $fakeBoundParameters
            )

            $typeNames = [PSObject].Assembly.GetTypes().Where{$_.Name.EndsWith('Ast')}.Name | Sort-Object 
            
            $typeNames | Where-Object { $_.LogName -like "$wordToComplete*" } | Foreach-Object { 
                [System.Management.Automation.CompletionResult]::new($_, $_, "ParameterValue", $_)
            }

        })]
        $AstType = '*',


        [Switch]
        $NoRecursion
    )

    Begin {
        $predicate = { param($astObject) $astObject.GetType().Name -like $AstType }
    }  

    Process {

    $errors = $null
    $ast    = switch ($PSCmdlet.ParameterSetName) {

        'File' {
            [System.Management.Automation.Language.Parser]::ParseFile($File, [ref]$null, [ref]$errors)
        }

        'Code' {
            [System.Management.Automation.Language.Parser]::ParseInput($Code, [ref]$null, [ref]$errors)
        }
    } 
    
    if ($errors) { throw [System.InvalidCastException]::new("Submitted text could not be converted to PowerShell because it contains syntax errors: $($errors | Out-String)")}

    $ast.FindAll($predicate, !$NoRecursion) |

        Add-Member -MemberType ScriptProperty -Name Type -Value { $this.GetType().Name } -PassThru

    }

}

function Get-BuildFunctions {
    <#
    
        .EXAMPLE

        $c = Get-PSModuleBuildFunctions -Path src -Recurse
        $c
    
    #>

    [CmdletBinding()]
    Param(

        [Parameter(
            ValueFromPipeline,
            ValueFromPipelineByPropertyName
        )]
        [Alias('FullName')]
        $Path = (Convert-Path ".\"),

        [Parameter(
            Position=2
        )]
        [regex]
        $Exclude = '\.tests.ps1$',

        [Switch]
        $Recurse

    )

    BEGIN {

        $baseMessage = "[ $($MyInvocation.InvocationName) ]"
        Write-Verbose "$baseMessage Executing"

    }

    PROCESS {

        if (-not (Test-Path -Path $Path -PathType Container -ErrorAction SilentlyContinue)) {
            Write-Error "$Path is not a valid Directory"
            return
        }

        $directories = switch ($Recurse) {
            $true {
                [System.IO.DirectoryInfo](Convert-Path $Path)
                Get-ChildItem -Path $Path -Directory -Force -ErrorAction SilentlyContinue -Recurse
            }
            $false {
                [System.IO.DirectoryInfo](Convert-Path $Path)
            }
        }

        $directories | Foreach-Object {

            $directory = $_

            $params = @{
                RootDirectory = $Path
                Path          = $directory.FullName
            }

            $functionScope = Get-BuildFunctionScope @params
            
            Get-ChildItem -Path $directory.FullName -File | Foreach-Object {

                $file = $_
                
                if ($file.Extension -eq '.ps1') {

                    if ($file.Name -notmatch $Exclude) {

                        $params = @{
                            File        = $file.FullName
                            AstType     = 'FunctionDefinitionAst'
                            ErrorAction = 'SilentlyContinue'
                        }
                        
                        Get-Ast @params | Foreach-Object {

                            $ast = $_

                            [PSCustomObject]@{
                                Scope = $functionScope
                                Name  = $ast.Name
                                AST   = $ast
                            }

                        }

                    }

                }

            }

        } | Sort-Object -Property Scope, Name


    }

}

function Get-BuildFunctionScope {
    <#
    
        .EXAMPLE

        $RootDirectory = '/Users/balmerj/Desktop/Github/JerryBalmer/PS-CommonFunctions/src'
        $Path = '/Users/balmerj/Desktop/Github/JerryBalmer/PS-CommonFunctions/src/psbuild/private'

        Get-PSScriptFileScopeType -RootDirectory $RootDirectory -Path $Path

        # Output:
        private

    #>

    [CmdletBinding()]
    Param(
        [Parameter(
            Mandatory,
            ValueFromPipeline,
            ValueFromPipelineByPropertyName,
            Position=0
        )]
        [Alias('FullName')]
        $RootDirectory,

        [Parameter(
            Mandatory,
            Position=1
        )]
        [String]
        $Path
    )

    BEGIN {

        $baseMessage = "[ $($MyInvocation.InvocationName) ]"
        Write-Verbose "$baseMessage Executing"

    }

    PROCESS {

        $split          = [System.Collections.ArrayList]@()
        $reversedSplit  = [System.Collections.ArrayList]@()
        $splitCharacter = if ($IsWindows) {
            '\'
        } else {
            '/'
        }

        $processString = $Path -Replace ([regex]::Escape($RootDirectory)),''

        $processString.Split($splitCharacter,[System.StringSplitOptions]::RemoveEmptyEntries) | Foreach-Object {
            $split.Add($_.ToString()) | Out-Null
        }

        ($split.Count - 1)..0 | Foreach-Object {
            $i = $_
            
            $splitItem = $split[$i]
            Write-Verbose " - Adding Split Item: $splitItem"
            $reversedSplit.Add($splitItem) | Out-Null
        }

        $test = $reversedSplit | Foreach-Object {
            $i = $_

            if ($i -eq 'public') {
                'public'
            } elseif ($i -eq 'private') {
                'private'
            }

        }

        $scope = if ($test) {
            $test | Select-Object -First 1
        } else {
            'public'
        }

        (Get-Culture).TextInfo.ToTitleCase($scope)

    }

}

function Split-String {
    <#
    https://stackoverflow.com/questions/16435240/how-to-split-string-by-string-in-powershell
    #>

    [CmdletBinding()]
    Param(
        [Parameter(
            Mandatory,
            ValueFromPipeline,
            ValueFromPipelineByPropertyName,
            Position=0
        )]
        [String]
        $String,

        [Parameter(
            Mandatory,
            Position=1
        )]
        $Separator,

        [Parameter(
            Position=2
        )]
        [Switch]
        $RemoveEmptyEntries,

        [Parameter(
            Position=3
        )]
        [Switch]
        $Reverse
    )

    BEGIN {
        $baseMessage = "[ $($MyInvocation.InvocationName) ]"
        Write-Verbose "$baseMessage Executing"
    }

    PROCESS {

        $_separator = if ($Separator.GetType().FullName -ne 'System.String[]') {
            [string[]]@($Separator)
        } else {
            $Separator
        }

        $result = if ($RemoveEmptyEntries) {
            $String.Split($_separator, [System.StringSplitOptions]::RemoveEmptyEntries)
        } else {
            $String.Split($separator)
        }

        if ($Reverse) {

            $array =[System.Collections.ArrayList]@()

            $result | Foreach-Object {
                $array.Add($_) | Out-Null
            }

            if ($array.Count -gt 0) {
                ($array.Count - 1)..0 | Foreach-Object {
                    $i = $_
                    $array[$i]
                }
            }
            

        } else {
            $result
        }

    }

}

function Update-Version {
    [CmdletBinding()]
    Param(

        [Parameter(
            Position=0,
            ValueFromPipeline
        )]
        [version]
        $Version,

        [Parameter(
            Position=1
        )]
        [ValidateSet(
            'Major',
            'Minor',
            'Build'
        )]
        [string]
        $Type = 'Build',
        
        [switch]
        $OutVersion

    )

    Begin {
        $baseMessage = "[ $($MyInvocation.InvocationName) ]"
        Write-Verbose "$baseMessage Executing"
        
    }

    Process {

        $v = $Version

        $major    = $v.Major
        $minor    = $v.Minor
        $build    = $v.Build

        $version = switch ($Type) {
            
            'Major' {

                [version]("$($major + 1).0.0")

            }

            'Minor' {
                [version]("$($major).$($minor + 1).0")
            }

            'Build' {
                [version]("$($major).$($minor).$($build + 1)")
            }

        }

    }

    End {
        if ($PSBoundParameters.ContainsKey('OutVersion')) {
            $version
        } else {
            $version.ToString()
        }
    }

}

function Invoke-EnsureDirectory {
    [CmdletBinding()]
    Param(
        [Parameter(
            Mandatory,
            ValueFromPipeline,
            ValueFromPipelineByPropertyName,
            Position=1
        )]
        [System.IO.DirectoryInfo]
        $Path,

        [Switch]
        $Force
    )

    BEGIN {

    }

    PROCESS {

        if (-not (Test-Path -Path $Path -PathType Container)) {

            if ($PSBoundParameters.ContainsKey('Force')) {
    
                $params = @{
                    Path        = $Path
                    ItemType    = 'Directory'
                    Force       = $true
                    ErrorAction = 'Stop'
                }
    
                New-Item @params | Select-Object -ExpandProperty FullName
    
            } else {
                Write-Error "Directory does not exist. Use the -Force parameter to create it. $Path"
                return
            }
    
        } else {
            (Resolve-Path $Path).Path
        }

    }

}



function Invoke-PSBuild {
    <#
    .SYNOPSIS
        Builds a PowerShell module
    .DESCRIPTION
        A PowerShell module that automates the building of PowerShell mdoules. It sets up the standard
        directories and output directories.
    .EXAMPLE
        Invoke-PSBuild -ModuleName 'Test'
    #>

    
    
    [CmdletBinding(DefaultParameterSetName='Standard')]
    Param(

        [Parameter(
            ParameterSetName='Standard',
            Mandatory,
            Position=0
        )]
        $ModuleName,

        [Parameter(
            ParameterSetName='Standard',
            Position=1
        )]
        [String]
        $Path = ".\",

        [Parameter(
            ParameterSetName='Standard',
            Position=1
        )]
        [String]
        $SourceDirectoryName = "src",

        [Parameter(
            ParameterSetName='Standard',
            Position=1
        )]
        [System.IO.DirectoryInfo]
        $OutputDirectory,

        [Switch]
        $PassThru,

        [Switch]
        $Force 

    )

    BEGIN {
        $baseMessage = "[ $($MyInvocation.InvocationName) ]"
        Write-Verbose "$baseMessage Executing"
    }

    PROCESS {

        if (-not $OutputDirectory) {
            $OutputDirectory = Join-Path (Convert-Path $Path) 'bin'
        }

        $psm1Name = "$($ModuleName).psm1"
        $psd1Name = "$($ModuleName).psd1"

        switch ($PSCmdlet.ParameterSetName) {

            'Standard' {

                ####################################################
                # Create Directories
                ####################################################
                
                Write-Verbose " - Ensuring the working directory"
                $params = @{
                    Path        = $Path
                    Force       = $PSBoundParameters.ContainsKey('Force')
                    ErrorAction = 'Stop'
                }

                $_workingDirectory = Invoke-EnsureDirectory @params

                ######################################

                Write-Verbose " - Ensuring the src directory"
                $params = @{
                    Path        = Join-Path $_workingDirectory $SourceDirectoryName
                    Force       = $Force
                    ErrorAction = 'Stop'
                }

                $_sourceDirectory = Invoke-EnsureDirectory @params

                ######################################

                Write-Verbose " - Ensuring the output directory"
                $params = @{
                    Path        = if ($OutputDirectory) {$OutputDirectory} else {Join-Path $_workingDirectory 'bin'}
                    Force       = $PSBoundParameters.ContainsKey('Force')
                    ErrorAction = 'Stop'
                }

                $_outputDirectory = Invoke-EnsureDirectory @params

                ######################################

                Write-Verbose " - Ensuring the module output directory"
                $params = @{
                    Path        = Join-Path $_outputDirectory $ModuleName
                    Force       = $PSBoundParameters.ContainsKey('Force')
                    ErrorAction = 'Stop'
                }

                $_moduleOutputDirectory = Invoke-EnsureDirectory @params

                ####################################################
                # Create Module Files
                ####################################################

                Write-Verbose " - Ensuring the $psm1Name file"
                $psm1Path = Join-Path $_sourceDirectory $psm1Name

                if (-not (Test-Path -Path $psm1Path -PathType Leaf)) {
                    
                    $params = @{
                        Path        = $psm1Path
                        ItemType    = 'File'
                        Force       = $true
                        ErrorAction = 'Stop'
                    }
        
                    New-Item @params | Select-Object -ExpandProperty FullName

                }

                ######################################

                Write-Verbose " - Ensuring the $psd1Name file"
                $psd1Path = Join-Path $_sourceDirectory $psd1Name

                if (-not (Test-Path -Path $psd1Path -PathType Leaf)) {
                    
                    $params = @{
                        Path          = $psd1Path
                        RootModule    = $psm1Name
                        ModuleVersion = "0.0.0"
                    }
        
                    New-ModuleManifest @params

                }

                ####################################################
                # Create Build Object
                ####################################################
                
                $Global:PSBUILD = [PSCustomObject]@{
                    ModuleName       = $ModuleName
                    BuildType        = $BuildType
                    CurrentVersion   = $null
                    BuildVersion     = $null
                    Items = [ordered]@{
                        Functions = Get-BuildFunctions -Path $_sourceDirectory -Recurse -Verbose:$false
                    }
                    Paths = [ordered]@{
                        Source = [ordered]@{
                            WorkingDirectory = $_workingDirectory
                            SourceDirectory  = $_sourceDirectory
                            PSM1 = $psm1Path
                            PSD1 = $psd1Path
                        }
                        Destination = [ordered]@{
                            OutputDirectory       = $_outputDirectory
                            ModuleOutputDirectory = $_moduleOutputDirectory
                            PSM1 = Join-Path $_moduleOutputDirectory $psm1Name
                            PSD1 = Join-Path $_moduleOutputDirectory $psd1Name
                        }
                    }
                    
                }

                ######################################

                Write-Verbose " - Pulling current version"

                $currentVersion       = (Import-PowerShellDataFile -Path $PSBUILD.Paths.Source.PSD1).ModuleVersion
                $PSBUILD.CurrentVersion = $currentVersion

                ######################################

                Write-Verbose " - Calculating Build Version"

                $PSBUILD.BuildVersion = $currentVersion | Update-Version -Verbose:$false

                ################################################
                ################################################
                # Process the .psm1 file
                ################################################
                ################################################

                $params = @{
                    Path        = $PSBUILD.Paths.Source.PSM1
                    Destination = $PSBUILD.Paths.Destination.PSM1
                    Force       = $true
                }

                Copy-Item @params

                $PSBUILD.Items.Functions | Sort-Object -Property Scope, Name | Foreach-Object {
                    $_function = $_
                    $_function.AST.Extent.text | Out-File -FilePath $PSBUILD.Paths.Destination.PSM1 -Append
                    ""                         | Out-File -FilePath $PSBUILD.Paths.Destination.PSM1 -Append
                }

                ################################################
                # Process the .psd1 file
                ################################################
        
                $manifestParams         = @{}
                $manifestParams['Path'] = $PSBUILD.Paths.Destination.PSD1

                # Copy the manifest
                $params = @{
                    Path        = $PSBUILD.Paths.Source.PSD1
                    Destination = $PSBUILD.Paths.Destination.PSD1
                    Force       = $true
                }

                Copy-Item @params

                #-------------------------------
                # Functions to Export
                #-------------------------------

                $functionsToExport = $PSBUILD.Items.Functions `
                    | Where-Object {$_.Scope -eq 'Public'} `
                    | Select-Object -ExpandProperty Name

                if ($functionsToExport) {
                    $manifestParams['FunctionsToExport'] = $functionsToExport
                }
                
                #-------------------------------
                # Update the Module Manifest
                #-------------------------------

                Update-ModuleManifest @manifestParams

                $mColor = 'Cyan'
                $bColor = 'Blue'
                $publicColor  = 'Green'
                $privateColor = 'Red'

                Write-Host "ModuleName: "           -ForegroundColor $mColor -NoNewline
                Write-Host "$($PSBUILD.ModuleName)" -ForegroundColor $bColor

                Write-Host "Version: "             -ForegroundColor $mColor -NoNewline
                Write-Host "$($PSBUILD.BuildVersion)" -ForegroundColor $bColor
                Write-Host "Functions:" -ForegroundColor $mColor
                Write-Host

                $PSBUILD.Items.Functions | Where-Object {$_.Scope -eq 'Public'} | Sort-Object -Property Name | Foreach-Object {
                    $functionName = $_.Name
                    Write-Host " Public "   -ForegroundColor $publicColor -NoNewline
                    Write-Host $functionName  -ForegroundColor $bColor
                }

                $PSBUILD.Items.Functions | Where-Object {$_.Scope -eq 'Private'} | Sort-Object -Property Name | Foreach-Object {
                    $functionName = $_.Name
                    Write-Host " Private "   -ForegroundColor $privateColor -NoNewline
                    Write-Host $functionName  -ForegroundColor $bColor
                }

                Write-Host

                if ($PassThru) {
                    $Global:PSBUILD
                }

            }

        }

    }

}