Speeding up PowerShell module development with RestartableSession

Speeding up PowerShell module development with RestartableSession

ยท

4 min read

Let's assume you are developing a PowerShell module. You would add small modifications to the module and test if they work as expected iteratively. In each iteration, after you edit the module code, how do you test the modification?

I like testing the module on the terminal interactively when I'm adding frequent and small changes in the early phase of development. I also do interactive testing if the module is designed for interactive usage, or if their results need to be checked on the terminal, such as modules for terminal customizations. However, there is an issue with module reloading.

Module Reloading is Hard

To reflect the code changes on the terminal, you have to run this command every time you edit the module:

Import-Module MyModule -Force

This is not enough sometimes. If your module has a nested module, and you are making changes to the nested module, you need to call Remove-Module first.

Remove-Module TopLevelModule
Import-Module TopLevelModule

Even worse, if the module exports classes and it's imported by the using module statement, you have to reboot the PowerShell session manually and import the module again. C# classes added by Add-Type have the same issue.

In this example code, if you edit the message in the line [1] or [2], you need to restart the PowerShell session to see the update.

# Test.psm1
class TestPSClass {
    static [String] Run() {
        return 'Hi' # [1]
    }
}

Add-Type -TypeDefinition @'
public class TestCSClass {
    public static System.String Run() {
        return "Hello"; // [2]
    }
}
'@
# On the terminal
using module ./Test.psm1
[TestPSClass]::Run()
[TestCSClass]::Run()

Typing Import-Module or Remove-Module many times might be okay but restarting the PowerShell session manually is a bit annoying. I have to automate this as a PowerShell enthusiast ๐Ÿ’ช.

RestartableSession Module

I made a module RestartableSession to reload modules properly no matter what kinds of modules they are. The module creates a nested session that can be easily restarted with a command. It also takes a ScriptBlock with -OnStart parameter that is invoked every time the session is restarted.

Enter-RSSession starts a new restartable session and invokes the specified ScriptBlock.

PS C:\> Enter-RSSession -OnStart {'Hello'}
Hello
RS(1) PS C:\>

Restart-RSSession restarts the session and invokes the ScriptBlock again.

RS(1) PS C:\> Restart-RSSession
Hello
RS(2) PS C:\>

Start-RSRestartFileWatcher is used to automatically call Restart-RSSession on file changes under a folder.

RS(2) PS C:\> Start-RSRestartFileWatcher -Path D:\PathToFolder -IncludeSubdirectories

Let's see how these work for module reloading.

Automatic Module Reloading

The following code monitors the file changes in the module directory. When a file is modified, it automatically restarts the session and imports the module.

$onStart = {
    Import-Module D:\PathToMyModule
    Start-RSRestartFileWatcher -Path D:\PathToMyModule -IncludeSubdirectories
}
Enter-RSSession -OnStart $onStart

If you need using module, you have to create the ScriptBlock from a string as it seems that the using statement cannot be written in a ScriptBlock directly.

$onStartString = 
@'
    using module D:\PathToMyModule
    Start-RSRestartFileWatcher -Path D:\PathToMyModule -IncludeSubdirectories
'@
$onStart = [ScriptBlock]::Create($onStartString)
Enter-RSSession -OnStart $onStart

Let's make a function so that it works for any module directory:

function Start-AutoReload($moduleDir) {
    $onStartString = 
@'
    using module "{0}"
    Start-RSRestartFileWatcher -Path "{0}" -IncludeSubdirectories
'@ -f $moduleDir

    $onStart = [ScriptBlock]::Create($onStartString)
    Enter-RSSession -OnStart $onStart
}

I have this function in my profile and at the beginning of the day, I run this command once on my terminal:

 Start-AutoReload D:\PathToMyModule

Then, it becomes an auto-reloading terminal for the module. Once I've hit ctrl+s to save the change, the terminal is ready to use with the module loaded on a fresh session.

The onStart ScriptBlock can be anything so you can build a binary module or even run a Pester test there.

Alternative Way using VS Code

If you use the PowerShell extension in VS Code, you can use createTemporaryIntegratedConsole debugger option to always create a new session when launching the debugger. By writing using module inside the script specified by the script option, you get a fresh session with your module loaded every time you launch the debugger.

// launch.json
"configurations": [
    {
        "name": "PowerShell: Module Interactive Session",
        "type": "PowerShell",
        "request": "launch",
        "script": "${workspaceFolder}/Import.ps1",
        "createTemporaryIntegratedConsole": true
    }
]
# Import.ps1
using module ./Test.psm1

If you don't mind pressing F5 and waiting for the debugger, this approach should be good enough.

Conclusion

Module reloading sometimes requires a session restart which is a bit annoying in the development iteration. In this article, I shared a way to automate the reload process using the RestartableSession module.

Check out the repository page too and let me know if you come up with some other cool use cases of the module.

References

ย