NpmPS.psm1

$Script:PSModuleRoot = $PSScriptRoot
# Importing from [/home/vsts/work/1/s/NpmPS\Public]
# ./NpmPS/Public/Get-NpmPackageInfo.ps1
function Get-NpmPackageInfo
{
    <#
        .Synopsis
        Get package info from NPM registry

        .Example
        Get-NpmPackageInfo -Name contoso-component -Registry 'http://contoso.local/npm'
        .Notes
    #>

    [cmdletbinding()]
    param(
        # Name of the npm package
        [Alias('PackageName')]
        [Parameter(
            Mandatory,
            Position = 0,
            ValueFromPipelineByPropertyName
        )]
        [ValidateNotNullOrEmpty()]
        [String]
        $Name,

        # NPM registry uri
        [Alias('URI','Repository')]
        [Parameter(
            Mandatory,
            Position = 1,
            ValueFromPipelineByPropertyName
        )]
        [ValidateNotNullOrEmpty()]
        [String]
        $Registry
    )

    process
    {
        try
        {
            # need trailing slash on registry name
            if ( $Registry -notmatch '/$' )
            {
                $Registry = "$Registry/"
            }

            $uri = "{0}{1}" -f $Registry, $Name

            Invoke-RestMethod -Uri $uri
        }
        catch
        {
            $PSCmdlet.ThrowTerminatingError( $PSItem )
        }
    }
}

# ./NpmPS/Public/Publish-NpmPackage.ps1
function Publish-NpmPackage
{
    <#
        .Synopsis
        Publishes a npm package

        .Example

        $registry = 'https://contoso.local/npm'
        $credential = Get-Credential

        $publishLDNpmPackageSplat = @{
            Credential = $credential
            Email = user@contoso.com
            Path = $path
            Registry = $registry
            Version = '0.1.0-rc.1'
            Tag = 'testrelease'
        }
        Publish-NpmPackage @publishLDNpmPackageSplat

        .Notes

    #>

    [cmdletbinding()]
    param(
        # Location of the package.json
        [Parameter(
            Mandatory,
            Position = 0,
            ValueFromPipelineByPropertyName
        )]
        [ValidateNotNullOrEmpty()]
        [String]
        $Path,

        # NPM Registry to publish
        [Alias('Repository')]
        [String]
        $Registry,

        # Username and API Token as password
        [PSCredential]
        $Credential,

        # Email
        [String]
        $Email,

        # SemVer
        [String]
        $Version,

        # tags to set when publishing
        [Alias('Tags')]
        [String[]]
        $Tag,

        # Force publish even if package already exists
        [Switch]
        $Force
    )

    begin
    {
        $npmProtocolRegex = '^https?:'
    }

    process
    {
        try
        {
            $Path = $Path -replace '\\\\[\w\d-]*\\(\w)\$', '$1:'

            if ( -not ( Test-Path -Path $Path ) )
            {
                Write-Error "Could not find [$Path] or access is denied" -ErrorAction Stop
            }
            elseif ( Test-Path -Path $Path -PathType Leaf )
            {
                $Path = Split-Path -Path $Path
            }
            $package = Join-Path -Path $Path -ChildPath 'package.json'

            if ( -not ( Test-Path -Path $package ) )
            {
                Write-Error "Could not find NPM Package [$package] or access is denied" -ErrorAction Stop
            }

            if ( $Registry -notmatch $npmProtocolRegex )
            {
                Write-Error "Registry URI [$Registry] is not valid. Should match regex pattern [$npmProtocolRegex]" -ErrorAction Stop
            }

            Write-Verbose "Deploying package from path [$path]"

            Push-Location -Path $Path -StackName PublishNpmPackage
            Write-Verbose "Working directory [$pwd]"

            if ( ![string]::IsNullOrEmpty( $Version ) )
            {
                if ($Version -notmatch '^v?\d+\.\d+(\.\d+)?')
                {
                    Write-Error "Version [$Version] is not a valid SemVer" -ErrorAction 'Stop'
                }

                # ProGet needs to use period instead of plus for semver
                $Version -replace '\+', '.'

                $json = Get-Content $package -Raw | ConvertFrom-LDJson -ErrorAction Stop

                $json.version = $Version

                Write-Verbose "Set version to [{0}]" -f $json.version
                $json | Format-LDJson | Set-Content -Path $package -Encoding UTF8
            }

            Write-Verbose "Contents of [$package]:"
            $packageData = Get-Content -Path $package -Raw
            $packageObject = $packageData | ConvertFrom-JSON

            Write-Verbose "Package version [$($packageObject.Version)]"

            Write-Verbose "Checking to see if package is already published"

            if ( Test-NpmPackage -Name $packageObject.name -Version $packageObject.version )
            {
                Write-Verbose 'This package is already published'
                if ( !$Force )
                {
                    return
                }
                Write-Warning "This version of the package is already published. This will invalidate existing checksums for this version."
            }

            Write-Verbose "NPM Version [$(npm --version)]"

            $password = $Credential.GetNetworkCredential().password

            # need trailing slash on registry name
            if ( $Registry -notmatch '/$' )
            {
                $Registry = "$Registry/"
            }

            # need registry without protocol prefix
            $shortRegName = $Registry -replace $npmProtocolRegex

            $config = @"
registry=${Registry}
${shortRegName}:_password=$password
${shortRegName}:username=$($credential.username)
${shortRegName}:email=$Email
${shortRegName}:always-auth=false
"@

            $configPath = Join-Path $pwd -ChildPath '.npmrc'

            Write-Verbose "Saving registry info to project local config [$configPath]"
            Set-Content -Path $configPath -Value $config

            Write-Verbose "Running [npm config list]"
            npm config list

            if ( $null -eq $PSBoundParameters.Tag  )
            {
                Write-Verbose "Running [npm publish]"
                npm publish -tag prerelease
            }
            else
            {
                Write-Verbose "Publishing with tags [$( $Tag -join ',' )]"
                foreach ($publishTag in $Tag)
                {
                    Write-Verbose "Running [npm publish -tag $publishTag]"
                    npm publish -tag $publishTag
                }
            }

            if ( Test-NpmPackage -Name $packageObject.name -Version $packageObject.version )
            {
                Write-Verbose 'This package is now available in the registry'
            }
            else
            {
                Write-Error "This published package could not be found in the registry [$registry]"
            }
        }
        catch
        {
            $PSCmdlet.ThrowTerminatingError( $PSItem )
        }
        finally
        {
            Pop-Location -StackName PublishNpmPackage
        }
    }
}

