functions/templating/Invoke-PSMDTemplate.ps1

function Invoke-PSMDTemplate {
    <#
    .SYNOPSIS
        Creates a project/file from a template.
     
    .DESCRIPTION
        This function takes a template and turns it into a finished file&folder structure.
        It does so by creating the files and folders stored within, replacing all parameters specified with values provided by the user.
         
        Missing parameters will be prompted for.
     
    .PARAMETER Template
        The template object to build from.
        Accepts objects returned by Get-PSMDTemplate.
     
    .PARAMETER TemplateName
        The name of the template to build from.
        Warning: This does wildcard interpretation, don't specify '*' unless you like answering parameter prompts.
     
    .PARAMETER Store
        The template store to retrieve tempaltes from.
        By default, all stores are queried.
     
    .PARAMETER Path
        Instead of a registered store, look in this path for templates.
     
    .PARAMETER OutPath
        The path in which to create the output.
        By default, it will create in the current directory.
     
    .PARAMETER Name
        The name of the produced output.
        Automatically inserted for any name parameter specified on creation.
        Also used for creating a root folder, when creating a project.
     
    .PARAMETER NoFolder
        Skip automatic folder creation for project templates.
        By default, this command will create a folder to place files&folders in when creating a project.
     
    .PARAMETER Encoding
        The encoding to apply to text files.
        The default setting for this can be configured by updating the 'PSFramework.Text.Encoding.DefaultWrite' configuration setting.
        The initial default value is utf8 with BOM.
     
    .PARAMETER Parameters
        A Hashtable containing parameters for use in creating the template.
     
    .PARAMETER Raw
        By default, all parameters will be replaced during invocation.
        In Raw mode, this is skipped, reproducing mostly the original template input (dynamic scriptblocks will now be named scriptblocks)).
     
    .PARAMETER GenerateObjects
        By default, Invoke-PSMDTemplate generates files.
        In GenerateObjects mode, no file but objects are created.
     
    .PARAMETER Force
        If the target path the template should be written to (filename or folder name within $OutPath), then overwrite it.
        By default, this function will fail if an overwrite is required.
     
    .PARAMETER Silent
        This places the function in unattended mode, causing it to error on anything requiring direct user input.
 
    .PARAMETER NoConfigFile
        By default, this command will look in the execution path and above for files named "PSMDConfig.psd1" to populate template parameters from.
     
    .PARAMETER EnableException
        Replaces user friendly yellow warnings with bloody red exceptions of doom!
        Use this if you want the function to throw terminating errors you want to catch.
     
    .PARAMETER Confirm
        If this switch is enabled, you will be prompted for confirmation before executing any operations that change state.
     
    .PARAMETER WhatIf
        If this switch is enabled, no actions are performed but informational messages will be displayed that explain what would happen if the command were to run.
     
    .EXAMPLE
        PS C:\> Invoke-PSMDTemplate -TemplateName "module"
         
        Creates a project based on the module template in the current folder, asking for all details.
     
    .EXAMPLE
        PS C:\> Invoke-PSMDTemplate -TemplateName "module" -Name "MyModule"
         
        Creates a project based on the module template with the name "MyModule"
 
    .EXAMPLE
        PS C:\> Invoke-PSMDTemplate MiniModule -Parameters @{ Author = 'Fred' }
 
        Creates a new project based on the template MiniModule and predefines the value for the "Author" placeholder.
