pnpm-settings-migrator 0.8.0 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.mjs CHANGED
@@ -1,87 +1,136 @@
1
1
  import process from "node:process";
2
- import { cac } from "cac";
3
2
  import consola$1, { consola } from "consola";
4
- import { resolve } from "pathe";
5
- import { Document, parse, stringify } from "yaml";
6
- import { access, readFile, rm, writeFile } from "node:fs/promises";
3
+ import { cac } from "cac";
4
+ import { basename, dirname, join, relative, resolve } from "pathe";
5
+ import camelcaseKeys from "camelcase-keys";
6
+ import { randomUUID } from "node:crypto";
7
+ import { access, chmod, readFile, rename, rm, stat, writeFile } from "node:fs/promises";
8
+ import { fileURLToPath } from "node:url";
7
9
  import { getColor } from "consola/utils";
8
- import { isPlainObject, isUndefined, pick } from "@ntnyq/utils";
10
+ import { isPlainObject, isUndefined } from "@ntnyq/utils";
9
11
  import { defu } from "defu";
10
- import camelcaseKeys from "camelcase-keys";
11
12
  import { readIniFile } from "read-ini-file";
12
13
  import { kebabCase } from "uncase";
13
- import { isDeepStrictEqual } from "node:util";
14
+ import { glob } from "tinyglobby";
14
15
  import detectIndent from "detect-indent";
16
+ import { Document, isMap, isNode, isScalar, isSeq, parseDocument, stringify } from "yaml";
17
+ import { isDeepStrictEqual } from "node:util";
15
18
  //#region package.json
16
19
  var name = "pnpm-settings-migrator";
17
- var version = "0.8.0";
20
+ var version = "0.9.0";
21
+ //#endregion
22
+ //#region src/cli-options.ts
23
+ /**
24
+ * Create the command-line parser shared by the executable and parser tests.
25
+ *
26
+ * @returns Configured command-line parser
27
+ */
28
+ function createCli() {
29
+ const cli = cac(name).version(version).option("--cwd <cwd>", "Current working directory").option("--sort-keys", "Sort keys when write pnpm-workspace.yaml").option("--compatibility <compatibility>", "Compatibility target (auto, v10, v11, v12)").option("--replace-deprecated", "Replace deprecated pnpm settings with new ones and remove old keys").option("--strategy <strategy>", "Strategy to handle conflicts (discard, merge, overwrite)").option("--no-yarn-resolutions", "Disable migrating resolutions field in package.json").option("--no-newline-between", "Disable adding newlines between each root keys").option("--no-show-changes", "Disable showing settings changes").option("--no-clean-npmrc", "Disable removing pnpm settings in .npmrc file").option("--no-clean-package-json", "Disable removing pnpm fields in package.json");
30
+ cli.help((sections) => {
31
+ if (!(cli.commands.length > 0 && cli.commands.every((command) => command.isDefaultCommand))) return sections;
32
+ return sections.filter((section) => section.title !== "Commands" && !section.title?.startsWith("For more info"));
33
+ });
34
+ return cli;
35
+ }
18
36
  //#endregion
19
37
  //#region src/constants.ts
20
38
  const NPMRC = ".npmrc";
21
39
  const PACKAGE_JSON = "package.json";
22
40
  const PNPM_WORKSPACE_YAML = "pnpm-workspace.yaml";
23
41
  /**
24
- * @see {@link https://github.com/pnpm/pnpm/blob/main/packages/types/src/package.ts}
25
- * Schema-aligned pnpm config fields that can be migrated from `.npmrc`.
42
+ * @see {@link https://pnpm.io/10.x/settings}
43
+ * Documented pnpm v10 config fields that can be migrated from `.npmrc`.
26
44
  *
27
45
  * Intentionally excluded workspace manifest-only fields:
28
46
  * - `catalog`
29
- * - `catalogMode`
30
47
  * - `catalogs`
31
- * - `cleanupUnusedCatalogs`
32
48
  * - `packages`
33
49
  */
