pnpm 12.15.0 → 12.16.0

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 (47) hide show
  1. package/CHANGELOG.md +189 -0
  2. package/README.md +4 -4
  3. package/bin/pnpm.mjs +11 -233
  4. package/dist/node_modules/.pnpm-workspace-state-v1.json +97 -269
  5. package/dist/node_modules/confbox/LICENSE +27 -0
  6. package/dist/node_modules/confbox/dist/_chunks/_format.d.mts +22 -0
  7. package/dist/node_modules/confbox/dist/_chunks/_format.mjs +1 -0
  8. package/dist/node_modules/confbox/dist/_chunks/libs/detect-indent.mjs +1 -0
  9. package/dist/node_modules/confbox/dist/_chunks/libs/ini.mjs +3 -0
  10. package/dist/node_modules/confbox/dist/_chunks/libs/js-yaml.d.mts +33 -0
  11. package/dist/node_modules/confbox/dist/_chunks/libs/js-yaml.mjs +38 -0
  12. package/dist/node_modules/confbox/dist/_chunks/libs/json5.mjs +14 -0
  13. package/dist/node_modules/confbox/dist/_chunks/libs/smol-toml.mjs +11 -0
  14. package/dist/node_modules/confbox/dist/_chunks/libs/strip-json-comments.mjs +4 -0
  15. package/dist/node_modules/confbox/dist/_chunks/rolldown-runtime.mjs +1 -0
  16. package/dist/node_modules/confbox/dist/index.d.mts +7 -0
  17. package/dist/node_modules/confbox/dist/index.mjs +1 -0
  18. package/dist/node_modules/confbox/dist/ini.d.mts +59 -0
  19. package/dist/node_modules/confbox/dist/ini.mjs +1 -0
  20. package/dist/node_modules/confbox/dist/json.d.mts +25 -0
  21. package/dist/node_modules/confbox/dist/json.mjs +1 -0
  22. package/dist/node_modules/confbox/dist/json5.d.mts +55 -0
  23. package/dist/node_modules/confbox/dist/json5.mjs +1 -0
  24. package/dist/node_modules/confbox/dist/jsonc.d.mts +29 -0
  25. package/dist/node_modules/confbox/dist/jsonc.mjs +1 -0
  26. package/dist/node_modules/confbox/dist/toml.d.mts +20 -0
  27. package/dist/node_modules/confbox/dist/toml.mjs +1 -0
  28. package/dist/node_modules/confbox/dist/yaml.d.mts +101 -0
  29. package/dist/node_modules/confbox/dist/yaml.mjs +1 -0
  30. package/dist/node_modules/confbox/package.json +68 -0
  31. package/dist/node_modules/get-pnpm/lib/corepack.js +79 -0
  32. package/dist/node_modules/get-pnpm/lib/downloadExecutable.js +10 -4
  33. package/dist/node_modules/get-pnpm/lib/extractTarballMember.js +45 -35
  34. package/dist/node_modules/get-pnpm/lib/index.js +3 -219
  35. package/dist/node_modules/get-pnpm/lib/install.js +293 -0
  36. package/dist/node_modules/get-pnpm/lib/npmConfig.js +280 -0
  37. package/dist/node_modules/get-pnpm/lib/npmSigningKeys.js +2 -6
  38. package/dist/node_modules/get-pnpm/lib/platformPackageName.js +11 -7
  39. package/dist/node_modules/get-pnpm/lib/pnpmConfig.js +297 -0
  40. package/dist/node_modules/get-pnpm/lib/projectPin.js +201 -0
  41. package/dist/node_modules/get-pnpm/lib/registry.js +265 -41
  42. package/dist/node_modules/get-pnpm/lib/resolveVersion.js +19 -2
  43. package/dist/node_modules/get-pnpm/lib/sameFileContents.js +31 -27
  44. package/dist/node_modules/get-pnpm/lib/shadowingPnpm.js +174 -0
  45. package/dist/node_modules/get-pnpm/lib/undoHint.js +12 -0
  46. package/dist/node_modules/get-pnpm/package.json +20 -14
  47. package/package.json +15 -15
