Support/Package/Execution/Eigenverft.Manifested.Sandbox.Package.Npm.ps1

<#
    Eigenverft.Manifested.Sandbox.Package.Npm
#>


function Get-PackageNpmGlobalConfigPath {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [psobject]$PackageResult
    )

    $packageRoot = Get-PackageRootFromInventoryPath -PackageAssignmentInventoryFilePath ([string]$PackageResult.PackageConfig.PackageAssignmentInventoryFilePath)
    return ([System.IO.Path]::GetFullPath((Join-Path $packageRoot 'Configuration\External\npm\npmrc')))
}

function New-PackageNpmCacheDirectory {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [psobject]$PackageResult
    )

    $packageRoot = Get-PackageRootFromInventoryPath -PackageAssignmentInventoryFilePath ([string]$PackageResult.PackageConfig.PackageAssignmentInventoryFilePath)
    $segments = @(
        'Caches'
        'npm'
        [string]$PackageResult.DefinitionId
        [string]$PackageResult.Package.releaseTrack
        [string]$PackageResult.Package.version
        [string]$PackageResult.Package.artifactDistributionVariant
    ) | ForEach-Object {
        ([string]$_).Trim() -replace '[\\/:\*\?"<>\|]', '-'
    }

    $cacheDirectory = [System.IO.Path]::GetFullPath((Join-Path $packageRoot ($segments -join '\')))
    $null = New-Item -ItemType Directory -Path $cacheDirectory -Force
    return $cacheDirectory
}

function Initialize-PackageNpmGlobalConfig {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [string]$GlobalConfigPath
    )

    $resolvedPath = [System.IO.Path]::GetFullPath($GlobalConfigPath)
    $directoryPath = Split-Path -Parent $resolvedPath
    if (-not [string]::IsNullOrWhiteSpace($directoryPath)) {
        $null = New-Item -ItemType Directory -Path $directoryPath -Force
    }

    if (-not (Test-Path -LiteralPath $resolvedPath -PathType Leaf)) {
        Set-Content -LiteralPath $resolvedPath -Value '' -Encoding UTF8
    }

    return $resolvedPath
}

function Resolve-PackageNpmInstallerCommand {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [psobject]$PackageResult
    )

    $install = Get-PackageAssignedInstallOperation -Release $PackageResult.Package
    if (-not $install) {
        throw "Package npm global package install for '$($PackageResult.PackageId)' requires packageOperations.assigned.install on the selected release."
    }
    if (-not $install.PSObject.Properties['installerCommand'] -or [string]::IsNullOrWhiteSpace([string]$install.installerCommand)) {
        throw "Package npm global package install for '$($PackageResult.PackageId)' requires packageOperations.assigned.install.installerCommand."
    }

    $installerCommand = [string]$install.installerCommand
    $dependencyInfo = Resolve-PackageDependencyCommandPath -PackageResult $PackageResult -CommandName $installerCommand
    Write-PackageExecutionMessage -Message ("[STATE] Installer command ready: definition='{0}', command='{1}', path='{2}'." -f $dependencyInfo.DefinitionId, $dependencyInfo.Command, $dependencyInfo.CommandPath)

    return $dependencyInfo
}

function Test-PackageNpmMaterializedInstallKind {
    [CmdletBinding()]
    param(
        [AllowNull()]
        [psobject]$Package
    )

    $install = Get-PackageAssignedInstallOperation -Release $Package
    return ($install -and
        $install.PSObject.Properties['kind'] -and
        [string]::Equals([string]$install.kind, 'npmMaterializedInstallGlobalPackage', [System.StringComparison]::OrdinalIgnoreCase))
}

function Get-PackageNpmResolvedPackageSpec {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [psobject]$PackageResult
    )

    $install = Get-PackageAssignedInstallOperation -Release $PackageResult.Package
    if (-not $install -or -not $install.PSObject.Properties['packageSpec'] -or [string]::IsNullOrWhiteSpace([string]$install.packageSpec)) {
        throw "Package npm materialized install for '$($PackageResult.PackageId)' requires packageOperations.assigned.install.packageSpec."
    }

    return (Resolve-PackageTemplateText -Text ([string]$install.packageSpec) -PackageConfig $PackageResult.PackageConfig -Package $PackageResult.Package)
}

function Get-PackageNpmMaterializationDirectory {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [psobject]$PackageResult
    )

    if ([string]::IsNullOrWhiteSpace([string]$PackageResult.PackageFileStagingDirectory)) {
        throw "Package npm materialization for '$($PackageResult.PackageId)' requires a package file staging directory."
    }

    return ([System.IO.Path]::GetFullPath((Join-Path ([string]$PackageResult.PackageFileStagingDirectory) 'npm-materialized')))
}

function Get-PackageNpmPlatform {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [psobject]$PackageConfig
    )

    switch -Regex ([string]$PackageConfig.Platform) {
        '^(windows|win32)$' { return 'win32' }
        '^(macos|darwin)$' { return 'darwin' }
        '^linux$' { return 'linux' }
        default { return ([string]$PackageConfig.Platform).ToLowerInvariant() }
    }
}

function Get-PackageNpmArchitecture {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [psobject]$PackageConfig
    )

    switch -Regex ([string]$PackageConfig.Architecture) {
        '^(x64|amd64)$' { return 'x64' }
        '^(arm64|aarch64)$' { return 'arm64' }
        '^(x86|ia32)$' { return 'ia32' }
        default { return ([string]$PackageConfig.Architecture).ToLowerInvariant() }
    }
}

