NovaModuleTools.psm1
|
function Get-MTProjectInfo { [CmdletBinding()] param( [Parameter(Position = 0)] [string]$Path = (Get-Location).Path ) $ProjectRoot = (Resolve-Path -LiteralPath $Path).Path $ProjectJson = [System.IO.Path]::Join($ProjectRoot, 'project.json') if (-not (Test-Path -LiteralPath $projectJson)) { throw "Not a project folder. project.json not found: $projectJson" } $jsonData = Get-Content -LiteralPath $projectJson -Raw | ConvertFrom-Json -AsHashtable $Out = @{} $Out['ProjectJSON'] = $ProjectJson foreach ($key in $jsonData.Keys) { $Out[$key] = $jsonData[$key] } foreach ($boolKey in @('BuildRecursiveFolders', 'FailOnDuplicateFunctionNames', 'SetSourcePath')) { if (-not $Out.ContainsKey($boolKey)) { $Out[$boolKey] = $true continue } $Out[$boolKey] = [bool]$Out[$boolKey] } $Out.ProjectJson = $projectJson $Out.PSTypeName = 'MTProjectInfo' $ProjectName = $jsonData.ProjectName ## Folders $Out['ProjectRoot'] = $ProjectRoot $Out['PublicDir'] = [System.IO.Path]::Join($ProjectRoot, 'src', 'public') $Out['PrivateDir'] = [System.IO.Path]::Join($ProjectRoot, 'src', 'private') $Out['ClassesDir'] = [System.IO.Path]::Join($ProjectRoot, 'src', 'classes') $Out['ResourcesDir'] = [System.IO.Path]::Join($ProjectRoot, 'src', 'resources') $Out['TestsDir'] = [System.IO.Path]::Join($ProjectRoot, 'tests') $Out['DocsDir'] = [System.IO.Path]::Join($ProjectRoot, 'docs') $Out['OutputDir'] = [System.IO.Path]::Join($ProjectRoot, 'dist') $Out['OutputModuleDir'] = [System.IO.Path]::Join($Out.OutputDir, $ProjectName) $Out['ModuleFilePSM1'] = [System.IO.Path]::Join($Out.OutputModuleDir, "$ProjectName.psm1") $Out['ManifestFilePSD1'] = [System.IO.Path]::Join($Out.OutputModuleDir, "$ProjectName.psd1") return [pscustomobject]$out } function Invoke-MTBuild { [CmdletBinding()] param ( ) $ErrorActionPreference = 'Stop' Reset-ProjectDist Build-Module $data = Get-MTProjectInfo if ($data.FailOnDuplicateFunctionNames) { Assert-BuiltModuleHasNoDuplicateFunctionName -ProjectInfo $data } Build-Manifest Build-Help Copy-ProjectResource } function Invoke-MTTest { [CmdletBinding()] param ( [string[]]$TagFilter, [string[]]$ExcludeTagFilter ) Test-ProjectSchema Pester | Out-Null $Script:data = Get-MTProjectInfo $pesterConfig = New-PesterConfiguration -Hashtable $data.Pester $testPath = if ($data.BuildRecursiveFolders) { $data.TestsDir } else { [System.IO.Path]::Join($data.TestsDir, '*.Tests.ps1') } $pesterConfig.Run.Path = $testPath $pesterConfig.Run.PassThru = $true $pesterConfig.Run.Exit = $true $pesterConfig.Run.Throw = $true $pesterConfig.Filter.Tag = $TagFilter $pesterConfig.Filter.ExcludeTag = $ExcludeTagFilter $pesterConfig.TestResult.OutputPath = './dist/TestResults.xml' $TestResult = Invoke-Pester -Configuration $pesterConfig if ($TestResult.Result -ne 'Passed') { Write-Error 'Tests failed' -ErrorAction Stop return $LASTEXITCODE } } function New-MTModule { [CmdletBinding(SupportsShouldProcess = $true)] param ( [string]$Path = (Get-Location).Path ) $ErrorActionPreference = 'Stop' Push-Location if (-not(Test-Path $Path)) { Write-Error 'Not a valid path' } $Questions = [ordered]@{ ProjectName = @{ Caption = 'Module Name' Message = 'Enter Module name of your choice, should be single word with no special characters' Prompt = 'Name' Default = 'MANDATORY' } Description = @{ Caption = 'Module Description' Message = 'What does your module do? Describe in simple words' Prompt = 'Description' Default = 'NovaModuleTools Module' } Version = @{ Caption = 'Semantic Version' Message = 'Starting Version of the module (Default: 0.0.1)' Prompt = 'Version' Default = '0.0.1' } Author = @{ Caption = 'Module Author' Message = 'Enter Author or company name' Prompt = 'Name' Default = 'PS' } PowerShellHostVersion = @{ Caption = 'Supported PowerShell Version' Message = 'What is minimum supported version of PowerShell for this module (Default: 7.4)' Prompt = 'Version' Default = '7.4' } EnableGit = @{ Caption = 'Git Version Control' Message = 'Do you want to enable version controlling using Git' Prompt = 'EnableGit' Default = 'No' Choice = @{ Yes = 'Enable Git' No = 'Skip Git initialization' } } EnablePester = @{ Caption = 'Pester Testing' Message = 'Do you want to enable basic Pester Testing' Prompt = 'EnablePester' Default = 'No' Choice = @{ Yes = 'Enable pester to perform testing' No = 'Skip pester testing' } } } $Answer = @{} $Questions.Keys | ForEach-Object { $Answer.$_ = Read-AwesomeHost -Ask $Questions.$_ } # TODO check other components if ($Answer.ProjectName -notmatch '^[A-Za-z][A-Za-z0-9_.]*$') { Write-Error 'Module Name invalid. Module should be one word and contain only Letters,Numbers and ' } $DirProject = Join-Path -Path $Path -ChildPath $Answer.ProjectName $DirSrc = Join-Path -Path $DirProject -ChildPath 'src' $DirPrivate = Join-Path -Path $DirSrc -ChildPath 'private' $DirPublic = Join-Path -Path $DirSrc -ChildPath 'public' $DirResources = Join-Path -Path $DirSrc -ChildPath 'resources' $DirClasses = Join-Path -Path $DirSrc -ChildPath 'classes' $DirTests = Join-Path -Path $DirProject -ChildPath 'tests' $ProjectJSONFile = Join-Path $DirProject -ChildPath 'project.json' if (Test-Path $DirProject) { Write-Error 'Project already exists, aborting' | Out-Null } # Setup Module Write-Message "`nStarted Module Scaffolding" -color Green Write-Message 'Setting up Directories' ($DirProject, $DirSrc, $DirPrivate, $DirPublic, $DirResources, $DirClasses) | ForEach-Object { 'Creating Directory: {0}' -f $_ | Write-Verbose New-Item -ItemType Directory -Path $_ | Out-Null } if ( $Answer.EnablePester -eq 'Yes') { Write-Message 'Include Pester Configs' New-Item -ItemType Directory -Path $DirTests | Out-Null } if ( $Answer.EnableGit -eq 'Yes') { Write-Message 'Initialize Git Repo' New-InitiateGitRepo -DirectoryPath $DirProject } ## Create ProjectJSON $JsonData = Get-Content (Get-ResourceFilePath -FileName 'ProjectTemplate.json') -Raw | ConvertFrom-Json -AsHashtable $JsonData.ProjectName = $Answer.ProjectName $JsonData.Description = $Answer.Description $JsonData.Version = $Answer.version $JsonData.Manifest.Author = $Answer.Author $JsonData.Manifest.PowerShellHostVersion = $Answer.PowerShellHostVersion $JsonData.Manifest.GUID = (New-Guid).GUID if ($Answer.EnablePester -eq 'No') { $JsonData.Remove('Pester') } Write-Verbose $JsonData $JsonData | ConvertTo-Json | Out-File $ProjectJSONFile 'Module {0} scaffolding complete' -f $Answer.ProjectName | Write-Message -color Green } function Publish-MTLocal { [CmdletBinding()] param( [string]$ModuleDirectoryPath ) if ($ModuleDirectoryPath) { if (-not (Test-Path $ModuleDirectoryPath -PathType Container)) { New-Item $ModuleDirectoryPath -ItemType Directory -Force | Out-Null } } else { $ModuleDirectoryPath = Get-LocalModulePath } Write-Verbose "Using $ModuleDirectoryPath as path" $ProjectInfo = Get-MTProjectInfo # Ensure module is locally built and ready if (-not (Test-Path $ProjectInfo.OutputModuleDir)) { throw 'Dist folder is empty, build the module before running publish command' } # Cleanup old files $OldModule = Join-Path -Path $ModuleDirectoryPath -ChildPath $ProjectInfo.ProjectName if (Test-Path -Path $OldModule) { Write-Verbose 'Removing old module files' Remove-Item -Recurse $OldModule -Force } # Copy New Files Write-Verbose 'Copying new Files' Copy-Item -Path $ProjectInfo.OutputModuleDir -Destination $ModuleDirectoryPath -Recurse -ErrorAction Stop Write-Verbose 'Module copy to local path complete, Refresh session or import module manually' } function Update-MTModuleVersion { [CmdletBinding(SupportsShouldProcess = $true)] param( [ValidateSet('Major', 'Minor', 'Patch')] [string]$Label = 'Patch', [switch]$PreviewRelease, [switch]$StableRelease ) Write-Verbose 'Running Version Update' $data = Get-MTProjectInfo $jsonContent = Get-Content -Path $data.ProjectJSON | ConvertFrom-Json [semver]$CurrentVersion = $jsonContent.Version $Major = $CurrentVersion.Major $Minor = $CurrentVersion.Minor if ($Label -eq 'Major') { $Major = $CurrentVersion.Major + 1 $Minor = 0 $Patch = 0 } elseif ($Label -eq 'Minor') { $Minor = $CurrentVersion.Minor + 1 $Patch = 0 } elseif ($Label -eq 'Patch') { $Patch = $CurrentVersion.Patch + 1 } if ($PreviewRelease) { $ReleaseType = 'preview' } elseif ($StableRelease) { $ReleaseType = $null } else { $ReleaseType = $CurrentVersion.PreReleaseLabel } $newVersion = [semver]::new($Major, $Minor, $Patch, $ReleaseType, $null) # Update the version in the JSON object $jsonContent.Version = $newVersion.ToString() Write-Host "Version bumped to : $newVersion" # Convert the JSON object back to JSON format $newJsonContent = $jsonContent | ConvertTo-Json # Write the updated JSON back to the file $newJsonContent | Set-Content -Path $data.ProjectJSON } function Add-ScriptFileContentToModuleBuilder { [CmdletBinding()] param( [Parameter(Mandatory)][System.Text.StringBuilder]$Builder, [Parameter(Mandatory)][pscustomobject]$ProjectInfo, [Parameter(Mandatory)][System.IO.FileInfo]$File ) if ($ProjectInfo.SetSourcePath) { $relativePath = Get-NormalizedRelativePath -Root $ProjectInfo.ProjectRoot -FullName $File.FullName $Builder.AppendLine("# Source: $relativePath") | Out-Null } $Builder.AppendLine([IO.File]::ReadAllText($File.FullName)) | Out-Null $Builder.AppendLine() | Out-Null } function Assert-BuiltModuleHasNoDuplicateFunctionName { [CmdletBinding()] param( [Parameter(Mandatory)][pscustomobject]$ProjectInfo ) $psm1Path = $ProjectInfo.ModuleFilePSM1 if (-not (Test-Path -LiteralPath $psm1Path)) { throw "Built module file not found: $psm1Path" } $parsed = Get-PowerShellAstFromFile -Path $psm1Path if ($parsed.Errors -and $parsed.Errors.Count -gt 0) { $messages = @($parsed.Errors | ForEach-Object { $_.Message }) -join '; ' throw "Built module contains parse errors and cannot be validated for duplicates. File: $psm1Path. Errors: $messages" } $topLevelFunctions = Get-TopLevelFunctionAst -Ast $parsed.Ast $duplicates = Get-DuplicateFunctionGroup -FunctionAst $topLevelFunctions if (-not $duplicates) { return } $sourceFiles = Get-ProjectScriptFile -ProjectInfo $ProjectInfo $sourceIndex = Get-FunctionSourceIndex -File $sourceFiles $errorText = Format-DuplicateFunctionErrorMessage -Psm1Path $psm1Path -DuplicateGroup $duplicates -SourceIndex $sourceIndex throw $errorText } function Build-Help { [CmdletBinding()] param( ) Write-Verbose 'Running Help update' $data = Get-MTProjectInfo $helpMarkdownFiles = Get-ChildItem -Path $data.DocsDir -Filter '*.md' -Recurse if (-not $helpMarkdownFiles) { Write-Verbose 'No help markdown files in docs directory, skipping building help' return } if (-not (Get-Module -Name Microsoft.PowerShell.PlatyPS -ListAvailable)) { throw 'The module Microsoft.PowerShell.PlatyPS must be installed for Markdown documentation to be generated.' } $AllCommandHelpFiles = $helpMarkdownFiles | Measure-PlatyPSMarkdown | Where-Object FileType -Match CommandHelp # Export to Dist folder $AllCommandHelpFiles | Import-MarkdownCommandHelp -Path { $_.FilePath } | Export-MamlCommandHelp -OutputFolder $data.OutputModuleDir | Out-Null # Rename the directory to match locale $HelpDirOld = Join-Path $data.OutputModuleDir $Data.ProjectName #TODO: hardcoded locale to en-US, change it based on Doc type $languageLocale = 'en-US' $HelpDirNew = Join-Path $data.OutputModuleDir $languageLocale Write-Verbose "Renamed folder to locale: $languageLocale" Rename-Item -Path $HelpDirOld -NewName $HelpDirNew } function Build-Manifest { Write-Verbose 'Building psd1 data file Manifest' $data = Get-MTProjectInfo ## TODO - DO schema check $PubFunctionFiles = Get-ChildItem -Path $data.PublicDir -Filter *.ps1 $functionToExport = @() $aliasToExport = @() $PubFunctionFiles.FullName | ForEach-Object { $functionToExport += Get-FunctionNameFromFile -filePath $_ $aliasToExport += Get-AliasInFunctionFromFile -filePath $_ } ## Import Format.ps1xml (if any) $FormatsToProcess = @() Get-ChildItem -Path $data.ResourcesDir -File -Filter '*Format.ps1xml' -ErrorAction SilentlyContinue | ForEach-Object { if ($data.copyResourcesToModuleRoot) { $FormatsToProcess += $_.Name } else { $FormatsToProcess += Join-Path -Path 'resources' -ChildPath $_.Name } } ## Import Types.ps1xml1 (if any) $TypesToProcess = @() Get-ChildItem -Path $data.ResourcesDir -File -Filter '*Types.ps1xml' -ErrorAction SilentlyContinue | ForEach-Object { if ($data.copyResourcesToModuleRoot) { $TypesToProcess += $_.Name } else { $TypesToProcess += Join-Path -Path 'resources' -ChildPath $_.Name } } $ManfiestAllowedParams = (Get-Command New-ModuleManifest).Parameters.Keys $sv = [semver]$data.Version $ParmsManifest = @{ Path = $data.ManifestFilePSD1 Description = $data.Description FunctionsToExport = $functionToExport AliasesToExport = $aliasToExport RootModule = "$($data.ProjectName).psm1" ModuleVersion = [version]$sv FormatsToProcess = $FormatsToProcess TypesToProcess = $TypesToProcess } ## Release lable if ($sv.PreReleaseLabel) { $ParmsManifest['Prerelease'] = $sv.PreReleaseLabel } # Accept only valid Manifest Parameters $data.Manifest.Keys | ForEach-Object { if ( $ManfiestAllowedParams -contains $_) { if ($data.Manifest.$_) { $ParmsManifest.add($_, $data.Manifest.$_ ) } } else { Write-Warning "Unknown parameter $_ in Manifest" } } try { New-ModuleManifest @ParmsManifest -ErrorAction Stop } catch { 'Failed to create Manifest: {0}' -f $_.Exception.Message | Write-Error -ErrorAction Stop } } function Build-Module { $data = Get-MTProjectInfo $MTBuildVersion = (Get-Command Invoke-MTBuild).Version Write-Verbose "Running ModuleTols Version: $MTBuildVersion" Write-Verbose 'Buidling module psm1 file' Test-ProjectSchema -Schema Build | Out-Null $sb = [System.Text.StringBuilder]::new() $files = Get-ProjectScriptFile -ProjectInfo $data foreach ($file in $files) { Add-ScriptFileContentToModuleBuilder -Builder $sb -ProjectInfo $data -File $file } try { Set-Content -Path $data.ModuleFilePSM1 -Value $sb.ToString() -Encoding 'UTF8' -ErrorAction Stop # psm1 file } catch { Write-Error 'Failed to create psm1 file' -ErrorAction Stop } } function Copy-ProjectResource { $data = Get-MTProjectInfo $resFolder = [System.IO.Path]::Join($data.ProjectRoot, 'src', 'resources') if (Test-Path $resFolder) { ## Copy to root folder instead of creating Resource Folder in module root if ($data.copyResourcesToModuleRoot) { # Copy the resources folder content to the OutputModuleDir $items = Get-ChildItem -Path $resFolder -ErrorAction SilentlyContinue if ($items) { Write-Verbose 'Files found in resource folder, copying resource folder content' foreach ($item in $items) { Copy-Item -Path $item.FullName -Destination ($data.OutputModuleDir) -Recurse -Force -ErrorAction Stop } } } else { # Copy the resources folder to the OutputModuleDir if (Get-ChildItem $resFolder -ErrorAction SilentlyContinue) { Write-Verbose 'Files found in resource folder, Copying resource folder' Copy-Item -Path $resFolder -Destination ($data.OutputModuleDir) -Recurse -Force -ErrorAction Stop } } } } function Format-DuplicateFunctionErrorMessage { [CmdletBinding()] param( [Parameter(Mandatory)][string]$Psm1Path, [Parameter(Mandatory)][object[]]$DuplicateGroup, [hashtable]$SourceIndex ) $lines = New-Object 'System.Collections.Generic.List[string]' $lines.Add("Duplicate top-level function names detected in built module: $Psm1Path") foreach ($dup in ($DuplicateGroup | Sort-Object -Property Name)) { $key = '' + $dup.Name $displayName = $dup.Group[0].Name $lines.Add('') $lines.Add("- $displayName") foreach ($occurrence in ($dup.Group | Sort-Object { $_.Extent.StartLineNumber })) { $lines.Add((" - dist line {0}" -f $occurrence.Extent.StartLineNumber)) } foreach ($sourceLine in (Get-DuplicateFunctionSourceLine -Key $key -SourceIndex $SourceIndex)) { $lines.Add($sourceLine) } } return ($lines -join "`n") } <# .SYNOPSIS Retrieves information about alias in a given function/file so it can be added to module manifest .DESCRIPTION Adding alias to module manifest and exporting it will ensure that functions can be called using alias without importing explicitly #> function Get-AliasInFunctionFromFile { param($filePath) try { $ast = [System.Management.Automation.Language.Parser]::ParseFile($filePath, [ref]$null, [ref]$null) $functionNodes = $ast.FindAll({ param($node) $node -is [System.Management.Automation.Language.FunctionDefinitionAst] }, $true) $function = $functionNodes[0] $paramsAttributes = $function.Body.ParamBlock.Attributes $aliases = ($paramsAttributes | Where-Object { $_.TypeName -like 'Alias' } | ForEach-Object PositionalArguments).Value $aliases } catch { return } } function Get-DuplicateFunctionGroup { [CmdletBinding()] param( [Parameter(Mandatory)][System.Management.Automation.Language.FunctionDefinitionAst[]]$FunctionAst ) return @( $FunctionAst | Group-Object -Property { ('' + $_.Name).ToLowerInvariant() } | Where-Object { $_.Count -gt 1 } ) } function Get-DuplicateFunctionSourceLine { [CmdletBinding()] param( [Parameter(Mandatory)][string]$Key, [hashtable]$SourceIndex ) if (-not $SourceIndex) { return @() } if (-not $SourceIndex.ContainsKey($Key)) { return @() } $lines = New-Object 'System.Collections.Generic.List[string]' $lines.Add(' - source files:') foreach ($src in ($SourceIndex[$Key] | Sort-Object Path, Line)) { $lines.Add((" - {0}:{1}" -f $src.Path, $src.Line)) } return @($lines) } function Get-FunctionNameFromFile { param($filePath) try { $moduleContent = Get-Content -Path $filePath -Raw $ast = [System.Management.Automation.Language.Parser]::ParseInput($moduleContent, [ref]$null, [ref]$null) $functionName = $ast.FindAll({ $args[0] -is [System.Management.Automation.Language.FunctionDefinitionAst] }, $false) | ForEach-Object { $_.Name } return $functionName } catch { return '' } } function Get-IndexableSourceFile { [CmdletBinding()] param( [Parameter(Mandatory)][object[]]$File ) return @($File | Where-Object { $_ -and -not [string]::IsNullOrWhiteSpace($_.FullName) }) } function Get-IndexableFunctionAstFromFile { [CmdletBinding()] param( [Parameter(Mandatory)][string]$Path ) return @(Get-TopLevelFunctionAstFromFile -Path $Path | Where-Object { $_ -and -not [string]::IsNullOrWhiteSpace($_.Name) }) } function Add-FunctionSourceIndexEntry { [CmdletBinding()] param( [Parameter(Mandatory)][hashtable]$Index, [Parameter(Mandatory)][System.IO.FileInfo]$File, [Parameter(Mandatory)][System.Management.Automation.Language.FunctionDefinitionAst]$FunctionAst ) $key = ('' + $FunctionAst.Name).ToLowerInvariant() $list = Get-OrCreateHashtableList -Index $Index -Key $key $list.Add([pscustomobject]@{ Path = $File.FullName Line = $FunctionAst.Extent.StartLineNumber }) } function Add-FunctionSourceIndexEntryFromFile { [CmdletBinding()] param( [Parameter(Mandatory)][hashtable]$Index, [Parameter(Mandatory)][System.IO.FileInfo]$File ) foreach ($fn in (Get-IndexableFunctionAstFromFile -Path $File.FullName)) { Add-FunctionSourceIndexEntry -Index $Index -File $File -FunctionAst $fn } } function Get-FunctionSourceIndex { [CmdletBinding()] param( [Parameter(Mandatory)][System.IO.FileInfo[]]$File ) $index = @{} foreach ($f in (Get-IndexableSourceFile -File $File)) { Add-FunctionSourceIndexEntryFromFile -Index $index -File $f } return $index } function Get-LocalModulePath { $sep = [IO.Path]::PathSeparator $ModulePaths = $env:PSModulePath -split $sep | ForEach-Object { $_.Trim() } | Select-Object -Unique if ($IsWindows) { $MatchPattern = '\\Documents\\PowerShell\\Modules' $Result = $ModulePaths | Where-Object { $_ -match $MatchPattern } | Select-Object -First 1 if ($Result -and (Test-Path $Result)) { return $Result } else { throw "No windows module path matching $MatchPattern found" } } else { # For Mac and Linux $MatchPattern = '/\.local/share/powershell/Modules$' $Result = $ModulePaths | Where-Object { $_ -match $MatchPattern } | Select-Object -First 1 if ($Result -and (Test-Path $Result)) { return $Result } else { throw "No macOS/Linux module path matching $MatchPattern found in PSModulePath." } } } function Get-NormalizedRelativePath { [CmdletBinding()] param( [Parameter(Mandatory)][string]$Root, [Parameter(Mandatory)][string]$FullName ) $rel = [System.IO.Path]::GetRelativePath($Root, $FullName) $rel = $rel -replace '\\', '/' return $rel } function Get-OrCreateHashtableList { [CmdletBinding()] param( [Parameter(Mandatory)][hashtable]$Index, [Parameter(Mandatory)][string]$Key ) if (-not $Index.ContainsKey($Key)) { $Index[$Key] = New-Object 'System.Collections.Generic.List[object]' } return $Index[$Key] } function Get-OrderedScriptFileForDirectory { [CmdletBinding()] param( [Parameter(Mandatory)][string]$Directory, [Parameter(Mandatory)][string]$ProjectRoot, [Parameter(Mandatory)][bool]$Recurse ) if (-not (Test-Path -LiteralPath $Directory)) { return @() } $items = if ($Recurse) { Get-ChildItem -Path $Directory -Filter '*.ps1' -File -Recurse -ErrorAction SilentlyContinue } else { Get-ChildItem -Path $Directory -Filter '*.ps1' -File -ErrorAction SilentlyContinue } $root = $ProjectRoot return @( $items | Sort-Object -Stable -Property @( @{ Expression = { (Get-NormalizedRelativePath -Root $root -FullName $_.FullName).ToLowerInvariant() } }, @{ Expression = { $_.FullName.ToLowerInvariant() } } ) ) } function Get-PowerShellAstFromFile { [CmdletBinding()] param( [Parameter(Mandatory)][string]$Path ) $tokens = $null $errors = $null $ast = [System.Management.Automation.Language.Parser]::ParseFile($Path, [ref]$tokens, [ref]$errors) return [pscustomobject]@{ Ast = $ast Errors = $errors } } function Get-ProjectScriptFile { [CmdletBinding()] param( [Parameter(Mandatory)][pscustomobject]$ProjectInfo ) $recurse = [bool]$ProjectInfo.BuildRecursiveFolders $ordered = New-Object 'System.Collections.Generic.List[System.IO.FileInfo]' $root = $ProjectInfo.ProjectRoot foreach ($f in (Get-OrderedScriptFileForDirectory -Directory $ProjectInfo.ClassesDir -ProjectRoot $root -Recurse:$recurse)) { $ordered.Add($f) } foreach ($f in (Get-OrderedScriptFileForDirectory -Directory $ProjectInfo.PublicDir -ProjectRoot $root -Recurse:$false)) { $ordered.Add($f) } foreach ($f in (Get-OrderedScriptFileForDirectory -Directory $ProjectInfo.PrivateDir -ProjectRoot $root -Recurse:$recurse)) { $ordered.Add($f) } return @($ordered) } function Get-ResourceFilePath { [CmdletBinding()] param( [Parameter(Mandatory)][string]$FileName ) $candidates = @( [System.IO.Path]::GetFullPath((Join-Path $PSScriptRoot "resources/$FileName")), [System.IO.Path]::GetFullPath((Join-Path $PSScriptRoot "../resources/$FileName")) ) | Select-Object -Unique foreach ($candidate in $candidates) { if (Test-Path -LiteralPath $candidate) { return $candidate } } throw "Resource file not found: $FileName. Checked: $($candidates -join ', ')" } function Get-TopLevelFunctionAst { [CmdletBinding()] param( [Parameter(Mandatory)][System.Management.Automation.Language.Ast]$Ast ) $all = @($Ast.FindAll({ param($n) $n -is [System.Management.Automation.Language.FunctionDefinitionAst] }, $true)) $top = foreach ($candidate in $all) { $nested = $false foreach ($other in $all) { if ($other -eq $candidate) { continue } if ($other.Extent.StartOffset -lt $candidate.Extent.StartOffset -and $other.Extent.EndOffset -gt $candidate.Extent.EndOffset) { $nested = $true break } } if (-not $nested) { $candidate } } return @($top) } function Get-TopLevelFunctionAstFromFile { [CmdletBinding()] param( [Parameter(Mandatory)][string]$Path ) $parsed = Get-PowerShellAstFromFile -Path $Path if ($parsed.Errors -and $parsed.Errors.Count -gt 0) { return @() } return @(Get-TopLevelFunctionAst -Ast $parsed.Ast) } function New-InitiateGitRepo { [CmdletBinding(SupportsShouldProcess = $true)] param( [Parameter(Mandatory = $true)] [string]$DirectoryPath ) # Check if Git is installed if (-not (Get-Command git -ErrorAction SilentlyContinue)) { Write-Warning 'Git is not installed. Please install Git and initialize repo manually' return } Push-Location -StackName 'GitInit' # Navigate to the specified directory Set-Location $DirectoryPath # Check if a Git repository already exists if (Test-Path -Path '.git') { Write-Warning 'A Git repository already exists in this directory.' return } if ($PSCmdlet.ShouldProcess($DirectoryPath, ("Initiating git on $DirectoryPath"))) { try { git init | Out-Null } catch { Write-Error 'Failed to initialize Git repo' } } Write-Verbose 'Git repository initialized successfully' Pop-Location -StackName 'GitInit' } function Read-AwesomeHost { [CmdletBinding()] param ( [Parameter()] [pscustomobject] $Ask ) ## For standard questions if ($null -eq $Ask.Choice) { do { $response = $Host.UI.Prompt($Ask.Caption, $Ask.Message, $Ask.Prompt) } while ($Ask.Default -eq 'MANDATORY' -and [string]::IsNullOrEmpty($response.Values)) if ([string]::IsNullOrEmpty($response.Values)) { $result = $Ask.Default } else { $result = $response.Values } } ## For Choice based if ($Ask.Choice) { $Cs = @() $Ask.Choice.Keys | ForEach-Object { $Cs += New-Object System.Management.Automation.Host.ChoiceDescription "&$_", $($Ask.Choice.$_) } $options = [System.Management.Automation.Host.ChoiceDescription[]]($Cs) $IndexOfDefault = $Cs.Label.IndexOf('&' + $Ask.Default) $response = $Host.UI.PromptForChoice($Ask.Caption, $Ask.Message, $options, $IndexOfDefault) $result = $Cs.Label[$response] -replace '&' } return $result } function Reset-ProjectDist { [CmdletBinding(SupportsShouldProcess = $true)] param ( ) $ErrorActionPreference = 'Stop' $data = Get-MTProjectInfo try { Write-Verbose 'Running dist folder reset' if (Test-Path $data.OutputDir) { Remove-Item -Path $data.OutputDir -Recurse -Force } # Setup Folders New-Item -Path $data.OutputDir -ItemType Directory -Force | Out-Null # Dist folder New-Item -Path $data.OutputModuleDir -Type Directory -Force | Out-Null # Module Folder } catch { Write-Error 'Failed to reset Dist folder' } } function Test-ProjectSchema { [CmdletBinding()] param ( [Parameter(Mandatory)] [ValidateSet('Build', 'Pester')] [string] $Schema ) Write-Verbose "Running Schema test against using $Schema schema" $SchemaPath = @{ Build = Get-ResourceFilePath -FileName 'Schema-Build.json' Pester = Get-ResourceFilePath -FileName 'Schema-Pester.json' } $result = switch ($Schema) { 'Build' { Test-Json -Path 'project.json' -Schema (Get-Content $SchemaPath.Build -Raw) -ErrorAction Stop } 'Pester' { Test-Json -Path 'project.json' -Schema (Get-Content $SchemaPath.Pester -Raw) -ErrorAction Stop } Default { $false } } return $result } function Write-Message { [CmdletBinding()] param ( [Parameter(ValueFromPipeline)] [string] $Text, [ValidateSet('Yello', 'Blue', 'Green')] [string] $color = 'Blue' ) PROCESS { Write-Host $Text -ForegroundColor $color } } |