feat: add node-mirror:<releaseDir> options (#4083)

close #4039
This commit is contained in:
Brandon Cheng
2021-12-10 13:07:58 -05:00
committed by GitHub
parent 5af305f395
commit 10a4bd4db2
11 changed files with 129 additions and 7 deletions

View File

@@ -0,0 +1,7 @@
---
"@pnpm/config": minor
"@pnpm/plugin-commands-env": minor
"pnpm": minor
---
New option added for: `node-mirror:<releaseDir>`. The string value of this dynamic option is used as the base URL for downloading node when `use-node-version` is specified. The `<releaseDir>` portion of this argument can be any dir in `https://nodejs.org/download`. Which `<releaseDir>` dynamic config option gets selected depends on the value of `use-node-version`. If 'use-node-version' is a simple `x.x.x` version string, `<releaseDir>` becomes `release` and `node-mirror:release` is read. Defaults to `https://nodejs.org/download/<releaseDir>/`.

View File

@@ -53,6 +53,8 @@
"@pnpm/prepare": "workspace:0.0.28",
"@types/adm-zip": "^0.4.34",
"execa": "npm:safe-execa@^0.1.1",
"nock": "12.0.3",
"node-fetch": "3.0.0-beta.9",
"path-name": "^1.0.0"
}
}

View File

@@ -58,7 +58,7 @@ export async function handler (opts: NvmNodeCommandOptions, params: string[]) {
throw new PnpmError('NOT_IMPLEMENTED_YET', '"pnpm env use <version>" can only be used with the "--global" option currently')
}
const fetch = createFetchFromRegistry(opts)
const { version: nodeVersion, releaseDir } = await resolveNodeVersion(fetch, params[1])
const { version: nodeVersion, releaseDir } = await resolveNodeVersion(fetch, params[1], opts.rawConfig)
if (!nodeVersion) {
throw new PnpmError('COULD_NOT_RESOLVE_NODEJS', `Couldn't find Node.js version matching ${params[1]}`)
}

View File

@@ -0,0 +1,12 @@
import { Config } from '@pnpm/config'
export default function getNodeMirror (rawConfig: Config['rawConfig'], releaseDir: string): string {
// This is a dynamic lookup since the 'use-node-version' option is allowed to be '<releaseDir>/<version>'
const configKey = `node-mirror:${releaseDir}`
const nodeMirror = rawConfig[configKey] ?? `https://nodejs.org/download/${releaseDir}/`
return normalizeNodeMirror(nodeMirror)
}
function normalizeNodeMirror (nodeMirror: string): string {
return nodeMirror.endsWith('/') ? nodeMirror : `${nodeMirror}/`
}

View File

