pnpm-settings-migrator 0.9.0 → 0.10.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,14 +1,13 @@
1
1
  import process from "node:process";
2
- import consola$1, { consola } from "consola";
3
2
  import { cac } from "cac";
4
- import { basename, dirname, join, relative, resolve } from "pathe";
5
- import camelcaseKeys from "camelcase-keys";
6
3
  import { randomUUID } from "node:crypto";
7
4
  import { access, chmod, readFile, rename, rm, stat, writeFile } from "node:fs/promises";
8
5
  import { fileURLToPath } from "node:url";
6
+ import { basename, dirname, join, relative, resolve } from "pathe";
9
7
  import { getColor } from "consola/utils";
10
8
  import { isPlainObject, isUndefined } from "@ntnyq/utils";
11
9
  import { defu } from "defu";
10
+ import camelcaseKeys from "camelcase-keys";
12
11
  import { readIniFile } from "read-ini-file";
13
12
  import { kebabCase } from "uncase";
14
13
  import { glob } from "tinyglobby";
@@ -17,7 +16,7 @@ import { Document, isMap, isNode, isScalar, isSeq, parseDocument, stringify } fr
17
16
  import { isDeepStrictEqual } from "node:util";
18
17
  //#region package.json
19
18
  var name = "pnpm-settings-migrator";
20
- var version = "0.9.0";
19
+ var version = "0.10.0";
21
20
  //#endregion
22
21
  //#region src/cli-options.ts
23
22
  /**
@@ -26,7 +25,7 @@ var version = "0.9.0";
26
25
  * @returns Configured command-line parser
27
26
  */
28
27
  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");
