Import-TaskFile.ps1


<#
.SYNOPSIS
    Import the given task files into the current runspace
.DESCRIPTION
    This script will import the functions defined in the module, the users ~/.stitch directory, and the current
    project. Before importing, the script will check the 'ExcludeTasksOnImport' list, which is a list of regex to
    block from being imported by this script
#>


param(
    [Parameter()]
    [string[]]$ExcludeTasksOnImport = (
        Get-BuildProperty ExcludeTasksOnImport @()
    ),

    [Parameter(
        DontShow
    )]
    [string]$InternalTaskPath = "$PSScriptRoot\BuildScripts"
)

if ($null -eq $script:ImportErrors) {
    $script:ImportErrors = [ordered]@{}
}

# Task files in the current project
$projectTaskFiles = [System.Collections.ArrayList]@()

# Task files in ~/.stitch
$systemTaskFiles = [System.Collections.ArrayList]@()

# Task files bundled with the stitch module
$moduleTaskFiles = [System.Collections.ArrayList]@()

# Collated Task files collection
$taskFiles = [System.Collections.ArrayList]@()

<#------------------------------------------------------------------
  Start with the bundled task files
------------------------------------------------------------------#>

Write-Debug "`n<$('-' * 80)"
Write-Debug 'Collecting Invoke-Build task files:'
if ($null -ne $InternalTaskPath) {
    if (Test-Path $InternalTaskPath) {
        Write-Debug " - Looking in the module's directory"
        $moduleTaskFiles = $InternalTaskPath | Find-InvokeBuildTaskFile
        if ($moduleTaskFiles.Count -gt 0) {
            Write-Debug " - Merging $($moduleTaskFiles.Count) task files"
            $moduleTaskFiles | Merge-FileCollection ([ref]$taskFiles)
        }
    }
} else {
    Write-Warning "Path to module task path is not set "
}
<#------------------------------------------------------------------
Layer on the system task files
------------------------------------------------------------------#>

Write-Debug "Looking for a stitch directory in user's home directory"
$systemPath = Find-LocalUserStitchDirectory
if ($null -ne $systemPath) {
    Write-Debug " - Looking in the system path $systemPath"
    $systemTaskFiles = $systemPath | Find-InvokeBuildTaskFile
} else {
    Write-Debug ' - Did not find a local user stitch directory'
}

if ($systemTaskFiles.Count -gt 0) {
    Write-Debug " - Merging $($systemTaskFiles.Count) task files"
    $systemTaskFiles | Merge-FileCollection ([ref]$taskFiles)

}

<#------------------------------------------------------------------
Layer on the project task files
------------------------------------------------------------------#>

#! hopefully, BuildConfigPath is set by .build.ps1
if ($null -ne $BuildConfigPath) {
    Write-Debug " - Looking in $BuildConfigPath"
    $projectTaskFiles = $BuildConfigPath | Find-InvokeBuildTaskFile
} else {
    Write-Debug 'BuildConfigPath is not set. No task files loaded from project'
}


if ($projectTaskFiles.Count -gt 0) {
    Write-Debug " - Merging $($projectTaskFiles.Count) task files"
    $projectTaskFiles | Merge-FileCollection ([ref]$taskFiles)
}

Write-Debug "Merged all task files."
Write-Debug "`n$('-' * 80)>"

<#------------------------------------------------------------------
  Now, Process the merged collection
------------------------------------------------------------------#>


Write-Debug "`n<$('-' * 80)"
Write-Debug "Importing $($taskFiles.Count) task files"

:file foreach ($file in $taskFiles) {
    #-------------------------------------------------------------------------------
    #region Exclusions

    if (($null -ne $ExcludeTasksOnImport) -and ($ExcludeTasksOnImport.Count -gt 0)) {
        :exclude foreach ($exclude in $ExcludeTasksOnImport) {
            # the filename matches at least one exclude, no need to keep checking
            if ($file.BaseName -match $exclude) {
                Write-Debug "$($file.BaseName) is excluded by pattern $exclude"
                #! do not import the script, go to the next file in the list
                continue file
            }
        }
    }

    #endregion Exclusions
    #-------------------------------------------------------------------------------

    try {
        Write-Debug " - $($file.Name)"
        . $file.FullName
    } catch {
        <#
             This rather long catch block is collecting the relavant error information,
             and passing it up to the $ImportErrors script variable.
             ! this is because the files are imported in the .build.ps1 file, but we want
             ! to report the errors after the logs have been initialized and the rest of
             ! the components have a chance to load.

             ! The errors are reported in Enter-Build
            #>

        $importScriptName = Get-Item $PSCommandPath | Select-Object -ExpandProperty Name
        $message = [System.Text.StringBuilder]::new()

        $errorException = $_.Exception

        <#
            If there were parse errors in the imported script, then there will be an 'Errors' entry for each
            #>

        if ($errorException.Errors.Count -gt 0) {
            # Format the first line of our processed error message
            if ($errorException.Errors.Count -eq 1) {
                $null = $message.Append("There was an error trying to import $($file.Name)")
            } else {
                $null = $message.Append("There where $($errorException.Errors.Count) errors trying to import $($file.Name)")
            }
            $null = $message.AppendLine(': (')
            # Collect each of the parse errors and format them
            foreach ($importError in $errorException.Errors) {
                $null = $message.AppendJoin(
                    '',
                    ' - ',
                    $importError.Extent.File,
                    ':',
                    $importError.Extent.StartLineNumber,
                    ':')
                $null = $message.AppendLine( $importError.Extent.StartColumnNumber)
                $null = $message.Append( ' - ')
                $null = $message.AppendLine($importError.Message)
            }
            <#
            If there aren't any Errors entries, then process the error record.
            #>

        } elseif ($errorException.ErrorRecord.Count -gt 0) {
            if ($errorException.ErrorRecord.Count -eq 1) {
                $null = $message.AppendLine("There was an error trying to import $($file.Name)")
            } else {
                $null = $message.AppendLine("There where $($errorException.ErrorRecord.Count) errors trying to import $($file.Name)")
            }
            $null = $message.AppendLine(': (')
            foreach ($importError in $errorException.ErrorRecord) {
                $null = $message.Append( ' - ')
                $null = $message.AppendJoin(
                    '',
                    ' - ',
                    $importError.InvocationInfo.ScriptName,
                    ':',
                    $importError.InvocationInfo.ScriptLineNumber,
                    ':')
                $null = $message.AppendLine( $importError.InvocationInfo.Offset.InLine)

                $null = $message.AppendLine($importError.Exception.Message)
                # Add the position message, unless it just points to this script
                if (-not($importError.InvocationInfo.PositionMessage -match [regex]::Escape($importScriptName))) {
                    $null = $message.AppendLine($importError.InvocationInfo.PositionMessage)
                }
            }
        } else {
            $null = $message.AppendLine("There was an error trying to import $($file.Name)")
            $null = $message.AppendLine($_)
        }
        Write-Debug "An error occured importing $($file.Name):`n$($message.ToString())"
        if ($null -ne $script:ImportErrors) {
            $script:ImportErrors.Add($file.Name, $message.ToString())
        } else {
            Write-Debug 'ImportErrors was not initialized'
            $message.ToString()
        }
    }
}

Write-Debug "`n$('-' * 80)>"