Public/Utilities/New-Package.ps1

function New-Package {
    <#
        .SYNOPSIS
        Package Alteryx tool(s)

        .DESCRIPTION
        Create an Alteryx installer package (.YXI) for a specified set of tools

        .NOTES
        File name: New-Package.psm1
        Author: Florian Carrier
        Creation date: 2021-06-15
        Last modified: 2021-07-05

        .LINK
        https://www.powershellgallery.com/packages/PSAYX

        .LINK
        https://help.alteryx.com/current/developer-help/package-tool
    #>

    [CmdletBinding (
        SupportsShouldProcess = $true
    )]
    Param (
        [Parameter (
            Position    = 1,
            Mandatory   = $true,
            HelpMessage = "Path to the directory containing the tool(s) to package"
        )]
        [ValidateNotNullOrEmpty ()]
        [String]
        $Path,
        [Parameter (
            Position    = 2,
            Mandatory   = $false,
            HelpMessage = "Name of the package"
        )]
        [ValidateNotNullOrEmpty ()]
        [String]
        $Name,
        [Parameter (
            Position    = 3,
            Mandatory   = $false,
            HelpMessage = "Author"
        )]
        [ValidateNotNullOrEmpty ()]
        [String]
        $Author = [Environment]::UserName,
        [Parameter (
            Position    = 4,
            Mandatory   = $false,
            HelpMessage = "Version number"
        )]
        [ValidatePattern ("\d+\.\d+\.\d+")]
        [String]
        $Version,
        [Parameter (
            Position    = 5,
            Mandatory   = $false,
            HelpMessage = "Tool category"
        )]
        [ValidateNotNullOrEmpty ()]
        [String]
        $CategoryName,
        [Parameter (
            Position    = 6,
            Mandatory   = $false,
            HelpMessage = "Description"
        )]
        [ValidateNotNullOrEmpty ()]
        [String]
        $Description,
        [Parameter (
            Position    = 7,
            Mandatory   = $false,
            HelpMessage = "Package icon"
        )]
        [ValidateNotNullOrEmpty ()]
        [String]
        $Icon,
        [Parameter (
            Position    = 8,
            Mandatory   = $false,
            HelpMessage = "Package compression level"
        )]
        [ValidateSet (
            "Fastest",
            "NoCompression",
            "Optimal"
        )]
        [String]
        $CompressionLevel = "Optimal",
        [Parameter (
          HelpMessage = "Switch to enable non-interactive mode"
        )]
        [Switch]
        $Unattended
    )
    Begin {
        # Get global preference variables
        Get-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState
        # Define configuration values
        $Properties = [Ordered]@{
            "Author"        = $Author
            "CategoryName"  = $CategoryName
            "Description"   = $Description
            "Icon"          = $Icon
            "Name"          = $Name
            "ToolVersion"   = $Version
        }
        # Default version number
        $DefaultVersion = "1.0.0"
        # Parent directory
        $ParentDirectory = Split-Path -Path $Path -Parent
        # Default package name
        if (-Not $PSBoundParameters.ContainsKey("Name")) {
            $Properties.Name = Split-Path -Path $Path -Leaf
        }
        # Configuration file name
        $Configuration = "config.xml"
        # XML configuration template
        $Template = '<?xml version="1.0"?>
<Configuration>
    <Properties>
        <MetaInfo>
            <Icon></Icon>
            <Name></Name>
            <CategoryName></CategoryName>
            <ToolVersion></ToolVersion>
            <Author></Author>
            <Description></Description>
        </MetaInfo>
    </Properties>
</Configuration>'

    }
    Process {
        Write-Log -Type "CHECK" -Message "Start creation of package ""$($Properties.Name)"""
        # Check if XML file already exist
        $XML = New-Object -TypeName "System.XML.XMLDocument"
        $ConfigurationPath = Join-Path -Path $Path -ChildPath $Configuration
        if (Test-Path -Path $ConfigurationPath) {
            Write-Log -Type "DEBUG" -Message "Load existing configuration file"
            try {
                # Try to import content as XML
                $XML.Load($ConfigurationPath)
                Write-Log -Type "DEBUG" -Message $XML.OuterXml
                # TODO Validate schema
                # Retrieve existing values
                # ! Create dummy copy of list of properties to prevent error "Collection was modified; enumeration operation may not execute."
                $DummyProperties = Copy-OrderedHashtable -Hashtable $Properties
                foreach ($Key in $DummyProperties.Keys) {
                    if (-Not $PSBoundParameters.ContainsKey($Key)) {
                        $XPath  = "Configuration/Properties/MetaInfo/$Key"
                        $Node   = Select-XMLNode -XML $XML -XPath $XPath
                        $Properties.$Key = $Node.InnerText
                        Write-Log -Type "DEBUG" -Message "$Key=$($Properties.$Key)"
                        if ($Key -eq "Name") {
                            Write-Log -Type "INFO" -Message "Updating package name to ""$($Properties.$Key)"""
                        }
                    }
                }
            } catch [System.Management.Automation.RuntimeException] {
                Write-Log -Type "DEBUG" -Message $Configuration
                Write-Log -Type "ERROR" -Message "Configuration file could not be loaded"
                Write-Log -Type "DEBUG" -Message "Overwriting configuration from template"
                $XML.LoadXml($Template)
            }
        } else {
            Write-Log -Type "DEBUG" -Message "No configuration file found - loading template"
            $XML.LoadXml($Template)
        }
        # Check version
        if ($null -eq $Properties.ToolVersion) {
            Write-Log -Type "ERROR" -Message "Missing package version number"
            Write-Log -Type "WARN"  -Message "Initialising version $DefaultVersion"
            $Properties.ToolVersion = $DefaultVersion
        } elseif ($Properties.ToolVersion -notmatch "\d+\.\d+\.\d+") {
            Write-Log -Type "ERROR" -Message "Invalid package version number"
            Write-Log -Type "WARN"  -Message "Package version must match semantic versionning format (MAJOR.MINOR.PATCH)"
            Write-Log -Type "WARN"  -Message "Initialising version $DefaultVersion"
            $Properties.ToolVersion = $DefaultVersion
        }
        # Check icon file
        if ($PSBoundParameters.ContainsKey("Icon")) {
            $IconPath = Join-Path -Path $Path -ChildPath $Icon
            if (-Not (Test-Path -Path $IconPath)) {
                Write-Log -Type "DEBUG" -Message $IconPath
                Write-Log -Type "ERROR" -Message "Icon file could not be found"
                Write-Log -Type "WARN"  -Message "Removing invalid icon file reference"
                $Properties.Icon = ""
            }
        } else {
            Write-Log -Type "WARN" -Message "No icon file has been set"
        }
        # Set configuration
        Write-Log -Type "INFO" -Message "Set package configuration"
        foreach ($Value in $Properties.GetEnumerator()) {
            $XPath          = "Configuration/Properties/MetaInfo/$($Value.Name)"
            $Node           = Select-XMLNode -XML $XML -XPath $XPath
            Write-Log -Type "DEBUG" -Message "$($Value.Name)=$($Value.Value)"
            $Node.InnerText = $Value.Value
        }
        # (Over)write configuration file
        Write-Log -Type "DEBUG" -Message $XML.OuterXml
        if ($PSCmdlet.ShouldProcess($ConfigurationPath, "XML.Save")) {
            $XML.Save($ConfigurationPath)
        }
        # Compress-Archive
        $ZIPFile            = [System.String]::Concat($Properties.Name, ".zip")
        $YXIPackage         = [System.String]::Concat($Properties.Name, ".yxi")
        $DestinationPath    = Join-Path -Path $ParentDirectory -ChildPath $ZIPFile
        Compress-Archive -Path (Join-Path -Path $Path -ChildPath "*") -DestinationPath $DestinationPath -CompressionLevel "Optimal" -Force -WhatIf:$WhatIfPreference
        # Rename package to YXI
        if ((Test-Path -Path $DestinationPath) -Or $WhatIfPreference) {
            $PackagePath = Join-Path -Path $ParentDirectory -ChildPath $YXIPackage
            if (Test-Path -Path $PackagePath) {
                Write-Log -Type "WARN" -Message "Path already exists $PackagePath"
                if ($Unattended -Or (Confirm-Prompt -Prompt "Do you want to overwrite the existing package?")) {
                    Remove-Item -Path $PackagePath -WhatIf:$WhatIfPreference
                    Write-Log -Type "WARN" -Message "Overwritting existing package"
                } else {
                    Write-Log -Type "WARN" -Message "Script terminated by user"
                    Write-Log -Type "INFO" -Message "No package was generated" -ErrorCode 0
                }
            }
            if ($PSCmdlet.ShouldProcess($DestinationPath, "Rename-Item")) {
                Rename-Item -Path $DestinationPath -NewName $YXIPackage -Force
            }
            Write-Log -Type "DEBUG" -Message $PackagePath
            Write-Log -Type "CHECK" -Message "Package ""$YXIPackage"" created successfully"
        } else {
            Write-Log -Type "ERROR" -Message "Package creation failed"
        }
    }
}