pnpm-settings-migrator 0.6.1 → 0.7.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,19 @@
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
5
  import { Document, parse } from "yaml";
9
- import { access, readFile, writeFile } from "node:fs/promises";
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 detectIndent from "detect-indent";
14
14
  //#region package.json
15
15
  var name = "pnpm-settings-migrator";
16
- var version = "0.6.1";
16
+ var version = "0.7.0";
17
17
  //#endregion
18
18
  //#region src/constants.ts
19
19
  const NPMRC = ".npmrc";
@@ -60,6 +60,7 @@ const PNPM_SETTINGS_FIELDS = [
60
60
  "httpProxy",
61
61
  "httpsProxy",
62
62
  "ignoredBuiltDependencies",
63
+ "ignoreDepScripts",
63
64
  "ignoreDeps",
64
65
  "ignoredOptionalDependencies",
65
66
  "ignorePatchFailures",
@@ -87,11 +88,14 @@ const PNPM_SETTINGS_FIELDS = [
87
88
  "overrides",
88
89
  "packageExtensions",
89
90
  "packageImportMethod",
91
+ "packageManagerStrict",
92
+ "packageManagerStrictVersion",
90
93
  "patchedDependencies",
91
94
  "peerDependencyRules",
92
95
  "peersSuffixMaxLength",
93
96
  "pnpmfile",
94
97
  "pnpmfileIgnorePkgManifest",
98
+ "pmOnFail",
95
99
  "preferFrozenLockfile",
96
100
  "preferWorkspacePackages",
97
101
  "production",
@@ -130,11 +134,17 @@ const PNPM_SETTINGS_FIELDS = [
130
134
  ];
131
135
  const PNPM_V11_REMOVED_SETTINGS = [
132
136
  "allowNonAppliedPatches",
137
+ "executionEnv",
138
+ "ignoreDepScripts",
133
139
  "ignorePatchFailures",
134
140
  "ignoredBuiltDependencies",
141
+ "managePackageManagerVersions",
135
142
  "neverBuiltDependencies",
136
143
  "onlyBuiltDependencies",
137
- "onlyBuiltDependenciesFile"
144
+ "onlyBuiltDependenciesFile",
145
+ "packageManagerStrict",
146
+ "packageManagerStrictVersion",
147
+ "useNodeVersion"
138
148
  ];
139
149
  //#endregion
140
150
  //#region src/options.ts
@@ -160,8 +170,19 @@ const VALID_STRATEGIES = [
160
170
  const VALID_COMPATIBILITIES = [
161
171
  "auto",
162
172
  "v10",
163
- "v11"
173
+ "v11",
174
+ "v12"
164
175
  ];
176
+ function resolveCompatibility(compatibility) {
177
+ if (!compatibility) return DEFAULT_OPTIONS.compatibility;
178
+ if (VALID_COMPATIBILITIES.includes(compatibility)) return compatibility;
179
+ throw new Error(`Invalid compatibility: ${compatibility}. Expected one of: ${VALID_COMPATIBILITIES.join(", ")}`);
180
+ }
181
+ function resolveStrategy(strategy) {
182
+ if (!strategy) return DEFAULT_OPTIONS.strategy;
183
+ if (VALID_STRATEGIES.includes(strategy)) return strategy;
184
+ throw new Error(`Invalid strategy: ${strategy}. Expected one of: ${VALID_STRATEGIES.join(", ")}`);
185
+ }
165
186
  /**
166
187
  * Resolve and normalize migration options with defaults.
167
188
  *
@@ -196,16 +217,6 @@ function resolveOptions(options = {}) {
196
217
  replaceDeprecated: options.replaceDeprecated ?? DEFAULT_OPTIONS.replaceDeprecated
197
218
  };
198
219
  }
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
220
  //#endregion
210
221
  //#region src/utils/fs.ts
211
222
  /**
@@ -225,6 +236,13 @@ async function fsReadFile(path) {
225
236
  return await readFile(path, "utf-8");
226
237
  }
227
238
  /**
239
+ * Remove a file if it exists.
240
+ * @param path - file path
241
+ */
242
+ async function fsRemoveFile(path) {
243
+ await rm(path, { force: true });
244
+ }
245
+ /**
228
246
  * Write file
229
247
  * @param path - file path
230
248
  * @param content - file content
@@ -240,40 +258,144 @@ const red = getColor("red");
240
258
  const bold = getColor("bold");
241
259
  const magenta = getColor("magenta");
242
260
  //#endregion
243
- //#region src/utils/merge.ts
261
+ //#region src/utils/compatibility.ts
262
+ const PNPM_REPLACEABLE_IN_V10_SETTINGS = [
263
+ "allowNonAppliedPatches",
264
+ "ignoredBuiltDependencies",
265
+ "neverBuiltDependencies",
266
+ "onlyBuiltDependencies",
267
+ "onlyBuiltDependenciesFile"
268
+ ];
244
269
  /**
245
- * Merge two objects based on the specified strategy.
246
- *
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
250
- *
251
- * @returns Merged pnpm workspace configuration
252
- *
253
- * @example
254
- * ```ts
255
- * // Discard strategy - keep existing values
256
- * mergeByStrategy({ packages: ['a'] }, { packages: ['b'] }, 'discard')
257
- * // => { packages: ['a'] }
258
- *
259
- * // Merge strategy - combine arrays with deduplication
260
- * mergeByStrategy({ packages: ['a'] }, { packages: ['b'] }, 'merge')
261
- * // => { packages: ['a', 'b'] }
262
- *
263
- * // Overwrite strategy - use incoming values
264
- * mergeByStrategy({ packages: ['a'] }, { packages: ['b'] }, 'overwrite')
265
- * // => { packages: ['b'] }
266
- * ```
270
+ * Read packages listed by the legacy `onlyBuiltDependenciesFile` setting.
267
271
  */
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);
272
+ async function readOnlyBuiltDependenciesFile(cwd, file) {
273
+ if (!file) return [];
274
+ const path = resolve(cwd, file);
275
+ const value = JSON.parse(await fsReadFile(path));
276
+ if (!Array.isArray(value) || !value.every((item) => typeof item === "string")) throw new TypeError(`Invalid onlyBuiltDependenciesFile: ${file}. Expected a JSON array of package names.`);
277
+ return value;
278
+ }
279
+ /**
280
+ * Build an `allowBuilds` map from legacy build-script settings.
281
+ */
282
+ async function collectAllowBuildsFromLegacy(incomingSettings, cwd) {
283
+ const allowBuilds = {};
284
+ const allowedFromFile = await readOnlyBuiltDependenciesFile(cwd, incomingSettings.onlyBuiltDependenciesFile);
285
+ for (const name of [...incomingSettings.onlyBuiltDependencies || [], ...allowedFromFile]) allowBuilds[name] = true;
286
+ for (const name of incomingSettings.ignoredBuiltDependencies || []) allowBuilds[name] = false;
287
+ for (const name of incomingSettings.neverBuiltDependencies || []) allowBuilds[name] = false;
288
+ return Object.keys(allowBuilds).length ? allowBuilds : void 0;
289
+ }
290
+ /**
291
+ * Resolve the pnpm v11 replacement for legacy package-manager settings.
292
+ */
293
+ function resolvePmOnFail(settings) {
294
+ if (settings.pmOnFail) return settings.pmOnFail;
295
+ if (settings.packageManagerStrictVersion === true) return "error";
296
+ if (settings.packageManagerStrict === false) return "warn";
297
+ if (settings.managePackageManagerVersions === false) return "ignore";
298
+ if (settings.managePackageManagerVersions === true) return "download";
299
+ }
300
+ /**
301
+ * Rename legacy audit settings while preserving values for manual ID updates.
302
+ */
303
+ function normalizeAuditConfig(settings, warnings) {
304
+ const { auditConfig } = settings;
305
+ const ignoreCves = auditConfig?.ignoreCves;
306
+ if (!ignoreCves) return;
307
+ auditConfig.ignoreGhsas ??= ignoreCves;
308
+ Reflect.deleteProperty(auditConfig, "ignoreCves");
309
+ warnings.push("auditConfig.ignoreCves was renamed to auditConfig.ignoreGhsas; replace each CVE ID with its corresponding GHSA ID.");
310
+ }
311
+ /**
312
+ * Resolve a removed Node.js runtime setting for migration to `package.json`.
313
+ */
314
+ function resolveRuntimeVersion(settings, warnings) {
315
+ const { useNodeVersion } = settings;
316
+ const executionEnvNodeVersion = settings.executionEnv?.nodeVersion;
317
+ if (useNodeVersion && executionEnvNodeVersion && useNodeVersion !== executionEnvNodeVersion) warnings.push(`Both useNodeVersion (${useNodeVersion}) and executionEnv.nodeVersion (${executionEnvNodeVersion}) were found; useNodeVersion takes precedence.`);
318
+ return useNodeVersion || executionEnvNodeVersion;
319
+ }
320
+ /**
321
+ * Normalize settings according to the selected compatibility target.
322
+ */
323
+ async function normalizeIncomingSettings(incomingSettings, options) {
324
+ const { compatibility, cwd, replaceDeprecated } = options;
325
+ const before = JSON.stringify(incomingSettings);
326
+ const warnings = [];
327
+ if (compatibility === "v10" && !replaceDeprecated) return {
328
+ changed: false,
329
+ warnings
330
+ };
331
+ if (incomingSettings.allowNonAppliedPatches !== void 0) incomingSettings.allowUnusedPatches ??= incomingSettings.allowNonAppliedPatches;
332
+ const allowBuildsFromLegacy = await collectAllowBuildsFromLegacy(incomingSettings, cwd);
333
+ if (allowBuildsFromLegacy) incomingSettings.allowBuilds = {
334
+ ...allowBuildsFromLegacy,
335
+ ...incomingSettings.allowBuilds || {}
336
+ };
337
+ if (compatibility === "v10") {
338
+ for (const key of PNPM_REPLACEABLE_IN_V10_SETTINGS) Reflect.deleteProperty(incomingSettings, key);
339
+ return {
340
+ changed: before !== JSON.stringify(incomingSettings),
341
+ warnings
342
+ };
274
343
  }
344
+ const pmOnFail = resolvePmOnFail(incomingSettings);
345
+ if (pmOnFail) incomingSettings.pmOnFail = pmOnFail;
346
+ normalizeAuditConfig(incomingSettings, warnings);
347
+ const runtimeVersion = resolveRuntimeVersion(incomingSettings, warnings);
348
+ if (incomingSettings.ignoreDepScripts !== void 0) warnings.push("ignoreDepScripts was removed in pnpm v11 and has no direct replacement.");
349
+ if (incomingSettings.ignorePatchFailures !== void 0) warnings.push("ignorePatchFailures was removed in pnpm v11; patch failures now always throw.");
350
+ for (const key of PNPM_V11_REMOVED_SETTINGS) Reflect.deleteProperty(incomingSettings, key);
351
+ return {
352
+ changed: before !== JSON.stringify(incomingSettings),
353
+ runtimeVersion,
354
+ warnings
355
+ };
275
356
  }
276
357
  /**
358
+ * Move a removed Node.js runtime setting to `package.json#devEngines.runtime`.
359
+ */
360
+ function migrateRuntimeToPackageJson(packageJson, runtimeVersion) {
361
+ if (!runtimeVersion) return { changed: false };
362
+ if (packageJson.devEngines?.runtime) return {
363
+ changed: false,
364
+ warning: "A devEngines.runtime declaration already exists; the removed pnpm Node.js runtime setting was not applied."
365
+ };
366
+ packageJson.devEngines = {
367
+ ...packageJson.devEngines,
368
+ runtime: {
369
+ name: "node",
370
+ version: runtimeVersion
371
+ }
372
+ };
373
+ return { changed: true };
374
+ }
375
+ /**
376
+ * Select the runtime setting that wins under the configured merge strategy.
377
+ */
378
+ function resolveRuntimeVersionByStrategy(existingVersion, incomingVersion, strategy) {
379
+ if (strategy === "overwrite") return incomingVersion ?? existingVersion;
380
+ return existingVersion ?? incomingVersion;
381
+ }
382
+ //#endregion
383
+ //#region src/utils/compatibility-target.ts
384
+ const PNPM_V11_MAJOR = 11;
385
+ const PNPM_V12_MAJOR = 12;
386
+ /**
387
+ * Resolve final compatibility target from user option and package manager hint.
388
+ */
389
+ function resolveCompatibilityTarget(compatibility, packageManager, devPackageManager) {
390
+ if (compatibility !== "auto") return compatibility;
391
+ const match = (packageManager || (devPackageManager?.name === "pnpm" ? `pnpm@${devPackageManager.version}` : void 0))?.match(/^pnpm@[\s<=>^~]*(?<major>\d+)(?:\.|\s|$)/u);
392
+ const major = Number(match?.groups?.major);
393
+ if (major >= PNPM_V12_MAJOR) return "v12";
394
+ return major >= PNPM_V11_MAJOR ? "v11" : "v10";
395
+ }
396
+ //#endregion
397
+ //#region src/utils/merge.ts
398
+ /**
277
399
  * Merge objects with priority to the first argument.
278
400
  * Only adds keys from second argument that don't exist in first.
279
401
  * For nested objects, recursively merges them.
@@ -314,6 +436,38 @@ function mergeWithArrayDedupe(existing, incoming) {
314
436
  }
315
437
  return result;
316
438
  }
439
+ /**
440
+ * Merge two objects based on the specified strategy.
441
+ *
442
+ * @param existing - Existing pnpm-workspace.yaml content
443
+ * @param incoming - New settings from package.json and .npmrc
444
+ * @param strategy - Merge strategy to use
445
+ *
446
+ * @returns Merged pnpm workspace configuration
447
+ *
448
+ * @example
449
+ * ```ts
450
+ * // Discard strategy - keep existing values
451
+ * mergeByStrategy({ packages: ['a'] }, { packages: ['b'] }, 'discard')
452
+ * // => { packages: ['a'] }
453
+ *
454
+ * // Merge strategy - combine arrays with deduplication
455
+ * mergeByStrategy({ packages: ['a'] }, { packages: ['b'] }, 'merge')
456
+ * // => { packages: ['a', 'b'] }
457
+ *
458
+ * // Overwrite strategy - use incoming values
459
+ * mergeByStrategy({ packages: ['a'] }, { packages: ['b'] }, 'overwrite')
460
+ * // => { packages: ['b'] }
461
+ * ```
462
+ */
463
+ function mergeByStrategy(existing, incoming, strategy) {
464
+ switch (strategy) {
465
+ case "discard": return discardMerge(existing, incoming);
466
+ case "merge": return mergeWithArrayDedupe(existing, incoming);
467
+ case "overwrite": return discardMerge(incoming, existing);
468
+ default: return defu(existing, incoming);
469
+ }
470
+ }
317
471
  //#endregion
318
472
  //#region src/utils/npmrc.ts
319
473
  const NPMRC_AUTH_OR_REGISTRY_KEYS = [
@@ -332,6 +486,33 @@ const NPMRC_AUTH_OR_REGISTRY_KEYS = [
332
486
  "tokenhelper",
333
487
  "username"
334
488
  ];
489
+ const NODE_MIRROR_KEY_PATTERN = /^node-mirror:(?<channel>.+)$/iu;
490
+ /**
491
+ * Normalize `.npmrc` key for case-insensitive matching.
492
+ */
493
+ function normalizeNpmrcKey(key) {
494
+ return key.trim().replace(/\[\]$/u, "").toLowerCase();
495
+ }
496
+ /**
497
+ * Extract key name from a raw `.npmrc` line.
498
+ */
499
+ function getNpmrcLineKey(line) {
500
+ const trimmed = line.trim();
501
+ if (!trimmed || trimmed.startsWith(";") || trimmed.startsWith("#")) return;
502
+ const index = trimmed.indexOf("=");
503
+ if (index <= 0) return;
504
+ return normalizeNpmrcKey(trimmed.slice(0, index));
505
+ }
506
+ /**
507
+ * Check whether a `.npmrc` key is auth/registry-related and should stay in
508
+ * pnpm v11 and newer.
509
+ */
510
+ function isNpmrcAuthOrRegistryKey(key) {
511
+ const normalized = normalizeNpmrcKey(key);
512
+ if (normalized.startsWith("//")) return true;
513
+ if (normalized === "registry" || normalized.endsWith(":registry")) return true;
514
+ return NPMRC_AUTH_OR_REGISTRY_KEYS.some((authKey) => normalized === authKey || normalized.endsWith(`:${authKey}`));
515
+ }
335
516
  /**
336
517
  * Remove pnpm-related settings from `.npmrc` file.
337
518
  *
@@ -351,14 +532,19 @@ const NPMRC_AUTH_OR_REGISTRY_KEYS = [
351
532
  * ```
352
533
  */
353
534
  async function pruneNpmrc(path, compatibility = "v10", migratedKeys = []) {
354
- const pnpmSettingsFields = PNPM_SETTINGS_FIELDS.map((v) => kebabCase(v));
535
+ const pnpmSettingsFields = PNPM_SETTINGS_FIELDS.map((field) => kebabCase(field));
355
536
  const migratedKeySet = new Set(migratedKeys.map(normalizeNpmrcKey));
356
- await fsWriteFile(path, (await fsReadFile(path)).split(/\r?\n/).filter((line) => {
537
+ const updatedContent = (await fsReadFile(path)).split(/\r?\n/u).filter((line) => {
357
538
  const key = getNpmrcLineKey(line);
358
539
  if (!key) return true;
359
- if (compatibility === "v11") return !migratedKeySet.has(normalizeNpmrcKey(key));
360
- return !pnpmSettingsFields.some((v) => key.startsWith(v));
361
- }).join("\n"));
540
+ if (compatibility !== "v10") return !migratedKeySet.has(normalizeNpmrcKey(key));
541
+ return !pnpmSettingsFields.some((field) => key.startsWith(field));
542
+ }).join("\n").trimEnd();
543
+ if (compatibility !== "v10" && !updatedContent.trim()) {
544
+ await fsRemoveFile(path);
545
+ return;
546
+ }
547
+ await fsWriteFile(path, updatedContent);
362
548
  }
363
549
  /**
364
550
  * Read and parse `.npmrc` file with camelCase key conversion.
@@ -382,56 +568,87 @@ async function pruneNpmrc(path, compatibility = "v10", migratedKeys = []) {
382
568
  /**
383
569
  * Read `.npmrc` and return settings that should be migrated into workspace config.
384
570
  *
385
- * - `v10`: returns all keys (legacy whitelist filtering happens in caller).
386
- * - `v11`: excludes auth/registry keys because pnpm still reads them from `.npmrc`.
571
+ * - `v10`: returns settings from the legacy whitelist.
572
+ * - `v11+`: excludes auth/registry keys because pnpm still reads them from
573
+ * `.npmrc`.
387
574
  */
388
575
  async function readMigratableNpmrc(path, compatibility) {
389
576
  const raw = await readIniFile(path);
390
- if (compatibility === "v10") return {
391
- keys: Object.keys(raw),
392
- settings: camelcaseKeys(raw)
393
- };
577
+ if (compatibility === "v10") {
578
+ const settings = camelcaseKeys(raw);
579
+ return {
580
+ keys: PNPM_SETTINGS_FIELDS,
581
+ settings: pick(settings, PNPM_SETTINGS_FIELDS)
582
+ };
583
+ }
394
584
  const migratable = {};
395
585
  const keys = [];
586
+ const nodeDownloadMirrors = {};
396
587
  for (const [key, value] of Object.entries(raw)) if (!isNpmrcAuthOrRegistryKey(key)) {
397
- migratable[key] = value;
398
588
  keys.push(key);
589
+ const nodeMirrorMatch = normalizeNpmrcKey(key).match(NODE_MIRROR_KEY_PATTERN);
590
+ if (nodeMirrorMatch?.groups?.channel) nodeDownloadMirrors[nodeMirrorMatch.groups.channel] = String(value);
591
+ else migratable[key] = value;
399
592
  }
593
+ const settings = camelcaseKeys(migratable);
594
+ if (Object.keys(nodeDownloadMirrors).length) settings.nodeDownloadMirrors = nodeDownloadMirrors;
400
595
  return {
401
596
  keys,
402
- settings: camelcaseKeys(migratable)
597
+ settings
403
598
  };
404
599
  }
405
- async function readNpmrc(path) {
406
- return camelcaseKeys(await readIniFile(path));
600
+ //#endregion
601
+ //#region src/utils/config.ts
602
+ function resolveYamlIndent(content) {
603
+ const detectedIndent = detectIndent(content).amount;
604
+ return detectedIndent > 0 ? detectedIndent : 2;
407
605
  }
408
- /**
409
- * Extract key name from a raw `.npmrc` line.
410
- */
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));
606
+ async function readPackageJson(path, exists) {
607
+ if (!exists) return {
608
+ indent: 2,
609
+ value: {}
610
+ };
611
+ const content = await fsReadFile(path);
612
+ return {
613
+ indent: detectIndent(content).indent,
614
+ value: JSON.parse(content)
615
+ };
417
616
  }
418
- /**
419
- * Check whether a `.npmrc` key is auth/registry-related and should stay in v11.
420
- */
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}`));
617
+ async function readPnpmWorkspace(path, exists) {
618
+ if (!exists) return {
619
+ indent: 2,
620
+ value: {}
621
+ };
622
+ const content = await fsReadFile(path);
623
+ return {
624
+ indent: resolveYamlIndent(content),
625
+ value: parse(content) ?? {}
626
+ };
426
627
  }
427
- /**
428
- * Normalize `.npmrc` key for case-insensitive matching.
429
- */
430
- function normalizeNpmrcKey(key) {
431
- return key.trim().replace(/\[\]$/, "").toLowerCase();
628
+ function resolvePackageJsonSettings(packageJson, yarnResolutions) {
629
+ const pnpmSettings = yarnResolutions && packageJson.resolutions ? {
630
+ ...packageJson.pnpm,
631
+ overrides: defu(packageJson.pnpm?.overrides, packageJson.resolutions)
632
+ } : { ...packageJson.pnpm };
633
+ if (pnpmSettings.overrides && !Object.keys(pnpmSettings.overrides).length) delete pnpmSettings.overrides;
634
+ return pnpmSettings;
432
635
  }
433
636
  //#endregion
434
637
  //#region src/core.ts
638
+ function hasSettingsSources(npmrcExists, packageJsonExists, pnpmWorkspaceExists) {
639
+ if (!npmrcExists) consola$1.info(`${dim(NPMRC)} not found`);
640
+ if (!packageJsonExists) consola$1.info(`${dim(PACKAGE_JSON)} not found`);
641
+ if (npmrcExists || packageJsonExists || pnpmWorkspaceExists) return true;
642
+ consola$1.warn("No pnpm settings files to migrate");
643
+ return false;
644
+ }
645
+ function hasMigratableSettings(sources) {
646
+ const { existingSettingsChanged, packageJson, pnpmSettingsInNpmrc, yarnResolutions } = sources;
647
+ return Boolean(existingSettingsChanged || packageJson.pnpm || yarnResolutions && packageJson.resolutions || Object.keys(pnpmSettingsInNpmrc).length);
648
+ }
649
+ function assertCanMigrateRuntime(runtimeVersion, packageJsonExists) {
650
+ if (runtimeVersion && !packageJsonExists) throw new Error("Cannot migrate the removed Node.js runtime setting without a package.json file.");
651
+ }
435
652
  /**
436
653
  * Migrate pnpm settings from legacy locations to `pnpm-workspace.yaml`.
437
654
  *
@@ -440,6 +657,7 @@ function normalizeNpmrcKey(key) {
440
657
  * - `package.json` pnpm field
441
658
  * - `.npmrc` pnpm-related settings
442
659
  * - `package.json` resolutions (optional, converts to pnpm overrides)
660
+ * - deprecated settings already present in `pnpm-workspace.yaml`
443
661
  *
444
662
  * @param rawOptions - Migration options
445
663
  * @param rawOptions.cwd - Current working directory (default: process.cwd())
@@ -477,53 +695,43 @@ async function migratePnpmSettings(rawOptions = {}) {
477
695
  fsExists(packageJsonPath),
478
696
  fsExists(pnpmWorkspaceYamlPath)
479
697
  ]);
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");
484
- return;
485
- }
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) : {
698
+ if (!hasSettingsSources(npmrcExists, packageJsonExists, pnpmWorkspaceExists)) return;
699
+ const [packageJson, pnpmWorkspace] = await Promise.all([readPackageJson(packageJsonPath, packageJsonExists), readPnpmWorkspace(pnpmWorkspaceYamlPath, pnpmWorkspaceExists)]);
700
+ const compatibility = resolveCompatibilityTarget(options.compatibility, packageJson.value.packageManager, packageJson.value.devEngines?.packageManager);
701
+ const npmrcMigratable = npmrcExists ? await readMigratableNpmrc(npmrcPath, compatibility) : {
505
702
  keys: [],
506
703
  settings: {}
507
704
  };
508
705
  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;
706
+ const pnpmSettingsInPackageJson = resolvePackageJsonSettings(packageJson.value, options.yarnResolutions);
521
707
  const incomingSettings = {
522
708
  ...pnpmSettingsInNpmrc,
523
709
  ...pnpmSettingsInPackageJson
524
710
  };
525
- normalizeIncomingSettings(incomingSettings, compatibility, options.replaceDeprecated);
526
- const pnpmWorkspaceResult = mergeByStrategy(pnpmWorkspaceYamlObject, incomingSettings, options.strategy);
711
+ const [existingNormalization, incomingNormalization] = await Promise.all([normalizeIncomingSettings(pnpmWorkspace.value, {
712
+ compatibility,
713
+ cwd: options.cwd,
714
+ replaceDeprecated: options.replaceDeprecated
715
+ }), normalizeIncomingSettings(incomingSettings, {
716
+ compatibility,
717
+ cwd: options.cwd,
718
+ replaceDeprecated: options.replaceDeprecated
719
+ })]);
720
+ for (const warning of [...existingNormalization.warnings, ...incomingNormalization.warnings]) consola$1.warn(warning);
721
+ const runtimeVersion = resolveRuntimeVersionByStrategy(existingNormalization.runtimeVersion, incomingNormalization.runtimeVersion, options.strategy);
722
+ assertCanMigrateRuntime(runtimeVersion, packageJsonExists);
723
+ const runtimeMigration = migrateRuntimeToPackageJson(packageJson.value, runtimeVersion);
724
+ if (runtimeMigration.warning) consola$1.warn(runtimeMigration.warning);
725
+ if (!hasMigratableSettings({
726
+ existingSettingsChanged: existingNormalization.changed,
727
+ packageJson: packageJson.value,
728
+ pnpmSettingsInNpmrc,
729
+ yarnResolutions: options.yarnResolutions
730
+ })) {
731
+ consola$1.warn("No pnpm settings fields to migrate");
732
+ return;
733
+ }
734
+ const pnpmWorkspaceResult = mergeByStrategy(pnpmWorkspace.value, incomingSettings, options.strategy);
527
735
  const yamlDocument = new Document({}, { sortMapEntries: options.sortKeys });
528
736
  Object.entries(pnpmWorkspaceResult).forEach(([key, value]) => {
529
737
  yamlDocument.add({
@@ -531,58 +739,28 @@ async function migratePnpmSettings(rawOptions = {}) {
531
739
  value
532
740
  });
533
741
  });
534
- const yamlContent = yamlDocument.toString({ indent: pnpmWorkspaceYamlIndent });
535
- await fsWriteFile(pnpmWorkspaceYamlPath, options.newlineBetween ? yamlContent.replace(/\n(?=[^\s#][^:\n]*:)/g, "\n\n") : yamlContent);
742
+ const yamlContent = yamlDocument.toString({ indent: pnpmWorkspace.indent });
743
+ await fsWriteFile(pnpmWorkspaceYamlPath, options.newlineBetween ? yamlContent.replace(/\n(?=[^\s#][^:\n]*:)/gu, "\n\n") : yamlContent);
536
744
  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));
745
+ let packageJsonChanged = runtimeMigration.changed;
746
+ if (packageJsonExists && options.cleanPackageJson) {
747
+ if (packageJson.value.pnpm) packageJsonChanged = true;
748
+ delete packageJson.value.pnpm;
749
+ if (options.yarnResolutions && packageJson.value.resolutions) {
750
+ packageJsonChanged = true;
751
+ delete packageJson.value.resolutions;
752
+ }
541
753
  }
754
+ if (packageJsonExists && packageJsonChanged) await fsWriteFile(packageJsonPath, JSON.stringify(packageJson.value, null, packageJson.indent));
542
755
  } catch (err) {
543
756
  consola$1.error("Failed to migrate pnpm settings:", err);
544
757
  throw err;
545
758
  }
546
759
  }
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
760
  //#endregion
583
761
  //#region src/cli.ts
584
762
  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();
763
+ 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-clean-npmrc", "Disable removing pnpm settings in .npmrc file").option("--no-clean-package-json", "Disable removing pnpm fields in package.json").help();
586
764
  cli.command("").action(async (options) => {
587
765
  try {
588
766
  consola.log(`\n${bold(magenta(name))} ${dim(`v${version}`)}`);