pnpm-settings-migrator 0.3.0 → 0.5.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/README.md CHANGED
@@ -31,6 +31,30 @@ Current working directory.
31
31
 
32
32
  Sort keys when write `pnpm-workspace.yaml`.
33
33
 
34
+ ### `--compatibility`
35
+
36
+ - **Type**: `'auto' | 'v10' | 'v11'`
37
+ - **Default**: `'auto'`
38
+
39
+ Compatibility target for migrated settings:
40
+
41
+ - `auto`: infer from `packageManager` (`pnpm@11+` => `v11`, otherwise `v10`)
42
+ - `v10`: keep legacy settings as-is
43
+ - `v11`: normalize to v11-compatible settings (`allowBuilds`, `allowUnusedPatches`, etc.)
44
+ and migrate all non auth/registry `.npmrc` entries to `pnpm-workspace.yaml`
45
+
46
+ ### `--replace-deprecated`
47
+
48
+ - **Type**: `boolean`
49
+ - **Default**: `false`
50
+
51
+ Force replacing deprecated pnpm settings with new keys and remove old keys during migration.
52
+
53
+ Example conversions:
54
+
55
+ - `allowNonAppliedPatches` -> `allowUnusedPatches`
56
+ - `onlyBuiltDependencies` / `ignoredBuiltDependencies` / `neverBuiltDependencies` -> `allowBuilds`
57
+
34
58
  ### `--strategy`
35
59
 
36
60
  - **Type**: `'discard' | 'merge' | 'overwrite'`
package/dist/cli.mjs CHANGED
@@ -13,7 +13,7 @@ import { readIniFile } from "read-ini-file";
13
13
  import { kebabCase } from "uncase";
14
14
  //#region package.json
15
15
  var name = "pnpm-settings-migrator";
16
- var version = "0.3.0";
16
+ var version = "0.5.0";
17
17
  //#endregion
18
18
  //#region src/constants.ts
19
19
  const NPMRC = ".npmrc";
@@ -50,6 +50,14 @@ const PNPM_SETTINGS_FIELDS = [
50
50
  "supportedArchitectures",
51
51
  "updateConfig"
52
52
  ];
53
+ const PNPM_V11_REMOVED_SETTINGS = [
54
+ "allowNonAppliedPatches",
55
+ "ignorePatchFailures",
56
+ "ignoredBuiltDependencies",
57
+ "neverBuiltDependencies",
58
+ "onlyBuiltDependencies",
59
+ "onlyBuiltDependenciesFile"
60
+ ];
53
61
  //#endregion
54
62
  //#region src/options.ts
55
63
  /**
@@ -58,8 +66,10 @@ const PNPM_SETTINGS_FIELDS = [
58
66
  const DEFAULT_OPTIONS = {
59
67
  cleanNpmrc: true,
60
68
  cleanPackageJson: true,
69
+ compatibility: "auto",
61
70
  cwd: process.cwd(),
62
71
  newlineBetween: true,
72
+ replaceDeprecated: false,
63
73
  sortKeys: false,
64
74
  strategy: "merge",
65
75
  yarnResolutions: true
@@ -69,6 +79,11 @@ const VALID_STRATEGIES = [
69
79
  "merge",
70
80
  "overwrite"
71
81
  ];
82
+ const VALID_COMPATIBILITIES = [
83
+ "auto",
84
+ "v10",
85
+ "v11"
86
+ ];
72
87
  /**
73
88
  * Resolve and normalize migration options with defaults.
74
89
  *
@@ -93,14 +108,21 @@ const VALID_STRATEGIES = [
93
108
  function resolveOptions(options = {}) {
94
109
  return {
95
110
  cleanNpmrc: options.cleanNpmrc ?? DEFAULT_OPTIONS.cleanNpmrc,
111
+ compatibility: resolveCompatibility(options.compatibility),
96
112
  cwd: options.cwd ?? DEFAULT_OPTIONS.cwd,
97
113
  newlineBetween: options.newlineBetween ?? DEFAULT_OPTIONS.newlineBetween,
98
114
  sortKeys: options.sortKeys ?? DEFAULT_OPTIONS.sortKeys,
99
115
  strategy: resolveStrategy(options.strategy),
100
116
  yarnResolutions: options.yarnResolutions ?? DEFAULT_OPTIONS.yarnResolutions,
101
- cleanPackageJson: options.cleanPackageJson ?? DEFAULT_OPTIONS.cleanPackageJson
117
+ cleanPackageJson: options.cleanPackageJson ?? DEFAULT_OPTIONS.cleanPackageJson,
118
+ replaceDeprecated: options.replaceDeprecated ?? DEFAULT_OPTIONS.replaceDeprecated
102
119
  };
103
120
  }
121
+ function resolveCompatibility(compatibility) {
122
+ if (!compatibility) return DEFAULT_OPTIONS.compatibility;
123
+ if (VALID_COMPATIBILITIES.includes(compatibility)) return compatibility;
124
+ throw new Error(`Invalid compatibility: ${compatibility}. Expected one of: ${VALID_COMPATIBILITIES.join(", ")}`);
125
+ }
104
126
  function resolveStrategy(strategy) {
105
127
  if (!strategy) return DEFAULT_OPTIONS.strategy;
106
128
  if (VALID_STRATEGIES.includes(strategy)) return strategy;
@@ -216,6 +238,22 @@ function mergeWithArrayDedupe(existing, incoming) {
216
238
  }
217
239
  //#endregion
218
240
  //#region src/utils/npmrc.ts
241
+ const NPMRC_AUTH_OR_REGISTRY_KEYS = [
242
+ "_auth",
243
+ "_authtoken",
244
+ "_password",
245
+ "always-auth",
246
+ "ca",
247
+ "cafile",
248
+ "cert",
249
+ "certfile",
250
+ "email",
251
+ "key",
252
+ "keyfile",
253
+ "otp",
254
+ "tokenhelper",
255
+ "username"
256
+ ];
219
257
  /**
220
258
  * Remove pnpm-related settings from `.npmrc` file.
221
259
  *
@@ -234,9 +272,15 @@ function mergeWithArrayDedupe(existing, incoming) {
234
272
  * await pruneNpmrc('/path/to/.npmrc')
235
273
  * ```
236
274
  */
