@pnpm/exe 12.0.0-rc.0 → 12.0.0-rc.10

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.
Files changed (31) hide show
  1. package/README.md +12 -0
  2. package/THIRD-PARTY-NOTICES.md +42 -0
  3. package/bin/pnpm.mjs +141 -0
  4. package/bin/pnpx.mjs +9 -0
  5. package/dist/node_modules/.bin/get-pnpm +50 -0
  6. package/dist/node_modules/.bin/node-gyp +13 -1
  7. package/dist/node_modules/.bin/node-which +13 -1
  8. package/dist/node_modules/.bin/nopt +13 -1
  9. package/dist/node_modules/.bin/semver +13 -1
  10. package/dist/node_modules/.package-map.json +1 -1
  11. package/dist/node_modules/.pnpm-workspace-state-v1.json +70 -21
  12. package/dist/node_modules/get-pnpm/bin/get-pnpm.js +11 -0
  13. package/dist/node_modules/get-pnpm/lib/downloadExecutable.js +99 -0
  14. package/dist/node_modules/get-pnpm/lib/extractTarball.js +23 -0
  15. package/dist/node_modules/get-pnpm/lib/extractTarballMember.js +128 -0
  16. package/dist/node_modules/get-pnpm/lib/index.js +208 -0
  17. package/dist/node_modules/get-pnpm/lib/npmSigningKeys.js +23 -0
  18. package/dist/node_modules/get-pnpm/lib/platformPackageName.js +60 -0
  19. package/dist/node_modules/get-pnpm/lib/registry.js +120 -0
  20. package/dist/node_modules/get-pnpm/lib/resolveVersion.js +36 -0
  21. package/dist/node_modules/get-pnpm/lib/sameFileContents.js +45 -0
  22. package/dist/node_modules/get-pnpm/lib/verifySignature.js +51 -0
  23. package/dist/node_modules/get-pnpm/package.json +45 -0
  24. package/dist/node_modules/node-gyp/.release-please-manifest.json +1 -1
  25. package/dist/node_modules/node-gyp/gyp/.release-please-manifest.json +1 -1
  26. package/dist/node_modules/node-gyp/gyp/pyproject.toml +4 -4
  27. package/dist/node_modules/node-gyp/lib/download.js +3 -3
  28. package/dist/node_modules/node-gyp/package.json +1 -1
  29. package/install.js +20 -97
  30. package/native-binary.mjs +116 -0
  31. package/package.json +13 -10
