PSPackageProject.psm1

# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License.

#region Private implementation functions

function GetPowerShellName {
    switch ($PSVersionTable.PSEdition) {
        'Core' {
            return 'PowerShell Core'
        }

        default {
            return 'Windows PowerShell'
        }
    }
}

function Test-ConfigFile {
    param(
        [Parameter(Mandatory)]
        [string] $Path
    )

    if ($Path.EndsWith('pspackageproject.json') -and (Test-Path $Path -PathType Leaf)) {
        return $true
    }
}

function SearchConfigFile {
    param(
        [Parameter(Mandatory)]
        [string] $Path
    )

    $startPath = $Path

    do {
        $configPath = Join-Path $startPath 'pspackageproject.json'

        if (-not (Test-Path $configPath)) {
            $startPath = Split-Path $startPath
        }
        else {
            return $configPath
        }
    } while ($newPath -ne '')
}

function Join-Path2 {
    param(
        [Parameter(Mandatory)]
        [string[]] $Path,

        [Parameter(Mandatory)]
        [string] $ChildPath,

        [string[]] $AdditionalChildPath
    )

    $paths = [System.Collections.ArrayList]::new()

    $Path | ForEach-Object { $null = $paths.Add($_) }
    $null = $paths.Add($ChildPath)
    $AdditionalChildPath | ForEach-Object { $null = $paths.Add($_) }

    [System.IO.Path]::Combine($paths)
}

function RunPwshCommandInSubprocess {
    param(
        [string]
        $Command
    )

    if (-not $script:pwshPath) {
        $script:pwshPath = (Get-Process -Id $PID).Path
    }

    & $script:pwshPath -NoProfile -NoLogo -Command $Command
}

function RunProjectBuild {
    param(
        [string]
        $ProjectRoot
    )

    & "$ProjectRoot/build.ps1" -Clean -Build
}

function GetHelpPath {
    param(
        [cultureinfo]
        $Culture,

        [string]
        $ProjectRoot
    )

    $ProjectRoot = Resolve-Path -Path $ProjectRoot

    $config = Get-PSPackageProjectConfiguration -ConfigPath $ProjectRoot

    $cultureName = $Culture.Name

    return (Join-Path2 -Path $ProjectRoot -ChildPath $config.HelpPath -AdditionalChildPath $cultureName)
}

function GetOutputModulePath {
    $config = Get-PSPackageProjectConfiguration
    return (Join-Path $config.BuildOutputPath $config.ModuleName)
}

function HasCmdletHelp {
    param(
        [string]
        $HelpResourcePath
    )

    if (-not (Test-Path -Path $HelpResourcePath)) {
        return $false
    }

    $files = Get-ChildItem -Path $HelpResourcePath -ErrorAction Ignore |
    Where-Object Name -Like '*.md' |
    Where-Object Name -NotLike 'about_*'

    return $files.Count -gt 0
}

function Initialize-CIYml {
    param(
        [Parameter(Mandatory)]
        [string] $Path
    )

    $boilerplateCIYml = Join-Path2 -Path $PSScriptRoot -ChildPath 'yml' -AdditionalChildPath 'ci_for_init.yml'
    $destYmlPath = New-Item (Join-Path -Path $Path -ChildPath '.ci') -ItemType Directory
    Copy-Item $boilerplateCIYml -Destination (Join-Path $destYmlPath -ChildPath 'ci.yml') -Force

    $boilerplateTestYml = Join-Path2 -Path $PSScriptRoot -ChildPath 'yml' -AdditionalChildPath 'test_for_init.yml'
    Copy-Item $boilerplateTestYml -Destination (Join-Path $destYmlPath -ChildPath 'test.yml') -Force

    $boilerplateReleaseYml = Join-Path2 -Path $PSScriptRoot -ChildPath 'yml' -AdditionalChildPath 'release_for_init.yml'
    Copy-Item $boilerplateReleaseYml -Destination (Join-Path $destYmlPath -ChildPath 'release.yml') -Force
}

