PsKrane.psm1
Enum ProjectType { Module Script } Enum ItemFileType{ Class PublicFunction PrivateFunction } Enum KraneTemplateType{ Class Function Script Test } Class KraneFile { #CraneFile is a class that represents the .Krane.json file that is used to store the configuration of the Krane project. [System.IO.FileInfo]$Path [System.Collections.Hashtable]$Data = @{} [Bool]$IsPresent KraneFile([String]$Path) { #Handeling case when Path doesn't exists yet (For creation scenarios) $Root = "" if ((Test-Path -Path $Path) -eq $False) { if ($Path.EndsWith(".krane.json")) { [System.Io.DirectoryInfo]$Root = ([System.Io.FileInfo]$Path).Directory } else { [System.Io.DirectoryInfo]$Root = $Path } } else { #Path exists. We need to determine if it is a file or a folder. $Item = Get-Item -Path $Path if ($Item.PSIsContainer) { $Root = $Item } else { $Root = $Item.Directory } } $this.Path = Join-Path -Path $Root.FullName -ChildPath ".krane.json" $this.IsPresent = $this.Path.Exists if (!$this.Path.Exists) { #Krane file doesn't exists. No point in importing data from a file that doesn't exists. $this.Data = @{} return } $Raw = Get-Content -Path $This.Path.FullName -Raw | ConvertFrom-Json #Convert the JSON to a hashtable as it is easier to manipulate. foreach ($key in $Raw.PsObject.Properties) { $this.Data.$($key.Name) = $key.Value } } [String]Get([String]$Key) { return $this.Data.$Key } [Void]Set([String]$Key, [String]$Value) { $this.Data.$Key = $Value } [Void]Save() { if (!($this.Path.Exists)) { $Null = [System.Io.Directory]::CreateDirectory($this.Path.Directory.FullName) | Out-Null } $this.Data | ConvertTo-Json | Out-File -FilePath $this.Path.FullName -Encoding utf8 -Force $this.Path.Refresh() $this.IsPresent = $this.File.Exists } [void]Fetch() { $Raw = Get-Content -Path $This.Path.FullName -Raw | ConvertFrom-Json #Convert the JSON to a hashtable as it is easier to manipulate. foreach ($key in $Raw.PsObject.Properties) { $this.Data.$($key.Name) = $key.Value } $this.Path.Refresh() $this.IsPresent = $this.Path.Exists } [String]ToString() { return "ProjectName:{0} ProjectType:{1}" -f $this.Get("Name"), $this.Get("ProjectType") } static [KraneFile] Create([System.IO.DirectoryInfo]$Path, [String]$Name, [ProjectType]$Type) { $KraneFile = [KraneFile]::New($Path) if ($KraneFile.Path.Exists) { Throw ".Krane File $($KraneFile.Path.FullName) already exists" } $KraneFile.Set("Name", $Name) $KraneFile.Set("ProjectType", $Type) $KraneFile.Set("ProjectVersion", "0.0.1") $KraneFile.Save() Return $KraneFile } } Class KraneProject { [System.IO.DirectoryInfo]$Root [KraneFile]$KraneFile [ProjectType]$ProjectType [String]$ProjectVersion [System.IO.DirectoryInfo]$TemplatesPath [KraneTemplateCollection]$Templates = [KraneTemplateCollection]::New() KraneProject() {} KraneProject([System.IO.DirectoryInfo]$Root) { $this.KraneFile = [KraneFile]::New($Root) $this.ProjectVersion = $this.KraneFile.Get("ProjectVersion") } AddItem([String]$Name, [String]$Type) { throw "Must be overwritten!" #Add an item to the project. The item can be a script, a module, a test, etc. } hidden [void] LoadTemplates() { #Load the templates from the templates folder $AllModuleTemplates = Get-ChildItem -Path "$($PSScriptRoot)\Templates" -Filter "*.KraneTemplate.ps1" foreach ($TemplateFile in $AllModuleTemplates) { $Template = [KraneTemplate]::New($TemplateFile) $Template.SetLocation([LocationType]::Module) $this.Templates.Templates.Add($Template) } $AllSystemTemplates = $null if ($global:PSVersionTable.os -match '^.*Windows.*$' ) { $AllSystemTemplates = Get-ChildItem -Path "$($env:ProgramData)\PsKrane\Templates" -Filter "*.KraneTemplate.ps1" -ErrorAction SilentlyContinue } elseif ($env:IsLinux) { $AllSystemTemplates = Get-ChildItem -Path " /opt/PsKrane/Templates" -Filter "*.KraneTemplate.ps1" -ErrorAction SilentlyContinue } elseif ($env:IsMacOS) { $AllSystemTemplates = Get-ChildItem -Path "/Applications/PsKrane/Templates" -Filter "*.KraneTemplate.ps1" -ErrorAction SilentlyContinue } foreach ($TemplateFile in $AllSystemTemplates) { $Template = [KraneTemplate]::New($TemplateFile) $Template.SetLocation([LocationType]::Customer) $this.Templates.Templates.Add($Template) } $ProjectRootFolder = $this.Root.FullName [System.Io.DirectoryInfo] $TemplatesProjectFolder = Join-Path -Path $ProjectRootFolder -ChildPath "Krane/Templates" if ($TemplatesProjectFolder.Exists) { $AllProjectTemplates = Get-ChildItem -Path $TemplatesProjectFolder.FullName -Filter "*.KraneTemplate.ps1" foreach ($TemplateFile in $AllProjectTemplates) { $Template = [KraneTemplate]::New($TemplateFile) $Template.SetLocation([LocationType]::Project) $this.Templates.Templates.Add($Template) } } } [System.Io.DirectoryInfo] GetTestsFolderPath() { return Join-Path -Path $this.Root.FullName -ChildPath "Tests" } [System.Io.DirectoryInfo] GetSourcesFolderPath() { return Join-Path -Path $this.Root.FullName -ChildPath "Sources" } [System.Io.DirectoryInfo] GetOutputsFolderPath() { return Join-Path -Path $this.Root.FullName -ChildPath "Outputs" } [System.Io.DirectoryInfo] GetBuildFolderPath() { return Join-Path -Path $this.Root.FullName -ChildPath "Outputs" } } Class KraneFileStructure { [System.IO.DirectoryInfo]$RootFolder [System.IO.DirectoryInfo]$BuildFolder [System.IO.DirectoryInfo]$SourcesFolder [System.IO.DirectoryInfo]$TestsFolder [System.IO.DirectoryInfo]$OutputsFolder [System.IO.DirectoryInfo]$Modulefolder [System.IO.DirectoryInfo]$NugetFolder KraneFileStructure([System.IO.DirectoryInfo]$Root) { $this.RootFolder = $Root $this.BuildFolder = Join-Path -Path $Root.FullName -ChildPath "Build" $this.SourcesFolder = Join-Path -Path $Root.FullName -ChildPath "Sources" $this.TestsFolder = Join-Path -Path $Root.FullName -ChildPath "Tests" $this.OutputsFolder = Join-Path -Path $Root.FullName -ChildPath "Outputs" $this.Modulefolder = Join-Path -Path $this.OutputsFolder.FullName -ChildPath "Module" $this.NugetFolder = Join-Path -Path $this.OutputsFolder.FullName -ChildPath "Nuget" } [string] ToString(){ return "RootFolder -> {0}" -f " $($this.RootFolder.FullName)" } } Class KraneModule : KraneProject { [String]$ModuleName hidden [System.IO.FileInfo]$ModuleFile hidden [System.IO.FileInfo]$ModuleDataFile hidden [System.IO.DirectoryInfo]$Build hidden [System.IO.DirectoryInfo]$Sources hidden [System.IO.DirectoryInfo]$Tests hidden [System.IO.DirectoryInfo]$Outputs [String[]] $Tags = @( 'PSEdition_Core', 'PSEdition_Desktop' ) [String]$Description [String]$ProjectUri [Bool]$IsGitInitialized [PsModule]$PsModule [TestHelper]$TestData [KraneFileStructure]$FileStructure Hidden [System.Collections.Hashtable]$ModuleData = @{} #Add option Overwrite KraneModule([System.IO.DirectoryInfo]$Root) { #When the module Name is Not passed, we assume that a .Krane.json file is already present at the root. Write-Debug "[KraneModule][Constructor([System.IO.DirectoryInfo]Root)] Start" Write-Debug "[KraneModule][Constructor([System.IO.DirectoryInfo]Root)] Creating module from root -> $($Root.FullName)" $this.KraneFile = [KraneFile]::New($Root) $this.ProjectType = [ProjectType]::Module #TODO Remove root, build, sources, tests and ouptuts and use $this.FilteStructure instead $this.Root = $Root $this.Build = "$($Root.FullName)\Build" $this.Sources = "$($Root.FullName)\Sources" $this.Tests = "$($Root.FullName)\Tests" $this.Outputs = "$($Root.FullName)\Outputs" $this.FileStructure = [KraneFileStructure]::New($Root) $this.LoadTemplates() #get the module name from the krane file $this.TestData = [PesterTestHelper]::New($this.Tests) $this.SetDescription("Created with Love using PsKrane") $mName = $this.KraneFile.Get("Name") $this.SetModuleName($mName) $this.ProjectType = $this.KraneFile.Get("ProjectType") $this.FetchModuleInfo() $this.FetchGitInitStatus() Write-Debug "[KraneModule][Constructor([System.IO.DirectoryInfo]Root)] End" } KraneModule([System.IO.DirectoryInfo]$Root, [String]$ModuleName) { #When the module Name is passed, we assume that the module is being created, and that there is not a .Krane.json file present. yet. #$this.KraneFile = [KraneFile]::New($Root) $Root = Join-Path -Path $Root -ChildPath $ModuleName $This.KraneFile = [KraneFile]::Create($Root, $ModuleName, [ProjectType]::Module) $this.ProjectType = [ProjectType]::Module #TODO Remove root, build, sources, tests and ouptuts and use $this.FilteStructure instead $this.Root = $Root $this.Build = "$Root\Build" $this.Sources = "$Root\Sources" $this.Tests = "$Root\Tests" $this.Outputs = "$Root\Outputs" $this.ModuleName = $ModuleName $this.SetDescription("Created with Love using PsKrane") $this.FileStructure = [KraneFileStructure]::New($Root) $this.ProjectVersion = $this.GetProjectVersion() $this.LoadTemplates() $this.FetchModuleInfo() $this.FetchGitInitStatus() } hidden [void] FetchModuleInfo() { if (($null -eq $this.ModuleName)) { Throw "Module Name not provided." } $this.SetModuleName($this.ModuleName) if ($this.ModuleDataFile.Exists) { $this.ModuleData = Import-PowerShellDataFile -Path $this.ModuleDataFile.FullName $this.Description = $this.ModuleData.Description $this.ProjectUri = $this.ModuleData.PrivateData.PsData.ProjectUri $this.Tags = $this.ModuleData.PrivateData.PsData.Tags } $this.PsModule = [PsModule]::New($this.ModuleFile.FullName) } [void] BuildModule() { Write-Verbose "[KraneModule][BuildModule] Start" Write-Verbose "[KraneModule][BuildModule][PSM1] Starting PSM1 Operations $($this.ModuleName)" if ($this.ModuleFile.Exists) { Write-Verbose "[KraneModule][BuildModule][PSM1] Module file already exists. Deleting." $this.ModuleFile.Delete() $this.ModuleFile.Refresh() } Write-Verbose "[KraneModule][BuildModule][PSM1] (Re)creating file $($this.ModuleFile.FullName)" $Null = New-Item -Path $this.ModuleFile.FullName -ItemType "file" -Force $MainPSM1Contents = @() Write-Verbose "[KraneModule][BuildModule][PSM1] Searching for classes and functions" [System.IO.FileInfo]$PreContentPath = Join-Path -Path $this.Sources.FullName -ChildPath "PreContent.ps1" If ($PrecontentPath.Exists) { Write-Verbose "[KraneModule][BuildModule][PSM1] Precontent.ps1 file found. Adding to module file." $MainPSM1Contents += $PreContentPath } else { Write-Verbose "[KraneModule][BuildModule][PSM1] No Precontent detected." } [System.IO.DirectoryInfo]$ClassFolderPath = Join-Path -Path $this.Sources.FullName -ChildPath "Classes" If ($ClassFolderPath.Exists) { $PublicClasses = Get-ChildItem -Path $ClassFolderPath.FullName -Filter *.ps1 | sort-object Name if ($PublicClasses) { write-Verbose "[KraneModule][BuildModule][PSM1] Classes Found. Importing..." $MainPSM1Contents += $PublicClasses } } [System.IO.DirectoryInfo]$PrivateFunctionsFolderPath = Join-Path -Path $this.Sources.FullName -ChildPath "Functions/Private" If ($PrivateFunctionsFolderPath.Exists) { $Privatefunctions = Get-ChildItem -Path $PrivateFunctionsFolderPath.FullName -Filter *.ps1 | sort-object Name if ($Privatefunctions) { write-Verbose "[KraneModule][BuildModule][PSM1] Private functions Found. Importing..." $MainPSM1Contents += $Privatefunctions } } $Publicfunctions = $null [System.IO.DirectoryInfo]$PublicFunctionsFolderPath = Join-Path -Path $this.Sources.FullName -ChildPath "Functions/Public" If ($PublicFunctionsFolderPath.Exists) { $Publicfunctions = Get-ChildItem -Path $PublicFunctionsFolderPath.FullName -Filter *.ps1 | sort-object Name if ($Publicfunctions) { write-Verbose "[KraneModule][BuildModule][PSM1] Public functions Found. Importing..." $MainPSM1Contents += $Publicfunctions } } [System.IO.FileInfo]$PostContentPath = Join-Path -Path $this.Sources.FullName -ChildPath "postContent.ps1" If ($PostContentPath.Exists) { write-Verbose "[KraneModule][BuildModule][PSM1] Postcontent Found. Importing..." $MainPSM1Contents += $PostContentPath } #Creating PSM1 write-Verbose "[KraneModule][BuildModule][PSM1] Building PSM1 content" Foreach ($file in $MainPSM1Contents) { write-Verbose "[KraneModule][BuildModule][PSM1] Adding -> $($File.FullName)" Get-Content $File.FullName | out-File -FilePath $this.ModuleFile.FullName -Encoding utf8 -Append } Write-verbose "[KraneModule][BuildModule][PSD1] Starding PSD1 actions. Adding functions to export" if (!$this.ModuleDataFile.Exists) { Write-verbose "[KraneModule][BuildModule][PSD1] Module Manifest not found. Creating one." New-ModuleManifest -Path $this.ModuleDataFile.FullName } $ManifestParams = @{} $ManifestParams.Path = $this.ModuleDataFile.FullName $ManifestParams.FunctionsToExport = $Publicfunctions.BaseName $ManifestParams.Tags = $This.Tags $ManifestParams.RootModule = $this.ModuleFile.Name if($this.ProjectUri) { $ManifestParams.ProjectUri = $this.ProjectUri } if($this.Description) { $ManifestParams.Description = $this.Description } $ManifestParams.ModuleVersion = $this.ProjectVersion Write-verbose "[KraneModule][BuildModule][PSD1] Writing Manifest settings:" foreach ($ManifestSetting in $ManifestParams.GetEnumerator()) { Write-Verbose "[KraneModule][BuildModule][PSD1][Setting] $($ManifestSetting.Key) -> $($ManifestSetting.Value)" } try { Update-ModuleManifest @ManifestParams } Catch { Write-Error "[KraneModule][BuildModule][PSD1] Error updating module manifest. $_" } Write-Verbose "[KraneModule][BuildModule] End" } [void] SetModuleName([String]$ModuleName) { $this.ModuleName = $ModuleName $this.ModuleFile = Join-Path -Path $this.Outputs.FullName -ChildPath "Module\$($ModuleName).psm1" $this.ModuleDataFile = Join-Path -Path $this.Outputs.FullName -ChildPath "Module\$($ModuleName).psd1" } [void] CreateBaseStructure() { if ($this.Outputs.Exists -eq $false) { $Null = New-Item -Path $this.Outputs.FullName -ItemType "directory" } if ($this.Build.Exists -eq $false) { $Null = New-Item -Path $this.Build.FullName -ItemType "directory" } if ($this.Sources.Exists -eq $false) { $Null = New-Item -Path $this.Sources.FullName -ItemType "directory" } [System.IO.DirectoryInfo] $PrivateFunctions = Join-Path -Path $this.Sources.FullName -ChildPath "Functions/Private" if ($PrivateFunctions.Exists -eq $false) { $Null = New-Item -Path $PrivateFunctions.FullName -ItemType "directory" } [System.IO.DirectoryInfo] $PublicFunctions = Join-Path -Path $this.Sources.FullName -ChildPath "Functions/Public" if ($PublicFunctions.Exists -eq $false) { $Null = New-Item -Path $PublicFunctions.FullName -ItemType "directory" } if ($this.Tests.Exists -eq $false) { $Null = New-Item -Path $this.Tests.FullName -ItemType "directory" } } [void]ReverseBuild([bool]$Force) { #ReverseBuild will take the module file and extract the content to the sources folder. Write-Debug "[KraneModule][ReverseBuild([bool]Force)] Start" $this.PsModule.ReverseBuild($Force) Write-Debug "[KraneModule][ReverseBuild([bool]Force)] End" } [void]ReverseBuild([string]$Name,[bool]$Force) { #ReverseBuild will take the module file and extract the content to the sources folder. Write-Debug "[KraneModule][ReverseBuild([string]Name,[bool]Force)] Start" $this.PsModule.ReverseBuild($Name,$Force) Write-Debug "[KraneModule][ReverseBuild([string]Name,[bool]Force)] End" } [string]GetProjectVersion() { return $this.KraneFile.Get("ProjectVersion") } #TODO: Check if this can be removed. Not sure this is actually used Fetch() { if ($this.Build.Exists) { $e = Import-PowerShellDataFile -Path $this.Build.FullName $this.ProjectVersion = $this.setProjectVersion($e.ModuleVersion) } } hidden [void]SetProjectVersion($Version) { $this.KraneFile.Set("ProjectVersion", $Version) $this.KraneFile.Save() } [Void] FetchGitInitStatus() { [System.IO.DirectoryInfo]$GitFolderpath = join-Path -Path $this.Root.FullName -ChildPath ".git\" $this.IsGitInitialized = $GitFolderpath.Exists } [void] AddItem([String]$Name, [ItemFileType]$Type) { #Add an item to the project. The item can be a script, a module, a test, etc. switch ($Type) { "Class" { $this.AddClass($Name) } "PublicFunction" { $this.AddPublicFunction($Name) } "PrivateFunction" { $this.AddPrivateFunction($Name) } "Test" { $this.AddTest($Name) } default { Throw "Type $Type not supported" } } } hidden AddClass([String]$Name, [String]$Content) { $ClassRootFolder = Join-Path -Path $this.Sources.FullName -ChildPath "Classes" [System.IO.FileInfo] $ClassPath = Join-Path -Path $ClassRootFolder -ChildPath "$Name.ps1" if ($ClassPath.Exists) { Throw "Class $Name already exists" } $Null = New-Item -Path $ClassPath.FullName -ItemType "file" -Value $Content -Force $this.PsModule.GetAstClasses($ClassRootFolder) } hidden AddPublicFunction([String]$Name, [String]$Content) { [System.IO.FileInfo] $FunctionPath = Join-Path -Path $this.Sources.FullName -ChildPath "Functions\Public\$Name.ps1" if ($FunctionPath.Exists) { Throw "Function $Name already exists at $($FunctionPath.FullName)" } $Null = New-Item -Path $FunctionPath.FullName -ItemType "file" -Value $Content $this.PsModule.GetASTFunctions($FunctionPath) } hidden AddPrivateFunction([String]$Name, [String]$Content) { [System.IO.FileInfo]$FunctionPath = Join-Path -Path $this.Sources.FullName -ChildPath "Functions\Private\$Name.ps1" if ($FunctionPath.Exists) { Throw "Function $Name already exists" } $Null = New-Item -Path $FunctionPath.FullName -ItemType "file" -Value $Content $this.PsModule.GetASTFunctions($FunctionPath) } [KraneTemplate[]] GetTemplate() { #Returns ALL existing templates return $this.Templates.GetTemplate() } [KraneTemplate] GetTemplate([KraneTemplateType]$TemplateType) { #Retrieve specific template by type $Template = $this.Templates | Where-Object { $_.Type -eq $TemplateType } if ($null -eq $Template) { Throw "Template '$TemplateType' not found" } Return $Template } [KraneTemplate[]] GetTemplate([String]$Type, [LocationType]$Location) { #Retrieves specific template by type and location. $Template = $this.Templates.GetTemplate($Type, $Location) if ($null -eq $Template) { Throw "Template '$Type' of location type '$Location' not found" } Return $Template } [KraneTemplate[]] GetTemplate([LocationType]$Location) { #Retrieves specific template by type and location. $Template = $this.Templates.GetTemplate($Location) Return $Template } [Void] ReloadAll(){ $this.ProjectVersion = $this.GetProjectVersion() $this.LoadTemplates() $this.FetchModuleInfo() $this.FetchGitInitStatus() } [Void]SetDescription([string]$Description){ $this.Description = $Description } } Class ModuleObfuscator { [String]$ModuleName [KraneModule]$Module [System.IO.DirectoryInfo]$Bin [System.IO.FileInfo]$BinaryModuleFile [System.IO.FileInfo]$ModuleDataFile Obfuscator() {} SetKraneModule([KraneModule]$Module) { $Module.ModuleDataFile.Refresh() if (!$Module.ModuleDataFile.Exists) { Write-Verbose "[BUILD][OBFUSCATE] Module data file Not found. Building module" $this.Module.BuildModule() } $this.Module = $Module $this.Bin = $Module.Outputs.FullName + "\Bin" $this.BinaryModuleFile = Join-Path -Path $this.Bin.FullName -ChildPath ($this.Module.ModuleFile.BaseName + ".dll") $this.ModuleDataFile = $this.Bin.FullName + "\" + $this.Module.ModuleName + ".psd1" } Obfuscate() { Write-Verbose "[BUILD][OBFUSCATE] Obfuscating module" Write-Verbose "[BUILD][OBFUSCATE] Starting psd1 operations" if (!$this.ModuleDataFile.Exists) { $this.Module.ModuleDataFile.CopyTo($this.ModuleDataFile.FullName) $this.ModuleDataFile.Refresh() } #Does seem to work. #Update-ModuleManifest -Path $this.ModuleDataFile.FullName -RootModule $this.BinaryModuleFile.Name $MdfContent = Get-Content -Path $this.ModuleDataFile.FullName $MdfContent.Replace($this.Module.ModuleFile.Name, $this.BinaryModuleFile.Name) | Set-Content -Path $this.ModuleDataFile.FullName #We obfuscate #Create the DLL in the Artifacts folder } } Class KraneFactory { static [KraneProject]GetProject([System.IO.FileInfo]$KraneFile) { $KraneDocument = [KraneFile]::New($KraneFile) $ProjectType = $KraneDocument.Get("ProjectType") $Root = $KraneFile.Directory switch ($ProjectType) { "Module" { write-verbose "[KraneFactory][GetProject] Returning root project of type Module $($Root.FullName)" $KM = [KraneModule]::New($Root) $KM.ProjectVersion = $KraneDocument.Get("ProjectVersion") return $KM } default { Throw "Project type $ProjectType not supported" } } Throw "Project type $ProjectType not supported" #For some strange reason, having the throw in the switch statement does no suffice for the compiler... } } Class NuSpecFile { [KraneModule]$KraneModule [String]$Version [System.IO.DirectoryInfo]$ExportFolderPath [System.IO.FileInfo]$NuSpecFilePath hidden [String]$RawContent NuspecFile([KraneModule]$KraneModule) { $this.SetKraneModule($KraneModule) $this.ExportFolderPath = Join-Path -Path $this.KraneModule.Outputs -ChildPath "Nuget" } [void] SetKraneModule([KraneModule]$KraneModule) { $this.KraneModule = $KraneModule } hidden [Void]Generate() { $psd1Data = Import-PowerShellDataFile -Path $this.KraneModule.ModuleDataFile.FullName $NuSpecString = @" <?xml version="1.0" encoding="utf-8"?> <package> <metadata> <id>{0}</id> <version>{1}</version> <authors>{2}</authors> <requireLicenseAcceptance>false</requireLicenseAcceptance> <license type="expression">MIT</license> <!-- <icon>icon.png</icon> --> <projectUrl>{3}</projectUrl> <description>{4}</description> <releaseNotes>{5}</releaseNotes> <copyright>Copyright All rights reserved</copyright> <tags>{6}</tags> <dependencies> </dependencies> </metadata> </package> "@ $Id = $this.KraneModule.ModuleName #0 $this.Version = $psd1Data.ModuleVersion #1 $Authors = $psd1Data.Author #2 $ProjectUri = $psd1Data.PrivateData.PsData.ProjectUri #3 $Description = $psd1Data.Description #4 $ReleaseNotes = $psd1Data.releaseNotes #4 $Tags = $psd1Data.PrivateData.PsData.tags -join "," #5 $Final = $NuSpecString -f $Id, $this.Version, $Authors, $ProjectUri, $Description, $ReleaseNotes, $Tags $this.RawContent = $Final } [void] CreateNuSpecFile() { $this.Generate() $Modulefolder = Join-Path -Path $this.KraneModule.Outputs.FullName -ChildPath "Module" if(-not (Test-Path $ModuleFolder)){ $null = New-Item -Path $ModuleFolder -ItemType Directory } $this.NuSpecFilePath = Join-Path -Path $Modulefolder -ChildPath ($this.KraneModule.ModuleName + ".nuspec") $null = $this.RawContent | Out-File -FilePath $this.NuspecFilePath.FullName -Encoding utf8 -Force } [void] CreateNugetFile() { if (!($this.ExportFolderPath.Exists)) { $this.ExportFolderPath.Create() } & nuget pack $this.NuSpecFilePath.FullName -OutputDirectory $this.ExportFolderPath } } Class PsScriptFile { [System.Io.FileInfo]$Path [String]$Content PsScriptFile(){} PsScriptFile([System.Io.FileInfo]$Path) { $this.Path = $Path if ($this.Path.Exists) { $this.Content = Get-Content -Path $this.Path.FullName -Raw } } } Class BuildScript : PsScriptFile { #Creates the build script that will be used to build the module and create the nuspec file BuildScript([KraneModule]$KraneModule) { $this.Path = Join-Path -Path $KraneModule.Build.FullName -ChildPath "Build.Krane.ps1" } BuildScript([System.Io.DirectoryInfo]$Path) { $this.Path = Join-Path -Path $Path.FullName -ChildPath "Build.Krane.ps1" if($this.Path.Exists){ $this.Content = Get-Content -Path $this.Path.FullName -Raw } } [void] CreateBuildScript() { $Content = @' # This script is used to invoke PsKrane and to build the module and create the nuspec file install-Module PsKrane -Repository PSGallery -Force import-Module PsKrane -Force $psr = $PSScriptRoot $Root = split-Path -Path $psr -Parent $KraneModule = Get-KraneProject -Root $Root $KraneModule.Description = "This module is a test module" $KraneModule.ProjectUri = "http://link.com" $KraneModule.BuildModule() New-KraneNugetFile -KraneModule $KraneModule -Force '@ $Content | Out-File -FilePath $this.Path.FullName -Encoding utf8 -Force } } Class TestScript : PsScriptFile { #A test script file that will be used to test launch tests [string]$Name [string]$Content TestScript([System.IO.FileInfo]$Path) { if(-not $Path.Exists) { Throw "Test script file $($Path.FullName) does not exist" } $this.Path = $Path $this.Name = $Path.Name.Replace(".Tests.ps1","") $this.Content = Get-Content -Path $Path.FullName -Raw } TestScript([KraneModule]$KraneModule, [String]$TestName) { $this.Name = $TestName if (!($TestName.Contains(".Tests.ps1"))) { $TestName = $TestName + ".Tests.ps1" } $this.Path = Join-Path -Path $KraneModule.Tests.FullName -ChildPath $TestName } [void] CreateTestScript() { Write-Debug "[PsKrane][TestScript][CreateTestScript()] Start" if (Test-Path $this.Path.FullName) { Write-debug "[PsKrane][TestScript][CreateTestScript]Test script $($this.Path.FullName) already exists" return } #Create the test script if ($this.Path.BaseName -match "^.*\.Tests$") { $ItemName = $this.Path.BaseName.Split(".")[0] }else{ $ItemName = $this.Path.BaseName } $this.Content = [TestScript]::GetTemplate($ItemName) Write-Debug "[PsKrane][TestScript][CreateTestScript]Creating Test script at -> $($this.Path.FullName)" $This.Content | Out-File -FilePath $this.Path.FullName -Encoding utf8 -Force Write-Debug "[PsKrane][TestScript][CreateTestScript] End" } static [string] GetTemplate([String]$ItemName) { $Template = @' Generated with love using PsKrane Import-Module PsKrane [System.IO.DirectoryInfo]$psroot = $PSScriptRoot $KraneProject = Get-KraneProject -Root $PsRoot.Parent Import-Module $($KraneProject.ModuleDataFile.FullName) -Force InModuleScope -ModuleName $KraneProject.ModuleName -ScriptBlock { Describe "[###ITEMNAME###] Testing" { it "Should return Plop" { $result = ###ITEMNAME### $result | Should -Be "Plop" } } } '@ $TemplateWithItemName = $Template -replace '###ITEMNAME###', $ItemName return $TemplateWithItemName } } Class GitHelper { [System.io.FileInfo]$Git GitHelper() { $GitCommand = Get-Command -Name "git" if ($null -eq $GitCommand) { Throw "Git not found. Please install git and make sure it is in the PATH" } Write-Verbose "[GitHelper] git command found at $($GitCommand.Source)" $this.Git = $GitCommand.Source } GitTag([string]$Tag) { try { Write-Verbose "[GitHelper][GitTag] tagging with value -> $tag" #& $this.Git.FullName tag -a $tag -m $tag $strOutput = & $this.Git.FullName tag -a $tag -m $tag 2>&1 if ($LASTEXITCODE -ne 0) { throw "Failed to write tag: $strOutput" } } catch { throw "Error creating tag $tag. $_" } } GitTag([string]$TagAnnotation, [String]$TagMessage) { try { Write-Verbose "[GitHelper][GitTag] tagging with anonotation -> $TagAnnotation and message $TagMessage" $strOutput = & $this.Git.FullName tag -a $TagAnnotation -m $TagMessage 2>&1 if ($LASTEXITCODE -ne 0) { throw "Failed to write tag: $strOutput" } } catch { throw "Error creating tag with annotation : $TagAnnotation and message: $TagMessage. error -> $_" } } GitCommit([string]$Message) { try { Write-Verbose "[GitHelper][GitCommit] commit with message -> $Message" $strOutput = & $this.Git.FullName commit -m $Message 2>&1 if ($LASTEXITCODE -ne 0) { throw "Failed to commit: $strOutput" } } catch { throw "Error creating commit. $_" } } GitAdd([string]$Path) { try { & $this.Git.FullName add $Path } catch { throw "Error adding $Path to git. $_" } } GitPushTags() { $strOutput = "" try { #& $this.Git.FullName push --tags Write-Verbose "[GitHelper][GitPushTags] pushing tags" $strOutput = & $this.Git.FullName push --tags -q 2>&1 if ($LASTEXITCODE -ne 0) { throw "LastExitcode: $LASTEXITCODE . Failed to push tags. Received output: $strOutput" } } catch { throw "Error pushing tags to git. output: $($strOutput). Error content: $_" } } GitPushWithTags() { try { Write-Verbose "[GitHelper][GitPushWithTags] pushing with tags" $strOutput = & $this.Git.FullName push --follow-tags 2>&1 if ($LASTEXITCODE -ne 0) { throw "Failed to push with tags: $strOutput" } } catch { throw "Error pushing with tags to git. $_" } } } Class PsFunction { [string]$Name [bool]$IsPrivate [bool]$HasCommentBasedHelp [bool]$HasTest [System.Io.FileInfo]$TestPath $CommentBasedHelp [System.Io.FileInfo]$Path hidden $RawAst PsFunction([System.Management.Automation.Language.FunctionDefinitionAst]$FunctionAst, [bool]$IsPrivate) { Write-Verbose "[PsFunction] Creating function: $($FunctionAst.Name) IsPrivate: $IsPrivate" $this.RawAst = $FunctionAst $this.Name = $FunctionAst.Name $this.IsPrivate = $IsPrivate $this.HasCommentBasedHelp = $FunctionAst.GetHelpContent().Length -gt 0 $this.CommentBasedHelp = $FunctionAst.GetHelpContent() } } Class PsModule { [String]$ModuleName [System.IO.FileInfo]$ModuleFile [System.IO.FileInfo]$ModuleDataFile [bool] $IsPresent [System.Collections.ArrayList]$Classes = [System.Collections.ArrayList]::New() [System.Collections.ArrayList]$Functions = [System.Collections.ArrayList]::New() [System.Collections.ArrayList]$Tests = [System.Collections.ArrayList]::New() Hidden [System.Collections.Hashtable]$ModuleData = @{} PsModule([System.IO.FileInfo]$Path) { if ($Path.Extension -ne '.psm1') { throw "Invalid file type $($Path.Extension) for module file $($Path.FullName)" } $this.ModuleFile = $Path $this.ModuleName = $Path.BaseName $PsdFileName = $Path.FullName.Replace('.psm1', '.psd1') $this.ModuleDataFile = $PsdFileName $this.FetchDataFileContent() if ($Path.Exists) { Write-Verbose "[PsModule] PSM1 file detected -> $($Path.FullName)" $this.IsPresent = $true $this.GetAstClasses($Path) $this.GetASTFunctions($Path) } else { $this.IsPresent = $false } #We are assuming that the Tests folder is located at the root level as the module file / at the same level as the .krane.json file $TestFolder = $Path.Directory.Parent.Parent.FullName + "\Tests" $this.FetchTests($TestFolder) } GetAstClasses([System.IO.FileInfo]$p) { $p.Refresh() Write-Verbose "[PsModule][GetAstClasses] Fetching classes from $($p.FullName)" If ( $P.Exists) { $Raw = [System.Management.Automation.Language.Parser]::ParseFile($p.FullName, [ref]$null, [ref]$Null) $ASTClasses = $Raw.FindAll( { $args[0] -is [System.Management.Automation.Language.TypeDefinitionAst] }, $true) foreach ($ASTClass in $ASTClasses) { $null = $this.Classes.Add($ASTClass) } } } #TODO Refactor this method, so that it is used by GetASTFunctionS($Path) GetASTFunction([System.IO.FileInfo]$Path, [String]$FunctionName) { Write-Verbose "[PsModule][GetAstFunctions] Fetching functions from $($Path.FullName)" $RawFunctions = $null $ParsedFile = [System.Management.Automation.Language.Parser]::ParseFile($Path.FullName, [ref]$null, [ref]$Null) $RawAstDocument = $ParsedFile.FindAll({ $args[0] -is [System.Management.Automation.Language.Ast] }, $true) If ( $RawASTDocument.Count -gt 0 ) { ## source: https://stackoverflow.com/questions/45929043/get-all-functions-in-a-powershell-script/45929412 $RawFunctions = $RawASTDocument.FindAll({ $args[0] -is [System.Management.Automation.Language.FunctionDefinitionAst] -and $($args[0].parent) -isnot [System.Management.Automation.Language.FunctionMemberAst] }) } $RawFunction = $RawFunctions | Where-Object { $_.Name -eq $FunctionName } if ($null -eq $RawFunction) { throw "Function $FunctionName not found in file $($Path.FullName)" } Write-Debug "[PsModule][GetAstFunctions] Found function '$($RawFunction.Name)'" if ($this.ModuleData.FunctionsToExport -contains $RawFunction.Name) { $IsPrivate = $false } else { $IsPrivate = $true } Write-Verbose "[PsModule][GetAstFunctions] Found function $($RawFunction.Name) IsPrivate: $IsPrivate" $Func = [PsFunction]::New($RawFunction, $IsPrivate) $ExistingFunction = $this.Functions | Where-Object { $_.Name -eq $FunctionName } if ($null -ne $ExistingFunction) { $this.Functions.Remove($ExistingFunction) } $null = $This.Functions.Add($Func) } GetASTFunctions([System.IO.FileInfo]$Path) { Write-Verbose "[PsModule][GetAstFunctions] Fetching functions from $($Path.FullName)" $RawFunctions = $null $ParsedFile = [System.Management.Automation.Language.Parser]::ParseFile($Path.FullName, [ref]$null, [ref]$Null) $RawAstDocument = $ParsedFile.FindAll({ $args[0] -is [System.Management.Automation.Language.Ast] }, $true) If ( $RawASTDocument.Count -gt 0 ) { ## source: https://stackoverflow.com/questions/45929043/get-all-functions-in-a-powershell-script/45929412 $RawFunctions = $RawASTDocument.FindAll({ $args[0] -is [System.Management.Automation.Language.FunctionDefinitionAst] -and $($args[0].parent) -isnot [System.Management.Automation.Language.FunctionMemberAst] }) } foreach ($RawFunction in $RawFunctions) { Write-Verbose "[PsModule][GetAstFunctions] Found function $($RawFunction.Name)" if($this.ModuleData.FunctionsToExport -contains $RawFunction.Name){ $IsPrivate = $false } else{ $IsPrivate = $true } Write-Verbose "[PsModule][GetAstFunctions] Found function $($RawFunction.Name) IsPrivate: $IsPrivate" $Func = [PsFunction]::New($RawFunction, $IsPrivate) $null = $This.Functions.Add($Func) } } FetchTests([System.IO.DirectoryInfo]$TestsFolderPath) { $null = $this.Tests.Clear() $AllTestFiles = Get-ChildItem -Path $TestsFolderPath.FullName -File -Recurse foreach($TestFile in $AllTestFiles){ $Test = [TestScript]::New($TestFile) $null = $this.Tests.Add($Test) } foreach($function in $this.Functions){ $Test = $this.Tests | Where-Object { $_.Name.Replace(".Tests.ps1","") -eq $function.Name } if($null -ne $Test){ $function.HasTest = $true $function.TestPath = $Test.Path } } } [TestScript[]] GetTests() { if($null -eq $this.Tests){ $this.FetchTests() } return $this.Tests } [Object[]] GetClasses() { return $this.Classes } [Object[]] GetAllFunctions() { return $this.Functions } [void] ReverseBuild([Bool]$Force) { $ExportFolderPath = $This.GetClassFolderPath().Directory #This method will take the module file and extract the content to the sources folder and put the functions in the right folder. #It is recommended to export to a folder called 'Sources' as other internal Krane functions rely on this folder structure. #By default, the files are NOT overwritten. Set $Force = $True to overwrite existing files. write-debug "[PsModule][ReverseBuild] Start" [System.IO.DirectoryInfo]$PrivatePath = Join-Path -Path $ExportFolderPath.FullName -ChildPath "Functions\Private" [System.IO.DirectoryInfo]$PublicPath = Join-Path -Path $ExportFolderPath.FullName -ChildPath "Functions\Public" [System.IO.DirectoryInfo]$ClassesFolder = Join-Path -Path $ExportFolderPath.FullName -ChildPath "Classes" if ($PrivatePath.Exists -eq $false) { write-debug "[PsModule][ReverseBuild] Creating folder -> $($PrivatePath.FullName)" $null = New-Item -Path $PrivatePath.FullName -ItemType "directory" -Force } if ($PublicPath.Exists -eq $false) { write-debug "[PsModule][ReverseBuild] Creating folder -> $($PublicPath.FullName)" $null = New-Item -Path $PublicPath.FullName -ItemType "directory" -Force } if ($ClassesFolder.Exists -eq $false) { write-debug "[PsModule][ReverseBuild] Creating folder -> $($ClassesFolder.FullName)" $null = New-Item -Path $ClassesFolder.FullName -ItemType "directory" -Force } $ParameterSplat = @{} $ParameterSplat.Force = $Force $ParameterSplat.Encoding = 'utf8' write-debug "[PsModule][ReverseBuild] Starting export of functions to individual files process" foreach ($funct in $this.functions) { $FileName = $funct.Name + ".ps1" if ($funct.IsPrivate) { $FullExportPath = Join-Path -Path $PrivatePath.FullName -ChildPath $FileName write-verbose "[PsModule][ReverseBuild] Exporting Private function '$($funct.Name)' to -> '$($FullExportPath)'" $funct.RawAst.Extent.Text | Out-File -FilePath $FullExportPath @ParameterSplat } else { $FullExportPath = Join-Path -Path $PublicPath.FullName -ChildPath $FileName write-verbose "[PsModule][ReverseBuild] Exporting public function '$($funct.Name)' to -> '$($FullExportPath)'" $funct.RawAst.Extent.Text | Out-File -FilePath $FullExportPath @ParameterSplat } } foreach ($Class in $this.Classes) { $FileName = $Class.Name + ".ps1" $FullExportPath = Join-Path -Path $ClassesFolder.FullName -ChildPath $FileName Write-Verbose "[PsModule][ReverseBuild] Exporting class '$($Class.Name)' to -> '$($FullExportPath)'" $Class.Extent.Text | Out-File -FilePath $FullExportPath @ParameterSplat } write-debug "[PsModule][ReverseBuild] End" } [void] ReverseBuild([String]$Name, [Bool]$Force) { #This method reverse builds a single function or class. write-Debug "[PsModule][ReverseBuild([String]Name,[Bool]Force)] Starting operations" $ParameterSplat = @{ Force = $Force Encoding = 'utf8' } $this.GetASTFunction($this.ModuleFile.FullName,$Name) $Function = $this.Functions | Where-Object { $_.Name -eq $Name } if($null -ne $Function){ #Element trying to reververse build is a function #The function is already existing. We will export it to the right folder, but we need to NOT touch the other existing functions. #GetAstFunctions will automatically load the functions and classes into the $this.PsModule.Functions list. #Since we are only interested in one function, we will backup the existing functions and classes and then reload the functions from the backup. $FileName = $Function.Name + ".ps1" if ($Function.IsPrivate) { $PrivatePath = $this.GetPrivateFolderPath() $FullExportPath = Join-Path -Path $PrivatePath.FullName -ChildPath $FileName write-Debug "[PsModule][ReverseBuild([String]Name,[Bool]Force)] Exporting Private function '$($Function.Name)' to -> '$($FullExportPath)'" $Function.RawAst.Extent.Text | Out-File -FilePath $FullExportPath @ParameterSplat } else { $PublicPath = $this.GetPublicFolderPath() $FullExportPath = Join-Path -Path $PublicPath.FullName -ChildPath $FileName Write-Debug "[PsModule][ReverseBuild([String]Name,[Bool]Force)] Exporting public function '$($Function.Name)' to -> '$($FullExportPath)'" $Function.RawAst.Extent.Text | Out-File -FilePath $FullExportPath @ParameterSplat } } else{ #$Name is a not a function, trying to see if it is a class $Class = $this.GetClasses() | Where-Object { $_.Name -eq $Name } if($null -ne $Class){ $ClassesFolder = $this.GetClassFolderPath() $FileName = $Class.Name + ".ps1" $FullExportPath = Join-Path -Path $ClassesFolder.FullName -ChildPath $FileName Write-Debug "[PsModule][ReverseBuild([String]Name,[Bool]Force)] Exporting class '$($Class.Name)' to -> '$($FullExportPath)'" $Class.Extent.Text | Out-File -FilePath $FullExportPath @ParameterSplat } else{ throw "[PsModule][ReverseBuild([String]Name,[Bool]Force)] Item $Name not found as either class nor function." } } write-Debug "[PsModule][ReverseBuild([String]Name,[Bool]Force)] End of operations" } [void]WriteTest([KraneModule]$KraneModule,[string]$Name) { #Write the tests to the test folder #writing tests for functions $ItemToCreate = $this.Functions| Where-Object { $_.Name -eq $Name } if($null -eq $ItemToCreate){ throw "[PsModule][WriteTest] Item function / Class named $Name is not found. Please ensure that $Name is already present in the module and try again." } $TestScript = [TestScript]::New($KraneModule, $ItemToCreate.Name) $TestScript.CreateTestScript() } [void]WriteTests([KraneModule]$KraneModule) { #Write the tests to the test folder #writing tests for functions foreach($function in $this.Functions){ $TestScript = [TestScript]::New($KraneModule,$function.Name) $TestScript.CreateTestScript() } #writing tests for classes foreach($class in $this.Classes){ $TestScript = [TestScript]::New($KraneModule,$class.Name) $TestScript.CreateTestScript() } } [void] FetchDataFileContent() { $this.ModuleDataFile.Refresh() if ($this.ModuleDataFile.Exists) { Write-Verbose "[PsModule] PSD1 file detected -> $($this.ModuleDataFile.FullName)" $this.ModuleData = Import-PowerShellDataFile -Path $this.ModuleDataFile.FullName } else { Write-Verbose "[PsModule] No PSD1 file found for $($this.ModuleDataFile.FullName)" } } [void] FetchModuleData(){ $this.FetchDataFileContent() $this.GetAstClasses($this.ModuleFile) $this.GetASTFunctions($this.ModuleFile) $this.FetchTests($this.ModuleFile.Directory.Parent.Parent.FullName + "\Tests") #The PsKrane convention stipulates that the Tests folder is located right under the root. We don't have access to the $KraneProject object here. } [PsFunction[]] GetPublicFunctions() { return $this.Functions | Where-Object { $_.IsPrivate -eq $false } } [PsFunction[]] GetPrivateFunctions() { return $this.Functions | Where-Object { $_.IsPrivate -eq $true } } [System.IO.FileInfo] GetPrivateFolderPath(){ $FileInfo = $this.ModuleFile.Directory.Parent.Parent.FullName + "\Sources\Functions\Private" return $FileInfo } [System.IO.FileInfo] GetPublicFolderPath(){ $FileInfo = $this.ModuleFile.Directory.Parent.Parent.FullName + "\Sources\Functions\Public" return $FileInfo } [System.IO.FileInfo] GetClassFolderPath(){ $FileInfo = $this.ModuleFile.Directory.Parent.Parent.FullName + "\Sources\Classes" return $FileInfo } [System.Io.FileInfo[]] GetPublicFunctionFiles(){ $AllFiles = Get-ChildItem -Path $this.GetPublicFolderPath().FullName -File return $AllFiles } [System.Io.FileInfo[]] GetPrivateFunctionFiles(){ $AllFiles = Get-ChildItem -Path $this.GetPrivateFolderPath().FullName -File return $AllFiles } [System.Io.FileInfo[]] GetClassFiles(){ $AllFiles = Get-ChildItem -Path $this.GetClassFolderPath().FullName -File return $AllFiles } } Class TestHelper {} Class PesterTestHelper : TestHelper { [object]$TestData [String[]]$Path [String]$Version = "Latest" [System.IO.FileInfo[]]$Tests PesterTestHelper() {} PesterTestHelper([System.IO.DirectoryInfo]$TestsFolderPath) { #Constructor to build based on Tests folder path and Version of Pester $PesterModule = Get-Module -Name Pester -ListAvailable if(-not $PesterModule){ write-warning "A Pester module could not be found on the system." } $this.Path = $TestsFolderPath $this.Tests = Get-ChildItem -Path $TestsFolderPath -File -Recurse $this.Version = 'Latest' } [void] InvokeTests() { if ($this.Version -eq 'Latest') { Import-Module -Name Pester -Force } else { Import-Module -Name Pester -RequiredVersion $this.Version -Force -Global } $this.TestData = Invoke-Pester -Path $this.Tests -PassThru -Show None } [void] InvokeTests([String[]]$TestsPath) { #Accepts eithern a string or an array of strings that should be the path to the test script(s) or the folder containing test scripts. if ([string]::IsNullOrEmpty($TestsPath)) { throw "No path provided for tests" } if ($this.Version -eq 'Latest') { Import-Module -Name Pester -Force } else { Import-Module -Name Pester -RequiredVersion $this.Version -Force -Global } $this.Path = $TestsPath $this.TestData = Invoke-Pester -Path $TestsPath -PassThru -Show None } [void] SetVersion([String]$Version) { $this.Version = $Version } [String] ToString() { return "Result: {0} PassedCount: {1} FailedCount: {2}" -f $this.TestData.Result, $this.TestData.PassedCount, $this.TestData.FailedCount } [object] GetFailedTests() { return $this.TestData.Failed } [object] GetPassedTests() { return $this.TestData.Passed } [void] FetchTest(){ } } function Get-KraneTestScript { <# .SYNOPSIS Retrieves a test script .DESCRIPTION Retrieves a test script .PARAMETER KraneModule The Krane module object .PARAMETER Name The name of the test script. .EXAMPLE Get-KraneTestScript -KraneModule $KraneModule -Name "TestScript" #> [CmdletBinding()] Param( [Parameter(Mandatory = $True)] [KraneModule]$KraneModule, [Parameter(Mandatory = $False)] [String]$Name ) if($Name){ if(-not $Name.EndsWith(".Tests.ps1")){ $Name = $Name.Replace(".Tests.ps1","") } $return = $KraneModule.PsModule.Tests | where-Object { $_.Name -eq $Name } return $return }else{ return $KraneModule.PsModule.Tests } } # Public functions Function Get-KraneProjectVersion { <# .SYNOPSIS Retrieves the version of the Krane project .DESCRIPTION Retrieves the version of the Krane project .PARAMETER KraneProject The Krane project object .EXAMPLE Get-KraneProjectVersion -KraneProject $KraneProject #> [CmdletBinding()] Param( [Parameter(Mandatory = $True)] [KraneProject]$KraneProject ) Return $KraneProject.KraneFile.Get("ProjectVersion") } Function New-KraneProject { <# .SYNOPSIS Creates a new Krane project .DESCRIPTION Will create a base .krane.json project file. The project can be either a module or a script. .PARAMETER Type Type of project to create. Can be either 'Module' or 'Script' .PARAMETER Name Name of the project .PARAMETER Path Root folder of the project .NOTES Author: Stéphane vg .LINK https://github.com/Stephanevg/PsKrane .EXAMPLE New-KraneProject -Type Module -Path C:\Users\Stephane\Code\KraneTest\wip -Name "wip" -verbose ModuleName : wip ModuleFile : C:\Users\Stephane\Code\KraneTest\wip\Outputs\Module\wip.psm1 ModuleDataFile : C:\Users\Stephane\Code\KraneTest\wip\Outputs\Module\wip.psd1 Build : C:\Users\Stephane\Code\KraneTest\wip\Build Sources : C:\Users\Stephane\Code\KraneTest\wip\Sources Tests : C:\Users\Stephane\Code\KraneTest\wip\Tests Outputs : C:\Users\Stephane\Code\KraneTest\wip\Outputs Tags : {PSEdition_Core, PSEdition_Desktop} Description : ProjectUri : KraneFile : ProjectName:wip ProjectType:Module ProjectType : Module Root : C:\Users\Stephane\Code\KraneTest\wip .EXAMPLE New-KraneProject -Type Module -Path C:\Users\Stephane\Code\KraneTest\plop -Name "Plop" C:\USERS\STEPHANE\CODE\KRANETEST\PLOP │ .krane.json ├───Build │ └───Build.Krane.ps1 ├───Outputs ├───Sources │ └───Functions │ ├───Private │ └───Public └───Tests .PARAMETER Type Type of project to create. Can be either 'Module' or 'Script' .PARAMETER Name Name of the project .PARAMETER Path Root folder of the project #> [cmdletBinding()] [OutputType([KraneProject])] Param( [Parameter(Mandatory = $True, HelpMessage = "Type of project to create. Can be either 'Module' or 'Script'")] [ProjectType]$Type, [Parameter(Mandatory = $True, HelpMessage = "Name of the project")] [String]$Name, [Parameter(Mandatory = $True, HelpMessage = "Root folder of the project")] [ValidateScript({ if(-not (Test-Path $_)){ throw "Path $_ doesn't exists" } $Item = Get-Item -Path $_ if($Item.GetType().Name -eq "DirectoryInfo"){ return $true }else{ throw "Path $_ is not a directory" } })] [object]$Path ) $ResolvedPath = Resolve-Path $Path $Path = Get-Item -Path $ResolvedPath.Path if ($Path.GetType().Name -ne "DirectoryInfo") { throw "Path $($Path.FullName) is not a directory" } [System.IO.DirectoryInfo]$DestinationPath = Join-Path -Path $Path.FullName -ChildPath $Name if ($DestinationPath.Exists) { $KraneFile = Get-ChildItem -Path $DestinationPath.FullName -Filter ".krane.json" if ($KraneFile) { Write-warning "[New-KraneProject] Project already exists at '$($DestinationPath.FullName)'." return } #Kranefile doesn't exists. This means the folder is empty. We can create the project } switch ($Type) { "Module" { $KraneProject = [KraneModule]::New($Path, $Name) } default { Throw "Project type $Type not supported" } } $KraneProject.CreateBaseStructure() Add-KraneBuildScript -KraneProject $KraneProject Return $KraneProject } Function New-KraneNuspecFile { <# .SYNOPSIS Creates a new NuSpec file .DESCRIPTION Creates a new Nuspec File based on a PsKrane project. .PARAMETER KraneProject The Krane project object .LINK https://github.com/Stephanevg/PsKrane .EXAMPLE $KraneProject = Get-KraneProject -Root C:\Plop\ New-KraneNuspecFile -KraneProject $KraneProject Generates a .nuspec file in .\Outputs\Module\ folder of the KraneProject #> Param( [Parameter(Mandatory = $True)] [KraneModule]$KraneModule ) $NuSpec = [NuSpecFile]::New($KraneModule) $NuSpec.CreateNuSpecFile() } Function Get-KraneProject { <# .SYNOPSIS Retrieves a Krane project .DESCRIPTION Retrieves a Krane project .PARAMETER Root The root folder of the project. If not specified, it assumes it is located in a folder called 'Build' in the root of the project. .EXAMPLE Get-KraneProject -Root C:\Code\MyKraneModule #> [CmdletBinding()] [OutputType([KraneProject])] Param( [Parameter(Mandatory = $False, HelpMessage = "Root folder of the project. If not specified, it assumes it is located in the current folder.")] [System.IO.DirectoryInfo]$Root ) # Retrieve parent folder if (!$Root) { #Stole this part from PSHTML $EC = Get-Variable ExecutionContext -ValueOnly $Root = $ec.SessionState.Path.CurrentLocation.Path write-Verbose "[Get-KraneProject] Root parameter was omitted. Using Current location: $Root" } ElseIf ($Root.Exists -eq $false) { Throw "Root $($Root.FullName) folder not found" } [System.IO.FileInfo]$KraneFile = Join-Path -Path $Root.FullName -ChildPath ".krane.json" If (!($KraneFile.Exists)) { Throw "No .Krane file found in $($Root.FullName). Verify the path, or create a new project using New-KraneProject" } write-Verbose "[Get-KraneProject] Fetching Krane project from path: $Root" Return [KraneFactory]::GetProject($KraneFile) } Function Add-KraneBuildScript { <# .SYNOPSIS Adds the build script 'Build.Krane.ps1' to the project. .DESCRIPTION Adds the build script to the project. The build script is used to invoke PsKrane and to build the module and create the nuspec file. This build script is buy default called Build.Krane.ps1 and located in the folder: '<KraneProject>\Build\'. The build script is created with a Base build template. But it is recommended to customize it to your needs. .PARAMETER Path The folder of where the Build.Krane.ps1 file should be created. .PARAMETER KraneProject The KraneProject object that represents the project .NOTES Author: Stephane van Gulick version: 0.1 .LINK http://github.com/stephanevg/PsKrane .EXAMPLE Add-BuildScript -Root C:\Users\Stephane\Code\KraneTest\wip #> [CmdletBinding()] Param( [Parameter(Mandatory = $False, ParameterSetName = "Path")] [System.IO.DirectoryInfo]$Path, [Parameter(Mandatory = $False, ParameterSetName = "KraneProject")] [KraneModule]$KraneProject ) Switch ($PSCmdlet.ParameterSetName) { "Path" { $BuildScript = [BuildScript]::New($Path) $BuildScript.CreateBuildScript() } "KraneProject" { $BuildScript = [BuildScript]::New($KraneProject.Build) $BuildScript.CreateBuildScript() } } } Function New-KraneTestScript { <# .SYNOPSIS Creates a new test script. .DESCRIPTION Creates a new test script in the Tests folder of the project. The Test script can be used for any purpose. If you want to create tests for existing functions / classes, have a look at Write-KraneTestScript .PARAMETER KraneProject The KraneProject object that represents the project Example: $KraneProject = Get-KraneProject .PARAMETER TestName The name of the test script .EXAMPLE New-KraneTestScript -KraneModule $KraneModule -TestName "Plop" Creates a new test script called Plop.Tests.ps1 in the Tests folder of the project #> [CmdletBinding()] Param( [Parameter(Mandatory = $True)] [KraneProject]$KraneProject, [Parameter(Mandatory = $True)] [String]$Name ) Write-Verbose "[New-KraneTestScript] Creating test '$Name'" $TestScript = [TestScript]::New($KraneProject, $Name) $TestScript.CreateTestScript() $KraneProject.PsModule.FetchTests($KraneProject.Tests.FullName) } #TODO TO delete ?? Function Write-KraneTestScript { <# .SYNOPSIS Creates one or more test scripts based on a $KraneModuleProject functions / Classes .DESCRIPTION Creates a new test script in the Tests folder of the project. Only tests for existing functions / classes can be created. .PARAMETER KraneProject The KraneProject object that represents the project (Uswe $KraneProject = Get-KraneProject to get the project) .PARAMETER TestName The name of the test script .EXAMPLE New-KraneTestScript -KraneModule $KraneModule -TestName "Plop" Creates a new test script called Plop.Tests.ps1 in the Tests folder of the project #> [CmdletBinding()] Param( [Parameter(Mandatory = $True)] [KraneProject]$KraneProject, [Parameter(Mandatory = $False,ParameterSetName = 'SingleItem')] [String]$Name, [Parameter(Mandatory = $False)] [Switch]$Force ) if($Name){ Write-Verbose "[Write-KraneTestScript] Creating test script for $Name" $KraneProject.PsModule.WriteTest($KraneProject,$Name) } else{ Write-Verbose "[Write-KraneTestScript] Creating test scripts for all functions" $KraneProject.PsModule.WriteTests($KraneProject) } $KraneProject.PsModule.FetchTests($KraneProject.Tests.FullName) } Function Invoke-KraneBuild { <# .SYNOPSIS Invokes the build script .DESCRIPTION Invokes the build script of the project. The build script is used to build the module and create the nuspec file. The build script is by default called Build.Krane.ps1 and located in the folder: '<KraneProject>\Build\'. The build script is created with a Base build template. But it is recommended to customize it to your needs. .PARAMETER KraneProject The KraneProject object that represents the project .EXAMPLE Invoke-KraneBuild -KraneProject $KraneProject #> [CmdletBinding()] Param( [KraneProject]$KraneProject ) $BuildFile = Join-Path -Path $KraneProject.Build.FullName -ChildPath "Build.Krane.ps1" if (!(Test-Path -Path $BuildFile)) { Throw "BuildFile $($BuildFile) not found. Please make sure it is there, and try again" } & $BuildFile } Function New-KraneNugetFile { <# .SYNOPSIS Creates a new nuget package .DESCRIPTION Create a new nuget package based for a specific kraneproject (Nuspec must already have been generated) .NOTES Information or caveats about the function e.g. 'This function is not supported in Linux' .LINK https://github.com/Stephanevg/PsKrane .EXAMPLE $KraneProject = Get-KraneProject -Root C:\Plop\ New-KraneNugetFile -KraneProject $KraneProject -Force Generates a .nupkg file in .\Outputs\Nuget\ folder of the KraneProject. -Force will create the nuspec file .PARAMETER KraneModule The KraneModule object that represents the project .PARAMETER Force Creates the nuspec file first #> Param( [Parameter(Mandatory = $True)] [KraneModule]$KraneModule, [Switch]$Force ) $NuSpec = [NuSpecFile]::New($KraneModule) if ($Force) { $NuSpec.CreateNuSpecFile() } $NuSpec.CreateNugetFile() } Function Invoke-KraneGitCommand { <# .SYNOPSIS Invokes a Git command .DESCRIPTION Invokes a Git command. The supported commands are: - tag: Creates a tag with the version of the project - PushTags: Pushes the tags to the remote repository - PushWithTags: Pushes the tags to the remote repository and pushes the changes .PARAMETER KraneProject The KraneProject object that represents the project .PARAMETER GitAction The Git action to perform. The supported actions are: 'tag', 'PushTags', 'PushWithTags' .PARAMETER Argument The argument to pass to the Git command. If not specified, the version of the project will be used. .EXAMPLE Invoke-KraneGitCommand -KraneProject $KraneProject -GitAction tag #> [CmdletBinding()] Param( [Parameter(Mandatory = $True)] [KraneProject]$KraneProject, [Parameter(Mandatory = $true)] [ValidateSet("tag", "PushTags", "PushWithTags")] [String]$GitAction, [String]$Argument ) $GitHelper = [GitHelper]::New() switch ($GitAction) { "tag" { if (!($Argument)) { $Argument = "v{0}" -f $KraneProject.ProjectVersion } Write-Verbose "[Invoke-KraneGitCommand] Invoking Git action $GitAction with argument $Argument" $GitHelper.GitTag($Argument) } "PushWithTags" { Write-Verbose "[Invoke-KraneGitCommand] Invoking Git action $GitAction" $GitHelper.GitPushWithTags() } "PushTags" { Write-Verbose "[Invoke-KraneGitCommand] Invoking Git action $GitAction" $GitHelper.GitPushTags() } } } Function Invoke-KraneTestScripts { <# .SYNOPSIS Invokes the test scripts .DESCRIPTION Invokes the test scripts of the project. The test scripts are used to test the functions and classes of the project. The test scripts are located in the folder: '<KraneProject>\Tests\'. .PARAMETER KraneProject The KraneProject object that represents the project .PARAMETER Version The version of Pester to use. By default, the latest version is used. .EXAMPLE Invoke-KraneTestScripts -KraneProject $KraneProject .EXAMPLE #with version 4.10.0 of Pester Invoke-KraneTestScripts -KraneProject $KraneProject -Version 4.10.0 #> [CmdletBinding()] Param( [Parameter(Mandatory = $True)] [KraneProject]$KraneProject, [Parameter(Mandatory = $False)] [String]$Version = "Latest" ) $TestHelper = [PesterTestHelper]::New() $TestHelper.SetVersion($Version) $TestHelper.InvokeTests($KraneProject.Tests.FullName) $KraneProject.TestData = $TestHelper Return $TestHelper } Enum LocationType { Module System Project } Class KraneTemplateCollection { [System.Collections.ArrayList]$Templates = [System.Collections.ArrayList]::New() KraneTemplateCollection() {} AddTemplate([KraneTemplate]$Template) { $null = $this.Templates.Add($Template) } [KraneTemplate[]] GetTemplate() { return $this.Templates } [KraneTemplate[]]GetTemplate([String]$Type) { $Template = $this.Templates | Where-Object { $_.Type -eq $Type } Return $Template } [KraneTemplate] GetTemplate([LocationType]$Location) { Write-Verbose "[KraneTemplateCollection] Getting template of by location -> $Location" $Template = $this.Templates | Where-Object { $_.Location -eq $Location } Return $Template } [KraneTemplate] GetTemplate([String]$Type, [LocationType]$Location) { Write-Verbose "[KraneTemplateCollection] Getting template of type $Type and location $Location" $Template = $this.Templates | Where-Object { $_.Type -eq $Type -and $_.Location -eq $Location } if ($null -eq $Template) { Throw "Template '$Type' of location type '$Location' not found" } Return $Template } } Class KraneTemplate { [String]$Type hidden [String]$Content [System.Io.FileInfo]$Path [LocationType]$Location KraneTemplate([System.Io.FileInfo]$Path) { Write-Verbose '[KraneTemplate] Start Constructor [System.Io.FileInfo]$Path' if ($Path.Exists -eq $false) { Throw "Template file $($Path.FullName) not found" } $This.Type = $Path.BaseName.Split(".")[0] $this.Path = $Path $this.Content = Get-Content -Path $Path.FullName -Raw Write-Verbose "[KraneTemplate] End Constructor" } SetLocation([LocationType]$Location) { $this.Location = $Location } [String] ToString() { return "{0}->{1}" -f $this.Type, $this.Location } [string] GetContent() { Write-Verbose "[KraneTemplate] Getting content of template $($this.Path.FullName)" return $this.Content } } function New-KraneItem { <# .SYNOPSIS This function helps to create a new item in the project .DESCRIPTION Items in a krane project kan be a private function, a public function or a class .PARAMETER KraneProject The KraneProject object that represents the project. .PARAMETER Name The name of the item to create. .PARAMETER Location This parameter specifies from where the template should be taken to create the item. The values that are supported are based on the values of enum 'LocationType'. By default, the location is set to 'Module' that is the location where the out-of-the-box templates are located. .PARAMETER Type The Type of the item to create. The Types that are supported are based on the values of enum 'KraneTemplateType'. .PARAMETER Visibility This parameter is ONLY available when Type is set to 'Function'. This setting allows to specify wheter a function should be 'Public' or 'Private'. This will result in the function being created in the right sub folder (functions/pubic or funtions/private). .LINK Specify a URI to a help page, this will show when Get-Help -Online is used. .EXAMPLE #The following example creates a new class called 'Myclass' in the krane project located at 'C:\MyKraneProject', and will use the tamplate located in the location 'Module'. $KraneProject = Get-KraneProject -Root "C:\MyKraneProject" New-KraneItem -KraneProject $KraneProject -Name Myclass -Type Class -Location 'Module' .EXAMPLE #The following example creates a new private function called 'plop' in the krane project located at 'C:\MyKraneProject' $KraneProject = Get-KraneProject -Root "C:\MyKraneProject" New-KraneItem -KraneProject $KraneProject -Name plop -Type Function -visibility "private" #> param( [Parameter(Mandatory = $True)] [KraneProject]$KraneProject, [Parameter(Mandatory = $True)] [KraneTemplateType]$Type, [Parameter(Mandatory = $True)] [String]$Name, [Parameter(Mandatory = $False)] [LocationType]$Location = [LocationType]::Module ) dynamicparam { # Create a dictionary to hold dynamic parameters $paramDictionary = New-Object System.Management.Automation.RuntimeDefinedParameterDictionary # Only create the FunctionType parameter if Type is "Function" if ($PSCmdlet.MyInvocation.BoundParameters["Type"] -eq "Function") { $attributeCollection = New-Object System.Collections.ObjectModel.Collection[System.Attribute] # Make the parameter mandatory when Type is "Function" $paramAttribute = New-Object System.Management.Automation.ParameterAttribute $paramAttribute.Mandatory = $true #Adding inline help. $paramAttribute.HelpMessage = "The visibility the function should have. Can be either 'Public' or 'Private'. In doubt, use 'Public'" # Define ValidateSet (Restricts input to "Public" or "Private") $validateSet = New-Object System.Management.Automation.ValidateSetAttribute("Public", "Private") $attributeCollection.Add($validateSet) $attributeCollection.Add($paramAttribute) # Define the parameter $functionTypeParam = New-Object System.Management.Automation.RuntimeDefinedParameter( "Visibility", [string], $attributeCollection ) # Add to dictionary $paramDictionary.Add("Visibility", $functionTypeParam) } return $paramDictionary } begin { # Retrieve dynamic parameter values if ($PSBoundParameters.ContainsKey("Visibility")) { $Visibility = $PSBoundParameters["Visibility"] } } process{ write-verbose "[New-KraneItem] Start of function" write-verbose "[New-KraneItem] Creating new item of type '$Type' with name '$Name' in location '$Location'" #Any function can be of type Prive or Public. We will use the same template for both and copy the content to the right location. switch ($Type) { 'PublicFunction' { $typ = "function" } 'PrivateFunction' { $typ = "function" } Default { $typ = $Type } } $Template = $KraneProject.GetTemplate($Type, $Location) if ($null -eq $Template) { throw "No Template not found for '$Name' in location '$location'" } switch ($Type) { "Class" { $NewContent = $Template.GetContent().Replace('###ClassName###', $Name) $KraneProject.addClass($Name, $NewContent) } "PublicFunction" { $NewContent = $Template.GetContent().Replace('###FunctionName###', $Name) $KraneProject.addPublicFunction($Name, $NewContent) } "PrivateFunction" { $NewContent = $Template.GetContent().Replace('###FunctionName###', $Name) $KraneProject.addPrivateFunction($Name, $Newcontent) } "Function" { $NewContent = $Template.GetContent().Replace('###FunctionName###', $Name) if ($Visibility -eq "Private") { $KraneProject.addPrivateFunction($Name, $NewContent) } else { $KraneProject.addPublicFunction($Name, $NewContent) } } "Test"{ #TODO Refactor, and either remove complete New-KRaneTestScript, or remove Test creation functionality from this function New-KraneTestScript -KraneProject $KraneProject -TestName $Name } default { throw "Type $Type not supported" } } #TODO Need to load the new data into $Krane.psmodule (getAllFunctions seems to NOT do the job. Add method 'LoadAll' ?) write-verbose "[New-KraneItem] End of function" } } function Get-KraneTemplate { <# .SYNOPSIS Retrieves all existing templates from a specific krane project. .DESCRIPTION Retrieves all existing templates from a specific krane project. .PARAMETER KraneProject The KraneProject object that represents the project. .PARAMETER Name The name of the template to retrieve. .PARAMETER Type The type of the template to retrieve. .PARAMETER Location The location where to search from to get the template. (System,Module,Project). .LINK https://www.github.com/stephanevg/PsKrane .EXAMPLE Get-KraneTemplate -KraneProject $KraneProject #> param( [Parameter(Mandatory = $True)] [KraneProject]$KraneProject, [Parameter(Mandatory = $False)] [String]$Name, [Parameter(Mandatory = $False)] [KraneTemplateType]$Type, [Parameter(Mandatory = $False)] [LocationType]$Location ) write-verbose "[Get-KraneTemplate] Start of function" $Template = $KraneProject.GetTemplate() if ($Name) { $Template = $Template | Where-Object { $_.Name -eq $Name } } #Since $Type is an Enum, when using if($type){} it is evaluated to be $false, although it DOES contain a value... #Using PsBoundParameters instead. if ($PSBoundParameters.ContainsKey("Type")) { $Template = $Template | Where-Object { $_.Type -eq $PSBoundParameters["Type"]} } if ($Location) { $Template = $Template | Where-Object { $_.Location -eq $Location } } if (-not $Template) { #No existing templates found. This can happen when several parameters are used, and some conditions are not met (name / location for instance). write-verbose "[Get-KraneTemplate] No templates found" return $null } write-verbose "[Get-KraneTemplate] End of function" return $Template } Function Invoke-KraneReverseBuild { <# .SYNOPSIS This function allows to decompose an existing .psm1 file into multiple individual .ps1 files. .DESCRIPTION Creates the individual function and class files (.ps1) based on the functions present in a .psm1 file. .NOTES Changelog: V1.0.0 14.01.2025 stephanevg -> Initial Creation .PARAMETER KraneModule The KraneModule object that represents the project .PARAMETER Force Overwrites the file(s) if already present. (Be sure you commit your changes BEFORE you use this one). .PARAMETER AddTests Allows to add a relevant test file. .PARAMETER ItemName Allows to limit the reverse build to a specific item. .EXAMPLE $KraneModule = Get-KraneProject -Root C:\MyModule\ Invoke-KraneReverseBuild -KraneModule $KraneModule .EXAMPLE #The following will add tests to you $KraneModule = Get-KraneProject -Root C:\MyModule\ Invoke-KraneReverseBuild -KraneModule $KraneModule -AddTests .EXAMPLE #The following limits the reverse build to a specific function called 'MyFunction' $KraneModule = Get-KraneProject -Root C:\MyModule\ Invoke-KraneReverseBuild -KraneModule $KraneModule -ItemName "MyFunction" #> [CmdletBinding()] Param( [Parameter(Mandatory = $True)] [KraneModule]$KraneModule, [Switch]$AddTests, [String]$ItemName, [Switch]$Force ) Write-Verbose "[Invoke-KraneReverseBuild] Starting operations" if($Force) { Write-Verbose "[Invoke-KraneReverseBuild] Force parameter detected. This will overwrite existing files. Make sure you have committed your changes before using this parameter" } if ($ItemName) { Write-Verbose "[Invoke-KraneReverseBuild] Limiting the reverse build to item $ItemName" $KraneModule.ReverseBuild($ItemName, $Force) }else{ Write-Verbose "[Invoke-KraneReverseBuild] Launching reverseBuild on all Item" $KraneModule.ReverseBuild($Force) Write-Verbose "[Invoke-KraneReverseBuild] Done" } if($AddTests){ Write-Verbose "[Invoke-KraneReverseBuild] AddTests parameter detected." if($ItemName){ $Functions = $KraneModule.psModule.Functions | Where-Object { $_.Name -eq $ItemName } }else{ $Functions = $KraneModule.psModule.Functions } foreach($function in $Functions){ Write-Verbose "[Invoke-KraneReverseBuild] Adding test function $($Function.Name)" New-KraneTestScript -TestName $Function.Name -KraneProject $KraneModule } foreach($Class in $KraneModule.PsModule.Classes){ Write-Verbose "[Invoke-KraneReverseBuild] Adding test for function $($Function.Name)" New-KraneTestScript -TestName $Class.Name -KraneProject $KraneModule } #Refreshing tests $KraneProject.PsModule.FetchTests($KraneModule.Tests) } Write-Verbose "[Invoke-KraneReverseBuild] End of operations" } |