28
+ 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", "Hide settings diff (keep summary and warnings)").option("--no-clean-npmrc", "Disable removing pnpm settings in .npmrc file").option("--no-clean-package-json", "Disable removing pnpm fields in package.json");
30
29
  cli.help((sections) => {
31
30
  if (!(cli.commands.length > 0 && cli.commands.every((command) => command.isDefaultCommand))) return sections;
32
31
  return sections.filter((section) => section.title !== "Commands" && !section.title?.startsWith("For more info"));
@@ -34,6 +33,139 @@ function createCli() {
34
33
  return cli;
35
34
  }
36
35
  //#endregion
36
+ //#region src/utils/fs.ts
37
+ /**
38
+ * Modulus that isolates Unix permission bits from a stat mode.
39
+ */
40
+ const FILE_PERMISSION_MODULUS = 512;
41
+ /**
42
+ * Resolve a filesystem path to a string suitable for a sibling temp file.
43
+ *
44
+ * @param path - Filesystem path to normalize
45
+ *
46
+ * @returns String path suitable for path manipulation
47
+ */
48
+ function resolvePathString(path) {
49
+ if (path instanceof URL) return fileURLToPath(path);
50
+ return typeof path === "string" ? path : path.toString();
51
+ }
52
+ /**
53
+ * Read an existing file's permission bits, if the file exists.
54
+ *
55
+ * @param path - Filesystem path to inspect
56
+ *
57
+ * @returns Unix permission bits, or `undefined` when the file does not exist
58
+ */
59
+ async function resolveExistingMode(path) {
60
+ try {
61
+ return (await stat(path)).mode % FILE_PERMISSION_MODULUS;
62
+ } catch (error) {
63
+ if (error.code === "ENOENT") return;
64
+ throw error;
65
+ }
66
+ }
67
+ /**
68
+ * Check whether a filesystem path exists.
69
+ *
70
+ * @param path - Filesystem path to check
71
+ *
72
+ * @returns `true` when the path is accessible, otherwise `false`
73
+ */
74
+ async function fsExists(path) {
75
+ return access(path).then(() => true).catch(() => false);
76
+ }
77
+ /**
78
+ * Read a UTF-8 text file.
79
+ *
80
+ * @param path - Filesystem path to read
81
+ *
82
+ * @returns Text content of the file
83
+ */
84
+ async function fsReadFile(path) {
85
+ return await readFile(path, "utf-8");
86
+ }
87
+ /**
88
+ * Remove a file if it exists.
89
+ *
90
+ * @param path - Filesystem path to remove
91
+ *
92
+ * @returns A promise that resolves after the file is removed
93
+ */
94
+ async function fsRemoveFile(path) {
95
+ await rm(path, { force: true });
96
+ }
97
+ /**
98
+ * Write a normalized UTF-8 text file with one trailing newline.
99
+ *
100
+ * @param path - Filesystem path to write
101
+ * @param content - Text content to write
102
+ *
103
+ * @returns A promise that resolves after the file is written
104
+ */
105
+ async function fsWriteFile(path, content) {
106
+ const pathString = resolvePathString(path);
107
+ const temporaryPath = join(dirname(pathString), `.${basename(pathString)}.${process.pid}.${randomUUID()}.tmp`);
108
+ const mode = await resolveExistingMode(path);
109
+ try {
110
+ await writeFile(temporaryPath, `${content.trimEnd()}\n`, {
111
+ encoding: "utf-8",
112
+ mode
113
+ });
114
+ if (mode !== void 0) await chmod(temporaryPath, mode);
115
+ await rename(temporaryPath, pathString);
116
+ } finally {
117
+ await rm(temporaryPath, { force: true });
118
+ }
119
+ }
120
+ /**
121
+ * Write a normalized text file only when its contents differ or it is missing.
122
+ *
123
+ * @param path - Destination file path
124
+ * @param content - Text content to normalize and write
125
+ *
126
+ * @returns Whether the destination file was created or changed
127
+ */
128
+ async function fsWriteFileIfChanged(path, content) {
129
+ if (await fsExists(path) && await fsReadFile(path) === `${content.trimEnd()}\n`) return false;
130
+ await fsWriteFile(path, content);
131
+ return true;
132
+ }
133
+ //#endregion
134
+ //#region src/utils/color.ts
135
+ /**
136
+ * Apply dim terminal styling to a value.
137
+ *
138
+ * @param text - Text or number to style
139
+ *
140
+ * @returns ANSI-styled string
141
+ */
142
+ const dim = getColor("dim");
143
+ /**
144
+ * Apply green terminal styling to a value.
145
+ *
146
+ * @param text - Text or number to style
147
+ *
148
+ * @returns ANSI-styled string
149
+ */
150
+ const green = getColor("green");
151
+ /**
152
+ * Apply red terminal styling to a value.
153
+ *
154
+ * @param text - Text or number to style
155
+ *
156
+ * @returns ANSI-styled string
157
+ */
158
+ const red = getColor("red");
159
+ /**
160
+ * Apply bold terminal styling to a value.
161
+ *
162
+ * @param text - Text or number to style
163
+ *
164
+ * @returns ANSI-styled string
165
+ */
166
+ const bold = getColor("bold");
167
+ getColor("magenta");
168
+ //#endregion
37
169
  //#region src/constants.ts
38
170
  const NPMRC = ".npmrc";
39
171
  const PACKAGE_JSON = "package.json";
@@ -205,133 +337,6 @@ const PNPM_V11_REMOVED_SETTINGS = [
205
337
  "useNodeVersion"
206
338
  ];
207
339
  //#endregion
208
- //#region src/utils/fs.ts
209
- /**
210
- * Modulus that isolates Unix permission bits from a stat mode.
211
- */
212
- const FILE_PERMISSION_MODULUS = 512;
213
- /**
214
- * Resolve a filesystem path to a string suitable for a sibling temp file.
215
- *
216
- * @param path - Filesystem path to normalize
217
- *
218
- * @returns String path suitable for path manipulation
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.
226
- *
227
- * @param path - Filesystem path to inspect
228
- *
229
- * @returns Unix permission bits, or `undefined` when the file does not exist
230
- */
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
- }
238
- }
239
- /**
240
- * Check whether a filesystem path exists.
241
- *
242
- * @param path - Filesystem path to check
243
- *
244
- * @returns `true` when the path is accessible, otherwise `false`
245
- */
246
- async function fsExists(path) {
247
- return access(path).then(() => true).catch(() => false);
248
- }
249
- /**
250
- * Read a UTF-8 text file.
251
- *
252
- * @param path - Filesystem path to read
253
- *
254
- * @returns Text content of the file
255
- */
256
- async function fsReadFile(path) {
257
- return await readFile(path, "utf-8");
258
- }
259
- /**
260
- * Remove a file if it exists.
261
- *
262
- * @param path - Filesystem path to remove
263
- *
264
- * @returns A promise that resolves after the file is removed
265
- */
266
- async function fsRemoveFile(path) {
267
- await rm(path, { force: true });
268
- }
269
- /**
270
- * Write a normalized UTF-8 text file with one trailing newline.
271
- *
272
- * @param path - Filesystem path to write
273
- * @param content - Text content to write
274
- *
275
- * @returns A promise that resolves after the file is written
276
- */
277
- async function fsWriteFile(path, content) {
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
- }
291
- }
292
- //#endregion
293
- //#region src/utils/color.ts
294
- /**
295
- * Apply dim terminal styling to a value.
296
- *
297
- * @param text - Text or number to style
298
- *
299
- * @returns ANSI-styled string
300
- */
301
- const dim = getColor("dim");
302
- /**
303
- * Apply green terminal styling to a value.
304
- *
305
- * @param text - Text or number to style
306
- *
307
- * @returns ANSI-styled string
308
- */
309
- const green = getColor("green");
310
- /**
311
- * Apply red terminal styling to a value.
312
- *
313
- * @param text - Text or number to style
314
- *
315
- * @returns ANSI-styled string
316
- */
317
- const red = getColor("red");
318
- /**
319
- * Apply bold terminal styling to a value.
320
- *
321
- * @param text - Text or number to style
322
- *
323
- * @returns ANSI-styled string
324
- */
325
- const bold = getColor("bold");
326
- /**
327
- * Apply magenta terminal styling to a value.
328
- *
329
- * @param text - Text or number to style
330
- *
331
- * @returns ANSI-styled string
332
- */
333
- const magenta = getColor("magenta");
334
- //#endregion
335
340
  //#region src/utils/deprecated-settings.ts