function Test-PSPesterResult
{
    [CmdletBinding()]
    param(
        [Parameter()]
        [string] $TestResultsFile = "pester-tests.xml"
    )

    if (!(Test-Path $TestResultsFile)) {
        throw "Test result file '$testResultsFile' not found for $TestArea."
    }

    $x = [xml](Get-Content -raw $testResultsFile)
    if ([int]$x.'test-results'.failures -gt 0) {
        Write-Error "TEST FAILURES"
        # switch between methods, SelectNode is not available on dotnet core
        if ( "System.Xml.XmlDocumentXPathExtensions" -as [Type] ) {
            $failures = [System.Xml.XmlDocumentXPathExtensions]::SelectNodes($x."test-results", './/test-case[@result = "Failure"]')
        }
        else {
            $failures = $x.SelectNodes('.//test-case[@result = "Failure"]')
        }
        foreach ( $testfail in $failures ) {
            Show-PSPesterError -testFailure $testfail
        }
    }
}

function Show-PSPesterError {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory)]
        [Xml.XmlElement]$testFailure
    )


    $description = $testFailure.description
    $name = $testFailure.name
    $message = $testFailure.failure.message
    $stackTrace = $testFailure.failure."stack-trace"

    $fullMsg = "`n{0}`n{1}`n{2}`n{3}`{4}" -f ("Description: " + $description), ("Name: " + $name), "message:", $message, "stack-trace:", $stackTrace

    Write-Error $fullMsg
}

function Invoke-FunctionalValidation {
    param ( $tags = "CI" )
    $config = Get-PSPackageProjectConfiguration
    try {
        $testResultFile = "result.pester.xml"
        $testPath = $config.TestPath
        $modStage = "{0}/{1}" -f $config.BuildOutputPath,$config.ModuleName
        $command = "import-module ${modStage} -Force -Verbose; Set-Location $testPath; Invoke-Pester -Path . -OutputFile ${testResultFile} -tags '$tags'"
        $output = RunPwshCommandInSubprocess -command $command | Foreach-Object { Write-Verbose -Verbose -Message $_ }
        return (Join-Path ${testPath} "$testResultFile")
    }
    catch {
        $output | Foreach-Object { Write-Warning "$_" }
        Write-Error "Error invoking tests"
    }
}

function Invoke-StaticValidation {

    $config = Get-PSPackageProjectConfiguration

    Write-Verbose -Message "Running ScriptAnalyzer" -Verbose
    $resultPSSA = RunScriptAnalysis -Location $config.BuildOutputPath

    Write-Verbose -Verbose -Message "PSSA result file: $resultPSSA"

    Test-PSPesterResult -TestResultsFile $resultPSSA

    Write-Verbose -Message "Running BinSkim" -Verbose
    $resultBinSkim = Invoke-BinSkim -Location (Join-Path2 -Path $config.BuildOutputPath -ChildPath $config.ModuleName)

    Test-PSPesterResult -TestResultsFile $resultBinSkim
}

function RunScriptAnalysis {
    try {
        Push-Location

        $pssaParams = @{
            Severity = 'Warning', 'ParseError'
            Path     = GetOutputModulePath
            Recurse  = $true
        }

        $results = Invoke-ScriptAnalyzer @pssaParams
        $xmlPath = ConvertPssaDiagnosticsToNUnit -Diagnostic $results
        # send back the xml file path.
        $xmlPath
        if ($env:TF_BUILD) {
            $powershellName = GetPowerShellName
            Publish-AzDevOpsTestResult -Path $xmlPath -Title "PSScriptAnalyzer $env:AGENT_OS - $powershellName Results" -Type NUnit
        }
    }
    finally {
        Pop-Location
    }
}