function Test-PackageNpmIntegrity {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [string]$Path,

        [Parameter(Mandatory = $true)]
        [string]$Integrity
    )

    if (-not (Test-Path -LiteralPath $Path -PathType Leaf)) {
        return $false
    }

    foreach ($token in @(([string]$Integrity) -split '\s+')) {
        if ([string]::IsNullOrWhiteSpace($token) -or $token -notmatch '^(?<algorithm>sha1|sha256|sha384|sha512)-(?<value>.+)$') {
            continue
        }

        $algorithm = $Matches.algorithm.ToUpperInvariant()
        $expected = $Matches.value
        $hashAlgorithm = [System.Security.Cryptography.HashAlgorithm]::Create($algorithm)
        if (-not $hashAlgorithm) {
            continue
        }

        $stream = [System.IO.File]::OpenRead([System.IO.Path]::GetFullPath($Path))
        try {
            $actual = [Convert]::ToBase64String($hashAlgorithm.ComputeHash($stream))
        }
        finally {
            $stream.Dispose()
            $hashAlgorithm.Dispose()
        }

        if ([string]::Equals($actual, $expected, [System.StringComparison]::Ordinal)) {
            return $true
        }
    }

    return $false
}

function ConvertTo-PackageNpmObjectArray {
    [CmdletBinding()]
    param(
        [AllowNull()]
        [object]$Value
    )

    if ($null -eq $Value) {
        return @()
    }

    if ($Value -is [System.Array]) {
        return @($Value | Where-Object { $null -ne $_ })
    }

    return @($Value)
}

function ConvertFrom-PackageNpmJsonOutput {
    [CmdletBinding()]
    param(
        [AllowNull()]
        [object[]]$Output,

        [Parameter(Mandatory = $true)]
        [string]$OperationName
    )

    $text = @($Output | Where-Object { $null -ne $_ }) -join [Environment]::NewLine
    if ([string]::IsNullOrWhiteSpace($text)) {
        throw "$OperationName did not return JSON output."
    }

    try {
        return @(ConvertTo-PackageNpmObjectArray -Value ($text | ConvertFrom-Json))
    }
    catch {
        $start = $text.IndexOf('[')
        $end = $text.LastIndexOf(']')
        if ($start -ge 0 -and $end -gt $start) {
            try {
                return @(ConvertTo-PackageNpmObjectArray -Value ($text.Substring($start, ($end - $start + 1)) | ConvertFrom-Json))
            }
            catch {
                # Report the original parse failure below; the sliced retry is only a noise guard.
            }
        }
        throw "$OperationName did not return parseable JSON output. $($_.Exception.Message)"
    }
}

function Test-PackageNpmPlatformListAllows {
    [CmdletBinding()]
    param(
        [AllowNull()]
        [object]$List,

        [Parameter(Mandatory = $true)]
        [string]$Value
    )

    $entries = @(ConvertTo-PackageNpmObjectArray -Value $List | ForEach-Object { [string]$_ })
    if ($entries.Count -eq 0) {
        return $true
    }

    $negative = @($entries | Where-Object { $_.StartsWith('!') } | ForEach-Object { $_.Substring(1) })
    if ($negative -contains $Value) {
        return $false
    }

    $positive = @($entries | Where-Object { -not $_.StartsWith('!') })
    return ($positive.Count -eq 0 -or $positive -contains $Value)
}

function Get-PackageNpmNameFromLockKey {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [string]$LockKey
    )

    $parts = $LockKey -split 'node_modules/'
    return ($parts[$parts.Count - 1]).TrimEnd('/')
}

function Get-PackageNpmFileNameFromResolved {
    [CmdletBinding()]
    param(
        [AllowNull()]
        [string]$Resolved
    )

    if ([string]::IsNullOrWhiteSpace($Resolved)) {
        return $null
    }

    try {
        $uri = [Uri]$Resolved
        return [Uri]::UnescapeDataString(($uri.AbsolutePath -split '/')[-1])
    }
    catch {
        return $null
    }
}

function Read-PackageNpmLockJson {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [string]$LockFilePath
    )

    $json = Get-Content -LiteralPath $LockFilePath -Raw
    $convertCommand = Get-Command -Name ConvertFrom-Json -ErrorAction Stop
    if ($convertCommand.Parameters.ContainsKey('AsHashTable')) {
        return ($json | ConvertFrom-Json -AsHashTable)
    }

    try {
        if (-not [System.Type]::GetType('System.Web.Script.Serialization.JavaScriptSerializer, System.Web.Extensions', $false)) {
            Add-Type -AssemblyName System.Web.Extensions -ErrorAction Stop
        }
        $serializer = New-Object System.Web.Script.Serialization.JavaScriptSerializer
        $serializer.MaxJsonLength = [int]::MaxValue
        return $serializer.DeserializeObject($json)
    }
    catch {
        throw "Unable to parse npm package-lock.json without PowerShell ConvertFrom-Json -AsHashTable support. $($_.Exception.Message)"
    }
}

