Public/New-ModuleCustomTemplate.ps1

<#
    .SYNOPSIS
    Generates the scaffolding for new tcs-style PowerShell modules.

    .DESCRIPTION
    For each name, creates <Path>/<Name> containing:

    - Classes, Public, Private, Public/Tests, Public/Templates and Private/Tests folders, each with a README.md.

    - <Name>.psm1 that dot-sources Private and Public functions, exports only the Public ones and runs the
      tcs.core configuration, telemetry and daily update check without writing anything to the pipeline.

    - <Name>.psd1 manifest (version 0.1.0, PowerShell 5.1+, Desktop and Core) that requires tcs.core 0.3.0
      or later plus any RequiredModules given. FunctionsToExport is an empty list: add each public function
      to it as you create it (wildcards are not used).

    - Config.ps1, an optional loader for module variables stored in the user's settings folder. It never
      writes into the module folder. Use -UncommentConfig to enable it in the psm1.

    - README.md and .gitignore.

    Existing files are left alone unless -Force is used.

    .PARAMETER Names
    The names of the modules you want to create. Accepts pipeline input.

    .PARAMETER Path
    The folder to create the modules in. Defaults to the current location.

    .PARAMETER Author
    Name of the module author, used in the Author and Copyright fields of the manifest. Defaults to the
    current user name.

    .PARAMETER CompanyName
    Value for the CompanyName field of the manifest. Defaults to an empty string.

    .PARAMETER Description
    Description of the module, used in the manifest and at the top of the module README.md.

    .PARAMETER RequiredModules
    Names of additional modules this module depends on. tcs.core (0.3.0 or later) is always added because
    the generated psm1 uses it.

    .PARAMETER UncommentConfig
    Enables the line in the psm1 that dot-sources Config.ps1, which loads module variables from Config.psd1
    in the user's settings folder for the module.

    .PARAMETER Force
    Overwrite files that already exist in the module folder.

    .EXAMPLE
    $Params = @{
        Names = 'Module1', 'Module2'
        Path = './modules'
        Author = 'Me Myself & I'
        CompanyName = 'My Awesome Company'
        Description = 'This module will rock your world!'
        RequiredModules = 'Microsoft.Graph.Authentication'
    }
    New-ModuleCustomTemplate @Params

    Creates scaffolding for two modules under ./modules with the given metadata.

    .INPUTS
    System.String[]. Module names (Names) can be passed by pipeline.

    .OUTPUTS
    None. Creates folders and files for each module.

    .NOTES
    Author: TheCodeSaiyan
#>

