pnpm-settings-migrator 0.8.1 → 0.9.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.
- package/README.md +33 -16
- package/dist/cli.mjs +1096 -166
- package/dist/index.d.mts +48 -12
- package/dist/index.mjs +1086 -166
- package/package.json +7 -5
package/dist/cli.mjs
CHANGED
|
@@ -1,26 +1,29 @@
|
|
|
1
1
|
import process from "node:process";
|
|
2
2
|
import consola$1, { consola } from "consola";
|
|
3
3
|
import { cac } from "cac";
|
|
4
|
-
import { basename, dirname, join, resolve } from "pathe";
|
|
4
|
+
import { basename, dirname, join, relative, resolve } from "pathe";
|
|
5
|
+
import camelcaseKeys from "camelcase-keys";
|
|
5
6
|
import { randomUUID } from "node:crypto";
|
|
6
7
|
import { access, chmod, readFile, rename, rm, stat, writeFile } from "node:fs/promises";
|
|
7
8
|
import { fileURLToPath } from "node:url";
|
|
8
9
|
import { getColor } from "consola/utils";
|
|
9
10
|
import { isPlainObject, isUndefined } from "@ntnyq/utils";
|
|
10
11
|
import { defu } from "defu";
|
|
11
|
-
import camelcaseKeys from "camelcase-keys";
|
|
12
12
|
import { readIniFile } from "read-ini-file";
|
|
13
13
|
import { kebabCase } from "uncase";
|
|
14
|
-
import {
|
|
15
|
-
import { Document, isMap, isNode, isScalar, isSeq, parseDocument, stringify } from "yaml";
|
|
14
|
+
import { glob } from "tinyglobby";
|
|
16
15
|
import detectIndent from "detect-indent";
|
|
16
|
+
import { Document, isMap, isNode, isScalar, isSeq, parseDocument, stringify } from "yaml";
|
|
17
|
+
import { isDeepStrictEqual } from "node:util";
|
|
17
18
|
//#region package.json
|
|
18
19
|
var name = "pnpm-settings-migrator";
|
|
19
|
-
var version = "0.
|
|
20
|
+
var version = "0.9.0";
|
|
20
21
|
//#endregion
|
|
21
22
|
//#region src/cli-options.ts
|
|
22
23
|
/**
|
|
23
24
|
* Create the command-line parser shared by the executable and parser tests.
|
|
25
|
+
*
|
|
26
|
+
* @returns Configured command-line parser
|
|
24
27
|
*/
|
|
25
28
|
function createCli() {
|
|
26
29
|
const cli = cac(name).version(version).option("--cwd <cwd>", "Current working directory").option("--sort-keys", "Sort keys when write pnpm-workspace.yaml").option("--compatibility <compatibility>", "Compatibility target (auto, v10, v11, v12)").option("--replace-deprecated", "Replace deprecated pnpm settings with new ones and remove old keys").option("--strategy <strategy>", "Strategy to handle conflicts (discard, merge, overwrite)").option("--no-yarn-resolutions", "Disable migrating resolutions field in package.json").option("--no-newline-between", "Disable adding newlines between each root keys").option("--no-show-changes", "Disable showing settings changes").option("--no-clean-npmrc", "Disable removing pnpm settings in .npmrc file").option("--no-clean-package-json", "Disable removing pnpm fields in package.json");
|
|
@@ -202,78 +205,6 @@ const PNPM_V11_REMOVED_SETTINGS = [
|
|
|
202
205
|
"useNodeVersion"
|
|
203
206
|
];
|
|
204
207
|
//#endregion
|
|
205
|
-
//#region src/options.ts
|
|
206
|
-
/**
|
|
207
|
-
* Default values for migration options.
|
|
208
|
-
*/
|
|
209
|
-
const DEFAULT_OPTIONS = {
|
|
210
|
-
cleanNpmrc: true,
|
|
211
|
-
cleanPackageJson: true,
|
|
212
|
-
compatibility: "auto",
|
|
213
|
-
newlineBetween: true,
|
|
214
|
-
replaceDeprecated: false,
|
|
215
|
-
showChanges: true,
|
|
216
|
-
sortKeys: false,
|
|
217
|
-
strategy: "merge",
|
|
218
|
-
yarnResolutions: true
|
|
219
|
-
};
|
|
220
|
-
const VALID_STRATEGIES = [
|
|
221
|
-
"discard",
|
|
222
|
-
"merge",
|
|
223
|
-
"overwrite"
|
|
224
|
-
];
|
|
225
|
-
const VALID_COMPATIBILITIES = [
|
|
226
|
-
"auto",
|
|
227
|
-
"v10",
|
|
228
|
-
"v11",
|
|
229
|
-
"v12"
|
|
230
|
-
];
|
|
231
|
-
function resolveCompatibility(compatibility) {
|
|
232
|
-
if (!compatibility) return DEFAULT_OPTIONS.compatibility;
|
|
233
|
-
if (VALID_COMPATIBILITIES.includes(compatibility)) return compatibility;
|
|
234
|
-
throw new Error(`Invalid compatibility: ${compatibility}. Expected one of: ${VALID_COMPATIBILITIES.join(", ")}`);
|
|
235
|
-
}
|
|
236
|
-
function resolveStrategy(strategy) {
|
|
237
|
-
if (!strategy) return DEFAULT_OPTIONS.strategy;
|
|
238
|
-
if (VALID_STRATEGIES.includes(strategy)) return strategy;
|
|
239
|
-
throw new Error(`Invalid strategy: ${strategy}. Expected one of: ${VALID_STRATEGIES.join(", ")}`);
|
|
240
|
-
}
|
|
241
|
-
/**
|
|
242
|
-
* Resolve and normalize migration options with defaults.
|
|
243
|
-
*
|
|
244
|
-
* This function takes partial options and returns a complete options object
|
|
245
|
-
* with all properties set to either the provided value or the default value.
|
|
246
|
-
*
|
|
247
|
-
* @param options - Partial migration options
|
|
248
|
-
*
|
|
249
|
-
* @returns Complete options object with all required properties
|
|
250
|
-
*
|
|
251
|
-
* @example
|
|
252
|
-
* ```ts
|
|
253
|
-
* // Use all defaults
|
|
254
|
-
* const opts = resolveOptions()
|
|
255
|
-
* // { cleanNpmrc: true, cleanPackageJson: true, cwd: '/current/dir', ... }
|
|
256
|
-
*
|
|
257
|
-
* // Override specific options
|
|
258
|
-
* const opts = resolveOptions({ sortKeys: true, cleanNpmrc: false })
|
|
259
|
-
* // { cleanNpmrc: false, cleanPackageJson: true, sortKeys: true, ... }
|
|
260
|
-
* ```
|
|
261
|
-
*/
|
|
262
|
-
function resolveOptions(options = {}) {
|
|
263
|
-
return {
|
|
264
|
-
cleanNpmrc: options.cleanNpmrc ?? DEFAULT_OPTIONS.cleanNpmrc,
|
|
265
|
-
compatibility: resolveCompatibility(options.compatibility),
|
|
266
|
-
cwd: options.cwd ?? process.cwd(),
|
|
267
|
-
newlineBetween: options.newlineBetween ?? DEFAULT_OPTIONS.newlineBetween,
|
|
268
|
-
showChanges: options.showChanges ?? DEFAULT_OPTIONS.showChanges,
|
|
269
|
-
sortKeys: options.sortKeys ?? DEFAULT_OPTIONS.sortKeys,
|
|
270
|
-
strategy: resolveStrategy(options.strategy),
|
|
271
|
-
yarnResolutions: options.yarnResolutions ?? DEFAULT_OPTIONS.yarnResolutions,
|
|
272
|
-
cleanPackageJson: options.cleanPackageJson ?? DEFAULT_OPTIONS.cleanPackageJson,
|
|
273
|
-
replaceDeprecated: options.replaceDeprecated ?? DEFAULT_OPTIONS.replaceDeprecated
|
|
274
|
-
};
|
|
275
|
-
}
|
|
276
|
-
//#endregion
|
|
277
208
|
//#region src/utils/fs.ts
|
|
278
209
|
/**
|
|
279
210
|
* Modulus that isolates Unix permission bits from a stat mode.
|
|
@@ -281,6 +212,10 @@ function resolveOptions(options = {}) {
|
|
|
281
212
|
const FILE_PERMISSION_MODULUS = 512;
|
|
282
213
|
/**
|
|
283
214
|
* Resolve a filesystem path to a string suitable for a sibling temp file.
|
|
215
|
+
*
|
|
216
|
+
* @param path - Filesystem path to normalize
|
|
217
|
+
*
|
|
218
|
+
* @returns String path suitable for path manipulation
|
|
284
219
|
*/
|
|
285
220
|
function resolvePathString(path) {
|
|
286
221
|
if (path instanceof URL) return fileURLToPath(path);
|
|
@@ -288,6 +223,10 @@ function resolvePathString(path) {
|
|
|
288
223
|
}
|
|
289
224
|
/**
|
|
290
225
|
* Read an existing file's permission bits, if the file exists.
|
|
226
|
+
*
|
|
227
|
+
* @param path - Filesystem path to inspect
|
|
228
|
+
*
|
|
229
|
+
* @returns Unix permission bits, or `undefined` when the file does not exist
|
|
291
230
|
*/
|
|
292
231
|
async function resolveExistingMode(path) {
|
|
293
232
|
try {
|
|
@@ -354,28 +293,190 @@ async function fsWriteFile(path, content) {
|
|
|
354
293
|
//#region src/utils/color.ts
|
|
355
294
|
/**
|
|
356
295
|
* Apply dim terminal styling to a value.
|
|
296
|
+
*
|
|
297
|
+
* @param text - Text or number to style
|
|
298
|
+
*
|
|
299
|
+
* @returns ANSI-styled string
|
|
357
300
|
*/
|
|
358
301
|
const dim = getColor("dim");
|
|
359
302
|
/**
|
|
360
303
|
* Apply green terminal styling to a value.
|
|
304
|
+
*
|
|
305
|
+
* @param text - Text or number to style
|
|
306
|
+
*
|
|
307
|
+
* @returns ANSI-styled string
|
|
361
308
|
*/
|
|
362
309
|
const green = getColor("green");
|
|
363
310
|
/**
|
|
364
311
|
* Apply red terminal styling to a value.
|
|
312
|
+
*
|
|
313
|
+
* @param text - Text or number to style
|
|
314
|
+
*
|
|
315
|
+
* @returns ANSI-styled string
|
|
365
316
|
*/
|
|
366
317
|
const red = getColor("red");
|
|
367
318
|
/**
|
|
368
319
|
* Apply bold terminal styling to a value.
|
|
320
|
+
*
|
|
321
|
+
* @param text - Text or number to style
|
|
322
|
+
*
|
|
323
|
+
* @returns ANSI-styled string
|
|
369
324
|
*/
|
|
370
325
|
const bold = getColor("bold");
|
|
371
326
|
/**
|
|
372
327
|
* Apply magenta terminal styling to a value.
|
|
328
|
+
*
|
|
329
|
+
* @param text - Text or number to style
|
|
330
|
+
*
|
|
331
|
+
* @returns ANSI-styled string
|
|
373
332
|
*/
|
|
374
333
|
const magenta = getColor("magenta");
|
|
375
334
|
//#endregion
|
|
335
|
+
//#region src/utils/deprecated-settings.ts
|
|
336
|
+
/**
|
|
337
|
+
* Replace deprecated audit fields with the structured `audit` setting.
|
|
338
|
+
*
|
|
339
|
+
* @param settings - Workspace settings to normalize in place
|
|
340
|
+
*/
|
|
341
|
+
function normalizeAuditSettings(settings) {
|
|
342
|
+
const level = settings.audit?.level ?? settings.auditLevel;
|
|
343
|
+
const ignore = settings.audit?.ignore ?? settings.auditConfig?.ignoreGhsas;
|
|
344
|
+
if (level !== void 0 || ignore !== void 0) settings.audit = {
|
|
345
|
+
...settings.audit,
|
|
346
|
+
...level === void 0 ? {} : { level },
|
|
347
|
+
...ignore === void 0 ? {} : { ignore }
|
|
348
|
+
};
|
|
349
|
+
Reflect.deleteProperty(settings, "auditConfig");
|
|
350
|
+
Reflect.deleteProperty(settings, "auditLevel");
|
|
351
|
+
}
|
|
352
|
+
/**
|
|
353
|
+
* Replace deprecated update fields with the structured `update` setting.
|
|
354
|
+
*
|
|
355
|
+
* @param settings - Workspace settings to normalize in place
|
|
356
|
+
*/
|
|
357
|
+
function normalizeUpdateSettings(settings) {
|
|
358
|
+
const { updateConfig } = settings;
|
|
359
|
+
if (!updateConfig) return;
|
|
360
|
+
settings.update = {
|
|
361
|
+
...updateConfig.ignoreDependencies === void 0 ? {} : { ignoreDeps: updateConfig.ignoreDependencies },
|
|
362
|
+
...updateConfig.changeset === void 0 ? {} : { changeset: updateConfig.changeset },
|
|
363
|
+
...updateConfig.githubActions === void 0 ? {} : { githubActions: updateConfig.githubActions },
|
|
364
|
+
...updateConfig.githubActionsServer === void 0 ? {} : { githubActionsServer: updateConfig.githubActionsServer },
|
|
365
|
+
...settings.update
|
|
366
|
+
};
|
|
367
|
+
Reflect.deleteProperty(settings, "updateConfig");
|
|
368
|
+
}
|
|
369
|
+
/**
|
|
370
|
+
* Replace former scalar aliases with their canonical v11+ settings.
|
|
371
|
+
*
|
|
372
|
+
* @param settings - Workspace settings to normalize in place
|
|
373
|
+
*/
|
|
374
|
+
function normalizeScalarAliases(settings) {
|
|
375
|
+
if (settings.cleanupUnusedCatalogs !== void 0) {
|
|
376
|
+
settings.catalogPrune ??= settings.cleanupUnusedCatalogs;
|
|
377
|
+
Reflect.deleteProperty(settings, "cleanupUnusedCatalogs");
|
|
378
|
+
}
|
|
379
|
+
if (settings.enableGlobalVirtualStore !== void 0) {
|
|
380
|
+
settings.virtualStoreType ??= settings.enableGlobalVirtualStore ? "global" : "project";
|
|
381
|
+
Reflect.deleteProperty(settings, "enableGlobalVirtualStore");
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
/**
|
|
385
|
+
* Replace the older side-effects cache spellings with one structured value.
|
|
386
|
+
*
|
|
387
|
+
* @param settings - Workspace settings to normalize in place
|
|
388
|
+
*/
|
|
389
|
+
function normalizeSideEffectsCache(settings) {
|
|
390
|
+
const declared = settings.sideEffectsCache;
|
|
391
|
+
const readonly = settings.sideEffectsCacheReadonly;
|
|
392
|
+
const remote = settings.remoteSideEffectsCache;
|
|
393
|
+
if (readonly === void 0 && remote === void 0) return;
|
|
394
|
+
const structured = typeof declared === "object" && declared !== null ? declared : void 0;
|
|
395
|
+
const shorthand = typeof declared === "boolean" ? declared : void 0;
|
|
396
|
+
settings.sideEffectsCache = {
|
|
397
|
+
...shorthand === void 0 ? {} : {
|
|
398
|
+
read: readonly === true ? true : shorthand,
|
|
399
|
+
write: readonly === true ? false : shorthand
|
|
400
|
+
},
|
|
401
|
+
...readonly === void 0 || shorthand !== void 0 ? {} : {
|
|
402
|
+
read: readonly,
|
|
403
|
+
...readonly ? { write: false } : {}
|
|
404
|
+
},
|
|
405
|
+
...remote === void 0 ? {} : { remote },
|
|
406
|
+
...structured
|
|
407
|
+
};
|
|
408
|
+
Reflect.deleteProperty(settings, "remoteSideEffectsCache");
|
|
409
|
+
Reflect.deleteProperty(settings, "sideEffectsCacheReadonly");
|
|
410
|
+
}
|
|
411
|
+
function addRegistryScope(declarations, url, scope) {
|
|
412
|
+
declarations[url] ??= {};
|
|
413
|
+
const declaration = declarations[url];
|
|
414
|
+
declaration.scopes = [.../* @__PURE__ */ new Set([...declaration.scopes ?? [], scope])];
|
|
415
|
+
}
|
|
416
|
+
/**
|
|
417
|
+
* Replace `namedRegistries` with URL-keyed registry declarations.
|
|
418
|
+
*
|
|
419
|
+
* A URL can expose only one canonical prefix. Conflicting legacy aliases are
|
|
420
|
+
* therefore retained for manual resolution instead of being discarded.
|
|
421
|
+
*
|
|
422
|
+
* @param settings - Workspace settings to normalize in place
|
|
423
|
+
* @param warnings - Collection that receives unresolved conflict warnings
|
|
424
|
+
*/
|
|
425
|
+
function normalizeNamedRegistries(settings, warnings) {
|
|
426
|
+
const { namedRegistries } = settings;
|
|
427
|
+
if (!namedRegistries) return;
|
|
428
|
+
const prefixesByUrl = /* @__PURE__ */ new Map();
|
|
429
|
+
for (const [prefix, url] of Object.entries(namedRegistries)) {
|
|
430
|
+
const prefixes = prefixesByUrl.get(url) ?? [];
|
|
431
|
+
prefixes.push(prefix);
|
|
432
|
+
prefixesByUrl.set(url, prefixes);
|
|
433
|
+
}
|
|
434
|
+
const conflicts = [...prefixesByUrl.entries()].filter(([, prefixes]) => prefixes.length > 1);
|
|
435
|
+
if (conflicts.length) {
|
|
436
|
+
warnings.push(`namedRegistries was kept because the new registries format supports one prefix per URL: ${conflicts.map(([url, prefixes]) => `${url} (${prefixes.join(", ")})`).join("; ")}.`);
|
|
437
|
+
return;
|
|
438
|
+
}
|
|
439
|
+
const declarations = {};
|
|
440
|
+
for (const [key, value] of Object.entries(settings.registries ?? {})) if (typeof value === "string") addRegistryScope(declarations, value, key === "default" ? "@" : key);
|
|
441
|
+
else declarations[key] = { ...value };
|
|
442
|
+
for (const [url, [prefix]] of prefixesByUrl) {
|
|
443
|
+
declarations[url] ??= {};
|
|
444
|
+
const declaration = declarations[url];
|
|
445
|
+
if (declaration.prefix && declaration.prefix !== prefix) {
|
|
446
|
+
warnings.push(`namedRegistries was kept because ${url} already declares prefix ${declaration.prefix}.`);
|
|
447
|
+
return;
|
|
448
|
+
}
|
|
449
|
+
declaration.prefix = prefix;
|
|
450
|
+
}
|
|
451
|
+
settings.registries = declarations;
|
|
452
|
+
Reflect.deleteProperty(settings, "namedRegistries");
|
|
453
|
+
}
|
|
454
|
+
/**
|
|
455
|
+
* Replace aliases that remain accepted but are no longer canonical.
|
|
456
|
+
*
|
|
457
|
+
* @param settings - Workspace settings to normalize in place
|
|
458
|
+
* @param warnings - Collection that receives unresolved conflict warnings
|
|
459
|
+
*
|
|
460
|
+
* @returns Nothing; settings and warnings are updated in place
|
|
461
|
+
*/
|
|
462
|
+
function normalizeCurrentAliases(settings, warnings) {
|
|
463
|
+
normalizeAuditSettings(settings);
|
|
464
|
+
normalizeUpdateSettings(settings);
|
|
465
|
+
normalizeScalarAliases(settings);
|
|
466
|
+
normalizeSideEffectsCache(settings);
|
|
467
|
+
normalizeNamedRegistries(settings, warnings);
|
|
468
|
+
}
|
|
469
|
+
//#endregion
|
|
376
470
|
//#region src/utils/legacy-build-settings.ts
|
|
377
471
|
/**
|
|
378
472
|
* Read packages listed by the legacy `onlyBuiltDependenciesFile` setting.
|
|
473
|
+
*
|
|
474
|
+
* @param cwd - Workspace root directory
|
|
475
|
+
* @param file - Path declared by the legacy setting
|
|
476
|
+
*
|
|
477
|
+
* @returns Validated package names, or an empty list when no file is declared
|
|
478
|
+
*
|
|
479
|
+
* @throws {TypeError} When the file does not contain an array of package names
|
|
379
480
|
*/
|
|
380
481
|
async function readOnlyBuiltDependenciesFile(cwd, file) {
|
|
381
482
|
if (!file) return [];
|
|
@@ -387,6 +488,13 @@ async function readOnlyBuiltDependenciesFile(cwd, file) {
|
|
|
387
488
|
/**
|
|
388
489
|
* Read and validate one legacy build dependency list. Configuration files are
|
|
389
490
|
* external input, so their runtime shape can differ from its declared type.
|
|
491
|
+
*
|
|
492
|
+
* @param settings - Workspace settings containing the legacy list
|
|
493
|
+
* @param key - Legacy setting name to read
|
|
494
|
+
*
|
|
495
|
+
* @returns Validated package names, or an empty list when the setting is absent
|
|
496
|
+
*
|
|
497
|
+
* @throws {TypeError} When the setting is not an array of package names
|
|
390
498
|
*/
|
|
391
499
|
function readLegacyBuildDependencyList(settings, key) {
|
|
392
500
|
const value = settings[key];
|
|
@@ -399,6 +507,11 @@ function readLegacyBuildDependencyList(settings, key) {
|
|
|
399
507
|
}
|
|
400
508
|
/**
|
|
401
509
|
* Build an `allowBuilds` map from legacy build-script settings.
|
|
510
|
+
*
|
|
511
|
+
* @param incomingSettings - Legacy settings to read and normalize
|
|
512
|
+
* @param cwd - Workspace root used to resolve dependency list files
|
|
513
|
+
*
|
|
514
|
+
* @returns Package permissions, or `undefined` when no legacy values exist
|
|
402
515
|
*/
|
|
403
516
|
async function collectAllowBuildsFromLegacy(incomingSettings, cwd) {
|
|
404
517
|
const allowBuilds = {};
|
|
@@ -499,6 +612,7 @@ async function normalizeIncomingSettings(incomingSettings, options) {
|
|
|
499
612
|
const pmOnFail = resolvePmOnFail(incomingSettings);
|
|
500
613
|
if (pmOnFail) incomingSettings.pmOnFail = pmOnFail;
|
|
501
614
|
normalizeAuditConfig(incomingSettings, warnings);
|
|
615
|
+
if (replaceDeprecated) normalizeCurrentAliases(incomingSettings, warnings);
|
|
502
616
|
const runtimeVersion = resolveRuntimeVersion(incomingSettings, warnings);
|
|
503
617
|
if (incomingSettings.ignoreDepScripts !== void 0) warnings.push("ignoreDepScripts was removed in pnpm v11 and has no direct replacement.");
|
|
504
618
|
if (incomingSettings.ignorePatchFailures !== void 0) warnings.push("ignorePatchFailures was removed in pnpm v11; patch failures now always throw.");
|
|
@@ -559,6 +673,10 @@ const PNPM_V11_MAJOR = 11;
|
|
|
559
673
|
const PNPM_V12_MAJOR = 12;
|
|
560
674
|
/**
|
|
561
675
|
* Resolve a pnpm major from a package manager declaration.
|
|
676
|
+
*
|
|
677
|
+
* @param hint - Package manager declaration to inspect
|
|
678
|
+
*
|
|
679
|
+
* @returns Detected pnpm major, or `undefined` when the hint does not match
|
|
562
680
|
*/
|
|
563
681
|
function resolvePnpmMajor(hint) {
|
|
564
682
|
const match = hint?.match(/^pnpm@[\s<=>^~]*(?<major>\d+)(?:\.|\s|$)/u);
|
|
@@ -566,6 +684,10 @@ function resolvePnpmMajor(hint) {
|
|
|
566
684
|
}
|
|
567
685
|
/**
|
|
568
686
|
* Normalize the single-or-array devEngines declaration.
|
|
687
|
+
*
|
|
688
|
+
* @param value - Optional single or multiple package manager declarations
|
|
689
|
+
*
|
|
690
|
+
* @returns Package manager declarations normalized to an array
|
|
569
691
|
*/
|
|
570
692
|
function normalizePackageManagers(value) {
|
|
571
693
|
if (Array.isArray(value)) return value;
|
|
@@ -664,6 +786,380 @@ function mergeByStrategy(existing, incoming, strategy) {
|
|
|
664
786
|
}
|
|
665
787
|
}
|
|
666
788
|
//#endregion
|
|
789
|
+
//#region src/settings-fields.ts
|
|
790
|
+
/**
|
|
791
|
+
* Settings that a project-level `pnpm-workspace.yaml` must never control.
|
|
792
|
+
*
|
|
793
|
+
* @see https://github.com/pnpm/pnpm/blob/main/pnpm11/config/reader/src/index.ts
|
|
794
|
+
* @see https://github.com/pnpm/pnpm/blob/v12.2.1/pnpm/crates/config/src/refused_keys.rs
|
|
795
|
+
*/
|
|
796
|
+
const PNPM_PROJECT_REFUSED_SETTINGS = [
|
|
797
|
+
"allProjects",
|
|
798
|
+
"allProjectsGraph",
|
|
799
|
+
"authConfig",
|
|
800
|
+
"bin",
|
|
801
|
+
"cliOptions",
|
|
802
|
+
"configByUri",
|
|
803
|
+
"configDir",
|
|
804
|
+
"explicitlySetKeys",
|
|
805
|
+
"finders",
|
|
806
|
+
"globalBinDir",
|
|
807
|
+
"globalDir",
|
|
808
|
+
"globalPkgDir",
|
|
809
|
+
"hooks",
|
|
810
|
+
"npmrcAuthFile",
|
|
811
|
+
"packageManager",
|
|
812
|
+
"packageManagerNetworkConfig",
|
|
813
|
+
"packageManagerRegistries",
|
|
814
|
+
"pnpmHomeDir",
|
|
815
|
+
"prodAllProjectsGraph",
|
|
816
|
+
"prodOnlySelectedProjectDirs",
|
|
817
|
+
"rootProjectManifest",
|
|
818
|
+
"rootProjectManifestDir",
|
|
819
|
+
"scope",
|
|
820
|
+
"selectedProjectsGraph",
|
|
821
|
+
"stateDir",
|
|
822
|
+
"userConfig",
|
|
823
|
+
"userconfig",
|
|
824
|
+
"wantedPackageManager",
|
|
825
|
+
"workspaceDir"
|
|
826
|
+
];
|
|
827
|
+
/**
|
|
828
|
+
* Settings added to, or recognized by, pnpm's v11 workspace config schema.
|
|
829
|
+
* Combined with the v10 settings below to form the complete v11 allowlist.
|
|
830
|
+
*
|
|
831
|
+
* @see https://github.com/pnpm/pnpm/blob/main/pnpm11/config/reader/src/types.ts
|
|
832
|
+
* @see https://github.com/pnpm/pnpm/blob/main/pnpm11/config/reader/src/unknownSettings.ts
|
|
833
|
+
*/
|
|
834
|
+
const PNPM_V11_ADDITIONAL_WORKSPACE_SETTINGS_FIELDS = [
|
|
835
|
+
"access",
|
|
836
|
+
"aggregateOutput",
|
|
837
|
+
"allowNew",
|
|
838
|
+
"allowSameVersion",
|
|
839
|
+
"audit",
|
|
840
|
+
"auditIgnorePrune",
|
|
841
|
+
"autoConfirmAllPrompts",
|
|
842
|
+
"bail",
|
|
843
|
+
"binLinks",
|
|
844
|
+
"catalog",
|
|
845
|
+
"catalogPrune",
|
|
846
|
+
"catalogs",
|
|
847
|
+
"changedFilesIgnorePattern",
|
|
848
|
+
"commitHooks",
|
|
849
|
+
"confirmModulesPurge",
|
|
850
|
+
"cpu",
|
|
851
|
+
"dedupePeers",
|
|
852
|
+
"depth",
|
|
853
|
+
"description",
|
|
854
|
+
"dev",
|
|
855
|
+
"dryRun",
|
|
856
|
+
"embedReadme",
|
|
857
|
+
"enablePnp",
|
|
858
|
+
"excludeLinksFromLockfile",
|
|
859
|
+
"extraBinPaths",
|
|
860
|
+
"extraEnv",
|
|
861
|
+
"fetchingConcurrency",
|
|
862
|
+
"filter",
|
|
863
|
+
"filterProd",
|
|
864
|
+
"force",
|
|
865
|
+
"frozenStore",
|
|
866
|
+
"git",
|
|
867
|
+
"gitTagVersion",
|
|
868
|
+
"global",
|
|
869
|
+
"globalPath",
|
|
870
|
+
"globalPrefix",
|
|
871
|
+
"hoistingLimits",
|
|
872
|
+
"ignoreCurrentSpecifiers",
|
|
873
|
+
"ignoreWorkspace",
|
|
874
|
+
"initAuthorEmail",
|
|
875
|
+
"initAuthorName",
|
|
876
|
+
"initAuthorUrl",
|
|
877
|
+
"initLicense",
|
|
878
|
+
"initPackageManager",
|
|
879
|
+
"initType",
|
|
880
|
+
"initVersion",
|
|
881
|
+
"json",
|
|
882
|
+
"legacyDirFiltering",
|
|
883
|
+
"libc",
|
|
884
|
+
"lockfileOnly",
|
|
885
|
+
"long",
|
|
886
|
+
"maxSockets",
|
|
887
|
+
"message",
|
|
888
|
+
"minimumReleaseAgeExcludePrune",
|
|
889
|
+
"minimumReleaseAgeIgnoreMissingTime",
|
|
890
|
+
"minimumReleaseAgeStrict",
|
|
891
|
+
"namedRegistries",
|
|
892
|
+
"nodeDownloadMirrors",
|
|
893
|
+
"nodeExperimentalPackageMap",
|
|
894
|
+
"nodePackageMapType",
|
|
895
|
+
"offline",
|
|
896
|
+
"only",
|
|
897
|
+
"os",
|
|
898
|
+
"packDestination",
|
|
899
|
+
"packGzipLevel",
|
|
900
|
+
"packageConfigs",
|
|
901
|
+
"packageLock",
|
|
902
|
+
"packages",
|
|
903
|
+
"parseable",
|
|
904
|
+
"patchesDir",
|
|
905
|
+
"pending",
|
|
906
|
+
"pmOnFail",
|
|
907
|
+
"pnpmExecPath",
|
|
908
|
+
"pnprServer",
|
|
909
|
+
"preferOffline",
|
|
910
|
+
"preserveAbsolutePaths",
|
|
911
|
+
"progress",
|
|
912
|
+
"provenance",
|
|
913
|
+
"publishBranch",
|
|
914
|
+
"recursive",
|
|
915
|
+
"registries",
|
|
916
|
+
"registriesByPrefix",
|
|
917
|
+
"registriesByScope",
|
|
918
|
+
"registry",
|
|
919
|
+
"registryOptionsByUrl",
|
|
920
|
+
"remoteSideEffectsCache",
|
|
921
|
+
"reporter",
|
|
922
|
+
"reporterHidePrefix",
|
|
923
|
+
"reverse",
|
|
924
|
+
"runtime",
|
|
925
|
+
"runtimeOnFail",
|
|
926
|
+
"save",
|
|
927
|
+
"saveCatalogName",
|
|
928
|
+
"saveDev",
|
|
929
|
+
"saveOptional",
|
|
930
|
+
"savePeer",
|
|
931
|
+
"saveProd",
|
|
932
|
+
"scriptsPrependNodePath",
|
|
933
|
+
"sideEffectsCacheRead",
|
|
934
|
+
"sideEffectsCacheWrite",
|
|
935
|
+
"signGitTag",
|
|
936
|
+
"skipManifestObfuscation",
|
|
937
|
+
"sort",
|
|
938
|
+
"stream",
|
|
939
|
+
"tasks",
|
|
940
|
+
"testPattern",
|
|
941
|
+
"trustLockfile",
|
|
942
|
+
"tryLoadDefaultPnpmfile",
|
|
943
|
+
"umask",
|
|
944
|
+
"update",
|
|
945
|
+
"useGitBranchLockfile",
|
|
946
|
+
"useLockfile",
|
|
947
|
+
"userAgent",
|
|
948
|
+
"version",
|
|
949
|
+
"versioning",
|
|
950
|
+
"virtualStoreOnly",
|
|
951
|
+
"virtualStoreType",
|
|
952
|
+
"workspacePackagePatterns",
|
|
953
|
+
"workspacePackages",
|
|
954
|
+
"workspacePrefix",
|
|
955
|
+
"workspaceRoot",
|
|
956
|
+
"yes"
|
|
957
|
+
];
|
|
958
|
+
const refusedProjectSettings = new Set(PNPM_PROJECT_REFUSED_SETTINGS);
|
|
959
|
+
/**
|
|
960
|
+
* Settings accepted by a pnpm v11 project workspace manifest.
|
|
961
|
+
*/
|
|
962
|
+
const PNPM_V11_WORKSPACE_SETTINGS_FIELDS = [.../* @__PURE__ */ new Set([...PNPM_V10_NPMRC_SETTINGS_FIELDS, ...PNPM_V11_ADDITIONAL_WORKSPACE_SETTINGS_FIELDS])].filter((field) => !refusedProjectSettings.has(field));
|
|
963
|
+
/**
|
|
964
|
+
* Settings that are supported by v11 but not by pnpm v12.
|
|
965
|
+
*/
|
|
966
|
+
const PNPM_V11_ONLY_WORKSPACE_SETTINGS = ["confirmModulesPurge", "packageConfigs"];
|
|
967
|
+
/**
|
|
968
|
+
* Settings introduced by pnpm v12.
|
|
969
|
+
*/
|
|
970
|
+
const PNPM_V12_ONLY_WORKSPACE_SETTINGS = [
|
|
971
|
+
"autoInstallPeersFromHighestMatch",
|
|
972
|
+
"externalDependencies",
|
|
973
|
+
"globalShims"
|
|
974
|
+
];
|
|
975
|
+
const v11OnlyWorkspaceSettings = new Set(PNPM_V11_ONLY_WORKSPACE_SETTINGS);
|
|
976
|
+
/**
|
|
977
|
+
* Settings accepted by a pnpm v12 project workspace manifest.
|
|
978
|
+
*/
|
|
979
|
+
const PNPM_V12_WORKSPACE_SETTINGS_FIELDS = [...PNPM_V11_WORKSPACE_SETTINGS_FIELDS.filter((field) => !v11OnlyWorkspaceSettings.has(field)), ...PNPM_V12_ONLY_WORKSPACE_SETTINGS];
|
|
980
|
+
/**
|
|
981
|
+
* Fields pnpm v11 accepts inside a `packageConfigs` entry.
|
|
982
|
+
*/
|
|
983
|
+
const PNPM_V11_PACKAGE_CONFIG_FIELDS = [
|
|
984
|
+
"hoist",
|
|
985
|
+
"modulesDir",
|
|
986
|
+
"overrides",
|
|
987
|
+
"saveExact",
|
|
988
|
+
"savePrefix"
|
|
989
|
+
];
|
|
990
|
+
//#endregion
|
|
991
|
+
//#region src/utils/settings-schema.ts
|
|
992
|
+
const PNPM_V10_WORKSPACE_SETTINGS_FIELDS = [
|
|
993
|
+
...PNPM_V10_NPMRC_SETTINGS_FIELDS,
|
|
994
|
+
"catalog",
|
|
995
|
+
"catalogs",
|
|
996
|
+
"packages"
|
|
997
|
+
];
|
|
998
|
+
const PROJECT_REFUSED_SETTINGS = new Set(PNPM_PROJECT_REFUSED_SETTINGS);
|
|
999
|
+
const PNPM_V11_ONLY_SETTINGS = new Set(PNPM_V11_ONLY_WORKSPACE_SETTINGS);
|
|
1000
|
+
const PNPM_V11_SETTINGS = new Set(PNPM_V11_WORKSPACE_SETTINGS_FIELDS);
|
|
1001
|
+
const PNPM_V12_ONLY_SETTINGS = new Set(PNPM_V12_ONLY_WORKSPACE_SETTINGS);
|
|
1002
|
+
const PNPM_V12_SETTINGS = new Set(PNPM_V12_WORKSPACE_SETTINGS_FIELDS);
|
|
1003
|
+
const PNPM_V10_SETTINGS = new Set(PNPM_V10_WORKSPACE_SETTINGS_FIELDS);
|
|
1004
|
+
const REGISTRY_SETTINGS = /* @__PURE__ */ new Set([
|
|
1005
|
+
"namedRegistries",
|
|
1006
|
+
"registries",
|
|
1007
|
+
"registriesByPrefix",
|
|
1008
|
+
"registriesByScope",
|
|
1009
|
+
"registry",
|
|
1010
|
+
"registryOptionsByUrl"
|
|
1011
|
+
]);
|
|
1012
|
+
const REGISTRY_CREDENTIAL_KEYS = /* @__PURE__ */ new Set([
|
|
1013
|
+
"auth",
|
|
1014
|
+
"authtoken",
|
|
1015
|
+
"password",
|
|
1016
|
+
"token",
|
|
1017
|
+
"tokenhelper",
|
|
1018
|
+
"username"
|
|
1019
|
+
]);
|
|
1020
|
+
const WORKSPACE_SCHEMA_DIRECTIVE = "$schema";
|
|
1021
|
+
/**
|
|
1022
|
+
* Create an empty collection of schema selection issues.
|
|
1023
|
+
*
|
|
1024
|
+
* @returns Empty issue lists for every supported issue category
|
|
1025
|
+
*/
|
|
1026
|
+
function createSettingsIssues() {
|
|
1027
|
+
return {
|
|
1028
|
+
incompatible: [],
|
|
1029
|
+
nonCamelCase: [],
|
|
1030
|
+
refused: [],
|
|
1031
|
+
unsafe: [],
|
|
1032
|
+
unknown: [],
|
|
1033
|
+
unsupported: []
|
|
1034
|
+
};
|
|
1035
|
+
}
|
|
1036
|
+
function hasUrlCredentials(value) {
|
|
1037
|
+
try {
|
|
1038
|
+
const url = new URL(value);
|
|
1039
|
+
return Boolean(url.username || url.password);
|
|
1040
|
+
} catch {
|
|
1041
|
+
return false;
|
|
1042
|
+
}
|
|
1043
|
+
}
|
|
1044
|
+
function isRegistryCredentialKey(key) {
|
|
1045
|
+
return REGISTRY_CREDENTIAL_KEYS.has(key.replaceAll("-", "").replaceAll("_", "").toLowerCase());
|
|
1046
|
+
}
|
|
1047
|
+
function isUnsafeRegistryUrl(value) {
|
|
1048
|
+
return value.includes("${") || hasUrlCredentials(value);
|
|
1049
|
+
}
|
|
1050
|
+
function containsUnsafeRegistryValue(value, checkCredentialKeys = false) {
|
|
1051
|
+
if (typeof value === "string") return isUnsafeRegistryUrl(value);
|
|
1052
|
+
if (!value || typeof value !== "object") return false;
|
|
1053
|
+
return Object.entries(value).some(([key, entryValue]) => checkCredentialKeys && isRegistryCredentialKey(key) || isUnsafeRegistryUrl(key) || containsUnsafeRegistryValue(entryValue, true));
|
|
1054
|
+
}
|
|
1055
|
+
function resolveCamelCaseKey(key) {
|
|
1056
|
+
return Object.keys(camelcaseKeys({ [key]: true }))[0] ?? key;
|
|
1057
|
+
}
|
|
1058
|
+
function resolveTargetSettings(compatibility) {
|
|
1059
|
+
switch (compatibility) {
|
|
1060
|
+
case "v10": return PNPM_V10_SETTINGS;
|
|
1061
|
+
case "v11": return PNPM_V11_SETTINGS;
|
|
1062
|
+
case "v12": return PNPM_V12_SETTINGS;
|
|
1063
|
+
default: throw new TypeError(`Unsupported compatibility target: ${compatibility}`);
|
|
1064
|
+
}
|
|
1065
|
+
}
|
|
1066
|
+
function isSettingFromAnotherMajor(key, compatibility) {
|
|
1067
|
+
if (compatibility === "v11") return PNPM_V12_ONLY_SETTINGS.has(key);
|
|
1068
|
+
if (compatibility === "v12") return PNPM_V11_ONLY_SETTINGS.has(key);
|
|
1069
|
+
return PNPM_V11_SETTINGS.has(key) || PNPM_V12_SETTINGS.has(key);
|
|
1070
|
+
}
|
|
1071
|
+
function resolveSettingIssue({ allowedFields, compatibility, key, npmrc, targetSettings, value }) {
|
|
1072
|
+
if (!npmrc && key !== resolveCamelCaseKey(key)) return "nonCamelCase";
|
|
1073
|
+
if (compatibility !== "v10" && PROJECT_REFUSED_SETTINGS.has(key)) return "refused";
|
|
1074
|
+
if (REGISTRY_SETTINGS.has(key) && containsUnsafeRegistryValue(value)) return "unsafe";
|
|
1075
|
+
if (!targetSettings.has(key)) return isSettingFromAnotherMajor(key, compatibility) ? "incompatible" : "unknown";
|
|
1076
|
+
if (allowedFields && !allowedFields.has(key)) return "unsupported";
|
|
1077
|
+
}
|
|
1078
|
+
/**
|
|
1079
|
+
* Select settings that are valid for a concrete pnpm workspace schema.
|
|
1080
|
+
*
|
|
1081
|
+
* Unrecognized, refused, incompatible, and destination-specific unsupported
|
|
1082
|
+
* settings are reported but never returned for migration.
|
|
1083
|
+
*
|
|
1084
|
+
* @param rawSettings - Settings read from a legacy configuration source
|
|
1085
|
+
* @param compatibility - Concrete pnpm compatibility target
|
|
1086
|
+
* @param options - Source spelling and destination field restrictions
|
|
1087
|
+
*
|
|
1088
|
+
* @returns Selected settings, original source keys, and rejected field groups
|
|
1089
|
+
*/
|
|
1090
|
+
function selectPnpmSettings(rawSettings, compatibility, options = {}) {
|
|
1091
|
+
const issues = createSettingsIssues();
|
|
1092
|
+
const keys = [];
|
|
1093
|
+
const settings = {};
|
|
1094
|
+
const targetSettings = resolveTargetSettings(compatibility);
|
|
1095
|
+
const allowedFields = options.allowedFields ? new Set(options.allowedFields) : void 0;
|
|
1096
|
+
for (const [originalKey, value] of Object.entries(rawSettings)) {
|
|
1097
|
+
const key = options.npmrc ? resolveCamelCaseKey(originalKey) : originalKey;
|
|
1098
|
+
const issue = resolveSettingIssue({
|
|
1099
|
+
allowedFields,
|
|
1100
|
+
compatibility,
|
|
1101
|
+
key,
|
|
1102
|
+
npmrc: Boolean(options.npmrc),
|
|
1103
|
+
targetSettings,
|
|
1104
|
+
value
|
|
1105
|
+
});
|
|
1106
|
+
if (issue) issues[issue].push(originalKey);
|
|
1107
|
+
else {
|
|
1108
|
+
keys.push(originalKey);
|
|
1109
|
+
settings[key] = value;
|
|
1110
|
+
}
|
|
1111
|
+
}
|
|
1112
|
+
return {
|
|
1113
|
+
issues,
|
|
1114
|
+
keys,
|
|
1115
|
+
settings
|
|
1116
|
+
};
|
|
1117
|
+
}
|
|
1118
|
+
function assertPackageConfigFields(settings) {
|
|
1119
|
+
const { packageConfigs } = settings;
|
|
1120
|
+
if (packageConfigs === void 0) return;
|
|
1121
|
+
const allowedFields = new Set(PNPM_V11_PACKAGE_CONFIG_FIELDS);
|
|
1122
|
+
const entries = Array.isArray(packageConfigs) ? packageConfigs.flatMap((entry, index) => {
|
|
1123
|
+
if (!entry || typeof entry !== "object" || Array.isArray(entry)) throw new TypeError(`packageConfigs[${index}] must be an object with a match array.`);
|
|
1124
|
+
const { match, ...config } = entry;
|
|
1125
|
+
if (!Array.isArray(match) || !match.every((projectName) => typeof projectName === "string")) throw new TypeError(`packageConfigs[${index}].match must be an array of package names.`);
|
|
1126
|
+
return [[`packageConfigs[${index}]`, config]];
|
|
1127
|
+
}) : Object.entries(packageConfigs);
|
|
1128
|
+
for (const [name, config] of entries) {
|
|
1129
|
+
if (!config || typeof config !== "object" || Array.isArray(config)) throw new TypeError(`${name} must contain a project settings object.`);
|
|
1130
|
+
const unsupported = Object.keys(config).filter((key) => !allowedFields.has(key));
|
|
1131
|
+
if (unsupported.length) throw new TypeError(`${name} contains unsupported project settings: ${unsupported.map((key) => JSON.stringify(key)).join(", ")}.`);
|
|
1132
|
+
}
|
|
1133
|
+
}
|
|
1134
|
+
function formatIssueList(keys) {
|
|
1135
|
+
return keys.map((key) => JSON.stringify(key)).join(", ");
|
|
1136
|
+
}
|
|
1137
|
+
/**
|
|
1138
|
+
* Assert that an existing workspace manifest matches the selected pnpm major.
|
|
1139
|
+
*
|
|
1140
|
+
* The migrator refuses to rewrite a manifest containing ignored settings so a
|
|
1141
|
+
* migration cannot silently preserve invalid output.
|
|
1142
|
+
*
|
|
1143
|
+
* @param settings - Existing workspace settings to validate
|
|
1144
|
+
* @param compatibility - Concrete pnpm compatibility target
|
|
1145
|
+
*
|
|
1146
|
+
* @returns Nothing when all settings match the target schema
|
|
1147
|
+
*
|
|
1148
|
+
* @throws {TypeError} When the manifest contains incompatible settings
|
|
1149
|
+
*/
|
|
1150
|
+
function assertCompatibleWorkspaceSettings(settings, compatibility) {
|
|
1151
|
+
if (compatibility === "v10") return;
|
|
1152
|
+
const { issues } = selectPnpmSettings(Object.fromEntries(Object.entries(settings).filter(([key]) => key !== WORKSPACE_SCHEMA_DIRECTIVE)), compatibility);
|
|
1153
|
+
const problems = [];
|
|
1154
|
+
if (issues.refused.length) problems.push(`refused: ${formatIssueList(issues.refused)}`);
|
|
1155
|
+
if (issues.incompatible.length) problems.push(`other pnpm major: ${formatIssueList(issues.incompatible)}`);
|
|
1156
|
+
if (issues.nonCamelCase.length) problems.push(`not camelCase: ${formatIssueList(issues.nonCamelCase)}`);
|
|
1157
|
+
if (issues.unknown.length) problems.push(`unrecognized: ${formatIssueList(issues.unknown)}`);
|
|
1158
|
+
if (issues.unsafe.length) problems.push(`unsafe registry URL: ${formatIssueList(issues.unsafe)}`);
|
|
1159
|
+
if (problems.length) throw new TypeError(`pnpm-workspace.yaml is incompatible with pnpm ${compatibility.slice(1)} (${problems.join("; ")}).`);
|
|
1160
|
+
if (compatibility === "v11") assertPackageConfigFields(settings);
|
|
1161
|
+
}
|
|
1162
|
+
//#endregion
|
|
667
1163
|
//#region src/utils/npmrc.ts
|
|
668
1164
|
/**
|
|
669
1165
|
* Authentication and registry keys that must remain in `.npmrc`.
|
|
@@ -688,6 +1184,9 @@ const NPMRC_AUTH_OR_REGISTRY_KEYS = [
|
|
|
688
1184
|
* Pattern for pnpm's channel-specific Node.js mirror keys.
|
|
689
1185
|
*/
|
|
690
1186
|
const NODE_MIRROR_KEY_PATTERN = /^node-mirror:(?<channel>.+)$/iu;
|
|
1187
|
+
function mergeSettingsIssues(target, source, sourceKeys) {
|
|
1188
|
+
for (const reason of Object.keys(target)) if (source[reason].length) target[reason].push(...sourceKeys ?? source[reason]);
|
|
1189
|
+
}
|
|
691
1190
|
/**
|
|
692
1191
|
* Normalize `.npmrc` key for case-insensitive matching.
|
|
693
1192
|
*
|
|
@@ -762,23 +1261,26 @@ async function pruneNpmrc(path, compatibility, migratedKeys) {
|
|
|
762
1261
|
* Read `.npmrc` and return settings that should be migrated into workspace config.
|
|
763
1262
|
*
|
|
764
1263
|
* - `v10`: returns settings from the legacy whitelist.
|
|
765
|
-
* - `v11+`:
|
|
766
|
-
*
|
|
1264
|
+
* - `v11+`: selects target-schema settings and retains auth, registry,
|
|
1265
|
+
* refused, incompatible, and unknown keys.
|
|
767
1266
|
*
|
|
768
1267
|
* @param path - Absolute path to the `.npmrc` file
|
|
769
1268
|
* @param compatibility - Concrete pnpm compatibility target
|
|
1269
|
+
* @param options - Optional destination-specific field restrictions
|
|
770
1270
|
*
|
|
771
1271
|
* @returns Migratable settings and their original `.npmrc` keys
|
|
772
1272
|
*
|
|
773
1273
|
* @throws {Error} When file reading or INI parsing fails
|
|
774
1274
|
*/
|
|
775
|
-
async function readMigratableNpmrc(path, compatibility) {
|
|
1275
|
+
async function readMigratableNpmrc(path, compatibility, options = {}) {
|
|
776
1276
|
const raw = await readIniFile(path);
|
|
1277
|
+
const issues = createSettingsIssues();
|
|
777
1278
|
if (compatibility === "v10") {
|
|
778
1279
|
const pnpmSettingsFields = new Set(PNPM_V10_NPMRC_SETTINGS_FIELDS.map((field) => normalizeNpmrcKey(kebabCase(field))));
|
|
779
1280
|
const keys = Object.keys(raw).filter((key) => pnpmSettingsFields.has(normalizeNpmrcKey(key)));
|
|
780
1281
|
const migratable = Object.fromEntries(keys.map((key) => [key, raw[key]]));
|
|
781
1282
|
return {
|
|
1283
|
+
issues,
|
|
782
1284
|
keys,
|
|
783
1285
|
settings: camelcaseKeys(migratable)
|
|
784
1286
|
};
|
|
@@ -786,20 +1288,233 @@ async function readMigratableNpmrc(path, compatibility) {
|
|
|
786
1288
|
const migratable = {};
|
|
787
1289
|
const keys = [];
|
|
788
1290
|
const nodeDownloadMirrors = {};
|
|
1291
|
+
const nodeDownloadMirrorKeys = [];
|
|
789
1292
|
for (const [key, value] of Object.entries(raw)) if (!isNpmrcAuthOrRegistryKey(key)) {
|
|
790
|
-
keys.push(key);
|
|
791
1293
|
const nodeMirrorMatch = normalizeNpmrcKey(key).match(NODE_MIRROR_KEY_PATTERN);
|
|
792
|
-
if (nodeMirrorMatch?.groups?.channel)
|
|
793
|
-
|
|
1294
|
+
if (nodeMirrorMatch?.groups?.channel) {
|
|
1295
|
+
nodeDownloadMirrorKeys.push(key);
|
|
1296
|
+
nodeDownloadMirrors[nodeMirrorMatch.groups.channel] = String(value);
|
|
1297
|
+
} else {
|
|
1298
|
+
const selected = selectPnpmSettings({ [key]: value }, compatibility, {
|
|
1299
|
+
allowedFields: options.allowedFields,
|
|
1300
|
+
npmrc: true
|
|
1301
|
+
});
|
|
1302
|
+
keys.push(...selected.keys);
|
|
1303
|
+
Object.assign(migratable, selected.settings);
|
|
1304
|
+
mergeSettingsIssues(issues, selected.issues);
|
|
1305
|
+
}
|
|
794
1306
|
}
|
|
795
1307
|
const settings = camelcaseKeys(migratable);
|
|
796
|
-
if (Object.keys(nodeDownloadMirrors).length)
|
|
1308
|
+
if (Object.keys(nodeDownloadMirrors).length) {
|
|
1309
|
+
const selected = selectPnpmSettings({ nodeDownloadMirrors }, compatibility, { allowedFields: options.allowedFields });
|
|
1310
|
+
if (selected.keys.length) {
|
|
1311
|
+
keys.push(...nodeDownloadMirrorKeys);
|
|
1312
|
+
settings.nodeDownloadMirrors = nodeDownloadMirrors;
|
|
1313
|
+
} else mergeSettingsIssues(issues, selected.issues, nodeDownloadMirrorKeys);
|
|
1314
|
+
}
|
|
797
1315
|
return {
|
|
1316
|
+
issues,
|
|
798
1317
|
keys,
|
|
799
1318
|
settings
|
|
800
1319
|
};
|
|
801
1320
|
}
|
|
802
1321
|
//#endregion
|
|
1322
|
+
//#region src/utils/config.ts
|
|
1323
|
+
/**
|
|
1324
|
+
* Resolve the indentation width for a YAML document.
|
|
1325
|
+
*
|
|
1326
|
+
* @param content - Raw YAML content
|
|
1327
|
+
*
|
|
1328
|
+
* @returns Detected indentation width or the project default
|
|
1329
|
+
*/
|
|
1330
|
+
function resolveYamlIndent(content) {
|
|
1331
|
+
const detectedIndent = detectIndent(content).amount;
|
|
1332
|
+
return detectedIndent > 0 ? detectedIndent : 2;
|
|
1333
|
+
}
|
|
1334
|
+
/**
|
|
1335
|
+
* Read and parse a package manifest when it exists.
|
|
1336
|
+
*
|
|
1337
|
+
* @param path - Absolute path to `package.json`
|
|
1338
|
+
* @param exists - Whether the package manifest exists
|
|
1339
|
+
*
|
|
1340
|
+
* @returns Parsed package manifest and detected indentation
|
|
1341
|
+
*
|
|
1342
|
+
* @throws {SyntaxError} When the package manifest contains invalid JSON
|
|
1343
|
+
*/
|
|
1344
|
+
async function readPackageJson(path, exists) {
|
|
1345
|
+
if (!exists) return {
|
|
1346
|
+
indent: 2,
|
|
1347
|
+
value: {}
|
|
1348
|
+
};
|
|
1349
|
+
const content = await fsReadFile(path);
|
|
1350
|
+
return {
|
|
1351
|
+
indent: detectIndent(content).indent,
|
|
1352
|
+
value: JSON.parse(content)
|
|
1353
|
+
};
|
|
1354
|
+
}
|
|
1355
|
+
/**
|
|
1356
|
+
* Read and parse a pnpm workspace manifest when it exists.
|
|
1357
|
+
*
|
|
1358
|
+
* @param path - Absolute path to `pnpm-workspace.yaml`
|
|
1359
|
+
* @param exists - Whether the workspace manifest exists
|
|
1360
|
+
*
|
|
1361
|
+
* @returns Parsed workspace settings and detected indentation
|
|
1362
|
+
*
|
|
1363
|
+
* @throws {Error} When the workspace manifest contains invalid YAML
|
|
1364
|
+
*/
|
|
1365
|
+
async function readPnpmWorkspace(path, exists) {
|
|
1366
|
+
if (!exists) return {
|
|
1367
|
+
document: new Document({}),
|
|
1368
|
+
indent: 2,
|
|
1369
|
+
value: {}
|
|
1370
|
+
};
|
|
1371
|
+
const content = await fsReadFile(path);
|
|
1372
|
+
const document = parseDocument(content);
|
|
1373
|
+
const [parseError] = document.errors;
|
|
1374
|
+
if (parseError) throw parseError;
|
|
1375
|
+
const parsedValue = document.toJS();
|
|
1376
|
+
if (parsedValue !== null && !isMap(document.contents)) throw new TypeError("pnpm-workspace.yaml must contain a root mapping.");
|
|
1377
|
+
if (parsedValue === null) document.contents = document.createNode({});
|
|
1378
|
+
return {
|
|
1379
|
+
document,
|
|
1380
|
+
indent: resolveYamlIndent(content),
|
|
1381
|
+
value: parsedValue ?? {}
|
|
1382
|
+
};
|
|
1383
|
+
}
|
|
1384
|
+
/**
|
|
1385
|
+
* Resolve migratable pnpm settings from a package manifest.
|
|
1386
|
+
*
|
|
1387
|
+
* Yarn resolutions are merged into pnpm overrides when enabled. Empty
|
|
1388
|
+
* overrides are removed from the result.
|
|
1389
|
+
*
|
|
1390
|
+
* @param packageJson - Parsed package manifest
|
|
1391
|
+
* @param yarnResolutions - Whether to convert Yarn resolutions to pnpm overrides
|
|
1392
|
+
* @param compatibility - Concrete pnpm compatibility target
|
|
1393
|
+
*
|
|
1394
|
+
* @returns Selected workspace settings and source cleanup metadata
|
|
1395
|
+
*/
|
|
1396
|
+
function resolvePackageJsonSettings(packageJson, yarnResolutions, compatibility) {
|
|
1397
|
+
const selected = selectPnpmSettings(Object.fromEntries(Object.entries(packageJson.pnpm ?? {})), compatibility);
|
|
1398
|
+
const migrateYarnResolutions = Boolean(yarnResolutions && packageJson.resolutions);
|
|
1399
|
+
const pnpmSettings = migrateYarnResolutions ? {
|
|
1400
|
+
...selected.settings,
|
|
1401
|
+
overrides: defu(selected.settings.overrides, packageJson.resolutions)
|
|
1402
|
+
} : { ...selected.settings };
|
|
1403
|
+
if (pnpmSettings.overrides && !Object.keys(pnpmSettings.overrides).length) delete pnpmSettings.overrides;
|
|
1404
|
+
return {
|
|
1405
|
+
issues: selected.issues,
|
|
1406
|
+
keys: selected.keys,
|
|
1407
|
+
settings: pnpmSettings,
|
|
1408
|
+
yarnResolutions: migrateYarnResolutions
|
|
1409
|
+
};
|
|
1410
|
+
}
|
|
1411
|
+
//#endregion
|
|
1412
|
+
//#region src/utils/project-npmrc.ts
|
|
1413
|
+
function resolvePackageJsonPatterns(patterns) {
|
|
1414
|
+
return patterns.map((pattern) => {
|
|
1415
|
+
const negated = pattern.startsWith("!");
|
|
1416
|
+
const workspacePattern = (negated ? pattern.slice(1) : pattern).replace(/\/+$/u, "");
|
|
1417
|
+
const packageJsonPattern = workspacePattern === "." ? PACKAGE_JSON : `${workspacePattern}/${PACKAGE_JSON}`;
|
|
1418
|
+
return negated ? `!${packageJsonPattern}` : packageJsonPattern;
|
|
1419
|
+
});
|
|
1420
|
+
}
|
|
1421
|
+
/**
|
|
1422
|
+
* Collect unique workspace package patterns from multiple settings sources.
|
|
1423
|
+
*
|
|
1424
|
+
* @param settingsSources - Workspace settings that may declare package patterns
|
|
1425
|
+
*
|
|
1426
|
+
* @returns Unique package patterns in source order
|
|
1427
|
+
*/
|
|
1428
|
+
function collectWorkspacePackagePatterns(...settingsSources) {
|
|
1429
|
+
return [...new Set(settingsSources.flatMap((settings) => Array.isArray(settings.packages) ? settings.packages : []))];
|
|
1430
|
+
}
|
|
1431
|
+
async function readProjectManifestCandidates(cwd, patterns) {
|
|
1432
|
+
if (!patterns.some((pattern) => !pattern.startsWith("!"))) return {
|
|
1433
|
+
candidates: [],
|
|
1434
|
+
warnings: []
|
|
1435
|
+
};
|
|
1436
|
+
const rootPackageJsonPath = resolve(cwd, PACKAGE_JSON);
|
|
1437
|
+
const packageJsonPaths = await glob(resolvePackageJsonPatterns(patterns), {
|
|
1438
|
+
absolute: true,
|
|
1439
|
+
cwd,
|
|
1440
|
+
dot: true,
|
|
1441
|
+
ignore: ["**/node_modules/**"],
|
|
1442
|
+
onlyFiles: true
|
|
1443
|
+
});
|
|
1444
|
+
const candidateResults = await Promise.all([...new Set(packageJsonPaths)].sort().filter((packageJsonPath) => resolve(packageJsonPath) !== rootPackageJsonPath).map(async (packageJsonPath) => {
|
|
1445
|
+
const projectDir = dirname(packageJsonPath);
|
|
1446
|
+
const npmrcPath = resolve(projectDir, NPMRC);
|
|
1447
|
+
if (!await fsExists(npmrcPath)) return {};
|
|
1448
|
+
const packageJson = await readPackageJson(packageJsonPath, true);
|
|
1449
|
+
if (!packageJson.value.name) return { warning: `${relative(cwd, npmrcPath)} was kept because its package.json has no name for packageConfigs matching.` };
|
|
1450
|
+
return { candidate: {
|
|
1451
|
+
npmrcPath,
|
|
1452
|
+
packageJsonPath,
|
|
1453
|
+
projectName: packageJson.value.name
|
|
1454
|
+
} };
|
|
1455
|
+
}));
|
|
1456
|
+
const candidates = [];
|
|
1457
|
+
const warnings = [];
|
|
1458
|
+
for (const result of candidateResults) {
|
|
1459
|
+
if (result.candidate) candidates.push(result.candidate);
|
|
1460
|
+
if (result.warning) warnings.push(result.warning);
|
|
1461
|
+
}
|
|
1462
|
+
return {
|
|
1463
|
+
candidates,
|
|
1464
|
+
warnings
|
|
1465
|
+
};
|
|
1466
|
+
}
|
|
1467
|
+
/**
|
|
1468
|
+
* Collect supported settings from subproject `.npmrc` files.
|
|
1469
|
+
*
|
|
1470
|
+
* pnpm v11 accepts five fields in `packageConfigs`. pnpm v12 does not support
|
|
1471
|
+
* `packageConfigs`, so its subproject settings are only reported and retained.
|
|
1472
|
+
*
|
|
1473
|
+
* @param cwd - Workspace root directory
|
|
1474
|
+
* @param patterns - Workspace package patterns used to discover subprojects
|
|
1475
|
+
* @param compatibility - Concrete pnpm compatibility target
|
|
1476
|
+
*
|
|
1477
|
+
* @returns Migratable project settings, source metadata, and warnings
|
|
1478
|
+
*/
|
|
1479
|
+
async function readProjectNpmrcMigrations(cwd, patterns, compatibility) {
|
|
1480
|
+
if (compatibility === "v10") return {
|
|
1481
|
+
packageConfigs: {},
|
|
1482
|
+
projects: [],
|
|
1483
|
+
warnings: []
|
|
1484
|
+
};
|
|
1485
|
+
const { candidates, warnings } = await readProjectManifestCandidates(cwd, patterns);
|
|
1486
|
+
const candidatesByName = /* @__PURE__ */ new Map();
|
|
1487
|
+
for (const candidate of candidates) {
|
|
1488
|
+
const namedCandidates = candidatesByName.get(candidate.projectName) ?? [];
|
|
1489
|
+
namedCandidates.push(candidate);
|
|
1490
|
+
candidatesByName.set(candidate.projectName, namedCandidates);
|
|
1491
|
+
}
|
|
1492
|
+
const packageConfigs = {};
|
|
1493
|
+
const projects = [];
|
|
1494
|
+
const allowedFields = compatibility === "v11" ? PNPM_V11_PACKAGE_CONFIG_FIELDS : [];
|
|
1495
|
+
const migrationResults = await Promise.all([...candidatesByName].map(async ([projectName, namedCandidates]) => {
|
|
1496
|
+
if (namedCandidates.length > 1) return { warning: `Subproject .npmrc files for duplicate package name ${JSON.stringify(projectName)} were kept: ${namedCandidates.map((candidate) => relative(cwd, candidate.packageJsonPath)).join(", ")}.` };
|
|
1497
|
+
const [candidate] = namedCandidates;
|
|
1498
|
+
return { project: {
|
|
1499
|
+
migratable: await readMigratableNpmrc(candidate.npmrcPath, compatibility, { allowedFields }),
|
|
1500
|
+
npmrcPath: candidate.npmrcPath,
|
|
1501
|
+
projectName
|
|
1502
|
+
} };
|
|
1503
|
+
}));
|
|
1504
|
+
for (const result of migrationResults) {
|
|
1505
|
+
if (result.warning) warnings.push(result.warning);
|
|
1506
|
+
if (result.project) {
|
|
1507
|
+
projects.push(result.project);
|
|
1508
|
+
if (result.project.migratable.keys.length) packageConfigs[result.project.projectName] = result.project.migratable.settings;
|
|
1509
|
+
}
|
|
1510
|
+
}
|
|
1511
|
+
return {
|
|
1512
|
+
packageConfigs,
|
|
1513
|
+
projects,
|
|
1514
|
+
warnings
|
|
1515
|
+
};
|
|
1516
|
+
}
|
|
1517
|
+
//#endregion
|
|
803
1518
|
//#region src/utils/settings-change.ts
|
|
804
1519
|
/**
|
|
805
1520
|
* Maximum dynamic-programming cells used for an exact line diff.
|
|
@@ -833,6 +1548,11 @@ function collectSettingsChanges(before, after) {
|
|
|
833
1548
|
*
|
|
834
1549
|
* This fallback deliberately gives up the exact LCS for unusually large
|
|
835
1550
|
* settings to keep reporting from exhausting memory after migration writes.
|
|
1551
|
+
*
|
|
1552
|
+
* @param beforeLines - YAML lines before migration
|
|
1553
|
+
* @param afterLines - YAML lines after migration
|
|
1554
|
+
*
|
|
1555
|
+
* @returns Classified diff lines in display order
|
|
836
1556
|
*/
|
|
837
1557
|
function createLinearLineDiff(beforeLines, afterLines) {
|
|
838
1558
|
let prefixLength = 0;
|
|
@@ -964,9 +1684,35 @@ function reportSettingsChanges(changes) {
|
|
|
964
1684
|
}
|
|
965
1685
|
}
|
|
966
1686
|
//#endregion
|
|
1687
|
+
//#region src/utils/settings-issue-report.ts
|
|
1688
|
+
function formatIssueKeys(keys) {
|
|
1689
|
+
return keys.map((key) => JSON.stringify(key)).join(", ");
|
|
1690
|
+
}
|
|
1691
|
+
/**
|
|
1692
|
+
* Report settings that were intentionally left in their source.
|
|
1693
|
+
*
|
|
1694
|
+
* @param options - Retained settings and destination context
|
|
1695
|
+
*
|
|
1696
|
+
* @returns Nothing; warnings are written through consola
|
|
1697
|
+
*/
|
|
1698
|
+
function reportSettingsIssues(options) {
|
|
1699
|
+
const { compatibility, issues, projectConfig = false, source } = options;
|
|
1700
|
+
if (issues.refused.length) consola$1.warn(`Kept project-refused settings in ${source}: ${formatIssueKeys(issues.refused)}. Configure machine settings globally and current-run paths on the command line.`);
|
|
1701
|
+
if (issues.incompatible.length) consola$1.warn(`Kept settings in ${source} that are incompatible with pnpm ${compatibility.slice(1)}: ${formatIssueKeys(issues.incompatible)}.`);
|
|
1702
|
+
if (issues.nonCamelCase.length) consola$1.warn(`Kept non-camelCase settings in ${source}: ${formatIssueKeys(issues.nonCamelCase)}. Workspace manifest keys must use camelCase.`);
|
|
1703
|
+
if (issues.unknown.length) consola$1.warn(`Kept settings in ${source} that pnpm ${compatibility.slice(1)} does not recognize: ${formatIssueKeys(issues.unknown)}.`);
|
|
1704
|
+
if (issues.unsupported.length) {
|
|
1705
|
+
const destination = projectConfig && compatibility === "v12" ? "pnpm v12 does not support packageConfigs" : "packageConfigs only accepts hoist, modulesDir, overrides, saveExact, and savePrefix";
|
|
1706
|
+
consola$1.warn(`Kept subproject settings in ${source}: ${formatIssueKeys(issues.unsupported)}; ${destination}.`);
|
|
1707
|
+
}
|
|
1708
|
+
if (issues.unsafe.length) consola$1.warn(`Kept unsafe registry settings in ${source}: ${formatIssueKeys(issues.unsafe)}. Remove credentials and dynamic URL interpolation before migrating them.`);
|
|
1709
|
+
}
|
|
1710
|
+
//#endregion
|
|
967
1711
|
//#region src/utils/yaml-document.ts
|
|
968
1712
|
/**
|
|
969
1713
|
* Sort every mapping in a YAML node without replacing its comments or anchors.
|
|
1714
|
+
*
|
|
1715
|
+
* @param node - YAML node whose nested mappings should be sorted
|
|
970
1716
|
*/
|
|
971
1717
|
function sortYamlMappings(node) {
|
|
972
1718
|
if (isMap(node)) {
|
|
@@ -983,6 +1729,11 @@ function sortYamlMappings(node) {
|
|
|
983
1729
|
/**
|
|
984
1730
|
* Apply semantic root changes to the parsed document while retaining untouched
|
|
985
1731
|
* YAML nodes and their source metadata.
|
|
1732
|
+
*
|
|
1733
|
+
* @param document - Parsed YAML document to update in place
|
|
1734
|
+
* @param options - Before/after settings and sorting preference
|
|
1735
|
+
*
|
|
1736
|
+
* @returns Nothing; the document is updated in place
|
|
986
1737
|
*/
|
|
987
1738
|
function updateYamlDocument(document, options) {
|
|
988
1739
|
const { after, before, sortKeys } = options;
|
|
@@ -992,91 +1743,253 @@ function updateYamlDocument(document, options) {
|
|
|
992
1743
|
}
|
|
993
1744
|
/**
|
|
994
1745
|
* Normalize blank lines immediately before root mapping keys.
|
|
1746
|
+
*
|
|
1747
|
+
* @param content - Serialized workspace YAML
|
|
1748
|
+
* @param newlineBetween - Whether root keys should be separated by blank lines
|
|
1749
|
+
*
|
|
1750
|
+
* @returns YAML with normalized root-key spacing
|
|
995
1751
|
*/
|
|
996
1752
|
function formatRootSpacing(content, newlineBetween) {
|
|
997
1753
|
return content.replace(/\n+(?=[^\s#][^:\n]*:)/gu, newlineBetween ? "\n\n" : "\n");
|
|
998
1754
|
}
|
|
999
1755
|
//#endregion
|
|
1000
|
-
//#region src/
|
|
1756
|
+
//#region src/migration-persistence.ts
|
|
1757
|
+
const SETTINGS_WITHOUT_REPLACEMENT = /* @__PURE__ */ new Set(["ignoreDepScripts", "ignorePatchFailures"]);
|
|
1758
|
+
const REPLACEMENT_SETTING_KEYS = {
|
|
1759
|
+
allowNonAppliedPatches: "allowUnusedPatches",
|
|
1760
|
+
auditConfig: "audit",
|
|
1761
|
+
auditLevel: "audit",
|
|
1762
|
+
cleanupUnusedCatalogs: "catalogPrune",
|
|
1763
|
+
enableGlobalVirtualStore: "virtualStoreType",
|
|
1764
|
+
ignoredBuiltDependencies: "allowBuilds",
|
|
1765
|
+
managePackageManagerVersions: "pmOnFail",
|
|
1766
|
+
namedRegistries: "registries",
|
|
1767
|
+
neverBuiltDependencies: "allowBuilds",
|
|
1768
|
+
onlyBuiltDependencies: "allowBuilds",
|
|
1769
|
+
onlyBuiltDependenciesFile: "allowBuilds",
|
|
1770
|
+
packageManagerStrict: "pmOnFail",
|
|
1771
|
+
packageManagerStrictVersion: "pmOnFail",
|
|
1772
|
+
remoteSideEffectsCache: "sideEffectsCache",
|
|
1773
|
+
sideEffectsCacheReadonly: "sideEffectsCache",
|
|
1774
|
+
updateConfig: "update"
|
|
1775
|
+
};
|
|
1776
|
+
function cleanPackageJsonSettings({ migratedKeys, packageJson, settings, yarnResolutionsApplied }) {
|
|
1777
|
+
let changed = false;
|
|
1778
|
+
if (packageJson.value.pnpm) {
|
|
1779
|
+
for (const key of migratedKeys) {
|
|
1780
|
+
Reflect.deleteProperty(packageJson.value.pnpm, key);
|
|
1781
|
+
changed = true;
|
|
1782
|
+
}
|
|
1783
|
+
if (!Object.keys(packageJson.value.pnpm).length) delete packageJson.value.pnpm;
|
|
1784
|
+
}
|
|
1785
|
+
if (settings.yarnResolutions && yarnResolutionsApplied) {
|
|
1786
|
+
changed = true;
|
|
1787
|
+
delete packageJson.value.resolutions;
|
|
1788
|
+
}
|
|
1789
|
+
return changed;
|
|
1790
|
+
}
|
|
1791
|
+
function containsMigratedValue(actual, expected) {
|
|
1792
|
+
if (Array.isArray(actual) && Array.isArray(expected)) return expected.every((expectedItem) => actual.some((actualItem) => containsMigratedValue(actualItem, expectedItem)));
|
|
1793
|
+
if (actual && expected && typeof actual === "object" && typeof expected === "object") return Object.entries(expected).every(([key, expectedValue]) => containsMigratedValue(actual[key], expectedValue));
|
|
1794
|
+
return Object.is(actual, expected);
|
|
1795
|
+
}
|
|
1796
|
+
function resolveSourceRuntimeVersion(sourceSettings, targetKey) {
|
|
1797
|
+
if (targetKey === "useNodeVersion") return Reflect.get(sourceSettings, "useNodeVersion");
|
|
1798
|
+
const executionEnv = Reflect.get(sourceSettings, "executionEnv");
|
|
1799
|
+
if (!executionEnv || typeof executionEnv !== "object" || Array.isArray(executionEnv)) return;
|
|
1800
|
+
return executionEnv.nodeVersion;
|
|
1801
|
+
}
|
|
1802
|
+
function resolveReplacementSettingKey(sourceKey, targetKey, npmrc) {
|
|
1803
|
+
if (npmrc && /^node-mirror:/iu.test(sourceKey)) return "nodeDownloadMirrors";
|
|
1804
|
+
return REPLACEMENT_SETTING_KEYS[targetKey];
|
|
1805
|
+
}
|
|
1806
|
+
function selectAppliedRootKeys({ finalSettings, incomingSettings, keys, npmrc = false, runtimeApplied, runtimeVersion, sourceSettings }) {
|
|
1807
|
+
return keys.filter((sourceKey) => {
|
|
1808
|
+
const targetKey = npmrc ? Object.keys(camelcaseKeys({ [sourceKey]: true }))[0] ?? sourceKey : sourceKey;
|
|
1809
|
+
if (!Object.hasOwn(incomingSettings, targetKey)) {
|
|
1810
|
+
if (targetKey === "executionEnv" || targetKey === "useNodeVersion") {
|
|
1811
|
+
const sourceRuntimeVersion = resolveSourceRuntimeVersion(sourceSettings, targetKey);
|
|
1812
|
+
return runtimeApplied && typeof sourceRuntimeVersion === "string" && sourceRuntimeVersion === runtimeVersion;
|
|
1813
|
+
}
|
|
1814
|
+
if (SETTINGS_WITHOUT_REPLACEMENT.has(targetKey)) return false;
|
|
1815
|
+
const replacementKey = resolveReplacementSettingKey(sourceKey, targetKey, npmrc);
|
|
1816
|
+
if (!replacementKey || !Object.hasOwn(incomingSettings, replacementKey)) return true;
|
|
1817
|
+
return containsMigratedValue(Reflect.get(finalSettings, replacementKey), Reflect.get(incomingSettings, replacementKey));
|
|
1818
|
+
}
|
|
1819
|
+
return containsMigratedValue(Reflect.get(finalSettings, targetKey), Reflect.get(incomingSettings, targetKey));
|
|
1820
|
+
});
|
|
1821
|
+
}
|
|
1822
|
+
function resolveProjectConfig(settings, projectName) {
|
|
1823
|
+
const { packageConfigs } = settings;
|
|
1824
|
+
if (!packageConfigs || typeof packageConfigs !== "object" || Array.isArray(packageConfigs)) return;
|
|
1825
|
+
const projectConfig = packageConfigs[projectName];
|
|
1826
|
+
return projectConfig && typeof projectConfig === "object" && !Array.isArray(projectConfig) ? projectConfig : void 0;
|
|
1827
|
+
}
|
|
1828
|
+
function selectAppliedProjectKeys(project, finalSettings) {
|
|
1829
|
+
const finalProjectConfig = resolveProjectConfig(finalSettings, project.projectName);
|
|
1830
|
+
if (!finalProjectConfig) return [];
|
|
1831
|
+
return project.migratable.keys.filter((sourceKey) => {
|
|
1832
|
+
const targetKey = Object.keys(camelcaseKeys({ [sourceKey]: true }))[0] ?? sourceKey;
|
|
1833
|
+
return containsMigratedValue(finalProjectConfig[targetKey], project.migratable.settings[targetKey]);
|
|
1834
|
+
});
|
|
1835
|
+
}
|
|
1001
1836
|
/**
|
|
1002
|
-
*
|
|
1837
|
+
* Persist destinations before pruning any legacy source file.
|
|
1003
1838
|
*
|
|
1004
|
-
* @param
|
|
1839
|
+
* @param options - Destination files, selected sources, and cleanup policy
|
|
1005
1840
|
*
|
|
1006
|
-
* @returns
|
|
1841
|
+
* @returns A promise that resolves after destinations and sources are persisted
|
|
1007
1842
|
*/
|
|
1008
|
-
function
|
|
1009
|
-
const
|
|
1010
|
-
|
|
1843
|
+
async function persistMigration(options) {
|
|
1844
|
+
const { cleanNpmrc, cleanPackageJson, compatibility, finalSettings, incomingSettings, npmrc, npmrcExists, npmrcPath, packageJson, packageJsonExists, packageJsonPath, packageJsonRuntimeChanged, packageJsonSettings, pnpmWorkspaceContent, pnpmWorkspacePath, projectNpmrcs, runtimeVersion } = options;
|
|
1845
|
+
const appliedPackageJsonKeys = selectAppliedRootKeys({
|
|
1846
|
+
finalSettings,
|
|
1847
|
+
incomingSettings,
|
|
1848
|
+
keys: packageJsonSettings.keys,
|
|
1849
|
+
runtimeApplied: packageJsonRuntimeChanged,
|
|
1850
|
+
runtimeVersion,
|
|
1851
|
+
sourceSettings: packageJsonSettings.settings
|
|
1852
|
+
});
|
|
1853
|
+
const appliedNpmrcKeys = selectAppliedRootKeys({
|
|
1854
|
+
finalSettings,
|
|
1855
|
+
incomingSettings,
|
|
1856
|
+
keys: npmrc.keys,
|
|
1857
|
+
npmrc: true,
|
|
1858
|
+
runtimeApplied: packageJsonRuntimeChanged,
|
|
1859
|
+
runtimeVersion,
|
|
1860
|
+
sourceSettings: npmrc.settings
|
|
1861
|
+
});
|
|
1862
|
+
const yarnResolutionsApplied = packageJsonSettings.yarnResolutions && containsMigratedValue(finalSettings.overrides, incomingSettings.overrides);
|
|
1863
|
+
const packageJsonSettingsChanged = packageJsonExists && cleanPackageJson ? cleanPackageJsonSettings({
|
|
1864
|
+
migratedKeys: appliedPackageJsonKeys,
|
|
1865
|
+
packageJson,
|
|
1866
|
+
settings: packageJsonSettings,
|
|
1867
|
+
yarnResolutionsApplied
|
|
1868
|
+
}) : false;
|
|
1869
|
+
await fsWriteFile(pnpmWorkspacePath, pnpmWorkspaceContent);
|
|
1870
|
+
if (packageJsonExists && (packageJsonRuntimeChanged || packageJsonSettingsChanged)) await fsWriteFile(packageJsonPath, JSON.stringify(packageJson.value, null, packageJson.indent));
|
|
1871
|
+
if (!cleanNpmrc) return;
|
|
1872
|
+
const pruneTasks = projectNpmrcs.projects.flatMap((project) => {
|
|
1873
|
+
const appliedKeys = selectAppliedProjectKeys(project, finalSettings);
|
|
1874
|
+
return appliedKeys.length ? [pruneNpmrc(project.npmrcPath, compatibility, appliedKeys)] : [];
|
|
1875
|
+
});
|
|
1876
|
+
if (npmrcExists && appliedNpmrcKeys.length) pruneTasks.push(pruneNpmrc(npmrcPath, compatibility, appliedNpmrcKeys));
|
|
1877
|
+
await Promise.all(pruneTasks);
|
|
1011
1878
|
}
|
|
1879
|
+
//#endregion
|
|
1880
|
+
//#region src/migration-sources.ts
|
|
1012
1881
|
/**
|
|
1013
|
-
* Read
|
|
1014
|
-
*
|
|
1015
|
-
* @param path - Absolute path to `package.json`
|
|
1016
|
-
* @param exists - Whether the package manifest exists
|
|
1882
|
+
* Read, schema-filter, report, and merge all legacy settings sources.
|
|
1017
1883
|
*
|
|
1018
|
-
* @
|
|
1884
|
+
* @param options - Migration target and source configuration
|
|
1019
1885
|
*
|
|
1020
|
-
* @
|
|
1886
|
+
* @returns Resolved source metadata and merged incoming settings
|
|
1021
1887
|
*/
|
|
1022
|
-
async function
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1888
|
+
async function resolveMigrationSources(options) {
|
|
1889
|
+
const { compatibility, cwd, npmrcExists, npmrcPath, packageJson, pnpmWorkspace, strategy, yarnResolutions } = options;
|
|
1890
|
+
const npmrc = npmrcExists ? await readMigratableNpmrc(npmrcPath, compatibility) : {
|
|
1891
|
+
issues: createSettingsIssues(),
|
|
1892
|
+
keys: [],
|
|
1893
|
+
settings: {}
|
|
1026
1894
|
};
|
|
1027
|
-
|
|
1895
|
+
reportSettingsIssues({
|
|
1896
|
+
compatibility,
|
|
1897
|
+
issues: npmrc.issues,
|
|
1898
|
+
source: NPMRC
|
|
1899
|
+
});
|
|
1900
|
+
const packageJsonSettings = resolvePackageJsonSettings(packageJson, yarnResolutions, compatibility);
|
|
1901
|
+
reportSettingsIssues({
|
|
1902
|
+
compatibility,
|
|
1903
|
+
issues: packageJsonSettings.issues,
|
|
1904
|
+
source: `${PACKAGE_JSON}#pnpm`
|
|
1905
|
+
});
|
|
1906
|
+
const projectNpmrcs = await readProjectNpmrcMigrations(cwd, collectWorkspacePackagePatterns(mergeByStrategy(pnpmWorkspace, mergeByStrategy(packageJsonSettings.settings, npmrc.settings, "merge"), strategy)), compatibility);
|
|
1907
|
+
for (const warning of projectNpmrcs.warnings) consola$1.warn(warning);
|
|
1908
|
+
for (const project of projectNpmrcs.projects) reportSettingsIssues({
|
|
1909
|
+
compatibility,
|
|
1910
|
+
issues: project.migratable.issues,
|
|
1911
|
+
projectConfig: true,
|
|
1912
|
+
source: relative(cwd, project.npmrcPath)
|
|
1913
|
+
});
|
|
1914
|
+
const npmrcSettings = mergeByStrategy(npmrc.settings, Object.keys(projectNpmrcs.packageConfigs).length ? { packageConfigs: projectNpmrcs.packageConfigs } : {}, "merge");
|
|
1028
1915
|
return {
|
|
1029
|
-
|
|
1030
|
-
|
|
1916
|
+
incomingSettings: mergeByStrategy(packageJsonSettings.settings, npmrcSettings, "merge"),
|
|
1917
|
+
npmrc,
|
|
1918
|
+
packageJson: packageJsonSettings,
|
|
1919
|
+
projectNpmrcs
|
|
1031
1920
|
};
|
|
1032
1921
|
}
|
|
1922
|
+
//#endregion
|
|
1923
|
+
//#region src/options.ts
|
|
1033
1924
|
/**
|
|
1034
|
-
*
|
|
1035
|
-
*
|
|
1036
|
-
* @param path - Absolute path to `pnpm-workspace.yaml`
|
|
1037
|
-
* @param exists - Whether the workspace manifest exists
|
|
1038
|
-
*
|
|
1039
|
-
* @returns Parsed workspace settings and detected indentation
|
|
1040
|
-
*
|
|
1041
|
-
* @throws {Error} When the workspace manifest contains invalid YAML
|
|
1925
|
+
* Default values for migration options.
|
|
1042
1926
|
*/
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1927
|
+
const DEFAULT_OPTIONS = {
|
|
1928
|
+
cleanNpmrc: true,
|
|
1929
|
+
cleanPackageJson: true,
|
|
1930
|
+
compatibility: "auto",
|
|
1931
|
+
newlineBetween: true,
|
|
1932
|
+
replaceDeprecated: false,
|
|
1933
|
+
showChanges: true,
|
|
1934
|
+
sortKeys: false,
|
|
1935
|
+
strategy: "merge",
|
|
1936
|
+
yarnResolutions: true
|
|
1937
|
+
};
|
|
1938
|
+
const VALID_STRATEGIES = [
|
|
1939
|
+
"discard",
|
|
1940
|
+
"merge",
|
|
1941
|
+
"overwrite"
|
|
1942
|
+
];
|
|
1943
|
+
const VALID_COMPATIBILITIES = [
|
|
1944
|
+
"auto",
|
|
1945
|
+
"v10",
|
|
1946
|
+
"v11",
|
|
1947
|
+
"v12"
|
|
1948
|
+
];
|
|
1949
|
+
function resolveCompatibility(compatibility) {
|
|
1950
|
+
if (!compatibility) return DEFAULT_OPTIONS.compatibility;
|
|
1951
|
+
if (VALID_COMPATIBILITIES.includes(compatibility)) return compatibility;
|
|
1952
|
+
throw new Error(`Invalid compatibility: ${compatibility}. Expected one of: ${VALID_COMPATIBILITIES.join(", ")}`);
|
|
1953
|
+
}
|
|
1954
|
+
function resolveStrategy(strategy) {
|
|
1955
|
+
if (!strategy) return DEFAULT_OPTIONS.strategy;
|
|
1956
|
+
if (VALID_STRATEGIES.includes(strategy)) return strategy;
|
|
1957
|
+
throw new Error(`Invalid strategy: ${strategy}. Expected one of: ${VALID_STRATEGIES.join(", ")}`);
|
|
1061
1958
|
}
|
|
1062
1959
|
/**
|
|
1063
|
-
* Resolve
|
|
1960
|
+
* Resolve and normalize migration options with defaults.
|
|
1064
1961
|
*
|
|
1065
|
-
*
|
|
1066
|
-
*
|
|
1962
|
+
* This function takes partial options and returns a complete options object
|
|
1963
|
+
* with all properties set to either the provided value or the default value.
|
|
1067
1964
|
*
|
|
1068
|
-
* @param
|
|
1069
|
-
*
|
|
1965
|
+
* @param options - Partial migration options
|
|
1966
|
+
*
|
|
1967
|
+
* @returns Complete options object with all required properties
|
|
1968
|
+
*
|
|
1969
|
+
* @example
|
|
1970
|
+
* ```ts
|
|
1971
|
+
* // Use all defaults
|
|
1972
|
+
* const opts = resolveOptions()
|
|
1973
|
+
* // { cleanNpmrc: true, cleanPackageJson: true, cwd: '/current/dir', ... }
|
|
1070
1974
|
*
|
|
1071
|
-
*
|
|
1975
|
+
* // Override specific options
|
|
1976
|
+
* const opts = resolveOptions({ sortKeys: true, cleanNpmrc: false })
|
|
1977
|
+
* // { cleanNpmrc: false, cleanPackageJson: true, sortKeys: true, ... }
|
|
1978
|
+
* ```
|
|
1072
1979
|
*/
|
|
1073
|
-
function
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1980
|
+
function resolveOptions(options = {}) {
|
|
1981
|
+
return {
|
|
1982
|
+
cleanNpmrc: options.cleanNpmrc ?? DEFAULT_OPTIONS.cleanNpmrc,
|
|
1983
|
+
compatibility: resolveCompatibility(options.compatibility),
|
|
1984
|
+
cwd: options.cwd ?? process.cwd(),
|
|
1985
|
+
newlineBetween: options.newlineBetween ?? DEFAULT_OPTIONS.newlineBetween,
|
|
1986
|
+
showChanges: options.showChanges ?? DEFAULT_OPTIONS.showChanges,
|
|
1987
|
+
sortKeys: options.sortKeys ?? DEFAULT_OPTIONS.sortKeys,
|
|
1988
|
+
strategy: resolveStrategy(options.strategy),
|
|
1989
|
+
yarnResolutions: options.yarnResolutions ?? DEFAULT_OPTIONS.yarnResolutions,
|
|
1990
|
+
cleanPackageJson: options.cleanPackageJson ?? DEFAULT_OPTIONS.cleanPackageJson,
|
|
1991
|
+
replaceDeprecated: options.replaceDeprecated ?? DEFAULT_OPTIONS.replaceDeprecated
|
|
1992
|
+
};
|
|
1080
1993
|
}
|
|
1081
1994
|
//#endregion
|
|
1082
1995
|
//#region src/core.ts
|
|
@@ -1088,8 +2001,8 @@ function hasSettingsSources(npmrcExists, packageJsonExists, pnpmWorkspaceExists)
|
|
|
1088
2001
|
return false;
|
|
1089
2002
|
}
|
|
1090
2003
|
function hasMigratableSettings(sources) {
|
|
1091
|
-
const { existingSettingsChanged,
|
|
1092
|
-
return Boolean(existingSettingsChanged ||
|
|
2004
|
+
const { existingSettingsChanged, npmrcKeys, packageJsonKeys, projectNpmrcKeys, yarnResolutions } = sources;
|
|
2005
|
+
return Boolean(existingSettingsChanged || npmrcKeys.length || packageJsonKeys.length || projectNpmrcKeys.length || yarnResolutions);
|
|
1093
2006
|
}
|
|
1094
2007
|
function assertCanMigrateRuntime(runtimeVersion, packageJsonExists) {
|
|
1095
2008
|
if (runtimeVersion && !packageJsonExists) throw new Error("Cannot migrate the removed Node.js runtime setting without a package.json file.");
|
|
@@ -1112,10 +2025,13 @@ function reportMigrationChanges(showChanges, before = {}, after = {}) {
|
|
|
1112
2025
|
* @param rawOptions.cwd - Current working directory (default: process.cwd())
|
|
1113
2026
|
* @param rawOptions.cleanNpmrc - Whether to remove pnpm settings from `.npmrc` (default: true)
|
|
1114
2027
|
* @param rawOptions.cleanPackageJson - Whether to remove pnpm field from `package.json` (default: true)
|
|
2028
|
+
* @param rawOptions.compatibility - Target pnpm major, or automatic detection (default: auto)
|
|
1115
2029
|
* @param rawOptions.yarnResolutions - Whether to migrate resolutions field (default: true)
|
|
1116
2030
|
* @param rawOptions.sortKeys - Whether to sort keys in output YAML (default: false)
|
|
1117
2031
|
* @param rawOptions.newlineBetween - Add newlines between root keys (default: true)
|
|
2032
|
+
* @param rawOptions.replaceDeprecated - Whether to replace deprecated settings (default: false)
|
|
1118
2033
|
* @param rawOptions.showChanges - Show settings changes after migration (default: true)
|
|
2034
|
+
* @param rawOptions.strategy - Conflict handling strategy (default: merge)
|
|
1119
2035
|
*
|
|
1120
2036
|
* @returns A promise that resolves when migration is complete
|
|
1121
2037
|
*
|
|
@@ -1152,12 +2068,18 @@ async function migratePnpmSettings(rawOptions = {}) {
|
|
|
1152
2068
|
const [packageJson, pnpmWorkspace] = await Promise.all([readPackageJson(packageJsonPath, packageJsonExists), readPnpmWorkspace(pnpmWorkspaceYamlPath, pnpmWorkspaceExists)]);
|
|
1153
2069
|
const pnpmWorkspaceBefore = structuredClone(pnpmWorkspace.value);
|
|
1154
2070
|
const compatibility = resolveCompatibilityTarget(options.compatibility, packageJson.value.packageManager, packageJson.value.devEngines?.packageManager);
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
2071
|
+
assertCompatibleWorkspaceSettings(pnpmWorkspace.value, compatibility);
|
|
2072
|
+
const sources = await resolveMigrationSources({
|
|
2073
|
+
compatibility,
|
|
2074
|
+
cwd: options.cwd,
|
|
2075
|
+
npmrcExists,
|
|
2076
|
+
npmrcPath,
|
|
2077
|
+
packageJson: packageJson.value,
|
|
2078
|
+
pnpmWorkspace: pnpmWorkspace.value,
|
|
2079
|
+
strategy: options.strategy,
|
|
2080
|
+
yarnResolutions: options.yarnResolutions
|
|
2081
|
+
});
|
|
2082
|
+
const { incomingSettings } = sources;
|
|
1161
2083
|
const [existingNormalization, incomingNormalization] = await Promise.all([normalizeIncomingSettings(pnpmWorkspace.value, {
|
|
1162
2084
|
compatibility,
|
|
1163
2085
|
cwd: options.cwd,
|
|
@@ -1174,9 +2096,10 @@ async function migratePnpmSettings(rawOptions = {}) {
|
|
|
1174
2096
|
if (runtimeMigration.warning) consola$1.warn(runtimeMigration.warning);
|
|
1175
2097
|
if (!hasMigratableSettings({
|
|
1176
2098
|
existingSettingsChanged: existingNormalization.changed,
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
2099
|
+
npmrcKeys: sources.npmrc.keys,
|
|
2100
|
+
packageJsonKeys: sources.packageJson.keys,
|
|
2101
|
+
projectNpmrcKeys: sources.projectNpmrcs.projects.flatMap((project) => project.migratable.keys),
|
|
2102
|
+
yarnResolutions: sources.packageJson.yarnResolutions
|
|
1180
2103
|
})) {
|
|
1181
2104
|
consola$1.warn("No pnpm settings fields to migrate");
|
|
1182
2105
|
reportMigrationChanges(options.showChanges);
|
|
@@ -1189,18 +2112,25 @@ async function migratePnpmSettings(rawOptions = {}) {
|
|
|
1189
2112
|
sortKeys: options.sortKeys
|
|
1190
2113
|
});
|
|
1191
2114
|
const finalYamlContent = formatRootSpacing(pnpmWorkspace.document.toString({ indent: pnpmWorkspace.indent }), options.newlineBetween);
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
2115
|
+
await persistMigration({
|
|
2116
|
+
cleanNpmrc: options.cleanNpmrc,
|
|
2117
|
+
cleanPackageJson: options.cleanPackageJson,
|
|
2118
|
+
compatibility,
|
|
2119
|
+
finalSettings: pnpmWorkspaceResult,
|
|
2120
|
+
incomingSettings,
|
|
2121
|
+
npmrc: sources.npmrc,
|
|
2122
|
+
npmrcExists,
|
|
2123
|
+
npmrcPath,
|
|
2124
|
+
packageJson,
|
|
2125
|
+
packageJsonExists,
|
|
2126
|
+
packageJsonPath,
|
|
2127
|
+
packageJsonRuntimeChanged: runtimeMigration.changed,
|
|
2128
|
+
packageJsonSettings: sources.packageJson,
|
|
2129
|
+
pnpmWorkspaceContent: finalYamlContent,
|
|
2130
|
+
pnpmWorkspacePath: pnpmWorkspaceYamlPath,
|
|
2131
|
+
projectNpmrcs: sources.projectNpmrcs,
|
|
2132
|
+
runtimeVersion
|
|
2133
|
+
});
|
|
1204
2134
|
reportMigrationChanges(options.showChanges, pnpmWorkspaceBefore, pnpmWorkspaceResult);
|
|
1205
2135
|
} catch (err) {
|
|
1206
2136
|
consola$1.error("Failed to migrate pnpm settings:", err);
|