pnpm-settings-migrator 0.2.0 → 0.4.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,18 @@ 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
+
34
46
  ### `--strategy`
35
47
 
36
48
  - **Type**: `'discard' | 'merge' | 'overwrite'`
package/dist/cli.mjs CHANGED
@@ -11,21 +11,15 @@ import { getColor } from "consola/utils";
11
11
  import camelcaseKeys from "camelcase-keys";
12
12
  import { readIniFile } from "read-ini-file";
13
13
  import { kebabCase } from "uncase";
14
-
15
14
  //#region package.json
16
15
  var name = "pnpm-settings-migrator";
17
- var version = "0.2.0";
18
-
16
+ var version = "0.4.0";
19
17
  //#endregion
20
18
  //#region src/constants.ts
21
19
  const NPMRC = ".npmrc";
22
20
  const PACKAGE_JSON = "package.json";
23
21
  const PNPM_WORKSPACE_YAML = "pnpm-workspace.yaml";
24
22
  /**
25
- * Default indent: 2 spaces
26
- */
27
- const DEFAULT_INDENT = 2;
28
- /**
29
23
  * @see {@link https://github.com/pnpm/pnpm/blob/main/packages/types/src/package.ts}
30
24
  */
31
25
  const PNPM_SETTINGS_FIELDS = [
@@ -36,21 +30,34 @@ const PNPM_SETTINGS_FIELDS = [
36
30
  "auditConfig",
37
31
  "configDependencies",
38
32
  "executionEnv",
33
+ "httpProxy",
34
+ "httpsProxy",
39
35
  "ignoredBuiltDependencies",
40
36
  "ignoredOptionalDependencies",
41
37
  "ignorePatchFailures",
42
38
  "neverBuiltDependencies",
39
+ "nodeDownloadMirrors",
40
+ "noProxy",
41
+ "npmrcAuthFile",
43
42
  "onlyBuiltDependencies",
44
43
  "onlyBuiltDependenciesFile",
45
44
  "overrides",
46
45
  "packageExtensions",
47
46
  "patchedDependencies",
48
47
  "peerDependencyRules",
48
+ "registries",
49
49
  "requiredScripts",
50
50
  "supportedArchitectures",
51
51
  "updateConfig"
52
52
  ];
53
-
53
+ const PNPM_V11_REMOVED_SETTINGS = [
54
+ "allowNonAppliedPatches",
55
+ "ignorePatchFailures",
56
+ "ignoredBuiltDependencies",
57
+ "neverBuiltDependencies",
58
+ "onlyBuiltDependencies",
59
+ "onlyBuiltDependenciesFile"
60
+ ];
54
61
  //#endregion
55
62
  //#region src/options.ts
56
63
  /**
@@ -59,6 +66,7 @@ const PNPM_SETTINGS_FIELDS = [
59
66
  const DEFAULT_OPTIONS = {
60
67
  cleanNpmrc: true,
61
68
  cleanPackageJson: true,
69
+ compatibility: "auto",
62
70
  cwd: process.cwd(),
63
71
  newlineBetween: true,
64
72
  sortKeys: false,
@@ -70,6 +78,11 @@ const VALID_STRATEGIES = [
70
78
  "merge",
71
79
  "overwrite"
72
80
  ];
81
+ const VALID_COMPATIBILITIES = [
82
+ "auto",
83
+ "v10",
84
+ "v11"
85
+ ];
73
86
  /**
74
87
  * Resolve and normalize migration options with defaults.
75
88
  *
@@ -94,6 +107,7 @@ const VALID_STRATEGIES = [
94
107
  function resolveOptions(options = {}) {
95
108
  return {
96
109
  cleanNpmrc: options.cleanNpmrc ?? DEFAULT_OPTIONS.cleanNpmrc,
110
+ compatibility: resolveCompatibility(options.compatibility),
97
111
  cwd: options.cwd ?? DEFAULT_OPTIONS.cwd,
98
112
  newlineBetween: options.newlineBetween ?? DEFAULT_OPTIONS.newlineBetween,
99
113
  sortKeys: options.sortKeys ?? DEFAULT_OPTIONS.sortKeys,
@@ -102,12 +116,16 @@ function resolveOptions(options = {}) {
102
116
  cleanPackageJson: options.cleanPackageJson ?? DEFAULT_OPTIONS.cleanPackageJson
103
117
  };
104
118
  }
119
+ function resolveCompatibility(compatibility) {
120
+ if (!compatibility) return DEFAULT_OPTIONS.compatibility;
121
+ if (VALID_COMPATIBILITIES.includes(compatibility)) return compatibility;
122
+ throw new Error(`Invalid compatibility: ${compatibility}. Expected one of: ${VALID_COMPATIBILITIES.join(", ")}`);
123
+ }
105
124
  function resolveStrategy(strategy) {
106
125
  if (!strategy) return DEFAULT_OPTIONS.strategy;
107
126
  if (VALID_STRATEGIES.includes(strategy)) return strategy;
108
127
  throw new Error(`Invalid strategy: ${strategy}. Expected one of: ${VALID_STRATEGIES.join(", ")}`);
109
128
  }
110
-
111
129
  //#endregion
112
130
  //#region src/utils/fs.ts
113
131
  /**
@@ -134,17 +152,13 @@ async function fsReadFile(path) {
134
152
  async function fsWriteFile(path, content) {
135
153
  await writeFile(path, `${content.trimEnd()}\n`, "utf-8");
136
154
  }
137
-
138
- //#endregion
139
- //#region src/utils/color.ts
140
- const cyan = getColor("cyan");
141
- const yellow = getColor("yellow");
155
+ getColor("cyan");
156
+ getColor("yellow");
142
157
  const dim = getColor("dim");
143
158
  const green = getColor("green");
144
159
  const red = getColor("red");
145
160
  const bold = getColor("bold");
146
161
  const magenta = getColor("magenta");
147
-
148
162
  //#endregion
149
163
  //#region src/utils/merge.ts
150
164
  /**
@@ -220,9 +234,24 @@ function mergeWithArrayDedupe(existing, incoming) {
220
234
  }
221
235
  return result;
222
236
  }
223
-
224
237
  //#endregion
225
238
  //#region src/utils/npmrc.ts
239
+ const NPMRC_AUTH_OR_REGISTRY_KEYS = [
240
+ "_auth",
241
+ "_authtoken",
242
+ "_password",
243
+ "always-auth",
244
+ "ca",
245
+ "cafile",
246
+ "cert",
247
+ "certfile",
248
+ "email",
249
+ "key",
250
+ "keyfile",
251
+ "otp",
252
+ "tokenhelper",
253
+ "username"
254
+ ];
226
255
  /**
227
256
  * Remove pnpm-related settings from `.npmrc` file.
228
257
  *
@@ -241,9 +270,15 @@ function mergeWithArrayDedupe(existing, incoming) {
241
270
  * await pruneNpmrc('/path/to/.npmrc')
242
271
  * ```
243
272
  */
244
- async function pruneNpmrc(path) {
273
+ async function pruneNpmrc(path, compatibility = "v10", migratedKeys = []) {
245
274
  const pnpmSettingsFields = PNPM_SETTINGS_FIELDS.map((v) => kebabCase(v));
246
- await fsWriteFile(path, (await fsReadFile(path)).split(/\r?\n/).filter((line) => !pnpmSettingsFields.some((v) => line.trim().startsWith(v))).join("\n"));
275
+ const migratedKeySet = new Set(migratedKeys.map(normalizeNpmrcKey));
276
+ await fsWriteFile(path, (await fsReadFile(path)).split(/\r?\n/).filter((line) => {
277
+ const key = getNpmrcLineKey(line);
278
+ if (!key) return true;
279
+ if (compatibility === "v11") return !migratedKeySet.has(normalizeNpmrcKey(key));
280
+ return !pnpmSettingsFields.some((v) => key.startsWith(v));
281
+ }).join("\n"));
247
282
  }
248
283
  /**
249
284
  * Read and parse `.npmrc` file with camelCase key conversion.
@@ -264,10 +299,57 @@ async function pruneNpmrc(path) {
264
299
  * // config.allowBuilds, config.packageExtensions, etc.
265
300
  * ```
266
301
  */
302
+ /**
303
+ * Read `.npmrc` and return settings that should be migrated into workspace config.
304
+ *
305
+ * - `v10`: returns all keys (legacy whitelist filtering happens in caller).
306
+ * - `v11`: excludes auth/registry keys because pnpm still reads them from `.npmrc`.
307
+ */
308
+ async function readMigratableNpmrc(path, compatibility) {
309
+ const raw = await readIniFile(path);
310
+ if (compatibility === "v10") return {
311
+ keys: Object.keys(raw),
312
+ settings: camelcaseKeys(raw)
313
+ };
314
+ const migratable = {};
315
+ const keys = [];
316
+ for (const [key, value] of Object.entries(raw)) if (!isNpmrcAuthOrRegistryKey(key)) {
317
+ migratable[key] = value;
318
+ keys.push(key);
319
+ }
320
+ return {
321
+ keys,
322
+ settings: camelcaseKeys(migratable)
323
+ };
324
+ }
267
325
  async function readNpmrc(path) {
268
326
  return camelcaseKeys(await readIniFile(path));
269
327
  }
270
-
328
+ /**
329
+ * Extract key name from a raw `.npmrc` line.
330
+ */
331
+ function getNpmrcLineKey(line) {
332
+ const trimmed = line.trim();
333
+ if (!trimmed || trimmed.startsWith(";") || trimmed.startsWith("#")) return;
334
+ const index = trimmed.indexOf("=");
335
+ if (index <= 0) return;
336
+ return normalizeNpmrcKey(trimmed.slice(0, index));
337
+ }
338
+ /**
339
+ * Check whether a `.npmrc` key is auth/registry-related and should stay in v11.
340
+ */
341
+ function isNpmrcAuthOrRegistryKey(key) {
342
+ const normalized = normalizeNpmrcKey(key);
343
+ if (normalized.startsWith("//")) return true;
344
+ if (normalized === "registry" || normalized.endsWith(":registry")) return true;
345
+ return NPMRC_AUTH_OR_REGISTRY_KEYS.some((v) => normalized === v || normalized.endsWith(`:${v}`));
346
+ }
347
+ /**
348
+ * Normalize `.npmrc` key for case-insensitive matching.
349
+ */
350
+ function normalizeNpmrcKey(key) {
351
+ return key.trim().replace(/\[\]$/, "").toLowerCase();
352
+ }
271
353
  //#endregion
272
354
  //#region src/core.ts
273
355
  /**
@@ -321,9 +403,9 @@ async function migratePnpmSettings(rawOptions = {}) {
321
403
  consola.warn("No pnpm settings files to migrate");
322
404
  return;
323
405
  }
324
- let packageJsonIndent = DEFAULT_INDENT;
406
+ let packageJsonIndent = 2;
325
407
  let packageJsonObject = {};
326
- let pnpmWorkspaceYamlIndent = DEFAULT_INDENT;
408
+ let pnpmWorkspaceYamlIndent = 2;
327
409
  let pnpmWorkspaceYamlObject = {};
328
410
  if (packageJsonExists) {
329
411
  const content = await fsReadFile(packageJsonPath);
@@ -335,7 +417,15 @@ async function migratePnpmSettings(rawOptions = {}) {
335
417
  pnpmWorkspaceYamlIndent = detectIndent(content).amount;
336
418
  pnpmWorkspaceYamlObject = parse(content);
337
419
  }
338
- const pnpmSettingsInNpmrc = npmrcExists ? pick(await readNpmrc(npmrcPath), PNPM_SETTINGS_FIELDS) : {};
420
+ const compatibility = resolveCompatibilityTarget(options.compatibility, packageJsonObject.packageManager);
421
+ const npmrcMigratable = npmrcExists ? compatibility === "v10" ? {
422
+ keys: PNPM_SETTINGS_FIELDS,
423
+ settings: pick(await readNpmrc(npmrcPath), PNPM_SETTINGS_FIELDS)
424
+ } : await readMigratableNpmrc(npmrcPath, compatibility) : {
425
+ keys: [],
426
+ settings: {}
427
+ };
428
+ const pnpmSettingsInNpmrc = npmrcMigratable.settings;
339
429
  const hasPnpmInPackageJson = !!packageJsonObject.pnpm;
340
430
  const hasResolutions = options.yarnResolutions && !!packageJsonObject.resolutions;
341
431
  const hasNpmrcSettings = Object.keys(pnpmSettingsInNpmrc).length > 0;
@@ -352,6 +442,7 @@ async function migratePnpmSettings(rawOptions = {}) {
352
442
  ...pnpmSettingsInNpmrc,
353
443
  ...pnpmSettingsInPackageJson
354
444
  };
445
+ normalizeIncomingSettings(incomingSettings, compatibility);
355
446
  const pnpmWorkspaceResult = mergeByStrategy(pnpmWorkspaceYamlObject, incomingSettings, options.strategy);
356
447
  const yamlDocument = new Document({}, { sortMapEntries: options.sortKeys });
357
448
  Object.entries(pnpmWorkspaceResult).forEach(([key, value]) => {
@@ -362,7 +453,7 @@ async function migratePnpmSettings(rawOptions = {}) {
362
453
  });
363
454
  const yamlContent = yamlDocument.toString({ indent: pnpmWorkspaceYamlIndent });
364
455
  await fsWriteFile(pnpmWorkspaceYamlPath, options.newlineBetween ? yamlContent.replace(/\n(?=[^\s#][^:\n]*:)/g, "\n\n") : yamlContent);
365
- if (npmrcExists && options.cleanNpmrc) await pruneNpmrc(npmrcPath);
456
+ if (npmrcExists && options.cleanNpmrc) await pruneNpmrc(npmrcPath, compatibility, npmrcMigratable.keys);
366
457
  if (packageJsonExists && options.cleanPackageJson && (packageJsonObject.pnpm || packageJsonObject.resolutions)) {
367
458
  delete packageJsonObject.pnpm;
368
459
  if (options.yarnResolutions) delete packageJsonObject.resolutions;
@@ -373,11 +464,41 @@ async function migratePnpmSettings(rawOptions = {}) {
373
464
  throw err;
374
465
  }
375
466
  }
376
-
467
+ /**
468
+ * Build v11 `allowBuilds` map from legacy build-script settings.
469
+ */
470
+ function collectAllowBuildsFromLegacy(incomingSettings) {
471
+ const allowBuilds = {};
472
+ for (const name of incomingSettings.onlyBuiltDependencies || []) allowBuilds[name] = true;
473
+ for (const name of incomingSettings.ignoredBuiltDependencies || []) allowBuilds[name] = false;
474
+ for (const name of incomingSettings.neverBuiltDependencies || []) allowBuilds[name] = false;
475
+ return Object.keys(allowBuilds).length ? allowBuilds : void 0;
476
+ }
477
+ /**
478
+ * Normalize incoming settings according to compatibility target.
479
+ */
480
+ function normalizeIncomingSettings(incomingSettings, compatibility) {
481
+ if (compatibility === "v10") return;
482
+ if (incomingSettings.allowNonAppliedPatches !== void 0) incomingSettings.allowUnusedPatches = incomingSettings.allowUnusedPatches ?? incomingSettings.allowNonAppliedPatches;
483
+ const allowBuildsFromLegacy = collectAllowBuildsFromLegacy(incomingSettings);
484
+ if (allowBuildsFromLegacy) incomingSettings.allowBuilds = {
485
+ ...allowBuildsFromLegacy,
486
+ ...incomingSettings.allowBuilds || {}
487
+ };
488
+ for (const key of PNPM_V11_REMOVED_SETTINGS) delete incomingSettings[key];
489
+ }
490
+ /**
491
+ * Resolve final compatibility target from user option and package manager hint.
492
+ */
493
+ function resolveCompatibilityTarget(compatibility, packageManager) {
494
+ if (compatibility !== "auto") return compatibility;
495
+ const [, major] = (packageManager || "").match(/^pnpm@(\d+)(?:\.|$)/) || [];
496
+ return Number(major) >= 11 ? "v11" : "v10";
497
+ }
377
498
  //#endregion
378
499
  //#region src/cli.ts
379
500
  const cli = cac(name);
380
- 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();
501
+ 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("--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();
381
502
  cli.command("").action(async (options) => {
382
503
  try {
383
504
  consola$1.log(`\n${bold(magenta(name))} ${dim(`v${version}`)}`);
@@ -391,6 +512,5 @@ cli.command("").action(async (options) => {
391
512
  }
392
513
  });
393
514
  cli.parse();
394
-
395
515
  //#endregion
396
- export { };
516
+ export {};
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
  *
@@ -68,6 +81,10 @@ interface PackageJson {
68
81
  * @compatibility npm, bun
69
82
  */
70
83
  overrides?: Record<string, string>;
84
+ /**
85
+ * Package manager declaration, for example `pnpm@11.0.0`
86
+ */
87
+ packageManager?: string;
71
88
  /**
72
89
  * pnpm settings
73
90
  */
@@ -84,11 +101,41 @@ interface PackageJson {
84
101
  * @pg
85
102
  */
86
103
  type NpmRC = Record<string, any>;
104
+ /**
105
+ * Deprecated `pnpm` settings in `package.json`
106
+ * @see {@link https://github.com/pnpm/pnpm/blob/main/core/types/CHANGELOG.md#major-changes}
107
+ */
108
+ interface PnpmSettingsDeprecated {
109
+ /**
110
+ * @deprecated
111
+ */
112
+ allowNonAppliedPatches?: boolean;
113
+ /**
114
+ * @deprecated
115
+ */
116
+ ignoredBuiltDependencies?: string[];
117
+ /**
118
+ * @deprecated
119
+ */
120
+ ignorePatchFailures?: boolean;
121
+ /**
122
+ * @deprecated
123
+ */
124
+ neverBuiltDependencies?: string[];
125
+ /**
126
+ * @deprecated
127
+ */
128
+ onlyBuiltDependencies?: string[];
129
+ /**
130
+ * @deprecated
131
+ */
132
+ onlyBuiltDependenciesFile?: string;
133
+ }
87
134
  /**
88
135
  * `pnpm-workspace` types
89
136
  * @pg
90
137
  */
91
- type PnpmWorkspace = PnpmSettings & PnpmWorkspaceLegacy;
138
+ type PnpmWorkspace = PnpmSettings & PnpmSettingsDeprecated & PnpmWorkspaceLegacy;
92
139
  //#endregion
93
140
  //#region src/core.d.ts
94
141
  /**
@@ -151,4 +198,4 @@ declare function migratePnpmSettings(rawOptions?: Options): Promise<void>;
151
198
  */
152
199
  declare function resolveOptions(options?: Options): Required<Options>;
153
200
  //#endregion
154
- export { MergeStrategy, NpmRC, Options, PackageJson, PnpmWorkspace, PnpmWorkspaceLegacy, migratePnpmSettings, resolveOptions };
201
+ export { CompatibilityTarget, MergeStrategy, NpmRC, Options, PackageJson, PnpmSettingsDeprecated, PnpmWorkspace, PnpmWorkspaceLegacy, migratePnpmSettings, resolveOptions };
package/dist/index.mjs CHANGED
@@ -10,16 +10,11 @@ import { getColor } from "consola/utils";
10
10
  import camelcaseKeys from "camelcase-keys";
11
11
  import { readIniFile } from "read-ini-file";
12
12
  import { kebabCase } from "uncase";
13
-
14
13
  //#region src/constants.ts
15
14
  const NPMRC = ".npmrc";
16
15
  const PACKAGE_JSON = "package.json";
17
16
  const PNPM_WORKSPACE_YAML = "pnpm-workspace.yaml";
18
17
  /**
19
- * Default indent: 2 spaces
20
- */
21
- const DEFAULT_INDENT = 2;
22
- /**
23
18
  * @see {@link https://github.com/pnpm/pnpm/blob/main/packages/types/src/package.ts}
24
19
  */
25
20
  const PNPM_SETTINGS_FIELDS = [
@@ -30,21 +25,34 @@ const PNPM_SETTINGS_FIELDS = [
30
25
  "auditConfig",
31
26
  "configDependencies",
32
27
  "executionEnv",
28
+ "httpProxy",
29
+ "httpsProxy",
33
30
  "ignoredBuiltDependencies",
34
31
  "ignoredOptionalDependencies",
35
32
  "ignorePatchFailures",
36
33
  "neverBuiltDependencies",
34
+ "nodeDownloadMirrors",
35
+ "noProxy",
36
+ "npmrcAuthFile",
37
37
  "onlyBuiltDependencies",
38
38
  "onlyBuiltDependenciesFile",
39
39
  "overrides",
40
40
  "packageExtensions",
41
41
  "patchedDependencies",
42
42
  "peerDependencyRules",
43
+ "registries",
43
44
  "requiredScripts",
44
45
  "supportedArchitectures",
45
46
  "updateConfig"
46
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,6 +61,7 @@ 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,
58
67
  sortKeys: false,
@@ -64,6 +73,11 @@ const VALID_STRATEGIES = [
64
73
  "merge",
65
74
  "overwrite"
66
75
  ];
76
+ const VALID_COMPATIBILITIES = [
77
+ "auto",
78
+ "v10",
79
+ "v11"
80
+ ];
67
81
  /**
68
82
  * Resolve and normalize migration options with defaults.
69
83
  *
@@ -88,6 +102,7 @@ const VALID_STRATEGIES = [
88
102
  function resolveOptions(options = {}) {
89
103
  return {
90
104
  cleanNpmrc: options.cleanNpmrc ?? DEFAULT_OPTIONS.cleanNpmrc,
105
+ compatibility: resolveCompatibility(options.compatibility),
91
106
  cwd: options.cwd ?? DEFAULT_OPTIONS.cwd,
92
107
  newlineBetween: options.newlineBetween ?? DEFAULT_OPTIONS.newlineBetween,
93
108
  sortKeys: options.sortKeys ?? DEFAULT_OPTIONS.sortKeys,
@@ -96,12 +111,16 @@ function resolveOptions(options = {}) {
96
111
  cleanPackageJson: options.cleanPackageJson ?? DEFAULT_OPTIONS.cleanPackageJson
97
112
  };
98
113
  }
114
+ function resolveCompatibility(compatibility) {
115
+ if (!compatibility) return DEFAULT_OPTIONS.compatibility;
116
+ if (VALID_COMPATIBILITIES.includes(compatibility)) return compatibility;
117
+ throw new Error(`Invalid compatibility: ${compatibility}. Expected one of: ${VALID_COMPATIBILITIES.join(", ")}`);
118
+ }
99
119
  function resolveStrategy(strategy) {
100
120
  if (!strategy) return DEFAULT_OPTIONS.strategy;
101
121
  if (VALID_STRATEGIES.includes(strategy)) return strategy;
102
122
  throw new Error(`Invalid strategy: ${strategy}. Expected one of: ${VALID_STRATEGIES.join(", ")}`);
103
123
  }
104
-
105
124
  //#endregion
106
125
  //#region src/utils/fs.ts
107
126
  /**
@@ -128,17 +147,13 @@ async function fsReadFile(path) {
128
147
  async function fsWriteFile(path, content) {
129
148
  await writeFile(path, `${content.trimEnd()}\n`, "utf-8");
130
149
  }
131
-
132
- //#endregion
133
- //#region src/utils/color.ts
134
- const cyan = getColor("cyan");
135
- const yellow = getColor("yellow");
150
+ getColor("cyan");
151
+ getColor("yellow");
136
152
  const dim = getColor("dim");
137
- const green = getColor("green");
138
- const red = getColor("red");
139
- const bold = getColor("bold");
140
- const magenta = getColor("magenta");
141
-
153
+ getColor("green");
154
+ getColor("red");
155
+ getColor("bold");
156
+ getColor("magenta");
142
157
  //#endregion
143
158
  //#region src/utils/merge.ts
144
159
  /**
@@ -214,9 +229,24 @@ function mergeWithArrayDedupe(existing, incoming) {
214
229
  }
215
230
  return result;
216
231
  }
217
-
218
232
  //#endregion
219
233
  //#region src/utils/npmrc.ts
234
+ const NPMRC_AUTH_OR_REGISTRY_KEYS = [
235
+ "_auth",
236
+ "_authtoken",
237
+ "_password",
238
+ "always-auth",
239
+ "ca",
240
+ "cafile",
241
+ "cert",
242
+ "certfile",
243
+ "email",
244
+ "key",
245
+ "keyfile",
246
+ "otp",
247
+ "tokenhelper",
248
+ "username"
249
+ ];
220
250
  /**
221
251
  * Remove pnpm-related settings from `.npmrc` file.
222
252
  *
@@ -235,9 +265,15 @@ function mergeWithArrayDedupe(existing, incoming) {
235
265
  * await pruneNpmrc('/path/to/.npmrc')
236
266
  * ```
237
267
  */
238
- async function pruneNpmrc(path) {
268
+ async function pruneNpmrc(path, compatibility = "v10", migratedKeys = []) {
239
269
  const pnpmSettingsFields = PNPM_SETTINGS_FIELDS.map((v) => kebabCase(v));
240
- await fsWriteFile(path, (await fsReadFile(path)).split(/\r?\n/).filter((line) => !pnpmSettingsFields.some((v) => line.trim().startsWith(v))).join("\n"));
270
+ const migratedKeySet = new Set(migratedKeys.map(normalizeNpmrcKey));
271
+ await fsWriteFile(path, (await fsReadFile(path)).split(/\r?\n/).filter((line) => {
272
+ const key = getNpmrcLineKey(line);
273
+ if (!key) return true;
274
+ if (compatibility === "v11") return !migratedKeySet.has(normalizeNpmrcKey(key));
275
+ return !pnpmSettingsFields.some((v) => key.startsWith(v));
276
+ }).join("\n"));
241
277
  }
242
278
  /**
243
279
  * Read and parse `.npmrc` file with camelCase key conversion.
@@ -258,10 +294,57 @@ async function pruneNpmrc(path) {
258
294
  * // config.allowBuilds, config.packageExtensions, etc.
259
295
  * ```
260
296
  */
297
+ /**
298
+ * Read `.npmrc` and return settings that should be migrated into workspace config.
299
+ *
300
+ * - `v10`: returns all keys (legacy whitelist filtering happens in caller).
301
+ * - `v11`: excludes auth/registry keys because pnpm still reads them from `.npmrc`.
302
+ */
303
+ async function readMigratableNpmrc(path, compatibility) {
304
+ const raw = await readIniFile(path);
305
+ if (compatibility === "v10") return {
306
+ keys: Object.keys(raw),
307
+ settings: camelcaseKeys(raw)
308
+ };
309
+ const migratable = {};
310
+ const keys = [];
311
+ for (const [key, value] of Object.entries(raw)) if (!isNpmrcAuthOrRegistryKey(key)) {
312
+ migratable[key] = value;
313
+ keys.push(key);
314
+ }
315
+ return {
316
+ keys,
317
+ settings: camelcaseKeys(migratable)
318
+ };
319
+ }
261
320
  async function readNpmrc(path) {
262
321
  return camelcaseKeys(await readIniFile(path));
263
322
  }
264
-
323
+ /**
324
+ * Extract key name from a raw `.npmrc` line.
325
+ */
326
+ function getNpmrcLineKey(line) {
327
+ const trimmed = line.trim();
328
+ if (!trimmed || trimmed.startsWith(";") || trimmed.startsWith("#")) return;
329
+ const index = trimmed.indexOf("=");
330
+ if (index <= 0) return;
331
+ return normalizeNpmrcKey(trimmed.slice(0, index));
332
+ }
333
+ /**
334
+ * Check whether a `.npmrc` key is auth/registry-related and should stay in v11.
335
+ */
336
+ function isNpmrcAuthOrRegistryKey(key) {
337
+ const normalized = normalizeNpmrcKey(key);
338
+ if (normalized.startsWith("//")) return true;
339
+ if (normalized === "registry" || normalized.endsWith(":registry")) return true;
340
+ return NPMRC_AUTH_OR_REGISTRY_KEYS.some((v) => normalized === v || normalized.endsWith(`:${v}`));
341
+ }
342
+ /**
343
+ * Normalize `.npmrc` key for case-insensitive matching.
344
+ */
345
+ function normalizeNpmrcKey(key) {
346
+ return key.trim().replace(/\[\]$/, "").toLowerCase();
347
+ }
265
348
  //#endregion
266
349
  //#region src/core.ts
267
350
  /**
@@ -315,9 +398,9 @@ async function migratePnpmSettings(rawOptions = {}) {
315
398
  consola.warn("No pnpm settings files to migrate");
316
399
  return;
317
400
  }
318
- let packageJsonIndent = DEFAULT_INDENT;
401
+ let packageJsonIndent = 2;
319
402
  let packageJsonObject = {};
320
- let pnpmWorkspaceYamlIndent = DEFAULT_INDENT;
403
+ let pnpmWorkspaceYamlIndent = 2;
321
404
  let pnpmWorkspaceYamlObject = {};
322
405
  if (packageJsonExists) {
323
406
  const content = await fsReadFile(packageJsonPath);
@@ -329,7 +412,15 @@ async function migratePnpmSettings(rawOptions = {}) {
329
412
  pnpmWorkspaceYamlIndent = detectIndent(content).amount;
330
413
  pnpmWorkspaceYamlObject = parse(content);
331
414
  }
332
- const pnpmSettingsInNpmrc = npmrcExists ? pick(await readNpmrc(npmrcPath), PNPM_SETTINGS_FIELDS) : {};
415
+ const compatibility = resolveCompatibilityTarget(options.compatibility, packageJsonObject.packageManager);
416
+ const npmrcMigratable = npmrcExists ? compatibility === "v10" ? {
417
+ keys: PNPM_SETTINGS_FIELDS,
418
+ settings: pick(await readNpmrc(npmrcPath), PNPM_SETTINGS_FIELDS)
419
+ } : await readMigratableNpmrc(npmrcPath, compatibility) : {
420
+ keys: [],
421
+ settings: {}
422
+ };
423
+ const pnpmSettingsInNpmrc = npmrcMigratable.settings;
333
424
  const hasPnpmInPackageJson = !!packageJsonObject.pnpm;
334
425
  const hasResolutions = options.yarnResolutions && !!packageJsonObject.resolutions;
335
426
  const hasNpmrcSettings = Object.keys(pnpmSettingsInNpmrc).length > 0;
@@ -346,6 +437,7 @@ async function migratePnpmSettings(rawOptions = {}) {
346
437
  ...pnpmSettingsInNpmrc,
347
438
  ...pnpmSettingsInPackageJson
348
439
  };
440
+ normalizeIncomingSettings(incomingSettings, compatibility);
349
441
  const pnpmWorkspaceResult = mergeByStrategy(pnpmWorkspaceYamlObject, incomingSettings, options.strategy);
350
442
  const yamlDocument = new Document({}, { sortMapEntries: options.sortKeys });
351
443
  Object.entries(pnpmWorkspaceResult).forEach(([key, value]) => {
@@ -356,7 +448,7 @@ async function migratePnpmSettings(rawOptions = {}) {
356
448
  });
357
449
  const yamlContent = yamlDocument.toString({ indent: pnpmWorkspaceYamlIndent });
358
450
  await fsWriteFile(pnpmWorkspaceYamlPath, options.newlineBetween ? yamlContent.replace(/\n(?=[^\s#][^:\n]*:)/g, "\n\n") : yamlContent);
359
- if (npmrcExists && options.cleanNpmrc) await pruneNpmrc(npmrcPath);
451
+ if (npmrcExists && options.cleanNpmrc) await pruneNpmrc(npmrcPath, compatibility, npmrcMigratable.keys);
360
452
  if (packageJsonExists && options.cleanPackageJson && (packageJsonObject.pnpm || packageJsonObject.resolutions)) {
361
453
  delete packageJsonObject.pnpm;
362
454
  if (options.yarnResolutions) delete packageJsonObject.resolutions;
@@ -367,6 +459,36 @@ async function migratePnpmSettings(rawOptions = {}) {
367
459
  throw err;
368
460
  }
369
461
  }
370
-
462
+ /**
463
+ * Build v11 `allowBuilds` map from legacy build-script settings.
464
+ */
465
+ function collectAllowBuildsFromLegacy(incomingSettings) {
466
+ const allowBuilds = {};
467
+ for (const name of incomingSettings.onlyBuiltDependencies || []) allowBuilds[name] = true;
468
+ for (const name of incomingSettings.ignoredBuiltDependencies || []) allowBuilds[name] = false;
469
+ for (const name of incomingSettings.neverBuiltDependencies || []) allowBuilds[name] = false;
470
+ return Object.keys(allowBuilds).length ? allowBuilds : void 0;
471
+ }
472
+ /**
473
+ * Normalize incoming settings according to compatibility target.
474
+ */
475
+ function normalizeIncomingSettings(incomingSettings, compatibility) {
476
+ if (compatibility === "v10") return;
477
+ if (incomingSettings.allowNonAppliedPatches !== void 0) incomingSettings.allowUnusedPatches = incomingSettings.allowUnusedPatches ?? incomingSettings.allowNonAppliedPatches;
478
+ const allowBuildsFromLegacy = collectAllowBuildsFromLegacy(incomingSettings);
479
+ if (allowBuildsFromLegacy) incomingSettings.allowBuilds = {
480
+ ...allowBuildsFromLegacy,
481
+ ...incomingSettings.allowBuilds || {}
482
+ };
483
+ for (const key of PNPM_V11_REMOVED_SETTINGS) delete incomingSettings[key];
484
+ }
485
+ /**
486
+ * Resolve final compatibility target from user option and package manager hint.
487
+ */
488
+ function resolveCompatibilityTarget(compatibility, packageManager) {
489
+ if (compatibility !== "auto") return compatibility;
490
+ const [, major] = (packageManager || "").match(/^pnpm@(\d+)(?:\.|$)/) || [];
491
+ return Number(major) >= 11 ? "v11" : "v10";
492
+ }
371
493
  //#endregion
372
- export { migratePnpmSettings, resolveOptions };
494
+ 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.2.0",
4
+ "version": "0.4.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,31 +36,31 @@
37
36
  },
38
37
  "sideEffects": false,
39
38
  "dependencies": {
40
- "@ntnyq/utils": "^0.11.0",
41
- "@pnpm/types": "^1001.3.0",
39
+ "@ntnyq/utils": "^0.13.2",
40
+ "@pnpm/types": "^1101.0.0",
42
41
  "cac": "^7.0.0",
43
42
  "camelcase-keys": "^10.0.2",
44
43
  "consola": "^3.4.2",
45
- "defu": "^6.1.4",
44
+ "defu": "^6.1.7",
46
45
  "detect-indent": "^7.0.2",
47
46
  "pathe": "^2.0.3",
48
- "read-ini-file": "^4.0.0",
47
+ "read-ini-file": "^5.0.0",
49
48
  "uncase": "^0.2.0",
50
- "yaml": "^2.8.2"
49
+ "yaml": "^2.8.4"
51
50
  },
52
51
  "devDependencies": {
53
- "@ntnyq/eslint-config": "^6.0.0-beta.11",
54
- "@types/node": "^25.3.2",
55
- "@typescript/native-preview": "^7.0.0-dev.20260227.1",
56
- "bumpp": "^10.4.1",
57
- "eslint": "^10.0.2",
52
+ "@ntnyq/eslint-config": "^6.1.3",
53
+ "@types/node": "^25.6.2",
54
+ "@typescript/native-preview": "^7.0.0-dev.20260510.1",
55
+ "bumpp": "^11.1.0",
56
+ "eslint": "^10.3.0",
58
57
  "husky": "^9.1.7",
59
- "nano-staged": "^0.9.0",
58
+ "nano-staged": "^1.0.2",
60
59
  "npm-run-all2": "^8.0.4",
61
- "oxfmt": "^0.35.0",
62
- "tsdown": "^0.21.0-beta.2",
63
- "typescript": "^5.9.3",
64
- "vitest": "^4.0.18"
60
+ "oxfmt": "^0.48.0",
61
+ "tsdown": "^0.22.0",
62
+ "typescript": "^6.0.3",
63
+ "vitest": "^4.1.5"
65
64
  },
66
65
  "nano-staged": {
67
66
  "*.{js,ts,mjs,tsx,md,yml,yaml,toml,json}": "eslint --fix",
@@ -78,5 +77,8 @@
78
77
  "release:version": "bumpp",
79
78
  "test": "vitest",
80
79
  "typecheck": "tsgo --noEmit"
80
+ },
81
+ "bin": {
82
+ "pnpm-settings-migrator": "./bin.mjs"
81
83
  }
82
84
  }