Public/Convert-ModuleNameAndReferences.ps1
|
<# .SYNOPSIS Copies a PowerShell module to a new location, optionally renaming the module and updating all references to the new name. .DESCRIPTION This function copies one or more PowerShell module directories to a specified output path. If the module name contains a dot (.), you can replace the first segment of the module name with a specified string. All references to the old module name in the copied text files are updated to the new name, and files named after the module (for example the .psd1 and .psm1) are renamed. Function names (Verb-OldSegmentNoun) and file names containing the segment are also updated. Binary files (files containing NUL characters) are copied unchanged. .PARAMETER ModulePaths An array of paths to the module directories to convert. .PARAMETER OutputPath The directory where the converted modules will be placed. Created if it does not exist. .PARAMETER ReplaceFirstSegmentWith If specified and the module name contains a dot, replaces the first segment of the module name (before the first dot) with this value. The segment is capitalized (PascalCase) when updating function and file names. Empty or whitespace is treated as "no replacement". .PARAMETER Force Overwrites the destination directory if it already exists. .EXAMPLE Convert-ModuleNameAndReferences -ModulePaths 'C:\git\tcs.confluence-1\ps\modules\tcs.confluence' -OutputPath 'C:\working\ModuleConversion' -ReplaceFirstSegmentWith 'X' Copies the module and renames it so that the first segment is replaced with 'X', updating all references in the files. .EXAMPLE Convert-ModuleNameAndReferences -ModulePaths 'C:\modules\my.module' -OutputPath 'C:\out' -ReplaceFirstSegmentWith 'tcs' -Force Overwrites the destination folder if C:\out\tcs.module already exists. .INPUTS System.String[]. Paths to module directories to convert (ModulePaths). .OUTPUTS None. Writes verbose output and creates files and folders under OutputPath. .NOTES Author: TheCodeSaiyan Date: 2025-09-11 Compatible with PowerShell Constrained Language Mode (no [regex] or [string]:: static methods; uses -replace operator and inline regex-literal escaping). #> function Convert-ModuleNameAndReferences { [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseSingularNouns', '', Justification = 'Public command name kept for backward compatibility.')] [CmdletBinding(SupportsShouldProcess = $true)] [OutputType([void])] param( [Parameter(Mandatory = $true)] [string[]]$ModulePaths, [Parameter(Mandatory = $true)] [string]$OutputPath, [string]$ReplaceFirstSegmentWith, [switch]$Force ) $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 try { # Escape for use in -match/-replace (Constrained Language Mode: no [regex]::Escape) $escapePattern = { param([string]$Text) $Text -replace '\\', '\\' -replace '\[', '\[' -replace '\]', '\]' -replace '\(', '\(' -replace '\)', '\)' -replace '\*', '\*' -replace '\+', '\+' -replace '\?', '\?' -replace '\.', '\.' -replace '\^', '\^' -replace '\$', '\$' -replace '\|', '\|' -replace '\{', '\{' -replace '\}', '\}' } if (-not (Test-Path -Path $OutputPath -PathType Container)) { if ($PSCmdlet.ShouldProcess($OutputPath, 'Create output folder')) { $null = New-Item -Path $OutputPath -ItemType Directory -Force -ErrorAction Stop } } foreach ($modulePath in $ModulePaths) { if (-not (Test-Path -Path $modulePath -PathType Container)) { Write-Warning "Module path not found: $modulePath" continue } $moduleName = Split-Path -Path $modulePath -Leaf $hasReplacement = $ReplaceFirstSegmentWith -and ($ReplaceFirstSegmentWith.Trim().Length -gt 0) -and ($moduleName -match '\.') if ($hasReplacement) { $oldFirstSegment = $moduleName -replace '\..*', '' $seg = $ReplaceFirstSegmentWith.Trim() $newModuleName = $moduleName -replace '^[^.]+', $seg $newSegmentCap = $seg.Substring(0, 1).ToUpper() + $seg.Substring(1) $oldFirstSegmentEscaped = & $escapePattern $oldFirstSegment } else { $newModuleName = $moduleName $newSegmentCap = $null $oldFirstSegmentEscaped = $null } $moduleNameEscaped = & $escapePattern $moduleName $destModulePath = Join-Path -Path $OutputPath -ChildPath $newModuleName if (Test-Path -Path $destModulePath) { if ($Force) { if ($PSCmdlet.ShouldProcess($destModulePath, 'Remove-Item')) { Remove-Item -Path $destModulePath -Recurse -Force } } else { Write-Error "Destination module already exists: $destModulePath. Use -Force to overwrite. Skipping." continue } } if ($PSCmdlet.ShouldProcess($destModulePath, 'Copy-Item')) { Copy-Item -Path $modulePath -Destination $destModulePath -Recurse -Force # Replace all references to the old module name in all text files $files = Get-ChildItem -Path $destModulePath -Recurse -File foreach ($file in $files) { $fullName = $file.FullName $content = Get-Content -Path $fullName -Raw -Encoding UTF8 if ($null -eq $content -or $content -match "`0") { # Empty or binary file: nothing to update continue } $updatedContent = $content -replace $moduleNameEscaped, $newModuleName if ($hasReplacement) { # Replace function names: Verb-OldSegmentNoun -> Verb-NewSegmentNoun $updatedContent = $updatedContent -replace "(?<=function\s+\w+-)$oldFirstSegmentEscaped(?=\w*)", $newSegmentCap # Only add segment when noun is absent (e.g. "function Get-" or "Get-.ps1" in text) $updatedContent = $updatedContent -replace '(?<=function\s+\w+-)(?=\s|$|\.)', $newSegmentCap } if ($updatedContent -cne $content -and $PSCmdlet.ShouldProcess($fullName, 'Update references')) { Set-Content -Path $fullName -Value $updatedContent -Encoding UTF8 -NoNewline } } # Rename files named after the module, e.g. old.name.psd1 -> new.name.psd1 if ($newModuleName -ne $moduleName) { $filesToRename = Get-ChildItem -Path $destModulePath -Recurse -File | Where-Object { $_.Name -match $moduleNameEscaped } foreach ($file in $filesToRename) { $newName = $file.Name -replace $moduleNameEscaped, $newModuleName if ($PSCmdlet.ShouldProcess($file.FullName, "Rename to $newName")) { Rename-Item -Path $file.FullName -NewName $newName -Force } } } # Rename files that contain the old first segment after dash if ($hasReplacement) { $filesToRename = Get-ChildItem -Path $destModulePath -Recurse -File | Where-Object { $_.Name -match "-$oldFirstSegmentEscaped" } foreach ($file in $filesToRename) { $newName = $file.Name -replace "-$oldFirstSegmentEscaped", "-$newSegmentCap" if ($PSCmdlet.ShouldProcess($file.FullName, "Rename to $newName")) { Rename-Item -Path $file.FullName -NewName $newName -Force } } # If file name is just Verb-.ps1 (no segment), add new segment $filesToAddSegment = Get-ChildItem -Path $destModulePath -Recurse -File | Where-Object { $_.Name -match '-\.ps1$' } foreach ($file in $filesToAddSegment) { $newName = $file.Name -replace '-\.ps1$', "-$newSegmentCap.ps1" if ($PSCmdlet.ShouldProcess($file.FullName, "Rename to $newName")) { Rename-Item -Path $file.FullName -NewName $newName -Force } } } Write-Verbose "Converted $moduleName to $newModuleName at $destModulePath" } } Invoke-TelemetryCollection @TelemetryArgs -Stage End } catch { Invoke-TelemetryCollection @TelemetryArgs -Stage End -Failed $true -Exception $_ throw } } |