pnpm-settings-migrator 0.1.0 → 0.3.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,34 +31,183 @@ Current working directory.
31
31
 
32
32
  Sort keys when write `pnpm-workspace.yaml`.
33
33
 
34
+ ### `--strategy`
35
+
36
+ - **Type**: `'discard' | 'merge' | 'overwrite'`
37
+ - **Default**: `'merge'`
38
+
39
+ Strategy to handle conflicts when merging settings with existing `pnpm-workspace.yaml`:
40
+
41
+ - `discard`: Keep existing values, only add new keys from incoming settings. For nested objects, merges keys from both.
42
+ - `merge`: Deep merge with array deduplication. Arrays are combined and deduplicated, objects are recursively merged, primitives keep existing values.
43
+ - `overwrite`: Use incoming values, only keep existing keys not present in incoming settings. For nested objects, merges keys from both.
44
+
34
45
  ### `--no-yarn-resolutions`
35
46
 
36
47
  - **Type**: `boolean`
37
- - **Default**: `false`
48
+ - **Default behavior**: `yarnResolutions=true` (use this flag to disable)
38
49
 
39
50
  Disable migrating `resolutions` field in `package.json`.
40
51
 
41
52
  ### `--no-clean-npmrc`
42
53
 
43
54
  - **Type**: `boolean`
44
- - **Default**: `false`
55
+ - **Default behavior**: `cleanNpmrc=true` (use this flag to disable)
45
56
 
46
57
  Disable removing pnpm settings in `.npmrc` file.
47
58
 
48
59
  ### `--no-clean-package-json`
49
60
 
50
61
  - **Type**: `boolean`
51
- - **Default**: `false`
62
+ - **Default behavior**: `cleanPackageJson=true` (use this flag to disable)
52
63
 
53
64
  Disable removing `pnpm` field in `package.json`.
54
65
 
55
66
  ### `--no-newline-between`
56
67
 
57
68
  - **Type**: `boolean`
58
- - **Default**: `false`
69
+ - **Default behavior**: `newlineBetween=true` (use this flag to disable)
59
70
 
60
71
  Disable adding newlines between each root keys.
61
72
 
73
+ ## Merge Strategy Examples
74
+
75
+ This document demonstrates how different merge strategies work when migrating pnpm settings.
76
+
77
+ ### Scenario
78
+
79
+ Existing `pnpm-workspace.yaml`:
80
+
81
+ ```yaml
82
+ packages:
83
+ - packages/*
84
+
85
+ overrides:
86
+ foo: 1.0.0
87
+ ```
88
+
89
+ Settings from `package.json`:
90
+
91
+ ```json
92
+ {
93
+ "pnpm": {
94
+ "packages": ["apps/*"],
95
+ "overrides": {
96
+ "bar": "2.0.0"
97
+ }
98
+ }
99
+ }
100
+ ```
101
+
102
+ ### Strategy: `discard` (Keep Existing)
103
+
104
+ ```bash
105
+ pnpm dlx pnpm-settings-migrator --strategy discard
106
+ ```
107
+
108
+ **Result:**
109
+
110
+ ```yaml
111
+ packages:
112
+ - packages/* # Kept existing array value
113
+
114
+ overrides:
115
+ foo: 1.0.0 # Kept existing key
116
+ bar: 2.0.0 # Added new key from package.json
117
+ ```
118
+
119
+ Use this when you want to preserve your existing configuration and only add new settings.
120
+
121
+ ### Strategy: `merge` (Smart Merge - Default)
122
+
123
+ ```bash
124
+ pnpm dlx pnpm-settings-migrator --strategy merge
125
+ ```
126
+
127
+ **Result:**
128
+
129
+ ```yaml
130
+ packages:
131
+ - packages/* # From existing
132
+ - apps/* # From package.json (deduplicated)
133
+
134
+ overrides:
135
+ foo: 1.0.0 # From existing
136
+ bar: 2.0.0 # From package.json
137
+ ```
138
+
139
+ Use this for intelligent merging that combines arrays and deeply merges objects.
140
+
141
+ ### Strategy: `overwrite` (Use Incoming)
142
+
143
+ ```bash
144
+ pnpm dlx pnpm-settings-migrator --strategy overwrite
145
+ ```
146
+
147
+ **Result:**
148
+
149
+ ```yaml
150
+ packages:
151
+ - apps/* # Replaced with incoming array value
152
+
153
+ overrides:
154
+ foo: 1.0.0 # Kept existing key (not in incoming)
155
+ bar: 2.0.0 # Added new key from package.json
156
+ ```
157
+
158
+ Use this when you want to prioritize settings from `package.json` and `.npmrc`.
159
+
160
+ ### Advanced Example
161
+
162
+ Existing `pnpm-workspace.yaml`:
163
+
164
+ ```yaml
165
+ packages:
166
+ - packages/*
167
+ - common
168
+
169
+ overrides:
170
+ react: 18.0.0
171
+
172
+ peerDependencyRules:
173
+ ignoreMissing:
174
+ - react-dom
175
+ ```
176
+
177
+ Settings from `package.json`:
178
+
179
+ ```json
180
+ {
181
+ "pnpm": {
182
+ "packages": ["apps/*", "common"],
183
+ "overrides": {
184
+ "vue": "3.0.0"
185
+ },
186
+ "peerDependencyRules": {
187
+ "ignoreMissing": ["vue-router"]
188
+ }
189
+ }
190
+ }
191
+ ```
192
+
193
+ #### With `--strategy merge`:
194
+
195
+ ```yaml
196
+ packages:
197
+ - packages/*
198
+ - common # Deduplicated
199
+ - apps/*
200
+
201
+ overrides:
202
+ react: 18.0.0
203
+ vue: 3.0.0
204
+
205
+ peerDependencyRules:
206
+ ignoreMissing:
207
+ - react-dom
208
+ - vue-router # Arrays merged and deduplicated
209
+ ```
210
+
62
211
  ## Context
