pnpm-settings-migrator 0.11.0 → 0.12.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
@@ -132,6 +132,24 @@ For pnpm 12.4.0 and later stable v12 releases, the migrator additionally accepts
132
132
  `cargoTargetDir`. On older or unconfirmed targets, a `tasks` object containing
133
133
  these fields stays in its source as a whole.
134
134
 
135
+ For pnpm 12.5.0 and later stable v12 releases, the migrator also accepts
136
+ `concurrencyGroups`, task `concurrencyGroup`, platform lists in
137
+ `supportedArchitectures`, Python `versions` / `overrides` / `constraints`, and
138
+ `registries` declarations with `ecosystem`. Python registry `packages` routes
139
+ require 12.5.1. Unsupported nested settings stay in their source as a whole.
140
+ The legacy OS/CPU/libc architecture mapping continues to work.
141
+
142
+ pnpm 12.5 removes `python.indexUrl` and `cargo.indexUrl`. Settings containing
143
+ these fields are retained with an incompatibility warning; existing workspaces
144
+ containing them fail validation before any files are written. Replace them with
145
+ URL-keyed `registries` entries using `ecosystem: pypi` or `ecosystem: cargo`.
146
+ The registry prefix `pkg` is reserved from 12.5 onward. Tool download mirrors
147
+ under `tools` belong in global `config.yaml` or `PNPM_CONFIG_TOOLS` and are
148
+ never migrated into the project workspace.
149
+
150
+ On the v11 line, `trustPolicyExcludePrune` requires a confirmed stable version
151
+ from 11.27.0 onward; v12 support still starts at 12.4.0.
152
+
135
153
  Automated v10 to v11 conversions include:
136
154
 
137
155
  - `managePackageManagerVersions`, `packageManagerStrict`, and
@@ -151,6 +169,9 @@ Notes:
151
169
  - `.npmrc` migration is aligned with the target pnpm workspace schema. Unknown
152
170
  keys and settings supported only by a different pnpm version stay in `.npmrc`
153
171
  with a warning.
172
+ - Scalar list settings such as `public-hoist-pattern=*eslint*` become YAML
173
+ arrays (`publicHoistPattern: ['*eslint*']`). Existing arrays and patterns
174
+ containing commas are preserved without splitting their values.
154
175
  - In `v11` and `v12`, auth/registry keys and project-refused machine settings
155
176
  such as `globalDir`, `stateDir`, `configDir`, and `scope` stay in `.npmrc`.
156
177
  - Registry declarations containing embedded credentials or dynamic `${...}`
@@ -177,6 +198,8 @@ Notes:
177
198
  resolutions remain in their original source.
178
199
  Unrecognized, refused, incompatible, or otherwise unsupported
179
200
  `package.json#pnpm` child keys remain in `package.json`.
201
+ INI sections in `.npmrc` are retained conservatively; their child names are
202
+ never treated as root keys during cleanup.
180
203
  - Runtime migration writes `devEngines.runtime` before removing a workspace
181
204
  runtime declaration. A failed write preserves source settings for retry.
182
205
  If an existing runtime declaration conflicts with a workspace runtime, migration
@@ -191,9 +214,10 @@ Notes:
191
214
  - The migrator does not update the `packageManager` version, CI environment variables,
192
215
  shell setup, or pnpm commands in scripts. When `packageManager` still pins pnpm 10,
193
216
  pass `--compatibility v11` explicitly and update the pin separately.
194
- - Compatibility checks cover pnpm 11.26.0, 12.3.4, and 12.4.0, retaining
195
- 11.25.0 and 12.2.1 regressions. As audited on 2026-09-10, 12.3.4 is npm
196
- `latest` and 12.4.0 is on `next-12`; the project stays pinned to 12.3.4. Its removed `pnpm install --resolution-only` CLI
217
+ - Compatibility checks cover pnpm 11.27.1 and 12.5.1, retaining 11.25.0,
218
+ 11.26.0, 12.2.1, 12.3.4, 12.4.0, 12.4.2, and 12.5.0 regressions.
219
+ As audited on 2026-09-22, npm `latest` is 12.5.1 and `latest-11` is 11.27.1;
220
+ the project is pinned to 12.5.1. Its removed `pnpm install --resolution-only` CLI
197
221
  flag is outside this settings migrator's scope; replace it with
198
222
  `pnpm peers check` in scripts before upgrading.
199
223
 