function Get-PackageNpmMaterializedPackagesFromLock {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [string]$LockFilePath,

        [Parameter(Mandatory = $true)]
        [string]$NpmPlatform,

        [Parameter(Mandatory = $true)]
        [string]$NpmArchitecture
    )

    $lock = Read-PackageNpmLockJson -LockFilePath $LockFilePath
    if (-not $lock) {
        return @()
    }

    $packageEntries = @()
    if ($lock -is [System.Collections.IDictionary]) {
        if (-not $lock.ContainsKey('packages')) {
            return @()
        }
        $packageEntries = @($lock['packages'].GetEnumerator() | ForEach-Object {
                [pscustomobject]@{ Name = [string]$_.Key; Value = $_.Value }
            })
    }
    elseif ($lock.PSObject.Properties['packages']) {
        $packageEntries = @($lock.packages.PSObject.Properties)
    }
    else {
        return @()
    }

    $packages = New-Object System.Collections.Generic.List[object]
    foreach ($property in @($packageEntries)) {
        $lockKey = [string]$property.Name
        $entry = $property.Value
        if ($lockKey -notlike '*node_modules/*' -or -not $entry) {
            continue
        }

        $version = if ($entry -is [System.Collections.IDictionary]) { [string]$entry['version'] } else { [string]$entry.version }
        $resolved = if ($entry -is [System.Collections.IDictionary]) { [string]$entry['resolved'] } else { [string]$entry.resolved }
        $integrity = if ($entry -is [System.Collections.IDictionary]) { [string]$entry['integrity'] } else { [string]$entry.integrity }
        if ([string]::IsNullOrWhiteSpace($version) -or [string]::IsNullOrWhiteSpace($resolved) -or [string]::IsNullOrWhiteSpace($integrity)) {
            continue
        }

        $os = if ($entry -is [System.Collections.IDictionary]) { $entry['os'] } else { $entry.os }
        $cpu = if ($entry -is [System.Collections.IDictionary]) { $entry['cpu'] } else { $entry.cpu }
        if (-not (Test-PackageNpmPlatformListAllows -List $os -Value $NpmPlatform) -or
            -not (Test-PackageNpmPlatformListAllows -List $cpu -Value $NpmArchitecture)) {
            continue
        }

        $packages.Add([pscustomobject]@{
            Name      = Get-PackageNpmNameFromLockKey -LockKey $lockKey
            Version   = $version
            Resolved  = $resolved
            Integrity = $integrity
            FileName  = Get-PackageNpmFileNameFromResolved -Resolved $resolved
            Optional  = if ($entry -is [System.Collections.IDictionary]) { [bool]$entry['optional'] } else { [bool]($entry.PSObject.Properties['optional'] -and $entry.optional) }
        }) | Out-Null
    }

    return @($packages.ToArray())
}

function Test-PackageNpmMaterializationDirectory {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [string]$Directory
    )

    if (-not (Test-Path -LiteralPath $Directory -PathType Container)) {
        return $null
    }

    $tarballPaths = @(Get-ChildItem -LiteralPath $Directory -Filter '*.tgz' -File -ErrorAction SilentlyContinue | ForEach-Object { [System.IO.Path]::GetFullPath($_.FullName) })
    if ($tarballPaths.Count -eq 0) {
        return $null
    }

    return [pscustomobject]@{
        Directory    = [System.IO.Path]::GetFullPath($Directory)
        TarballPaths = @($tarballPaths)
    }
}

function Copy-PackageNpmMaterializationDirectory {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [string]$SourceDirectory,

        [Parameter(Mandatory = $true)]
        [string]$TargetDirectory,

        [Parameter(Mandatory = $true)]
        [psobject]$Materialization
    )

    $null = New-Item -ItemType Directory -Path $TargetDirectory -Force
    $copiedTarballs = New-Object System.Collections.Generic.List[string]
    foreach ($sourcePath in @($Materialization.TarballPaths)) {
        $targetPath = [System.IO.Path]::GetFullPath((Join-Path $TargetDirectory (Split-Path -Leaf ([string]$sourcePath))))
        $null = Copy-FileToPath -SourcePath ([string]$sourcePath) -TargetPath $targetPath -Overwrite
        $copiedTarballs.Add($targetPath) | Out-Null
    }

    return [pscustomobject]@{
        Directory    = [System.IO.Path]::GetFullPath($TargetDirectory)
        TarballPaths = @($copiedTarballs.ToArray())
    }
}

function Find-PackageNpmMaterializationInDepots {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [psobject]$PackageResult
    )

    foreach ($depotSource in @(Get-PackagePackageDepotSources -PackageConfig $PackageResult.PackageConfig)) {
        if ([string]::IsNullOrWhiteSpace([string]$depotSource.basePath)) {
            continue
        }

        $candidateDirectory = [System.IO.Path]::GetFullPath((Join-Path ([string]$depotSource.basePath) ([string]$PackageResult.PackageDepotRelativeDirectory)))
        $materialization = Test-PackageNpmMaterializationDirectory -Directory $candidateDirectory
        if ($materialization) {
            $materialization | Add-Member -MemberType NoteProperty -Name SourceId -Value ([string]$depotSource.id) -Force
            $materialization | Add-Member -MemberType NoteProperty -Name SourceDirectory -Value $candidateDirectory -Force
            return $materialization
        }
    }

    return $null
}

