Invoke-PSModuleBuild.psm1
function Get-Ast { [CmdletBinding( DefaultParameterSetName = "File" )] Param( [Parameter( ParameterSetName = "File", Mandatory, ValueFromPipeline, Position=0 )] [string] $File, [Parameter( ParameterSetName = "Code", Mandatory, ValueFromPipeline, Position=0 )] [string] $Code, [ArgumentCompleter({ param( $commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameters ) $typeNames = [PSObject].Assembly.GetTypes().Where{$_.Name.EndsWith('Ast')}.Name | Sort-Object $typeNames | Where-Object { $_.LogName -like "$wordToComplete*" } | Foreach-Object { [System.Management.Automation.CompletionResult]::new($_, $_, "ParameterValue", $_) } })] $AstType = '*', [Switch] $NoRecursion ) Begin { $predicate = { param($astObject) $astObject.GetType().Name -like $AstType } } Process { $errors = $null $ast = switch ($PSCmdlet.ParameterSetName) { 'File' { [System.Management.Automation.Language.Parser]::ParseFile($File, [ref]$null, [ref]$errors) } 'Code' { [System.Management.Automation.Language.Parser]::ParseInput($Code, [ref]$null, [ref]$errors) } } if ($errors) { throw [System.InvalidCastException]::new("Submitted text could not be converted to PowerShell because it contains syntax errors: $($errors | Out-String)")} $ast.FindAll($predicate, !$NoRecursion) | Add-Member -MemberType ScriptProperty -Name Type -Value { $this.GetType().Name } -PassThru } } function Get-PSModuleBuildFunctions { <# .EXAMPLE $c = Get-PSModuleBuildFunctions -Path src -Recurse $c #> [CmdletBinding()] Param( [Parameter( ValueFromPipeline, ValueFromPipelineByPropertyName )] [Alias('FullName')] $Path = (Convert-Path ".\"), [Parameter( Position=2 )] [regex] $Exclude = '\.tests.ps1$', [Switch] $Recurse ) BEGIN { $baseMessage = "[ $($MyInvocation.InvocationName) ]" Write-Verbose "$baseMessage Executing" } PROCESS { if (-not (Test-Path -Path $Path -PathType Container -ErrorAction SilentlyContinue)) { Write-Error "$Path is not a valid Directory" return } $directories = switch ($Recurse) { $true { [System.IO.DirectoryInfo](Convert-Path $Path) Get-ChildItem -Path $Path -Directory -Force -ErrorAction SilentlyContinue -Recurse } $false { [System.IO.DirectoryInfo](Convert-Path $Path) } } $directories | Foreach-Object { $directory = $_ $params = @{ RootDirectory = $Path Path = $directory.FullName } $functionScope = Get-PSModuleBuildFunctionScope @params Get-ChildItem -Path $directory.FullName -File | Foreach-Object { $file = $_ if ($file.Extension -eq '.ps1') { if ($file.Name -notmatch $Exclude) { $params = @{ File = $file.FullName AstType = 'FunctionDefinitionAst' ErrorAction = 'SilentlyContinue' } Get-Ast @params | Foreach-Object { $ast = $_ [PSCustomObject]@{ Scope = $functionScope Name = $ast.Name AST = $ast } } } } } } | Sort-Object -Property Scope, Name } } function Get-PSModuleBuildFunctionScope { <# .EXAMPLE $RootDirectory = '/Users/balmerj/Desktop/Github/JerryBalmer/PS-CommonFunctions/src' $Path = '/Users/balmerj/Desktop/Github/JerryBalmer/PS-CommonFunctions/src/psbuild/private' Get-PSScriptFileScopeType -RootDirectory $RootDirectory -Path $Path # Output: private #> [CmdletBinding()] Param( [Parameter( Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName, Position=0 )] [Alias('FullName')] $RootDirectory, [Parameter( Mandatory, Position=1 )] [String] $Path ) BEGIN { $baseMessage = "[ $($MyInvocation.InvocationName) ]" Write-Verbose "$baseMessage Executing" } PROCESS { $split = [System.Collections.ArrayList]@() $reversedSplit = [System.Collections.ArrayList]@() $splitCharacter = if ($IsWindows) { '\' } else { '/' } $processString = $Path -Replace ([regex]::Escape($RootDirectory)),'' $processString.Split($splitCharacter,[System.StringSplitOptions]::RemoveEmptyEntries) | Foreach-Object { $split.Add($_.ToString()) | Out-Null } ($split.Count - 1)..0 | Foreach-Object { $i = $_ $splitItem = $split[$i] Write-Verbose " - Adding Split Item: $splitItem" $reversedSplit.Add($splitItem) | Out-Null } $test = $reversedSplit | Foreach-Object { $i = $_ if ($i -eq 'public') { 'public' } elseif ($i -eq 'private') { 'private' } } $scope = if ($test) { $test | Select-Object -First 1 } else { 'public' } (Get-Culture).TextInfo.ToTitleCase($scope) } } function Split-String { <# https://stackoverflow.com/questions/16435240/how-to-split-string-by-string-in-powershell #> [CmdletBinding()] Param( [Parameter( Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName, Position=0 )] [String] $String, [Parameter( Mandatory, Position=1 )] $Separator, [Parameter( Position=2 )] [Switch] $RemoveEmptyEntries, [Parameter( Position=3 )] [Switch] $Reverse ) BEGIN { $baseMessage = "[ $($MyInvocation.InvocationName) ]" Write-Verbose "$baseMessage Executing" } PROCESS { $_separator = if ($Separator.GetType().FullName -ne 'System.String[]') { [string[]]@($Separator) } else { $Separator } $result = if ($RemoveEmptyEntries) { $String.Split($_separator, [System.StringSplitOptions]::RemoveEmptyEntries) } else { $String.Split($separator) } if ($Reverse) { $array =[System.Collections.ArrayList]@() $result | Foreach-Object { $array.Add($_) | Out-Null } if ($array.Count -gt 0) { ($array.Count - 1)..0 | Foreach-Object { $i = $_ $array[$i] } } } else { $result } } } function Update-Version { [CmdletBinding()] Param( [Parameter( Position=0, ValueFromPipeline )] [version] $Version, [Parameter( Position=1 )] [ValidateSet( 'Major', 'Minor', 'Build' )] [string] $Type = 'Build', [switch] $OutVersion ) Begin { $baseMessage = "[ $($MyInvocation.InvocationName) ]" Write-Verbose "$baseMessage Executing" } Process { $v = $Version $major = $v.Major $minor = $v.Minor $build = $v.Build $version = switch ($Type) { 'Major' { [version]("$($major + 1).0.0") } 'Minor' { [version]("$($major).$($minor + 1).0") } 'Build' { [version]("$($major).$($minor).$($build + 1)") } } } End { if ($PSBoundParameters.ContainsKey('OutVersion')) { $version } else { $version.ToString() } } } function Initialize-PSModuleBuild { [CmdletBinding()] Param( [Parameter( Mandatory, Position=0 )] [String] $Name, [Parameter( Position=1 )] [String] $Path = (Convert-Path ".\"), [Switch] $Force ) BEGIN { $baseMessage = "[ $($MyInvocation.InvocationName) ]" Write-Verbose "$baseMessage Executing" } PROCESS { Remove-Variable -Name 'PSModuleBuild' -Force -ErrorAction SilentlyContinue $contentInvokeBuild = @" [CmdletBinding()] Param( ) ################################################################################################################## # Globals ################################################################################################################## if (`$PSBoundParameters.ContainsKey('Verbose')) { `$Global:BUILD_VERBOSE = `$true } else { `$Global:BUILD_VERBOSE = `$true } ################################################################################################################## # Argument Completers ################################################################################################################## Register-ArgumentCompleter -CommandName Invoke-Build.ps1 -ParameterName Task -ScriptBlock { param(`$commandName, `$parameterName, `$wordToComplete, `$commandAst, `$boundParameters) (Invoke-Build -Task ?? -File (`$boundParameters['File'])).get_Keys() -like "`$wordToComplete*" | .{process{ New-Object System.Management.Automation.CompletionResult `$_, `$_, 'ParameterValue', `$_ }} } Register-ArgumentCompleter -CommandName Invoke-Build.ps1 -ParameterName File -ScriptBlock { param(`$commandName, `$parameterName, `$wordToComplete, `$commandAst, `$boundParameters) Get-ChildItem -Directory -Name "`$wordToComplete*" | .{process{ New-Object System.Management.Automation.CompletionResult `$_, `$_, 'ProviderContainer', `$_ }} if (!(`$boundParameters['Task'] -eq '**')) { Get-ChildItem -File -Name "`$wordToComplete*.ps1" | .{process{ New-Object System.Management.Automation.CompletionResult `$_, `$_, 'Command', `$_ }} } } ################################################################################################################## # Tasks ################################################################################################################## task Build { Initialize-PSModuleBuild -Name '$Name' -Verbose:`$BUILD_VERBOSE | Out-Null Invoke-PSModuleBuild -Verbose:`$BUILD_VERBOSE } task Test Build, { `$configuration = New-PesterConfiguration `$configuration.Output.Verbosity = if (`$BUILD_VERBOSE) {'Detailed'} else {'Normal'} Invoke-Pester -Configuration `$configuration } task Release Test, { if (-not (Get-Variable -Name 'PSGalleryApiKey' -Scope Global -ErrorAction SilentlyContinue)) { `$Global:PSGalleryApiKey = Read-Host -Prompt "Enter your PSGallery Nuget Api Key" } Invoke-PSModuleBuildRelease -Verbose:`$BUILD_VERBOSE `$params = @{ Path = `$PSModuleBuild.Paths.BinModuleDirectory NuGetApiKey = `$Global:PSGalleryApiKey Repository = 'PSGallery' Force = `$true WhatIf = `$true } Publish-Module @params } task . Build "@ $contentGitIgnore = @' bin/** '@ $contentTestFile = @' BeforeDiscovery { } Describe 'SomeTestName' { BeforeAll { } It 'True and True' { $true | Should -Be $true } } '@ try { if (-not (Test-Path -Path $Path -ErrorAction SilentlyContinue)) { if ($Force) { try { $params = @{ Path = $Path ItemType = 'Directory' Force = $true } New-Item @params | Out-Null } catch { Write-Error "Failed to create the path: $Path." return } } else { Write-Error "Path does not exist $Path. Use the -Force parameter if you want to create the path." return } } # Directories $moduleDirectory = (Resolve-Path $Path).Path $sourceDirectory = Join-Path $moduleDirectory 'src' $binDirectory = Join-Path $moduleDirectory 'bin' $binModuleDirectory = Join-Path $binDirectory $Name $testDirectory = Join-Path $moduleDirectory 'tests' $testResultsDirectory = Join-Path $testDirectory '_results' # Files $invokeBuildFilePath = Join-Path $moduleDirectory '.build.ps1' $gitIgnorePath = Join-Path $moduleDirectory '.gitIgnore' $testFile = Join-Path $testDirectory '_.tests.ps1' $sourcePsm1 = Join-Path $sourceDirectory "$($Name).psm1" $sourcePsd1 = Join-Path $sourceDirectory "$($Name).psd1" # Create the directories if (-not (Test-Path -Path $sourceDirectory -PathType Container)) { Write-Verbose " - Creating the src directory." try { $params = @{ Path = $sourceDirectory ItemType = 'Directory' Force = $true } New-Item @params | Out-Null } catch { Write-Error "Failed to create the src directory: $sourceDirectory." return } } if (-not (Test-Path -Path $binDirectory -PathType Container)) { Write-Verbose " - Creating the bin directory." try { $params = @{ Path = $binDirectory ItemType = 'Directory' Force = $true } New-Item @params | Out-Null } catch { Write-Error "Failed to create the bin directory: $binDirectory." return } } if (-not (Test-Path -Path $testDirectory -PathType Container)) { Write-Verbose " - Creating the tests directory." try { $params = @{ Path = $testDirectory ItemType = 'Directory' Force = $true } New-Item @params | Out-Null } catch { Write-Error "Failed to create the tests directory: $testDirectory." return } } if (-not (Test-Path -Path $testResultsDirectory -PathType Container)) { Write-Verbose " - Creating the test results directory." try { $params = @{ Path = $testResultsDirectory ItemType = 'Directory' Force = $true } New-Item @params | Out-Null } catch { Write-Error "Failed to create the test results directory: $testResultsDirectory." return } } # Create the files if (-not (Test-Path -Path $invokeBuildFilePath -PathType Leaf)) { Write-Verbose " - Creating the .build.ps1 file." try { $params = @{ Path = $invokeBuildFilePath Value = $contentInvokeBuild ItemType = 'File' Force = $true } New-Item @params | Out-Null } catch { Write-Error "Failed to create the .build.ps1 file $invokeBuildFilePath." return } } if (-not (Test-Path -Path $gitIgnorePath -PathType Leaf)) { Write-Verbose " - Creating the .gitIgnore file." try { $params = @{ Path = $gitIgnorePath Value = $contentGitIgnore ItemType = 'File' Force = $true } New-Item @params | Out-Null } catch { Write-Error "Failed to create the .gitIgnore file $gitIgnorePath." return } } if (-not (Test-Path -Path $testFile -PathType Leaf)) { Write-Verbose " - Creating the _.tests.ps1 file." try { $params = @{ Path = $testFile Value = $contentTestFile ItemType = 'File' Force = $true } New-Item @params | Out-Null } catch { Write-Error "Failed to create the _.tests.ps1 file $testFile." return } } # Module Files ------------------------------- if (-not (Test-Path -Path $sourcePsm1 -PathType Leaf)) { Write-Verbose " - Creating the .psm1 file." try { $params = @{ Path = $sourcePsm1 ItemType = 'File' Force = $true } New-Item @params | Out-Null } catch { Write-Error "Failed to create the .psm1 file $sourcePsm1." return } } if (-not (Test-Path -Path $sourcePsd1 -PathType Leaf)) { Write-Verbose " - Creating the .psd1 file." try { New-ModuleManifest -Path $sourcePsd1 -RootModule "$($Name).psm1" } catch { Write-Error "Failed to create the .psm1 file $sourcePsm1." return } } else { # Test to see if the root module is still the same or not. } #============================================================= #============================================================= #============================================================= $Global:PSModuleBuild = @{ Name = $Name Objects = [PSCustomObject]@{ Functions = $null } Paths = [PSCustomObject]@{ ModuleDirectory = $moduleDirectory SrcDirectory = $sourceDirectory BinDirectory = $binDirectory BinModuleDirectory = $binModuleDirectory TestDirectory = $testDirectory TestOutputDirectory = $testOutputDirectory PSM1 = [PSCustomObject]@{ Source = $sourcePsm1 Destination = Join-Path $binModuleDirectory "$($Name).psm1" } PSD1 = [PSCustomObject]@{ Source = $sourcePsd1 Destination = Join-Path $binModuleDirectory "$($Name).psd1" } } } } catch { Remove-Variable -Name 'PSModuleBuild' -Force -ErrorAction SilentlyContinue } finally { $Global:PSModuleBuild } } } function Invoke-PSModuleBuild { [CmdletBinding()] Param( ) BEGIN { $baseMessage = "[ $($MyInvocation.InvocationName) ]" Write-Verbose "$baseMessage Executing" } PROCESS { if (-not (Get-Variable -name 'PSModuleBuild' -Scope 'Global' -ErrorAction SilentlyContinue)) { Write-Error "You must first run the command Initialize-PSModuleBuild before this can be called." return } $Global:PSModuleBuild.Objects.Functions = Get-PSModuleBuildFunctions -Path $Global:PSModuleBuild.Paths.SrcDirectory -Recurse -Verbose:$false ################################################ # Prep the bin directory ################################################ Get-ChildItem -Path $PSModuleBuild.Paths.BinDirectory | Foreach-Object { $_ | Remove-Item -Force -Recurse } New-Item -Path $PSModuleBuild.Paths.BinModuleDirectory -ItemType Directory -Force | Out-Null ################################################ # Process the .psm1 file ################################################ $params = @{ Path = $PSModuleBuild.Paths.PSM1.Source Destination = $PSModuleBuild.Paths.PSM1.Destination Force = $true } Copy-Item @params $PSModuleBuild.Objects.Functions | Foreach-Object { $_function = $_ $_function.AST.Extent.text | Out-File -FilePath $PSModuleBuild.Paths.PSM1.Destination -Append "" | Out-File -FilePath $PSModuleBuild.Paths.PSM1.Destination -Append } ################################################ # Process the .psd1 file ################################################ $manifestParams = @{} $manifestParams['Path'] = $PSModuleBuild.Paths.PSD1.Destination # Copy the manifest $params = @{ Path = $PSModuleBuild.Paths.PSD1.Source Destination = $PSModuleBuild.Paths.PSD1.Destination Force = $true } Copy-Item @params #------------------------------- # Functions to Export #------------------------------- $functionsToExport = $PSModuleBuild.Objects.Functions ` | Where-Object {$_.Scope -eq 'Public'} ` | Select-Object -ExpandProperty Name if ($functionsToExport) { $manifestParams['FunctionsToExport'] = $PSModuleBuild.Paths.PSD1.Destination } #------------------------------- # Update the Module Manifest #------------------------------- Update-ModuleManifest @manifestParams } } function Invoke-PSModuleBuildRelease { [CmdletBinding( DefaultParameterSetName='Standard' )] Param( [Parameter( ParameterSetName='Standard', Position=0 )] [ValidateSet('Build','Minor','Major')] [String] $BuildType = 'Build', [Parameter( ParameterSetName='ForceVersion', Mandatory, Position=0 )] [Version] $ForceVersion ) BEGIN { $baseMessage = "[ $($MyInvocation.InvocationName) ]" Write-Verbose "$baseMessage Executing" } PROCESS { #=================================================================================== Write-Verbose " - Checking if build has been initialized." if (-not (Get-Variable -name 'PSModuleBuild' -Scope 'Global' -ErrorAction SilentlyContinue)) { Write-Verbose " - Build has NOT been initialized." Write-Error "You must first run the command Initialize-PSModuleBuild before this can be called." return } else { Write-Verbose " - Build has been initialized." } #=================================================================================== Write-Verbose " - Verifying that the build .psd1 has been created." if (-not (Test-Path -Path $PSModuleBuild.Paths.PSD1.Destination -PathType Leaf -ErrorAction SilentlyContinue)) { Write-Verbose " - Build .psd1 was NOT found at path $($PSModuleBuild.Paths.PSD1.Destination)" Write-Error "The .psd1 in the bin directory was not found at the path $($PSModuleBuild.Paths.PSD1.Destination)" return } else { Write-Verbose " - Build .psd1 was found at path $($PSModuleBuild.Paths.PSD1.Destination)" } #=================================================================================== Write-Verbose " - Importing $($PSModuleBuild.Paths.PSD1.Destination)" $manifest = Import-PowerShellDataFile -Path $PSModuleBuild.Paths.PSD1.Destination -Verbose:$false Write-Verbose " - Pulling current module version" $versionToSet = switch ($PSCmdlet.ParameterSetName) { 'Standard' { if ($manifest.ContainsKey('ModuleVersion')) { $___version = ($manifest.ModuleVersion).ToString() Write-Verbose " - Current Version is $___version" $___version | Update-Version -Type $BuildType -Verbose:$false } else { Write-Verbose " - No Version Found. Setting to 0.0.1" "0.0.1" } } 'ForceVersion' { $ForceVersion.ToString() } } Write-Verbose " - Updating the .psd1 ModuleVersion to $versionToSet" $params = @{ Path = $PSModuleBuild.Paths.PSD1.Destination ModuleVersion = $versionToSet Verbose = $false ErrorAction = 'Stop' } Update-ModuleManifest @params Write-Verbose " - Copying Build .psd1 to the src directory." $params = @{ Path = $PSModuleBuild.Paths.PSD1.Destination Destination = $PSModuleBuild.Paths.PSD1.Source Force = $true Verbose = $false } Copy-Item @params } } |