From 09dec2a0d99c1b77c5664039ac320b33f1fa9179 Mon Sep 17 00:00:00 2001 From: Joel Bennett Date: Mon, 23 Oct 2023 00:47:31 -0400 Subject: [PATCH 1/4] Add Aspects (for cross-cutting concerns) semver:feature --- ...{AliasVisitor.ps1 => 00. AliasVisitor.ps1} | 0 Source/Classes/10. ParameterPosition.ps1 | 5 + Source/Classes/11. TextReplace.ps1 | 5 + Source/Classes/20. ModuleBuilderAspect.ps1 | 10 ++ Source/Classes/21. ParameterExtractor.ps1 | 51 ++++++++ Source/Classes/22. AddParameterAspect.ps1 | 34 +++++ Source/Classes/23. MergeBlocksAspect.ps1 | 116 ++++++++++++++++++ Source/Private/GetBuildInfo.ps1 | 11 ++ Source/Private/MergeAspect.ps1 | 59 +++++++++ Source/Public/Build-Module.ps1 | 19 +++ 10 files changed, 310 insertions(+) rename Source/Classes/{AliasVisitor.ps1 => 00. AliasVisitor.ps1} (100%) create mode 100644 Source/Classes/10. ParameterPosition.ps1 create mode 100644 Source/Classes/11. TextReplace.ps1 create mode 100644 Source/Classes/20. ModuleBuilderAspect.ps1 create mode 100644 Source/Classes/21. ParameterExtractor.ps1 create mode 100644 Source/Classes/22. AddParameterAspect.ps1 create mode 100644 Source/Classes/23. MergeBlocksAspect.ps1 create mode 100644 Source/Private/MergeAspect.ps1 diff --git a/Source/Classes/AliasVisitor.ps1 b/Source/Classes/00. AliasVisitor.ps1 similarity index 100% rename from Source/Classes/AliasVisitor.ps1 rename to Source/Classes/00. AliasVisitor.ps1 diff --git a/Source/Classes/10. ParameterPosition.ps1 b/Source/Classes/10. ParameterPosition.ps1 new file mode 100644 index 0000000..e760cc8 --- /dev/null +++ b/Source/Classes/10. ParameterPosition.ps1 @@ -0,0 +1,5 @@ +class ParameterPosition { + [string]$Name + [int]$StartOffset + [string]$Text +} diff --git a/Source/Classes/11. TextReplace.ps1 b/Source/Classes/11. TextReplace.ps1 new file mode 100644 index 0000000..1cc0356 --- /dev/null +++ b/Source/Classes/11. TextReplace.ps1 @@ -0,0 +1,5 @@ +class TextReplace { + [int]$StartOffset = 0 + [int]$EndOffset = 0 + [string]$Text = '' +} diff --git a/Source/Classes/20. ModuleBuilderAspect.ps1 b/Source/Classes/20. ModuleBuilderAspect.ps1 new file mode 100644 index 0000000..749e4ac --- /dev/null +++ b/Source/Classes/20. ModuleBuilderAspect.ps1 @@ -0,0 +1,10 @@ +class ModuleBuilderAspect : AstVisitor { + [List[TextReplace]]$Replacements = @() + [ScriptBlock]$Where = { $true } + [Ast]$Aspect + + [List[TextReplace]]Generate([Ast]$ast) { + $ast.Visit($this) + return $this.Replacements + } +} diff --git a/Source/Classes/21. ParameterExtractor.ps1 b/Source/Classes/21. ParameterExtractor.ps1 new file mode 100644 index 0000000..a8194eb --- /dev/null +++ b/Source/Classes/21. ParameterExtractor.ps1 @@ -0,0 +1,51 @@ +class ParameterExtractor : AstVisitor { + [ParameterPosition[]]$Parameters = @() + [int]$InsertLineNumber = -1 + [int]$InsertColumnNumber = -1 + [int]$InsertOffset = -1 + + ParameterExtractor([Ast]$Ast) { + $ast.Visit($this) + } + + [AstVisitAction] VisitParamBlock([ParamBlockAst]$ast) { + if ($Ast.Parameters) { + $Text = $ast.Extent.Text -split "\r?\n" + + $FirstLine = $ast.Extent.StartLineNumber + $NextLine = 1 + $this.Parameters = @( + foreach ($parameter in $ast.Parameters | Select-Object Name -Expand Extent) { + [ParameterPosition]@{ + Name = $parameter.Name + StartOffset = $parameter.StartOffset + Text = if (($parameter.StartLineNumber - $FirstLine) -ge $NextLine) { + Write-Debug "Extracted parameter $($Parameter.Name) with surrounding lines" + # Take lines after the last parameter + $Lines = @($Text[$NextLine..($parameter.EndLineNumber - $FirstLine)].Where{ ![string]::IsNullOrWhiteSpace($_) }) + # If the last line extends past the end of the parameter, trim that line + if ($Lines.Length -gt 0 -and $parameter.EndColumnNumber -lt $Lines[-1].Length) { + $Lines[-1] = $Lines[-1].SubString($parameter.EndColumnNumber) + } + # Don't return the commas, we'll add them back later + ($Lines -join "`n").TrimEnd(",") + } else { + Write-Debug "Extracted parameter $($Parameter.Name) text exactly" + $parameter.Text.TrimEnd(",") + } + } + $NextLine = 1 + $parameter.EndLineNumber - $FirstLine + } + ) + + $this.InsertLineNumber = $ast.Parameters[-1].Extent.EndLineNumber + $this.InsertColumnNumber = $ast.Parameters[-1].Extent.EndColumnNumber + $this.InsertOffset = $ast.Parameters[-1].Extent.EndOffset + } else { + $this.InsertLineNumber = $ast.Extent.EndLineNumber + $this.InsertColumnNumber = $ast.Extent.EndColumnNumber - 1 + $this.InsertOffset = $ast.Extent.EndOffset - 1 + } + return [AstVisitAction]::StopVisit + } +} diff --git a/Source/Classes/22. AddParameterAspect.ps1 b/Source/Classes/22. AddParameterAspect.ps1 new file mode 100644 index 0000000..fddf7b3 --- /dev/null +++ b/Source/Classes/22. AddParameterAspect.ps1 @@ -0,0 +1,34 @@ +class AddParameterAspect : ModuleBuilderAspect { + [System.Management.Automation.HiddenAttribute()] + [ParameterExtractor]$AdditionalParameterCache + + [ParameterExtractor]GetAdditional() { + if (!$this.AdditionalParameterCache) { + $this.AdditionalParameterCache = $this.Aspect + } + return $this.AdditionalParameterCache + } + + [AstVisitAction] VisitFunctionDefinition([FunctionDefinitionAst]$ast) { + if (!$ast.Where($this.Where)) { + return [AstVisitAction]::SkipChildren + } + $Existing = [ParameterExtractor]$ast + $Additional = $this.GetAdditional().Parameters.Where{ $_.Name -notin $Existing.Parameters.Name } + if (($Text = $Additional.Text -join ",`n`n")) { + $Replacement = [TextReplace]@{ + StartOffset = $Existing.InsertOffset + EndOffset = $Existing.InsertOffset + Text = if ($Existing.Parameters.Count -gt 0) { + ",`n`n" + $Text + } else { + "`n" + $Text + } + } + + Write-Debug "Adding parameters to $($ast.name): $($Additional.Name -join ', ')" + $this.Replacements.Add($Replacement) + } + return [AstVisitAction]::SkipChildren + } +} diff --git a/Source/Classes/23. MergeBlocksAspect.ps1 b/Source/Classes/23. MergeBlocksAspect.ps1 new file mode 100644 index 0000000..f65be72 --- /dev/null +++ b/Source/Classes/23. MergeBlocksAspect.ps1 @@ -0,0 +1,116 @@ +class MergeBlocksAspect : ModuleBuilderAspect { + [System.Management.Automation.HiddenAttribute()] + [NamedBlockAst]$BeginBlockTemplate + + [System.Management.Automation.HiddenAttribute()] + [NamedBlockAst]$ProcessBlockTemplate + + [System.Management.Automation.HiddenAttribute()] + [NamedBlockAst]$EndBlockTemplate + + [List[TextReplace]]Generate([Ast]$ast) { + if (!($this.BeginBlockTemplate = $this.Aspect.Find({ $args[0] -is [NamedBlockAst] -and $args[0].BlockKind -eq "Begin" }, $false))) { + Write-Debug "No Aspect for BeginBlock" + } else { + Write-Debug "BeginBlock Aspect: $($this.BeginBlockTemplate)" + } + if (!($this.ProcessBlockTemplate = $this.Aspect.Find({ $args[0] -is [NamedBlockAst] -and $args[0].BlockKind -eq "Process" }, $false))) { + Write-Debug "No Aspect for ProcessBlock" + } else { + Write-Debug "ProcessBlock Aspect: $($this.ProcessBlockTemplate)" + } + if (!($this.EndBlockTemplate = $this.Aspect.Find({ $args[0] -is [NamedBlockAst] -and $args[0].BlockKind -eq "End" }, $false))) { + Write-Debug "No Aspect for EndBlock" + } else { + Write-Debug "EndBlock Aspect: $($this.EndBlockTemplate)" + } + + $ast.Visit($this) + return $this.Replacements + } + + # The [Alias(...)] attribute on functions matters, but we can't export aliases that are defined inside a function + [AstVisitAction] VisitFunctionDefinition([FunctionDefinitionAst]$ast) { + if (!$ast.Where($this.Where)) { + return [AstVisitAction]::SkipChildren + } + + if ($this.BeginBlockTemplate) { + if ($ast.Body.BeginBlock) { + $BeginExtent = $ast.Body.BeginBlock.Extent + $BeginBlockText = ($BeginExtent.Text -replace "^begin[\s\r\n]*{|}[\s\r\n]*$", "`n").Trim("`r`n").TrimEnd("`r`n ") + + $Replacement = [TextReplace]@{ + StartOffset = $BeginExtent.StartOffset + EndOffset = $BeginExtent.EndOffset + Text = $this.BeginBlockTemplate.Extent.Text.Replace("existingcode", $BeginBlockText) + } + + $this.Replacements.Add( $Replacement ) + } else { + Write-Debug "$($ast.Name) Missing BeginBlock" + } + } + + if ($this.ProcessBlockTemplate) { + if ($ast.Body.ProcessBlock) { + # In a "filter" function, the process block may contain the param block + $ProcessBlockExtent = $ast.Body.ProcessBlock.Extent + + if ($ast.Body.ProcessBlock.UnNamed -and $ast.Body.ParamBlock.Extent.Text) { + # Trim the paramBlock out of the end block + $ProcessBlockText = $ProcessBlockExtent.Text.Remove( + $ast.Body.ParamBlock.Extent.StartOffset - $ProcessBlockExtent.StartOffset, + $ast.Body.ParamBlock.Extent.EndOffset - $ast.Body.ParamBlock.Extent.StartOffset) + $StartOffset = $ast.Body.ParamBlock.Extent.EndOffset + } else { + # Trim the `process {` ... `}` because we're inserting it into the template process + $ProcessBlockText = ($ProcessBlockExtent.Text -replace "^process[\s\r\n]*{|}[\s\r\n]*$", "`n").Trim("`r`n").TrimEnd("`r`n ") + $StartOffset = $ProcessBlockExtent.StartOffset + } + + $Replacement = [TextReplace]@{ + StartOffset = $StartOffset + EndOffset = $ProcessBlockExtent.EndOffset + Text = $this.ProcessBlockTemplate.Extent.Text.Replace("existingcode", $ProcessBlockText) + } + + $this.Replacements.Add( $Replacement ) + } else { + Write-Debug "$($ast.Name) Missing ProcessBlock" + } + } + + if ($this.EndBlockTemplate) { + if ($ast.Body.EndBlock) { + # The end block is a problem because it frequently contains the param block, which must be left alone + $EndBlockExtent = $ast.Body.EndBlock.Extent + + $EndBlockText = $EndBlockExtent.Text + $StartOffset = $EndBlockExtent.StartOffset + if ($ast.Body.EndBlock.UnNamed -and $ast.Body.ParamBlock.Extent.Text) { + # Trim the paramBlock out of the end block + $EndBlockText = $EndBlockExtent.Text.Remove( + $ast.Body.ParamBlock.Extent.StartOffset - $EndBlockExtent.StartOffset, + $ast.Body.ParamBlock.Extent.EndOffset - $ast.Body.ParamBlock.Extent.StartOffset) + $StartOffset = $ast.Body.ParamBlock.Extent.EndOffset + } else { + # Trim the `end {` ... `}` because we're inserting it into the template end + $EndBlockText = ($EndBlockExtent.Text -replace "^end[\s\r\n]*{|}[\s\r\n]*$", "`n").Trim("`r`n").TrimEnd("`r`n ") + } + + $Replacement = [TextReplace]@{ + StartOffset = $StartOffset + EndOffset = $EndBlockExtent.EndOffset + Text = $this.EndBlockTemplate.Extent.Text.Replace("existingcode", $EndBlockText) + } + + $this.Replacements.Add( $Replacement ) + } else { + Write-Debug "$($ast.Name) Missing EndBlock" + } + } + + return [AstVisitAction]::SkipChildren + } +} diff --git a/Source/Private/GetBuildInfo.ps1 b/Source/Private/GetBuildInfo.ps1 index b9cf51c..e4f1ecd 100644 --- a/Source/Private/GetBuildInfo.ps1 +++ b/Source/Private/GetBuildInfo.ps1 @@ -111,6 +111,17 @@ function GetBuildInfo { } } + # Make sure Aspects is an array of objects (instead of hashtables) + if ($BuildInfo.Aspects) { + $BuildInfo.Aspects = $BuildInfo.Aspects | ForEach-Object { + if ($_ -is [hashtable]) { + [PSCustomObject]$_ + } else { + $_ + } + } + } + $BuildInfo = $BuildInfo | Update-Object $ParameterValues Write-Debug "Using Module Manifest $($BuildInfo.SourcePath)" diff --git a/Source/Private/MergeAspect.ps1 b/Source/Private/MergeAspect.ps1 new file mode 100644 index 0000000..a9c879d --- /dev/null +++ b/Source/Private/MergeAspect.ps1 @@ -0,0 +1,59 @@ +function MergeAspect { + <# + .SYNOPSIS + Merge features of a script into commands from a module, using a ModuleBuilderAspect + .DESCRIPTION + This is an aspect-oriented programming approach for adding cross-cutting features to functions in a module. + + The [ModuleBuilderAspect] implementations are [AstVisitors] that return [TextReplace] object representing modifications to be performed on the source. + #> + [CmdletBinding()] + param( + # The path to the RootModule psm1 to merge the aspect into + [Parameter(Mandatory, Position = 0)] + [string]$RootModule, + + # The name of the ModuleBuilder Generator to invoke. + # There are two built in: + # - MergeBlocks. Supports Before/After/Around blocks for aspects like error handling or authentication. + # - AddParameter. Supports adding common parameters to functions (usually in conjunction with MergeBlock that use those parameters) + [Parameter(Mandatory, ValueFromPipelineByPropertyName)] + [ValidateScript({ (($_ -As [Type]), ("${_}Aspect" -As [Type])).BaseType -eq [ModuleBuilderAspect] })] + [string]$Action, + + # The name(s) of functions in the module to run the generator against. Supports wildcards. + [Parameter(Mandatory, ValueFromPipelineByPropertyName)] + [string[]]$Function, + + # The name of the script path or function that contains the base which drives the generator + [Parameter(Mandatory, ValueFromPipelineByPropertyName)] + [string]$Source + ) + process { + #! We can't reuse the AST because it needs to be updated after we change it + #! But we can handle this in a wrapper + Write-Verbose "Parsing $RootModule for $Action with $Source" + $Ast = ConvertToAst $RootModule + + $Action = if ($Action -As [Type]) { + $Action + } elseif ("${Action}Aspect" -As [Type]) { + "${Action}Aspect" + } else { + throw "Can't find $Action ModuleBuilderAspect" + } + + $Aspect = New-Object $Action -Property @{ + Where = { $Func = $_; $Function.ForEach({ $Func.Name -like $_ }) -contains $true }.GetNewClosure() + Aspect = @(Get-Command (Join-Path $AspectDirectory $Source), $Source -ErrorAction Ignore)[0].ScriptBlock.Ast + } + + #! Process replacements from the bottom up, so the line numbers work + $Content = Get-Content $RootModule -Raw + Write-Verbose "Generating $Action in $RootModule" + foreach ($replacement in $Aspect.Generate($Ast.Ast) | Sort-Object StartOffset -Descending) { + $Content = $Content.Remove($replacement.StartOffset, ($replacement.EndOffset - $replacement.StartOffset)).Insert($replacement.StartOffset, $replacement.Text) + } + Set-Content $RootModule $Content + } +} diff --git a/Source/Public/Build-Module.ps1 b/Source/Public/Build-Module.ps1 index f2c80c4..d067cea 100644 --- a/Source/Public/Build-Module.ps1 +++ b/Source/Public/Build-Module.ps1 @@ -137,6 +137,19 @@ function Build-Module { [ValidateSet("Clean", "Build", "CleanBuild")] [string]$Target = "CleanBuild", + # A list of Aspects to apply to the module + # Each aspect contains a Function (pattern), Action and Source + # For example: + # @{ Function = "*"; Action = "MergeBlocks"; Source = "TraceBlocks" } + # There are only two Actions built in: + # - AddParameter. Supports adding common parameters to functions + # - MergeBlocks. Supports adding code Before/After/Around existing blocks for aspects like error handling or authentication. + [PSCustomObject[]]$Aspects, + + # The folder (relative to the module folder) which contains the scripts to be used as Source for Aspects + # Defaults to "Aspects" + [string]$AspectDirectory = "[Aa]spects", + # Output the ModuleInfo of the "built" module [switch]$Passthru ) @@ -269,6 +282,12 @@ function Build-Module { } } + if ($ModuleInfo.Aspects) { + $AspectDirectory = Join-Path -Path $ModuleInfo.ModuleBase -ChildPath $ModuleInfo.AspectDirectory | Convert-Path -ErrorAction SilentlyContinue + Write-Verbose "Apply $($ModuleInfo.Aspects.Count) Aspects from $AspectDirectory" + $ModuleInfo.Aspects | MergeAspect $RootModule + } + # This is mostly for testing ... if ($ModuleInfo.Passthru) { Get-Module $OutputManifest -ListAvailable From be8a9ae288b25e5f7ab73873365627d0656adfcc Mon Sep 17 00:00:00 2001 From: Joel Bennett Date: Sun, 14 Jan 2024 11:18:58 -0500 Subject: [PATCH 2/4] WIP: Modern .NET calls them Generators --- .../ModuleBuilderExtensions.ps1 | 215 ++++++++++++++++++ Source/Classes/20. ModuleBuilderAspect.ps1 | 10 - Source/Classes/20. ModuleBuilderGenerator.ps1 | 67 ++++++ Source/Classes/21. ParameterExtractor.ps1 | 9 + ...rAspect.ps1 => 22. ParameterGenerator.ps1} | 5 +- ...locksAspect.ps1 => 23. BlockGenerator.ps1} | 6 +- Source/ModuleBuilder.psd1 | 2 +- .../{MergeAspect.ps1 => InvokeGenerator.ps1} | 10 +- Source/Public/Build-Module.ps1 | 2 +- 9 files changed, 307 insertions(+), 19 deletions(-) create mode 100644 PotentialContribution/ModuleBuilderExtensions.ps1 delete mode 100644 Source/Classes/20. ModuleBuilderAspect.ps1 create mode 100644 Source/Classes/20. ModuleBuilderGenerator.ps1 rename Source/Classes/{22. AddParameterAspect.ps1 => 22. ParameterGenerator.ps1} (89%) rename Source/Classes/{23. MergeBlocksAspect.ps1 => 23. BlockGenerator.ps1} (97%) rename Source/Private/{MergeAspect.ps1 => InvokeGenerator.ps1} (86%) diff --git a/PotentialContribution/ModuleBuilderExtensions.ps1 b/PotentialContribution/ModuleBuilderExtensions.ps1 new file mode 100644 index 0000000..718a810 --- /dev/null +++ b/PotentialContribution/ModuleBuilderExtensions.ps1 @@ -0,0 +1,215 @@ +using namespace System.Management.Automation.Language +using namespace System.Collections.Generic + + + + + +# There should be an abstract class for ModuleBuilderGenerator that has a contract for this: + + +# Should be called on a block to extract the (first) parameters from that block +class ParameterExtractor : AstVisitor { + [ParameterPosition[]]$Parameters = @() + [int]$InsertLineNumber = -1 + [int]$InsertColumnNumber = -1 + [int]$InsertOffset = -1 + + ParameterExtractor([Ast]$Ast) { + $ast.Visit($this) + } + + [AstVisitAction] VisitParamBlock([ParamBlockAst]$ast) { + if ($Ast.Parameters) { + $Text = $ast.Extent.Text -split "\r?\n" + + $FirstLine = $ast.Extent.StartLineNumber + $NextLine = 1 + $this.Parameters = @( + foreach ($parameter in $ast.Parameters | Select-Object Name -Expand Extent) { + [ParameterPosition]@{ + Name = $parameter.Name + StartOffset = $parameter.StartOffset + Text = if (($parameter.StartLineNumber - $FirstLine) -ge $NextLine) { + Write-Debug "Extracted parameter $($Parameter.Name) with surrounding lines" + # Take lines after the last parameter + $Lines = @($Text[$NextLine..($parameter.EndLineNumber - $FirstLine)].Where{![string]::IsNullOrWhiteSpace($_)}) + # If the last line extends past the end of the parameter, trim that line + if ($Lines.Length -gt 0 -and $parameter.EndColumnNumber -lt $Lines[-1].Length) { + $Lines[-1] = $Lines[-1].SubString($parameter.EndColumnNumber) + } + # Don't return the commas, we'll add them back later + ($Lines -join "`n").TrimEnd(",") + } else { + Write-Debug "Extracted parameter $($Parameter.Name) text exactly" + $parameter.Text.TrimEnd(",") + } + } + $NextLine = 1 + $parameter.EndLineNumber - $FirstLine + } + ) + + $this.InsertLineNumber = $ast.Parameters[-1].Extent.EndLineNumber + $this.InsertColumnNumber = $ast.Parameters[-1].Extent.EndColumnNumber + $this.InsertOffset = $ast.Parameters[-1].Extent.EndOffset + } else { + $this.InsertLineNumber = $ast.Extent.EndLineNumber + $this.InsertColumnNumber = $ast.Extent.EndColumnNumber - 1 + $this.InsertOffset = $ast.Extent.EndOffset - 1 + } + return [AstVisitAction]::StopVisit + } +} + +class AddParameter : ModuleBuilderGenerator { + [System.Management.Automation.HiddenAttribute()] + [ParameterExtractor]$AdditionalParameterCache + + [ParameterExtractor]GetAdditional() { + if (!$this.AdditionalParameterCache) { + $this.AdditionalParameterCache = $this.Aspect + } + return $this.AdditionalParameterCache + } + + [AstVisitAction] VisitFunctionDefinition([FunctionDefinitionAst]$ast) { + if (!$ast.Where($this.Where)) { + return [AstVisitAction]::SkipChildren + } + $Existing = [ParameterExtractor]$ast + $Additional = $this.GetAdditional().Parameters.Where{ $_.Name -notin $Existing.Parameters.Name } + if (($Text = $Additional.Text -join ",`n`n")) { + $Replacement = [TextReplace]@{ + StartOffset = $Existing.InsertOffset + EndOffset = $Existing.InsertOffset + Text = if ($Existing.Parameters.Count -gt 0) { + ",`n`n" + $Text + } else { + "`n" + $Text + } + } + + Write-Debug "Adding parameters to $($ast.name): $($Additional.Name -join ', ')" + $this.Replacements.Add($Replacement) + } + return [AstVisitAction]::SkipChildren + } +} + +class MergeBlocks : ModuleBuilderGenerator { + [System.Management.Automation.HiddenAttribute()] + [NamedBlockAst]$BeginBlockTemplate + + [System.Management.Automation.HiddenAttribute()] + [NamedBlockAst]$ProcessBlockTemplate + + [System.Management.Automation.HiddenAttribute()] + [NamedBlockAst]$EndBlockTemplate + + [List[TextReplace]]Generate([Ast]$ast) { + if (!($this.BeginBlockTemplate = $this.Aspect.Find({ $args[0] -is [NamedBlockAst] -and $args[0].BlockKind -eq "Begin" }, $false))) { + Write-Debug "No Aspect for BeginBlock" + } else { + Write-Debug "BeginBlock Aspect: $($this.BeginBlockTemplate)" + } + if (!($this.ProcessBlockTemplate = $this.Aspect.Find({ $args[0] -is [NamedBlockAst] -and $args[0].BlockKind -eq "Process" }, $false))) { + Write-Debug "No Aspect for ProcessBlock" + } else { + Write-Debug "ProcessBlock Aspect: $($this.ProcessBlockTemplate)" + } + if (!($this.EndBlockTemplate = $this.Aspect.Find({ $args[0] -is [NamedBlockAst] -and $args[0].BlockKind -eq "End" }, $false))) { + Write-Debug "No Aspect for EndBlock" + } else { + Write-Debug "EndBlock Aspect: $($this.EndBlockTemplate)" + } + + $ast.Visit($this) + return $this.Replacements + } + + # The [Alias(...)] attribute on functions matters, but we can't export aliases that are defined inside a function + [AstVisitAction] VisitFunctionDefinition([FunctionDefinitionAst]$ast) { + if (!$ast.Where($this.Where)) { + return [AstVisitAction]::SkipChildren + } + + if ($this.BeginBlockTemplate) { + if ($ast.Body.BeginBlock) { + $BeginExtent = $ast.Body.BeginBlock.Extent + $BeginBlockText = ($BeginExtent.Text -replace "^begin[\s\r\n]*{|}[\s\r\n]*$", "`n").Trim("`r`n").TrimEnd("`r`n ") + + $Replacement = [TextReplace]@{ + StartOffset = $BeginExtent.StartOffset + EndOffset = $BeginExtent.EndOffset + Text = $this.BeginBlockTemplate.Extent.Text.Replace("existingcode", $BeginBlockText) + } + + $this.Replacements.Add( $Replacement ) + } else { + Write-Debug "$($ast.Name) Missing BeginBlock" + } + } + + if ($this.ProcessBlockTemplate) { + if ($ast.Body.ProcessBlock) { + # In a "filter" function, the process block may contain the param block + $ProcessBlockExtent = $ast.Body.ProcessBlock.Extent + + if ($ast.Body.ProcessBlock.UnNamed -and $ast.Body.ParamBlock.Extent.Text) { + # Trim the paramBlock out of the end block + $ProcessBlockText = $ProcessBlockExtent.Text.Remove( + $ast.Body.ParamBlock.Extent.StartOffset - $ProcessBlockExtent.StartOffset, + $ast.Body.ParamBlock.Extent.EndOffset - $ast.Body.ParamBlock.Extent.StartOffset) + $StartOffset = $ast.Body.ParamBlock.Extent.EndOffset + } else { + # Trim the `process {` ... `}` because we're inserting it into the template process + $ProcessBlockText = ($ProcessBlockExtent.Text -replace "^process[\s\r\n]*{|}[\s\r\n]*$", "`n").Trim("`r`n").TrimEnd("`r`n ") + $StartOffset = $ProcessBlockExtent.StartOffset + } + + $Replacement = [TextReplace]@{ + StartOffset = $StartOffset + EndOffset = $ProcessBlockExtent.EndOffset + Text = $this.ProcessBlockTemplate.Extent.Text.Replace("existingcode", $ProcessBlockText) + } + + $this.Replacements.Add( $Replacement ) + } else { + Write-Debug "$($ast.Name) Missing ProcessBlock" + } + } + + if ($this.EndBlockTemplate) { + if ($ast.Body.EndBlock) { + # The end block is a problem because it frequently contains the param block, which must be left alone + $EndBlockExtent = $ast.Body.EndBlock.Extent + + $EndBlockText = $EndBlockExtent.Text + $StartOffset = $EndBlockExtent.StartOffset + if ($ast.Body.EndBlock.UnNamed -and $ast.Body.ParamBlock.Extent.Text) { + # Trim the paramBlock out of the end block + $EndBlockText = $EndBlockExtent.Text.Remove( + $ast.Body.ParamBlock.Extent.StartOffset - $EndBlockExtent.StartOffset, + $ast.Body.ParamBlock.Extent.EndOffset - $ast.Body.ParamBlock.Extent.StartOffset) + $StartOffset = $ast.Body.ParamBlock.Extent.EndOffset + } else { + # Trim the `end {` ... `}` because we're inserting it into the template end + $EndBlockText = ($EndBlockExtent.Text -replace "^end[\s\r\n]*{|}[\s\r\n]*$", "`n").Trim("`r`n").TrimEnd("`r`n ") + } + + $Replacement = [TextReplace]@{ + StartOffset = $StartOffset + EndOffset = $EndBlockExtent.EndOffset + Text = $this.EndBlockTemplate.Extent.Text.Replace("existingcode", $EndBlockText) + } + + $this.Replacements.Add( $Replacement ) + } else { + Write-Debug "$($ast.Name) Missing EndBlock" + } + } + + return [AstVisitAction]::SkipChildren + } +} + diff --git a/Source/Classes/20. ModuleBuilderAspect.ps1 b/Source/Classes/20. ModuleBuilderAspect.ps1 deleted file mode 100644 index 749e4ac..0000000 --- a/Source/Classes/20. ModuleBuilderAspect.ps1 +++ /dev/null @@ -1,10 +0,0 @@ -class ModuleBuilderAspect : AstVisitor { - [List[TextReplace]]$Replacements = @() - [ScriptBlock]$Where = { $true } - [Ast]$Aspect - - [List[TextReplace]]Generate([Ast]$ast) { - $ast.Visit($this) - return $this.Replacements - } -} diff --git a/Source/Classes/20. ModuleBuilderGenerator.ps1 b/Source/Classes/20. ModuleBuilderGenerator.ps1 new file mode 100644 index 0000000..d45103b --- /dev/null +++ b/Source/Classes/20. ModuleBuilderGenerator.ps1 @@ -0,0 +1,67 @@ +using namespace System.Management.Automation.Language +using namespace System.Collections.Generic +class TextReplace { + [int]$StartOffset = 0 + [int]$EndOffset = 0 + [string]$Text = '' +} + +class ModuleBuilderGenerator { + hidden [List[TextReplace]]$Replacements = @() + + [void] Replace($StartOffset, $EndOffset, $Text) { + $this.Replacements.Add([TextReplace]@{ + StartOffset = $StartOffset + EndOffset = $EndOffset + Text = $Text + }) + } + + [void] Insert($StartOffset, $Text) { + $this.Replacements.Add([TextReplace]@{ + StartOffset = $StartOffset + EndOffset = $StartOffset + Text = $Text + }) + } + + [ScriptBlock]$Filter = { $true } + + [Ast]$Ast + + hidden [string]$Path + + ModuleBuilderGenerator($Path) { + $this.Path = $Path + $this.Ast = ConvertToAst $Path + + } + + AddParameter([ScriptBlock]$FromScriptBlock) { + [ParameterExtractor]$ExistingParameters = $this.Ast + [ParameterExtractor]$AdditionalParameters = $FromScriptBlock.Ast + + $Additional = $AdditionalParameters.Parameters.Where{ $_.Name -notin $ExistingParameters.Parameters.Name } + if (($Text = $Additional.Text -join ",`n`n")) { + $Replacement = [TextReplace]@{ + StartOffset = $ExistingParameters.InsertOffset + EndOffset = $ExistingParameters.InsertOffset + Text = if ($ExistingParameters.Parameters.Count -gt 0) { + ",`n`n" + $Text + } else { + "`n" + $Text + } + } + + Write-Debug "Adding parameters to $($this.Ast.name): $($Additional.Name -join ', ')" + $this.Replacements.Add($Replacement) + } + } + + + + [List[TextReplace]]Generate([Ast]$ast) { + $ast.Visit($this) + return $this.Replacements + } +} diff --git a/Source/Classes/21. ParameterExtractor.ps1 b/Source/Classes/21. ParameterExtractor.ps1 index a8194eb..cd70bff 100644 --- a/Source/Classes/21. ParameterExtractor.ps1 +++ b/Source/Classes/21. ParameterExtractor.ps1 @@ -1,3 +1,12 @@ +using namespace System.Management.Automation.Language +using namespace System.Collections.Generic + +class ParameterPosition { + [string]$Name + [int]$StartOffset + [string]$Text +} + class ParameterExtractor : AstVisitor { [ParameterPosition[]]$Parameters = @() [int]$InsertLineNumber = -1 diff --git a/Source/Classes/22. AddParameterAspect.ps1 b/Source/Classes/22. ParameterGenerator.ps1 similarity index 89% rename from Source/Classes/22. AddParameterAspect.ps1 rename to Source/Classes/22. ParameterGenerator.ps1 index fddf7b3..4359a8c 100644 --- a/Source/Classes/22. AddParameterAspect.ps1 +++ b/Source/Classes/22. ParameterGenerator.ps1 @@ -1,4 +1,7 @@ -class AddParameterAspect : ModuleBuilderAspect { +using namespace System.Management.Automation.Language +using namespace System.Collections.Generic + +class ParameterGenerator : ModuleBuilderGenerator { [System.Management.Automation.HiddenAttribute()] [ParameterExtractor]$AdditionalParameterCache diff --git a/Source/Classes/23. MergeBlocksAspect.ps1 b/Source/Classes/23. BlockGenerator.ps1 similarity index 97% rename from Source/Classes/23. MergeBlocksAspect.ps1 rename to Source/Classes/23. BlockGenerator.ps1 index f65be72..7c7c2eb 100644 --- a/Source/Classes/23. MergeBlocksAspect.ps1 +++ b/Source/Classes/23. BlockGenerator.ps1 @@ -1,4 +1,7 @@ -class MergeBlocksAspect : ModuleBuilderAspect { +using namespace System.Management.Automation.Language +using namespace System.Collections.Generic + +class BlockGenerator : ModuleBuilderGenerator { [System.Management.Automation.HiddenAttribute()] [NamedBlockAst]$BeginBlockTemplate @@ -40,6 +43,7 @@ class MergeBlocksAspect : ModuleBuilderAspect { $BeginExtent = $ast.Body.BeginBlock.Extent $BeginBlockText = ($BeginExtent.Text -replace "^begin[\s\r\n]*{|}[\s\r\n]*$", "`n").Trim("`r`n").TrimEnd("`r`n ") + $Replacement = [TextReplace]@{ StartOffset = $BeginExtent.StartOffset EndOffset = $BeginExtent.EndOffset diff --git a/Source/ModuleBuilder.psd1 b/Source/ModuleBuilder.psd1 index f9bcb72..514c879 100644 --- a/Source/ModuleBuilder.psd1 +++ b/Source/ModuleBuilder.psd1 @@ -11,7 +11,7 @@ # Release Notes have to be here, so we can update them ReleaseNotes = ' - Fix case sensitivity of defaults for SourceDirectories and PublicFilter + Add support for Aspect Oriented Programming (AOP) with the new `Aspects` parameter. ' # Tags applied to this module. These help with module discovery in online galleries. diff --git a/Source/Private/MergeAspect.ps1 b/Source/Private/InvokeGenerator.ps1 similarity index 86% rename from Source/Private/MergeAspect.ps1 rename to Source/Private/InvokeGenerator.ps1 index a9c879d..6f42e6e 100644 --- a/Source/Private/MergeAspect.ps1 +++ b/Source/Private/InvokeGenerator.ps1 @@ -1,11 +1,11 @@ -function MergeAspect { +function InvokeGenerator { <# .SYNOPSIS - Merge features of a script into commands from a module, using a ModuleBuilderAspect + Generate code using a ModuleBuilderGenerator .DESCRIPTION This is an aspect-oriented programming approach for adding cross-cutting features to functions in a module. - The [ModuleBuilderAspect] implementations are [AstVisitors] that return [TextReplace] object representing modifications to be performed on the source. + The [ModuleBuilderGenerator] implementations are [AstVisitors] that return [TextReplace] object representing modifications to be performed on the source. #> [CmdletBinding()] param( @@ -18,7 +18,7 @@ function MergeAspect { # - MergeBlocks. Supports Before/After/Around blocks for aspects like error handling or authentication. # - AddParameter. Supports adding common parameters to functions (usually in conjunction with MergeBlock that use those parameters) [Parameter(Mandatory, ValueFromPipelineByPropertyName)] - [ValidateScript({ (($_ -As [Type]), ("${_}Aspect" -As [Type])).BaseType -eq [ModuleBuilderAspect] })] + [ValidateScript({ (($_ -As [Type]), ("${_}Aspect" -As [Type])).BaseType -eq [ModuleBuilderGenerator] })] [string]$Action, # The name(s) of functions in the module to run the generator against. Supports wildcards. @@ -40,7 +40,7 @@ function MergeAspect { } elseif ("${Action}Aspect" -As [Type]) { "${Action}Aspect" } else { - throw "Can't find $Action ModuleBuilderAspect" + throw "Can't find $Action ModuleBuilderGenerator" } $Aspect = New-Object $Action -Property @{ diff --git a/Source/Public/Build-Module.ps1 b/Source/Public/Build-Module.ps1 index d067cea..9827908 100644 --- a/Source/Public/Build-Module.ps1 +++ b/Source/Public/Build-Module.ps1 @@ -285,7 +285,7 @@ function Build-Module { if ($ModuleInfo.Aspects) { $AspectDirectory = Join-Path -Path $ModuleInfo.ModuleBase -ChildPath $ModuleInfo.AspectDirectory | Convert-Path -ErrorAction SilentlyContinue Write-Verbose "Apply $($ModuleInfo.Aspects.Count) Aspects from $AspectDirectory" - $ModuleInfo.Aspects | MergeAspect $RootModule + $ModuleInfo.Aspects | InvokeGenerator $RootModule } # This is mostly for testing ... From eace81799e3179ddaded3ab049331821a33092aa Mon Sep 17 00:00:00 2001 From: Joel Bennett Date: Sun, 4 Feb 2024 01:07:51 -0500 Subject: [PATCH 3/4] WIP: Tests to show it works and how to use it --- Source/Classes/10. ParameterPosition.ps1 | 5 - ...extReplace.ps1 => 10. TextReplacement.ps1} | 2 +- Source/Classes/20. ParameterExtractor.ps1 | 51 ++++++++++ Source/Classes/21. ParameterExtractor.ps1 | 60 ------------ ...tor.ps1 => 30. ModuleBuilderGenerator.ps1} | 15 +-- ...nerator.ps1 => 40. ParameterGenerator.ps1} | 19 ++-- ...ckGenerator.ps1 => 50. BlockGenerator.ps1} | 28 +++--- Source/ModuleBuilder.psd1 | 3 +- Source/Private/GetBuildInfo.ps1 | 6 +- Source/Private/InvokeGenerator.ps1 | 41 ++++---- Source/Public/Build-Module.ps1 | 37 ++++---- Tests/Integration/Source2.Tests.ps1 | 14 +++ .../Source2/Generators/NewTerminalBlock.ps1 | 95 +++++++++++++++++++ .../Generators/TracingAndErrorHandling.ps1 | 36 +++++++ .../Source2/Public/Show-HistoryId.ps1 | 9 ++ .../Source2/Public/Show-HostName.ps1 | 12 +++ Tests/Integration/Source2/Source2.psd1 | 56 +++++++++++ Tests/Integration/Source2/build.psd1 | 4 + 18 files changed, 357 insertions(+), 136 deletions(-) delete mode 100644 Source/Classes/10. ParameterPosition.ps1 rename Source/Classes/{11. TextReplace.ps1 => 10. TextReplacement.ps1} (75%) create mode 100644 Source/Classes/20. ParameterExtractor.ps1 delete mode 100644 Source/Classes/21. ParameterExtractor.ps1 rename Source/Classes/{20. ModuleBuilderGenerator.ps1 => 30. ModuleBuilderGenerator.ps1} (83%) rename Source/Classes/{22. ParameterGenerator.ps1 => 40. ParameterGenerator.ps1} (64%) rename Source/Classes/{23. BlockGenerator.ps1 => 50. BlockGenerator.ps1} (81%) create mode 100644 Tests/Integration/Source2.Tests.ps1 create mode 100644 Tests/Integration/Source2/Generators/NewTerminalBlock.ps1 create mode 100644 Tests/Integration/Source2/Generators/TracingAndErrorHandling.ps1 create mode 100644 Tests/Integration/Source2/Public/Show-HistoryId.ps1 create mode 100644 Tests/Integration/Source2/Public/Show-HostName.ps1 create mode 100644 Tests/Integration/Source2/Source2.psd1 create mode 100644 Tests/Integration/Source2/build.psd1 diff --git a/Source/Classes/10. ParameterPosition.ps1 b/Source/Classes/10. ParameterPosition.ps1 deleted file mode 100644 index e760cc8..0000000 --- a/Source/Classes/10. ParameterPosition.ps1 +++ /dev/null @@ -1,5 +0,0 @@ -class ParameterPosition { - [string]$Name - [int]$StartOffset - [string]$Text -} diff --git a/Source/Classes/11. TextReplace.ps1 b/Source/Classes/10. TextReplacement.ps1 similarity index 75% rename from Source/Classes/11. TextReplace.ps1 rename to Source/Classes/10. TextReplacement.ps1 index 1cc0356..e33e732 100644 --- a/Source/Classes/11. TextReplace.ps1 +++ b/Source/Classes/10. TextReplacement.ps1 @@ -1,4 +1,4 @@ -class TextReplace { +class TextReplacement { [int]$StartOffset = 0 [int]$EndOffset = 0 [string]$Text = '' diff --git a/Source/Classes/20. ParameterExtractor.ps1 b/Source/Classes/20. ParameterExtractor.ps1 new file mode 100644 index 0000000..f8c674e --- /dev/null +++ b/Source/Classes/20. ParameterExtractor.ps1 @@ -0,0 +1,51 @@ +using namespace System.Management.Automation.Language +using namespace System.Collections.Generic + +class ParameterExtractor : AstVisitor { + [System.Collections.Specialized.OrderedDictionary]$Parameters = @{} + [int]$InsertLineNumber = -1 + [int]$InsertColumnNumber = -1 + [int]$InsertOffset = -1 + + ParameterExtractor([Ast]$Ast) { + $ast.Visit($this) + } + + [AstVisitAction] VisitParamBlock([ParamBlockAst]$ast) { + if ($Ast.Parameters) { + $Text = $ast.Extent.Text -split "\r?\n" + + $FirstLine = $ast.Extent.StartLineNumber + $NextLine = 1 + foreach ($parameter in $ast.Parameters | Select-Object Name -Expand Extent) { + $this.Parameters.Add($parameter.Name, ([TextReplacement]@{ + StartOffset = $parameter.StartOffset + Text = if (($parameter.StartLineNumber - $FirstLine) -ge $NextLine) { + Write-Debug "Extracted parameter $($Parameter.Name) with surrounding lines" + # Take lines after the last parameter + $Lines = @($Text[$NextLine..($parameter.EndLineNumber - $FirstLine)].Where{ ![string]::IsNullOrWhiteSpace($_) }) + # If the last line extends past the end of the parameter, trim that line + if ($Lines.Length -gt 0 -and $parameter.EndColumnNumber -lt $Lines[-1].Length) { + $Lines[-1] = $Lines[-1].SubString($parameter.EndColumnNumber) + } + # Don't return the commas, we'll add them back later + ($Lines -join "`n").TrimEnd(",") + } else { + Write-Debug "Extracted parameter $($Parameter.Name) text exactly" + $parameter.Text.TrimEnd(",") + } + })) + $NextLine = 1 + $parameter.EndLineNumber - $FirstLine + } + + $this.InsertLineNumber = $ast.Parameters[-1].Extent.EndLineNumber + $this.InsertColumnNumber = $ast.Parameters[-1].Extent.EndColumnNumber + $this.InsertOffset = $ast.Parameters[-1].Extent.EndOffset + } else { + $this.InsertLineNumber = $ast.Extent.EndLineNumber + $this.InsertColumnNumber = $ast.Extent.EndColumnNumber - 1 + $this.InsertOffset = $ast.Extent.EndOffset - 1 + } + return [AstVisitAction]::StopVisit + } +} diff --git a/Source/Classes/21. ParameterExtractor.ps1 b/Source/Classes/21. ParameterExtractor.ps1 deleted file mode 100644 index cd70bff..0000000 --- a/Source/Classes/21. ParameterExtractor.ps1 +++ /dev/null @@ -1,60 +0,0 @@ -using namespace System.Management.Automation.Language -using namespace System.Collections.Generic - -class ParameterPosition { - [string]$Name - [int]$StartOffset - [string]$Text -} - -class ParameterExtractor : AstVisitor { - [ParameterPosition[]]$Parameters = @() - [int]$InsertLineNumber = -1 - [int]$InsertColumnNumber = -1 - [int]$InsertOffset = -1 - - ParameterExtractor([Ast]$Ast) { - $ast.Visit($this) - } - - [AstVisitAction] VisitParamBlock([ParamBlockAst]$ast) { - if ($Ast.Parameters) { - $Text = $ast.Extent.Text -split "\r?\n" - - $FirstLine = $ast.Extent.StartLineNumber - $NextLine = 1 - $this.Parameters = @( - foreach ($parameter in $ast.Parameters | Select-Object Name -Expand Extent) { - [ParameterPosition]@{ - Name = $parameter.Name - StartOffset = $parameter.StartOffset - Text = if (($parameter.StartLineNumber - $FirstLine) -ge $NextLine) { - Write-Debug "Extracted parameter $($Parameter.Name) with surrounding lines" - # Take lines after the last parameter - $Lines = @($Text[$NextLine..($parameter.EndLineNumber - $FirstLine)].Where{ ![string]::IsNullOrWhiteSpace($_) }) - # If the last line extends past the end of the parameter, trim that line - if ($Lines.Length -gt 0 -and $parameter.EndColumnNumber -lt $Lines[-1].Length) { - $Lines[-1] = $Lines[-1].SubString($parameter.EndColumnNumber) - } - # Don't return the commas, we'll add them back later - ($Lines -join "`n").TrimEnd(",") - } else { - Write-Debug "Extracted parameter $($Parameter.Name) text exactly" - $parameter.Text.TrimEnd(",") - } - } - $NextLine = 1 + $parameter.EndLineNumber - $FirstLine - } - ) - - $this.InsertLineNumber = $ast.Parameters[-1].Extent.EndLineNumber - $this.InsertColumnNumber = $ast.Parameters[-1].Extent.EndColumnNumber - $this.InsertOffset = $ast.Parameters[-1].Extent.EndOffset - } else { - $this.InsertLineNumber = $ast.Extent.EndLineNumber - $this.InsertColumnNumber = $ast.Extent.EndColumnNumber - 1 - $this.InsertOffset = $ast.Extent.EndOffset - 1 - } - return [AstVisitAction]::StopVisit - } -} diff --git a/Source/Classes/20. ModuleBuilderGenerator.ps1 b/Source/Classes/30. ModuleBuilderGenerator.ps1 similarity index 83% rename from Source/Classes/20. ModuleBuilderGenerator.ps1 rename to Source/Classes/30. ModuleBuilderGenerator.ps1 index d45103b..e2551ad 100644 --- a/Source/Classes/20. ModuleBuilderGenerator.ps1 +++ b/Source/Classes/30. ModuleBuilderGenerator.ps1 @@ -1,16 +1,11 @@ using namespace System.Management.Automation.Language using namespace System.Collections.Generic -class TextReplace { - [int]$StartOffset = 0 - [int]$EndOffset = 0 - [string]$Text = '' -} class ModuleBuilderGenerator { - hidden [List[TextReplace]]$Replacements = @() + hidden [List[TextReplacement]]$Replacements = @() [void] Replace($StartOffset, $EndOffset, $Text) { - $this.Replacements.Add([TextReplace]@{ + $this.Replacements.Add([TextReplacement]@{ StartOffset = $StartOffset EndOffset = $EndOffset Text = $Text @@ -18,7 +13,7 @@ class ModuleBuilderGenerator { } [void] Insert($StartOffset, $Text) { - $this.Replacements.Add([TextReplace]@{ + $this.Replacements.Add([TextReplacement]@{ StartOffset = $StartOffset EndOffset = $StartOffset Text = $Text @@ -43,7 +38,7 @@ class ModuleBuilderGenerator { $Additional = $AdditionalParameters.Parameters.Where{ $_.Name -notin $ExistingParameters.Parameters.Name } if (($Text = $Additional.Text -join ",`n`n")) { - $Replacement = [TextReplace]@{ + $Replacement = [TextReplacement]@{ StartOffset = $ExistingParameters.InsertOffset EndOffset = $ExistingParameters.InsertOffset Text = if ($ExistingParameters.Parameters.Count -gt 0) { @@ -60,7 +55,7 @@ class ModuleBuilderGenerator { - [List[TextReplace]]Generate([Ast]$ast) { + [List[TextReplacement]]Generate([Ast]$ast) { $ast.Visit($this) return $this.Replacements } diff --git a/Source/Classes/22. ParameterGenerator.ps1 b/Source/Classes/40. ParameterGenerator.ps1 similarity index 64% rename from Source/Classes/22. ParameterGenerator.ps1 rename to Source/Classes/40. ParameterGenerator.ps1 index 4359a8c..c55d4b0 100644 --- a/Source/Classes/22. ParameterGenerator.ps1 +++ b/Source/Classes/40. ParameterGenerator.ps1 @@ -5,21 +5,26 @@ class ParameterGenerator : ModuleBuilderGenerator { [System.Management.Automation.HiddenAttribute()] [ParameterExtractor]$AdditionalParameterCache - [ParameterExtractor]GetAdditional() { + ParameterGenerator($Path) : base($Path) {} + + [System.Collections.Generic.Dictionary[string, TextReplacement]]GetAdditional() { if (!$this.AdditionalParameterCache) { - $this.AdditionalParameterCache = $this.Aspect + $this.AdditionalParameterCache = [ParameterExtractor]$this.Ast } - return $this.AdditionalParameterCache + return $this.AdditionalParameterCache.Parameters } [AstVisitAction] VisitFunctionDefinition([FunctionDefinitionAst]$ast) { if (!$ast.Where($this.Where)) { return [AstVisitAction]::SkipChildren } + $Existing = [ParameterExtractor]$ast - $Additional = $this.GetAdditional().Parameters.Where{ $_.Name -notin $Existing.Parameters.Name } - if (($Text = $Additional.Text -join ",`n`n")) { - $Replacement = [TextReplace]@{ + + $AdditionalParameters = $this.GetAdditional() + $Additional = $AdditionalParameters.Keys.Where{ $_.Name -notin $Existing.Parameters.Name } + if (($Text = $AdditionalParameters.Values.Text -join ",`n`n")) { + $Replacement = [TextReplacement]@{ StartOffset = $Existing.InsertOffset EndOffset = $Existing.InsertOffset Text = if ($Existing.Parameters.Count -gt 0) { @@ -29,7 +34,7 @@ class ParameterGenerator : ModuleBuilderGenerator { } } - Write-Debug "Adding parameters to $($ast.name): $($Additional.Name -join ', ')" + Write-Debug "Adding parameters to $($ast.name): $($Additional.Keys -join ', ')" $this.Replacements.Add($Replacement) } return [AstVisitAction]::SkipChildren diff --git a/Source/Classes/23. BlockGenerator.ps1 b/Source/Classes/50. BlockGenerator.ps1 similarity index 81% rename from Source/Classes/23. BlockGenerator.ps1 rename to Source/Classes/50. BlockGenerator.ps1 index 7c7c2eb..d835fe5 100644 --- a/Source/Classes/23. BlockGenerator.ps1 +++ b/Source/Classes/50. BlockGenerator.ps1 @@ -11,21 +11,23 @@ class BlockGenerator : ModuleBuilderGenerator { [System.Management.Automation.HiddenAttribute()] [NamedBlockAst]$EndBlockTemplate - [List[TextReplace]]Generate([Ast]$ast) { - if (!($this.BeginBlockTemplate = $this.Aspect.Find({ $args[0] -is [NamedBlockAst] -and $args[0].BlockKind -eq "Begin" }, $false))) { - Write-Debug "No Aspect for BeginBlock" + BlockGenerator($Path) : base($Path) {} + + [List[TextReplacement]]Generate([Ast]$ast) { + if (!($this.BeginBlockTemplate = $this.Ast.Find({ $args[0] -is [NamedBlockAst] -and $args[0].BlockKind -eq "Begin" }, $false))) { + Write-Debug "No Generator for BeginBlock" } else { - Write-Debug "BeginBlock Aspect: $($this.BeginBlockTemplate)" + Write-Debug "BeginBlock Generator: $($this.BeginBlockTemplate)" } - if (!($this.ProcessBlockTemplate = $this.Aspect.Find({ $args[0] -is [NamedBlockAst] -and $args[0].BlockKind -eq "Process" }, $false))) { - Write-Debug "No Aspect for ProcessBlock" + if (!($this.ProcessBlockTemplate = $this.Ast.Find({ $args[0] -is [NamedBlockAst] -and $args[0].BlockKind -eq "Process" }, $false))) { + Write-Debug "No Generator for ProcessBlock" } else { - Write-Debug "ProcessBlock Aspect: $($this.ProcessBlockTemplate)" + Write-Debug "ProcessBlock Generator: $($this.ProcessBlockTemplate)" } - if (!($this.EndBlockTemplate = $this.Aspect.Find({ $args[0] -is [NamedBlockAst] -and $args[0].BlockKind -eq "End" }, $false))) { - Write-Debug "No Aspect for EndBlock" + if (!($this.EndBlockTemplate = $this.Ast.Find({ $args[0] -is [NamedBlockAst] -and $args[0].BlockKind -eq "End" }, $false))) { + Write-Debug "No Generator for EndBlock" } else { - Write-Debug "EndBlock Aspect: $($this.EndBlockTemplate)" + Write-Debug "EndBlock Generator: $($this.EndBlockTemplate)" } $ast.Visit($this) @@ -44,7 +46,7 @@ class BlockGenerator : ModuleBuilderGenerator { $BeginBlockText = ($BeginExtent.Text -replace "^begin[\s\r\n]*{|}[\s\r\n]*$", "`n").Trim("`r`n").TrimEnd("`r`n ") - $Replacement = [TextReplace]@{ + $Replacement = [TextReplacement]@{ StartOffset = $BeginExtent.StartOffset EndOffset = $BeginExtent.EndOffset Text = $this.BeginBlockTemplate.Extent.Text.Replace("existingcode", $BeginBlockText) @@ -73,7 +75,7 @@ class BlockGenerator : ModuleBuilderGenerator { $StartOffset = $ProcessBlockExtent.StartOffset } - $Replacement = [TextReplace]@{ + $Replacement = [TextReplacement]@{ StartOffset = $StartOffset EndOffset = $ProcessBlockExtent.EndOffset Text = $this.ProcessBlockTemplate.Extent.Text.Replace("existingcode", $ProcessBlockText) @@ -103,7 +105,7 @@ class BlockGenerator : ModuleBuilderGenerator { $EndBlockText = ($EndBlockExtent.Text -replace "^end[\s\r\n]*{|}[\s\r\n]*$", "`n").Trim("`r`n").TrimEnd("`r`n ") } - $Replacement = [TextReplace]@{ + $Replacement = [TextReplacement]@{ StartOffset = $StartOffset EndOffset = $EndBlockExtent.EndOffset Text = $this.EndBlockTemplate.Extent.Text.Replace("existingcode", $EndBlockText) diff --git a/Source/ModuleBuilder.psd1 b/Source/ModuleBuilder.psd1 index 514c879..843f37e 100644 --- a/Source/ModuleBuilder.psd1 +++ b/Source/ModuleBuilder.psd1 @@ -11,7 +11,8 @@ # Release Notes have to be here, so we can update them ReleaseNotes = ' - Add support for Aspect Oriented Programming (AOP) with the new `Aspects` parameter. + Add support for Generators, so you can weave in solutions for cross-cutting concerns at "build" time. + Specifically targeting support for module-wide common parameters, standardized fall-back error handling, etc. ' # Tags applied to this module. These help with module discovery in online galleries. diff --git a/Source/Private/GetBuildInfo.ps1 b/Source/Private/GetBuildInfo.ps1 index e4f1ecd..b01cff8 100644 --- a/Source/Private/GetBuildInfo.ps1 +++ b/Source/Private/GetBuildInfo.ps1 @@ -111,9 +111,9 @@ function GetBuildInfo { } } - # Make sure Aspects is an array of objects (instead of hashtables) - if ($BuildInfo.Aspects) { - $BuildInfo.Aspects = $BuildInfo.Aspects | ForEach-Object { + # Make sure Generators is an array of objects (instead of hashtables) + if ($BuildInfo.Generators) { + $BuildInfo.Generators = $BuildInfo.Generators | ForEach-Object { if ($_ -is [hashtable]) { [PSCustomObject]$_ } else { diff --git a/Source/Private/InvokeGenerator.ps1 b/Source/Private/InvokeGenerator.ps1 index 6f42e6e..cc51f67 100644 --- a/Source/Private/InvokeGenerator.ps1 +++ b/Source/Private/InvokeGenerator.ps1 @@ -5,53 +5,54 @@ function InvokeGenerator { .DESCRIPTION This is an aspect-oriented programming approach for adding cross-cutting features to functions in a module. - The [ModuleBuilderGenerator] implementations are [AstVisitors] that return [TextReplace] object representing modifications to be performed on the source. + The [ModuleBuilderGenerator] implementations are [AstVisitors] that return [TextReplacement] objects representing modifications to be performed on the source. #> + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSReviewUnusedParameter', 'Function', Justification = 'PSSA reads the AST wrong')] [CmdletBinding()] param( - # The path to the RootModule psm1 to merge the aspect into + # The path to the RootModule psm1 to apply the Generator to [Parameter(Mandatory, Position = 0)] [string]$RootModule, # The name of the ModuleBuilder Generator to invoke. - # There are two built in: - # - MergeBlocks. Supports Before/After/Around blocks for aspects like error handling or authentication. - # - AddParameter. Supports adding common parameters to functions (usually in conjunction with MergeBlock that use those parameters) + # There are only two Generators built in: + # - ParameterGenerator. Supports adding parameters to functions in the module. Parameters come from a template file, which must be a script with a param block. + # - BlockGenerator. Supports adding code before, after, and around existing blocks for generators like error handling, authentication, and implementations for common parameters. The added blocks come from a template file, which must be a script with named begin/process/end blocks. [Parameter(Mandatory, ValueFromPipelineByPropertyName)] - [ValidateScript({ (($_ -As [Type]), ("${_}Aspect" -As [Type])).BaseType -eq [ModuleBuilderGenerator] })] - [string]$Action, + [ValidateScript({ (($_ -As [Type]), ("${_}Generator" -As [Type])).BaseType -eq [ModuleBuilderGenerator] })] + [string]$Generator, - # The name(s) of functions in the module to run the generator against. Supports wildcards. + # The name(s) of functions in the RootModule to run the generator against. Supports wildcards. [Parameter(Mandatory, ValueFromPipelineByPropertyName)] [string[]]$Function, - # The name of the script path or function that contains the base which drives the generator + # The name of a template file that customizes the generator [Parameter(Mandatory, ValueFromPipelineByPropertyName)] - [string]$Source + [string]$Template ) process { #! We can't reuse the AST because it needs to be updated after we change it #! But we can handle this in a wrapper - Write-Verbose "Parsing $RootModule for $Action with $Source" + Write-Verbose "Parsing $RootModule for $Generator with $Template" $Ast = ConvertToAst $RootModule - $Action = if ($Action -As [Type]) { - $Action - } elseif ("${Action}Aspect" -As [Type]) { - "${Action}Aspect" + $Generator = if ($Generator -As [Type]) { + $Generator + } elseif ("${Generator}Generator" -As [Type]) { + "${Generator}Generator" } else { - throw "Can't find $Action ModuleBuilderGenerator" + throw "Can't find $Generator ModuleBuilderGenerator" } - $Aspect = New-Object $Action -Property @{ + $Generator = New-Object $Generator -Property @{ Where = { $Func = $_; $Function.ForEach({ $Func.Name -like $_ }) -contains $true }.GetNewClosure() - Aspect = @(Get-Command (Join-Path $AspectDirectory $Source), $Source -ErrorAction Ignore)[0].ScriptBlock.Ast + Generator = @(Get-Command (Join-Path $GeneratorDirectory $Template), $Template -ErrorAction Ignore)[0].ScriptBlock.Ast } #! Process replacements from the bottom up, so the line numbers work $Content = Get-Content $RootModule -Raw - Write-Verbose "Generating $Action in $RootModule" - foreach ($replacement in $Aspect.Generate($Ast.Ast) | Sort-Object StartOffset -Descending) { + Write-Verbose "Generating $Generator in $RootModule" + foreach ($replacement in $Generator.Generate($Ast.Ast) | Sort-Object StartOffset -Descending) { $Content = $Content.Remove($replacement.StartOffset, ($replacement.EndOffset - $replacement.StartOffset)).Insert($replacement.StartOffset, $replacement.Text) } Set-Content $RootModule $Content diff --git a/Source/Public/Build-Module.ps1 b/Source/Public/Build-Module.ps1 index 9827908..ab431e4 100644 --- a/Source/Public/Build-Module.ps1 +++ b/Source/Public/Build-Module.ps1 @@ -137,18 +137,23 @@ function Build-Module { [ValidateSet("Clean", "Build", "CleanBuild")] [string]$Target = "CleanBuild", - # A list of Aspects to apply to the module - # Each aspect contains a Function (pattern), Action and Source - # For example: - # @{ Function = "*"; Action = "MergeBlocks"; Source = "TraceBlocks" } - # There are only two Actions built in: - # - AddParameter. Supports adding common parameters to functions - # - MergeBlocks. Supports adding code Before/After/Around existing blocks for aspects like error handling or authentication. - [PSCustomObject[]]$Aspects, - - # The folder (relative to the module folder) which contains the scripts to be used as Source for Aspects - # Defaults to "Aspects" - [string]$AspectDirectory = "[Aa]spects", + # A list of Generators to apply to the module. Each Generator has: + # - Function: The name(s) of functions in the RootModule to run the generator against. Supports wildcards. + # - Generator: The Typename of the ModuleBuilder Generator to invoke. + # - Template: the base name of a file that serves as input to the generator + # The template is technically optional, but the built-in generators both require a template. + # + # For example, if you wrote a template file "TraceBlocks.ps1", you could add it to the build like this: + # @{ Function = "*"; Generator = "Block"; Template = "TraceBlocks" } + # + # There are two Generators built-in: + # - ParameterGenerator. Supports adding parameters to functions in the module. Parameters come from a template file, which must be a script with a param block. + # - BlockGenerator. Supports adding code before, after, and around existing blocks for generators like error handling, authentication, and implementations for common parameters. The added blocks come from a template file, which must be a script with named begin/process/end blocks. + [PSCustomObject[]]$Generators, + + # The folder (relative to the module folder) which contains the scripts to be used as Source for Generators + # Defaults to "Generators" + [string]$GeneratorDirectory = "[Gg]enerators", # Output the ModuleInfo of the "built" module [switch]$Passthru @@ -282,10 +287,10 @@ function Build-Module { } } - if ($ModuleInfo.Aspects) { - $AspectDirectory = Join-Path -Path $ModuleInfo.ModuleBase -ChildPath $ModuleInfo.AspectDirectory | Convert-Path -ErrorAction SilentlyContinue - Write-Verbose "Apply $($ModuleInfo.Aspects.Count) Aspects from $AspectDirectory" - $ModuleInfo.Aspects | InvokeGenerator $RootModule + if ($ModuleInfo.Generators) { + $GeneratorDirectory = Join-Path -Path $ModuleInfo.ModuleBase -ChildPath $ModuleInfo.GeneratorDirectory | Convert-Path -ErrorAction SilentlyContinue + Write-Verbose "Apply $($ModuleInfo.Generators.Count) Generators from $GeneratorDirectory" + $ModuleInfo.Generators | InvokeGenerator $RootModule } # This is mostly for testing ... diff --git a/Tests/Integration/Source2.Tests.ps1 b/Tests/Integration/Source2.Tests.ps1 new file mode 100644 index 0000000..b457665 --- /dev/null +++ b/Tests/Integration/Source2.Tests.ps1 @@ -0,0 +1,14 @@ +#requires -Module ModuleBuilder +. $PSScriptRoot\..\Convert-FolderSeparator.ps1 + +Describe "Using Generators" -Tag Integration { + $Output = Build-Module $PSScriptRoot\Source2\build.psd1 -Passthru + $Module = [IO.Path]::ChangeExtension($Output.Path, "psm1") + + $Metadata = Import-Metadata $Output.Path + + It "Still builds the right functions and updates the manifest" { + $Metadata.FunctionsToExport | Should -Be @('Show-HistoryId', 'Show-HostName') + } + +} diff --git a/Tests/Integration/Source2/Generators/NewTerminalBlock.ps1 b/Tests/Integration/Source2/Generators/NewTerminalBlock.ps1 new file mode 100644 index 0000000..4c42740 --- /dev/null +++ b/Tests/Integration/Source2/Generators/NewTerminalBlock.ps1 @@ -0,0 +1,95 @@ +using module ModuleBuilder +class TerminalBlockGenerator : ModuleBuilderGenerator { + + [void] Generate() { + + $base.AddParameter({ + param( + [Alias("Prepend")] + [String]$Prefix, + + [Alias("Suffix", "Append")] + [String]$Postfix, + + # The separator character(s) are used between blocks of output by this scriptblock + # Pass two characters: the first for normal (Left aligned) blocks, the second for right-aligned blocks + [ArgumentCompleter({ + [System.Collections.Generic.List[System.Management.Automation.CompletionResult]]::new( + [System.Management.Automation.CompletionResult[]]@( + # The Consolas-friendly block characters ▌and▐ and ╲ followed by all the extended Terminal characters + @([string[]][char[]]@(@(0xe0b0..0xe0d4) + @(0x2588..0x259b) + @(0x256d..0x2572))).ForEach({ + [System.Management.Automation.CompletionResult]::new("'$_'", $_, "ParameterValue", $_) }) + )) + })] + [String]$Separator, + + # The cap character(s) are used on the ends of blocks of output + # Pass two characters: the first for the left side, the second for the right side. + [ArgumentCompleter({ + [System.Collections.Generic.List[System.Management.Automation.CompletionResult]]::new( + [System.Management.Automation.CompletionResult[]]@( + # The Consolas-friendly block characters ▌and▐ and ╲ followed by all the extended Terminal characters + @([string[]][char[]]@(@(0xe0b0..0xe0d4) + @(0x2588..0x259b) + @(0x256d..0x2572))).ForEach({ + [System.Management.Automation.CompletionResult]::new("'$_'", $_, "ParameterValue", $_) }) + )) + })] + [PoshCode.BlockCaps]$Caps, + + # The foreground color to use when the last command succeeded + [Alias("ForegroundColor", "Fg", "DFg")] + [AllowNull()][EmptyStringAsNull()] + [ArgumentCompleter([PoshCode.Pansies.Palettes.X11Palette])] + [PoshCode.Pansies.RgbColor]$DefaultForegroundColor, + + # The background color to use when the last command succeeded + [Alias("BackgroundColor", "Bg", "DBg")] + [AllowNull()][EmptyStringAsNull()] + [ArgumentCompleter([PoshCode.Pansies.Palettes.X11Palette])] + [PoshCode.Pansies.RgbColor]$DefaultBackgroundColor, + + # The foreground color to use when the process is elevated (running as administrator) + [Alias("AdminFg", "AFg")] + [AllowNull()][EmptyStringAsNull()] + [ArgumentCompleter([PoshCode.Pansies.Palettes.X11Palette])] + [PoshCode.Pansies.RgbColor]$AdminForegroundColor, + + # The background color to use when the process is elevated (running as administrator) + [Alias("AdminBg", "ABg")] + [AllowNull()][EmptyStringAsNull()] + [ArgumentCompleter([PoshCode.Pansies.Palettes.X11Palette])] + [PoshCode.Pansies.RgbColor]$AdminBackgroundColor, + + # The foreground color to use when the last command failed + [Alias("ErrorFg", "EFg")] + [AllowNull()][EmptyStringAsNull()] + [ArgumentCompleter([PoshCode.Pansies.Palettes.X11Palette])] + [PoshCode.Pansies.RgbColor]$ErrorForegroundColor, + + # The background color to use when the last command failed + [Alias("ErrorBg", "EBg")] + [AllowNull()][EmptyStringAsNull()] + [ArgumentCompleter([PoshCode.Pansies.Palettes.X11Palette])] + [PoshCode.Pansies.RgbColor]$ErrorBackgroundColor + ) + }) + + $base.AddBeforeEnd(@' + # Support default parameter values + $Parameters = Get-ParameterValue + $Parameters["Content"] = { +'@) + $base.AddAfterEnd(@' + }.GetNewClosure() + + # Strip common parameters if they're on here (so we can use -Verbose) + foreach ($name in @($Parameters.Keys.Where{ $_ -notin [PoshCode.TerminalBlock].GetProperties().Name })) { + $null = $Parameters.Remove($name) + } + + # Store the InvocationInfo for serialization + $Parameters["MyInvocation"] = [System.Management.Automation.InvocationInfo].GetProperty("ScriptPosition", [System.Reflection.BindingFlags]"Instance,NonPublic").GetValue($MyInvocation).Text + + [PoshCode.TerminalBlock]$Parameters +'@) + } +} diff --git a/Tests/Integration/Source2/Generators/TracingAndErrorHandling.ps1 b/Tests/Integration/Source2/Generators/TracingAndErrorHandling.ps1 new file mode 100644 index 0000000..2073f1f --- /dev/null +++ b/Tests/Integration/Source2/Generators/TracingAndErrorHandling.ps1 @@ -0,0 +1,36 @@ + [CmdletBinding()] + param() + begin { + Write-Information "Enter $($PSCmdlet.MyInvocation.MyCommand.Name)" -Tags "BeginBlock", "Enter" + try { + existingcode + } catch { + Write-Information $_ -Tags "BeginBlock", "Exception", "Unhandled" + throw + } finally { + Write-Information "Leave $($PSCmdlet.MyInvocation.MyCommand.Name)" -Tags "BeginBlock", "Leave" + } + } + process { + Write-Information "Enter $($PSCmdlet.MyInvocation.MyCommand.Name)" -Tags "ProcessBlock", "Enter" + try { + existingcode + } catch { + Write-Information $_ -Tags "ProcessBlock", "Exception", "Unhandled" + throw + } finally { + Write-Information "Leave $($PSCmdlet.MyInvocation.MyCommand.Name)" -Tags "ProcessBlock", "Leave" + } + } + end { + Write-Information "Enter $($PSCmdlet.MyInvocation.MyCommand.Name)" -Tags "EndBlock", "Enter" + try { + existingcode + } catch { + Write-Information $_ -Tags "EndBlock", "Exception", "Unhandled" + throw + } finally { + Write-Information "Leave $($PSCmdlet.MyInvocation.MyCommand.Name)" -Tags "EndBlock", "Leave" + } + } + diff --git a/Tests/Integration/Source2/Public/Show-HistoryId.ps1 b/Tests/Integration/Source2/Public/Show-HistoryId.ps1 new file mode 100644 index 0000000..f08abf4 --- /dev/null +++ b/Tests/Integration/Source2/Public/Show-HistoryId.ps1 @@ -0,0 +1,9 @@ +function Show-HistoryId { + <# + .SYNOPSIS + Shows the ID of the command you're about to type (this WILL BE the History ID of the command you run) + #> + [CmdletBinding()] + param() + $MyInvocation.HistoryId +} diff --git a/Tests/Integration/Source2/Public/Show-HostName.ps1 b/Tests/Integration/Source2/Public/Show-HostName.ps1 new file mode 100644 index 0000000..0ef5680 --- /dev/null +++ b/Tests/Integration/Source2/Public/Show-HostName.ps1 @@ -0,0 +1,12 @@ +function Show-HostName { + <# + .SYNOPSIS + Gets the hostname of the current machine + .DESCRIPTION + Calls [Environment]::MachineName + #> + [OutputType([string])] + [CmdletBinding(DefaultParameterSetName = "SimpleFormat")] + param() + [Environment]::MachineName +} diff --git a/Tests/Integration/Source2/Source2.psd1 b/Tests/Integration/Source2/Source2.psd1 new file mode 100644 index 0000000..f55bb44 --- /dev/null +++ b/Tests/Integration/Source2/Source2.psd1 @@ -0,0 +1,56 @@ +@{ + # The module version should be SemVer.org compatible + ModuleVersion = "1.0.0" + + # PrivateData is where all third-party metadata goes + PrivateData = @{ + # PrivateData.PSData is the PowerShell Gallery data + PSData = @{ + # Prerelease string should be here, so we can set it + Prerelease = '' + + # Release Notes have to be here, so we can update them + ReleaseNotes = ' + A test module with generators + ' + + # Tags applied to this module. These help with module discovery in online galleries. + Tags = 'Authoring','Build','Development','BestPractices' + + # A URL to the license for this module. + LicenseUri = 'https://github.com/PoshCode/ModuleBuilder/blob/master/LICENSE' + + # A URL to the main website for this project. + ProjectUri = 'https://github.com/PoshCode/ModuleBuilder' + + # A URL to an icon representing this module. + IconUri = 'https://github.com/PoshCode/ModuleBuilder/blob/resources/ModuleBuilder.png?raw=true' + } # End of PSData + } # End of PrivateData + + # The main script module that is automatically loaded as part of this module + RootModule = 'Source2.psm1' + + # Modules that must be imported into the global environment prior to importing this module + RequiredModules = @() + + # Default prefix for commands exported from this module. Override the default prefix using Import-Module -Prefix. + DefaultCommandPrefix = 'Source' + + # Always define FunctionsToExport as an empty @() which will be replaced on build + FunctionsToExport = @() + AliasesToExport = @() + + # ID used to uniquely identify this module + GUID = 'a264e183-e0f7-4219-bc80-c30d14e0e98e' + Description = 'A module for authoring and building PowerShell modules' + + # Common stuff for all our modules: + CompanyName = 'PoshCode' + Author = 'Joel Bennett' + Copyright = "Copyright 2018 Joel Bennett" + + # Minimum version of the Windows PowerShell engine required by this module + PowerShellVersion = '5.1' + CompatiblePSEditions = @('Core','Desktop') +} diff --git a/Tests/Integration/Source2/build.psd1 b/Tests/Integration/Source2/build.psd1 new file mode 100644 index 0000000..47482ad --- /dev/null +++ b/Tests/Integration/Source2/build.psd1 @@ -0,0 +1,4 @@ +@{ + Path = "Source2.psd1" + OutputDirectory = "..\Result1" +} From 2e8074ce761007b090eb664593b20c3837ab9a6e Mon Sep 17 00:00:00 2001 From: Joel Bennett Date: Sun, 4 Feb 2024 01:08:15 -0500 Subject: [PATCH 4/4] Update the build to PS 7.4 --- Earthfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Earthfile b/Earthfile index be47ddf..21b8d45 100644 --- a/Earthfile +++ b/Earthfile @@ -1,6 +1,6 @@ VERSION 0.7 IMPORT github.com/poshcode/tasks -FROM mcr.microsoft.com/dotnet/sdk:7.0 +FROM mcr.microsoft.com/dotnet/sdk:8.0 WORKDIR /work ARG --global EARTHLY_GIT_ORIGIN_URL