PSPublishModule.psm1

function Add-Directory {
    [CmdletBinding()]
    param($dir)
    $exists = Test-Path -Path $dir
    if ($exists -eq $false) {$createdDirectory = mkdir $dir} else {}
}
function Add-FilesWithFolders {
    [CmdletBinding()]
    param ($file, $FullProjectPath, $directory)
    $LinkPrivatePublicFiles = @()
    $path = $file
    foreach ($dir in $directory) {
        if ($path -like "$dir*") {
            $LinkPrivatePublicFiles += $path
            Write-Verbose "Adding file to linking list of files $path"
        }
    }
    return $LinkPrivatePublicFiles
}
function Add-FilesWithFoldersNew {
    [CmdletBinding()]
    param($File, $FullProjectPath, $directory)
}
function Add-ObjectTo {
    [CmdletBinding()]
    param($Object, $Type)
    Write-Verbose "Adding $($Object) to $Type"
    return $Object
}
function Copy-File {
    [CmdletBinding()]
    param ($Source,
        $Destination)
    if ((Test-Path $Source) -and !(Test-Path $Destination)) {Copy-Item -Path $Source -Destination $Destination}
}
function Export-PSData {
    [cmdletbinding()]
    <#
    .Synopsis
        Exports property bags into a data file
    .Description
        Exports property bags and the first level of any other object into a ps data file (.psd1)
    .Link
        https://github.com/StartAutomating/Pipeworks
        Import-PSData
    .Example
        Get-Web -Url http://www.youtube.com/watch?v=xPRC3EDR_GU -AsMicrodata -ItemType http://schema.org/VideoObject |
            Export-PSData .\PipeworksQuickstart.video.psd1
    #>

    [OutputType([IO.FileInfo])]
    param([Parameter(Mandatory = $true, ValueFromPipeline = $true)]
        [PSObject[]]
        $InputObject,
        [Parameter(Mandatory = $true, Position = 0)]
        [string]
        $DataFile)
    begin {$AllObjects = New-Object Collections.ArrayList}
    process {$null = $AllObjects.AddRange($InputObject)}
    end {
        $text = $AllObjects |
            Write-PowerShellHashtable
        $text |
            Set-Content -Path $DataFile
        Get-Item -Path $DataFile
    }
}
function Find-EnumsList {
    [CmdletBinding()]
    param ([string] $ProjectPath)
    if ($PSEdition -eq 'Core') {$Enums = Get-ChildItem -Path $ProjectPath\Enums\*.ps1 -ErrorAction SilentlyContinue -FollowSymlink} else {$Enums = Get-ChildItem -Path $ProjectPath\Enums\*.ps1 -ErrorAction SilentlyContinue}
    $Opening = '@('
    $Closing = ')'
    $Adding = ','
    $EnumsList = New-ArrayList
    Add-ToArray -List $EnumsList -Element $Opening
    Foreach ($import in @($Enums)) {
        $Entry = "'Enums\$($import.Name)'"
        Add-ToArray -List $EnumsList -Element $Entry
        Add-ToArray -List $EnumsList -Element $Adding
    }
    Remove-FromArray -List $EnumsList -LastElement
    Add-ToArray -List $EnumsList -Element $Closing
    return [string] $EnumsList
}
function Format-Code {
    [cmdletbinding()]
    param([string] $FilePath,
        $FormatCode)
    if ($FormatCode.Enabled) {
        if ($FormatCode.RemoveComments) {
            Write-Verbose "Removing Comments - $FilePath"
            $Output = Remove-Comments -FilePath $FilePath
        } else {$Output = Get-Content -LiteralPath $FilePath -Raw}
        if ($null -eq $FormatCode.FormatterSettings) {$FormatCode.FormatterSettings = $Script:FormatterSettings}
        Write-Verbose "Formatting - $FilePath"
        try {$Output = Invoke-Formatter -ScriptDefinition $Output -Settings $FormatCode.FormatterSettings -Verbose:$false} catch {
            $ErrorMessage = $_.Exception.Message
            Write-Error "Format-Code - Formatting on file $FilePath failed. Error: $ErrorMessage"
            Exit
        }
        $Output = foreach ($O in $Output) {if ($O.Trim() -ne '') {$O.Trim()}}
        try {$Output | Out-File -LiteralPath $FilePath -NoNewline -Encoding utf8} catch {
            $ErrorMessage = $_.Exception.Message
            Write-Error "Format-Code - Resaving file $FilePath failed. Error: $ErrorMessage"
            Exit
        }
    }
}
function Format-PSD1 {
    [cmdletbinding()]
    param([string] $PSD1FilePath,
        $FormatCode)
    if ($FormatCode.Enabled) {
        $Output = Get-Content -LiteralPath $PSD1FilePath -Raw
        if ($FormatCode.RemoveComments) {
            Write-Verbose "Removing Comments - $PSD1FilePath"
            $Output = Remove-Comments -ScriptContent $Output
        }
        Write-Verbose "Formatting - $PSD1FilePath"
        if ($null -eq $FormatCode.FormatterSettings) {$FormatCode.FormatterSettings = $Script:FormatterSettings}
        $Output = Invoke-Formatter -ScriptDefinition $Output -Settings $FormatCode.FormatterSettings
        $Output | Out-File -LiteralPath $PSD1FilePath -NoNewline
    }
}
function Format-UsingNamespace {
    [CmdletBinding()]
    param([string] $FilePath,
        [string] $FilePathSave,
        [string] $FilePathUsing)
    if ($FilePathSave -eq '') {$FilePathSave = $FilePath}
    $FileStream = New-Object -TypeName IO.FileStream -ArgumentList ($FilePath), ([System.IO.FileMode]::Open), ([System.IO.FileAccess]::Read), ([System.IO.FileShare]::ReadWrite)
    $ReadFile = New-Object -TypeName System.IO.StreamReader -ArgumentList ($FileStream, [System.Text.Encoding]::UTF8, $true)
    $UsingNamespaces = [System.Collections.Generic.List[string]]::new()
    $AddTypes = [System.Collections.Generic.List[string]]::new()
    $Content = while (!$ReadFile.EndOfStream) {
        $Line = $ReadFile.ReadLine()
        if ($Line -like 'using namespace*') {$UsingNamespaces.Add($Line)} else {$Line}
    }
    $ReadFile.Close()
    $null = New-Item -Path $FilePathSave -ItemType file -Force
    if ($UsingNamespaces) {
        $null = New-Item -Path $FilePathUsing -ItemType file -Force
        $UsingNamespaces = $UsingNamespaces.Trim() | Sort-Object -Unique
        $UsingNamespaces | Add-Content -LiteralPath $FilePathUsing -Encoding utf8
        $Content | Add-Content -LiteralPath $FilePathUsing -Encoding utf8
        return $true
    } else {
        $Content | Add-Content -LiteralPath $FilePathSave -Encoding utf8
        return $False
    }
}
Function Get-AliasTarget {
    [cmdletbinding()]
    param ([Parameter(Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName)]
        [Alias('PSPath', 'FullName')]
        [string[]]$Path)
    process {
        foreach ($File in $Path) {
            $FileAst = [System.Management.Automation.Language.Parser]::ParseFile($File, [ref]$null, [ref]$null)
            $FunctionName = $FileAst.FindAll( {param ($ast)
                    $ast -is [System.Management.Automation.Language.FunctionDefinitionAst]}, $true).Name
            $AliasDefinitions = $FileAst.FindAll( {param ($ast)
                    $ast -is [System.Management.Automation.Language.StringConstantExpressionAst] -And $ast.Value -match '(New|Set)-Alias'}, $true)
            $AliasTarget = $AliasDefinitions.Parent.CommandElements.Where( {$_.StringConstantType -eq 'BareWord' -and
                    $_.Value -notin ('New-Alias', 'Set-Alias', $FunctionName)}).Value
            $Attributes = $FileAst.FindAll( {param ($ast)
                    $ast -is [System.Management.Automation.Language.AttributeAst]}, $true)
            $AliasDefinitions = $Attributes.Where( {$_.TypeName.Name -eq 'Alias' -and $_.Parent -is [System.Management.Automation.Language.ParamBlockAst]})
            $AliasTarget += $AliasDefinitions.PositionalArguments.Value
            [PsCustomObject]@{Function = $FunctionName
                Alias = $AliasTarget
            }
        }
    }
}
function Get-FunctionAliases {
    [cmdletbinding()]
    param([string] $Path)
    Import-Module $Path -Force -Verbose:$False
    $Names = Get-FunctionNames -Path $Path
    $Aliases = foreach ($Name in $Names) {Get-Alias | Where-Object {$_.Definition -eq $Name}}
    return $Aliases
}
function Get-FunctionAliasesFromFolder {
    [cmdletbinding()]
    param([string] $FullProjectPath,
        [string[]] $Folder)
    foreach ($F in $Folder) {
        $Path = [IO.Path]::Combine($FullProjectPath, $F)
        if ($PSEdition -eq 'Core') {$Files = Get-ChildItem -Path $Path -File -Recurse -FollowSymlink} else {$Files = Get-ChildItem -Path $Path -File -Recurse}
        $AliasesToExport = foreach ($file in $Files) {Get-AliasTarget -Path $File.FullName | Select-Object -ExpandProperty Alias}
        $AliasesToExport
    }
}
function Get-FunctionNames {
    [cmdletbinding()]
    param([string] $Path,
        [switch] $Recurse)
    [Management.Automation.Language.Parser]::ParseFile((Resolve-Path $Path),
        [ref]$null,
        [ref]$null).FindAll( {param($c)$c -is [Management.Automation.Language.FunctionDefinitionAst]}, $Recurse).Name
}
function Get-FunctionNamesFromFolder {
    [cmdletbinding()]
    param([string] $FullProjectPath,
        [string[]] $Folder)
    foreach ($F in $Folder) {
        $Path = [IO.Path]::Combine($FullProjectPath, $F)
        if ($PSEdition -eq 'Core') {$Files = Get-ChildItem -Path $Path -File -Recurse -FollowSymlink} else {$Files = Get-ChildItem -Path $Path -File -Recurse}
        $FunctionToExport = foreach ($file in $Files) {Get-FunctionNames -Path $File.FullName}
        $FunctionToExport
    }
}
Function Get-ScriptComments {
    <#
    .Synopsis
    Get comments from a PowerShell script file.
    .Description
    This command will use the AST parser to go through a PowerShell script, either a .ps1 or .psm1 file, and display only the comments.
    .Example
    PS C:\> get-scriptcomments c:\scripts\MyScript.ps1
    #>

    [cmdletbinding()]
    Param([Parameter(Position = 0, Mandatory, HelpMessage = "Enter the path of a PS1 file",
            ValueFromPipeline, ValueFromPipelineByPropertyName)]
        [Alias("PSPath", "Name")]
        [ValidateScript( {Test-Path $_})]
        [ValidatePattern("\.ps(1|m1)$")]
        [string]$Path)
    Begin {
        Write-Verbose -Message "Starting $($MyInvocation.Mycommand)"
        New-Variable astTokens -force
        New-Variable astErr -force
    }
    Process {
        $Path = Convert-Path -Path $Path
        Write-Verbose -Message "Parsing $Path"
        $ast = [System.Management.Automation.Language.Parser]::ParseFile($Path, [ref]$astTokens, [ref]$astErr)
        $asttokens.where( {$_.kind -eq 'comment'}) | Select-Object -ExpandProperty Text
        $ast
    }
    End {Write-Verbose -Message "Ending $($MyInvocation.Mycommand)"}
}
function Merge-Module {
    [CmdletBinding()]
    param ([string] $ModuleName,
        [string] $ModulePathSource,
        [string] $ModulePathTarget,
        [Parameter(Mandatory = $false, ValueFromPipeline = $false)]
        [ValidateSet("ASC", "DESC", "NONE")]
        [string] $Sort = 'NONE',
        [string[]] $FunctionsToExport,
        [string[]] $AliasesToExport,
        [Array] $LibrariesCore,
        [Array] $LibrariesDefault,
        $FormatCodePSM1,
        $FormatCodePSD1)
    $PSM1FilePath = "$ModulePathTarget\$ModuleName.psm1"
    $PSD1FilePath = "$ModulePathTarget\$ModuleName.psd1"
    if ($PSEdition -eq 'Core') {$ScriptFunctions = Get-ChildItem -Path $ModulePathSource\*.ps1 -ErrorAction SilentlyContinue -Recurse -FollowSymlink} else {$ScriptFunctions = Get-ChildItem -Path $ModulePathSource\*.ps1 -ErrorAction SilentlyContinue -Recurse}
    if ($Sort -eq 'ASC') {$ScriptFunctions = $ScriptFunctions | Sort-Object -Property Name} elseif ($Sort -eq 'DESC') {$ScriptFunctions = $ScriptFunctions | Sort-Object -Descending -Property Name}
    foreach ($FilePath in $ScriptFunctions) {
        $Content = Get-Content -Path $FilePath -Raw
        $Content = $Content.Replace('$PSScriptRoot\', '$PSScriptRoot\')
        $Content = $Content.Replace('$PSScriptRoot\', '$PSScriptRoot\')
        try {$Content | Out-File -Append -LiteralPath $PSM1FilePath -Encoding utf8} catch {
            $ErrorMessage = $_.Exception.Message
            Write-Error "Merge-Module - Merge on file $FilePath failed. Error: $ErrorMessage"
            Exit
        }
    }
    $FilePathUsing = "$ModulePathTarget\$ModuleName.ps1"
    $UsingInPlace = Format-UsingNamespace -FilePath $PSM1FilePath -FilePathUsing $FilePathUsing
    if ($UsingInPlace) {Format-Code -FilePath $FilePathUsing -FormatCode $FormatCodePSM1}
    New-PSMFile -Path $PSM1FilePath -FunctionNames $FunctionsToExport -FunctionAliaes $AliasesToExport -LibrariesCore $LibrariesCore -LibrariesDefault $LibrariesDefault -ModuleName $ModuleName -UsingNamespaces:$UsingInPlace
    Format-Code -FilePath $PSM1FilePath -FormatCode $FormatCodePSM1
    New-PersonalManifest -Configuration $Configuration -ManifestPath $PSD1FilePath
    Format-Code -FilePath $PSD1FilePath -FormatCode $FormatCodePSD1
}
function New-CreateModule {
    [CmdletBinding()]
    param ([string] $ProjectName,
        $ModulePath,
        $ProjectPath)
    $FullProjectPath = "$projectPath\$projectName"
    $Folders = 'Private', 'Public', 'Examples', 'Ignore', 'Publish', 'Enums', 'Data'
    Add-Directory $FullProjectPath
    foreach ($folder in $Folders) {Add-Directory "$FullProjectPath\$folder"}
    Copy-File -Source "$PSScriptRoot\Data\Example-Gitignore.txt" -Destination "$FullProjectPath\.gitignore"
    Copy-File -Source "$PSScriptRoot\Data\Example-LicenseMIT.txt" -Destination "$FullProjectPath\License"
    Copy-File -Source "$PSScriptRoot\Data\Example-ModuleStarter.ps1" -Destination "$FullProjectPath\$ProjectName.psm1"
}
function New-PersonalManifest {
    [CmdletBinding()]
    param([System.Collections.IDictionary] $Configuration,
        [string] $ManifestPath,
        [switch] $AddScriptsToProcess)
    $Manifest = $Configuration.Information.Manifest
    $Manifest.Path = $ManifestPath
    if (-not $AddScriptsToProcess) {$Manifest.ScriptsToProcess = @()}
    New-ModuleManifest @Manifest
    if ($Configuration.Steps.PublishModule.Prerelease -ne '') {
        $Data = Import-PowerShellDataFile -Path $Configuration.Information.Manifest.Path
        if ($Data.ScriptsToProcess.Count -eq 0) {$Data.Remove('ScriptsToProcess')}
        if ($Data.CmdletsToExport.Count -eq 0) {$Data.Remove('CmdletsToExport')}
        $Data.PrivateData.PSData.Prerelease = $Configuration.Steps.PublishModule.Prerelease
        $Data | Export-PSData -DataFile $Configuration.Information.Manifest.Path
    }
    Write-Verbose "Converting $($Configuration.Information.Manifest.Path)"
    (Get-Content $Manifest.Path) | Out-FileUtf8NoBom $Manifest.Path
}
function New-PrepareManifest {
    [CmdletBinding()]
    param($ProjectName,
        $modulePath,
        $projectPath,
        $functionToExport,
        $projectUrl)
    Set-Location "$projectPath\$ProjectName"
    $manifest = @{Path = ".\$ProjectName.psd1"
        RootModule = "$ProjectName.psm1"
        Author = 'Przemyslaw Klys'
        CompanyName = 'Evotec'
        Copyright = 'Evotec (c) 2011-2019. All rights reserved.'
        Description = "Simple project"
        FunctionsToExport = $functionToExport
        CmdletsToExport = ''
        VariablesToExport = ''
        AliasesToExport = ''
        FileList = "$ProjectName.psm1", "$ProjectName.psd1"
        HelpInfoURI = $projectUrl
        ProjectUri = $projectUrl
    }
    New-ModuleManifest @manifest
}
function New-PrepareModule {
    [CmdletBinding()]
    param ([System.Collections.IDictionary] $Configuration)
    if (-not $Configuration) {return}
    if (-not $Configuration.Information.DirectoryModulesCore) {$Configuration.Information.DirectoryModulesCore = "$Env:USERPROFILE\Documents\PowerShell\Modules"}
    if (-not $Configuration.Information.DirectoryModules) {$Configuration.Information.DirectoryModules = "$Env:USERPROFILE\Documents\WindowsPowerShell\Modules"}
    if ($Configuration.Steps.BuildModule.Enable) {Start-ModuleBuilding -Configuration $Configuration -Core:$false}
    if ($Configuration.Steps.BuildModule.EnableDesktop) {Start-ModuleBuilding -Configuration $Configuration -Core:$false}
    if ($Configuration.Steps.BuildModule.EnableCore) {Start-ModuleBuilding -Configuration $Configuration -Core:$true}
}
function New-PSMFile {
    [cmdletbinding()]
    param([string] $Path,
        [string[]] $FunctionNames,
        [string[]] $FunctionAliaes,
        [Array] $LibrariesCore,
        [Array] $LibrariesDefault,
        [string] $ModuleName,
        [switch] $UsingNamespaces)
    try {
        if ($FunctionNames.Count -gt 0) {
            $Functions = ($FunctionNames | Sort-Object -Unique) -join "','"
            $Functions = "'$Functions'"
        } else {$Functions = @()}
        if ($FunctionAliaes.Count -gt 0) {
            $Aliases = ($FunctionAliaes | Sort-Object -Unique) -join "','"
            $Aliases = "'$Aliases'"
        } else {$Aliases = @()}
        "" | Add-Content -Path $Path
        if ($LibrariesCore.Count -gt 0 -and $LibrariesDefault.Count -gt 0) {
            'if ($PSEdition -eq ''Core'') {' | Add-Content -Path $Path
            foreach ($File in $LibrariesCore) {
                $Output = 'Add-Type -Path $PSScriptRoot\' + $File
                $Output | Add-Content -Path $Path
            }
            '} else {' | Add-Content -Path $Path
            foreach ($File in $LibrariesDefault) {
                $Output = 'Add-Type -Path $PSScriptRoot\' + $File
                $Output | Add-Content -Path $Path
            }
            '}' | Add-Content -Path $Path
        } elseif ($LibrariesCore.Count -gt 0) {
            foreach ($File in $LibrariesCore) {
                $Output = 'Add-Type -Path $PSScriptRoot\' + $File
                $Output | Add-Content -Path $Path
            }
        } elseif ($LibrariesDefault.Count -gt 0) {
            foreach ($File in $LibrariesDefault) {
                $Output = 'Add-Type -Path $PSScriptRoot\' + $File
                $Output | Add-Content -Path $Path
            }
        }
        if ($UsingNamespaces) {'. $PSScriptRoot\' + "$ModuleName.ps1" | Add-Content -Path $Path}
        @"
 
Export-ModuleMember ``
    -Function @($Functions) ``
    -Alias @($Aliases)
"@
 | Add-Content -Path $Path
    } catch {
        $ErrorMessage = $_.Exception.Message
        Write-Error "New-PSM1File from $ModuleName failed build. Error: $ErrorMessage"
        Exit
    }
}
function New-PublishModule {
    [cmdletbinding()]
    param($projectName,
        $apikey,
        [bool] $RequireForce)
    Publish-Module -Name $projectName -Repository PSGallery -NuGetApiKey $apikey -Force:$RequireForce -verbose
}
<#
.SYNOPSIS
  Outputs to a UTF-8-encoded file *without a BOM* (byte-order mark).
 
.DESCRIPTION
  Mimics the most important aspects of Out-File:
  * Input objects are sent to Out-String first.
  * -Append allows you to append to an existing file, -NoClobber prevents
    overwriting of an existing file.
  * -Width allows you to specify the line width for the text representations
     of input objects that aren't strings.
  However, it is not a complete implementation of all Out-String parameters:
  * Only a literal output path is supported, and only as a parameter.
  * -Force is not supported.
 
  Caveat: *All* pipeline input is buffered before writing output starts,
          but the string representations are generated and written to the target
          file one by one.
 
.NOTES
  The raison d'être for this advanced function is that, as of PowerShell v5,
  Out-File still lacks the ability to write UTF-8 files without a BOM:
  using -Encoding UTF8 invariably prepends a BOM.
 
#>

function Out-FileUtf8NoBom {
    [CmdletBinding()]
    param([Parameter(Mandatory, Position = 0)] [string] $LiteralPath,
        [switch] $Append,
        [switch] $NoClobber,
        [AllowNull()] [int] $Width,
        [Parameter(ValueFromPipeline)] $InputObject)
    [System.IO.Directory]::SetCurrentDirectory($PWD)
    $LiteralPath = [IO.Path]::GetFullPath($LiteralPath)
    if ($NoClobber -and (Test-Path $LiteralPath)) {Throw [IO.IOException] "The file '$LiteralPath' already exists."}
    $sw = New-Object IO.StreamWriter $LiteralPath, $Append
    $htOutStringArgs = @{}
    if ($Width) {$htOutStringArgs += @{Width = $Width}
    }
    try {$Input | Out-String -Stream @htOutStringArgs | ForEach-Object {$sw.WriteLine($_)}} finally {$sw.Dispose()}
}
function Remove-Comments {
    Param ([string] $FilePath,
        [parameter(ValueFromPipeline = $True)] $Scriptblock,
        [string] $ScriptContent)
    Begin {
        $Items = @()
        if ($PSBoundParameters['FilePath']) {
            $ScriptBlockString = [IO.File]::ReadAllText((Resolve-Path $FilePath))
            $ScriptBlock = [ScriptBlock]::Create($ScriptBlockString)
        } elseif ($PSBoundParameters['ScriptContent']) {$ScriptBlock = [ScriptBlock]::Create($ScriptContent)} else {}
    }
    Process {$Items += $Scriptblock}
    End {
        $OldScript = $Items -join [environment]::NewLine
        If (-not $OldScript.Trim(" `n`r`t")) {return}
        $Tokens = [System.Management.Automation.PSParser]::Tokenize($OldScript, [ref]$Null)
        $AllowedComments = @('requires'
            '.SYNOPSIS'
            '.DESCRIPTION'
            '.PARAMETER'
            '.EXAMPLE'
            '.INPUTS'
            '.OUTPUTS'
            '.NOTES'
            '.LINK'
            '.COMPONENT'
            '.ROLE'
            '.FUNCTIONALITY'
            '.FORWARDHELPCATEGORY'
            '.REMOTEHELPRUNSPACE'
            '.EXTERNALHELP')
        $Tokens = $Tokens.ForEach{If ($_.Type -ne 'Comment') {$_} Else {
                $CommentText = $_.Content.Substring($_.Content.IndexOf('#') + 1)
                $FirstInnerToken = [System.Management.Automation.PSParser]::Tokenize($CommentText, [ref]$Null) |
                    Where-Object {$_.Type -ne 'NewLine'} |
                    Select-Object -First 1
                If ($FirstInnerToken.Content -in $AllowedComments) {$_}
            }}
        $NewScriptText = ''
        $SkipNext = $False
        If ($Tokens.Count -gt 1) {
            ForEach ($i in (0..($Tokens.Count - 2))) {
                If (-not $SkipNext -and
                    $Tokens[$i ].Type -ne 'LineContinuation' -and ($Tokens[$i ].Type -notin ('NewLine', 'StatementSeparator') -or
                        $Tokens[$i + 1].Type -notin ('NewLine', 'StatementSeparator', 'GroupEnd'))) {
                    If ($Tokens[$i].Type -in ('String', 'Variable')) {$NewScriptText += $OldScript.Substring($Tokens[$i].Start, $Tokens[$i].Length)} Else {$NewScriptText += $Tokens[$i].Content}
                    If ($Tokens[$i ].Type -notin ('NewLine', 'GroupStart', 'StatementSeparator') -and
                        $Tokens[$i + 1].Type -notin ('NewLine', 'GroupEnd', 'StatementSeparator') -and
                        $Tokens[$i].EndLine -eq $Tokens[$i + 1].StartLine -and
                        $Tokens[$i + 1].StartColumn - $Tokens[$i].EndColumn -gt 0) {$NewScriptText += ' '}
                    $SkipNext = $Tokens[$i].Type -eq 'GroupStart' -and $Tokens[$i + 1].Type -in ('NewLine', 'StatementSeparator')
                } Else {$SkipNext = $SkipNext -and $Tokens[$i + 1].Type -in ('NewLine', 'StatementSeparator')}
            }
        }
        If ($Tokens) {If ($Tokens[$i].Type -in ('String', 'Variable')) {$NewScriptText += $OldScript.Substring($Tokens[-1].Start, $Tokens[-1].Length)} Else {$NewScriptText += $Tokens[-1].Content}}
        $NewScriptText = $NewScriptText.TrimStart("`n`r;")
        If ($Items.Count -eq 1) {If ($Items[0] -is [scriptblock]) {return [scriptblock]::Create($NewScriptText)} Else {return $NewScriptText}} Else {return $NewScriptText.Split("`n`r", [System.StringSplitOptions]::RemoveEmptyEntries)}
    }
}
function Remove-Directory {
    [CmdletBinding()]
    param ([string] $Dir)
    if (-not [string]::IsNullOrWhiteSpace($Dir)) {
        $exists = Test-Path -Path $Dir
        if ($exists) {Remove-Item $dir -Confirm:$false -Recurse} else {}
    }
}
$Script:FormatterSettings = @{IncludeRules = @('PSPlaceOpenBrace',
        'PSPlaceCloseBrace',
        'PSUseConsistentWhitespace',
        'PSUseConsistentIndentation',
        'PSAlignAssignmentStatement',
        'PSUseCorrectCasing')
    Rules = @{PSPlaceOpenBrace = @{Enable = $true
            OnSameLine = $true
            NewLineAfter = $true
            IgnoreOneLineBlock = $true
        }
        PSPlaceCloseBrace = @{Enable = $true
            NewLineAfter = $false
            IgnoreOneLineBlock = $true
            NoEmptyLineBefore = $false
        }
        PSUseConsistentIndentation = @{Enable = $true
            Kind = 'space'
            PipelineIndentation = 'IncreaseIndentationAfterEveryPipeline'
            IndentationSize = 4
        }
        PSUseConsistentWhitespace = @{Enable = $true
            CheckInnerBrace = $true
            CheckOpenBrace = $true
            CheckOpenParen = $true
            CheckOperator = $true
            CheckPipe = $true
            CheckSeparator = $true
        }
        PSUseCorrectCasing = @{Enable = $true}
    }
}
function Set-LinkedFiles {
    [CmdletBinding()]
    param($LinkFiles,
        $FullModulePath,
        $FullProjectPath,
        [switch] $Delete)
    foreach ($file in $LinkFiles) {
        $Path = "$FullModulePath\$file"
        $Path2 = "$FullProjectPath\$file"
        if ($Delete) {
            if (Test-ReparsePoint -path $Path) {
                Write-Verbose "Removing symlink first $path"
                Remove-Item $Path -Confirm:$false
            }
        }
        Write-Verbose "Creating symlink from $path2 (source) to $path (target)"
        $linkingFiles = cmd /c mklink $path $path2
    }
}
function Start-ModuleBuilding {
    [CmdletBinding()]
    param([System.Collections.IDictionary] $Configuration,
        [switch] $Core)
    if ($Core) {[string] $FullModulePath = [IO.path]::Combine($Configuration.Information.DirectoryModulesCore, $Configuration.Information.ModuleName)} else {[string] $FullModulePath = [IO.path]::Combine($Configuration.Information.DirectoryModules, $Configuration.Information.ModuleName)}
    [string] $FullTemporaryPath = [IO.path]::GetTempPath() + '' + $Configuration.Information.ModuleName
    [string] $FullProjectPath = [IO.Path]::Combine($Configuration.Information.DirectoryProjects, $Configuration.Information.ModuleName)
    [string] $ProjectName = $Configuration.Information.ModuleName
    Write-Verbose '----------------------------------------------------'
    Write-Verbose "Project Name: $ProjectName"
    Write-Verbose "Full module path: $FullModulePath"
    Write-Verbose "Full project path: $FullProjectPath"
    Write-Verbose "Full module path to delete: $FullModulePathDelete"
    Write-Verbose "Full temporary path: $FullTemporaryPath"
    Write-Verbose "PSScriptRoot: $PSScriptRoot"
    Write-Verbose "PSEdition: $PSEdition"
    Write-Verbose '----------------------------------------------------'
    $CurrentLocation = (Get-Location).Path
    Set-Location -Path $FullProjectPath
    Remove-Directory $FullModulePath
    Remove-Directory $FullTemporaryPath
    Add-Directory $FullModulePath
    Add-Directory $FullTemporaryPath
    $DirectoryTypes = 'Public', 'Private', 'Lib', 'Bin', 'Enums', 'Images', 'Templates', 'Resources'
    $LinkDirectories = @()
    $LinkPrivatePublicFiles = @()
    $Configuration.Information.Manifest.RootModule = "$($ProjectName).psm1"
    $Configuration.Information.Manifest.FunctionsToExport = @()
    $Configuration.Information.Manifest.CmdletsToExport = @()
    $Configuration.Information.Manifest.VariablesToExport = @()
    $Configuration.Information.Manifest.AliasesToExport = @()
    if ($Configuration.Steps.BuildModule) {
        if ($PSEdition -eq 'core') {
            $Directories = Get-ChildItem -Path $FullProjectPath -Directory -Recurse -FollowSymlink
            $Files = Get-ChildItem -Path $FullProjectPath -File -Recurse -FollowSymlink
            $FilesRoot = Get-ChildItem -Path $FullProjectPath -File -FollowSymlink
        } else {
            $Directories = Get-ChildItem -Path $FullProjectPath -Directory -Recurse
            $Files = Get-ChildItem -Path $FullProjectPath -File -Recurse
            $FilesRoot = Get-ChildItem -Path $FullProjectPath -File
        }
        $LinkDirectories = foreach ($directory in $Directories) {
            $RelativeDirectoryPath = (Resolve-Path -LiteralPath $directory.FullName -Relative).Replace('.\', '')
            $RelativeDirectoryPath = "$RelativeDirectoryPath\"
            foreach ($LookupDir in $DirectoryTypes) {if ($RelativeDirectoryPath -like "$LookupDir\*") {Add-ObjectTo -Object $RelativeDirectoryPath -Type 'Directory List'}}
        }
        $AllFiles = foreach ($File in $Files) {
            $RelativeFilePath = (Resolve-Path -LiteralPath $File.FullName -Relative).Replace('.\', '')
            $RelativeFilePath
        }
        $RootFiles = foreach ($File in $FilesRoot) {
            $RelativeFilePath = (Resolve-Path -LiteralPath $File.FullName -Relative).Replace('.\', '')
            $RelativeFilePath
        }
        $LinkFilesRoot = foreach ($File in $RootFiles) {
            switch -Wildcard ($file) {
                '*.psd1' {Add-ObjectTo -Object $File -Type 'Root Files List'}
                '*.psm1' {Add-ObjectTo -Object $File -Type 'Root Files List'}
                'License*' {Add-ObjectTo -Object $File -Type 'Root Files List'}
            }
        }
        $LinkPrivatePublicFiles = foreach ($file in $AllFiles) {
            switch -Wildcard ($file) {
                "*.dll" {Add-FilesWithFolders -file $file -FullProjectPath $FullProjectPath -directory 'Lib'}
                "*.exe" {Add-FilesWithFolders -file $file -FullProjectPath $FullProjectPath -directory 'Bin'}
                '*.ps1' {Add-FilesWithFolders -file $file -FullProjectPath $FullProjectPath -directory 'Private', 'Public', 'Enums'}
                '*license*' {Add-FilesWithFolders -file $file -FullProjectPath $FullProjectPath -directory 'Lib', 'Resources'}
                '*.jpg' {Add-FilesWithFolders -file $file -FullProjectPath $FullProjectPath -directory 'Images', 'Resources'}
                '*.png' {Add-FilesWithFolders -file $file -FullProjectPath $FullProjectPath -directory 'Images', 'Resources'}
                '*.xml' {Add-FilesWithFolders -file $file -FullProjectPath $FullProjectPath -directory 'Templates'}
                '*.docx' {Add-FilesWithFolders -file $file -FullProjectPath $FullProjectPath -directory 'Templates'}
                '*.js' {Add-FilesWithFolders -file $file -FullProjectPath $FullProjectPath -directory 'Resources'}
                '*.css' {Add-FilesWithFolders -file $file -FullProjectPath $FullProjectPath -directory 'Resources'}
                '*.rcs' {Add-FilesWithFolders -file $file -FullProjectPath $FullProjectPath -directory 'Resources'}
                '*.gif' {Add-FilesWithFolders -file $file -FullProjectPath $FullProjectPath -directory 'Resources'}
                '*.html' {Add-FilesWithFolders -file $file -FullProjectPath $FullProjectPath -directory 'Resources'}
                '*.txt' {Add-FilesWithFolders -file $file -FullProjectPath $FullProjectPath -directory 'Resources'}
            }
        }
        if ($Configuration.Information.Manifest) {
            $Functions = Get-FunctionNamesFromFolder -FullProjectPath $FullProjectPath -Folder $Configuration.Information.FunctionsToExport
            if ($Functions) {
                Write-Verbose "Functions export: $Functions"
                $Configuration.Information.Manifest.FunctionsToExport = $Functions
            }
            $Aliases = Get-FunctionAliasesFromFolder -FullProjectPath $FullProjectPath -Folder $Configuration.Information.AliasesToExport
            if ($Aliases) {
                Write-Verbose "Aliases export: $Aliases"
                $Configuration.Information.Manifest.AliasesToExport = $Aliases
            }
            if (-not [string]::IsNullOrWhiteSpace($Configuration.Information.ScriptsToProcess)) {
                $StartsWithEnums = "$($Configuration.Information.ScriptsToProcess)\"
                $FilesEnums = $LinkPrivatePublicFiles | Where-Object {($_).StartsWith($StartsWithEnums)}
                if ($FilesEnums.Count -gt 0) {
                    Write-Verbose "ScriptsToProcess export: $FilesEnums"
                    $Configuration.Information.Manifest.ScriptsToProcess = $FilesEnums
                }
            }
            $PSD1FilePath = "$FullProjectPath\$ProjectName.psd1"
            New-PersonalManifest -Configuration $Configuration -ManifestPath $PSD1FilePath -AddScriptsToProcess
            Format-Code -FilePath $PSD1FilePath -FormatCode $Configuration.Options.Standard.FormatCodePSD1
        }
        if ($Configuration.Steps.BuildModule.Merge) {
            foreach ($Directory in $LinkDirectories) {
                $Dir = "$FullTemporaryPath\$Directory"
                Add-Directory $Dir
            }
            $LinkDirectoriesWithSupportFiles = $LinkDirectories | Where-Object {$_ -ne 'Public\' -and $_ -ne 'Private\'}
            foreach ($Directory in $LinkDirectoriesWithSupportFiles) {
                $Dir = "$FullModulePath\$Directory"
                Add-Directory $Dir
            }
            Write-Verbose '[+] Linking files from Root Dir'
            Set-LinkedFiles -LinkFiles $LinkFilesRoot -FullModulePath $FullTemporaryPath -FullProjectPath $FullProjectPath
            Write-Verbose '[+] Linking files from Sub Dir'
            Set-LinkedFiles -LinkFiles $LinkPrivatePublicFiles -FullModulePath $FullTemporaryPath -FullProjectPath $FullProjectPath
            $FilesToLink = $LinkPrivatePublicFiles | Where-Object {$_ -notlike '*.ps1' -and $_ -notlike '*.psd1'}
            Set-LinkedFiles -LinkFiles $FilesToLink -FullModulePath $FullModulePath -FullProjectPath $FullProjectPath
            if (-not [string]::IsNullOrWhiteSpace($Configuration.Information.LibrariesCore)) {
                $StartsWithCore = "$($Configuration.Information.LibrariesCore)\"
                $FilesLibrariesCore = $LinkPrivatePublicFiles | Where-Object {($_).StartsWith($StartsWithCore)}
            }
            if (-not [string]::IsNullOrWhiteSpace($Configuration.Information.LibrariesDefault)) {
                $StartsWithDefault = "$($Configuration.Information.LibrariesDefault)\"
                $FilesLibrariesDefault = $LinkPrivatePublicFiles | Where-Object {($_).StartsWith($StartsWithDefault)}
            }
            Merge-Module -ModuleName $ProjectName -ModulePathSource $FullTemporaryPath -ModulePathTarget $FullModulePath -Sort $Configuration.Options.Merge.Sort -FunctionsToExport $Configuration.Information.Manifest.FunctionsToExport -AliasesToExport $Configuration.Information.Manifest.AliasesToExport -LibrariesCore $FilesLibrariesCore -LibrariesDefault $FilesLibrariesDefault -FormatCodePSM1 $Configuration.Options.Merge.FormatCodePSM1 -FormatCodePSD1 $Configuration.Options.Merge.FormatCodePSD1
        } else {
            foreach ($Directory in $LinkDirectories) {
                $Dir = "$FullModulePath\$Directory"
                Add-Directory $Dir
            }
            Write-Verbose '[+] Linking files from Root Dir'
            Set-LinkedFiles -LinkFiles $LinkFilesRoot -FullModulePath $FullModulePath -FullProjectPath $FullProjectPath
            Write-Verbose '[+] Linking files from Sub Dir'
            Set-LinkedFiles -LinkFiles $LinkPrivatePublicFiles -FullModulePath $FullModulePath -FullProjectPath $FullProjectPath
        }
    }
    if ($Configuration.Steps.PublishModule.Enabled) {
        if ($Configuration.Options.PowerShellGallery.FromFile) {
            $ApiKey = Get-Content -Path $Configuration.Options.PowerShellGallery.ApiKey
            New-PublishModule -ProjectName $Configuration.Information.ModuleName -ApiKey $ApiKey -RequireForce $Configuration.Steps.PublishModule.RequireForce
        } else {New-PublishModule -ProjectName $Configuration.Information.ModuleName -ApiKey $Configuration.Options.PowerShellGallery.ApiKey -RequireForce $Configuration.Steps.PublishModule.RequireForce}
    }
    Set-Location -Path $CurrentLocation
    if ($Configuration) {
        if ($Configuration.Options.ImportModules.RequiredModules) {foreach ($Module in $Configuration.Information.Manifest.RequiredModules) {Import-Module -Name $Module -Force -ErrorAction Stop -Verbose:$Configuration.Options.ImportModules.Verbose}}
        if ($Configuration.Options.ImportModules.Self) {Import-Module -Name $ProjectName -Force -ErrorAction Stop}
        if ($Configuration.Steps.BuildDocumentation) {
            $DocumentationPath = "$FullProjectPath\$($Configuration.Options.Documentation.Path)"
            $ReadMePath = "$FullProjectPath\$($Configuration.Options.Documentation.PathReadme)"
            Write-Verbose "Generating documentation to $DocumentationPath with $ReadMePath"
            if (-not (Test-Path -Path $DocumentationPath)) {$null = New-Item -Path "$FullProjectPath\Docs" -ItemType Directory -Force}
            $Files = Get-ChildItem -Path $DocumentationPath
            if ($Files.Count -gt 0) {$null = Update-MarkdownHelpModule $DocumentationPath -RefreshModulePage -ModulePagePath $ReadMePath -ErrorAction Stop} else {
                $null = New-MarkdownHelp -Module $ProjectName -WithModulePage -OutputFolder $DocumentationPath -ErrorAction Stop
                $null = Move-Item -Path "$DocumentationPath\$ProjectName.md" -Destination $ReadMePath
                if ($Configuration.Options.Documentation.UpdateWhenNew) {$null = Update-MarkdownHelpModule $DocumentationPath -RefreshModulePage -ModulePagePath $ReadMePath -ErrorAction Stop}
            }
        }
    }
}
function Test-ReparsePoint {
    [CmdletBinding()]
    param ([string]$path)
    $file = Get-Item $path -Force -ea SilentlyContinue
    return [bool]($file.Attributes -band [IO.FileAttributes]::ReparsePoint)
}
function Write-PowerShellHashtable {
    [cmdletbinding()]
    <#
    .Synopsis
        Takes an creates a script to recreate a hashtable
    .Description
        Allows you to take a hashtable and create a hashtable you would embed into a script.
 
        Handles nested hashtables and indents nested hashtables automatically.
    .Parameter inputObject
        The hashtable to turn into a script
    .Parameter scriptBlock
        Determines if a string or a scriptblock is returned
    .Example
        # Corrects the presentation of a PowerShell hashtable
        @{Foo='Bar';Baz='Bing';Boo=@{Bam='Blang'}} | Write-PowerShellHashtable
    .Outputs
        [string]
    .Outputs
        [ScriptBlock]
    .Link
        https://github.com/StartAutomating/Pipeworks
        about_hash_tables
    #>

    [OutputType([string], [ScriptBlock])]
    param([Parameter(Position = 0, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)]
        [PSObject]
        $InputObject,
        [Alias('ScriptBlock')]
        [switch]$AsScriptBlock,
        [Switch]$Sort)
    process {
        $callstack = @(foreach ($_ in (Get-PSCallStack)) {if ($_.Command -eq "Write-PowerShellHashtable") {$_}})
        $depth = $callStack.Count
        if ($inputObject -isnot [Hashtable]) {$newInputObject = @{PSTypeName = @($inputobject.pstypenames)[-1]}
            foreach ($prop in $inputObject.psobject.properties) {$newInputObject[$prop.Name] = $prop.Value}
            $inputObject = $newInputObject
        }
        if ($inputObject -is [Hashtable]) {
            $scriptString = ""
            $indent = $depth * 4
            $scriptString += "@{
"

            $items = $inputObject.GetEnumerator()
            if ($Sort) {$items = $items | Sort-Object Key}
            foreach ($kv in $items) {
                $scriptString += " " * $indent
                $keyString = "$($kv.Key)"
                if ($keyString.IndexOfAny(" _.#-+:;()'!?^@#$%&".ToCharArray()) -ne -1) {if ($keyString.IndexOf("'") -ne -1) {$scriptString += "'$($keyString.Replace("'","''"))'="} else {$scriptString += "'$keyString'="}} elseif ($keyString) {$scriptString += "$keyString="}
                $value = $kv.Value
                if ($value -is [string]) {$value = "'" + $value.Replace("'", "''").Replace("’", "’’").Replace("‘", "‘‘") + "'"} elseif ($value -is [ScriptBlock]) {$value = "{$value}"} elseif ($value -is [switch]) {$value = if ($value) {'$true'} else {'$false'}} elseif ($value -is [DateTime]) {$value = if ($value) {"[DateTime]'$($value.ToString("o"))'"}} elseif ($value -is [bool]) {$value = if ($value) {'$true'} else {'$false'}} elseif ($value -and $value.GetType -and ($value.GetType().IsArray -or $value -is [Collections.IList])) {
                    $value = foreach ($v in $value) {if ($v -is [Hashtable]) {Write-PowerShellHashtable $v} elseif ($v -is [Object] -and $v -isnot [string]) {Write-PowerShellHashtable $v} else {("'" + "$v".Replace("'", "''").Replace("’", "’’").Replace("‘", "‘‘") + "'")}}
                    $oldOfs = $ofs
                    $ofs = ",$(' ' * ($indent + 4))"
                    $value = "$value"
                    $ofs = $oldOfs
                } elseif ($value -as [Hashtable[]]) {
                    $value = foreach ($v in $value) {Write-PowerShellHashtable $v}
                    $value = $value -join ","
                } elseif ($value -is [Hashtable]) {$value = "$(Write-PowerShellHashtable $value)"} elseif ($value -as [Double]) {$value = "$value"} else {
                    $valueString = "'$value'"
                    if ($valueString[0] -eq "'" -and
                        $valueString[1] -eq "@" -and
                        $valueString[2] -eq "{") {$value = Write-PowerShellHashtable -InputObject $value} else {$value = $valueString}
                }
                $scriptString += "$value
"

            }
            $scriptString += " " * ($depth - 1) * 4
            $scriptString += "}"
            if ($AsScriptBlock) {[ScriptBlock]::Create($scriptString)} else {$scriptString}
        }
    }
}
Export-ModuleMember -Function @('New-PrepareModule') -Alias @()