functions/powershell/module/Update-Psm1FromSource.ps1

function Update-Psm1FromSource {
<#
.SYNOPSIS
Generates a .psm1 module file from function source files and optionally updates the module manifest.
 
.DESCRIPTION
This function scans the `functions` folder of a PowerShell module, filters `.ps1` files using an optional `exclude-files.json`, and assembles a `.psm1` file by dot-sourcing each included function. It also loads the associated compiled DLL (from the `lib` folder), and optionally updates the module manifest with the exported function names.
 
To identify functions to export, a temporary module is created and imported to detect all defined functions before writing the final `.psm1`.
 
.PARAMETER ModuleRoot
The root directory of the module. This path must contain the module folder, a `functions` subfolder, and optionally the `exclude-files.json`.
 
.PARAMETER UpdateManifest
If specified, the associated module manifest (`.psd1`) will be updated with the list of exported functions.
 
.OUTPUTS
No direct output. The function writes the `.psm1` file and optionally updates the `.psd1` manifest file.
 
.EXAMPLE
Update-Psm1FromSource -ModuleRoot "C:\Modules\MyModule" -UpdateManifest
 
Generates `MyModule.psm1` from function files and updates `MyModule.psd1` with exported function names.
 
.NOTES
• Uses a temporary copy of the module to detect exported functions via Import-Module.
• Supports exclusion of specific files or folders via `exclude-files.json`.
• Ensures the associated DLL (`lib\<ModuleName>.dll`) is correctly loaded in the module.
#>

    [CmdletBinding()]
    param (
        [Parameter(Mandatory)]
        [string]$ModuleRoot,

        [string]$Psm1HeaderBlock,
        [string]$Psm1VariablesBlock,

        [switch]$UpdateManifest
    )

    $ModuleRoot = (Resolve-Path -Path $ModuleRoot).Path
    $moduleName = Split-Path -Path $ModuleRoot -Leaf

    $psm1Path = Join-Path $ModuleRoot "$moduleName.psm1"
    $manifestPath = Join-Path $ModuleRoot "$moduleName.psd1"
    $functionsFolder = Join-Path $ModuleRoot 'functions'
    # $dllRelativePath = "lib\$moduleName.dll"
    $ExcludeListFile = Join-Path $ModuleRoot 'exclude-files.json'

    # Erstelle temporäres Verzeichnis
    $tempRoot = (New-TempDirectory -Name $moduleName).FullName
    $tempPsm1Path = Join-Path $tempRoot "$moduleName.psm1"

    # Header mit literalem Pfad (kein $PSScriptRoot im Temp-Modul!)
# $header = @"
# #NotRequires -RunAsAdministrator
# "@
# # Load the compiled C# DLL
# Import-Module '$tempRoot\$dllRelativePath'

    # $dllRelativePath = "lib\$moduleName.dll"
    # $tempFilePath = Join-Path -Path $tempRoot -ChildPath $dllRelativePath
    # $tempFileDir = Split-Path -Path $tempFilePath
    # New-Directory -Path $tempFileDir | Out-Null
    # Write-Verbose "Copy $(Split-Path -Leaf $dllRelativePath) to $tempFilePath"
    # Copy-Item -Path $(Join-Path -Path $ModuleRoot -ChildPath $dllRelativePath) -Destination $tempFilePath

    # Dot-source Zeilen sammeln
    $dotSourceLines = @()
    $finalDotSourceLines = @()
    $exportAliases = @()
    $functionFiles = Get-ChildItem -Path $functionsFolder -Recurse -Filter '*.ps1' | Sort-Object FullName
    $functionFiles = Get-FilteredFiles -Files $functionFiles -ExcludeListFile $ExcludeListFile -RootPath $ModuleRoot
    foreach ($file in $functionFiles) {
        $relativePath = $file.FullName.Substring($ModuleRoot.Length).TrimStart('\')
        $dotSourceLines += ". `"$tempRoot\$relativePath`""
        $finalDotSourceLines += (". `$PSScriptRoot\$relativePath` ").TrimEnd()
        
        $tempFilePath = Join-Path -Path $tempRoot -ChildPath $relativePath
        $tempFileDir  = Split-Path -Path $tempFilePath
        New-Directory -Path $tempFileDir | Out-Null
        Write-Verbose "Copy $($file.Name) to $tempFilePath"
        Copy-Item -Path $file.FullName -Destination $tempFilePath

        Write-Verbose "Search for Set-Alias definitions"
        Select-String -Path $file.FullName -Pattern 'Set-Alias\s+-Name\s+(\S+)' | ForEach-Object {
            $aliasMatches = [regex]::Match($_.Line, 'Set-Alias\s+-Name\s+(\S+)')
            if ($aliasMatches.Success) {
                $exportAliases += $aliasMatches.Groups[1].Value
            }
        }
    }

    # Erzeuge temporäre psm1-Datei zum Importieren
    $tempContent = @()
    if (-not ([string]::IsNullOrWhiteSpace($Psm1HeaderBlock))) {
        $tempContent += $Psm1HeaderBlock
        $tempContent += ""
    }

    $tempContent += $dotSourceLines

    if (-not ([string]::IsNullOrWhiteSpace($Psm1VariablesBlock))) {
        $tempContent += ""
        $tempContent += $Psm1VariablesBlock
    }
    
    Set-Content -Path $tempPsm1Path -Value $tempContent -Encoding UTF8

    # Temporär importieren, um Funktionen zu erkennen
    Write-Verbose "Import module $tempPsm1Path"
    Import-Module $tempPsm1Path -Force

    $module = Get-Module -Name $moduleName
    $exportFunctions = $module.ExportedFunctions.Values.Name
    Write-Host "Functions loaded in temp module (Name: $moduleName, loaded from Path: $tempPsm1Path):"
    $exportFunctions

    $exportCmdlets = $module.ExportedCmdlets.Values.Name
    Write-Host "Cmdlets loaded in temp module (Name: $moduleName, loaded from Path: $tempPsm1Path):"
    $exportCmdlets

    Write-Host "Aliases loaded in temp module (Name: $moduleName, loaded from Path: $tempPsm1Path):"
    $exportAliases

    $confirmation = Read-Host "Geladene Funktionsliste, Cmdlets und Aliase korrekt und fortfahren? (y/n)"
    if ($confirmation -ne 'y') {
        throw '.psm1-Update abgebrochen.'
    }

    Remove-Item -Path $tempRoot -Recurse -Force

    # Export-Zeilen schreiben
    $exportLines = @()
    # Functions
    if ($exportFunctions.Count -gt 0) {
        $grouped = $exportFunctions | Sort-Object | Group-Object { ($_ -split '-')[0] }
        foreach ($group in $grouped) {
            $exportLines += ("Export-ModuleMember -Function " + ($group.Group -join ', ')).TrimEnd()
        }
    }
    #Aliases
    if ($exportAliases.Count -gt 0) {
        $grouped = $exportAliases | Sort-Object | Group-Object { ($_ -split '-')[0] }
        foreach ($group in $grouped) {
            $exportLines += ("Export-ModuleMember -Alias " + ($group.Group -join ', ')).TrimEnd()
        }
    }

    # Schreibe neue .psm1-Datei
# $finalHeader = @"
# #NotRequires -RunAsAdministrator
# "@
# # Load the compiled C# DLL
# Write-Verbose "Import `'`$PSScriptRoot\$dllRelativePath`'"
# Import-Module "`$PSScriptRoot\$dllRelativePath"

    $psm1Content = @()

    if (-not ([string]::IsNullOrWhiteSpace($Psm1HeaderBlock))) {
        $psm1Content += $Psm1HeaderBlock
        $psm1Content += ""
    }

    $psm1Content += $finalDotSourceLines
    $psm1Content += ""
    $psm1Content += $exportLines

    if (-not ([string]::IsNullOrWhiteSpace($Psm1VariablesBlock))) {
        $psm1Content += ""
        $psm1Content += $Psm1VariablesBlock
    }

    Set-Content -Path $psm1Path -Value $psm1Content -Encoding UTF8
    
    Write-Host "✅ Updated $psm1Path with $($dotSourceLines.Count) dot-sourced files and $($exportFunctions.Count) exported functions."

    if ($UpdateManifest) {
        Update-ModuleManifest -Path $manifestPath -FunctionsToExport $exportFunctions -CmdletsToExport $exportCmdlets -AliasesToExport $exportAliases
        Write-Host "✅ Updated $manifestPath with $($exportFunctions.Count) functions."
    }
}