function ConvertPssaDiagnosticsToNUnit {
    param(
        [Parameter(ValueFromPipeline)]
        [Microsoft.Windows.PowerShell.ScriptAnalyzer.Generic.DiagnosticRecord[]]
        $Diagnostic
    )

    $sb = [System.Text.StringBuilder]::new()
    $null = $sb.Append("Describe 'PSScriptAnalyzer Diagnostics' { `n")
    foreach ($d in $Diagnostic) {
        $ruleName = $d.RuleName
        $message = $d.Message -replace "'", "``"
        $null = $sb.Append("It '$ruleName' { `nthrow '$message' }`n")
    }
    if ($null -eq $Diagnostic) {
        $null = $sb.Append('It "no failures found" { $true | Should -Be $true }')
    }
    $null = $sb.Append('}')

    $testPath = Join-Path ([System.IO.Path]::GetTempPath()) "pssa.tests.ps1"
    $xmlPath = Join-Path ([System.IO.Path]::GetTempPath()) "pssa.xml"

    try {
        Set-Content -Path $testPath -Value $sb.ToString()
        Invoke-Pester -Script $testPath -OutputFormat NUnitXml -OutputFile $xmlPath
    }
    finally {
        Remove-Item -Path $testPath -Force
    }

    return $xmlPath
}

<#
.SYNOPSIS
Generates help file stubs.
 
.DESCRIPTION
Generates stubs for about_*.md help documentation for a given module.
 
.PARAMETER ProjectRoot
The repository root directory path.
 
.PARAMETER ModuleName
The name of the module to generate help for.
 
.PARAMETER Culture
The culture or locale the help is to be generated in/for.
#>

function Initialize-PSPackageProjectHelp {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [string]
        $ProjectRoot,

        [Parameter(Mandatory)]
        [string]
        $ModuleName,

        [Parameter()]
        [cultureinfo]
        $Culture = [cultureinfo]::CurrentCulture
    )

    $ProjectRoot = Resolve-Path -Path $ProjectRoot

    $helpResourcePath = GetHelpPath -ProjectRoot $ProjectRoot -Culture $Culture

    $null = New-Item -Path $helpResourcePath -ItemType Directory -ErrorAction Stop

    New-MarkdownAboutHelp -OutputFolder $helpResourcePath -AboutName $ModuleName
}

#endregion Private implementation functions

#region Public commands

function Invoke-PSPackageProjectTest {
    param(
        [Parameter()]
        [ValidateSet("Functional", "StaticAnalysis")]
        [string[]]
        $Type
    )

    END {
        if ($Type -contains "Functional" ) {
            # this will return a path to the results
            $resultFile = Invoke-FunctionalValidation
            Test-PSPesterResult -TestResultsFile $resultFile
            $powershellName = GetPowerShellName
            Publish-AzDevOpsTestResult -Path $resultFile -Title "Functional Tests - $env:AGENT_OS - $powershellName Results" -Type NUnit
        }

        if ($Type -contains "StaticAnalysis" ) {
            Invoke-StaticValidation
        }
    }
}