34
- const PNPM_SETTINGS_FIELDS = [
50
+ const PNPM_V10_NPMRC_SETTINGS_FIELDS = [
35
51
  "allowBuilds",
36
52
  "allowedDeprecatedVersions",
37
53
  "allowNonAppliedPatches",
38
54
  "allowUnusedPatches",
39
55
  "auditConfig",
56
+ "auditLevel",
57
+ "autoInstallPeers",
58
+ "blockExoticSubdeps",
59
+ "cacheDir",
60
+ "catalogMode",
61
+ "childConcurrency",
62
+ "ci",
63
+ "cleanupUnusedCatalogs",
64
+ "color",
40
65
  "configDependencies",
66
+ "dangerouslyAllowAllBuilds",
41
67
  "dedupeDirectDeps",
42
68
  "dedupeInjectedDeps",
43
69
  "dedupePeerDependents",
44
70
  "deployAllFiles",
71
+ "disallowWorkspaceCycles",
72
+ "dlxCacheMaxAge",
73
+ "enableGlobalVirtualStore",
74
+ "enableModulesDir",
75
+ "enablePrePostScripts",
45
76
  "engineStrict",
46
77
  "executionEnv",
78
+ "extendNodePath",
79
+ "failIfNoMatch",
47
80
  "fetchMinSpeedKiBps",
81
+ "fetchRetries",
48
82
  "fetchRetryFactor",
49
83
  "fetchRetryMaxtimeout",
50
84
  "fetchRetryMintimeout",
51
- "fetchRetryTimeout",
85
+ "fetchTimeout",
52
86
  "fetchWarnTimeoutMs",
53
87
  "forceLegacyDeploy",
88
+ "frozenLockfile",
89
+ "gitBranchLockfile",
54
90
  "gitChecks",
55
91
  "gitShallowHosts",
56
- "globalVirtualStore",
92
+ "globalBinDir",
93
+ "globalDir",
94
+ "globalPnpmfile",
57
95
  "globalVirtualStoreDir",
58
96
  "hoist",
59
97
  "hoistPattern",
60
98
  "hoistWorkspacePackages",
61
99
  "httpProxy",
62
100
  "httpsProxy",
101
+ "ignoreCompatibilityDb",
63
102
  "ignoredBuiltDependencies",
64
103
  "ignoreDepScripts",
65
- "ignoreDeps",
66
104
  "ignoredOptionalDependencies",
67
105
  "ignorePatchFailures",
106
+ "ignorePnpmfile",
107
+ "ignoreScripts",
68
108
  "ignoreWorkspaceCycles",
109
+ "ignoreWorkspaceRootCheck",
110
+ "includeWorkspaceRoot",
69
111
  "injectWorkspacePackages",
70
112
  "linkWorkspacePackages",
113
+ "localAddress",
71
114
  "lockfile",
72
115
  "lockfileDir",
116
+ "lockfileIncludeTarballUrl",
117
+ "loglevel",
73
118
  "managePackageManagerVersions",
119
+ "maxsockets",
74
120
  "mergeGitBranchLockfiles",
75
121
  "mergeGitBranchLockfilesBranchPattern",
122
+ "minimumReleaseAge",
123
+ "minimumReleaseAgeExclude",
76
124
  "modulesCacheMaxAge",
125
+ "modulesDir",
77
126
  "networkConcurrency",
78
127
  "neverBuiltDependencies",
79
- "nodeDownloadMirrors",
80
128
  "nodeLinker",
81
- "noproxy",
129
+ "nodeOptions",
130
+ "nodeVersion",
82
131
  "noProxy",
132
+ "noproxy",
83
133
  "npmPath",
84
- "npmrcAuthFile",
85
134
  "onlyBuiltDependencies",
86
135
  "onlyBuiltDependenciesFile",
87
136
  "optimisticRepeatInstall",
@@ -95,38 +144,46 @@ const PNPM_SETTINGS_FIELDS = [
95
144
  "peerDependencyRules",
96
145
  "peersSuffixMaxLength",
97
146
  "pnpmfile",
98
- "pnpmfileIgnorePkgManifest",
99
- "pmOnFail",
100
147
  "preferFrozenLockfile",
148
+ "preferSymlinkedExecutables",
101
149
  "preferWorkspacePackages",
102
150
  "production",
103
151
  "proxy",
104
152
  "publicHoistPattern",
105
153
  "recursiveInstall",
106
- "registries",
154
+ "registrySupportsTimeField",
107
155
  "requiredScripts",
108
156
  "resolutionMode",
109
157
  "resolvePeersFromWorkspaceRoot",
110
158
  "saveExact",
111
159
  "savePrefix",
160
+ "saveWorkspaceProtocol",
112
161
  "scriptShell",
113
162
  "shamefullyHoist",
114
163
  "sharedWorkspaceLockfile",
115
164
  "shellEmulator",
116
165
  "sideEffectsCache",
117
166
  "sideEffectsCacheReadonly",
167
+ "stateDir",
118
168
  "storeDir",
119
169
  "strictDepBuilds",
120
170
  "strictPeerDependencies",
121
171
  "strictSsl",
172
+ "strictStorePkgContentCheck",
122
173
  "supportedArchitectures",
174
+ "symlink",
123
175
  "syncInjectedDepsAfterScripts",
124
176
  "tag",
125
- "targetDependenciesField",
126
- "timeout",
177
+ "trustPolicy",
178
+ "trustPolicyExclude",
179
+ "trustPolicyIgnoreAfter",
180
+ "unsafePerm",
127
181
  "updateConfig",
182
+ "updateNotifier",
128
183
  "useBetaCli",
129
184
  "useNodeVersion",
185
+ "useRunningStoreServer",
186
+ "useStderr",
130
187
  "verifyDepsBeforeRun",
131
188
  "verifyStoreIntegrity",
132
189
  "virtualStoreDir",
@@ -148,80 +205,37 @@ const PNPM_V11_REMOVED_SETTINGS = [
148
205
  "useNodeVersion"
149
206
  ];
150
207
  //#endregion
151
- //#region src/options.ts
208
+ //#region src/utils/fs.ts
152
209
  /**
153
- * Default values for migration options.
210
+ * Modulus that isolates Unix permission bits from a stat mode.
154
211
  */
155
- const DEFAULT_OPTIONS = {
156
- cleanNpmrc: true,
157
- cleanPackageJson: true,
158
- compatibility: "auto",
159
- cwd: process.cwd(),
160
- newlineBetween: true,
161
- replaceDeprecated: false,
162
- showChanges: true,
163
- sortKeys: false,
164
- strategy: "merge",
165
- yarnResolutions: true
166
- };
167
- const VALID_STRATEGIES = [
168
- "discard",
169
- "merge",
170
- "overwrite"
171
- ];
172
- const VALID_COMPATIBILITIES = [
173
- "auto",
174
- "v10",
175
- "v11",
176
- "v12"
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
- }
212
+ const FILE_PERMISSION_MODULUS = 512;
188
213
  /**
189
- * Resolve and normalize migration options with defaults.
190
- *
191
- * This function takes partial options and returns a complete options object
192
- * with all properties set to either the provided value or the default value.
214
+ * Resolve a filesystem path to a string suitable for a sibling temp file.
193
215
  *
194
- * @param options - Partial migration options
216
+ * @param path - Filesystem path to normalize
195
217
  *
196
- * @returns Complete options object with all required properties
218
+ * @returns String path suitable for path manipulation
219
+ */
220
+ function resolvePathString(path) {
221
+ if (path instanceof URL) return fileURLToPath(path);
222
+ return typeof path === "string" ? path : path.toString();
223
+ }
224
+ /**
225
+ * Read an existing file's permission bits, if the file exists.
197
226
  *
198
- * @example
199
- * ```ts
200
- * // Use all defaults
201
- * const opts = resolveOptions()
202
- * // { cleanNpmrc: true, cleanPackageJson: true, cwd: '/current/dir', ... }
227
+ * @param path - Filesystem path to inspect
203
228
  *
204
- * // Override specific options
205
- * const opts = resolveOptions({ sortKeys: true, cleanNpmrc: false })
206
- * // { cleanNpmrc: false, cleanPackageJson: true, sortKeys: true, ... }
207
- * ```
229
+ * @returns Unix permission bits, or `undefined` when the file does not exist
208
230
  */
209
- function resolveOptions(options = {}) {
210
- return {
211
- cleanNpmrc: options.cleanNpmrc ?? DEFAULT_OPTIONS.cleanNpmrc,
212
- compatibility: resolveCompatibility(options.compatibility),
213
- cwd: options.cwd ?? DEFAULT_OPTIONS.cwd,
214
- newlineBetween: options.newlineBetween ?? DEFAULT_OPTIONS.newlineBetween,
215
- showChanges: options.showChanges ?? DEFAULT_OPTIONS.showChanges,
216
- sortKeys: options.sortKeys ?? DEFAULT_OPTIONS.sortKeys,
217
- strategy: resolveStrategy(options.strategy),
218
- yarnResolutions: options.yarnResolutions ?? DEFAULT_OPTIONS.yarnResolutions,
219
- cleanPackageJson: options.cleanPackageJson ?? DEFAULT_OPTIONS.cleanPackageJson,
220
- replaceDeprecated: options.replaceDeprecated ?? DEFAULT_OPTIONS.replaceDeprecated
221
- };
231
+ async function resolveExistingMode(path) {
232
+ try {
233
+ return (await stat(path)).mode % FILE_PERMISSION_MODULUS;
234
+ } catch (error) {
235
+ if (error.code === "ENOENT") return;
236
+ throw error;
237
+ }
222
238
  }
223
- //#endregion
224
- //#region src/utils/fs.ts
225
239
  /**
226
240
  * Check whether a filesystem path exists.
227
241
  *
@@ -261,52 +275,208 @@ async function fsRemoveFile(path) {
261
275
  * @returns A promise that resolves after the file is written
262
276
  */
263
277
  async function fsWriteFile(path, content) {
264
- await writeFile(path, `${content.trimEnd()}\n`, "utf-8");
278
+ const pathString = resolvePathString(path);
279
+ const temporaryPath = join(dirname(pathString), `.${basename(pathString)}.${process.pid}.${randomUUID()}.tmp`);
280
+ const mode = await resolveExistingMode(path);
281
+ try {
282
+ await writeFile(temporaryPath, `${content.trimEnd()}\n`, {
283
+ encoding: "utf-8",
284
+ mode
285
+ });
286
+ if (mode !== void 0) await chmod(temporaryPath, mode);
287
+ await rename(temporaryPath, pathString);
288
+ } finally {
289
+ await rm(temporaryPath, { force: true });
290
+ }
265
291
  }
266
- getColor("cyan");
267
- getColor("yellow");
292
+ //#endregion
293
+ //#region src/utils/color.ts
268
294
  /**
269
295
  * Apply dim terminal styling to a value.
296
+ *
297
+ * @param text - Text or number to style
298
+ *
299
+ * @returns ANSI-styled string
270
300
  */
271
301
  const dim = getColor("dim");
272
302
  /**
273
303
  * Apply green terminal styling to a value.
304
+ *
305
+ * @param text - Text or number to style
306
+ *
307
+ * @returns ANSI-styled string
274
308
  */
275
309
  const green = getColor("green");
276
310
  /**
277
311
  * Apply red terminal styling to a value.
312
+ *
313
+ * @param text - Text or number to style
314
+ *
315
+ * @returns ANSI-styled string
278
316
  */
279
317
  const red = getColor("red");
280
318
  /**
281
319
  * Apply bold terminal styling to a value.
320
+ *
321
+ * @param text - Text or number to style
322
+ *
323
+ * @returns ANSI-styled string
282
324
  */
283
325
  const bold = getColor("bold");
284
326
  /**
285
327
  * Apply magenta terminal styling to a value.
328
+ *
329
+ * @param text - Text or number to style
330
+ *
331
+ * @returns ANSI-styled string
286
332
  */
287
333
  const magenta = getColor("magenta");
288
334
  //#endregion
289
- //#region src/utils/compatibility.ts
335
+ //#region src/utils/deprecated-settings.ts
290
336
  /**
291
- * Legacy build settings that can be replaced for pnpm v10.
337
+ * Replace deprecated audit fields with the structured `audit` setting.
338
+ *
339
+ * @param settings - Workspace settings to normalize in place
292
340
  */
293
- const PNPM_REPLACEABLE_IN_V10_SETTINGS = [
294
- "allowNonAppliedPatches",
295
- "ignoredBuiltDependencies",
296
- "neverBuiltDependencies",
297
- "onlyBuiltDependencies",
298
- "onlyBuiltDependenciesFile"
299
- ];
341
+ function normalizeAuditSettings(settings) {
342
+ const level = settings.audit?.level ?? settings.auditLevel;
343
+ const ignore = settings.audit?.ignore ?? settings.auditConfig?.ignoreGhsas;
344
+ if (level !== void 0 || ignore !== void 0) settings.audit = {
345
+ ...settings.audit,
346
+ ...level === void 0 ? {} : { level },
347
+ ...ignore === void 0 ? {} : { ignore }
348
+ };
349
+ Reflect.deleteProperty(settings, "auditConfig");
350
+ Reflect.deleteProperty(settings, "auditLevel");
351
+ }
352
+ /**
353
+ * Replace deprecated update fields with the structured `update` setting.
354
+ *
355
+ * @param settings - Workspace settings to normalize in place
356
+ */
357
+ function normalizeUpdateSettings(settings) {
358
+ const { updateConfig } = settings;
359
+ if (!updateConfig) return;
360
+ settings.update = {
361
+ ...updateConfig.ignoreDependencies === void 0 ? {} : { ignoreDeps: updateConfig.ignoreDependencies },
362
+ ...updateConfig.changeset === void 0 ? {} : { changeset: updateConfig.changeset },
363
+ ...updateConfig.githubActions === void 0 ? {} : { githubActions: updateConfig.githubActions },
364
+ ...updateConfig.githubActionsServer === void 0 ? {} : { githubActionsServer: updateConfig.githubActionsServer },
365
+ ...settings.update
366
+ };
367
+ Reflect.deleteProperty(settings, "updateConfig");
368
+ }
369
+ /**
370
+ * Replace former scalar aliases with their canonical v11+ settings.
371
+ *
372
+ * @param settings - Workspace settings to normalize in place
373
+ */
374
+ function normalizeScalarAliases(settings) {
375
+ if (settings.cleanupUnusedCatalogs !== void 0) {
376
+ settings.catalogPrune ??= settings.cleanupUnusedCatalogs;
377
+ Reflect.deleteProperty(settings, "cleanupUnusedCatalogs");
378
+ }
379
+ if (settings.enableGlobalVirtualStore !== void 0) {
380
+ settings.virtualStoreType ??= settings.enableGlobalVirtualStore ? "global" : "project";
381
+ Reflect.deleteProperty(settings, "enableGlobalVirtualStore");
382
+ }
383
+ }
384
+ /**
385
+ * Replace the older side-effects cache spellings with one structured value.
386
+ *
387
+ * @param settings - Workspace settings to normalize in place
388
+ */
389
+ function normalizeSideEffectsCache(settings) {
390
+ const declared = settings.sideEffectsCache;
391
+ const readonly = settings.sideEffectsCacheReadonly;
392
+ const remote = settings.remoteSideEffectsCache;
393
+ if (readonly === void 0 && remote === void 0) return;
394
+ const structured = typeof declared === "object" && declared !== null ? declared : void 0;
395
+ const shorthand = typeof declared === "boolean" ? declared : void 0;
396
+ settings.sideEffectsCache = {
397
+ ...shorthand === void 0 ? {} : {
398
+ read: readonly === true ? true : shorthand,
399
+ write: readonly === true ? false : shorthand
400
+ },
401
+ ...readonly === void 0 || shorthand !== void 0 ? {} : {
402
+ read: readonly,
403
+ ...readonly ? { write: false } : {}
404
+ },
405
+ ...remote === void 0 ? {} : { remote },
406
+ ...structured
407
+ };
408
+ Reflect.deleteProperty(settings, "remoteSideEffectsCache");
409
+ Reflect.deleteProperty(settings, "sideEffectsCacheReadonly");
410
+ }
411
+ function addRegistryScope(declarations, url, scope) {
412
+ declarations[url] ??= {};
413
+ const declaration = declarations[url];
414
+ declaration.scopes = [.../* @__PURE__ */ new Set([...declaration.scopes ?? [], scope])];
415
+ }
416
+ /**
417
+ * Replace `namedRegistries` with URL-keyed registry declarations.
418
+ *
419
+ * A URL can expose only one canonical prefix. Conflicting legacy aliases are
420
+ * therefore retained for manual resolution instead of being discarded.
421
+ *
422
+ * @param settings - Workspace settings to normalize in place
423
+ * @param warnings - Collection that receives unresolved conflict warnings
424
+ */
425
+ function normalizeNamedRegistries(settings, warnings) {
426
+ const { namedRegistries } = settings;
427
+ if (!namedRegistries) return;
428
+ const prefixesByUrl = /* @__PURE__ */ new Map();
429
+ for (const [prefix, url] of Object.entries(namedRegistries)) {
430
+ const prefixes = prefixesByUrl.get(url) ?? [];
431
+ prefixes.push(prefix);
432
+ prefixesByUrl.set(url, prefixes);
433
+ }
434
+ const conflicts = [...prefixesByUrl.entries()].filter(([, prefixes]) => prefixes.length > 1);
435
+ if (conflicts.length) {
436
+ warnings.push(`namedRegistries was kept because the new registries format supports one prefix per URL: ${conflicts.map(([url, prefixes]) => `${url} (${prefixes.join(", ")})`).join("; ")}.`);
437
+ return;
438
+ }
439
+ const declarations = {};
440
+ for (const [key, value] of Object.entries(settings.registries ?? {})) if (typeof value === "string") addRegistryScope(declarations, value, key === "default" ? "@" : key);
441
+ else declarations[key] = { ...value };
442
+ for (const [url, [prefix]] of prefixesByUrl) {
443
+ declarations[url] ??= {};
444
+ const declaration = declarations[url];
445
+ if (declaration.prefix && declaration.prefix !== prefix) {
446
+ warnings.push(`namedRegistries was kept because ${url} already declares prefix ${declaration.prefix}.`);
447
+ return;
448
+ }
449
+ declaration.prefix = prefix;
450
+ }
451
+ settings.registries = declarations;
452
+ Reflect.deleteProperty(settings, "namedRegistries");
453
+ }
454
+ /**
455
+ * Replace aliases that remain accepted but are no longer canonical.
456
+ *
457
+ * @param settings - Workspace settings to normalize in place
458
+ * @param warnings - Collection that receives unresolved conflict warnings
459
+ *
460
+ * @returns Nothing; settings and warnings are updated in place
461
+ */
462
+ function normalizeCurrentAliases(settings, warnings) {
463
+ normalizeAuditSettings(settings);
464
+ normalizeUpdateSettings(settings);
465
+ normalizeScalarAliases(settings);
466
+ normalizeSideEffectsCache(settings);
467
+ normalizeNamedRegistries(settings, warnings);
468
+ }
469
+ //#endregion
470
+ //#region src/utils/legacy-build-settings.ts
300
471
  /**
301
472
  * Read packages listed by the legacy `onlyBuiltDependenciesFile` setting.
302
473
  *
303
- * @param cwd - Working directory used to resolve the referenced file
304
- * @param file - Relative path stored in `onlyBuiltDependenciesFile`
474
+ * @param cwd - Workspace root directory
475
+ * @param file - Path declared by the legacy setting
305
476
  *
306
- * @returns Package names read from the referenced JSON file
477
+ * @returns Validated package names, or an empty list when no file is declared
307
478
  *
308
- * @throws {TypeError} When the referenced JSON value is not a string array
309
- * @throws {SyntaxError} When the referenced file contains invalid JSON
479
+ * @throws {TypeError} When the file does not contain an array of package names
310
480
  */
311
481
  async function readOnlyBuiltDependenciesFile(cwd, file) {
312
482
  if (!file) return [];
@@ -316,21 +486,56 @@ async function readOnlyBuiltDependenciesFile(cwd, file) {
316
486
  return value;
317
487
  }
318
488
  /**
489
+ * Read and validate one legacy build dependency list. Configuration files are
490
+ * external input, so their runtime shape can differ from its declared type.
491
+ *
492
+ * @param settings - Workspace settings containing the legacy list
493
+ * @param key - Legacy setting name to read
494
+ *
495
+ * @returns Validated package names, or an empty list when the setting is absent
496
+ *
497
+ * @throws {TypeError} When the setting is not an array of package names
498
+ */
499
+ function readLegacyBuildDependencyList(settings, key) {
500
+ const value = settings[key];
501
+ if (value === void 0) return [];
502
+ if (!Array.isArray(value) || !value.every((item) => typeof item === "string")) {
503
+ const npmrcKey = key.replace(/[A-Z]/gu, (character) => `-${character.toLowerCase()}`);
504
+ throw new TypeError(`Invalid ${key}: expected an array of package names. In .npmrc, use ${npmrcKey}[]=<package>.`);
505
+ }
506
+ return value;
507
+ }
508
+ /**
319
509
  * Build an `allowBuilds` map from legacy build-script settings.
320
510
  *
321
- * @param incomingSettings - Settings containing legacy build declarations
322
- * @param cwd - Working directory used to resolve referenced files
511
+ * @param incomingSettings - Legacy settings to read and normalize
512
+ * @param cwd - Workspace root used to resolve dependency list files
323
513
  *
324
- * @returns Package-to-permission map, or `undefined` when no entries exist
514
+ * @returns Package permissions, or `undefined` when no legacy values exist
325
515
  */
326
516
  async function collectAllowBuildsFromLegacy(incomingSettings, cwd) {
327
517
  const allowBuilds = {};
518
+ const allowed = readLegacyBuildDependencyList(incomingSettings, "onlyBuiltDependencies");
519
+ const ignored = readLegacyBuildDependencyList(incomingSettings, "ignoredBuiltDependencies");
520
+ const neverBuilt = readLegacyBuildDependencyList(incomingSettings, "neverBuiltDependencies");
328
521
  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;
522
+ for (const name of [...allowed, ...allowedFromFile]) allowBuilds[name] = true;
523
+ for (const name of ignored) allowBuilds[name] = false;
524
+ for (const name of neverBuilt) allowBuilds[name] = false;
332
525
  return Object.keys(allowBuilds).length ? allowBuilds : void 0;
333
526
  }
527
+ //#endregion
528
+ //#region src/utils/compatibility.ts
529
+ /**
530
+ * Legacy build settings that can be replaced for pnpm v10.
531
+ */
532
+ const PNPM_REPLACEABLE_IN_V10_SETTINGS = [
533
+ "allowNonAppliedPatches",
534
+ "ignoredBuiltDependencies",
535
+ "neverBuiltDependencies",
536
+ "onlyBuiltDependencies",
537
+ "onlyBuiltDependenciesFile"
538
+ ];
334
539
  /**
335
540
  * Resolve the pnpm v11 replacement for legacy package-manager settings.
336
541
  *
@@ -407,6 +612,7 @@ async function normalizeIncomingSettings(incomingSettings, options) {
407
612
  const pmOnFail = resolvePmOnFail(incomingSettings);
408
613
  if (pmOnFail) incomingSettings.pmOnFail = pmOnFail;
409
614
  normalizeAuditConfig(incomingSettings, warnings);
615
+ if (replaceDeprecated) normalizeCurrentAliases(incomingSettings, warnings);
410
616
  const runtimeVersion = resolveRuntimeVersion(incomingSettings, warnings);
411
617
  if (incomingSettings.ignoreDepScripts !== void 0) warnings.push("ignoreDepScripts was removed in pnpm v11 and has no direct replacement.");
412
618
  if (incomingSettings.ignorePatchFailures !== void 0) warnings.push("ignorePatchFailures was removed in pnpm v11; patch failures now always throw.");
@@ -466,6 +672,28 @@ const PNPM_V11_MAJOR = 11;
466
672
  */
467
673
  const PNPM_V12_MAJOR = 12;
468
674
  /**
675
+ * Resolve a pnpm major from a package manager declaration.
676
+ *
677
+ * @param hint - Package manager declaration to inspect
678
+ *
679
+ * @returns Detected pnpm major, or `undefined` when the hint does not match
680
+ */
681
+ function resolvePnpmMajor(hint) {
682
+ const match = hint?.match(/^pnpm@[\s<=>^~]*(?<major>\d+)(?:\.|\s|$)/u);
683
+ return match?.groups?.major ? Number(match.groups.major) : void 0;
684
+ }
685
+ /**
686
+ * Normalize the single-or-array devEngines declaration.
687
+ *
688
+ * @param value - Optional single or multiple package manager declarations
689
+ *
690
+ * @returns Package manager declarations normalized to an array
691
+ */
692
+ function normalizePackageManagers(value) {
693
+ if (Array.isArray(value)) return value;
694
+ return value ? [value] : [];
695
+ }
696
+ /**
469
697
  * Resolve final compatibility target from user option and package manager hint.
470
698
  *
471
699
  * @param compatibility - Explicit or automatically detected compatibility target
@@ -476,10 +704,11 @@ const PNPM_V12_MAJOR = 12;
476
704
  */
477
705
  function resolveCompatibilityTarget(compatibility, packageManager, devPackageManager) {
478
706
  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";
707
+ const packageManagerMajor = resolvePnpmMajor(packageManager);
708
+ const devPackageManagerMajors = normalizePackageManagers(devPackageManager).filter((item) => item.name === "pnpm").map((item) => resolvePnpmMajor(`pnpm@${item.version ?? ""}`)).filter((major) => major !== void 0);
709
+ const major = packageManagerMajor ?? (devPackageManagerMajors.length ? Math.max(...devPackageManagerMajors) : void 0);
710
+ if (major !== void 0 && major >= PNPM_V12_MAJOR) return "v12";
711
+ return major !== void 0 && major >= PNPM_V11_MAJOR ? "v11" : "v10";
483
712
  }
484
713
  //#endregion
485
714
  //#region src/utils/merge.ts
@@ -557,6 +786,380 @@ function mergeByStrategy(existing, incoming, strategy) {
557
786
  }
558
787
  }
559
788
  //#endregion
789
+ //#region src/settings-fields.ts
790
+ /**
791
+ * Settings that a project-level `pnpm-workspace.yaml` must never control.
792
+ *
793
+ * @see https://github.com/pnpm/pnpm/blob/main/pnpm11/config/reader/src/index.ts
794
+ * @see https://github.com/pnpm/pnpm/blob/v12.2.1/pnpm/crates/config/src/refused_keys.rs
795
+ */
796
+ const PNPM_PROJECT_REFUSED_SETTINGS = [
797
+ "allProjects",
798
+ "allProjectsGraph",
799
+ "authConfig",
800
+ "bin",
801
+ "cliOptions",
802
+ "configByUri",
803
+ "configDir",
804
+ "explicitlySetKeys",
805
+ "finders",
806
+ "globalBinDir",
807
+ "globalDir",
808
+ "globalPkgDir",
809
+ "hooks",
810
+ "npmrcAuthFile",
811
+ "packageManager",
812
+ "packageManagerNetworkConfig",
813
+ "packageManagerRegistries",
814
+ "pnpmHomeDir",
815
+ "prodAllProjectsGraph",
816
+ "prodOnlySelectedProjectDirs",
817
+ "rootProjectManifest",
818
+ "rootProjectManifestDir",
819
+ "scope",
820
+ "selectedProjectsGraph",
821
+ "stateDir",
822
+ "userConfig",
823
+ "userconfig",
824
+ "wantedPackageManager",
825
+ "workspaceDir"
826
+ ];
827
+ /**
828
+ * Settings added to, or recognized by, pnpm's v11 workspace config schema.
829
+ * Combined with the v10 settings below to form the complete v11 allowlist.
830
+ *
831
+ * @see https://github.com/pnpm/pnpm/blob/main/pnpm11/config/reader/src/types.ts
832
+ * @see https://github.com/pnpm/pnpm/blob/main/pnpm11/config/reader/src/unknownSettings.ts
833
+ */
834
+ const PNPM_V11_ADDITIONAL_WORKSPACE_SETTINGS_FIELDS = [
835
+ "access",
836
+ "aggregateOutput",
837
+ "allowNew",
838
+ "allowSameVersion",
839
+ "audit",
840
+ "auditIgnorePrune",
841
+ "autoConfirmAllPrompts",
842
+ "bail",
843
+ "binLinks",
844
+ "catalog",
845
+ "catalogPrune",
846
+ "catalogs",
847
+ "changedFilesIgnorePattern",
848
+ "commitHooks",
849
+ "confirmModulesPurge",
850
+ "cpu",
851
+ "dedupePeers",
852
+ "depth",
853
+ "description",
854
+ "dev",
855
+ "dryRun",
856
+ "embedReadme",
857
+ "enablePnp",
858
+ "excludeLinksFromLockfile",
859
+ "extraBinPaths",
860
+ "extraEnv",
861
+ "fetchingConcurrency",
862
+ "filter",
863
+ "filterProd",
864
+ "force",
865
+ "frozenStore",
866
+ "git",
867
+ "gitTagVersion",
868
+ "global",
869
+ "globalPath",
870
+ "globalPrefix",
871
+ "hoistingLimits",
872
+ "ignoreCurrentSpecifiers",
873
+ "ignoreWorkspace",
874
+ "initAuthorEmail",
875
+ "initAuthorName",
876
+ "initAuthorUrl",
877
+ "initLicense",
878
+ "initPackageManager",
879
+ "initType",
880
+ "initVersion",
881
+ "json",
882
+ "legacyDirFiltering",
883
+ "libc",
884
+ "lockfileOnly",
885
+ "long",
886
+ "maxSockets",
887
+ "message",
888
+ "minimumReleaseAgeExcludePrune",
889
+ "minimumReleaseAgeIgnoreMissingTime",
890
+ "minimumReleaseAgeStrict",
891
+ "namedRegistries",
892
+ "nodeDownloadMirrors",
893
+ "nodeExperimentalPackageMap",
894
+ "nodePackageMapType",
895
+ "offline",
896
+ "only",
897
+ "os",
898
+ "packDestination",
899
+ "packGzipLevel",
900
+ "packageConfigs",
901
+ "packageLock",
902
+ "packages",
903
+ "parseable",
904
+ "patchesDir",
905
+ "pending",
906
+ "pmOnFail",
907
+ "pnpmExecPath",
908
+ "pnprServer",
909
+ "preferOffline",
910
+ "preserveAbsolutePaths",
911
+ "progress",
912
+ "provenance",
913
+ "publishBranch",
914
+ "recursive",
915
+ "registries",
916
+ "registriesByPrefix",
917
+ "registriesByScope",
918
+ "registry",
919
+ "registryOptionsByUrl",
920
+ "remoteSideEffectsCache",
921
+ "reporter",
922
+ "reporterHidePrefix",
923
+ "reverse",
924
+ "runtime",
925
+ "runtimeOnFail",
926
+ "save",
927
+ "saveCatalogName",
928
+ "saveDev",
929
+ "saveOptional",
930
+ "savePeer",
931
+ "saveProd",
932
+ "scriptsPrependNodePath",
933
+ "sideEffectsCacheRead",
934
+ "sideEffectsCacheWrite",
935
+ "signGitTag",
936
+ "skipManifestObfuscation",
937
+ "sort",
938
+ "stream",
939
+ "tasks",
940
+ "testPattern",
941
+ "trustLockfile",
942
+ "tryLoadDefaultPnpmfile",
943
+ "umask",
944
+ "update",
945
+ "useGitBranchLockfile",
946
+ "useLockfile",
947
+ "userAgent",
948
+ "version",
949
+ "versioning",
950
+ "virtualStoreOnly",
951
+ "virtualStoreType",
952
+ "workspacePackagePatterns",
953
+ "workspacePackages",
954
+ "workspacePrefix",
955
+ "workspaceRoot",
956
+ "yes"
957
+ ];
958
+ const refusedProjectSettings = new Set(PNPM_PROJECT_REFUSED_SETTINGS);
959
+ /**
960
+ * Settings accepted by a pnpm v11 project workspace manifest.
961
+ */
962
+ const PNPM_V11_WORKSPACE_SETTINGS_FIELDS = [.../* @__PURE__ */ new Set([...PNPM_V10_NPMRC_SETTINGS_FIELDS, ...PNPM_V11_ADDITIONAL_WORKSPACE_SETTINGS_FIELDS])].filter((field) => !refusedProjectSettings.has(field));
963
+ /**
964
+ * Settings that are supported by v11 but not by pnpm v12.
965
+ */
966
+ const PNPM_V11_ONLY_WORKSPACE_SETTINGS = ["confirmModulesPurge", "packageConfigs"];
967
+ /**
968
+ * Settings introduced by pnpm v12.
969
+ */
970
+ const PNPM_V12_ONLY_WORKSPACE_SETTINGS = [
971
+ "autoInstallPeersFromHighestMatch",
972
+ "externalDependencies",
973
+ "globalShims"
974
+ ];
975
+ const v11OnlyWorkspaceSettings = new Set(PNPM_V11_ONLY_WORKSPACE_SETTINGS);
976
+ /**
977
+ * Settings accepted by a pnpm v12 project workspace manifest.
978
+ */
979
+ const PNPM_V12_WORKSPACE_SETTINGS_FIELDS = [...PNPM_V11_WORKSPACE_SETTINGS_FIELDS.filter((field) => !v11OnlyWorkspaceSettings.has(field)), ...PNPM_V12_ONLY_WORKSPACE_SETTINGS];
980
+ /**
981
+ * Fields pnpm v11 accepts inside a `packageConfigs` entry.
982
+ */
983
+ const PNPM_V11_PACKAGE_CONFIG_FIELDS = [
984
+ "hoist",
985
+ "modulesDir",
986
+ "overrides",
987
+ "saveExact",
988
+ "savePrefix"
989
+ ];
990
+ //#endregion
991
+ //#region src/utils/settings-schema.ts
992
+ const PNPM_V10_WORKSPACE_SETTINGS_FIELDS = [
993
+ ...PNPM_V10_NPMRC_SETTINGS_FIELDS,
994
+ "catalog",
995
+ "catalogs",
996
+ "packages"
997
+ ];
998
+ const PROJECT_REFUSED_SETTINGS = new Set(PNPM_PROJECT_REFUSED_SETTINGS);
999
+ const PNPM_V11_ONLY_SETTINGS = new Set(PNPM_V11_ONLY_WORKSPACE_SETTINGS);
1000
+ const PNPM_V11_SETTINGS = new Set(PNPM_V11_WORKSPACE_SETTINGS_FIELDS);
1001
+ const PNPM_V12_ONLY_SETTINGS = new Set(PNPM_V12_ONLY_WORKSPACE_SETTINGS);
1002
+ const PNPM_V12_SETTINGS = new Set(PNPM_V12_WORKSPACE_SETTINGS_FIELDS);
1003
+ const PNPM_V10_SETTINGS = new Set(PNPM_V10_WORKSPACE_SETTINGS_FIELDS);
1004
+ const REGISTRY_SETTINGS = /* @__PURE__ */ new Set([
1005
+ "namedRegistries",
1006
+ "registries",
1007
+ "registriesByPrefix",
1008
+ "registriesByScope",
1009
+ "registry",
1010
+ "registryOptionsByUrl"
1011
+ ]);
1012
+ const REGISTRY_CREDENTIAL_KEYS = /* @__PURE__ */ new Set([
1013
+ "auth",
1014
+ "authtoken",
1015
+ "password",
1016
+ "token",
1017
+ "tokenhelper",
1018
+ "username"
1019
+ ]);
1020
+ const WORKSPACE_SCHEMA_DIRECTIVE = "$schema";
1021
+ /**
1022
+ * Create an empty collection of schema selection issues.
1023
+ *
1024
+ * @returns Empty issue lists for every supported issue category
1025
+ */
1026
+ function createSettingsIssues() {
1027
+ return {
1028
+ incompatible: [],
1029
+ nonCamelCase: [],
1030
+ refused: [],
1031
+ unsafe: [],
1032
+ unknown: [],
1033
+ unsupported: []
1034
+ };
1035
+ }
1036
+ function hasUrlCredentials(value) {
1037
+ try {
1038
+ const url = new URL(value);
1039
+ return Boolean(url.username || url.password);
1040
+ } catch {
1041
+ return false;
1042
+ }
1043
+ }
1044
+ function isRegistryCredentialKey(key) {
1045
+ return REGISTRY_CREDENTIAL_KEYS.has(key.replaceAll("-", "").replaceAll("_", "").toLowerCase());
1046
+ }
1047
+ function isUnsafeRegistryUrl(value) {
1048
+ return value.includes("${") || hasUrlCredentials(value);
1049
+ }
1050
+ function containsUnsafeRegistryValue(value, checkCredentialKeys = false) {
1051
+ if (typeof value === "string") return isUnsafeRegistryUrl(value);
1052
+ if (!value || typeof value !== "object") return false;
1053
+ return Object.entries(value).some(([key, entryValue]) => checkCredentialKeys && isRegistryCredentialKey(key) || isUnsafeRegistryUrl(key) || containsUnsafeRegistryValue(entryValue, true));
1054
+ }
1055
+ function resolveCamelCaseKey(key) {
1056
+ return Object.keys(camelcaseKeys({ [key]: true }))[0] ?? key;
1057
+ }
1058
+ function resolveTargetSettings(compatibility) {
1059
+ switch (compatibility) {
1060
+ case "v10": return PNPM_V10_SETTINGS;
1061
+ case "v11": return PNPM_V11_SETTINGS;
1062
+ case "v12": return PNPM_V12_SETTINGS;
1063
+ default: throw new TypeError(`Unsupported compatibility target: ${compatibility}`);
1064
+ }
1065
+ }
1066
+ function isSettingFromAnotherMajor(key, compatibility) {
1067
+ if (compatibility === "v11") return PNPM_V12_ONLY_SETTINGS.has(key);
1068
+ if (compatibility === "v12") return PNPM_V11_ONLY_SETTINGS.has(key);
1069
+ return PNPM_V11_SETTINGS.has(key) || PNPM_V12_SETTINGS.has(key);
1070
+ }
1071
+ function resolveSettingIssue({ allowedFields, compatibility, key, npmrc, targetSettings, value }) {
1072
+ if (!npmrc && key !== resolveCamelCaseKey(key)) return "nonCamelCase";
1073
+ if (compatibility !== "v10" && PROJECT_REFUSED_SETTINGS.has(key)) return "refused";
1074
+ if (REGISTRY_SETTINGS.has(key) && containsUnsafeRegistryValue(value)) return "unsafe";
1075
+ if (!targetSettings.has(key)) return isSettingFromAnotherMajor(key, compatibility) ? "incompatible" : "unknown";
1076
+ if (allowedFields && !allowedFields.has(key)) return "unsupported";
1077
+ }
1078
+ /**
1079
+ * Select settings that are valid for a concrete pnpm workspace schema.
1080
+ *
1081
+ * Unrecognized, refused, incompatible, and destination-specific unsupported
1082
+ * settings are reported but never returned for migration.
1083
+ *
1084
+ * @param rawSettings - Settings read from a legacy configuration source
1085
+ * @param compatibility - Concrete pnpm compatibility target
1086
+ * @param options - Source spelling and destination field restrictions
1087
+ *
1088
+ * @returns Selected settings, original source keys, and rejected field groups
1089
+ */
1090
+ function selectPnpmSettings(rawSettings, compatibility, options = {}) {
1091
+ const issues = createSettingsIssues();
1092
+ const keys = [];
1093
+ const settings = {};
1094
+ const targetSettings = resolveTargetSettings(compatibility);
1095
+ const allowedFields = options.allowedFields ? new Set(options.allowedFields) : void 0;
1096
+ for (const [originalKey, value] of Object.entries(rawSettings)) {
1097
+ const key = options.npmrc ? resolveCamelCaseKey(originalKey) : originalKey;
1098
+ const issue = resolveSettingIssue({
1099
+ allowedFields,
1100
+ compatibility,
1101
+ key,
1102
+ npmrc: Boolean(options.npmrc),
1103
+ targetSettings,
1104
+ value
1105
+ });
1106
+ if (issue) issues[issue].push(originalKey);
1107
+ else {
1108
+ keys.push(originalKey);
1109
+ settings[key] = value;
1110
+ }
1111
+ }
1112
+ return {
1113
+ issues,
1114
+ keys,
1115
+ settings
1116
+ };
1117
+ }
1118
+ function assertPackageConfigFields(settings) {
1119
+ const { packageConfigs } = settings;
1120
+ if (packageConfigs === void 0) return;
1121
+ const allowedFields = new Set(PNPM_V11_PACKAGE_CONFIG_FIELDS);
1122
+ const entries = Array.isArray(packageConfigs) ? packageConfigs.flatMap((entry, index) => {
1123
+ if (!entry || typeof entry !== "object" || Array.isArray(entry)) throw new TypeError(`packageConfigs[${index}] must be an object with a match array.`);
1124
+ const { match, ...config } = entry;
1125
+ if (!Array.isArray(match) || !match.every((projectName) => typeof projectName === "string")) throw new TypeError(`packageConfigs[${index}].match must be an array of package names.`);
1126
+ return [[`packageConfigs[${index}]`, config]];
1127
+ }) : Object.entries(packageConfigs);
1128
+ for (const [name, config] of entries) {
1129
+ if (!config || typeof config !== "object" || Array.isArray(config)) throw new TypeError(`${name} must contain a project settings object.`);
1130
+ const unsupported = Object.keys(config).filter((key) => !allowedFields.has(key));
1131
+ if (unsupported.length) throw new TypeError(`${name} contains unsupported project settings: ${unsupported.map((key) => JSON.stringify(key)).join(", ")}.`);
1132
+ }
1133
+ }
1134
+ function formatIssueList(keys) {
1135
+ return keys.map((key) => JSON.stringify(key)).join(", ");
1136
+ }
1137
+ /**
1138
+ * Assert that an existing workspace manifest matches the selected pnpm major.
1139
+ *
1140
+ * The migrator refuses to rewrite a manifest containing ignored settings so a
1141
+ * migration cannot silently preserve invalid output.
1142
+ *
1143
+ * @param settings - Existing workspace settings to validate
1144
+ * @param compatibility - Concrete pnpm compatibility target
1145
+ *
1146
+ * @returns Nothing when all settings match the target schema
1147
+ *
1148
+ * @throws {TypeError} When the manifest contains incompatible settings
1149
+ */
1150
+ function assertCompatibleWorkspaceSettings(settings, compatibility) {
1151
+ if (compatibility === "v10") return;
1152
+ const { issues } = selectPnpmSettings(Object.fromEntries(Object.entries(settings).filter(([key]) => key !== WORKSPACE_SCHEMA_DIRECTIVE)), compatibility);
1153
+ const problems = [];
1154
+ if (issues.refused.length) problems.push(`refused: ${formatIssueList(issues.refused)}`);
1155
+ if (issues.incompatible.length) problems.push(`other pnpm major: ${formatIssueList(issues.incompatible)}`);
1156
+ if (issues.nonCamelCase.length) problems.push(`not camelCase: ${formatIssueList(issues.nonCamelCase)}`);
1157
+ if (issues.unknown.length) problems.push(`unrecognized: ${formatIssueList(issues.unknown)}`);
1158
+ if (issues.unsafe.length) problems.push(`unsafe registry URL: ${formatIssueList(issues.unsafe)}`);
1159
+ if (problems.length) throw new TypeError(`pnpm-workspace.yaml is incompatible with pnpm ${compatibility.slice(1)} (${problems.join("; ")}).`);
1160
+ if (compatibility === "v11") assertPackageConfigFields(settings);
1161
+ }
1162
+ //#endregion
560
1163
  //#region src/utils/npmrc.ts
561
1164
  /**
562
1165
  * Authentication and registry keys that must remain in `.npmrc`.
@@ -581,6 +1184,9 @@ const NPMRC_AUTH_OR_REGISTRY_KEYS = [
581
1184
  * Pattern for pnpm's channel-specific Node.js mirror keys.
582
1185
  */
583
1186
  const NODE_MIRROR_KEY_PATTERN = /^node-mirror:(?<channel>.+)$/iu;
1187
+ function mergeSettingsIssues(target, source, sourceKeys) {
1188
+ for (const reason of Object.keys(target)) if (source[reason].length) target[reason].push(...sourceKeys ?? source[reason]);
1189
+ }
584
1190
  /**
585
1191
  * Normalize `.npmrc` key for case-insensitive matching.
586
1192
  *
@@ -622,9 +1228,8 @@ function isNpmrcAuthOrRegistryKey(key) {
622
1228
  /**
623
1229
  * Remove pnpm-related settings from `.npmrc` file.
624
1230
  *
625
- * This function reads the `.npmrc` file, filters out all lines that start with
626
- * pnpm-specific configuration keys (as defined in PNPM_SETTINGS_FIELDS), and
627
- * writes the cleaned content back to the file.
1231
+ * This function reads the `.npmrc` file, removes lines whose exact normalized
1232
+ * keys were migrated, and writes the cleaned content back to the file.
628
1233
  *
629
1234
  * @param path - Absolute path to the `.npmrc` file
630
1235
  * @param compatibility - Concrete pnpm compatibility target
@@ -636,17 +1241,15 @@ function isNpmrcAuthOrRegistryKey(key) {
636
1241
  *
637
1242
  * @example
638
1243
  * ```ts
639
- * await pruneNpmrc('/path/to/.npmrc')
1244
+ * await pruneNpmrc('/path/to/.npmrc', 'v10', ['node-linker'])
640
1245
  * ```
641
1246
  */
642
- async function pruneNpmrc(path, compatibility = "v10", migratedKeys = []) {
643
- const pnpmSettingsFields = PNPM_SETTINGS_FIELDS.map((field) => kebabCase(field));
1247
+ async function pruneNpmrc(path, compatibility, migratedKeys) {
644
1248
  const migratedKeySet = new Set(migratedKeys.map(normalizeNpmrcKey));
645
1249
  const updatedContent = (await fsReadFile(path)).split(/\r?\n/u).filter((line) => {
646
1250
  const key = getNpmrcLineKey(line);
647
1251
  if (!key) return true;
648
- if (compatibility !== "v10") return !migratedKeySet.has(normalizeNpmrcKey(key));
649
- return !pnpmSettingsFields.some((field) => key.startsWith(field));
1252
+ return !migratedKeySet.has(key);
650
1253
  }).join("\n").trimEnd();
651
1254
  if (compatibility !== "v10" && !updatedContent.trim()) {
652
1255
  await fsRemoveFile(path);
@@ -658,44 +1261,271 @@ async function pruneNpmrc(path, compatibility = "v10", migratedKeys = []) {
658
1261
  * Read `.npmrc` and return settings that should be migrated into workspace config.
659
1262
  *
660
1263
  * - `v10`: returns settings from the legacy whitelist.
661
- * - `v11+`: excludes auth/registry keys because pnpm still reads them from
662
- * `.npmrc`.
1264
+ * - `v11+`: selects target-schema settings and retains auth, registry,
1265
+ * refused, incompatible, and unknown keys.
663
1266
  *
664
1267
  * @param path - Absolute path to the `.npmrc` file
665
1268
  * @param compatibility - Concrete pnpm compatibility target
1269
+ * @param options - Optional destination-specific field restrictions
666
1270
  *
667
1271
  * @returns Migratable settings and their original `.npmrc` keys
668
1272
  *
669
1273
  * @throws {Error} When file reading or INI parsing fails
670
1274
  */
671
- async function readMigratableNpmrc(path, compatibility) {
1275
+ async function readMigratableNpmrc(path, compatibility, options = {}) {
672
1276
  const raw = await readIniFile(path);
1277
+ const issues = createSettingsIssues();
673
1278
  if (compatibility === "v10") {
674
- const settings = camelcaseKeys(raw);
1279
+ const pnpmSettingsFields = new Set(PNPM_V10_NPMRC_SETTINGS_FIELDS.map((field) => normalizeNpmrcKey(kebabCase(field))));
1280
+ const keys = Object.keys(raw).filter((key) => pnpmSettingsFields.has(normalizeNpmrcKey(key)));
1281
+ const migratable = Object.fromEntries(keys.map((key) => [key, raw[key]]));
675
1282
  return {
676
- keys: PNPM_SETTINGS_FIELDS,
677
- settings: pick(settings, PNPM_SETTINGS_FIELDS)
1283
+ issues,
1284
+ keys,
1285
+ settings: camelcaseKeys(migratable)
678
1286
  };
679
1287
  }
680
1288
  const migratable = {};
681
1289
  const keys = [];
682
1290
  const nodeDownloadMirrors = {};
1291
+ const nodeDownloadMirrorKeys = [];
683
1292
  for (const [key, value] of Object.entries(raw)) if (!isNpmrcAuthOrRegistryKey(key)) {
684
- keys.push(key);
685
1293
  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;
688
- }
1294
+ if (nodeMirrorMatch?.groups?.channel) {
1295
+ nodeDownloadMirrorKeys.push(key);
1296
+ nodeDownloadMirrors[nodeMirrorMatch.groups.channel] = String(value);
1297
+ } else {
1298
+ const selected = selectPnpmSettings({ [key]: value }, compatibility, {
1299
+ allowedFields: options.allowedFields,
1300
+ npmrc: true
1301
+ });
1302
+ keys.push(...selected.keys);
1303
+ Object.assign(migratable, selected.settings);
1304
+ mergeSettingsIssues(issues, selected.issues);
1305
+ }
1306
+ }
689
1307
  const settings = camelcaseKeys(migratable);
690
- if (Object.keys(nodeDownloadMirrors).length) settings.nodeDownloadMirrors = nodeDownloadMirrors;
1308
+ if (Object.keys(nodeDownloadMirrors).length) {
1309
+ const selected = selectPnpmSettings({ nodeDownloadMirrors }, compatibility, { allowedFields: options.allowedFields });
1310
+ if (selected.keys.length) {
1311
+ keys.push(...nodeDownloadMirrorKeys);
1312
+ settings.nodeDownloadMirrors = nodeDownloadMirrors;
1313
+ } else mergeSettingsIssues(issues, selected.issues, nodeDownloadMirrorKeys);
1314
+ }
691
1315
  return {
1316
+ issues,
692
1317
  keys,
693
1318
  settings
694
1319
  };
695
1320
  }
696
1321
  //#endregion
1322
+ //#region src/utils/config.ts
1323
+ /**
1324
+ * Resolve the indentation width for a YAML document.
1325
+ *
1326
+ * @param content - Raw YAML content
1327
+ *
1328
+ * @returns Detected indentation width or the project default
1329
+ */
1330
+ function resolveYamlIndent(content) {
1331
+ const detectedIndent = detectIndent(content).amount;
1332
+ return detectedIndent > 0 ? detectedIndent : 2;
1333
+ }
1334
+ /**
1335
+ * Read and parse a package manifest when it exists.
1336
+ *
1337
+ * @param path - Absolute path to `package.json`
1338
+ * @param exists - Whether the package manifest exists
1339
+ *
1340
+ * @returns Parsed package manifest and detected indentation
1341
+ *
1342
+ * @throws {SyntaxError} When the package manifest contains invalid JSON
1343
+ */
1344
+ async function readPackageJson(path, exists) {
1345
+ if (!exists) return {
1346
+ indent: 2,
1347
+ value: {}
1348
+ };
1349
+ const content = await fsReadFile(path);
1350
+ return {
1351
+ indent: detectIndent(content).indent,
1352
+ value: JSON.parse(content)
1353
+ };
1354
+ }
1355
+ /**
1356
+ * Read and parse a pnpm workspace manifest when it exists.
1357
+ *
1358
+ * @param path - Absolute path to `pnpm-workspace.yaml`
1359
+ * @param exists - Whether the workspace manifest exists
1360
+ *
1361
+ * @returns Parsed workspace settings and detected indentation
1362
+ *
1363
+ * @throws {Error} When the workspace manifest contains invalid YAML
1364
+ */
1365
+ async function readPnpmWorkspace(path, exists) {
1366
+ if (!exists) return {
1367
+ document: new Document({}),
1368
+ indent: 2,
1369
+ value: {}
1370
+ };
1371
+ const content = await fsReadFile(path);
1372
+ const document = parseDocument(content);
1373
+ const [parseError] = document.errors;
1374
+ if (parseError) throw parseError;
1375
+ const parsedValue = document.toJS();
1376
+ if (parsedValue !== null && !isMap(document.contents)) throw new TypeError("pnpm-workspace.yaml must contain a root mapping.");
1377
+ if (parsedValue === null) document.contents = document.createNode({});
1378
+ return {
1379
+ document,
1380
+ indent: resolveYamlIndent(content),
1381
+ value: parsedValue ?? {}
1382
+ };
1383
+ }
1384
+ /**
1385
+ * Resolve migratable pnpm settings from a package manifest.
1386
+ *
1387
+ * Yarn resolutions are merged into pnpm overrides when enabled. Empty
1388
+ * overrides are removed from the result.
1389
+ *
1390
+ * @param packageJson - Parsed package manifest
1391
+ * @param yarnResolutions - Whether to convert Yarn resolutions to pnpm overrides
1392
+ * @param compatibility - Concrete pnpm compatibility target
1393
+ *
1394
+ * @returns Selected workspace settings and source cleanup metadata
1395
+ */
1396
+ function resolvePackageJsonSettings(packageJson, yarnResolutions, compatibility) {
1397
+ const selected = selectPnpmSettings(Object.fromEntries(Object.entries(packageJson.pnpm ?? {})), compatibility);
1398
+ const migrateYarnResolutions = Boolean(yarnResolutions && packageJson.resolutions);
1399
+ const pnpmSettings = migrateYarnResolutions ? {
1400
+ ...selected.settings,
1401
+ overrides: defu(selected.settings.overrides, packageJson.resolutions)
1402
+ } : { ...selected.settings };
1403
+ if (pnpmSettings.overrides && !Object.keys(pnpmSettings.overrides).length) delete pnpmSettings.overrides;
1404
+ return {
1405
+ issues: selected.issues,
1406
+ keys: selected.keys,
1407
+ settings: pnpmSettings,
1408
+ yarnResolutions: migrateYarnResolutions
1409
+ };
1410
+ }
1411
+ //#endregion
1412
+ //#region src/utils/project-npmrc.ts
1413
+ function resolvePackageJsonPatterns(patterns) {
1414
+ return patterns.map((pattern) => {
1415
+ const negated = pattern.startsWith("!");
1416
+ const workspacePattern = (negated ? pattern.slice(1) : pattern).replace(/\/+$/u, "");
1417
+ const packageJsonPattern = workspacePattern === "." ? PACKAGE_JSON : `${workspacePattern}/${PACKAGE_JSON}`;
1418
+ return negated ? `!${packageJsonPattern}` : packageJsonPattern;
1419
+ });
1420
+ }
1421
+ /**
1422
+ * Collect unique workspace package patterns from multiple settings sources.
1423
+ *
1424
+ * @param settingsSources - Workspace settings that may declare package patterns
1425
+ *
1426
+ * @returns Unique package patterns in source order
1427
+ */
1428
+ function collectWorkspacePackagePatterns(...settingsSources) {
1429
+ return [...new Set(settingsSources.flatMap((settings) => Array.isArray(settings.packages) ? settings.packages : []))];
1430
+ }
1431
+ async function readProjectManifestCandidates(cwd, patterns) {
1432
+ if (!patterns.some((pattern) => !pattern.startsWith("!"))) return {
1433
+ candidates: [],
1434
+ warnings: []
1435
+ };
1436
+ const rootPackageJsonPath = resolve(cwd, PACKAGE_JSON);
1437
+ const packageJsonPaths = await glob(resolvePackageJsonPatterns(patterns), {
1438
+ absolute: true,
1439
+ cwd,
1440
+ dot: true,
1441
+ ignore: ["**/node_modules/**"],
1442
+ onlyFiles: true
1443
+ });
1444
+ const candidateResults = await Promise.all([...new Set(packageJsonPaths)].sort().filter((packageJsonPath) => resolve(packageJsonPath) !== rootPackageJsonPath).map(async (packageJsonPath) => {
1445
+ const projectDir = dirname(packageJsonPath);
1446
+ const npmrcPath = resolve(projectDir, NPMRC);
1447
+ if (!await fsExists(npmrcPath)) return {};
1448
+ const packageJson = await readPackageJson(packageJsonPath, true);
1449
+ if (!packageJson.value.name) return { warning: `${relative(cwd, npmrcPath)} was kept because its package.json has no name for packageConfigs matching.` };
1450
+ return { candidate: {
1451
+ npmrcPath,
1452
+ packageJsonPath,
1453
+ projectName: packageJson.value.name
1454
+ } };
1455
+ }));
1456
+ const candidates = [];
1457
+ const warnings = [];
1458
+ for (const result of candidateResults) {
1459
+ if (result.candidate) candidates.push(result.candidate);
1460
+ if (result.warning) warnings.push(result.warning);
1461
+ }
1462
+ return {
1463
+ candidates,
1464
+ warnings
1465
+ };
1466
+ }
1467
+ /**
1468
+ * Collect supported settings from subproject `.npmrc` files.
1469
+ *
1470
+ * pnpm v11 accepts five fields in `packageConfigs`. pnpm v12 does not support
1471
+ * `packageConfigs`, so its subproject settings are only reported and retained.
1472
+ *
1473
+ * @param cwd - Workspace root directory
1474
+ * @param patterns - Workspace package patterns used to discover subprojects
1475
+ * @param compatibility - Concrete pnpm compatibility target
1476
+ *
1477
+ * @returns Migratable project settings, source metadata, and warnings
1478
+ */
1479
+ async function readProjectNpmrcMigrations(cwd, patterns, compatibility) {
1480
+ if (compatibility === "v10") return {
1481
+ packageConfigs: {},
1482
+ projects: [],
1483
+ warnings: []
1484
+ };
1485
+ const { candidates, warnings } = await readProjectManifestCandidates(cwd, patterns);
1486
+ const candidatesByName = /* @__PURE__ */ new Map();
1487
+ for (const candidate of candidates) {
1488
+ const namedCandidates = candidatesByName.get(candidate.projectName) ?? [];
1489
+ namedCandidates.push(candidate);
1490
+ candidatesByName.set(candidate.projectName, namedCandidates);
1491
+ }
1492
+ const packageConfigs = {};
1493
+ const projects = [];
1494
+ const allowedFields = compatibility === "v11" ? PNPM_V11_PACKAGE_CONFIG_FIELDS : [];
1495
+ const migrationResults = await Promise.all([...candidatesByName].map(async ([projectName, namedCandidates]) => {
1496
+ if (namedCandidates.length > 1) return { warning: `Subproject .npmrc files for duplicate package name ${JSON.stringify(projectName)} were kept: ${namedCandidates.map((candidate) => relative(cwd, candidate.packageJsonPath)).join(", ")}.` };
1497
+ const [candidate] = namedCandidates;
1498
+ return { project: {
1499
+ migratable: await readMigratableNpmrc(candidate.npmrcPath, compatibility, { allowedFields }),
1500
+ npmrcPath: candidate.npmrcPath,
1501
+ projectName
1502
+ } };
1503
+ }));
1504
+ for (const result of migrationResults) {
1505
+ if (result.warning) warnings.push(result.warning);
1506
+ if (result.project) {
1507
+ projects.push(result.project);
1508
+ if (result.project.migratable.keys.length) packageConfigs[result.project.projectName] = result.project.migratable.settings;
1509
+ }
1510
+ }
1511
+ return {
1512
+ packageConfigs,
1513
+ projects,
1514
+ warnings
1515
+ };
1516
+ }
1517
+ //#endregion
697
1518
  //#region src/utils/settings-change.ts
698
1519
  /**
1520
+ * Maximum dynamic-programming cells used for an exact line diff.
1521
+ */
1522
+ const MAX_LCS_CELLS = 1e6;
1523
+ /**
1524
+ * URL userinfo can contain proxy usernames and passwords. It must never be
1525
+ * printed in a migration report.
1526
+ */
1527
+ const URL_USERINFO_PATTERN = /(?<scheme>[a-z][a-z\d+.-]*:\/\/)[^/\s@]+@/giu;
1528
+ /**
699
1529
  * Collect changed root settings between two workspace configurations.
700
1530
  *
701
1531
  * @param before - Workspace settings before migration
@@ -714,6 +1544,43 @@ function collectSettingsChanges(before, after) {
714
1544
  }));
715
1545
  }
716
1546
  /**
1547
+ * Create a bounded-memory diff that retains shared prefixes and suffixes.
1548
+ *
1549
+ * This fallback deliberately gives up the exact LCS for unusually large
1550
+ * settings to keep reporting from exhausting memory after migration writes.
1551
+ *
1552
+ * @param beforeLines - YAML lines before migration
1553
+ * @param afterLines - YAML lines after migration
1554
+ *
1555
+ * @returns Classified diff lines in display order
1556
+ */
1557
+ function createLinearLineDiff(beforeLines, afterLines) {
1558
+ let prefixLength = 0;
1559
+ const maximumPrefixLength = Math.min(beforeLines.length, afterLines.length);
1560
+ while (prefixLength < maximumPrefixLength && beforeLines[prefixLength] === afterLines[prefixLength]) prefixLength++;
1561
+ let suffixLength = 0;
1562
+ const maximumSuffixLength = maximumPrefixLength - prefixLength;
1563
+ while (suffixLength < maximumSuffixLength && beforeLines[beforeLines.length - suffixLength - 1] === afterLines[afterLines.length - suffixLength - 1]) suffixLength++;
1564
+ return [
1565
+ ...beforeLines.slice(0, prefixLength).map((value) => ({
1566
+ kind: "unchanged",
1567
+ value
1568
+ })),
1569
+ ...beforeLines.slice(prefixLength, beforeLines.length - suffixLength).map((value) => ({
1570
+ kind: "removed",
1571
+ value
1572
+ })),
1573
+ ...afterLines.slice(prefixLength, afterLines.length - suffixLength).map((value) => ({
1574
+ kind: "added",
1575
+ value
1576
+ })),
1577
+ ...beforeLines.slice(beforeLines.length - suffixLength).map((value) => ({
1578
+ kind: "unchanged",
1579
+ value
1580
+ }))
1581
+ ];
1582
+ }
1583
+ /**
717
1584
  * Create a line-based diff using the longest common subsequence.
718
1585
  *
719
1586
  * @param beforeLines - YAML lines before migration
@@ -722,6 +1589,7 @@ function collectSettingsChanges(before, after) {
722
1589
  * @returns Classified diff lines in display order
723
1590
  */
724
1591
  function createLineDiff(beforeLines, afterLines) {
1592
+ if (beforeLines.length * afterLines.length > MAX_LCS_CELLS) return createLinearLineDiff(beforeLines, afterLines);
725
1593
  const lengths = Array.from({ length: beforeLines.length + 1 }, () => Array(afterLines.length + 1).fill(0));
726
1594
  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
1595
  const diff = [];
@@ -776,6 +1644,17 @@ function formatSettingLines(key, value) {
776
1644
  return stringify({ [key]: value }).trimEnd().split("\n");
777
1645
  }
778
1646
  /**
1647
+ * Remove credentials embedded in URLs while preserving enough context to
1648
+ * identify the changed setting.
1649
+ *
1650
+ * @param value - Rendered YAML diff line
1651
+ *
1652
+ * @returns Diff line safe to print to a terminal or CI log
1653
+ */
1654
+ function redactUrlCredentials(value) {
1655
+ return value.replace(URL_USERINFO_PATTERN, "$<scheme>***@");
1656
+ }
1657
+ /**
779
1658
  * Format a setting change as a GitHub-style YAML diff.
780
1659
  *
781
1660
  * @param change - Root setting change to format
@@ -783,7 +1662,10 @@ function formatSettingLines(key, value) {
783
1662
  * @returns Classified YAML diff lines
784
1663
  */
785
1664
  function createSettingsDiffLines(change) {
786
- return createLineDiff(formatSettingLines(change.key, change.before), formatSettingLines(change.key, change.after));
1665
+ return createLineDiff(formatSettingLines(change.key, change.before), formatSettingLines(change.key, change.after)).map((line) => ({
1666
+ kind: line.kind,
1667
+ value: redactUrlCredentials(line.value)
1668
+ }));
787
1669
  }
788
1670
  /**
789
1671
  * Print a summary and GitHub-style YAML diff for every changed setting.
@@ -802,78 +1684,312 @@ function reportSettingsChanges(changes) {
802
1684
  }
803
1685
  }
804
1686
  //#endregion
805
- //#region src/utils/config.ts
1687
+ //#region src/utils/settings-issue-report.ts
1688
+ function formatIssueKeys(keys) {
1689
+ return keys.map((key) => JSON.stringify(key)).join(", ");
1690
+ }
806
1691
  /**
807
- * Resolve the indentation width for a YAML document.
1692
+ * Report settings that were intentionally left in their source.
808
1693
  *
809
- * @param content - Raw YAML content
1694
+ * @param options - Retained settings and destination context
810
1695
  *
811
- * @returns Detected indentation width or the project default
1696
+ * @returns Nothing; warnings are written through consola
812
1697
  */
813
- function resolveYamlIndent(content) {
814
- const detectedIndent = detectIndent(content).amount;
815
- return detectedIndent > 0 ? detectedIndent : 2;
1698
+ function reportSettingsIssues(options) {
1699
+ const { compatibility, issues, projectConfig = false, source } = options;
1700
+ if (issues.refused.length) consola$1.warn(`Kept project-refused settings in ${source}: ${formatIssueKeys(issues.refused)}. Configure machine settings globally and current-run paths on the command line.`);
1701
+ if (issues.incompatible.length) consola$1.warn(`Kept settings in ${source} that are incompatible with pnpm ${compatibility.slice(1)}: ${formatIssueKeys(issues.incompatible)}.`);
1702
+ if (issues.nonCamelCase.length) consola$1.warn(`Kept non-camelCase settings in ${source}: ${formatIssueKeys(issues.nonCamelCase)}. Workspace manifest keys must use camelCase.`);
1703
+ if (issues.unknown.length) consola$1.warn(`Kept settings in ${source} that pnpm ${compatibility.slice(1)} does not recognize: ${formatIssueKeys(issues.unknown)}.`);
1704
+ if (issues.unsupported.length) {
1705
+ const destination = projectConfig && compatibility === "v12" ? "pnpm v12 does not support packageConfigs" : "packageConfigs only accepts hoist, modulesDir, overrides, saveExact, and savePrefix";
1706
+ consola$1.warn(`Kept subproject settings in ${source}: ${formatIssueKeys(issues.unsupported)}; ${destination}.`);
1707
+ }
1708
+ if (issues.unsafe.length) consola$1.warn(`Kept unsafe registry settings in ${source}: ${formatIssueKeys(issues.unsafe)}. Remove credentials and dynamic URL interpolation before migrating them.`);
816
1709
  }
1710
+ //#endregion
1711
+ //#region src/utils/yaml-document.ts
817
1712
  /**
818
- * Read and parse a package manifest when it exists.
1713
+ * Sort every mapping in a YAML node without replacing its comments or anchors.
819
1714
  *
820
- * @param path - Absolute path to `package.json`
821
- * @param exists - Whether the package manifest exists
1715
+ * @param node - YAML node whose nested mappings should be sorted
1716
+ */
1717
+ function sortYamlMappings(node) {
1718
+ if (isMap(node)) {
1719
+ node.items.sort((left, right) => {
1720
+ const leftKey = isScalar(left.key) ? String(left.key.value) : "";
1721
+ const rightKey = isScalar(right.key) ? String(right.key.value) : "";
1722
+ return leftKey.localeCompare(rightKey);
1723
+ });
1724
+ for (const pair of node.items) if (isNode(pair.value)) sortYamlMappings(pair.value);
1725
+ } else if (isSeq(node)) {
1726
+ for (const item of node.items) if (isNode(item)) sortYamlMappings(item);
1727
+ }
1728
+ }
1729
+ /**
1730
+ * Apply semantic root changes to the parsed document while retaining untouched
1731
+ * YAML nodes and their source metadata.
822
1732
  *
823
- * @returns Parsed package manifest and detected indentation
1733
+ * @param document - Parsed YAML document to update in place
1734
+ * @param options - Before/after settings and sorting preference
824
1735
  *
825
- * @throws {SyntaxError} When the package manifest contains invalid JSON
1736
+ * @returns Nothing; the document is updated in place
826
1737
  */
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
- };
1738
+ function updateYamlDocument(document, options) {
1739
+ const { after, before, sortKeys } = options;
1740
+ for (const key of Object.keys(before)) if (!Object.hasOwn(after, key)) document.delete(key);
1741
+ for (const [key, value] of Object.entries(after)) if (!Object.hasOwn(before, key) || !isDeepStrictEqual(Reflect.get(before, key), value)) document.set(key, value);
1742
+ if (sortKeys) sortYamlMappings(document.contents);
837
1743
  }
838
1744
  /**
839
- * Read and parse a pnpm workspace manifest when it exists.
1745
+ * Normalize blank lines immediately before root mapping keys.
840
1746
  *
841
- * @param path - Absolute path to `pnpm-workspace.yaml`
842
- * @param exists - Whether the workspace manifest exists
1747
+ * @param content - Serialized workspace YAML
1748
+ * @param newlineBetween - Whether root keys should be separated by blank lines
843
1749
  *
844
- * @returns Parsed workspace settings and detected indentation
1750
+ * @returns YAML with normalized root-key spacing
1751
+ */
1752
+ function formatRootSpacing(content, newlineBetween) {
1753
+ return content.replace(/\n+(?=[^\s#][^:\n]*:)/gu, newlineBetween ? "\n\n" : "\n");
1754
+ }
1755
+ //#endregion
1756
+ //#region src/migration-persistence.ts
1757
+ const SETTINGS_WITHOUT_REPLACEMENT = /* @__PURE__ */ new Set(["ignoreDepScripts", "ignorePatchFailures"]);
1758
+ const REPLACEMENT_SETTING_KEYS = {
1759
+ allowNonAppliedPatches: "allowUnusedPatches",
1760
+ auditConfig: "audit",
1761
+ auditLevel: "audit",
1762
+ cleanupUnusedCatalogs: "catalogPrune",
1763
+ enableGlobalVirtualStore: "virtualStoreType",
1764
+ ignoredBuiltDependencies: "allowBuilds",
1765
+ managePackageManagerVersions: "pmOnFail",
1766
+ namedRegistries: "registries",
1767
+ neverBuiltDependencies: "allowBuilds",
1768
+ onlyBuiltDependencies: "allowBuilds",
1769
+ onlyBuiltDependenciesFile: "allowBuilds",
1770
+ packageManagerStrict: "pmOnFail",
1771
+ packageManagerStrictVersion: "pmOnFail",
1772
+ remoteSideEffectsCache: "sideEffectsCache",
1773
+ sideEffectsCacheReadonly: "sideEffectsCache",
1774
+ updateConfig: "update"
1775
+ };
1776
+ function cleanPackageJsonSettings({ migratedKeys, packageJson, settings, yarnResolutionsApplied }) {
1777
+ let changed = false;
1778
+ if (packageJson.value.pnpm) {
1779
+ for (const key of migratedKeys) {
1780
+ Reflect.deleteProperty(packageJson.value.pnpm, key);
1781
+ changed = true;
1782
+ }
1783
+ if (!Object.keys(packageJson.value.pnpm).length) delete packageJson.value.pnpm;
1784
+ }
1785
+ if (settings.yarnResolutions && yarnResolutionsApplied) {
1786
+ changed = true;
1787
+ delete packageJson.value.resolutions;
1788
+ }
1789
+ return changed;
1790
+ }
1791
+ function containsMigratedValue(actual, expected) {
1792
+ if (Array.isArray(actual) && Array.isArray(expected)) return expected.every((expectedItem) => actual.some((actualItem) => containsMigratedValue(actualItem, expectedItem)));
1793
+ if (actual && expected && typeof actual === "object" && typeof expected === "object") return Object.entries(expected).every(([key, expectedValue]) => containsMigratedValue(actual[key], expectedValue));
1794
+ return Object.is(actual, expected);
1795
+ }
1796
+ function resolveSourceRuntimeVersion(sourceSettings, targetKey) {
1797
+ if (targetKey === "useNodeVersion") return Reflect.get(sourceSettings, "useNodeVersion");
1798
+ const executionEnv = Reflect.get(sourceSettings, "executionEnv");
1799
+ if (!executionEnv || typeof executionEnv !== "object" || Array.isArray(executionEnv)) return;
1800
+ return executionEnv.nodeVersion;
1801
+ }
1802
+ function resolveReplacementSettingKey(sourceKey, targetKey, npmrc) {
1803
+ if (npmrc && /^node-mirror:/iu.test(sourceKey)) return "nodeDownloadMirrors";
1804
+ return REPLACEMENT_SETTING_KEYS[targetKey];
1805
+ }
1806
+ function selectAppliedRootKeys({ finalSettings, incomingSettings, keys, npmrc = false, runtimeApplied, runtimeVersion, sourceSettings }) {
1807
+ return keys.filter((sourceKey) => {
1808
+ const targetKey = npmrc ? Object.keys(camelcaseKeys({ [sourceKey]: true }))[0] ?? sourceKey : sourceKey;
1809
+ if (!Object.hasOwn(incomingSettings, targetKey)) {
1810
+ if (targetKey === "executionEnv" || targetKey === "useNodeVersion") {
1811
+ const sourceRuntimeVersion = resolveSourceRuntimeVersion(sourceSettings, targetKey);
1812
+ return runtimeApplied && typeof sourceRuntimeVersion === "string" && sourceRuntimeVersion === runtimeVersion;
1813
+ }
1814
+ if (SETTINGS_WITHOUT_REPLACEMENT.has(targetKey)) return false;
1815
+ const replacementKey = resolveReplacementSettingKey(sourceKey, targetKey, npmrc);
1816
+ if (!replacementKey || !Object.hasOwn(incomingSettings, replacementKey)) return true;
1817
+ return containsMigratedValue(Reflect.get(finalSettings, replacementKey), Reflect.get(incomingSettings, replacementKey));
1818
+ }
1819
+ return containsMigratedValue(Reflect.get(finalSettings, targetKey), Reflect.get(incomingSettings, targetKey));
1820
+ });
1821
+ }
1822
+ function resolveProjectConfig(settings, projectName) {
1823
+ const { packageConfigs } = settings;
1824
+ if (!packageConfigs || typeof packageConfigs !== "object" || Array.isArray(packageConfigs)) return;
1825
+ const projectConfig = packageConfigs[projectName];
1826
+ return projectConfig && typeof projectConfig === "object" && !Array.isArray(projectConfig) ? projectConfig : void 0;
1827
+ }
1828
+ function selectAppliedProjectKeys(project, finalSettings) {
1829
+ const finalProjectConfig = resolveProjectConfig(finalSettings, project.projectName);
1830
+ if (!finalProjectConfig) return [];
1831
+ return project.migratable.keys.filter((sourceKey) => {
1832
+ const targetKey = Object.keys(camelcaseKeys({ [sourceKey]: true }))[0] ?? sourceKey;
1833
+ return containsMigratedValue(finalProjectConfig[targetKey], project.migratable.settings[targetKey]);
1834
+ });
1835
+ }
1836
+ /**
1837
+ * Persist destinations before pruning any legacy source file.
845
1838
  *
846
- * @throws {Error} When the workspace manifest contains invalid YAML
1839
+ * @param options - Destination files, selected sources, and cleanup policy
1840
+ *
1841
+ * @returns A promise that resolves after destinations and sources are persisted
847
1842
  */
848
- async function readPnpmWorkspace(path, exists) {
849
- if (!exists) return {
850
- indent: 2,
851
- value: {}
1843
+ async function persistMigration(options) {
1844
+ const { cleanNpmrc, cleanPackageJson, compatibility, finalSettings, incomingSettings, npmrc, npmrcExists, npmrcPath, packageJson, packageJsonExists, packageJsonPath, packageJsonRuntimeChanged, packageJsonSettings, pnpmWorkspaceContent, pnpmWorkspacePath, projectNpmrcs, runtimeVersion } = options;
1845
+ const appliedPackageJsonKeys = selectAppliedRootKeys({
1846
+ finalSettings,
1847
+ incomingSettings,
1848
+ keys: packageJsonSettings.keys,
1849
+ runtimeApplied: packageJsonRuntimeChanged,
1850
+ runtimeVersion,
1851
+ sourceSettings: packageJsonSettings.settings
1852
+ });
1853
+ const appliedNpmrcKeys = selectAppliedRootKeys({
1854
+ finalSettings,
1855
+ incomingSettings,
1856
+ keys: npmrc.keys,
1857
+ npmrc: true,
1858
+ runtimeApplied: packageJsonRuntimeChanged,
1859
+ runtimeVersion,
1860
+ sourceSettings: npmrc.settings
1861
+ });
1862
+ const yarnResolutionsApplied = packageJsonSettings.yarnResolutions && containsMigratedValue(finalSettings.overrides, incomingSettings.overrides);
1863
+ const packageJsonSettingsChanged = packageJsonExists && cleanPackageJson ? cleanPackageJsonSettings({
1864
+ migratedKeys: appliedPackageJsonKeys,
1865
+ packageJson,
1866
+ settings: packageJsonSettings,
1867
+ yarnResolutionsApplied
1868
+ }) : false;
1869
+ await fsWriteFile(pnpmWorkspacePath, pnpmWorkspaceContent);
1870
+ if (packageJsonExists && (packageJsonRuntimeChanged || packageJsonSettingsChanged)) await fsWriteFile(packageJsonPath, JSON.stringify(packageJson.value, null, packageJson.indent));
1871
+ if (!cleanNpmrc) return;
1872
+ const pruneTasks = projectNpmrcs.projects.flatMap((project) => {
1873
+ const appliedKeys = selectAppliedProjectKeys(project, finalSettings);
1874
+ return appliedKeys.length ? [pruneNpmrc(project.npmrcPath, compatibility, appliedKeys)] : [];
1875
+ });
1876
+ if (npmrcExists && appliedNpmrcKeys.length) pruneTasks.push(pruneNpmrc(npmrcPath, compatibility, appliedNpmrcKeys));
1877
+ await Promise.all(pruneTasks);
1878
+ }
1879
+ //#endregion
1880
+ //#region src/migration-sources.ts
1881
+ /**
1882
+ * Read, schema-filter, report, and merge all legacy settings sources.
1883
+ *
1884
+ * @param options - Migration target and source configuration
1885
+ *
1886
+ * @returns Resolved source metadata and merged incoming settings
1887
+ */
1888
+ async function resolveMigrationSources(options) {
1889
+ const { compatibility, cwd, npmrcExists, npmrcPath, packageJson, pnpmWorkspace, strategy, yarnResolutions } = options;
1890
+ const npmrc = npmrcExists ? await readMigratableNpmrc(npmrcPath, compatibility) : {
1891
+ issues: createSettingsIssues(),
1892
+ keys: [],
1893
+ settings: {}
852
1894
  };
853
- const content = await fsReadFile(path);
1895
+ reportSettingsIssues({
1896
+ compatibility,
1897
+ issues: npmrc.issues,
1898
+ source: NPMRC
1899
+ });
1900
+ const packageJsonSettings = resolvePackageJsonSettings(packageJson, yarnResolutions, compatibility);
1901
+ reportSettingsIssues({
1902
+ compatibility,
1903
+ issues: packageJsonSettings.issues,
1904
+ source: `${PACKAGE_JSON}#pnpm`
1905
+ });
1906
+ const projectNpmrcs = await readProjectNpmrcMigrations(cwd, collectWorkspacePackagePatterns(mergeByStrategy(pnpmWorkspace, mergeByStrategy(packageJsonSettings.settings, npmrc.settings, "merge"), strategy)), compatibility);
1907
+ for (const warning of projectNpmrcs.warnings) consola$1.warn(warning);
1908
+ for (const project of projectNpmrcs.projects) reportSettingsIssues({
1909
+ compatibility,
1910
+ issues: project.migratable.issues,
1911
+ projectConfig: true,
1912
+ source: relative(cwd, project.npmrcPath)
1913
+ });
1914
+ const npmrcSettings = mergeByStrategy(npmrc.settings, Object.keys(projectNpmrcs.packageConfigs).length ? { packageConfigs: projectNpmrcs.packageConfigs } : {}, "merge");
854
1915
  return {
855
- indent: resolveYamlIndent(content),
856
- value: parse(content) ?? {}
1916
+ incomingSettings: mergeByStrategy(packageJsonSettings.settings, npmrcSettings, "merge"),
1917
+ npmrc,
1918
+ packageJson: packageJsonSettings,
1919
+ projectNpmrcs
857
1920
  };
858
1921
  }
1922
+ //#endregion
1923
+ //#region src/options.ts
859
1924
  /**
860
- * Resolve migratable pnpm settings from a package manifest.
1925
+ * Default values for migration options.
1926
+ */
1927
+ const DEFAULT_OPTIONS = {
1928
+ cleanNpmrc: true,
1929
+ cleanPackageJson: true,
1930
+ compatibility: "auto",
1931
+ newlineBetween: true,
1932
+ replaceDeprecated: false,
1933
+ showChanges: true,
1934
+ sortKeys: false,
1935
+ strategy: "merge",
1936
+ yarnResolutions: true
1937
+ };
1938
+ const VALID_STRATEGIES = [
1939
+ "discard",
1940
+ "merge",
1941
+ "overwrite"
1942
+ ];
1943
+ const VALID_COMPATIBILITIES = [
1944
+ "auto",
1945
+ "v10",
1946
+ "v11",
1947
+ "v12"
1948
+ ];
1949
+ function resolveCompatibility(compatibility) {
1950
+ if (!compatibility) return DEFAULT_OPTIONS.compatibility;
1951
+ if (VALID_COMPATIBILITIES.includes(compatibility)) return compatibility;
1952
+ throw new Error(`Invalid compatibility: ${compatibility}. Expected one of: ${VALID_COMPATIBILITIES.join(", ")}`);
1953
+ }
1954
+ function resolveStrategy(strategy) {
1955
+ if (!strategy) return DEFAULT_OPTIONS.strategy;
1956
+ if (VALID_STRATEGIES.includes(strategy)) return strategy;
1957
+ throw new Error(`Invalid strategy: ${strategy}. Expected one of: ${VALID_STRATEGIES.join(", ")}`);
1958
+ }
1959
+ /**
1960
+ * Resolve and normalize migration options with defaults.
861
1961
  *
862
- * Yarn resolutions are merged into pnpm overrides when enabled. Empty
863
- * overrides are removed from the result.
1962
+ * This function takes partial options and returns a complete options object
1963
+ * with all properties set to either the provided value or the default value.
864
1964
  *
865
- * @param packageJson - Parsed package manifest
866
- * @param yarnResolutions - Whether to convert Yarn resolutions to pnpm overrides
1965
+ * @param options - Partial migration options
1966
+ *
1967
+ * @returns Complete options object with all required properties
867
1968
  *
868
- * @returns Pnpm workspace settings resolved from the package manifest
1969
+ * @example
1970
+ * ```ts
1971
+ * // Use all defaults
1972
+ * const opts = resolveOptions()
1973
+ * // { cleanNpmrc: true, cleanPackageJson: true, cwd: '/current/dir', ... }
1974
+ *
1975
+ * // Override specific options
1976
+ * const opts = resolveOptions({ sortKeys: true, cleanNpmrc: false })
1977
+ * // { cleanNpmrc: false, cleanPackageJson: true, sortKeys: true, ... }
1978
+ * ```
869
1979
  */
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;
1980
+ function resolveOptions(options = {}) {
1981
+ return {
1982
+ cleanNpmrc: options.cleanNpmrc ?? DEFAULT_OPTIONS.cleanNpmrc,
1983
+ compatibility: resolveCompatibility(options.compatibility),
1984
+ cwd: options.cwd ?? process.cwd(),
1985
+ newlineBetween: options.newlineBetween ?? DEFAULT_OPTIONS.newlineBetween,
1986
+ showChanges: options.showChanges ?? DEFAULT_OPTIONS.showChanges,
1987
+ sortKeys: options.sortKeys ?? DEFAULT_OPTIONS.sortKeys,
1988
+ strategy: resolveStrategy(options.strategy),
1989
+ yarnResolutions: options.yarnResolutions ?? DEFAULT_OPTIONS.yarnResolutions,
1990
+ cleanPackageJson: options.cleanPackageJson ?? DEFAULT_OPTIONS.cleanPackageJson,
1991
+ replaceDeprecated: options.replaceDeprecated ?? DEFAULT_OPTIONS.replaceDeprecated
1992
+ };
877
1993
  }
878
1994
  //#endregion
879
1995
  //#region src/core.ts
@@ -885,8 +2001,8 @@ function hasSettingsSources(npmrcExists, packageJsonExists, pnpmWorkspaceExists)
885
2001
  return false;
886
2002
  }
887
2003
  function hasMigratableSettings(sources) {
888
- const { existingSettingsChanged, packageJson, pnpmSettingsInNpmrc, yarnResolutions } = sources;
889
- return Boolean(existingSettingsChanged || packageJson.pnpm || yarnResolutions && packageJson.resolutions || Object.keys(pnpmSettingsInNpmrc).length);
2004
+ const { existingSettingsChanged, npmrcKeys, packageJsonKeys, projectNpmrcKeys, yarnResolutions } = sources;
2005
+ return Boolean(existingSettingsChanged || npmrcKeys.length || packageJsonKeys.length || projectNpmrcKeys.length || yarnResolutions);
890
2006
  }
891
2007
  function assertCanMigrateRuntime(runtimeVersion, packageJsonExists) {
892
2008
  if (runtimeVersion && !packageJsonExists) throw new Error("Cannot migrate the removed Node.js runtime setting without a package.json file.");
@@ -909,10 +2025,13 @@ function reportMigrationChanges(showChanges, before = {}, after = {}) {
909
2025
  * @param rawOptions.cwd - Current working directory (default: process.cwd())
910
2026
  * @param rawOptions.cleanNpmrc - Whether to remove pnpm settings from `.npmrc` (default: true)
911
2027
  * @param rawOptions.cleanPackageJson - Whether to remove pnpm field from `package.json` (default: true)
2028
+ * @param rawOptions.compatibility - Target pnpm major, or automatic detection (default: auto)
912
2029
  * @param rawOptions.yarnResolutions - Whether to migrate resolutions field (default: true)
913
2030
  * @param rawOptions.sortKeys - Whether to sort keys in output YAML (default: false)
914
2031
  * @param rawOptions.newlineBetween - Add newlines between root keys (default: true)
2032
+ * @param rawOptions.replaceDeprecated - Whether to replace deprecated settings (default: false)
915
2033
  * @param rawOptions.showChanges - Show settings changes after migration (default: true)
2034
+ * @param rawOptions.strategy - Conflict handling strategy (default: merge)
916
2035
  *
917
2036
  * @returns A promise that resolves when migration is complete
918
2037
  *
@@ -949,16 +2068,18 @@ async function migratePnpmSettings(rawOptions = {}) {
949
2068
  const [packageJson, pnpmWorkspace] = await Promise.all([readPackageJson(packageJsonPath, packageJsonExists), readPnpmWorkspace(pnpmWorkspaceYamlPath, pnpmWorkspaceExists)]);
950
2069
  const pnpmWorkspaceBefore = structuredClone(pnpmWorkspace.value);
951
2070
  const compatibility = resolveCompatibilityTarget(options.compatibility, packageJson.value.packageManager, packageJson.value.devEngines?.packageManager);
952
- const npmrcMigratable = npmrcExists ? await readMigratableNpmrc(npmrcPath, compatibility) : {
953
- keys: [],
954
- settings: {}
955
- };
956
- const pnpmSettingsInNpmrc = npmrcMigratable.settings;
957
- const pnpmSettingsInPackageJson = resolvePackageJsonSettings(packageJson.value, options.yarnResolutions);
958
- const incomingSettings = {
959
- ...pnpmSettingsInNpmrc,
960
- ...pnpmSettingsInPackageJson
961
- };
2071
+ assertCompatibleWorkspaceSettings(pnpmWorkspace.value, compatibility);
2072
+ const sources = await resolveMigrationSources({
2073
+ compatibility,
2074
+ cwd: options.cwd,
2075
+ npmrcExists,
2076
+ npmrcPath,
2077
+ packageJson: packageJson.value,
2078
+ pnpmWorkspace: pnpmWorkspace.value,
2079
+ strategy: options.strategy,
2080
+ yarnResolutions: options.yarnResolutions
2081
+ });
2082
+ const { incomingSettings } = sources;
962
2083
  const [existingNormalization, incomingNormalization] = await Promise.all([normalizeIncomingSettings(pnpmWorkspace.value, {
963
2084
  compatibility,
964
2085
  cwd: options.cwd,
@@ -975,35 +2096,41 @@ async function migratePnpmSettings(rawOptions = {}) {
975
2096
  if (runtimeMigration.warning) consola$1.warn(runtimeMigration.warning);
976
2097
  if (!hasMigratableSettings({
977
2098
  existingSettingsChanged: existingNormalization.changed,
978
- packageJson: packageJson.value,
979
- pnpmSettingsInNpmrc,
980
- yarnResolutions: options.yarnResolutions
2099
+ npmrcKeys: sources.npmrc.keys,
2100
+ packageJsonKeys: sources.packageJson.keys,
2101
+ projectNpmrcKeys: sources.projectNpmrcs.projects.flatMap((project) => project.migratable.keys),
2102
+ yarnResolutions: sources.packageJson.yarnResolutions
981
2103
  })) {
982
2104
  consola$1.warn("No pnpm settings fields to migrate");
983
2105
  reportMigrationChanges(options.showChanges);
984
2106
  return;
985
2107
  }
986
2108
  const pnpmWorkspaceResult = mergeByStrategy(pnpmWorkspace.value, incomingSettings, options.strategy);
987
- const yamlDocument = new Document({}, { sortMapEntries: options.sortKeys });
988
- Object.entries(pnpmWorkspaceResult).forEach(([key, value]) => {
989
- yamlDocument.add({
990
- key,
991
- value
992
- });
2109
+ updateYamlDocument(pnpmWorkspace.document, {
2110
+ after: pnpmWorkspaceResult,
2111
+ before: pnpmWorkspaceBefore,
2112
+ sortKeys: options.sortKeys
2113
+ });
2114
+ const finalYamlContent = formatRootSpacing(pnpmWorkspace.document.toString({ indent: pnpmWorkspace.indent }), options.newlineBetween);
2115
+ await persistMigration({
2116
+ cleanNpmrc: options.cleanNpmrc,
2117
+ cleanPackageJson: options.cleanPackageJson,
2118
+ compatibility,
2119
+ finalSettings: pnpmWorkspaceResult,
2120
+ incomingSettings,
2121
+ npmrc: sources.npmrc,
2122
+ npmrcExists,
2123
+ npmrcPath,
2124
+ packageJson,
2125
+ packageJsonExists,
2126
+ packageJsonPath,
2127
+ packageJsonRuntimeChanged: runtimeMigration.changed,
2128
+ packageJsonSettings: sources.packageJson,
2129
+ pnpmWorkspaceContent: finalYamlContent,
2130
+ pnpmWorkspacePath: pnpmWorkspaceYamlPath,
2131
+ projectNpmrcs: sources.projectNpmrcs,
2132
+ runtimeVersion
993
2133
  });
994
- const yamlContent = yamlDocument.toString({ indent: pnpmWorkspace.indent });
995
- await fsWriteFile(pnpmWorkspaceYamlPath, options.newlineBetween ? yamlContent.replace(/\n(?=[^\s#][^:\n]*:)/gu, "\n\n") : yamlContent);
996
- if (npmrcExists && options.cleanNpmrc) await pruneNpmrc(npmrcPath, compatibility, npmrcMigratable.keys);
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
- }
1005
- }
1006
- if (packageJsonExists && packageJsonChanged) await fsWriteFile(packageJsonPath, JSON.stringify(packageJson.value, null, packageJson.indent));
1007
2134
  reportMigrationChanges(options.showChanges, pnpmWorkspaceBefore, pnpmWorkspaceResult);
1008
2135
  } catch (err) {
1009
2136
  consola$1.error("Failed to migrate pnpm settings:", err);
@@ -1012,9 +2139,8 @@ async function migratePnpmSettings(rawOptions = {}) {
1012
2139
  }
1013
2140
  //#endregion
1014
2141
  //#region src/cli.ts
1015
- const cli = cac(name);
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();
1017
- cli.command("").action(async (options) => {
2142
+ const cli = createCli();
2143
+ cli.command("").usage("[options]").action(async (options) => {
1018
2144
  try {
1019
2145
  consola.log(`\n${bold(magenta(name))} ${dim(`v${version}`)}`);
1020
2146
  consola.log(dim("\n--------------\n"));