function New-PackageNpmMaterializationFromRegistry {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [psobject]$PackageResult,

        [Parameter(Mandatory = $true)]
        [string]$PackageSpec,

        [Parameter(Mandatory = $true)]
        [string]$NpmPlatform,

        [Parameter(Mandatory = $true)]
        [string]$NpmArchitecture,

        [Parameter(Mandatory = $true)]
        [string]$TargetDirectory
    )

    Write-PackageExecutionMessage -Message ("[STATE] Materializing npm package spec '{0}'." -f $PackageSpec)
    $installerCommandInfo = Resolve-PackageNpmInstallerCommand -PackageResult $PackageResult
    $cacheDirectory = New-PackageNpmCacheDirectory -PackageResult $PackageResult
    $globalConfigPath = Initialize-PackageNpmGlobalConfig -GlobalConfigPath (Get-PackageNpmGlobalConfigPath -PackageResult $PackageResult)
    $targetFullPath = [System.IO.Path]::GetFullPath($TargetDirectory)
    $resolutionDirectory = [System.IO.Path]::GetFullPath((Join-Path $targetFullPath '.npm-resolution'))

    Remove-PathIfExists -Path $resolutionDirectory | Out-Null
    $null = New-Item -ItemType Directory -Path $targetFullPath -Force
    $null = New-Item -ItemType Directory -Path $resolutionDirectory -Force

    $lockArguments = @('install', '--package-lock-only', '--ignore-scripts', '--no-audit', '--no-fund', '--cache', $cacheDirectory)
    $lockArguments += @(Get-NpmGlobalConfigArguments -GlobalConfigPath $globalConfigPath)
    $lockArguments += $PackageSpec

    Push-Location $resolutionDirectory
    try {
        & ([string]$installerCommandInfo.CommandPath) @lockArguments
        $exitCode = $LASTEXITCODE
        if ($null -eq $exitCode) {
            $exitCode = 0
        }
    }
    finally {
        Pop-Location
    }
    if ($exitCode -ne 0) {
        throw "npm metadata resolution for '$PackageSpec' failed with exit code $exitCode."
    }

    $lockFilePath = Join-Path $resolutionDirectory 'package-lock.json'
    if (-not (Test-Path -LiteralPath $lockFilePath -PathType Leaf)) {
        throw "npm metadata resolution for '$PackageSpec' did not produce package-lock.json."
    }

    $packages = @(Get-PackageNpmMaterializedPackagesFromLock -LockFilePath $lockFilePath -NpmPlatform $NpmPlatform -NpmArchitecture $NpmArchitecture)
    if ($packages.Count -eq 0) {
        throw "npm metadata resolution for '$PackageSpec' did not produce any materializable packages."
    }

    foreach ($package in $packages) {
        $packageName = [string]$package.Name
        $packageVersion = [string]$package.Version
        $knownFileName = [string]$package.FileName
        $knownIntegrity = [string]$package.Integrity
        if ([string]::IsNullOrWhiteSpace($packageName) -or [string]::IsNullOrWhiteSpace($packageVersion)) {
            throw "npm materialization package metadata must include name and version."
        }
        if (-not [string]::IsNullOrWhiteSpace($knownFileName) -and -not [string]::IsNullOrWhiteSpace($knownIntegrity)) {
            $knownTargetPath = [System.IO.Path]::GetFullPath((Join-Path $targetFullPath $knownFileName))
            if (Test-PackageNpmIntegrity -Path $knownTargetPath -Integrity $knownIntegrity) {
                continue
            }
        }

        $packageSpecForPack = '{0}@{1}' -f $packageName, $packageVersion
        $packArguments = @('pack', $packageSpecForPack, '--pack-destination', $targetFullPath, '--json', '--cache', $cacheDirectory)
        $packArguments += @(Get-NpmGlobalConfigArguments -GlobalConfigPath $globalConfigPath)

        Write-PackageExecutionMessage -Message ("[STATE] Packing npm materialized package '{0}'." -f $packageSpecForPack)
        Push-Location $targetFullPath
        try {
            $packOutput = & ([string]$installerCommandInfo.CommandPath) @packArguments
            $packExitCode = $LASTEXITCODE
            if ($null -eq $packExitCode) {
                $packExitCode = 0
            }
        }
        finally {
            Pop-Location
        }
        if ($packExitCode -ne 0) {
            throw "npm pack for '$packageSpecForPack' failed with exit code $packExitCode."
        }

        $packItems = @(ConvertFrom-PackageNpmJsonOutput -Output $packOutput -OperationName "npm pack for '$packageSpecForPack'")
        $packItem = @($packItems | Where-Object {
                [string]::Equals([string]$_.name, $packageName, [System.StringComparison]::OrdinalIgnoreCase) -and
                [string]::Equals([string]$_.version, $packageVersion, [System.StringComparison]::OrdinalIgnoreCase)
            } | Select-Object -First 1)
        if ($packItem.Count -eq 0) {
            $packItem = @($packItems | Select-Object -First 1)
        }

        $fileName = [string]$packItem[0].filename
        if ([string]::IsNullOrWhiteSpace($fileName)) {
            $fileName = $knownFileName
        }
        $integrity = [string]$packItem[0].integrity
        if ([string]::IsNullOrWhiteSpace($integrity)) {
            $integrity = $knownIntegrity
        }
        if ([string]::IsNullOrWhiteSpace($fileName) -or [string]::IsNullOrWhiteSpace($integrity)) {
            throw "npm pack for '$packageSpecForPack' did not report tarball filename and integrity metadata."
        }
        $targetPath = [System.IO.Path]::GetFullPath((Join-Path $targetFullPath $fileName))
        if (-not (Test-PackageNpmIntegrity -Path $targetPath -Integrity $integrity)) {
            throw "npm pack output '$fileName' did not satisfy integrity metadata."
        }
    }

    $materialization = Test-PackageNpmMaterializationDirectory -Directory $targetFullPath
    if (-not $materialization) {
        throw "npm materialization for '$PackageSpec' could not be validated after download."
    }

    return $materialization
}