63
212
 
64
213
  - [Moving settings to pnpm-workspace.yaml](https://github.com/orgs/pnpm/discussions/9037)
package/dist/cli.mjs CHANGED
@@ -1,7 +1,7 @@
1
1
  import process from "node:process";
2
2
  import { cac } from "cac";
3
3
  import consola, { consola as consola$1 } from "consola";
4
- import { pick } from "@ntnyq/utils";
4
+ import { isPlainObject, isUndefined, pick } from "@ntnyq/utils";
5
5
  import { defu } from "defu";
6
6
  import detectIndent from "detect-indent";
7
7
  import { resolve } from "pathe";
@@ -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.1.0";
18
-
16
+ var version = "0.3.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,26 @@ 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
-
54
53
  //#endregion
55
54
  //#region src/options.ts
56
55
  /**
@@ -65,6 +64,11 @@ const DEFAULT_OPTIONS = {
65
64
  strategy: "merge",
66
65
  yarnResolutions: true
67
66
  };
67
+ const VALID_STRATEGIES = [
68
+ "discard",
69
+ "merge",
70
+ "overwrite"
71
+ ];
68
72
  /**
69
73
  * Resolve and normalize migration options with defaults.
70
74
  *
@@ -92,12 +96,16 @@ function resolveOptions(options = {}) {
92
96
  cwd: options.cwd ?? DEFAULT_OPTIONS.cwd,
93
97
  newlineBetween: options.newlineBetween ?? DEFAULT_OPTIONS.newlineBetween,
94
98
  sortKeys: options.sortKeys ?? DEFAULT_OPTIONS.sortKeys,
95
- strategy: options.strategy ?? DEFAULT_OPTIONS.strategy,
99
+ strategy: resolveStrategy(options.strategy),
96
100
  yarnResolutions: options.yarnResolutions ?? DEFAULT_OPTIONS.yarnResolutions,
97
101
  cleanPackageJson: options.cleanPackageJson ?? DEFAULT_OPTIONS.cleanPackageJson
98
102
  };
99
103
  }
100
-
104
+ function resolveStrategy(strategy) {
105
+ if (!strategy) return DEFAULT_OPTIONS.strategy;
106
+ if (VALID_STRATEGIES.includes(strategy)) return strategy;
107
+ throw new Error(`Invalid strategy: ${strategy}. Expected one of: ${VALID_STRATEGIES.join(", ")}`);
108
+ }
101
109
  //#endregion
102
110
  //#region src/utils/fs.ts
103
111
  /**
@@ -124,17 +132,88 @@ async function fsReadFile(path) {
124
132
  async function fsWriteFile(path, content) {
125
133
  await writeFile(path, `${content.trimEnd()}\n`, "utf-8");
126
134
  }
127
-
128
- //#endregion
129
- //#region src/utils/color.ts
130
- const cyan = getColor("cyan");
131
- const yellow = getColor("yellow");
135
+ getColor("cyan");
136
+ getColor("yellow");
132
137
  const dim = getColor("dim");
133
138
  const green = getColor("green");
134
139
  const red = getColor("red");
135
140
  const bold = getColor("bold");
136
141
  const magenta = getColor("magenta");
137
-
142
+ //#endregion
143
+ //#region src/utils/merge.ts
144
+ /**
145
+ * Merge two objects based on the specified strategy.
146
+ *
147
+ * @param existing - Existing pnpm-workspace.yaml content
148
+ * @param incoming - New settings from package.json and .npmrc
149
+ * @param strategy - Merge strategy to use
150
+ *
151
+ * @returns Merged pnpm workspace configuration
152
+ *
153
+ * @example
154
+ * ```ts
155
+ * // Discard strategy - keep existing values
156
+ * mergeByStrategy({ packages: ['a'] }, { packages: ['b'] }, 'discard')
157
+ * // => { packages: ['a'] }
158
+ *
159
+ * // Merge strategy - combine arrays with deduplication
160
+ * mergeByStrategy({ packages: ['a'] }, { packages: ['b'] }, 'merge')
161
+ * // => { packages: ['a', 'b'] }
162
+ *
163
+ * // Overwrite strategy - use incoming values
164
+ * mergeByStrategy({ packages: ['a'] }, { packages: ['b'] }, 'overwrite')
165
+ * // => { packages: ['b'] }
166
+ * ```
167
+ */
168
+ function mergeByStrategy(existing, incoming, strategy) {
169
+ switch (strategy) {
170
+ case "discard": return discardMerge(existing, incoming);
171
+ case "merge": return mergeWithArrayDedupe(existing, incoming);
172
+ case "overwrite": return discardMerge(incoming, existing);
173
+ default: return defu(existing, incoming);
174
+ }
175
+ }
176
+ /**
177
+ * Merge objects with priority to the first argument.
178
+ * Only adds keys from second argument that don't exist in first.
179
+ * For nested objects, recursively merges them.
180
+ *
181
+ * @param priority - Object with priority values
182
+ * @param fallback - Object with fallback values
183
+ *
184
+ * @returns Merged result
185
+ */
186
+ function discardMerge(priority, fallback) {
187
+ const result = { ...priority };
188
+ for (const [key, fallbackValue] of Object.entries(fallback)) {
189
+ const priorityValue = result[key];
190
+ if (isUndefined(priorityValue)) result[key] = fallbackValue;
191
+ else if (isPlainObject(priorityValue) && isPlainObject(fallbackValue)) result[key] = discardMerge(priorityValue, fallbackValue);
192
+ }
193
+ return result;
194
+ }
195
+ /**
196
+ * Deep merge two objects with array deduplication.
197
+ *
198
+ * For arrays, this function takes the union of both arrays and removes duplicates.
199
+ * For objects, it recursively merges them.
200
+ * For primitives, it prefers existing values.
201
+ *
202
+ * @param existing - Existing values
203
+ * @param incoming - New values
204
+ *
205
+ * @returns Merged result with deduplicated arrays
206
+ */
207
+ function mergeWithArrayDedupe(existing, incoming) {
208
+ const result = { ...existing };
209
+ for (const [key, incomingValue] of Object.entries(incoming)) {
210
+ const existingValue = result[key];
211
+ if (isUndefined(existingValue)) result[key] = incomingValue;
212
+ else if (Array.isArray(existingValue) && Array.isArray(incomingValue)) result[key] = Array.from(new Set([...existingValue, ...incomingValue]));
213
+ else if (isPlainObject(existingValue) && isPlainObject(incomingValue)) result[key] = mergeWithArrayDedupe(existingValue, incomingValue);
214
+ }
215
+ return result;
216
+ }
138
217
  //#endregion
139
218
  //#region src/utils/npmrc.ts
140
219
  /**
@@ -181,7 +260,6 @@ async function pruneNpmrc(path) {
181
260
  async function readNpmrc(path) {
182
261
  return camelcaseKeys(await readIniFile(path));
183
262
  }
184
-
185
263
  //#endregion
186
264
  //#region src/core.ts
187
265
  /**
@@ -235,9 +313,9 @@ async function migratePnpmSettings(rawOptions = {}) {
235
313
  consola.warn("No pnpm settings files to migrate");
236
314
  return;
237
315
  }
238
- let packageJsonIndent = DEFAULT_INDENT;
316
+ let packageJsonIndent = 2;
239
317
  let packageJsonObject = {};
240
- let pnpmWorkspaceYamlIndent = DEFAULT_INDENT;
318
+ let pnpmWorkspaceYamlIndent = 2;
241
319
  let pnpmWorkspaceYamlObject = {};
242
320
  if (packageJsonExists) {
243
321
  const content = await fsReadFile(packageJsonPath);
@@ -262,19 +340,20 @@ async function migratePnpmSettings(rawOptions = {}) {
262
340
  overrides: defu(packageJsonObject.pnpm?.overrides, packageJsonObject.resolutions)
263
341
  } : { ...packageJsonObject.pnpm };
264
342
  if (pnpmSettingsInPackageJson.overrides && !Object.keys(pnpmSettingsInPackageJson.overrides).length) delete pnpmSettingsInPackageJson.overrides;
265
- const pnpmWorkspaceResult = defu(pnpmWorkspaceYamlObject, {
343
+ const incomingSettings = {
266
344
  ...pnpmSettingsInNpmrc,
267
345
  ...pnpmSettingsInPackageJson
268
- });
346
+ };
347
+ const pnpmWorkspaceResult = mergeByStrategy(pnpmWorkspaceYamlObject, incomingSettings, options.strategy);
269
348
  const yamlDocument = new Document({}, { sortMapEntries: options.sortKeys });
270
- Object.entries(pnpmWorkspaceResult).forEach(([key, value], index) => {
349
+ Object.entries(pnpmWorkspaceResult).forEach(([key, value]) => {
271
350
  yamlDocument.add({
272
351
  key,
273
352
  value
274
353
  });
275
- if (options.newlineBetween && index < Object.keys(pnpmWorkspaceResult).length - 1) {}
276
354
  });
277
- await fsWriteFile(pnpmWorkspaceYamlPath, yamlDocument.toString({ indent: pnpmWorkspaceYamlIndent }));
355
+ const yamlContent = yamlDocument.toString({ indent: pnpmWorkspaceYamlIndent });
356
+ await fsWriteFile(pnpmWorkspaceYamlPath, options.newlineBetween ? yamlContent.replace(/\n(?=[^\s#][^:\n]*:)/g, "\n\n") : yamlContent);
278
357
  if (npmrcExists && options.cleanNpmrc) await pruneNpmrc(npmrcPath);
279
358
  if (packageJsonExists && options.cleanPackageJson && (packageJsonObject.pnpm || packageJsonObject.resolutions)) {
280
359
  delete packageJsonObject.pnpm;
@@ -286,11 +365,10 @@ async function migratePnpmSettings(rawOptions = {}) {
286
365
  throw err;
287
366
  }
288
367
  }
289
-
290
368
  //#endregion
291
369
  //#region src/cli.ts
292
370
  const cli = cac(name);
293
- cli.version(version).option("--cwd [cwd]", "Current working directory").option("--sort-keys", "Sort keys when write pnpm-workspace.yaml").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();
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();
294
372
  cli.command("").action(async (options) => {
295
373
  try {
296
374
  consola$1.log(`\n${bold(magenta(name))} ${dim(`v${version}`)}`);
@@ -304,6 +382,5 @@ cli.command("").action(async (options) => {
304
382
  }
305
383
  });
306
384
  cli.parse();
307
-
308
385
  //#endregion
309
- export { };
386
+ export {};
package/dist/index.d.mts CHANGED
@@ -1,6 +1,13 @@
1
1
  import { PnpmSettings } from "@pnpm/types";
2
2
 
3
- //#region src/options.d.ts
3
+ //#region src/types.d.ts
4
+ /**
5
+ * Merge strategy for combining pnpm settings
6
+ */
7
+ type MergeStrategy = 'discard' | 'merge' | 'overwrite';
8
+ /**
9
+ * Options for pnpm settings migration
10
+ */
4
11
  interface Options {
5
12
  /**
6
13
  * Whether to remove pnpm settings in `.npmrc` file
@@ -34,7 +41,7 @@ interface Options {
34
41
  /**
35
42
  * Strategy to handle conflicts
36
43
  */
37
- strategy?: 'discard' | 'merge' | 'overwrite';
44
+ strategy?: MergeStrategy;
38
45
  /**
39
46
  * Whether to migrate `resolutions` filed in `package.json`
40
47
  *
@@ -43,27 +50,67 @@ interface Options {
43
50
  yarnResolutions?: boolean;
44
51
  }
45
52
  /**
46
- * Resolve and normalize migration options with defaults.
47
- *
48
- * This function takes partial options and returns a complete options object
49
- * with all properties set to either the provided value or the default value.
50
- *
51
- * @param options - Partial migration options
52
- *
53
- * @returns Complete options object with all required properties
54
- *
55
- * @example
56
- * ```ts
57
- * // Use all defaults
58
- * const opts = resolveOptions()
59
- * // { cleanNpmrc: true, cleanPackageJson: true, cwd: '/current/dir', ... }
60
- *
61
- * // Override specific options
62
- * const opts = resolveOptions({ sortKeys: true, cleanNpmrc: false })
63
- * // { cleanNpmrc: false, cleanPackageJson: true, sortKeys: true, ... }
64
- * ```
53
+ * legacy `pnpm-workspace` types
65
54
  */
66
- declare function resolveOptions(options?: Options): Required<Options>;
55
+ type PnpmWorkspaceLegacy = {
56
+ catalog?: Record<string, string>;
57
+ catalogs?: Record<string, Record<string, string>>;
58
+ packages?: string[];
59
+ };
60
+ /**
61
+ * `package-json` types
62
+ * @pg
63
+ */
64
+ interface PackageJson {
65
+ /**
66
+ * same as `pnpm.overrides`
67
+ *
68
+ * @compatibility npm, bun
69
+ */
70
+ overrides?: Record<string, string>;
71
+ /**
72
+ * pnpm settings
73
+ */
74
+ pnpm?: PnpmSettings;
75
+ /**
76
+ * same as `pnpm.overrides`
77
+ *
78
+ * @compatibility yarn, bun
79
+ */
80
+ resolutions?: Record<string, string>;
81
+ }
82
+ /**
83
+ * `.npmrc` types
84
+ * @pg
85
+ */
86
+ type NpmRC = Record<string, any>;
87
+ /**
88
+ * Deprecated `pnpm` settings in `package.json`
89
+ * @see {@link https://github.com/pnpm/pnpm/blob/main/core/types/CHANGELOG.md#major-changes}
90
+ */
91
+ interface PnpmSettingsDeprecated {
92
+ /**
93
+ * @deprecated
94
+ */
95
+ ignoredBuiltDependencies?: string[];
96
+ /**
97
+ * @deprecated
98
+ */
99
+ neverBuiltDependencies?: string[];
100
+ /**
101
+ * @deprecated
102
+ */
103
+ onlyBuiltDependencies?: string[];
104
+ /**
105
+ * @deprecated
106
+ */
107
+ onlyBuiltDependenciesFile?: string;
108
+ }
109
+ /**
110
+ * `pnpm-workspace` types
111
+ * @pg
112
+ */
113
+ type PnpmWorkspace = PnpmSettings & PnpmSettingsDeprecated & PnpmWorkspaceLegacy;
67
114
  //#endregion
68
115
  //#region src/core.d.ts
69
116
  /**
@@ -102,46 +149,28 @@ declare function resolveOptions(options?: Options): Required<Options>;
102
149
  */
103
150
  declare function migratePnpmSettings(rawOptions?: Options): Promise<void>;
104
151
  //#endregion
105
- //#region src/types.d.ts
106
- /**
107
- * legacy `pnpm-workspace` types
108
- */
109
- type PnpmWorkspaceLegacy = {
110
- catalog?: Record<string, string>;
111
- catalogs?: Record<string, Record<string, string>>;
112
- packages?: string[];
113
- };
114
- /**
115
- * `package-json` types
116
- * @pg
117
- */
118
- interface PackageJson {
119
- /**
120
- * same as `pnpm.overrides`
121
- *
122
- * @compatibility npm, bun
123
- */
124
- overrides?: Record<string, string>;
125
- /**
126
- * pnpm settings
127
- */
128
- pnpm?: PnpmSettings;
129
- /**
130
- * same as `pnpm.overrides`
131
- *
132
- * @compatibility yarn, bun
133
- */
134
- resolutions?: Record<string, string>;
135
- }
136
- /**
137
- * `.npmrc` types
138
- * @pg
139
- */
140
- type NpmRC = Record<string, any>;
152
+ //#region src/options.d.ts
141
153
  /**
142
- * `pnpm-workspace` types
143
- * @pg
154
+ * Resolve and normalize migration options with defaults.
155
+ *
156
+ * This function takes partial options and returns a complete options object
157
+ * with all properties set to either the provided value or the default value.
158
+ *
159
+ * @param options - Partial migration options
160
+ *
161
+ * @returns Complete options object with all required properties
162
+ *
163
+ * @example
164
+ * ```ts
165
+ * // Use all defaults
166
+ * const opts = resolveOptions()
167
+ * // { cleanNpmrc: true, cleanPackageJson: true, cwd: '/current/dir', ... }
168
+ *
169
+ * // Override specific options
170
+ * const opts = resolveOptions({ sortKeys: true, cleanNpmrc: false })
171
+ * // { cleanNpmrc: false, cleanPackageJson: true, sortKeys: true, ... }
172
+ * ```
144
173
  */
145
- type PnpmWorkspace = PnpmSettings & PnpmWorkspaceLegacy;
174
+ declare function resolveOptions(options?: Options): Required<Options>;
146
175
  //#endregion
147
- export { NpmRC, Options, PackageJson, PnpmWorkspace, PnpmWorkspaceLegacy, migratePnpmSettings, resolveOptions };
176
+ export { MergeStrategy, NpmRC, Options, PackageJson, PnpmSettingsDeprecated, PnpmWorkspace, PnpmWorkspaceLegacy, migratePnpmSettings, resolveOptions };
package/dist/index.mjs CHANGED
@@ -1,4 +1,4 @@
1
- import { pick } from "@ntnyq/utils";
1
+ import { isPlainObject, isUndefined, pick } from "@ntnyq/utils";
2
2
  import consola from "consola";
3
3
  import { defu } from "defu";
4
4
  import detectIndent from "detect-indent";
@@ -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,26 @@ 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
48
  //#endregion
49
49
  //#region src/options.ts
50
50
  /**
@@ -59,6 +59,11 @@ const DEFAULT_OPTIONS = {
59
59
  strategy: "merge",
60
60
  yarnResolutions: true
61
61
  };
62
+ const VALID_STRATEGIES = [
63
+ "discard",
64
+ "merge",
65
+ "overwrite"
66
+ ];
62
67
  /**
63
68
  * Resolve and normalize migration options with defaults.
64
69
  *
@@ -86,12 +91,16 @@ function resolveOptions(options = {}) {
86
91
  cwd: options.cwd ?? DEFAULT_OPTIONS.cwd,
87
92
  newlineBetween: options.newlineBetween ?? DEFAULT_OPTIONS.newlineBetween,
88
93
  sortKeys: options.sortKeys ?? DEFAULT_OPTIONS.sortKeys,
89
- strategy: options.strategy ?? DEFAULT_OPTIONS.strategy,
94
+ strategy: resolveStrategy(options.strategy),
90
95
  yarnResolutions: options.yarnResolutions ?? DEFAULT_OPTIONS.yarnResolutions,
91
96
  cleanPackageJson: options.cleanPackageJson ?? DEFAULT_OPTIONS.cleanPackageJson
92
97
  };
93
98
  }
94
-
99
+ function resolveStrategy(strategy) {
100
+ if (!strategy) return DEFAULT_OPTIONS.strategy;
101
+ if (VALID_STRATEGIES.includes(strategy)) return strategy;
102
+ throw new Error(`Invalid strategy: ${strategy}. Expected one of: ${VALID_STRATEGIES.join(", ")}`);
103
+ }
95
104
  //#endregion
96
105
  //#region src/utils/fs.ts
97
106
  /**
@@ -118,17 +127,88 @@ async function fsReadFile(path) {
118
127
  async function fsWriteFile(path, content) {
119
128
  await writeFile(path, `${content.trimEnd()}\n`, "utf-8");
120
129
  }
121
-
122
- //#endregion
123
- //#region src/utils/color.ts
124
- const cyan = getColor("cyan");
125
- const yellow = getColor("yellow");
130
+ getColor("cyan");
131
+ getColor("yellow");
126
132
  const dim = getColor("dim");
127
- const green = getColor("green");
128
- const red = getColor("red");
129
- const bold = getColor("bold");
130
- const magenta = getColor("magenta");
131
-
133
+ getColor("green");
134
+ getColor("red");
135
+ getColor("bold");
136
+ getColor("magenta");
137
+ //#endregion
138
+ //#region src/utils/merge.ts
139
+ /**
140
+ * Merge two objects based on the specified strategy.
141
+ *
142
+ * @param existing - Existing pnpm-workspace.yaml content
143
+ * @param incoming - New settings from package.json and .npmrc
144
+ * @param strategy - Merge strategy to use
145
+ *
146
+ * @returns Merged pnpm workspace configuration
147
+ *
148
+ * @example
149
+ * ```ts
150
+ * // Discard strategy - keep existing values
151
+ * mergeByStrategy({ packages: ['a'] }, { packages: ['b'] }, 'discard')
152
+ * // => { packages: ['a'] }
153
+ *
154
+ * // Merge strategy - combine arrays with deduplication
155
+ * mergeByStrategy({ packages: ['a'] }, { packages: ['b'] }, 'merge')
156
+ * // => { packages: ['a', 'b'] }
157
+ *
158
+ * // Overwrite strategy - use incoming values
159
+ * mergeByStrategy({ packages: ['a'] }, { packages: ['b'] }, 'overwrite')
160
+ * // => { packages: ['b'] }
161
+ * ```
162
+ */
163
+ function mergeByStrategy(existing, incoming, strategy) {
164
+ switch (strategy) {
165
+ case "discard": return discardMerge(existing, incoming);
166
+ case "merge": return mergeWithArrayDedupe(existing, incoming);
167
+ case "overwrite": return discardMerge(incoming, existing);
168
+ default: return defu(existing, incoming);
169
+ }
170
+ }
171
+ /**
172
+ * Merge objects with priority to the first argument.
173
+ * Only adds keys from second argument that don't exist in first.
174
+ * For nested objects, recursively merges them.
175
+ *
176
+ * @param priority - Object with priority values
177
+ * @param fallback - Object with fallback values
178
+ *
179
+ * @returns Merged result
180
+ */
181
+ function discardMerge(priority, fallback) {
182
+ const result = { ...priority };
183
+ for (const [key, fallbackValue] of Object.entries(fallback)) {
184
+ const priorityValue = result[key];
185
+ if (isUndefined(priorityValue)) result[key] = fallbackValue;
186
+ else if (isPlainObject(priorityValue) && isPlainObject(fallbackValue)) result[key] = discardMerge(priorityValue, fallbackValue);
187
+ }
188
+ return result;
189
+ }
190
+ /**
191
+ * Deep merge two objects with array deduplication.
192
+ *
193
+ * For arrays, this function takes the union of both arrays and removes duplicates.
194
+ * For objects, it recursively merges them.
195
+ * For primitives, it prefers existing values.
196
+ *
197
+ * @param existing - Existing values
198
+ * @param incoming - New values
199
+ *
200
+ * @returns Merged result with deduplicated arrays
201
+ */
202
+ function mergeWithArrayDedupe(existing, incoming) {
203
+ const result = { ...existing };
204
+ for (const [key, incomingValue] of Object.entries(incoming)) {
205
+ const existingValue = result[key];
206
+ if (isUndefined(existingValue)) result[key] = incomingValue;
207
+ else if (Array.isArray(existingValue) && Array.isArray(incomingValue)) result[key] = Array.from(new Set([...existingValue, ...incomingValue]));
208
+ else if (isPlainObject(existingValue) && isPlainObject(incomingValue)) result[key] = mergeWithArrayDedupe(existingValue, incomingValue);
209
+ }
210
+ return result;
211
+ }
132
212
  //#endregion
133
213
  //#region src/utils/npmrc.ts
134
214
  /**
@@ -175,7 +255,6 @@ async function pruneNpmrc(path) {
175
255
  async function readNpmrc(path) {
176
256
  return camelcaseKeys(await readIniFile(path));
177
257
  }
178
-
179
258
  //#endregion
180
259
  //#region src/core.ts
181
260
  /**
@@ -229,9 +308,9 @@ async function migratePnpmSettings(rawOptions = {}) {
229
308
  consola.warn("No pnpm settings files to migrate");
230
309
  return;
231
310
  }
232
- let packageJsonIndent = DEFAULT_INDENT;
311
+ let packageJsonIndent = 2;
233
312
  let packageJsonObject = {};
234
- let pnpmWorkspaceYamlIndent = DEFAULT_INDENT;
313
+ let pnpmWorkspaceYamlIndent = 2;
235
314
  let pnpmWorkspaceYamlObject = {};
236
315
  if (packageJsonExists) {
237
316
  const content = await fsReadFile(packageJsonPath);
@@ -256,19 +335,20 @@ async function migratePnpmSettings(rawOptions = {}) {
256
335
  overrides: defu(packageJsonObject.pnpm?.overrides, packageJsonObject.resolutions)
257
336
  } : { ...packageJsonObject.pnpm };
258
337
  if (pnpmSettingsInPackageJson.overrides && !Object.keys(pnpmSettingsInPackageJson.overrides).length) delete pnpmSettingsInPackageJson.overrides;
259
- const pnpmWorkspaceResult = defu(pnpmWorkspaceYamlObject, {
338
+ const incomingSettings = {
260
339
  ...pnpmSettingsInNpmrc,
261
340
  ...pnpmSettingsInPackageJson
262
- });
341
+ };
342
+ const pnpmWorkspaceResult = mergeByStrategy(pnpmWorkspaceYamlObject, incomingSettings, options.strategy);
263
343
  const yamlDocument = new Document({}, { sortMapEntries: options.sortKeys });
264
- Object.entries(pnpmWorkspaceResult).forEach(([key, value], index) => {
344
+ Object.entries(pnpmWorkspaceResult).forEach(([key, value]) => {
265
345
  yamlDocument.add({
266
346
  key,
267
347
  value
268
348
  });
269
- if (options.newlineBetween && index < Object.keys(pnpmWorkspaceResult).length - 1) {}
270
349
  });
271
- await fsWriteFile(pnpmWorkspaceYamlPath, yamlDocument.toString({ indent: pnpmWorkspaceYamlIndent }));
350
+ const yamlContent = yamlDocument.toString({ indent: pnpmWorkspaceYamlIndent });
351
+ await fsWriteFile(pnpmWorkspaceYamlPath, options.newlineBetween ? yamlContent.replace(/\n(?=[^\s#][^:\n]*:)/g, "\n\n") : yamlContent);
272
352
  if (npmrcExists && options.cleanNpmrc) await pruneNpmrc(npmrcPath);
273
353
  if (packageJsonExists && options.cleanPackageJson && (packageJsonObject.pnpm || packageJsonObject.resolutions)) {
274
354
  delete packageJsonObject.pnpm;
@@ -280,6 +360,5 @@ async function migratePnpmSettings(rawOptions = {}) {
280
360
  throw err;
281
361
  }
282
362
  }
283
-
284
363
  //#endregion
285
- export { migratePnpmSettings, resolveOptions };
364
+ 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.1.0",
4
+ "version": "0.3.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",
@@ -37,43 +37,46 @@
37
37
  },
38
38
  "sideEffects": false,
39
39
  "dependencies": {
40
- "@ntnyq/utils": "^0.9.2",
41
- "@pnpm/types": "^1001.2.0",
42
- "cac": "^6.7.14",
43
- "camelcase-keys": "^10.0.1",
40
+ "@ntnyq/utils": "^0.11.6",
41
+ "@pnpm/types": "^1100.0.0",
42
+ "cac": "^7.0.0",
43
+ "camelcase-keys": "^10.0.2",
44
44
  "consola": "^3.4.2",
45
- "defu": "^6.1.4",
45
+ "defu": "^6.1.7",
46
46
  "detect-indent": "^7.0.2",
47
47
  "pathe": "^2.0.3",
48
- "read-ini-file": "^4.0.0",
48
+ "read-ini-file": "^5.0.0",
49
49
  "uncase": "^0.2.0",
50
- "yaml": "^2.8.2"
50
+ "yaml": "^2.8.3"
51
51
  },
52
52
  "devDependencies": {
53
- "@ntnyq/eslint-config": "^5.8.0",
54
- "@ntnyq/prettier-config": "^3.0.1",
55
- "@types/node": "^24.10.4",
56
- "bumpp": "^10.3.2",
57
- "eslint": "^9.39.2",
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",
58
58
  "husky": "^9.1.7",
59
- "nano-staged": "^0.9.0",
59
+ "nano-staged": "^1.0.2",
60
60
  "npm-run-all2": "^8.0.4",
61
- "prettier": "^3.7.4",
62
- "tsdown": "^0.18.0",
63
- "typescript": "^5.9.3",
64
- "vitest": "^4.0.15"
61
+ "oxfmt": "^0.44.0",
62
+ "tsdown": "^0.21.7",
63
+ "typescript": "^6.0.2",
64
+ "vitest": "^4.1.4"
65
65
  },
66
66
  "nano-staged": {
67
- "*.{js,ts,mjs,cjs,md,yml,yaml,toml,json}": "eslint --fix"
67
+ "*.{js,ts,mjs,tsx,md,yml,yaml,toml,json}": "eslint --fix",
68
+ "*": "oxfmt --no-error-on-unmatched-pattern"
68
69
  },
69
70
  "scripts": {
70
71
  "build": "tsdown",
71
72
  "dev": "tsdown --watch",
73
+ "format": "oxfmt",
74
+ "format:check": "oxfmt --check",
72
75
  "lint": "eslint",
73
76
  "release": "run-s release:check release:version",
74
- "release:check": "run-s lint typecheck test",
77
+ "release:check": "run-s lint format:check typecheck test",
75
78
  "release:version": "bumpp",
76
79
  "test": "vitest",
77
- "typecheck": "tsc --noEmit"
80
+ "typecheck": "tsgo --noEmit"
78
81
  }
79
82
  }