237
- async function pruneNpmrc(path) {
275
+ async function pruneNpmrc(path, compatibility = "v10", migratedKeys = []) {
238
276
  const pnpmSettingsFields = PNPM_SETTINGS_FIELDS.map((v) => kebabCase(v));
239
- await fsWriteFile(path, (await fsReadFile(path)).split(/\r?\n/).filter((line) => !pnpmSettingsFields.some((v) => line.trim().startsWith(v))).join("\n"));
277
+ const migratedKeySet = new Set(migratedKeys.map(normalizeNpmrcKey));
278
+ await fsWriteFile(path, (await fsReadFile(path)).split(/\r?\n/).filter((line) => {
279
+ const key = getNpmrcLineKey(line);
280
+ if (!key) return true;
281
+ if (compatibility === "v11") return !migratedKeySet.has(normalizeNpmrcKey(key));
282
+ return !pnpmSettingsFields.some((v) => key.startsWith(v));
283
+ }).join("\n"));
240
284
  }
241
285
  /**
242
286
  * Read and parse `.npmrc` file with camelCase key conversion.
@@ -257,9 +301,57 @@ async function pruneNpmrc(path) {
257
301
  * // config.allowBuilds, config.packageExtensions, etc.
258
302
  * ```
259
303
  */
304
+ /**
305
+ * Read `.npmrc` and return settings that should be migrated into workspace config.
306
+ *
307
+ * - `v10`: returns all keys (legacy whitelist filtering happens in caller).
308
+ * - `v11`: excludes auth/registry keys because pnpm still reads them from `.npmrc`.
309
+ */
310
+ async function readMigratableNpmrc(path, compatibility) {
311
+ const raw = await readIniFile(path);
312
+ if (compatibility === "v10") return {
313
+ keys: Object.keys(raw),
314
+ settings: camelcaseKeys(raw)
315
+ };
316
+ const migratable = {};
317
+ const keys = [];
318
+ for (const [key, value] of Object.entries(raw)) if (!isNpmrcAuthOrRegistryKey(key)) {
319
+ migratable[key] = value;
320
+ keys.push(key);
321
+ }
322
+ return {
323
+ keys,
324
+ settings: camelcaseKeys(migratable)
325
+ };
326
+ }
260
327
  async function readNpmrc(path) {
261
328
  return camelcaseKeys(await readIniFile(path));
262
329
  }
330
+ /**
331
+ * Extract key name from a raw `.npmrc` line.
332
+ */
333
+ function getNpmrcLineKey(line) {
334
+ const trimmed = line.trim();
335
+ if (!trimmed || trimmed.startsWith(";") || trimmed.startsWith("#")) return;
336
+ const index = trimmed.indexOf("=");
337
+ if (index <= 0) return;
338
+ return normalizeNpmrcKey(trimmed.slice(0, index));
339
+ }
340
+ /**
341
+ * Check whether a `.npmrc` key is auth/registry-related and should stay in v11.
342
+ */
343
+ function isNpmrcAuthOrRegistryKey(key) {
344
+ const normalized = normalizeNpmrcKey(key);
345
+ if (normalized.startsWith("//")) return true;
346
+ if (normalized === "registry" || normalized.endsWith(":registry")) return true;
347
+ return NPMRC_AUTH_OR_REGISTRY_KEYS.some((v) => normalized === v || normalized.endsWith(`:${v}`));
348
+ }
349
+ /**
350
+ * Normalize `.npmrc` key for case-insensitive matching.
351
+ */
352
+ function normalizeNpmrcKey(key) {
353
+ return key.trim().replace(/\[\]$/, "").toLowerCase();
354
+ }
263
355
  //#endregion
264
356
  //#region src/core.ts
265
357
  /**
@@ -327,7 +419,15 @@ async function migratePnpmSettings(rawOptions = {}) {
327
419
  pnpmWorkspaceYamlIndent = detectIndent(content).amount;
328
420
  pnpmWorkspaceYamlObject = parse(content);
329
421
  }
330
- const pnpmSettingsInNpmrc = npmrcExists ? pick(await readNpmrc(npmrcPath), PNPM_SETTINGS_FIELDS) : {};
422
+ const compatibility = resolveCompatibilityTarget(options.compatibility, packageJsonObject.packageManager);
423
+ const npmrcMigratable = npmrcExists ? compatibility === "v10" ? {
424
+ keys: PNPM_SETTINGS_FIELDS,
425
+ settings: pick(await readNpmrc(npmrcPath), PNPM_SETTINGS_FIELDS)
426
+ } : await readMigratableNpmrc(npmrcPath, compatibility) : {
427
+ keys: [],
428
+ settings: {}
429
+ };
430
+ const pnpmSettingsInNpmrc = npmrcMigratable.settings;
331
431
  const hasPnpmInPackageJson = !!packageJsonObject.pnpm;
332
432
  const hasResolutions = options.yarnResolutions && !!packageJsonObject.resolutions;
333
433
  const hasNpmrcSettings = Object.keys(pnpmSettingsInNpmrc).length > 0;
@@ -344,6 +444,7 @@ async function migratePnpmSettings(rawOptions = {}) {
344
444
  ...pnpmSettingsInNpmrc,
345
445
  ...pnpmSettingsInPackageJson
346
446
  };
447
+ normalizeIncomingSettings(incomingSettings, compatibility, options.replaceDeprecated);
347
448
  const pnpmWorkspaceResult = mergeByStrategy(pnpmWorkspaceYamlObject, incomingSettings, options.strategy);
348
449
  const yamlDocument = new Document({}, { sortMapEntries: options.sortKeys });
349
450
  Object.entries(pnpmWorkspaceResult).forEach(([key, value]) => {
@@ -354,7 +455,7 @@ async function migratePnpmSettings(rawOptions = {}) {
354
455
  });
355
456
  const yamlContent = yamlDocument.toString({ indent: pnpmWorkspaceYamlIndent });
356
457
  await fsWriteFile(pnpmWorkspaceYamlPath, options.newlineBetween ? yamlContent.replace(/\n(?=[^\s#][^:\n]*:)/g, "\n\n") : yamlContent);
357
- if (npmrcExists && options.cleanNpmrc) await pruneNpmrc(npmrcPath);
458
+ if (npmrcExists && options.cleanNpmrc) await pruneNpmrc(npmrcPath, compatibility, npmrcMigratable.keys);
358
459
  if (packageJsonExists && options.cleanPackageJson && (packageJsonObject.pnpm || packageJsonObject.resolutions)) {
359
460
  delete packageJsonObject.pnpm;
360
461
  if (options.yarnResolutions) delete packageJsonObject.resolutions;
@@ -365,10 +466,41 @@ async function migratePnpmSettings(rawOptions = {}) {
365
466
  throw err;
366
467
  }
367
468
  }
469
+ /**
470
+ * Build v11 `allowBuilds` map from legacy build-script settings.
471
+ */
472
+ function collectAllowBuildsFromLegacy(incomingSettings) {
473
+ const allowBuilds = {};
474
+ for (const name of incomingSettings.onlyBuiltDependencies || []) allowBuilds[name] = true;
475
+ for (const name of incomingSettings.ignoredBuiltDependencies || []) allowBuilds[name] = false;
476
+ for (const name of incomingSettings.neverBuiltDependencies || []) allowBuilds[name] = false;
477
+ return Object.keys(allowBuilds).length ? allowBuilds : void 0;
478
+ }
479
+ /**
480
+ * Normalize incoming settings according to compatibility target.
481
+ */
482
+ function normalizeIncomingSettings(incomingSettings, compatibility, replaceDeprecated) {
483
+ if (compatibility === "v10" && !replaceDeprecated) return;
484
+ if (incomingSettings.allowNonAppliedPatches !== void 0) incomingSettings.allowUnusedPatches = incomingSettings.allowUnusedPatches ?? incomingSettings.allowNonAppliedPatches;
485
+ const allowBuildsFromLegacy = collectAllowBuildsFromLegacy(incomingSettings);
486
+ if (allowBuildsFromLegacy) incomingSettings.allowBuilds = {
487
+ ...allowBuildsFromLegacy,
488
+ ...incomingSettings.allowBuilds || {}
489
+ };
490
+ for (const key of PNPM_V11_REMOVED_SETTINGS) delete incomingSettings[key];
491
+ }
492
+ /**
493
+ * Resolve final compatibility target from user option and package manager hint.
494
+ */
495
+ function resolveCompatibilityTarget(compatibility, packageManager) {
496
+ if (compatibility !== "auto") return compatibility;
497
+ const [, major] = (packageManager || "").match(/^pnpm@(\d+)(?:\.|$)/) || [];
498
+ return Number(major) >= 11 ? "v11" : "v10";
499
+ }
368
500
  //#endregion
369
501
  //#region src/cli.ts
370
502
  const cli = cac(name);
371
- cli.version(version).option("--cwd [cwd]", "Current working directory").option("--sort-keys", "Sort keys when write pnpm-workspace.yaml").option("--strategy <strategy>", "Strategy to handle conflicts (discard, merge, overwrite)").option("--no-yarn-resolutions", "Disable migrating resolutions field in package.json").option("--no-newline-between", "Disable adding newlines between each root keys").option("--no-clean-npmrc", "Disable removing pnpm settings in .npmrc file").option("--no-clean-package-json", "Disable removing pnpm fields in package.json").help();
503
+ cli.version(version).option("--cwd [cwd]", "Current working directory").option("--sort-keys", "Sort keys when write pnpm-workspace.yaml").option("--compatibility <compatibility>", "Compatibility target (auto, v10, v11)").option("--replace-deprecated", "Replace deprecated pnpm settings with new ones and remove old keys").option("--strategy <strategy>", "Strategy to handle conflicts (discard, merge, overwrite)").option("--no-yarn-resolutions", "Disable migrating resolutions field in package.json").option("--no-newline-between", "Disable adding newlines between each root keys").option("--no-clean-npmrc", "Disable removing pnpm settings in .npmrc file").option("--no-clean-package-json", "Disable removing pnpm fields in package.json").help();
372
504
  cli.command("").action(async (options) => {
373
505
  try {
374
506
  consola$1.log(`\n${bold(magenta(name))} ${dim(`v${version}`)}`);
package/dist/index.d.mts CHANGED
@@ -2,7 +2,11 @@ import { PnpmSettings } from "@pnpm/types";
2
2
 
3
3
  //#region src/types.d.ts
4
4
  /**
5
- * Merge strategy for combining pnpm settings
5
+ * Compatibility target used by the migrator.
6
+ */
7
+ type CompatibilityTarget = 'auto' | 'v10' | 'v11';
8
+ /**
9
+ * Merge strategy for combining pnpm settings.
6
10
  */
7
11
  type MergeStrategy = 'discard' | 'merge' | 'overwrite';
8
12
  /**
@@ -21,6 +25,15 @@ interface Options {
21
25
  * @default true
22
26
  */
23
27
  cleanPackageJson?: boolean;
28
+ /**
29
+ * pnpm compatibility target.
30
+ * - `auto`: detect from `packageManager` (fallback to `v10`)
31
+ * - `v10`: keep legacy v10 settings
32
+ * - `v11`: normalize to v11-compatible settings
33
+ *
34
+ * @default 'auto'
35
+ */
36
+ compatibility?: CompatibilityTarget;
24
37
  /**
25
38
  * Current working directory
26
39
  *
@@ -32,6 +45,12 @@ interface Options {
32
45
  * @default true
33
46
  */
34
47
  newlineBetween?: boolean;
48
+ /**
49
+ * Replace deprecated settings with new ones and remove deprecated keys.
50
+ *
51
+ * @default false
52
+ */
53
+ replaceDeprecated?: boolean;
35
54
  /**
36
55
  * Sort keys when write `pnpm-workspace.yaml`
37
56
  *
@@ -68,6 +87,10 @@ interface PackageJson {
68
87
  * @compatibility npm, bun
69
88
  */
70
89
  overrides?: Record<string, string>;
90
+ /**
91
+ * Package manager declaration, for example `pnpm@11.0.0`
92
+ */
93
+ packageManager?: string;
71
94
  /**
72
95
  * pnpm settings
73
96
  */
@@ -89,10 +112,18 @@ type NpmRC = Record<string, any>;
89
112
  * @see {@link https://github.com/pnpm/pnpm/blob/main/core/types/CHANGELOG.md#major-changes}
90
113
  */
91
114
  interface PnpmSettingsDeprecated {
115
+ /**
116
+ * @deprecated
117
+ */
118
+ allowNonAppliedPatches?: boolean;
92
119
  /**
93
120
  * @deprecated
94
121
  */
95
122
  ignoredBuiltDependencies?: string[];
123
+ /**
124
+ * @deprecated
125
+ */
126
+ ignorePatchFailures?: boolean;
96
127
  /**
97
128
  * @deprecated
98
129
  */
@@ -173,4 +204,4 @@ declare function migratePnpmSettings(rawOptions?: Options): Promise<void>;
173
204
  */
174
205
  declare function resolveOptions(options?: Options): Required<Options>;
175
206
  //#endregion
176
- export { MergeStrategy, NpmRC, Options, PackageJson, PnpmSettingsDeprecated, PnpmWorkspace, PnpmWorkspaceLegacy, migratePnpmSettings, resolveOptions };
207
+ export { CompatibilityTarget, MergeStrategy, NpmRC, Options, PackageJson, PnpmSettingsDeprecated, PnpmWorkspace, PnpmWorkspaceLegacy, migratePnpmSettings, resolveOptions };
package/dist/index.mjs CHANGED
@@ -45,6 +45,14 @@ const PNPM_SETTINGS_FIELDS = [
45
45
  "supportedArchitectures",
46
46
  "updateConfig"
47
47
  ];
48
+ const PNPM_V11_REMOVED_SETTINGS = [
49
+ "allowNonAppliedPatches",
50
+ "ignorePatchFailures",
51
+ "ignoredBuiltDependencies",
52
+ "neverBuiltDependencies",
53
+ "onlyBuiltDependencies",
54
+ "onlyBuiltDependenciesFile"
55
+ ];
48
56
  //#endregion
49
57
  //#region src/options.ts
50
58
  /**
@@ -53,8 +61,10 @@ const PNPM_SETTINGS_FIELDS = [
53
61
  const DEFAULT_OPTIONS = {
54
62
  cleanNpmrc: true,
55
63
  cleanPackageJson: true,
64
+ compatibility: "auto",
56
65
  cwd: process.cwd(),
57
66
  newlineBetween: true,
67
+ replaceDeprecated: false,
58
68
  sortKeys: false,
59
69
  strategy: "merge",
60
70
  yarnResolutions: true
@@ -64,6 +74,11 @@ const VALID_STRATEGIES = [
64
74
  "merge",
65
75
  "overwrite"
66
76
  ];
77
+ const VALID_COMPATIBILITIES = [
78
+ "auto",
79
+ "v10",
80
+ "v11"
81
+ ];
67
82
  /**
68
83
  * Resolve and normalize migration options with defaults.
69
84
  *
@@ -88,14 +103,21 @@ const VALID_STRATEGIES = [
88
103
  function resolveOptions(options = {}) {
89
104
  return {
90
105
  cleanNpmrc: options.cleanNpmrc ?? DEFAULT_OPTIONS.cleanNpmrc,
106
+ compatibility: resolveCompatibility(options.compatibility),
91
107
  cwd: options.cwd ?? DEFAULT_OPTIONS.cwd,
92
108
  newlineBetween: options.newlineBetween ?? DEFAULT_OPTIONS.newlineBetween,
93
109
  sortKeys: options.sortKeys ?? DEFAULT_OPTIONS.sortKeys,
94
110
  strategy: resolveStrategy(options.strategy),
95
111
  yarnResolutions: options.yarnResolutions ?? DEFAULT_OPTIONS.yarnResolutions,
96
- cleanPackageJson: options.cleanPackageJson ?? DEFAULT_OPTIONS.cleanPackageJson
112
+ cleanPackageJson: options.cleanPackageJson ?? DEFAULT_OPTIONS.cleanPackageJson,
113
+ replaceDeprecated: options.replaceDeprecated ?? DEFAULT_OPTIONS.replaceDeprecated
97
114
  };
98
115
  }
116
+ function resolveCompatibility(compatibility) {
117
+ if (!compatibility) return DEFAULT_OPTIONS.compatibility;
118
+ if (VALID_COMPATIBILITIES.includes(compatibility)) return compatibility;
119
+ throw new Error(`Invalid compatibility: ${compatibility}. Expected one of: ${VALID_COMPATIBILITIES.join(", ")}`);
120
+ }
99
121
  function resolveStrategy(strategy) {
100
122
  if (!strategy) return DEFAULT_OPTIONS.strategy;
101
123
  if (VALID_STRATEGIES.includes(strategy)) return strategy;
@@ -211,6 +233,22 @@ function mergeWithArrayDedupe(existing, incoming) {
211
233
  }
212
234
  //#endregion
213
235
  //#region src/utils/npmrc.ts
236
+ const NPMRC_AUTH_OR_REGISTRY_KEYS = [
237
+ "_auth",
238
+ "_authtoken",
239
+ "_password",
240
+ "always-auth",
241
+ "ca",
242
+ "cafile",
243
+ "cert",
244
+ "certfile",
245
+ "email",
246
+ "key",
247
+ "keyfile",
248
+ "otp",
249
+ "tokenhelper",
250
+ "username"
251
+ ];
214
252
  /**
215
253
  * Remove pnpm-related settings from `.npmrc` file.
216
254
  *
@@ -229,9 +267,15 @@ function mergeWithArrayDedupe(existing, incoming) {
229
267
  * await pruneNpmrc('/path/to/.npmrc')
230
268
  * ```
231
269
  */
232
- async function pruneNpmrc(path) {
270
+ async function pruneNpmrc(path, compatibility = "v10", migratedKeys = []) {
233
271
  const pnpmSettingsFields = PNPM_SETTINGS_FIELDS.map((v) => kebabCase(v));
234
- await fsWriteFile(path, (await fsReadFile(path)).split(/\r?\n/).filter((line) => !pnpmSettingsFields.some((v) => line.trim().startsWith(v))).join("\n"));
272
+ const migratedKeySet = new Set(migratedKeys.map(normalizeNpmrcKey));
273
+ await fsWriteFile(path, (await fsReadFile(path)).split(/\r?\n/).filter((line) => {
274
+ const key = getNpmrcLineKey(line);
275
+ if (!key) return true;
276
+ if (compatibility === "v11") return !migratedKeySet.has(normalizeNpmrcKey(key));
277
+ return !pnpmSettingsFields.some((v) => key.startsWith(v));
278
+ }).join("\n"));
235
279
  }
236
280
  /**
237
281
  * Read and parse `.npmrc` file with camelCase key conversion.
@@ -252,9 +296,57 @@ async function pruneNpmrc(path) {
252
296
  * // config.allowBuilds, config.packageExtensions, etc.
253
297
  * ```
254
298
  */
299
+ /**
300
+ * Read `.npmrc` and return settings that should be migrated into workspace config.
301
+ *
302
+ * - `v10`: returns all keys (legacy whitelist filtering happens in caller).
303
+ * - `v11`: excludes auth/registry keys because pnpm still reads them from `.npmrc`.
304
+ */
305
+ async function readMigratableNpmrc(path, compatibility) {
306
+ const raw = await readIniFile(path);
307
+ if (compatibility === "v10") return {
308
+ keys: Object.keys(raw),
309
+ settings: camelcaseKeys(raw)
310
+ };
311
+ const migratable = {};
312
+ const keys = [];
313
+ for (const [key, value] of Object.entries(raw)) if (!isNpmrcAuthOrRegistryKey(key)) {
314
+ migratable[key] = value;
315
+ keys.push(key);
316
+ }
317
+ return {
318
+ keys,
319
+ settings: camelcaseKeys(migratable)
320
+ };
321
+ }
255
322
  async function readNpmrc(path) {
256
323
  return camelcaseKeys(await readIniFile(path));
257
324
  }
325
+ /**
326
+ * Extract key name from a raw `.npmrc` line.
327
+ */
328
+ function getNpmrcLineKey(line) {
329
+ const trimmed = line.trim();
330
+ if (!trimmed || trimmed.startsWith(";") || trimmed.startsWith("#")) return;
331
+ const index = trimmed.indexOf("=");
332
+ if (index <= 0) return;
333
+ return normalizeNpmrcKey(trimmed.slice(0, index));
334
+ }
335
+ /**
336
+ * Check whether a `.npmrc` key is auth/registry-related and should stay in v11.
337
+ */
338
+ function isNpmrcAuthOrRegistryKey(key) {
339
+ const normalized = normalizeNpmrcKey(key);
340
+ if (normalized.startsWith("//")) return true;
341
+ if (normalized === "registry" || normalized.endsWith(":registry")) return true;
342
+ return NPMRC_AUTH_OR_REGISTRY_KEYS.some((v) => normalized === v || normalized.endsWith(`:${v}`));
343
+ }
344
+ /**
345
+ * Normalize `.npmrc` key for case-insensitive matching.
346
+ */
347
+ function normalizeNpmrcKey(key) {
348
+ return key.trim().replace(/\[\]$/, "").toLowerCase();
349
+ }
258
350
  //#endregion
259
351
  //#region src/core.ts
260
352
  /**
@@ -322,7 +414,15 @@ async function migratePnpmSettings(rawOptions = {}) {
322
414
  pnpmWorkspaceYamlIndent = detectIndent(content).amount;
323
415
  pnpmWorkspaceYamlObject = parse(content);
324
416
  }
325
- const pnpmSettingsInNpmrc = npmrcExists ? pick(await readNpmrc(npmrcPath), PNPM_SETTINGS_FIELDS) : {};
417
+ const compatibility = resolveCompatibilityTarget(options.compatibility, packageJsonObject.packageManager);
418
+ const npmrcMigratable = npmrcExists ? compatibility === "v10" ? {
419
+ keys: PNPM_SETTINGS_FIELDS,
420
+ settings: pick(await readNpmrc(npmrcPath), PNPM_SETTINGS_FIELDS)
421
+ } : await readMigratableNpmrc(npmrcPath, compatibility) : {
422
+ keys: [],
423
+ settings: {}
424
+ };
425
+ const pnpmSettingsInNpmrc = npmrcMigratable.settings;
326
426
  const hasPnpmInPackageJson = !!packageJsonObject.pnpm;
327
427
  const hasResolutions = options.yarnResolutions && !!packageJsonObject.resolutions;
328
428
  const hasNpmrcSettings = Object.keys(pnpmSettingsInNpmrc).length > 0;
@@ -339,6 +439,7 @@ async function migratePnpmSettings(rawOptions = {}) {
339
439
  ...pnpmSettingsInNpmrc,
340
440
  ...pnpmSettingsInPackageJson
341
441
  };
442
+ normalizeIncomingSettings(incomingSettings, compatibility, options.replaceDeprecated);
342
443
  const pnpmWorkspaceResult = mergeByStrategy(pnpmWorkspaceYamlObject, incomingSettings, options.strategy);
343
444
  const yamlDocument = new Document({}, { sortMapEntries: options.sortKeys });
344
445
  Object.entries(pnpmWorkspaceResult).forEach(([key, value]) => {
@@ -349,7 +450,7 @@ async function migratePnpmSettings(rawOptions = {}) {
349
450
  });
350
451
  const yamlContent = yamlDocument.toString({ indent: pnpmWorkspaceYamlIndent });
351
452
  await fsWriteFile(pnpmWorkspaceYamlPath, options.newlineBetween ? yamlContent.replace(/\n(?=[^\s#][^:\n]*:)/g, "\n\n") : yamlContent);
352
- if (npmrcExists && options.cleanNpmrc) await pruneNpmrc(npmrcPath);
453
+ if (npmrcExists && options.cleanNpmrc) await pruneNpmrc(npmrcPath, compatibility, npmrcMigratable.keys);
353
454
  if (packageJsonExists && options.cleanPackageJson && (packageJsonObject.pnpm || packageJsonObject.resolutions)) {
354
455
  delete packageJsonObject.pnpm;
355
456
  if (options.yarnResolutions) delete packageJsonObject.resolutions;
@@ -360,5 +461,36 @@ async function migratePnpmSettings(rawOptions = {}) {
360
461
  throw err;
361
462
  }
362
463
  }
464
+ /**
465
+ * Build v11 `allowBuilds` map from legacy build-script settings.
466
+ */
467
+ function collectAllowBuildsFromLegacy(incomingSettings) {
468
+ const allowBuilds = {};
469
+ for (const name of incomingSettings.onlyBuiltDependencies || []) allowBuilds[name] = true;
470
+ for (const name of incomingSettings.ignoredBuiltDependencies || []) allowBuilds[name] = false;
471
+ for (const name of incomingSettings.neverBuiltDependencies || []) allowBuilds[name] = false;
472
+ return Object.keys(allowBuilds).length ? allowBuilds : void 0;
473
+ }
474
+ /**
475
+ * Normalize incoming settings according to compatibility target.
476
+ */
477
+ function normalizeIncomingSettings(incomingSettings, compatibility, replaceDeprecated) {
478
+ if (compatibility === "v10" && !replaceDeprecated) return;
479
+ if (incomingSettings.allowNonAppliedPatches !== void 0) incomingSettings.allowUnusedPatches = incomingSettings.allowUnusedPatches ?? incomingSettings.allowNonAppliedPatches;
480
+ const allowBuildsFromLegacy = collectAllowBuildsFromLegacy(incomingSettings);
481
+ if (allowBuildsFromLegacy) incomingSettings.allowBuilds = {
482
+ ...allowBuildsFromLegacy,
483
+ ...incomingSettings.allowBuilds || {}
484
+ };
485
+ for (const key of PNPM_V11_REMOVED_SETTINGS) delete incomingSettings[key];
486
+ }
487
+ /**
488
+ * Resolve final compatibility target from user option and package manager hint.
489
+ */
490
+ function resolveCompatibilityTarget(compatibility, packageManager) {
491
+ if (compatibility !== "auto") return compatibility;
492
+ const [, major] = (packageManager || "").match(/^pnpm@(\d+)(?:\.|$)/) || [];
493
+ return Number(major) >= 11 ? "v11" : "v10";
494
+ }
363
495
  //#endregion
364
496
  export { migratePnpmSettings, resolveOptions };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "pnpm-settings-migrator",
3
3
  "type": "module",
4
- "version": "0.3.0",
4
+ "version": "0.5.0",
5
5
  "description": "Move pnpm settings from `pnpm` field in `package.json` and `.npmrc` file to `pnpm-workspace.yaml`",
6
6
  "keywords": [
7
7
  "migrator",
@@ -27,7 +27,6 @@
27
27
  },
28
28
  "main": "./dist/index.mjs",
29
29
  "types": "./dist/index.d.mts",
30
- "bin": "./bin.mjs",
31
30
  "files": [
32
31
  "bin.mjs",
33
32
  "dist"
@@ -37,8 +36,8 @@
37
36
  },
38
37
  "sideEffects": false,
39
38
  "dependencies": {
40
- "@ntnyq/utils": "^0.11.6",
41
- "@pnpm/types": "^1100.0.0",
39
+ "@ntnyq/utils": "^0.13.2",
40
+ "@pnpm/types": "^1101.1.0",
42
41
  "cac": "^7.0.0",
43
42
  "camelcase-keys": "^10.0.2",
44
43
  "consola": "^3.4.2",
@@ -47,21 +46,20 @@
47
46
  "pathe": "^2.0.3",
48
47
  "read-ini-file": "^5.0.0",
49
48
  "uncase": "^0.2.0",
50
- "yaml": "^2.8.3"
49
+ "yaml": "^2.9.0"
51
50
  },
52
51
  "devDependencies": {
53
- "@ntnyq/eslint-config": "^6.0.1",
54
- "@types/node": "^25.6.0",
55
- "@typescript/native-preview": "^7.0.0-dev.20260410.1",
56
- "bumpp": "^11.0.1",
57
- "eslint": "^10.2.0",
52
+ "@ntnyq/eslint-config": "^6.1.3",
53
+ "@types/node": "^25.7.0",
54
+ "@typescript/native-preview": "^7.0.0-dev.20260512.1",
55
+ "bumpp": "^11.1.0",
56
+ "eslint": "^10.3.0",
58
57
  "husky": "^9.1.7",
59
58
  "nano-staged": "^1.0.2",
60
59
  "npm-run-all2": "^8.0.4",
61
- "oxfmt": "^0.44.0",
62
- "tsdown": "^0.21.7",
63
- "typescript": "^6.0.2",
64
- "vitest": "^4.1.4"
60
+ "oxfmt": "^0.49.0",
61
+ "tsdown": "^0.22.0",
62
+ "vitest": "^4.1.6"
65
63
  },
66
64
  "nano-staged": {
67
65
  "*.{js,ts,mjs,tsx,md,yml,yaml,toml,json}": "eslint --fix",
@@ -78,5 +76,8 @@
78
76
  "release:version": "bumpp",
79
77
  "test": "vitest",
80
78
  "typecheck": "tsgo --noEmit"
79
+ },
80
+ "bin": {
81
+ "pnpm-settings-migrator": "./bin.mjs"
81
82
  }
82
83
  }