function Invoke-PackageNpmMaterializationDepotDistribution {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [psobject]$PackageResult,

        [Parameter(Mandatory = $true)]
        [psobject]$Materialization
    )

    $mode = if ($PackageResult.PackageConfig.PSObject.Properties['DepotDistributionMode'] -and
        -not [string]::IsNullOrWhiteSpace([string]$PackageResult.PackageConfig.DepotDistributionMode)) {
        [string]$PackageResult.PackageConfig.DepotDistributionMode
    }
    else {
        'packageFocused'
    }

    $files = New-Object System.Collections.Generic.List[object]
    foreach ($tarballPath in @($Materialization.TarballPaths)) {
        if ([string]::IsNullOrWhiteSpace([string]$tarballPath) -or -not (Test-Path -LiteralPath ([string]$tarballPath) -PathType Leaf)) {
            continue
        }
        $files.Add([pscustomobject]@{
            FileName   = Split-Path -Leaf ([string]$tarballPath)
            SourcePath = [System.IO.Path]::GetFullPath([string]$tarballPath)
        }) | Out-Null
    }

    $actions = New-Object System.Collections.Generic.List[object]
    if ([string]::Equals($mode, 'disabled', [System.StringComparison]::OrdinalIgnoreCase)) {
        return [pscustomobject]@{ Mode = $mode; Status = 'Skipped'; Reason = 'DisabledByPolicy'; Actions = @(); CopiedCount = 0; FailedCount = 0; SkippedCount = 0 }
    }

    foreach ($mirrorSource in @(Get-PackageDepotDistributionTargets -PackageConfig $PackageResult.PackageConfig)) {
        foreach ($file in @($files.ToArray())) {
            if (-not [string]::Equals([string]$mirrorSource.kind, 'filesystem', [System.StringComparison]::OrdinalIgnoreCase)) {
                $actions.Add([pscustomobject]@{ DepotId = [string]$mirrorSource.id; FileName = [string]$file.FileName; Action = 'Skip'; Status = 'Skipped'; Reason = 'UnsupportedDepotKind'; SourcePath = [string]$file.SourcePath; TargetPath = $null; EnsureExists = [bool]$mirrorSource.ensureExists; ErrorMessage = $null }) | Out-Null
                continue
            }
            if ([string]::IsNullOrWhiteSpace([string]$mirrorSource.basePath)) {
                $actions.Add([pscustomobject]@{ DepotId = [string]$mirrorSource.id; FileName = [string]$file.FileName; Action = 'Skip'; Status = 'Skipped'; Reason = 'MissingBasePath'; SourcePath = [string]$file.SourcePath; TargetPath = $null; EnsureExists = [bool]$mirrorSource.ensureExists; ErrorMessage = $null }) | Out-Null
                continue
            }

            $targetDirectory = [System.IO.Path]::GetFullPath((Join-Path ([string]$mirrorSource.basePath) ([string]$PackageResult.PackageDepotRelativeDirectory)))
            $targetPath = [System.IO.Path]::GetFullPath((Join-Path $targetDirectory ([string]$file.FileName)))
            $sourceFullPath = [System.IO.Path]::GetFullPath([string]$file.SourcePath)
            if ([string]::Equals($sourceFullPath, $targetPath, [System.StringComparison]::OrdinalIgnoreCase)) {
                $actions.Add([pscustomobject]@{ DepotId = [string]$mirrorSource.id; FileName = [string]$file.FileName; Action = 'Skip'; Status = 'Skipped'; Reason = 'SourceIsTarget'; SourcePath = $sourceFullPath; TargetPath = $targetPath; EnsureExists = [bool]$mirrorSource.ensureExists; ErrorMessage = $null }) | Out-Null
                continue
            }

            $match = Test-PackageDepotDistributionFileMatches -SourcePath $sourceFullPath -TargetPath $targetPath
            if ($match.Matches) {
                $actions.Add([pscustomobject]@{ DepotId = [string]$mirrorSource.id; FileName = [string]$file.FileName; Action = 'Skip'; Status = 'Skipped'; Reason = [string]$match.Reason; SourcePath = $sourceFullPath; TargetPath = $targetPath; EnsureExists = [bool]$mirrorSource.ensureExists; ErrorMessage = $null }) | Out-Null
                continue
            }
            if ([string]::Equals($mode, 'packageFocused', [System.StringComparison]::OrdinalIgnoreCase) -and
                -not [string]::Equals([string]$match.Reason, 'Missing', [System.StringComparison]::OrdinalIgnoreCase)) {
                $actions.Add([pscustomobject]@{ DepotId = [string]$mirrorSource.id; FileName = [string]$file.FileName; Action = 'Skip'; Status = 'Skipped'; Reason = 'DifferentTargetPreservedByPackageFocusedPolicy'; SourcePath = $sourceFullPath; TargetPath = $targetPath; EnsureExists = [bool]$mirrorSource.ensureExists; ErrorMessage = [string]$match.Reason }) | Out-Null
                continue
            }

            try {
                if ($mirrorSource.ensureExists) {
                    $null = New-Item -ItemType Directory -Path $targetDirectory -Force
                }
                $null = Copy-FileToPath -SourcePath $sourceFullPath -TargetPath $targetPath -Overwrite
                $actions.Add([pscustomobject]@{ DepotId = [string]$mirrorSource.id; FileName = [string]$file.FileName; Action = 'Copy'; Status = 'Copied'; Reason = [string]$match.Reason; SourcePath = $sourceFullPath; TargetPath = $targetPath; EnsureExists = [bool]$mirrorSource.ensureExists; ErrorMessage = $null }) | Out-Null
            }
            catch {
                $actions.Add([pscustomobject]@{ DepotId = [string]$mirrorSource.id; FileName = [string]$file.FileName; Action = 'Copy'; Status = 'Failed'; Reason = [string]$match.Reason; SourcePath = $sourceFullPath; TargetPath = $targetPath; EnsureExists = [bool]$mirrorSource.ensureExists; ErrorMessage = $_.Exception.Message }) | Out-Null
            }
        }
    }

    $copiedCount = @($actions.ToArray() | Where-Object { [string]::Equals([string]$_.Status, 'Copied', [System.StringComparison]::OrdinalIgnoreCase) }).Count
    $failedCount = @($actions.ToArray() | Where-Object { [string]::Equals([string]$_.Status, 'Failed', [System.StringComparison]::OrdinalIgnoreCase) }).Count
    $skippedCount = @($actions.ToArray() | Where-Object { [string]::Equals([string]$_.Status, 'Skipped', [System.StringComparison]::OrdinalIgnoreCase) }).Count

    return [pscustomobject]@{
        Mode         = $mode
        Status       = if ($actions.Count -eq 0) { 'Skipped' } else { 'Planned' }
        Reason       = if ($actions.Count -eq 0) { 'NoDepotTargets' } else { $null }
        Actions      = @($actions.ToArray())
        CopiedCount  = $copiedCount
        FailedCount  = $failedCount
        SkippedCount = $skippedCount
    }
}