@@ -12,6 +12,7 @@ import tempy from 'tempy'
import loadJsonFile from 'load-json-file'
import writeJsonFile from 'write-json-file'
import normalizeArch from './normalizeArch'
import getNodeMirror from './getNodeMirror'
export type NvmNodeCommandOptions = Pick<Config,
| 'bin'
@@ -31,6 +32,7 @@ export type NvmNodeCommandOptions = Pick<Config,
| 'key'
| 'localAddress'
| 'noProxy'
| 'rawConfig'
| 'strictSsl'
| 'storeDir'
| 'useNodeVersion'
@@ -66,7 +68,8 @@ export async function getNodeDir (fetch: FetchFromRegistry, opts: NvmNodeCommand
async function installNode (fetch: FetchFromRegistry, wantedNodeVersion: string, versionDir: string, opts: NvmNodeCommandOptions & { releaseDir?: string }) {
await fs.promises.mkdir(versionDir, { recursive: true })
const { tarball, pkgName } = getNodeJSTarball(wantedNodeVersion, opts.releaseDir ?? 'release')
const nodeMirror = getNodeMirror(opts.rawConfig, opts.releaseDir ?? 'release')
const { tarball, pkgName } = getNodeJSTarball(wantedNodeVersion, nodeMirror)
if (tarball.endsWith('.zip')) {
await downloadAndUnpackZip(fetch, tarball, versionDir, pkgName)
return
@@ -115,14 +118,14 @@ async function downloadAndUnpackZip (
await fs.promises.unlink(tmp)
}
function getNodeJSTarball (nodeVersion: string, releaseDir: string) {
function getNodeJSTarball (nodeVersion: string, nodeMirror: string) {
const platform = process.platform === 'win32' ? 'win' : process.platform
const arch = normalizeArch(process.platform, process.arch)
const extension = platform === 'win' ? 'zip' : 'tar.gz'
const pkgName = `node-v${nodeVersion}-${platform}-${arch}`
return {
pkgName,
tarball: `https://nodejs.org/download/${releaseDir}/v${nodeVersion}/${pkgName}.${extension}`,
tarball: `${nodeMirror}v${nodeVersion}/${pkgName}.${extension}`,
}
}

View File

@@ -1,15 +1,18 @@
import { Config } from '@pnpm/config'
import { FetchFromRegistry } from '@pnpm/fetch'
import semver from 'semver'
import versionSelectorType from 'version-selector-type'
import getNodeMirror from './getNodeMirror'
interface NodeVersion {
version: string
lts: false | string
}
export default async function resolveNodeVersion (fetch: FetchFromRegistry, rawVersionSelector: string) {
export default async function resolveNodeVersion (fetch: FetchFromRegistry, rawVersionSelector: string, rawConfig: Config['rawConfig']) {
const { releaseDir, version } = parseNodeVersionSelector(rawVersionSelector)
const response = await fetch(`https://nodejs.org/download/${releaseDir}/index.json`)
const nodeMirrorBaseUrl = getNodeMirror(rawConfig, releaseDir)
const response = await fetch(`${nodeMirrorBaseUrl}index.json`)
const allVersions = (await response.json()) as NodeVersion[]
if (version === 'latest') {
return {

View File

@@ -4,6 +4,7 @@ import PnpmError from '@pnpm/error'
import { tempDir } from '@pnpm/prepare'
import { env } from '@pnpm/plugin-commands-env'
import * as execa from 'execa'
import nock from 'nock'
import PATH from 'path-name'
test('install Node (and npm, npx) by exact version of Node.js', async () => {
@@ -49,6 +50,29 @@ test('install Node (and npm, npx) by exact version of Node.js', async () => {
}
})
test('resolveNodeVersion uses node-mirror:release option', async () => {
tempDir()
const configDir = path.resolve('config')
const nockScope = nock('https://pnpm-node-mirror-test.localhost')
.get('/download/release/index.json')
.reply(200, [])
await expect(
env.handler({
bin: process.cwd(),
configDir,
global: true,
pnpmHomeDir: process.cwd(),
rawConfig: {
'node-mirror:release': 'https://pnpm-node-mirror-test.localhost/download/release',
},
}, ['use', '16.4.0'])
).rejects.toEqual(new PnpmError('COULD_NOT_RESOLVE_NODEJS', 'Couldn\'t find Node.js version matching 16.4.0'))
expect(nockScope.isDone()).toBeTruthy()
})
test('fail if a non-existend Node.js version is tried to be installed', async () => {
tempDir()

View File

@@ -0,0 +1,23 @@
import getNodeMirror from '@pnpm/plugin-commands-env/lib/getNodeMirror'
test.each([
['release', { 'node-mirror:release': 'http://test.mirror.localhost/release' }, 'http://test.mirror.localhost/release/'],
['nightly', { 'node-mirror:nightly': 'http://test.mirror.localhost/nightly' }, 'http://test.mirror.localhost/nightly/'],
['rc', { 'node-mirror:rc': 'http://test.mirror.localhost/rc' }, 'http://test.mirror.localhost/rc/'],
['test', { 'node-mirror:test': 'http://test.mirror.localhost/test' }, 'http://test.mirror.localhost/test/'],
['v8-canary', { 'node-mirror:v8-canary': 'http://test.mirror.localhost/v8-canary' }, 'http://test.mirror.localhost/v8-canary/'],
])('getNodeMirror(%s, %s)', (releaseDir, rawConfig, expected) => {
expect(getNodeMirror(rawConfig, releaseDir)).toBe(expected)
})
test('getNodeMirror uses defaults', () => {
const rawConfig = {}
expect(getNodeMirror(rawConfig, 'release')).toBe('https://nodejs.org/download/release/')
})
test('getNodeMirror returns base url with trailing /', () => {
const rawConfig = {
'node-mirror:release': 'http://test.mirror.localhost',
}
expect(getNodeMirror(rawConfig, 'release')).toBe('http://test.mirror.localhost/')
})

View File

@@ -1,5 +1,47 @@
import AdmZip from 'adm-zip'
import { Response } from 'node-fetch'
import path from 'path'
import { Readable } from 'stream'
import { node } from '@pnpm/plugin-commands-env'
import { tempDir } from '@pnpm/prepare'
test('check API (placeholder test)', async () => {
expect(typeof node.getNodeDir).toBe('function')
})
test('install Node uses node-mirror:release option', async () => {
tempDir()
const configDir = path.resolve('config')
const nodeMirrorRelease = 'https://pnpm-node-mirror-test.localhost/download/release'
const opts: node.NvmNodeCommandOptions = {
bin: process.cwd(),
configDir,
global: true,
pnpmHomeDir: process.cwd(),
rawConfig: {
'node-mirror:release': nodeMirrorRelease,
},
useNodeVersion: '16.4.0',
}
const fetchMock = jest.fn(async (url: string) => {
if (url.endsWith('.zip')) {
// The Windows code path for pnpm's node bootstrapping expects a subdir
// within the .zip file.
const pkgName = path.basename(url, '.zip')
const zip = new AdmZip()
zip.addFile(`${pkgName}/dummy-file`, Buffer.from('test'))
return new Response(Readable.from(zip.toBuffer()))
}
return new Response(Readable.from(Buffer.alloc(0)))
})
await node.getNodeDir(fetchMock, opts)
for (const call of fetchMock.mock.calls) {
expect(call[0]).toMatch(nodeMirrorRelease)
}
})

View File

@@ -3,6 +3,8 @@ import resolveNodeVersion from '@pnpm/plugin-commands-env/lib/resolveNodeVersion
const fetch = createFetchFromRegistry({})
const rawConfig = {}
test.each([
['6', '6.17.1', 'release'],
['16.0.0-rc.0', '16.0.0-rc.0', 'rc'],
@@ -12,7 +14,7 @@ test.each([
['argon', '4.9.1', 'release'],
['latest', /.+/, 'release'],
])('Node.js %s is resolved', async (spec, version, releaseDir) => {
const node = await resolveNodeVersion(fetch, spec)
const node = await resolveNodeVersion(fetch, spec, rawConfig)
expect(node.version).toMatch(version)
expect(node.releaseDir).toBe(releaseDir)
})

4
pnpm-lock.yaml generated
View File

@@ -2050,6 +2050,8 @@ importers:
adm-zip: ^0.5.5
execa: npm:safe-execa@^0.1.1
load-json-file: ^6.2.0
nock: 12.0.3
node-fetch: 3.0.0-beta.9
path-name: ^1.0.0
rename-overwrite: ^4.0.0
render-help: ^1.0.1
@@ -2080,6 +2082,8 @@ importers:
'@pnpm/prepare': link:../../privatePackages/prepare
'@types/adm-zip': 0.4.34
execa: /safe-execa/0.1.1
nock: 12.0.3
node-fetch: 3.0.0-beta.9
path-name: 1.0.0
packages/plugin-commands-installation: