process/replacer.psm1

using module ..\models\bundlerConfig.psm1
using module ..\models\fileInfo.psm1
using module ..\helpers\astHelpers.psm1

using namespace System.Management.Automation.Language

class Replacer {
    [BundlerConfig]$_config
    [AstHelpers]$_astHelper

    Replacer([BundlerConfig]$config) {
        $this._config = $config
        $this._astHelper = [AstHelpers]::new()
    }

    [hashtable]getReplacements([System.Collections.Specialized.OrderedDictionary]$importsMap) {
        $replacementsMap = @{}
        $namespaces = [System.Collections.Specialized.OrderedDictionary]::new()
        $assemblies = [System.Collections.Specialized.OrderedDictionary]::new()
        $addTypes = [System.Collections.Specialized.OrderedDictionary]::new()
        $classes = [System.Collections.Specialized.OrderedDictionary]::new()
        $headerComments = ""
        $paramBlock = ""

        foreach ($file in $importsMap.Values) {
            $replacements = [System.Collections.ArrayList]::new()
            $replacementsMap[$file.id] = $replacements

            if ($file.isEntry) { 
                $headerComments = $this.fillHeaderCommentsReplacements($file, $replacements) 
                $paramBlock = $this.fillRootParamsReplacements($file, $replacements)
            }

            # Fill import replacements
            $this.fillImportReplacements($file, $replacements)

            # Assemblies replacements
            $this.fillAssembliesReplacements($file, $assemblies, $replacements)

            # Namespaces replacements
            $this.fillNamespacesReplacements($file, $namespaces, $replacements)

            # Add-Types replacements
            $this.fillAddTypesReplacements($file, $addTypes, $replacements)

            # Classes replacements
            $this.fillClassesReplacements($file, $classes, $replacements)
        }

        return @{
            headerComments  = $headerComments
            assemblies      = $assemblies
            namespaces      = $namespaces
            paramBlock      = $paramBlock
            addTypes        = $addTypes
            classes         = $classes
            replacementsMap = $replacementsMap
        }
    }

    # Fill import replacements
    [void]fillImportReplacements([FileInfo]$file, [System.Collections.ArrayList]$replacements) {
        $processedImports = @{}

        foreach ($importInfo in $file.imports.Values) {
            $importFile = $importInfo.file
            $importId = $importFile.id
            $moduleName = [System.IO.Path]::GetFileNameWithoutExtension($importFile.Path)
            $value = ""
            $replacement = @{
                Start  = $importInfo.ImportAst.Extent.StartOffset
                Length = $importInfo.ImportAst.Extent.EndOffset - $importInfo.ImportAst.Extent.StartOffset
                # replace whole dot-import statement
                Value  = $value
            }

            $replacements.Add($replacement)

            # Not import types (Classes, interfaces, structs, enums)
            if ($importFile.typesOnly) { continue }

            if ($importInfo.type -eq 'dot') {
                #$replacement.Value = '. ($ExecutionContext.SessionState.PSVariable.GetValue("' + $this._config.modulesSourceMapVarName + '"))["' + $importId + '"]'
                $replacement.Value = '. $global:' + $this._config.modulesSourceMapVarName + '["' + $importId + '"]' 
            }
            elseif ($importInfo.type -eq 'ampersand') {
                #$replacement.Value = '& ($ExecutionContext.SessionState.PSVariable.GetValue("' + $this._config.modulesSourceMapVarName + '"))["' + $importId + '"]'
                $replacement.Value = '& $global:' + $this._config.modulesSourceMapVarName + '["' + $importId + '"]' 
            }
            elseif ($importInfo.type -eq 'using') {
                #$replacement.Value = 'Import-Module (New-Module -ScriptBlock $ExecutionContext.SessionState.PSVariable.GetValue("' + $this._config.modulesSourceMapVarName + '")["' + $importId + '"]) -Force -DisableNameChecking'
                $replacement.Value = 'Import-Module (New-Module -Name ' + $moduleName + ' -ScriptBlock $global:' + $this._config.modulesSourceMapVarName + '["' + $importId + '"]) -DisableNameChecking' 
            }
            elseif ($importInfo.type -eq 'module') {
                # Import-Module can be passed a paths array. We replace one import to many imports.
                $importParams = $this._astHelper.GetNamedParametersMap($importInfo.ImportAst)
                $importParams["DisableNameChecking"] = $null
                $paramsStr = $this._astHelper.ConvertParamsAstMapToString($importParams)

                #$value = 'Import-Module (New-Module -ScriptBlock $ExecutionContext.SessionState.PSVariable.GetValue("' + $this._config.modulesSourceMapVarName + '")["' + $importId + '"])' + $paramsStr
                $value = 'Import-Module (New-Module -Name ' + $moduleName + ' -ScriptBlock $global:' + $this._config.modulesSourceMapVarName + '["' + $importId + '"])' + $paramsStr
                if ($processedImports.ContainsKey($importInfo.ImportAst)) {
                    $replacement = $processedImports[$importInfo.ImportAst]
                    $replacement.Value += [Environment]::NewLine + $value
                }
                else {
                    $replacement.Value = $value
                }
            } 
        }
    }