336
341
  /**
337
342
  * Replace deprecated audit fields with the structured `audit` setting.
@@ -1408,6 +1413,29 @@ function resolvePackageJsonSettings(packageJson, yarnResolutions, compatibility)
1408
1413
  yarnResolutions: migrateYarnResolutions
1409
1414
  };
1410
1415
  }
1416
+ /**
1417
+ * Remove only legacy package settings already applied to their destination.
1418
+ *
1419
+ * @param options - Parsed manifest, applied source keys, and resolutions status
1420
+ *
1421
+ * @returns Whether legacy settings were removed from the manifest
1422
+ */
1423
+ function cleanPackageJsonSettings(options) {
1424
+ const { migratedKeys, packageJson, settings, yarnResolutionsApplied } = options;
1425
+ let changed = false;
1426
+ if (packageJson.value.pnpm) {
1427
+ for (const key of migratedKeys) {
1428
+ Reflect.deleteProperty(packageJson.value.pnpm, key);
1429
+ changed = true;
1430
+ }
1431
+ if (!Object.keys(packageJson.value.pnpm).length) delete packageJson.value.pnpm;
1432
+ }
1433
+ if (settings.yarnResolutions && yarnResolutionsApplied) {
1434
+ changed = true;
1435
+ delete packageJson.value.resolutions;
1436
+ }
1437
+ return changed;
1438
+ }
1411
1439
  //#endregion
1412
1440
  //#region src/utils/project-npmrc.ts
1413
1441
  function resolvePackageJsonPatterns(patterns) {
@@ -1668,20 +1696,18 @@ function createSettingsDiffLines(change) {
1668
1696
  }));
1669
1697
  }
1670
1698
  /**
1671
- * Print a summary and GitHub-style YAML diff for every changed setting.
1699
+ * Format a colored YAML diff, with one blank line between changed settings.
1672
1700
  *
1673
- * @param changes - Root setting changes to report
1701
+ * @param changes - Root setting changes to format
1674
1702
  *
1675
- * @returns Nothing; output is written through consola
1703
+ * @returns Diff text without leading or trailing blank lines
1676
1704
  */
1677
- function reportSettingsChanges(changes) {
1678
- consola$1.info(`${changes.length} ${changes.length === 1 ? "setting" : "settings"} changed`);
1679
- for (const [index, change] of changes.entries()) {
1680
- if (index > 0) consola$1.log("");
1681
- for (const line of createSettingsDiffLines(change)) if (line.kind === "added") consola$1.log(green(`+ ${line.value}`));
1682
- else if (line.kind === "removed") consola$1.log(red(`- ${line.value}`));
1683
- else consola$1.log(dim(` ${line.value}`));
1684
- }
1705
+ function formatSettingsChanges(changes) {
1706
+ return changes.map((change) => createSettingsDiffLines(change).map((line) => {
1707
+ if (line.kind === "added") return green(`+ ${line.value}`);
1708
+ if (line.kind === "removed") return red(`- ${line.value}`);
1709
+ return dim(` ${line.value}`);
1710
+ }).join("\n")).join("\n\n");
1685
1711
  }
1686
1712
  //#endregion
1687
1713
  //#region src/utils/settings-issue-report.ts
@@ -1689,23 +1715,25 @@ function formatIssueKeys(keys) {
1689
1715
  return keys.map((key) => JSON.stringify(key)).join(", ");
1690
1716
  }
1691
1717
  /**
1692
- * Report settings that were intentionally left in their source.
1718
+ * Format warnings for settings that were intentionally left in their source.
1693
1719
  *
1694
1720
  * @param options - Retained settings and destination context
1695
1721
  *
1696
- * @returns Nothing; warnings are written through consola
1722
+ * @returns Warning messages in issue-category order
1697
1723
  */
1698
- function reportSettingsIssues(options) {
1724
+ function formatSettingsIssues(options) {
1699
1725
  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)}.`);
1726
+ const warnings = [];
1727
+ if (issues.refused.length) warnings.push(`Kept project-refused settings in ${source}: ${formatIssueKeys(issues.refused)}. Configure machine settings globally and current-run paths on the command line.`);
1728
+ if (issues.incompatible.length) warnings.push(`Kept settings in ${source} that are incompatible with pnpm ${compatibility.slice(1)}: ${formatIssueKeys(issues.incompatible)}.`);
1729
+ if (issues.nonCamelCase.length) warnings.push(`Kept non-camelCase settings in ${source}: ${formatIssueKeys(issues.nonCamelCase)}. Workspace manifest keys must use camelCase.`);
1730
+ if (issues.unknown.length) warnings.push(`Kept settings in ${source} that pnpm ${compatibility.slice(1)} does not recognize: ${formatIssueKeys(issues.unknown)}.`);
1704
1731
  if (issues.unsupported.length) {
1705
1732
  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}.`);
