Migrate-Module.ps1

#requires -version 7.5

# A proof-of-concept script to migrate all resources from a MOF-based DSC module
# to a class-based set of DSC resources. Each resource will be exported to a separate
# file under a Classes folder.

# .\Migrate-module.ps1 -Module xWindowsUpdate -DestinationPath d:\temp\WindowsUpdateDsc

Param(
    [Parameter(
        Mandatory,
        HelpMessage = "The name of the module for the DSC resource. Use fully-qualified name to specify a version."
    )]
    [ValidateNotNullOrEmpty()]
    [object]$Module,

    [Parameter(
        Mandatory,
        HelpMessage = "The destination path for the new module, including the new module name."
    )]
    [ValidateNotNullOrEmpty()]
    [String]$DestinationPath,

    [Parameter(HelpMessage = "Define a version number for the new resource module")]
    [ValidateNotNullOrEmpty()]
    [version]$NewVersion = "0.1.0"
)

Import-Module $PSScriptRoot\DSCResourceMigration.psd1 -Force

$newName = Split-Path -Path $DestinationPath -Leaf
$rootModule = Join-Path -Path $DestinationPath -ChildPath "$newName.psm1"

#get the source module root path to check for supporting modules
if ($module -is [String]) {
    $modRoot = Get-Module -Name $Module -ListAvailable -OutVariable mod | Split-Path
}
else {
    $modRoot = Get-Module -FullyQualifiedName $Module -ListAvailable -OutVariable mod | Split-Path
}

if (-Not $modRoot) {
    Throw "Failed to find the module."
    Return
}
else {
    Write-Verbose "Using source module root $modRoot"
}

#get DSCResources from the module
Write-Verbose "Checking $($mod.path) for DscResourcesToExport"
$import = Import-PowerShellDataFile -path $mod.path
if ($import.DscResourcesToExport) {
    $resources = $import.DscResourcesToExport
}
else {
    Write-Verbose "Checking for resources under $modRoot"
    if (Test-Path $modRoot\DscResources ) {
        $resources = (Get-ChildItem -Path $modRoot\DscResources -ErrorAction stop).name
    }
    else {
        Throw "Can't determine the location for the DSC Resource source files"
    }
}

#Create new directory structure
Write-Verbose "Creating a module structure $DestinationPath"
$subFolders = "docs", "en-us", "functions", "tests", "samples", "classes"

if (-Not (Test-Path -Path $DestinationPath)) {
    [void](New-Item -ItemType Directory -Path $DestinationPath)
}
foreach ($sub in $subFolders) {
    if (Test-Path (Join-Path -Path $DestinationPath -ChildPath $sub)) {
        Write-Verbose "Skipping $sub.name"
    }
    else {
        New-Item -ItemType Directory -Path $DestinationPath -Name $sub
    }
}

#copy existing supporting modules if found
Write-Verbose "Testing $modRoot for a Modules folder"
if (Test-Path "$modRoot\modules" ) {
    Write-Verbose "Copying supporting modules"
    Copy-Item -Path $modRoot\modules -Destination $DestinationPath -Container -Force -Recurse
}

Write-Verbose "Creating $rootModule"
#dot source the class files to the psm1 file
@"
Get-ChildItem -path `$PSScriptRoot\classes |
ForEach-Object {. `$_.FullName}
"@
 | Out-File -FilePath $rootModule

#convert each resource into a class in its own folder
#each migrated class should be as complete and self-contained as possible,
#even though there may be duplicate helper functions
foreach ($Name in $Resources ) {
    Write-Verbose "Converting MOF for $name from to Class"
    Write-Verbose "Creating a folder for the class"
    $classFolder = New-Item -Path "$DestinationPath\Classes" -Name $Name -ItemType Directory -Force
    $classFile = Join-Path -Path $classFolder.FullName -ChildPath "$name.ps1"
    $classFunctions = New-Item -Name functions -Path $classFolder.FullName -ItemType Directory -Force

    New-DSCClassDefinition -name $name -module $module | Out-File -FilePath $classFile

    #export helper functions to individual files under .\functions
    Write-Verbose "Getting non-TargetResource code"
    #get all commands in the psm1 other than the Get/Set/Test functions
    #this could be turned into a function
    $resource = Get-DscResource -Name $Name -Module $module | Select-Object -First 1
    $ast = Get-AST -path $resource.path
    $found = $ast.FindAll({ $args[0] -is [System.Management.Automation.Language.Ast] }, $true)
    $h = $found | Group-Object { $_.GetType().Name } -AsHashTable -AsString

    $other = $h["NamedBlockAST"][0].statements |
    Where-Object { $_.name -notmatch "[(get)|(set)|(test)]-TargetResource" } |
    Select-Object extent

    #export functions to separate files
    $funcs = $ast.FindAll({ $args[0] -is [System.Management.Automation.Language.FunctionDefinitionAst] }, $true) |
    Where-Object { $_.name -notmatch "targetResource" }
    foreach ($fun in $funcs) {
        $fPath = Join-Path -Path $classFunctions.FullName -ChildPath "$($fun.name).ps1"
        Write-Verbose "Exporting $fPath"
        $fun.Extent.text | Out-File -FilePath $fPath -Force
    }

    Write-Verbose "Adding non-function code to the class file"
    "`n# TODO: IF IMPORTING HELPER MODULES YOU MAY NEED TO FIX PATH REFERENCES" | Out-File -FilePath $classFile -Append
    $other | Where-Object { $_.extent.text -notmatch "Export-ModuleMember|function" } |
    ForEach-Object {
        $_.Extent.text | Out-File -FilePath $classFile -Append
    }

    @"
 
#dot source supporting functions
Get-ChildItem `$PSScriptRoot\functions\*.ps1 | ForEach-Object { . `$_.FullName}
 
"@
 | Out-File -FilePath $classFile -Append

    #append a copy of the original schema.mof to the new class .ps1 file
    Write-Verbose "Creating a copy of the original schema.mof"
    $MofPath = Get-SchemaMofPath -name $Name -module $module
    @"
<#
original schema.mof
$( Get-Content -Path $MofPath | Out-String)
#>
"@
  | Out-File -FilePath $classFile -Append

} #foreach resource

#create the module manifest
$manifestPath = Join-Path -Path $DestinationPath -ChildPath "$newName.psd1"
Write-Verbose "Creating manifest $manifestPath"
New-ModuleManifest -Path $manifestPath -RootModule "$newName.psm1" -DscResourcesToExport $resources -ModuleVersion $newversion

Write-Host "Migration complete. Open $DestinationPath in your editor to continue." -ForegroundColor Green