├── .github └── workflows │ └── TestBuildAndPublish.yml ├── CHANGELOG.md ├── Compress-ScriptBlock.min.gzip.ps1 ├── Compress-ScriptBlock.min.ps1 ├── Compress-ScriptBlock.ps1 ├── GitHub ├── Actions │ └── PSMinifier.ps1 └── Jobs │ └── MinifyPSMinifier.psd1 ├── LICENSE ├── Minify.psx.ps1 ├── PSMinifier.GitHubAction.PSDevOps.ps1 ├── PSMinifier.psd1 ├── PSMinifier.psm1 ├── PSMinifier.tests.ps1 ├── README.md ├── action.yml └── en-us └── About_PSMinifier.help.txt /.github/workflows/TestBuildAndPublish.yml: -------------------------------------------------------------------------------- 1 |  2 | name: Test Build And Publish 3 | on: 4 | workflow_dispatch: 5 | push: 6 | jobs: 7 | PowerShellStaticAnalysis: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - name: InstallScriptCop 11 | id: InstallScriptCop 12 | shell: pwsh 13 | run: | 14 | [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 15 | Install-Module -Name ScriptCop -Repository PSGallery -Force -Scope CurrentUser 16 | Import-Module ScriptCop -Force -PassThru 17 | - name: InstallPSScriptAnalyzer 18 | id: InstallPSScriptAnalyzer 19 | shell: pwsh 20 | run: | 21 | [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 22 | Install-Module -Name PSScriptAnalyzer -Repository PSGallery -Force -Scope CurrentUser 23 | Import-Module PSScriptAnalyzer -Force -PassThru 24 | - name: InstallPSDevOps 25 | id: InstallPSDevOps 26 | shell: pwsh 27 | run: | 28 | [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 29 | Install-Module -Name PSDevOps -Repository PSGallery -Force -Scope CurrentUser 30 | Import-Module PSDevOps -Force -PassThru 31 | - name: Check out repository 32 | uses: actions/checkout@v2 33 | - name: RunScriptCop 34 | id: RunScriptCop 35 | shell: pwsh 36 | run: | 37 | $Parameters = @{} 38 | $Parameters.ModulePath = ${env:ModulePath} 39 | foreach ($k in @($parameters.Keys)) { 40 | if ([String]::IsNullOrEmpty($parameters[$k])) { 41 | $parameters.Remove($k) 42 | } 43 | } 44 | Write-Host "::debug:: RunScriptCop $(@(foreach ($p in $Parameters.GetEnumerator()) {'-' + $p.Key + ' ' + $p.Value}) -join ' ')" 45 | & {param([string]$ModulePath) 46 | Import-Module ScriptCop, PSDevOps -PassThru | Out-Host 47 | 48 | if (-not $ModulePath) { 49 | $orgName, $moduleName = $env:GITHUB_REPOSITORY -split "/" 50 | $ModulePath = ".\$moduleName.psd1" 51 | } 52 | if ($ModulePath -like '*PSDevOps*') { 53 | Remove-Module PSDeVOps # If running ScriptCop on PSDeVOps, we need to remove the global module first. 54 | } 55 | 56 | 57 | $importedModule =Import-Module $ModulePath -Force -PassThru 58 | 59 | $importedModule | Out-Host 60 | 61 | $importedModule | 62 | Test-Command | 63 | Tee-Object -Variable scriptCopIssues | 64 | Out-Host 65 | 66 | foreach ($issue in $scriptCopIssues) { 67 | Write-GitHubWarning -Message "$($issue.ItemWithProblem): $($issue.Problem)" 68 | } 69 | } @Parameters 70 | - name: RunPSScriptAnalyzer 71 | id: RunPSScriptAnalyzer 72 | shell: pwsh 73 | run: | 74 | Import-Module PSScriptAnalyzer, PSDevOps -PassThru | Out-Host 75 | $invokeScriptAnalyzerSplat = @{Path='.\'} 76 | if ($ENV:PSScriptAnalyzer_Recurse) { 77 | $invokeScriptAnalyzerSplat.Recurse = $true 78 | } 79 | $result = Invoke-ScriptAnalyzer @invokeScriptAnalyzerSplat 80 | 81 | foreach ($r in $result) { 82 | if ('information', 'warning' -contains $r.Severity) { 83 | Write-GitHubWarning -Message "$($r.RuleName) : $($r.Message)" -SourcePath $r.ScriptPath -LineNumber $r.Line -ColumnNumber $r.Column 84 | } 85 | elseif ($r.Severity -eq 'Error') { 86 | Write-GitHubError -Message "$($r.RuleName) : $($r.Message)" -SourcePath $r.ScriptPath -LineNumber $r.Line -ColumnNumber $r.Column 87 | } 88 | } 89 | TestPowerShellOnLinux: 90 | runs-on: ubuntu-latest 91 | steps: 92 | - name: InstallPester 93 | id: InstallPester 94 | shell: pwsh 95 | run: | 96 | $Parameters = @{} 97 | $Parameters.PesterMaxVersion = ${env:PesterMaxVersion} 98 | foreach ($k in @($parameters.Keys)) { 99 | if ([String]::IsNullOrEmpty($parameters[$k])) { 100 | $parameters.Remove($k) 101 | } 102 | } 103 | Write-Host "::debug:: InstallPester $(@(foreach ($p in $Parameters.GetEnumerator()) {'-' + $p.Key + ' ' + $p.Value}) -join ' ')" 104 | & {<# 105 | .Synopsis 106 | Installs Pester 107 | .Description 108 | Installs Pester 109 | #> 110 | param( 111 | # The maximum pester version. Defaults to 4.99.99. 112 | [string] 113 | $PesterMaxVersion = '4.99.99' 114 | ) 115 | [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 116 | Install-Module -Name Pester -Repository PSGallery -Force -Scope CurrentUser -MaximumVersion $PesterMaxVersion -SkipPublisherCheck -AllowClobber 117 | Import-Module Pester -Force -PassThru -MaximumVersion $PesterMaxVersion} @Parameters 118 | - name: Check out repository 119 | uses: actions/checkout@v2 120 | - name: RunPester 121 | id: RunPester 122 | shell: pwsh 123 | run: | 124 | $Parameters = @{} 125 | $Parameters.ModulePath = ${env:ModulePath} 126 | $Parameters.PesterMaxVersion = ${env:PesterMaxVersion} 127 | foreach ($k in @($parameters.Keys)) { 128 | if ([String]::IsNullOrEmpty($parameters[$k])) { 129 | $parameters.Remove($k) 130 | } 131 | } 132 | Write-Host "::debug:: RunPester $(@(foreach ($p in $Parameters.GetEnumerator()) {'-' + $p.Key + ' ' + $p.Value}) -join ' ')" 133 | & {<# 134 | .Synopsis 135 | Runs Pester 136 | .Description 137 | Runs Pester tests after importing a PowerShell module 138 | #> 139 | param( 140 | # The module path. If not provided, will default to the second half of the repository ID. 141 | [string] 142 | $ModulePath, 143 | # The Pester max version. By default, this is pinned to 4.99.99. 144 | [string] 145 | $PesterMaxVersion = '4.99.99' 146 | ) 147 | 148 | $global:ErrorActionPreference = 'continue' 149 | $global:ProgressPreference = 'silentlycontinue' 150 | 151 | $orgName, $moduleName = $env:GITHUB_REPOSITORY -split "/" 152 | if (-not $ModulePath) { $ModulePath = ".\$moduleName.psd1" } 153 | $importedPester = Import-Module Pester -Force -PassThru -MaximumVersion $PesterMaxVersion 154 | $importedModule = Import-Module $ModulePath -Force -PassThru 155 | $importedPester, $importedModule | Out-Host 156 | 157 | 158 | 159 | $result = 160 | Invoke-Pester -PassThru -Verbose -OutputFile ".\$moduleName.TestResults.xml" -OutputFormat NUnitXml ` 161 | -CodeCoverage "$($importedModule | Split-Path)\*-*.ps1" -CodeCoverageOutputFile ".\$moduleName.Coverage.xml" 162 | 163 | "::set-output name=TotalCount::$($result.TotalCount)", 164 | "::set-output name=PassedCount::$($result.PassedCount)", 165 | "::set-output name=FailedCount::$($result.FailedCount)" | Out-Host 166 | if ($result.FailedCount -gt 0) { 167 | "::debug:: $($result.FailedCount) tests failed" 168 | foreach ($r in $result.TestResult) { 169 | if (-not $r.Passed) { 170 | "::error::$($r.describe, $r.context, $r.name -join ' ') $($r.FailureMessage)" 171 | } 172 | } 173 | throw "::error:: $($result.FailedCount) tests failed" 174 | } 175 | } @Parameters 176 | - name: PublishTestResults 177 | uses: actions/upload-artifact@v2 178 | with: 179 | name: PesterResults 180 | path: '**.TestResults.xml' 181 | if: ${{always()}} 182 | MinifyPSMinifier: 183 | runs-on: ubuntu-latest 184 | steps: 185 | - name: Check out repository 186 | uses: actions/checkout@v2 187 | - name: Use PSMinifier Action 188 | uses: StartAutomating/PSMinifier@master 189 | id: Minify 190 | with: 191 | CommitMessage: Minifying $($_.Name) 192 | - name: OutputMinifier 193 | run: | 194 | echo GitHubActor $GITHUB_ACTOR 195 | echo Original Size ${{ steps.Minify.outputs.OriginalSize }} 196 | echo Minified Size ${{ steps.Minify.outputs.MinifiedSize }} 197 | echo Minified Percent ${{ steps.Minify.outputs.MinifiedPercent }} 198 | 199 | 200 | shell: bash 201 | - name: Minify and GZip PSMinifier 202 | uses: StartAutomating/PSMinifier@master 203 | id: MinifyGZip 204 | with: 205 | GZip: true 206 | CommitMessage: Minifying and GZipping $($_.Name) 207 | - name: OutputMinifierGZip 208 | run: | 209 | echo Original Size ${{ steps.MinifyGZip.outputs.OriginalSize }} 210 | echo Minified Size ${{ steps.MinifyGZip.outputs.MinifiedSize }} 211 | echo Minified Percent ${{ steps.MinifyGZip.outputs.MinifiedPercent }} 212 | 213 | shell: bash 214 | - name: PublishMinifications 215 | uses: actions/upload-artifact@v2 216 | with: 217 | name: Minified 218 | path: '**.min.*ps1' 219 | if: ${{always()}} 220 | TagReleaseAndPublish: 221 | runs-on: ubuntu-latest 222 | if: ${{ success() }} 223 | steps: 224 | - name: Check out repository 225 | uses: actions/checkout@v2 226 | - name: TagModuleVersion 227 | id: TagModuleVersion 228 | shell: pwsh 229 | run: | 230 | $Parameters = @{} 231 | $Parameters.ModulePath = ${env:ModulePath} 232 | $Parameters.UserEmail = ${env:UserEmail} 233 | $Parameters.UserName = ${env:UserName} 234 | $Parameters.TagVersionFormat = ${env:TagVersionFormat} 235 | $Parameters.TagAnnotationFormat = ${env:TagAnnotationFormat} 236 | foreach ($k in @($parameters.Keys)) { 237 | if ([String]::IsNullOrEmpty($parameters[$k])) { 238 | $parameters.Remove($k) 239 | } 240 | } 241 | Write-Host "::debug:: TagModuleVersion $(@(foreach ($p in $Parameters.GetEnumerator()) {'-' + $p.Key + ' ' + $p.Value}) -join ' ')" 242 | & {param( 243 | [string] 244 | $ModulePath, 245 | 246 | # The user email associated with a git commit. 247 | [string] 248 | $UserEmail, 249 | 250 | # The user name associated with a git commit. 251 | [string] 252 | $UserName, 253 | 254 | # The tag version format (default value: 'v$(imported.Version)') 255 | # This can expand variables. $imported will contain the imported module. 256 | [string] 257 | $TagVersionFormat = 'v$($imported.Version)', 258 | 259 | # The tag version format (default value: '$($imported.Name) $(imported.Version)') 260 | # This can expand variables. $imported will contain the imported module. 261 | [string] 262 | $TagAnnotationFormat = '$($imported.Name) $($imported.Version)' 263 | ) 264 | 265 | 266 | $gitHubEvent = if ($env:GITHUB_EVENT_PATH) { 267 | [IO.File]::ReadAllText($env:GITHUB_EVENT_PATH) | ConvertFrom-Json 268 | } else { $null } 269 | 270 | 271 | @" 272 | ::group::GitHubEvent 273 | $($gitHubEvent | ConvertTo-Json -Depth 100) 274 | ::endgroup:: 275 | "@ | Out-Host 276 | 277 | if (-not ($gitHubEvent.head_commit.message -match "Merge Pull Request #(?\d+)") -and 278 | (-not $gitHubEvent.psobject.properties['inputs'])) { 279 | "::warning::Pull Request has not merged, skipping" | Out-Host 280 | return 281 | } 282 | 283 | 284 | 285 | $imported = 286 | if (-not $ModulePath) { 287 | $orgName, $moduleName = $env:GITHUB_REPOSITORY -split "/" 288 | Import-Module ".\$moduleName.psd1" -Force -PassThru -Global 289 | } else { 290 | Import-Module $modulePath -Force -PassThru -Global 291 | } 292 | 293 | if (-not $imported) { return } 294 | 295 | $targetVersion =$ExecutionContext.InvokeCommand.ExpandString($TagVersionFormat) 296 | $existingTags = git tag --list 297 | 298 | @" 299 | Target Version: $targetVersion 300 | 301 | Existing Tags: 302 | $($existingTags -join [Environment]::NewLine) 303 | "@ | Out-Host 304 | 305 | $versionTagExists = $existingTags | Where-Object { $_ -match $targetVersion } 306 | 307 | if ($versionTagExists) { 308 | "::warning::Version $($versionTagExists)" 309 | return 310 | } 311 | 312 | if (-not $UserName) { $UserName = $env:GITHUB_ACTOR } 313 | if (-not $UserEmail) { $UserEmail = "$UserName@github.com" } 314 | git config --global user.email $UserEmail 315 | git config --global user.name $UserName 316 | 317 | git tag -a $targetVersion -m $ExecutionContext.InvokeCommand.ExpandString($TagAnnotationFormat) 318 | git push origin --tags 319 | 320 | if ($env:GITHUB_ACTOR) { 321 | exit 0 322 | }} @Parameters 323 | - name: ReleaseModule 324 | id: ReleaseModule 325 | shell: pwsh 326 | run: | 327 | $Parameters = @{} 328 | $Parameters.ModulePath = ${env:ModulePath} 329 | $Parameters.UserEmail = ${env:UserEmail} 330 | $Parameters.UserName = ${env:UserName} 331 | $Parameters.TagVersionFormat = ${env:TagVersionFormat} 332 | foreach ($k in @($parameters.Keys)) { 333 | if ([String]::IsNullOrEmpty($parameters[$k])) { 334 | $parameters.Remove($k) 335 | } 336 | } 337 | Write-Host "::debug:: ReleaseModule $(@(foreach ($p in $Parameters.GetEnumerator()) {'-' + $p.Key + ' ' + $p.Value}) -join ' ')" 338 | & {param( 339 | [string] 340 | $ModulePath, 341 | 342 | # The user email associated with a git commit. 343 | [string] 344 | $UserEmail, 345 | 346 | # The user name associated with a git commit. 347 | [string] 348 | $UserName, 349 | 350 | # The tag version format (default value: 'v$(imported.Version)') 351 | # This can expand variables. $imported will contain the imported module. 352 | [string] 353 | $TagVersionFormat = 'v$($imported.Version)' 354 | ) 355 | 356 | 357 | $gitHubEvent = if ($env:GITHUB_EVENT_PATH) { 358 | [IO.File]::ReadAllText($env:GITHUB_EVENT_PATH) | ConvertFrom-Json 359 | } else { $null } 360 | 361 | 362 | @" 363 | ::group::GitHubEvent 364 | $($gitHubEvent | ConvertTo-Json -Depth 100) 365 | ::endgroup:: 366 | "@ | Out-Host 367 | 368 | if (-not ($gitHubEvent.head_commit.message -match "Merge Pull Request #(?\d+)") -and 369 | (-not $gitHubEvent.psobject.properties['inputs'])) { 370 | "::warning::Pull Request has not merged, skipping" | Out-Host 371 | return 372 | } 373 | 374 | 375 | 376 | $imported = 377 | if (-not $ModulePath) { 378 | $orgName, $moduleName = $env:GITHUB_REPOSITORY -split "/" 379 | Import-Module ".\$moduleName.psd1" -Force -PassThru -Global 380 | } else { 381 | Import-Module $modulePath -Force -PassThru -Global 382 | } 383 | 384 | if (-not $imported) { return } 385 | 386 | $targetVersion =$ExecutionContext.InvokeCommand.ExpandString($TagVersionFormat) 387 | $targetReleaseName = $targetVersion 388 | $releasesURL = 'https://api.github.com/repos/${{github.repository}}/releases' 389 | "Release URL: $releasesURL" | Out-Host 390 | $listOfReleases = Invoke-RestMethod -Uri $releasesURL -Method Get -Headers @{ 391 | "Accept" = "application/vnd.github.v3+json" 392 | "Authorization" = 'Bearer ${{ secrets.GITHUB_TOKEN }}' 393 | } 394 | 395 | $releaseExists = $listOfReleases | Where-Object tag_name -eq $targetVersion 396 | 397 | if ($releaseExists) { 398 | "::warning::Release '$($releaseExists.Name )' Already Exists" | Out-Host 399 | return 400 | } 401 | 402 | 403 | Invoke-RestMethod -Uri $releasesURL -Method Post -Body ( 404 | [Ordered]@{ 405 | owner = '${{github.owner}}' 406 | repo = '${{github.repository}}' 407 | tag_name = $targetVersion 408 | name = "$($imported.Name) $targetVersion" 409 | body = 410 | if ($env:RELEASENOTES) { 411 | $env:RELEASENOTES 412 | } elseif ($imported.PrivateData.PSData.ReleaseNotes) { 413 | $imported.PrivateData.PSData.ReleaseNotes 414 | } else { 415 | "$($imported.Name) $targetVersion" 416 | } 417 | draft = if ($env:RELEASEISDRAFT) { [bool]::Parse($env:RELEASEISDRAFT) } else { $false } 418 | prerelease = if ($env:PRERELEASE) { [bool]::Parse($env:PRERELEASE) } else { $false } 419 | } | ConvertTo-Json 420 | ) -Headers @{ 421 | "Accept" = "application/vnd.github.v3+json" 422 | "Content-type" = "application/json" 423 | "Authorization" = 'Bearer ${{ secrets.GITHUB_TOKEN }}' 424 | } 425 | } @Parameters 426 | - name: PublishPowerShellGallery 427 | id: PublishPowerShellGallery 428 | shell: pwsh 429 | run: | 430 | $Parameters = @{} 431 | $Parameters.ModulePath = ${env:ModulePath} 432 | foreach ($k in @($parameters.Keys)) { 433 | if ([String]::IsNullOrEmpty($parameters[$k])) { 434 | $parameters.Remove($k) 435 | } 436 | } 437 | Write-Host "::debug:: PublishPowerShellGallery $(@(foreach ($p in $Parameters.GetEnumerator()) {'-' + $p.Key + ' ' + $p.Value}) -join ' ')" 438 | & {param( 439 | [string] 440 | $ModulePath 441 | ) 442 | $gitHubEvent = if ($env:GITHUB_EVENT_PATH) { 443 | [IO.File]::ReadAllText($env:GITHUB_EVENT_PATH) | ConvertFrom-Json 444 | } else { $null } 445 | 446 | 447 | @" 448 | ::group::GitHubEvent 449 | $($gitHubEvent | ConvertTo-Json -Depth 100) 450 | ::endgroup:: 451 | "@ | Out-Host 452 | 453 | if (-not ($gitHubEvent.head_commit.message -match "Merge Pull Request #(?\d+)") -and 454 | (-not $gitHubEvent.psobject.properties['inputs'])) { 455 | "::warning::Pull Request has not merged, skipping" | Out-Host 456 | return 457 | } 458 | 459 | 460 | $imported = 461 | if (-not $ModulePath) { 462 | $orgName, $moduleName = $env:GITHUB_REPOSITORY -split "/" 463 | Import-Module ".\$moduleName.psd1" -Force -PassThru -Global 464 | } else { 465 | Import-Module $modulePath -Force -PassThru -Global 466 | } 467 | 468 | if (-not $imported) { return } 469 | 470 | $foundModule = try { Find-Module -Name $imported.Name -ErrorAction SilentlyContinue } catch {} 471 | 472 | if ($foundModule -and $foundModule.Version -ge $imported.Version) { 473 | "::warning::Gallery Version of $moduleName is more recent ($($foundModule.Version) >= $($imported.Version))" | Out-Host 474 | } else { 475 | 476 | $gk = '${{secrets.GALLERYKEY}}' 477 | 478 | $rn = Get-Random 479 | $moduleTempFolder = Join-Path $pwd "$rn" 480 | $moduleTempPath = Join-Path $moduleTempFolder $moduleName 481 | New-Item -ItemType Directory -Path $moduleTempPath -Force | Out-Host 482 | 483 | Write-Host "Staging Directory: $ModuleTempPath" 484 | 485 | $imported | Split-Path | 486 | Get-ChildItem -Force | 487 | Where-Object Name -NE $rn | 488 | Copy-Item -Destination $moduleTempPath -Recurse 489 | 490 | $moduleGitPath = Join-Path $moduleTempPath '.git' 491 | Write-Host "Removing .git directory" 492 | if (Test-Path $moduleGitPath) { 493 | Remove-Item -Recurse -Force $moduleGitPath 494 | } 495 | Write-Host "Module Files:" 496 | Get-ChildItem $moduleTempPath -Recurse 497 | Write-Host "Publishing $moduleName [$($imported.Version)] to Gallery" 498 | Publish-Module -Path $moduleTempPath -NuGetApiKey $gk 499 | if ($?) { 500 | Write-Host "Published to Gallery" 501 | } else { 502 | Write-Host "Gallery Publish Failed" 503 | exit 1 504 | } 505 | } 506 | } @Parameters 507 | 508 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ### v1.1.4 2 | * Compress-ScriptBlock: Aliasing PSMinify (#13) 3 | * Adding support for Minify transpiler in PipeScript (#11) 4 | * Compress-ScriptBlock: Returning [ScriptBlock] if possible (#12) 5 | --- 6 | 7 | ### v1.1.3 8 | --- 9 | Compress-ScriptBlock bugfix: now handling using statements (Issue #6). Improvements to try/catch (Issue #7) 10 | 11 | ### v1.1.2 12 | ---- 13 | Compress-ScriptBlock bugfix: now handling [Hashtables] and [Ordered] (Issue #2) 14 | 15 | ### v1.1.1 16 | ---- 17 | Compress-ScriptBlock bugfix: try/catch/finally blocks now appropriately handled (Issue #3) 18 | 19 | ### v1.1 20 | ---- 21 | Compress-ScriptBlock now has -OutputPath/-PassThru 22 | Added Support for GitHub Action 23 | 24 | ### v1.0 25 | ---- 26 | Initial Version of Minifier 27 | -------------------------------------------------------------------------------- /Compress-ScriptBlock.min.gzip.ps1: -------------------------------------------------------------------------------- 1 | . $([ScriptBlock]::Create(([IO.StreamReader]::new(( 2 | [IO.Compression.GZipStream]::new([IO.MemoryStream]::new( 3 | [Convert]::FromBase64String(' 4 | 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 48 | 539gcM9iwjoAAA== 49 | ')), 50 | [IO.Compression.CompressionMode]'Decompress')), 51 | [Text.Encoding]::unicode)).ReadToEnd() 52 | )) -------------------------------------------------------------------------------- /Compress-ScriptBlock.min.ps1: -------------------------------------------------------------------------------- 1 | function Compress-ScriptBlock{[OutputType([string])][Alias('PSMinify')]param([Parameter(Mandatory,ValueFromPipelineByPropertyName,ValueFromPipeline)][ScriptBlock]$ScriptBlock,[Parameter(ValueFromPipelineByPropertyName)][string]$Name,[Parameter(ValueFromPipelineByPropertyName)][switch]$Anonymous,[Parameter(ValueFromPipelineByPropertyName)][Alias('Zip', 'Compress')][switch]$GZip,[Parameter(ValueFromPipelineByPropertyName)][Alias('Dot', '.')][switch]$DotSource,[Parameter(ValueFromPipelineByPropertyName)][Alias('NoBlocks')][switch]$NoBlock,[Parameter(ValueFromPipelineByPropertyName)][string]$OutputPath,[Parameter(ValueFromPipelineByPropertyName)][switch]$PassThru)begin{foreach($_ in 'BinaryExpression','Expression','ScriptBlockExpression','ParenExpression','ArrayExpression','SubExpression','Command','CommandExpression','IfStatement','LoopStatement','Hashtable','ConvertExpression','FunctionDefinition','AssignmentStatement','Pipeline','Statement','TryStatement','CommandExpression'){$ExecutionContext.SessionState.PSVariable.Set($_, "Management.Automation.Language.${_}Ast" -as [Type])};$CompressScriptBlockAst={param($ast)if($ast.Body){$ast=$ast.Body};$dps=$ast.DynamicParamBlock.Statements;$bs=$ast.BeginBlock.Statements;$ps=$ast.ProcessBlock.Statements;$es=$ast.EndBlock.Statements;@(if($ast.UsingStatements){(@(foreach($using in $ast.UsingStatements){"$using"})-join';')+';'};if($ast.ParamBlock){$pb=$ast.ParamBlock;foreach($a in $pb.Attributes){$a};'param(';@(foreach($p in $pb.Parameters){@(foreach($a in $p.Attributes){$a};$p.Name)-join''})-join',';')'};if($dps){'dynamicParam{';@($dps|& $CompressStatement)-join';';'}'};if($bs){'begin{';@($bs|& $CompressStatement)-join';';'}'};if($ps){'process{';@($ps|& $CompressStatement)-join';';'}'};if($es){if($bs-or$ps){'end{'};@($es|& $CompressStatement)-join';';if($bs-or$ps){'}'}})-join''};$CompressStatement={param([Parameter(ValueFromPipeline=$true)][Management.Automation.Language.StatementAst]$s)process{if($s-is$IfStatement){$nc=0;@(foreach($c in $s.Clauses){if(-not $nc){'if'}else{'elseif'};'(';@($c.Item1.PipelineElements|& $CompressPipeline)-join'|';')';'{';@($c.Item2.Statements|& $CompressStatement)-join';';$nc++;'}'};if($s.ElseClause){'else{';@($s.ElseClause.Statements|& $CompressStatement)-join';';'}'})-join''}elseif($s-is$LoopStatement){$loopType=$s.GetType().Name.Replace('StatementAst','');@(if($s.Label){":$($s.Label) "};if($loopType-eq'foreach'){'foreach(';$s.Variable;' in ';& $compressPart $s.Condition;')'}elseif($loopType-eq'for'){'for(';$s.Initializer;';';$s.Condition;';';$s.Iterator;')'}elseif($loopType-eq'while'){'while(';$s.Condition;')'}elseif($loopType-eq'dowhile'){'do'};'{';@($s.Body.Statements|& $CompressStatement)-join';';'}';if($loopType-eq'dowhile'){'while(';$s.Condition;')'})-join''}elseif($s-is$AssignmentStatement){$as=$s;@($as.Left.ToString().Trim();$as.ErrorPosition.Text;if($as.Right-is[Management.Automation.Language.StatementAst]){@($as.right|& $CompressStatement)-join';'})-join''}elseif($s-is$Pipeline){@($s.PipelineElements|& $CompressPipeline)-join'|'}elseif($s-is$TryStatement){@('try{';@($s.Body.statements|& $CompressStatement)-join';';'}';foreach($cc in $s.CatchClauses){'catch';if($cc.CatchTypes){foreach($ct in $cc.CatchTypes){' [';$ct.TypeName.FullName;']'}};'{';@($cc.Body.statements|& $CompressStatement)-join';';'}'};if($s.Finally){'finally{';$($s.Finally.statements|& $CompressStatement)-join';';'}'})-join''}elseif($s-is$CommandExpression){if($s.Expression){$s.Expression|& $CompressExpression}else{$s.ToString()}}elseif($s-is$FunctionDefinition){$(if($s.IsWorklow){"workflow "}elseif($s.IsFilter){"filter "}else{"function "})+$s.Name+"{$(& $CompressScriptBlockAst $s.Body)}"}else{$s.ToString()}}};$CompressPipeline={param([Parameter(ValueFromPipeline=$true)][Management.Automation.Language.CommandBaseAst]$p)process{if($p-is$CommandExpression){& $CompressExpression $p.Expression}elseif($p-is$Command){@(if($p.InvocationOperator-eq'Ampersand'){'&'}elseif($p.InvocationOperator-eq'Dot'){'.'};foreach($e in $p.CommandElements){if($e.ScriptBlock){"{$(& $CompressScriptBlockAst $e.ScriptBlock)}"}else{$e}})-join' '}elseif($p){$p.ToString()}else{$null=$null}}};$CompressExpression={param([Parameter(ValueFromPipeline=$true,Position=0)][Management.Automation.Language.ExpressionAst]$e)process{@(if($e-is$BinaryExpression){if($e.Left-is$Expression){& $CompressExpression $e.Left}else{$e.Left};$e.ErrorPosition;if($e.Right-is$Expression){& $CompressExpression $e.Right}else{$e.Right}}elseif($e-is$ScriptBlockExpression){'{';& $CompressScriptBlockAst $e.ScriptBlock;'}'}elseif($e-is$ParenExpression){'(';. $compressPart $e;')'}elseif($e-is$ArrayExpression){'@(';. $compressPart $e;')'}elseif($e-is$SubExpression){'$(';. $compressPart $e;')'}elseif($e-is$convertExpression){"[$($e.Type.TypeName -replace '\s')]";. $CompressExpression $e.Child}elseif($e-is$hashtable){'@{'+(@(foreach($kvp in $e.KeyValuePairs){@(. $compressPart $kvp.Item1;'=';. $compressPart $kvp.Item2)-join''})-join';')+'}'}elseif($e.Elements){@(foreach($_ in $e.Elements){. $CompressPart $_})-join','}else{$e.ToString()})-join''}};$CompressPart={param([Parameter(ValueFromPipeline=$true,Position=0)]$p)process{if($p.SubExpression){@($p.Subexpression.Statements|& $CompressStatement)-join';'}elseif($p.Pipeline){@($p.Pipeline.PipelineElements|& $CompressPipeline)-join'|'}elseif($p-is$FunctionDefinition){$(if($p.IsWorklow){"workflow "}elseif($p.IsFilter){"filter "}else{"function "})+$p.Name+"{$(& $CompressScriptBlockAst $p.Body)}"}elseif($p.ScriptBlock){"{$(& $CompressScriptBlockAst $p.ScriptBlock)}"}elseif($p-is$Pipeline){@($p.PipelineElements|& $CompressPipeline)-join'|'}else{$p}}}}process{if($_-is[Management.Automation.CommandInfo]){$name=''};$compressedScriptBlock=& $CompressScriptBlockAst $ScriptBlock.Ast;$compressedScriptBlock=if(-not $GZip){$compressedScriptBlock}else{$data=[Text.Encoding]::Unicode.GetBytes($compressedScriptBlock);$ms=[IO.MemoryStream]::new();$cs=[IO.Compression.GZipStream]::new($ms, [Io.Compression.CompressionMode]"Compress");$cs.Write($Data, 0, $Data.Length);$cs.Close();$cs.Dispose();if($NoBlock){"`$([ScriptBlock]::Create(([IO.StreamReader]::new(([IO.Compression.GZipStream]::new([IO.MemoryStream]::new([Convert]::FromBase64String('$([Convert]::ToBase64String($ms.ToArray()))')),[IO.Compression.CompressionMode]'Decompress')),[Text.Encoding]::unicode)).ReadToEnd()))"}else{"`$([ScriptBlock]::Create(([IO.StreamReader]::new(( 2 | [IO.Compression.GZipStream]::new([IO.MemoryStream]::new( 3 | [Convert]::FromBase64String(' 4 | $([Convert]::ToBase64String($ms.ToArray(), 'InsertLineBreaks')) 5 | ')), 6 | [IO.Compression.CompressionMode]'Decompress')), 7 | [Text.Encoding]::unicode)).ReadToEnd() 8 | ))"};$ms.Close();$ms.Dispose()};if($DotSource){$compressedScriptBlock=if($GZip){". $compressedScriptBlock"}else{". {$compressedScriptBlock}"}};$minified=if($Name-and-not $Anonymous){if(-not $GZip-and-not $DotSource){$compressedScriptBlock="{$compressedScriptBlock}"};if($Name-match'\W'){"`${$name} = $compressedScriptBlock"}else{"`$$name = $compressedScriptBlock"}}else{$compressedScriptBlock};if($OutputPath){$unresolvedOutputPath=$ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($OutputPath);[IO.File]::WriteAllText("$unresolvedOutputPath", $minified);if($PassThru-and[IO.File]::Exists("$unresolvedOutputPath")){[IO.FileInfo]"$unresolvedOutputPath"}}elseif($($minifiedScriptBlock=[ScriptBlock]::Create($minified);$minifiedScriptBlock)){$minifiedScriptBlock}else{$minified}}} -------------------------------------------------------------------------------- /Compress-ScriptBlock.ps1: -------------------------------------------------------------------------------- 1 | #requires -Version 3.0 2 | function Compress-ScriptBlock 3 | { 4 | <# 5 | .Synopsis 6 | Compresss a script block 7 | .Description 8 | Compresss a script block into a minified version of itself. 9 | 10 | Minified scripts remove documentation and minimize the spacing between statements. 11 | 12 | This makes scripts significantly smaller and less readable, and also makes them more embeddable. 13 | 14 | Additionally, Compress-ScriptBlock can -GZip the content to further compress the output. 15 | If -NoBlock is passed, the minified and compressed output will be returned in a single line. 16 | 17 | ScriptBlocks can be given a -Name, which will declare them as a variable. 18 | This will happen automatically when piping in a command. 19 | This can be avoided by passing -Anonymous. 20 | .Example 21 | $compressedSelf = Get-Command Compress-ScriptBlock | Compress-ScriptBlock 22 | .Example 23 | Get-Module PSMinifier | # Get the minifier module's 24 | Split-Path | # root path 25 | Join-Path -ChildPath Compress-ScriptBlock.ps1 | # join it with Compress-ScriptBlock.ps1 26 | Get-Command | # get the command 27 | Compress-ScriptBlock -GZip -DotSource | # Compress it, anonymized, and dot-sourced 28 | Set-Content -Path ( # And put it back into 29 | Get-Module PSMinifier | 30 | Split-Path | 31 | Join-Path -ChildPath Compress-ScriptBlock.min.gzip.ps1 # Compress-ScriptBlock.min.gzip.ps1 32 | ) 33 | .Example 34 | Get-Module PSMinifier | # Get the minifier module's 35 | Split-Path | # root path 36 | Join-Path -ChildPath Compress-ScriptBlock.ps1 | # join it with Compress-ScriptBlock.ps1 37 | Get-Command | # get the command 38 | Compress-ScriptBlock | # Compress it, anonymized 39 | Set-Content -Path ( # And put it back into 40 | Get-Module PSMinifier | 41 | Split-Path | 42 | Join-Path -ChildPath Compress-ScriptBlock.min.ps1 # Compress-ScriptBlock.min.gzip.ps1 43 | ) 44 | #> 45 | [OutputType([string])] 46 | [Alias('PSMinify')] 47 | param( 48 | # The ScriptBlock that will be compressed. 49 | [Parameter(Mandatory,ValueFromPipelineByPropertyName,ValueFromPipeline)] 50 | [ScriptBlock] 51 | $ScriptBlock, 52 | 53 | # If provided, will assign the script block to a named variable. 54 | [Parameter(ValueFromPipelineByPropertyName)] 55 | [string] 56 | $Name, 57 | 58 | # If set, will ignore any provided name. 59 | [Parameter(ValueFromPipelineByPropertyName)] 60 | [switch] 61 | $Anonymous, 62 | 63 | # If set, the minified content will be encoded as GZip, further reducing it's size. 64 | [Parameter(ValueFromPipelineByPropertyName)] 65 | [Alias('Zip', 'Compress')] 66 | [switch] 67 | $GZip, 68 | 69 | # If set, will dot source the compressed content. 70 | [Parameter(ValueFromPipelineByPropertyName)] 71 | [Alias('Dot', '.')] 72 | [switch] 73 | $DotSource, 74 | 75 | # If set, zipped minified content will be encoded without blocks, making it a very long single line. 76 | # This parameter is only valid with -GZip. 77 | [Parameter(ValueFromPipelineByPropertyName)] 78 | [Alias('NoBlocks')] 79 | [switch] 80 | $NoBlock, 81 | 82 | # If provided, will write the minified content to the specified path, instead of outputting it. 83 | [Parameter(ValueFromPipelineByPropertyName)] 84 | [string] 85 | $OutputPath, 86 | 87 | # If provided with -OutputPath, will output the file written to disk. 88 | [Parameter(ValueFromPipelineByPropertyName)] 89 | [switch] 90 | $PassThru 91 | ) 92 | 93 | begin { 94 | # First, we declare a number of quick variables to access AST types. 95 | foreach ($_ in 'BinaryExpression','Expression','ScriptBlockExpression','ParenExpression','ArrayExpression', 96 | 'SubExpression', 'Command','CommandExpression', 'IfStatement', 'LoopStatement', 'Hashtable', 'ConvertExpression', 97 | 'FunctionDefinition','AssignmentStatement','Pipeline','Statement','TryStatement','CommandExpression') { 98 | $ExecutionContext.SessionState.PSVariable.Set($_, "Management.Automation.Language.${_}Ast" -as [Type]) 99 | } 100 | 101 | # Next, we declare a bunch of ScriptBlocks that handle different scenarios for compressing the AST. 102 | # These will recursively call each other as needed. 103 | 104 | $CompressScriptBlockAst = { # The topmost compresses a given Script Block's AST. 105 | param($ast) 106 | 107 | if ($ast.Body) { $ast = $ast.Body } # If the AST had an inner body AST, use that instead 108 | 109 | $dps = $ast.DynamicParamBlock.Statements 110 | $bs = $ast.BeginBlock.Statements 111 | $ps = $ast.ProcessBlock.Statements 112 | $es = $ast.EndBlock.Statements 113 | 114 | @( 115 | if ($ast.UsingStatements) { 116 | (@(foreach ($using in $ast.UsingStatements) { 117 | "$using" 118 | }) -join ';') + ';' 119 | } 120 | if ($ast.ParamBlock) { # Walk thru the param block. 121 | $pb = $ast.ParamBlock 122 | foreach ($a in $pb.Attributes) { 123 | $a # Declaration attributes are emitted unaltered 124 | } 125 | 'param(' 126 | @(foreach ($p in $pb.Parameters) { 127 | @(foreach ($a in $p.Attributes) { 128 | $a # Parameter attributes are emitted unaltered 129 | } 130 | $p.Name) -join '' # then we emit the name and all attributes in one statement 131 | }) -join ',' 132 | ')' 133 | } 134 | # then, for dynamicParam, begin, process, and end, redeclare the blocks and minify the statements. 135 | if ($dps) { 136 | 'dynamicParam{' 137 | @($dps | & $CompressStatement) -join ';' 138 | '}' 139 | } 140 | if ($bs) { 141 | 'begin{' 142 | @($bs | & $CompressStatement) -join ';' 143 | '}' 144 | } 145 | if ($ps) { 146 | 'process{' 147 | @($ps | & $CompressStatement) -join ';' 148 | '}' 149 | } 150 | if ($es) { 151 | if ($bs -or $ps) { 'end{'} 152 | @($es | & $CompressStatement) -join ';' 153 | if ($bs -or $ps) { '}'} 154 | }) -join '' 155 | } 156 | 157 | 158 | $CompressStatement = { # Compressing statements is the tricky part. 159 | param( 160 | [Parameter(ValueFromPipeline=$true)] 161 | [Management.Automation.Language.StatementAst]$s) 162 | process { 163 | if ($s -is $IfStatement) { # If it's an if statement 164 | $nc = 0 165 | @(foreach ($c in $s.Clauses) { # minify each clause 166 | if( -not $nc){ 167 | 'if' 168 | } else { 169 | 'elseif' 170 | } 171 | '(' # by compressing the condition pipeline 172 | @($c.Item1.PipelineElements | & $CompressPipeline) -join '|' 173 | ')' 174 | '{' # and compressing the inner statements 175 | @($c.Item2.Statements | & $CompressStatement) -join ';' 176 | $nc++ 177 | '}' 178 | } 179 | if ($s.ElseClause) { 180 | 'else{' 181 | @($s.ElseClause.Statements | & $CompressStatement) -join ';' 182 | '}' 183 | } 184 | ) -join '' 185 | } 186 | elseif ($s -is $LoopStatement) { # If it's a loop 187 | $loopType = $s.GetType().Name.Replace('StatementAst','') # determine it's type 188 | @( 189 | if ($s.Label) { # add the loop label if it exists 190 | ":$($s.Label) " 191 | } 192 | if ($loopType -eq 'foreach') { # and recreate each loop condition. 193 | 'foreach(' 194 | $s.Variable 195 | ' in ' 196 | & $compressPart $s.Condition 197 | ')' 198 | } elseif ($loopType -eq 'for') { 199 | 'for(' 200 | $s.Initializer 201 | ';' 202 | $s.Condition 203 | ';' 204 | $s.Iterator 205 | ')' 206 | } elseif ($loopType -eq 'while') { 207 | 'while(' 208 | $s.Condition 209 | ')' 210 | } elseif ($loopType -eq 'dowhile') { 211 | 'do' 212 | } 213 | 214 | '{' 215 | @($s.Body.Statements | & $CompressStatement) -join ';' 216 | '}' 217 | if ($loopType -eq 'dowhile') { 218 | 'while(' 219 | $s.Condition 220 | ')' 221 | } 222 | ) -join '' 223 | } 224 | elseif ($s -is $AssignmentStatement) { # If it's an assignment, 225 | $as = $s 226 | @( 227 | $as.Left.ToString().Trim() 228 | $as.ErrorPosition.Text 229 | if ($as.Right -is [Management.Automation.Language.StatementAst]) { 230 | @($as.right | & $CompressStatement) -join ';' # compress the right side 231 | }) -join '' 232 | } 233 | elseif ($s -is $Pipeline) { # If it's a pipeline 234 | @($s.PipelineElements | & $CompressPipeline) -join '|' # minify the pipeline and join by | 235 | } 236 | elseif ($s -is $TryStatement) { # If it's a type/catch 237 | @( 238 | 'try{' 239 | @($s.Body.statements | & $CompressStatement) -join ';' # minify the try 240 | '}' 241 | foreach ($cc in $s.CatchClauses) { # then each of the catches 242 | 'catch' 243 | if ($cc.CatchTypes) { 244 | foreach ($ct in $cc.CatchTypes) { 245 | ' [' 246 | $ct.TypeName.FullName 247 | ']' 248 | } 249 | } 250 | '{' 251 | @($cc.Body.statements | & $CompressStatement) -join ';' 252 | '}' 253 | } 254 | if ($s.Finally) { # then the finally (if it exists) 255 | 'finally{' 256 | $($s.Finally.statements | & $CompressStatement) -join ';' 257 | '}' 258 | } 259 | ) -join '' 260 | } 261 | elseif ($s -is $CommandExpression) { # If it's a command expression 262 | if ($s.Expression) { 263 | $s.Expression | & $CompressExpression # minify the expression 264 | } else { 265 | $s.ToString() 266 | } 267 | } 268 | elseif ($s -is $FunctionDefinition) { # If it's a function 269 | $(if ($s.IsWorklow) { "workflow " } 270 | elseif ($s.IsFilter) { "filter " } 271 | else { "function " }) + $s.Name + "{$(& $CompressScriptBlockAst $s.Body)}" # redeclare it with a minified body. 272 | } 273 | else { 274 | $s.ToString() 275 | } 276 | } 277 | } 278 | 279 | $CompressPipeline = { # If we're compressing a pipeline 280 | param( 281 | [Parameter(ValueFromPipeline=$true)] 282 | [Management.Automation.Language.CommandBaseAst]$p) 283 | 284 | process { 285 | if ($p -is $CommandExpression) { 286 | & $CompressExpression $p.Expression # compress each expression 287 | } elseif ($p -is $Command) { 288 | @( 289 | if ($p.InvocationOperator -eq 'Ampersand') { 290 | '&' 291 | } elseif ($p.InvocationOperator -eq 'Dot') { 292 | '.' 293 | } 294 | foreach ($e in $p.CommandElements) { 295 | if ($e.ScriptBlock) { 296 | "{$(& $CompressScriptBlockAst $e.ScriptBlock)}" # and compress any nested script blocks 297 | } else { $e } 298 | }) -join ' ' 299 | } elseif ($p) { 300 | $p.ToString() 301 | } else { 302 | $null = $null 303 | } 304 | } 305 | } 306 | 307 | $CompressExpression = { # If we're compressing an expression, 308 | param( 309 | [Parameter(ValueFromPipeline=$true,Position=0)] 310 | [Management.Automation.Language.ExpressionAst]$e) 311 | process { 312 | @( 313 | if ($e -is $BinaryExpression) # and it's a binary expression 314 | { 315 | if ($e.Left -is $Expression) { # compress the left 316 | & $CompressExpression $e.Left 317 | } else { 318 | $e.Left 319 | } 320 | $e.ErrorPosition 321 | if ($e.Right -is $Expression) { # and the right. 322 | & $CompressExpression $e.Right 323 | } else { 324 | $e.Right 325 | } 326 | } 327 | elseif ($e -is $ScriptBlockExpression) # If it was a script expression 328 | { 329 | '{' 330 | & $CompressScriptBlockAst $e.ScriptBlock # minify the script. 331 | '}' 332 | } 333 | elseif ($e -is $ParenExpression) { # If it was a paren expresssion, arrayexpression, or subexpression 334 | '(' 335 | . $compressPart $e # we have to minify each part of the expression. 336 | ')' 337 | } 338 | elseif ($e -is $ArrayExpression) { 339 | '@(' 340 | . $compressPart $e 341 | ')' 342 | } 343 | elseif ($e -is $SubExpression) { 344 | '$(' 345 | . $compressPart $e 346 | ')' 347 | } 348 | elseif ($e -is $convertExpression) { 349 | "[$($e.Type.TypeName -replace '\s')]" 350 | . $CompressExpression $e.Child 351 | } 352 | elseif ($e -is $hashtable) { 353 | '@{' + 354 | (@(foreach ($kvp in $e.KeyValuePairs) { 355 | @(. $compressPart $kvp.Item1 356 | '=' 357 | . $compressPart $kvp.Item2 358 | ) -join '' 359 | }) -join ';')+ '}' 360 | } 361 | elseif ($e.Elements) { 362 | @(foreach ($_ in $e.Elements) { 363 | . $CompressPart $_ 364 | }) -join ',' 365 | } 366 | else { 367 | $e.ToString() 368 | }) -join '' 369 | 370 | } 371 | } 372 | 373 | 374 | $CompressPart = { # If we're minifying pars of an expression 375 | param([Parameter(ValueFromPipeline=$true,Position=0)]$p) 376 | process { 377 | if ($p.SubExpression) { @($p.Subexpression.Statements | & $CompressStatement) -join ';' } # join minified subexpression statements by ;, 378 | elseif ($p.Pipeline) { @($p.Pipeline.PipelineElements | & $CompressPipeline) -join '|' } # pipeline elements by |, 379 | elseif ($p -is $FunctionDefinition) { # redeclare any functions, minified 380 | $(if ($p.IsWorklow) { "workflow " } 381 | elseif ($p.IsFilter) { "filter " } 382 | else { "function " }) + $p.Name + "{$(& $CompressScriptBlockAst $p.Body)}" 383 | } 384 | elseif ($p.ScriptBlock) { "{$(& $CompressScriptBlockAst $p.ScriptBlock)}" } # minify any script blocks 385 | elseif ($p -is $Pipeline) { 386 | @($p.PipelineElements | & $CompressPipeline) -join '|' 387 | } 388 | else { $p } # any emit anything we don't know about. 389 | } 390 | } 391 | } 392 | 393 | process { 394 | if ($_ -is [Management.Automation.CommandInfo]) { $name = '' } 395 | # Now, call our minifier with this script block's AST 396 | $compressedScriptBlock = & $CompressScriptBlockAst $ScriptBlock.Ast 397 | 398 | $compressedScriptBlock = # After that, resassign $CompressedScriptBlock as needed 399 | if (-not $GZip) { 400 | $compressedScriptBlock 401 | } 402 | else { # If we're GZIPing, 403 | $data = [Text.Encoding]::Unicode.GetBytes($compressedScriptBlock) # compress the content 404 | $ms = [IO.MemoryStream]::new() 405 | $cs = [IO.Compression.GZipStream]::new($ms, [Io.Compression.CompressionMode]"Compress") 406 | $cs.Write($Data, 0, $Data.Length) 407 | $cs.Close() 408 | $cs.Dispose() 409 | 410 | if ($NoBlock) { # If we're using -NoBlocks, emit it as a single line 411 | "`$([ScriptBlock]::Create(([IO.StreamReader]::new(([IO.Compression.GZipStream]::new([IO.MemoryStream]::new([Convert]::FromBase64String('$([Convert]::ToBase64String($ms.ToArray()))')),[IO.Compression.CompressionMode]'Decompress')),[Text.Encoding]::unicode)).ReadToEnd()))" 412 | } 413 | else 414 | { 415 | # Otherwise, add _some_ whitespace. 416 | "`$([ScriptBlock]::Create(([IO.StreamReader]::new(( 417 | [IO.Compression.GZipStream]::new([IO.MemoryStream]::new( 418 | [Convert]::FromBase64String(' 419 | $([Convert]::ToBase64String($ms.ToArray(), 'InsertLineBreaks')) 420 | ')), 421 | [IO.Compression.CompressionMode]'Decompress')), 422 | [Text.Encoding]::unicode)).ReadToEnd() 423 | ))" 424 | } 425 | 426 | $ms.Close() 427 | $ms.Dispose() 428 | } 429 | 430 | if ($DotSource) { # If we're dot sourcing, 431 | 432 | $compressedScriptBlock = # reassign $compressedScriptBlock again 433 | if ($GZip) { 434 | ". $compressedScriptBlock" # if we're GZipping, fine (since the return value of this will be a script block) 435 | } else { 436 | ". {$compressedScriptBlock}" # otherwise, wrap it in {}s. 437 | } 438 | } 439 | 440 | $minified = 441 | if ($Name -and -not $Anonymous) { # If we've provided a -Name and don't want to be -Anonymous, we're assigning to a variable. 442 | if (-not $GZip -and -not $DotSource) { # If it's not GZipped or dotted, 443 | $compressedScriptBlock = "{$compressedScriptBlock}" # we need to wrap it in {}s. 444 | } 445 | if ($Name -match '\W') { # If the name contained non-word characters, 446 | "`${$name} = $compressedScriptBlock" # we need to wrap it in {}s. 447 | } else { 448 | "`$$name = $compressedScriptBlock" # otherwise, it's just $name = $compressedScriptBlock 449 | } 450 | } else { 451 | $compressedScriptBlock 452 | } 453 | 454 | if ($OutputPath) { # If we've been provided an -OutputPath 455 | # figure out what it might be 456 | $unresolvedOutputPath = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($OutputPath) 457 | [IO.File]::WriteAllText("$unresolvedOutputPath", $minified) # and then write content to disk. 458 | if ($PassThru -and [IO.File]::Exists("$unresolvedOutputPath")) { 459 | [IO.FileInfo]"$unresolvedOutputPath" 460 | } 461 | } elseif ($( 462 | $minifiedScriptBlock = [ScriptBlock]::Create($minified) 463 | $minifiedScriptBlock 464 | )) { 465 | $minifiedScriptBlock # Otherwise, output the minified content. 466 | } else { 467 | $minified 468 | } 469 | } 470 | } -------------------------------------------------------------------------------- /GitHub/Actions/PSMinifier.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | .Synopsis 3 | PSMinifier Action 4 | .Description 5 | Runs PSMinifier on code in the workspace, and creates .min.ps1 files. 6 | #> 7 | param( 8 | # One or more wildcards of files to include. 9 | # If not provided, all .ps1 in a workspace will be included. 10 | [string[]] 11 | $Include, 12 | 13 | # One or more wildcards of files to exclude. 14 | [string[]] 15 | $Exclude = "*.*.ps1", 16 | 17 | # If set, the minified content will be encoded as GZip, further reducing it's size. 18 | [switch] 19 | $GZip, 20 | 21 | # If set, zipped minified content will be encoded without blocks, making it a very long single line. 22 | # This parameter is only valid with -GZip. 23 | [switch] 24 | $NoBlock, 25 | 26 | # If provided, will commit changes made to the workspace with this commit message. 27 | [string] 28 | $CommitMessage, 29 | 30 | # The user email associated with a git commit. 31 | [string] 32 | $UserEmail, 33 | 34 | # The user name associated with a git commit. 35 | [string] 36 | $UserName 37 | ) 38 | 39 | "::group::Parameters" | Out-Host 40 | [PSCustomObject]$PSBoundParameters | Format-List | Out-Host 41 | "::endgroup::" | Out-Host 42 | 43 | @" 44 | ::group::GitHubEvent 45 | $($gitHubEvent | ConvertTo-Json -Depth 100) 46 | ::endgroup:: 47 | "@ | Out-Host 48 | 49 | $PSD1Found = Get-ChildItem -Recurse -Filter "*.psd1" | Where-Object Name -eq 'PSMinifier.psd1' | Select-Object -First 1 50 | 51 | if ($PSD1Found) { 52 | $psMinifierPath = $PSD1Found 53 | Import-Module $PSD1Found -Force -PassThru | Out-Host 54 | } 55 | elseif ($env:GITHUB_ACTION_PATH) { 56 | $psMinifierPath = Join-Path $env:GITHUB_ACTION_PATH 'PSMinifier.psd1' 57 | if (Test-path $psMinifierPath) { 58 | Import-Module $psMinifierPath -Force -PassThru | Out-String 59 | } else { 60 | throw "PSMinifier not found" 61 | } 62 | } elseif (-not (Get-Module PSMinifier)) { 63 | Get-ChildItem env: | Out-String 64 | throw "Action Path not found" 65 | } 66 | 67 | "::debug::PSMinifier Loaded from Path - $($psMinifierPath)" | Out-Host 68 | 69 | if (-not $env:GITHUB_WORKSPACE) { throw "No GitHub workspace" } 70 | if (-not $CommitMessage -and $gitHubEvent.head_commit.message) { 71 | $CommitMessage = $gitHubEvent.head_commit.message 72 | } 73 | 74 | $compressSplat = @{} + $PSBoundParameters 75 | $compressSplat.Remove('Include') 76 | $compressSplat.Remove('Exclude') 77 | $compressSplat.Remove('CommitMessage') 78 | $compressSplat.Remove('UserEmail') 79 | $compressSplat.Remove('UserName') 80 | if ($GZip) { $compressSplat.DotSource = $true } 81 | 82 | "EXCLUDING $Exclude" | Out-Host 83 | 84 | $commandsToMinify = 85 | @(Get-ChildItem -LiteralPath $env:GITHUB_WORKSPACE -Filter *.ps1 | 86 | Where-Object { 87 | $fileInfo = $_ 88 | if ($fileInfo.Name -like '*.min.*ps1') { return } # Don't overminify 89 | if ($Include) { 90 | foreach ($inc in $Include) { 91 | if ($fileInfo.Name -like $inc) { return $true } 92 | } 93 | } else { return $true } 94 | } | 95 | Where-Object { 96 | $fileInfo = $_ 97 | if ($Exclude) { 98 | foreach ($ex in $Exclude) { 99 | if ($fileInfo.Name -like $ex) { return $false } 100 | } 101 | return $true 102 | } else { 103 | return $true 104 | } 105 | } | 106 | Get-Command { $_.FullName }) 107 | 108 | 109 | $minifiedCommands = 110 | @($commandsToMinify | 111 | Compress-ScriptBlock @compressSplat -OutputPath { 112 | if ($GZip) { 113 | $_.Source -replace '\.ps1$', '.min.gzip.ps1' 114 | } else { 115 | $_.Source -replace '\.ps1$', '.min.ps1' 116 | } 117 | } -PassThru) 118 | "::group::Minified Commands" | Out-Host 119 | $minifiedCommands | Out-Host 120 | "::endgroup::" | Out-Host 121 | 122 | "::group::Summary" | Out-Host 123 | $totalOriginal = 0 124 | $totalMinified = 0 125 | for ($n =0 ; $n -lt $commandsToMinify.Length; $n++) { 126 | $safeName = $commandsToMinify[$n].Name -replace '\W' 127 | $originalSize = ([IO.FileInfo]$($commandsToMinify[$n].Source)).Length 128 | $totalOriginal+=$originalSize 129 | $minifiedSize = $minifiedCommands[$n].Length 130 | $totalMinified = $minifiedSize 131 | $minifiedPercent = $minifiedCommands[$n].Length / $originalSize 132 | "$($commandsToMinify[$n].name) -> $($minifiedCommands[$n].Name) - $([Math]::Round($minifiedPercent * 100, 2))%" | Out-Host 133 | "::set-output name=$($safeName)_MinifiedSize::$minifiedSize" | Out-Host 134 | "::set-output name=$($safeName)_MinifiedPercent::$minifiedPercent" | Out-Host 135 | } 136 | "Total Original Size: $([Math]::Round(($totalOriginal /1kb),2))kb" | Out-Host 137 | "::set-output name=OriginalSize::$totalOriginal" | Out-Host 138 | "::set-output name=MinifiedSize::$totalMinified" | Out-Host 139 | "::set-output name=MinifiedPercent::$($totalOriginal / $totalMinified)"| Out-Host 140 | "Total Minified Size: $([Math]::Round(($totalMinified /1kb),2))kb" | Out-Host 141 | 142 | "::endgroup::" | Out-Host 143 | 144 | if ($CommitMessage -and $minifiedCommands) { 145 | if (-not $UserName) { $UserName = $env:GITHUB_ACTOR } 146 | if (-not $UserEmail) { $UserEmail = "$UserName@github.com" } 147 | git config --global user.email $UserEmail 148 | git config --global user.name $UserName 149 | 150 | $filesUpdated = 0 151 | $minifiedCommands | 152 | ForEach-Object { 153 | $gitStatusOutput = git status $_.Fullname -s 154 | if ($gitStatusOutput) { 155 | git add $_.Fullname 156 | $filesUpdated++ 157 | } else { 158 | "No need to Commit $($_.FullName)" | Out-Host 159 | } 160 | } 161 | 162 | if ($filesUpdated) { 163 | $ErrorActionPreference = 'continue' 164 | $gitPushed = git push 2>&1 165 | "Git Push Output: $($gitPushed | Out-String)" 166 | $LASTEXITCODE = 0 167 | exit 0 168 | } else { 169 | "Nothing to Push" | Out-Host 170 | } 171 | } -------------------------------------------------------------------------------- /GitHub/Jobs/MinifyPSMinifier.psd1: -------------------------------------------------------------------------------- 1 | @{ 2 | "runs-on" = "ubuntu-latest" 3 | steps = @('Checkout','UseMinifierAction', 'OutputMinifier', 'UseMinifierActionGZip', 'OutputMinifierGZip', 'PublishMinified') 4 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Start-Automating 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Minify.psx.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | .SYNOPSIS 3 | Minify PipeScript Transpiler 4 | .DESCRIPTION 5 | Uses Compress-ScriptBlock to minify a section of PowerShell code 6 | .EXAMPLE 7 | { 8 | [minify]{ 9 | "a" 10 | "b" 11 | Get-Process -id $pid | Where-Object WorkingSet -gt 10mb 12 | } 13 | } | .>PipeScript 14 | .LINK 15 | https://github.com/StartAutomating/PipeScript 16 | #> 17 | param( 18 | [Parameter(Mandatory,ValueFromPipeline)] 19 | [ScriptBlock] 20 | $ScriptBlock 21 | ) 22 | 23 | Compress-ScriptBlock -ScriptBlock $ScriptBlock 24 | -------------------------------------------------------------------------------- /PSMinifier.GitHubAction.PSDevOps.ps1: -------------------------------------------------------------------------------- 1 | #requires -Module PSMinifier 2 | #requires -Module PSDevOps 3 | Import-BuildStep -ModuleName PSMinifier 4 | New-GitHubAction -Name "PSMinifier" -Description 'A Miniature Minifier For PowerShell' -Action PSMinifier -Icon minimize -ActionOutput ([Ordered]@{ 5 | OriginalSize = [Ordered]@{ 6 | description = "The Original Size of all files" 7 | value = '${{steps.PSMinifier.outputs.OriginalSize}}' 8 | } 9 | MinifiedSize = [Ordered]@{ 10 | description = "The Total Size of all minified files" 11 | value = '${{steps.PSMinifier.outputs.MinifiedSize}}' 12 | } 13 | MinifiedPercent= [Ordered]@{ 14 | description = "The Percentage Saved by minifying" 15 | value = '${{steps.PSMinifier.outputs.MinifiedPercent}}' 16 | } 17 | })| 18 | Set-Content .\action.yml -Encoding UTF8 -PassThru 19 | -------------------------------------------------------------------------------- /PSMinifier.psd1: -------------------------------------------------------------------------------- 1 | @{ 2 | ModuleVersion = '1.1.4' 3 | PowerShellVersion = '3.0' 4 | RootModule = 'PSMinifier.psm1' 5 | Description = 'A Miniature Minifier For PowerShell' 6 | Guid = '534cd9d0-e4b4-4c96-ae0f-6f31224274b9' 7 | Author = 'Start-Automating' 8 | Copyright = '2020 Start-Automating' 9 | PrivateData = @{ 10 | PSData = @{ 11 | Tags = 'Minifier', 'PipeScript' 12 | ProjectURI = 'https://github.com/StartAutomating/PSMinifier' 13 | LicenseURI = 'https://github.com/StartAutomating/PSMinifier/blob/master/LICENSE' 14 | ReleaseNotes = @' 15 | ### v1.1.4 16 | * Compress-ScriptBlock: Aliasing PSMinify (#13) 17 | * Adding support for Minify transpiler in PipeScript (#11) 18 | * Compress-ScriptBlock: Returning [ScriptBlock] if possible (#12) 19 | --- 20 | ### v1.1.3 21 | --- 22 | Compress-ScriptBlock bugfix: now handling using statements (Issue #6). Improvements to try/catch (Issue #7) 23 | 24 | ### v1.1.2 25 | ---- 26 | Compress-ScriptBlock bugfix: now handling [Hashtables] and [Ordered] (Issue #2) 27 | 28 | ### v1.1.1 29 | ---- 30 | Compress-ScriptBlock bugfix: try/catch/finally blocks now appropriately handled (Issue #3) 31 | 32 | ### v1.1 33 | ---- 34 | Compress-ScriptBlock now has -OutputPath/-PassThru 35 | Added Support for GitHub Action 36 | 37 | ### v1.0 38 | ---- 39 | Initial Version of Minifier 40 | '@ 41 | } 42 | } 43 | } -------------------------------------------------------------------------------- /PSMinifier.psm1: -------------------------------------------------------------------------------- 1 | . $PSScriptRoot\Compress-ScriptBlock.ps1 -------------------------------------------------------------------------------- /PSMinifier.tests.ps1: -------------------------------------------------------------------------------- 1 | #requires -Module Pester 2 | describe 'PSMinifier' { 3 | it 'Makes scripts smaller' { 4 | $compressScriptBlock = Get-Command Compress-ScriptBlock 5 | $compressed = $compressScriptBlock | 6 | Compress-ScriptBlock -Anonymous 7 | if ($compressed.Length -gt $compressScriptBlock.ScriptBlock.ToString().Length) { 8 | throw "Minified length $($compressed.Length) exceeds input length $($compressScriptBlock.ScriptBlock.ToString().Length)" 9 | } 10 | } 11 | 12 | it 'Can -GZip them to make them even smaller' { 13 | $compressScriptBlock = Get-Command Compress-ScriptBlock 14 | $compressed = $compressScriptBlock | 15 | Compress-ScriptBlock -Anonymous 16 | $gzipped = $compressScriptBlock | 17 | Compress-ScriptBlock -GZip -Anonymous 18 | if ($gzipped.Length -gt $compressScriptBlock.ScriptBlock.ToString().Length) { 19 | throw "GZipped length $($gzipped.Length) exceeds input length $($compressScriptBlock.ScriptBlock.ToString().Length)" 20 | } 21 | 22 | if ($gzipped.Length -gt $compressed.Length) { 23 | throw "GZipped length $($gzipped.Length) exceeds minified length $($compressed.Length)" 24 | } 25 | } 26 | 27 | it 'Can shrink try catch blocks' { 28 | 29 | $originalScript = { 30 | try { 31 | thisMightThrowBecauseThereIsNoCommand | # this commend should go away. 32 | thispipeline also uses a lot of whitespace 33 | } catch [System.SystemException] { 34 | "Oh No! An Exception Occured" | 35 | Out-String 36 | } finally { 37 | "FinallyGotSomethingDone" | Out-String 38 | } 39 | } 40 | $compresedScript = [Scriptblock]::Create((Compress-ScriptBlock -ScriptBlock $originalScript)) 41 | "$compressedScript".Length | 42 | Should -BeLessThan "$originalScript".Length 43 | @(& $compresedScript) -join '' | 44 | Should -BeLike "*Oh*No!*An*Exception*Occured*FinallyGotSomethingDone*" 45 | } 46 | 47 | it 'Can compress a [hashtable]' { 48 | Compress-ScriptBlock -ScriptBlock { 49 | @{ 50 | a = "b" 51 | c = "d" 52 | e = @{ 53 | f = "h" 54 | } 55 | } 56 | } | Should -Not -Match "\n" 57 | } 58 | 59 | it 'Can compress an ordered [hashtable]' { 60 | Compress-ScriptBlock -ScriptBlock { 61 | [Ordered]@{ 62 | a = "b" 63 | c = "d" 64 | e = @{ 65 | f = "h" 66 | } 67 | } 68 | } | Should -Not -Match "\n" 69 | } 70 | 71 | it 'Can compress a using statement' { 72 | $compressed = Compress-ScriptBlock -ScriptBlock ([ScriptBlock]::Create(@" 73 | using namespace System.Security.Cryptography 74 | using namespace System.Windows.Forms 75 | echo 1 76 | echo 2 77 | "@)) 78 | $compressed | Should -Match "using" 79 | $compressed | Should -Not -Match "\n" 80 | 81 | Invoke-Expression "$compressed" | Select-Object -First 1 | Should -Be 1 82 | } 83 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | PSMinifier [1.1.3] 3 | ================ 4 | A Miniature Minifier For PowerShell 5 | 6 | ---------------- 7 | 8 | 9 | ### Using the PSMinifier GitHub Action: 10 | 11 | 12 | The PSMinifier action is easy to use. By default, it will minify all .ps1 files not named *.*.ps1 within your GitHub Workspace. 13 | 14 | ~~~Yaml 15 | - name: PSMinifier 16 | uses: StartAutomating/PSMinifier@v1.1.3 17 | ~~~ 18 | 19 | This will generate a .min.ps1 for every PowerShell in your workspace. 20 | 21 | #### Commiting Minified Code 22 | 23 | 24 | If you would like to check in the minified code, simply provide a commit message 25 | 26 | ~~~yaml 27 | - name: PSMinifier 28 | uses: StartAutomating/PSMinifier@v1.1.3 29 | with: 30 | CommitMessage: "Minifying $($_.Name)" 31 | ~~~ 32 | 33 | 34 | #### Including and Excluding Paths 35 | 36 | The parameters of the GitHub action largely map to the parameters of Compress-ScriptBlock, with a couple of notable exceptions: 37 | * Include 38 | * Exclude 39 | ~~~yaml 40 | - name: PSMinifier 41 | uses: StartAutomating/PSMinifier@v1.1.3 42 | with: 43 | Include: "*.ps1" 44 | Exclude: "*.tests.ps1" 45 | CommitMessage: "Minifying $($_.Name)" 46 | ~~~ 47 | 48 | #### GZipping Minified Code 49 | 50 | For even more space savings and obfuscation, you can GZip minified code: 51 | ~~~yaml 52 | - name: PSMinifier 53 | uses: StartAutomating/PSMinifier@v1.1.3 54 | with: 55 | CommitMessage: "Minifying and GZipping $($_.Name)" 56 | GZip: true 57 | ~~~ 58 | 59 | 60 | ### PSMinifier Action Output 61 | 62 | The PSMinifier action includes some output parameters, such as: 63 | * MinifiedPercent 64 | * MinifiedSize 65 | * OriginalSize 66 | 67 | ~~~yaml 68 | - name: Use PSMinifier Action 69 | uses: StartAutomating/PSMinifier@v1.1.3 70 | id: Minify 71 | with: 72 | CommitMessage: Minifying $($_.Name) 73 | - name: OutputMinifier 74 | run: | 75 | echo Original Size ${{ steps.Minify.outputs.OriginalSize }} 76 | echo Minified Size ${{ steps.Minify.outputs.MinifiedSize }} 77 | echo Minified Percent ${{ steps.Minify.outputs.MinifiedPercent }} 78 | ~~~ 79 | 80 | 81 | 82 | ---------------- 83 | ### Module Commands 84 | ----------------------- 85 | | Verb|Noun | 86 | |-------:|:-----------| 87 | |Compress|-ScriptBlock| 88 | ----------------------- 89 | --- 90 | PSMinifier is a minature minifier for PowerShell. 91 | 92 | Minification makes your scripts smaller and much harder to read. Thus it is helpful when trying to reduce filesize, and not 93 | helpful when trying to make readable code. 94 | 95 | PSMinifier can minify itself with: 96 | 97 | ~~~ 98 | # This returns the minified contents of the definition of Compress-ScriptBlock 99 | Compress-ScriptBlock -ScriptBlock (Get-Command Compress-ScriptBlock).ScriptBlock 100 | ~~~ 101 | 102 | Or more elegantly, with: 103 | 104 | ~~~ 105 | # This returns the minified contents of Compress-ScriptBlock, assigned to a variable ${Compressed-ScriptBlock} 106 | Get-Command Compress-ScriptBlock | Compress-ScriptBlock 107 | ~~~ 108 | 109 | 110 | If that isn't compact or opaque enough, you can also -GZip the contents of a ScriptBlock 111 | 112 | ~~~ 113 | Get-Command Compress-ScriptBlock | Compress-ScriptBlock -GZip 114 | ~~~ 115 | 116 | If that isn't minified enough, you can pass both -GZip and -NoBlock to recreate the script block in one long line. 117 | ~~~ 118 | Get-Command Compress-ScriptBlock | Compress-ScriptBlock -GZip -NoBlock 119 | ~~~ 120 | 121 | 122 | Both of the preceeding examples minified a function into an anonymous Script Block. This can be useful for embedding single 123 | commands. 124 | You can also minify a .ps1 file to include directly within a module. 125 | ~~~ 126 | Get-Command Compress-ScriptBlock | # Get Compress-ScriptBlock 127 | Foreach-Object {$_.ScriptBlock.File }| # Get the file it is declared in 128 | Get-Command | # get the command (if you were in the directory, this would be Get-Command .\Compress-ScriptBlock.ps1) 129 | Compress-ScriptBlock -Anonymous # compress the script, but don't assign it to a variable. 130 | ~~~ 131 | 132 | 133 | If you want to GZip a ScriptBlock and declare the functions within it, you have to pass -DotSource: 134 | 135 | ~~~ 136 | Get-Command Compress-ScriptBlock | # Get Compress-ScriptBlock 137 | Foreach-Object {$_.ScriptBlock.File }| # Get the file it is declared in 138 | Get-Command | # get the command 139 | Compress-ScriptBlock -Anonymous -GZip -DotSource # compress and dot-source the script. 140 | ~~~ 141 | 142 | 143 | PSMinifier works by walking the PowerShell Abstract Syntax Tree and recreating a minimal version of your script. As such, any 144 | inline help will be lost. 145 | PSMinifier will not minify strings, as to do so would change functionality. 146 | 147 | Importantly, _PSMinifier is not an optimizer_. It does not change the content of your scripts, attempt to improve their 148 | performance, or rename your variables. 149 | 150 | If PSMinifier fails to minify your script, please open an issue on GitHub. 151 | -------------------------------------------------------------------------------- /action.yml: -------------------------------------------------------------------------------- 1 | 2 | name: PSMinifier 3 | description: A Miniature Minifier For PowerShell 4 | inputs: 5 | Include: 6 | required: false 7 | description: | 8 | One or more wildcards of files to include. 9 | If not provided, all .ps1 in a workspace will be included. 10 | Exclude: 11 | required: false 12 | default: '"*.*.ps1"' 13 | description: One or more wildcards of files to exclude. 14 | GZip: 15 | required: false 16 | description: If set, the minified content will be encoded as GZip, further reducing it's size. 17 | NoBlock: 18 | required: false 19 | description: | 20 | If set, zipped minified content will be encoded without blocks, making it a very long single line. 21 | This parameter is only valid with -GZip. 22 | CommitMessage: 23 | required: false 24 | description: If provided, will commit changes made to the workspace with this commit message. 25 | UserEmail: 26 | required: false 27 | description: The user email associated with a git commit. 28 | UserName: 29 | required: false 30 | description: The user name associated with a git commit. 31 | branding: 32 | icon: minimize 33 | color: blue 34 | outputs: 35 | 36 | OriginalSize: 37 | description: The Original Size of all files 38 | value: ${{steps.PSMinifier.outputs.OriginalSize}} 39 | MinifiedSize: 40 | description: The Total Size of all minified files 41 | value: ${{steps.PSMinifier.outputs.MinifiedSize}} 42 | MinifiedPercent: 43 | description: The Percentage Saved by minifying 44 | value: ${{steps.PSMinifier.outputs.MinifiedPercent}} 45 | runs: 46 | using: composite 47 | steps: 48 | - name: PSMinifier 49 | id: PSMinifier 50 | shell: pwsh 51 | env: 52 | CommitMessage: ${{inputs.CommitMessage}} 53 | NoBlock: ${{inputs.NoBlock}} 54 | UserEmail: ${{inputs.UserEmail}} 55 | GZip: ${{inputs.GZip}} 56 | Exclude: ${{inputs.Exclude}} 57 | Include: ${{inputs.Include}} 58 | UserName: ${{inputs.UserName}} 59 | run: | 60 | $Parameters = @{} 61 | $Parameters.Include = ${env:Include} 62 | $Parameters.Include = $parameters.Include -split ';' -replace '^[''"]' -replace '[''"]$' 63 | $Parameters.Exclude = ${env:Exclude} 64 | $Parameters.Exclude = $parameters.Exclude -split ';' -replace '^[''"]' -replace '[''"]$' 65 | $Parameters.GZip = ${env:GZip} 66 | $Parameters.GZip = $parameters.GZip -match 'true'; 67 | $Parameters.NoBlock = ${env:NoBlock} 68 | $Parameters.NoBlock = $parameters.NoBlock -match 'true'; 69 | $Parameters.CommitMessage = ${env:CommitMessage} 70 | $Parameters.UserEmail = ${env:UserEmail} 71 | $Parameters.UserName = ${env:UserName} 72 | foreach ($k in @($parameters.Keys)) { 73 | if ([String]::IsNullOrEmpty($parameters[$k])) { 74 | $parameters.Remove($k) 75 | } 76 | } 77 | Write-Host "::debug:: PSMinifier $(@(foreach ($p in $Parameters.GetEnumerator()) {'-' + $p.Key + ' ' + $p.Value}) -join ' ')" 78 | & {<# 79 | .Synopsis 80 | PSMinifier Action 81 | .Description 82 | Runs PSMinifier on code in the workspace, and creates .min.ps1 files. 83 | #> 84 | param( 85 | # One or more wildcards of files to include. 86 | # If not provided, all .ps1 in a workspace will be included. 87 | [string[]] 88 | $Include, 89 | 90 | # One or more wildcards of files to exclude. 91 | [string[]] 92 | $Exclude = "*.*.ps1", 93 | 94 | # If set, the minified content will be encoded as GZip, further reducing it's size. 95 | [switch] 96 | $GZip, 97 | 98 | # If set, zipped minified content will be encoded without blocks, making it a very long single line. 99 | # This parameter is only valid with -GZip. 100 | [switch] 101 | $NoBlock, 102 | 103 | # If provided, will commit changes made to the workspace with this commit message. 104 | [string] 105 | $CommitMessage, 106 | 107 | # The user email associated with a git commit. 108 | [string] 109 | $UserEmail, 110 | 111 | # The user name associated with a git commit. 112 | [string] 113 | $UserName 114 | ) 115 | 116 | "::group::Parameters" | Out-Host 117 | [PSCustomObject]$PSBoundParameters | Format-List | Out-Host 118 | "::endgroup::" | Out-Host 119 | 120 | @" 121 | ::group::GitHubEvent 122 | $($gitHubEvent | ConvertTo-Json -Depth 100) 123 | ::endgroup:: 124 | "@ | Out-Host 125 | 126 | $PSD1Found = Get-ChildItem -Recurse -Filter "*.psd1" | Where-Object Name -eq 'PSMinifier.psd1' | Select-Object -First 1 127 | 128 | if ($PSD1Found) { 129 | $psMinifierPath = $PSD1Found 130 | Import-Module $PSD1Found -Force -PassThru | Out-Host 131 | } 132 | elseif ($env:GITHUB_ACTION_PATH) { 133 | $psMinifierPath = Join-Path $env:GITHUB_ACTION_PATH 'PSMinifier.psd1' 134 | if (Test-path $psMinifierPath) { 135 | Import-Module $psMinifierPath -Force -PassThru | Out-String 136 | } else { 137 | throw "PSMinifier not found" 138 | } 139 | } elseif (-not (Get-Module PSMinifier)) { 140 | Get-ChildItem env: | Out-String 141 | throw "Action Path not found" 142 | } 143 | 144 | "::debug::PSMinifier Loaded from Path - $($psMinifierPath)" | Out-Host 145 | 146 | if (-not $env:GITHUB_WORKSPACE) { throw "No GitHub workspace" } 147 | if (-not $CommitMessage -and $gitHubEvent.head_commit.message) { 148 | $CommitMessage = $gitHubEvent.head_commit.message 149 | } 150 | 151 | $compressSplat = @{} + $PSBoundParameters 152 | $compressSplat.Remove('Include') 153 | $compressSplat.Remove('Exclude') 154 | $compressSplat.Remove('CommitMessage') 155 | $compressSplat.Remove('UserEmail') 156 | $compressSplat.Remove('UserName') 157 | if ($GZip) { $compressSplat.DotSource = $true } 158 | 159 | "EXCLUDING $Exclude" | Out-Host 160 | 161 | $commandsToMinify = 162 | @(Get-ChildItem -LiteralPath $env:GITHUB_WORKSPACE -Filter *.ps1 | 163 | Where-Object { 164 | $fileInfo = $_ 165 | if ($fileInfo.Name -like '*.min.*ps1') { return } # Don't overminify 166 | if ($Include) { 167 | foreach ($inc in $Include) { 168 | if ($fileInfo.Name -like $inc) { return $true } 169 | } 170 | } else { return $true } 171 | } | 172 | Where-Object { 173 | $fileInfo = $_ 174 | if ($Exclude) { 175 | foreach ($ex in $Exclude) { 176 | if ($fileInfo.Name -like $ex) { return $false } 177 | } 178 | return $true 179 | } else { 180 | return $true 181 | } 182 | } | 183 | Get-Command { $_.FullName }) 184 | 185 | 186 | $minifiedCommands = 187 | @($commandsToMinify | 188 | Compress-ScriptBlock @compressSplat -OutputPath { 189 | if ($GZip) { 190 | $_.Source -replace '\.ps1$', '.min.gzip.ps1' 191 | } else { 192 | $_.Source -replace '\.ps1$', '.min.ps1' 193 | } 194 | } -PassThru) 195 | "::group::Minified Commands" | Out-Host 196 | $minifiedCommands | Out-Host 197 | "::endgroup::" | Out-Host 198 | 199 | "::group::Summary" | Out-Host 200 | $totalOriginal = 0 201 | $totalMinified = 0 202 | for ($n =0 ; $n -lt $commandsToMinify.Length; $n++) { 203 | $safeName = $commandsToMinify[$n].Name -replace '\W' 204 | $originalSize = ([IO.FileInfo]$($commandsToMinify[$n].Source)).Length 205 | $totalOriginal+=$originalSize 206 | $minifiedSize = $minifiedCommands[$n].Length 207 | $totalMinified = $minifiedSize 208 | $minifiedPercent = $minifiedCommands[$n].Length / $originalSize 209 | "$($commandsToMinify[$n].name) -> $($minifiedCommands[$n].Name) - $([Math]::Round($minifiedPercent * 100, 2))%" | Out-Host 210 | "::set-output name=$($safeName)_MinifiedSize::$minifiedSize" | Out-Host 211 | "::set-output name=$($safeName)_MinifiedPercent::$minifiedPercent" | Out-Host 212 | } 213 | "Total Original Size: $([Math]::Round(($totalOriginal /1kb),2))kb" | Out-Host 214 | "::set-output name=OriginalSize::$totalOriginal" | Out-Host 215 | "::set-output name=MinifiedSize::$totalMinified" | Out-Host 216 | "::set-output name=MinifiedPercent::$($totalOriginal / $totalMinified)"| Out-Host 217 | "Total Minified Size: $([Math]::Round(($totalMinified /1kb),2))kb" | Out-Host 218 | 219 | "::endgroup::" | Out-Host 220 | 221 | if ($CommitMessage -and $minifiedCommands) { 222 | if (-not $UserName) { $UserName = $env:GITHUB_ACTOR } 223 | if (-not $UserEmail) { $UserEmail = "$UserName@github.com" } 224 | git config --global user.email $UserEmail 225 | git config --global user.name $UserName 226 | 227 | $filesUpdated = 0 228 | $minifiedCommands | 229 | ForEach-Object { 230 | $gitStatusOutput = git status $_.Fullname -s 231 | if ($gitStatusOutput) { 232 | git add $_.Fullname 233 | $filesUpdated++ 234 | } else { 235 | "No need to Commit $($_.FullName)" | Out-Host 236 | } 237 | } 238 | 239 | if ($filesUpdated) { 240 | $ErrorActionPreference = 'continue' 241 | $gitPushed = git push 2>&1 242 | "Git Push Output: $($gitPushed | Out-String)" 243 | $LASTEXITCODE = 0 244 | exit 0 245 | } else { 246 | "Nothing to Push" | Out-Host 247 | } 248 | }} @Parameters 249 | 250 | -------------------------------------------------------------------------------- /en-us/About_PSMinifier.help.txt: -------------------------------------------------------------------------------- 1 | PSMinifier is a minature minifier for PowerShell. 2 | 3 | Minification makes your scripts smaller and much harder to read. Thus it is helpful when trying to reduce filesize, and not helpful when trying to make readable code. 4 | 5 | PSMinifier can minify itself with: 6 | 7 | ~~~ 8 | # This returns the minified contents of the definition of Compress-ScriptBlock 9 | Compress-ScriptBlock -ScriptBlock (Get-Command Compress-ScriptBlock).ScriptBlock 10 | ~~~ 11 | 12 | Or more elegantly, with: 13 | 14 | ~~~ 15 | # This returns the minified contents of Compress-ScriptBlock, assigned to a variable ${Compressed-ScriptBlock} 16 | Get-Command Compress-ScriptBlock | Compress-ScriptBlock 17 | ~~~ 18 | 19 | 20 | If that isn't compact or opaque enough, you can also -GZip the contents of a ScriptBlock 21 | 22 | ~~~ 23 | Get-Command Compress-ScriptBlock | Compress-ScriptBlock -GZip 24 | ~~~ 25 | 26 | If that isn't minified enough, you can pass both -GZip and -NoBlock to recreate the script block in one long line. 27 | ~~~ 28 | Get-Command Compress-ScriptBlock | Compress-ScriptBlock -GZip -NoBlock 29 | ~~~ 30 | 31 | 32 | Both of the preceeding examples minified a function into an anonymous Script Block. This can be useful for embedding single commands. 33 | You can also minify a .ps1 file to include directly within a module. 34 | ~~~ 35 | Get-Command Compress-ScriptBlock | # Get Compress-ScriptBlock 36 | Foreach-Object {$_.ScriptBlock.File }| # Get the file it is declared in 37 | Get-Command | # get the command (if you were in the directory, this would be Get-Command .\Compress-ScriptBlock.ps1) 38 | Compress-ScriptBlock -Anonymous # compress the script, but don't assign it to a variable. 39 | ~~~ 40 | 41 | 42 | If you want to GZip a ScriptBlock and declare the functions within it, you have to pass -DotSource: 43 | 44 | ~~~ 45 | Get-Command Compress-ScriptBlock | # Get Compress-ScriptBlock 46 | Foreach-Object {$_.ScriptBlock.File }| # Get the file it is declared in 47 | Get-Command | # get the command 48 | Compress-ScriptBlock -Anonymous -GZip -DotSource # compress and dot-source the script. 49 | ~~~ 50 | 51 | 52 | PSMinifier works by walking the PowerShell Abstract Syntax Tree and recreating a minimal version of your script. As such, any inline help will be lost. 53 | PSMinifier will not minify strings, as to do so would change functionality. 54 | 55 | Importantly, _PSMinifier is not an optimizer_. It does not change the content of your scripts, attempt to improve their performance, or rename your variables. 56 | 57 | If PSMinifier fails to minify your script, please open an issue on GitHub. --------------------------------------------------------------------------------