1733
+ warnings.push(`Kept subproject settings in ${source}: ${formatIssueKeys(issues.unsupported)}; ${destination}.`);
1707
1734
  }
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.`);
1735
+ if (issues.unsafe.length) warnings.push(`Kept unsafe registry settings in ${source}: ${formatIssueKeys(issues.unsafe)}. Remove credentials and dynamic URL interpolation before migrating them.`);
1736
+ return warnings;
1709
1737
  }
1710
1738
  //#endregion
1711
1739
  //#region src/utils/yaml-document.ts
@@ -1753,6 +1781,32 @@ function formatRootSpacing(content, newlineBetween) {
1753
1781
  return content.replace(/\n+(?=[^\s#][^:\n]*:)/gu, newlineBetween ? "\n\n" : "\n");
1754
1782
  }
1755
1783
  //#endregion
1784
+ //#region src/cli-output.ts
1785
+ function formatMigrationSummary(result) {
1786
+ if (!result.hasConfigurationFiles) return "ℹ No configuration files found.";
1787
+ const count = result.settingsChanges.length;
1788
+ if (count) return `${green("✔")} ${count} ${count === 1 ? "setting" : "settings"} changed`;
1789
+ if (result.packageJsonRuntimeChanged) return `${green("✔")} Migration completed. Node.js runtime updated in package.json.`;
1790
+ if (result.sourceSettingsCleaned) return `${green("✔")} Migration completed. Source settings cleaned up.`;
1791
+ if (result.changedFiles.length) return `${green("✔")} Migration completed. Configuration files updated.`;
1792
+ return "ℹ No changes needed.";
1793
+ }
1794
+ /**
1795
+ * Format warnings, one outcome summary, and optional settings details.
1796
+ *
1797
+ * @param result - Completed migration outcome
1798
+ * @param showChanges - Whether to include the settings diff
1799
+ *
1800
+ * @returns Terminal output with one blank line between blocks and none at its edges
1801
+ */
1802
+ function formatMigrationOutput(result, showChanges = true) {
1803
+ const blocks = [];
1804
+ if (result.warnings.length) blocks.push(result.warnings.map((warning) => `${bold("WARN")} ${warning}`).join("\n"));
1805
+ blocks.push(formatMigrationSummary(result));
1806
+ if (showChanges && result.settingsChanges.length) blocks.push(formatSettingsChanges(result.settingsChanges));
1807
+ return blocks.join("\n\n");
1808
+ }
1809
+ //#endregion
1756
1810
  //#region src/migration-persistence.ts
1757
1811
  const SETTINGS_WITHOUT_REPLACEMENT = /* @__PURE__ */ new Set(["ignoreDepScripts", "ignorePatchFailures"]);
1758
1812
  const REPLACEMENT_SETTING_KEYS = {
@@ -1773,21 +1827,6 @@ const REPLACEMENT_SETTING_KEYS = {
1773
1827
  sideEffectsCacheReadonly: "sideEffectsCache",
1774
1828
  updateConfig: "update"
1775
1829
  };
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
1830
  function containsMigratedValue(actual, expected) {
1792
1831
  if (Array.isArray(actual) && Array.isArray(expected)) return expected.every((expectedItem) => actual.some((actualItem) => containsMigratedValue(actualItem, expectedItem)));
1793
1832
  if (actual && expected && typeof actual === "object" && typeof expected === "object") return Object.entries(expected).every(([key, expectedValue]) => containsMigratedValue(actual[key], expectedValue));
@@ -1838,7 +1877,7 @@ function selectAppliedProjectKeys(project, finalSettings) {
1838
1877
  *
1839
1878
  * @param options - Destination files, selected sources, and cleanup policy
1840
1879
  *
1841
- * @returns A promise that resolves after destinations and sources are persisted
1880
+ * @returns Changed file paths and applied cleanup/runtime changes
1842
1881
  */
1843
1882
  async function persistMigration(options) {
1844
1883
  const { cleanNpmrc, cleanPackageJson, compatibility, finalSettings, incomingSettings, npmrc, npmrcExists, npmrcPath, packageJson, packageJsonExists, packageJsonPath, packageJsonRuntimeChanged, packageJsonSettings, pnpmWorkspaceContent, pnpmWorkspacePath, projectNpmrcs, runtimeVersion } = options;
@@ -1866,20 +1905,31 @@ async function persistMigration(options) {
1866
1905
  settings: packageJsonSettings,
1867
1906
  yarnResolutionsApplied
1868
1907
  }) : 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;
1908
+ const result = {
1909
+ changedFiles: [],
1910
+ sourceSettingsCleaned: packageJsonSettingsChanged,
1911
+ packageJsonRuntimeChanged
1912
+ };
1913
+ if (await fsWriteFileIfChanged(pnpmWorkspacePath, pnpmWorkspaceContent)) result.changedFiles.push(pnpmWorkspacePath);
1914
+ if (packageJsonExists && (packageJsonRuntimeChanged || packageJsonSettingsChanged)) {
1915
+ await fsWriteFileIfChanged(packageJsonPath, JSON.stringify(packageJson.value, null, packageJson.indent));
1916
+ result.changedFiles.push(packageJsonPath);
1917
+ }
1918
+ if (!cleanNpmrc) return result;
1872
1919
  const pruneTasks = projectNpmrcs.projects.flatMap((project) => {
1873
1920
  const appliedKeys = selectAppliedProjectKeys(project, finalSettings);
1874
- return appliedKeys.length ? [pruneNpmrc(project.npmrcPath, compatibility, appliedKeys)] : [];
1921
+ return appliedKeys.length ? [pruneNpmrc(project.npmrcPath, compatibility, appliedKeys).then(() => project.npmrcPath)] : [];
1875
1922
  });
1876
- if (npmrcExists && appliedNpmrcKeys.length) pruneTasks.push(pruneNpmrc(npmrcPath, compatibility, appliedNpmrcKeys));
1877
- await Promise.all(pruneTasks);
1923
+ if (npmrcExists && appliedNpmrcKeys.length) pruneTasks.push(pruneNpmrc(npmrcPath, compatibility, appliedNpmrcKeys).then(() => npmrcPath));
1924
+ const cleanedNpmrcPaths = await Promise.all(pruneTasks);
1925
+ result.changedFiles.push(...cleanedNpmrcPaths);
1926
+ result.sourceSettingsCleaned ||= cleanedNpmrcPaths.length > 0;
1927
+ return result;
1878
1928
  }
1879
1929
  //#endregion
1880
1930
  //#region src/migration-sources.ts
1881
1931
  /**
1882
- * Read, schema-filter, report, and merge all legacy settings sources.
1932
+ * Read, schema-filter, and merge legacy settings while collecting warnings.
1883
1933
  *
1884
1934
  * @param options - Migration target and source configuration
1885
1935
  *
@@ -1887,32 +1937,34 @@ async function persistMigration(options) {
1887
1937
  */
1888
1938
  async function resolveMigrationSources(options) {
1889
1939
  const { compatibility, cwd, npmrcExists, npmrcPath, packageJson, pnpmWorkspace, strategy, yarnResolutions } = options;
1940
+ const warnings = [];
1890
1941
  const npmrc = npmrcExists ? await readMigratableNpmrc(npmrcPath, compatibility) : {
1891
1942
  issues: createSettingsIssues(),
1892
1943
  keys: [],
1893
1944
  settings: {}
1894
1945
  };
1895
- reportSettingsIssues({
1946
+ warnings.push(...formatSettingsIssues({
1896
1947
  compatibility,
1897
1948
  issues: npmrc.issues,
1898
1949
  source: NPMRC
1899
- });
1950
+ }));
1900
1951
  const packageJsonSettings = resolvePackageJsonSettings(packageJson, yarnResolutions, compatibility);
1901
- reportSettingsIssues({
1952
+ warnings.push(...formatSettingsIssues({
1902
1953
  compatibility,
1903
1954
  issues: packageJsonSettings.issues,
1904
1955
  source: `${PACKAGE_JSON}#pnpm`