function Invoke-PackageNpmMaterialization {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [psobject]$PackageResult
    )

    if (-not (Test-PackageNpmMaterializedInstallKind -Package $PackageResult.Package)) {
        return $PackageResult
    }

    $packageSpec = Get-PackageNpmResolvedPackageSpec -PackageResult $PackageResult
    $npmPlatform = Get-PackageNpmPlatform -PackageConfig $PackageResult.PackageConfig
    $npmArchitecture = Get-PackageNpmArchitecture -PackageConfig $PackageResult.PackageConfig
    $stageDirectory = Get-PackageNpmMaterializationDirectory -PackageResult $PackageResult
    Remove-PathIfExists -Path $stageDirectory | Out-Null

    $depotMaterialization = Find-PackageNpmMaterializationInDepots -PackageResult $PackageResult
    if ($depotMaterialization) {
        $copied = Copy-PackageNpmMaterializationDirectory -SourceDirectory ([string]$depotMaterialization.SourceDirectory) -TargetDirectory $stageDirectory -Materialization $depotMaterialization
        $PackageResult | Add-Member -MemberType NoteProperty -Name NpmMaterialization -Value ([pscustomobject]@{
            Success         = $true
            Status          = 'HydratedFromDepot'
            PackageSpec     = $packageSpec
            NpmPlatform     = $npmPlatform
            NpmArchitecture = $npmArchitecture
            SourceId        = [string]$depotMaterialization.SourceId
            TarballPaths    = @($copied.TarballPaths)
            DepotDistribution = $null
        }) -Force
        Write-PackageExecutionMessage -Message ("[ACTION] Hydrated npm materialization from depot '{0}'." -f [string]$depotMaterialization.SourceId)
    }
    else {
        $materialization = New-PackageNpmMaterializationFromRegistry -PackageResult $PackageResult -PackageSpec $packageSpec -NpmPlatform $npmPlatform -NpmArchitecture $npmArchitecture -TargetDirectory $stageDirectory
        $PackageResult | Add-Member -MemberType NoteProperty -Name NpmMaterialization -Value ([pscustomobject]@{
            Success         = $true
            Status          = 'MaterializedFromRegistry'
            PackageSpec     = $packageSpec
            NpmPlatform     = $npmPlatform
            NpmArchitecture = $npmArchitecture
            SourceId        = 'npmRegistry'
            TarballPaths    = @($materialization.TarballPaths)
            DepotDistribution = $null
        }) -Force
        Write-PackageExecutionMessage -Message ("[ACTION] Materialized npm package spec '{0}' with {1} tarball(s)." -f $packageSpec, @($materialization.TarballPaths).Count)
    }

    $distribution = Invoke-PackageNpmMaterializationDepotDistribution -PackageResult $PackageResult -Materialization $PackageResult.NpmMaterialization
    $PackageResult.NpmMaterialization.DepotDistribution = $distribution
    Write-PackageExecutionMessage -Message ("[STATE] npm materialization depot distribution completed: mode='{0}', copied={1}, skipped={2}, failed={3}." -f [string]$distribution.Mode, [int]$distribution.CopiedCount, [int]$distribution.SkippedCount, [int]$distribution.FailedCount)

    return $PackageResult
}

