Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
389 changes: 389 additions & 0 deletions .editorconfig

Large diffs are not rendered by default.

4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1 +1,5 @@
Build
bin
artifacts
src/ModuleFast/obj
src/ModuleFast/bin
6 changes: 6 additions & 0 deletions Directory.Build.props
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
<Project>
<PropertyGroup>
<!-- Use the .NET SDK Artifacts Output Layout: all bin/obj go to artifacts/ -->
<UseArtifactsOutput>true</UseArtifactsOutput>
</PropertyGroup>
</Project>
10 changes: 10 additions & 0 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<Project>
<PropertyGroup>
<!-- Central Package Management: all NuGet versions defined here -->
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
</PropertyGroup>
<ItemGroup>
<PackageVersion Include="System.Management.Automation" Version="7.4.0" />
<PackageVersion Include="NuGet.Versioning" Version="6.8.0" />
</ItemGroup>
</Project>
13 changes: 13 additions & 0 deletions ModuleFast.build.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -34,13 +34,25 @@ Task Clean {
}
}

Task BuildCSharp {
$csprojPath = Join-Path $PSScriptRoot 'src' 'ModuleFast' 'ModuleFast.csproj'
# Artifacts Output Layout managed by Directory.Build.props — no -o needed
dotnet build $csprojPath --nologo -c Release
}

Task CopyFiles {
Copy-Item @c -Path @(
'ModuleFast.psd1'
'ModuleFast.psm1'
'LICENSE'
) -Destination $ModuleOutFolderPath
Copy-Item @c -Path 'ModuleFast.ps1' -Destination $Destination

# Copy DLL and its dependencies from Artifacts Output to the module bin folder
$artifactsBinPath = Join-Path $PSScriptRoot 'artifacts' 'bin' 'ModuleFast' 'release'
$moduleBinPath = Join-Path $ModuleOutFolderPath 'bin' 'ModuleFast'
New-Item -ItemType Directory -Path $moduleBinPath -Force | Out-Null
Copy-Item @c -Path (Join-Path $artifactsBinPath '*') -Destination $moduleBinPath -Recurse
}