1905
- });
1956
+ }));
1906
1957
  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({
1958
+ warnings.push(...projectNpmrcs.warnings);
1959
+ for (const project of projectNpmrcs.projects) warnings.push(...formatSettingsIssues({
1909
1960
  compatibility,
1910
1961
  issues: project.migratable.issues,
1911
1962
  projectConfig: true,
1912
1963
  source: relative(cwd, project.npmrcPath)
1913
- });
1964
+ }));
1914
1965
  const npmrcSettings = mergeByStrategy(npmrc.settings, Object.keys(projectNpmrcs.packageConfigs).length ? { packageConfigs: projectNpmrcs.packageConfigs } : {}, "merge");
1915
1966
  return {
1967
+ warnings,
1916
1968
  incomingSettings: mergeByStrategy(packageJsonSettings.settings, npmrcSettings, "merge"),
1917
1969
  npmrc,
1918
1970
  packageJson: packageJsonSettings,
@@ -1993,13 +2045,6 @@ function resolveOptions(options = {}) {
1993
2045
  }
1994
2046
  //#endregion
1995
2047
  //#region src/core.ts
1996
- function hasSettingsSources(npmrcExists, packageJsonExists, pnpmWorkspaceExists) {
1997
- if (!npmrcExists) consola$1.info(`${dim(NPMRC)} not found`);
1998
- if (!packageJsonExists) consola$1.info(`${dim(PACKAGE_JSON)} not found`);
1999
- if (npmrcExists || packageJsonExists || pnpmWorkspaceExists) return true;
2000
- consola$1.warn("No pnpm settings files to migrate");
2001
- return false;
2002
- }
2003
2048
  function hasMigratableSettings(sources) {
2004
2049
  const { existingSettingsChanged, npmrcKeys, packageJsonKeys, projectNpmrcKeys, yarnResolutions } = sources;
2005
2050
  return Boolean(existingSettingsChanged || npmrcKeys.length || packageJsonKeys.length || projectNpmrcKeys.length || yarnResolutions);
@@ -2007,10 +2052,6 @@ function hasMigratableSettings(sources) {
2007
2052
  function assertCanMigrateRuntime(runtimeVersion, packageJsonExists) {
2008
2053
  if (runtimeVersion && !packageJsonExists) throw new Error("Cannot migrate the removed Node.js runtime setting without a package.json file.");
2009
2054
  }
2010
- function reportMigrationChanges(showChanges, before = {}, after = {}) {
2011
- if (!showChanges) return;
2012
- reportSettingsChanges(collectSettingsChanges(before, after));
2013
- }
2014
2055
  /**
2015
2056
  * Migrate pnpm settings from legacy locations to `pnpm-workspace.yaml`.
2016
2057
  *
@@ -2030,10 +2071,10 @@ function reportMigrationChanges(showChanges, before = {}, after = {}) {
2030
2071
  * @param rawOptions.sortKeys - Whether to sort keys in output YAML (default: false)
2031
2072
  * @param rawOptions.newlineBetween - Add newlines between root keys (default: true)
2032
2073
  * @param rawOptions.replaceDeprecated - Whether to replace deprecated settings (default: false)
2033
- * @param rawOptions.showChanges - Show settings changes after migration (default: true)
2074
+ * @param rawOptions.showChanges - CLI display preference; does not affect the result
2034
2075
  * @param rawOptions.strategy - Conflict handling strategy (default: merge)
2035
2076
  *
2036
- * @returns A promise that resolves when migration is complete
2077
+ * @returns Settings changes, changed files, cleanup status, and warnings; no logs are emitted
2037
2078
  *
2038
2079
  * @throws {Error} When file operations fail or JSON/YAML parsing errors occur
2039
2080
  *
@@ -2051,107 +2092,106 @@ function reportMigrationChanges(showChanges, before = {}, after = {}) {
2051
2092
  * ```
2052
2093
  */
2053
2094
  async function migratePnpmSettings(rawOptions = {}) {
2054
- try {
2055
- const options = resolveOptions(rawOptions);
2056
- const npmrcPath = resolve(options.cwd, NPMRC);
2057
- const packageJsonPath = resolve(options.cwd, PACKAGE_JSON);
2058
- const pnpmWorkspaceYamlPath = resolve(options.cwd, PNPM_WORKSPACE_YAML);
2059
- const [npmrcExists, packageJsonExists, pnpmWorkspaceExists] = await Promise.all([
2060
- fsExists(npmrcPath),
2061
- fsExists(packageJsonPath),
2062
- fsExists(pnpmWorkspaceYamlPath)
2063
- ]);
2064
- if (!hasSettingsSources(npmrcExists, packageJsonExists, pnpmWorkspaceExists)) {
2065
- reportMigrationChanges(options.showChanges);
2066
- return;
2067
- }
2068
- const [packageJson, pnpmWorkspace] = await Promise.all([readPackageJson(packageJsonPath, packageJsonExists), readPnpmWorkspace(pnpmWorkspaceYamlPath, pnpmWorkspaceExists)]);
2069
- const pnpmWorkspaceBefore = structuredClone(pnpmWorkspace.value);
2070
- const compatibility = resolveCompatibilityTarget(options.compatibility, packageJson.value.packageManager, packageJson.value.devEngines?.packageManager);
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;
2083
- const [existingNormalization, incomingNormalization] = await Promise.all([normalizeIncomingSettings(pnpmWorkspace.value, {
2084
- compatibility,
2085
- cwd: options.cwd,
2086
- replaceDeprecated: options.replaceDeprecated
2087
- }), normalizeIncomingSettings(incomingSettings, {
2088
- compatibility,
2089
- cwd: options.cwd,
2090
- replaceDeprecated: options.replaceDeprecated
2091
- })]);
2092
- for (const warning of [...existingNormalization.warnings, ...incomingNormalization.warnings]) consola$1.warn(warning);
2093
- const runtimeVersion = resolveRuntimeVersionByStrategy(existingNormalization.runtimeVersion, incomingNormalization.runtimeVersion, options.strategy);
2094
- assertCanMigrateRuntime(runtimeVersion, packageJsonExists);
2095
- const runtimeMigration = migrateRuntimeToPackageJson(packageJson.value, runtimeVersion);
2096
- if (runtimeMigration.warning) consola$1.warn(runtimeMigration.warning);
2097
- if (!hasMigratableSettings({
2098
- existingSettingsChanged: existingNormalization.changed,
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
2103
- })) {
2104
- consola$1.warn("No pnpm settings fields to migrate");
2105
- reportMigrationChanges(options.showChanges);
2106
- return;
2107
- }
2108
- const pnpmWorkspaceResult = mergeByStrategy(pnpmWorkspace.value, incomingSettings, options.strategy);
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
2133
- });
2134
- reportMigrationChanges(options.showChanges, pnpmWorkspaceBefore, pnpmWorkspaceResult);
2135
- } catch (err) {
2136
- consola$1.error("Failed to migrate pnpm settings:", err);
2137
- throw err;
2138
- }
2095
+ const options = resolveOptions(rawOptions);
2096
+ const npmrcPath = resolve(options.cwd, NPMRC);
2097
+ const packageJsonPath = resolve(options.cwd, PACKAGE_JSON);
2098
+ const pnpmWorkspaceYamlPath = resolve(options.cwd, PNPM_WORKSPACE_YAML);
2099
+ const [npmrcExists, packageJsonExists, pnpmWorkspaceExists] = await Promise.all([
2100
+ fsExists(npmrcPath),
2101
+ fsExists(packageJsonPath),
2102
+ fsExists(pnpmWorkspaceYamlPath)
2103
+ ]);
2104
+ const result = {
2105
+ hasConfigurationFiles: npmrcExists || packageJsonExists || pnpmWorkspaceExists,
2106
+ settingsChanges: [],
2107
+ changedFiles: [],
2108
+ sourceSettingsCleaned: false,
2109
+ packageJsonRuntimeChanged: false,
2110
+ warnings: []
2111
+ };
2112
+ if (!result.hasConfigurationFiles) return result;
2113
+ const [packageJson, pnpmWorkspace] = await Promise.all([readPackageJson(packageJsonPath, packageJsonExists), readPnpmWorkspace(pnpmWorkspaceYamlPath, pnpmWorkspaceExists)]);
2114
+ const pnpmWorkspaceBefore = structuredClone(pnpmWorkspace.value);
2115
+ const compatibility = resolveCompatibilityTarget(options.compatibility, packageJson.value.packageManager, packageJson.value.devEngines?.packageManager);
2116
+ assertCompatibleWorkspaceSettings(pnpmWorkspace.value, compatibility);
2117
+ const sources = await resolveMigrationSources({
2118
+ compatibility,
2119
+ cwd: options.cwd,
2120
+ npmrcExists,
2121
+ npmrcPath,
2122
+ packageJson: packageJson.value,
2123
+ pnpmWorkspace: pnpmWorkspace.value,
2124
+ strategy: options.strategy,
2125
+ yarnResolutions: options.yarnResolutions
2126
+ });
2127
+ const { incomingSettings } = sources;
2128
+ const [existingNormalization, incomingNormalization] = await Promise.all([normalizeIncomingSettings(pnpmWorkspace.value, {
2129
+ compatibility,
2130
+ cwd: options.cwd,
2131
+ replaceDeprecated: options.replaceDeprecated
2132
+ }), normalizeIncomingSettings(incomingSettings, {
2133
+ compatibility,
2134
+ cwd: options.cwd,
2135
+ replaceDeprecated: options.replaceDeprecated
2136
+ })]);
2137
+ result.warnings.push(...sources.warnings, ...existingNormalization.warnings, ...incomingNormalization.warnings);
2138
+ const runtimeVersion = resolveRuntimeVersionByStrategy(existingNormalization.runtimeVersion, incomingNormalization.runtimeVersion, options.strategy);
2139
+ assertCanMigrateRuntime(runtimeVersion, packageJsonExists);
2140
+ const runtimeMigration = migrateRuntimeToPackageJson(packageJson.value, runtimeVersion);
2141
+ if (runtimeMigration.warning) result.warnings.push(runtimeMigration.warning);
2142
+ if (!hasMigratableSettings({
2143
+ existingSettingsChanged: existingNormalization.changed,
2144
+ npmrcKeys: sources.npmrc.keys,
2145
+ packageJsonKeys: sources.packageJson.keys,
2146
+ projectNpmrcKeys: sources.projectNpmrcs.projects.flatMap((project) => project.migratable.keys),
2147
+ yarnResolutions: sources.packageJson.yarnResolutions
2148
+ })) return result;
2149
+ const pnpmWorkspaceResult = mergeByStrategy(pnpmWorkspace.value, incomingSettings, options.strategy);
2150
+ updateYamlDocument(pnpmWorkspace.document, {
2151
+ after: pnpmWorkspaceResult,
2152
+ before: pnpmWorkspaceBefore,
2153
+ sortKeys: options.sortKeys
2154
+ });
2155
+ const finalYamlContent = formatRootSpacing(pnpmWorkspace.document.toString({ indent: pnpmWorkspace.indent }), options.newlineBetween);
2156
+ const persistence = await persistMigration({
2157
+ cleanNpmrc: options.cleanNpmrc,
2158
+ cleanPackageJson: options.cleanPackageJson,
2159
+ compatibility,
2160
+ finalSettings: pnpmWorkspaceResult,
2161
+ incomingSettings,
2162
+ npmrc: sources.npmrc,
2163
+ npmrcExists,
2164
+ npmrcPath,
2165
+ packageJson,
2166
+ packageJsonExists,
2167
+ packageJsonPath,
2168
+ packageJsonRuntimeChanged: runtimeMigration.changed,
2169
+ packageJsonSettings: sources.packageJson,
2170
+ pnpmWorkspaceContent: finalYamlContent,
2171
+ pnpmWorkspacePath: pnpmWorkspaceYamlPath,
2172
+ projectNpmrcs: sources.projectNpmrcs,
2173
+ runtimeVersion
2174
+ });
2175
+ return {
2176
+ ...result,
2177
+ ...persistence,
2178
+ settingsChanges: collectSettingsChanges(pnpmWorkspaceBefore, pnpmWorkspaceResult)
2179
+ };
2139
2180
  }
2140
2181
  //#endregion
2141
2182
  //#region src/cli.ts
2142
2183
  const cli = createCli();
2143
2184
  cli.command("").usage("[options]").action(async (options) => {
2144
- try {
2145
- consola.log(`\n${bold(magenta(name))} ${dim(`v${version}`)}`);
2146
- consola.log(dim("\n--------------\n"));
2147
- await migratePnpmSettings(options);
2148
- consola.success(green("pnpm settings migrate has finished"));
2149
- } catch (err) {
2150
- consola.fail(red(String(err)));
2151
- if (err instanceof Error && err.stack) consola.fail(dim(err.stack?.split("\n").slice(1).join("\n")));
2152
- process.exit(1);
2153
- }
2185
+ const result = await migratePnpmSettings(options);
2186
+ process.stdout.write(`${formatMigrationOutput(result, options.showChanges)}\n`);
2154
2187
  });
2155
- cli.parse();
2188
+ try {
2189
+ cli.parse(process.argv, { run: false });
2190
+ await cli.runMatchedCommand();
2191
+ } catch (error) {
2192
+ const message = error instanceof Error ? error.message : String(error);
2193
+ process.stderr.write(`${red("✖")} ${message.trim()}\n`);
2194
+ process.exitCode = 1;
2195
+ }
2156
2196
  //#endregion
2157
2197
  export {};