function Install-PackageNpmPackage {
<#
.SYNOPSIS
Installs an exact npm package spec into a staged Package-owned prefix.
#>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [psobject]$PackageResult
    )

    $install = Get-PackageAssignedInstallOperation -Release $PackageResult.Package
    if (-not $install) {
        throw "Package npm global package install for '$($PackageResult.PackageId)' requires packageOperations.assigned.install on the selected release."
    }
    if (-not $install.PSObject.Properties['packageSpec'] -or [string]::IsNullOrWhiteSpace([string]$install.packageSpec)) {
        throw "Package npm global package install for '$($PackageResult.PackageId)' requires packageOperations.assigned.install.packageSpec."
    }

    $packageSpec = Resolve-PackageTemplateText -Text ([string]$install.packageSpec) -PackageConfig $PackageResult.PackageConfig -Package $PackageResult.Package
    $installerCommandInfo = Resolve-PackageNpmInstallerCommand -PackageResult $PackageResult
    $cacheDirectory = New-PackageNpmCacheDirectory -PackageResult $PackageResult
    $globalConfigPath = Initialize-PackageNpmGlobalConfig -GlobalConfigPath (Get-PackageNpmGlobalConfigPath -PackageResult $PackageResult)
    if ([string]::IsNullOrWhiteSpace([string]$PackageResult.PackageInstallStageDirectory)) {
        throw "Package npm global package install for '$($PackageResult.PackageId)' requires a package install stage directory."
    }
    $stagePath = [System.IO.Path]::GetFullPath([string]$PackageResult.PackageInstallStageDirectory)
    Remove-PathIfExists -Path $stagePath | Out-Null
    $null = New-Item -ItemType Directory -Path $stagePath -Force
    $stagePromoted = $false

    $commandArguments = @('install', '-g', '--prefix', $stagePath, '--cache', $cacheDirectory)
    $commandArguments += @(Get-NpmGlobalConfigArguments -GlobalConfigPath $globalConfigPath)
    $commandArguments += $packageSpec

    Write-PackageExecutionMessage -Message ("[STATE] npm global package install:")
    Write-PackageExecutionMessage -Message ("[PATH] npm command: {0}" -f $installerCommandInfo.CommandPath)
    Write-PackageExecutionMessage -Message ("[PATH] npm stage: {0}" -f $stagePath)
    Write-PackageExecutionMessage -Message ("[PATH] npm cache: {0}" -f $cacheDirectory)
    Write-PackageExecutionMessage -Message ("[PATH] npm global config: {0}" -f $globalConfigPath)
    Write-PackageExecutionMessage -Message ("[STATE] npm package spec: {0}" -f $packageSpec)

    try {
        Push-Location $stagePath
        try {
            & $installerCommandInfo.CommandPath @commandArguments
            $exitCode = $LASTEXITCODE
            if ($null -eq $exitCode) {
                $exitCode = 0
            }
        }
        finally {
            Pop-Location
        }

        if ($exitCode -ne 0) {
            throw "Package npm global package install for '$($PackageResult.PackageId)' failed with exit code $exitCode."
        }

        $installParent = Split-Path -Parent $PackageResult.InstallDirectory
        if (-not [string]::IsNullOrWhiteSpace($installParent)) {
            $null = New-Item -ItemType Directory -Path $installParent -Force
        }
        Remove-PathIfExists -Path $PackageResult.InstallDirectory | Out-Null
        Move-Item -LiteralPath $stagePath -Destination $PackageResult.InstallDirectory -Force
        $stagePromoted = $true
    }
    finally {
        if (-not $stagePromoted) {
            Write-PackageExecutionMessage -Level 'WRN' -Message ("[WARN] Preserving failed npm package install stage '{0}' for inspection." -f $stagePath)
        }
    }

    return [pscustomobject]@{
        Status           = Get-PackageOwnedInstallStatus -PackageResult $PackageResult
        InstallKind      = 'npmGlobalPackage'
        InstallDirectory = $PackageResult.InstallDirectory
        ReusedExisting   = $false
        InstallerCommand = $installerCommandInfo.Command
        InstallerCommandPath = $installerCommandInfo.CommandPath
        PackageSpec      = $packageSpec
        CommandArguments = @($commandArguments)
        CacheDirectory   = $cacheDirectory
        GlobalConfigPath = $globalConfigPath
        StagePath        = $stagePath
        ExitCode         = $exitCode
    }
}

