psake.ps1
Properties { $ModuleName = 'PSGetLocalMonitors' $InitialVersion = '1.0.0' $Author = 'Michael Free' $CompanyName = 'Michael Free' $Copyright = '(c) 2025 Michael Free. All rights reserved.' $Description = 'Module description' $ModuleManifest = 'PSGetLocalMonitors.psd1' $projectPath = "$PSScriptRoot" $PublicFunctions = @('Get-LocalMonitor') $PrivateFunctions = @() $moduleContent = @' foreach ($folder in @('Private', 'Public')) { $root = Join-Path -Path $PSScriptRoot -ChildPath $folder if (Test-Path -Path $root) { Write-Verbose "processing folder $root" $files = Get-ChildItem -Path $root -Filter '*.ps1' $files | Where-Object { $_.Name -notlike '*.Tests.ps1' } | ForEach-Object { Write-Verbose "Dot-sourcing $($_.Name)" . $_.FullName } } } $exportedFunctions = (Get-ChildItem -Path (Join-Path $PSScriptRoot 'Public') -Filter '*.ps1').BaseName Export-ModuleMember -Function $exportedFunctions '@ } Task default -Depends InitializeProject, ScaffoldProject, EnforceSyleRules, AnalyzeAndLintScripts, PerformTests, CheckCommentBasedHelp, ValidateManifest, BuildDocumentation Task InitializeProject { Write-Warning "Initializing project at $($PSScriptRoot)" $requiredModules = @('PlatyPS', 'Pester', 'PSScriptAnalyzer') foreach ($module in $requiredModules) { if (-not (Get-Module -ListAvailable -Name $module)) { Write-Warning "$($module) not found. Installing..." try { Install-Module -Name $module -Scope CurrentUser -Force -ErrorAction Stop } catch { Write-Error "Failed to install $($module) : $_" continue } } try { Import-Module $module -Force -ErrorAction Stop } catch { Write-Error "Failed to import $($module): $_" } } if (Get-Module -Name $ModuleName) { Write-Warning "$ModuleName is currently loaded. Removing it from the session..." try { Remove-Module -Name $ModuleName -Force -ErrorAction Stop } catch { throw "Failed to remove $($ModuleName): $_" } } $modulePath = "$PSScriptRoot\$ModuleName.psm1" if (-not (Test-Path -Path $modulePath)) { Write-Warning "$modulePath not found. Creating it..." try { $moduleContent | Out-File -FilePath $modulePath -Encoding UTF8 -Force Write-Warning "$moduleFile created successfully." } catch { throw "Failed to create $($modulePath): $_" } } $requiredDirs = @('Tests', 'Public', 'Private', 'Other') foreach ($dir in $requiredDirs) { $fullPath = Join-Path -Path $PSScriptRoot -ChildPath $dir if (-not (Test-Path -Path $fullPath)) { New-Item -ItemType Directory -Path $fullPath -Force | Out-Null } else { Write-Verbose "Directory already exists: $fullPath" } } } Task ScaffoldProject -Depends InitializeProject { foreach ($funcName in $PublicFunctions) { $filename = "$funcName.ps1" $filepath = Join-Path -Path (Join-Path $projectPath 'Public') -ChildPath $filename if (-not (Test-Path -Path $filepath)) { Write-Warning "Creating function file: $filepath" $FunctionTemplate = @" function $funcName { return '$false' } "@ $FunctionTemplate | Out-File -FilePath $filepath -Encoding UTF8 -Force } else { Write-Verbose "Function file already exists: $filepath" } } if ($PrivateFunctions.Count -gt 0) { foreach ($funcName in $PrivateFunctions) { $filename = "$funcName.ps1" $filepath = Join-Path -Path (Join-Path $projectPath 'Private') -ChildPath $filename if (-not (Test-Path -Path $filepath)) { Write-Output "Creating private function file: $filepath" $FunctionTemplate = @" function $funcName { return '$false' } "@ $FunctionTemplate | Out-File -FilePath $filepath -Encoding UTF8 -Force } } } $allFunctions = @($PublicFunctions + $PrivateFunctions) if ($allFunctions.Count -gt 0) { foreach ($funcName in $allFunctions) { $testFileName = "$funcName.Tests.ps1" $testFilePath = Join-Path -Path (Join-Path $projectPath 'Tests') -ChildPath $testFileName if (-not (Test-Path -Path $testFilePath)) { $TestTemplate = @" Describe '$funcName' { It 'should be implemented' { Throw 'Test not implemented for $funcName' } } "@ Write-Output "Creating test file: $($testFilePath)" $TestTemplate | Out-File -FilePath $testFilePath -Encoding UTF8 -Force } } } } Task EnforceSyleRules -Depends ScaffoldProject { $ruleset = @{ IncludeRules = @( 'PSPlaceOpenBrace', 'PSPlaceCloseBrace', 'PSUseConsistentWhitespace', 'PSUseConsistentIndentation', 'PSAlignAssignmentStatement', 'PSUseCorrectCasing', 'PSAvoidTrailingWhitespace', 'PSAvoidUsingDoubleQuotesForConstantString' ) Rules = @{ PSPlaceOpenBrace = @{ Enable = $true OnSameLine = $true NewLineAfter = $true } PSPlaceCloseBrace = @{ Enable = $true NoEmptyLineBefore = $false } PSUseConsistentIndentation = @{ Enable = $true Kind = 'space' IndentationSize = 2 } PSUseConsistentWhitespace = @{ Enable = $true CheckOperator = $true } PSAlignAssignmentStatement = @{ Enable = $true } PSUseCorrectCasing = @{ Enable = $true EnforceAutomaticVariables = $true } PSAvoidTrailingWhitespace = @{ Enable = $true } PSAvoidUsingDoubleQuotesForConstantString = @{ Enable = $true } } } $files = Get-ChildItem -Path $PSScriptRoot -Recurse -Include *.ps1, *.psm1 -File if ($files.Count -eq 0) { throw 'No Powershell files to format' } foreach ($file in $files) { Write-Output "Formatting $($file.FullName)..." try { $originalContent = Get-Content -Raw -Path $file.FullName $formattedContent = Invoke-Formatter -ScriptDefinition $originalContent -Settings $ruleset if ($formattedContent -ne $originalContent) { $formattedContent | Set-Content -Path $file.FullName -Encoding UTF8 Write-Output "Formatted: $($file.FullName)" } } catch { throw "Failed to format $($file.FullName): $_" } } } Task AnalyzeAndLintScripts -Depends EnforceSyleRules { $files = Get-ChildItem -Path $PSScriptRoot -Recurse -Include *.ps1, *.psm1 -File | Where-Object { $_.Name -notlike '*.Tests.ps1' -and $_.Name -ne 'psake.ps1' } if ($files.Count -eq 0) { throw 'No powershell files found to analyze.' } $issuesFound = $false foreach ($file in $files) { Write-Output "Analyzing $($file.FullName)..." $results = Invoke-ScriptAnalyzer -Path $file.FullName -Severity Warning, Error if ($results) { $issuesFound = $true Write-Warning "Issues found in $($file.FullName):" foreach ($issue in $results) { Write-Warning " [$($issue.Severity)] Line $($issue.Line): $($issue.RuleName) - $($issue.Message)" } } } if ($issuesFound) { throw 'Script analysis found issues. Please fix them before continuing.' } } Task PerformTests -Depends AnalyzeAndLintScripts { $testsPath = Join-Path $PSScriptRoot 'Tests' if (-not (Test-Path $testsPath)) { Write-Warning "Tests directory not found at: $testsPath" return } $testFiles = Get-ChildItem -Path $testsPath -Recurse -Filter *.Tests.ps1 -File if ($testFiles.Count -eq 0) { Write-Warning "No test files found in: $testsPath" return } $hasFailures = $false foreach ($testFile in $testFiles) { Write-Output "Running Pester test: $($testFile.FullName)" $result = Invoke-Pester -Script $testFile.FullName -PassThru -EnableExit if ($result.FailedCount -gt 0) { Write-Warning "$($result.FailedCount) test(s) failed in: $($testFile.Name)" $hasFailures = $true } } if ($hasFailures) { throw 'One or more Pester tests failed. See output for details.' } } Task CheckCommentBasedHelp { $missingHelp = @() $files = Get-ChildItem -Path $PSScriptRoot -Recurse -Include *.ps1, *.psm1 -File | Where-Object { $_.Name -notlike '*.Tests.ps1' -and $_.Name -ne 'psake.ps1' } foreach ($file in $files) { $lines = Get-Content $file.FullName $relativePath = $file.FullName.Replace("$PSScriptRoot\", '') $isInPublicOrPrivate = $relativePath -match '^(Public|Private)[\\/]+' $inHelpBlock = $false $helpBlockEndLine = -10 $lineNumber = 0 foreach ($line in $lines) { $trimmed = $line.Trim() if ($trimmed -like '<#*') { $inHelpBlock = $true } if ($inHelpBlock -and $trimmed -like '*#>') { $inHelpBlock = $false $helpBlockEndLine = $lineNumber } if ($trimmed -match '^function\s+([a-zA-Z0-9_-]+)') { $funcName = $Matches[1] # Function is allowed if it appears within 1 line after the help block ends if (($lineNumber - $helpBlockEndLine) -gt 1) { Write-Warning "Missing help above function '$funcName' in file: $relativePath" $missingHelp += "$($file.FullName):$funcName" } } $lineNumber++ } # For non-function files (outside Public/Private), ensure file-level help exists $content = $lines -join "`n" $hasFunction = $content -match 'function\s+\w+' if (-not $hasFunction -and -not $isInPublicOrPrivate) { if ($content -notmatch '^\s*<#') { Write-Warning "Missing file-level comment-based help: $relativePath" $missingHelp += $file.FullName } } } if ($missingHelp.Count -gt 0) { Write-Error 'Documentation check failed. Missing comment-based help in the following:' $missingHelp | ForEach-Object { Write-Error " - $_" } throw "$($missingHelp.Count) file(s)/function(s) missing comment-based help." } } Task BuildDocumentation { $moduleParentFolder = (Get-Item $PSScriptRoot).Parent.FullName $moduleFolder = Join-Path $moduleParentFolder $ModuleName $docsOutputFolder = Join-Path $moduleFolder 'Docs' $helpOutputFolder = Join-Path $moduleFolder 'en-US' if (-not ($env:PSModulePath -split ';' | Where-Object { $_ -eq $moduleParentFolder })) { $env:PSModulePath += ";$moduleParentFolder" } if (-not (Test-Path $moduleFolder)) { throw "Module folder '$moduleFolder' does not exist." } if (-not (Test-Path $docsOutputFolder)) { New-Item -Path $docsOutputFolder -ItemType Directory | Out-Null } if (-not (Test-Path $helpOutputFolder)) { New-Item -Path $helpOutputFolder -ItemType Directory | Out-Null } $loadedModules = Get-module if ($loadedModules.Name -eq $moduleName) { remove-module -name $moduleName } Import-Module "$PSScriptRoot\$ModuleName.psm1" get-module $exportedFunctions = (Get-Module -Name $moduleName).ExportedCommands.Keys | Sort-Object $existingDocs = Get-ChildItem -Path $docsOutputFolder -Filter '*.md' | Select-Object -ExpandProperty BaseName | Sort-Object if (-not ($exportedFunctions -eq $existingDocs)) { Import-Module "$PSScriptRoot\$ModuleName.psm1" Get-ChildItem -Path $docsOutputFolder -Filter '*.md' | Remove-Item -Force New-MarkdownHelp -Module $moduleName ` -OutputFolder $docsOutputFolder ` -Force ` -WithModulePage ` -Encoding ([System.Text.Encoding]::UTF8) } $aboutMdFile = Join-Path $docsOutputFolder "about_$moduleName.md" if (-not (Test-Path $aboutMdFile)) { New-MarkdownAboutHelp -OutputFolder $docsOutputFolder -AboutName $moduleName } if (-not (Test-Path "$helpOutputFolder\$ModuleName-help.xml")) { New-ExternalHelp -Path $docsOutputFolder -OutputPath $helpOutputFolder -Force } Remove-Module -Name $ModuleName -Force } Task ValidateManifest { if (-not (Test-Path -Path $ModuleManifest)) { New-ModuleManifest -Path $ModuleManifest ` -RootModule "$ModuleName.psm1" ` -Author $Author ` -CompanyName $CompanyName ` -Copyright $Copyright ` -Description $Description ` -ModuleVersion $InitialVersion ` -Tags @('Placeholder') ` -FunctionsToExport '*' ` -NestedModules @() ` -RequiredModules @() ` -PrivateData @{ PSData = @{ LicenseUri = '' ProjectUri = '' IconUri = '' ReleaseNotes = '' } } } if (Test-Path -Path $ModuleManifest) { $manifestData = Import-PowerShellDataFile -Path $ModuleManifest $currentVersionString = $manifestData.ModuleVersion $currentVersion = [Version]$currentVersionString $build = if ($currentVersion.Build -ge 0) { $currentVersion.Build } else { 0 } $newBuild = $build + 1 $newVersion = New-Object System.Version($currentVersion.Major, $currentVersion.Minor, $newBuild) $newVersionString = $newVersion.ToString() $manifestText = Get-Content -Path $ModuleManifest -Raw $pattern = '(?m)^ModuleVersion\s*=\s*''.*?''' $replacement = "ModuleVersion = '$newVersionString'" if ($manifestText -match $pattern) { $newManifestText = $manifestText -replace $pattern, $replacement Set-Content -Path $ModuleManifest -Value $newManifestText -Encoding UTF8 } } #Import-Module .\PSModulePipeline.psd1 -Verbose -Force #Test-ModuleManifest -Path .\PSModulePipeline.psd1 # try { # Test-ModuleManifest -Path .\PSModulePipeline.psd1 # Write-Host "Manifest validation passed." #} #catch { # Write-Error "Manifest validation failed: $_" #} } |