Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: support pnpm env list to list global or remote Node.js versions #5625

Merged
merged 8 commits into from
Nov 12, 2022
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changeset/orange-seahorses-attack.md
@@ -0,0 +1,6 @@
---
"@pnpm/node.resolver": minor
"@pnpm/plugin-commands-env": minor
---

Support `pnpm env list` to list global or remote Node.js versions
19 changes: 19 additions & 0 deletions packages/node.resolver/src/index.ts
Expand Up @@ -24,6 +24,25 @@ export async function resolveNodeVersion (
return pickedVersion.substring(1)
}

export async function resolveNodeVersionList (
lvqq marked this conversation as resolved.
Show resolved Hide resolved
fetch: FetchFromRegistry,
versionSpec?: string,
nodeMirrorBaseUrl?: string
): Promise<string[]> {
const response = await fetch(`${nodeMirrorBaseUrl ?? 'https://nodejs.org/download/release/'}index.json`)
const allVersions = (await response.json()) as NodeVersion[]
if (!versionSpec) return allVersions.map(({ version }) => version.substring(1))
if (versionSpec === 'latest') {
return [allVersions[0].version.substring(1)]
}
const { versions, versionRange } = filterVersions(allVersions, versionSpec)
const pickedVersions = versions.map(({ version }) => version.substring(1)).filter(version => semver.satisfies(version, versionRange, {
includePrerelease: true,
loose: true,
}))
return pickedVersions
}

function filterVersions (versions: NodeVersion[], versionSelector: string) {
if (versionSelector === 'lts') {
return {
Expand Down
10 changes: 10 additions & 0 deletions packages/node.resolver/test/index.ts
@@ -0,0 +1,10 @@
import { createFetchFromRegistry } from '@pnpm/fetch'
import { resolveNodeVersionList } from '@pnpm/node.resolver'
import semver from 'semver'

const fetch = createFetchFromRegistry({})

test('resolve Node.js version list', async () => {
const versions = await resolveNodeVersionList(fetch, '16')
expect(versions.every(version => semver.satisfies(version, '16'))).toBe(true)
})
2 changes: 2 additions & 0 deletions packages/plugin-commands-env/package.json
Expand Up @@ -43,13 +43,15 @@
"@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",
"devDependencies": {
"@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",
Expand Down
59 changes: 58 additions & 1 deletion packages/plugin-commands-env/src/env.ts
Expand Up @@ -3,12 +3,13 @@ import path from 'path'
import { docsUrl } from '@pnpm/cli-utils'
import { PnpmError } from '@pnpm/error'
import { createFetchFromRegistry } from '@pnpm/fetch'
import { resolveNodeVersion } from '@pnpm/node.resolver'
import { resolveNodeVersion, resolveNodeVersionList } from '@pnpm/node.resolver'
import { globalInfo } from '@pnpm/logger'
import { removeBin } from '@pnpm/remove-bins'
import cmdShim from '@zkochan/cmd-shim'
import rimraf from '@zkochan/rimraf'
import renderHelp from 'render-help'
import semver from 'semver'
import { getNodeDir, NvmNodeCommandOptions, getNodeVersionsBaseDir } from './node'
import { getNodeMirror } from './getNodeMirror'
import { parseNodeEditionSpecifier } from './parseNodeEditionSpecifier'
Expand All @@ -20,6 +21,7 @@ export function rcOptionsTypes () {
export function cliOptionsTypes () {
return {
global: Boolean,
remote: Boolean,
}
}

Expand All @@ -41,6 +43,11 @@ export function help () {
name: 'remove',
shortAlias: 'rm',
},
{
description: 'List the versions of Node.JS installed via pnpm.',
name: 'list',
shortAlias: 'ls',
},
],
},
{
Expand All @@ -51,6 +58,10 @@ export function help () {
name: '--global',
shortAlias: '-g',
},
{
description: 'List the remote versions of Node.JS.',
name: '--remote',
},
],
},
],
Expand All @@ -67,6 +78,13 @@ export function help () {
'pnpm env remove --global argon',
'pnpm env remove --global latest',
'pnpm env remove --global rc/16',
'pnpm env list --global',
'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',
],
})
}
Expand Down Expand Up @@ -173,6 +191,45 @@ export async function handler (opts: NvmNodeCommandOptions, params: string[]) {
return `Node.js ${nodeVersion} is removed
${versionDir}`
}
case 'list':
case 'ls': {
if (!opts.global && !opts.cliOptions?.remote) {
zkochan marked this conversation as resolved.
Show resolved Hide resolved
throw new PnpmError('NOT_IMPLEMENTED_YET', '"pnpm env list <option>" can only be used with the "--global" option or "--remote" option currently')
}
if (opts.cliOptions?.remote) {
const fetch = createFetchFromRegistry(opts)
const { releaseChannel, versionSpecifier } = parseNodeEditionSpecifier(params[1] ?? '')
const nodeMirrorBaseUrl = getNodeMirror(opts.rawConfig, releaseChannel)
const nodeVersionList = await resolveNodeVersionList(fetch, versionSpecifier, nodeMirrorBaseUrl)
// make the newest version located in the end of output
return nodeVersionList.reverse().join('\n')
}
const nodeDir = getNodeVersionsBaseDir(opts.pnpmHomeDir)
const nodePath = path.resolve(opts.pnpmHomeDir, process.platform === 'win32' ? 'node.exe' : 'node')
let currentNodeVersion: string | undefined
let nodeLink: string | undefined

if (!existsSync(nodeDir)) {
throw new PnpmError('ENV_NO_NODE_DIRECTORY', `Couldn't find Node.js directory in ${nodeDir}`)
}

try {
nodeLink = await fs.readlink(nodePath)
} catch (err) {
nodeLink = undefined
}

const nodeVersionDirs = await fs.readdir(nodeDir)
const nodeVersions = nodeVersionDirs.filter(nodeVersion => {
const nodeSrc = path.join(nodeDir, nodeVersion, process.platform === 'win32' ? 'node.exe' : 'bin/node')
const nodeVersionDir = path.join(nodeDir, nodeVersion)
if (nodeLink?.includes(nodeVersionDir)) {
currentNodeVersion = nodeVersion
}
return semver.valid(nodeVersion) && existsSync(nodeSrc)
})
return nodeVersions.map(nodeVersion => `${nodeVersion === currentNodeVersion ? '*' : ' '} ${nodeVersion}`).join('\n')
lvqq marked this conversation as resolved.
Show resolved Hide resolved
}
default: {
throw new PnpmError('ENV_UNKNOWN_SUBCOMMAND', 'This subcommand is not known')
}
Expand Down
2 changes: 1 addition & 1 deletion packages/plugin-commands-env/src/node.ts
Expand Up @@ -31,7 +31,7 @@ export type NvmNodeCommandOptions = Pick<Config,
| 'storeDir'
| 'useNodeVersion'
| 'pnpmHomeDir'
> & Partial<Pick<Config, 'configDir'>>
> & Partial<Pick<Config, 'configDir' | 'cliOptions'>>