function Install-PackageNpmMaterializedInstallGlobalPackage {
<#
.SYNOPSIS
Installs a materialized npm package spec from local tarballs into a staged Package-owned prefix.
#>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [psobject]$PackageResult
    )

    $install = Get-PackageAssignedInstallOperation -Release $PackageResult.Package
    if (-not $install) {
        throw "Package npm materialized install for '$($PackageResult.PackageId)' requires packageOperations.assigned.install on the selected release."
    }
    if (-not $PackageResult.PSObject.Properties['NpmMaterialization'] -or -not $PackageResult.NpmMaterialization -or -not $PackageResult.NpmMaterialization.Success) {
        throw "Package npm materialized install for '$($PackageResult.PackageId)' requires prepared npm materialization."
    }

    $packageSpec = Get-PackageNpmResolvedPackageSpec -PackageResult $PackageResult
    $tarballPaths = @($PackageResult.NpmMaterialization.TarballPaths)
    if ($tarballPaths.Count -eq 0) {
        throw "Package npm materialized install for '$($PackageResult.PackageId)' has no local materialized tarballs."
    }
    foreach ($tarballPath in $tarballPaths) {
        if ([string]::IsNullOrWhiteSpace([string]$tarballPath) -or -not (Test-Path -LiteralPath ([string]$tarballPath) -PathType Leaf)) {
            throw "Package npm materialized install for '$($PackageResult.PackageId)' is missing materialized tarball '$tarballPath'."
        }
    }

    $installerCommandInfo = Resolve-PackageNpmInstallerCommand -PackageResult $PackageResult
    $cacheDirectory = New-PackageNpmCacheDirectory -PackageResult $PackageResult
    $globalConfigPath = Initialize-PackageNpmGlobalConfig -GlobalConfigPath (Get-PackageNpmGlobalConfigPath -PackageResult $PackageResult)
    if ([string]::IsNullOrWhiteSpace([string]$PackageResult.PackageInstallStageDirectory)) {
        throw "Package npm materialized install for '$($PackageResult.PackageId)' requires a package install stage directory."
    }

    $stagePath = [System.IO.Path]::GetFullPath([string]$PackageResult.PackageInstallStageDirectory)
    Remove-PathIfExists -Path $stagePath | Out-Null
    $null = New-Item -ItemType Directory -Path $stagePath -Force
    $stagePromoted = $false

    $cacheAddArguments = @('cache', 'add')
    $cacheAddArguments += @($tarballPaths)
    $cacheAddArguments += @('--cache', $cacheDirectory)
    $cacheAddArguments += @(Get-NpmGlobalConfigArguments -GlobalConfigPath $globalConfigPath)

    $commandArguments = @('install', '-g', '--prefix', $stagePath, '--cache', $cacheDirectory, '--offline')
    $commandArguments += @(Get-NpmGlobalConfigArguments -GlobalConfigPath $globalConfigPath)
    $commandArguments += $packageSpec

    Write-PackageExecutionMessage -Message ("[STATE] npm materialized package install:")
    Write-PackageExecutionMessage -Message ("[PATH] npm command: {0}" -f $installerCommandInfo.CommandPath)
    Write-PackageExecutionMessage -Message ("[PATH] npm stage: {0}" -f $stagePath)
    Write-PackageExecutionMessage -Message ("[PATH] npm cache: {0}" -f $cacheDirectory)
    Write-PackageExecutionMessage -Message ("[STATE] npm materialized tarballs: {0}" -f ($tarballPaths -join ', '))
    Write-PackageExecutionMessage -Message ("[STATE] npm package spec: {0}" -f $packageSpec)

    try {
        & ([string]$installerCommandInfo.CommandPath) @cacheAddArguments
        $cacheAddExitCode = $LASTEXITCODE
        if ($null -eq $cacheAddExitCode) {
            $cacheAddExitCode = 0
        }
        if ($cacheAddExitCode -ne 0) {
            throw "npm cache add for materialized package '$($PackageResult.PackageId)' failed with exit code $cacheAddExitCode."
        }

        Push-Location $stagePath
        try {
            & ([string]$installerCommandInfo.CommandPath) @commandArguments
            $exitCode = $LASTEXITCODE
            if ($null -eq $exitCode) {
                $exitCode = 0
            }
        }
        finally {
            Pop-Location
        }

        if ($exitCode -ne 0) {
            throw "Package npm materialized install for '$($PackageResult.PackageId)' failed with exit code $exitCode."
        }

        $installParent = Split-Path -Parent $PackageResult.InstallDirectory
        if (-not [string]::IsNullOrWhiteSpace($installParent)) {
            $null = New-Item -ItemType Directory -Path $installParent -Force
        }
        Remove-PathIfExists -Path $PackageResult.InstallDirectory | Out-Null
        Move-Item -LiteralPath $stagePath -Destination $PackageResult.InstallDirectory -Force
        $stagePromoted = $true
    }
    finally {
        if (-not $stagePromoted) {
            Write-PackageExecutionMessage -Level 'WRN' -Message ("[WARN] Preserving failed npm materialized install stage '{0}' for inspection." -f $stagePath)
        }
    }

    return [pscustomobject]@{
        Status           = Get-PackageOwnedInstallStatus -PackageResult $PackageResult
        InstallKind      = 'npmMaterializedInstallGlobalPackage'
        InstallDirectory = $PackageResult.InstallDirectory
        ReusedExisting   = $false
        InstallerCommand = $installerCommandInfo.Command
        InstallerCommandPath = $installerCommandInfo.CommandPath
        PackageSpec      = $packageSpec
        MaterializedTarballPaths = @($tarballPaths)
        CacheAddArguments = @($cacheAddArguments)
        CommandArguments = @($commandArguments)
        CacheDirectory   = $cacheDirectory
        GlobalConfigPath = $globalConfigPath
        StagePath        = $stagePath
        ExitCode         = $exitCode
    }
}