Task Version {
Expand Down Expand Up @@ -93,6 +105,7 @@ Task Package Package.Nuget, Package.Zip
#Supported High Level Tasks
Task Build @(
'Clean'
'BuildCSharp'
'CopyFiles'
'Version'
'GetNugetVersioningAssembly'
Expand Down
4 changes: 2 additions & 2 deletions ModuleFast.psd1
Original file line number Diff line number Diff line change
Expand Up @@ -69,10 +69,10 @@
# NestedModules = @()

# Functions to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no functions to export.
FunctionsToExport = 'Install-ModuleFast', 'Get-ModuleFastPlan', 'Clear-ModuleFastCache'
FunctionsToExport = @()

# Cmdlets to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no cmdlets to export.
#CmdletsToExport = '*'
CmdletsToExport = 'Install-ModuleFast', 'Get-ModuleFastPlan', 'Clear-ModuleFastCache', 'Import-ModuleManifest'

# Variables to export from this module
#VariablesToExport = '*'
Expand Down
2,329 changes: 28 additions & 2,301 deletions ModuleFast.psm1

Large diffs are not rendered by default.

3 changes: 3 additions & 0 deletions ModuleFast.slnx
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
<Solution>
<Project Path="Source/ModuleFast/ModuleFast.csproj" />
</Solution>
120 changes: 60 additions & 60 deletions ModuleFast.tests.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -3,87 +3,87 @@ using namespace Microsoft.PowerShell.Commands
using namespace System.Collections.Generic
using namespace System.Diagnostics.CodeAnalysis
using namespace NuGet.Versioning
using namespace ModuleFast

. $PSScriptRoot/ModuleFast.ps1 -ImportNuGetVersioning
Import-Module $PSScriptRoot/ModuleFast.psm1 -Force
Import-Module $PSScriptRoot/ModuleFast.psd1 -Force

BeforeAll {
if ($env:MFURI) {
$PSDefaultParameterValues['*-ModuleFast*:Source'] = $env:MFURI
}
}

InModuleScope 'ModuleFast' {
Describe 'ModuleFastSpec' {
Context 'Constructors' {
It 'Getters' {
$spec = [ModuleFastSpec]'Test'
'Name', 'Guid', 'Min', 'Max', 'Required' | ForEach-Object {
$spec.PSObject.Properties.name | Should -Contain $PSItem
}
}

It 'Name' {
$spec = [ModuleFastSpec]'Test'
$spec.Name | Should -Be 'Test'
$spec.Guid | Should -Be ([Guid]::Empty)
$spec.Min | Should -BeNull
$spec.Max | Should -BeNull
$spec.Required | Should -BeNull
# ModuleFastSpec is a public C# class — no InModuleScope needed
Describe 'ModuleFastSpec' {
Context 'Constructors' {
It 'Getters' {
$spec = [ModuleFastSpec]'Test'
'Name', 'Guid', 'Min', 'Max', 'Required' | ForEach-Object {
$spec.PSObject.Properties.name | Should -Contain $PSItem
}
}

It 'Has non-settable properties' {
$spec = [ModuleFastSpec]'Test'
{ $spec.Min = '1' } | Should -Throw
{ $spec.Max = '1' } | Should -Throw
{ $spec.Required = '1' } | Should -Throw
{ $spec.Name = 'fake' } | Should -Throw
{ $spec.Guid = New-Guid } | Should -Throw
}
It 'Name' {
$spec = [ModuleFastSpec]'Test'
$spec.Name | Should -Be 'Test'
$spec.Guid | Should -Be ([Guid]::Empty)
$spec.Min | Should -BeNull
$spec.Max | Should -BeNull
$spec.Required | Should -BeNull
}

It 'ModuleSpecification' {
$in = [ModuleSpecification]@{
ModuleName = 'Test'
ModuleVersion = '2.1.5'
}
$spec = [ModuleFastSpec]$in
$spec.Name | Should -Be 'Test'
$spec.Guid | Should -Be ([Guid]::Empty)
$spec.Min | Should -Be '2.1.5'
$spec.Max | Should -BeNull
$spec.Required | Should -BeNull
}
It 'Has non-settable properties' {
$spec = [ModuleFastSpec]'Test'
{ $spec.Min = '1' } | Should -Throw
{ $spec.Max = '1' } | Should -Throw
{ $spec.Required = '1' } | Should -Throw
{ $spec.Name = 'fake' } | Should -Throw
{ $spec.Guid = New-Guid } | Should -Throw
}

Context 'ModuleSpecification Conversion' {
It 'Name' {
$spec = [ModuleSpecification][ModuleFastSpec]'Test'
$spec.Name | Should -Be 'Test'
$spec.Version | Should -Be '0.0'
$spec.RequiredVersion | Should -BeNull
$spec.MaximumVersion | Should -BeNull
}
It 'RequiredVersion' {
$spec = [ModuleSpecification][ModuleFastSpec]::new('Test', '1.2.3')
$spec.Name | Should -Be 'Test'
$spec.RequiredVersion | Should -Be '1.2.3.0'
$spec.Version | Should -BeNull
$spec.MaximumVersion | Should -BeNull
It 'ModuleSpecification' {
$in = [ModuleSpecification]@{
ModuleName = 'Test'
ModuleVersion = '2.1.5'
}
$spec = [ModuleFastSpec]$in
$spec.Name | Should -Be 'Test'
$spec.Guid | Should -Be ([Guid]::Empty)
$spec.Min | Should -Be '2.1.5'
$spec.Max | Should -BeNull
$spec.Required | Should -BeNull
}
}

Describe 'Import-ModuleManifest' {
It 'Reads Dynamic Manifest' {
$Mocks = "$PSScriptRoot/Test/Mocks"
$manifest = Import-ModuleManifest "$Mocks/Dynamic.psd1"
$manifest | Should -BeOfType [System.Collections.Hashtable]
$manifest.ModuleVersion | Should -Be '1.0.0'
$manifest.RootModule | Should -Be 'coreclr\PrtgAPI.PowerShell.dll'
Context 'ModuleSpecification Conversion' {
It 'Name' {
$spec = [ModuleSpecification][ModuleFastSpec]'Test'
$spec.Name | Should -Be 'Test'
$spec.Version | Should -Be '0.0'
$spec.RequiredVersion | Should -BeNull
$spec.MaximumVersion | Should -BeNull
}
It 'RequiredVersion' {
$spec = [ModuleSpecification][ModuleFastSpec]::new('Test', '1.2.3')
$spec.Name | Should -Be 'Test'
$spec.RequiredVersion | Should -Be '1.2.3.0'
$spec.Version | Should -BeNull
$spec.MaximumVersion | Should -BeNull
}
}
}

# Import-ModuleManifest is now a binary cmdlet — no InModuleScope needed
Describe 'Import-ModuleManifest' {
It 'Reads Dynamic Manifest' {
$Mocks = "$PSScriptRoot/Test/Mocks"
$manifest = Import-ModuleManifest "$Mocks/Dynamic.psd1"
$manifest | Should -BeOfType [System.Collections.Hashtable]
$manifest.ModuleVersion | Should -Be '1.0.0'
$manifest.RootModule | Should -Be 'coreclr\PrtgAPI.PowerShell.dll'
}
}

Describe 'Get-ModuleFastPlan' -Tag 'E2E' {
BeforeAll {
$SCRIPT:__existingPSModulePath = $env:PSModulePath
Expand Down
13 changes: 13 additions & 0 deletions Source/ModuleFast/Commands/ClearModuleFastCacheCommand.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
using System.Management.Automation;

namespace ModuleFast.Commands;

[Cmdlet(VerbsCommon.Clear, "ModuleFastCache")]
public class ClearModuleFastCacheCommand : PSCmdlet
{
protected override void ProcessRecord()
{
WriteDebug("Flushing ModuleFast Request Cache");
ModuleFastCache.Instance.Clear();
}
}
102 changes: 102 additions & 0 deletions Source/ModuleFast/Commands/GetModuleFastPlanCommand.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
using System.Collections.Generic;
using System.Management.Automation;
using System.Threading;

namespace ModuleFast.Commands;

/// <remarks>
/// THIS COMMAND IS DEPRECATED AND WILL NOT RECEIVE PARAMETER UPDATES. Please use Install-ModuleFast -Plan instead.
/// </remarks>
[Cmdlet(VerbsCommon.Get, "ModuleFastPlan")]
[OutputType(typeof(ModuleFastInfo))]
public class GetModuleFastPlanCommand : PSCmdlet
{
[Parameter(Position = 0, Mandatory = true, ValueFromPipeline = true)]
[Alias("Name")]
public ModuleFastSpec[]? Specification { get; set; }

[Parameter]
public string Source { get; set; } = "https://pwsh.gallery/index.json";

[Parameter]
public SwitchParameter Prerelease { get; set; }

[Parameter]
public SwitchParameter Update { get; set; }

[Parameter]
public PSCredential? Credential { get; set; }

[Parameter]
public int Timeout { get; set; } = 30;

[Parameter]
public string? Destination { get; set; }

[Parameter]
public SwitchParameter DestinationOnly { get; set; }

[Parameter]
public SwitchParameter StrictSemVer { get; set; }

private readonly HashSet<ModuleFastSpec> _specs = new();

protected override void ProcessRecord()
{
foreach (var spec in Specification ?? [])
_specs.Add(spec);
}

protected override void EndProcessing()
{
if (Update) ModuleFastCache.Instance.Clear();

// Normalize source
if (Uri.TryCreate(Source, UriKind.Absolute, out var srcUri) &&
srcUri.Scheme is not "http" and not "https")
{
Source = $"https://{Source}/index.json";
}

var httpClient = ModuleFastClient.Create(Credential, Timeout);
var planner = new ModuleFastPlanner(httpClient, Source);

string[] modulePaths;
if (DestinationOnly && !string.IsNullOrEmpty(Destination))
{
modulePaths = [Destination];
}
else if (!string.IsNullOrEmpty(Destination))
{
var envPaths = Environment.GetEnvironmentVariable("PSModulePath")
?.Split(Path.PathSeparator, StringSplitOptions.RemoveEmptyEntries) ?? [];
modulePaths = [Destination, .. envPaths];
}
else
{
modulePaths = Environment.GetEnvironmentVariable("PSModulePath")
?.Split(Path.PathSeparator, StringSplitOptions.RemoveEmptyEntries) ?? [];
}

try
{
var task = planner.GetPlanAsync(
_specs,
modulePaths,
Update,
Prerelease,
StrictSemVer,
DestinationOnly,
CancellationToken.None,
this);

var plan = task.GetAwaiter().GetResult();
foreach (var info in plan)
WriteObject(info);
}
catch (Exception ex) when (ex is not PipelineStoppedException)
{
ThrowTerminatingError(new ErrorRecord(ex, "GetModuleFastPlanFailed", ErrorCategory.NotSpecified, null));
}
}
}
39 changes: 39 additions & 0 deletions Source/ModuleFast/Commands/ImportModuleManifestCommand.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
using System.Collections;
using System.Management.Automation;

namespace ModuleFast.Commands;

/// <summary>
/// Imports a module manifest from a path, handling dynamic manifest formats.
/// NOTE: This cmdlet is primarily for internal use and testing.
/// </summary>
[Cmdlet(VerbsData.Import, "ModuleManifest")]
[OutputType(typeof(Hashtable))]
public class ImportModuleManifestCommand : PSCmdlet
{
[Parameter(Mandatory = true, Position = 0, ValueFromPipeline = true)]
public string? Path { get; set; }

protected override void ProcessRecord()
{
if (string.IsNullOrEmpty(Path))
{
ThrowTerminatingError(new ErrorRecord(
new ArgumentNullException(nameof(Path)),
"PathRequired",
ErrorCategory.InvalidArgument,
null));
return;
}

try
{
var result = ModuleManifestReader.ImportModuleManifest(Path, this);
WriteObject(result);
}
catch (Exception ex)
{
ThrowTerminatingError(new ErrorRecord(ex, "ImportModuleManifestFailed", ErrorCategory.ReadError, Path));
}
}
}
Loading