@@ -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
+ }
@@ -0,0 +1,208 @@
1
+ import { spawnSync } from 'node:child_process';
2
+ import fs from 'node:fs';
3
+ import os from 'node:os';
4
+ import path from 'node:path';
5
+ import { extractTarball } from './extractTarball.js';
6
+ import { isMusl, platformPackageName } from './platformPackageName.js';
7
+ import { downloadTarball, fetchPackument, fetchVersionMeta, registryFromEnv } from './registry.js';
8
+ import { majorVersion, resolveVersion } from './resolveVersion.js';
9
+ import { verifyRegistrySignature } from './verifySignature.js';
10
+ export { downloadPnpmExecutable } from './downloadExecutable.js';
11
+ export { extractTarballMember } from './extractTarballMember.js';
12
+ export { isMusl, platformPackageName } from './platformPackageName.js';
13
+ export { DEFAULT_REGISTRY, registryFromEnv } from './registry.js';
14
+ export { majorVersion, resolveVersion } from './resolveVersion.js';
15
+ export { verifyRegistrySignature } from './verifySignature.js';
16
+ /**
17
+ * The package whose dist-tags name every pnpm release. `@pnpm/exe` carries the
18
+ * same versions today, but only `pnpm` is published from v12 onward, so its
19
+ * tags are the ones that cannot go stale.
20
+ */
21
+ const CLI_PKG_NAME = 'pnpm';
22
+ /** Holds the unpacked tarballs while the installation is assembled beside it. */
23
+ const UNPACK_DIR = '.unpack';
24
+ /** Where the `dist/` tree that ships beside the executable is published. */
25
+ function wrapperPackageName(major) {
26
+ return major >= 12 ? CLI_PKG_NAME : '@pnpm/exe';
27
+ }
28
+ const USAGE = `Usage: npx get-pnpm [version]
29
+
30
+ Installs pnpm as a standalone executable and adds it to your PATH.
31
+
32
+ Arguments:
33
+ version An exact version (11.20.0), a major (12), or a dist-tag
34
+ (latest, next-12). Defaults to $PNPM_VERSION, then "latest".
35
+
36
+ Environment variables:
37
+ PNPM_VERSION Version to install when no argument is given.
38
+ PNPM_HOME Directory to install pnpm into.
39
+ npm_config_registry Registry to download pnpm from.
40
+ `;
41
+ export async function runCli(argv) {
42
+ const positional = [];
43
+ for (const arg of argv) {
44
+ if (arg === '--help' || arg === '-h') {
45
+ console.log(USAGE);
46
+ return 0;
47
+ }
48
+ if (arg.startsWith('-')) {
49
+ throw new Error(`Unknown option "${arg}".\n\n${USAGE}`);
50
+ }
51
+ positional.push(arg);
52
+ }
53
+ if (positional.length > 1) {
54
+ throw new Error(`Expected at most one version, got ${positional.length}.\n\n${USAGE}`);
55
+ }
56
+ return installPnpm({
57
+ versionSpec: positional[0] ?? process.env.PNPM_VERSION ?? 'latest',
58
+ registry: registryFromEnv(),
59
+ });
60
+ }
61
+ /**
62
+ * Downloads the pnpm executable and hands over to `pnpm setup`, which installs
63
+ * it globally and puts it on the PATH.
64
+ *
65
+ * Every download is checked against the checksum the registry published for it,
66
+ * and that checksum against npm's signature — see `verifyRegistrySignature`.
67
+ *
68
+ * The temporary directory is assembled to look like the release tarball that
69
+ * https://get.pnpm.io/install.sh downloads — the executable next to its `dist/`
70
+ * tree — because `pnpm setup` installs that directory as-is.
71
+ *
72
+ * @returns the exit code of `pnpm setup`.
73
+ */
74
+ export async function installPnpm(opts) {
75
+ const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'pnpm-install-'));
76
+ const removeTmpDir = () => {
77
+ fs.rmSync(tmpDir, { recursive: true, force: true });
78
+ };
79
+ // Registered for the duration of the call and no longer: this is also a
80
+ // library function, and handlers left behind would accumulate and outlive the
81
+ // directory they exist to clean up.
82
+ const signals = ['SIGINT', 'SIGTERM', 'SIGHUP'];
83
+ const onSignal = () => {
84
+ removeTmpDir();
85
+ process.exit(1);
86
+ };
87
+ for (const signal of signals) {
88
+ process.once(signal, onSignal);
89
+ }
90
+ try {
91
+ const { binPath } = await downloadPnpm({ ...opts, dest: tmpDir });
92
+ const { error, status } = spawnSync(binPath, ['setup', '--force'], { stdio: 'inherit' });
93
+ if (error != null)
94
+ throw error;
95
+ return status ?? 1;
96
+ }
97
+ finally {
98
+ for (const signal of signals) {
99
+ process.off(signal, onSignal);
100
+ }
101
+ removeTmpDir();
102
+ }
103
+ }
104
+ /**
105
+ * Downloads the pnpm executable into `dest`, laid out the way the release
106
+ * tarball lays it out — the executable next to the `dist/` tree it loads.
107
+ *
108
+ * Every download is checked against the checksum the registry published for it,
109
+ * and that checksum against npm's signature; see `verifyRegistrySignature`.
110
+ * Nothing outside `dest` is touched, so a caller that manages its own PATH — a
111
+ * CI action, say — can use this without the global install `installPnpm` does.
112
+ *
113
+ * @returns the version installed and the path to the executable.
114
+ */
115
+ export async function downloadPnpm(opts) {
116
+ const packument = await fetchPackument(opts.registry, CLI_PKG_NAME);
117
+ const version = resolveVersion(packument, opts.versionSpec);
118
+ const major = majorVersion(version);
119
+ const platformPkgName = platformPackageName({
120
+ major,
121
+ platform: process.platform,
122
+ arch: process.arch,
123
+ musl: isMusl(),
124
+ });
125
+ const { dest } = opts;
126
+ fs.mkdirSync(dest, { recursive: true });
127
+ try {
128
+ console.log(`==> Downloading pnpm ${version}`);
129
+ const executable = process.platform === 'win32' ? 'pnpm.exe' : 'pnpm';
130
+ const fetchPackage = verifiedPackageFetcher({ dir: dest, registry: opts.registry, version, keys: opts.keys });
131
+ // Settled, not `all`: a rejection there would leave the other fetch writing
132
+ // into the directory the `finally` below is about to remove.
133
+ const [platformResult, wrapperResult] = await Promise.allSettled([
134
+ fetchPackage(platformPkgName),
135
+ // v11 was the first release to keep files next to the executable; up to
136
+ // v10 the executable is self-contained and ships no `dist/`.
137
+ major >= 11 ? fetchPackage(wrapperPackageName(major)) : Promise.resolve(undefined),
138
+ ]);
139
+ if (platformResult.status === 'rejected')
140
+ throw platformResult.reason;
141
+ if (wrapperResult.status === 'rejected')
142
+ throw wrapperResult.reason;
143
+ const platformPkg = platformResult.value;
144
+ const wrapperPkg = wrapperResult.value;
145
+ const binPath = path.join(dest, executable);
146
+ fs.renameSync(path.join(platformPkg.dir, executable), binPath);
147
+ fs.chmodSync(binPath, 0o755);
148
+ if (wrapperPkg != null) {
149
+ fs.rmSync(path.join(dest, 'dist'), { recursive: true, force: true });
150
+ fs.renameSync(path.join(wrapperPkg.dir, 'dist'), path.join(dest, 'dist'));
151
+ writeManifest({ dest, executable, version, wrapperDir: wrapperPkg.dir, major });
152
+ }
153
+ return { version, binPath };
154
+ }
155
+ finally {
156
+ // `pnpm setup` installs this directory as a package, so nothing may be left
157
+ // in it that does not belong in the installation.
158
+ fs.rmSync(path.join(dest, UNPACK_DIR), { recursive: true, force: true });
159
+ }
160
+ }
161
+ /**
162
+ * `pnpm setup` installs the directory as a package, writing a minimal manifest
163
+ * when there is none — which is what the release tarball relies on. That
164
+ * tarball bundles the runtime dependencies inside `dist/`; the registry copy
165
+ * declares them instead, so up to v11 they have to be declared here or the
166
+ * install silently loses them (`@reflink/reflink`, and with it copy-on-write
167
+ * cloning). From v12 the `dist/` tree is self-contained again, so the manifest
168
+ * `setup` writes is left to it.
169
+ */
170
+ function writeManifest(opts) {
171
+ if (opts.major >= 12)
172
+ return;
173
+ const wrapper = JSON.parse(fs.readFileSync(path.join(opts.wrapperDir, 'package.json'), 'utf8'));
174
+ if (wrapper.dependencies == null)
175
+ return;
176
+ fs.writeFileSync(path.join(opts.dest, 'package.json'), JSON.stringify({
177
+ name: '@pnpm/exe',
178
+ version: opts.version,
179
+ type: 'module',
180
+ bin: { pnpm: opts.executable, pn: opts.executable },
181
+ dependencies: wrapper.dependencies,
182
+ }));
183
+ }
184
+ /** Downloads packages of one version into one directory, verifying each. */
185
+ function verifiedPackageFetcher(opts) {
186
+ return async function fetchPackage(pkgName) {
187
+ const meta = await fetchVersionMeta(opts.registry, pkgName, opts.version);
188
+ if (!meta.dist.integrity) {
189
+ throw new Error(`The npm registry published no checksum for ${pkgName}@${opts.version}, so it cannot be verified.`);
190
+ }
191
+ verifyRegistrySignature({
192
+ name: pkgName,
193
+ version: opts.version,
194
+ integrity: meta.dist.integrity,
195
+ signatures: meta.dist.signatures,
196
+ keys: opts.keys,
197
+ });
198
+ // Unpack away from the directory being assembled: `pnpm` is both a package
199
+ // name and the name of the executable that ends up beside it.
200
+ const unpackDir = path.join(opts.dir, UNPACK_DIR, pkgName.replaceAll('/', '-'));
201
+ const tarball = `${unpackDir}.tgz`;
202
+ fs.mkdirSync(path.dirname(tarball), { recursive: true });
203
+ await downloadTarball(meta, tarball, { registry: opts.registry });
204
+ extractTarball(tarball, unpackDir);
205
+ fs.rmSync(tarball);
206
+ return { dir: path.join(unpackDir, 'package') };
207
+ };
208
+ }
@@ -0,0 +1,23 @@
1
+ /* eslint-disable */
2
+ // GENERATED — npm's public registry signing keys, mirrored from
3
+ // https://registry.npmjs.org/-/npm/v1/keys
4
+ //
5
+ // Refresh with: node scripts/update-npm-keys.mjs --update
6
+ // A scheduled workflow runs the check, so a rotation arrives as a pull request
7
+ // rather than as a failed install.
8
+ export const NPM_SIGNING_KEYS = [
9
+ {
10
+ "expires": null,
11
+ "keyid": "SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U",
12
+ "keytype": "ecdsa-sha2-nistp256",
13
+ "scheme": "ecdsa-sha2-nistp256",
14
+ "key": "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEY6Ya7W++7aUPzvMTrezH6Ycx3c+HOKYCcNGybJZSCJq/fd7Qa8uuAKtdIkUQtQiEKERhAmE5lMMJhP8OkDOa2g=="
15
+ },
16
+ {
17
+ "expires": "2025-01-29T00:00:00.000Z",
18
+ "keyid": "SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA",
19
+ "keytype": "ecdsa-sha2-nistp256",
20
+ "scheme": "ecdsa-sha2-nistp256",
21
+ "key": "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE1Olb3zMAFFxXKHiIkQO5cJ3Yhl5i6UPp+IhuteBJbuHcA5UogKo0EWtlWwW6KSaKoTNEYL7JlCQiVnkhBktUgg=="
22
+ }
23
+ ];
@@ -0,0 +1,60 @@
1
+ /**
2
+ * Name of the npm package that carries the pnpm executable for `target`.
3
+ *
4
+ * pnpm ships the binary in a per-host package that `@pnpm/exe` lists as an
5
+ * optional dependency. The naming scheme changed with the Rust rewrite: v12
6
+ * publishes `@pnpm/exe.<process.platform>-<arch>[-musl]`, while v11 and older
7
+ * publish `@pnpm/<macos|win|linux|linuxstatic>-<arch>`.
8
+ *
9
+ * @throws if pnpm publishes no binary for the host — either because the
10
+ * architecture was never supported, or because of the v11-only Intel macOS gap.
11
+ */
12
+ export function platformPackageName({ major, platform, arch, musl }) {
13
+ if (arch !== 'x64' && arch !== 'arm64') {
14
+ throw new Error('Sorry! pnpm currently only provides pre-built binaries for x86_64/arm64 architectures.');
15
+ }
16
+ if (platform !== 'darwin' && platform !== 'linux' && platform !== 'win32') {
17
+ throw new Error(`Sorry! pnpm does not provide a pre-built binary for ${platform}.`);
18
+ }
19
+ if (platform === 'darwin' && arch === 'x64' && major === 11) {
20
+ throw new Error(`pnpm v11 does not provide a working binary for Intel macOS (darwin-x64) due to an upstream Node.js SEA bug.
21
+
22
+ Install pnpm a different way instead:
23
+ npx get-pnpm 12 # pnpm v12 ships an Intel macOS binary
24
+ npm install -g pnpm # uses your system Node.js
25
+ brew install pnpm # via Homebrew
26
+
27
+ More context: https://github.com/pnpm/pnpm/issues/11423`);
28
+ }
29
+ const linuxMusl = platform === 'linux' && musl;
30
+ if (major >= 12) {
31
+ return `@pnpm/exe.${platform}-${arch}${linuxMusl ? '-musl' : ''}`;
32
+ }
33
+ return `@pnpm/${legacyOsSegment(platform, linuxMusl)}-${arch}`;
34
+ }
35
+ function legacyOsSegment(platform, isMusl) {
36
+ switch (platform) {
37
+ case 'darwin': return 'macos';
38
+ case 'win32': return 'win';
39
+ default: return isMusl ? 'linuxstatic' : 'linux';
40
+ }
41
+ }
42
+ /**
43
+ * Whether this host's libc is musl rather than glibc.
44
+ *
45
+ * Probed the way pnpm's own wrappers do it rather than through a dependency:
46
+ * glibc builds report a `glibcVersionRuntime`, musl builds leave it unset.
47
+ * Keeping this package dependency-free means `npx` fetches one thing before
48
+ * pnpm exists.
49
+ */
50
+ export function isMusl() {
51
+ if (process.platform !== 'linux')
52
+ return false;
53
+ try {
54
+ const report = process.report?.getReport();
55
+ return report?.header != null && !report.header.glibcVersionRuntime;
56
+ }
57
+ catch {
58
+ return false;
59
+ }
60
+ }
@@ -0,0 +1,120 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { createWriteStream } from 'node:fs';
3
+ import { pipeline } from 'node:stream/promises';
4
+ const ABBREVIATED_PACKUMENT = 'application/vnd.npm.install-v1+json';
5
+ export const DEFAULT_REGISTRY = 'https://registry.npmjs.org/';
6
+ /** The registry npx/npm is configured with, so a mirror stays a mirror. */
7
+ export function registryFromEnv() {
8
+ return normalizeRegistry(process.env.npm_config_registry ?? process.env.NPM_CONFIG_REGISTRY ?? DEFAULT_REGISTRY);
9
+ }
10
+ /**
11
+ * A registry URL that can be used as a base for a relative path.
12
+ *
13
+ * `new URL('pkg', 'https://mirror.example.com/npm')` drops the last segment,
14
+ * so a registry served from a subpath needs its trailing slash to survive.
15
+ */
16
+ export function normalizeRegistry(registry) {
17
+ return registry.endsWith('/') ? registry : `${registry}/`;
18
+ }
19
+ // Node's fetch applies no timeout of its own: a registry that accepts the
20
+ // connection and then stalls would hang the install with no output. Metadata is
21
+ // small and quick; the tarball budget has to survive a slow connection carrying
22
+ // a ~150 MB download.
23
+ const METADATA_TIMEOUT_MS = 30_000;
24
+ const TARBALL_TIMEOUT_MS = 15 * 60_000;
25
+ export async function fetchPackument(registry, pkgName, headers) {
26
+ return fetchJson(new URL(pkgName, registry), ABBREVIATED_PACKUMENT, headers);
27
+ }
28
+ export async function fetchVersionMeta(registry, pkgName, version, headers) {
29
+ return fetchJson(new URL(`${pkgName}/${version}`, registry), 'application/json', headers);
30
+ }
31
+ /**
32
+ * Streams `meta.dist.tarball` to `dest`, verifying the checksum the registry
33
+ * published for it. A mismatch removes nothing — the caller discards the whole
34
+ * temporary directory.
35
+ *
36
+ * `registry` re-hosts a tarball URL that points at npm onto that registry, so a
37
+ * mirror that answered the metadata request serves the download too; `headers`
38
+ * travel only to the registry's own origin, never to a download host it names.
39
+ */
40
+ export async function downloadTarball(meta, dest, opts = {}) {
41
+ const url = tarballUrl(meta, opts.registry);
42
+ const response = await request(url, undefined, TARBALL_TIMEOUT_MS, headersFor(url, opts));
43
+ const [algorithm, expected] = checksum(meta);
44
+ const hash = createHash(algorithm);
45
+ const body = response.body;
46
+ await pipeline(async function* () {
47
+ for await (const chunk of body) {
48
+ hash.update(chunk);
49
+ yield chunk;
50
+ }
51
+ }, createWriteStream(dest));
52
+ const actual = hash.digest('base64');
53
+ if (actual !== expected) {
54
+ throw new Error(`The download from ${url.href} does not match the checksum the npm registry published for it. Refusing to install.`);
55
+ }
56
+ }
57
+ /**
58
+ * Where to download `meta`'s tarball from.
59
+ *
60
+ * Registries that proxy npm hand back npm's own URL. Following it would leave
61
+ * the mirror the metadata came from — for an air-gapped one, it would not
62
+ * resolve at all — so the path is re-hosted onto `registry`. Matched by origin,
63
+ * so a host that merely starts with npm's is left alone.
64
+ */
65
+ export function tarballUrl(meta, registry) {
66
+ const url = new URL(meta.dist.tarball);
67
+ if (registry == null || url.origin !== new URL(DEFAULT_REGISTRY).origin)
68
+ return url;
69
+ return new URL(`${url.pathname.replace(/^\//, '')}${url.search}`, normalizeRegistry(registry));
70
+ }
71
+ function headersFor(url, opts) {
72
+ if (opts.headers == null || opts.registry == null)
73
+ return undefined;
74
+ return url.origin === new URL(opts.registry).origin ? opts.headers : undefined;
75
+ }
76
+ /**
77
+ * The algorithm and digest to check a tarball against.
78
+ *
79
+ * `integrity` is an SRI string, which may hold several space-separated entries;
80
+ * the first is used. Only the digest that the registry signature covers is
81
+ * accepted — `shasum` is SHA-1, and a package without `integrity` has already
82
+ * been refused before any download starts.
83
+ */
84
+ function checksum(meta) {
85
+ const entry = meta.dist.integrity?.trim().split(/\s+/)[0];
86
+ if (!entry) {
87
+ throw new Error(`The registry published no checksum for ${meta.dist.tarball}, so it cannot be verified.`);
88
+ }
89
+ const separator = entry.indexOf('-');
90
+ if (separator === -1) {
91
+ throw new Error(`The registry published an unreadable checksum for ${meta.dist.tarball}: ${entry}`);
92
+ }
93
+ return [entry.slice(0, separator), entry.slice(separator + 1)];
94
+ }
95
+ async function fetchJson(url, accept, headers) {
96
+ const response = await request(url, accept, METADATA_TIMEOUT_MS, headers);
97
+ return await response.json();
98
+ }
99
+ async function request(url, accept, timeoutMs, headers) {
100
+ let response;
101
+ try {
102
+ response = await fetch(url, {
103
+ signal: AbortSignal.timeout(timeoutMs),
104
+ headers: { ...headers, ...(accept ? { accept } : {}) },
105
+ });
106
+ }
107
+ catch (err) {
108
+ const reason = err.name === 'TimeoutError'
109
+ ? `timed out after ${Math.round(timeoutMs / 1000)}s`
110
+ : err.message;
111
+ throw new Error(`Could not reach ${url.href}: ${reason}`, { cause: err });
112
+ }
113
+ if (!response.ok) {
114
+ throw new Error(`Could not download ${url.href}: ${response.status} ${response.statusText}`);
115
+ }
116
+ if (response.body == null) {
117
+ throw new Error(`Empty response from ${url.href}`);
118
+ }
119
+ return response;
120
+ }
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Turns what the user asked for into a concrete pnpm version.
3
+ *
4
+ * `spec` may be a dist-tag (`latest`, `next-12`), an exact version (`11.20.0`,
5
+ * with an optional leading `v`), or a bare major (`12`), which picks that
6
+ * major's stable release and falls back to its prerelease lane.
7
+ *
8
+ * Dist-tags win over exact versions, matching `install.ps1`.
9
+ *
10
+ * @throws if `spec` matches neither a dist-tag nor a published version.
11
+ */
12
+ export function resolveVersion(packument, spec) {
13
+ const distTags = packument['dist-tags'];
14
+ if (distTags[spec])
15
+ return distTags[spec];
16
+ const version = spec.startsWith('v') ? spec.slice(1) : spec;
17
+ if (packument.versions[version])
18
+ return version;
19
+ if (/^\d+$/.test(version)) {
20
+ const majorTag = distTags[`latest-${version}`] ?? distTags[`next-${version}`];
21
+ if (majorTag)
22
+ return majorTag;
23
+ }
24
+ throw new Error(`Sorry! pnpm version "${spec}" could not be found. Available tags: ${Object.keys(distTags).sort().join(', ')}`);
25
+ }
26
+ /** The major of an exact version, as the package layout depends on it. */
27
+ export function majorVersion(version) {
28
+ const field = version.split('.')[0] ?? '';
29
+ // Digits only: `Number` reads '' as 0 and '0x10' as 16, and a caller-supplied
30
+ // version that means neither should say so here rather than fail later as a
31
+ // package name nobody publishes.
32
+ if (!/^\d+$/.test(field)) {
33
+ throw new Error(`Could not read a major version from "${version}".`);
34
+ }
35
+ return Number(field);
36
+ }
@@ -0,0 +1,45 @@
1
+ import fs from 'node:fs';
2
+ const CHUNK_SIZE = 64 * 1024;
3
+ /**
4
+ * Whether two paths hold the same bytes.
5
+ *
6
+ * Compared rather than hashed: the answer is usually "no" at the first
7
+ * differing byte, and there is nothing to gain from reading further.
8
+ *
9
+ * `b` is the untrusted side — a path something else may hold — so anything
10
+ * other than a readable regular file of the same size is simply not the same
11
+ * file, rather than an error.
12
+ */
13
+ export function sameFileContents(a, b) {
14
+ const sizeA = fs.statSync(a).size;
15
+ const statB = fs.lstatSync(b, { throwIfNoEntry: false });
16
+ if (statB?.isFile() !== true || statB.size !== sizeA)
17
+ return false;
18
+ let fdA;
19
+ let fdB;
20
+ try {
21
+ fdA = fs.openSync(a, 'r');
22
+ fdB = fs.openSync(b, 'r');
23
+ const bufferA = Buffer.alloc(CHUNK_SIZE);
24
+ const bufferB = Buffer.alloc(CHUNK_SIZE);
25
+ while (true) {
26
+ const readA = fs.readSync(fdA, bufferA, 0, CHUNK_SIZE, null);
27
+ const readB = fs.readSync(fdB, bufferB, 0, CHUNK_SIZE, null);
28
+ if (readA !== readB)
29
+ return false;
30
+ if (readA === 0)
31
+ return true;
32
+ if (!bufferA.subarray(0, readA).equals(bufferB.subarray(0, readB)))
33
+ return false;
34
+ }
35
+ }
36
+ catch {
37
+ return false;
38
+ }
39
+ finally {
40
+ if (fdA !== undefined)
41
+ fs.closeSync(fdA);
42
+ if (fdB !== undefined)
43
+ fs.closeSync(fdB);
44
+ }
45
+ }