pnpm-settings-migrator 0.6.1 → 0.8.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,19 +1,20 @@
1
1
  import process from "node:process";
2
2
  import { cac } from "cac";
3
3
  import consola$1, { consola } from "consola";
4
- import { isPlainObject, isUndefined, pick } from "@ntnyq/utils";
5
- import { defu } from "defu";
6
- import detectIndent from "detect-indent";
7
4
  import { resolve } from "pathe";
8
- import { Document, parse } from "yaml";
9
- import { access, readFile, writeFile } from "node:fs/promises";
5
+ import { Document, parse, stringify } from "yaml";
6
+ import { access, readFile, rm, writeFile } from "node:fs/promises";
10
7
  import { getColor } from "consola/utils";
8
+ import { isPlainObject, isUndefined, pick } from "@ntnyq/utils";
9
+ import { defu } from "defu";
11
10
  import camelcaseKeys from "camelcase-keys";
12
11
  import { readIniFile } from "read-ini-file";
13
12
  import { kebabCase } from "uncase";
13
+ import { isDeepStrictEqual } from "node:util";
14
+ import detectIndent from "detect-indent";
14
15
  //#region package.json
15
16
  var name = "pnpm-settings-migrator";
16
- var version = "0.6.1";
17
+ var version = "0.8.0";
17
18
  //#endregion
18
19
  //#region src/constants.ts
19
20
  const NPMRC = ".npmrc";
