Invoke-PSModuleBuild.psm1

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-PSModuleBuildFunctions {
    <#
    
        .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-PSModuleBuildFunctionScope @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-PSModuleBuildFunctionScope {
    <#
    
        .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 Initialize-PSModuleBuild {
    [CmdletBinding()]
    Param(

        [Parameter(
            Mandatory,
            Position=0
        )]
        [String]
        $Name,

        [Parameter(
            Position=1
        )]
        [String]
        $Path = (Convert-Path ".\"),

        [Switch]
        $Force

    )

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

    PROCESS {

        Remove-Variable -Name 'PSModuleBuild' -Force -ErrorAction SilentlyContinue

$contentInvokeBuild = @"
[CmdletBinding()]
Param(

)

##################################################################################################################
# Globals
##################################################################################################################

if (`$PSBoundParameters.ContainsKey('Verbose')) {
    `$Global:BUILD_VERBOSE = `$true
} else {
    `$Global:BUILD_VERBOSE = `$true
}

##################################################################################################################
# Argument Completers
##################################################################################################################

Register-ArgumentCompleter -CommandName Invoke-Build.ps1 -ParameterName Task -ScriptBlock {
    param(`$commandName, `$parameterName, `$wordToComplete, `$commandAst, `$boundParameters)

    (Invoke-Build -Task ?? -File (`$boundParameters['File'])).get_Keys() -like "`$wordToComplete*" | .{process{
        New-Object System.Management.Automation.CompletionResult `$_, `$_, 'ParameterValue', `$_
    }}
}

Register-ArgumentCompleter -CommandName Invoke-Build.ps1 -ParameterName File -ScriptBlock {
    param(`$commandName, `$parameterName, `$wordToComplete, `$commandAst, `$boundParameters)

    Get-ChildItem -Directory -Name "`$wordToComplete*" | .{process{
        New-Object System.Management.Automation.CompletionResult `$_, `$_, 'ProviderContainer', `$_
    }}

    if (!(`$boundParameters['Task'] -eq '**')) {
        Get-ChildItem -File -Name "`$wordToComplete*.ps1" | .{process{
            New-Object System.Management.Automation.CompletionResult `$_, `$_, 'Command', `$_
        }}
    }
}

##################################################################################################################
# Tasks
##################################################################################################################

task Build {

    Initialize-PSModuleBuild -Name '$Name' -Verbose:`$BUILD_VERBOSE | Out-Null

    Invoke-PSModuleBuild -Verbose:`$BUILD_VERBOSE

}

task Test Build, {

    `$configuration = New-PesterConfiguration
    `$configuration.Output.Verbosity = if (`$BUILD_VERBOSE) {'Detailed'} else {'Normal'}

    Invoke-Pester -Configuration `$configuration

}

task Release Test, {

    if (-not (Get-Variable -Name 'PSGalleryApiKey' -Scope Global -ErrorAction SilentlyContinue)) {
        `$Global:PSGalleryApiKey = Read-Host -Prompt "Enter your PSGallery Nuget Api Key"
    }

    Invoke-PSModuleBuildRelease -Verbose:`$BUILD_VERBOSE

    `$params = @{
        Path = `$PSModuleBuild.Paths.BinModuleDirectory
        NuGetApiKey = `$Global:PSGalleryApiKey
        Repository = 'PSGallery'
        Force = `$true
        WhatIf = `$true
    }
    
    Publish-Module @params

}

task . Build






"@


$contentGitIgnore = @'
bin/**
'@


$contentTestFile = @'
BeforeDiscovery {
    

}

Describe 'SomeTestName' {

    BeforeAll {

    }

    It 'True and True' {
        $true | Should -Be $true
    }

}
'@


        try {

            if (-not (Test-Path -Path $Path -ErrorAction SilentlyContinue)) {
                if ($Force) {
                    try {
    
                        $params = @{
                            Path     = $Path
                            ItemType = 'Directory'
                            Force    = $true
                        }
                        New-Item @params | Out-Null
    
                    } catch {
                        Write-Error "Failed to create the path: $Path."
                        return
                    }
                } else {
                    Write-Error "Path does not exist $Path. Use the -Force parameter if you want to create the path."
                    return
                }
            }
    
            # Directories
            $moduleDirectory      = (Resolve-Path $Path).Path
            $sourceDirectory      = Join-Path $moduleDirectory 'src'
            $binDirectory         = Join-Path $moduleDirectory 'bin'
            $binModuleDirectory   = Join-Path $binDirectory    $Name
            $testDirectory        = Join-Path $moduleDirectory 'tests'
            $testResultsDirectory = Join-Path $testDirectory   '_results'

            # Files
            $invokeBuildFilePath = Join-Path $moduleDirectory '.build.ps1'
            $gitIgnorePath       = Join-Path $moduleDirectory '.gitIgnore'
            $testFile            = Join-Path $testDirectory   '_.tests.ps1'

            $sourcePsm1 = Join-Path $sourceDirectory "$($Name).psm1"
            $sourcePsd1 = Join-Path $sourceDirectory "$($Name).psd1"

            # Create the directories

            if (-not (Test-Path -Path $sourceDirectory -PathType Container)) {
                Write-Verbose " - Creating the src directory."
                try {
    
                    $params = @{
                        Path     = $sourceDirectory
                        ItemType = 'Directory'
                        Force    = $true
                    }
                    New-Item @params | Out-Null

                } catch {
                    Write-Error "Failed to create the src directory: $sourceDirectory."
                    return
                }

            }

            if (-not (Test-Path -Path $binDirectory -PathType Container)) {
                Write-Verbose " - Creating the bin directory."
                try {
    
                    $params = @{
                        Path     = $binDirectory
                        ItemType = 'Directory'
                        Force    = $true
                    }
                    New-Item @params | Out-Null

                } catch {
                    Write-Error "Failed to create the bin directory: $binDirectory."
                    return
                }

            }

            if (-not (Test-Path -Path $testDirectory -PathType Container)) {
                Write-Verbose " - Creating the tests directory."
                try {
    
                    $params = @{
                        Path     = $testDirectory
                        ItemType = 'Directory'
                        Force    = $true
                    }
                    New-Item @params | Out-Null

                } catch {
                    Write-Error "Failed to create the tests directory: $testDirectory."
                    return
                }

            }

            if (-not (Test-Path -Path $testResultsDirectory -PathType Container)) {
                Write-Verbose " - Creating the test results directory."
                try {
    
                    $params = @{
                        Path     = $testResultsDirectory
                        ItemType = 'Directory'
                        Force    = $true
                    }
                    New-Item @params | Out-Null

                } catch {
                    Write-Error "Failed to create the test results directory: $testResultsDirectory."
                    return
                }

            }

            # Create the files

            if (-not (Test-Path -Path $invokeBuildFilePath  -PathType Leaf)) {
                Write-Verbose " - Creating the .build.ps1 file."
                try {
    
                    $params = @{
                        Path     = $invokeBuildFilePath
                        Value    = $contentInvokeBuild
                        ItemType = 'File'
                        Force    = $true
                    }

                    New-Item @params | Out-Null

                } catch {
                    Write-Error "Failed to create the .build.ps1 file $invokeBuildFilePath."
                    return
                }

            }

            if (-not (Test-Path -Path $gitIgnorePath -PathType Leaf)) {
                Write-Verbose " - Creating the .gitIgnore file."
                try {
    
                    $params = @{
                        Path     = $gitIgnorePath
                        Value    = $contentGitIgnore
                        ItemType = 'File'
                        Force    = $true
                    }

                    New-Item @params | Out-Null

                } catch {
                    Write-Error "Failed to create the .gitIgnore file $gitIgnorePath."
                    return
                }

            }

            if (-not (Test-Path -Path $testFile -PathType Leaf)) {
                Write-Verbose " - Creating the _.tests.ps1 file."
                try {
    
                    $params = @{
                        Path     = $testFile
                        Value    = $contentTestFile
                        ItemType = 'File'
                        Force    = $true
                    }

                    New-Item @params | Out-Null

                } catch {
                    Write-Error "Failed to create the _.tests.ps1 file $testFile."
                    return
                }

            }

            # Module Files -------------------------------

            if (-not (Test-Path -Path $sourcePsm1 -PathType Leaf)) {
                Write-Verbose " - Creating the .psm1 file."
                try {
    
                    $params = @{
                        Path     = $sourcePsm1
                        ItemType = 'File'
                        Force    = $true
                    }
                    New-Item @params | Out-Null

                } catch {
                    Write-Error "Failed to create the .psm1 file $sourcePsm1."
                    return
                }

            }

            if (-not (Test-Path -Path $sourcePsd1 -PathType Leaf)) {
                Write-Verbose " - Creating the .psd1 file."
                try {
    
                    New-ModuleManifest -Path $sourcePsd1 -RootModule "$($Name).psm1"

                } catch {
                    Write-Error "Failed to create the .psm1 file $sourcePsm1."
                    return
                }

            } else {
                # Test to see if the root module is still the same or not.
            }
            
            #=============================================================
            #=============================================================
            #=============================================================

            $Global:PSModuleBuild = @{
                Name = $Name
                Objects = [PSCustomObject]@{
                    Functions = $null
                }
                Paths = [PSCustomObject]@{
                    ModuleDirectory     = $moduleDirectory
                    SrcDirectory        = $sourceDirectory
                    BinDirectory        = $binDirectory
                    BinModuleDirectory  = $binModuleDirectory
                    TestDirectory       = $testDirectory
                    TestOutputDirectory = $testOutputDirectory
                    PSM1 = [PSCustomObject]@{
                        Source      = $sourcePsm1
                        Destination = Join-Path $binModuleDirectory "$($Name).psm1"
                    }
                    PSD1 = [PSCustomObject]@{
                        Source      = $sourcePsd1
                        Destination = Join-Path $binModuleDirectory "$($Name).psd1"
                    }
                }
            }

        } catch {

            Remove-Variable -Name 'PSModuleBuild' -Force -ErrorAction SilentlyContinue

        } finally {

            $Global:PSModuleBuild

        }

    }

}

function Invoke-PSModuleBuild {
    [CmdletBinding()]
    Param(

    )

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

    PROCESS {
        
        if (-not (Get-Variable -name 'PSModuleBuild' -Scope 'Global' -ErrorAction SilentlyContinue)) {
            Write-Error "You must first run the command Initialize-PSModuleBuild before this can be called."
            return
        }

        $Global:PSModuleBuild.Objects.Functions = Get-PSModuleBuildFunctions -Path $Global:PSModuleBuild.Paths.SrcDirectory -Recurse -Verbose:$false

        ################################################
        # Prep the bin directory
        ################################################

        Get-ChildItem -Path $PSModuleBuild.Paths.BinDirectory | Foreach-Object {
            $_ | Remove-Item -Force -Recurse
        }

        New-Item -Path $PSModuleBuild.Paths.BinModuleDirectory -ItemType Directory -Force | Out-Null

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

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

        Copy-Item @params

        $PSModuleBuild.Objects.Functions | Foreach-Object {
            $_function = $_
            $_function.AST.Extent.text | Out-File -FilePath $PSModuleBuild.Paths.PSM1.Destination -Append
            ""                         | Out-File -FilePath $PSModuleBuild.Paths.PSM1.Destination -Append
        }

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

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

        Copy-Item @params

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

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

        if ($functionsToExport) {
            $manifestParams['FunctionsToExport'] = $PSModuleBuild.Paths.PSD1.Destination
        }
        
        #-------------------------------
        # Update the Module Manifest
        #-------------------------------

        Update-ModuleManifest @manifestParams

    }

}

function Invoke-PSModuleBuildRelease {
    [CmdletBinding(
        DefaultParameterSetName='Standard'
    )]
    Param(

        [Parameter(
            ParameterSetName='Standard',
            Position=0
        )]
        [ValidateSet('Build','Minor','Major')]
        [String]
        $BuildType = 'Build',

        [Parameter(
            ParameterSetName='ForceVersion',
            Mandatory,
            Position=0
        )]
        [Version]
        $ForceVersion

    )

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

    PROCESS {
        
        #===================================================================================

        Write-Verbose " - Checking if build has been initialized."
        if (-not (Get-Variable -name 'PSModuleBuild' -Scope 'Global' -ErrorAction SilentlyContinue)) {
            Write-Verbose " - Build has NOT been initialized."
            Write-Error "You must first run the command Initialize-PSModuleBuild before this can be called."
            return
        } else {
            Write-Verbose " - Build has been initialized."
        }

        #===================================================================================

        Write-Verbose " - Verifying that the build .psd1 has been created."
        if (-not (Test-Path -Path $PSModuleBuild.Paths.PSD1.Destination -PathType Leaf -ErrorAction SilentlyContinue)) {
            Write-Verbose " - Build .psd1 was NOT found at path $($PSModuleBuild.Paths.PSD1.Destination)"
            Write-Error "The .psd1 in the bin directory was not found at the path $($PSModuleBuild.Paths.PSD1.Destination)"
            return
        } else {
            Write-Verbose " - Build .psd1 was found at path $($PSModuleBuild.Paths.PSD1.Destination)"
        }

        #===================================================================================

        Write-Verbose " - Importing $($PSModuleBuild.Paths.PSD1.Destination)"

        $manifest = Import-PowerShellDataFile -Path $PSModuleBuild.Paths.PSD1.Destination -Verbose:$false

        Write-Verbose " - Pulling current module version"

        $versionToSet = switch ($PSCmdlet.ParameterSetName) {

            'Standard' {

                if ($manifest.ContainsKey('ModuleVersion')) {
                    $___version = ($manifest.ModuleVersion).ToString()
                    Write-Verbose " - Current Version is $___version"
                    $___version | Update-Version -Type $BuildType -Verbose:$false
                } else {
                    Write-Verbose " - No Version Found. Setting to 0.0.1"
                    "0.0.1"
                }

            }

            'ForceVersion' {
                $ForceVersion.ToString()
            }

        }

        Write-Verbose " - Updating the .psd1 ModuleVersion to $versionToSet"

        $params = @{
            Path          = $PSModuleBuild.Paths.PSD1.Destination
            ModuleVersion = $versionToSet
            Verbose       = $false
            ErrorAction   = 'Stop'
        }

        Update-ModuleManifest @params

        Write-Verbose " - Copying Build .psd1 to the src directory."

        $params = @{
            Path        = $PSModuleBuild.Paths.PSD1.Destination
            Destination = $PSModuleBuild.Paths.PSD1.Source
            Force       = $true
            Verbose     = $false
        }

        Copy-Item @params

    }

}