PsKrane.psm1
Enum ProjectType { Module Script } 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 -Path $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.Save() Return $KraneFile } } Class KraneProject { [KraneFile]$KraneFile [ProjectType]$ProjectType [System.IO.DirectoryInfo]$Root KraneProject() {} KraneProject([System.IO.DirectoryInfo]$Root) { $this.KraneFile = [KraneFile]::New($Root) } } Class KraneModule : KraneProject { [String]$ModuleName [System.IO.FileInfo]$ModuleFile [System.IO.FileInfo]$ModuleDataFile [System.IO.DirectoryInfo]$Build [System.IO.DirectoryInfo]$Sources [System.IO.DirectoryInfo]$Tests [System.IO.DirectoryInfo]$Outputs [String[]] $Tags = @( 'PSEdition_Core', 'PSEdition_Desktop' ) [String]$Description [String]$ProjectUri 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. $this.KraneFile = [KraneFile]::New($Root) $this.ProjectType = [ProjectType]::Module $this.Root = $Root $this.Build = "$($Root.FullName)\Build" $this.Sources = "$($Root.FullName)\Sources" $this.Tests = "$($Root.FullName)\Tests" $this.Outputs = "$($Root.FullName)\Outputs" #get the module name from the krane file $mName = $this.KraneFile.Get("Name") $this.SetModuleName($mName) $this.ProjectType = $this.KraneFile.Get("ProjectType") $this.FetchModuleInfo() } 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) $This.KraneFile = [KraneFile]::Create($Root, $ModuleName, [ProjectType]::Module) $this.ProjectType = [ProjectType]::Module $this.Root = $Root $this.Build = "$Root\Build" $this.Sources = "$Root\Sources" $this.Tests = "$Root\Tests" $this.Outputs = "$Root\Outputs" $this.ModuleName = $ModuleName <# if (($this.Build.Exists -eq $false) -or ($this.Sources.Exists -eq $false) -or ($this.Tests.Exists -eq $false)) { Throw "No Build, Sources or Tests folder found in $($This.Root)" } #> $this.FetchModuleInfo() } 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 } } [void] BuildModule() { if ($this.ModuleFile.Exists) { $this.ModuleFile.Delete() $this.ModuleFile.Refresh() } Write-Verbose "[BUILD][START] Creating module file." $Null = New-Item -Path $this.ModuleFile.FullName -ItemType "file" -Force $MainPSM1Contents = @() [System.IO.DirectoryInfo]$ClassFolderPath = Join-Path -Path $this.Sources -ChildPath "Classes" If ($ClassFolderPath.Exists) { write-Verbose "[Classes] Folder Found" $PublicClasses = Get-ChildItem -Path $ClassFolderPath.FullName -Filter *.ps1 | sort-object Name $MainPSM1Contents += $PublicClasses } [System.IO.DirectoryInfo]$PrivateFunctionsFolderPath = Join-Path -Path $this.Sources -ChildPath "Functions/Private" If ($ClassFolderPath.Exists) { write-Verbose "[Functions] Private functions Found" $Privatefunctions = Get-ChildItem -Path $PrivateFunctionsFolderPath.FullName -Filter *.ps1 | sort-object Name $MainPSM1Contents += $Privatefunctions } $Publicfunctions = $null [System.IO.DirectoryInfo]$PublicFunctionsFolderPath = Join-Path -Path $this.Sources -ChildPath "Functions/Public" If ($ClassFolderPath.Exists) { write-Verbose "[Functions] Public functions Found" $Publicfunctions = Get-ChildItem -Path $PublicFunctionsFolderPath.FullName -Filter *.ps1 | sort-object Name $MainPSM1Contents += $Publicfunctions } [System.IO.FileInfo]$PreContentPath = Join-Path -Path $this.Sources.FullName -ChildPath "PreContent.ps1" If ($PrecontentPath.Exists) { write-Verbose "[BUILD][Pre] Pre content Found. Adding to module file" Get-Content -Path $PreContentPath.FullName | out-File -FilePath $this.ModuleFile.FullName -Encoding utf8 -Append } else { write-Verbose "[BUILD][Pre] No Pre content Found. Skipping." } #Creating PSM1 Write-Verbose "[BUILD][START][MAIN PSM1] Building main PSM1" Foreach ($file in $MainPSM1Contents) { Get-Content $File.FullName | out-File -FilePath $this.ModuleFile.FullName -Encoding utf8 -Append } [System.IO.FileInfo]$PostContentPath = Join-Path -Path $this.Sources.FullName -ChildPath "postContent.ps1" If ($PostContentPath.Exists) { Write-verbose "[BUILD][START][POST] PostContent.ps1 file found. Adding to module file." $file = Get-item $PostContentPath Get-content $File.FullName | out-File -FilePath $this.ModuleFile.FullName -Encoding utf8 -Append } else { Write-Verbose "[BUILD][START][POST] No post content file found!" } Write-verbose "[BUILD][START][PSD1] Starding PSD1 actions. Adding functions to export" if (!$this.ModuleDataFile.Exists) { New-ModuleManifest -Path $this.ModuleDataFile.FullName } Update-ModuleManifest -Path $this.ModuleDataFile.FullName -FunctionsToExport $Publicfunctions.BaseName -Tags $This.Tags -RootModule $this.ModuleFile.Name -Description $this.Description -ProjectUri $this.ProjectUri Write-verbose "[BUILD][END]End of Build Process" } [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" } } } 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 "Creating root project of type Module $($Root.FullName)" return [KraneModule]::New($Root) } 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" } SetKraneModule([KraneModule]$KraneModule) { $this.KraneModule = $KraneModule } hidden [Void]Generate() { $psd1Data = Import-PowerShellDataFile -Path $this.KraneModule.ModuleDataFile $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>{5}</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, $Tags $this.RawContent = $Final } [System.IO.FileInfo] CreateNuSpecFile() { $this.Generate() $Modulefolder = Join-Path -Path $this.KraneModule.Outputs.FullName -ChildPath "Module" $this.NuSpecFilePath = Join-Path -Path $Modulefolder -ChildPath ($this.KraneModule.ModuleName + ".nuspec") $this.RawContent | Out-File -FilePath $this.NuspecFilePath -Encoding utf8 -Force Return $this.NuspecFilePath } CreateNugetFile() { & nuget pack $this.NuSpecFilePath.FullName -OutputDirectory $this.ExportFolderPath -Version "1.0.0" } } # Public functions 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. Use -Force to create the base structure of the project. .NOTES Information or caveats about the function e.g. 'This function is not supported in Linux' .LINK Specify a URI to a help page, this will show when Get-Help -Online is used. .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" -Force When using force, it will create the base structure of the project. C:\USERS\STEPHANE\CODE\KRANETEST\PLOP │ .krane.json ├───Build ├───Outputs ├───Sources │ └───Functions │ ├───Private │ └───Public └───Tests #> [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")] [System.IO.DirectoryInfo]$Path, [Switch]$Force ) switch($Type) { "Module" { $KraneProject = [KraneModule]::New($Path, $Name) } default { Throw "Project type $Type not supported" } } if($Force){ $KraneProject.CreateBaseStructure() } Return $KraneProject } Function New-KraneNuspecFile { Param( [Parameter(Mandatory = $True)] [KraneModule]$KraneModule ) $NuSpec = [NuSpecFile]::New($KraneModule) $NuSpec.CreateNuSpecFile() $NuSpec.CreateNugetFile() } Function Get-KraneProject { [CmdletBinding()] [OutputType([KraneProject])] Param( [Parameter(Mandatory = $False, HelpMessage = "Root folder of the project. If not specified, it assumes it is located in a folder called 'Build' in the root of the project.")] [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) } |