@@ -60,6 +61,7 @@ const PNPM_SETTINGS_FIELDS = [
60
61
  "httpProxy",
61
62
  "httpsProxy",
62
63
  "ignoredBuiltDependencies",
64
+ "ignoreDepScripts",
63
65
  "ignoreDeps",
64
66
  "ignoredOptionalDependencies",
65
67
  "ignorePatchFailures",
@@ -87,11 +89,14 @@ const PNPM_SETTINGS_FIELDS = [
87
89
  "overrides",
88
90
  "packageExtensions",
89
91
  "packageImportMethod",
92
+ "packageManagerStrict",
93
+ "packageManagerStrictVersion",
90
94
  "patchedDependencies",
91
95
  "peerDependencyRules",
92
96
  "peersSuffixMaxLength",
93
97
  "pnpmfile",
94
98
  "pnpmfileIgnorePkgManifest",
99
+ "pmOnFail",
95
100
  "preferFrozenLockfile",
96
101
  "preferWorkspacePackages",
97
102
  "production",
@@ -130,11 +135,17 @@ const PNPM_SETTINGS_FIELDS = [
130
135
  ];
131
136
  const PNPM_V11_REMOVED_SETTINGS = [
132
137
  "allowNonAppliedPatches",
138
+ "executionEnv",
139
+ "ignoreDepScripts",
133
140
  "ignorePatchFailures",
134
141
  "ignoredBuiltDependencies",
142
+ "managePackageManagerVersions",
135
143
  "neverBuiltDependencies",
136
144
  "onlyBuiltDependencies",
137
- "onlyBuiltDependenciesFile"
145
+ "onlyBuiltDependenciesFile",
146
+ "packageManagerStrict",
147
+ "packageManagerStrictVersion",
148
+ "useNodeVersion"
138
149
  ];
139
150
  //#endregion
140
151
  //#region src/options.ts
@@ -148,6 +159,7 @@ const DEFAULT_OPTIONS = {
148
159
  cwd: process.cwd(),
149
160
  newlineBetween: true,
150
161
  replaceDeprecated: false,
162
+ showChanges: true,
151
163
  sortKeys: false,
152
164
  strategy: "merge",
153
165
  yarnResolutions: true
@@ -160,8 +172,19 @@ const VALID_STRATEGIES = [
160
172
  const VALID_COMPATIBILITIES = [
161
173
  "auto",
162
174
  "v10",
163
- "v11"
175
+ "v11",
176
+ "v12"
164
177
  ];
178
+ function resolveCompatibility(compatibility) {
179
+ if (!compatibility) return DEFAULT_OPTIONS.compatibility;
180
+ if (VALID_COMPATIBILITIES.includes(compatibility)) return compatibility;
181
+ throw new Error(`Invalid compatibility: ${compatibility}. Expected one of: ${VALID_COMPATIBILITIES.join(", ")}`);
182
+ }
183
+ function resolveStrategy(strategy) {
184
+ if (!strategy) return DEFAULT_OPTIONS.strategy;
185
+ if (VALID_STRATEGIES.includes(strategy)) return strategy;
186
+ throw new Error(`Invalid strategy: ${strategy}. Expected one of: ${VALID_STRATEGIES.join(", ")}`);
187
+ }
165
188
  /**
166
189
  * Resolve and normalize migration options with defaults.
167
190
  *
@@ -189,6 +212,7 @@ function resolveOptions(options = {}) {
189
212
  compatibility: resolveCompatibility(options.compatibility),
190
213
  cwd: options.cwd ?? DEFAULT_OPTIONS.cwd,
191
214
  newlineBetween: options.newlineBetween ?? DEFAULT_OPTIONS.newlineBetween,
215
+ showChanges: options.showChanges ?? DEFAULT_OPTIONS.showChanges,
192
216
  sortKeys: options.sortKeys ?? DEFAULT_OPTIONS.sortKeys,
193
217
  strategy: resolveStrategy(options.strategy),
194
218
  yarnResolutions: options.yarnResolutions ?? DEFAULT_OPTIONS.yarnResolutions,
@@ -196,83 +220,269 @@ function resolveOptions(options = {}) {
196
220
  replaceDeprecated: options.replaceDeprecated ?? DEFAULT_OPTIONS.replaceDeprecated
197
221
  };
198
222
  }
199
- function resolveCompatibility(compatibility) {
200
- if (!compatibility) return DEFAULT_OPTIONS.compatibility;
201
- if (VALID_COMPATIBILITIES.includes(compatibility)) return compatibility;
202
- throw new Error(`Invalid compatibility: ${compatibility}. Expected one of: ${VALID_COMPATIBILITIES.join(", ")}`);
203
- }
204
- function resolveStrategy(strategy) {
205
- if (!strategy) return DEFAULT_OPTIONS.strategy;
206
- if (VALID_STRATEGIES.includes(strategy)) return strategy;
207
- throw new Error(`Invalid strategy: ${strategy}. Expected one of: ${VALID_STRATEGIES.join(", ")}`);
208
- }
209
223
  //#endregion
210
224
  //#region src/utils/fs.ts
211
225
  /**
212
- * Check if a path exists
213
- * @param path - given path
214
- * @returns `true` if exists, false otherwise
226
+ * Check whether a filesystem path exists.
227
+ *
228
+ * @param path - Filesystem path to check
229
+ *
230
+ * @returns `true` when the path is accessible, otherwise `false`
215
231
  */
216
232
  async function fsExists(path) {
217
233
  return access(path).then(() => true).catch(() => false);
218
234
  }
219
235
  /**
220
- * Read file content
221
- * @param path - file path
222
- * @returns content of given file
236
+ * Read a UTF-8 text file.
237
+ *
238
+ * @param path - Filesystem path to read
239
+ *
240
+ * @returns Text content of the file
223
241
  */
224
242
  async function fsReadFile(path) {
225
243
  return await readFile(path, "utf-8");
226
244
  }
227
245
  /**
228
- * Write file
229
- * @param path - file path
230
- * @param content - file content
246
+ * Remove a file if it exists.
247
+ *
248
+ * @param path - Filesystem path to remove
249
+ *
250
+ * @returns A promise that resolves after the file is removed
251
+ */
252
+ async function fsRemoveFile(path) {
253
+ await rm(path, { force: true });
254
+ }
255
+ /**
256
+ * Write a normalized UTF-8 text file with one trailing newline.
257
+ *
258
+ * @param path - Filesystem path to write
259
+ * @param content - Text content to write
260
+ *
261
+ * @returns A promise that resolves after the file is written
231
262
  */
232
263
  async function fsWriteFile(path, content) {
233
264
  await writeFile(path, `${content.trimEnd()}\n`, "utf-8");
234
265
  }
235
266
  getColor("cyan");
236
267
  getColor("yellow");
268
+ /**
269
+ * Apply dim terminal styling to a value.
270
+ */
237
271
  const dim = getColor("dim");
272
+ /**
273
+ * Apply green terminal styling to a value.
274
+ */
238
275
  const green = getColor("green");
276
+ /**
277
+ * Apply red terminal styling to a value.
278
+ */
239
279
  const red = getColor("red");
280
+ /**
281
+ * Apply bold terminal styling to a value.
282
+ */
240
283
  const bold = getColor("bold");
284
+ /**
285
+ * Apply magenta terminal styling to a value.
286
+ */
241
287
  const magenta = getColor("magenta");
242
288
  //#endregion
243
- //#region src/utils/merge.ts
289
+ //#region src/utils/compatibility.ts
244
290
  /**
245
- * Merge two objects based on the specified strategy.
291
+ * Legacy build settings that can be replaced for pnpm v10.
292
+ */
293
+ const PNPM_REPLACEABLE_IN_V10_SETTINGS = [
294
+ "allowNonAppliedPatches",
295
+ "ignoredBuiltDependencies",
296
+ "neverBuiltDependencies",
297
+ "onlyBuiltDependencies",
298
+ "onlyBuiltDependenciesFile"
299
+ ];
300
+ /**
301
+ * Read packages listed by the legacy `onlyBuiltDependenciesFile` setting.
246
302
  *
247
- * @param existing - Existing pnpm-workspace.yaml content
248
- * @param incoming - New settings from package.json and .npmrc
249
- * @param strategy - Merge strategy to use
303
+ * @param cwd - Working directory used to resolve the referenced file
304
+ * @param file - Relative path stored in `onlyBuiltDependenciesFile`
250
305
  *
251
- * @returns Merged pnpm workspace configuration
306
+ * @returns Package names read from the referenced JSON file
252
307
  *
253
- * @example
254
- * ```ts
255
- * // Discard strategy - keep existing values
256
- * mergeByStrategy({ packages: ['a'] }, { packages: ['b'] }, 'discard')
257
- * // => { packages: ['a'] }
308
+ * @throws {TypeError} When the referenced JSON value is not a string array
309
+ * @throws {SyntaxError} When the referenced file contains invalid JSON
310
+ */
311
+ async function readOnlyBuiltDependenciesFile(cwd, file) {
312
+ if (!file) return [];
313
+ const path = resolve(cwd, file);
314
+ const value = JSON.parse(await fsReadFile(path));
315
+ if (!Array.isArray(value) || !value.every((item) => typeof item === "string")) throw new TypeError(`Invalid onlyBuiltDependenciesFile: ${file}. Expected a JSON array of package names.`);
316
+ return value;
317
+ }
318
+ /**
319
+ * Build an `allowBuilds` map from legacy build-script settings.
258
320
  *
259
- * // Merge strategy - combine arrays with deduplication
260
- * mergeByStrategy({ packages: ['a'] }, { packages: ['b'] }, 'merge')
261
- * // => { packages: ['a', 'b'] }
321
+ * @param incomingSettings - Settings containing legacy build declarations
322
+ * @param cwd - Working directory used to resolve referenced files
262
323
  *
263
- * // Overwrite strategy - use incoming values
264
- * mergeByStrategy({ packages: ['a'] }, { packages: ['b'] }, 'overwrite')
265
- * // => { packages: ['b'] }
266
- * ```
324
+ * @returns Package-to-permission map, or `undefined` when no entries exist
267
325
  */
268
- function mergeByStrategy(existing, incoming, strategy) {
269
- switch (strategy) {
270
- case "discard": return discardMerge(existing, incoming);
271
- case "merge": return mergeWithArrayDedupe(existing, incoming);
272
- case "overwrite": return discardMerge(incoming, existing);
273
- default: return defu(existing, incoming);
326
+ async function collectAllowBuildsFromLegacy(incomingSettings, cwd) {
327
+ const allowBuilds = {};
328
+ const allowedFromFile = await readOnlyBuiltDependenciesFile(cwd, incomingSettings.onlyBuiltDependenciesFile);
329
+ for (const name of [...incomingSettings.onlyBuiltDependencies || [], ...allowedFromFile]) allowBuilds[name] = true;
330
+ for (const name of incomingSettings.ignoredBuiltDependencies || []) allowBuilds[name] = false;
331
+ for (const name of incomingSettings.neverBuiltDependencies || []) allowBuilds[name] = false;
332
+ return Object.keys(allowBuilds).length ? allowBuilds : void 0;
333
+ }
334
+ /**
335
+ * Resolve the pnpm v11 replacement for legacy package-manager settings.
336
+ *
337
+ * @param settings - Settings containing legacy package-manager declarations
338
+ *
339
+ * @returns Resolved `pmOnFail` behavior, or `undefined` when not configured
340
+ */
341
+ function resolvePmOnFail(settings) {
342
+ if (settings.pmOnFail) return settings.pmOnFail;
343
+ if (settings.packageManagerStrictVersion === true) return "error";
344
+ if (settings.packageManagerStrict === false) return "warn";
345
+ if (settings.managePackageManagerVersions === false) return "ignore";
346
+ if (settings.managePackageManagerVersions === true) return "download";
347
+ }
348
+ /**
349
+ * Rename legacy audit settings while preserving values for manual ID updates.
350
+ *
351
+ * @param settings - Settings whose audit configuration may be normalized
352
+ * @param warnings - Warning collection updated with manual follow-up guidance
353
+ *
354
+ * @returns Nothing; the settings and warning collection are mutated in place
355
+ */
356
+ function normalizeAuditConfig(settings, warnings) {
357
+ const { auditConfig } = settings;
358
+ const ignoreCves = auditConfig?.ignoreCves;
359
+ if (!ignoreCves) return;
360
+ auditConfig.ignoreGhsas ??= ignoreCves;
361
+ Reflect.deleteProperty(auditConfig, "ignoreCves");
362
+ warnings.push("auditConfig.ignoreCves was renamed to auditConfig.ignoreGhsas; replace each CVE ID with its corresponding GHSA ID.");
363
+ }
364
+ /**
365
+ * Resolve a removed Node.js runtime setting for migration to `package.json`.
366
+ *
367
+ * @param settings - Settings containing legacy runtime declarations
368
+ * @param warnings - Warning collection updated when declarations conflict
369
+ *
370
+ * @returns Runtime version to migrate, or `undefined` when not configured
371
+ */
372
+ function resolveRuntimeVersion(settings, warnings) {
373
+ const { useNodeVersion } = settings;
374
+ const executionEnvNodeVersion = settings.executionEnv?.nodeVersion;
375
+ if (useNodeVersion && executionEnvNodeVersion && useNodeVersion !== executionEnvNodeVersion) warnings.push(`Both useNodeVersion (${useNodeVersion}) and executionEnv.nodeVersion (${executionEnvNodeVersion}) were found; useNodeVersion takes precedence.`);
376
+ return useNodeVersion || executionEnvNodeVersion;
377
+ }
378
+ /**
379
+ * Normalize settings according to the selected compatibility target.
380
+ *
381
+ * @param incomingSettings - Settings to normalize in place
382
+ * @param options - Compatibility target and normalization context
383
+ *
384
+ * @returns Normalization result, extracted runtime version, and warnings
385
+ */
386
+ async function normalizeIncomingSettings(incomingSettings, options) {
387
+ const { compatibility, cwd, replaceDeprecated } = options;
388
+ const before = JSON.stringify(incomingSettings);
389
+ const warnings = [];
390
+ if (compatibility === "v10" && !replaceDeprecated) return {
391
+ changed: false,
392
+ warnings
393
+ };
394
+ if (incomingSettings.allowNonAppliedPatches !== void 0) incomingSettings.allowUnusedPatches ??= incomingSettings.allowNonAppliedPatches;
395
+ const allowBuildsFromLegacy = await collectAllowBuildsFromLegacy(incomingSettings, cwd);
396
+ if (allowBuildsFromLegacy) incomingSettings.allowBuilds = {
397
+ ...allowBuildsFromLegacy,
398
+ ...incomingSettings.allowBuilds || {}
399
+ };
400
+ if (compatibility === "v10") {
401
+ for (const key of PNPM_REPLACEABLE_IN_V10_SETTINGS) Reflect.deleteProperty(incomingSettings, key);
402
+ return {
403
+ changed: before !== JSON.stringify(incomingSettings),
404
+ warnings
405
+ };
274
406
  }
407
+ const pmOnFail = resolvePmOnFail(incomingSettings);
408
+ if (pmOnFail) incomingSettings.pmOnFail = pmOnFail;
409
+ normalizeAuditConfig(incomingSettings, warnings);
410
+ const runtimeVersion = resolveRuntimeVersion(incomingSettings, warnings);
411
+ if (incomingSettings.ignoreDepScripts !== void 0) warnings.push("ignoreDepScripts was removed in pnpm v11 and has no direct replacement.");
412
+ if (incomingSettings.ignorePatchFailures !== void 0) warnings.push("ignorePatchFailures was removed in pnpm v11; patch failures now always throw.");
413
+ for (const key of PNPM_V11_REMOVED_SETTINGS) Reflect.deleteProperty(incomingSettings, key);
414
+ return {
415
+ changed: before !== JSON.stringify(incomingSettings),
416
+ runtimeVersion,
417
+ warnings
418
+ };
275
419
  }
420
+ //#endregion
421
+ //#region src/utils/compatibility-runtime.ts
422
+ /**
423
+ * Move a removed Node.js runtime setting to `package.json#devEngines.runtime`.
424
+ *
425
+ * @param packageJson - Package manifest to update in place
426
+ * @param runtimeVersion - Node.js runtime version extracted from pnpm settings
427
+ *
428
+ * @returns Migration result indicating whether the manifest changed
429
+ */
430
+ function migrateRuntimeToPackageJson(packageJson, runtimeVersion) {
431
+ if (!runtimeVersion) return { changed: false };
432
+ if (packageJson.devEngines?.runtime) return {
433
+ changed: false,
434
+ warning: "A devEngines.runtime declaration already exists; the removed pnpm Node.js runtime setting was not applied."
435
+ };
436
+ packageJson.devEngines = {
437
+ ...packageJson.devEngines,
438
+ runtime: {
439
+ name: "node",
440
+ version: runtimeVersion
441
+ }
442
+ };
443
+ return { changed: true };
444
+ }
445
+ /**
446
+ * Select the runtime setting that wins under the configured merge strategy.
447
+ *
448
+ * @param existingVersion - Runtime version from existing workspace settings
449
+ * @param incomingVersion - Runtime version from incoming settings
450
+ * @param strategy - Merge strategy controlling value precedence
451
+ *
452
+ * @returns Selected runtime version, or `undefined` when neither source has one
453
+ */
454
+ function resolveRuntimeVersionByStrategy(existingVersion, incomingVersion, strategy) {
455
+ if (strategy === "overwrite") return incomingVersion ?? existingVersion;
456
+ return existingVersion ?? incomingVersion;
457
+ }
458
+ //#endregion
459
+ //#region src/utils/compatibility-target.ts
460
+ /**
461
+ * pnpm major version that introduced the v11 settings schema.
462
+ */
463
+ const PNPM_V11_MAJOR = 11;
464
+ /**
465
+ * pnpm major version that introduced the v12 compatibility target.
466
+ */
467
+ const PNPM_V12_MAJOR = 12;
468
+ /**
469
+ * Resolve final compatibility target from user option and package manager hint.
470
+ *
471
+ * @param compatibility - Explicit or automatically detected compatibility target
472
+ * @param packageManager - `packageManager` declaration from `package.json`
473
+ * @param devPackageManager - Package manager declaration from `devEngines`
474
+ *
475
+ * @returns Resolved concrete compatibility target
476
+ */
477
+ function resolveCompatibilityTarget(compatibility, packageManager, devPackageManager) {
478
+ if (compatibility !== "auto") return compatibility;
479
+ const match = (packageManager || (devPackageManager?.name === "pnpm" ? `pnpm@${devPackageManager.version}` : void 0))?.match(/^pnpm@[\s<=>^~]*(?<major>\d+)(?:\.|\s|$)/u);
480
+ const major = Number(match?.groups?.major);
481
+ if (major >= PNPM_V12_MAJOR) return "v12";
482
+ return major >= PNPM_V11_MAJOR ? "v11" : "v10";
483
+ }
484
+ //#endregion
485
+ //#region src/utils/merge.ts
276
486
  /**
277
487
  * Merge objects with priority to the first argument.
278
488
  * Only adds keys from second argument that don't exist in first.
@@ -314,8 +524,43 @@ function mergeWithArrayDedupe(existing, incoming) {
314
524
  }
315
525
  return result;
316
526
  }
527
+ /**
528
+ * Merge two objects based on the specified strategy.
529
+ *
530
+ * @param existing - Existing pnpm-workspace.yaml content
531
+ * @param incoming - New settings from package.json and .npmrc
532
+ * @param strategy - Merge strategy to use
533
+ *
534
+ * @returns Merged pnpm workspace configuration
535
+ *
536
+ * @example
537
+ * ```ts
538
+ * // Discard strategy - keep existing values
539
+ * mergeByStrategy({ packages: ['a'] }, { packages: ['b'] }, 'discard')
540
+ * // => { packages: ['a'] }
541
+ *
542
+ * // Merge strategy - combine arrays with deduplication
543
+ * mergeByStrategy({ packages: ['a'] }, { packages: ['b'] }, 'merge')
544
+ * // => { packages: ['a', 'b'] }
545
+ *
546
+ * // Overwrite strategy - use incoming values
547
+ * mergeByStrategy({ packages: ['a'] }, { packages: ['b'] }, 'overwrite')
548
+ * // => { packages: ['b'] }
549
+ * ```
550
+ */
551
+ function mergeByStrategy(existing, incoming, strategy) {
552
+ switch (strategy) {
553
+ case "discard": return discardMerge(existing, incoming);
554
+ case "merge": return mergeWithArrayDedupe(existing, incoming);
555
+ case "overwrite": return discardMerge(incoming, existing);
556
+ default: return defu(existing, incoming);
557
+ }
558
+ }
317
559
  //#endregion
318
560
  //#region src/utils/npmrc.ts
561
+ /**
562
+ * Authentication and registry keys that must remain in `.npmrc`.
563
+ */
319
564
  const NPMRC_AUTH_OR_REGISTRY_KEYS = [
320
565
  "_auth",
321
566
  "_authtoken",
@@ -333,6 +578,48 @@ const NPMRC_AUTH_OR_REGISTRY_KEYS = [
333
578
  "username"
334
579
  ];
335
580
  /**
581
+ * Pattern for pnpm's channel-specific Node.js mirror keys.
582
+ */
583
+ const NODE_MIRROR_KEY_PATTERN = /^node-mirror:(?<channel>.+)$/iu;
584
+ /**
585
+ * Normalize `.npmrc` key for case-insensitive matching.
586
+ *
587
+ * @param key - Raw `.npmrc` key
588
+ *
589
+ * @returns Trimmed lower-case key without an array suffix
590
+ */
591
+ function normalizeNpmrcKey(key) {
592
+ return key.trim().replace(/\[\]$/u, "").toLowerCase();
593
+ }
594
+ /**
595
+ * Extract key name from a raw `.npmrc` line.
596
+ *
597
+ * @param line - Raw line from an `.npmrc` file
598
+ *
599
+ * @returns Normalized key, or `undefined` for comments and invalid lines
600
+ */
601
+ function getNpmrcLineKey(line) {
602
+ const trimmed = line.trim();
603
+ if (!trimmed || trimmed.startsWith(";") || trimmed.startsWith("#")) return;
604
+ const index = trimmed.indexOf("=");
605
+ if (index <= 0) return;
606
+ return normalizeNpmrcKey(trimmed.slice(0, index));
607
+ }
608
+ /**
609
+ * Check whether a `.npmrc` key is auth/registry-related and should stay in
610
+ * pnpm v11 and newer.
611
+ *
612
+ * @param key - Raw or normalized `.npmrc` key
613
+ *
614
+ * @returns `true` when the key must remain in `.npmrc`
615
+ */
616
+ function isNpmrcAuthOrRegistryKey(key) {
617
+ const normalized = normalizeNpmrcKey(key);
618
+ if (normalized.startsWith("//")) return true;
619
+ if (normalized === "registry" || normalized.endsWith(":registry")) return true;
620
+ return NPMRC_AUTH_OR_REGISTRY_KEYS.some((authKey) => normalized === authKey || normalized.endsWith(`:${authKey}`));
621
+ }
622
+ /**
336
623
  * Remove pnpm-related settings from `.npmrc` file.
337
624
  *
338
625
  * This function reads the `.npmrc` file, filters out all lines that start with
@@ -340,6 +627,8 @@ const NPMRC_AUTH_OR_REGISTRY_KEYS = [
340
627
  * writes the cleaned content back to the file.
341
628
  *
342
629
  * @param path - Absolute path to the `.npmrc` file
630
+ * @param compatibility - Concrete pnpm compatibility target
631
+ * @param migratedKeys - Original `.npmrc` keys that were migrated
343
632
  *
344
633
  * @returns A promise that resolves when the file has been pruned
345
634
  *
@@ -351,87 +640,261 @@ const NPMRC_AUTH_OR_REGISTRY_KEYS = [
351
640
  * ```
352
641
  */
353
642
  async function pruneNpmrc(path, compatibility = "v10", migratedKeys = []) {
354
- const pnpmSettingsFields = PNPM_SETTINGS_FIELDS.map((v) => kebabCase(v));
643
+ const pnpmSettingsFields = PNPM_SETTINGS_FIELDS.map((field) => kebabCase(field));
355
644
  const migratedKeySet = new Set(migratedKeys.map(normalizeNpmrcKey));
356
- await fsWriteFile(path, (await fsReadFile(path)).split(/\r?\n/).filter((line) => {
645
+ const updatedContent = (await fsReadFile(path)).split(/\r?\n/u).filter((line) => {
357
646
  const key = getNpmrcLineKey(line);
358
647
  if (!key) return true;
359
- if (compatibility === "v11") return !migratedKeySet.has(normalizeNpmrcKey(key));
360
- return !pnpmSettingsFields.some((v) => key.startsWith(v));
361
- }).join("\n"));
648
+ if (compatibility !== "v10") return !migratedKeySet.has(normalizeNpmrcKey(key));
649
+ return !pnpmSettingsFields.some((field) => key.startsWith(field));
650
+ }).join("\n").trimEnd();
651
+ if (compatibility !== "v10" && !updatedContent.trim()) {
652
+ await fsRemoveFile(path);
653
+ return;
654
+ }
655
+ await fsWriteFile(path, updatedContent);
362
656
  }
363
657
  /**
364
- * Read and parse `.npmrc` file with camelCase key conversion.
658
+ * Read `.npmrc` and return settings that should be migrated into workspace config.
365
659
  *
366
- * This function reads an `.npmrc` INI-format file and parses it into an object.
367
- * All keys are automatically converted from kebab-case to camelCase for easier
368
- * JavaScript consumption (e.g., `allow-builds` → `allowBuilds`).
660
+ * - `v10`: returns settings from the legacy whitelist.
661
+ * - `v11+`: excludes auth/registry keys because pnpm still reads them from
662
+ * `.npmrc`.
369
663
  *
370
664
  * @param path - Absolute path to the `.npmrc` file
665
+ * @param compatibility - Concrete pnpm compatibility target
371
666
  *
372
- * @returns A promise that resolves to the parsed `.npmrc` configuration object with camelCase keys
667
+ * @returns Migratable settings and their original `.npmrc` keys
373
668
  *
374
669
  * @throws {Error} When file reading or INI parsing fails
375
- *
376
- * @example
377
- * ```ts
378
- * const config = await readNpmrc('/path/to/.npmrc')
379
- * // config.allowBuilds, config.packageExtensions, etc.
380
- * ```
381
- */
382
- /**
383
- * Read `.npmrc` and return settings that should be migrated into workspace config.
384
- *
385
- * - `v10`: returns all keys (legacy whitelist filtering happens in caller).
386
- * - `v11`: excludes auth/registry keys because pnpm still reads them from `.npmrc`.
387
670
  */
388
671
  async function readMigratableNpmrc(path, compatibility) {
389
672
  const raw = await readIniFile(path);
390
- if (compatibility === "v10") return {
391
- keys: Object.keys(raw),
392
- settings: camelcaseKeys(raw)
393
- };
673
+ if (compatibility === "v10") {
674
+ const settings = camelcaseKeys(raw);
675
+ return {
676
+ keys: PNPM_SETTINGS_FIELDS,
677
+ settings: pick(settings, PNPM_SETTINGS_FIELDS)
678
+ };
679
+ }
394
680
  const migratable = {};
395
681
  const keys = [];
682
+ const nodeDownloadMirrors = {};
396
683
  for (const [key, value] of Object.entries(raw)) if (!isNpmrcAuthOrRegistryKey(key)) {
397
- migratable[key] = value;
398
684
  keys.push(key);
685
+ const nodeMirrorMatch = normalizeNpmrcKey(key).match(NODE_MIRROR_KEY_PATTERN);
686
+ if (nodeMirrorMatch?.groups?.channel) nodeDownloadMirrors[nodeMirrorMatch.groups.channel] = String(value);
687
+ else migratable[key] = value;
399
688
  }
689
+ const settings = camelcaseKeys(migratable);
690
+ if (Object.keys(nodeDownloadMirrors).length) settings.nodeDownloadMirrors = nodeDownloadMirrors;
400
691
  return {
401
692
  keys,
402
- settings: camelcaseKeys(migratable)
693
+ settings
403
694
  };
404
695
  }
405
- async function readNpmrc(path) {
406
- return camelcaseKeys(await readIniFile(path));
696
+ //#endregion
697
+ //#region src/utils/settings-change.ts
698
+ /**
699
+ * Collect changed root settings between two workspace configurations.
700
+ *
701
+ * @param before - Workspace settings before migration
702
+ * @param after - Workspace settings after migration
703
+ *
704
+ * @returns Changed root settings with their before and after values
705
+ */
706
+ function collectSettingsChanges(before, after) {
707
+ const beforeSettings = { ...before };
708
+ const afterSettings = { ...after };
709
+ const keys = /* @__PURE__ */ new Set([...Object.keys(beforeSettings), ...Object.keys(afterSettings)]);
710
+ return Array.from(keys).filter((key) => !isDeepStrictEqual(beforeSettings[key], afterSettings[key])).map((key) => ({
711
+ after: afterSettings[key],
712
+ before: beforeSettings[key],
713
+ key
714
+ }));
407
715
  }
408
716
  /**
409
- * Extract key name from a raw `.npmrc` line.
717
+ * Create a line-based diff using the longest common subsequence.
718
+ *
719
+ * @param beforeLines - YAML lines before migration
720
+ * @param afterLines - YAML lines after migration
721
+ *
722
+ * @returns Classified diff lines in display order
410
723
  */
411
- function getNpmrcLineKey(line) {
412
- const trimmed = line.trim();
413
- if (!trimmed || trimmed.startsWith(";") || trimmed.startsWith("#")) return;
414
- const index = trimmed.indexOf("=");
415
- if (index <= 0) return;
416
- return normalizeNpmrcKey(trimmed.slice(0, index));
724
+ function createLineDiff(beforeLines, afterLines) {
725
+ const lengths = Array.from({ length: beforeLines.length + 1 }, () => Array(afterLines.length + 1).fill(0));
726
+ for (let beforeIndex = beforeLines.length - 1; beforeIndex >= 0; beforeIndex--) for (let afterIndex = afterLines.length - 1; afterIndex >= 0; afterIndex--) lengths[beforeIndex][afterIndex] = beforeLines[beforeIndex] === afterLines[afterIndex] ? lengths[beforeIndex + 1][afterIndex + 1] + 1 : Math.max(lengths[beforeIndex + 1][afterIndex], lengths[beforeIndex][afterIndex + 1]);
727
+ const diff = [];
728
+ let beforeIndex = 0;
729
+ let afterIndex = 0;
730
+ while (beforeIndex < beforeLines.length && afterIndex < afterLines.length) if (beforeLines[beforeIndex] === afterLines[afterIndex]) {
731
+ diff.push({
732
+ kind: "unchanged",
733
+ value: beforeLines[beforeIndex]
734
+ });
735
+ beforeIndex++;
736
+ afterIndex++;
737
+ } else if (lengths[beforeIndex + 1][afterIndex] >= lengths[beforeIndex][afterIndex + 1]) {
738
+ diff.push({
739
+ kind: "removed",
740
+ value: beforeLines[beforeIndex]
741
+ });
742
+ beforeIndex++;
743
+ } else {
744
+ diff.push({
745
+ kind: "added",
746
+ value: afterLines[afterIndex]
747
+ });
748
+ afterIndex++;
749
+ }
750
+ while (beforeIndex < beforeLines.length) {
751
+ diff.push({
752
+ kind: "removed",
753
+ value: beforeLines[beforeIndex]
754
+ });
755
+ beforeIndex++;
756
+ }
757
+ while (afterIndex < afterLines.length) {
758
+ diff.push({
759
+ kind: "added",
760
+ value: afterLines[afterIndex]
761
+ });
762
+ afterIndex++;
763
+ }
764
+ return diff;
417
765
  }
418
766
  /**
419
- * Check whether a `.npmrc` key is auth/registry-related and should stay in v11.
767
+ * Format one root setting as YAML lines.
768
+ *
769
+ * @param key - Root setting key
770
+ * @param value - Setting value, or `undefined` when the key is absent
771
+ *
772
+ * @returns YAML lines for the setting, or an empty array when absent
420
773
  */
421
- function isNpmrcAuthOrRegistryKey(key) {
422
- const normalized = normalizeNpmrcKey(key);
423
- if (normalized.startsWith("//")) return true;
424
- if (normalized === "registry" || normalized.endsWith(":registry")) return true;
425
- return NPMRC_AUTH_OR_REGISTRY_KEYS.some((v) => normalized === v || normalized.endsWith(`:${v}`));
774
+ function formatSettingLines(key, value) {
775
+ if (value === void 0) return [];
776
+ return stringify({ [key]: value }).trimEnd().split("\n");
426
777
  }
427
778
  /**
428
- * Normalize `.npmrc` key for case-insensitive matching.
779
+ * Format a setting change as a GitHub-style YAML diff.
780
+ *
781
+ * @param change - Root setting change to format
782
+ *
783
+ * @returns Classified YAML diff lines
429
784
  */
430
- function normalizeNpmrcKey(key) {
431
- return key.trim().replace(/\[\]$/, "").toLowerCase();
785
+ function createSettingsDiffLines(change) {
786
+ return createLineDiff(formatSettingLines(change.key, change.before), formatSettingLines(change.key, change.after));
787
+ }
788
+ /**
789
+ * Print a summary and GitHub-style YAML diff for every changed setting.
790
+ *
791
+ * @param changes - Root setting changes to report
792
+ *
793
+ * @returns Nothing; output is written through consola
794
+ */
795
+ function reportSettingsChanges(changes) {
796
+ consola$1.info(`${changes.length} ${changes.length === 1 ? "setting" : "settings"} changed`);
797
+ for (const [index, change] of changes.entries()) {
798
+ if (index > 0) consola$1.log("");
799
+ for (const line of createSettingsDiffLines(change)) if (line.kind === "added") consola$1.log(green(`+ ${line.value}`));
800
+ else if (line.kind === "removed") consola$1.log(red(`- ${line.value}`));
801
+ else consola$1.log(dim(` ${line.value}`));
802
+ }
803
+ }
804
+ //#endregion
805
+ //#region src/utils/config.ts
806
+ /**
807
+ * Resolve the indentation width for a YAML document.
808
+ *
809
+ * @param content - Raw YAML content
810
+ *
811
+ * @returns Detected indentation width or the project default
812
+ */
813
+ function resolveYamlIndent(content) {
814
+ const detectedIndent = detectIndent(content).amount;
815
+ return detectedIndent > 0 ? detectedIndent : 2;
816
+ }
817
+ /**
818
+ * Read and parse a package manifest when it exists.
819
+ *
820
+ * @param path - Absolute path to `package.json`
821
+ * @param exists - Whether the package manifest exists
822
+ *
823
+ * @returns Parsed package manifest and detected indentation
824
+ *
825
+ * @throws {SyntaxError} When the package manifest contains invalid JSON
826
+ */
827
+ async function readPackageJson(path, exists) {
828
+ if (!exists) return {
829
+ indent: 2,
830
+ value: {}
831
+ };
832
+ const content = await fsReadFile(path);
833
+ return {
834
+ indent: detectIndent(content).indent,
835
+ value: JSON.parse(content)
836
+ };
837
+ }
838
+ /**
839
+ * Read and parse a pnpm workspace manifest when it exists.
840
+ *
841
+ * @param path - Absolute path to `pnpm-workspace.yaml`
842
+ * @param exists - Whether the workspace manifest exists
843
+ *
844
+ * @returns Parsed workspace settings and detected indentation
845
+ *
846
+ * @throws {Error} When the workspace manifest contains invalid YAML
847
+ */
848
+ async function readPnpmWorkspace(path, exists) {
849
+ if (!exists) return {
850
+ indent: 2,
851
+ value: {}
852
+ };
853
+ const content = await fsReadFile(path);
854
+ return {
855
+ indent: resolveYamlIndent(content),
856
+ value: parse(content) ?? {}
857
+ };
858
+ }
859
+ /**
860
+ * Resolve migratable pnpm settings from a package manifest.
861
+ *
862
+ * Yarn resolutions are merged into pnpm overrides when enabled. Empty
863
+ * overrides are removed from the result.
864
+ *
865
+ * @param packageJson - Parsed package manifest
866
+ * @param yarnResolutions - Whether to convert Yarn resolutions to pnpm overrides
867
+ *
868
+ * @returns Pnpm workspace settings resolved from the package manifest
869
+ */
870
+ function resolvePackageJsonSettings(packageJson, yarnResolutions) {
871
+ const pnpmSettings = yarnResolutions && packageJson.resolutions ? {
872
+ ...packageJson.pnpm,
873
+ overrides: defu(packageJson.pnpm?.overrides, packageJson.resolutions)
874
+ } : { ...packageJson.pnpm };
875
+ if (pnpmSettings.overrides && !Object.keys(pnpmSettings.overrides).length) delete pnpmSettings.overrides;
876
+ return pnpmSettings;
432
877
  }
433
878
  //#endregion
434
879
  //#region src/core.ts
880
+ function hasSettingsSources(npmrcExists, packageJsonExists, pnpmWorkspaceExists) {
881
+ if (!npmrcExists) consola$1.info(`${dim(NPMRC)} not found`);
882
+ if (!packageJsonExists) consola$1.info(`${dim(PACKAGE_JSON)} not found`);
883
+ if (npmrcExists || packageJsonExists || pnpmWorkspaceExists) return true;
884
+ consola$1.warn("No pnpm settings files to migrate");
885
+ return false;
886
+ }
887
+ function hasMigratableSettings(sources) {
888
+ const { existingSettingsChanged, packageJson, pnpmSettingsInNpmrc, yarnResolutions } = sources;
889
+ return Boolean(existingSettingsChanged || packageJson.pnpm || yarnResolutions && packageJson.resolutions || Object.keys(pnpmSettingsInNpmrc).length);
890
+ }
891
+ function assertCanMigrateRuntime(runtimeVersion, packageJsonExists) {
892
+ if (runtimeVersion && !packageJsonExists) throw new Error("Cannot migrate the removed Node.js runtime setting without a package.json file.");
893
+ }
894
+ function reportMigrationChanges(showChanges, before = {}, after = {}) {
895
+ if (!showChanges) return;
896
+ reportSettingsChanges(collectSettingsChanges(before, after));
897
+ }
435
898
  /**
436
899
  * Migrate pnpm settings from legacy locations to `pnpm-workspace.yaml`.
437
900
  *
@@ -440,6 +903,7 @@ function normalizeNpmrcKey(key) {
440
903
  * - `package.json` pnpm field
441
904
  * - `.npmrc` pnpm-related settings
442
905
  * - `package.json` resolutions (optional, converts to pnpm overrides)
906
+ * - deprecated settings already present in `pnpm-workspace.yaml`
443
907
  *
444
908
  * @param rawOptions - Migration options
445
909
  * @param rawOptions.cwd - Current working directory (default: process.cwd())
@@ -448,6 +912,7 @@ function normalizeNpmrcKey(key) {
448
912
  * @param rawOptions.yarnResolutions - Whether to migrate resolutions field (default: true)
449
913
  * @param rawOptions.sortKeys - Whether to sort keys in output YAML (default: false)
450
914
  * @param rawOptions.newlineBetween - Add newlines between root keys (default: true)
915
+ * @param rawOptions.showChanges - Show settings changes after migration (default: true)
451
916
  *
452
917
  * @returns A promise that resolves when migration is complete
453
918
  *
@@ -477,53 +942,48 @@ async function migratePnpmSettings(rawOptions = {}) {
477
942
  fsExists(packageJsonPath),
478
943
  fsExists(pnpmWorkspaceYamlPath)
479
944
  ]);
480
- if (!npmrcExists) consola$1.info(`${dim(NPMRC)} not found`);
481
- if (!packageJsonExists) consola$1.info(`${dim(PACKAGE_JSON)} not found`);
482
- if (!npmrcExists && !packageJsonExists) {
483
- consola$1.warn("No pnpm settings files to migrate");
945
+ if (!hasSettingsSources(npmrcExists, packageJsonExists, pnpmWorkspaceExists)) {
946
+ reportMigrationChanges(options.showChanges);
484
947
  return;
485
948
  }
486
- let packageJsonIndent = 2;
487
- let packageJsonObject = {};
488
- let pnpmWorkspaceYamlIndent = 2;
489
- let pnpmWorkspaceYamlObject = {};
490
- if (packageJsonExists) {
491
- const content = await fsReadFile(packageJsonPath);
492
- packageJsonIndent = detectIndent(content).indent;
493
- packageJsonObject = JSON.parse(content);
494
- }
495
- if (pnpmWorkspaceExists) {
496
- const content = await fsReadFile(pnpmWorkspaceYamlPath);
497
- pnpmWorkspaceYamlIndent = resolveYamlIndent(content);
498
- pnpmWorkspaceYamlObject = parse(content) ?? {};
499
- }
500
- const compatibility = resolveCompatibilityTarget(options.compatibility, packageJsonObject.packageManager);
501
- const npmrcMigratable = npmrcExists ? compatibility === "v10" ? {
502
- keys: PNPM_SETTINGS_FIELDS,
503
- settings: pick(await readNpmrc(npmrcPath), PNPM_SETTINGS_FIELDS)
504
- } : await readMigratableNpmrc(npmrcPath, compatibility) : {
949
+ const [packageJson, pnpmWorkspace] = await Promise.all([readPackageJson(packageJsonPath, packageJsonExists), readPnpmWorkspace(pnpmWorkspaceYamlPath, pnpmWorkspaceExists)]);
950
+ const pnpmWorkspaceBefore = structuredClone(pnpmWorkspace.value);
951
+ const compatibility = resolveCompatibilityTarget(options.compatibility, packageJson.value.packageManager, packageJson.value.devEngines?.packageManager);
952
+ const npmrcMigratable = npmrcExists ? await readMigratableNpmrc(npmrcPath, compatibility) : {
505
953
  keys: [],
506
954
  settings: {}
507
955
  };
508
956
  const pnpmSettingsInNpmrc = npmrcMigratable.settings;
509
- const hasPnpmInPackageJson = !!packageJsonObject.pnpm;
510
- const hasResolutions = options.yarnResolutions && !!packageJsonObject.resolutions;
511
- const hasNpmrcSettings = Object.keys(pnpmSettingsInNpmrc).length > 0;
512
- if (!hasPnpmInPackageJson && !hasResolutions && !hasNpmrcSettings) {
513
- consola$1.warn("No pnpm settings fields to migrate");
514
- return;
515
- }
516
- const pnpmSettingsInPackageJson = options.yarnResolutions && packageJsonObject.resolutions ? {
517
- ...packageJsonObject.pnpm,
518
- overrides: defu(packageJsonObject.pnpm?.overrides, packageJsonObject.resolutions)
519
- } : { ...packageJsonObject.pnpm };
520
- if (pnpmSettingsInPackageJson.overrides && !Object.keys(pnpmSettingsInPackageJson.overrides).length) delete pnpmSettingsInPackageJson.overrides;
957
+ const pnpmSettingsInPackageJson = resolvePackageJsonSettings(packageJson.value, options.yarnResolutions);
521
958
  const incomingSettings = {
522
959
  ...pnpmSettingsInNpmrc,
523
960
  ...pnpmSettingsInPackageJson
524
961
  };
525
- normalizeIncomingSettings(incomingSettings, compatibility, options.replaceDeprecated);
526
- const pnpmWorkspaceResult = mergeByStrategy(pnpmWorkspaceYamlObject, incomingSettings, options.strategy);
962
+ const [existingNormalization, incomingNormalization] = await Promise.all([normalizeIncomingSettings(pnpmWorkspace.value, {
963
+ compatibility,
964
+ cwd: options.cwd,
965
+ replaceDeprecated: options.replaceDeprecated
966
+ }), normalizeIncomingSettings(incomingSettings, {
967
+ compatibility,
968
+ cwd: options.cwd,
969
+ replaceDeprecated: options.replaceDeprecated
970
+ })]);
971
+ for (const warning of [...existingNormalization.warnings, ...incomingNormalization.warnings]) consola$1.warn(warning);
972
+ const runtimeVersion = resolveRuntimeVersionByStrategy(existingNormalization.runtimeVersion, incomingNormalization.runtimeVersion, options.strategy);
973
+ assertCanMigrateRuntime(runtimeVersion, packageJsonExists);
974
+ const runtimeMigration = migrateRuntimeToPackageJson(packageJson.value, runtimeVersion);
975
+ if (runtimeMigration.warning) consola$1.warn(runtimeMigration.warning);
976
+ if (!hasMigratableSettings({
977
+ existingSettingsChanged: existingNormalization.changed,
978
+ packageJson: packageJson.value,
979
+ pnpmSettingsInNpmrc,
980
+ yarnResolutions: options.yarnResolutions
981
+ })) {
982
+ consola$1.warn("No pnpm settings fields to migrate");
983
+ reportMigrationChanges(options.showChanges);
984
+ return;
985
+ }
986
+ const pnpmWorkspaceResult = mergeByStrategy(pnpmWorkspace.value, incomingSettings, options.strategy);
527
987
  const yamlDocument = new Document({}, { sortMapEntries: options.sortKeys });
528
988
  Object.entries(pnpmWorkspaceResult).forEach(([key, value]) => {
529
989
  yamlDocument.add({
@@ -531,58 +991,29 @@ async function migratePnpmSettings(rawOptions = {}) {
531
991
  value
532
992
  });
533
993
  });
534
- const yamlContent = yamlDocument.toString({ indent: pnpmWorkspaceYamlIndent });
535
- await fsWriteFile(pnpmWorkspaceYamlPath, options.newlineBetween ? yamlContent.replace(/\n(?=[^\s#][^:\n]*:)/g, "\n\n") : yamlContent);
994
+ const yamlContent = yamlDocument.toString({ indent: pnpmWorkspace.indent });
995
+ await fsWriteFile(pnpmWorkspaceYamlPath, options.newlineBetween ? yamlContent.replace(/\n(?=[^\s#][^:\n]*:)/gu, "\n\n") : yamlContent);
536
996
  if (npmrcExists && options.cleanNpmrc) await pruneNpmrc(npmrcPath, compatibility, npmrcMigratable.keys);
537
- if (packageJsonExists && options.cleanPackageJson && (packageJsonObject.pnpm || packageJsonObject.resolutions)) {
538
- delete packageJsonObject.pnpm;
539
- if (options.yarnResolutions) delete packageJsonObject.resolutions;
540
- await fsWriteFile(packageJsonPath, JSON.stringify(packageJsonObject, null, packageJsonIndent));
997
+ let packageJsonChanged = runtimeMigration.changed;
998
+ if (packageJsonExists && options.cleanPackageJson) {
999
+ if (packageJson.value.pnpm) packageJsonChanged = true;
1000
+ delete packageJson.value.pnpm;
1001
+ if (options.yarnResolutions && packageJson.value.resolutions) {
1002
+ packageJsonChanged = true;
1003
+ delete packageJson.value.resolutions;
1004
+ }
541
1005
  }
1006
+ if (packageJsonExists && packageJsonChanged) await fsWriteFile(packageJsonPath, JSON.stringify(packageJson.value, null, packageJson.indent));
1007
+ reportMigrationChanges(options.showChanges, pnpmWorkspaceBefore, pnpmWorkspaceResult);
542
1008
  } catch (err) {
543
1009
  consola$1.error("Failed to migrate pnpm settings:", err);
544
1010
  throw err;
545
1011
  }
546
1012
  }
547
- /**
548
- * Build v11 `allowBuilds` map from legacy build-script settings.
549
- */
550
- function collectAllowBuildsFromLegacy(incomingSettings) {
551
- const allowBuilds = {};
552
- for (const name of incomingSettings.onlyBuiltDependencies || []) allowBuilds[name] = true;
553
- for (const name of incomingSettings.ignoredBuiltDependencies || []) allowBuilds[name] = false;
554
- for (const name of incomingSettings.neverBuiltDependencies || []) allowBuilds[name] = false;
555
- return Object.keys(allowBuilds).length ? allowBuilds : void 0;
556
- }
557
- /**
558
- * Normalize incoming settings according to compatibility target.
559
- */
560
- function normalizeIncomingSettings(incomingSettings, compatibility, replaceDeprecated) {
561
- if (compatibility === "v10" && !replaceDeprecated) return;
562
- if (incomingSettings.allowNonAppliedPatches !== void 0) incomingSettings.allowUnusedPatches = incomingSettings.allowUnusedPatches ?? incomingSettings.allowNonAppliedPatches;
563
- const allowBuildsFromLegacy = collectAllowBuildsFromLegacy(incomingSettings);
564
- if (allowBuildsFromLegacy) incomingSettings.allowBuilds = {
565
- ...allowBuildsFromLegacy,
566
- ...incomingSettings.allowBuilds || {}
567
- };
568
- for (const key of PNPM_V11_REMOVED_SETTINGS) delete incomingSettings[key];
569
- }
570
- /**
571
- * Resolve final compatibility target from user option and package manager hint.
572
- */
573
- function resolveCompatibilityTarget(compatibility, packageManager) {
574
- if (compatibility !== "auto") return compatibility;
575
- const [, major] = (packageManager || "").match(/^pnpm@(\d+)(?:\.|$)/) || [];
576
- return Number(major) >= 11 ? "v11" : "v10";
577
- }
578
- function resolveYamlIndent(content) {
579
- const detectedIndent = detectIndent(content).amount;
580
- return detectedIndent > 0 ? detectedIndent : 2;
581
- }
582
1013
  //#endregion
583
1014
  //#region src/cli.ts
584
1015
  const cli = cac(name);
585
- cli.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)").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-clean-npmrc", "Disable removing pnpm settings in .npmrc file").option("--no-clean-package-json", "Disable removing pnpm fields in package.json").help();
1016
+ cli.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").help();
586
1017
  cli.command("").action(async (options) => {
587
1018
  try {
588
1019
  consola.log(`\n${bold(magenta(name))} ${dim(`v${version}`)}`);