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/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 { isDeepStrictEqual } from "node:util";
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.8.1";
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+`: excludes auth/registry keys because pnpm still reads them from
766
- * `.npmrc`.
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) nodeDownloadMirrors[nodeMirrorMatch.groups.channel] = String(value);
793
- else migratable[key] = value;
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) settings.nodeDownloadMirrors = nodeDownloadMirrors;
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/utils/config.ts
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
- * Resolve the indentation width for a YAML document.
1837
+ * Persist destinations before pruning any legacy source file.
1003
1838
  *
1004
- * @param content - Raw YAML content
1839
+ * @param options - Destination files, selected sources, and cleanup policy
1005
1840
  *
1006
- * @returns Detected indentation width or the project default
1841
+ * @returns A promise that resolves after destinations and sources are persisted
1007
1842
  */
1008
- function resolveYamlIndent(content) {
1009
- const detectedIndent = detectIndent(content).amount;
1010
- return detectedIndent > 0 ? detectedIndent : 2;
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 and parse a package manifest when it exists.
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
- * @returns Parsed package manifest and detected indentation
1884
+ * @param options - Migration target and source configuration
1019
1885
  *
1020
- * @throws {SyntaxError} When the package manifest contains invalid JSON
1886
+ * @returns Resolved source metadata and merged incoming settings
1021
1887
  */
1022
- async function readPackageJson(path, exists) {
1023
- if (!exists) return {
1024
- indent: 2,
1025
- value: {}
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
- const content = await fsReadFile(path);
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
- indent: detectIndent(content).indent,
1030
- value: JSON.parse(content)
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
- * Read and parse a pnpm workspace manifest when it exists.
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
- async function readPnpmWorkspace(path, exists) {
1044
- if (!exists) return {
1045
- document: new Document({}),
1046
- indent: 2,
1047
- value: {}
1048
- };
1049
- const content = await fsReadFile(path);
1050
- const document = parseDocument(content);
1051
- const [parseError] = document.errors;
1052
- if (parseError) throw parseError;
1053
- const parsedValue = document.toJS();
1054
- if (parsedValue !== null && !isMap(document.contents)) throw new TypeError("pnpm-workspace.yaml must contain a root mapping.");
1055
- if (parsedValue === null) document.contents = document.createNode({});
1056
- return {
1057
- document,
1058
- indent: resolveYamlIndent(content),
1059
- value: parsedValue ?? {}
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 migratable pnpm settings from a package manifest.
1960
+ * Resolve and normalize migration options with defaults.
1064
1961
  *
1065
- * Yarn resolutions are merged into pnpm overrides when enabled. Empty
1066
- * overrides are removed from the result.
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 packageJson - Parsed package manifest
1069
- * @param yarnResolutions - Whether to convert Yarn resolutions to pnpm overrides
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
- * @returns Pnpm workspace settings resolved from the package manifest
1975
+ * // Override specific options
1976
+ * const opts = resolveOptions({ sortKeys: true, cleanNpmrc: false })
1977
+ * // { cleanNpmrc: false, cleanPackageJson: true, sortKeys: true, ... }
1978
+ * ```
1072
1979
  */
1073
- function resolvePackageJsonSettings(packageJson, yarnResolutions) {
1074
- const pnpmSettings = yarnResolutions && packageJson.resolutions ? {
1075
- ...packageJson.pnpm,
1076
- overrides: defu(packageJson.pnpm?.overrides, packageJson.resolutions)
1077
- } : { ...packageJson.pnpm };
1078
- if (pnpmSettings.overrides && !Object.keys(pnpmSettings.overrides).length) delete pnpmSettings.overrides;
1079
- return pnpmSettings;
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, packageJson, pnpmSettingsInNpmrc, yarnResolutions } = sources;
1092
- return Boolean(existingSettingsChanged || packageJson.pnpm || yarnResolutions && packageJson.resolutions || Object.keys(pnpmSettingsInNpmrc).length);
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
- const npmrcMigratable = npmrcExists ? await readMigratableNpmrc(npmrcPath, compatibility) : {
1156
- keys: [],
1157
- settings: {}
1158
- };
1159
- const pnpmSettingsInNpmrc = npmrcMigratable.settings;
1160
- const incomingSettings = mergeByStrategy(resolvePackageJsonSettings(packageJson.value, options.yarnResolutions), pnpmSettingsInNpmrc, "merge");
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
- packageJson: packageJson.value,
1178
- pnpmSettingsInNpmrc,
1179
- yarnResolutions: options.yarnResolutions
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
- let packageJsonChanged = runtimeMigration.changed;
1193
- if (packageJsonExists && options.cleanPackageJson) {
1194
- if (packageJson.value.pnpm) packageJsonChanged = true;
1195
- delete packageJson.value.pnpm;
1196
- if (options.yarnResolutions && packageJson.value.resolutions) {
1197
- packageJsonChanged = true;
1198
- delete packageJson.value.resolutions;
1199
- }
1200
- }
1201
- await fsWriteFile(pnpmWorkspaceYamlPath, finalYamlContent);
1202
- if (packageJsonExists && packageJsonChanged) await fsWriteFile(packageJsonPath, JSON.stringify(packageJson.value, null, packageJson.indent));
1203
- if (npmrcExists && options.cleanNpmrc) await pruneNpmrc(npmrcPath, compatibility, npmrcMigratable.keys);
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);