Public/Update-ModuleManifestExports.ps1
|
<# .SYNOPSIS Sets FunctionsToExport in a module manifest to the functions defined in the module's Public folder. .DESCRIPTION Update-ModuleManifestExports parses every .ps1 file under <ModulePath>/Public (including subfolders such as Public/Generated/Functions) and collects the names of the functions defined at the top level of each file. Pester test files (*.Tests.ps1) and files in Tests folders are ignored. The FunctionsToExport entry of the manifest is then replaced with that sorted list (or added when the manifest has none). The rest of the manifest, its line endings and its UTF-8 byte order mark are left as they are. The manifest is only written when the list changes. .PARAMETER ModulePath The module folder (the folder that contains the manifest and the Public folder), or the path of the manifest itself. Accepts pipeline input, including objects with a FullName or Path property. .PARAMETER ManifestPath The manifest to update. Defaults to <folder name>.psd1 in ModulePath. .EXAMPLE Update-ModuleManifestExports -ModulePath ./modules/my.module Sets FunctionsToExport in ./modules/my.module/my.module.psd1 to the functions in ./modules/my.module/Public. .EXAMPLE Update-ModuleManifestExports -ModulePath ./modules/my.module -WhatIf Shows whether the manifest would change without writing it. .INPUTS System.String. The module path can be passed by pipeline. .OUTPUTS System.Management.Automation.PSCustomObject with ManifestPath, FunctionsToExport (the new list), Changed (the list differs from the manifest) and Updated (the manifest was written). .NOTES Author: TheCodeSaiyan Uses the PowerShell parser, so it needs Full Language Mode. New-FunctionsFromSwagger calls it after generating functions. #> function Update-ModuleManifestExports { [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseSingularNouns', '', Justification = 'FunctionsToExport is a list; the name describes the manifest field it updates.')] [CmdletBinding(SupportsShouldProcess = $true)] [OutputType([pscustomobject])] param( [Parameter(Mandatory = $true, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)] [Alias('FullName', 'Path')] [ValidateNotNullOrEmpty()] [string]$ModulePath, [string]$ManifestPath ) begin { $TelemetryArgs = @{ ModuleName = $MyInvocation.MyCommand.Module.Name ModuleVersion = [string]$MyInvocation.MyCommand.Module.Version CommandName = $MyInvocation.MyCommand.Name ExecutionID = [guid]::NewGuid().ToString() } Invoke-TelemetryCollection @TelemetryArgs -Stage Start -ClearTimer $Failed = $false } process { try { $targetManifest = $ManifestPath $resolvedPath = (Resolve-Path -LiteralPath $ModulePath -ErrorAction Stop).ProviderPath if (Test-Path -LiteralPath $resolvedPath -PathType Leaf) { if (-not $targetManifest) { $targetManifest = $resolvedPath } $moduleFolder = Split-Path -Path $resolvedPath -Parent } else { $moduleFolder = $resolvedPath if (-not $targetManifest) { $targetManifest = Join-Path -Path $moduleFolder -ChildPath ((Split-Path -Path $moduleFolder -Leaf) + '.psd1') } } if (-not (Test-Path -LiteralPath $targetManifest -PathType Leaf)) { throw "Module manifest not found: $targetManifest" } $targetManifest = (Resolve-Path -LiteralPath $targetManifest).ProviderPath # Functions defined at the top level of each public script (nested helper functions are not exported) $names = New-Object System.Collections.Generic.List[string] $publicPath = Join-Path -Path $moduleFolder -ChildPath 'Public' if (Test-Path -LiteralPath $publicPath -PathType Container) { $publicFiles = Get-ChildItem -LiteralPath $publicPath -Filter '*.ps1' -File -Recurse | Where-Object { $_.Name -notlike '*.Tests.ps1' -and ($_.FullName.Substring($publicPath.Length) -split '[\\/]' | Where-Object { $_ -eq 'Tests' }).Count -eq 0 } foreach ($file in $publicFiles) { $tokens = $null $parseErrors = $null $ast = [System.Management.Automation.Language.Parser]::ParseFile($file.FullName, [ref]$tokens, [ref]$parseErrors) if (@($parseErrors).Count -gt 0) { Write-Warning "Skipping '$($file.FullName)': it does not parse ($($parseErrors[0].Message))." continue } $definitions = $ast.FindAll({ param($node) $node -is [System.Management.Automation.Language.FunctionDefinitionAst] }, $false) foreach ($definition in $definitions) { if (-not $names.Contains($definition.Name)) { $names.Add($definition.Name) } } } } $functionNames = @($names | Sort-Object) # Locate the FunctionsToExport value in the manifest hashtable $content = Get-Content -LiteralPath $targetManifest -Raw -Encoding UTF8 if ($null -eq $content) { $content = '' } $tokens = $null $parseErrors = $null $manifestAst = [System.Management.Automation.Language.Parser]::ParseInput($content, [ref]$tokens, [ref]$parseErrors) if (@($parseErrors).Count -gt 0) { throw "The manifest '$targetManifest' does not parse: $($parseErrors[0].Message)" } $hashtable = $manifestAst.Find({ param($node) $node -is [System.Management.Automation.Language.HashtableAst] }, $false) if (-not $hashtable) { throw "The manifest '$targetManifest' does not contain a hashtable." } $newLine = if ($content.Contains("`r`n")) { "`r`n" } else { "`n" } $entry = $null foreach ($pair in $hashtable.KeyValuePairs) { if ($pair.Item1.Extent.Text.Trim("'", '"') -eq 'FunctionsToExport') { $entry = $pair; break } } $current = @() if ($entry) { $current = @($entry.Item2.FindAll({ param($node) $node -is [System.Management.Automation.Language.StringConstantExpressionAst] }, $true) | ForEach-Object Value) $keyLine = $content.Substring(0, $entry.Item1.Extent.StartOffset) $indent = ($keyLine.Substring($keyLine.LastIndexOf("`n") + 1)) -replace '\S.*$', '' } else { $indent = ' ' } if ($functionNames.Count -eq 0) { $valueText = '@()' } else { $itemLines = @($functionNames | ForEach-Object { $indent + ' ' + "'" + $_.Replace("'", "''") + "'" }) $valueText = '@(' + $newLine + ($itemLines -join (',' + $newLine)) + $newLine + $indent + ')' } if ($entry) { $start = $entry.Item2.Extent.StartOffset $end = $entry.Item2.Extent.EndOffset $updatedContent = $content.Substring(0, $start) + $valueText + $content.Substring($end) } else { # Add the entry before the closing brace of the manifest hashtable $closing = $hashtable.Extent.EndOffset - 1 $before = $content.Substring(0, $closing).TrimEnd() $updatedContent = $before + $newLine + $indent + 'FunctionsToExport = ' + $valueText + $newLine + $content.Substring($closing) } $changed = -not $entry -or ((@($current) -join "`n") -cne ($functionNames -join "`n")) $updated = $false if ($changed -and $PSCmdlet.ShouldProcess($targetManifest, "Set FunctionsToExport to $($functionNames.Count) function(s)")) { Write-Utf8TextFile -Path $targetManifest -Content $updatedContent -Utf8Bom:(Test-Utf8Bom -Path $targetManifest) $updated = $true } [pscustomobject]@{ ManifestPath = $targetManifest FunctionsToExport = $functionNames Changed = [bool]$changed Updated = $updated } } catch { $Failed = $true Write-Error -ErrorRecord $_ } } end { Invoke-TelemetryCollection @TelemetryArgs -Stage End -Failed $Failed } } |