package/dist/cli.mjs CHANGED
@@ -110,6 +110,30 @@ const NPMRC_AUTH_OR_REGISTRY_KEYS = [
110
110
  * Pattern for pnpm's channel-specific Node.js mirror keys.
111
111
  */
112
112
  const NODE_MIRROR_KEY_PATTERN = /^node-mirror:(?<channel>.+)$/iu;
113
+ /**
114
+ * List settings accepting a single string in legacy `.npmrc` files.
115
+ * Workspace YAML requires these values to be arrays.
116
+ * Legacy build permission lists keep their explicit array validation.
117
+ */
118
+ const NPMRC_STRING_ARRAY_SETTINGS = [
119
+ "changedFilesIgnorePattern",
120
+ "externalDependencies",
121
+ "extraBinPaths",
122
+ "filter",
123
+ "filterProd",
124
+ "gitShallowHosts",
125
+ "hoistPattern",
126
+ "ignoredOptionalDependencies",
127
+ "mergeGitBranchLockfilesBranchPattern",
128
+ "minimumReleaseAgeExclude",
129
+ "packages",
130
+ "publicHoistPattern",
131
+ "requiredScripts",
132
+ "syncInjectedDepsAfterScripts",
133
+ "testPattern",
134
+ "trustPolicyExclude",
135
+ "workspacePackagePatterns"
136
+ ];
113
137
  //#endregion
114
138
  //#region src/constants/pnpm-v10.ts
115
139
  /**
@@ -298,6 +322,7 @@ const PNPM_PROJECT_REFUSED_SETTINGS = [
298
322
  "scope",
299
323
  "selectedProjectsGraph",
300
324
  "stateDir",
325
+ "tools",
301
326
  "userConfig",
302
327
  "userconfig",
303
328
  "wantedPackageManager",
@@ -586,32 +611,64 @@ const PNPM_WORKSPACE_YAML = "pnpm-workspace.yaml";
586
611
  //#endregion
587
612
  //#region src/constants/pnpm-capabilities.ts
588
613
  /**
614
+ * Release introducing platform lists, registry ecosystems, and Python options.
615
+ */
616
+ const PNPM_V12_5_MINIMUM_VERSION = [
617
+ 12,
618
+ 5,
619
+ 0
620
+ ];
621
+ /**
622
+ * Release introducing package routes in Python registry declarations.
623
+ */
624
+ const PNPM_V12_5_1_MINIMUM_VERSION = [
625
+ 12,
626
+ 5,
627
+ 1
628
+ ];
629
+ /**
589
630
  * Version-gated settings. Add a release entry only when supported fields change.
590
631
  *
591
632
  * @see https://github.com/pnpm/pnpm/blob/v12.4.0/pnpm/crates/config/src/workspace_yaml.rs
592
633
  */
593
- const PNPM_SETTINGS_CAPABILITIES = [{
594
- minimumVersion: [
595
- 12,
596
- 4,
597
- 0
598
- ],
599
- workspaceFields: [
600
- "cargo",
601
- "packageConfigs",
602
- "pipelineBase",
603
- "pipelines",
604
- "python",
605
- "trustPolicyExcludePrune"
606
- ],
607
- taskFields: [
608
- "cache",
609
- "cargoTargetDir",
610
- "env",
611
- "inputs",
612
- "outputs"
613
- ]
614
- }];
634
+ const PNPM_SETTINGS_CAPABILITIES = [
635
+ {
636
+ minimumVersion: [
637
+ 11,
638
+ 27,
639
+ 0
640
+ ],
641
+ workspaceFields: ["trustPolicyExcludePrune"],
642
+ taskFields: []
643
+ },
644
+ {
645
+ minimumVersion: [
646
+ 12,
647
+ 4,
648
+ 0
649
+ ],
650
+ workspaceFields: [
651
+ "cargo",
652
+ "packageConfigs",
653
+ "pipelineBase",
654
+ "pipelines",
655
+ "python",
656
+ "trustPolicyExcludePrune"
657
+ ],
658
+ taskFields: [
659
+ "cache",
660
+ "cargoTargetDir",
661
+ "env",
662
+ "inputs",
663
+ "outputs"
664
+ ]
665
+ },
666
+ {
667
+ minimumVersion: PNPM_V12_5_MINIMUM_VERSION,
668
+ workspaceFields: ["concurrencyGroups"],
669
+ taskFields: ["concurrencyGroup"]
670
+ }
671
+ ];
615
672
  /**
616
673
  * Known workspace fields that require a confirmed version.
617
674
  */
@@ -761,8 +818,13 @@ function resolveOptions(options = {}) {
761
818
  function discardMerge(priority, fallback) {
762
819
  const result = { ...priority };
763
820
  for (const [key, fallbackValue] of Object.entries(fallback)) {
764
- const priorityValue = result[key];
765
- if (isUndefined(priorityValue)) result[key] = fallbackValue;
821
+ const priorityValue = Object.hasOwn(result, key) ? result[key] : void 0;
822
+ if (isUndefined(priorityValue)) Object.defineProperty(result, key, {
823
+ configurable: true,
824
+ enumerable: true,
825
+ value: fallbackValue,
826
+ writable: true
827
+ });
766
828
  else if (isPlainObject(priorityValue) && isPlainObject(fallbackValue)) result[key] = discardMerge(priorityValue, fallbackValue);
767
829
  }
768
830
  return result;
@@ -782,8 +844,13 @@ function discardMerge(priority, fallback) {
782
844
  function mergeWithArrayDedupe(existing, incoming) {
783
845
  const result = { ...existing };
784
846
  for (const [key, incomingValue] of Object.entries(incoming)) {
785
- const existingValue = result[key];
786
- if (isUndefined(existingValue)) result[key] = incomingValue;
847
+ const existingValue = Object.hasOwn(result, key) ? result[key] : void 0;
848
+ if (isUndefined(existingValue)) Object.defineProperty(result, key, {
849
+ configurable: true,
850
+ enumerable: true,
851
+ value: incomingValue,
852
+ writable: true
853
+ });
787
854
  else if (Array.isArray(existingValue) && Array.isArray(incomingValue)) {
788
855
  const items = Array.from(/* @__PURE__ */ new Set([...existingValue, ...incomingValue]));
789
856
  result[key] = items.filter((item, index) => !item || typeof item !== "object" || !items.slice(0, index).some((previous) => isDeepStrictEqual(previous, item)));
@@ -863,6 +930,41 @@ function formatSettingsIssues(options) {
863
930
  return warnings;
864
931
  }
865
932
  //#endregion
933
+ //#region src/features/settings/versioned-values.ts
934
+ /**
935
+ * Check value shapes that changed within a pnpm major release.
936
+ *
937
+ * Reject the whole setting so cleanup cannot discard unsupported nested values.
938
+ *
939
+ * @param key - Canonical workspace setting name
940
+ * @param value - Incoming or existing setting value
941
+ * @param target - Confirmed target version and major schema
942
+ *
943
+ * @returns Whether the value uses a shape unsupported by the selected release
944
+ */
945
+ function hasIncompatibleSettingValue(key, value, target) {
946
+ const supportsV12_5 = supportsMinimumVersion(target.version, PNPM_V12_5_MINIMUM_VERSION);
947
+ if (key === "supportedArchitectures" && Array.isArray(value)) return !supportsV12_5;
948
+ if (!value || typeof value !== "object") return false;
949
+ if (key === "python" || key === "cargo") {
950
+ if (supportsV12_5 && Object.hasOwn(value, "indexUrl")) return true;
951
+ if (key === "python" && !supportsV12_5) return [
952
+ "versions",
953
+ "overrides",
954
+ "constraints"
955
+ ].some((field) => Object.hasOwn(value, field));
956
+ }
957
+ if (key === "namedRegistries" && supportsV12_5) return Object.keys(value).some((prefix) => prefix.toLowerCase() === "pkg");
958
+ if (key === "registries") return Object.values(value).some((entry) => {
959
+ if (!entry || typeof entry !== "object") return false;
960
+ if (Object.hasOwn(entry, "ecosystem") && !supportsV12_5) return true;
961
+ if (Object.hasOwn(entry, "packages") && !supportsMinimumVersion(target.version, PNPM_V12_5_1_MINIMUM_VERSION)) return true;
962
+ const prefix = Reflect.get(entry, "prefix");
963
+ return supportsV12_5 && typeof prefix === "string" && prefix.toLowerCase() === "pkg";
964
+ });
965
+ return false;
966
+ }
967
+ //#endregion
866
968
  //#region src/features/settings/schema.ts
867
969
  /**
868
970
  * Create an empty collection of schema selection issues.
@@ -969,6 +1071,7 @@ function resolveSettingIssue({ allowedFields, target, key, npmrc, value }) {
969
1071
  if (compatibility !== "v10" && PROJECT_REFUSED_SETTINGS.has(key)) return "refused";
970
1072
  if (REGISTRY_SETTINGS.has(key) && containsUnsafeRegistryValue(value)) return "unsafe";
971
1073
  if (PROXY_SETTINGS.has(key) && typeof value === "string" && value.includes("${")) return "unsafe";
1074
+ if (hasIncompatibleSettingValue(key, value, target)) return "incompatible";
972
1075
  if (key === "tasks" && value && typeof value === "object" && Object.values(value).some((task) => task && typeof task === "object" && Object.keys(task).some((field) => !taskSettings.has(field)))) return "incompatible";
973
1076
  if (!workspaceSettings.has(key)) return isSettingFromAnotherVersion(key, compatibility) ? "incompatible" : "unknown";
974
1077
  if (allowedFields && !allowedFields.has(key)) return "unsupported";
@@ -1351,6 +1454,17 @@ function normalizeNpmrcKey(key) {
1351
1454
  return key.trim().replace(/\[\]$/u, "").toLowerCase();
1352
1455
  }
1353
1456
  /**
1457
+ * Convert scalar list settings to the array representation required by YAML.
1458
+ *
1459
+ * @param settings - Selected `.npmrc` settings with camelCase keys
1460
+ *
1461
+ * @returns Settings with each scalar list value preserved as one array item
1462
+ */
1463
+ function normalizeNpmrcSettings(settings) {
1464
+ for (const key of NPMRC_STRING_ARRAY_SETTINGS) if (typeof settings[key] === "string") settings[key] = [settings[key]];
1465
+ return settings;
1466
+ }
1467
+ /**
1354
1468
  * Extract key name from a raw `.npmrc` line.
1355
1469
  *
1356
1470
  * @param line - Raw line from an `.npmrc` file
@@ -1382,7 +1496,8 @@ function isNpmrcAuthOrRegistryKey(key) {
1382
1496
  * Remove pnpm-related settings from `.npmrc` file.
1383
1497
  *
1384
1498
  * This function reads the `.npmrc` file, removes lines whose exact normalized
1385
- * keys were migrated, and writes the cleaned content back to the file.
1499
+ * root keys were migrated, and writes the cleaned content back to the file.
1500
+ * INI sections are retained because their child keys are not root settings.
1386
1501
  *
1387
1502
  * @param path - Absolute path to the `.npmrc` file
1388
1503
  * @param compatibility - Concrete pnpm compatibility target
@@ -1399,7 +1514,11 @@ function isNpmrcAuthOrRegistryKey(key) {
1399
1514
  */
1400
1515
  async function pruneNpmrc(path, compatibility, migratedKeys) {
1401
1516
  const migratedKeySet = new Set(migratedKeys.map(normalizeNpmrcKey));
1402
- const updatedContent = (await fsReadFile(path)).split(/\r?\n/u).filter((line) => {
1517
+ const content = await fsReadFile(path);
1518
+ let inSection = false;
1519
+ const updatedContent = content.split(/\r?\n/u).filter((line) => {
1520
+ if (/^\[[^\]]*\]\s*$/u.test(line.replace(/^\uFEFF/u, ""))) inSection = true;
1521
+ if (inSection) return true;
1403
1522
  const key = getNpmrcLineKey(line);
1404
1523
  if (!key) return true;
1405
1524
  return !migratedKeySet.has(key);
@@ -1443,7 +1562,7 @@ async function readMigratableNpmrc(path, target, options = {}) {
1443
1562
  return {
1444
1563
  issues,
1445
1564
  keys,
1446
- settings: camelcaseKeys(migratable)
1565
+ settings: normalizeNpmrcSettings(camelcaseKeys(migratable))
1447
1566
  };
1448
1567
  }
1449
1568
  const migratable = {};
@@ -1465,7 +1584,7 @@ async function readMigratableNpmrc(path, target, options = {}) {
1465
1584
  mergeSettingsIssues(issues, selected.issues);
1466
1585
  }
1467
1586
  }
1468
- const settings = camelcaseKeys(migratable);
1587
+ const settings = normalizeNpmrcSettings(camelcaseKeys(migratable));
1469
1588
  if (Object.keys(nodeDownloadMirrors).length) {
1470
1589
  const selected = selectPnpmSettings({ nodeDownloadMirrors }, target, { allowedFields: options.allowedFields });
1471
1590
  if (selected.keys.length) {
@@ -2554,7 +2673,7 @@ const bold = getColor("bold");
2554
2673
  //#endregion
2555
2674
  //#region package.json
2556
2675
  var name = "pnpm-settings-migrator";
2557
- var version = "0.11.0";
2676
+ var version = "0.12.0";
2558
2677
  //#endregion
2559
2678
  //#region src/cli/options.ts
2560
2679
  /**
package/dist/index.d.mts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { Document } from "yaml";
2
- import { PnpmSettings } from "@pnpm/types";
2
+ import { PnpmSettings, RegistryDeclaration } from "@pnpm/types";
3
3
  //#region src/types/options.d.ts
4
4
  /**
5
5
  * Compatibility target used by the migrator.
@@ -93,15 +93,19 @@ type ResolvedOptions = Required<Omit<Options, 'targetVersion'>> & Pick<Options,
93
93
  */
94
94
  type GlobalShimPolicy = boolean | 'always' | 'auto' | 'prompt';
95
95
  /**
96
- * Pipeline task configuration supported by pnpm 12.4.0.
96
+ * Task configuration, including pnpm 12.4 caching and 12.5 concurrency groups.
97
97
  *
98
- * @see https://github.com/pnpm/pnpm/blob/v12.4.0/pnpm/crates/config/src/workspace_yaml.rs
98
+ * @see https://github.com/pnpm/pnpm/blob/v12.5.1/pnpm/crates/config/src/workspace_yaml/sections.rs
99
99
  */
100
100
  interface PnpmTaskSettings {
101
101
  /**
102
102
  * Maximum concurrent executions of this task.
103
103
  */
104
104
  concurrency?: number;
105
+ /**
106
+ * Machine-wide concurrency group to count this task against (pnpm 12.5+).
107
+ */
108
+ concurrencyGroup?: string;
105
109
  /**
106
110
  * Prerequisite tasks; a caret prefix refers to workspace dependencies.
107
111
  */
@@ -140,7 +144,8 @@ interface PnpmPythonSettings {
140
144
  */
141
145
  executable?: string;
142
146
  /**
143
- * Python package index URL.
147
+ * Python package index URL accepted by pnpm 12.4.
148
+ * @deprecated pnpm 12.5+ uses a registries entry with ecosystem: pypi.
144
149
  */
145
150
  indexUrl?: string;
146
151
  /**
@@ -151,6 +156,18 @@ interface PnpmPythonSettings {
151
156
  * Python dependency groups to install.
152
157
  */
153
158
  groups?: string[];
159
+ /**
160
+ * Python versions to resolve the lockfile for (pnpm 12.5+).
161
+ */
162
+ versions?: string[];
163
+ /**
164
+ * Python requirements that override dependency versions (pnpm 12.5+).
165
+ */
166
+ overrides?: string[];
167
+ /**
168
+ * Additional constraints on Python dependency versions (pnpm 12.5+).
169
+ */
170
+ constraints?: string[];
154
171
  }
155
172
  /**
156
173
  * Cargo dependency management configuration introduced in pnpm 12.4.0.
@@ -161,14 +178,41 @@ interface PnpmCargoSettings {
161
178
  */
162
179
  enabled?: boolean;
163
180
  /**
164
- * Cargo registry index URL.
181
+ * Cargo registry index URL accepted by pnpm 12.4.
182
+ * @deprecated pnpm 12.5+ uses a registries entry with ecosystem: cargo.
165
183
  */
166
184
  indexUrl?: string;
167
185
  }
186
+ /**
187
+ * URL-keyed registry declaration with pnpm 12.5 ecosystem support.
188
+ */
189
+ interface PnpmRegistryDeclaration extends RegistryDeclaration {
190
+ /**
191
+ * Package ecosystem served by the registry; omitted means npm (pnpm 12.5+).
192
+ */
193
+ ecosystem?: 'cargo' | 'npm' | 'pypi';
194
+ /**
195
+ * Python package names or trailing-prefix patterns; * selects the default
196
+ * index (pnpm 12.5.1+).
197
+ */
198
+ packages?: string[];
199
+ }
168
200
  /**
169
201
  * Settings supported by pnpm v12, subject to the resolved version capabilities.
170
202
  */
171
203
  interface PnpmSettingsV12 {
204
+ /**
205
+ * Machine-wide limits shared by tasks in each group (pnpm 12.5+).
206
+ */
207
+ concurrencyGroups?: Record<string, number>;
208
+ /**
209
+ * Legacy OS/CPU/libc axes, or platform names and Rust triples (pnpm 12.5+).
210
+ */
211
+ supportedArchitectures?: PnpmSettings['supportedArchitectures'] | string[];
212
+ /**
213
+ * Legacy scope routes or URL-keyed declarations with ecosystem settings.
214
+ */
215
+ registries?: Record<string, PnpmRegistryDeclaration | string>;
172
216
  /**
173
217
  * Peer auto-installation policy accepted only by the v12 target.
174
218
  */
@@ -360,7 +404,7 @@ interface PnpmSettingsV11 {
360
404
  /**
361
405
  * `pnpm-workspace.yaml` types.
362
406
  */
363
- type PnpmWorkspace = Omit<PnpmSettings, 'tasks'> & PnpmSettingsDeprecated & PnpmSettingsV11 & PnpmSettingsV12 & PnpmWorkspaceLegacy;
407
+ type PnpmWorkspace = Omit<PnpmSettings, 'registries' | 'supportedArchitectures' | 'tasks'> & PnpmSettingsDeprecated & PnpmSettingsV11 & PnpmSettingsV12 & PnpmWorkspaceLegacy;
364
408
  //#endregion
365
409
  //#region src/types/package-json.d.ts
366
410
  /**
@@ -568,4 +612,4 @@ export declare function migratePnpmSettings(rawOptions?: Options): Promise<Migra
568
612
  */
569
613
  export declare function resolveOptions(options?: Options): ResolvedOptions;
570
614
  //#endregion
571
- export type { CompatibilityTarget, GlobalShimPolicy, MergeStrategy, MigrationResult, NpmRC, Options, PackageJson, PackageManagerEngine, PnpmCargoSettings, PnpmPythonSettings, PnpmSettingsDeprecated, PnpmSettingsV11, PnpmSettingsV12, PnpmTaskSettings, PnpmWorkspace, PnpmWorkspaceLegacy, ResolvedOptions, RuntimeEngine, SettingsChange };
615
+ export type { CompatibilityTarget, GlobalShimPolicy, MergeStrategy, MigrationResult, NpmRC, Options, PackageJson, PackageManagerEngine, PnpmCargoSettings, PnpmPythonSettings, PnpmRegistryDeclaration, PnpmSettingsDeprecated, PnpmSettingsV11, PnpmSettingsV12, PnpmTaskSettings, PnpmWorkspace, PnpmWorkspaceLegacy, ResolvedOptions, RuntimeEngine, SettingsChange };
package/dist/index.mjs CHANGED
@@ -108,6 +108,30 @@ const NPMRC_AUTH_OR_REGISTRY_KEYS = [
108
108
  * Pattern for pnpm's channel-specific Node.js mirror keys.
109
109
  */
110
110
  const NODE_MIRROR_KEY_PATTERN = /^node-mirror:(?<channel>.+)$/iu;
111
+ /**
112
+ * List settings accepting a single string in legacy `.npmrc` files.
113
+ * Workspace YAML requires these values to be arrays.
114
+ * Legacy build permission lists keep their explicit array validation.
115
+ */
116
+ const NPMRC_STRING_ARRAY_SETTINGS = [
117
+ "changedFilesIgnorePattern",
118
+ "externalDependencies",
119
+ "extraBinPaths",
120
+ "filter",
121
+ "filterProd",
122
+ "gitShallowHosts",
123
+ "hoistPattern",
124
+ "ignoredOptionalDependencies",
125
+ "mergeGitBranchLockfilesBranchPattern",
126
+ "minimumReleaseAgeExclude",
127
+ "packages",
128
+ "publicHoistPattern",
129
+ "requiredScripts",
130
+ "syncInjectedDepsAfterScripts",
131
+ "testPattern",
132
+ "trustPolicyExclude",
133
+ "workspacePackagePatterns"
134
+ ];
111
135
  //#endregion
112
136
  //#region src/constants/pnpm-v10.ts
113
137
  /**
@@ -296,6 +320,7 @@ const PNPM_PROJECT_REFUSED_SETTINGS = [
296
320
  "scope",
297
321
  "selectedProjectsGraph",
298
322
  "stateDir",
323
+ "tools",
299
324
  "userConfig",
300
325
  "userconfig",
301
326
  "wantedPackageManager",
@@ -579,32 +604,64 @@ const PNPM_WORKSPACE_YAML = "pnpm-workspace.yaml";
579
604
  //#endregion
580
605
  //#region src/constants/pnpm-capabilities.ts
581
606
  /**
607
+ * Release introducing platform lists, registry ecosystems, and Python options.
608
+ */
609
+ const PNPM_V12_5_MINIMUM_VERSION = [
610
+ 12,
611
+ 5,
612
+ 0
613
+ ];
614
+ /**
615
+ * Release introducing package routes in Python registry declarations.
616
+ */
617
+ const PNPM_V12_5_1_MINIMUM_VERSION = [
618
+ 12,
619
+ 5,
620
+ 1
621
+ ];
622
+ /**
582
623
  * Version-gated settings. Add a release entry only when supported fields change.
583
624
  *
584
625
  * @see https://github.com/pnpm/pnpm/blob/v12.4.0/pnpm/crates/config/src/workspace_yaml.rs
585
626
  */
586
- const PNPM_SETTINGS_CAPABILITIES = [{
587
- minimumVersion: [
588
- 12,
589
- 4,
590
- 0
591
- ],
592
- workspaceFields: [
593
- "cargo",
594
- "packageConfigs",
595
- "pipelineBase",
596
- "pipelines",
597
- "python",
598
- "trustPolicyExcludePrune"
599
- ],
600
- taskFields: [
601
- "cache",
602
- "cargoTargetDir",
603
- "env",
604
- "inputs",
605
- "outputs"
606
- ]
607
- }];
627
+ const PNPM_SETTINGS_CAPABILITIES = [
628
+ {
629
+ minimumVersion: [
630
+ 11,
631
+ 27,
632
+ 0
633
+ ],
634
+ workspaceFields: ["trustPolicyExcludePrune"],
635
+ taskFields: []
636
+ },
637
+ {
638
+ minimumVersion: [
639
+ 12,
640
+ 4,
641
+ 0
642
+ ],
643
+ workspaceFields: [
644
+ "cargo",
645
+ "packageConfigs",
646
+ "pipelineBase",
647
+ "pipelines",
648
+ "python",
649
+ "trustPolicyExcludePrune"
650
+ ],
651
+ taskFields: [
652
+ "cache",
653
+ "cargoTargetDir",
654
+ "env",
655
+ "inputs",
656
+ "outputs"
657
+ ]
658
+ },
659
+ {
660
+ minimumVersion: PNPM_V12_5_MINIMUM_VERSION,
661
+ workspaceFields: ["concurrencyGroups"],
662
+ taskFields: ["concurrencyGroup"]
663
+ }
664
+ ];
608
665
  /**
609
666
  * Known workspace fields that require a confirmed version.
610
667
  */
@@ -754,8 +811,13 @@ function resolveOptions(options = {}) {
754
811
  function discardMerge(priority, fallback) {
755
812
  const result = { ...priority };
756
813
  for (const [key, fallbackValue] of Object.entries(fallback)) {
757
- const priorityValue = result[key];
758
- if (isUndefined(priorityValue)) result[key] = fallbackValue;
814
+ const priorityValue = Object.hasOwn(result, key) ? result[key] : void 0;
815
+ if (isUndefined(priorityValue)) Object.defineProperty(result, key, {
816
+ configurable: true,
817
+ enumerable: true,
818
+ value: fallbackValue,
819
+ writable: true
820
+ });
759
821
  else if (isPlainObject(priorityValue) && isPlainObject(fallbackValue)) result[key] = discardMerge(priorityValue, fallbackValue);
760
822
  }
761
823
  return result;
@@ -775,8 +837,13 @@ function discardMerge(priority, fallback) {
775
837
  function mergeWithArrayDedupe(existing, incoming) {
776
838
  const result = { ...existing };
777
839
  for (const [key, incomingValue] of Object.entries(incoming)) {
778
- const existingValue = result[key];
779
- if (isUndefined(existingValue)) result[key] = incomingValue;
840
+ const existingValue = Object.hasOwn(result, key) ? result[key] : void 0;
841
+ if (isUndefined(existingValue)) Object.defineProperty(result, key, {
842
+ configurable: true,
843
+ enumerable: true,
844
+ value: incomingValue,
845
+ writable: true
846
+ });
780
847
  else if (Array.isArray(existingValue) && Array.isArray(incomingValue)) {
781
848
  const items = Array.from(/* @__PURE__ */ new Set([...existingValue, ...incomingValue]));
782
849
  result[key] = items.filter((item, index) => !item || typeof item !== "object" || !items.slice(0, index).some((previous) => isDeepStrictEqual(previous, item)));
@@ -856,6 +923,41 @@ function formatSettingsIssues(options) {
856
923
  return warnings;
857
924
  }
858
925
  //#endregion
926
+ //#region src/features/settings/versioned-values.ts
927
+ /**
928
+ * Check value shapes that changed within a pnpm major release.
929
+ *
930
+ * Reject the whole setting so cleanup cannot discard unsupported nested values.
931
+ *
932
+ * @param key - Canonical workspace setting name
933
+ * @param value - Incoming or existing setting value
934
+ * @param target - Confirmed target version and major schema
935
+ *
936
+ * @returns Whether the value uses a shape unsupported by the selected release
937
+ */
938
+ function hasIncompatibleSettingValue(key, value, target) {
939
+ const supportsV12_5 = supportsMinimumVersion(target.version, PNPM_V12_5_MINIMUM_VERSION);
940
+ if (key === "supportedArchitectures" && Array.isArray(value)) return !supportsV12_5;
941
+ if (!value || typeof value !== "object") return false;
942
+ if (key === "python" || key === "cargo") {
943
+ if (supportsV12_5 && Object.hasOwn(value, "indexUrl")) return true;
944
+ if (key === "python" && !supportsV12_5) return [
945
+ "versions",
946
+ "overrides",
947
+ "constraints"
948
+ ].some((field) => Object.hasOwn(value, field));
949
+ }
950
+ if (key === "namedRegistries" && supportsV12_5) return Object.keys(value).some((prefix) => prefix.toLowerCase() === "pkg");
951
+ if (key === "registries") return Object.values(value).some((entry) => {
952
+ if (!entry || typeof entry !== "object") return false;
953
+ if (Object.hasOwn(entry, "ecosystem") && !supportsV12_5) return true;
954
+ if (Object.hasOwn(entry, "packages") && !supportsMinimumVersion(target.version, PNPM_V12_5_1_MINIMUM_VERSION)) return true;
955
+ const prefix = Reflect.get(entry, "prefix");
956
+ return supportsV12_5 && typeof prefix === "string" && prefix.toLowerCase() === "pkg";
957
+ });
958
+ return false;
959
+ }
960
+ //#endregion
859
961
  //#region src/features/settings/schema.ts
860
962
  /**
861
963
  * Create an empty collection of schema selection issues.
@@ -962,6 +1064,7 @@ function resolveSettingIssue({ allowedFields, target, key, npmrc, value }) {
962
1064
  if (compatibility !== "v10" && PROJECT_REFUSED_SETTINGS.has(key)) return "refused";
963
1065
  if (REGISTRY_SETTINGS.has(key) && containsUnsafeRegistryValue(value)) return "unsafe";
964
1066
  if (PROXY_SETTINGS.has(key) && typeof value === "string" && value.includes("${")) return "unsafe";
1067
+ if (hasIncompatibleSettingValue(key, value, target)) return "incompatible";
965
1068
  if (key === "tasks" && value && typeof value === "object" && Object.values(value).some((task) => task && typeof task === "object" && Object.keys(task).some((field) => !taskSettings.has(field)))) return "incompatible";
966
1069
  if (!workspaceSettings.has(key)) return isSettingFromAnotherVersion(key, compatibility) ? "incompatible" : "unknown";
967
1070
  if (allowedFields && !allowedFields.has(key)) return "unsupported";
@@ -1344,6 +1447,17 @@ function normalizeNpmrcKey(key) {
1344
1447
  return key.trim().replace(/\[\]$/u, "").toLowerCase();
1345
1448
  }
1346
1449
  /**
1450
+ * Convert scalar list settings to the array representation required by YAML.
1451
+ *
1452
+ * @param settings - Selected `.npmrc` settings with camelCase keys
1453
+ *
1454
+ * @returns Settings with each scalar list value preserved as one array item
1455
+ */
1456
+ function normalizeNpmrcSettings(settings) {
1457
+ for (const key of NPMRC_STRING_ARRAY_SETTINGS) if (typeof settings[key] === "string") settings[key] = [settings[key]];
1458
+ return settings;
1459
+ }
1460
+ /**
1347
1461
  * Extract key name from a raw `.npmrc` line.
1348
1462
  *
1349
1463
  * @param line - Raw line from an `.npmrc` file
@@ -1375,7 +1489,8 @@ function isNpmrcAuthOrRegistryKey(key) {
1375
1489
  * Remove pnpm-related settings from `.npmrc` file.
1376
1490
  *
1377
1491
  * This function reads the `.npmrc` file, removes lines whose exact normalized
1378
- * keys were migrated, and writes the cleaned content back to the file.
1492
+ * root keys were migrated, and writes the cleaned content back to the file.
1493
+ * INI sections are retained because their child keys are not root settings.
1379
1494
  *
1380
1495
  * @param path - Absolute path to the `.npmrc` file
1381
1496
  * @param compatibility - Concrete pnpm compatibility target
@@ -1392,7 +1507,11 @@ function isNpmrcAuthOrRegistryKey(key) {
1392
1507
  */
1393
1508
  async function pruneNpmrc(path, compatibility, migratedKeys) {
1394
1509
  const migratedKeySet = new Set(migratedKeys.map(normalizeNpmrcKey));
1395
- const updatedContent = (await fsReadFile(path)).split(/\r?\n/u).filter((line) => {
1510
+ const content = await fsReadFile(path);
1511
+ let inSection = false;
1512
+ const updatedContent = content.split(/\r?\n/u).filter((line) => {
1513
+ if (/^\[[^\]]*\]\s*$/u.test(line.replace(/^\uFEFF/u, ""))) inSection = true;
1514
+ if (inSection) return true;
1396
1515
  const key = getNpmrcLineKey(line);
1397
1516
  if (!key) return true;
1398
1517
  return !migratedKeySet.has(key);
@@ -1436,7 +1555,7 @@ async function readMigratableNpmrc(path, target, options = {}) {
1436
1555
  return {
1437
1556
  issues,
1438
1557
  keys,
1439
- settings: camelcaseKeys(migratable)
1558
+ settings: normalizeNpmrcSettings(camelcaseKeys(migratable))
1440
1559
  };
1441
1560
  }
1442
1561
  const migratable = {};
@@ -1458,7 +1577,7 @@ async function readMigratableNpmrc(path, target, options = {}) {
1458
1577
  mergeSettingsIssues(issues, selected.issues);
1459
1578
  }
1460
1579
  }
1461
- const settings = camelcaseKeys(migratable);
1580
+ const settings = normalizeNpmrcSettings(camelcaseKeys(migratable));
1462
1581
  if (Object.keys(nodeDownloadMirrors).length) {
1463
1582
  const selected = selectPnpmSettings({ nodeDownloadMirrors }, target, { allowedFields: options.allowedFields });
1464
1583
  if (selected.keys.length) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pnpm-settings-migrator",
3
- "version": "0.11.0",
3
+ "version": "0.12.0",
4
4
  "description": "Move pnpm settings from `pnpm` field in `package.json` and `.npmrc` file to `pnpm-workspace.yaml`",
5
5
  "keywords": [
6
6
  "migrator",
@@ -42,10 +42,10 @@
42
42
  "access": "public"
43
43
  },
44
44
  "dependencies": {
45
- "@ntnyq/utils": "^0.22.0",
45
+ "@ntnyq/utils": "^0.23.0",
46
46
  "@pnpm/types": "^1102.1.0",
47
47
  "cac": "^7.0.0",
48
- "camelcase-keys": "^10.0.2",
48
+ "camelcase-keys": "^10.0.3",
49
49
  "consola": "^3.4.2",
50
50
  "defu": "^6.1.7",
51
51
  "detect-indent": "^7.0.2",
@@ -56,16 +56,17 @@
56
56
  "yaml": "^2.9.1"
57
57
  },
58
58
  "devDependencies": {
59
- "@types/node": "^26.5.1",
59
+ "@types/node": "^26.6.2",
60
60
  "bumpp": "^12.3.0",
61
61
  "husky": "^9.1.7",
62
62
  "nano-staged": "^1.0.2",
63
63
  "npm-run-all2": "^9.0.3",
64
- "oxfmt": "^0.67.0",
65
- "oxlint": "^1.82.0",
64
+ "oxfmt": "^0.70.0",
65
+ "oxlint": "^1.85.0",
66
66
  "tsdown": "^0.23.0",
67
+ "tsx": "^4.23.15",
67
68
  "typescript": "^7.0.2",
68
- "vitest": "^5.0.0"
69
+ "vitest": "^5.0.1"
69
70
  },
70
71
  "nano-staged": {
71
72
  "*.{js,ts,mjs,tsx}": "oxlint --fix",
@@ -84,7 +85,7 @@
84
85
  "release:check": "run-s lint format:check typecheck test",
85
86
  "release:version": "bumpp",
86
87
  "test": "vitest",
87
- "test:compatibility": "node scripts/verify-pnpm-compatibility.mjs",
88
+ "test:compatibility": "tsx scripts/verify-pnpm-compatibility.ts",
88
89
  "typecheck": "tsc --noEmit"
89
90
  }
90
91
  }