function New-ModuleCustomTemplate {
    [CmdletBinding(SupportsShouldProcess = $true)]
    [OutputType([void])]
    param (
        [Parameter(Mandatory = $true, ValueFromPipeline = $true)]
        [ValidateNotNullOrEmpty()]
        [string[]]$Names,

        [ValidateNotNullOrEmpty()]
        [string]$Path = $PWD.Path,

        [string]$Author = $(if ($env:USER) { $env:USER } else { $env:USERNAME }),

        [string]$CompanyName = '',

        [string]$Description = 'Module Description',

        [string[]]$RequiredModules = @(),

        [switch]$UncommentConfig,

        [switch]$Force
    )

    begin {
        $TelemetryArgs = @{
            ModuleName    = $MyInvocation.MyCommand.Module.Name
            ModuleVersion = [string]$MyInvocation.MyCommand.Module.Version
            CommandName   = $MyInvocation.MyCommand.Name
            ExecutionID   = [guid]::NewGuid().ToString()
        }
        Invoke-TelemetryCollection @TelemetryArgs -Stage Start -ClearTimer

        $Templates = Join-Path -Path (Split-Path -Path $PSScriptRoot -Parent) -ChildPath 'Templates'
        $ModuleTemplate = Join-Path -Path $Templates -ChildPath 'Module.psm1'
        $ConfigTemplate = Join-Path -Path $Templates -ChildPath 'Config.ps1'
        $GitIgnoreTemplate = Join-Path -Path $Templates -ChildPath 'GitIgnore'
        $ManifestTemplate = Join-Path -Path $Templates -ChildPath 'Module.psd1'

        # tcs.core is always required; add any other modules after it
        $TcsCoreRequirement = "@{ ModuleName = 'tcs.core'; ModuleVersion = '0.3.0' }"
        $RequiredModuleLines = @($TcsCoreRequirement)
        foreach ($RequiredModule in @($RequiredModules | Where-Object { $_ -and $_ -ne 'tcs.core' })) {
            $RequiredModuleLines += "'" + $RequiredModule.Replace("'", "''") + "'"
        }
        $RequiredModulesText = $RequiredModuleLines -join ",`n "

        $Failures = 0
    }
    process {
        foreach ($Name in $Names) {
            $ParentPath = Join-Path -Path $Path -ChildPath $Name
            if (-not $PSCmdlet.ShouldProcess($ParentPath, 'Create module scaffolding')) {
                continue
            }
            try {
                $Directories = @(
                    'Classes'
                    'Public'
                    'Private'
                    (Join-Path -Path 'Public' -ChildPath 'Tests')
                    (Join-Path -Path 'Public' -ChildPath 'Templates')
                    (Join-Path -Path 'Private' -ChildPath 'Tests')
                )
                if (-not (Test-Path -Path $ParentPath)) {
                    Write-Verbose "Creating module path: $ParentPath."
                    $null = New-Item -Path $ParentPath -ItemType Directory -ErrorAction Stop
                }
                foreach ($Directory in $Directories) {
                    $FullPath = Join-Path -Path $ParentPath -ChildPath $Directory
                    if (-not (Test-Path -Path $FullPath)) {
                        Write-Verbose "Creating path: $FullPath."
                        $null = New-Item -Path $FullPath -ItemType Directory -ErrorAction Stop
                    }
                    $ReadmePath = Join-Path -Path $FullPath -ChildPath 'README.md'
                    if ($Force -or -not (Test-Path -Path $ReadmePath)) {
                        Set-Content -Path $ReadmePath -Value "# $Name $($Directory -replace '\\', '/')" -Encoding UTF8 -ErrorAction Stop
                    }
                }

                $Files = @{
                    'README.md'  = "# $Name PowerShell Module`n`n*$Description*`n"
                    'Config.ps1' = Get-Content -Path $ConfigTemplate -Raw -ErrorAction Stop
                    '.gitignore' = Get-Content -Path $GitIgnoreTemplate -Raw -ErrorAction Stop
                }
                $ModuleContent = (Get-Content -Path $ModuleTemplate -Raw -ErrorAction Stop).Replace('###TEMPLATE_MODULE_NAME', $Name)
                if ($UncommentConfig) {
                    $ModuleContent = $ModuleContent -replace '(?m)^#\.\s', '. '
                }
                $Files["$Name.psm1"] = $ModuleContent

                foreach ($FileName in $Files.Keys) {
                    $FilePath = Join-Path -Path $ParentPath -ChildPath $FileName
                    if ((Test-Path -Path $FilePath) -and -not $Force) {
                        Write-Warning "'$FilePath' already exists and was not changed. Use -Force to overwrite it."
                        continue
                    }
                    Set-Content -Path $FilePath -Value $Files[$FileName] -NoNewline -Encoding UTF8 -Force -ErrorAction Stop
                    Write-Verbose "Created $FilePath."
                }

                $ManifestPath = Join-Path -Path $ParentPath -ChildPath "$Name.psd1"
                if ((Test-Path -Path $ManifestPath) -and -not $Force) {
                    Write-Warning "'$ManifestPath' already exists and was not changed. Use -Force to overwrite it."
                }
                else {
                    $ManifestValues = [ordered]@{
                        '###TEMPLATE_MODULE_NAME'  = $Name
                        '###TEMPLATE_GUID'         = [guid]::NewGuid().ToString()
                        '###TEMPLATE_AUTHOR'       = $Author
                        '###TEMPLATE_COMPANY_NAME' = $CompanyName
                        '###TEMPLATE_COPYRIGHT'    = "(c) $(Get-Date -UFormat %Y) $Author. All rights reserved."
                        '###TEMPLATE_DESCRIPTION'  = $Description
                    }
                    $ManifestContent = Get-Content -Path $ManifestTemplate -Raw -ErrorAction Stop
                    foreach ($Token in $ManifestValues.Keys) {
                        # Values are placed inside single-quoted strings, so escape single quotes
                        $ManifestContent = $ManifestContent.Replace($Token, ([string]$ManifestValues[$Token]).Replace("'", "''"))
                    }
                    $ManifestContent = $ManifestContent.Replace($TcsCoreRequirement, $RequiredModulesText)
                    Set-Content -Path $ManifestPath -Value $ManifestContent -NoNewline -Encoding UTF8 -Force -ErrorAction Stop
                    Write-Verbose "Created module manifest $ManifestPath."
                }
            }
            catch {
                $Failures++
                Write-Error "Failed to create the module scaffolding for '$Name' at '$ParentPath': $($_.Exception.Message)"
            }
        }
    }
    end {
        Invoke-TelemetryCollection @TelemetryArgs -Stage End -Failed ($Failures -gt 0)
    }
}