#>

    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSPossibleIncorrectUsageOfAssignmentOperator", "")]
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSReviewUnusedParameter', '')]
    [OutputType([PSModuleDevelopment.Template.TemplateResult])]
    [Alias('imt')]
    [CmdletBinding(SupportsShouldProcess = $true)]
    param (
        [Parameter(Mandatory = $true, Position = 0, ParameterSetName = 'NameStore')]
        [Parameter(Mandatory = $true, Position = 0, ParameterSetName = 'NamePath')]
        [string]
        $TemplateName,
        
        [Parameter(Mandatory = $true, ValueFromPipeline = $true, ParameterSetName = 'Template')]
        [PSModuleDevelopment.Template.TemplateInfo[]]
        $Template,
        
        [Parameter(ParameterSetName = 'NameStore')]
        [string]
        $Store = "*",
        
        [Parameter(Mandatory = $true, ParameterSetName = 'NamePath')]
        [string]
        $Path,
        
        [Parameter(Position = 2)]
        [PSFramework.Validation.PsfValidateScript('PSFramework.Validate.FSPath.Folder', ErrorString = 'PSFramework.Validate.FSPath.Folder')]
        [string]
        $OutPath = (Get-PSFConfigValue -FullName 'PSModuleDevelopment.Template.OutPath' -Fallback "."),
        
        [Parameter(Position = 1)]
        [string]
        $Name,
        
        [PSFEncoding]
        $Encoding = (Get-PSFConfigValue -FullName 'PSFramework.Text.Encoding.DefaultWrite'),
        
        [switch]
        $NoFolder,
        
        [hashtable]
        $Parameters = @{ },
        
        [switch]
        $Raw,
        
        [switch]
        $GenerateObjects,
        
        [switch]
        $Force,
        
        [switch]
        $Silent,

        [switch]
        $NoConfigFile,
        
        [switch]
        $EnableException
    )
    
    begin {
        $resolvedPath = Resolve-PSFPath -Path $OutPath

        $templates = @()
        switch ($PSCmdlet.ParameterSetName) {
            'NameStore' { $templates = Get-PSMDTemplate -TemplateName $TemplateName -Store $Store }
            'NamePath' { $templates = Get-PSMDTemplate -TemplateName $TemplateName -Path $Path }
        }
        if ($TemplateName -and -not $templates) {
            Stop-PSFFunction -String 'Invoke-PSMDTemplate.Template.NotFound' -StringValues $TemplateName -EnableException $EnableException -Cmdlet $PSCmdlet
            return
        }
        
        #region Parameter Processing
        if (-not $Parameters) { $Parameters = @{ } }
        if ($Name) { $Parameters["Name"] = $Name }
        
        if ($NoConfigFile) { $paramCloned = Resolve-TemplateParameter -Configuration $Parameters -FromConfiguration}
        else { $paramCloned = Resolve-TemplateParameter -Path $resolvedPath -Configuration $Parameters -FromConfiguration }
        #endregion Parameter Processing
        
        #region Helper function
        function Invoke-Template {
            [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "")]
            [CmdletBinding()]
            param (
                [PSModuleDevelopment.Template.TemplateInfo]
                $Template,
                
                [string]
                $OutPath,
                
                [PSFEncoding]
                $Encoding,
                
                [bool]
                $NoFolder,
                
                [hashtable]
                $Parameters,
                
                [bool]
                $Raw,
                
                [switch]
                $GenerateObjects,
        
                [bool]
                $Silent
            )
            Write-PSFMessage -Level Verbose -Message "Processing template $($item)" -Tag 'template', 'invoke' -FunctionName Invoke-PSMDTemplate
            
            $templateData = Import-PSFClixml -Path $Template.Path -ErrorAction Stop
            #region Process Parameters
            foreach ($parameter in $templateData.Parameters) {
                if (-not $parameter) { continue }
                if (-not $Parameters.ContainsKey($parameter)) {
                    if ($Silent) { throw "Parameter not specified: $parameter" }
                    try {
                        $value = Read-Host -Prompt "Enter value for parameter '$parameter'" -ErrorAction Stop
                        $Parameters[$parameter] = $value
                    }
                    catch { throw }
                }
            }
            #endregion Process Parameters
            
            #region Scripts
            $scriptParameters = @{ }
            
            if (-not $Raw) {
                foreach ($scriptParam in $templateData.Scripts.Values) {
                    if (-not $scriptParam) { continue }
                    try { $scriptParameters[$scriptParam.Name] = "$([scriptblock]::Create($scriptParam.StringScript).Invoke())" }
                    catch {
                        if ($Silent) { throw (New-Object System.Exception("Scriptblock $($scriptParam.Name) failed during execution: $_", $_.Exception)) }
                        else {
                            Write-PSFMessage -Level Warning -Message "Scriptblock $($scriptParam.Name) failed during execution. Please specify a custom value or use CTRL+C to terminate creation" -ErrorRecord $_ -FunctionName "Invoke-PSMDTemplate" -ModuleName 'PSModuleDevelopment'
                            $scriptParameters[$scriptParam.Name] = Read-Host -Prompt "Value for script $($scriptParam.Name)"
                        }
                    }
                }
            }
            #endregion Scripts
            $createdTemplateItems = switch ($templateData.Type.ToString()) {
                #region File
                "File" {
                    foreach ($child in $templateData.Children) {
                        New-TemplateItem -Item $child -Path $OutPath -ParameterFlat $Parameters -ParameterScript $scriptParameters -Raw $Raw
                    }
                    if ($Raw -and $templateData.Scripts.Values) {
                        $templateData.Scripts.Values | Export-Clixml -Path (Join-Path $OutPath "_PSMD_ParameterScripts.xml")
                    }
                }
                #endregion File
                
                #region Project
                "Project" {
                    #region Resolve output folder
                    if (-not $NoFolder) {
                        if ($Parameters["Name"]) {
                            $projectName = $Parameters["Name"]
                            $projectFullName = Join-Path $OutPath $projectName
                            if ((Test-Path $projectFullName) -and (-not $Force)) {
                                throw "Project root folder already exists: $projectFullName"
                            }
                            $newFolder = New-Item -Path $OutPath -Name $Parameters["Name"] -ItemType Directory -ErrorAction Stop -Force
                        }
                        else {
                            throw "Parameter Name is needed to create a project without setting the -NoFolder parameter!"
                        }
                    }
                    else { $newFolder = Get-Item $OutPath }
                    #endregion Resolve output folder
                    
                    foreach ($child in $templateData.Children) {
                        New-TemplateItem -Item $child -Path $newFolder.FullName -ParameterFlat $Parameters -ParameterScript $scriptParameters -Raw $Raw
                    }
                    
                    #region Write Config File (Raw)
                    if ($Raw) {
                        $guid = [System.Guid]::NewGuid().ToString()
                        $optionsTemplate = @"
@{
    TemplateName = "$($Template.Name)"
    Version = ([Version]"$($Template.Version)")
    Tags = $(($Template.Tags | ForEach-Object { "'$_'" }) -join ",")
    Author = "$($Template.Author)"
    Description = "$($Template.Description)"
þþþPLACEHOLDER-$($guid)þþþ
}
"@

                        if ($params = $templateData.Scripts.Values) {
                            $list = @()
                            foreach ($param in $params) {
                                $list += @"
    $($param.Name) = {
        $($param.StringScript)
    }
"@

                            }
                            $optionsTemplate = $optionsTemplate -replace "þþþPLACEHOLDER-$($guid)þþþ", ($list -join "`n`n")
                        }
                        else {
                            $optionsTemplate = $optionsTemplate -replace "þþþPLACEHOLDER-$($guid)þþþ", ""
                        }
                        
                        [PSModuleDevelopment.Template.TemplateResult]@{
                            Name     = "PSMDTemplate.ps1"
                            Path     = $newFolder.FullName
                            FullPath = (Join-Path $newFolder.FullName "PSMDTemplate.ps1")
                            Content  = $optionsTemplate
                        }
                    }
                    #endregion Write Config File (Raw)
                }
                #endregion Project
            }
            If ($GenerateObjects) {
                return $createdTemplateItems
            }
            Write-TemplateResult -TemplateResult $createdTemplateItems -Encoding $Encoding
        }
        
        function New-TemplateItem {
            [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "")]
            [OutputType([PSModuleDevelopment.Template.TemplateResult])]
            [CmdletBinding()]
            param (
                [PSModuleDevelopment.Template.TemplateItemBase]
                $Item,
                
                [string]
                $Path,
                
                [hashtable]
                $ParameterFlat,
                
                [hashtable]
                $ParameterScript,
                
                [bool]
                $Raw
            )
            Write-PSFMessage -Level Verbose -Message "Creating Template-Item: $($Item.Name) ($($Item.RelativePath))" -FunctionName Invoke-PSMDTemplate -ModuleName PSModuleDevelopment -Tag 'create', 'template'
            
            $identifier = $Item.Identifier
            $isFile = $Item.GetType().Name -eq 'TemplateItemFile'
            
            #region File
            if ($isFile) {
                $fileName = $Item.Name
                if (-not $Raw) {
                    foreach ($param in $Item.FileSystemParameterFlat) {
                        $fileName = [PSModuleDevelopment.Utility.UtilityHost]::Replace($fileName, "$($identifier)$($param)$($identifier)", $ParameterFlat[$param], $false)
                    }
                    foreach ($param in $Item.FileSystemParameterScript) {
                        $fileName = [PSModuleDevelopment.Utility.UtilityHost]::Replace($fileName, "$($identifier)$($param)$($identifier)", $ParameterScript[$param], $false)
                    }
                }
                $destPath = Join-Path $Path $fileName
                
                if ($Item.PlainText) {
                    $text = $Item.Value
                    if (-not $Raw) {
                        foreach ($param in $Item.ContentParameterFlat) {
                            $text = [PSModuleDevelopment.Utility.UtilityHost]::Replace($text, "$($identifier)$($param)$($identifier)", $ParameterFlat[$param], $false)
                        }
                        foreach ($param in $Item.ContentParameterScript) {
                            $text = [PSModuleDevelopment.Utility.UtilityHost]::Replace($text, "$($identifier)!$($param)!$($identifier)", $ParameterScript[$param], $false)
                        }
                    }
                    return [PSModuleDevelopment.Template.TemplateResult]@{
                        Name     = $fileName
                        Path     = $Path
                        FullPath = $destPath
                        Content  = $text
                    }
                }
                else {
                    $bytes = [System.Convert]::FromBase64String($Item.Value)
                    return [PSModuleDevelopment.Template.TemplateResult]@{
                        Name     = $fileName
                        Path     = $Path
                        FullPath = $destPath
                        Content  = $bytes
                        IsText   = $false
                    }
                }
            }
            #endregion File
            
            #region Folder
            else {
                $folderName = $Item.Name
                if (-not $Raw) {
                    foreach ($param in $Item.FileSystemParameterFlat) {
                        $folderName = $folderName -replace "$($identifier)$([regex]::Escape($param))$($identifier)", $ParameterFlat[$param]
                    }
                    foreach ($param in $Item.FileSystemParameterScript) {
                        $folderName = $folderName -replace "$($identifier)!$([regex]::Escape($param))!$($identifier)", $ParameterScript[$param]
                    }
                }
                $folder = Join-Path -Path $Path -ChildPath $folderName

                # Return a folder object to make sure empty folders are not excluded
                [PSModuleDevelopment.Template.TemplateResult]@{
                    Name     = $folderName
                    Path     = $Path
                    FullPath = $folder
                    IsFolder = $true
                    IsText   = $false
                }
                
                foreach ($child in $Item.Children) {
                    New-TemplateItem -Item $child -Path $folder -ParameterFlat $ParameterFlat -ParameterScript $ParameterScript -Raw $Raw
                }
            }
            #endregion Folder
        }
        
        function Write-TemplateResult {
            [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "")]
            [CmdletBinding()]
            param (
                [PSModuleDevelopment.Template.TemplateResult[]]
                $TemplateResult,
                
                [PSFEncoding]
                $Encoding
            )
            $msgParam = @{ Level = 'Verbose'; FunctionName = 'Invoke-PSMDTemplate' }
            foreach ($item in $TemplateResult | Sort-Object { $_.FullPath.Length }) {
                Write-PSFMessage @msgParam -Message "Creating file: $($item.FullPath)" -Tag 'create', 'template'
                if (-not (Test-Path $item.Path)) {
                    Write-PSFMessage -Level Verbose -Message "Creating Folder $($item.Path)"
                    $null = New-Item -Path $item.Path -ItemType Directory
                }
                if ($item.IsFolder) {
                    if (-not (Test-Path $item.FullPath)) {
                        Write-PSFMessage  @msgParam -Message "Creating Folder $($item.FullPath)"
                        $null = New-Item -Path $item.FullPath -ItemType Directory
                    }
                    continue
                }
                if ($item.IsText) {
                    Write-PSFMessage @msgParam -Message "Creating as a Text-File"
                    [System.IO.File]::WriteAllText($item.FullPath, $item.Content, $Encoding)
                }
                else {
                    Write-PSFMessage @msgParam -Message "Creating as a Binary-File"
                    [System.IO.File]::WriteAllBytes($item.FullPath, $item.Content)
                }
            }
        }
        #endregion Helper function
    }
    process {
        if (Test-PSFFunctionInterrupt) { return }
        
        $invokeParam = @{
            Parameters      = $paramCloned
            OutPath         = $resolvedPath
            NoFolder        = $NoFolder
            Encoding        = $Encoding
            Raw             = $Raw
            Silent          = $Silent
            GenerateObjects = $GenerateObjects
        }
        
        foreach ($item in $Template) {
            Invoke-PSFProtectedCommand -ActionString 'Invoke-PSMDTemplate.Invoking' -ActionStringValues $item -Target $item -ScriptBlock {
                Invoke-Template @invokeParam -Template $item
            } -EnableException $EnableException -PSCmdlet $PSCmdlet -Continue
        }
        foreach ($item in $templates) {
            Invoke-PSFProtectedCommand -ActionString 'Invoke-PSMDTemplate.Invoking' -ActionStringValues $item -Target $item -ScriptBlock {
                Invoke-Template @invokeParam -Template $item
            } -EnableException $EnableException -PSCmdlet $PSCmdlet -Continue
        }
    }
}