Netscoot.Core/Public/Move-PowerShellModule.ps1

function Move-PowerShellModule {
    <#
    .SYNOPSIS
        Move a PowerShell module folder and update the script paths that reference it or that
        it uses.
 
    .DESCRIPTION
        Moves a module directory (git mv when tracked). Scripts elsewhere that import the module
        by path (Import-Module, `using module`) or dot-source one of its files are repointed, and
        the module's own .ps1/.psm1 paths to files outside it are rebased, with the same precise,
        BOM-preserving edits as Move-PowerShellScript. The manifest's entries are module-relative,
        so the .psd1 is left unchanged and only validated with Test-ModuleManifest.
 
        Limits (warned, not fixed): a path built from variables is reported as a possible dynamic
        reference; any path computed at runtime cannot be reconciled automatically.
 
    .PARAMETER ModulePath
        Path to the module folder, or directly to its .psd1 manifest. Accepts pipeline input (a
        path string or a Get-ChildItem/Get-Item item; other object types are rejected).
 
    .PARAMETER Destination
        Where to move the module folder, following `git mv` rules: An existing directory means move
        into it (keeping the name); otherwise it is the module's new folder path.
 
    .PARAMETER Force
        Proceed with a plain file move when git is unavailable instead of aborting. The plain move is a PowerShell `Move-Item` (same on every platform) and does not preserve git history.
 
    .PARAMETER NoJournal
        Skip recording this move in the undo journal for this call, even when journaling is enabled
        (Undo-Netscoot will not see this move).
 
    .OUTPUTS
        Netscoot.PSModuleMoveResult
 
    .EXAMPLE
        # Preview; lists the callers and module paths it will update
        Move-PowerShellModule -ModulePath ./tools/Mayo -Destination ./modules/Mayo -WhatIf
        # Move it for real
        Move-PowerShellModule -ModulePath ./tools/Mayo -Destination ./modules/Mayo
        # Point at the .psd1 instead of the folder - same result
        Move-PowerShellModule -ModulePath ./tools/Mayo/Mayo.psd1 -Destination ./modules/Mayo
    #>

    [CmdletBinding(SupportsShouldProcess, ConfirmImpact = 'High')]
    [OutputType('Netscoot.PSModuleMoveResult')]
    param(
        [Parameter(Mandatory, Position = 0, ValueFromPipeline)]
        [Netscoot.PathInputTransform()]
        [ValidateNotNullOrEmpty()]
        [string]$ModulePath,

        [Parameter(Mandatory)]
        [ValidateNotNullOrEmpty()]
        [string]$Destination,
        [switch]$Force,
        [switch]$NoJournal
    )

    process {
    $src = Resolve-FullPath $ModulePath
    if ($src -match '\.psd1$') {
        $manifestName = Split-Path -Leaf $src
        $moduleDir    = Split-Path -Parent $src
    } else {
        $moduleDir = $src
        $manifest  = Get-ChildItem -LiteralPath $moduleDir -Filter '*.psd1' | Select-Object -First 1
        if (-not $manifest) {
            $PSCmdlet.WriteError([System.Management.Automation.ErrorRecord]::new(
                    [System.IO.FileNotFoundException]::new("No .psd1 manifest found in $moduleDir"),
                    'ManifestNotFound', [System.Management.Automation.ErrorCategory]::ObjectNotFound, $moduleDir))
            return
        }
        $manifestName = $manifest.Name
    }

    # git mv semantics: an existing destination directory means "move the module folder into it";
    # otherwise Destination is the module's new folder path.
    $newDir = Resolve-MoveTarget -Source $moduleDir -Destination $Destination
    if (Test-Path -LiteralPath $newDir) {
        $PSCmdlet.WriteError([System.Management.Automation.ErrorRecord]::new(
                [System.IO.IOException]::new("Destination already exists: $newDir"),
                'DestinationExists', [System.Management.Automation.ErrorCategory]::ResourceExists, $newDir))
        return
    }

    $newManifest = Join-Path $newDir $manifestName
    $repoRoot = Get-RepositoryRoot -StartPath $moduleDir
    $moduleName = [System.IO.Path]::GetFileNameWithoutExtension($manifestName)

    # Paths from elsewhere into the module (Import-Module by path, dot-sourcing a module file) and
    # paths from the module's own files to outside it both break when the folder moves. Manifest
    # entries are module-relative, so the .psd1 itself needs no change.
    $incoming = @()
    $outgoing = @()
    $dynamicRefs = @()
    foreach ($f in (Find-PowerShellFiles -Root $repoRoot)) {
        $scan = Get-PowerShellScriptReferences -File $f.FullName
        $fileInside = Test-PathUnder $f.FullName $moduleDir
        foreach ($ref in $scan.Paths) {
            $targetInside = (Test-PathEqual $ref.Target $moduleDir) -or (Test-PathUnder $ref.Target $moduleDir)
            if ($fileInside -and -not $targetInside) { $outgoing += $ref }
            elseif (-not $fileInside -and $targetInside) { $incoming += $ref }
        }
        if (-not $fileInside) { $dynamicRefs += @($scan.Dynamic | Where-Object { $_.Raw -match [regex]::Escape($moduleName) }) }
    }
    $inNewDir = {
        param($Path)
        $rel = $Path.Substring($moduleDir.Length).TrimStart('\', '/')
        if ($rel) { Join-Path $newDir $rel } else { $newDir }
    }

    Write-MovePlan -Cmdlet $PSCmdlet -Caption "Move-PowerShellModule $manifestName $moduleDir -> $newDir" -Items ([ordered]@{
            'callers to update'                  = @($incoming | ForEach-Object { "$(Split-Path -Leaf $_.File): $($_.Raw)" })
            'module paths pointing outside to rebase' = @($outgoing | ForEach-Object { "$(Split-Path -Leaf $_.File): $($_.Raw)" })
        })
    foreach ($d in $dynamicRefs) {
        Write-Warning "Possible dynamic reference to module $moduleName in $($d.File): `"$($d.Raw)`" - could not resolve statically; verify by hand."
    }

    $performed = $false
    $skippedCount = 0

    if ($PSCmdlet.ShouldProcess("$moduleDir -> $newDir", 'Move PowerShell module and update the paths that reference it')) {
        $ctx = Resolve-MoveContext -Cmdlet $PSCmdlet -Force:$Force -TargetForError $moduleDir
        if (-not $ctx) { return }

        $pointAt = { param($Ref, $Target) [void]$Ref.PointAt($Target) }
        $followFile = { param($Ref, $NewFile) [void]$Ref.FollowFile($NewFile) }
        $items = @()
        foreach ($ref in $incoming) {
            $target = & $inNewDir $ref.Target
            $items += New-MoveItem -Description "caller $(Split-Path -Leaf $ref.File): $($ref.Raw) -> $($ref.RawPointingAt($target))" `
                -Reattach $pointAt -ReattachArgs @($ref, $target)
        }
        foreach ($ref in $outgoing) {
            $newFile = & $inNewDir $ref.File
            $items += New-MoveItem -Description "module file $(Split-Path -Leaf $ref.File): $($ref.Raw) -> $($ref.RawFollowing($newFile))" `
                -Reattach $followFile -ReattachArgs @($ref, $newFile)
        }

        $move = { param($UseGit, $Src, $Dst, $Repository) Move-PathTracked -UseGit $UseGit -Source $Src -Destination $Dst -RepositoryRoot $Repository }
        $backup = @($incoming | ForEach-Object { $_.File }) + @($outgoing | ForEach-Object { $_.File })
        $planResult = Invoke-MovePlan -Caption "Move module $manifestName" -Items $items -Move $move `
            -MoveArgs @($ctx.UseGit, $moduleDir, $newDir, $repoRoot) `
            -BackupPath $backup -Rollback $move -RollbackArgs @($ctx.UseGit, $newDir, $moduleDir, $repoRoot) `
            -RepositoryRoot $repoRoot -Command 'Move-PowerShellModule' -Engine 'powershell' -Source $moduleDir -Destination $newDir `
            -UndoParams @{ ModulePath = $newDir; Destination = $moduleDir; Force = [bool]$Force } -NoJournal:$NoJournal
        $performed = $true
        $skippedCount = $planResult.Skipped

        if (-not (Test-ModuleManifest -Path $newManifest -ErrorAction SilentlyContinue)) {
            Write-Warning "Test-ModuleManifest reported problems for $newManifest"
        }
    }

    New-MoveResult -TypeName 'Netscoot.PSModuleMoveResult' -Engine 'powershell' -Source $moduleDir -Destination $newDir `
        -Performed $performed -SkippedCount $skippedCount -Extra ([ordered]@{ Manifest = $manifestName })
    }
}