diff --git a/docs/advanced/releases.md b/docs/advanced/releases.md index 3f77899d..d545ad04 100644 --- a/docs/advanced/releases.md +++ b/docs/advanced/releases.md @@ -160,11 +160,13 @@ Staging is performed by: - `tools/New-IdleModulePackage.ps1` -This script copies the `IdLE` meta-module and required nested modules into a local `Modules/` folder and patches the staged +This script copies the `IdLE` meta-module and baseline nested modules (`IdLE.Core`, `IdLE.Steps.Common`) into a local `Modules/` folder and patches the staged `IdLE.psd1` so `NestedModules` use in-package relative paths (e.g. `./Modules/IdLE.Core/IdLE.Core.psd1`). +For details on baseline vs optional modules and the non-blocking import policy, see **[Installation Guide](../getting-started/installation.md#what-gets-imported)**. + > This approach avoids repository restructuring while ensuring that `Install-Module IdLE` + `Import-Module IdLE` works -> after installation. +> reliably on any clean PowerShell 7 environment without external dependencies. ## Versioning and naming diff --git a/docs/getting-started/installation.md b/docs/getting-started/installation.md index 053ead88..ed837888 100644 --- a/docs/getting-started/installation.md +++ b/docs/getting-started/installation.md @@ -22,8 +22,8 @@ Install-Module -Name IdLE -Scope CurrentUser Import-Module IdLE ``` -> Note: The `IdLE` meta-module loads the bundled nested modules (e.g. `IdLE.Core`, built-in steps, and the mock provider -> used by examples) from within the installed package. +> Note: The `IdLE` module automatically imports the baseline modules (`IdLE.Core` and `IdLE.Steps.Common`). +> Optional modules (providers, additional step modules) are shipped with the package but not auto-imported. ### Verify installation @@ -61,9 +61,9 @@ Get-Command -Module IdLE.Core ## What gets imported -### Default: `IdLE` meta-module (batteries included) +### Default: `IdLE` meta-module (baseline) -`IdLE` is the **batteries-included** entrypoint. Importing it loads: +`IdLE` is the **baseline** entrypoint. Importing it automatically loads: - **IdLE.Core** — the workflow engine (step-agnostic) - **IdLE.Steps.Common** — first-party built-in steps (e.g. `IdLE.Step.EmitEvent`, `IdLE.Step.EnsureAttribute`) @@ -71,7 +71,9 @@ Get-Command -Module IdLE.Core Built-in steps are **available to the engine by default**, but are intentionally **not exported into the global session state**. This keeps your PowerShell session clean while still allowing workflows to reference built-in steps by `Step.Type`. -**When to use:** Most users and production scenarios. +**Non-blocking guarantee:** `Import-Module IdLE` always succeeds on a clean PowerShell 7 environment without any external dependencies (RSAT, AD tools, third-party modules, etc.). + +**When to use:** All users and production scenarios. ### Advanced: Engine-only import @@ -85,14 +87,21 @@ Import-Module ./src/IdLE.Core/IdLE.Core.psd1 -Force --- -## Provider modules (optional) +## Optional modules (shipped but not auto-imported) + +The `IdLE` package ships additional modules that are **not automatically imported**. These modules may have system-specific or tool-specific dependencies and are imported explicitly when needed: -The core engine is provider-agnostic. Provider modules are **packaged with IdLE** but must be **imported separately** when needed. +- **Provider modules:** `IdLE.Provider.AD`, `IdLE.Provider.EntraID`, `IdLE.Provider.ExchangeOnline`, `IdLE.Provider.DirectorySync.EntraConnect` +- **Optional step modules:** `IdLE.Steps.DirectorySync`, `IdLE.Steps.Mailbox` +- **Development/testing modules:** `IdLE.Provider.Mock` -Example: +Example (from source): ```powershell -# From source +# Import baseline (auto-imports Core and Steps.Common) +Import-Module ./src/IdLE/IdLE.psd1 -Force + +# Import optional provider when needed Import-Module ./src/IdLE.Provider.AD/IdLE.Provider.AD.psd1 -Force ``` diff --git a/examples/Invoke-IdleDemo.ps1 b/examples/Invoke-IdleDemo.ps1 index 9e2e11b4..b3ebe188 100644 --- a/examples/Invoke-IdleDemo.ps1 +++ b/examples/Invoke-IdleDemo.ps1 @@ -226,7 +226,7 @@ function Select-DemoWorkflows { # Import modules from the repo (path-based import, no global installation required). Import-Module (Join-Path $PSScriptRoot '..\src\IdLE\IdLE.psd1') -Force -ErrorAction Stop -Import-Module (Join-Path $PSScriptRoot '..\src\IdLE.Steps.Common\IdLE.Steps.Common.psd1') -Force -ErrorAction Stop +# Mock provider is optional and must be imported explicitly (not auto-imported by IdLE) Import-Module (Join-Path $PSScriptRoot '..\src\IdLE.Provider.Mock\IdLE.Provider.Mock.psd1') -Force -ErrorAction Stop $available = @(Get-DemoWorkflows -Category $Category) diff --git a/src/IdLE.Core/Public/Invoke-IdlePlanObject.ps1 b/src/IdLE.Core/Public/Invoke-IdlePlanObject.ps1 index e3409041..596b35f4 100644 --- a/src/IdLE.Core/Public/Invoke-IdlePlanObject.ps1 +++ b/src/IdLE.Core/Public/Invoke-IdlePlanObject.ps1 @@ -111,7 +111,42 @@ function Invoke-IdlePlanObject { ) } - $cmd = Get-Command -Name ([string]$handlerName) -CommandType Function -ErrorAction Stop + # Resolve the handler command. + # The handler name can be: + # 1) A simple function name (e.g. "Invoke-IdleStepEmitEvent") - globally available + # 2) A module-qualified name (e.g. "IdLE.Steps.Common\Invoke-IdleStepEmitEvent") - from a nested module + # + # Module-qualified names are used for built-in steps that are loaded as nested modules + # and not exported globally to keep the session clean. + + $cmd = $null + + # Try simple lookup first (globally available commands) + $cmd = Get-Command -Name ([string]$handlerName) -CommandType Function -ErrorAction SilentlyContinue + + # If not found and name contains backslash, try module-qualified lookup + if ($null -eq $cmd -and ([string]$handlerName).Contains('\')) { + $parts = ([string]$handlerName).Split('\', 2) + if ($parts.Count -eq 2) { + $moduleName = $parts[0] + $commandName = $parts[1] + + # Get-Module -All returns loaded modules (including nested/hidden modules) + # We use -All to find modules that are loaded but not in the global session state + $modules = @(Get-Module -Name $moduleName -All) + if ($modules.Count -gt 0) { + $module = $modules[0] + if ($null -ne $module.ExportedCommands -and $module.ExportedCommands.ContainsKey($commandName)) { + $cmd = $module.ExportedCommands[$commandName] + } + } + } + } + + if ($null -eq $cmd) { + throw [System.ArgumentException]::new("Step handler '$handlerName' for step type '$StepType' could not be resolved to a valid command.", 'Providers') + } + return $cmd } diff --git a/src/IdLE/IdLE.psd1 b/src/IdLE/IdLE.psd1 index 24951b81..77a3b171 100644 --- a/src/IdLE/IdLE.psd1 +++ b/src/IdLE/IdLE.psd1 @@ -9,10 +9,7 @@ NestedModules = @( '..\IdLE.Core\IdLE.Core.psd1', - '..\IdLE.Steps.Common\IdLE.Steps.Common.psd1', - '..\IdLE.Steps.DirectorySync\IdLE.Steps.DirectorySync.psd1', - '..\IdLE.Provider.DirectorySync.EntraConnect\IdLE.Provider.DirectorySync.EntraConnect.psd1', - '..\IdLE.Provider.AD\IdLE.Provider.AD.psd1' + '..\IdLE.Steps.Common\IdLE.Steps.Common.psd1' ) FunctionsToExport = @( diff --git a/tests/ModuleSurface.Tests.ps1 b/tests/ModuleSurface.Tests.ps1 index 0e0f7ee3..b74009dd 100644 --- a/tests/ModuleSurface.Tests.ps1 +++ b/tests/ModuleSurface.Tests.ps1 @@ -156,6 +156,34 @@ Describe 'Module manifests and public surface' { ($idle.NestedModules | Where-Object Name -eq 'IdLE.Steps.Common') | Should -Not -BeNullOrEmpty } + It 'IdLE auto-imports only baseline modules (Core and Steps.Common), not optional modules' { + # Clean test state - remove baseline modules to ensure fresh import + # We don't remove optional modules to maintain test isolation (other tests may have them loaded) + Remove-Module IdLE, IdLE.Core, IdLE.Steps.Common -Force -ErrorAction SilentlyContinue + Import-Module $idlePsd1 -Force -ErrorAction Stop + + $idle = Get-Module IdLE + $idle | Should -Not -BeNullOrEmpty + + # Define expected baseline modules in one place + $baselineModules = @('IdLE.Core', 'IdLE.Steps.Common') + + # Baseline modules should be auto-imported (explicit positive check) + foreach ($moduleName in $baselineModules) { + ($idle.NestedModules | Where-Object Name -eq $moduleName) | Should -Not -BeNullOrEmpty -Because "$moduleName should be auto-imported" + } + + # Only baseline modules should be nested (count check ensures no extras) + @($idle.NestedModules).Count | Should -Be $baselineModules.Count + + # Verify no optional modules are nested (generalized negative check using pattern) + # This pattern matches: IdLE.Provider.* or IdLE.Steps.* (except Steps.Common) + $nestedNames = @($idle.NestedModules | Select-Object -ExpandProperty Name) + $optionalModulePattern = '^IdLE\.(Provider\.|Steps\.(?!Common$))' + $unexpectedModules = $nestedNames | Where-Object { $_ -match $optionalModulePattern } + $unexpectedModules | Should -BeNullOrEmpty -Because "Optional modules should not be auto-imported" + } + It 'Steps module exports the intended step functions' { Remove-Module IdLE.Steps.Common -Force -ErrorAction SilentlyContinue Import-Module $stepsPsd1 -Force -ErrorAction Stop