function Invoke-BinSkim {
    [CmdletBinding(DefaultParameterSetName = 'byPath')]
    param(
        [Parameter(ParameterSetName = 'byPath', Mandatory)]
        [string]
        $Location,
        [Parameter(ParameterSetName = 'byPath')]
        [string]
        $Filter = '*'
    )

    $testscript = @'
Describe "BinSkim" {
    BeforeAll{
        $outputPath = Join-Path -Path ([System.io.path]::GetTempPath()) -ChildPath 'pspackageproject-results.json'
        if(Test-Path $outputPath)
        {
            $results = Get-Content $outputPath | ConvertFrom-Json
        }
    }
 
    foreach($file in $results.runs.files.PsObject.Properties.Name)
    {
        foreach($rule in $results.runs.rules.psobject.properties.name)
        {
            $fileResults = @($results.runs.results |
                Where-Object {
                    Write-Verbose "$($_.ruleId) -eq $rule"
                    $_.locations.analysisTarget.uri -eq $File -and $_.ruleId -eq $rule})
 
            $message = $null
            if($fileResults.Count -ne 0) {
                $fileResult = $fileResults[0]
                $message = $results.runs.rules.$rule.messageFormats.($fileResult.Level) -f ($fileResult.formattedRuleMessage.arguments)
            }
 
            if($message){
                it "$file should not have errors for " {
                    throw $message
                }
            }
        }
    }
}
'@

    $eligbleFiles = @(Get-ChildItem -Path $Location -Filter $Filter -Recurse -File | Where-Object { $_.Extension -in '.exe','.dll','','.so','.dylib'})
    if($eligbleFiles.Count -ne 0)
    {
        $rpkgs = "Register-PackageSource"
        $sourceName = 'Nuget'
        & $rpkgs -ProviderName NuGet -Name $sourceName -Location https://api.nuget.org/v3/index.json -erroraction ignore
        $packageName = 'microsoft.codeanalysis.binskim'
        $packageLocation = Join-Path2 -Path ([System.io.path]::GetTempPath()) -ChildPath 'pspackageproject-packages'
        if ($IsLinux) {
            $binaryName = 'BinSkim'
            $rid = 'linux-x64'
        }
        elseif ($IsWindows -ne $false) {
            $binaryName = 'BinSkim.exe'
            if ([Environment]::Is64BitOperatingSystem) {
                $rid = 'win-x64'
            }
            else {
                $rid = 'win-x86'
            }
        }
        else {
            Write-Warning "unsupported platform"
            return
        }

        Write-Verbose -Message "Finding binskim..." -Verbose
        $fpkg = "Find-Package"
        $packageInfo = & $fpkg -Name $packageName -Source $sourceName
        $dirName = $packageInfo.Name + '.' + $packageInfo.Version
        $toolLocation = Join-Path2 -Path $packageLocation -ChildPath $dirName -AdditionalChildPath 'tools', 'netcoreapp2.0', $rid, $binaryName
        if (!(test-path -path $toolLocation)) {
            Write-Verbose -Message "Installing binskim..." -Verbose
            $ipkg = "Install-Package"
            $packageInfo | & $ipkg -Destination $packageLocation -Force
        }

        if ($IsLinux) {
            chmod a+x $toolLocation
        }

        $resolvedPath = (Resolve-Path -Path $Location).ProviderPath
        $toAnalyze = Join-Path2 -Path $resolvedPath -ChildPath $Filter

        $outputPath = Join-Path2 -Path ([System.io.path]::GetTempPath()) -ChildPath 'pspackageproject-results.json'
        Write-Verbose -Message "Running binskim..." -Verbose
        & $toolLocation analyze $toAnalyze --output $outputPath --pretty-print --recurse  > binskim.log 2>&1
        Write-Verbose -Message "binskim exitcode: $LASTEXITCODE" -Verbose
        $PowerShellName = GetPowerShellName
        Publish-Artifact -Path ./binskim.log -Name "binskim-log-${env:AGENT_OS}-${PowerShellName}"

        $testsPath = Join-Path2 -Path ([System.io.path]::GetTempPath()) -ChildPath 'pspackageproject' -AdditionalChildPath 'BinSkim', 'binskim.tests.ps1'

        $null = New-Item -ItemType Directory -Path (Split-Path $testsPath)

        $testscript | Out-File $testsPath -Force

        Write-Verbose -Message "Generating test results..." -Verbose

        $xmlPath = "$PWD/binskim-results.xml"
        Invoke-Pester -Script $testsPath -OutputFile $xmlPath -OutputFormat NUnitXml

    }
    else {
        $test = 'Describe "BinSkim Diagnostics" { It "no failures found" { $true | Should -Be $true } }'

        $testPath = Join-Path ([System.IO.Path]::GetTempPath()) "binskim.tests.ps1"
        $xmlPath = Join-Path ([System.IO.Path]::GetTempPath()) "binskim-results.xml"

        try {
            Set-Content -Path $testPath -Value $test
            Invoke-Pester -Script $testPath -OutputFormat NUnitXml -OutputFile $xmlPath
        }
        finally {
            Remove-Item -Path $testPath -Force
        }

    }
    Publish-AzDevOpsTestResult -Path $xmlPath -Title "BinSkim $env:AGENT_OS - $PowerShellName Results" -Type NUnit
    return $xmlPath
}

