mirror of
https://github.com/pnpm/pnpm.git
synced 2026-03-27 11:31:45 -04:00
feat: support pnpm env list to list global or remote Node.js versions (#5625)
close #5546 Co-authored-by: Zoltan Kochan <z@kochan.io>
This commit is contained in:
6
.changeset/orange-seahorses-attack.md
Normal file
6
.changeset/orange-seahorses-attack.md
Normal file
@@ -0,0 +1,6 @@
|
||||
---
|
||||
"pnpm": minor
|
||||
"@pnpm/plugin-commands-env": minor
|
||||
---
|
||||
|
||||
Support `pnpm env list` to list global or remote Node.js versions [#5546](https://github.com/pnpm/pnpm/issues/5546).
|
||||
5
.changeset/six-cats-travel.md
Normal file
5
.changeset/six-cats-travel.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@pnpm/node.resolver": minor
|
||||
---
|
||||
|
||||
Export a new function: resolveNodeVersions.
|
||||
@@ -7,27 +7,54 @@ interface NodeVersion {
|
||||
lts: false | string
|
||||
}
|
||||
|
||||
const SEMVER_OPTS = {
|
||||
includePrerelease: true,
|
||||
loose: true,
|
||||
}
|
||||
|
||||
export async function resolveNodeVersion (
|
||||
fetch: FetchFromRegistry,
|
||||
versionSpec: string,
|
||||
nodeMirrorBaseUrl?: string
|
||||
): Promise<string | null> {
|
||||
const response = await fetch(`${nodeMirrorBaseUrl ?? 'https://nodejs.org/download/release/'}index.json`)
|
||||
const allVersions = (await response.json()) as NodeVersion[]
|
||||
const allVersions = await fetchAllVersions(fetch, nodeMirrorBaseUrl)
|
||||
if (versionSpec === 'latest') {
|
||||
return allVersions[0].version.substring(1)
|
||||
return allVersions[0].version
|
||||
}
|
||||
const { versions, versionRange } = filterVersions(allVersions, versionSpec)
|
||||
const pickedVersion = semver.maxSatisfying(
|
||||
versions.map(({ version }) => version), versionRange, { includePrerelease: true, loose: true })
|
||||
if (!pickedVersion) return null
|
||||
return pickedVersion.substring(1)
|
||||
return semver.maxSatisfying(versions, versionRange, SEMVER_OPTS) ?? null
|
||||
}
|
||||
|
||||
export async function resolveNodeVersions (
|
||||
fetch: FetchFromRegistry,
|
||||
versionSpec?: string,
|
||||
nodeMirrorBaseUrl?: string
|
||||
): Promise<string[]> {
|
||||
const allVersions = await fetchAllVersions(fetch, nodeMirrorBaseUrl)
|
||||
if (!versionSpec) {
|
||||
return allVersions.map(({ version }) => version)
|
||||
}
|
||||
if (versionSpec === 'latest') {
|
||||
return [allVersions[0].version]
|
||||
}
|
||||
const { versions, versionRange } = filterVersions(allVersions, versionSpec)
|
||||
return versions.filter(version => semver.satisfies(version, versionRange, SEMVER_OPTS))
|
||||
}
|
||||
|
||||
async function fetchAllVersions (fetch: FetchFromRegistry, nodeMirrorBaseUrl?: string): Promise<NodeVersion[]> {
|
||||
const response = await fetch(`${nodeMirrorBaseUrl ?? 'https://nodejs.org/download/release/'}index.json`)
|
||||
return ((await response.json()) as NodeVersion[]).map(({ version, lts }) => ({
|
||||
version: version.substring(1),
|
||||
lts,
|
||||
}))
|
||||
}
|
||||
|
||||
function filterVersions (versions: NodeVersion[], versionSelector: string) {
|
||||
if (versionSelector === 'lts') {
|
||||
return {
|
||||
versions: versions.filter(({ lts }) => lts !== false),
|
||||
versions: versions
|
||||
.filter(({ lts }) => lts !== false)
|
||||
.map(({ version }) => version),
|
||||
versionRange: '*',
|
||||
}
|
||||
}
|
||||
@@ -35,9 +62,14 @@ function filterVersions (versions: NodeVersion[], versionSelector: string) {
|
||||
if (vst?.type === 'tag') {
|
||||
const wantedLtsVersion = vst.normalized.toLowerCase()
|
||||
return {
|
||||
versions: versions.filter(({ lts }) => typeof lts === 'string' && lts.toLowerCase() === wantedLtsVersion),
|
||||
versions: versions
|
||||
.filter(({ lts }) => typeof lts === 'string' && lts.toLowerCase() === wantedLtsVersion)
|
||||
.map(({ version }) => version),
|
||||
versionRange: '*',
|
||||
}
|
||||
}
|
||||
return { versions, versionRange: versionSelector }
|
||||
return {
|
||||
versions: versions.map(({ version }) => version),
|
||||
versionRange: versionSelector,
|
||||
}
|
||||
}
|
||||
|
||||
20
packages/node.resolver/test/resolveNodeVersions.test.ts
Normal file
20
packages/node.resolver/test/resolveNodeVersions.test.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import { createFetchFromRegistry } from '@pnpm/fetch'
|
||||
import { resolveNodeVersions } from '@pnpm/node.resolver'
|
||||
|
||||
const fetch = createFetchFromRegistry({})
|
||||
|
||||
test('resolve specified version list', async () => {
|
||||
const versions = await resolveNodeVersions(fetch, '16')
|
||||
expect(versions.length).toBeGreaterThan(1)
|
||||
expect(versions.every(version => version.match(/^16.+/))).toBeTruthy()
|
||||
})
|
||||
|
||||
test('resolve latest version', async () => {
|
||||
const versions = await resolveNodeVersions(fetch, 'latest')
|
||||
expect(versions.length).toEqual(1)
|
||||
})
|
||||
|
||||
test('resolve all versions', async () => {
|
||||
const versions = await resolveNodeVersions(fetch)
|
||||
expect(versions.length).toBeGreaterThan(1)
|
||||
})
|
||||
@@ -43,6 +43,7 @@
|
||||
"@zkochan/rimraf": "^2.1.2",
|
||||
"load-json-file": "^6.2.0",
|
||||
"render-help": "^1.0.2",
|
||||
"semver": "^7.3.8",
|
||||
"write-json-file": "^4.3.0"
|
||||
},
|
||||
"funding": "https://opencollective.com/pnpm",
|
||||
@@ -50,6 +51,7 @@
|
||||
"@pnpm/plugin-commands-env": "workspace:*",
|
||||
"@pnpm/prepare": "workspace:*",
|
||||
"@types/adm-zip": "^0.4.34",
|
||||
"@types/semver": "7.3.13",
|
||||
"adm-zip": "^0.5.9",
|
||||
"execa": "npm:safe-execa@^0.1.2",
|
||||
"nock": "13.2.9",
|
||||
|
||||
@@ -12,6 +12,8 @@ import renderHelp from 'render-help'
|
||||
import { getNodeDir, NvmNodeCommandOptions, getNodeVersionsBaseDir } from './node'
|
||||
import { getNodeMirror } from './getNodeMirror'
|
||||
import { parseNodeEditionSpecifier } from './parseNodeEditionSpecifier'
|
||||
import { listLocalVersions, listRemoteVersions } from './envList'
|
||||
import { getNodeExecPathInBinDir, getNodeExecPathAndTargetDir, getNodeExecPathInNodeDir } from './utils'
|
||||
|
||||
export function rcOptionsTypes () {
|
||||
return {}
|
||||
@@ -20,6 +22,7 @@ export function rcOptionsTypes () {
|
||||
export function cliOptionsTypes () {
|
||||
return {
|
||||
global: Boolean,
|
||||
remote: Boolean,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,14 +36,19 @@ export function help () {
|
||||
title: 'Commands',
|
||||
list: [
|
||||
{
|
||||
description: 'Installs the specified version of Node.JS. The npm CLI bundled with the given Node.js version gets installed as well.',
|
||||
description: 'Installs the specified version of Node.js. The npm CLI bundled with the given Node.js version gets installed as well.',
|
||||
name: 'use',
|
||||
},
|
||||
{
|
||||
description: 'Removes the specified version of Node.JS.',
|
||||
description: 'Removes the specified version of Node.js.',
|
||||
name: 'remove',
|
||||
shortAlias: 'rm',
|
||||
},
|
||||
{
|
||||
description: 'List Node.js versions available locally or remotely',
|
||||
name: 'list',
|
||||
shortAlias: 'ls',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
@@ -51,6 +59,10 @@ export function help () {
|
||||
name: '--global',
|
||||
shortAlias: '-g',
|
||||
},
|
||||
{
|
||||
description: 'List the remote versions of Node.js',
|
||||
name: '--remote',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
@@ -67,6 +79,13 @@ export function help () {
|
||||
'pnpm env remove --global argon',
|
||||
'pnpm env remove --global latest',
|
||||
'pnpm env remove --global rc/16',
|
||||
'pnpm env list',
|
||||
'pnpm env list --remote',
|
||||
'pnpm env list --remote 16',
|
||||
'pnpm env list --remote lts',
|
||||
'pnpm env list --remote argon',
|
||||
'pnpm env list --remote latest',
|
||||
'pnpm env list --remote rc/16',
|
||||
],
|
||||
})
|
||||
}
|
||||
@@ -92,8 +111,8 @@ export async function handler (opts: NvmNodeCommandOptions, params: string[]) {
|
||||
useNodeVersion: nodeVersion,
|
||||
nodeMirrorBaseUrl,
|
||||
})
|
||||
const src = path.join(nodeDir, process.platform === 'win32' ? 'node.exe' : 'bin/node')
|
||||
const dest = path.join(opts.bin, process.platform === 'win32' ? 'node.exe' : 'node')
|
||||
const src = getNodeExecPathInNodeDir(nodeDir)
|
||||
const dest = getNodeExecPathInBinDir(opts.bin)
|
||||
try {
|
||||
await fs.unlink(dest)
|
||||
} catch (err) {}
|
||||
@@ -143,16 +162,10 @@ export async function handler (opts: NvmNodeCommandOptions, params: string[]) {
|
||||
throw new PnpmError('ENV_NO_NODE_DIRECTORY', `Couldn't find Node.js directory in ${versionDir}`)
|
||||
}
|
||||
|
||||
const nodePath = path.resolve(opts.pnpmHomeDir, process.platform === 'win32' ? 'node.exe' : 'node')
|
||||
let nodeLink: string | undefined
|
||||
try {
|
||||
nodeLink = await fs.readlink(nodePath)
|
||||
} catch (err) {
|
||||
nodeLink = undefined
|
||||
}
|
||||
const { nodePath, nodeLink } = await getNodeExecPathAndTargetDir(opts.pnpmHomeDir)
|
||||
|
||||
if (nodeLink?.includes(versionDir)) {
|
||||
globalInfo(`Node.JS version ${nodeVersion} was detected as the default one, removing ...`)
|
||||
globalInfo(`Node.js version ${nodeVersion as string} was detected as the default one, removing ...`)
|
||||
|
||||
const npmPath = path.resolve(opts.pnpmHomeDir, 'npm')
|
||||
const npxPath = path.resolve(opts.pnpmHomeDir, 'npx')
|
||||
@@ -170,9 +183,21 @@ export async function handler (opts: NvmNodeCommandOptions, params: string[]) {
|
||||
|
||||
await rimraf(versionDir)
|
||||
|
||||
return `Node.js ${nodeVersion} is removed
|
||||
return `Node.js ${nodeVersion as string} is removed
|
||||
${versionDir}`
|
||||
}
|
||||
case 'list':
|
||||
case 'ls': {
|
||||
if (opts.remote) {
|
||||
const nodeVersionList = await listRemoteVersions(opts, params[1])
|
||||
// Make the newest version located in the end of output
|
||||
return nodeVersionList.reverse().join('\n')
|
||||
}
|
||||
const { currentVersion, versions } = await listLocalVersions(opts)
|
||||
return versions
|
||||
.map(nodeVersion => `${nodeVersion === currentVersion ? '*' : ' '} ${nodeVersion}`)
|
||||
.join('\n')
|
||||
}
|
||||
default: {
|
||||
throw new PnpmError('ENV_UNKNOWN_SUBCOMMAND', 'This subcommand is not known')
|
||||
}
|
||||
|
||||
38
packages/plugin-commands-env/src/envList.ts
Normal file
38
packages/plugin-commands-env/src/envList.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import { promises as fs, existsSync } from 'fs'
|
||||
import path from 'path'
|
||||
import { createFetchFromRegistry } from '@pnpm/fetch'
|
||||
import { resolveNodeVersions } from '@pnpm/node.resolver'
|
||||
import { PnpmError } from '@pnpm/error'
|
||||
import semver from 'semver'
|
||||
import { getNodeMirror } from './getNodeMirror'
|
||||
import { getNodeVersionsBaseDir, NvmNodeCommandOptions } from './node'
|
||||
import { parseNodeEditionSpecifier } from './parseNodeEditionSpecifier'
|
||||
import { getNodeExecPathAndTargetDir, getNodeExecPathInNodeDir } from './utils'
|
||||
|
||||
export async function listLocalVersions (opts: NvmNodeCommandOptions) {
|
||||
const nodeBaseDir = getNodeVersionsBaseDir(opts.pnpmHomeDir)
|
||||
if (!existsSync(nodeBaseDir)) {
|
||||
throw new PnpmError('ENV_NO_NODE_DIRECTORY', `Couldn't find Node.js directory in ${nodeBaseDir}`)
|
||||
}
|
||||
const { nodeLink } = await getNodeExecPathAndTargetDir(opts.pnpmHomeDir)
|
||||
const nodeVersionDirs = await fs.readdir(nodeBaseDir)
|
||||
return nodeVersionDirs.reduce(({ currentVersion, versions }, nodeVersion) => {
|
||||
const nodeVersionDir = path.join(nodeBaseDir, nodeVersion)
|
||||
const nodeExec = getNodeExecPathInNodeDir(nodeVersionDir)
|
||||
if (nodeLink?.startsWith(nodeVersionDir)) {
|
||||
currentVersion = nodeVersion
|
||||
}
|
||||
if (semver.valid(nodeVersion) && existsSync(nodeExec)) {
|
||||
versions.push(nodeVersion)
|
||||
}
|
||||
return { currentVersion, versions }
|
||||
}, { currentVersion: undefined as string | undefined, versions: [] as string[] })
|
||||
}
|
||||
|
||||
export async function listRemoteVersions (opts: NvmNodeCommandOptions, versionSpec?: string) {
|
||||
const fetch = createFetchFromRegistry(opts)
|
||||
const { releaseChannel, versionSpecifier } = parseNodeEditionSpecifier(versionSpec ?? '')
|
||||
const nodeMirrorBaseUrl = getNodeMirror(opts.rawConfig, releaseChannel)
|
||||
const nodeVersionList = await resolveNodeVersions(fetch, versionSpecifier, nodeMirrorBaseUrl)
|
||||
return nodeVersionList
|
||||
}
|
||||
@@ -31,7 +31,9 @@ export type NvmNodeCommandOptions = Pick<Config,
|
||||
| 'storeDir'
|
||||
| 'useNodeVersion'
|
||||
| 'pnpmHomeDir'
|
||||
> & Partial<Pick<Config, 'configDir'>>
|
||||
> & Partial<Pick<Config, 'configDir' | 'cliOptions'>> & {
|
||||
remote?: boolean
|
||||
}
|
||||
|
||||
export async function getNodeBinDir (opts: NvmNodeCommandOptions) {
|
||||
const fetch = createFetchFromRegistry(opts)
|
||||
|
||||
21
packages/plugin-commands-env/src/utils.ts
Normal file
21
packages/plugin-commands-env/src/utils.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import { promises as fs } from 'fs'
|
||||
import path from 'path'
|
||||
|
||||
export async function getNodeExecPathAndTargetDir (pnpmHomeDir: string) {
|
||||
const nodePath = getNodeExecPathInBinDir(pnpmHomeDir)
|
||||
let nodeLink: string | undefined
|
||||
try {
|
||||
nodeLink = await fs.readlink(nodePath)
|
||||
} catch (err) {
|
||||
nodeLink = undefined
|
||||
}
|
||||
return { nodePath, nodeLink }
|
||||
}
|
||||
|
||||
export function getNodeExecPathInBinDir (pnpmHomeDir: string) {
|
||||
return path.resolve(pnpmHomeDir, process.platform === 'win32' ? 'node.exe' : 'node')
|
||||
}
|
||||
|
||||
export function getNodeExecPathInNodeDir (nodeDir: string) {
|
||||
return path.join(nodeDir, process.platform === 'win32' ? 'node.exe' : 'bin/node')
|
||||
}
|
||||
@@ -6,6 +6,7 @@ import { env, node } from '@pnpm/plugin-commands-env'
|
||||
import * as execa from 'execa'
|
||||
import nock from 'nock'
|
||||
import PATH from 'path-name'
|
||||
import semver from 'semver'
|
||||
|
||||
test('install Node (and npm, npx) by exact version of Node.js', async () => {
|
||||
tempDir()
|
||||
@@ -211,3 +212,56 @@ describe('env remove', () => {
|
||||
expect(() => execa.sync('node', ['-v'], opts)).toThrowError()
|
||||
})
|
||||
})
|
||||
|
||||
describe('env list', () => {
|
||||
test('list local Node.js versions', async () => {
|
||||
tempDir()
|
||||
const configDir = path.resolve('config')
|
||||
|
||||
await env.handler({
|
||||
bin: process.cwd(),
|
||||
configDir,
|
||||
global: true,
|
||||
pnpmHomeDir: process.cwd(),
|
||||
rawConfig: {},
|
||||
}, ['use', '16.4.0'])
|
||||
|
||||
const version = await env.handler({
|
||||
bin: process.cwd(),
|
||||
configDir,
|
||||
pnpmHomeDir: process.cwd(),
|
||||
rawConfig: {},
|
||||
}, ['list'])
|
||||
|
||||
expect(version).toMatch('16.4.0')
|
||||
})
|
||||
test('list local versions fails if Node.js directory not found', async () => {
|
||||
tempDir()
|
||||
const configDir = path.resolve('config')
|
||||
const pnpmHomeDir = path.resolve('specified-dir')
|
||||
|
||||
await expect(
|
||||
env.handler({
|
||||
bin: process.cwd(),
|
||||
configDir,
|
||||
pnpmHomeDir,
|
||||
rawConfig: {},
|
||||
}, ['list'])
|
||||
).rejects.toEqual(new PnpmError('ENV_NO_NODE_DIRECTORY', `Couldn't find Node.js directory in ${path.join(pnpmHomeDir, 'nodejs')}`))
|
||||
})
|
||||
test('list remote Node.js versions', async () => {
|
||||
tempDir()
|
||||
const configDir = path.resolve('config')
|
||||
|
||||
const versionStr = await env.handler({
|
||||
bin: process.cwd(),
|
||||
configDir,
|
||||
pnpmHomeDir: process.cwd(),
|
||||
rawConfig: {},
|
||||
remote: true,
|
||||
}, ['list', '16'])
|
||||
|
||||
const versions = versionStr.split('\n')
|
||||
expect(versions.every(version => semver.satisfies(version, '16'))).toBeTruthy()
|
||||
})
|
||||
})
|
||||
|
||||
6
pnpm-lock.yaml
generated
6
pnpm-lock.yaml
generated
@@ -3203,6 +3203,9 @@ importers:
|
||||
render-help:
|
||||
specifier: ^1.0.2
|
||||
version: 1.0.2
|
||||
semver:
|
||||
specifier: ^7.3.8
|
||||
version: 7.3.8
|
||||
write-json-file:
|
||||
specifier: ^4.3.0
|
||||
version: 4.3.0
|
||||
@@ -3216,6 +3219,9 @@ importers:
|
||||
'@types/adm-zip':
|
||||
specifier: ^0.4.34
|
||||
version: 0.4.34
|
||||
'@types/semver':
|
||||
specifier: 7.3.13
|
||||
version: 7.3.13
|
||||
adm-zip:
|
||||
specifier: ^0.5.9
|
||||
version: 0.5.9
|
||||
|
||||
Reference in New Issue
Block a user