# ./NpmPS/Public/Test-NpmPackage.ps1
function Test-NpmPackage
{
    <#
        .Synopsis
        Tests to see if specified package is already published

        .Example
        $Name = 'contoso-component'
        $Registry = 'http://contoso.local/npm/'
        Test-NpmPackage -Name $Name -Registry $Registry

        .Example
        $Name = 'contoso-component'
        $Registry = 'http://contoso.local/npm/'
        Test-NpmPackage -Name $Name -Registry $Registry -Version 0.0.1 -Tag Latest

        .Notes

    #>

    [cmdletbinding()]
    param(
        # Name of the npm package
        [Alias('PackageName')]
        [Parameter(
            Mandatory,
            Position = 0,
            ValueFromPipelineByPropertyName
        )]
        [ValidateNotNullOrEmpty()]
        [String]
        $Name,

        # NPM Registry uri
        [Alias('URI','Repository')]
        [Parameter(
            Mandatory,
            Position = 1,
            ValueFromPipelineByPropertyName
        )]
        [ValidateNotNullOrEmpty()]
        [String]
        $Registry,

        # NPM Package Version
        [Parameter(
            Position = 2,
            ValueFromPipelineByPropertyName
        )]
        [String]
        $Version,

        # Package tag
        [Parameter(
            Position = 3,
            ValueFromPipelineByPropertyName
        )]
        [String]
        $Tag
    )

    process
    {
        try
        {
            try
            {
                Write-Verbose "Querying for [$Name] in [$Registry]"
                $package = Get-NpmPackageInfo -Name $Name -Registry $Registry -ErrorAction Stop
            }
            catch
            {
                Write-Verbose 'Was not able to connect to registry or find the package'
                Write-Verbose $PSItem
                return $false
            }

            if ( $null -eq $package )
            {
                Write-Verbose 'No package was found'
                return $false
            }

            if ( ![String]::IsNullOrEmpty( $Version ) )
            {
                if ( $null -eq $package.versions )
                {
                    Write-Verbose "This package does not have a version"
                    return $false
                }

                if ( -not $package.versions.$Version )
                {
                    Write-Verbose "This package version [$Version] was not found"
                    return $false
                }
            }

            if ( ![String]::IsNullOrEmpty( $Tag ) )
            {
                if ( $null -eq $package.'dist-tags' -or @($package.'dist-tags').count -lt 1 )
                {
                    Write-Verbose "This package does not have a tag"
                    return $false
                }

                if ( -not $package.'dist-tags'.$Tag )
                {
                    Write-Verbose "This package tag [$Tag] was not found"
                    return $false
                }
            }

            if ( ![String]::IsNullOrEmpty( $Version ) -and
                 ![String]::IsNullOrEmpty( $Tag ) )
            {
                if ( $package.'dist-tags'.$Tag -ne $Version )
                {
                    Write-Verbose ( "This package has tag [{0}][{1}] but did not match version [{2}]" -f $Tag,$package.'dist-tags'[$Tag],$Version )
                    return $false
                }
            }

            $true
        }
        catch
        {
            $PSCmdlet.ThrowTerminatingError( $PSItem )
        }
    }
}