export async function getNodeBinDir (opts: NvmNodeCommandOptions) {
const fetch = createFetchFromRegistry(opts)
Expand Down
74 changes: 74 additions & 0 deletions packages/plugin-commands-env/test/env.test.ts
Expand Up @@ -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()
Expand Down Expand Up @@ -210,4 +211,77 @@ describe('env remove', () => {

expect(() => execa.sync('node', ['-v'], opts)).toThrowError()
})

test('list global Node.js versions', async () => {
tempDir()

const configDir = path.resolve('config')

await env.handler({
bin: process.cwd(),
configDir,
global: true,
pnpmHomeDir: process.cwd(),
rawConfig: {},
}, ['use', '14.20.0'])

await env.handler({
bin: process.cwd(),
configDir,
global: true,
pnpmHomeDir: process.cwd(),
rawConfig: {},
}, ['use', '16.4.0'])

const versionStr = await env.handler({
bin: process.cwd(),
configDir,
global: true,
pnpmHomeDir: process.cwd(),
rawConfig: {},
}, ['list'])

const versions = versionStr.split('\n')
const expected = [
expect.stringContaining('14.20.0'),
expect.stringContaining('16.4.0'),
]

expect(versions).toEqual(
expect.arrayContaining(expected)
)
})

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: {},
cliOptions: {
remote: true,
},
}, ['list', '16'])

const versions = versionStr.split('\n')

expect(versions.every(version => semver.satisfies(version, '16'))).toBe(true)
})

test('list versions failed if --global or --option is missing', async () => {
tempDir()

await expect(
env.handler({
bin: process.cwd(),
global: false,
pnpmHomeDir: process.cwd(),
rawConfig: {},
}, ['list'])
).rejects.toEqual(new PnpmError('NOT_IMPLEMENTED_YET', '"pnpm env list <option>" can only be used with the "--global" option or "--remote" option currently'))
})
})
6 changes: 6 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.