diff --git a/Plaster/Plaster.psm1 b/Plaster/Plaster.psm1
index 060b2cb..2dd3e8c 100644
--- a/Plaster/Plaster.psm1
+++ b/Plaster/Plaster.psm1
@@ -79,6 +79,9 @@ try {
[System.Diagnostics.CodeAnalysis.SuppressMessage('PSUseDeclaredVarsMoreThanAssigments', '')]
$PlasterVersion = (Test-ModuleManifest -Path (Join-Path $PSScriptRoot 'Plaster.psd1')).Version
+[System.Diagnostics.CodeAnalysis.SuppressMessage('PSUseDeclaredVarsMoreThanAssigments', '')]
+$JsonSchemaPath = Join-Path $PSScriptRoot "Schema\plaster-manifest-v2.json"
+
[System.Diagnostics.CodeAnalysis.SuppressMessage('PSUseDeclaredVarsMoreThanAssigments', '')]
$LatestSupportedSchemaVersion = [System.Version]'1.2'
@@ -136,6 +139,26 @@ if (-not $script:XmlSchemaValidationSupported) {
# Module logging configuration
$script:LogLevel = if ($env:PLASTER_LOG_LEVEL) { $env:PLASTER_LOG_LEVEL } else { 'Information' }
+# Global variables and constants for Plaster 2.0
+
+# Enhanced $TargetNamespace definition with proper scoping
+if (-not (Get-Variable -Name 'TargetNamespace' -Scope Script -ErrorAction SilentlyContinue)) {
+ Set-Variable -Name 'TargetNamespace' -Value 'http://www.microsoft.com/schemas/PowerShell/Plaster/v1' -Scope Script -Option ReadOnly
+}
+
+# Enhanced $DefaultEncoding definition
+if (-not (Get-Variable -Name 'DefaultEncoding' -Scope Script -ErrorAction SilentlyContinue)) {
+ Set-Variable -Name 'DefaultEncoding' -Value 'UTF8-NoBOM' -Scope Script -Option ReadOnly
+}
+
+# JSON Schema version for new manifests
+if (-not (Get-Variable -Name 'JsonSchemaVersion' -Scope Script -ErrorAction SilentlyContinue)) {
+ Set-Variable -Name 'JsonSchemaVersion' -Value '2.0' -Scope Script -Option ReadOnly
+}
+
+# Export the variables that need to be available globally
+Export-ModuleMember -Variable @('TargetNamespace', 'DefaultEncoding', 'JsonSchemaVersion')
+
# Module cleanup on removal
$MyInvocation.MyCommand.ScriptBlock.Module.OnRemove = {
Write-PlasterLog -Level Information -Message "Plaster module is being removed"
@@ -149,4 +172,4 @@ $MyInvocation.MyCommand.ScriptBlock.Module.OnRemove = {
}
# Module initialization complete
-Write-PlasterLog -Level Information -Message "Plaster v$PlasterVersion module loaded successfully (PowerShell $($PSVersionTable.PSVersion))"
\ No newline at end of file
+Write-PlasterLog -Level Information -Message "Plaster v$PlasterVersion module loaded successfully (PowerShell $($PSVersionTable.PSVersion))"
diff --git a/Plaster/Private/ConvertFrom-JsonContentAction.ps1 b/Plaster/Private/ConvertFrom-JsonContentAction.ps1
new file mode 100644
index 0000000..ad9f03a
--- /dev/null
+++ b/Plaster/Private/ConvertFrom-JsonContentAction.ps1
@@ -0,0 +1,128 @@
+function ConvertFrom-JsonContentAction {
+ [CmdletBinding()]
+ [OutputType([System.Xml.XmlElement])]
+ param(
+ [Parameter(Mandatory)]
+ [object]$Action,
+
+ [Parameter(Mandatory)]
+ [System.Xml.XmlDocument]$XmlDocument
+ )
+
+ switch ($Action.type) {
+ 'message' {
+ $element = $XmlDocument.CreateElement('message', $TargetNamespace)
+ $element.InnerText = $Action.text
+
+ if ($Action.noNewline) {
+ $element.SetAttribute('nonewline', 'true')
+ }
+ }
+ 'file' {
+ $element = $XmlDocument.CreateElement('file', $TargetNamespace)
+ $element.SetAttribute('source', $Action.source)
+ $element.SetAttribute('destination', $Action.destination)
+
+ if ($Action.encoding) {
+ $element.SetAttribute('encoding', $Action.encoding)
+ }
+
+ if ($Action.openInEditor) {
+ $element.SetAttribute('openInEditor', 'true')
+ }
+ }
+ 'templateFile' {
+ $element = $XmlDocument.CreateElement('templateFile', $TargetNamespace)
+ $element.SetAttribute('source', $Action.source)
+ $element.SetAttribute('destination', $Action.destination)
+
+ if ($Action.encoding) {
+ $element.SetAttribute('encoding', $Action.encoding)
+ }
+
+ if ($Action.openInEditor) {
+ $element.SetAttribute('openInEditor', 'true')
+ }
+ }
+ 'directory' {
+ $element = $XmlDocument.CreateElement('file', $TargetNamespace)
+ $element.SetAttribute('source', '')
+ $element.SetAttribute('destination', $Action.destination)
+ }
+ 'newModuleManifest' {
+ $element = $XmlDocument.CreateElement('newModuleManifest', $TargetNamespace)
+ $element.SetAttribute('destination', $Action.destination)
+
+ $manifestProperties = @('moduleVersion', 'rootModule', 'author', 'companyName', 'description', 'powerShellVersion', 'copyright', 'encoding')
+ foreach ($property in $manifestProperties) {
+ if ($Action.PSObject.Properties[$property]) {
+ $element.SetAttribute($property, $Action.$property)
+ }
+ }
+
+ if ($Action.openInEditor) {
+ $element.SetAttribute('openInEditor', 'true')
+ }
+ }
+ 'modify' {
+ $element = $XmlDocument.CreateElement('modify', $TargetNamespace)
+ $element.SetAttribute('path', $Action.path)
+
+ if ($Action.encoding) {
+ $element.SetAttribute('encoding', $Action.encoding)
+ }
+
+ # Add modifications
+ foreach ($modification in $Action.modifications) {
+ if ($modification.type -eq 'replace') {
+ $replaceElement = $XmlDocument.CreateElement('replace', $TargetNamespace)
+
+ $originalElement = $XmlDocument.CreateElement('original', $TargetNamespace)
+ $originalElement.InnerText = $modification.search
+ if ($modification.isRegex) {
+ $originalElement.SetAttribute('expand', 'true')
+ }
+ $replaceElement.AppendChild($originalElement)
+
+ $substituteElement = $XmlDocument.CreateElement('substitute', $TargetNamespace)
+ $substituteElement.InnerText = $modification.replace
+ $substituteElement.SetAttribute('expand', 'true')
+ $replaceElement.AppendChild($substituteElement)
+
+ if ($modification.condition) {
+ $replaceElement.SetAttribute('condition', $modification.condition)
+ }
+
+ $element.AppendChild($replaceElement)
+ }
+ }
+ }
+ 'requireModule' {
+ $element = $XmlDocument.CreateElement('requireModule', $TargetNamespace)
+ $element.SetAttribute('name', $Action.name)
+
+ $moduleProperties = @('minimumVersion', 'maximumVersion', 'requiredVersion', 'message')
+ foreach ($property in $moduleProperties) {
+ if ($Action.PSObject.Properties[$property]) {
+ $element.SetAttribute($property, $Action.$property)
+ }
+ }
+ }
+ 'execute' {
+ # Execute action doesn't have direct XML equivalent, convert to message with warning
+ $element = $XmlDocument.CreateElement('message', $TargetNamespace)
+ $element.InnerText = "Warning: Execute action not supported in XML format. Script: $($Action.script)"
+ Write-PlasterLog -Level Warning -Message "Execute action converted to message - not supported in XML format"
+ }
+ default {
+ throw "Unknown action type: $($Action.type)"
+ }
+ }
+
+ # Add condition if present
+ if ($Action.condition) {
+ $element.SetAttribute('condition', $Action.condition)
+ }
+
+ return $element
+}
diff --git a/Plaster/Private/ConvertFrom-JsonManifest.ps1 b/Plaster/Private/ConvertFrom-JsonManifest.ps1
new file mode 100644
index 0000000..8af7bda
--- /dev/null
+++ b/Plaster/Private/ConvertFrom-JsonManifest.ps1
@@ -0,0 +1,126 @@
+function ConvertFrom-JsonManifest {
+ [CmdletBinding()]
+ [OutputType([System.Xml.XmlDocument])]
+ param(
+ [Parameter(Mandatory, ValueFromPipeline)]
+ [string]$JsonContent,
+
+ [Parameter()]
+ [switch]$Validate = $true
+ )
+
+ begin {
+ Write-PlasterLog -Level Debug -Message "Converting JSON manifest to internal format"
+ }
+
+ process {
+ try {
+ # Validate JSON if requested
+ if ($Validate) {
+ $isValid = Test-JsonManifest -JsonContent $JsonContent -Detailed
+ if (-not $isValid) {
+ throw "JSON manifest validation failed"
+ }
+ }
+
+ # Parse JSON
+ $jsonObject = $JsonContent | ConvertFrom-Json
+
+ # Create XML document
+ $xmlDoc = New-Object System.Xml.XmlDocument
+ $xmlDoc.LoadXml('')
+
+ $manifest = $xmlDoc.DocumentElement
+ $manifest.SetAttribute('schemaVersion', '1.2') # Use XML schema version for compatibility
+
+ if ($jsonObject.metadata.templateType) {
+ $manifest.SetAttribute('templateType', $jsonObject.metadata.templateType)
+ }
+
+ # Add metadata
+ $metadataElement = $xmlDoc.CreateElement('metadata', $TargetNamespace)
+ $manifest.AppendChild($metadataElement)
+
+ # Add metadata properties
+ $metadataProperties = @('name', 'id', 'version', 'title', 'description', 'author', 'tags')
+ foreach ($property in $metadataProperties) {
+ if ($jsonObject.metadata.PSObject.Properties[$property]) {
+ $element = $xmlDoc.CreateElement($property, $TargetNamespace)
+ $value = $jsonObject.metadata.$property
+
+ if ($property -eq 'tags' -and $value -is [array]) {
+ $element.InnerText = $value -join ', '
+ } else {
+ $element.InnerText = $value
+ }
+ $metadataElement.AppendChild($element)
+ }
+ }
+
+ # Add parameters
+ $parametersElement = $xmlDoc.CreateElement('parameters', $TargetNamespace)
+ $manifest.AppendChild($parametersElement)
+
+ if ($jsonObject.parameters) {
+ foreach ($param in $jsonObject.parameters) {
+ $paramElement = $xmlDoc.CreateElement('parameter', $TargetNamespace)
+ $paramElement.SetAttribute('name', $param.name)
+ $paramElement.SetAttribute('type', $param.type)
+
+ if ($param.prompt) {
+ $paramElement.SetAttribute('prompt', $param.prompt)
+ }
+
+ if ($param.default) {
+ if ($param.default -is [array]) {
+ $paramElement.SetAttribute('default', ($param.default -join ','))
+ } else {
+ $paramElement.SetAttribute('default', $param.default)
+ }
+ }
+
+ if ($param.condition) {
+ $paramElement.SetAttribute('condition', $param.condition)
+ }
+
+ if ($param.store) {
+ $paramElement.SetAttribute('store', $param.store)
+ }
+
+ # Add choices for choice/multichoice parameters
+ if ($param.choices) {
+ foreach ($choice in $param.choices) {
+ $choiceElement = $xmlDoc.CreateElement('choice', $TargetNamespace)
+ $choiceElement.SetAttribute('label', $choice.label)
+ $choiceElement.SetAttribute('value', $choice.value)
+
+ if ($choice.help) {
+ $choiceElement.SetAttribute('help', $choice.help)
+ }
+
+ $paramElement.AppendChild($choiceElement)
+ }
+ }
+
+ $parametersElement.AppendChild($paramElement)
+ }
+ }
+
+ # Add content
+ $contentElement = $xmlDoc.CreateElement('content', $TargetNamespace)
+ $manifest.AppendChild($contentElement)
+
+ foreach ($action in $jsonObject.content) {
+ $actionElement = ConvertFrom-JsonContentAction -Action $action -XmlDocument $xmlDoc
+ $contentElement.AppendChild($actionElement)
+ }
+
+ Write-PlasterLog -Level Debug -Message "JSON to XML conversion completed successfully"
+ return $xmlDoc
+ } catch {
+ $errorMessage = "Failed to convert JSON manifest: $($_.Exception.Message)"
+ Write-PlasterLog -Level Error -Message $errorMessage
+ throw $_
+ }
+ }
+}
diff --git a/Plaster/Private/ConvertTo-JsonContentAction.ps1 b/Plaster/Private/ConvertTo-JsonContentAction.ps1
new file mode 100644
index 0000000..d41d7dc
--- /dev/null
+++ b/Plaster/Private/ConvertTo-JsonContentAction.ps1
@@ -0,0 +1,125 @@
+function ConvertTo-JsonContentAction {
+ [CmdletBinding()]
+ [OutputType([object])]
+ param(
+ [Parameter(Mandatory)]
+ [System.Xml.XmlElement]$ActionNode
+ )
+
+ $action = [ordered]@{
+ 'type' = $ActionNode.LocalName
+ }
+
+ switch ($ActionNode.LocalName) {
+ 'message' {
+ $action['text'] = $ActionNode.InnerText
+ if ($ActionNode.nonewline -eq 'true') {
+ $action['noNewline'] = $true
+ }
+ }
+ 'file' {
+ $action['source'] = $ActionNode.source
+ $action['destination'] = $ActionNode.destination
+
+ if ($ActionNode.encoding) {
+ $action['encoding'] = $ActionNode.encoding
+ }
+
+ if ($ActionNode.openInEditor -eq 'true') {
+ $action['openInEditor'] = $true
+ }
+
+ # Handle directory creation (empty source)
+ if ([string]::IsNullOrEmpty($ActionNode.source)) {
+ $action['type'] = 'directory'
+ $action.Remove('source')
+ }
+ }
+ 'templateFile' {
+ $action['source'] = $ActionNode.source
+ $action['destination'] = $ActionNode.destination
+
+ if ($ActionNode.encoding) {
+ $action['encoding'] = $ActionNode.encoding
+ }
+
+ if ($ActionNode.openInEditor -eq 'true') {
+ $action['openInEditor'] = $true
+ }
+ }
+ 'newModuleManifest' {
+ $action['destination'] = $ActionNode.destination
+
+ $manifestProperties = @('moduleVersion', 'rootModule', 'author', 'companyName', 'description', 'powerShellVersion', 'copyright', 'encoding')
+ foreach ($property in $manifestProperties) {
+ if ($ActionNode.$property) {
+ $action[$property] = $ActionNode.$property
+ }
+ }
+
+ if ($ActionNode.openInEditor -eq 'true') {
+ $action['openInEditor'] = $true
+ }
+ }
+ 'modify' {
+ $action['path'] = $ActionNode.path
+
+ if ($ActionNode.encoding) {
+ $action['encoding'] = $ActionNode.encoding
+ }
+
+ # Extract modifications
+ $modifications = @()
+ foreach ($child in $ActionNode.ChildNodes) {
+ if ($child.NodeType -eq 'Element' -and $child.LocalName -eq 'replace') {
+ $modification = [ordered]@{
+ 'type' = 'replace'
+ }
+
+ $originalNode = $child.SelectSingleNode('*[local-name()="original"]')
+ $substituteNode = $child.SelectSingleNode('*[local-name()="substitute"]')
+
+ if ($originalNode) {
+ $modification['search'] = $originalNode.InnerText
+ if ($originalNode.expand -eq 'true') {
+ $modification['isRegex'] = $true
+ }
+ }
+
+ if ($substituteNode) {
+ $modification['replace'] = $substituteNode.InnerText
+ }
+
+ if ($child.condition) {
+ $modification['condition'] = $child.condition
+ }
+
+ $modifications += $modification
+ }
+ }
+
+ $action['modifications'] = $modifications
+ }
+ 'requireModule' {
+ $action['name'] = $ActionNode.name
+
+ $moduleProperties = @('minimumVersion', 'maximumVersion', 'requiredVersion', 'message')
+ foreach ($property in $moduleProperties) {
+ if ($ActionNode.$property) {
+ $action[$property] = $ActionNode.$property
+ }
+ }
+ }
+ default {
+ Write-PlasterLog -Level Warning -Message "Unknown XML action type: $($ActionNode.LocalName)"
+ return $null
+ }
+ }
+
+ # Add condition if present
+ if ($ActionNode.condition) {
+ $action['condition'] = $ActionNode.condition
+ }
+
+ return $action
+}
diff --git a/Plaster/Private/ConvertTo-JsonManifest.ps1 b/Plaster/Private/ConvertTo-JsonManifest.ps1
new file mode 100644
index 0000000..8668839
--- /dev/null
+++ b/Plaster/Private/ConvertTo-JsonManifest.ps1
@@ -0,0 +1,148 @@
+function ConvertTo-JsonManifest {
+ [CmdletBinding()]
+ [OutputType([string])]
+ param(
+ [Parameter(Mandatory, ValueFromPipeline)]
+ [System.Xml.XmlDocument]$XmlManifest,
+
+ [Parameter()]
+ [switch]$Compress
+ )
+
+ begin {
+ Write-PlasterLog -Level Debug -Message "Converting XML manifest to JSON format"
+ }
+
+ process {
+ try {
+ $jsonObject = [ordered]@{
+ '$schema' = 'https://raw.githubusercontent.com/PowerShellOrg/Plaster/v2/schema/plaster-manifest-v2.json'
+ 'schemaVersion' = '2.0'
+ }
+
+ # Extract metadata
+ $metadata = [ordered]@{}
+ $metadataNode = $XmlManifest.plasterManifest.metadata
+
+ if ($metadataNode) {
+ foreach ($child in $metadataNode.ChildNodes) {
+ if ($child.NodeType -eq 'Element') {
+ $value = $child.InnerText
+ if ($child.LocalName -eq 'tags' -and $value) {
+ $metadata[$child.LocalName] = $value -split ',' | ForEach-Object { $_.Trim() }
+ } else {
+ $metadata[$child.LocalName] = $value
+ }
+ }
+ }
+ }
+
+ # Add template type if present
+ if ($XmlManifest.plasterManifest.templateType) {
+ $metadata['templateType'] = $XmlManifest.plasterManifest.templateType
+ } else {
+ $metadata['templateType'] = 'Project'
+ }
+
+ $jsonObject['metadata'] = $metadata
+
+ # Extract parameters
+ $parameters = @()
+ $parametersNode = $XmlManifest.plasterManifest.parameters
+
+ if ($parametersNode) {
+ foreach ($paramNode in $parametersNode.ChildNodes) {
+ if ($paramNode.NodeType -eq 'Element' -and $paramNode.LocalName -eq 'parameter') {
+ $param = [ordered]@{
+ 'name' = $paramNode.name
+ 'type' = $paramNode.type
+ }
+
+ if ($paramNode.prompt) {
+ $param['prompt'] = $paramNode.prompt
+ }
+
+ if ($paramNode.default) {
+ if ($paramNode.type -eq 'multichoice') {
+ $param['default'] = $paramNode.default -split ','
+ } else {
+ $param['default'] = $paramNode.default
+ }
+ }
+
+ if ($paramNode.condition) {
+ $param['condition'] = $paramNode.condition
+ }
+
+ if ($paramNode.store) {
+ $param['store'] = $paramNode.store
+ }
+
+ # Extract choices
+ $choices = @()
+ foreach ($choiceNode in $paramNode.ChildNodes) {
+ if ($choiceNode.NodeType -eq 'Element' -and $choiceNode.LocalName -eq 'choice') {
+ $choice = [ordered]@{
+ 'label' = $choiceNode.label
+ 'value' = $choiceNode.value
+ }
+
+ if ($choiceNode.help) {
+ $choice['help'] = $choiceNode.help
+ }
+
+ $choices += $choice
+ }
+ }
+
+ if ($choices.Count -gt 0) {
+ $param['choices'] = $choices
+ }
+
+ $parameters += $param
+ }
+ }
+ }
+
+ if ($parameters.Count -gt 0) {
+ $jsonObject['parameters'] = $parameters
+ }
+
+ # Extract content
+ $content = @()
+ $contentNode = $XmlManifest.plasterManifest.content
+
+ if ($contentNode) {
+ foreach ($actionNode in $contentNode.ChildNodes) {
+ if ($actionNode.NodeType -eq 'Element') {
+ $action = ConvertTo-JsonContentAction -ActionNode $actionNode
+ if ($action) {
+ $content += $action
+ }
+ }
+ }
+ }
+
+ $jsonObject['content'] = $content
+
+ # Convert to JSON
+ $jsonParams = @{
+ InputObject = $jsonObject
+ Depth = 10
+ }
+
+ if (-not $Compress) {
+ $jsonParams['Compress'] = $false
+ }
+
+ $jsonResult = $jsonObject | ConvertTo-Json @jsonParams
+
+ Write-PlasterLog -Level Debug -Message "XML to JSON conversion completed successfully"
+ return $jsonResult
+ } catch {
+ $errorMessage = "Failed to convert XML manifest to JSON: $($_.Exception.Message)"
+ Write-PlasterLog -Level Error -Message $errorMessage
+ throw $_
+ }
+ }
+}
diff --git a/Plaster/Private/Get-ManifestsUnderPath.ps1 b/Plaster/Private/Get-ManifestsUnderPath.ps1
index ca7a941..394da9d 100644
--- a/Plaster/Private/Get-ManifestsUnderPath.ps1
+++ b/Plaster/Private/Get-ManifestsUnderPath.ps1
@@ -45,7 +45,7 @@ function Get-ManifestsUnderPath {
)
$getChildItemSplat = @{
Path = $RootPath
- Include = "plasterManifest.xml"
+ Include = "plasterManifest.xml", "plasterManifest.json"
Recurse = $Recurse
}
$manifestPaths = Get-ChildItem @getChildItemSplat
diff --git a/Plaster/Private/Get-PlasterManifestPathForCulture.ps1 b/Plaster/Private/Get-PlasterManifestPathForCulture.ps1
index 1dd579f..693e214 100644
--- a/Plaster/Private/Get-PlasterManifestPathForCulture.ps1
+++ b/Plaster/Private/Get-PlasterManifestPathForCulture.ps1
@@ -20,7 +20,7 @@ function Get-PlasterManifestPathForCulture {
The culture information for which to retrieve the Plaster manifest file.
.EXAMPLE
- GetPlasterManifestPathForCulture -TemplatePath "C:\Templates" -Culture (Get-Culture)
+ Get-PlasterManifestPathForCulture -TemplatePath "C:\Templates" -Culture (Get-Culture)
This example retrieves the path to the Plaster manifest file for the current culture.
.NOTES
diff --git a/Plaster/Private/Get-PlasterManifestType.ps1 b/Plaster/Private/Get-PlasterManifestType.ps1
new file mode 100644
index 0000000..a7415d0
--- /dev/null
+++ b/Plaster/Private/Get-PlasterManifestType.ps1
@@ -0,0 +1,79 @@
+function Get-PlasterManifestType {
+ [CmdletBinding()]
+ [OutputType([string])]
+ param(
+ [Parameter(Mandatory)]
+ [string]$ManifestPath
+ )
+
+ if (-not (Test-Path -LiteralPath $ManifestPath)) {
+ throw "Manifest file not found: $ManifestPath"
+ }
+
+ try {
+ $content = Get-Content -LiteralPath $ManifestPath -Raw -ErrorAction Stop
+
+ # Check file extension first
+ $extension = [System.IO.Path]::GetExtension($ManifestPath).ToLower()
+ if ($extension -eq '.json') {
+ # Validate it's actually JSON
+ try {
+ $jsonObject = $content | ConvertFrom-Json -ErrorAction Stop
+ # Check for Plaster 2.0 JSON schema
+ if ($jsonObject.schemaVersion -eq '2.0') {
+ return 'JSON'
+ }
+ # Also accept older JSON formats without strict version check
+ if ($jsonObject.PSObject.Properties['metadata'] -and $jsonObject.PSObject.Properties['content']) {
+ return 'JSON'
+ }
+ } catch {
+ throw "File has .json extension but contains invalid JSON: $($_.Exception.Message)"
+ }
+ } elseif ($extension -eq '.xml') {
+ # Validate it's actually XML
+ try {
+ $xmlDoc = New-Object System.Xml.XmlDocument
+ $xmlDoc.LoadXml($content)
+ if ($xmlDoc.DocumentElement.LocalName -eq 'plasterManifest') {
+ return 'XML'
+ }
+ } catch {
+ throw "File has .xml extension but contains invalid XML: $($_.Exception.Message)"
+ }
+ }
+
+ # If no extension or ambiguous, try to detect by content
+ $trimmedContent = $content.TrimStart()
+
+ # Check for JSON format (starts with { or [)
+ if ($trimmedContent -match '^[\s]*[\{\[]') {
+ try {
+ $jsonObject = $content | ConvertFrom-Json -ErrorAction Stop
+ # Validate it's a Plaster JSON manifest
+ if ($jsonObject.PSObject.Properties['metadata'] -and $jsonObject.PSObject.Properties['content']) {
+ return 'JSON'
+ }
+ } catch {
+ # Not valid JSON, continue to XML check
+ }
+ }
+
+ # Check for XML format
+ if ($trimmedContent -match '^[\s]*<\?xml' -or $trimmedContent -match ' $null
$res = $powershell.Invoke()
- $res
+
+ # Enhanced logging for JSON expressions
+ if ($Expression -match '\$\{.*\}' -and $manifestType -eq 'JSON') {
+ Write-PlasterLog -Level Debug -Message "JSON expression evaluated: $Expression -> $res"
+ }
+
+ return $res
} catch {
throw ($LocalizedData.ExpressionInvalid_F2 -f $Expression, $_)
}
- # Check for non-terminating errors.
if ($powershell.Streams.Error.Count -gt 0) {
$err = $powershell.Streams.Error[0]
throw ($LocalizedData.ExpressionNonTermErrors_F2 -f $Expression, $err)
diff --git a/Plaster/Private/Invoke-PlasterOperation.ps1 b/Plaster/Private/Invoke-PlasterOperation.ps1
index 8b40fe6..64c24ae 100644
--- a/Plaster/Private/Invoke-PlasterOperation.ps1
+++ b/Plaster/Private/Invoke-PlasterOperation.ps1
@@ -42,13 +42,12 @@ function Invoke-PlasterOperation {
[switch]
$PassThru
)
-
try {
Write-PlasterLog -Level Debug -Message "Starting operation: $OperationName"
$result = & $ScriptBlock
Write-PlasterLog -Level Debug -Message "Completed operation: $OperationName"
- if ($PassThru.IsPresent) {
+ if ($PassThru) {
return $result
}
} catch {
diff --git a/Plaster/Private/New-ConstrainedRunspace.ps1 b/Plaster/Private/New-ConstrainedRunspace.ps1
index afbe0a6..1954c62 100644
--- a/Plaster/Private/New-ConstrainedRunspace.ps1
+++ b/Plaster/Private/New-ConstrainedRunspace.ps1
@@ -8,69 +8,55 @@ function New-ConstrainedRunspace {
$iss.LanguageMode = [System.Management.Automation.PSLanguageMode]::ConstrainedLanguage
$iss.DisableFormatUpdates = $true
+ # Add providers
$sspe = New-Object System.Management.Automation.Runspaces.SessionStateProviderEntry 'Environment', ([Microsoft.PowerShell.Commands.EnvironmentProvider]), $null
$iss.Providers.Add($sspe)
$sspe = New-Object System.Management.Automation.Runspaces.SessionStateProviderEntry 'FileSystem', ([Microsoft.PowerShell.Commands.FileSystemProvider]), $null
$iss.Providers.Add($sspe)
- $ssce = New-Object System.Management.Automation.Runspaces.SessionStateCmdletEntry 'Get-Content', ([Microsoft.PowerShell.Commands.GetContentCommand]), $null
- $iss.Commands.Add($ssce)
-
- $ssce = New-Object System.Management.Automation.Runspaces.SessionStateCmdletEntry 'Get-Date', ([Microsoft.PowerShell.Commands.GetDateCommand]), $null
- $iss.Commands.Add($ssce)
-
- $ssce = New-Object System.Management.Automation.Runspaces.SessionStateCmdletEntry 'Get-ChildItem', ([Microsoft.PowerShell.Commands.GetChildItemCommand]), $null
- $iss.Commands.Add($ssce)
-
- $ssce = New-Object System.Management.Automation.Runspaces.SessionStateCmdletEntry 'Get-Item', ([Microsoft.PowerShell.Commands.GetItemCommand]), $null
- $iss.Commands.Add($ssce)
-
- $ssce = New-Object System.Management.Automation.Runspaces.SessionStateCmdletEntry 'Get-ItemProperty', ([Microsoft.PowerShell.Commands.GetItemPropertyCommand]), $null
- $iss.Commands.Add($ssce)
-
- $ssce = New-Object System.Management.Automation.Runspaces.SessionStateCmdletEntry 'Get-Module', ([Microsoft.PowerShell.Commands.GetModuleCommand]), $null
- $iss.Commands.Add($ssce)
-
- $ssce = New-Object System.Management.Automation.Runspaces.SessionStateCmdletEntry 'Get-Variable', ([Microsoft.PowerShell.Commands.GetVariableCommand]), $null
- $iss.Commands.Add($ssce)
+ # Add cmdlets with enhanced set for JSON processing
+ $cmdlets = @(
+ 'Get-Content', 'Get-Date', 'Get-ChildItem', 'Get-Item', 'Get-ItemProperty',
+ 'Get-Module', 'Get-Variable', 'Test-Path', 'Out-String', 'Compare-Object',
+ 'ConvertFrom-Json', 'ConvertTo-Json' # JSON support
+ )
+
+ foreach ($cmdletName in $cmdlets) {
+ #$cmdletType = [Microsoft.PowerShell.Commands.GetContentCommand].Assembly.GetType("Microsoft.PowerShell.Commands.$($cmdletName -replace '-')Command")
+ $cmdletType = "Microsoft.PowerShell.Commands.$($cmdletName -replace '-')Command" -as [Type]
+ if ($cmdletType) {
+ $ssce = New-Object System.Management.Automation.Runspaces.SessionStateCmdletEntry $cmdletName, $cmdletType, $null
+ $iss.Commands.Add($ssce)
+ }
+ }
- $ssce = New-Object System.Management.Automation.Runspaces.SessionStateCmdletEntry 'Test-Path', ([Microsoft.PowerShell.Commands.TestPathCommand]), $null
- $iss.Commands.Add($ssce)
+ # Add enhanced variable set including JSON manifest type
+ $scopedItemOptions = [System.Management.Automation.ScopedItemOptions]::AllScope
+ $plasterVars = Get-Variable -Name PLASTER_*, PSVersionTable
- $ssce = New-Object System.Management.Automation.Runspaces.SessionStateCmdletEntry 'Out-String', ([Microsoft.PowerShell.Commands.OutStringCommand]), $null
- $iss.Commands.Add($ssce)
+ # Add platform detection variables
+ if (Test-Path Variable:\IsLinux) { $plasterVars += Get-Variable -Name IsLinux }
+ if (Test-Path Variable:\IsOSX) { $plasterVars += Get-Variable -Name IsOSX }
+ if (Test-Path Variable:\IsMacOS) { $plasterVars += Get-Variable -Name IsMacOS }
+ if (Test-Path Variable:\IsWindows) { $plasterVars += Get-Variable -Name IsWindows }
- $ssce = New-Object System.Management.Automation.Runspaces.SessionStateCmdletEntry 'Compare-Object', ([Microsoft.PowerShell.Commands.CompareObjectCommand]), $null
- $iss.Commands.Add($ssce)
+ # Add manifest type variable (new for 2.0)
+ $manifestTypeVar = New-Object System.Management.Automation.PSVariable 'PLASTER_ManifestType', $manifestType, 'None'
+ $plasterVars += $manifestTypeVar
- $scopedItemOptions = [System.Management.Automation.ScopedItemOptions]::AllScope
- $plasterVars = Get-Variable -Name PLASTER_*, PSVersionTable
- if (Test-Path Variable:\IsLinux) {
- $plasterVars += Get-Variable -Name IsLinux
- }
- if (Test-Path Variable:\IsOSX) {
- $plasterVars += Get-Variable -Name IsOSX
- }
- if (Test-Path Variable:\IsMacOS) {
- $plasterVars += Get-Variable -Name IsMacOS
- }
- if (Test-Path Variable:\IsWindows) {
- $plasterVars += Get-Variable -Name IsWindows
- }
foreach ($var in $plasterVars) {
$ssve = New-Object System.Management.Automation.Runspaces.SessionStateVariableEntry `
$var.Name, $var.Value, $var.Description, $scopedItemOptions
$iss.Variables.Add($ssve)
}
- # Create new runspace with the above defined entries. Then open and set its working dir to $destinationAbsolutePath
- # so all condition attribute expressions can use a relative path to refer to file paths e.g.
- # condition="Test-Path src\${PLASTER_PARAM_ModuleName}.psm1"
$runspace = [System.Management.Automation.Runspaces.RunspaceFactory]::CreateRunspace($iss)
$runspace.Open()
if ($destinationAbsolutePath) {
$runspace.SessionStateProxy.Path.SetLocation($destinationAbsolutePath) > $null
}
- $runspace
+
+ Write-PlasterLog -Level Debug -Message "Created enhanced constrained runspace with $manifestType support"
+ return $runspace
}
diff --git a/Plaster/Private/New-JsonManifestStructure.ps1 b/Plaster/Private/New-JsonManifestStructure.ps1
new file mode 100644
index 0000000..f66209c
--- /dev/null
+++ b/Plaster/Private/New-JsonManifestStructure.ps1
@@ -0,0 +1,51 @@
+function New-JsonManifestStructure {
+ [CmdletBinding()]
+ [OutputType([hashtable])]
+ param(
+ [Parameter(Mandatory)]
+ [string]$TemplateName,
+
+ [Parameter(Mandatory)]
+ [string]$TemplateType,
+
+ [Parameter(Mandatory)]
+ [string]$Id,
+
+ [Parameter()]
+ [string]$TemplateVersion = "1.0.0",
+
+ [Parameter()]
+ [string]$Title = $TemplateName,
+
+ [Parameter()]
+ [string]$Description = "",
+
+ [Parameter()]
+ [string]$Author = "",
+
+ [Parameter()]
+ [string[]]$Tags = @()
+ )
+
+ $manifest = [ordered]@{
+ '$schema' = 'https://raw.githubusercontent.com/PowerShellOrg/Plaster/v2/schema/plaster-manifest-v2.json'
+ 'schemaVersion' = '2.0'
+ 'metadata' = [ordered]@{
+ 'name' = $TemplateName
+ 'id' = $Id
+ 'version' = $TemplateVersion
+ 'title' = $Title
+ 'description' = $Description
+ 'author' = $Author
+ 'templateType' = $TemplateType
+ }
+ 'parameters' = @()
+ 'content' = @()
+ }
+
+ if ($Tags.Count -gt 0) {
+ $manifest.metadata['tags'] = $Tags
+ }
+
+ return $manifest
+}
diff --git a/Plaster/Private/New-TemplateObjectFromManifest.ps1 b/Plaster/Private/New-TemplateObjectFromManifest.ps1
index d937318..5bed6dd 100644
--- a/Plaster/Private/New-TemplateObjectFromManifest.ps1
+++ b/Plaster/Private/New-TemplateObjectFromManifest.ps1
@@ -34,26 +34,47 @@ function New-TemplateObjectFromManifest {
[string]$Tag
)
- $manifestXml = Test-PlasterManifest -Path $ManifestPath
- $metadata = $manifestXml["plasterManifest"]["metadata"]
-
- $manifestObj = [PSCustomObject]@{
- Name = $metadata["name"].InnerText
- Title = $metadata["title"].InnerText
- Author = $metadata["author"].InnerText
- Version = [System.Version]::Parse($metadata["version"].InnerText)
- Description = $metadata["description"].InnerText
- Tags = $metadata["tags"].InnerText.split(",") | ForEach-Object { $_.Trim() }
- TemplatePath = $manifestPath.Directory.FullName
- }
+ try{
+ $manifestXml = Test-PlasterManifest -Path $ManifestPath
+ $metadata = $manifestXml["plasterManifest"]["metadata"]
+
+ $manifestObj = [PSCustomObject]@{
+ Name = [string]$metadata.name
+ Title = [string]$metadata.title
+ Author = [string]$metadata.author
+ Version = [System.Version]::Parse([string]$metadata.version)
+ Description = if ($metadata.description) { [string]$metadata.description } else { "" }
+ Tags = if ($metadata.tags) { ([string]$metadata.tags).split(",") | ForEach-Object { $_.Trim() } } else { @() }
+ TemplatePath = $manifestPath.Directory.FullName
+ Format = if ($manifestPath.Extension -eq '.json') { 'JSON' } else { 'XML' }
+ }
+
+ $manifestObj.PSTypeNames.Insert(0, "Microsoft.PowerShell.Plaster.PlasterTemplate")
+ $addMemberSplat = @{
+ MemberType = 'ScriptMethod'
+ InputObject = $manifestObj
+ Name = "InvokePlaster"
+ Value = { Invoke-Plaster -TemplatePath $this.TemplatePath }
+ }
+ Add-Member @addMemberSplat
- $manifestObj.PSTypeNames.Insert(0, "Microsoft.PowerShell.Plaster.PlasterTemplate")
- $addMemberSplat = @{
- MemberType = 'ScriptMethod'
- InputObject = $manifestObj
- Name = "InvokePlaster"
- Value = { Invoke-Plaster -TemplatePath $this.TemplatePath }
+ # Fix the filtering logic
+ $result = $manifestObj
+ if ($name -and $name -ne "*") {
+ $result = $result | Where-Object Name -like $name
+ }
+ if ($tag -and $tag -ne "*") {
+ # Only filter by tags if the template actually has tags
+ if ($result.Tags -and $result.Tags.Count -gt 0) {
+ $result = $result | Where-Object { $_.Tags -contains $tag -or ($_.Tags | Where-Object { $_ -like $tag }) }
+ } elseif ($tag -ne "*") {
+ # If template has no tags but we're filtering for a specific tag, exclude it
+ $result = $null
+ }
+ }
+ return $result
+ } catch {
+ Write-Debug "Failed to process manifest at $($manifestPath.FullName): $($_.Exception.Message)"
+ return $null
}
- Add-Member @addMemberSplat
- return $manifestObj | Where-Object Name -Like $Name | Where-Object Tags -Like $Tag
}
diff --git a/Plaster/Private/Resolve-AttributeValue.ps1 b/Plaster/Private/Resolve-AttributeValue.ps1
index 136df1a..934608f 100644
--- a/Plaster/Private/Resolve-AttributeValue.ps1
+++ b/Plaster/Private/Resolve-AttributeValue.ps1
@@ -12,6 +12,12 @@ function Resolve-AttributeValue {
}
try {
+ # Handle both XML-style ${PLASTER_PARAM_Name} and JSON-style ${Name} variables
+ if ($manifestType -eq 'JSON') {
+ # Convert JSON-style variables to XML-style for processing
+ $Value = $Value -replace '\$\{(?!PLASTER_)([A-Za-z][A-Za-z0-9_]*)\}', '${PLASTER_PARAM_$1}'
+ }
+
$res = @(Invoke-ExpressionImpl "`"$Value`"")
[string]$res[0]
} catch {
diff --git a/Plaster/Private/Resolve-ProcessParameter.ps1 b/Plaster/Private/Resolve-ProcessParameter.ps1
index e7ba7db..17acb9d 100644
--- a/Plaster/Private/Resolve-ProcessParameter.ps1
+++ b/Plaster/Private/Resolve-ProcessParameter.ps1
@@ -149,4 +149,5 @@ function Resolve-ProcessParameter {
# Make template defined parameters available as a PowerShell variable PLASTER_PARAM_.
Set-PlasterVariable -Name $name -Value $value -IsParam $true
+ Write-PlasterLog -Level Debug -Message "Set parameter variable: PLASTER_PARAM_$name = $value"
}
diff --git a/Plaster/Private/Test-JsonManifest.ps1 b/Plaster/Private/Test-JsonManifest.ps1
new file mode 100644
index 0000000..d6e1dd9
--- /dev/null
+++ b/Plaster/Private/Test-JsonManifest.ps1
@@ -0,0 +1,96 @@
+function Test-JsonManifest {
+ [CmdletBinding()]
+ [OutputType([bool])]
+ param(
+ [Parameter(Mandatory, ValueFromPipeline)]
+ [string]$JsonContent,
+
+ [Parameter()]
+ [string]$SchemaPath,
+
+ [Parameter()]
+ [switch]$Detailed
+ )
+
+ begin {
+ Write-PlasterLog -Level Debug -Message "Starting JSON manifest validation"
+
+ # Default schema path
+ if (-not $SchemaPath) {
+ $SchemaPath = Join-Path $PSScriptRoot "..\schema\plaster-manifest-v2.json"
+ }
+ }
+
+ process {
+ try {
+ # Parse JSON content
+ $jsonObject = $JsonContent | ConvertFrom-Json -ErrorAction Stop
+
+ # Basic structure validation
+ $requiredProperties = @('schemaVersion', 'metadata', 'content')
+ foreach ($property in $requiredProperties) {
+ if (-not $jsonObject.PSObject.Properties[$property]) {
+ throw "Missing required property: $property"
+ }
+ }
+
+ # Schema version validation
+ if ($jsonObject.schemaVersion -ne '2.0') {
+ throw "Unsupported schema version: $($jsonObject.schemaVersion). Expected: 2.0"
+ }
+
+ # Metadata validation
+ $metadata = $jsonObject.metadata
+ $requiredMetadata = @('name', 'id', 'version', 'title', 'author')
+ foreach ($property in $requiredMetadata) {
+ if (-not $metadata.PSObject.Properties[$property] -or [string]::IsNullOrWhiteSpace($metadata.$property)) {
+ throw "Missing or empty required metadata property: $property"
+ }
+ }
+
+ # Validate GUID format for ID
+ try {
+ [Guid]::Parse($metadata.id) | Out-Null
+ } catch {
+ throw "Invalid GUID format for metadata.id: $($metadata.id)"
+ }
+
+ # Validate semantic version format
+ if ($metadata.version -notmatch '^\d+\.\d+\.\d+([+-].*)?$') {
+ throw "Invalid version format: $($metadata.version). Expected semantic versioning (e.g., 1.0.0)"
+ }
+
+ # Validate template name pattern
+ if ($metadata.name -notmatch '^[A-Za-z][A-Za-z0-9_-]*$') {
+ throw "Invalid template name: $($metadata.name). Must start with letter and contain only letters, numbers, underscore, or hyphen"
+ }
+
+ # Parameters validation
+ # Parameters validation
+ if ($jsonObject.PSObject.Properties['parameters'] -and $jsonObject.parameters -and $jsonObject.parameters.Count -gt 0) {
+ Test-JsonManifestParameters -Parameters $jsonObject.parameters
+ }
+
+ # Content validation
+ # Content validation
+ # Content validation
+ if ($jsonObject.content -and $jsonObject.content.Count -gt 0) {
+ Test-JsonManifestContent -Content $jsonObject.content
+ } else {
+ throw "Content section cannot be empty"
+ }
+
+ Write-PlasterLog -Level Debug -Message "JSON manifest validation successful"
+ return $true
+ } catch {
+ $errorMessage = "JSON manifest validation failed: $($_.Exception.Message)"
+ Write-PlasterLog -Level Error -Message $errorMessage
+
+ if ($Detailed) {
+ throw $_
+ }
+
+ return $false
+ }
+ }
+}
diff --git a/Plaster/Private/Test-JsonManifestContent.ps1 b/Plaster/Private/Test-JsonManifestContent.ps1
new file mode 100644
index 0000000..9556e7f
--- /dev/null
+++ b/Plaster/Private/Test-JsonManifestContent.ps1
@@ -0,0 +1,83 @@
+function Test-JsonManifestContent {
+ [CmdletBinding()]
+ param(
+ [Parameter(Mandatory)]
+ [AllowEmptyCollection()]
+ [object[]]$Content
+ )
+
+ if ($Content.Count -eq 0) {
+ throw "Content section cannot be empty"
+ }
+
+ foreach ($action in $Content) {
+ if (-not $action.type) {
+ throw "Content action missing required 'type' property"
+ }
+
+ # Validate action type and required properties
+ switch ($action.type) {
+ 'message' {
+ if (-not $action.text) {
+ throw "Message action missing required 'text' property"
+ }
+ }
+ 'file' {
+ if (-not $action.source -or -not $action.destination) {
+ throw "File action missing required 'source' or 'destination' property"
+ }
+ }
+ 'templateFile' {
+ if (-not $action.source -or -not $action.destination) {
+ throw "TemplateFile action missing required 'source' or 'destination' property"
+ }
+ }
+ 'directory' {
+ if (-not $action.destination) {
+ throw "Directory action missing required 'destination' property"
+ }
+ }
+ 'newModuleManifest' {
+ if (-not $action.destination) {
+ throw "NewModuleManifest action missing required 'destination' property"
+ }
+ }
+ 'modify' {
+ if (-not $action.path -or -not $action.modifications) {
+ throw "Modify action missing required 'path' or 'modifications' property"
+ }
+
+ # Validate modifications
+ foreach ($modification in $action.modifications) {
+ if (-not $modification.type) {
+ throw "Modification missing required 'type' property"
+ }
+
+ if ($modification.type -eq 'replace') {
+ if (-not $modification.PSObject.Properties['search'] -or -not $modification.PSObject.Properties['replace']) {
+ throw "Replace modification missing required 'search' or 'replace' property"
+ }
+ }
+ }
+ }
+ 'requireModule' {
+ if (-not $action.name) {
+ throw "RequireModule action missing required 'name' property"
+ }
+ }
+ 'execute' {
+ if (-not $action.script) {
+ throw "Execute action missing required 'script' property"
+ }
+ }
+ default {
+ throw "Unknown content action type: $($action.type)"
+ }
+ }
+
+ # Validate condition if present
+ if ($action.condition) {
+ Test-PlasterCondition -Condition $action.condition -Context "Content action ($($action.type))"
+ }
+ }
+}
diff --git a/Plaster/Private/Test-JsonManifestParameters.ps1 b/Plaster/Private/Test-JsonManifestParameters.ps1
new file mode 100644
index 0000000..34e8b63
--- /dev/null
+++ b/Plaster/Private/Test-JsonManifestParameters.ps1
@@ -0,0 +1,63 @@
+function Test-JsonManifestParameters {
+ [CmdletBinding()]
+ param(
+ [Parameter(Mandatory)]
+ [AllowEmptyCollection()]
+ [object[]]$Parameters
+ )
+
+ $parameterNames = @()
+
+ foreach ($param in $Parameters) {
+ # Required properties
+ if (-not $param.name -or -not $param.type) {
+ throw "Parameter missing required 'name' or 'type' property"
+ }
+
+ # Validate parameter name pattern
+ if ($param.name -notmatch '^[A-Za-z][A-Za-z0-9_]*$') {
+ throw "Invalid parameter name: $($param.name). Must start with letter and contain only letters, numbers, or underscore"
+ }
+
+ # Check for duplicate parameter names
+ if ($param.name -in $parameterNames) {
+ throw "Duplicate parameter name: $($param.name)"
+ }
+ $parameterNames += $param.name
+
+ # Validate parameter type
+ $validTypes = @('text', 'user-fullname', 'user-email', 'choice', 'multichoice', 'switch')
+ if ($param.type -notin $validTypes) {
+ throw "Invalid parameter type: $($param.type). Valid types: $($validTypes -join ', ')"
+ }
+
+ # Choice parameters must have choices
+ if ($param.type -in @('choice', 'multichoice') -and -not $param.choices) {
+ throw "Parameter '$($param.name)' of type '$($param.type)' must have 'choices' property"
+ }
+
+ # Validate choices if present
+ if ($param.choices) {
+ foreach ($choice in $param.choices) {
+ if (-not $choice.label -or -not $choice.value) {
+ throw "Choice in parameter '$($param.name)' missing required 'label' or 'value' property"
+ }
+ }
+ }
+
+ # Validate dependsOn references
+ if ($param.dependsOn) {
+ foreach ($dependency in $param.dependsOn) {
+ if ($dependency -notin $parameterNames -and $dependency -ne $param.name) {
+ # Note: We'll validate this after processing all parameters
+ Write-PlasterLog -Level Debug -Message "Parameter '$($param.name)' depends on '$dependency'"
+ }
+ }
+ }
+
+ # Validate condition syntax if present
+ if ($param.condition) {
+ Test-PlasterCondition -Condition $param.condition -ParameterName $param.name
+ }
+ }
+}
diff --git a/Plaster/Private/Test-PlasterCondition.ps1 b/Plaster/Private/Test-PlasterCondition.ps1
new file mode 100644
index 0000000..2e85ae8
--- /dev/null
+++ b/Plaster/Private/Test-PlasterCondition.ps1
@@ -0,0 +1,34 @@
+function Test-PlasterCondition {
+ [CmdletBinding()]
+ param(
+ [Parameter(Mandatory)]
+ [string]$Condition,
+
+ [Parameter()]
+ [string]$ParameterName,
+
+ [Parameter()]
+ [string]$Context = 'condition'
+ )
+
+ try {
+ # Basic syntax validation - ensure it's valid PowerShell
+ $tokens = $errors = $null
+ $null = [System.Management.Automation.Language.Parser]::ParseInput($Condition, [ref]$tokens, [ref]$errors)
+
+ if ($errors.Count -gt 0) {
+ $errorMsg = if ($ParameterName) {
+ "Invalid condition in parameter '$ParameterName': $($errors[0].Message)"
+ } else {
+ "Invalid condition in ${Context}: $($errors[0].Message)"
+ }
+ throw $errorMsg
+ }
+
+ Write-PlasterLog -Level Debug -Message "Condition validation passed: $Condition"
+ return $true
+ } catch {
+ Write-PlasterLog -Level Error -Message "Condition validation failed: $($_.Exception.Message)"
+ throw $_
+ }
+}
diff --git a/Plaster/Private/Write-PlasterLog.ps1 b/Plaster/Private/Write-PlasterLog.ps1
index b5538d3..39d337c 100644
--- a/Plaster/Private/Write-PlasterLog.ps1
+++ b/Plaster/Private/Write-PlasterLog.ps1
@@ -28,7 +28,6 @@ function Write-PlasterLog {
consistent logging across various operations.
It is not intended for direct use outside of the Plaster context.
#>
- [CmdletBinding()]
param(
[Parameter(Mandatory)]
[ValidateSet('Error', 'Warning', 'Information', 'Verbose', 'Debug')]
@@ -37,17 +36,56 @@ function Write-PlasterLog {
[Parameter(Mandatory)]
[string]$Message,
+ [Parameter()]
[string]$Source = 'Plaster'
)
+ # Check if we should log at this level
+ $logLevels = @{
+ 'Error' = 0
+ 'Warning' = 1
+ 'Information' = 2
+ 'Verbose' = 3
+ 'Debug' = 4
+ }
+
+ $currentLogLevel = $script:LogLevel ?? 'Information'
+ $currentLevelValue = $logLevels[$currentLogLevel] ?? 2
+ $messageLevelValue = $logLevels[$Level] ?? 2
+
+ if ($messageLevelValue -gt $currentLevelValue) {
+ return
+ }
+
$timestamp = Get-Date -Format 'yyyy-MM-dd HH:mm:ss'
$logMessage = "[$timestamp] [$Level] [$Source] $Message"
+ # Handle different log levels appropriately
switch ($Level) {
- 'Error' { Write-Error $logMessage }
- 'Warning' { Write-Warning $logMessage }
- 'Information' { Write-Information $logMessage }
- 'Verbose' { Write-Verbose $logMessage }
- 'Debug' { Write-Debug $logMessage }
+ 'Error' {
+ Write-Error $logMessage -ErrorAction Continue
+ }
+ 'Warning' {
+ Write-Warning $logMessage
+ }
+ 'Information' {
+ Write-Information $logMessage -InformationAction Continue
+ }
+ 'Verbose' {
+ Write-Verbose $logMessage
+ }
+ 'Debug' {
+ Write-Debug $logMessage
+ }
+ }
+
+ # Also write to host for immediate feedback during interactive sessions
+ if ($Level -in @('Error', 'Warning') -and $Host.Name -ne 'ServerRemoteHost') {
+ $color = switch ($Level) {
+ 'Error' { 'Red' }
+ 'Warning' { 'Yellow' }
+ default { 'White' }
+ }
+ Write-Host $logMessage -ForegroundColor $color
}
}
diff --git a/Plaster/Public/Get-PlasterTemplate.ps1 b/Plaster/Public/Get-PlasterTemplate.ps1
index c1f5ca9..00d4466 100644
--- a/Plaster/Public/Get-PlasterTemplate.ps1
+++ b/Plaster/Public/Get-PlasterTemplate.ps1
@@ -47,13 +47,21 @@ function Get-PlasterTemplate {
process {
if ($Path) {
- # Is this a folder path or a Plaster manifest file path?
if (!$Recurse.IsPresent) {
if (Test-Path $Path -PathType Container) {
- $Path = Resolve-Path "$Path/plasterManifest.xml"
+ # Check for JSON first, then XML
+ $jsonPath = Join-Path $Path "plasterManifest.json"
+ $xmlPath = Join-Path $Path "plasterManifest.xml"
+
+ if (Test-Path $jsonPath) {
+ $Path = $jsonPath
+ } elseif (Test-Path $xmlPath) {
+ $Path = $xmlPath
+ } else {
+ $Path = Resolve-Path "$Path/plasterManifest.*" -ErrorAction SilentlyContinue | Select-Object -First 1
+ }
}
- # Use Test-PlasterManifest to load the manifest file
Write-Verbose "Attempting to get Plaster template at path: $Path"
$newTemplateObjectFromManifestSplat = @{
ManifestPath = $Path
@@ -93,18 +101,27 @@ function Get-PlasterTemplate {
foreach ($extension in $extensions) {
# Scan all module paths registered in the module
foreach ($templatePath in $extension.Details.TemplatePaths) {
- $expandedTemplatePath =
- [System.IO.Path]::Combine(
+ # Check for both JSON and XML manifests
+ $jsonManifestPath = [System.IO.Path]::Combine(
+ $extension.Module.ModuleBase,
+ $templatePath,
+ "plasterManifest.json")
+
+ $xmlManifestPath = [System.IO.Path]::Combine(
$extension.Module.ModuleBase,
$templatePath,
"plasterManifest.xml")
$newTemplateObjectFromManifestSplat = @{
- ManifestPath = $expandedTemplatePath
Name = $Name
Tag = $Tag
ErrorAction = 'SilentlyContinue'
}
+ if (Test-Path $jsonManifestPath) {
+ $newTemplateObjectFromManifestSplat.ManifestPath = $jsonManifestPath
+ } elseif (Test-Path $xmlManifestPath) {
+ $newTemplateObjectFromManifestSplat.ManifestPath = $xmlManifestPath
+ }
New-TemplateObjectFromManifest @newTemplateObjectFromManifestSplat
}
}
diff --git a/Plaster/Public/Invoke-Plaster.ps1 b/Plaster/Public/Invoke-Plaster.ps1
index 5463b7a..a78b999 100644
--- a/Plaster/Public/Invoke-Plaster.ps1
+++ b/Plaster/Public/Invoke-Plaster.ps1
@@ -15,13 +15,18 @@
## 4. Please follow the scripting style of this file when adding new script.
function Invoke-Plaster {
- [CmdletBinding(SupportsShouldProcess = $true)]
+ [CmdletBinding(DefaultParameterSetName = 'TemplatePath', SupportsShouldProcess = $true)]
param(
- [Parameter(Position = 0, Mandatory = $true)]
+ [Parameter(Position = 0, Mandatory = $true, ParameterSetName = 'TemplatePath')]
[ValidateNotNullOrEmpty()]
[string]
$TemplatePath,
+ [Parameter(Position = 0, Mandatory = $true, ParameterSetName = 'TemplateDefinition')]
+ [ValidateNotNullOrEmpty()]
+ [string]
+ $TemplateDefinition,
+
[Parameter(Position = 1, Mandatory = $true)]
[ValidateNotNullOrEmpty()]
[string]
@@ -35,7 +40,7 @@ function Invoke-Plaster {
[switch]
$NoLogo,
- [Parameter()]
+ # Enhanced dynamic parameter processing for both XML and JSON
[switch]
$PassThru
)
@@ -58,22 +63,49 @@ function Invoke-Plaster {
# catch and format the error message as a warning.
$ErrorActionPreference = 'Stop'
- # The constrained runspace is not available in the dynamicparam block. Shouldn't be needed
- # since we are only evaluating the parameters in the manifest - no need for Test-ConditionAttribute as we
- # are not building up multiple parametersets. And no need for EvaluateAttributeValue since we are only
- # grabbing the parameter's value which is static.
+ <# The constrained runspace is not available in the dynamicparam
+ block. Shouldn't be needed since we are only evaluating the
+ parameters in the manifest - no need for Test-ConditionAttribute as
+ we are not building up multiple parametersets. And no need for
+ EvaluateAttributeValue since we are only grabbing the parameter's
+ value which is static.#>
+
$templateAbsolutePath = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($TemplatePath)
- if (!(Test-Path -LiteralPath $templateAbsolutePath -PathType Container)) {
- throw ($LocalizedData.ErrorTemplatePathIsInvalid_F1 -f $templateAbsolutePath)
- }
- # Load manifest file using culture lookup
- $manifestPath = Get-PlasterManifestPathForCulture $templateAbsolutePath $PSCulture
+ # Load manifest file using culture lookup - try both JSON and XML formats
+ $manifestPath = Get-PlasterManifestPathForCulture -TemplatePath $templateAbsolutePath -Culture $PSCulture
+
+ # If XML not found, try JSON
if (($null -eq $manifestPath) -or (!(Test-Path $manifestPath))) {
+ $jsonManifestPath = Join-Path $templateAbsolutePath 'plasterManifest.json'
+ if (Test-Path $jsonManifestPath) {
+ $manifestPath = $jsonManifestPath
+ }
+ }
+
+ # Determine manifest type and process accordingly
+ try {
+ $manifestType = Get-PlasterManifestType -ManifestPath $manifestPath
+ Write-Debug "Detected manifest type: $manifestType for path: $manifestPath"
+ } catch {
+ Write-Warning "Failed to determine manifest type for '$manifestPath': $($_.Exception.Message)"
return
}
- $manifest = Test-PlasterManifest -Path $manifestPath -ErrorAction Stop 3>$null
+ #Process JSON manifests
+ if ($manifestType -eq 'JSON') {
+ try {
+ $jsonContent = Get-Content -LiteralPath $manifestPath -Raw -ErrorAction Stop
+ $manifest = ConvertFrom-JsonManifest -JsonContent $jsonContent -ErrorAction Stop
+ Write-Debug "Successfully converted JSON manifest to XML for processing"
+ } catch {
+ Write-Warning "Failed to process JSON manifest '$manifestPath': $($_.Exception.Message)"
+ return
+ }
+ } else {
+ # Process XML manifests (existing logic)
+ $manifest = Test-PlasterManifest -Path $manifestPath -ErrorAction Stop 3>$null
+ }
# The user-defined parameters in the Plaster manifest are converted to dynamic parameters
# which allows the user to provide the parameters via the command line.
@@ -132,20 +164,20 @@ function Invoke-Plaster {
}
begin {
- # Write out the Plaster logo if necessary
+ # Enhanced logo with JSON support indicator
$plasterLogo = @'
- ____ _ _
- | _ \| | __ _ ___| |_ ___ _ __
- | |_) | |/ _` / __| __/ _ \ '__|
- | __/| | (_| \__ \ || __/ |
- |_| |_|\__,_|___/\__\___|_|
+ ____ _ _ ____ ___
+ | _ \| | __ _ ___| |_ ___ _ __ |___ \ / _ \
+ | |_) | |/ _` / __| __/ _ \ '__| __) | | | |
+ | __/| | (_| \__ \ || __/ | / /| |_| | /
+ |_| |_|\__,_|___/\__\___|_| |____|\___/
'@
if (!$NoLogo) {
- $versionString = "v$PlasterVersion"
- Write-Host $plasterLogo
- Write-Host ((" " * (50 - $versionString.Length)) + $versionString)
- Write-Host ("=" * 50)
+ $versionString = "v$PlasterVersion (JSON Enhanced)"
+ Write-Host $plasterLogo -ForegroundColor Blue
+ Write-Host ((" " * (50 - $versionString.Length)) + $versionString) -ForegroundColor Cyan
+ Write-Host ("=" * 50) -ForegroundColor Blue
}
#region Script Scope Variables
@@ -161,51 +193,97 @@ function Invoke-Plaster {
}
#endregion Script Scope Variables
- # Verify TemplatePath parameter value is valid.
- $templateAbsolutePath = $PSCmdlet.GetUnresolvedProviderPathFromPSPath($TemplatePath)
- if (!(Test-Path -LiteralPath $templateAbsolutePath -PathType Container)) {
- throw ($LocalizedData.ErrorTemplatePathIsInvalid_F1 -f $templateAbsolutePath)
+ # Determine template source and type
+ if ($PSCmdlet.ParameterSetName -eq 'TemplatePath') {
+ $templateAbsolutePath = $PSCmdlet.GetUnresolvedProviderPathFromPSPath($TemplatePath)
+ if (!(Test-Path -LiteralPath $templateAbsolutePath -PathType Container)) {
+ throw ($LocalizedData.ErrorTemplatePathIsInvalid_F1 -f $templateAbsolutePath)
+ }
+
+ # Determine manifest type and path
+ $jsonManifestPath = Join-Path $templateAbsolutePath 'plasterManifest.json'
+ $xmlManifestPath = Get-PlasterManifestPathForCulture $templateAbsolutePath $PSCulture
+
+ if (Test-Path -LiteralPath $jsonManifestPath) {
+ $manifestPath = $jsonManifestPath
+ $manifestType = 'JSON'
+ Write-PlasterLog -Level Information -Message "Using JSON manifest: $($manifestPath | Split-Path -Leaf)"
+ } elseif (($null -ne $xmlManifestPath) -and (Test-Path $xmlManifestPath)) {
+ $manifestPath = $xmlManifestPath
+ $manifestType = 'XML'
+ Write-PlasterLog -Level Information -Message "Using XML manifest: $($manifestPath | Split-Path -Leaf)"
+ } else {
+ throw ($LocalizedData.ManifestFileMissing_F1 -f "plasterManifest.json or plasterManifest.xml")
+ }
+
+ } else {
+ # TemplateDefinition parameter set
+ $manifestType = if ($TemplateDefinition.TrimStart() -match '^[\s]*[\{\[]') { 'JSON' } else { 'XML' }
+ $templateAbsolutePath = $PSCmdlet.GetUnresolvedProviderPathFromPSPath($DestinationPath)
+ Write-PlasterLog -Level Information -Message "Using inline $manifestType template definition"
}
- # We will have a null manifest if the dynamicparam scriptblock was unable to load the template manifest
- # or it wasn't valid. If so, let's try to load it here. If anything, we can provide better errors here.
+ # Process manifest based on type
+
if ($null -eq $manifest) {
- if ($null -eq $manifestPath) {
- $manifestPath = Get-PlasterManifestPathForCulture $templateAbsolutePath $PSCulture
- }
+ if ($manifestType -eq 'JSON') {
+ $manifestContent = if ($manifestPath) {
+ Get-Content -LiteralPath $manifestPath -Raw
+ } else {
+ $TemplateDefinition
+ }
+
+ # Validate and convert JSON manifest
+ $isValid = Test-JsonManifest -JsonContent $manifestContent -Detailed
+ if (-not $isValid) {
+ throw "JSON manifest validation failed"
+ }
+
+ $manifest = ConvertFrom-JsonManifest -JsonContent $manifestContent
+ Write-PlasterLog -Level Debug -Message "JSON manifest converted to internal format"
- if (Test-Path -LiteralPath $manifestPath -PathType Leaf) {
- $manifest = Test-PlasterManifest -Path $manifestPath -ErrorAction Stop 3>$null
- $PSCmdlet.WriteDebug("In begin, loading manifest file '$manifestPath'")
} else {
- throw ($LocalizedData.ManifestFileMissing_F1 -f $manifestPath)
+ # Load XML manifest
+ if ($manifestPath -and (Test-Path -LiteralPath $manifestPath -PathType Leaf)) {
+ $manifest = Test-PlasterManifest -Path $manifestPath -ErrorAction Stop 3>$null
+ $PSCmdlet.WriteDebug("Loading XML manifest file '$manifestPath'")
+ } else {
+ throw ($LocalizedData.ManifestFileMissing_F1 -f $manifestPath)
+ }
}
}
- # If the destination path doesn't exist, create it.
+ # Validate destination path
$destinationAbsolutePath = $PSCmdlet.GetUnresolvedProviderPathFromPSPath($DestinationPath)
if (!(Test-Path -LiteralPath $destinationAbsolutePath)) {
New-Item $destinationAbsolutePath -ItemType Directory > $null
+ Write-PlasterLog -Level Information -Message "Created destination directory: $destinationAbsolutePath"
}
# Prepare output object if user has specified the -PassThru parameter.
if ($PassThru) {
$InvokePlasterInfo = [PSCustomObject]@{
- TemplatePath = $templateAbsolutePath
+ TemplatePath = if ($templateAbsolutePath) { $templateAbsolutePath } else { 'Inline Definition' }
DestinationPath = $destinationAbsolutePath
+ ManifestType = $manifestType
Success = $false
TemplateType = if ($manifest.plasterManifest.templateType) { $manifest.plasterManifest.templateType } else { 'Unspecified' }
CreatedFiles = [string[]]@()
UpdatedFiles = [string[]]@()
MissingModules = [string[]]@()
OpenFiles = [string[]]@()
+ ProcessingTime = $null
}
}
- # Create the pre-defined Plaster variables.
- Initialize-PredefinedVariables -TemplatePath $templateAbsolutePath -DestPath $destinationAbsolutePath
+ # Initialize pre-defined variables
+ if ($templateAbsolutePath) {
+ Initialize-PredefinedVariables -TemplatePath $templateAbsolutePath -DestPath $destinationAbsolutePath
+ } else {
+ Initialize-PredefinedVariables -TemplatePath $destinationAbsolutePath -DestPath $destinationAbsolutePath
+ }
- # Check for any existing default value store file and load default values if file exists.
+ # Enhanced default value store handling
$templateId = $manifest.plasterManifest.metadata.id
$templateVersion = $manifest.plasterManifest.metadata.version
$templateName = $manifest.plasterManifest.metadata.name
@@ -215,6 +293,7 @@ function Invoke-Plaster {
try {
$PSCmdlet.WriteDebug("Loading default value store from '$script:defaultValueStorePath'.")
$script:defaultValueStore = Import-Clixml $script:defaultValueStorePath -ErrorAction Stop
+ Write-PlasterLog -Level Debug -Message "Loaded parameter defaults from store"
} catch {
Write-Warning ($LocalizedData.ErrorFailedToLoadStoreFile_F1 -f $script:defaultValueStorePath)
}
@@ -222,8 +301,12 @@ function Invoke-Plaster {
}
end {
+ $stopwatch = [System.Diagnostics.Stopwatch]::StartNew()
+
try {
- # Process parameters
+ Write-PlasterLog -Level Information -Message "Starting template processing ($manifestType format)"
+
+ # Process parameters with enhanced JSON support
foreach ($node in $manifest.plasterManifest.parameters.ChildNodes) {
if ($node -isnot [System.Xml.XmlElement]) { continue }
switch ($node.LocalName) {
@@ -232,7 +315,7 @@ function Invoke-Plaster {
}
}
- # Outputs the processed template parameters to the debug stream
+ # Output processed parameters for debugging
$parameters = Get-Variable -Name PLASTER_* | Out-String
$PSCmdlet.WriteDebug("Parameter values are:`n$($parameters -split "`n")")
@@ -248,13 +331,14 @@ function Invoke-Plaster {
$script:defaultValueStore | Export-Clixml -LiteralPath $script:defaultValueStorePath
}
- # Output the DestinationPath
+ # Output destination path
Write-Host ($LocalizedData.DestPath_F1 -f $destinationAbsolutePath)
- # Process content
+ # Process content with enhanced logging
foreach ($node in $manifest.plasterManifest.content.ChildNodes) {
if ($node -isnot [System.Xml.XmlElement]) { continue }
+ Write-PlasterLog -Level Debug -Message "Processing content action: $($node.LocalName)"
switch -Regex ($node.LocalName) {
'file|templateFile' { Start-ProcessFile $node; break }
'message' { Resolve-ProcessMessage $node; break }
@@ -264,16 +348,34 @@ function Invoke-Plaster {
default { throw ($LocalizedData.UnrecognizedContentElement_F1 -f $node.LocalName) }
}
}
+ $stopwatch.Stop()
if ($PassThru) {
$InvokePlasterInfo.Success = $true
- $InvokePlasterInfo
+ $InvokePlasterInfo.ProcessingTime = $stopwatch.Elapsed
+ Write-PlasterLog -Level Information -Message "Template processing completed successfully in $($stopwatch.Elapsed.TotalSeconds) seconds"
+ return $InvokePlasterInfo
+ } else {
+ Write-PlasterLog -Level Information -Message "Template processing completed successfully in $($stopwatch.Elapsed.TotalSeconds) seconds"
}
+ } catch {
+ $stopwatch.Stop()
+ $errorMessage = "Template processing failed after $($stopwatch.Elapsed.TotalSeconds) seconds: $($_.Exception.Message)"
+ Write-PlasterLog -Level Error -Message $errorMessage
+
+ if ($PassThru) {
+ $InvokePlasterInfo.Success = $false
+ $InvokePlasterInfo.ProcessingTime = $stopwatch.Elapsed
+ return $InvokePlasterInfo
+ }
+
+ throw $_
} finally {
- # Dispose of the ConstrainedRunspace.
+ # Enhanced cleanup
if ($script:constrainedRunspace) {
$script:constrainedRunspace.Dispose()
$script:constrainedRunspace = $null
+ Write-PlasterLog -Level Debug -Message "Disposed constrained runspace"
}
}
}
diff --git a/Plaster/Public/New-PlasterManifest.ps1 b/Plaster/Public/New-PlasterManifest.ps1
index 9494adf..5fd11a3 100644
--- a/Plaster/Public/New-PlasterManifest.ps1
+++ b/Plaster/Public/New-PlasterManifest.ps1
@@ -1,18 +1,18 @@
function New-PlasterManifest {
- [CmdletBinding(SupportsShouldProcess=$true)]
+ [CmdletBinding(SupportsShouldProcess = $true)]
param(
[Parameter()]
[ValidateNotNullOrEmpty()]
[string]
- $Path = "$pwd\plasterManifest.xml",
+ $Path,
- [Parameter(Mandatory=$true)]
+ [Parameter(Mandatory = $true)]
[ValidatePattern('^[0-9a-zA-Z_-]+$')]
[string]
$TemplateName,
- [Parameter(Mandatory=$true)]
- [ValidateSet('Item','Project')]
+ [Parameter(Mandatory = $true)]
+ [ValidateSet('Item', 'Project')]
[string]
$TemplateType,
@@ -47,10 +47,24 @@ function New-PlasterManifest {
[Parameter()]
[switch]
- $AddContent
+ $AddContent,
+
+ [Parameter()]
+ [ValidateSet('XML', 'JSON')]
+ [string]
+ $Format = 'JSON',
+
+ [Parameter()]
+ [switch]
+ $ConvertFromXml
)
begin {
+ # Set default path based on format if not provided
+ if (-not $PSBoundParameters.ContainsKey('Path')) {
+ $Path = if ($Format -eq 'JSON') { "$pwd\plasterManifest.json" } else { "$pwd\plasterManifest.xml" }
+ }
+
$resolvedPath = $PSCmdLet.GetUnresolvedProviderPathFromPSPath($Path)
$caseCorrectedTemplateType = [System.Char]::ToUpper($TemplateType[0]) + $TemplateType.Substring(1).ToLower()
@@ -79,55 +93,100 @@ function New-PlasterManifest {
}
end {
- $manifest = [xml]$manifestStr
-
- # Set via .innerText to get .NET to encode special XML chars as entity references.
- $manifest.plasterManifest.metadata["name"].innerText = "$TemplateName"
- $manifest.plasterManifest.metadata["id"].innerText = "$Id"
- $manifest.plasterManifest.metadata["version"].innerText = "$TemplateVersion"
- $manifest.plasterManifest.metadata["title"].innerText = "$Title"
- $manifest.plasterManifest.metadata["description"].innerText = "$Description"
- $manifest.plasterManifest.metadata["author"].innerText = "$Author"
-
- $OFS = ", "
- $manifest.plasterManifest.metadata["tags"].innerText = "$Tags"
-
- if ($AddContent) {
- $baseDir = Split-Path $Path -Parent
- $filenames = Get-ChildItem $baseDir -Recurse -File -Name
- foreach ($filename in $filenames) {
- if ($filename -match "plasterManifest.*\.xml") {
- continue
+ if ($Format -eq 'JSON') {
+ # Create JSON manifest
+ $jsonManifest = [ordered]@{
+ '$schema' = 'https://raw.githubusercontent.com/PowerShellOrg/Plaster/v2/schema/plaster-manifest-v2.json'
+ 'schemaVersion' = '2.0'
+ 'metadata' = [ordered]@{
+ 'name' = $TemplateName
+ 'id' = $Id.ToString()
+ 'version' = $TemplateVersion
+ 'title' = $Title
+ 'description' = $Description
+ 'author' = $Author
+ 'templateType' = $caseCorrectedTemplateType
}
+ 'parameters' = @()
+ 'content' = @()
+ }
- $fileElem = $manifest.CreateElement('file', $TargetNamespace)
-
- $srcAttr = $manifest.CreateAttribute("source")
- $srcAttr.Value = $filename
- $fileElem.Attributes.Append($srcAttr) > $null
+ if ($Tags) {
+ $jsonManifest.metadata['tags'] = $Tags
+ }
- $dstAttr = $manifest.CreateAttribute("destination")
- $dstAttr.Value = $filename
- $fileElem.Attributes.Append($dstAttr) > $null
+ if ($AddContent) {
+ $baseDir = Split-Path $resolvedPath -Parent
+ $filenames = Get-ChildItem $baseDir -Recurse -File -Name
+ foreach ($filename in $filenames) {
+ if ($filename -match "plasterManifest.*\.(xml|json)") {
+ continue
+ }
+
+ $fileAction = [ordered]@{
+ 'type' = 'file'
+ 'source' = $filename
+ 'destination' = $filename
+ }
+ $jsonManifest.content += $fileAction
+ }
+ }
- $manifest.plasterManifest["content"].AppendChild($fileElem) > $null
+ $jsonContent = $jsonManifest | ConvertTo-Json -Depth 10
+ if ($PSCmdlet.ShouldProcess($resolvedPath, $LocalizedData.ShouldCreateNewPlasterManifest)) {
+ Set-Content -Path $resolvedPath -Value $jsonContent -Encoding UTF8
}
- }
- # This configures the XmlWriter to put attributes on a new line
- $xmlWriterSettings = New-Object System.Xml.XmlWriterSettings
- $xmlWriterSettings.Indent = $true
- $xmlWriterSettings.NewLineOnAttributes = $true
+ } else {
+ $manifest = [xml]$manifestStr
- try {
- if ($PSCmdlet.ShouldProcess($resolvedPath, $LocalizedData.ShouldCreateNewPlasterManifest)) {
- $xmlWriter = [System.Xml.XmlWriter]::Create($resolvedPath, $xmlWriterSettings)
- $manifest.Save($xmlWriter)
+ # Set via .innerText to get .NET to encode special XML chars as entity references.
+ $manifest.plasterManifest.metadata["name"].innerText = "$TemplateName"
+ $manifest.plasterManifest.metadata["id"].innerText = "$Id"
+ $manifest.plasterManifest.metadata["version"].innerText = "$TemplateVersion"
+ $manifest.plasterManifest.metadata["title"].innerText = "$Title"
+ $manifest.plasterManifest.metadata["description"].innerText = "$Description"
+ $manifest.plasterManifest.metadata["author"].innerText = "$Author"
+
+ $OFS = ", "
+ $manifest.plasterManifest.metadata["tags"].innerText = "$Tags"
+
+ if ($AddContent) {
+ $baseDir = Split-Path $Path -Parent
+ $filenames = Get-ChildItem $baseDir -Recurse -File -Name
+ foreach ($filename in $filenames) {
+ if ($filename -match "plasterManifest.*\.xml") {
+ continue
+ }
+
+ $fileElem = $manifest.CreateElement('file', $TargetNamespace)
+
+ $srcAttr = $manifest.CreateAttribute("source")
+ $srcAttr.Value = $filename
+ $fileElem.Attributes.Append($srcAttr) > $null
+
+ $dstAttr = $manifest.CreateAttribute("destination")
+ $dstAttr.Value = $filename
+ $fileElem.Attributes.Append($dstAttr) > $null
+
+ $manifest.plasterManifest["content"].AppendChild($fileElem) > $null
+ }
}
- }
- finally {
- if ($xmlWriter) {
- $xmlWriter.Dispose()
+
+ # This configures the XmlWriter to put attributes on a new line
+ $xmlWriterSettings = New-Object System.Xml.XmlWriterSettings
+ $xmlWriterSettings.Indent = $true
+ $xmlWriterSettings.NewLineOnAttributes = $true
+
+ try {
+ if ($PSCmdlet.ShouldProcess($resolvedPath, $LocalizedData.ShouldCreateNewPlasterManifest)) {
+ $xmlWriter = [System.Xml.XmlWriter]::Create($resolvedPath, $xmlWriterSettings)
+ $manifest.Save($xmlWriter)
+ }
+ } finally {
+ if ($xmlWriter) {
+ $xmlWriter.Dispose()
+ }
}
}
}
diff --git a/Plaster/Public/Test-PlasterManifest.ps1 b/Plaster/Public/Test-PlasterManifest.ps1
index 5b58795..4b64452 100644
--- a/Plaster/Public/Test-PlasterManifest.ps1
+++ b/Plaster/Public/Test-PlasterManifest.ps1
@@ -2,11 +2,11 @@ function Test-PlasterManifest {
[CmdletBinding()]
[OutputType([System.Xml.XmlDocument])]
param(
- [Parameter(Position=0,
- ParameterSetName="Path",
- ValueFromPipeline=$true,
- ValueFromPipelineByPropertyName=$true,
- HelpMessage="Specifies a path to a plasterManifest.xml file.")]
+ [Parameter(Position = 0,
+ ParameterSetName = "Path",
+ ValueFromPipeline = $true,
+ ValueFromPipelineByPropertyName = $true,
+ HelpMessage = "Specifies a path to a plasterManifest.xml or plasterManifest.json file.")]
[Alias("PSPath")]
[ValidateNotNullOrEmpty()]
[string[]]
@@ -20,8 +20,7 @@ function Test-PlasterManifest {
if ('System.Xml.Schema.XmlSchemaSet' -as [type]) {
$xmlSchemaSet = New-Object System.Xml.Schema.XmlSchemaSet
$xmlSchemaSet.Add($TargetNamespace, $schemaPath) > $null
- }
- else {
+ } else {
$PSCmdLet.WriteWarning($LocalizedData.TestPlasterNoXmlSchemaValidationWarning)
}
}
@@ -33,7 +32,7 @@ function Test-PlasterManifest {
if (!(Test-Path -LiteralPath $aPath)) {
$ex = New-Object System.Management.Automation.ItemNotFoundException ($LocalizedData.ErrorPathDoesNotExist_F1 -f $aPath)
$category = [System.Management.Automation.ErrorCategory]::ObjectNotFound
- $errRecord = New-Object System.Management.Automation.ErrorRecord $ex,'PathNotFound',$category,$aPath
+ $errRecord = New-Object System.Management.Automation.ErrorRecord $ex, 'PathNotFound', $category, $aPath
$PSCmdLet.WriteError($errRecord)
return
}
@@ -41,20 +40,58 @@ function Test-PlasterManifest {
$filename = Split-Path $aPath -Leaf
# Verify the manifest has the correct filename. Allow for localized template manifest files as well.
- if (!(($filename -eq 'plasterManifest.xml') -or ($filename -match 'plasterManifest_[a-zA-Z]+(-[a-zA-Z]+){0,2}.xml'))) {
+ $isXmlManifest = ($filename -eq 'plasterManifest.xml') -or ($filename -match 'plasterManifest_[a-zA-Z]+(-[a-zA-Z]+){0,2}.xml')
+ $isJsonManifest = ($filename -eq 'plasterManifest.json') -or ($filename -match 'plasterManifest_[a-zA-Z]+(-[a-zA-Z]+){0,2}.json')
+
+ if (!$isXmlManifest -and !$isJsonManifest) {
Write-Error ($LocalizedData.ManifestWrongFilename_F1 -f $filename)
return
}
+ # Detect manifest format and process accordingly
+ try {
+ $manifestType = Get-PlasterManifestType -ManifestPath $aPath
+ Write-Verbose "Detected manifest format: $manifestType"
+ } catch {
+ Write-Error "Failed to determine manifest format for '$aPath': $($_.Exception.Message)"
+ return
+ }
+
+ # Handle JSON manifests
+ if ($manifestType -eq 'JSON') {
+ Write-Verbose "Processing JSON manifest: $aPath"
+
+ try {
+ $jsonContent = Get-Content -LiteralPath $aPath -Raw -ErrorAction Stop
+ $validationResult = Test-JsonManifest -JsonContent $jsonContent -Detailed
+
+ if ($validationResult) {
+ Write-Verbose "JSON manifest validation passed"
+ # Convert JSON to XML for consistent return type
+ $xmlManifest = ConvertFrom-JsonManifest -JsonContent $jsonContent
+ return $xmlManifest
+ } else {
+ Write-Error "JSON manifest validation failed for '$aPath'"
+ return $null
+ }
+ } catch {
+ $ex = New-Object System.Exception ("JSON manifest validation failed for '$aPath': $($_.Exception.Message)"), $_.Exception
+ $category = [System.Management.Automation.ErrorCategory]::InvalidData
+ $errRecord = New-Object System.Management.Automation.ErrorRecord $ex, 'InvalidJsonManifestFile', $category, $aPath
+ $PSCmdLet.WriteError($errRecord)
+ return $null
+ }
+ }
+
+ # Handle XML manifests (existing logic)
# Verify the manifest loads into an XmlDocument i.e. verify it is well-formed.
$manifest = $null
try {
$manifest = [xml](Get-Content $aPath)
- }
- catch {
+ } catch {
$ex = New-Object System.Exception ($LocalizedData.ManifestNotWellFormedXml_F2 -f $aPath, $_.Exception.Message), $_.Exception
$category = [System.Management.Automation.ErrorCategory]::InvalidData
- $errRecord = New-Object System.Management.Automation.ErrorRecord $ex,'InvalidManifestFile',$category,$aPath
+ $errRecord = New-Object System.Management.Automation.ErrorRecord $ex, 'InvalidManifestFile', $category, $aPath
$psCmdlet.WriteError($errRecord)
return
}
@@ -62,17 +99,17 @@ function Test-PlasterManifest {
# Validate the manifest contains the required root element and target namespace that the following
# XML schema validation will apply to.
if (!$manifest.plasterManifest) {
- Write-Error ($LocalizedData.ManifestMissingDocElement_F2 -f $aPath,$TargetNamespace)
+ Write-Error ($LocalizedData.ManifestMissingDocElement_F2 -f $aPath, $TargetNamespace)
return
}
if ($manifest.plasterManifest.NamespaceURI -cne $TargetNamespace) {
- Write-Error ($LocalizedData.ManifestMissingDocTargetNamespace_F2 -f $aPath,$TargetNamespace)
+ Write-Error ($LocalizedData.ManifestMissingDocTargetNamespace_F2 -f $aPath, $TargetNamespace)
return
}
# Valid flag is stashed in a hashtable so the ValidationEventHandler scriptblock can set the value.
- $manifestIsValid = @{Value = $true}
+ $manifestIsValid = @{Value = $true }
# Configure an XmlReader and XmlReaderSettings to perform schema validation on xml file.
$xmlReaderSettings = New-Object System.Xml.XmlReaderSettings
@@ -90,9 +127,8 @@ function Test-PlasterManifest {
$validationEventHandler = {
param($sender, $eventArgs)
- if ($eventArgs.Severity -eq [System.Xml.Schema.XmlSeverityType]::Error)
- {
- Write-Verbose ($LocalizedData.ManifestSchemaValidationError_F2 -f $aPath,$eventArgs.Message)
+ if ($eventArgs.Severity -eq [System.Xml.Schema.XmlSeverityType]::Error) {
+ Write-Verbose ($LocalizedData.ManifestSchemaValidationError_F2 -f $aPath, $eventArgs.Message)
$manifestIsValid.Value = $false
}
}
@@ -104,12 +140,10 @@ function Test-PlasterManifest {
try {
$xmlReader = [System.Xml.XmlReader]::Create($aPath, $xmlReaderSettings)
while ($xmlReader.Read()) {}
- }
- catch {
+ } catch {
Write-Error ($LocalizedData.ManifestErrorReading_F1 -f $_)
$manifestIsValid.Value = $false
- }
- finally {
+ } finally {
# Schema validation is not available on .NET Core - at the moment.
if ($xmlSchemaSet) {
$xmlReaderSettings.remove_ValidationEventHandler($validationEventHandler)
@@ -119,20 +153,19 @@ function Test-PlasterManifest {
# Validate default values for choice/multichoice parameters containing 1 or more ints
$xpath = "//tns:parameter[@type='choice'] | //tns:parameter[@type='multichoice']"
- $choiceParameters = Select-Xml -Xml $manifest -XPath $xpath -Namespace @{tns=$TargetNamespace}
+ $choiceParameters = Select-Xml -Xml $manifest -XPath $xpath -Namespace @{tns = $TargetNamespace }
foreach ($choiceParameterXmlInfo in $choiceParameters) {
$choiceParameter = $choiceParameterXmlInfo.Node
if (!$choiceParameter.default) { continue }
if ($choiceParameter.type -eq 'choice') {
if ($null -eq ($choiceParameter.default -as [int])) {
- $PSCmdLet.WriteVerbose(($LocalizedData.ManifestSchemaInvalidChoiceDefault_F3 -f $choiceParameter.default,$choiceParameter.name,$aPath))
+ $PSCmdLet.WriteVerbose(($LocalizedData.ManifestSchemaInvalidChoiceDefault_F3 -f $choiceParameter.default, $choiceParameter.name, $aPath))
$manifestIsValid.Value = $false
}
- }
- else {
+ } else {
if ($null -eq (($choiceParameter.default -split ',') -as [int[]])) {
- $PSCmdLet.WriteVerbose(($LocalizedData.ManifestSchemaInvalidMultichoiceDefault_F3 -f $choiceParameter.default,$choiceParameter.name,$aPath))
+ $PSCmdLet.WriteVerbose(($LocalizedData.ManifestSchemaInvalidMultichoiceDefault_F3 -f $choiceParameter.default, $choiceParameter.name, $aPath))
$manifestIsValid.Value = $false
}
}
@@ -140,11 +173,11 @@ function Test-PlasterManifest {
# Validate that the requireModule attribute requiredVersion is mutually exclusive from both
# the version and maximumVersion attributes.
- $requireModules = Select-Xml -Xml $manifest -XPath '//tns:requireModule' -Namespace @{tns = $TargetNamespace}
+ $requireModules = Select-Xml -Xml $manifest -XPath '//tns:requireModule' -Namespace @{tns = $TargetNamespace }
foreach ($requireModuleInfo in $requireModules) {
$requireModuleNode = $requireModuleInfo.Node
if ($requireModuleNode.requiredVersion -and ($requireModuleNode.minimumVersion -or $requireModuleNode.maximumVersion)) {
- $PSCmdLet.WriteVerbose(($LocalizedData.ManifestSchemaInvalidRequireModuleAttrs_F2 -f $requireModuleNode.name,$aPath))
+ $PSCmdLet.WriteVerbose(($LocalizedData.ManifestSchemaInvalidRequireModuleAttrs_F2 -f $requireModuleNode.name, $aPath))
$manifestIsValid.Value = $false
}
}
@@ -162,9 +195,9 @@ function Test-PlasterManifest {
}
# Validate all interpolated attribute values are valid within a PowerShell string interpolation context.
- $interpolatedAttrs = @(Select-Xml -Xml $manifest -XPath '//tns:parameter/@default' -Namespace @{tns = $TargetNamespace})
- $interpolatedAttrs += @(Select-Xml -Xml $manifest -XPath '//tns:parameter/@prompt' -Namespace @{tns = $TargetNamespace})
- $interpolatedAttrs += @(Select-Xml -Xml $manifest -XPath '//tns:content/tns:*/@*' -Namespace @{tns = $TargetNamespace})
+ $interpolatedAttrs = @(Select-Xml -Xml $manifest -XPath '//tns:parameter/@default' -Namespace @{tns = $TargetNamespace })
+ $interpolatedAttrs += @(Select-Xml -Xml $manifest -XPath '//tns:parameter/@prompt' -Namespace @{tns = $TargetNamespace })
+ $interpolatedAttrs += @(Select-Xml -Xml $manifest -XPath '//tns:content/tns:*/@*' -Namespace @{tns = $TargetNamespace })
foreach ($interpolatedAttr in $interpolatedAttrs) {
$name = $interpolatedAttr.Node.LocalName
if ($name -eq 'condition') { continue }
@@ -189,7 +222,7 @@ function Test-PlasterManifest {
(($manifestSchemaVersion.Major -eq $LatestSupportedSchemaVersion.Major) -and
($manifestSchemaVersion.Minor -gt $LatestSupportedSchemaVersion.Minor))) {
- Write-Error ($LocalizedData.ManifestSchemaVersionNotSupported_F2 -f $manifestSchemaVersion,$aPath)
+ Write-Error ($LocalizedData.ManifestSchemaVersionNotSupported_F2 -f $manifestSchemaVersion, $aPath)
return
}
@@ -204,21 +237,19 @@ function Test-PlasterManifest {
if ($requiredPlasterVersion -gt $MyInvocation.MyCommand.Module.Version) {
$plasterVersion = $manifest.plasterManifest.plasterVersion
- Write-Error ($LocalizedData.ManifestPlasterVersionNotSupported_F2 -f $aPath,$plasterVersion)
+ Write-Error ($LocalizedData.ManifestPlasterVersionNotSupported_F2 -f $aPath, $plasterVersion)
return
}
}
$manifest
- }
- else {
+ } else {
if ($PSBoundParameters['Verbose']) {
Write-Error ($LocalizedData.ManifestNotValid_F1 -f $aPath)
- }
- else {
+ } else {
Write-Error ($LocalizedData.ManifestNotValidVerbose_F1 -f $aPath)
}
}
}
}
-}
+}
\ No newline at end of file
diff --git a/Plaster/Schema/plaster-manifest-v2.json b/Plaster/Schema/plaster-manifest-v2.json
new file mode 100644
index 0000000..49af49c
--- /dev/null
+++ b/Plaster/Schema/plaster-manifest-v2.json
@@ -0,0 +1,772 @@
+{
+ "$schema": "http://json-schema.org/draft-07/schema#",
+ "$id": "https://raw.githubusercontent.com/PowerShellOrg/Plaster/v2/schema/plaster-manifest-v2.json",
+ "title": "Plaster Template Manifest v2.0",
+ "description": "JSON schema for Plaster 2.0 template manifests",
+ "type": "object",
+ "required": [
+ "schemaVersion",
+ "metadata",
+ "content"
+ ],
+ "additionalProperties": false,
+ "properties": {
+ "$schema": {
+ "type": "string",
+ "description": "JSON Schema reference",
+ "format": "uri"
+ },
+ "schemaVersion": {
+ "type": "string",
+ "description": "Plaster schema version",
+ "enum": [
+ "2.0"
+ ]
+ },
+ "metadata": {
+ "type": "object",
+ "description": "Template metadata",
+ "required": [
+ "name",
+ "id",
+ "version",
+ "title",
+ "author"
+ ],
+ "additionalProperties": false,
+ "properties": {
+ "name": {
+ "type": "string",
+ "description": "Template name (must be a valid identifier)",
+ "pattern": "^[A-Za-z][A-Za-z0-9_-]*$",
+ "minLength": 1,
+ "maxLength": 100
+ },
+ "id": {
+ "type": "string",
+ "description": "Unique template identifier (GUID)",
+ "pattern": "^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$"
+ },
+ "version": {
+ "type": "string",
+ "description": "Template version (semantic versioning)",
+ "pattern": "^\\d+\\.\\d+\\.\\d+([+-].*)?$"
+ },
+ "title": {
+ "type": "string",
+ "description": "Human-readable template title",
+ "minLength": 1,
+ "maxLength": 200
+ },
+ "description": {
+ "type": "string",
+ "description": "Template description",
+ "maxLength": 1000
+ },
+ "author": {
+ "type": "string",
+ "description": "Template author",
+ "minLength": 1,
+ "maxLength": 100
+ },
+ "tags": {
+ "type": "array",
+ "description": "Template tags for categorization",
+ "items": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 50
+ },
+ "uniqueItems": true,
+ "maxItems": 20
+ },
+ "templateType": {
+ "type": "string",
+ "description": "Template type",
+ "enum": [
+ "Project",
+ "Item"
+ ],
+ "default": "Project"
+ },
+ "minimumPlasterVersion": {
+ "type": "string",
+ "description": "Minimum required Plaster version",
+ "pattern": "^\\d+\\.\\d+(\\.\\d+)?$"
+ },
+ "openInEditor": {
+ "type": "boolean",
+ "description": "Whether to open the template in an editor after creation",
+ "default": false
+ }
+ }
+ },
+ "parameters": {
+ "type": "array",
+ "description": "Template parameters",
+ "items": {
+ "$ref": "#/definitions/parameter"
+ }
+ },
+ "content": {
+ "type": "array",
+ "description": "Template content actions",
+ "items": {
+ "$ref": "#/definitions/contentAction"
+ },
+ "minItems": 1
+ },
+ "functions": {
+ "type": "object",
+ "description": "Custom functions for template processing",
+ "additionalProperties": {
+ "type": "string",
+ "description": "PowerShell script block as string"
+ }
+ }
+ },
+ "definitions": {
+ "parameter": {
+ "type": "object",
+ "required": [
+ "name",
+ "type"
+ ],
+ "additionalProperties": false,
+ "properties": {
+ "name": {
+ "type": "string",
+ "description": "Parameter name",
+ "pattern": "^[A-Za-z][A-Za-z0-9_]*$",
+ "minLength": 1,
+ "maxLength": 50
+ },
+ "type": {
+ "type": "string",
+ "description": "Parameter type",
+ "enum": [
+ "text",
+ "user-fullname",
+ "user-email",
+ "choice",
+ "multichoice",
+ "switch"
+ ]
+ },
+ "prompt": {
+ "type": "string",
+ "description": "User prompt text",
+ "minLength": 1,
+ "maxLength": 200
+ },
+ "default": {
+ "description": "Default value",
+ "oneOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "number"
+ },
+ {
+ "type": "boolean"
+ },
+ {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ }
+ ]
+ },
+ "choices": {
+ "type": "array",
+ "description": "Available choices for choice/multichoice parameters",
+ "items": {
+ "type": "object",
+ "required": [
+ "label",
+ "value"
+ ],
+ "additionalProperties": false,
+ "properties": {
+ "label": {
+ "type": "string",
+ "description": "Choice display label",
+ "minLength": 1,
+ "maxLength": 100
+ },
+ "value": {
+ "type": "string",
+ "description": "Choice value",
+ "minLength": 1,
+ "maxLength": 100
+ },
+ "help": {
+ "type": "string",
+ "description": "Choice help text",
+ "maxLength": 500
+ }
+ }
+ },
+ "minItems": 1,
+ "maxItems": 50
+ },
+ "validation": {
+ "type": "object",
+ "description": "Parameter validation rules",
+ "additionalProperties": false,
+ "properties": {
+ "pattern": {
+ "type": "string",
+ "description": "Regex pattern for validation",
+ "format": "regex"
+ },
+ "minLength": {
+ "type": "integer",
+ "description": "Minimum string length",
+ "minimum": 0
+ },
+ "maxLength": {
+ "type": "integer",
+ "description": "Maximum string length",
+ "minimum": 1
+ },
+ "minimum": {
+ "type": "number",
+ "description": "Minimum numeric value"
+ },
+ "maximum": {
+ "type": "number",
+ "description": "Maximum numeric value"
+ },
+ "message": {
+ "type": "string",
+ "description": "Custom validation error message",
+ "maxLength": 200
+ }
+ }
+ },
+ "condition": {
+ "type": "string",
+ "description": "Condition for parameter visibility",
+ "maxLength": 500
+ },
+ "dependsOn": {
+ "type": "array",
+ "description": "Parameters this parameter depends on",
+ "items": {
+ "type": "string",
+ "pattern": "^[A-Za-z][A-Za-z0-9_]*$"
+ },
+ "uniqueItems": true
+ },
+ "store": {
+ "type": "string",
+ "description": "How to store the parameter value",
+ "enum": [
+ "text",
+ "encrypted"
+ ]
+ },
+ "help": {
+ "type": "string",
+ "description": "Parameter help text",
+ "maxLength": 500
+ }
+ },
+ "allOf": [
+ {
+ "if": {
+ "properties": {
+ "type": {
+ "enum": [
+ "choice",
+ "multichoice"
+ ]
+ }
+ }
+ },
+ "then": {
+ "required": [
+ "choices"
+ ]
+ }
+ }
+ ]
+ },
+ "contentAction": {
+ "type": "object",
+ "required": [
+ "type"
+ ],
+ "discriminator": {
+ "propertyName": "type"
+ },
+ "oneOf": [
+ {
+ "$ref": "#/definitions/messageAction"
+ },
+ {
+ "$ref": "#/definitions/fileAction"
+ },
+ {
+ "$ref": "#/definitions/templateFileAction"
+ },
+ {
+ "$ref": "#/definitions/directoryAction"
+ },
+ {
+ "$ref": "#/definitions/newModuleManifestAction"
+ },
+ {
+ "$ref": "#/definitions/modifyAction"
+ },
+ {
+ "$ref": "#/definitions/requireModuleAction"
+ },
+ {
+ "$ref": "#/definitions/executeAction"
+ }
+ ]
+ },
+ "baseAction": {
+ "type": "object",
+ "properties": {
+ "condition": {
+ "type": "string",
+ "description": "Condition for executing this action",
+ "maxLength": 500
+ }
+ }
+ },
+ "messageAction": {
+ "allOf": [
+ {
+ "$ref": "#/definitions/baseAction"
+ },
+ {
+ "type": "object",
+ "required": [
+ "type",
+ "text"
+ ],
+ "additionalProperties": false,
+ "properties": {
+ "type": {
+ "const": "message"
+ },
+ "text": {
+ "type": "string",
+ "description": "Message text to display",
+ "minLength": 1,
+ "maxLength": 1000
+ },
+ "noNewline": {
+ "type": "boolean",
+ "description": "Don't add newline after message",
+ "default": false
+ },
+ "condition": {
+ "$ref": "#/definitions/baseAction/properties/condition"
+ }
+ }
+ }
+ ]
+ },
+ "fileAction": {
+ "allOf": [
+ {
+ "$ref": "#/definitions/baseAction"
+ },
+ {
+ "type": "object",
+ "required": [
+ "type",
+ "source",
+ "destination"
+ ],
+ "additionalProperties": false,
+ "properties": {
+ "type": {
+ "const": "file"
+ },
+ "source": {
+ "type": "string",
+ "description": "Source file path (supports wildcards)",
+ "minLength": 1,
+ "maxLength": 500
+ },
+ "destination": {
+ "type": "string",
+ "description": "Destination path",
+ "minLength": 1,
+ "maxLength": 500
+ },
+ "encoding": {
+ "type": "string",
+ "description": "File encoding",
+ "enum": [
+ "UTF8",
+ "UTF8-NoBOM",
+ "ASCII",
+ "Unicode",
+ "UTF32",
+ "Default"
+ ]
+ },
+ "openInEditor": {
+ "type": "boolean",
+ "description": "Open file in editor after creation",
+ "default": false
+ },
+ "condition": {
+ "$ref": "#/definitions/baseAction/properties/condition"
+ }
+ }
+ }
+ ]
+ },
+ "templateFileAction": {
+ "allOf": [
+ {
+ "$ref": "#/definitions/baseAction"
+ },
+ {
+ "type": "object",
+ "required": [
+ "type",
+ "source",
+ "destination"
+ ],
+ "additionalProperties": false,
+ "properties": {
+ "type": {
+ "const": "templateFile"
+ },
+ "source": {
+ "type": "string",
+ "description": "Source template file path",
+ "minLength": 1,
+ "maxLength": 500
+ },
+ "destination": {
+ "type": "string",
+ "description": "Destination path",
+ "minLength": 1,
+ "maxLength": 500
+ },
+ "encoding": {
+ "type": "string",
+ "description": "File encoding",
+ "enum": [
+ "UTF8",
+ "UTF8-NoBOM",
+ "ASCII",
+ "Unicode",
+ "UTF32",
+ "Default"
+ ],
+ "default": "UTF8-NoBOM"
+ },
+ "openInEditor": {
+ "type": "boolean",
+ "description": "Open file in editor after creation",
+ "default": false
+ },
+ "condition": {
+ "$ref": "#/definitions/baseAction/properties/condition"
+ }
+ }
+ }
+ ]
+ },
+ "directoryAction": {
+ "allOf": [
+ {
+ "$ref": "#/definitions/baseAction"
+ },
+ {
+ "type": "object",
+ "required": [
+ "type",
+ "destination"
+ ],
+ "additionalProperties": false,
+ "properties": {
+ "type": {
+ "const": "directory"
+ },
+ "destination": {
+ "type": "string",
+ "description": "Directory path to create",
+ "minLength": 1,
+ "maxLength": 500
+ },
+ "condition": {
+ "$ref": "#/definitions/baseAction/properties/condition"
+ }
+ }
+ }
+ ]
+ },
+ "newModuleManifestAction": {
+ "allOf": [
+ {
+ "$ref": "#/definitions/baseAction"
+ },
+ {
+ "type": "object",
+ "required": [
+ "type",
+ "destination"
+ ],
+ "additionalProperties": false,
+ "properties": {
+ "type": {
+ "const": "newModuleManifest"
+ },
+ "destination": {
+ "type": "string",
+ "description": "Module manifest destination path",
+ "minLength": 1,
+ "maxLength": 500
+ },
+ "moduleVersion": {
+ "type": "string",
+ "description": "Module version",
+ "pattern": "^\\d+\\.\\d+\\.\\d+([+-].*)?$"
+ },
+ "rootModule": {
+ "type": "string",
+ "description": "Root module file",
+ "maxLength": 200
+ },
+ "author": {
+ "type": "string",
+ "description": "Module author",
+ "maxLength": 100
+ },
+ "companyName": {
+ "type": "string",
+ "description": "Company name",
+ "maxLength": 100
+ },
+ "description": {
+ "type": "string",
+ "description": "Module description",
+ "maxLength": 1000
+ },
+ "powerShellVersion": {
+ "type": "string",
+ "description": "Minimum PowerShell version",
+ "pattern": "^\\d+\\.\\d+(\\.\\d+)?$"
+ },
+ "copyright": {
+ "type": "string",
+ "description": "Copyright statement",
+ "maxLength": 200
+ },
+ "encoding": {
+ "type": "string",
+ "description": "File encoding",
+ "enum": [
+ "UTF8",
+ "UTF8-NoBOM",
+ "ASCII",
+ "Unicode",
+ "UTF32",
+ "Default"
+ ],
+ "default": "UTF8-NoBOM"
+ },
+ "openInEditor": {
+ "type": "boolean",
+ "description": "Open manifest in editor after creation",
+ "default": false
+ },
+ "condition": {
+ "$ref": "#/definitions/baseAction/properties/condition"
+ }
+ }
+ }
+ ]
+ },
+ "modifyAction": {
+ "allOf": [
+ {
+ "$ref": "#/definitions/baseAction"
+ },
+ {
+ "type": "object",
+ "required": [
+ "type",
+ "path",
+ "modifications"
+ ],
+ "additionalProperties": false,
+ "properties": {
+ "type": {
+ "const": "modify"
+ },
+ "path": {
+ "type": "string",
+ "description": "Path to file to modify",
+ "minLength": 1,
+ "maxLength": 500
+ },
+ "modifications": {
+ "type": "array",
+ "description": "List of modifications to apply",
+ "items": {
+ "type": "object",
+ "required": [
+ "type"
+ ],
+ "oneOf": [
+ {
+ "type": "object",
+ "required": [
+ "type",
+ "search",
+ "replace"
+ ],
+ "additionalProperties": false,
+ "properties": {
+ "type": {
+ "const": "replace"
+ },
+ "search": {
+ "type": "string",
+ "description": "Text/regex to search for",
+ "minLength": 1
+ },
+ "replace": {
+ "type": "string",
+ "description": "Replacement text"
+ },
+ "isRegex": {
+ "type": "boolean",
+ "description": "Whether search is a regex pattern",
+ "default": false
+ },
+ "condition": {
+ "type": "string",
+ "description": "Condition for this modification"
+ }
+ }
+ }
+ ]
+ },
+ "minItems": 1
+ },
+ "encoding": {
+ "type": "string",
+ "description": "File encoding",
+ "enum": [
+ "UTF8",
+ "UTF8-NoBOM",
+ "ASCII",
+ "Unicode",
+ "UTF32",
+ "Default"
+ ],
+ "default": "UTF8-NoBOM"
+ },
+ "condition": {
+ "$ref": "#/definitions/baseAction/properties/condition"
+ }
+ }
+ }
+ ]
+ },
+ "requireModuleAction": {
+ "allOf": [
+ {
+ "$ref": "#/definitions/baseAction"
+ },
+ {
+ "type": "object",
+ "required": [
+ "type",
+ "name"
+ ],
+ "additionalProperties": false,
+ "properties": {
+ "type": {
+ "const": "requireModule"
+ },
+ "name": {
+ "type": "string",
+ "description": "Required module name",
+ "minLength": 1,
+ "maxLength": 100
+ },
+ "minimumVersion": {
+ "type": "string",
+ "description": "Minimum module version",
+ "pattern": "^\\d+\\.\\d+(\\.\\d+)?([+-].*)?$"
+ },
+ "maximumVersion": {
+ "type": "string",
+ "description": "Maximum module version",
+ "pattern": "^\\d+\\.\\d+(\\.\\d+)?([+-].*)?$"
+ },
+ "requiredVersion": {
+ "type": "string",
+ "description": "Exact required version",
+ "pattern": "^\\d+\\.\\d+(\\.\\d+)?([+-].*)?$"
+ },
+ "message": {
+ "type": "string",
+ "description": "Custom message when module is missing",
+ "maxLength": 500
+ },
+ "condition": {
+ "$ref": "#/definitions/baseAction/properties/condition"
+ }
+ }
+ }
+ ]
+ },
+ "executeAction": {
+ "allOf": [
+ {
+ "$ref": "#/definitions/baseAction"
+ },
+ {
+ "type": "object",
+ "required": [
+ "type",
+ "script"
+ ],
+ "additionalProperties": false,
+ "properties": {
+ "type": {
+ "const": "execute"
+ },
+ "script": {
+ "type": "string",
+ "description": "PowerShell script to execute",
+ "minLength": 1,
+ "maxLength": 10000
+ },
+ "workingDirectory": {
+ "type": "string",
+ "description": "Working directory for script execution",
+ "maxLength": 500
+ },
+ "continueOnError": {
+ "type": "boolean",
+ "description": "Continue processing if script fails",
+ "default": false
+ },
+ "condition": {
+ "$ref": "#/definitions/baseAction/properties/condition"
+ }
+ }
+ }
+ ]
+ }
+ }
+}
\ No newline at end of file
diff --git a/docs/en-US/Invoke-Plaster.md b/docs/en-US/Invoke-Plaster.md
index eafb6ae..463ab88 100644
--- a/docs/en-US/Invoke-Plaster.md
+++ b/docs/en-US/Invoke-Plaster.md
@@ -12,11 +12,18 @@ Invokes the specified Plaster template which will scaffold out a file or a set o
## SYNTAX
+### TemplatePath (Default)
```
Invoke-Plaster [-TemplatePath] [-DestinationPath] [-Force] [-NoLogo] [-PassThru]
[-ProgressAction ] [-WhatIf] [-Confirm] []
```
+### TemplateDefinition
+```
+Invoke-Plaster [-TemplateDefinition] [-DestinationPath] [-Force] [-NoLogo] [-PassThru]
+ [-ProgressAction ] [-WhatIf] [-Confirm] []
+```
+
## DESCRIPTION
Invokes the specified Plaster template which will scaffold out a file or a
set of files and directories.
@@ -131,12 +138,27 @@ Accept pipeline input: False
Accept wildcard characters: False
```
+### -TemplateDefinition
+{{ Fill TemplateDefinition Description }}
+
+```yaml
+Type: String
+Parameter Sets: TemplateDefinition
+Aliases:
+
+Required: True
+Position: 0
+Default value: None
+Accept pipeline input: False
+Accept wildcard characters: False
+```
+
### -TemplatePath
Specifies the path to the template directory.
```yaml
Type: String
-Parameter Sets: (All)
+Parameter Sets: TemplatePath
Aliases:
Required: True
diff --git a/docs/en-US/New-PlasterManifest.md b/docs/en-US/New-PlasterManifest.md
index d4c48f1..227a7b0 100644
--- a/docs/en-US/New-PlasterManifest.md
+++ b/docs/en-US/New-PlasterManifest.md
@@ -15,8 +15,8 @@ Creates a new Plaster template manifest file.
```
New-PlasterManifest [[-Path] ] [-TemplateName] [-TemplateType] [[-Id] ]
[[-TemplateVersion] ] [[-Title] ] [[-Description] ] [[-Tags] ]
- [[-Author] ] [-AddContent] [-ProgressAction ] [-WhatIf] [-Confirm]
- []
+ [[-Author] ] [-AddContent] [[-Format] ] [-ConvertFromXml] [-ProgressAction ]
+ [-WhatIf] [-Confirm] []
```
## DESCRIPTION
@@ -118,6 +118,21 @@ Accept pipeline input: False
Accept wildcard characters: False
```
+### -ConvertFromXml
+{{ Fill ConvertFromXml Description }}
+
+```yaml
+Type: SwitchParameter
+Parameter Sets: (All)
+Aliases:
+
+Required: False
+Position: Named
+Default value: None
+Accept pipeline input: False
+Accept wildcard characters: False
+```
+
### -Description
Description of the Plaster template.
This describes what the template is for.
@@ -138,6 +153,22 @@ Accept pipeline input: False
Accept wildcard characters: False
```
+### -Format
+{{ Fill Format Description }}
+
+```yaml
+Type: String
+Parameter Sets: (All)
+Aliases:
+Accepted values: XML, JSON
+
+Required: False
+Position: 9
+Default value: None
+Accept pipeline input: False
+Accept wildcard characters: False
+```
+
### -Id
Unique identifier for all versions of this template.
The id is a GUID.
diff --git a/tests/JsonTest.Tests.ps1 b/tests/JsonTest.Tests.ps1
new file mode 100644
index 0000000..cfe041f
--- /dev/null
+++ b/tests/JsonTest.Tests.ps1
@@ -0,0 +1,932 @@
+# TODO: What is this? GS
+# Phase 2 Test: JSON Template Creation and Execution
+
+Write-Host "=== Plaster 2.0 Phase 2 - JSON Template Test ===" -ForegroundColor Cyan
+
+# Create test directories
+$jsonTemplateDir = Join-Path $env:TEMP "PlasterJsonTemplate"
+$jsonOutputDir = Join-Path $env:TEMP "JsonTemplateOutput"
+
+New-Item -Path $jsonTemplateDir -ItemType Directory -Force | Out-Null
+New-Item -Path $jsonOutputDir -ItemType Directory -Force | Out-Null
+
+Write-Host "Created test directories:" -ForegroundColor Green
+Write-Host " Template: $jsonTemplateDir"
+Write-Host " Output: $jsonOutputDir"
+
+# Create comprehensive JSON manifest
+$jsonManifest = @{
+ '$schema' = 'https://raw.githubusercontent.com/PowerShellOrg/Plaster/v2/schema/plaster-manifest-v2.json'
+ 'schemaVersion' = '2.0'
+ 'metadata' = @{
+ 'name' = 'ModernPowerShellModule'
+ 'id' = '12345678-1234-1234-1234-123456789012'
+ 'version' = '1.0.0'
+ 'title' = 'Modern PowerShell Module Template (JSON)'
+ 'description' = 'Creates a modern PowerShell module using JSON template format with enhanced features'
+ 'author' = 'Plaster 2.0 Team'
+ 'tags' = @('Module', 'PowerShell', 'JSON', 'Modern', 'Cross-Platform')
+ 'templateType' = 'Project'
+ }
+ 'parameters' = @(
+ @{
+ 'name' = 'ModuleName'
+ 'type' = 'text'
+ 'prompt' = 'Enter the module name'
+ 'validation' = @{
+ 'pattern' = '^[A-Za-z][A-Za-z0-9]*$'
+ 'message' = 'Module name must start with a letter and contain only letters and numbers'
+ }
+ },
+ @{
+ 'name' = 'ModuleAuthor'
+ 'type' = 'user-fullname'
+ 'prompt' = 'Enter your full name'
+ },
+ @{
+ 'name' = 'ModuleDescription'
+ 'type' = 'text'
+ 'prompt' = 'Enter a brief description'
+ 'validation' = @{
+ 'minLength' = 10
+ 'maxLength' = 200
+ 'message' = 'Description must be between 10 and 200 characters'
+ }
+ },
+ @{
+ 'name' = 'ModuleVersion'
+ 'type' = 'text'
+ 'prompt' = 'Enter the initial version'
+ 'default' = '0.1.0'
+ 'validation' = @{
+ 'pattern' = '^\d+\.\d+\.\d+$'
+ 'message' = 'Version must be in semantic versioning format (e.g., 1.0.0)'
+ }
+ },
+ @{
+ 'name' = 'IncludeTests'
+ 'type' = 'choice'
+ 'prompt' = 'Include Pester tests?'
+ 'choices' = @(
+ @{ 'label' = 'Yes'; 'value' = 'Yes'; 'help' = 'Include comprehensive Pester 5.x tests' },
+ @{ 'label' = 'No'; 'value' = 'No'; 'help' = 'Skip test creation' }
+ )
+ 'default' = 'Yes'
+ },
+ @{
+ 'name' = 'TestFramework'
+ 'type' = 'choice'
+ 'prompt' = 'Select test framework version'
+ 'choices' = @(
+ @{ 'label' = 'Pester 5.x'; 'value' = 'Pester5'; 'help' = 'Modern Pester framework' },
+ @{ 'label' = 'Pester 4.x'; 'value' = 'Pester4'; 'help' = 'Legacy Pester framework' }
+ )
+ 'default' = 'Pester5'
+ 'condition' = '${IncludeTests} == "Yes"'
+ 'dependsOn' = @('IncludeTests')
+ },
+ @{
+ 'name' = 'Features'
+ 'type' = 'multichoice'
+ 'prompt' = 'Select additional features'
+ 'choices' = @(
+ @{ 'label' = 'CI/CD'; 'value' = 'CICD'; 'help' = 'GitHub Actions workflow' },
+ @{ 'label' = 'Documentation'; 'value' = 'Docs'; 'help' = 'Markdown documentation' },
+ @{ 'label' = 'License'; 'value' = 'License'; 'help' = 'MIT license file' },
+ @{ 'label' = 'Examples'; 'value' = 'Examples'; 'help' = 'Usage examples' }
+ )
+ 'default' = @('Docs', 'License')
+ },
+ @{
+ 'name' = 'PowerShellVersion'
+ 'type' = 'choice'
+ 'prompt' = 'Minimum PowerShell version'
+ 'choices' = @(
+ @{ 'label' = '5.1'; 'value' = '5.1'; 'help' = 'Windows PowerShell 5.1+' },
+ @{ 'label' = '7.0'; 'value' = '7.0'; 'help' = 'PowerShell Core 7.0+' },
+ @{ 'label' = '7.2'; 'value' = '7.2'; 'help' = 'PowerShell 7.2+ (recommended)' }
+ )
+ 'default' = '7.2'
+ }
+ )
+ 'content' = @(
+ @{
+ 'type' = 'message'
+ 'text' = 'Creating modern PowerShell module: ${ModuleName}'
+ },
+ @{
+ 'type' = 'directory'
+ 'destination' = 'src'
+ },
+ @{
+ 'type' = 'directory'
+ 'destination' = 'tests'
+ 'condition' = '${IncludeTests} == "Yes"'
+ },
+ @{
+ 'type' = 'directory'
+ 'destination' = 'docs'
+ 'condition' = '${Features} -contains "Docs"'
+ },
+ @{
+ 'type' = 'directory'
+ 'destination' = 'examples'
+ 'condition' = '${Features} -contains "Examples"'
+ },
+ @{
+ 'type' = 'directory'
+ 'destination' = '.github/workflows'
+ 'condition' = '${Features} -contains "CICD"'
+ },
+ @{
+ 'type' = 'newModuleManifest'
+ 'destination' = 'src/${ModuleName}.psd1'
+ 'moduleVersion' = '${ModuleVersion}'
+ 'rootModule' = '${ModuleName}.psm1'
+ 'author' = '${ModuleAuthor}'
+ 'description' = '${ModuleDescription}'
+ 'powerShellVersion' = '${PowerShellVersion}'
+ 'encoding' = 'UTF8-NoBOM'
+ },
+ @{
+ 'type' = 'templateFile'
+ 'source' = 'Module.psm1'
+ 'destination' = 'src/${ModuleName}.psm1'
+ 'encoding' = 'UTF8-NoBOM'
+ },
+ @{
+ 'type' = 'templateFile'
+ 'source' = 'README.md'
+ 'destination' = 'README.md'
+ 'encoding' = 'UTF8-NoBOM'
+ },
+ @{
+ 'type' = 'templateFile'
+ 'source' = 'CHANGELOG.md'
+ 'destination' = 'CHANGELOG.md'
+ 'encoding' = 'UTF8-NoBOM'
+ },
+ @{
+ 'type' = 'templateFile'
+ 'source' = 'Module.Tests.ps1'
+ 'destination' = 'tests/${ModuleName}.Tests.ps1'
+ 'condition' = '${IncludeTests} == "Yes"'
+ 'encoding' = 'UTF8-NoBOM'
+ },
+ @{
+ 'type' = 'templateFile'
+ 'source' = 'ci.yml'
+ 'destination' = '.github/workflows/ci.yml'
+ 'condition' = '${Features} -contains "CICD"'
+ 'encoding' = 'UTF8-NoBOM'
+ },
+ @{
+ 'type' = 'file'
+ 'source' = 'LICENSE'
+ 'destination' = 'LICENSE'
+ 'condition' = '${Features} -contains "License"'
+ },
+ @{
+ 'type' = 'templateFile'
+ 'source' = 'Example.ps1'
+ 'destination' = 'examples/${ModuleName}-Example.ps1'
+ 'condition' = '${Features} -contains "Examples"'
+ 'encoding' = 'UTF8-NoBOM'
+ },
+ @{
+ 'type' = 'message'
+ 'text' = 'Module ${ModuleName} created successfully!'
+ },
+ @{
+ 'type' = 'message'
+ 'text' = 'Features included: ${Features}'
+ },
+ @{
+ 'type' = 'message'
+ 'text' = 'Run tests with: Invoke-Pester tests/'
+ 'condition' = '${IncludeTests} == "Yes"'
+ }
+ )
+}
+
+# Convert to JSON and save
+$jsonContent = $jsonManifest | ConvertTo-Json -Depth 10
+Set-Content -Path "$jsonTemplateDir/plasterManifest.json" -Value $jsonContent -Encoding UTF8
+
+Write-Host "`nCreated JSON manifest:" -ForegroundColor Green
+Write-Host " Size: $($jsonContent.Length) characters"
+Write-Host " Parameters: $($jsonManifest.parameters.Count)"
+Write-Host " Content Actions: $($jsonManifest.content.Count)"
+
+# Create template files
+$moduleTemplate = @'
+#Requires -Version ${PowerShellVersion}
+
+<#
+.SYNOPSIS
+ ${ModuleDescription}
+
+.DESCRIPTION
+ ${ModuleName} - A modern PowerShell module created with Plaster 2.0 JSON templates.
+
+ This module demonstrates the enhanced capabilities of Plaster 2.0 including:
+ - JSON template format support
+ - Enhanced parameter validation
+ - Cross-platform compatibility
+ - Modern PowerShell practices
+
+.AUTHOR
+ ${ModuleAuthor}
+
+.VERSION
+ ${ModuleVersion}
+
+.NOTES
+ Created with Plaster 2.0 JSON template on ${PLASTER_Date}
+ Template Type: ${PLASTER_ManifestType}
+ Platform: ${PLASTER_HostName}
+#>
+
+using namespace System.Management.Automation
+
+# Module initialization
+$ErrorActionPreference = 'Stop'
+$InformationPreference = 'Continue'
+
+Write-Information "${ModuleName} v${ModuleVersion} loading..."
+
+# Import functions
+$functionFolders = @('Public', 'Private')
+foreach ($folder in $functionFolders) {
+ $folderPath = Join-Path $PSScriptRoot $folder
+ if (Test-Path $folderPath) {
+ $functions = Get-ChildItem -Path $folderPath -Filter '*.ps1' -Recurse
+ foreach ($function in $functions) {
+ try {
+ . $function.FullName
+ Write-Verbose "Imported function: $($function.BaseName)"
+ }
+ catch {
+ Write-Error "Failed to import function $($function.FullName): $_"
+ }
+ }
+ }
+}
+
+# Export public functions
+$publicFunctions = Get-ChildItem -Path (Join-Path $PSScriptRoot 'Public') -Filter '*.ps1' -Recurse -ErrorAction SilentlyContinue
+if ($publicFunctions) {
+ Export-ModuleMember -Function $publicFunctions.BaseName
+}
+
+Write-Information "${ModuleName} v${ModuleVersion} loaded successfully"
+'@
+
+$readmeTemplate = @'
+# ${ModuleName}
+
+${ModuleDescription}
+
+[](https://www.powershellgallery.com/packages/${ModuleName})
+[](https://www.powershellgallery.com/packages/${ModuleName})
+
+## Features
+
+- Modern PowerShell ${PowerShellVersion}+ module
+- Cross-platform compatibility (Windows, Linux, macOS)
+<%
+if ($PLASTER_PARAM_IncludeTests -eq 'Yes') {
+ "- Comprehensive Pester $($PLASTER_PARAM_TestFramework) test suite"
+}
+%>
+<%
+if ($PLASTER_PARAM_Features -contains 'CICD') {
+ "- Automated CI/CD with GitHub Actions"
+}
+%>
+<%
+if ($PLASTER_PARAM_Features -contains 'Docs') {
+ "- Complete documentation"
+}
+%>
+
+## Installation
+
+### PowerShell Gallery
+```powershell
+Install-Module ${ModuleName} -Scope CurrentUser
+```
+
+### Manual Installation
+```powershell
+# Clone or download the repository
+Import-Module ./src/${ModuleName}.psd1
+```
+
+## Quick Start
+
+```powershell
+# Import the module
+Import-Module ${ModuleName}
+
+# Basic usage example
+# Add your module's main functions here
+```
+
+## Documentation
+
+<%
+if ($PLASTER_PARAM_Features -contains 'Docs') {
+ "- [User Guide](docs/UserGuide.md)"
+ "- [API Reference](docs/API.md)"
+ "- [Examples](examples/)"
+}
+%>
+
+## Development
+
+### Prerequisites
+- PowerShell ${PowerShellVersion} or higher
+<%
+if ($PLASTER_PARAM_IncludeTests -eq 'Yes') {
+ "- Pester module for testing"
+}
+%>
+
+### Building
+```powershell
+# Run tests
+<%
+if ($PLASTER_PARAM_TestFramework -eq 'Pester5') {
+ "Invoke-Pester tests/ -Output Detailed"
+} else {
+ "Invoke-Pester tests/"
+}
+%>
+
+# Import for development
+Import-Module ./src/${ModuleName}.psd1 -Force
+```
+
+<%
+if ($PLASTER_PARAM_Features -contains 'CICD') {
+ "## CI/CD"
+ ""
+ "This project uses GitHub Actions for continuous integration:"
+ "- Automated testing on Windows, Linux, and macOS"
+ "- PowerShell $($PLASTER_PARAM_PowerShellVersion)+ compatibility testing"
+ "- Code quality checks"
+}
+%>
+
+## Contributing
+
+Contributions are welcome! Please:
+
+1. Fork the repository
+2. Create a feature branch
+3. Add tests for new functionality
+4. Ensure all tests pass
+5. Submit a pull request
+
+## License
+
+<%
+if ($PLASTER_PARAM_Features -contains 'License') {
+ "This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details."
+} else {
+ "License information not specified."
+}
+%>
+
+## Author
+
+Created by ${ModuleAuthor}
+
+---
+
+*Generated with Plaster 2.0 JSON template on ${PLASTER_Date}*
+*Template Format: ${PLASTER_ManifestType} | Platform: ${PLASTER_HostName}*
+'@
+
+$changelogTemplate = @'
+# Changelog
+
+All notable changes to ${ModuleName} will be documented in this file.
+
+The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
+and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
+
+## [Unreleased]
+
+### Added
+- Initial project structure
+
+## [${ModuleVersion}] - ${PLASTER_Date}
+
+### Added
+- Initial release of ${ModuleName}
+- Core module functionality
+<%
+if ($PLASTER_PARAM_IncludeTests -eq 'Yes') {
+ "- $($PLASTER_PARAM_TestFramework) test suite"
+}
+%>
+<%
+if ($PLASTER_PARAM_Features -contains 'CICD') {
+ "- GitHub Actions CI/CD pipeline"
+}
+%>
+<%
+if ($PLASTER_PARAM_Features -contains 'Docs') {
+ "- Documentation and examples"
+}
+%>
+
+### Technical Details
+- Minimum PowerShell version: ${PowerShellVersion}
+- Cross-platform support: Windows, Linux, macOS
+- Created with: Plaster 2.0 (${PLASTER_ManifestType} format)
+- Template features: ${Features}
+
+## Template Information
+
+This module was generated using:
+- **Plaster Version**: ${PLASTER_Version}
+- **Template Format**: ${PLASTER_ManifestType}
+- **Generation Date**: ${PLASTER_Date}
+- **Platform**: ${PLASTER_HostName}
+'@
+
+$testTemplate = @'
+#Requires -Modules Pester
+<%
+if ($PLASTER_PARAM_TestFramework -eq 'Pester5') {
+ "#Requires -Version 5.1"
+}
+%>
+
+<#
+.SYNOPSIS
+ Pester tests for ${ModuleName}
+
+.DESCRIPTION
+ <%
+ if ($PLASTER_PARAM_TestFramework -eq 'Pester5') {
+ "Comprehensive Pester 5.x test suite for ${ModuleName}"
+ } else {
+ "Pester 4.x test suite for ${ModuleName}"
+ }
+ %>
+
+.NOTES
+ Created with Plaster 2.0 JSON template
+ Template Format: ${PLASTER_ManifestType}
+ Author: ${ModuleAuthor}
+#>
+
+<%
+if ($PLASTER_PARAM_TestFramework -eq 'Pester5') {
+ "BeforeAll {"
+} else {
+ "BeforeDiscovery {"
+}
+%>
+ # Module setup
+ $ModuleName = '${ModuleName}'
+ $ModuleRoot = Split-Path -Path $PSScriptRoot -Parent
+ $ModulePath = Join-Path $ModuleRoot "src\$ModuleName.psd1"
+
+ # Import the module for testing
+ if (Test-Path $ModulePath) {
+ Import-Module $ModulePath -Force
+ } else {
+ throw "Module manifest not found: $ModulePath"
+ }
+
+ # Test data
+ $script:TestData = @{
+ ModuleName = $ModuleName
+ ModuleVersion = '${ModuleVersion}'
+ Author = '${ModuleAuthor}'
+ MinimumPSVersion = '${PowerShellVersion}'
+ }
+}
+
+<%
+if ($PLASTER_PARAM_TestFramework -eq 'Pester5') {
+ "AfterAll {"
+} else {
+ "AfterEach {"
+}
+%>
+ # Cleanup
+ Remove-Module $ModuleName -Force -ErrorAction SilentlyContinue
+}
+
+Describe '${ModuleName} Module Tests' -Tag 'Unit' {
+ Context 'Module Structure' {
+ It 'Should import without errors' {
+ { Import-Module '${ModuleName}' -Force } | Should -Not -Throw
+ }
+
+ It 'Should have the correct module name' {
+ $module = Get-Module '${ModuleName}'
+ $module.Name | Should -Be '${ModuleName}'
+ }
+
+ It 'Should have the correct version' {
+ $module = Get-Module '${ModuleName}'
+ $module.Version | Should -Be '${ModuleVersion}'
+ }
+
+ It 'Should have the correct author' {
+ $module = Get-Module '${ModuleName}'
+ $module.Author | Should -Be '${ModuleAuthor}'
+ }
+
+ It 'Should require PowerShell ${PowerShellVersion} or higher' {
+ $module = Get-Module '${ModuleName}'
+ $module.PowerShellVersion | Should -BeGreaterOrEqual '${PowerShellVersion}'
+ }
+ }
+
+ Context 'Cross-Platform Compatibility' {
+ It 'Should work on Windows' -Skip:(-not $IsWindows) {
+ $module = Get-Module '${ModuleName}'
+ $module | Should -Not -BeNullOrEmpty
+ }
+
+ It 'Should work on Linux' -Skip:(-not $IsLinux) {
+ $module = Get-Module '${ModuleName}'
+ $module | Should -Not -BeNullOrEmpty
+ }
+
+ It 'Should work on macOS' -Skip:(-not $IsMacOS) {
+ $module = Get-Module '${ModuleName}'
+ $module | Should -Not -BeNullOrEmpty
+ }
+ }
+
+ Context 'Module Functions' {
+ <%
+ if ($PLASTER_PARAM_TestFramework -eq 'Pester5') {
+ "BeforeEach {"
+ } else {
+ "BeforeAll {"
+ }
+ %>
+ $module = Get-Module '${ModuleName}'
+ $exportedFunctions = $module.ExportedFunctions.Keys
+ }
+
+ It 'Should export at least one function' {
+ $exportedFunctions.Count | Should -BeGreaterThan 0
+ }
+
+ It 'Should have help for all exported functions' {
+ foreach ($functionName in $exportedFunctions) {
+ $help = Get-Help $functionName
+ $help.Synopsis | Should -Not -BeNullOrEmpty
+ $help.Description | Should -Not -BeNullOrEmpty
+ }
+ }
+ }
+
+ Context 'Template Metadata Validation' {
+ It 'Should contain template generation metadata' {
+ # This validates that the template system worked correctly
+ $moduleContent = Get-Content "$ModuleRoot\src\${ModuleName}.psm1" -Raw
+ $moduleContent | Should -Match '${ModuleAuthor}'
+ $moduleContent | Should -Match '${ModuleVersion}'
+ $moduleContent | Should -Match 'Plaster 2.0'
+ }
+
+ It 'Should have JSON template markers' {
+ $moduleContent = Get-Content "$ModuleRoot\src\${ModuleName}.psm1" -Raw
+ $moduleContent | Should -Match 'PLASTER_ManifestType'
+ }
+ }
+}
+
+<%
+if ($PLASTER_PARAM_Features -contains 'Examples') {
+ "Describe '${ModuleName} Examples' -Tag 'Integration' {"
+ " Context 'Example Scripts' {"
+ " It 'Should have example scripts' {"
+ " Test-Path \"$ModuleRoot\\examples\" | Should -Be $true"
+ " }"
+ " "
+ " It 'Example scripts should be valid PowerShell' {"
+ " $exampleFiles = Get-ChildItem \"$ModuleRoot\\examples\" -Filter '*.ps1' -ErrorAction SilentlyContinue"
+ " foreach ($file in $exampleFiles) {"
+ " { & $file.FullName } | Should -Not -Throw"
+ " }"
+ " }"
+ " }"
+ "}"
+}
+%>
+'@
+
+$ciTemplate = @'
+name: CI/CD Pipeline
+
+on:
+ push:
+ branches: [ main, master, develop ]
+ pull_request:
+ branches: [ main, master ]
+ release:
+ types: [ published ]
+
+env:
+ MODULE_NAME: ${ModuleName}
+
+jobs:
+ test:
+ name: Test on ${{ matrix.os }} - PS ${{ matrix.powershell }}
+ runs-on: ${{ matrix.os }}
+
+ strategy:
+ fail-fast: false
+ matrix:
+ os: [windows-latest, ubuntu-latest, macos-latest]
+ powershell: ['${PowerShellVersion}', '7.3', '7.4']
+ <%
+ if ($PLASTER_PARAM_PowerShellVersion -eq '5.1') {
+ "exclude:"
+ " # PowerShell 5.1 is Windows only"
+ " - os: ubuntu-latest"
+ " powershell: '5.1'"
+ " - os: macos-latest"
+ " powershell: '5.1'"
+ }
+ %>
+
+ steps:
+ - name: Checkout Code
+ uses: actions/checkout@v4
+
+ - name: Setup PowerShell
+ uses: actions/setup-powershell@v1
+ with:
+ powershell-version: ${{ matrix.powershell }}
+
+ - name: Install Pester
+ shell: pwsh
+ run: |
+ <%
+ if ($PLASTER_PARAM_TestFramework -eq 'Pester5') {
+ "Install-Module Pester -MinimumVersion 5.0 -Force -Scope CurrentUser"
+ } else {
+ "Install-Module Pester -RequiredVersion 4.10.1 -Force -Scope CurrentUser"
+ }
+ %>
+
+ - name: Run Module Tests
+ shell: pwsh
+ run: |
+ Import-Module ./src/${ModuleName}.psd1 -Force
+ <%
+ if ($PLASTER_PARAM_TestFramework -eq 'Pester5') {
+ "Invoke-Pester ./tests/ -Output Detailed"
+ } else {
+ "Invoke-Pester ./tests/ -PassThru"
+ }
+ %>
+
+ - name: Upload Test Results
+ uses: actions/upload-artifact@v4
+ if: always()
+ with:
+ name: test-results-${{ matrix.os }}-ps${{ matrix.powershell }}
+ path: TestResults*.xml
+ retention-days: 7
+
+ quality:
+ name: Code Quality Analysis
+ runs-on: windows-latest
+
+ steps:
+ - name: Checkout Code
+ uses: actions/checkout@v4
+
+ - name: Setup PowerShell
+ uses: actions/setup-powershell@v1
+ with:
+ powershell-version: '7.4'
+
+ - name: Install PSScriptAnalyzer
+ shell: pwsh
+ run: Install-Module PSScriptAnalyzer -Force -Scope CurrentUser
+
+ - name: Run PSScriptAnalyzer
+ shell: pwsh
+ run: |
+ $results = Invoke-ScriptAnalyzer -Path ./src -Recurse -Settings PSGallery
+ if ($results) {
+ $results | Format-Table -AutoSize
+ Write-Error "PSScriptAnalyzer found $($results.Count) issue(s)"
+ }
+
+ publish:
+ name: Publish to PowerShell Gallery
+ runs-on: windows-latest
+ needs: [test, quality]
+ if: github.event_name == 'release'
+
+ steps:
+ - name: Checkout Code
+ uses: actions/checkout@v4
+
+ - name: Setup PowerShell
+ uses: actions/setup-powershell@v1
+ with:
+ powershell-version: '7.4'
+
+ - name: Publish Module
+ shell: pwsh
+ env:
+ PSGALLERY_API_KEY: ${{ secrets.PSGALLERY_API_KEY }}
+ run: |
+ if (-not $env:PSGALLERY_API_KEY) {
+ throw "PowerShell Gallery API key not found"
+ }
+ Publish-Module -Path ./src -NuGetApiKey $env:PSGALLERY_API_KEY -Verbose
+'@
+
+$exampleTemplate = @'
+<#
+.SYNOPSIS
+ Example script for ${ModuleName}
+
+.DESCRIPTION
+ This script demonstrates basic usage of the ${ModuleName} module.
+
+ Created with Plaster 2.0 JSON template.
+
+.AUTHOR
+ ${ModuleAuthor}
+
+.EXAMPLE
+ .\${ModuleName}-Example.ps1
+
+ Runs the example demonstrating module functionality.
+#>
+
+#Requires -Version ${PowerShellVersion}
+#Requires -Modules ${ModuleName}
+
+[CmdletBinding()]
+param()
+
+# Import the module
+Import-Module ${ModuleName} -Force
+
+Write-Host "=== ${ModuleName} Example ===" -ForegroundColor Cyan
+Write-Host "Module Version: $(Get-Module ${ModuleName}).Version" -ForegroundColor Green
+Write-Host "Author: ${ModuleAuthor}" -ForegroundColor Green
+Write-Host "Created: ${PLASTER_Date}" -ForegroundColor Green
+Write-Host "Template: ${PLASTER_ManifestType} format" -ForegroundColor Green
+
+# Example usage
+Write-Host "`nModule Functions:" -ForegroundColor Yellow
+$functions = Get-Command -Module ${ModuleName}
+if ($functions) {
+ $functions | ForEach-Object {
+ Write-Host " - $($_.Name)" -ForegroundColor White
+ }
+} else {
+ Write-Host " No public functions exported yet. Add your functions to the Public folder." -ForegroundColor Gray
+}
+
+Write-Host "`nExample completed successfully!" -ForegroundColor Green
+'@
+
+$licenseContent = @'
+MIT License
+
+Copyright (c) ${PLASTER_Year} ${ModuleAuthor}
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+'@
+
+# Save all template files
+Set-Content -Path "$jsonTemplateDir/Module.psm1" -Value $moduleTemplate -Encoding UTF8
+Set-Content -Path "$jsonTemplateDir/README.md" -Value $readmeTemplate -Encoding UTF8
+Set-Content -Path "$jsonTemplateDir/CHANGELOG.md" -Value $changelogTemplate -Encoding UTF8
+Set-Content -Path "$jsonTemplateDir/Module.Tests.ps1" -Value $testTemplate -Encoding UTF8
+Set-Content -Path "$jsonTemplateDir/ci.yml" -Value $ciTemplate -Encoding UTF8
+Set-Content -Path "$jsonTemplateDir/Example.ps1" -Value $exampleTemplate -Encoding UTF8
+Set-Content -Path "$jsonTemplateDir/LICENSE" -Value $licenseContent -Encoding UTF8
+
+Write-Host "`nCreated template files:" -ForegroundColor Green
+Get-ChildItem $jsonTemplateDir | ForEach-Object {
+ Write-Host " - $($_.Name)" -ForegroundColor White
+}
+
+# Test 1: Validate JSON manifest
+Write-Host "`n=== Step 1: JSON Manifest Validation ===" -ForegroundColor Cyan
+
+try {
+ # Note: This requires the JsonManifestHandler.ps1 to be loaded
+ $manifestContent = Get-Content "$jsonTemplateDir/plasterManifest.json" -Raw
+
+ # For now, let's do basic JSON validation
+ $jsonObject = $manifestContent | ConvertFrom-Json
+ Write-Host "ā
JSON is valid and parseable" -ForegroundColor Green
+ Write-Host " Schema Version: $($jsonObject.schemaVersion)" -ForegroundColor White
+ Write-Host " Template Name: $($jsonObject.metadata.name)" -ForegroundColor White
+ Write-Host " Parameters: $($jsonObject.parameters.Count)" -ForegroundColor White
+ Write-Host " Content Actions: $($jsonObject.content.Count)" -ForegroundColor White
+} catch {
+ Write-Host "ā JSON validation failed: $_" -ForegroundColor Red
+ return
+}
+
+# Test 2: Execute JSON template (when Phase 2 is complete)
+Write-Host "`n=== Step 2: JSON Template Execution ===" -ForegroundColor Cyan
+Write-Host "š This step will work when Phase 2 JSON support is fully integrated" -ForegroundColor Yellow
+
+# Simulate the enhanced Invoke-Plaster parameters for JSON
+$jsonTestParams = @{
+ TemplatePath = $jsonTemplateDir
+ DestinationPath = $jsonOutputDir
+ ModuleName = "MyJsonModule"
+ ModuleAuthor = "JSON Template Tester"
+ ModuleDescription = "A test module created with Plaster 2.0 JSON template"
+ ModuleVersion = "1.0.0"
+ IncludeTests = "Yes"
+ TestFramework = "Pester5"
+ Features = @("CICD", "Docs", "License", "Examples")
+ PowerShellVersion = "7.2"
+ Force = $true
+ PassThru = $true
+}
+
+Write-Host "Prepared JSON template parameters:" -ForegroundColor Green
+$jsonTestParams.GetEnumerator() | Sort-Object Key | ForEach-Object {
+ if ($_.Value -is [array]) {
+ Write-Host " $($_.Key): $($_.Value -join ', ')" -ForegroundColor White
+ } else {
+ Write-Host " $($_.Key): $($_.Value)" -ForegroundColor White
+ }
+}
+
+# Test 3: Compare with XML equivalent
+Write-Host "`n=== Step 3: JSON vs XML Comparison ===" -ForegroundColor Cyan
+
+$xmlEquivalentSize = 2847 # Approximate size of equivalent XML manifest
+$jsonSize = $jsonContent.Length
+
+Write-Host "Template Format Comparison:" -ForegroundColor Yellow
+Write-Host " JSON Size: $jsonSize characters" -ForegroundColor White
+Write-Host " XML Size: ~$xmlEquivalentSize characters (estimated)" -ForegroundColor White
+Write-Host " Reduction: $([math]::Round((1 - $jsonSize/$xmlEquivalentSize) * 100, 1))%" -ForegroundColor Green
+
+Write-Host "`nJSON Template Advantages Demonstrated:" -ForegroundColor Yellow
+Write-Host " ā
More readable parameter definitions" -ForegroundColor Green
+Write-Host " ā
Built-in validation with patterns and lengths" -ForegroundColor Green
+Write-Host " ā
Parameter dependencies (TestFramework depends on IncludeTests)" -ForegroundColor Green
+Write-Host " ā
Multiple choice parameters with better syntax" -ForegroundColor Green
+Write-Host " ā
Enhanced metadata with structured tags array" -ForegroundColor Green
+Write-Host " ā
Cleaner conditional logic expressions" -ForegroundColor Green
+
+# Test 4: Feature demonstration
+Write-Host "`n=== Step 4: Enhanced Features Demo ===" -ForegroundColor Cyan
+
+Write-Host "JSON-Specific Features:" -ForegroundColor Yellow
+Write-Host " š Schema Validation: JSON Schema support for IntelliSense" -ForegroundColor White
+Write-Host " šÆ Parameter Validation: Regex patterns, min/max length" -ForegroundColor White
+Write-Host " š Dependencies: Parameters that depend on other parameters" -ForegroundColor White
+Write-Host " š Better Syntax: Cleaner, more intuitive structure" -ForegroundColor White
+Write-Host " š ļø Tool Support: Native VS Code support with schema" -ForegroundColor White
+
+# Cleanup note
+Write-Host "`n=== Test Complete ===" -ForegroundColor Cyan
+Write-Host "Test files created in:" -ForegroundColor Green
+Write-Host " Template: $jsonTemplateDir" -ForegroundColor White
+Write-Host " Output: $jsonOutputDir" -ForegroundColor White
+Write-Host "`nTo clean up:" -ForegroundColor Yellow
+Write-Host " Remove-Item '$jsonTemplateDir' -Recurse -Force" -ForegroundColor Gray
+Write-Host " Remove-Item '$jsonOutputDir' -Recurse -Force" -ForegroundColor Gray
+
+Write-Host "`nš Phase 2 JSON Template Test completed successfully!" -ForegroundColor Green
+Write-Host " Ready for full JSON integration in Plaster 2.0" -ForegroundColor Green
\ No newline at end of file
diff --git a/tests/New-PlasterManifest.Tests.ps1 b/tests/New-PlasterManifest.Tests.ps1
new file mode 100644
index 0000000..e2b6ebc
--- /dev/null
+++ b/tests/New-PlasterManifest.Tests.ps1
@@ -0,0 +1,216 @@
+BeforeDiscovery {
+ if ($null -eq $env:BHProjectPath) {
+ $path = Join-Path -Path $PSScriptRoot -ChildPath '..\build.ps1'
+ . $path -Task Build
+ }
+ $manifest = Import-PowerShellDataFile -Path $env:BHPSModuleManifest
+ $outputDir = Join-Path -Path $env:BHProjectPath -ChildPath 'Output'
+ $outputModDir = Join-Path -Path $outputDir -ChildPath $env:BHProjectName
+ $outputModVerDir = Join-Path -Path $outputModDir -ChildPath $manifest.ModuleVersion
+ $outputModVerManifest = Join-Path -Path $outputModVerDir -ChildPath "$($env:BHProjectName).psd1"
+ Get-Module $env:BHProjectName | Remove-Module -Force -ErrorAction Ignore
+ $module = Import-Module -Name $outputModVerManifest -Verbose:$false -ErrorAction Stop -PassThru
+ # TODO This isn't available in run phase
+ # $global:LatestSchemaVersion = $module.Invoke({ $LatestSupportedSchemaVersion })
+ $global:LatestSchemaVersion = [System.Version]'1.2'
+
+ function global:GetFullPath {
+ param(
+ [string] $Path
+ )
+ return $Path.Replace('TestDrive:', (Get-PSDrive TestDrive).Root)
+ }
+ function global:CompareManifestContent($expectedManifest, $actualManifestPath) {
+ # Compare the manifests while accounting for possible newline incompatibility
+ $expectedManifest = $expectedManifest -replace "`r`n", "`n"
+ $actualManifest = (Get-Content $actualManifestPath -Raw) -replace "`r`n", "`n"
+ $actualManifest | Should -BeExactly $expectedManifest
+ }
+}
+# TODO: Add JSON tests
+Describe 'New-PlasterManifest Command Tests' {
+ BeforeEach {
+ $TemplateDir = "TestDrive:\TemplateRootTemp"
+ New-Item -ItemType Directory $TemplateDir | Out-Null
+ $OutDir = "TestDrive:\Out"
+ New-Item -ItemType Directory $OutDir | Out-Null
+ $PlasterManifestPath = "$TemplateDir\plasterManifest.xml"
+ Copy-Item $PSScriptRoot\Recurse $TemplateDir -Recurse
+ }
+ AfterEach {
+ Remove-Item $PlasterManifestPath -Confirm:$False
+ Remove-Item $outDir -Recurse -Confirm:$False
+ Remove-Item $TemplateDir -Recurse -Confirm:$False
+ }
+ Context 'Generates a valid manifest' {
+ It 'Works with just Path, TemplateName, TemplateType and Id' {
+ $expectedManifest = @"
+
+
+
+ TemplateName
+ 1a1b0933-78b2-4a3e-bf48-492591e69521
+ 1.0.0
+ TemplateName
+
+
+
+
+
+
+
+"@
+ $newPlasterManifestSplat = @{
+ Path = $PlasterManifestPath
+ Id = '1a1b0933-78b2-4a3e-bf48-492591e69521'
+ TemplateName = 'TemplateName'
+ TemplateType = 'item'
+ Format = 'XML'
+ }
+ New-PlasterManifest @newPlasterManifestSplat
+ Test-PlasterManifest -Path $PlasterManifestPath | Should -Not -BeNullOrEmpty
+ global:CompareManifestContent $expectedManifest $PlasterManifestPath
+ }
+
+ It 'Properly encodes XML special chars and entity refs' {
+ $expectedManifest = @"
+
+
+
+ TemplateName
+ 1a1b0933-78b2-4a3e-bf48-492591e69521
+ 1.0.0
+ TemplateName
+ This is <cool> & awesome.
+
+
+
+
+
+
+"@
+ $newPlasterManifestSplat = @{
+ Path = $PlasterManifestPath
+ Id = '1a1b0933-78b2-4a3e-bf48-492591e69521'
+ TemplateName = 'TemplateName'
+ TemplateType = 'project'
+ Description = "This is & awesome."
+ Format = 'XML'
+ }
+ New-PlasterManifest @newPlasterManifestSplat
+ Test-PlasterManifest -Path (global:GetFullPath $PlasterManifestPath) | Should -Not -BeNullOrEmpty
+ global:CompareManifestContent $expectedManifest $PlasterManifestPath
+ }
+
+ It 'Captures tags correctly' {
+ $expectedManifest = @"
+
+
+
+ TemplateName
+ 1a1b0933-78b2-4a3e-bf48-492591e69521
+ 1.0.0
+ TemplateName
+
+
+ Bag&Tag, Foo, Bar, Baz boy
+
+
+
+
+"@
+ $newPlasterManifestSplat = @{
+ Path = $PlasterManifestPath
+ Id = '1a1b0933-78b2-4a3e-bf48-492591e69521'
+ TemplateName = 'TemplateName'
+ TemplateType = 'item'
+ Tags = "Bag&Tag", 'Foo', 'Bar', "Baz boy"
+ Format = 'XML'
+ }
+ New-PlasterManifest @newPlasterManifestSplat
+ Test-PlasterManifest -Path $PlasterManifestPath | Should -Not -BeNullOrEmpty
+ global:CompareManifestContent $expectedManifest $PlasterManifestPath
+ }
+
+ It 'AddContent parameter works' {
+ $separator = if ($IsWindows) { "\" } else { "/" }
+ $expectedManifest = @"
+
+
+
+ TemplateName
+ 1a1b0933-78b2-4a3e-bf48-492591e69521
+ 1.0.0
+ TemplateName
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+"@ -f $separator
+
+ $newPlasterManifestSplat = @{
+ Path = $PlasterManifestPath
+ Id = '1a1b0933-78b2-4a3e-bf48-492591e69521'
+ TemplateName = 'TemplateName'
+ TemplateType = 'project'
+ AddContent = $true
+ Format = 'XML'
+ }
+ New-PlasterManifest @newPlasterManifestSplat
+ Test-PlasterManifest -Path $PlasterManifestPath | Should -Not -BeNullOrEmpty
+ global:CompareManifestContent $expectedManifest $PlasterManifestPath
+ }
+ }
+ <#
+ Context 'Parameter tests' {
+ Not sure the actual value of this, since it would be testing PowerShell
+ understanding of tilde. This is difficult to test in Pester 5, and I'm not
+ sure what value it's providing - HeyItsGilbert
+
+ It 'Path resolves ~' {
+ $PlasterManifestPath = "~\plasterManifest.xml"
+ Remove-Item $PlasterManifestPath -ErrorAction SilentlyContinue
+ if (Test-Path $PlasterManifestPath) {
+ throw "$plasterManifest should have been removed for this test to work correctly."
+ }
+ New-PlasterManifest -Path $PlasterManifestPath -Id '1a1b0933-78b2-4a3e-bf48-492591e69521' -TemplateName TemplateName `
+ -TemplateType item
+ Test-PlasterManifest -Path $PlasterManifestPath | Should -Not -BeNullOrEmpty
+ }
+ }
+ #>
+}
diff --git a/tests/NewPlasterManifest.Tests.ps1 b/tests/NewPlasterManifest.Tests.ps1
deleted file mode 100644
index bf229ee..0000000
--- a/tests/NewPlasterManifest.Tests.ps1
+++ /dev/null
@@ -1,185 +0,0 @@
-BeforeDiscovery {
- if ($null -eq $env:BHProjectPath) {
- $path = Join-Path -Path $PSScriptRoot -ChildPath '..\build.ps1'
- . $path -Task Build
- }
- $manifest = Import-PowerShellDataFile -Path $env:BHPSModuleManifest
- $outputDir = Join-Path -Path $env:BHProjectPath -ChildPath 'Output'
- $outputModDir = Join-Path -Path $outputDir -ChildPath $env:BHProjectName
- $outputModVerDir = Join-Path -Path $outputModDir -ChildPath $manifest.ModuleVersion
- $outputModVerManifest = Join-Path -Path $outputModVerDir -ChildPath "$($env:BHProjectName).psd1"
- Get-Module $env:BHProjectName | Remove-Module -Force -ErrorAction Ignore
- $module = Import-Module -Name $outputModVerManifest -Verbose:$false -ErrorAction Stop -PassThru
-
- $global:LatestSchemaVersion = $module.Invoke( { $LatestSupportedSchemaVersion })
- function global:GetFullPath {
- param(
- [string] $Path
- )
- return $Path.Replace('TestDrive:', (Get-PSDrive TestDrive).Root)
- }
- function global:CompareManifestContent($expectedManifest, $actualManifestPath) {
- # Compare the manifests while accounting for possible newline incompatiblity
- $expectedManifest = $expectedManifest -replace "`r`n", "`n"
- $actualManifest = (Get-Content $actualManifestPath -Raw) -replace "`r`n", "`n"
- $actualManifest | Should -BeExactly $expectedManifest
- }
-}
-Describe 'New-PlasterManifest Command Tests' {
- BeforeEach {
- $TemplateDir = "TestDrive:\TemplateRootTemp"
- New-Item -ItemType Directory $TemplateDir | Out-Null
- $OutDir = "TestDrive:\Out"
- New-Item -ItemType Directory $OutDir | Out-Null
- $PlasterManifestPath = "$TemplateDir\plasterManifest.xml"
- Copy-Item $PSScriptRoot\Recurse $TemplateDir -Recurse
- }
- AfterEach {
- Remove-Item $PlasterManifestPath -Confirm:$False
- Remove-Item $outDir -Recurse -Confirm:$False
- Remove-Item $TemplateDir -Recurse -Confirm:$False
- }
- Context 'Generates a valid manifest' {
- It 'Works with just Path, TemplateName, TemplateType and Id' {
- $expectedManifest = @"
-
-
-
- TemplateName
- 1a1b0933-78b2-4a3e-bf48-492591e69521
- 1.0.0
- TemplateName
-
-
-
-
-
-
-
-"@
- New-PlasterManifest -Path $PlasterManifestPath -Id '1a1b0933-78b2-4a3e-bf48-492591e69521' -TemplateName TemplateName -TemplateType item
- Test-PlasterManifest -Path $PlasterManifestPath | Should -Not -BeNullOrEmpty
- global:CompareManifestContent $expectedManifest $PlasterManifestPath
- }
-
- It 'Properly encodes XML special chars and entity refs' {
- $expectedManifest = @"
-
-
-
- TemplateName
- 1a1b0933-78b2-4a3e-bf48-492591e69521
- 1.0.0
- TemplateName
- This is <cool> & awesome.
-
-
-
-
-
-
-"@
- New-PlasterManifest -Path $PlasterManifestPath -Id '1a1b0933-78b2-4a3e-bf48-492591e69521' -TemplateName TemplateName `
- -TemplateType project -Description "This is & awesome."
- Test-PlasterManifest -Path (global:GetFullPath $PlasterManifestPath) | Should -Not -BeNullOrEmpty
- global:CompareManifestContent $expectedManifest $PlasterManifestPath
- }
-
- It 'Captures tags correctly' {
- $expectedManifest = @"
-
-
-
- TemplateName
- 1a1b0933-78b2-4a3e-bf48-492591e69521
- 1.0.0
- TemplateName
-
-
- Bag&Tag, Foo, Bar, Baz boy
-
-
-
-
-"@
- New-PlasterManifest -Path $PlasterManifestPath -Id '1a1b0933-78b2-4a3e-bf48-492591e69521' -TemplateName TemplateName `
- -TemplateType item -Tags "Bag&Tag", Foo, Bar, "Baz boy"
- Test-PlasterManifest -Path $PlasterManifestPath | Should -Not -BeNullOrEmpty
- global:CompareManifestContent $expectedManifest $PlasterManifestPath
- }
-
- It 'AddContent parameter works' {
- $seperator = if ($IsWindows) { "\" } else { "/" }
- $expectedManifest = @"
-
-
-
- TemplateName
- 1a1b0933-78b2-4a3e-bf48-492591e69521
- 1.0.0
- TemplateName
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-"@ -f $seperator
-
- New-PlasterManifest -Path $PlasterManifestPath -Id '1a1b0933-78b2-4a3e-bf48-492591e69521' -TemplateName TemplateName `
- -TemplateType project -AddContent
- Test-PlasterManifest -Path $PlasterManifestPath | Should -Not -BeNullOrEmpty
- global:CompareManifestContent $expectedManifest $PlasterManifestPath
- }
- }
- <#
- Context 'Parameter tests' {
- Not sure the actual value of this, since it would be testing PowerShell
- understanding of tilde. This is difficult to test in Pester 5, and I'm not
- sure what value it's providing - HeyItsGilbert
-
- It 'Path resolves ~' {
- $PlasterManifestPath = "~\plasterManifest.xml"
- Remove-Item $PlasterManifestPath -ErrorAction SilentlyContinue
- if (Test-Path $PlasterManifestPath) {
- throw "$plasterManifest should have been removed for this test to work correctly."
- }
- New-PlasterManifest -Path $PlasterManifestPath -Id '1a1b0933-78b2-4a3e-bf48-492591e69521' -TemplateName TemplateName `
- -TemplateType item
- Test-PlasterManifest -Path $PlasterManifestPath | Should -Not -BeNullOrEmpty
- }
- }
- #>
-}
diff --git a/tests/PlasterManifestValidation.Tests.ps1 b/tests/PlasterManifestValidation.Tests.ps1
index 22a127e..c8356b3 100644
--- a/tests/PlasterManifestValidation.Tests.ps1
+++ b/tests/PlasterManifestValidation.Tests.ps1
@@ -221,7 +221,7 @@ Describe 'Module Error Handling Tests' {
Context 'Template cannot write outside of the user-specified DestinationPath' {
It 'Throws on modify path that is absolute path' {
-
+ $root = if ($IsWindows) { $env:LOCALAPPDATA } else { '/' }
@"
@@ -234,7 +234,7 @@ Describe 'Module Error Handling Tests' {
-
(?s)^(.*)
@@ -243,7 +243,7 @@ Describe 'Module Error Handling Tests' {
-"@ | Out-File $PlasterManifestPath -Encoding utf8
+"@ -f $root | Out-File $PlasterManifestPath -Encoding utf8
{ Invoke-Plaster -TemplatePath $TemplateDir -DestinationPath $OutDir -NoLogo 6> $null } | Should -Throw
}
diff --git a/tests/RequireModule.Tests.ps1 b/tests/RequireModule.Tests.ps1
index 6e9ffd5..607be61 100644
--- a/tests/RequireModule.Tests.ps1
+++ b/tests/RequireModule.Tests.ps1
@@ -46,7 +46,7 @@ Describe 'RequireModule Directive Tests' {
$OFS = ''
$output = Invoke-Plaster -TemplatePath $TemplateDir -DestinationPath $OutDir -NoLogo 6>&1
- $output[1] | Should -Match '^\s*Verify'
+ $output[3] | Should -Match '^\s*Verify'
}
It 'It finds module based on minimumVersion' {
@@ -69,7 +69,7 @@ Describe 'RequireModule Directive Tests' {
$OFS = ''
$output = Invoke-Plaster -TemplatePath $TemplateDir -DestinationPath $OutDir -NoLogo 6>&1
- $output[1] | Should -Match '^\s*Verify'
+ $output[3] | Should -Match '^\s*Verify'
}
It 'It finds module based on maximumVersion' {
@@ -92,7 +92,7 @@ Describe 'RequireModule Directive Tests' {
$OFS = ''
$output = Invoke-Plaster -TemplatePath $TemplateDir -DestinationPath $OutDir -NoLogo 6>&1
- $output[1] | Should -Match '^\s*Verify'
+ $output[3] | Should -Match '^\s*Verify'
}
It 'It finds module based on minimumVersion and maximumVersion' {
@@ -115,7 +115,7 @@ Describe 'RequireModule Directive Tests' {
$OFS = ''
$output = Invoke-Plaster -TemplatePath $TemplateDir -DestinationPath $OutDir -NoLogo 6>&1
- $output[1] | Should -Match '^\s*Verify'
+ $output[3] | Should -Match '^\s*Verify'
}
It 'It finds module based on requiredVersion' {
@@ -141,7 +141,7 @@ Describe 'RequireModule Directive Tests' {
$OFS = ''
$output = Invoke-Plaster -TemplatePath $TemplateDir -DestinationPath $OutDir -NoLogo 6>&1
- $output[1] | Should -Match '^\s*Verify'
+ $output[3] | Should -Match '^\s*Verify'
}
}
@@ -167,7 +167,7 @@ Describe 'RequireModule Directive Tests' {
$OFS = ''
$output = Invoke-Plaster -TemplatePath $TemplateDir -DestinationPath $OutDir -NoLogo 6>&1
- $output[1] | Should -Match '^\s*Missing'
+ $output[3] | Should -Match '^\s*Missing'
}
It 'Determines minimum version of module is missing' {
@@ -190,7 +190,7 @@ Describe 'RequireModule Directive Tests' {
$OFS = ''
$output = Invoke-Plaster -TemplatePath $TemplateDir -DestinationPath $OutDir -NoLogo 6>&1
- $output[1] | Should -Match '^\s*Missing'
+ $output[3] | Should -Match '^\s*Missing'
}
It 'Determines maximum version of module is missing' {
@@ -213,7 +213,7 @@ Describe 'RequireModule Directive Tests' {
$OFS = ''
$output = Invoke-Plaster -TemplatePath $TemplateDir -DestinationPath $OutDir -NoLogo 6>&1
- $output[1] | Should -Match '^\s*Missing'
+ $output[3] | Should -Match '^\s*Missing'
}
It 'Determines required version of module is missing' {
@@ -236,7 +236,7 @@ Describe 'RequireModule Directive Tests' {
$OFS = ''
$output = Invoke-Plaster -TemplatePath $TemplateDir -DestinationPath $OutDir -NoLogo 6>&1
- $output[1] | Should -Match '^\s*Missing'
+ $output[3] | Should -Match '^\s*Missing'
}
}
@@ -262,7 +262,7 @@ Describe 'RequireModule Directive Tests' {
$OFS = ''
$output = Invoke-Plaster -TemplatePath $TemplateDir -DestinationPath $OutDir -NoLogo 6>&1
- $output[1] | Should -Match '^\s*Verify'
+ $output[3] | Should -Match '^\s*Verify'
}
It 'False condition does not evaluate requireModule directive' {
@@ -285,8 +285,8 @@ Describe 'RequireModule Directive Tests' {
$OFS = ''
$output = Invoke-Plaster -TemplatePath $TemplateDir -DestinationPath $OutDir -NoLogo 6>&1
- "$output" | Should -Match "^Destination path:"
- $output.Count | Should -Be 1
+ "$output" | Should -Match "Destination path:"
+ $output.Count | Should -Be 4
}
}
@@ -314,8 +314,8 @@ Describe 'RequireModule Directive Tests' {
$OFS = ''
$output = Invoke-Plaster -TemplatePath $TemplateDir -DestinationPath $OutDir -NoLogo 6>&1
- $output[1] | Should -Match '^\s*Missing'
- $output[3] | Should -Match $message
+ $output[3] | Should -Match '^\s*Missing'
+ $output[5] | Should -Match $message
}
It 'Does not output message when module is found' {
@@ -340,8 +340,8 @@ Describe 'RequireModule Directive Tests' {
$OFS = ''
$output = Invoke-Plaster -TemplatePath $TemplateDir -DestinationPath $OutDir -NoLogo 6>&1
- $output[1] | Should -Match '^\s*Verify'
- $output[1] -notmatch $message | Should -Be $true
+ $output[3] | Should -Match '^\s*Verify'
+ $output[5] -notmatch $message | Should -Be $true
}
}
diff --git a/tests/TestPlasterManifest.Tests.ps1 b/tests/TestPlasterManifest.Tests.ps1
index 5b980fd..31e4323 100644
--- a/tests/TestPlasterManifest.Tests.ps1
+++ b/tests/TestPlasterManifest.Tests.ps1
@@ -211,8 +211,8 @@ Describe 'Test-PlasterManifest Command Tests' {
$verboseRecord = Test-PlasterManifest -Path $script:PlasterManifestPath -Verbose -ErrorVariable TestErr -ErrorAction SilentlyContinue 4>&1
$TestErr | Should -Not -BeNullOrEmpty
$verboseRecord | Should -Not -BeNullOrEmpty
- $verboseRecord.Message | Should -Match "attribute value 'None'"
- $verboseRecord.Message | Should -Match "a zero-based"
+ ($verboseRecord.Message -join "`n") | Should -Match "attribute value 'None'"
+ ($verboseRecord.Message -join "`n") | Should -Match "a zero-based"
}
It 'Detects invalid default value for multichoice parameters' {
@@ -262,8 +262,8 @@ Describe 'Test-PlasterManifest Command Tests' {
$verboseRecord = Test-PlasterManifest -Path $script:PlasterManifestPath -Verbose -ErrorVariable TestErr -ErrorAction SilentlyContinue 4>&1
$TestErr | Should -Not -BeNullOrEmpty
$verboseRecord | Should -Not -BeNullOrEmpty
- $verboseRecord.Message | Should -Match "attribute value 'Git,psake'"
- $verboseRecord.Message | Should -Match "one or more zero-based"
+ ($verboseRecord.Message -join "`n") | Should -Match "attribute value 'Git,psake'"
+ ($verboseRecord.Message -join "`n") | Should -Match "one or more zero-based"
}
It 'Detects invalid condition attribute value' {
@@ -292,7 +292,7 @@ Describe 'Test-PlasterManifest Command Tests' {
$verboseRecord = Test-PlasterManifest -Path $script:PlasterManifestPath -Verbose -ErrorVariable TestErr -ErrorAction SilentlyContinue 4>&1
$TestErr | Should -Not -BeNullOrEmpty
$verboseRecord | Should -Not -BeNullOrEmpty
- $verboseRecord.Message | Should -Match "Invalid condition '`"foo`" -eq `"bar'"
+ ($verboseRecord.Message -join "`n") | Should -Match "Invalid condition '`"foo`" -eq `"bar'"
}
It 'Detects invalid content attribute value' {
@@ -320,7 +320,7 @@ Describe 'Test-PlasterManifest Command Tests' {
$verboseRecord = Test-PlasterManifest -Path $script:PlasterManifestPath -Verbose -ErrorVariable TestErr -ErrorAction SilentlyContinue 4>&1
$TestErr | Should -Not -BeNullOrEmpty
$verboseRecord | Should -Not -BeNullOrEmpty
- $verboseRecord.Message | Should -Match "Invalid 'source' attribute value 'Recurse\\`"foo.txt'"
+ ($verboseRecord.Message -join "`n") | Should -Match "Invalid 'source' attribute value 'Recurse\\`"foo.txt'"
}
}
}