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