function Publish-AzDevOpsTestResult {
    [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingWriteHost", "")]
    param(
        [parameter(Mandatory)]
        [string]
        $Path,
        [parameter(Mandatory)]
        [string]
        $Title,
        [string]
        $Type = 'NUnit'
    )

    $artifactPath = (Resolve-Path $Path).ProviderPath

    Write-Verbose -Verbose -Message "Uploading $artifactPath"

    # Just do nothing if we are not in AzDevOps
    if ($env:TF_BUILD) {
        Write-Host "##vso[results.publish type=$Type;mergeResults=true;runTitle=$Title;publishRunAttachments=true;resultFiles=$artifactPath;failTaskOnFailedTests=true;]"
    }
}

<#
.SYNOPSIS
Create or update cmdlet help stubs markdown files.
 
.DESCRIPTION
Creates or updates the cmdlet help resource files
for the given module.
The generated help will be stubs, requiring regions with {{ }}
to be filled in.
 
.PARAMETER ProjectRoot
The path to the repository root of the module.
 
.PARAMETER ModuleName
The name of the module to generate cmdlet help for.
 
.PARAMETER Culture
The culture in which cmdlet help should be created.
#>

function Add-PSPackageProjectCmdletHelp {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [string]
        $ProjectRoot,

        [Parameter(Mandatory)]
        [string]
        $ModuleName,

        [Parameter()]
        [cultureinfo]
        $Culture
    )

    $ProjectRoot = Resolve-Path -Path $ProjectRoot

    $helpResourcePath = GetHelpPath -ProjectRoot $ProjectRoot -Culture $Culture

    RunProjectBuild -ProjectRoot $ProjectRoot

    $outModulePath = GetOutputModulePath -ProjectRoot $ProjectRoot -ModuleName $ModuleName

    if (-not (HasCmdletHelp -HelpResourcePath $helpResourcePath)) {
        New-Item -Path $helpResourcePath -ItemType Directory -ErrorAction Ignore
        RunPwshCommandInSubprocess -Command "Import-Module '$outModulePath'; New-MarkdownHelp -Module $ModuleName -OutputFolder '$helpResourcePath'"
        return
    }

    RunPwshCommandInSubprocess -Command "Import-Module '$outModulePath'; Update-MarkdownHelp -Path '$helpResourcePath'"
}

<#
.SYNOPSIS
Assembles help files into staging output.
 
.DESCRIPTION
Compiles markdown help resources into
PowerShell external help files and places
them into the staging location.
 
.PARAMETER ProjectRoot
The path to the project repository root.
 
.PARAMETER ModuleName
The name of the module to publish help for.
 
.PARAMETER Culture
The locale or culture the help is written for.
#>

function Export-PSPackageProjectHelp {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [string]
        $ProjectRoot,

        [Parameter(Mandatory)]
        [string]
        $ModuleName,

        [Parameter()]
        [cultureinfo]
        $Culture = [cultureinfo]::CurrentCulture
    )

    $cultureName = $Culture.Name

    $helpResourcePath = GetHelpPath -ProjectRoot $ProjectRoot -Culture $Culture

    $outModulePath = GetOutputModulePath -ProjectRoot $ProjectRoot -ModuleName $ModuleName

    New-ExternalHelp -Path $helpResourcePath -OutputPath "$outModulePath/$cultureName" -Force
}

function Invoke-PSPackageProjectBuild {
    [CmdletBinding()]
    param(
        [Parameter()]
        [ScriptBlock]
        $BuildScript
    )

    Write-Verbose -Verbose -Message "Invoking build script"

    $BuildScript.Invoke()

    New-PSPackageProjectPackage

    Write-Verbose -Verbose -Message "Finished invoking build script"
}

