From da9153fd1cc0c69b6cb5e3d55b71597910e5f148 Mon Sep 17 00:00:00 2001 From: Joel Bennett Date: Fri, 15 May 2026 23:29:34 -0400 Subject: [PATCH 01/10] Explicitly add a Windows PowerShell job to the build matrix. There are people still using this there. --- .github/workflows/build.yml | 60 +++++++++++++++--------- Source/Public/Invoke-ScriptGenerator.ps1 | 8 ++-- Tests/Integration/Parameters.Tests.ps1 | 11 ++--- Tests/Private/InitializeBuild.Tests.ps1 | 8 ++-- Tests/Public/Add-Parameter.Tests.ps1 | 12 ++--- Tests/Public/Merge-ScriptBlock.Tests.ps1 | 32 ++++++------- build.build.ps1 | 2 +- 7 files changed, 76 insertions(+), 57 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index a30e165..d678fee 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -70,6 +70,10 @@ jobs: fail-fast: false matrix: os: [ windows-latest, ubuntu-latest, macos-latest ] + shell: [ pwsh ] + include: + - os: windows-latest + shell: powershell steps: - name: Download build.requires.psd1 uses: actions/download-artifact@v8 @@ -79,39 +83,53 @@ jobs: uses: actions/download-artifact@v8 with: name: ModuleBuilder - path: output/ModuleBuilder # /home/runner/work/ModuleBuilder/ModuleBuilder/output/ModuleBuilder + path: Modules/ModuleBuilder # /home/runner/work/ModuleBuilder/ModuleBuilder/output/ModuleBuilder - name: Download Pester Tests uses: actions/download-artifact@v8 with: name: PesterTests path: PesterTests - - name: Install Output Modules + # Avoid installing ModuleBuilder with ModuleFast, so there's only one copy + - name: Remove ModuleBuilder from build.requires shell: pwsh run: | # PowerShell - # https://docs.github.com/en/actions/use-cases-and-examples/building-and-testing/building-and-testing-powershell#powershell-module-locations - $ModuleDestination = if ($IsWindows) { - Join-Path ([Environment]::GetFolderPath('MyDocuments')) 'PowerShell/Modules' - } else { - Join-Path $HOME '.local/share/powershell/Modules' - } - - Get-ChildItem -Directory output -OutVariable Modules - | Move-Item -Destination { Join-Path $ModuleDestination $_.Name } -Force - - Write-Host "Installing $($Modules -join ', ') to $ModuleDestination" - Get-ChildItem -Directory $ModuleDestination - Write-Host "PSModulePath:" - $Env:PSModulePath -split ([IO.Path]::PathSeparator) | Out-Host - - # Avoid installing ModuleBuilder with ModuleFast, so there's only one copy - @(Get-Content build.requires.psd1) - | Where { $_ -notmatch "ModuleBuilder"} - | Set-Content build.requires.psd1 + @(Get-Content build.requires.psd1).Where({ $_ -notmatch "ModuleBuilder"}) | Set-Content build.requires.psd1 - name: ⚡ Install Required Modules uses: JustinGrote/ModuleFast-action@v0.0.1 + env: + MODULEFAST_DESTINATION: ${{ github.workspace }}/Modules - name: Invoke-Pester + if: matrix.shell == 'powershell' + shell: powershell + env: + MODULEFAST_DESTINATION: ${{ github.workspace }}/Modules + run: | # PowerShell + $Env:PSModulePath = $Env:MODULEFAST_DESTINATION + [IO.Path]::PathSeparator + $Env:PSModulePath + + # For the cross-platform matrix we don't need to do coverage or anything complicated + $Result = Invoke-Pester . -PassThru + @( + "## Pester Tests for ${{ matrix.os }}" + "" + $Result.Duration.ToString() + "| Total | Passed | Failed |" + "|------:|-------:|-------:|" + "| $($Result.TotalCount) | $($Result.PassedCount) | $($Result.FailedCount) |" + "" + "| Duration | Total | Passed | Failed | Skipped | Name |" + "|---------:|------:|-------:|-------:|--------:|:-----|" + @($Result.Containers).ForEach{ + "| $($_.Duration) | $($_.TotalCount) | $($_.PassedCount) | $($_.FailedCount) | $($_.SkippedCount) | $($_.Name) |" + } + ) | Out-File -FilePath $env:GITHUB_STEP_SUMMARY + - name: Invoke-Pester + if: matrix.shell == 'pwsh' shell: pwsh + env: + MODULEFAST_DESTINATION: ${{ github.workspace }}/Modules run: | # PowerShell + $Env:PSModulePath = $Env:MODULEFAST_DESTINATION + [IO.Path]::PathSeparator + $Env:PSModulePath + # For the cross-platform matrix we don't need to do coverage or anything complicated $Result = Invoke-Pester . -PassThru @( diff --git a/Source/Public/Invoke-ScriptGenerator.ps1 b/Source/Public/Invoke-ScriptGenerator.ps1 index dc5d0d2..165f1bc 100644 --- a/Source/Public/Invoke-ScriptGenerator.ps1 +++ b/Source/Public/Invoke-ScriptGenerator.ps1 @@ -114,9 +114,11 @@ function Invoke-ScriptGenerator { } # Find that generator... - $GeneratorCmd = Get-Command -Name ${Generator} -ParameterType Ast -ErrorAction Ignore <# -CommandType Function #> - | Where-Object { $_.OutputType.Name -eq "TextReplacement" -or ($_.CommandType -eq "Alias" -and $_.Definition -like "PesterMock*" ) } - | Select-Object -First 1 + $GeneratorCmd = Get-Command -Name ${Generator} -ParameterType Ast -ErrorAction Ignore <# -CommandType Function #> | + Where-Object { + $_.OutputType.Name -eq "TextReplacement" -or ($_.CommandType -eq "Alias" -and $_.Definition -like "PesterMock*" ) + } | + Select-Object -First 1 if (-not $GeneratorCmd) { Write-Error "Generator missconfiguration. Unable to find Generator = '$Generator'" diff --git a/Tests/Integration/Parameters.Tests.ps1 b/Tests/Integration/Parameters.Tests.ps1 index 2cec9c9..8ffdfc6 100644 --- a/Tests/Integration/Parameters.Tests.ps1 +++ b/Tests/Integration/Parameters.Tests.ps1 @@ -11,15 +11,14 @@ Describe "Parameters" -Tag Integration { New-Item $PSScriptRoot/Result3/Parameters/3.0.0/DeleteMe.md -ItemType File -Force Write-Host "Module Under Test:" - Get-Command Build-Module - | Get-Module -Name { $_.Source } - | Get-Item - | Out-Host + Get-Command Build-Module | + Get-Module -Name { $_.Source } | + Get-Item | + Out-Host } It "Passthru is read from the build manifest" { - Build-Module (Convert-FolderSeparator "$PSScriptRoot/Parameters/build.psd1") -Verbose -OutVariable Output - | Out-Host + Build-Module (Convert-FolderSeparator "$PSScriptRoot/Parameters/build.psd1") -Verbose -OutVariable Output | Out-Host $Output | Should -Not -BeNullOrEmpty $Output.Path | Convert-FolderSeparator | Should -Be (Convert-FolderSeparator "$PSScriptRoot/Result3/Parameters/3.0.0/Parameters.psd1") diff --git a/Tests/Private/InitializeBuild.Tests.ps1 b/Tests/Private/InitializeBuild.Tests.ps1 index 5db014b..b03d024 100644 --- a/Tests/Private/InitializeBuild.Tests.ps1 +++ b/Tests/Private/InitializeBuild.Tests.ps1 @@ -52,11 +52,11 @@ Describe "InitializeBuild" { $Result.Result.Name | Should -Be "MyModule" $Result.Result.SourceDirectories | Should -Be @("Classes", "Private", "Public") - (Convert-FolderSeparator $Result.Result.ModuleBase) - | Should -Be (Convert-FolderSeparator "TestDrive:\Source") + (Convert-FolderSeparator $Result.Result.ModuleBase) | + Should -Be (Convert-FolderSeparator "TestDrive:\Source") - (Convert-FolderSeparator $Result.Result.SourcePath) - | Should -Be (Convert-FolderSeparator "TestDrive:\Source\MyModule.psd1") + (Convert-FolderSeparator $Result.Result.SourcePath) | + Should -Be (Convert-FolderSeparator "TestDrive:\Source\MyModule.psd1") } It "Returns default values from the Build Command" { diff --git a/Tests/Public/Add-Parameter.Tests.ps1 b/Tests/Public/Add-Parameter.Tests.ps1 index da3ac2b..13d7684 100644 --- a/Tests/Public/Add-Parameter.Tests.ps1 +++ b/Tests/Public/Add-Parameter.Tests.ps1 @@ -65,12 +65,12 @@ Describe "Add-Parameter" { $showDate | Should -Not -BeNullOrEmpty - $showDate.Body.ParamBlock.Parameters.Name.VariablePath.UserPath - | Should -Be @('Format', 'ForegroundColor', 'BackgroundColor') + $showDate.Body.ParamBlock.Parameters.Name.VariablePath.UserPath | + Should -Be @('Format', 'ForegroundColor', 'BackgroundColor') $showUserName | Should -Not -BeNullOrEmpty - $showUserName.Body.ParamBlock.Parameters.Name.VariablePath.UserPath - | Should -Be @('ForegroundColor', 'BackgroundColor') + $showUserName.Body.ParamBlock.Parameters.Name.VariablePath.UserPath | + Should -Be @('ForegroundColor', 'BackgroundColor') # Get-Date Should not be modified, since it does not match the FunctionName filter $getDate = $Ast.Find({ @@ -79,8 +79,8 @@ Describe "Add-Parameter" { $node.Name -eq 'Get-Date' }, $true) $getDate | Should -Not -BeNullOrEmpty - $getDate.Body.ParamBlock.Parameters.Name.VariablePath.UserPath - | Should -Be @('Format') + $getDate.Body.ParamBlock.Parameters.Name.VariablePath.UserPath | + Should -Be @('Format') } } } diff --git a/Tests/Public/Merge-ScriptBlock.Tests.ps1 b/Tests/Public/Merge-ScriptBlock.Tests.ps1 index d15d11f..6e2941f 100644 --- a/Tests/Public/Merge-ScriptBlock.Tests.ps1 +++ b/Tests/Public/Merge-ScriptBlock.Tests.ps1 @@ -60,24 +60,24 @@ Describe "Merge-ScriptBlock" { $showDate | Should -Not -BeNullOrEmpty - $showDate.Body.EndBlock -split "`n" - | ForEach-Object { $_.Trim() } - | Select-Object -Skip 1 - | Select-Object -First 3 - | Should -Be @( - "`$ForegroundColor.ToVt() + `$BackgroundColor.ToVt(`$true) + (" - "Get-Date -Format `$Format" - ") + `"``e[0m`"" - ) + $showDate.Body.EndBlock -split "`n" | + ForEach-Object { $_.Trim() } | + Select-Object -Skip 1 | + Select-Object -First 3 | + Should -Be @( + "`$ForegroundColor.ToVt() + `$BackgroundColor.ToVt(`$true) + (" + "Get-Date -Format `$Format" + ") + `"``e[0m`"" + ) $showUserName | Should -Not -BeNullOrEmpty - $showUserName.Body.EndBlock -split "`n" - | ForEach-Object { $_.Trim() } - | Select-Object -Skip 1 - | Select-Object -First 3 - | Should -Be @( - "`$ForegroundColor.ToVt() + `$BackgroundColor.ToVt(`$true) + (" - "[Environment]::UserName" + $showUserName.Body.EndBlock -split "`n" | + ForEach-Object { $_.Trim() } | + Select-Object -Skip 1 | + Select-Object -First 3 | + Should -Be @( + "`$ForegroundColor.ToVt() + `$BackgroundColor.ToVt(`$true) + (" + "[Environment]::UserName" ") + `"``e[0m`"" ) } diff --git a/build.build.ps1 b/build.build.ps1 index 053c3ec..53e6e49 100644 --- a/build.build.ps1 +++ b/build.build.ps1 @@ -19,7 +19,7 @@ Write-Information "$($PSStyle.Foreground.BrightMagenta)build.build.ps1$($PSStyle ## Self-contained build script - can be invoked directly or via Invoke-Build if ($MyInvocation.ScriptName -notlike '*Invoke-Build.ps1') { - . (Convert-Path ../../[tT]asks/scripts/Invoke-Build.ps1) -File $MyInvocation.MyCommand.Path @PSBoundParameters -Result Result + & (Convert-Path ../../[tT]asks/scripts/Invoke-Build.ps1) -File $MyInvocation.MyCommand.Path @PSBoundParameters -Result Result if ($Result.Error) { $Error[-1].ScriptStackTrace | Out-Host From 666ec3e4da93e3efc6bff0662c8c77cf45875bd7 Mon Sep 17 00:00:00 2001 From: Joel Bennett Date: Sat, 16 May 2026 17:13:37 -0400 Subject: [PATCH 02/10] Fix Windows PowerShell regressions in tests. --- Tests/Private/CompressToBase64.Tests.ps1 | 8 ++++---- Tests/Private/ConvertToAst.Tests.ps1 | 2 +- .../Public/ConvertTo-SourceLineNumber.Tests.ps1 | 17 +++++++---------- 3 files changed, 12 insertions(+), 15 deletions(-) diff --git a/Tests/Private/CompressToBase64.Tests.ps1 b/Tests/Private/CompressToBase64.Tests.ps1 index 898d81f..b11bcc0 100644 --- a/Tests/Private/CompressToBase64.Tests.ps1 +++ b/Tests/Private/CompressToBase64.Tests.ps1 @@ -4,7 +4,7 @@ Describe "CompressToBase64" { Context "It compresses and encodes a file for embedding into a script" { BeforeAll { $Base64 = InModuleScope ModuleBuilder { - CompressToBase64 $PSCommandPath + CompressToBase64 (Join-Path $PSScriptRoot CompressToBase64.Tests.ps1) } } @@ -22,14 +22,14 @@ Describe "CompressToBase64" { $OutputStream.Seek(0, "Begin") $Source = [System.IO.StreamReader]::new($OutputStream, $true).ReadToEnd() - $Source | Should -Be (Get-Content $PSCommandPath -Raw) + $Source | Should -Be (Get-Content (Join-Path $PSScriptRoot CompressToBase64.Tests.ps1) -Raw) } } Context "It wraps the Base64 encoded content in the specified command" { BeforeAll { $Base64 = InModuleScope ModuleBuilder { - CompressToBase64 $PSCommandPath -ExpandScriptName ImportBase64Module + CompressToBase64 (Join-Path $PSScriptRoot CompressToBase64.Tests.ps1) -ExpandScriptName ImportBase64Module } } @@ -49,7 +49,7 @@ Describe "CompressToBase64" { Context "It wraps the Base64 encoded content in the specified scriptblock" { BeforeAll { $Base64 = InModuleScope ModuleBuilder { - Get-ChildItem $PSCommandPath | CompressToBase64 -ExpandScript { ImportBase64Module } + Get-ChildItem (Join-Path $PSScriptRoot CompressToBase64.Tests.ps1) | CompressToBase64 -ExpandScript { ImportBase64Module } } } diff --git a/Tests/Private/ConvertToAst.Tests.ps1 b/Tests/Private/ConvertToAst.Tests.ps1 index a71589c..92b668b 100644 --- a/Tests/Private/ConvertToAst.Tests.ps1 +++ b/Tests/Private/ConvertToAst.Tests.ps1 @@ -4,7 +4,7 @@ Describe "ConvertToAst" { Context "It returns a ParseResult for file paths" { BeforeAll { $ParseResult = InModuleScope ModuleBuilder { - ConvertToAst -Code $PSCommandPath + ConvertToAst -Code (Join-Path $PSScriptRoot ConvertToAst.Tests.ps1) } } diff --git a/Tests/Public/ConvertTo-SourceLineNumber.Tests.ps1 b/Tests/Public/ConvertTo-SourceLineNumber.Tests.ps1 index 8f432d9..7e1eb44 100644 --- a/Tests/Public/ConvertTo-SourceLineNumber.Tests.ps1 +++ b/Tests/Public/ConvertTo-SourceLineNumber.Tests.ps1 @@ -35,29 +35,26 @@ Describe "ConvertTo-SourceLineNumber" { } It "Should throw if the SourceFile doesn't exist" { - { Convert-LineNumber -SourceFile TestDrive:/NoSuchFile -SourceLineNumber 10 } | - Should -Throw "'TestDrive:/NoSuchFile' does not exist" + { ConvertTo-SourceLineNumber -SourceFile TestDrive:${\}NoSuchFile -SourceLineNumber 10 } | + Should -Throw "'TestDrive:${\}NoSuchFile' does not exist" } It 'Should work with an error PositionMessage' { $line = Select-String -Path $Convert_LineNumber_ModulePath 'function Set-Source {' | ForEach-Object LineNumber - $SourceLocation = "At ${Convert_LineNumber_ModulePath}:$line char:17" | Convert-LineNumber - # This test is assuming you built the code on Windows. Should Convert-LineNumber convert the path? + $SourceLocation = "At ${Convert_LineNumber_ModulePath}:$line char:17" | ConvertTo-SourceLineNumber $SourceLocation.SourceFile | Should -Be ".${\}Public${\}Set-Source.ps1" $SourceLocation.SourceLineNumber | Should -Be 1 } It 'Should work with ScriptStackTrace messages' { - $SourceFile = Join-Path $Convert_LineNumber_ModuleSource Public/Set-Source.ps1 | Convert-Path - - $outputLine = Select-String -Path $Convert_LineNumber_ModulePath "sto͞o′pĭd" | % LineNumber - $sourceLine = Select-String -Path $SourceFile "sto͞o′pĭd" | % LineNumber + $SourceFile = Join-Path $Convert_LineNumber_ModuleSource (Join-Path Public Set-Source.ps1) | Convert-Path - $SourceLocation = "At Set-Source, ${Convert_LineNumber_ModulePath}: line $outputLine" | Convert-LineNumber + $outputLine = Select-String -Path $Convert_LineNumber_ModulePath "sto͞o′pĭd" | ForEach-Object LineNumber + $sourceLine = Select-String -Path $SourceFile "sto͞o′pĭd" | ForEach-Object LineNumber - # This test is assuming you built the code on Windows. Should Convert-LineNumber convert the path? + $SourceLocation = "At Set-Source, ${Convert_LineNumber_ModulePath}: line $outputLine" | ConvertTo-SourceLineNumber $SourceLocation.SourceFile | Should -Be ".${\}Public${\}Set-Source.ps1" $SourceLocation.SourceLineNumber | Should -Be $sourceLine } From 8ca33f94944f7485e5713cfabf236c618085cc86 Mon Sep 17 00:00:00 2001 From: Joel Bennett Date: Mon, 18 May 2026 22:41:17 -0400 Subject: [PATCH 03/10] Add test covverage Put back the tests for: - Move-UsingStatement - Update-AliasesToExport --- Source/Public/Update-AliasesToExport.ps1 | 33 +- Tests/Public/Move-UsingStatement.Tests.ps1 | 116 +++++++ .../Public/Move-UsingStatement.Tests.ps1.old | 148 --------- Tests/Public/Update-AliasesToExport.Tests.ps1 | 308 ++++++++++++++++++ .../Update-AliasesToExport.Tests.ps1.old | 180 ---------- 5 files changed, 454 insertions(+), 331 deletions(-) create mode 100644 Tests/Public/Move-UsingStatement.Tests.ps1 delete mode 100644 Tests/Public/Move-UsingStatement.Tests.ps1.old create mode 100644 Tests/Public/Update-AliasesToExport.Tests.ps1 delete mode 100644 Tests/Public/Update-AliasesToExport.Tests.ps1.old diff --git a/Source/Public/Update-AliasesToExport.ps1 b/Source/Public/Update-AliasesToExport.ps1 index 0a11423..5532f2b 100644 --- a/Source/Public/Update-AliasesToExport.ps1 +++ b/Source/Public/Update-AliasesToExport.ps1 @@ -20,7 +20,14 @@ function Update-AliasesToExport { # The path to the module manifest that should contain the aliases [Parameter(Mandatory, ValueFromPipelineByPropertyName)] - [string]$ModuleManifest + [string]$ModuleManifest, + + # Controls what to set AliasesToExport to when no aliases are found by static analysis. + # DoNotSet: (default) leave the manifest unchanged. + # Wildcard: set AliasesToExport = '*'. + # EmptyArray: set AliasesToExport = @(). + [ValidateSet("DoNotSet", "Wildcard", "EmptyArray")] + [string]$WhenNoAliases = "DoNotSet" ) begin { # This is used only to parse the parameters to New|Set|Remove-Alias @@ -141,9 +148,29 @@ function Update-AliasesToExport { } } process { + $null = Get-Metadata -Path $ModuleManifest -PropertyName AliasesToExport -ErrorAction SilentlyContinue -ErrorVariable Failed + if ($Failed) { + Write-Warning "Can't update AliasesToExport in '$ModuleManifest' unless it's already set." + return + } + $Visitor = [AliasExportGenerator]::new() $ScriptModule.Visit($Visitor) - Update-Metadata -Path $ModuleManifest -PropertyName AliasesToExport -Value $Visitor.Aliases -WhatIf:$WhatIfPreference -Confirm:($ConfirmPreference -eq 'Low') + if ($Visitor.Aliases.Count -gt 0) { + $newValue = $Visitor.Aliases + } else { + switch ($WhenNoAliases) { + "DoNotSet" { + return + } + "Wildcard" { + $newValue = '*' + } + "EmptyArray" { + $newValue = @() + } + } + } + Update-Metadata -Path $ModuleManifest -PropertyName AliasesToExport -Value $newValue -WhatIf:$WhatIfPreference -Confirm:($ConfirmPreference -eq 'Low') } } - diff --git a/Tests/Public/Move-UsingStatement.Tests.ps1 b/Tests/Public/Move-UsingStatement.Tests.ps1 new file mode 100644 index 0000000..c284252 --- /dev/null +++ b/Tests/Public/Move-UsingStatement.Tests.ps1 @@ -0,0 +1,116 @@ +#requires -Module ModuleBuilder +Describe "Move-UsingStatement" { + Context "Moving Using Statements to the beginning of the file" { + BeforeDiscovery { + $TestCases = @( + @{ + TestCaseName = 'Moves all using statements in `n terminated files to the top' + PSM1File = "function x {`n}`n" + + "using namespace System.IO`n`n" + + "function y {`n}`n" + + "using namespace System.Drawing" + ErrorBefore = 2 + ErrorAfter = 0 + }, + @{ + TestCaseName = 'Moves all using statements in `r`n terminated files to the top' + PSM1File = "function x {`r`n}`r`n" + + "USING namespace System.IO`r`n`r`n" + + "function y {`r`n}`r`n" + + "USING namespace System.Drawing" + ErrorBefore = 2 + ErrorAfter = 0 + }, + @{ + TestCaseName = 'Prevents duplicate using statements' + PSM1File = "using namespace System.IO`r`n" + + "function x {`r`n}`r`n`r`n" + + "using namespace System.IO`r`n" + + "function y {`r`n}`r`n" + + "USING namespace System.IO" + ExpectedResult = "using namespace System.IO`r`n" + + "# using namespace System.IO`r`n" + + "function x {`r`n}`r`n`r`n" + + "# using namespace System.IO`r`n" + + "function y {`r`n}`r`n" + + "# USING namespace System.IO" + ErrorBefore = 2 + ErrorAfter = 0 + }, + @{ + TestCaseName = 'Does not change the content when there are no out-of-place using statements' + PSM1File = "using namespace System.IO`r`n`r`n" + + "using namespace System.Drawing`r`n" + + "function x {`r`n}`r`n" + + "function y {`r`n}`r`n" + ErrorBefore = 0 + ErrorAfter = 0 + }, + @{ + TestCaseName = 'Moves using statements even if types are used' + PSM1File = "function x {`r`n}`r`n" + + "using namespace System.IO`r`n`r`n" + + "function y {`r`n}`r`n" + + "using namespace System.Collections.Generic`r`n" + + "function z { [Dictionary[String,PSObject]]::new() }" + ErrorBefore = 2 + ErrorAfter = 0 + }, + @{ + TestCaseName = 'Moves using statements even when there are (other) parse errors' + PSM1File = "using namespace System.IO`r`n`r`n" + + "function x {`r`n}`r`n" + + "using namespace System.Drawing`r`n" + + "function y {`r`n}`r`n}" + ErrorBefore = 2 + ErrorAfter = 1 + } + ) + } + + It '' -TestCases $TestCases { + param($TestCaseName, $PSM1File, $ErrorBefore, $ErrorAfter, $ExpectedResult) + + $testModuleFile = "$TestDrive\MyModule.psm1" + Set-Content $testModuleFile -Value $PSM1File -Encoding UTF8 -NoNewline + + # Verify parse errors exist before applying the generator + $ErrorFound = $null + $null = [System.Management.Automation.Language.Parser]::ParseFile( + $testModuleFile, + [ref]$null, + [ref]$ErrorFound + ) + $ErrorFound.Count | Should -Be $ErrorBefore + + # Apply the generator and get the resulting text + $result = Invoke-ScriptGenerator -Path $testModuleFile -Generator Move-UsingStatement -Parameters @{} + + # Verify parse errors after applying the generator + $ErrorFound = $null + $null = [System.Management.Automation.Language.Parser]::ParseInput( + $result, + 'testfile', + [ref]$null, + [ref]$ErrorFound + ) + $ErrorFound.Count | Should -Be $ErrorAfter + + if ($ExpectedResult) { + $result.Trim() | Should -Be $ExpectedResult -Because "there should be no duplicate using statements in:`n$result" + } + } + } + + Context "When Move-UsingStatement should do nothing" { + It 'Should not change the output when there are no using statement errors' { + $PSM1File = "using namespace System.IO; function x {}" + $testModuleFile = "$TestDrive\MyModule.psm1" + Set-Content $testModuleFile -Value $PSM1File -Encoding UTF8 -NoNewline + + $result = Invoke-ScriptGenerator -Path $testModuleFile -Generator Move-UsingStatement -Parameters @{} + + $result.Trim() | Should -Be $PSM1File.Trim() + } + } +} diff --git a/Tests/Public/Move-UsingStatement.Tests.ps1.old b/Tests/Public/Move-UsingStatement.Tests.ps1.old deleted file mode 100644 index 6216599..0000000 --- a/Tests/Public/Move-UsingStatement.Tests.ps1.old +++ /dev/null @@ -1,148 +0,0 @@ -#requires -Module ModuleBuilder -Describe "Move-UsingStatement" { - BeforeAll { - $CommandInfo = InModuleScope ModuleBuilder { Get-Command Move-UsingStatement } - } - - Context "Necessary Parameters" { - - It 'has a mandatory InputObject parameter' { - $AST = $CommandInfo.Parameters['InputObject'] - $AST | Should -Not -BeNullOrEmpty - $AST.ParameterType | Should -Be ([System.Management.Automation.Language.Ast]) - $AST.Attributes.Where{ $_ -is [Parameter] }.Mandatory | Should -Be $true - } - } - - Context "Moving Using Statements to the beginning of the file" { - BeforeAll { - $MoveUsingStatementsCmd = InModuleScope ModuleBuilder { - $null = Mock Write-Warning { } - { param($RootModule) - ConvertToAst $RootModule | MoveUsingStatements - } - } - - $TestCases = @( - @{ - TestCaseName = 'Moves all using statements in `n terminated files to the top' - PSM1File = "function x {`n}`n" + - "using namespace System.IO`n`n" + #UsingMustBeAtStartOfScript - "function y {`n}`n" + - "using namespace System.Drawing" #UsingMustBeAtStartOfScript - ErrorBefore = 2 - ErrorAfter = 0 - }, - @{ - TestCaseName = 'Moves all using statements in`r`n terminated files to the top' - PSM1File = "function x {`r`n}`r`n" + - "USING namespace System.IO`r`n`r`n" + #UsingMustBeAtStartOfScript - "function y {`r`n}`r`n" + - "USING namespace System.Drawing" #UsingMustBeAtStartOfScript - ErrorBefore = 2 - ErrorAfter = 0 - }, - @{ - TestCaseName = 'Prevents duplicate using statements' - PSM1File = "using namespace System.IO`r`n" + #UsingMustBeAtStartOfScript - "function x {`r`n}`r`n`r`n" + - "using namespace System.IO`r`n" + #UsingMustBeAtStartOfScript - "function y {`r`n}`r`n" + - "USING namespace System.IO" #UsingMustBeAtStartOfScript - ExpectedResult = "using namespace System.IO`r`n" + - "#using namespace System.IO`r`n" + - "function x {`r`n}`r`n`r`n" + - "#using namespace System.IO`r`n" + - "function y {`r`n}`r`n" + - "#USING namespace System.IO" - ErrorBefore = 2 - ErrorAfter = 0 - }, - @{ - TestCaseName = 'Does not change the content again if there are no out-of-place using statements' - PSM1File = "using namespace System.IO`r`n`r`n" + - "using namespace System.Drawing`r`n" + - "function x {`r`n}`r`n" + - "function y {`r`n}`r`n" - ErrorBefore = 0 - ErrorAfter = 0 - }, - @{ - TestCaseName = 'Moves using statements even if types are used' - PSM1File = "function x {`r`n}`r`n" + - "using namespace System.IO`r`n`r`n" + #UsingMustBeAtStartOfScript - "function y {`r`n}`r`n" + - "using namespace System.Collections.Generic" + #UsingMustBeAtStartOfScript - "function z { [Dictionary[String,PSObject]]::new() }" #TypeNotFound - ErrorBefore = 3 - ErrorAfter = 0 - }, - @{ - TestCaseName = 'Moves using statements even when there are (other) parse errors' - PSM1File = "using namespace System.IO`r`n`r`n" + - "function x {`r`n}`r`n" + - "using namespace System.Drawing`r`n" + # UsingMustBeAtStartOfScript - "function y {`r`n}`r`n}" # Extra } at the end - ErrorBefore = 2 - ErrorAfter = 1 - } - ) - } - - It '' -TestCases $TestCases { - param($TestCaseName, $PSM1File, $ErrorBefore, $ErrorAfter, $ExpectedResult) - - $testModuleFile = "$TestDrive/MyModule.psm1" - Set-Content $testModuleFile -value $PSM1File -Encoding UTF8 - # Before - $ErrorFound = $null - $null = [System.Management.Automation.Language.Parser]::ParseFile( - $testModuleFile, - [ref]$null, - [ref]$ErrorFound - ) - $ErrorFound.Count | Should -Be $ErrorBefore - - # After - &$MoveUsingStatementCmd -RootModule $testModuleFile - - $null = [System.Management.Automation.Language.Parser]::ParseFile( - $testModuleFile, - [ref]$null, - [ref]$ErrorFound - ) - $ErrorFound.Count | Should -Be $ErrorAfter - if ($ExpectedResult) { - $ActualResult = Get-Content $testModuleFile -Raw - $ActualResult.Trim() | Should -Be $ExpectedResult -Because "there should be no duplicate using statements in:`n$ActualResult" - } - } - } - - Context "When MoveUsingStatements should do nothing" { - BeforeAll { - $MoveUsingStatementsCmd = InModuleScope ModuleBuilder { - $null = Mock Write-Warning {} - $null = Mock Set-Content {} - $null = Mock Write-Debug {} -ParameterFilter { $Message -eq "No using statement errors found." } - - { param($RootModule) - ConvertToAst $RootModule | MoveUsingStatements - } - } - } - - It 'Should not do anything when there are no using statement errors' { - $testModuleFile = "$TestDrive\MyModule.psm1" - $PSM1File = "using namespace System.IO; function x {}" - Set-Content $testModuleFile -value $PSM1File -Encoding UTF8 - - &$MoveUsingStatementCmd -RootModule $testModuleFile -Debug - - (Get-Content -Raw $testModuleFile).Trim() | Should -Be $PSM1File - - Assert-MockCalled -CommandName Set-Content -Times 0 -ModuleName ModuleBuilder - Assert-MockCalled -CommandName Write-Debug -Times 1 -ModuleName ModuleBuilder - } - } -} diff --git a/Tests/Public/Update-AliasesToExport.Tests.ps1 b/Tests/Public/Update-AliasesToExport.Tests.ps1 new file mode 100644 index 0000000..d6fe60c --- /dev/null +++ b/Tests/Public/Update-AliasesToExport.Tests.ps1 @@ -0,0 +1,308 @@ +#requires -Module ModuleBuilder +Describe "Update-AliasesToExport" { + BeforeAll { + $ManifestPath = "$TestDrive\TestModule.psd1" + } + + Context "Parsing [Alias()] attributes on functions" { + BeforeEach { + New-ModuleManifest -Path $ManifestPath -AliasesToExport @() + } + + It "Returns a collection of aliases from the [Alias()] attribute" { + Invoke-ScriptGenerator -Code { + function Test-Alias { + [Alias("Foo", "Bar", "Alias")] + param() + } + } -Generator Update-AliasesToExport -Parameters @{ ModuleManifest = $ManifestPath } + + $aliases = Get-Metadata -Path $ManifestPath -PropertyName AliasesToExport + $aliases | Sort-Object | Should -Be (@("Foo", "Bar", "Alias") | Sort-Object) + } + + It "Parses only top-level functions (skips nested function aliases)" { + Invoke-ScriptGenerator -Code { + function Test-Alias { + [Alias("TA", "TAlias")] + param() + } + + function TestAlias { + [Alias("T")] + param() + + # This nested function's alias should NOT be exported + function Test-Negative { + [Alias("TN")] + param() + } + } + } -Generator Update-AliasesToExport -Parameters @{ ModuleManifest = $ManifestPath } + + $aliases = Get-Metadata -Path $ManifestPath -PropertyName AliasesToExport + $aliases | Should -HaveCount 3 + $aliases | Should -BeIn @("TA", "TAlias", "T") + "TN" | Should -Not -BeIn $aliases + } + } + + Context "Parsing New-Alias" { + BeforeEach { + New-ModuleManifest -Path $ManifestPath -AliasesToExport @() + } + + It "Parses alias names regardless of parameter order" { + Invoke-ScriptGenerator -Code { + New-Alias -N 'Alias1' -Va 'Write-Verbose' + New-Alias -Value 'Write-Verbose' -Name 'Alias2' + New-Alias Alias3 Write-Verbose + New-Alias -Value 'Write-Verbose' 'Alias4' + New-Alias 'Alias5' -Value 'Write-Verbose' + } -Generator Update-AliasesToExport -Parameters @{ ModuleManifest = $ManifestPath } + + $aliases = Get-Metadata -Path $ManifestPath -PropertyName AliasesToExport + $aliases | Sort-Object | Should -Be (@('Alias1', 'Alias2', 'Alias3', 'Alias4', 'Alias5') | Sort-Object) + } + + It "Ignores aliases defined in nested function scope" { + Invoke-ScriptGenerator -Code { + New-Alias -Name 'Alias1' -Value 'Write-Verbose' + New-Alias -Value 'Write-Verbose' -Name 'Alias2' + New-Alias 'Alias3' 'Write-Verbose' + function Get-Something { + param() + New-Alias -Name Alias4 -Value 'Write-Verbose' + New-Alias -Name Alias5 -Va 'Write-Verbose' + } + } -Generator Update-AliasesToExport -Parameters @{ ModuleManifest = $ManifestPath } + + $aliases = Get-Metadata -Path $ManifestPath -PropertyName AliasesToExport + $aliases | Sort-Object | Should -Be (@('Alias1', 'Alias2', 'Alias3') | Sort-Object) + } + + It "Ignores aliases that have global scope" { + Invoke-ScriptGenerator -Code { + New-Alias -Name Alias1 -Scope Global -Value Write-Verbose + New-Alias -Scope Global -Value 'Write-Verbose' -Name 'Alias2' + New-Alias -Sc Global 'Alias3' 'Write-Verbose' + New-Alias -Va 'Write-Verbose' 'Alias4' -S Global + New-Alias Alias5 -Value 'Write-Verbose' -Scope Global + } -Generator Update-AliasesToExport -Parameters @{ ModuleManifest = $ManifestPath } + + $aliases = Get-Metadata -Path $ManifestPath -PropertyName AliasesToExport + $aliases | Should -BeNullOrEmpty + } + } + + Context "Parsing Set-Alias" { + BeforeEach { + New-ModuleManifest -Path $ManifestPath -AliasesToExport @() + } + + It "Parses alias names regardless of parameter order" { + Invoke-ScriptGenerator -Code { + Set-Alias -Name Alias1 -Value Write-Verbose + Set-Alias -Va 'Write-Verbose' -N 'Alias2' + Set-Alias Alias3 Write-Verbose + Set-Alias -Va 'Write-Verbose' 'Alias4' + Set-Alias 'Alias5' -Value 'Write-Verbose' + } -Generator Update-AliasesToExport -Parameters @{ ModuleManifest = $ManifestPath } + + $aliases = Get-Metadata -Path $ManifestPath -PropertyName AliasesToExport + $aliases | Sort-Object | Should -Be (@('Alias1', 'Alias2', 'Alias3', 'Alias4', 'Alias5') | Sort-Object) + } + + It "Ignores aliases defined in nested function scope" { + Invoke-ScriptGenerator -Code { + Set-Alias -Name 'Alias1' -Value 'Write-Verbose' + Set-Alias -Value 'Write-Verbose' -Name 'Alias2' + Set-Alias 'Alias3' 'Write-Verbose' + function Get-Something { + param() + Set-Alias -Name 'Alias4' -Value 'Write-Verbose' + Set-Alias -Name 'Alias5' -Value 'Write-Verbose' + } + } -Generator Update-AliasesToExport -Parameters @{ ModuleManifest = $ManifestPath } + + $aliases = Get-Metadata -Path $ManifestPath -PropertyName AliasesToExport + $aliases | Sort-Object | Should -Be (@('Alias1', 'Alias2', 'Alias3') | Sort-Object) + } + + It "Ignores aliases that have global scope" { + Invoke-ScriptGenerator -Code { + Set-Alias -N 'Alias1' -Scope Global -Value 'Write-Verbose' + Set-Alias -Scope Global -Value 'Write-Verbose' -Name 'Alias2' + Set-Alias -Sc Global Alias3 Write-Verbose + Set-Alias -Va 'Write-Verbose' 'Alias4' -Sc Global + Set-Alias 'Alias5' -Value 'Write-Verbose' -Scope Global + } -Generator Update-AliasesToExport -Parameters @{ ModuleManifest = $ManifestPath } + + $aliases = Get-Metadata -Path $ManifestPath -PropertyName AliasesToExport + $aliases | Should -BeNullOrEmpty + } + + # It "Detects variable Name as dynamic alias generation and sets AliasesToExport = '*'" { + # Invoke-ScriptGenerator -Code { + # $taskAlias = "my-task" + # Set-Alias -Name $taskAlias -Value 'Write-Verbose' + # } -Generator Update-AliasesToExport -Parameters @{ ModuleManifest = $ManifestPath } -WarningVariable warnings 3>$null + + # $aliases = Get-Metadata -Path $ManifestPath -PropertyName AliasesToExport + # $aliases | Should -Be '*' + # $warnings | Should -Not -BeNullOrEmpty + # } + + # It "Detects dynamic alias generation inside ForEach-Object and sets AliasesToExport = '*'" { + # Invoke-ScriptGenerator -Code { + # @('a', 'b') | ForEach-Object { + # $taskAlias = "task-$_" + # Set-Alias -Name $taskAlias -Value 'Write-Verbose' + # } + # } -Generator Update-AliasesToExport -Parameters @{ ModuleManifest = $ManifestPath } -WarningVariable warnings 3>$null + + # $aliases = Get-Metadata -Path $ManifestPath -PropertyName AliasesToExport + # $aliases | Should -Be '*' + # $warnings | Should -Not -BeNullOrEmpty + # } + + It "Does NOT flag Set-Alias with variable Name inside a function definition as dynamic" { + Invoke-ScriptGenerator -Code { + Set-Alias 'TopAlias' 'Write-Verbose' + function Set-DynamicAlias { + param($name) + Set-Alias -Name $name -Value 'Write-Verbose' + } + } -Generator Update-AliasesToExport -Parameters @{ ModuleManifest = $ManifestPath } -WarningVariable warnings 3>$null + + $aliases = Get-Metadata -Path $ManifestPath -PropertyName AliasesToExport + $aliases | Should -Be 'TopAlias' + $warnings | Should -BeNullOrEmpty + } + } + + Context "Remove-Alias cancels Alias exports" { + BeforeEach { + New-ModuleManifest -Path $ManifestPath -AliasesToExport @() + } + + It "Parses Remove-Alias regardless of parameter order" { + Invoke-ScriptGenerator -Code { + New-Alias -Name Alias1 -Value Write-Verbose + Set-Alias -Value 'Write-Verbose' -Name 'Alias2' + New-Alias Alias3 Write-Verbose + Set-Alias -Value 'Write-Verbose' 'Alias4' + Set-Alias 'Alias5' -Value 'Write-Verbose' + Remove-Alias Alias1 + Remove-Alias -Name Alias2 + Remove-Alias -N Alias5 + } -Generator Update-AliasesToExport -Parameters @{ ModuleManifest = $ManifestPath } + + $aliases = Get-Metadata -Path $ManifestPath -PropertyName AliasesToExport + $aliases | Sort-Object | Should -Be (@('Alias3', 'Alias4') | Sort-Object) + } + + It "Ignores Remove-Alias in nested function scopes" { + Invoke-ScriptGenerator -Code { + Set-Alias -Name 'Alias1' -Value 'Write-Verbose' + New-Alias -Value 'Write-Verbose' -Name 'Alias2' + Set-Alias 'Alias3' 'Write-Verbose' + function Get-Something { + param() + Set-Alias -Name 'Alias4' -Value 'Write-Verbose' + Set-Alias -Name 'Alias5' -Value 'Write-Verbose' + Remove-Alias -Name Alias1 + } + } -Generator Update-AliasesToExport -Parameters @{ ModuleManifest = $ManifestPath } + + $aliases = Get-Metadata -Path $ManifestPath -PropertyName AliasesToExport + $aliases | Sort-Object | Should -Be (@('Alias1', 'Alias2', 'Alias3') | Sort-Object) + } + + It "Does not fail when removing an alias that was already global scope (never added)" { + Invoke-ScriptGenerator -Code { + Set-Alias -Name Alias1 -Scope Global -Value Write-Verbose + Remove-Alias -Name Alias1 + } -Generator Update-AliasesToExport -Parameters @{ ModuleManifest = $ManifestPath } + + $aliases = Get-Metadata -Path $ManifestPath -PropertyName AliasesToExport + $aliases | Should -BeNullOrEmpty + } + } + + Context "When AliasesToExport is missing in the manifest" { + It "Writes a warning and does not throw" { + # Minimal manifest without AliasesToExport + New-ModuleManifest -Path $ManifestPath + (Get-Content $ManifestPath).ForEach{ + if ($_ -match 'AliasesToExport') { + '# ' + $_ + } else { + $_ + } + } | Set-Content $ManifestPath + + try { + Invoke-ScriptGenerator -Code { + function Test-Alias { + [Alias("TA")] param() + } + } -Generator Update-AliasesToExport -Parameters @{ ModuleManifest = $ManifestPath } -WarningAction Stop + } catch { + $FAILURE = $_ + } + $FAILURE | Should -Match 'the preference variable "WarningPreference" or common parameter is set to Stop' + $FAILURE | Should -Match "Can't update AliasesToExport" + } + } + + Context "WhenNoAliases parameter" { + BeforeEach { + New-ModuleManifest -Path $ManifestPath -AliasesToExport @('ExistingAlias') + } + + It "Does not update the manifest by default (DoNotSet) when no aliases are found" { + Invoke-ScriptGenerator -Code { + function Get-Something { + param() + } + } -Generator Update-AliasesToExport -Parameters @{ ModuleManifest = $ManifestPath } + + $aliases = Get-Metadata -Path $ManifestPath -PropertyName AliasesToExport + $aliases | Should -Be 'ExistingAlias' + } + + It "Sets AliasesToExport = '*' when WhenNoAliases = 'Wildcard' and no aliases found" { + Invoke-ScriptGenerator -Code { + function Get-Something { + param() + } + } -Generator Update-AliasesToExport -Parameters @{ ModuleManifest = $ManifestPath; WhenNoAliases = 'Wildcard' } + + $aliases = Get-Metadata -Path $ManifestPath -PropertyName AliasesToExport + $aliases | Should -Be '*' + } + + It "Sets AliasesToExport = @() when WhenNoAliases = 'EmptyArray' and no aliases found" { + Invoke-ScriptGenerator -Code { + function Get-Something { + param() + } + } -Generator Update-AliasesToExport -Parameters @{ ModuleManifest = $ManifestPath; WhenNoAliases = 'EmptyArray' } + + $aliases = Get-Metadata -Path $ManifestPath -PropertyName AliasesToExport + $aliases | Should -BeNullOrEmpty + } + + It "Always updates with found static aliases regardless of WhenNoAliases" { + Invoke-ScriptGenerator -Code { + function Test-Alias { + [Alias("TA")] param() + } + } -Generator Update-AliasesToExport -Parameters @{ ModuleManifest = $ManifestPath; WhenNoAliases = 'DoNotSet' } + + $aliases = Get-Metadata -Path $ManifestPath -PropertyName AliasesToExport + $aliases | Should -Be 'TA' + } + } +} diff --git a/Tests/Public/Update-AliasesToExport.Tests.ps1.old b/Tests/Public/Update-AliasesToExport.Tests.ps1.old deleted file mode 100644 index e9d2156..0000000 --- a/Tests/Public/Update-AliasesToExport.Tests.ps1.old +++ /dev/null @@ -1,180 +0,0 @@ -#requires -Module ModuleBuilder -Describe "GetCommandAlias" { - BeforeAll { - $CommandInfo = InModuleScope ModuleBuilder { Get-Command GetCommandAlias } - } - - Context "Mandatory Parameter" { - It 'has a mandatory AST parameter' { - $AST = $CommandInfo.Parameters['AST'] - $AST | Should -Not -BeNullOrEmpty - $AST.ParameterType | Should -Be ([System.Management.Automation.Language.Ast]) - $AST.Attributes.Where{ $_ -is [Parameter] }.Mandatory | Should -Be $true - } - - } - - Context "Parsing Alias Parameters" { - # It used to return a hashtable, but we no longer care what the alias points to - It "Returns a collection of aliases" { - $Result = &$CommandInfo -Ast { - function Test-Alias { - [Alias("Foo","Bar","Alias")] - param() - } - }.Ast - - $Result | Should -Be @("Foo", "Bar", "Alias") - } - - It "Parses only top-level functions, and returns them in order" { - $Result = &$CommandInfo -Ast { - function Test-Alias { - [Alias("TA", "TAlias")] - param() - } - - function TestAlias { - [Alias("T")] - param() - - # This should not return - function Test-Negative { - [Alias("TN")] - param() - } - } - }.Ast - - $Result | Should -Be "TA","TAlias", "T" - } - } - - Context "Parsing New-Alias" { - It "Parses alias names regardless of parameter order" { - $Result = &$CommandInfo -Ast { - New-Alias -N 'Alias1' -Va 'Write-Verbose' - New-Alias -Value 'Write-Verbose' -Name 'Alias2' - New-Alias Alias3 Write-Verbose - New-Alias -Value 'Write-Verbose' 'Alias4' - New-Alias 'Alias5' -Value 'Write-Verbose' - }.Ast - - $Result | Should -Be 'Alias1', 'Alias2', 'Alias3', 'Alias4', 'Alias5' - } - - It "Ignores aliases defined in nested function scope" { - $Result = &$CommandInfo -Ast { - New-Alias -Name 'Alias1' -Value 'Write-Verbose' - New-Alias -Value 'Write-Verbose' -Name 'Alias2' - New-Alias 'Alias3' 'Write-Verbose' - function Get-Something { - param() - - New-Alias -Name Alias4 -Value 'Write-Verbose' - New-Alias -Name Alias5 -Va 'Write-Verbose' - } - }.Ast - - $Result | Should -Be 'Alias1', 'Alias2', 'Alias3' - } - - It "Ignores aliases that already have global scope" { - $Result = &$CommandInfo -Ast { - New-Alias -Name Alias1 -Scope Global -Value Write-Verbose - New-Alias -Scope Global -Value 'Write-Verbose' -Name 'Alias2' - New-Alias -Sc Global 'Alias3' 'Write-Verbose' - New-Alias -Va 'Write-Verbose' 'Alias4' -S Global - New-Alias Alias5 -Value 'Write-Verbose' -Scope Global - }.Ast - - $Result | Should -BeNullOrEmpty - } - } - - Context "Parsing Set-Alias" { - It "Parses alias names regardless of parameter order" { - $Result = &$CommandInfo -Ast { - Set-Alias -Name Alias1 -Value Write-Verbose - Set-Alias -Va 'Write-Verbose' -N 'Alias2' - Set-Alias Alias3 Write-Verbose - Set-Alias -Va 'Write-Verbose' 'Alias4' - Set-Alias 'Alias5' -Value 'Write-Verbose' - }.Ast - - $Result | Should -Be 'Alias1', 'Alias2', 'Alias3', 'Alias4', 'Alias5' - } - - It "Ignores aliases defined in nested function scope" { - $Result = &$CommandInfo -Ast { - Set-Alias -Name 'Alias1' -Value 'Write-Verbose' - Set-Alias -Value 'Write-Verbose' -Name 'Alias2' - Set-Alias 'Alias3' 'Write-Verbose' - function Get-Something { - param() - - Set-Alias -Name 'Alias4' -Value 'Write-Verbose' - Set-Alias -Name 'Alias5' -Value 'Write-Verbose' - } - }.Ast - - $Result | Should -Be 'Alias1', 'Alias2', 'Alias3' - } - - It "Ignores aliases that already have global scope" { - $Result = &$CommandInfo -Ast { - Set-Alias -N 'Alias1' -Scope Global -Value 'Write-Verbose' - Set-Alias -Scope Global -Value 'Write-Verbose' -Name 'Alias2' - Set-Alias -Sc Global Alias3 Write-Verbose - Set-Alias -Va 'Write-Verbose' 'Alias4' -Sc Global - Set-Alias 'Alias5' -Value 'Write-Verbose' -Scope Global - }.Ast - - $Result | Should -BeNullOrEmpty - } - } - - - Context "Remove-Alias cancels Alias exports" { - It "Parses parameters regardless of name" { - $Result = &$CommandInfo -Ast { - New-Alias -Name Alias1 -Value Write-Verbose - Set-Alias -Value 'Write-Verbose' -Name 'Alias2' - New-Alias Alias3 Write-Verbose - Set-Alias -Value 'Write-Verbose' 'Alias4' - Set-Alias 'Alias5' -Value 'Write-Verbose' - Remove-Alias Alias1 - Remove-Alias -Name Alias2 - Remove-Alias -N Alias5 - }.Ast - - $Result | Should -Be 'Alias3', 'Alias4' - } - - It "Ignores removals in function scopes" { - $Result = &$CommandInfo -Ast { - Set-Alias -Name 'Alias1' -Value 'Write-Verbose' - New-Alias -Value 'Write-Verbose' -Name 'Alias2' - Set-Alias 'Alias3' 'Write-Verbose' - function Get-Something { - param() - - Set-Alias -Name 'Alias4' -Value 'Write-Verbose' - Set-Alias -Name 'Alias5' -Value 'Write-Verbose' - Remove-Alias -Name Alias1 - } - }.Ast - - $Result | Should -Be 'Alias1', 'Alias2', 'Alias3' - } - - It "Does not fail when removing aliases that were ignored because of global scope" { - $Result = &$CommandInfo -Ast { - Set-Alias -Name Alias1 -Scope Global -Value Write-Verbose - Remove-Alias -Name Alias1 - }.Ast - - $Result | Should -BeNullOrEmpty - } - } -} From 67775a3c41b5f05838743bf9461d84d8032a59ca Mon Sep 17 00:00:00 2001 From: Joel Bennett Date: Tue, 19 May 2026 00:06:06 -0400 Subject: [PATCH 04/10] Fix more WIndows PS problems --- Source/Private/CompressToBase64.ps1 | 21 ++++++++---- Tests/Public/Move-UsingStatement.Tests.ps1 | 4 +-- Tests/Public/Update-AliasesToExport.Tests.ps1 | 33 +++++++++---------- 3 files changed, 32 insertions(+), 26 deletions(-) diff --git a/Source/Private/CompressToBase64.ps1 b/Source/Private/CompressToBase64.ps1 index ff02d83..a68eb20 100644 --- a/Source/Private/CompressToBase64.ps1 +++ b/Source/Private/CompressToBase64.ps1 @@ -36,12 +36,21 @@ function CompressToBase64 { process { foreach ($File in $Path | Convert-Path) { $Source = [System.IO.MemoryStream][System.IO.File]::ReadAllBytes($File) - $OutputStream = [System.IO.Compression.DeflateStream]::new( - [System.IO.MemoryStream]::new(), - [System.IO.Compression.CompressionMode]::Compress) - $Source.CopyTo($OutputStream) - $OutputStream.Flush() - $ByteArray = $OutputStream.BaseStream.ToArray() + # Write-Debug "Read $($Source.Length) bytes from $File" + + $MemoryStream = [System.IO.MemoryStream]::new() + $DeflateStream = [System.IO.Compression.DeflateStream]::new( + $MemoryStream, + [System.IO.Compression.CompressionMode]::Compress, + $true) + $Source.CopyTo($DeflateStream) + # Framework 4.x (Windows PS) doesn't flush until we close the DeflateStream + $DeflateStream.Dispose() + $ByteArray = $MemoryStream.ToArray() + $MemoryStream.Dispose() + $Source.Dispose() + # Write-Debug "Compressed to $($ByteArray.Length) bytes" + if (!$ExpandScript) { [Convert]::ToBase64String($ByteArray) } else { diff --git a/Tests/Public/Move-UsingStatement.Tests.ps1 b/Tests/Public/Move-UsingStatement.Tests.ps1 index c284252..26df63c 100644 --- a/Tests/Public/Move-UsingStatement.Tests.ps1 +++ b/Tests/Public/Move-UsingStatement.Tests.ps1 @@ -71,7 +71,7 @@ Describe "Move-UsingStatement" { It '' -TestCases $TestCases { param($TestCaseName, $PSM1File, $ErrorBefore, $ErrorAfter, $ExpectedResult) - $testModuleFile = "$TestDrive\MyModule.psm1" + $testModuleFile = Join-Path $TestDrive "MyModule.psm1" Set-Content $testModuleFile -Value $PSM1File -Encoding UTF8 -NoNewline # Verify parse errors exist before applying the generator @@ -97,7 +97,7 @@ Describe "Move-UsingStatement" { $ErrorFound.Count | Should -Be $ErrorAfter if ($ExpectedResult) { - $result.Trim() | Should -Be $ExpectedResult -Because "there should be no duplicate using statements in:`n$result" + $result.Trim() -split "[\r\n]+" -match "^\s*using" | Should -Be ($ExpectedResult.Trim() -split "[\r\n]+" -match "^\s*using") } } } diff --git a/Tests/Public/Update-AliasesToExport.Tests.ps1 b/Tests/Public/Update-AliasesToExport.Tests.ps1 index d6fe60c..bb63241 100644 --- a/Tests/Public/Update-AliasesToExport.Tests.ps1 +++ b/Tests/Public/Update-AliasesToExport.Tests.ps1 @@ -1,7 +1,7 @@ #requires -Module ModuleBuilder Describe "Update-AliasesToExport" { BeforeAll { - $ManifestPath = "$TestDrive\TestModule.psd1" + $ManifestPath = Join-Path $TestDrive "TestModule.psd1" } Context "Parsing [Alias()] attributes on functions" { @@ -234,25 +234,22 @@ Describe "Update-AliasesToExport" { It "Writes a warning and does not throw" { # Minimal manifest without AliasesToExport New-ModuleManifest -Path $ManifestPath - (Get-Content $ManifestPath).ForEach{ - if ($_ -match 'AliasesToExport') { - '# ' + $_ - } else { - $_ + (Get-Content $ManifestPath) -replace "^(.*AliasesToExport.*)$", '# $1' | Set-Content $ManifestPath + + Mock Write-Warning -ModuleName ModuleBuilder + Mock Update-Metadata -ModuleName ModuleBuilder + + Invoke-ScriptGenerator -Code { + function Test-Alias { + [Alias("TA")] param() } - } | Set-Content $ManifestPath + } -Generator Update-AliasesToExport -Parameters @{ ModuleManifest = $ManifestPath } - try { - Invoke-ScriptGenerator -Code { - function Test-Alias { - [Alias("TA")] param() - } - } -Generator Update-AliasesToExport -Parameters @{ ModuleManifest = $ManifestPath } -WarningAction Stop - } catch { - $FAILURE = $_ - } - $FAILURE | Should -Match 'the preference variable "WarningPreference" or common parameter is set to Stop' - $FAILURE | Should -Match "Can't update AliasesToExport" + Assert-MockCalled Write-Warning -ModuleName ModuleBuilder -Exactly 1 -Scope It + # It does not even try to update the metadata + Assert-MockCalled Update-Metadata -ModuleName ModuleBuilder -Exactly 0 -Scope It + # It does not, in fact, update the AliasesToExport + Get-Metadata -Path $ManifestPath -PropertyName AliasesToExport -ErrorAction Ignore | Should -BeNullOrEmpty } } From f570a54b2543e893dc3b9d6235bba063f8c670f8 Mon Sep 17 00:00:00 2001 From: Joel Bennett Date: Tue, 19 May 2026 00:53:56 -0400 Subject: [PATCH 05/10] Update build and fix ModuleFast --- .github/workflows/build.yml | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index d678fee..9afe42c 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -79,11 +79,6 @@ jobs: uses: actions/download-artifact@v8 with: name: build.requires.psd1 - - name: Download Build Output - uses: actions/download-artifact@v8 - with: - name: ModuleBuilder - path: Modules/ModuleBuilder # /home/runner/work/ModuleBuilder/ModuleBuilder/output/ModuleBuilder - name: Download Pester Tests uses: actions/download-artifact@v8 with: @@ -95,9 +90,15 @@ jobs: run: | # PowerShell @(Get-Content build.requires.psd1).Where({ $_ -notmatch "ModuleBuilder"}) | Set-Content build.requires.psd1 - name: ⚡ Install Required Modules - uses: JustinGrote/ModuleFast-action@v0.0.1 + uses: JustinGrote/ModuleFast-action@v1.0.1 env: MODULEFAST_DESTINATION: ${{ github.workspace }}/Modules + # Copy over the build output AFTER Install-ModuleFast, because it's caching my build output :( + - name: Download Build Output + uses: actions/download-artifact@v8 + with: + name: ModuleBuilder + path: Modules/ModuleBuilder # /home/runner/work/ModuleBuilder/ModuleBuilder/output/ModuleBuilder - name: Invoke-Pester if: matrix.shell == 'powershell' shell: powershell From 52d225c76f7f6b5781fb851f816f40f9b5166bff Mon Sep 17 00:00:00 2001 From: Joel Bennett Date: Tue, 19 May 2026 22:52:02 -0400 Subject: [PATCH 06/10] Refactor test that's failing in the matrix --- .../ConvertTo-SourceLineNumber.Tests.ps1 | 26 +++++++++++-------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/Tests/Public/ConvertTo-SourceLineNumber.Tests.ps1 b/Tests/Public/ConvertTo-SourceLineNumber.Tests.ps1 index 7e1eb44..5c6d4f1 100644 --- a/Tests/Public/ConvertTo-SourceLineNumber.Tests.ps1 +++ b/Tests/Public/ConvertTo-SourceLineNumber.Tests.ps1 @@ -1,4 +1,12 @@ Describe "ConvertTo-SourceLineNumber" { + BeforeDiscovery { + ${global:\} = [io.path]::DirectorySeparatorChar + $TestCases = @( + @{ outputLine = 40; sourceFile = ".${\}Private${\}TestUnExportedAliases.ps1"; sourceLine = 13 } + @{ outputLine = 48; sourceFile = ".${\}Public${\}Get-Source.ps1"; sourceLine = 5 } + @{ outputLine = 56; sourceFile = ".${\}Public${\}Set-Source.ps1"; sourceLine = 3 } + ) + } # use the integration test code BeforeAll { Build-Module $PSScriptRoot/../Integration/Source1/build.psd1 -Passthru @@ -7,13 +15,6 @@ Describe "ConvertTo-SourceLineNumber" { $global:Convert_LineNumber_ModulePath = Convert-Path "./../Integration/Result1/Source1/1.0.0/Source1.psm1" $global:Convert_LineNumber_ModuleSource = Convert-Path "./../Integration/Source1" $global:Convert_LineNumber_ModuleContent = Get-Content $global:Convert_LineNumber_ModulePath - ${global:\} = [io.path]::DirectorySeparatorChar - - $global:TestCases = @( - @{ outputLine = 40; sourceFile = ".${\}Private${\}TestUnExportedAliases.ps1"; sourceLine = 13 } - @{ outputLine = 48; sourceFile = ".${\}Public${\}Get-Source.ps1"; sourceLine = 5 } - @{ outputLine = 56; sourceFile = ".${\}Public${\}Set-Source.ps1"; sourceLine = 3 } - ) } AfterAll { Pop-Location -StackName ConvertTo-SourceLineNumber @@ -28,7 +29,7 @@ Describe "ConvertTo-SourceLineNumber" { $line = (Get-Content (Join-Path $Convert_LineNumber_ModuleSource $SourceLocation.SourceFile))[$SourceLocation.SourceLineNumber - 1] try { - $Convert_LineNumber_ModuleContent[$outputLine -1] | Should -Be $line + $Convert_LineNumber_ModuleContent[$outputLine - 1] | Should -Be $line } catch { throw "Failed to match module line $outputLine to $($SourceLocation.SourceFile) line $($SourceLocation.SourceLineNumber).`nExpected $Line`nBut got $($Convert_LineNumber_ModuleContent[$outputLine -1])" } @@ -40,7 +41,7 @@ Describe "ConvertTo-SourceLineNumber" { } It 'Should work with an error PositionMessage' { - $line = Select-String -Path $Convert_LineNumber_ModulePath 'function Set-Source {' | ForEach-Object LineNumber + $line = (Select-String -Path $Convert_LineNumber_ModulePath 'function Set-Source {').LineNumber $SourceLocation = "At ${Convert_LineNumber_ModulePath}:$line char:17" | ConvertTo-SourceLineNumber $SourceLocation.SourceFile | Should -Be ".${\}Public${\}Set-Source.ps1" @@ -51,8 +52,11 @@ Describe "ConvertTo-SourceLineNumber" { $SourceFile = Join-Path $Convert_LineNumber_ModuleSource (Join-Path Public Set-Source.ps1) | Convert-Path - $outputLine = Select-String -Path $Convert_LineNumber_ModulePath "sto͞o′pĭd" | ForEach-Object LineNumber - $sourceLine = Select-String -Path $SourceFile "sto͞o′pĭd" | ForEach-Object LineNumber + $outputLine = (Select-String -Path $Convert_LineNumber_ModulePath "sto͞o′pĭd").LineNumber + $sourceLine = (Select-String -Path $SourceFile "sto͞o′pĭd").LineNumber + + $sourceLine | Should -BeGreaterThan 0 -Because "the test string 'sto͞o′pĭd' is definitely found in the source file" + $outputLine | Should -BeGreaterThan 0 -Because "the test string 'sto͞o′pĭd' should be found in the module" $SourceLocation = "At Set-Source, ${Convert_LineNumber_ModulePath}: line $outputLine" | ConvertTo-SourceLineNumber $SourceLocation.SourceFile | Should -Be ".${\}Public${\}Set-Source.ps1" From cebe9bd48c9e0f6b443598d5c3218962bc4b41fc Mon Sep 17 00:00:00 2001 From: Joel Bennett Date: Tue, 19 May 2026 23:31:24 -0400 Subject: [PATCH 07/10] Unicode is a pain. --- Tests/Public/ConvertTo-SourceLineNumber.Tests.ps1 | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/Tests/Public/ConvertTo-SourceLineNumber.Tests.ps1 b/Tests/Public/ConvertTo-SourceLineNumber.Tests.ps1 index 5c6d4f1..0db813c 100644 --- a/Tests/Public/ConvertTo-SourceLineNumber.Tests.ps1 +++ b/Tests/Public/ConvertTo-SourceLineNumber.Tests.ps1 @@ -1,4 +1,4 @@ -Describe "ConvertTo-SourceLineNumber" { +Describe "ConvertTo-SourceLineNumber" { BeforeDiscovery { ${global:\} = [io.path]::DirectorySeparatorChar $TestCases = @( @@ -55,8 +55,10 @@ Describe "ConvertTo-SourceLineNumber" { $outputLine = (Select-String -Path $Convert_LineNumber_ModulePath "sto͞o′pĭd").LineNumber $sourceLine = (Select-String -Path $SourceFile "sto͞o′pĭd").LineNumber - $sourceLine | Should -BeGreaterThan 0 -Because "the test string 'sto͞o′pĭd' is definitely found in the source file" - $outputLine | Should -BeGreaterThan 0 -Because "the test string 'sto͞o′pĭd' should be found in the module" + Get-Content $SourceFile + + $sourceLine | Should -BeGreaterThan 0 -Because "the test string 'sto͞o′pĭd' is definitely found in '$SourceFile'" + $outputLine | Should -BeGreaterThan 0 -Because "the test string 'sto͞o′pĭd' should be found in the module '$Convert_LineNumber_ModulePath'" $SourceLocation = "At Set-Source, ${Convert_LineNumber_ModulePath}: line $outputLine" | ConvertTo-SourceLineNumber $SourceLocation.SourceFile | Should -Be ".${\}Public${\}Set-Source.ps1" From 3fd07732e0292ca1ced6e849f06debc752fe3e63 Mon Sep 17 00:00:00 2001 From: Joel Bennett Date: Tue, 19 May 2026 23:47:37 -0400 Subject: [PATCH 08/10] Unicode is a pain. --- Tests/Public/ConvertTo-SourceLineNumber.Tests.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/Public/ConvertTo-SourceLineNumber.Tests.ps1 b/Tests/Public/ConvertTo-SourceLineNumber.Tests.ps1 index 0db813c..c9654cf 100644 --- a/Tests/Public/ConvertTo-SourceLineNumber.Tests.ps1 +++ b/Tests/Public/ConvertTo-SourceLineNumber.Tests.ps1 @@ -55,7 +55,7 @@ $outputLine = (Select-String -Path $Convert_LineNumber_ModulePath "sto͞o′pĭd").LineNumber $sourceLine = (Select-String -Path $SourceFile "sto͞o′pĭd").LineNumber - Get-Content $SourceFile + Get-Content $Convert_LineNumber_ModulePath | Out-Host $sourceLine | Should -BeGreaterThan 0 -Because "the test string 'sto͞o′pĭd' is definitely found in '$SourceFile'" $outputLine | Should -BeGreaterThan 0 -Because "the test string 'sto͞o′pĭd' should be found in the module '$Convert_LineNumber_ModulePath'" From fff0373d9cb19d9cf0f4065224d151a05946a738 Mon Sep 17 00:00:00 2001 From: Joel Bennett Date: Wed, 20 May 2026 00:16:46 -0400 Subject: [PATCH 09/10] Debugging encoding problems with WIndows PowerShell --- Source/Private/SetModuleContent.ps1 | 1 + Tests/Public/ConvertTo-SourceLineNumber.Tests.ps1 | 2 ++ 2 files changed, 3 insertions(+) diff --git a/Source/Private/SetModuleContent.ps1 b/Source/Private/SetModuleContent.ps1 index 376eac6..add7ae4 100644 --- a/Source/Private/SetModuleContent.ps1 +++ b/Source/Private/SetModuleContent.ps1 @@ -34,6 +34,7 @@ function SetModuleContent { ) begin { Write-Debug "SetModuleContent WorkingDirectory $WorkingDirectory" + Write-Debug "Encoding $Encoding" Push-Location $WorkingDirectory -StackName SetModuleContent $ContentStarted = $false # There has been no content yet diff --git a/Tests/Public/ConvertTo-SourceLineNumber.Tests.ps1 b/Tests/Public/ConvertTo-SourceLineNumber.Tests.ps1 index c9654cf..1c6075d 100644 --- a/Tests/Public/ConvertTo-SourceLineNumber.Tests.ps1 +++ b/Tests/Public/ConvertTo-SourceLineNumber.Tests.ps1 @@ -9,7 +9,9 @@ } # use the integration test code BeforeAll { + $DebugPreference = "Continue" Build-Module $PSScriptRoot/../Integration/Source1/build.psd1 -Passthru + $DebugPreference = "SilentlyContinue" Push-Location $PSScriptRoot -StackName ConvertTo-SourceLineNumber $global:Convert_LineNumber_ModulePath = Convert-Path "./../Integration/Result1/Source1/1.0.0/Source1.psm1" From 749514593077bc6d0f14e99f83b9f7800ffc5425 Mon Sep 17 00:00:00 2001 From: Joel Bennett Date: Wed, 20 May 2026 00:44:12 -0400 Subject: [PATCH 10/10] Fixed encoding bug in the ScriptGenerator Also added Encoding parmeter to ConvertTo-Script --- Source/Private/SetModuleContent.ps1 | 6 +++--- Source/Public/Build-Module.ps1 | 2 +- Source/Public/ConvertTo-Script.ps1 | 11 +++++++++-- Source/Public/Invoke-ScriptGenerator.ps1 | 20 +++++++++++++------- 4 files changed, 26 insertions(+), 13 deletions(-) diff --git a/Source/Private/SetModuleContent.ps1 b/Source/Private/SetModuleContent.ps1 index add7ae4..fc92966 100644 --- a/Source/Private/SetModuleContent.ps1 +++ b/Source/Private/SetModuleContent.ps1 @@ -1,7 +1,7 @@ function SetModuleContent { <# .SYNOPSIS - A wrapper for Set-Content that handles arrays of file paths + A wrapper for Set-Content that copies lists of files .DESCRIPTION The implementation here is strongly dependent on Build-Module doing the right thing Build-Module can optionally pass a PREFIX or SUFFIX, but otherwise only passes files @@ -15,7 +15,7 @@ function SetModuleContent { [CmdletBinding()] param( # Where to write the joined output - [Parameter(Position=0, Mandatory)] + [Parameter(Position = 0, Mandatory)] [string]$OutputPath, # Input files, the scripts that will be copied to the output path @@ -28,7 +28,7 @@ function SetModuleContent { # The working directory (allows relative paths for other values) [string]$WorkingDirectory = $pwd, - # The encoding defaults to UTF8 (or UTF8NoBom on Core) + # The encoding defaults to UTF8 (or UTF8Bom on Core) [Parameter(DontShow)] [string]$Encoding = $(if($IsCoreCLR) { "UTF8Bom" } else { "UTF8" }) ) diff --git a/Source/Public/Build-Module.ps1 b/Source/Public/Build-Module.ps1 index 800184e..c671db7 100644 --- a/Source/Public/Build-Module.ps1 +++ b/Source/Public/Build-Module.ps1 @@ -306,7 +306,7 @@ function Build-Module { Join-Path -Path $ModuleInfo.ModuleBase -ChildPath $_ | Convert-Path -ErrorAction SilentlyContinue } - $ModuleInfo.Generators | Invoke-ScriptGenerator -Path $RootModule -Overwrite + $ModuleInfo.Generators | Invoke-ScriptGenerator -Path $RootModule -Overwrite -Encoding $ModuleInfo.Encoding # This is mostly for testing ... diff --git a/Source/Public/ConvertTo-Script.ps1 b/Source/Public/ConvertTo-Script.ps1 index c267e26..ce69239 100644 --- a/Source/Public/ConvertTo-Script.ps1 +++ b/Source/Public/ConvertTo-Script.ps1 @@ -53,7 +53,12 @@ function ConvertTo-Script { # This is used to find the module manifest, # But the the script will be saved in the same location [Parameter(Mandatory, ValueFromPipelineByPropertyName)] - [string]$Path + [string]$Path, + + # File encoding for output RootModule (defaults to UTF8) + # Converted to System.Text.Encoding for PowerShell 6 (and something else for PowerShell 5) + [ValidateSet("UTF8", "UTF8Bom", "UTF8NoBom", "UTF7", "ASCII", "Unicode", "UTF32")] + [string]$Encoding = $(if ($IsCoreCLR) { "UTF8Bom" } else { "UTF8" }) ) begin { Write-Debug " ENTER: ConvertTo-Script BEGIN $Path $FunctionName" @@ -81,6 +86,8 @@ function ConvertTo-Script { return [AstVisitAction]::Continue } } + + $SetContentCmd = $ExecutionContext.InvokeCommand.GetCommand('Microsoft.PowerShell.Management\Set-Content', [System.Management.Automation.CommandTypes]::Cmdlet) Write-Debug " EXIT: ConvertTo-Script BEGIN" } process { @@ -131,7 +138,7 @@ function ConvertTo-Script { Get-Item $Path ) | CompressToBase64 -ExpandScriptName ImportBase64Module "$FunctionName @PSBoundParameters" - ) | Set-Content "$FunctionName.ps1" + ) | & $SetContentCmd -Path "$FunctionName.ps1" -Encoding $Encoding Update-ScriptFileInfo "$FunctionName.ps1" -Version $Manifest.ModuleVersion -Author $Manifest.Author -CompanyName $Manifest.CompanyName -Copyright $Manifest.Copyright -Tags $Manifest.PrivateData.PSData.Tags -ProjectUri $Manifest.PrivateData.PSData.ProjectUri -LicenseUri $Manifest.PrivateData.PSData.LicenseUri -IconUri $Manifest.PrivateData.PSData.IconUri -ReleaseNotes $Manifest.PrivateData.PSData.ReleaseNotes diff --git a/Source/Public/Invoke-ScriptGenerator.ps1 b/Source/Public/Invoke-ScriptGenerator.ps1 index 165f1bc..dcea7b1 100644 --- a/Source/Public/Invoke-ScriptGenerator.ps1 +++ b/Source/Public/Invoke-ScriptGenerator.ps1 @@ -68,15 +68,21 @@ function Invoke-ScriptGenerator { # If set, will overwrite the Source with the generated content. # Use with care, as this will modify the source file! - [switch]$Overwrite + [switch]$Overwrite, + + # The encoding defaults to UTF8 (or UTF8Bom on Core) + [Parameter()] + [string]$Encoding = $(if($IsCoreCLR) { "UTF8Bom" } else { "UTF8" }) ) begin { $AstParam = @{} + $PSBoundParameters $null = $AstParam.Remove("Overwrite") $null = $AstParam.Remove("Generator") $null = $AstParam.Remove("Parameters") + $null = $AstParam.Remove("Encoding") $ParseResults = ConvertToAst @AstParam [StringBuilder]$Builder = $ParseResults.Ast.Extent.Text + $SetContentCmd = $ExecutionContext.InvokeCommand.GetCommand('Microsoft.PowerShell.Management\Set-Content', [System.Management.Automation.CommandTypes]::Cmdlet) } process { if (-not $PSBoundParameters.ContainsKey("Generator") -and $Parameters.ContainsKey("Generator")) { @@ -115,10 +121,10 @@ function Invoke-ScriptGenerator { # Find that generator... $GeneratorCmd = Get-Command -Name ${Generator} -ParameterType Ast -ErrorAction Ignore <# -CommandType Function #> | - Where-Object { - $_.OutputType.Name -eq "TextReplacement" -or ($_.CommandType -eq "Alias" -and $_.Definition -like "PesterMock*" ) - } | - Select-Object -First 1 + Where-Object { + $_.OutputType.Name -eq "TextReplacement" -or ($_.CommandType -eq "Alias" -and $_.Definition -like "PesterMock*" ) + } | + Select-Object -First 1 if (-not $GeneratorCmd) { Write-Error "Generator missconfiguration. Unable to find Generator = '$Generator'" @@ -137,14 +143,14 @@ function Invoke-ScriptGenerator { $ParseResults = ConvertToAst -Code $Builder.ToString() -Path $ParseResults.Path # In case a Generator tries to use the actual files, update the content if ($Overwrite -and $ParseResults.Path -and $ParseResults.Path -ne "scriptblock") { - Set-Content $ParseResults.Path $Builder + & $SetContentCmd -Path $ParseResults.Path -Value $Builder -Encoding $Encoding } } } end { Write-Debug "Overwrite: $Overwrite and it's a file: $(([bool]$ParseResults.Path) -and $ParseResults.Path -ne "scriptblock") (Content is $($Builder.Length) long)" if ($Overwrite -and $ParseResults.Path -and $ParseResults.Path -ne "scriptblock") { - Set-Content $ParseResults.Path $Builder + & $SetContentCmd -Path $ParseResults.Path -Value $Builder -Encoding $Encoding } else { $Builder.ToString() }