NovaModuleTools.psm1
|
# Source: src/public/GetMTProjectInfo.ps1 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 } # Source: src/public/GetNovaProjectInfo.ps1 function Get-NovaProjectInfo { [CmdletBinding()] param( [Parameter(Position = 0)] [string]$Path = (Get-Location).Path, [switch]$Version ) $projectInfo = Get-MTProjectInfo -Path $Path if ($Version) { return $projectInfo.Version } return $projectInfo } # Source: src/public/InvokeMTBuild.ps1 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 } # Source: src/public/InvokeMTTests.ps1 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 } } # Source: src/public/InvokeNovaBuild.ps1 function Invoke-NovaBuild { [CmdletBinding()] param() Invoke-MTBuild } # Source: src/public/InvokeNovaCli.ps1 function Invoke-NovaCli { [CmdletBinding()] [Alias('nova')] param( [Parameter(Position = 0)] [ValidateSet('info', 'version', 'build', 'test', 'init', 'publish', 'bump', 'release')] [string]$Command, [Parameter(Position = 1, ValueFromRemainingArguments)] [string[]]$Arguments ) if ( [string]::IsNullOrWhiteSpace($Command)) { throw 'Missing command. Use one of: info, version, build, test, init, publish, bump, release' } switch ($Command) { 'info' { return Get-NovaProjectInfo } 'version' { return Get-NovaProjectInfo -Version } 'build' { return Invoke-NovaBuild } 'test' { return Test-NovaBuild } 'init' { if ($Arguments.Count -gt 0) { return New-NovaModule -Path $Arguments[0] } return New-NovaModule } 'bump' { return Update-NovaModuleVersion } 'publish' { $options = ConvertFrom-NovaCliArgument -Arguments $Arguments return Publish-NovaModule @options } 'release' { $options = ConvertFrom-NovaCliArgument -Arguments $Arguments return Invoke-NovaRelease -PublishOption $options } } } # Source: src/public/InvokeNovaRelease.ps1 function Invoke-NovaRelease { [CmdletBinding()] param( [hashtable]$PublishOption = @{}, [string]$Path = (Get-Location).Path ) Push-Location -LiteralPath $Path try { if ($PublishOption.Local) { Write-Verbose 'Using local release mode.' } Invoke-NovaBuild Test-NovaBuild $versionResult = Update-NovaModuleVersion Invoke-NovaBuild if ( $PublishOption.ContainsKey('Repository')) { Publish-NovaBuiltModule -Repository $PublishOption.Repository -ApiKey $PublishOption.ApiKey } else { Publish-NovaBuiltModule -ModuleDirectoryPath $PublishOption.ModuleDirectoryPath } return $versionResult } finally { Pop-Location } } # Source: src/public/NewMTModule.ps1 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 } # Source: src/public/NewNovaModule.ps1 function New-NovaModule { [CmdletBinding(SupportsShouldProcess = $true)] param( [string]$Path = (Get-Location).Path ) New-MTModule -Path $Path } # Source: src/public/PublishMTLocal.ps1 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' } # Source: src/public/PublishNovaModule.ps1 function Publish-NovaModule { [CmdletBinding(DefaultParameterSetName = 'Local')] param( [Parameter(ParameterSetName = 'Local')] [switch]$Local, [Parameter(ParameterSetName = 'Repository', Mandatory)] [string]$Repository, [string]$ModuleDirectoryPath, [string]$ApiKey ) Invoke-NovaBuild Test-NovaBuild if ($Local) { Write-Verbose 'Using local publish mode.' } if ($PSCmdlet.ParameterSetName -eq 'Repository') { Publish-NovaBuiltModule -Repository $Repository -ApiKey $ApiKey return } Publish-NovaBuiltModule -ModuleDirectoryPath $ModuleDirectoryPath } # Source: src/public/TestNovaBuild.ps1 function Test-NovaBuild { [CmdletBinding()] param( [string[]]$TagFilter, [string[]]$ExcludeTagFilter ) Invoke-MTTest -TagFilter $TagFilter -ExcludeTagFilter $ExcludeTagFilter } # Source: src/public/UpdateModVersion.ps1 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 } # Source: src/public/UpdateNovaModuleVersion.ps1 function Update-NovaModuleVersion { [CmdletBinding(SupportsShouldProcess = $true)] param( [string]$Path = (Get-Location).Path ) $projectRoot = (Resolve-Path -LiteralPath $Path).Path $before = Get-MTProjectInfo -Path $projectRoot $commitMessages = Get-GitCommitMessageForVersionBump -ProjectRoot $projectRoot $label = Get-VersionLabelFromCommitSet -Messages $commitMessages Push-Location -LiteralPath $projectRoot try { Update-MTModuleVersion -Label $label $after = Get-MTProjectInfo } finally { Pop-Location } return [pscustomobject]@{ PreviousVersion = $before.Version NewVersion = $after.Version Label = $label CommitCount = $commitMessages.Count } } # Source: src/private/AddScriptFileContentToModuleBuilder.ps1 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 } # Source: src/private/AssertBuiltModuleHasNoDuplicateFunctionNames.ps1 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 } # Source: src/private/BuildHelp.ps1 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 } # Source: src/private/BuildManifest.ps1 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 } } # Source: src/private/BuildModule.ps1 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 } } # Source: src/private/CopyProjectResource.ps1 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 } } } } # Source: src/private/FormatDuplicateFunctionErrorMessage.ps1 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") } # Source: src/private/GetAliasNameFromFunction.ps1 <# .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 } } # Source: src/private/GetDuplicateFunctionGroup.ps1 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 } ) } # Source: src/private/GetDuplicateFunctionSourceLine.ps1 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) } # Source: src/private/GetFunctionNameFromFile.ps1 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 '' } } # Source: src/private/GetFunctionSourceIndex.ps1 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 } # Source: src/private/GetGitCommitMessagesForVersionBump.ps1 function Get-GitCommitMessageForVersionBump { [CmdletBinding()] param( [Parameter(Mandatory)][string]$ProjectRoot ) if (-not (Test-Path -LiteralPath (Join-Path $ProjectRoot '.git'))) { return @() } $format = '%s%n%b%n--END-COMMIT--' $lastTag = & git -C $ProjectRoot describe --tags --abbrev=0 2> $null if ($LASTEXITCODE -eq 0 -and -not [string]::IsNullOrWhiteSpace($lastTag)) { $raw = & git -C $ProjectRoot log "$lastTag..HEAD" --format=$format 2> $null } else { $raw = & git -C $ProjectRoot log --format=$format 2> $null } if ($LASTEXITCODE -ne 0 -or -not $raw) { return @() } $text = ($raw -join [Environment]::NewLine) $commits = $text -split '(?m)^--END-COMMIT--\r?$' return @($commits | ForEach-Object {$_.Trim()} | Where-Object {-not [string]::IsNullOrWhiteSpace($_)}) } # Source: src/private/GetLocalModulePath.ps1 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." } } } # Source: src/private/GetNormalizedRelativePath.ps1 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 } # Source: src/private/GetNovaCliOptions.ps1 function ConvertFrom-NovaCliArgument { [CmdletBinding()] param( [string[]]$Arguments ) $options = @{} $index = 0 while ($index -lt $Arguments.Count) { $token = $Arguments[$index] switch -Regex ($token) { '^(--local|-Local)$' { $options.Local = $true } '^(--repository|-Repository)$' { $index++ if ($index -ge $Arguments.Count) { throw 'Missing value for --repository' } $options.Repository = $Arguments[$index] } '^(--path|-Path|-ModuleDirectoryPath)$' { $index++ if ($index -ge $Arguments.Count) { throw 'Missing value for --path' } $options.ModuleDirectoryPath = $Arguments[$index] } '^(--apikey|-ApiKey)$' { $index++ if ($index -ge $Arguments.Count) { throw 'Missing value for --apikey' } $options.ApiKey = $Arguments[$index] } default { throw "Unknown argument: $token" } } $index++ } return $options } # Source: src/private/GetOrCreateHashtableList.ps1 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] } # Source: src/private/GetOrderedScriptFileForDirectory.ps1 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() } } ) ) } # Source: src/private/GetPowerShellAstFromFile.ps1 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 } } # Source: src/private/GetProjectScriptFiles.ps1 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) } # Source: src/private/GetResourceFilePath.ps1 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 ', ')" } # Source: src/private/GetTopLevelFunctionAst.ps1 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) } # Source: src/private/GetTopLevelFunctionAstFromFile.ps1 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) } # Source: src/private/GetVersionLabelFromCommitMessages.ps1 function Get-VersionLabelFromCommitSet { [CmdletBinding()] param( [Parameter(Mandatory)][string[]]$Messages ) if ($Messages | Where-Object {$_ -match '(?im)BREAKING CHANGE|^[a-z]+(\(.+\))?!:'}) { return 'Major' } if ($Messages | Where-Object {$_ -match '(?im)^\s*feat(\(.+\))?:'}) { return 'Minor' } if ($Messages | Where-Object {$_ -match '(?im)^\s*fix(\(.+\))?:'}) { return 'Patch' } return 'Patch' } # Source: src/private/InitiateGitRepo.ps1 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' } # Source: src/private/PublishNovaBuiltModule.ps1 function Publish-NovaBuiltModule { [CmdletBinding(DefaultParameterSetName = 'Local')] param( [Parameter(ParameterSetName = 'Repository')] [string]$Repository, [string]$ModuleDirectoryPath, [string]$ApiKey ) $projectInfo = Get-MTProjectInfo if (-not (Test-Path -LiteralPath $projectInfo.OutputModuleDir)) { throw 'Dist folder is empty, build the module before running publish command' } if ($PSCmdlet.ParameterSetName -eq 'Repository') { $resolvedApiKey = $ApiKey if ($Repository -eq 'PSGallery' -and [string]::IsNullOrWhiteSpace($resolvedApiKey)) { $resolvedApiKey = $env:PSGALLERY_API } $publishParams = @{ Path = $projectInfo.OutputModuleDir Repository = $Repository Verbose = $true } if (-not [string]::IsNullOrWhiteSpace($resolvedApiKey)) { $publishParams.ApiKey = $resolvedApiKey } Publish-PSResource @publishParams return } Publish-MTLocal -ModuleDirectoryPath $ModuleDirectoryPath } # Source: src/private/ReadAwesomeHost.ps1 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 } # Source: src/private/ResetProjectDist.ps1 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' } } # Source: src/private/TestProjectSchema.ps1 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 } # Source: src/private/WriteMessage.ps1 function Write-Message { [CmdletBinding()] param ( [Parameter(ValueFromPipeline)] [string] $Text, [ValidateSet('Yello', 'Blue', 'Green')] [string] $color = 'Blue' ) PROCESS { Write-Host $Text -ForegroundColor $color } } |