function New-PSPackageProjectPackage
{
    [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions","")]
    param ()
    Write-Verbose -Message "Starting New-PSPackageProjectPackage" -Verbose
    $ErrorActionPreference = 'Stop'
    $config = Get-PSPackageProjectConfiguration
    $modulePath = Join-Path2 -Path $config.BuildOutputPath -ChildPath $config.ModuleName
    $sourceName = 'pspackageproject-local-repo'
    $packageLocation = Join-Path2 -Path ([System.io.path]::GetTempPath()) -ChildPath $sourceName
    $modulesLocation = Join-Path2 -Path $packageLocation -ChildPath 'modules'

    if (Test-Path $modulesLocation) {
        Remove-Item $modulesLocation -Recurse -Force -ErrorAction Ignore
    }

    $null = New-Item -Path $modulesLocation -Force -ItemType Directory

    Write-Verbose -Message "Starting dependency download" -Verbose

    # TODO : dynamically detect module dependecies and save them
    Save-Package2 -Name PlatyPs, Pester -Location $modulesLocation
    Save-Package2 -Name PSScriptAnalyzer -RequiredVersion '1.18.0' -Location $modulesLocation

    Write-Verbose -Message "Dependency download complete" -Verbose
    if (!(Get-PSRepository -Name $sourceName -ErrorAction Ignore)) {
        Register-PSRepository -Name $sourceName -SourceLocation $modulesLocation -PublishLocation $modulesLocation
    }

    Write-Verbose -Verbose -Message "Starting to publish module: $modulePath"
    Publish-Module -Path $modulePath -Repository $sourceName -NuGetApiKey 'fake' -Force

    Write-Verbose -Message "Local package published" -Verbose

    $nupkgPath = (Get-ChildItem -Path $modulesLocation -Filter "$($config.ModuleName)*.nupkg").FullName
    Publish-Artifact -Path $nupkgPath -Name nupkg

    Write-Verbose -Message "Starting New-PSPackageProjectPackage" -Verbose
}

# Wrapper to push artifact
function Publish-Artifact
{
    [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingWriteHost", "")]
    param(
        [Parameter(Mandatory)]
        [ValidateScript({Test-Path -Path $_})]
        $Path,
        [string]
        $Name
    )

    $resolvedPath = (Resolve-Path -Path $Path).ProviderPath

    if(!$Name)
    {
        $artifactName = [system.io.path]::GetFileName($Path)
    }
    else
    {
        $artifactName = $Name
    }

    if ($env:TF_BUILD) {
        # In Azure DevOps
        Write-Host "##vso[artifact.upload containerfolder=$artifactName;artifactname=$artifactName;]$resolvedPath"
    }
}

function Save-Package2
{
    param(
        [string[]]
        $Name,
        [String]
        $Location,
        [string]
        $RequiredVersion
    )

    $spkg = "Save-Package"
    if($RequiredVersion) {
        & $spkg -Name $Name -Source 'https://www.powershellgallery.com/api/v2' -Path $Location -ProviderName NuGet -RequiredVersion $RequiredVersion
    } else {
        & $spkg -Name $Name -Source 'https://www.powershellgallery.com/api/v2' -Path $Location -ProviderName NuGet
    }

}

function Initialize-PSPackageProject {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true, Position = 0)]
        [string]$ModuleName,
        [string]$ModuleBase = ".",
        [switch]$Force
    )

    if ( [System.Management.Automation.WildcardPattern]::ContainsWildcardCharacters($ModuleBase) ) {
        throw "Modulebase '${ModuleBase}' contains wildcards"
    }

    $ModuleRoot = (Resolve-Path $ModuleBase -ea SilentlyContinue ).Path
    if ( $ModuleRoot -and ! $force ) {
        throw "'${ModuleRoot}' already exists, use -Force to overwrite"
    }

    if ( ! $ModuleRoot ) {
        $ModuleRoot = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($ModuleBase)
    }
    $null = New-Item -ItemType Directory -Path $ModuleRoot

    # make pspackageproject.json
    $jsonPrj =
    @{
        SourcePath = "src"
        ModuleName = "${ModuleName}"
        TestPath = 'test'
        HelpPath = 'help'
        BuildOutputPath = 'out'
        Culture = [CultureInfo]::CurrentCulture.Name # This needs to be settable
    } | ConvertTo-Json

    if ($PSEdition -eq 'Core') {
        $jsonPrj | Out-File (Join-Path ${moduleRoot} "pspackageproject.json") -Encoding utf8NoBOM
    }
    else {
        Write-Warning -Message "UTF-8 characters for module name are not supported in Windows PowerShell."
        $jsonPrj | Out-File (Join-Path ${moduleRoot} "pspackageproject.json") -Encoding ascii
    }

    # Create the help directory
    # and populate a couple of files
    Initialize-PSPackageProjectHelp -ProjectRoot $ModuleRoot -ModuleName $ModuleName

    # Create the scaffold for .psd1 and .psm1
    $moduleSourceBase = Join-Path $ModuleRoot "src"
    $null = New-Item -ItemType Directory -Path $moduleSourceBase
    $moduleFileWithoutExtension = Join-Path $moduleSourceBase ${ModuleName}
    New-ModuleManifest -Path "${moduleFileWithoutExtension}.psd1" -CmdletsToExport "verb-noun" -RootModule "./${ModuleName}.dll"
    $null = New-Item -Type File "${moduleFileWithoutExtension}.psm1"

    # Create a directory for cs sources and create a classlib csproj file with
    # System.Management.Automation as a package reference
    $moduleCodeBase = Join-Path $moduleSourceBase "code"
    $null = New-Item -ItemType Directory -Path $moduleCodeBase
    try {
        Push-Location $moduleCodeBase
        $output = dotnet new classlib -f netstandard2.0 --no-restore --force
        $output += dotnet add package PowerShellStandard.Library --no-restore
        Move-Item code.csproj "${ModuleName}.csproj"
        $str = @"
using System;
using System.Management.Automation;
 
namespace ${ModuleName}
{
    [Cmdlet("verb","noun")]
    public class Cmdlet1 : PSCmdlet
    {
        [Parameter(Mandatory=true,Position=0)]
        public string Name {get;set;}
 
        protected override void ProcessRecord()
        {
            WriteObject(Name);
        }
    }
}
"@

    $str | Out-File Class1.cs
    }
    finally {
        Pop-Location
    }

    # make test folder and create a test template
    $testDir = Join-Path $moduleRoot 'test'
    $testTemplate = Join-Path $testDir "${moduleName}.Tests.ps1"
    $null = New-Item -ItemType Directory -Path "${testDir}"
    @"
