@pnpm/exe 12.0.0-rc.4 → 12.0.0-rc.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/THIRD-PARTY-NOTICES.md +42 -0
- package/bin/pnpm.mjs +141 -0
- package/bin/pnpx.mjs +9 -0
- package/dist/node_modules/.bin/get-pnpm +50 -0
- package/dist/node_modules/.package-map.json +1 -1
- package/dist/node_modules/.pnpm-workspace-state-v1.json +5 -4
- package/dist/node_modules/get-pnpm/bin/get-pnpm.js +11 -0
- package/dist/node_modules/get-pnpm/lib/downloadExecutable.js +99 -0
- package/dist/node_modules/get-pnpm/lib/extractTarball.js +23 -0
- package/dist/node_modules/get-pnpm/lib/extractTarballMember.js +128 -0
- package/dist/node_modules/get-pnpm/lib/index.js +208 -0
- package/dist/node_modules/get-pnpm/lib/npmSigningKeys.js +23 -0
- package/dist/node_modules/get-pnpm/lib/platformPackageName.js +60 -0
- package/dist/node_modules/get-pnpm/lib/registry.js +120 -0
- package/dist/node_modules/get-pnpm/lib/resolveVersion.js +36 -0
- package/dist/node_modules/get-pnpm/lib/sameFileContents.js +45 -0
- package/dist/node_modules/get-pnpm/lib/verifySignature.js +51 -0
- package/dist/node_modules/get-pnpm/package.json +45 -0
- package/dist/node_modules/node-gyp/.release-please-manifest.json +1 -1
- package/dist/node_modules/node-gyp/gyp/.release-please-manifest.json +1 -1
- package/dist/node_modules/node-gyp/gyp/pyproject.toml +4 -4
- package/dist/node_modules/node-gyp/lib/download.js +3 -3
- package/dist/node_modules/node-gyp/package.json +1 -1
- package/install.js +20 -97
- package/native-binary.mjs +116 -0
- package/package.json +13 -10
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# Third-Party Notices
|
|
2
|
+
|
|
3
|
+
This product includes software derived from third-party sources. The
|
|
4
|
+
components below, and their license terms, are listed here in satisfaction
|
|
5
|
+
of those terms.
|
|
6
|
+
|
|
7
|
+
## Yarn (`@yarnpkg/nm`, `@yarnpkg/extensions`)
|
|
8
|
+
|
|
9
|
+
- Source: <https://github.com/yarnpkg/berry>
|
|
10
|
+
- License: BSD 2-Clause
|
|
11
|
+
|
|
12
|
+
The `nodeLinker: hoisted` layout is produced by a Rust port of the hoisting
|
|
13
|
+
algorithm in `@yarnpkg/nm`, and the built-in package-compatibility database
|
|
14
|
+
is a copy of the one in `@yarnpkg/extensions`.
|
|
15
|
+
|
|
16
|
+
```text
|
|
17
|
+
BSD 2-Clause License
|
|
18
|
+
|
|
19
|
+
Copyright (c) 2016-present, Yarn Contributors.
|
|
20
|
+
All rights reserved.
|
|
21
|
+
|
|
22
|
+
Redistribution and use in source and binary forms, with or without
|
|
23
|
+
modification, are permitted provided that the following conditions are met:
|
|
24
|
+
|
|
25
|
+
1. Redistributions of source code must retain the above copyright notice, this
|
|
26
|
+
list of conditions and the following disclaimer.
|
|
27
|
+
|
|
28
|
+
2. Redistributions in binary form must reproduce the above copyright notice,
|
|
29
|
+
this list of conditions and the following disclaimer in the documentation
|
|
30
|
+
and/or other materials provided with the distribution.
|
|
31
|
+
|
|
32
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
|
33
|
+
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
34
|
+
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
35
|
+
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
|
36
|
+
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
|
37
|
+
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
|
38
|
+
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
|
39
|
+
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
|
40
|
+
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
41
|
+
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
42
|
+
```
|
package/bin/pnpm.mjs
ADDED
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Corepack's entry point into pnpm. Corepack hardcodes `./bin/pnpm.mjs` and
|
|
3
|
+
// `./bin/pnpx.mjs` for every pnpm >=11 (see its `config.json`) and loads them
|
|
4
|
+
// into its own Node.js process, which a native executable cannot be loaded into.
|
|
5
|
+
//
|
|
6
|
+
// Nothing else runs this file: `package.json#bin` still points at the native
|
|
7
|
+
// binary, so an ordinary `npm install -g pnpm` never pays for a Node.js startup.
|
|
8
|
+
//
|
|
9
|
+
// Corepack installs no dependencies and runs no lifecycle scripts, so the
|
|
10
|
+
// `@pnpm/exe.<target>` package that carries the binary is absent and
|
|
11
|
+
// `install.js` never ran. The binary is therefore downloaded on first use and
|
|
12
|
+
// kept next to this wrapper — where the native binary also finds the `dist/`
|
|
13
|
+
// payload it ships node-gyp in.
|
|
14
|
+
//
|
|
15
|
+
// The download itself is `get-pnpm`, the package behind https://get.pnpm.io,
|
|
16
|
+
// which already knows how to verify one; it travels in that same `dist/`
|
|
17
|
+
// payload. What is left here is Corepack's environment, which it does not know:
|
|
18
|
+
// where to download from, what credentials to use, and whose signature to trust.
|
|
19
|
+
import { Buffer } from 'node:buffer'
|
|
20
|
+
import { spawnSync } from 'node:child_process'
|
|
21
|
+
import console from 'node:console'
|
|
22
|
+
import fs from 'node:fs'
|
|
23
|
+
import os from 'node:os'
|
|
24
|
+
import path from 'node:path'
|
|
25
|
+
import process from 'node:process'
|
|
26
|
+
import { URL } from 'node:url'
|
|
27
|
+
import { readWrapperManifest, resolveInstalledBinary, wrapperDir } from '../native-binary.mjs'
|
|
28
|
+
|
|
29
|
+
// Deliberately not the `pnpm` placeholder that `install.js` overwrites: a name
|
|
30
|
+
// of its own is what tells a downloaded binary apart from the placeholder.
|
|
31
|
+
const DOWNLOADED_BINARY = path.join(
|
|
32
|
+
wrapperDir,
|
|
33
|
+
process.platform === 'win32' ? 'pnpm-native.exe' : 'pnpm-native'
|
|
34
|
+
)
|
|
35
|
+
const GET_PNPM = new URL('../dist/node_modules/get-pnpm/lib/index.js', import.meta.url)
|
|
36
|
+
const DEFAULT_REGISTRY = 'https://registry.npmjs.org'
|
|
37
|
+
|
|
38
|
+
run(await nativeBinary())
|
|
39
|
+
|
|
40
|
+
function run (binary) {
|
|
41
|
+
// Ctrl-C reaches the whole foreground process group, so the binary gets its
|
|
42
|
+
// own SIGINT; exiting here first would hand the terminal back while it is
|
|
43
|
+
// still shutting down.
|
|
44
|
+
process.on('SIGINT', () => {})
|
|
45
|
+
|
|
46
|
+
const result = spawnSync(binary, process.argv.slice(2), { stdio: 'inherit' })
|
|
47
|
+
if (result.error != null) {
|
|
48
|
+
fail(`Could not run the pnpm binary at ${binary}: ${result.error.message}`)
|
|
49
|
+
}
|
|
50
|
+
process.exitCode = result.signal == null
|
|
51
|
+
? result.status ?? 1
|
|
52
|
+
: 128 + (os.constants.signals[result.signal] ?? 0)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async function nativeBinary () {
|
|
56
|
+
const installed = resolveInstalledBinary()
|
|
57
|
+
if (installed != null) {
|
|
58
|
+
return installed
|
|
59
|
+
}
|
|
60
|
+
// A plain file, not merely something at that path: what a previous run left
|
|
61
|
+
// is a file, and a directory there would be spawned as if it were a binary.
|
|
62
|
+
if (fs.lstatSync(DOWNLOADED_BINARY, { throwIfNoEntry: false })?.isFile() === true) {
|
|
63
|
+
return DOWNLOADED_BINARY
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (process.env.COREPACK_ENABLE_NETWORK === '0') {
|
|
67
|
+
fail('Network access is disabled by the environment, so the pnpm binary cannot be downloaded.')
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const { version } = readWrapperManifest()
|
|
71
|
+
console.error(`Downloading the pnpm ${version} binary for ${process.platform}-${process.arch}...`)
|
|
72
|
+
const { downloadPnpmExecutable } = await import(GET_PNPM).catch((err) => {
|
|
73
|
+
fail(`This copy of the pnpm package is missing the downloader it needs: ${err.message}`)
|
|
74
|
+
})
|
|
75
|
+
try {
|
|
76
|
+
await downloadPnpmExecutable({
|
|
77
|
+
version,
|
|
78
|
+
registry: process.env.COREPACK_NPM_REGISTRY || DEFAULT_REGISTRY,
|
|
79
|
+
destPath: DOWNLOADED_BINARY,
|
|
80
|
+
headers: registryHeaders(),
|
|
81
|
+
...signaturePolicy(),
|
|
82
|
+
})
|
|
83
|
+
} catch (err) {
|
|
84
|
+
fail(`Could not download the pnpm ${version} binary: ${err.message}`)
|
|
85
|
+
}
|
|
86
|
+
return DOWNLOADED_BINARY
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Credentials for the registry, read the way Corepack reads its own. `get-pnpm`
|
|
91
|
+
* keeps them on that registry's origin, so a download host it names never
|
|
92
|
+
* receives them.
|
|
93
|
+
*/
|
|
94
|
+
function registryHeaders () {
|
|
95
|
+
const { COREPACK_NPM_TOKEN, COREPACK_NPM_USERNAME, COREPACK_NPM_PASSWORD } = process.env
|
|
96
|
+
if (COREPACK_NPM_TOKEN) {
|
|
97
|
+
return { authorization: `Bearer ${COREPACK_NPM_TOKEN}` }
|
|
98
|
+
}
|
|
99
|
+
if (COREPACK_NPM_USERNAME && COREPACK_NPM_PASSWORD) {
|
|
100
|
+
const credentials = Buffer.from(`${COREPACK_NPM_USERNAME}:${COREPACK_NPM_PASSWORD}`, 'utf8')
|
|
101
|
+
return { authorization: `Basic ${credentials.toString('base64')}` }
|
|
102
|
+
}
|
|
103
|
+
return undefined
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Whose signature over the download to trust, following `COREPACK_INTEGRITY_KEYS`
|
|
108
|
+
* exactly as Corepack does: npm's own keys when it is unset, the keys it names
|
|
109
|
+
* when it holds a key set, and no signature check at all when it is `0` or
|
|
110
|
+
* empty — which is the state a registry that re-publishes packages, and
|
|
111
|
+
* therefore carries no npm signatures, already has to be in for Corepack to
|
|
112
|
+
* have installed this wrapper from it.
|
|
113
|
+
*/
|
|
114
|
+
function signaturePolicy () {
|
|
115
|
+
const configured = process.env.COREPACK_INTEGRITY_KEYS
|
|
116
|
+
if (configured == null) {
|
|
117
|
+
return {}
|
|
118
|
+
}
|
|
119
|
+
if (configured === '' || configured === '0') {
|
|
120
|
+
return { verifySignature: false }
|
|
121
|
+
}
|
|
122
|
+
let keys
|
|
123
|
+
try {
|
|
124
|
+
keys = JSON.parse(configured).npm
|
|
125
|
+
} catch (err) {
|
|
126
|
+
fail(`COREPACK_INTEGRITY_KEYS is not readable as JSON: ${err.message}`)
|
|
127
|
+
}
|
|
128
|
+
// An absent or malformed `npm` entry would otherwise be passed on as no keys
|
|
129
|
+
// at all, which falls back to npm's own — the opposite of what setting the
|
|
130
|
+
// variable asked for. An empty set is left alone: it names no key to trust,
|
|
131
|
+
// and Corepack reads it the same way, so every download is refused.
|
|
132
|
+
if (!Array.isArray(keys)) {
|
|
133
|
+
fail('COREPACK_INTEGRITY_KEYS holds no "npm" key set to verify the pnpm binary against.')
|
|
134
|
+
}
|
|
135
|
+
return { keys }
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function fail (message) {
|
|
139
|
+
console.error(message)
|
|
140
|
+
process.exit(1)
|
|
141
|
+
}
|
package/bin/pnpx.mjs
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Corepack's `pnpx` entry point; see ./pnpm.mjs. The native binary infers the
|
|
3
|
+
// `dlx` subcommand from the name it was launched under, which does not survive
|
|
4
|
+
// being spawned from here, so inject it the way the `pnpx` shell script does.
|
|
5
|
+
import process from 'node:process'
|
|
6
|
+
|
|
7
|
+
process.argv = [...process.argv.slice(0, 2), 'dlx', ...process.argv.slice(2)]
|
|
8
|
+
|
|
9
|
+
await import('./pnpm.mjs')
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
#!/bin/sh
|
|
2
|
+
# Resolve $0 through symlinks so basedir is the shim's real directory.
|
|
3
|
+
# Cap hops at the kernel's ELOOP limit so a cycle cannot hang the shim.
|
|
4
|
+
link="$0"
|
|
5
|
+
hops=0
|
|
6
|
+
while [ -L "$link" ] && [ "$hops" -lt 40 ]; do
|
|
7
|
+
hops=$((hops+1))
|
|
8
|
+
target=$(readlink "$link")
|
|
9
|
+
case "$target" in
|
|
10
|
+
/*) link="$target" ;;
|
|
11
|
+
*) link="$(dirname "$link")/$target" ;;
|
|
12
|
+
esac
|
|
13
|
+
done
|
|
14
|
+
basedir=$(dirname "$(echo "$link" | sed -e 's,\\,/,g')")
|
|
15
|
+
basedir_win="$basedir"
|
|
16
|
+
exe=""
|
|
17
|
+
msys=""
|
|
18
|
+
|
|
19
|
+
case `uname -a` in
|
|
20
|
+
*CYGWIN*|*MINGW*|*MSYS*)
|
|
21
|
+
if command -v cygpath > /dev/null 2>&1; then
|
|
22
|
+
basedir_win=`cygpath -w "$basedir"`
|
|
23
|
+
fi
|
|
24
|
+
exe=".exe"
|
|
25
|
+
msys="true"
|
|
26
|
+
;;
|
|
27
|
+
*WSL2*)
|
|
28
|
+
if command -v wslpath > /dev/null 2>&1; then
|
|
29
|
+
basedir_win="$(wslpath -w "$basedir" 2> /dev/null)"
|
|
30
|
+
if [ $? -ne 0 ] || [ -z "$basedir_win" ]; then
|
|
31
|
+
basedir_win="$basedir"
|
|
32
|
+
else
|
|
33
|
+
exe=".exe"
|
|
34
|
+
fi
|
|
35
|
+
fi
|
|
36
|
+
;;
|
|
37
|
+
esac
|
|
38
|
+
|
|
39
|
+
if [ -n "$exe" ] && [ -x "$basedir/node.exe" ]; then
|
|
40
|
+
exec "$basedir/node.exe" "$basedir_win/../get-pnpm/bin/get-pnpm.js" "$@"
|
|
41
|
+
elif [ -x "$basedir/node" ]; then
|
|
42
|
+
exec "$basedir/node" "$basedir/../get-pnpm/bin/get-pnpm.js" "$@"
|
|
43
|
+
elif command -v node >/dev/null 2>&1; then
|
|
44
|
+
exec node "$basedir/../get-pnpm/bin/get-pnpm.js" "$@"
|
|
45
|
+
elif [ -n "$exe" ] && command -v node.exe >/dev/null 2>&1; then
|
|
46
|
+
exec node.exe "$basedir_win/../get-pnpm/bin/get-pnpm.js" "$@"
|
|
47
|
+
else
|
|
48
|
+
exec node "$basedir/../get-pnpm/bin/get-pnpm.js" "$@"
|
|
49
|
+
fi
|
|
50
|
+
# cmd-shim-target=/home/runner/_work/pnpm/pnpm/pnpm/npm/pnpm/temp-deploy/node_modules/get-pnpm/bin/get-pnpm.js
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"packages":{".":{"url":"..","dependencies":{"node-gyp":"node-gyp","pacquet":"."}},"@isaacs/fs-minipass":{"url":"./@isaacs/fs-minipass","dependencies":{"@isaacs/fs-minipass":"@isaacs/fs-minipass","minipass":"minipass"}},"abbrev":{"url":"./abbrev","dependencies":{"abbrev":"abbrev"}},"chownr":{"url":"./chownr","dependencies":{"chownr":"chownr"}},"env-paths":{"url":"./env-paths","dependencies":{"env-paths":"env-paths"}},"exponential-backoff":{"url":"./exponential-backoff","dependencies":{"exponential-backoff":"exponential-backoff"}},"fdir":{"url":"./fdir","dependencies":{"fdir":"fdir","picomatch":"picomatch"}},"graceful-fs":{"url":"./graceful-fs","dependencies":{"graceful-fs":"graceful-fs"}},"isexe":{"url":"./isexe","dependencies":{"isexe":"isexe"}},"minipass":{"url":"./minipass","dependencies":{"minipass":"minipass"}},"minizlib":{"url":"./minizlib","dependencies":{"minipass":"minipass","minizlib":"minizlib"}},"node-gyp":{"url":"./node-gyp","dependencies":{"env-paths":"env-paths","exponential-backoff":"exponential-backoff","graceful-fs":"graceful-fs","node-gyp":"node-gyp","nopt":"nopt","proc-log":"proc-log","semver":"semver","tar":"tar","tinyglobby":"tinyglobby","undici":"undici","which":"which"}},"nopt":{"url":"./nopt","dependencies":{"abbrev":"abbrev","nopt":"nopt"}},"picomatch":{"url":"./picomatch","dependencies":{"picomatch":"picomatch"}},"proc-log":{"url":"./proc-log","dependencies":{"proc-log":"proc-log"}},"semver":{"url":"./semver","dependencies":{"semver":"semver"}},"tar":{"url":"./tar","dependencies":{"@isaacs/fs-minipass":"@isaacs/fs-minipass","chownr":"chownr","minipass":"minipass","minizlib":"minizlib","tar":"tar","yallist":"yallist"}},"tinyglobby":{"url":"./tinyglobby","dependencies":{"fdir":"fdir","picomatch":"picomatch","tinyglobby":"tinyglobby"}},"undici":{"url":"./undici","dependencies":{"undici":"undici"}},"which":{"url":"./which","dependencies":{"isexe":"isexe","which":"which"}},"yallist":{"url":"./yallist","dependencies":{"yallist":"yallist"}}}}
|
|
1
|
+
{"packages":{".":{"url":"..","dependencies":{"get-pnpm":"get-pnpm","node-gyp":"node-gyp","pacquet":"."}},"@isaacs/fs-minipass":{"url":"./@isaacs/fs-minipass","dependencies":{"@isaacs/fs-minipass":"@isaacs/fs-minipass","minipass":"minipass"}},"abbrev":{"url":"./abbrev","dependencies":{"abbrev":"abbrev"}},"chownr":{"url":"./chownr","dependencies":{"chownr":"chownr"}},"env-paths":{"url":"./env-paths","dependencies":{"env-paths":"env-paths"}},"exponential-backoff":{"url":"./exponential-backoff","dependencies":{"exponential-backoff":"exponential-backoff"}},"fdir":{"url":"./fdir","dependencies":{"fdir":"fdir","picomatch":"picomatch"}},"get-pnpm":{"url":"./get-pnpm","dependencies":{"get-pnpm":"get-pnpm"}},"graceful-fs":{"url":"./graceful-fs","dependencies":{"graceful-fs":"graceful-fs"}},"isexe":{"url":"./isexe","dependencies":{"isexe":"isexe"}},"minipass":{"url":"./minipass","dependencies":{"minipass":"minipass"}},"minizlib":{"url":"./minizlib","dependencies":{"minipass":"minipass","minizlib":"minizlib"}},"node-gyp":{"url":"./node-gyp","dependencies":{"env-paths":"env-paths","exponential-backoff":"exponential-backoff","graceful-fs":"graceful-fs","node-gyp":"node-gyp","nopt":"nopt","proc-log":"proc-log","semver":"semver","tar":"tar","tinyglobby":"tinyglobby","undici":"undici","which":"which"}},"nopt":{"url":"./nopt","dependencies":{"abbrev":"abbrev","nopt":"nopt"}},"picomatch":{"url":"./picomatch","dependencies":{"picomatch":"picomatch"}},"proc-log":{"url":"./proc-log","dependencies":{"proc-log":"proc-log"}},"semver":{"url":"./semver","dependencies":{"semver":"semver"}},"tar":{"url":"./tar","dependencies":{"@isaacs/fs-minipass":"@isaacs/fs-minipass","chownr":"chownr","minipass":"minipass","minizlib":"minizlib","tar":"tar","yallist":"yallist"}},"tinyglobby":{"url":"./tinyglobby","dependencies":{"fdir":"fdir","picomatch":"picomatch","tinyglobby":"tinyglobby"}},"undici":{"url":"./undici","dependencies":{"undici":"undici"}},"which":{"url":"./which","dependencies":{"isexe":"isexe","which":"which"}},"yallist":{"url":"./yallist","dependencies":{"yallist":"yallist"}}}}
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
{
|
|
2
|
-
"lastValidatedTimestamp":
|
|
2
|
+
"lastValidatedTimestamp": 1786812811685,
|
|
3
3
|
"projects": {
|
|
4
4
|
"/home/runner/_work/pnpm/pnpm/pnpm/npm/pnpm/temp-deploy": {
|
|
5
5
|
"name": "pacquet",
|
|
6
|
-
"version": "12.0.0-rc.
|
|
6
|
+
"version": "12.0.0-rc.6"
|
|
7
7
|
}
|
|
8
8
|
},
|
|
9
9
|
"pnpmfiles": [],
|
|
@@ -131,7 +131,7 @@
|
|
|
131
131
|
"escape-string-regexp": "^5.0.0",
|
|
132
132
|
"eslint": "^10.8.1",
|
|
133
133
|
"eslint-plugin-import-x": "^4.17.1",
|
|
134
|
-
"eslint-plugin-jest": "^29.16.
|
|
134
|
+
"eslint-plugin-jest": "^29.16.1",
|
|
135
135
|
"eslint-plugin-n": "^18.3.0",
|
|
136
136
|
"eslint-plugin-promise": "^7.3.0",
|
|
137
137
|
"eslint-plugin-regexp": "^3.1.1",
|
|
@@ -142,6 +142,7 @@
|
|
|
142
142
|
"fast-glob": "^3.3.3",
|
|
143
143
|
"fs-extra": "^11.4.0",
|
|
144
144
|
"fuse-native": "^2.2.6",
|
|
145
|
+
"get-pnpm": "^0.0.3",
|
|
145
146
|
"get-port": "^7.2.0",
|
|
146
147
|
"ghooks": "2.0.4",
|
|
147
148
|
"graceful-fs": "^4.2.11",
|
|
@@ -248,7 +249,7 @@
|
|
|
248
249
|
"ts-jest-resolver": "2.0.1",
|
|
249
250
|
"typescript": "6.0.3",
|
|
250
251
|
"typescript-eslint": "^8.67.0",
|
|
251
|
-
"undici": "^7.
|
|
252
|
+
"undici": "^7.29.0",
|
|
252
253
|
"unified": "^11.0.5",
|
|
253
254
|
"validate-npm-package-name": "7.0.2",
|
|
254
255
|
"version-selector-type": "^3.0.0",
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { runCli } from '../lib/index.js'
|
|
3
|
+
|
|
4
|
+
try {
|
|
5
|
+
process.exitCode = await runCli(process.argv.slice(2))
|
|
6
|
+
} catch (err) {
|
|
7
|
+
// Anything can be thrown, and reading `.message` off a non-Error would either
|
|
8
|
+
// print `undefined` or throw again, burying the reason under a stack trace.
|
|
9
|
+
console.error(err instanceof Error ? err.message : String(err))
|
|
10
|
+
process.exitCode = 1
|
|
11
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { randomBytes } from 'node:crypto';
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { extractTarballMember } from './extractTarballMember.js';
|
|
5
|
+
import { isMusl, platformPackageName } from './platformPackageName.js';
|
|
6
|
+
import { downloadTarball, fetchVersionMeta, normalizeRegistry } from './registry.js';
|
|
7
|
+
import { majorVersion } from './resolveVersion.js';
|
|
8
|
+
import { sameFileContents } from './sameFileContents.js';
|
|
9
|
+
import { verifyRegistrySignature } from './verifySignature.js';
|
|
10
|
+
/** Name the executable is published under inside its platform package. */
|
|
11
|
+
function executableName() {
|
|
12
|
+
return process.platform === 'win32' ? 'pnpm.exe' : 'pnpm';
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Places the pnpm executable for this host at `destPath`, and nothing else.
|
|
16
|
+
*
|
|
17
|
+
* The narrow half of {@link downloadPnpm}, for a caller that already knows the
|
|
18
|
+
* exact version and already has whatever else it needs: no dist-tag lookup (so
|
|
19
|
+
* no packument download), no `dist/` tree, no directory to assemble. Corepack
|
|
20
|
+
* is the case it exists for — it unpacks the `pnpm` package itself but installs
|
|
21
|
+
* none of its dependencies, so the executable has to arrive separately.
|
|
22
|
+
*
|
|
23
|
+
* The download is checked against the checksum the registry published for it,
|
|
24
|
+
* and that checksum against npm's signature, exactly as {@link downloadPnpm}
|
|
25
|
+
* does; nothing is written to `destPath` until both pass. See
|
|
26
|
+
* {@link DownloadExecutableOptions.verifySignature} for the one waiver.
|
|
27
|
+
*
|
|
28
|
+
* Placement is atomic and tolerates losing a race: a concurrent call that got
|
|
29
|
+
* there first keeps its copy, since both placed the same verified bytes.
|
|
30
|
+
*
|
|
31
|
+
* @returns the package the executable came from.
|
|
32
|
+
*/
|
|
33
|
+
export async function downloadPnpmExecutable(opts) {
|
|
34
|
+
const { version, destPath } = opts;
|
|
35
|
+
const registry = normalizeRegistry(opts.registry);
|
|
36
|
+
const packageName = platformPackageName({
|
|
37
|
+
major: majorVersion(version),
|
|
38
|
+
platform: process.platform,
|
|
39
|
+
arch: process.arch,
|
|
40
|
+
musl: isMusl(),
|
|
41
|
+
});
|
|
42
|
+
const meta = await fetchVersionMeta(registry, packageName, version, opts.headers);
|
|
43
|
+
if (!meta.dist.integrity) {
|
|
44
|
+
throw new Error(`The npm registry published no checksum for ${packageName}@${version}, so it cannot be verified.`);
|
|
45
|
+
}
|
|
46
|
+
if (opts.verifySignature !== false) {
|
|
47
|
+
verifyRegistrySignature({
|
|
48
|
+
name: packageName,
|
|
49
|
+
version,
|
|
50
|
+
integrity: meta.dist.integrity,
|
|
51
|
+
signatures: meta.dist.signatures,
|
|
52
|
+
keys: opts.keys,
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
const scratch = `${destPath}.${randomBytes(6).toString('hex')}`;
|
|
56
|
+
const tarball = `${scratch}.tgz`;
|
|
57
|
+
const staged = `${scratch}.tmp`;
|
|
58
|
+
fs.mkdirSync(path.dirname(destPath), { recursive: true });
|
|
59
|
+
try {
|
|
60
|
+
await downloadTarball(meta, tarball, { registry, headers: opts.headers });
|
|
61
|
+
const member = `package/${executableName()}`;
|
|
62
|
+
if (!await extractTarballMember(tarball, member, staged, 0o755)) {
|
|
63
|
+
throw new Error(`The ${packageName}@${version} tarball contains no ${member}.`);
|
|
64
|
+
}
|
|
65
|
+
place(staged, destPath);
|
|
66
|
+
}
|
|
67
|
+
finally {
|
|
68
|
+
fs.rmSync(tarball, { force: true });
|
|
69
|
+
fs.rmSync(staged, { force: true });
|
|
70
|
+
}
|
|
71
|
+
return { packageName };
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* The codes Windows fails a rename with when the destination is held open —
|
|
75
|
+
* which is what a concurrent call executing the copy it just placed looks like.
|
|
76
|
+
* POSIX replaces the destination instead, so a failure there is a real one.
|
|
77
|
+
*/
|
|
78
|
+
const DESTINATION_IN_USE = new Set(['EPERM', 'EACCES', 'EBUSY']);
|
|
79
|
+
/**
|
|
80
|
+
* Moves the verified executable into place, keeping the copy a concurrent call
|
|
81
|
+
* placed if that is what stops the rename.
|
|
82
|
+
*
|
|
83
|
+
* A lost race is not assumed from the failure alone: the copy already there has
|
|
84
|
+
* to hold the same bytes as the one just verified, or this call has no idea
|
|
85
|
+
* what it would be reporting success for. Anything else — no permission on the
|
|
86
|
+
* directory, a directory or an unrelated file at `destPath` — is a failure.
|
|
87
|
+
*/
|
|
88
|
+
function place(staged, destPath) {
|
|
89
|
+
try {
|
|
90
|
+
fs.renameSync(staged, destPath);
|
|
91
|
+
}
|
|
92
|
+
catch (err) {
|
|
93
|
+
const code = err.code ?? '';
|
|
94
|
+
if (!DESTINATION_IN_USE.has(code))
|
|
95
|
+
throw err;
|
|
96
|
+
if (!sameFileContents(staged, destPath))
|
|
97
|
+
throw err;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { spawnSync } from 'node:child_process';
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
/**
|
|
4
|
+
* Unpacks a package tarball into `dest`, leaving its `package/` root in place.
|
|
5
|
+
*
|
|
6
|
+
* Shells out to `tar`, which every supported host has: macOS and Linux ship it,
|
|
7
|
+
* and Windows has had bsdtar since Windows 10 1803. Member selection and
|
|
8
|
+
* `--strip-components` are avoided because busybox tar and bsdtar disagree
|
|
9
|
+
* about them.
|
|
10
|
+
*/
|
|
11
|
+
export function extractTarball(tarball, dest) {
|
|
12
|
+
fs.mkdirSync(dest, { recursive: true });
|
|
13
|
+
const { error, status, stderr } = spawnSync('tar', ['-xzf', tarball, '-C', dest], { encoding: 'utf8' });
|
|
14
|
+
if (error != null) {
|
|
15
|
+
if (error.code === 'ENOENT') {
|
|
16
|
+
throw new Error('This installer needs the `tar` command, which was not found on your PATH.');
|
|
17
|
+
}
|
|
18
|
+
throw error;
|
|
19
|
+
}
|
|
20
|
+
if (status !== 0) {
|
|
21
|
+
throw new Error(`Could not extract ${tarball}: ${stderr.trim()}`);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { createReadStream, createWriteStream } from 'node:fs';
|
|
2
|
+
import { pipeline } from 'node:stream/promises';
|
|
3
|
+
import { createGunzip } from 'node:zlib';
|
|
4
|
+
const BLOCK_SIZE = 512;
|
|
5
|
+
/** Regular file, in both the old (`\0`) and the ustar (`0`) spelling. */
|
|
6
|
+
const FILE_TYPES = new Set(['0', '\0']);
|
|
7
|
+
/**
|
|
8
|
+
* Writes one file out of a package tarball to `dest`, and nothing else.
|
|
9
|
+
*
|
|
10
|
+
* Reads the archive in-process rather than shelling out to `tar` the way
|
|
11
|
+
* {@link extractTarball} does: a caller that wants a single executable, in an
|
|
12
|
+
* environment it does not control (a Corepack cache in a minimal image), should
|
|
13
|
+
* not need a `tar` on the PATH for it. Only regular files are considered, which
|
|
14
|
+
* is all an npm tarball holds beyond directories, and the parse stays streaming
|
|
15
|
+
* so the archive never lands in memory.
|
|
16
|
+
*
|
|
17
|
+
* `dest` is created exclusively — an existing file, or a symlink planted at
|
|
18
|
+
* that path, fails the write rather than being followed.
|
|
19
|
+
*
|
|
20
|
+
* @param tarball Path to the gzipped archive.
|
|
21
|
+
* @param memberPath Path of the wanted file inside it, e.g. `package/pnpm`.
|
|
22
|
+
* @param dest Path to write it to.
|
|
23
|
+
* @param mode Permissions for `dest`.
|
|
24
|
+
* @returns whether the member was there.
|
|
25
|
+
*/
|
|
26
|
+
export async function extractTarballMember(tarball, memberPath, dest, mode = 0o644) {
|
|
27
|
+
let found = false;
|
|
28
|
+
await pipeline(createReadStream(tarball), createGunzip(), async function (source) {
|
|
29
|
+
const reader = new BlockReader(source);
|
|
30
|
+
while (true) {
|
|
31
|
+
const header = await reader.read(BLOCK_SIZE);
|
|
32
|
+
// The archive ends with zero-filled blocks; one is enough to stop.
|
|
33
|
+
if (header == null || header[0] === 0)
|
|
34
|
+
return;
|
|
35
|
+
const entry = parseHeader(header);
|
|
36
|
+
if (FILE_TYPES.has(entry.type) && entry.path === memberPath) {
|
|
37
|
+
const written = await reader.pipe(entry.size, createWriteStream(dest, { flags: 'wx', mode }));
|
|
38
|
+
if (written !== entry.size) {
|
|
39
|
+
throw new Error(`${tarball} ends after ${written} of the ${entry.size} bytes it declares for ${memberPath}.`);
|
|
40
|
+
}
|
|
41
|
+
found = true;
|
|
42
|
+
// The rest of the archive holds nothing this caller asked for, and
|
|
43
|
+
// the checksum that vouches for it was checked before any of it was
|
|
44
|
+
// read.
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
await reader.skip(Math.ceil(entry.size / BLOCK_SIZE) * BLOCK_SIZE);
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
return found;
|
|
51
|
+
}
|
|
52
|
+
function parseHeader(header) {
|
|
53
|
+
const name = readString(header, 0, 100);
|
|
54
|
+
const prefix = readString(header, 345, 155);
|
|
55
|
+
return {
|
|
56
|
+
path: prefix === '' ? name : `${prefix}/${name}`,
|
|
57
|
+
size: parseInt(readString(header, 124, 12).trim() || '0', 8),
|
|
58
|
+
type: String.fromCharCode(header[156]),
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
function readString(header, start, length) {
|
|
62
|
+
const field = header.subarray(start, start + length);
|
|
63
|
+
const end = field.indexOf(0);
|
|
64
|
+
return field.toString('utf8', 0, end === -1 ? field.length : end);
|
|
65
|
+
}
|
|
66
|
+
class BlockReader {
|
|
67
|
+
#iterator;
|
|
68
|
+
#buffered = [];
|
|
69
|
+
#buffedBytes = 0;
|
|
70
|
+
#done = false;
|
|
71
|
+
constructor(source) {
|
|
72
|
+
this.#iterator = source[Symbol.asyncIterator]();
|
|
73
|
+
}
|
|
74
|
+
/** The next `size` bytes, or `null` once the stream ends. */
|
|
75
|
+
async read(size) {
|
|
76
|
+
if (!await this.#fill(size))
|
|
77
|
+
return null;
|
|
78
|
+
return this.#take(size);
|
|
79
|
+
}
|
|
80
|
+
async skip(size) {
|
|
81
|
+
let left = size;
|
|
82
|
+
while (left > 0) {
|
|
83
|
+
if (!await this.#fill(1))
|
|
84
|
+
return;
|
|
85
|
+
left -= this.#take(Math.min(left, this.#buffedBytes)).length;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Hands the next `size` bytes to `destination`, without collecting them.
|
|
90
|
+
*
|
|
91
|
+
* @returns how many bytes there were, which is fewer than `size` only when
|
|
92
|
+
* the stream ended early.
|
|
93
|
+
*/
|
|
94
|
+
async pipe(size, destination) {
|
|
95
|
+
const self = this;
|
|
96
|
+
let left = size;
|
|
97
|
+
await pipeline(async function* () {
|
|
98
|
+
while (left > 0) {
|
|
99
|
+
if (!await self.#fill(1))
|
|
100
|
+
return;
|
|
101
|
+
const chunk = self.#take(Math.min(left, self.#buffedBytes));
|
|
102
|
+
left -= chunk.length;
|
|
103
|
+
yield chunk;
|
|
104
|
+
}
|
|
105
|
+
}, destination);
|
|
106
|
+
return size - left;
|
|
107
|
+
}
|
|
108
|
+
/** Reads until at least `size` bytes are buffered, or the stream ends. */
|
|
109
|
+
async #fill(size) {
|
|
110
|
+
while (this.#buffedBytes < size && !this.#done) {
|
|
111
|
+
const { value, done } = await this.#iterator.next();
|
|
112
|
+
if (done === true) {
|
|
113
|
+
this.#done = true;
|
|
114
|
+
}
|
|
115
|
+
else {
|
|
116
|
+
this.#buffered.push(value);
|
|
117
|
+
this.#buffedBytes += value.length;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
return this.#buffedBytes >= size;
|
|
121
|
+
}
|
|
122
|
+
#take(size) {
|
|
123
|
+
const joined = this.#buffered.length === 1 ? this.#buffered[0] : Buffer.concat(this.#buffered);
|
|
124
|
+
this.#buffered = joined.length > size ? [joined.subarray(size)] : [];
|
|
125
|
+
this.#buffedBytes = joined.length - size;
|
|
126
|
+
return joined.subarray(0, size);
|
|
127
|
+
}
|
|
128
|
+
}
|