    # Fill replacements for assemblies
    [void]fillAssembliesReplacements([FileInfo]$file, [System.Collections.Specialized.OrderedDictionary]$assemblies, [System.Collections.ArrayList]$replacements) {
        $usingStatements = $file.Ast.FindAll( { $args[0] -is [UsingStatementAst] -and $args[0].UsingStatementKind -eq "Assembly" }, $false)
        foreach ($usingStatement in $usingStatements) {
            $assemblies[$usingStatement.Name.Extent.ToString()] = "using assembly $($usingStatement.Name.Extent.ToString())"
            $replacements.Add(@{start = $usingStatement.Extent.StartOffset; Length = $usingStatement.Extent.EndOffset - $usingStatement.Extent.StartOffset; value = "" })
        }
    }


    # Fill replacements for namespaces
    [void]fillNamespacesReplacements([FileInfo]$file, [System.Collections.Specialized.OrderedDictionary]$namespaces, [System.Collections.ArrayList]$replacements) {
        $usingStatements = $file.Ast.FindAll( { $args[0] -is [UsingStatementAst] -and $args[0].UsingStatementKind -eq "Namespace" }, $false)
        foreach ($usingStatement in $usingStatements) {
            $namespaces[$usingStatement.Name.Value] = "using namespace $($usingStatement.Name.Value)"
            $replacements.Add(@{start = $usingStatement.Extent.StartOffset; Length = $usingStatement.Extent.EndOffset - $usingStatement.Extent.StartOffset; value = "" })
        }
    }

    # Fill replacements for Add-Type
    [void]fillAddTypesReplacements([FileInfo]$file, [System.Collections.Specialized.OrderedDictionary]$addTypes, [System.Collections.ArrayList]$replacements) {
        $usingStatements = $file.Ast.FindAll( { $args[0] -is [CommandAst] -and $args[0].GetCommandName() -eq "Add-Type" }, $false)
        foreach ($usingStatement in $usingStatements) {
            $text = $usingStatement.Extent.Text
            $addTypes[$text] = $text
            $replacements.Add(@{start = $usingStatement.Extent.StartOffset; Length = $usingStatement.Extent.EndOffset - $usingStatement.Extent.StartOffset; value = "" })
        }
    }

    # Fill classes replacements
    [void]fillClassesReplacements([FileInfo]$file, [System.Collections.Specialized.OrderedDictionary]$classes, [System.Collections.ArrayList]$replacements) {
        $typeDefinitions = $file.Ast.FindAll( { $args[0] -is [TypeDefinitionAst] }, $false)
        foreach ($typeDefinition in $typeDefinitions) {
            if ($classes.Contains($typeDefinition.Name)) { Write-Host " Duplicate class name: '$($typeDefinition.Name)' in file: $($file.path)" -ForegroundColor Orange }
            $classes[$typeDefinition.Name] = $typeDefinition.Extent.Text
            $replacements.Add(@{start = $typeDefinition.Extent.StartOffset; Length = $typeDefinition.Extent.EndOffset - $typeDefinition.Extent.StartOffset; value = "" })
        }
    }

    # Fill replacements and extract header comments
    [string]fillHeaderCommentsReplacements([FileInfo]$file, [System.Collections.ArrayList]$replacements) {
        if (-not $file.isEntry -or -not $this._config.keepHeaderComments) { return "" }
        $tokenKind = [System.Management.Automation.Language.TokenKind]
        $header = ""
        $headerEnd = 0
        foreach ($token in $file.tokens) {
            if ($token.Kind -ne $tokenKind::Comment -and $token.Kind -ne $tokenKind::NewLine) { break }
            $headerEnd = $token.Extent.EndOffset
            $header += $token.Extent.Text
        }
        if ($header) {
            $replacements.Add(@{start = 0; Length = $headerEnd; value = "" })
            return $header.Trim()
        }

        return ""
    }

    # Fill replacements and extract param block for entry file
    [string]fillRootParamsReplacements([FileInfo]$file, [System.Collections.ArrayList]$replacements) {
        if (-not $file.isEntry -or -not $file.Ast.ParamBlock) { return "" }
        $fileAst = $file.Ast
        $source = $fileAst.Extent.Text

        $startOffset = $fileAst.ParamBlock.Extent.StartOffset
        $endOffset = $fileAst.ParamBlock.Extent.EndOffset

        if ($fileAst.ParamBlock.Attributes) { 
            $startOffset = $fileAst.ParamBlock.Attributes[0].Extent.StartOffset
        }

        $replacements.Add(@{start = $startOffset; Length = $endOffset - $startOffset; value = "" })
        return ($source.Substring($startOffset, $endOffset - $startOffset)).Trim()
    }
}