Skip to content
Merged
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
6 changes: 6 additions & 0 deletions .changeset/fix-outdated-deprecation-display.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@pnpm/plugin-commands-outdated": patch
"pnpm": patch
---

Show deprecation in table/list formats when latest version is deprecated [#8658](https://github.com/pnpm/pnpm/issues/8658).
5 changes: 5 additions & 0 deletions .changeset/funny-melons-pay.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@pnpm/fetch": patch
---

When the node-fetch request redirects an installation link and returns a relative path, URL parsing may fail [#10286](https://github.com/pnpm/pnpm/pull/10286).
12 changes: 10 additions & 2 deletions network/fetch/src/fetchFromRegistry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,10 +81,10 @@ export function createFetchFromRegistry (defaultOpts: CreateFetchFromRegistryOpt
return response
}

redirects++
// This is a workaround to remove authorization headers on redirect.
// Related pnpm issue: https://github.com/pnpm/pnpm/issues/1815
redirects++
urlObject = new URL(response.headers.get('location')!)
urlObject = resolveRedirectUrl(response, urlObject)
if (!headers['authorization'] || originalHost === urlObject.host) continue
delete headers.authorization
}
Expand Down Expand Up @@ -116,3 +116,11 @@ function getHeaders (
}
return headers
}

function resolveRedirectUrl (response: Response, currentUrl: URL): URL {
const location = response.headers.get('location')
if (!location) {
throw new Error(`Redirect location header missing for ${currentUrl.toString()}`)
}
return new URL(location, currentUrl)
}
49 changes: 49 additions & 0 deletions network/fetch/test/fetchFromRegistry.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ import fs from 'fs'

const CERTS_DIR = path.join(import.meta.dirname, '__certs__')

afterEach(() => {
nock.cleanAll()
})

test('fetchFromRegistry', async () => {
const fetchFromRegistry = createFetchFromRegistry({})
const res = await fetchFromRegistry('https://registry.npmjs.org/is-positive')
Expand Down Expand Up @@ -138,3 +142,48 @@ test('fail if the client certificate is not provided', async () => {
}
expect(err?.code).toMatch(/ECONNRESET|ERR_SSL_TLSV13_ALERT_CERTIFICATE_REQUIRED/)
})

test('redirect to protocol-relative URL', async () => {
nock('http://registry.pnpm.io/')
.get('/foo')
.reply(302, '', { location: '//registry.other.org/foo' })
nock('http://registry.other.org/')
.get('/foo')
.reply(200, { ok: true })

const fetchFromRegistry = createFetchFromRegistry({ fullMetadata: true })
const res = await fetchFromRegistry(
'http://registry.pnpm.io/foo'
)

expect(await res.json()).toStrictEqual({ ok: true })
expect(nock.isDone()).toBeTruthy()
})

test('redirect to relative URL', async () => {
nock('http://registry.pnpm.io/')
.get('/bar/baz')
.reply(302, '', { location: '../foo' })
nock('http://registry.pnpm.io/')
.get('/foo')
.reply(200, { ok: true })

const fetchFromRegistry = createFetchFromRegistry({ fullMetadata: true })
const res = await fetchFromRegistry(
'http://registry.pnpm.io/bar/baz'
)

expect(await res.json()).toStrictEqual({ ok: true })
expect(nock.isDone()).toBeTruthy()
})

test('redirect without location header throws error', async () => {
nock('http://registry.pnpm.io/')
.get('/missing-location')
.reply(302, 'found')

const fetchFromRegistry = createFetchFromRegistry({ fullMetadata: true })
await expect(fetchFromRegistry(
'http://registry.pnpm.io/missing-location'
)).rejects.toThrow(/Redirect location header missing/)
})
7 changes: 6 additions & 1 deletion reviewing/plugin-commands-outdated/src/outdated.ts
Original file line number Diff line number Diff line change
Expand Up @@ -382,7 +382,12 @@ export function renderLatest (outdatedPkg: OutdatedWithVersionDiff): string {
: latestManifest.version
}

return colorizeSemverDiff.default({ change, diff })
const versionText = colorizeSemverDiff.default({ change, diff })
if (latestManifest.deprecated) {
return `${versionText} ${chalk.redBright('(deprecated)')}`
}

return versionText
}

export function renderDetails ({ latestManifest }: OutdatedPackage): string {
Expand Down
48 changes: 48 additions & 0 deletions reviewing/plugin-commands-outdated/test/renderLatest.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { outdated } from '@pnpm/plugin-commands-outdated'
import semverDiff from '@pnpm/semver-diff'
import { type PackageManifest } from '@pnpm/types'
import { type OutdatedWithVersionDiff } from '../src/utils.js'
import chalk from 'chalk'

test('renderLatest: outdated and deprecated', () => {
const diffResult = semverDiff.default('0.0.1', '1.0.0')
const outdatedPkg: OutdatedWithVersionDiff = {
...diffResult,
alias: 'foo',
belongsTo: 'dependencies',
current: '0.0.1',
latestManifest: {
name: 'foo',
version: '1.0.0',
deprecated: 'This package is deprecated',
} as PackageManifest,
packageName: 'foo',
wanted: '0.0.1',
}

const output = outdated.renderLatest(outdatedPkg)

expect(output).toContain('1.0.0')
expect(output).toContain(chalk.redBright('(deprecated)'))
})

test('renderLatest: outdated and not deprecated', () => {
const diffResult = semverDiff.default('0.0.1', '1.0.0')
const outdatedPkg: OutdatedWithVersionDiff = {
...diffResult,
alias: 'foo',
belongsTo: 'dependencies',
current: '0.0.1',
latestManifest: {
name: 'foo',
version: '1.0.0',
} as PackageManifest,
packageName: 'foo',
wanted: '0.0.1',
}

const output = outdated.renderLatest(outdatedPkg)

expect(output).not.toContain('(deprecated)')
expect(output).toContain('1.0.0')
})
Loading