Describe "Test ${moduleName}" -tags CI {
    BeforeAll {
    }
    BeforeEach {
    }
    AfterEach {
    }
    AfterAll {
    }
    It "This is the first test for ${moduleName}" {
        `$name = "Hello World"
        verb-noun -name `$name | Should -BeExactly `$name
    }
}
"@
 | Out-File "${testTemplate}"

    # make CI ymls
    Initialize-CIYml -Path ${moduleRoot}

    # make build.ps1
    $boilerplateBuildScript = Join-Path -Path $PSScriptRoot -ChildPath 'build_for_init.ps1'
    Copy-Item $boilerplateBuildScript -Destination (Join-Path $ModuleRoot -ChildPath 'build.ps1') -Force
}

function Get-PSPackageProjectConfiguration {
    param(
        [Parameter()]
        [string] $ConfigPath = "."
    )

    $resolvedPath = Resolve-Path $ConfigPath

    $foundConfigFilePath = if (Test-Path $resolvedPath -PathType Container) {
        SearchConfigFile -Path $resolvedPath
    }
    else {
        if (Test-ConfigFile -Path $resolvedPath) {
            $resolvedPath
        }
    }

    if (Test-Path $foundConfigFilePath) {
        $configObj = Get-Content -Path $foundConfigFilePath | ConvertFrom-Json

        # Populate with full paths

        $projectRoot = Split-Path $foundConfigFilePath

        $configObj.SourcePath = Join-Path $projectRoot -ChildPath $configObj.SourcePath
        $configObj.TestPath = Join-Path $projectRoot -ChildPath $configObj.TestPath
        $configObj.HelpPath = Join-Path $projectRoot -ChildPath $configObj.HelpPath
        $configObj.BuildOutputPath = Join-Path $projectRoot -ChildPath $configObj.BuildOutputPath

        $configObj
    }
    else {
        throw "'pspackageproject.json' not found at: $resolvePath or any or its parent"
    }
}

#endregion Public commands