@@ -0,0 +1,297 @@
1
+ import fs from 'node:fs';
2
+ import os from 'node:os';
3
+ import path from 'node:path';
4
+ import { parseYAML } from 'confbox/yaml';
5
+ import { certificateAuthorities, stripQuotes } from './npmConfig.js';
6
+ import { findUp } from './projectPin.js';
7
+ import { DEFAULT_REGISTRY, isUrlSecureForCredentials, normalizeRegistry } from './registry.js';
8
+ /**
9
+ * The registry to download pnpm from, with the credentials and CA certificates
10
+ * for it, read the way pnpm reads them for an `@pnpm` package.
11
+ *
12
+ * - Sources, highest first: the project `.npmrc` (the workspace root's, or else
13
+ * the one in `cwd`), then the user `.npmrc`. Only top-level settings count.
14
+ * - The registry is `@pnpm:registry`, then `registry` from the environment,
15
+ * then `registry`.
16
+ * - `${VAR}` placeholders are expanded in the user `.npmrc` only. A project
17
+ * `.npmrc` entry that has one is dropped, as pnpm drops it: a repository
18
+ * must not route the user's environment to a registry.
19
+ * - Credentials are those of the `@pnpm` scope, then the registry-wide ones,
20
+ * for the longest `//host[:port]/path/` prefix of the registry, then the
21
+ * same without the port.
22
+ */
23
+ export function pnpmRegistryConfig(opts) {
24
+ const env = opts.env ?? process.env;
25
+ const cwd = opts.cwd ?? process.cwd();
26
+ const sources = [
27
+ ...(opts.readProjectNpmrc ? [readNpmrc(path.join(projectDir(cwd), '.npmrc'), projectEntry)] : []),
28
+ readNpmrc(userNpmrcPath(env, opts.homeDir), (key, value) => [expandEnv(key, env), expandEnv(value, env)]),
29
+ ];
30
+ const settings = new Map([...sources].reverse().flatMap(({ entries }) => entries));
31
+ const registry = normalizeRegistry(settings.get('@pnpm:registry') || settingFromEnv(env, 'registry') || settings.get('registry') || DEFAULT_REGISTRY);
32
+ const headers = npmrcHeaders(settings, registry);
33
+ const ca = certificateAuthorities(registry, sources);
34
+ return {
35
+ registry,
36
+ ...(headers == null ? {} : { headers }),
37
+ ...(ca.length === 0 ? {} : { ca }),
38
+ };
39
+ }
40
+ /**
41
+ * The proxy to download pnpm through, resolved the way pnpm resolves it for
42
+ * `pnpm self-update` and the download of the version a project pins.
43
+ *
44
+ * - Each of `httpsProxy`, `httpProxy`, `noProxy`, `proxy`, and `noproxy`
45
+ * comes from the first source that names it, highest first: the
46
+ * `PNPM_CONFIG_*` variables, then the global `config.yaml`, then the user
47
+ * `.npmrc`. The project's `.npmrc` and `pnpm-workspace.yaml` do not count.
48
+ * - An empty, `false`, or `null` value reads as unset. `proxy` then stands in
49
+ * for `httpsProxy`, and `noproxy` for `noProxy`, and after them
50
+ * `HTTPS_PROXY`, `HTTP_PROXY`, `PROXY`, and `NO_PROXY` apply.
51
+ * - The HTTP proxy defaults to the HTTPS one. `proxy` set to `false` turns
52
+ * both off, the environment included.
53
+ *
54
+ * @throws if the global `config.yaml` cannot be read or parsed, as pnpm does.
55
+ */
56
+ export function pnpmProxyConfig(opts = {}) {
57
+ const env = opts.env ?? process.env;
58
+ const npmrc = new Map(readNpmrc(userNpmrcPath(env, opts.homeDir), (key, value) => [expandEnv(key, env), expandEnv(value, env)]).entries);
59
+ const overlays = [
60
+ {
61
+ httpsProxy: pnpmSettingFromEnv(env, 'https_proxy'),
62
+ httpProxy: pnpmSettingFromEnv(env, 'http_proxy'),
63
+ noProxy: pnpmSettingFromEnv(env, 'no_proxy'),
64
+ noproxy: pnpmSettingFromEnv(env, 'noproxy'),
65
+ proxy: pnpmSettingFromEnv(env, 'proxy'),
66
+ },
67
+ readGlobalConfigProxySettings(opts, env),
68
+ {
69
+ httpsProxy: npmrc.get('https-proxy'),
70
+ httpProxy: npmrc.get('http-proxy'),
71
+ noProxy: npmrc.get('no-proxy'),
72
+ noproxy: npmrc.get('noproxy'),
73
+ proxy: npmrc.get('proxy'),
74
+ },
75
+ ];
76
+ const legacyProxy = overlaid(overlays, 'proxy');
77
+ const httpsProxySetting = proxyUrl(overlaid(overlays, 'httpsProxy')) ??
78
+ (legacyProxy === false || legacyProxy === 'false' ? false : proxyUrl(legacyProxy));
79
+ const httpsProxy = httpsProxySetting === false ? undefined : httpsProxySetting ?? envValue(env, 'https_proxy');
80
+ const httpProxy = proxyUrl(overlaid(overlays, 'httpProxy')) ??
81
+ (httpsProxySetting === false ? undefined : httpsProxy ?? envValue(env, 'http_proxy') ?? envValue(env, 'proxy'));
82
+ const noProxy = noProxyValue(overlaid(overlays, 'noProxy')) ?? noProxyValue(overlaid(overlays, 'noproxy')) ?? envValue(env, 'no_proxy');
83
+ return {
84
+ ...(httpsProxy == null ? {} : { HTTPS_PROXY: httpsProxy }),
85
+ ...(httpProxy == null ? {} : { HTTP_PROXY: httpProxy }),
86
+ ...(noProxy == null ? {} : { NO_PROXY: noProxy }),
87
+ };
88
+ }
89
+ function overlaid(overlays, key) {
90
+ return overlays.find((overlay) => overlay[key] !== undefined)?.[key];
91
+ }
92
+ function proxyUrl(value) {
93
+ return typeof value === 'string' && !UNSET_VALUES.has(value) ? value : undefined;
94
+ }
95
+ /** `true` bypasses the proxy for every host, which Node.js spells `*`. */
96
+ function noProxyValue(value) {
97
+ return value === true ? '*' : proxyUrl(value);
98
+ }
99
+ const UNSET_VALUES = new Set(['', 'false', 'null']);
100
+ function pnpmSettingFromEnv(env, key) {
101
+ return env[`pnpm_config_${key}`] ?? env[`PNPM_CONFIG_${key.toUpperCase()}`];
102
+ }
103
+ function envValue(env, key) {
104
+ return env[key] ?? env[key.toUpperCase()];
105
+ }
106
+ function readGlobalConfigProxySettings(opts, env) {
107
+ const file = path.join(pnpmConfigDir(opts, env), 'config.yaml');
108
+ let settings;
109
+ try {
110
+ settings = parseYAML(fs.readFileSync(file, 'utf8'));
111
+ }
112
+ catch (err) {
113
+ if (err.code === 'ENOENT')
114
+ return {};
115
+ throw new Error(`Could not read ${file}: ${err.message}`, { cause: err });
116
+ }
117
+ if (settings == null)
118
+ return {};
119
+ if (typeof settings !== 'object' || Array.isArray(settings)) {
120
+ throw new Error(`${file} does not hold a map of settings.`);
121
+ }
122
+ const { httpsProxy, httpProxy, noProxy, noproxy, proxy } = settings;
123
+ return {
124
+ httpsProxy: expandEnvIn(httpsProxy, env),
125
+ httpProxy: expandEnvIn(httpProxy, env),
126
+ noProxy: expandEnvIn(noProxy, env),
127
+ noproxy: expandEnvIn(noproxy, env),
128
+ proxy: expandEnvIn(proxy, env),
129
+ };
130
+ }
131
+ /** A null value leaves the setting to a lower source, as pnpm reads the file. */
132
+ function expandEnvIn(value, env) {
133
+ if (value === null)
134
+ return undefined;
135
+ return typeof value === 'string' ? expandEnv(value, env) : value;
136
+ }
137
+ /** The directory pnpm keeps the global `config.yaml` in. */
138
+ function pnpmConfigDir(opts, env) {
139
+ const platform = opts.platform ?? process.platform;
140
+ const dirFromEnv = (name) => {
141
+ const value = env[name];
142
+ return value && platform === 'win32' ? expandWindowsEnv(name, value, env) : value || undefined;
143
+ };
144
+ const xdgConfigHome = dirFromEnv('XDG_CONFIG_HOME');
145
+ if (xdgConfigHome)
146
+ return path.join(xdgConfigHome, 'pnpm');
147
+ const homeDir = opts.homeDir ?? os.homedir();
148
+ if (platform === 'darwin')
149
+ return path.join(homeDir, 'Library/Preferences/pnpm');
150
+ const localAppData = platform === 'win32' ? dirFromEnv('LOCALAPPDATA') : undefined;
151
+ if (localAppData)
152
+ return path.join(localAppData, 'pnpm/config');
153
+ return path.join(homeDir, '.config/pnpm');
154
+ }
155
+ /** The longest value Windows allows in an environment variable. */
156
+ const MAX_WINDOWS_ENV_LENGTH = 32_767;
157
+ /**
158
+ * Expands the `%NAME%` references Windows leaves in a value it did not expand
159
+ * itself, as pnpm does before using the value as a directory.
160
+ *
161
+ * @throws if a reference names no variable, as pnpm does.
162
+ */
163
+ function expandWindowsEnv(name, value, env) {
164
+ const lookup = (varName) => {
165
+ const key = Object.keys(env).find((candidate) => candidate.toUpperCase() === varName.toUpperCase());
166
+ return key == null ? undefined : env[key];
167
+ };
168
+ let current = value;
169
+ // Bounded, because a variable can reference itself, even twice over.
170
+ for (let round = 0; round < 32 && current.includes('%'); round++) {
171
+ const next = current.replace(/%([^%]+)%/g, (reference, varName) => lookup(varName) ?? reference);
172
+ if (next === current || next.length > MAX_WINDOWS_ENV_LENGTH)
173
+ break;
174
+ current = next;
175
+ }
176
+ const unexpanded = /%[^%]+%/.exec(current);
177
+ if (unexpanded != null) {
178
+ throw new Error(`${name} contains an unexpanded environment variable: ${unexpanded[0]}`);
179
+ }
180
+ return current;
181
+ }
182
+ function settingFromEnv(env, key) {
183
+ const upper = key.toUpperCase();
184
+ return env[`pnpm_config_${key}`] || env[`PNPM_CONFIG_${upper}`] ||
185
+ env[`npm_config_${key}`] || env[`NPM_CONFIG_${upper}`] || undefined;
186
+ }
187
+ function userNpmrcPath(env, homeDir) {
188
+ return settingFromEnv(env, 'userconfig') ?? path.join(homeDir ?? os.homedir(), '.npmrc');
189
+ }
190
+ function projectEntry(key, value, file) {
191
+ if (!key.includes('${') && !value.includes('${'))
192
+ return [key, value];
193
+ if (key.startsWith('//') || key === 'registry' || key.endsWith(':registry')) {
194
+ console.warn(`Ignored ${key} from ${file}: a project .npmrc cannot use environment variables in registry settings or credentials.`);
195
+ }
196
+ return undefined;
197
+ }
198
+ function projectDir(cwd) {
199
+ const start = path.resolve(cwd);
200
+ return findUp(start, undefined, (dir) => fs.existsSync(path.join(dir, 'pnpm-workspace.yaml'))) ?? start;
201
+ }
202
+ function readNpmrc(file, parseEntry) {
203
+ const source = { dir: path.dirname(file), entries: [] };
204
+ for (const [key, value] of topLevelSettings(readNpmrcText(file))) {
205
+ const entry = parseEntry(key, value, file);
206
+ if (entry != null)
207
+ source.entries.push(entry);
208
+ }
209
+ return source;
210
+ }
211
+ function readNpmrcText(file) {
212
+ try {
213
+ return fs.readFileSync(file, 'utf8');
214
+ }
215
+ catch (err) {
216
+ const { code } = err;
217
+ if (code !== 'ENOENT' && code !== 'EISDIR') {
218
+ console.warn(`Could not read ${file}, so its settings are not used: ${err.message}`);
219
+ }
220
+ return '';
221
+ }
222
+ }
223
+ function topLevelSettings(text) {
224
+ const settings = [];
225
+ for (const rawLine of text.split(/\r?\n/)) {
226
+ const line = rawLine.trim();
227
+ // Settings under a `[section]` are not top-level ones.
228
+ if (line.startsWith('['))
229
+ break;
230
+ const separator = line.indexOf('=');
231
+ if (separator === -1 || line.startsWith(';') || line.startsWith('#'))
232
+ continue;
233
+ settings.push([line.slice(0, separator).trim(), stripQuotes(line.slice(separator + 1).trim())]);
234
+ }
235
+ return settings;
236
+ }
237
+ /**
238
+ * Replaces `${NAME}` with the variable, or with nothing when it is unset.
239
+ * `${NAME-fallback}` falls back when the variable is unset, and
240
+ * `${NAME:-fallback}` also when it is empty, as in pnpm.
241
+ */
242
+ function expandEnv(value, env) {
243
+ return value.replace(/\$\{([^${}]+)\}/g, (_match, expression) => {
244
+ const dash = expression.indexOf('-');
245
+ if (dash === -1)
246
+ return env[expression] ?? '';
247
+ const fallbackOnEmpty = expression[dash - 1] === ':';
248
+ const variable = env[expression.slice(0, fallbackOnEmpty ? dash - 1 : dash)];
249
+ const fallback = expression.slice(dash + 1);
250
+ return (fallbackOnEmpty ? variable || undefined : variable) ?? fallback;
251
+ });
252
+ }
253
+ function npmrcHeaders(settings, registry) {
254
+ const url = new URL(registry);
255
+ if (!isUrlSecureForCredentials(url))
256
+ return undefined;
257
+ const prefixes = registryPrefixes(url);
258
+ for (const scope of [':@pnpm', '']) {
259
+ for (const prefix of prefixes) {
260
+ const authorization = npmrcAuthorization(settings, `${prefix}${scope}`);
261
+ if (authorization != null)
262
+ return { authorization };
263
+ }
264
+ }
265
+ return undefined;
266
+ }
267
+ /** Each `//host[:port]/path/` prefix of `url`, longest first, then the same without the port. */
268
+ function registryPrefixes(url) {
269
+ const prefixes = [];
270
+ for (const host of new Set([url.host, url.hostname])) {
271
+ let pathname = url.pathname;
272
+ while (true) {
273
+ prefixes.push(`//${host}${pathname}`);
274
+ if (pathname === '/')
275
+ break;
276
+ pathname = pathname.slice(0, pathname.lastIndexOf('/', pathname.length - 2) + 1);
277
+ }
278
+ }
279
+ return prefixes;
280
+ }
281
+ function npmrcAuthorization(settings, prefix) {
282
+ const token = settings.get(`${prefix}:_authToken`);
283
+ if (token)
284
+ return `Bearer ${token}`;
285
+ const auth = settings.get(`${prefix}:_auth`);
286
+ if (auth)
287
+ return `Basic ${auth}`;
288
+ const username = settings.get(`${prefix}:username`);
289
+ const password = settings.get(`${prefix}:_password`);
290
+ if (username && password) {
291
+ return `Basic ${base64(`${username}:${Buffer.from(password, 'base64').toString('utf8')}`)}`;
292
+ }
293
+ return undefined;
294
+ }
295
+ function base64(text) {
296
+ return Buffer.from(text, 'utf8').toString('base64');
297
+ }
@@ -0,0 +1,201 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { parseJSON5 } from 'confbox/json5';
4
+ import { parseYAML } from 'confbox/yaml';
5
+ import picomatch from 'picomatch';
6
+ import semver from 'semver';
7
+ /** The basenames pnpm reads a project manifest from, in the order it tries them. */
8
+ const MANIFEST_NAMES = ['package.json', 'package.json5', 'package.yaml'];
9
+ /**
10
+ * The pnpm version that the project `dir` belongs to pins, read the way pnpm
11
+ * reads it to switch to that version.
12
+ *
13
+ * - The workspace is the nearest directory up from `dir` with a
14
+ * `pnpm-workspace.yaml`, and the project the nearest one with a manifest.
15
+ * - The workspace root and the projects its `packages` patterns match read
16
+ * the pin of the root's manifest. Any other project reads its own.
17
+ * - When that manifest pins nothing, the pin of the other one applies.
18
+ * - `devEngines.packageManager` takes precedence over `packageManager`, which
19
+ * pnpm reads only as an exact version.
20
+ * - A range pin comes with the version the lockfile already settled on, which
21
+ * is the one pnpm switches to while it stays in range. For the workspace,
22
+ * that lockfile is in `lockfileDir`, or else the root. Any other project
23
+ * reads its own. A pin taken from the other manifest has none.
24
+ *
25
+ * `opts.root` stops the search for both at that directory, such as the root
26
+ * of a checkout, which keeps a workspace that happens to sit above it out.
27
+ *
28
+ * @returns `undefined` when there is no pnpm pin to read: no manifest, or
29
+ * one that pins no package manager or another one.
30
+ * @throws if a manifest pins pnpm in a way pnpm itself would reject, or a
31
+ * manifest or `pnpm-workspace.yaml` cannot be parsed.
32
+ */
33
+ export function readProjectPin(dir, opts = {}) {
34
+ const start = path.resolve(dir);
35
+ const root = opts.root == null ? undefined : path.resolve(opts.root);
36
+ const workspaceDir = findUp(start, root, (candidate) => fs.existsSync(path.join(candidate, 'pnpm-workspace.yaml')));
37
+ const projectDir = findUp(start, root, (candidate) => MANIFEST_NAMES.some((name) => fs.existsSync(path.join(candidate, name))));
38
+ const sources = pinSources(workspaceDir, projectDir);
39
+ const primary = pinOf(readManifest(sources.primary.manifestDir));
40
+ if (primary == null)
41
+ return pinOf(readManifest(sources.fallbackManifestDir));
42
+ if (semver.valid(primary.spec) != null || sources.primary.lockfileDir == null)
43
+ return primary;
44
+ const lockedVersion = lockedPnpmVersion(sources.primary.lockfileDir, primary.spec);
45
+ return lockedVersion == null ? primary : { ...primary, lockedVersion };
46
+ }
47
+ function pinSources(workspaceDir, projectDir) {
48
+ if (workspaceDir == null)
49
+ return { primary: { manifestDir: projectDir, lockfileDir: projectDir } };
50
+ const workspace = readWorkspaceManifest(workspaceDir);
51
+ if (projectDir == null || isWorkspaceProject(workspaceDir, workspace, projectDir)) {
52
+ const lockfileDir = typeof workspace.lockfileDir === 'string' ? path.resolve(workspaceDir, workspace.lockfileDir) : workspaceDir;
53
+ return { primary: { manifestDir: workspaceDir, lockfileDir }, fallbackManifestDir: projectDir };
54
+ }
55
+ return { primary: { manifestDir: projectDir, lockfileDir: projectDir }, fallbackManifestDir: workspaceDir };
56
+ }
57
+ /**
58
+ * The top-level settings of `pnpm-workspace.yaml`. Settings that its
59
+ * `include` delegates to other files are not read, as pnpm does not read them
60
+ * before it switches.
61
+ */
62
+ function readWorkspaceManifest(workspaceDir) {
63
+ const file = path.join(workspaceDir, 'pnpm-workspace.yaml');
64
+ const value = parseYaml(file, fs.readFileSync(file, 'utf8'));
65
+ return isRecord(value) ? value : {};
66
+ }
67
+ /**
68
+ * Whether `projectDir` is the workspace root or a project its `packages`
69
+ * patterns match. Without `packages`, the root is the only one.
70
+ */
71
+ function isWorkspaceProject(workspaceDir, workspace, projectDir) {
72
+ const relative = path.relative(workspaceDir, projectDir).split(path.sep).join('/');
73
+ if (relative === '')
74
+ return true;
75
+ if (relative.startsWith('../') || relative.split('/').some((part) => part === 'node_modules' || part === 'bower_components'))
76
+ return false;
77
+ const patterns = Array.isArray(workspace.packages) ? workspace.packages.filter((pattern) => typeof pattern === 'string') : [];
78
+ const matches = (pattern) => picomatch.isMatch(relative, pattern.replace(/^\.\//, '').replace(/\/$/, ''));
79
+ const excludes = patterns.filter((pattern) => pattern.startsWith('!')).map((pattern) => pattern.slice(1));
80
+ return !excludes.some(matches) && patterns.filter((pattern) => !pattern.startsWith('!')).some(matches);
81
+ }
82
+ function readManifest(dir) {
83
+ if (dir == null)
84
+ return undefined;
85
+ for (const name of MANIFEST_NAMES) {
86
+ const manifestPath = path.join(dir, name);
87
+ const content = readOptionalFile(manifestPath);
88
+ if (content == null)
89
+ continue;
90
+ const value = parseManifest(manifestPath, content);
91
+ if (!isRecord(value))
92
+ throw new Error(`${manifestPath} does not hold an object.`);
93
+ return { path: manifestPath, value };
94
+ }
95
+ return undefined;
96
+ }
97
+ function parseManifest(file, content) {
98
+ if (file.endsWith('.yaml'))
99
+ return parseYaml(file, content);
100
+ try {
101
+ // Windows tools write a byte order mark, which pnpm reads past.
102
+ const text = content.replace(/^\uFEFF/, '');
103
+ return file.endsWith('.json5') ? parseJSON5(text) : JSON.parse(text);
104
+ }
105
+ catch (err) {
106
+ throw new Error(`${file} is not valid ${file.endsWith('.json5') ? 'JSON5' : 'JSON'}: ${err.message}`, { cause: err });
107
+ }
108
+ }
109
+ function parseYaml(file, content) {
110
+ try {
111
+ return parseYAML(content);
112
+ }
113
+ catch (err) {
114
+ throw new Error(`${file} is not valid YAML: ${err.message}`, { cause: err });
115
+ }
116
+ }
117
+ function pinOf(manifest) {
118
+ const wanted = manifest == null ? undefined : wantedPnpm(manifest);
119
+ return wanted == null ? undefined : { ...wanted, manifestPath: manifest.path };
120
+ }
121
+ function wantedPnpm(manifest) {
122
+ const engine = devEnginesPackageManager(manifest.value);
123
+ if (engine != null)
124
+ return engine.name === 'pnpm' ? devEnginesPin(manifest, engine.version) : undefined;
125
+ const { packageManager } = manifest.value;
126
+ return typeof packageManager === 'string' ? packageManagerPin(manifest, packageManager) : undefined;
127
+ }
128
+ function devEnginesPin(manifest, version) {
129
+ if (typeof version !== 'string' || semver.validRange(version) == null) {
130
+ throw new Error(`${manifest.path} names pnpm in devEngines.packageManager, but no version or range of it.`);
131
+ }
132
+ return { spec: version, field: 'devEngines.packageManager' };
133
+ }
134
+ function packageManagerPin(manifest, packageManager) {
135
+ // `pnpm@12.1.0+sha512.<hash>`: the build names the artifact Corepack downloaded,
136
+ // not a pnpm release.
137
+ const separator = packageManager.lastIndexOf('@');
138
+ if (separator <= 0 || packageManager.slice(0, separator) !== 'pnpm')
139
+ return undefined;
140
+ const version = packageManager.slice(separator + 1).split('+')[0];
141
+ if (semver.valid(version) !== version) {
142
+ throw new Error(`${manifest.path} sets packageManager to "${packageManager}", which pins no exact pnpm version.`);
143
+ }
144
+ return { spec: version, field: 'packageManager' };
145
+ }
146
+ /** The entry that governs pnpm: pnpm's own in a list, or else the first one. */
147
+ function devEnginesPackageManager(manifest) {
148
+ const declared = isRecord(manifest.devEngines) ? manifest.devEngines.packageManager : undefined;
149
+ if (!Array.isArray(declared))
150
+ return isRecord(declared) ? declared : undefined;
151
+ const entries = declared.filter(isRecord);
152
+ return entries.find((entry) => entry.name === 'pnpm') ?? entries[0];
153
+ }
154
+ function lockedPnpmVersion(lockfileDir, range) {
155
+ const lockfilePath = path.join(lockfileDir, 'pnpm-lock.yaml');
156
+ const content = readOptionalFile(lockfilePath);
157
+ const envDocument = content == null ? undefined : extractEnvDocument(content);
158
+ if (envDocument == null)
159
+ return undefined;
160
+ const locked = valueAt(parseYaml(lockfilePath, envDocument), ['importers', '.', 'packageManagerDependencies', 'pnpm', 'version']);
161
+ // In range the way pnpm checks a locked version, prereleases included.
162
+ if (typeof locked !== 'string' || semver.valid(locked) == null)
163
+ return undefined;
164
+ return semver.satisfies(locked, range, { includePrerelease: true }) ? locked : undefined;
165
+ }
166
+ /**
167
+ * The document that leads a pnpm 11+ lockfile and records the package
168
+ * manager. Sliced out rather than parsed with the rest, which runs to
169
+ * megabytes in a large workspace.
170
+ */
171
+ function extractEnvDocument(content) {
172
+ const normalized = content.replace(/^\uFEFF/, '').replaceAll('\r\n', '\n');
173
+ if (!normalized.startsWith('---\n'))
174
+ return undefined;
175
+ const end = normalized.indexOf('\n---\n', 4);
176
+ return end === -1 ? normalized.slice(4).replace(/\n---\n?$/, '') : normalized.slice(4, end);
177
+ }
178
+ function valueAt(value, keys) {
179
+ return keys.reduce((current, key) => isRecord(current) ? current[key] : undefined, value);
180
+ }
181
+ export function findUp(start, root, matches) {
182
+ for (let dir = start;; dir = path.dirname(dir)) {
183
+ if (matches(dir))
184
+ return dir;
185
+ if (dir === root || path.dirname(dir) === dir)
186
+ return undefined;
187
+ }
188
+ }
189
+ function readOptionalFile(file) {
190
+ try {
191
+ return fs.readFileSync(file, 'utf8');
192
+ }
193
+ catch (err) {
194
+ if (err.code === 'ENOENT')
195
+ return undefined;
196
+ throw err;
197
+ }
198
+ }
199
+ function isRecord(value) {
200
+ return typeof value === 'object' && value != null && !Array.isArray(value);
201
+ }