pnpm-settings-migrator 0.6.1 → 0.8.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 +56 -3
- package/dist/cli.mjs +607 -176
- package/dist/index.d.mts +81 -7
- package/dist/index.mjs +601 -176
- package/package.json +27 -28
package/dist/cli.mjs
CHANGED
|
@@ -1,19 +1,20 @@
|
|
|
1
1
|
import process from "node:process";
|
|
2
2
|
import { cac } from "cac";
|
|
3
3
|
import consola$1, { consola } from "consola";
|
|
4
|
-
import { isPlainObject, isUndefined, pick } from "@ntnyq/utils";
|
|
5
|
-
import { defu } from "defu";
|
|
6
|
-
import detectIndent from "detect-indent";
|
|
7
4
|
import { resolve } from "pathe";
|
|
8
|
-
import { Document, parse } from "yaml";
|
|
9
|
-
import { access, readFile, writeFile } from "node:fs/promises";
|
|
5
|
+
import { Document, parse, stringify } from "yaml";
|
|
6
|
+
import { access, readFile, rm, writeFile } from "node:fs/promises";
|
|
10
7
|
import { getColor } from "consola/utils";
|
|
8
|
+
import { isPlainObject, isUndefined, pick } from "@ntnyq/utils";
|
|
9
|
+
import { defu } from "defu";
|
|
11
10
|
import camelcaseKeys from "camelcase-keys";
|
|
12
11
|
import { readIniFile } from "read-ini-file";
|
|
13
12
|
import { kebabCase } from "uncase";
|
|
13
|
+
import { isDeepStrictEqual } from "node:util";
|
|
14
|
+
import detectIndent from "detect-indent";
|
|
14
15
|
//#region package.json
|
|
15
16
|
var name = "pnpm-settings-migrator";
|
|
16
|
-
var version = "0.
|
|
17
|
+
var version = "0.8.0";
|
|
17
18
|
//#endregion
|
|
18
19
|
//#region src/constants.ts
|
|
19
20
|
const NPMRC = ".npmrc";
|
|
@@ -60,6 +61,7 @@ const PNPM_SETTINGS_FIELDS = [
|
|
|
60
61
|
"httpProxy",
|
|
61
62
|
"httpsProxy",
|
|
62
63
|
"ignoredBuiltDependencies",
|
|
64
|
+
"ignoreDepScripts",
|
|
63
65
|
"ignoreDeps",
|
|
64
66
|
"ignoredOptionalDependencies",
|
|
65
67
|
"ignorePatchFailures",
|
|
@@ -87,11 +89,14 @@ const PNPM_SETTINGS_FIELDS = [
|
|
|
87
89
|
"overrides",
|
|
88
90
|
"packageExtensions",
|
|
89
91
|
"packageImportMethod",
|
|
92
|
+
"packageManagerStrict",
|
|
93
|
+
"packageManagerStrictVersion",
|
|
90
94
|
"patchedDependencies",
|
|
91
95
|
"peerDependencyRules",
|
|
92
96
|
"peersSuffixMaxLength",
|
|
93
97
|
"pnpmfile",
|
|
94
98
|
"pnpmfileIgnorePkgManifest",
|
|
99
|
+
"pmOnFail",
|
|
95
100
|
"preferFrozenLockfile",
|
|
96
101
|
"preferWorkspacePackages",
|
|
97
102
|
"production",
|
|
@@ -130,11 +135,17 @@ const PNPM_SETTINGS_FIELDS = [
|
|
|
130
135
|
];
|
|
131
136
|
const PNPM_V11_REMOVED_SETTINGS = [
|
|
132
137
|
"allowNonAppliedPatches",
|
|
138
|
+
"executionEnv",
|
|
139
|
+
"ignoreDepScripts",
|
|
133
140
|
"ignorePatchFailures",
|
|
134
141
|
"ignoredBuiltDependencies",
|
|
142
|
+
"managePackageManagerVersions",
|
|
135
143
|
"neverBuiltDependencies",
|
|
136
144
|
"onlyBuiltDependencies",
|
|
137
|
-
"onlyBuiltDependenciesFile"
|
|
145
|
+
"onlyBuiltDependenciesFile",
|
|
146
|
+
"packageManagerStrict",
|
|
147
|
+
"packageManagerStrictVersion",
|
|
148
|
+
"useNodeVersion"
|
|
138
149
|
];
|
|
139
150
|
//#endregion
|
|
140
151
|
//#region src/options.ts
|
|
@@ -148,6 +159,7 @@ const DEFAULT_OPTIONS = {
|
|
|
148
159
|
cwd: process.cwd(),
|
|
149
160
|
newlineBetween: true,
|
|
150
161
|
replaceDeprecated: false,
|
|
162
|
+
showChanges: true,
|
|
151
163
|
sortKeys: false,
|
|
152
164
|
strategy: "merge",
|
|
153
165
|
yarnResolutions: true
|
|
@@ -160,8 +172,19 @@ const VALID_STRATEGIES = [
|
|
|
160
172
|
const VALID_COMPATIBILITIES = [
|
|
161
173
|
"auto",
|
|
162
174
|
"v10",
|
|
163
|
-
"v11"
|
|
175
|
+
"v11",
|
|
176
|
+
"v12"
|
|
164
177
|
];
|
|
178
|
+
function resolveCompatibility(compatibility) {
|
|
179
|
+
if (!compatibility) return DEFAULT_OPTIONS.compatibility;
|
|
180
|
+
if (VALID_COMPATIBILITIES.includes(compatibility)) return compatibility;
|
|
181
|
+
throw new Error(`Invalid compatibility: ${compatibility}. Expected one of: ${VALID_COMPATIBILITIES.join(", ")}`);
|
|
182
|
+
}
|
|
183
|
+
function resolveStrategy(strategy) {
|
|
184
|
+
if (!strategy) return DEFAULT_OPTIONS.strategy;
|
|
185
|
+
if (VALID_STRATEGIES.includes(strategy)) return strategy;
|
|
186
|
+
throw new Error(`Invalid strategy: ${strategy}. Expected one of: ${VALID_STRATEGIES.join(", ")}`);
|
|
187
|
+
}
|
|
165
188
|
/**
|
|
166
189
|
* Resolve and normalize migration options with defaults.
|
|
167
190
|
*
|
|
@@ -189,6 +212,7 @@ function resolveOptions(options = {}) {
|
|
|
189
212
|
compatibility: resolveCompatibility(options.compatibility),
|
|
190
213
|
cwd: options.cwd ?? DEFAULT_OPTIONS.cwd,
|
|
191
214
|
newlineBetween: options.newlineBetween ?? DEFAULT_OPTIONS.newlineBetween,
|
|
215
|
+
showChanges: options.showChanges ?? DEFAULT_OPTIONS.showChanges,
|
|
192
216
|
sortKeys: options.sortKeys ?? DEFAULT_OPTIONS.sortKeys,
|
|
193
217
|
strategy: resolveStrategy(options.strategy),
|
|
194
218
|
yarnResolutions: options.yarnResolutions ?? DEFAULT_OPTIONS.yarnResolutions,
|
|
@@ -196,83 +220,269 @@ function resolveOptions(options = {}) {
|
|
|
196
220
|
replaceDeprecated: options.replaceDeprecated ?? DEFAULT_OPTIONS.replaceDeprecated
|
|
197
221
|
};
|
|
198
222
|
}
|
|
199
|
-
function resolveCompatibility(compatibility) {
|
|
200
|
-
if (!compatibility) return DEFAULT_OPTIONS.compatibility;
|
|
201
|
-
if (VALID_COMPATIBILITIES.includes(compatibility)) return compatibility;
|
|
202
|
-
throw new Error(`Invalid compatibility: ${compatibility}. Expected one of: ${VALID_COMPATIBILITIES.join(", ")}`);
|
|
203
|
-
}
|
|
204
|
-
function resolveStrategy(strategy) {
|
|
205
|
-
if (!strategy) return DEFAULT_OPTIONS.strategy;
|
|
206
|
-
if (VALID_STRATEGIES.includes(strategy)) return strategy;
|
|
207
|
-
throw new Error(`Invalid strategy: ${strategy}. Expected one of: ${VALID_STRATEGIES.join(", ")}`);
|
|
208
|
-
}
|
|
209
223
|
//#endregion
|
|
210
224
|
//#region src/utils/fs.ts
|
|
211
225
|
/**
|
|
212
|
-
* Check
|
|
213
|
-
*
|
|
214
|
-
* @
|
|
226
|
+
* Check whether a filesystem path exists.
|
|
227
|
+
*
|
|
228
|
+
* @param path - Filesystem path to check
|
|
229
|
+
*
|
|
230
|
+
* @returns `true` when the path is accessible, otherwise `false`
|
|
215
231
|
*/
|
|
216
232
|
async function fsExists(path) {
|
|
217
233
|
return access(path).then(() => true).catch(() => false);
|
|
218
234
|
}
|
|
219
235
|
/**
|
|
220
|
-
* Read file
|
|
221
|
-
*
|
|
222
|
-
* @
|
|
236
|
+
* Read a UTF-8 text file.
|
|
237
|
+
*
|
|
238
|
+
* @param path - Filesystem path to read
|
|
239
|
+
*
|
|
240
|
+
* @returns Text content of the file
|
|
223
241
|
*/
|
|
224
242
|
async function fsReadFile(path) {
|
|
225
243
|
return await readFile(path, "utf-8");
|
|
226
244
|
}
|
|
227
245
|
/**
|
|
228
|
-
*
|
|
229
|
-
*
|
|
230
|
-
* @param
|
|
246
|
+
* Remove a file if it exists.
|
|
247
|
+
*
|
|
248
|
+
* @param path - Filesystem path to remove
|
|
249
|
+
*
|
|
250
|
+
* @returns A promise that resolves after the file is removed
|
|
251
|
+
*/
|
|
252
|
+
async function fsRemoveFile(path) {
|
|
253
|
+
await rm(path, { force: true });
|
|
254
|
+
}
|
|
255
|
+
/**
|
|
256
|
+
* Write a normalized UTF-8 text file with one trailing newline.
|
|
257
|
+
*
|
|
258
|
+
* @param path - Filesystem path to write
|
|
259
|
+
* @param content - Text content to write
|
|
260
|
+
*
|
|
261
|
+
* @returns A promise that resolves after the file is written
|
|
231
262
|
*/
|
|
232
263
|
async function fsWriteFile(path, content) {
|
|
233
264
|
await writeFile(path, `${content.trimEnd()}\n`, "utf-8");
|
|
234
265
|
}
|
|
235
266
|
getColor("cyan");
|
|
236
267
|
getColor("yellow");
|
|
268
|
+
/**
|
|
269
|
+
* Apply dim terminal styling to a value.
|
|
270
|
+
*/
|
|
237
271
|
const dim = getColor("dim");
|
|
272
|
+
/**
|
|
273
|
+
* Apply green terminal styling to a value.
|
|
274
|
+
*/
|
|
238
275
|
const green = getColor("green");
|
|
276
|
+
/**
|
|
277
|
+
* Apply red terminal styling to a value.
|
|
278
|
+
*/
|
|
239
279
|
const red = getColor("red");
|
|
280
|
+
/**
|
|
281
|
+
* Apply bold terminal styling to a value.
|
|
282
|
+
*/
|
|
240
283
|
const bold = getColor("bold");
|
|
284
|
+
/**
|
|
285
|
+
* Apply magenta terminal styling to a value.
|
|
286
|
+
*/
|
|
241
287
|
const magenta = getColor("magenta");
|
|
242
288
|
//#endregion
|
|
243
|
-
//#region src/utils/
|
|
289
|
+
//#region src/utils/compatibility.ts
|
|
244
290
|
/**
|
|
245
|
-
*
|
|
291
|
+
* Legacy build settings that can be replaced for pnpm v10.
|
|
292
|
+
*/
|
|
293
|
+
const PNPM_REPLACEABLE_IN_V10_SETTINGS = [
|
|
294
|
+
"allowNonAppliedPatches",
|
|
295
|
+
"ignoredBuiltDependencies",
|
|
296
|
+
"neverBuiltDependencies",
|
|
297
|
+
"onlyBuiltDependencies",
|
|
298
|
+
"onlyBuiltDependenciesFile"
|
|
299
|
+
];
|
|
300
|
+
/**
|
|
301
|
+
* Read packages listed by the legacy `onlyBuiltDependenciesFile` setting.
|
|
246
302
|
*
|
|
247
|
-
* @param
|
|
248
|
-
* @param
|
|
249
|
-
* @param strategy - Merge strategy to use
|
|
303
|
+
* @param cwd - Working directory used to resolve the referenced file
|
|
304
|
+
* @param file - Relative path stored in `onlyBuiltDependenciesFile`
|
|
250
305
|
*
|
|
251
|
-
* @returns
|
|
306
|
+
* @returns Package names read from the referenced JSON file
|
|
252
307
|
*
|
|
253
|
-
* @
|
|
254
|
-
*
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
308
|
+
* @throws {TypeError} When the referenced JSON value is not a string array
|
|
309
|
+
* @throws {SyntaxError} When the referenced file contains invalid JSON
|
|
310
|
+
*/
|
|
311
|
+
async function readOnlyBuiltDependenciesFile(cwd, file) {
|
|
312
|
+
if (!file) return [];
|
|
313
|
+
const path = resolve(cwd, file);
|
|
314
|
+
const value = JSON.parse(await fsReadFile(path));
|
|
315
|
+
if (!Array.isArray(value) || !value.every((item) => typeof item === "string")) throw new TypeError(`Invalid onlyBuiltDependenciesFile: ${file}. Expected a JSON array of package names.`);
|
|
316
|
+
return value;
|
|
317
|
+
}
|
|
318
|
+
/**
|
|
319
|
+
* Build an `allowBuilds` map from legacy build-script settings.
|
|
258
320
|
*
|
|
259
|
-
*
|
|
260
|
-
*
|
|
261
|
-
* // => { packages: ['a', 'b'] }
|
|
321
|
+
* @param incomingSettings - Settings containing legacy build declarations
|
|
322
|
+
* @param cwd - Working directory used to resolve referenced files
|
|
262
323
|
*
|
|
263
|
-
*
|
|
264
|
-
* mergeByStrategy({ packages: ['a'] }, { packages: ['b'] }, 'overwrite')
|
|
265
|
-
* // => { packages: ['b'] }
|
|
266
|
-
* ```
|
|
324
|
+
* @returns Package-to-permission map, or `undefined` when no entries exist
|
|
267
325
|
*/
|
|
268
|
-
function
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
326
|
+
async function collectAllowBuildsFromLegacy(incomingSettings, cwd) {
|
|
327
|
+
const allowBuilds = {};
|
|
328
|
+
const allowedFromFile = await readOnlyBuiltDependenciesFile(cwd, incomingSettings.onlyBuiltDependenciesFile);
|
|
329
|
+
for (const name of [...incomingSettings.onlyBuiltDependencies || [], ...allowedFromFile]) allowBuilds[name] = true;
|
|
330
|
+
for (const name of incomingSettings.ignoredBuiltDependencies || []) allowBuilds[name] = false;
|
|
331
|
+
for (const name of incomingSettings.neverBuiltDependencies || []) allowBuilds[name] = false;
|
|
332
|
+
return Object.keys(allowBuilds).length ? allowBuilds : void 0;
|
|
333
|
+
}
|
|
334
|
+
/**
|
|
335
|
+
* Resolve the pnpm v11 replacement for legacy package-manager settings.
|
|
336
|
+
*
|
|
337
|
+
* @param settings - Settings containing legacy package-manager declarations
|
|
338
|
+
*
|
|
339
|
+
* @returns Resolved `pmOnFail` behavior, or `undefined` when not configured
|
|
340
|
+
*/
|
|
341
|
+
function resolvePmOnFail(settings) {
|
|
342
|
+
if (settings.pmOnFail) return settings.pmOnFail;
|
|
343
|
+
if (settings.packageManagerStrictVersion === true) return "error";
|
|
344
|
+
if (settings.packageManagerStrict === false) return "warn";
|
|
345
|
+
if (settings.managePackageManagerVersions === false) return "ignore";
|
|
346
|
+
if (settings.managePackageManagerVersions === true) return "download";
|
|
347
|
+
}
|
|
348
|
+
/**
|
|
349
|
+
* Rename legacy audit settings while preserving values for manual ID updates.
|
|
350
|
+
*
|
|
351
|
+
* @param settings - Settings whose audit configuration may be normalized
|
|
352
|
+
* @param warnings - Warning collection updated with manual follow-up guidance
|
|
353
|
+
*
|
|
354
|
+
* @returns Nothing; the settings and warning collection are mutated in place
|
|
355
|
+
*/
|
|
356
|
+
function normalizeAuditConfig(settings, warnings) {
|
|
357
|
+
const { auditConfig } = settings;
|
|
358
|
+
const ignoreCves = auditConfig?.ignoreCves;
|
|
359
|
+
if (!ignoreCves) return;
|
|
360
|
+
auditConfig.ignoreGhsas ??= ignoreCves;
|
|
361
|
+
Reflect.deleteProperty(auditConfig, "ignoreCves");
|
|
362
|
+
warnings.push("auditConfig.ignoreCves was renamed to auditConfig.ignoreGhsas; replace each CVE ID with its corresponding GHSA ID.");
|
|
363
|
+
}
|
|
364
|
+
/**
|
|
365
|
+
* Resolve a removed Node.js runtime setting for migration to `package.json`.
|
|
366
|
+
*
|
|
367
|
+
* @param settings - Settings containing legacy runtime declarations
|
|
368
|
+
* @param warnings - Warning collection updated when declarations conflict
|
|
369
|
+
*
|
|
370
|
+
* @returns Runtime version to migrate, or `undefined` when not configured
|
|
371
|
+
*/
|
|
372
|
+
function resolveRuntimeVersion(settings, warnings) {
|
|
373
|
+
const { useNodeVersion } = settings;
|
|
374
|
+
const executionEnvNodeVersion = settings.executionEnv?.nodeVersion;
|
|
375
|
+
if (useNodeVersion && executionEnvNodeVersion && useNodeVersion !== executionEnvNodeVersion) warnings.push(`Both useNodeVersion (${useNodeVersion}) and executionEnv.nodeVersion (${executionEnvNodeVersion}) were found; useNodeVersion takes precedence.`);
|
|
376
|
+
return useNodeVersion || executionEnvNodeVersion;
|
|
377
|
+
}
|
|
378
|
+
/**
|
|
379
|
+
* Normalize settings according to the selected compatibility target.
|
|
380
|
+
*
|
|
381
|
+
* @param incomingSettings - Settings to normalize in place
|
|
382
|
+
* @param options - Compatibility target and normalization context
|
|
383
|
+
*
|
|
384
|
+
* @returns Normalization result, extracted runtime version, and warnings
|
|
385
|
+
*/
|
|
386
|
+
async function normalizeIncomingSettings(incomingSettings, options) {
|
|
387
|
+
const { compatibility, cwd, replaceDeprecated } = options;
|
|
388
|
+
const before = JSON.stringify(incomingSettings);
|
|
389
|
+
const warnings = [];
|
|
390
|
+
if (compatibility === "v10" && !replaceDeprecated) return {
|
|
391
|
+
changed: false,
|
|
392
|
+
warnings
|
|
393
|
+
};
|
|
394
|
+
if (incomingSettings.allowNonAppliedPatches !== void 0) incomingSettings.allowUnusedPatches ??= incomingSettings.allowNonAppliedPatches;
|
|
395
|
+
const allowBuildsFromLegacy = await collectAllowBuildsFromLegacy(incomingSettings, cwd);
|
|
396
|
+
if (allowBuildsFromLegacy) incomingSettings.allowBuilds = {
|
|
397
|
+
...allowBuildsFromLegacy,
|
|
398
|
+
...incomingSettings.allowBuilds || {}
|
|
399
|
+
};
|
|
400
|
+
if (compatibility === "v10") {
|
|
401
|
+
for (const key of PNPM_REPLACEABLE_IN_V10_SETTINGS) Reflect.deleteProperty(incomingSettings, key);
|
|
402
|
+
return {
|
|
403
|
+
changed: before !== JSON.stringify(incomingSettings),
|
|
404
|
+
warnings
|
|
405
|
+
};
|
|
274
406
|
}
|
|
407
|
+
const pmOnFail = resolvePmOnFail(incomingSettings);
|
|
408
|
+
if (pmOnFail) incomingSettings.pmOnFail = pmOnFail;
|
|
409
|
+
normalizeAuditConfig(incomingSettings, warnings);
|
|
410
|
+
const runtimeVersion = resolveRuntimeVersion(incomingSettings, warnings);
|
|
411
|
+
if (incomingSettings.ignoreDepScripts !== void 0) warnings.push("ignoreDepScripts was removed in pnpm v11 and has no direct replacement.");
|
|
412
|
+
if (incomingSettings.ignorePatchFailures !== void 0) warnings.push("ignorePatchFailures was removed in pnpm v11; patch failures now always throw.");
|
|
413
|
+
for (const key of PNPM_V11_REMOVED_SETTINGS) Reflect.deleteProperty(incomingSettings, key);
|
|
414
|
+
return {
|
|
415
|
+
changed: before !== JSON.stringify(incomingSettings),
|
|
416
|
+
runtimeVersion,
|
|
417
|
+
warnings
|
|
418
|
+
};
|
|
275
419
|
}
|
|
420
|
+
//#endregion
|
|
421
|
+
//#region src/utils/compatibility-runtime.ts
|
|
422
|
+
/**
|
|
423
|
+
* Move a removed Node.js runtime setting to `package.json#devEngines.runtime`.
|
|
424
|
+
*
|
|
425
|
+
* @param packageJson - Package manifest to update in place
|
|
426
|
+
* @param runtimeVersion - Node.js runtime version extracted from pnpm settings
|
|
427
|
+
*
|
|
428
|
+
* @returns Migration result indicating whether the manifest changed
|
|
429
|
+
*/
|
|
430
|
+
function migrateRuntimeToPackageJson(packageJson, runtimeVersion) {
|
|
431
|
+
if (!runtimeVersion) return { changed: false };
|
|
432
|
+
if (packageJson.devEngines?.runtime) return {
|
|
433
|
+
changed: false,
|
|
434
|
+
warning: "A devEngines.runtime declaration already exists; the removed pnpm Node.js runtime setting was not applied."
|
|
435
|
+
};
|
|
436
|
+
packageJson.devEngines = {
|
|
437
|
+
...packageJson.devEngines,
|
|
438
|
+
runtime: {
|
|
439
|
+
name: "node",
|
|
440
|
+
version: runtimeVersion
|
|
441
|
+
}
|
|
442
|
+
};
|
|
443
|
+
return { changed: true };
|
|
444
|
+
}
|
|
445
|
+
/**
|
|
446
|
+
* Select the runtime setting that wins under the configured merge strategy.
|
|
447
|
+
*
|
|
448
|
+
* @param existingVersion - Runtime version from existing workspace settings
|
|
449
|
+
* @param incomingVersion - Runtime version from incoming settings
|
|
450
|
+
* @param strategy - Merge strategy controlling value precedence
|
|
451
|
+
*
|
|
452
|
+
* @returns Selected runtime version, or `undefined` when neither source has one
|
|
453
|
+
*/
|
|
454
|
+
function resolveRuntimeVersionByStrategy(existingVersion, incomingVersion, strategy) {
|
|
455
|
+
if (strategy === "overwrite") return incomingVersion ?? existingVersion;
|
|
456
|
+
return existingVersion ?? incomingVersion;
|
|
457
|
+
}
|
|
458
|
+
//#endregion
|
|
459
|
+
//#region src/utils/compatibility-target.ts
|
|
460
|
+
/**
|
|
461
|
+
* pnpm major version that introduced the v11 settings schema.
|
|
462
|
+
*/
|
|
463
|
+
const PNPM_V11_MAJOR = 11;
|
|
464
|
+
/**
|
|
465
|
+
* pnpm major version that introduced the v12 compatibility target.
|
|
466
|
+
*/
|
|
467
|
+
const PNPM_V12_MAJOR = 12;
|
|
468
|
+
/**
|
|
469
|
+
* Resolve final compatibility target from user option and package manager hint.
|
|
470
|
+
*
|
|
471
|
+
* @param compatibility - Explicit or automatically detected compatibility target
|
|
472
|
+
* @param packageManager - `packageManager` declaration from `package.json`
|
|
473
|
+
* @param devPackageManager - Package manager declaration from `devEngines`
|
|
474
|
+
*
|
|
475
|
+
* @returns Resolved concrete compatibility target
|
|
476
|
+
*/
|
|
477
|
+
function resolveCompatibilityTarget(compatibility, packageManager, devPackageManager) {
|
|
478
|
+
if (compatibility !== "auto") return compatibility;
|
|
479
|
+
const match = (packageManager || (devPackageManager?.name === "pnpm" ? `pnpm@${devPackageManager.version}` : void 0))?.match(/^pnpm@[\s<=>^~]*(?<major>\d+)(?:\.|\s|$)/u);
|
|
480
|
+
const major = Number(match?.groups?.major);
|
|
481
|
+
if (major >= PNPM_V12_MAJOR) return "v12";
|
|
482
|
+
return major >= PNPM_V11_MAJOR ? "v11" : "v10";
|
|
483
|
+
}
|
|
484
|
+
//#endregion
|
|
485
|
+
//#region src/utils/merge.ts
|
|
276
486
|
/**
|
|
277
487
|
* Merge objects with priority to the first argument.
|
|
278
488
|
* Only adds keys from second argument that don't exist in first.
|
|
@@ -314,8 +524,43 @@ function mergeWithArrayDedupe(existing, incoming) {
|
|
|
314
524
|
}
|
|
315
525
|
return result;
|
|
316
526
|
}
|
|
527
|
+
/**
|
|
528
|
+
* Merge two objects based on the specified strategy.
|
|
529
|
+
*
|
|
530
|
+
* @param existing - Existing pnpm-workspace.yaml content
|
|
531
|
+
* @param incoming - New settings from package.json and .npmrc
|
|
532
|
+
* @param strategy - Merge strategy to use
|
|
533
|
+
*
|
|
534
|
+
* @returns Merged pnpm workspace configuration
|
|
535
|
+
*
|
|
536
|
+
* @example
|
|
537
|
+
* ```ts
|
|
538
|
+
* // Discard strategy - keep existing values
|
|
539
|
+
* mergeByStrategy({ packages: ['a'] }, { packages: ['b'] }, 'discard')
|
|
540
|
+
* // => { packages: ['a'] }
|
|
541
|
+
*
|
|
542
|
+
* // Merge strategy - combine arrays with deduplication
|
|
543
|
+
* mergeByStrategy({ packages: ['a'] }, { packages: ['b'] }, 'merge')
|
|
544
|
+
* // => { packages: ['a', 'b'] }
|
|
545
|
+
*
|
|
546
|
+
* // Overwrite strategy - use incoming values
|
|
547
|
+
* mergeByStrategy({ packages: ['a'] }, { packages: ['b'] }, 'overwrite')
|
|
548
|
+
* // => { packages: ['b'] }
|
|
549
|
+
* ```
|
|
550
|
+
*/
|
|
551
|
+
function mergeByStrategy(existing, incoming, strategy) {
|
|
552
|
+
switch (strategy) {
|
|
553
|
+
case "discard": return discardMerge(existing, incoming);
|
|
554
|
+
case "merge": return mergeWithArrayDedupe(existing, incoming);
|
|
555
|
+
case "overwrite": return discardMerge(incoming, existing);
|
|
556
|
+
default: return defu(existing, incoming);
|
|
557
|
+
}
|
|
558
|
+
}
|
|
317
559
|
//#endregion
|
|
318
560
|
//#region src/utils/npmrc.ts
|
|
561
|
+
/**
|
|
562
|
+
* Authentication and registry keys that must remain in `.npmrc`.
|
|
563
|
+
*/
|
|
319
564
|
const NPMRC_AUTH_OR_REGISTRY_KEYS = [
|
|
320
565
|
"_auth",
|
|
321
566
|
"_authtoken",
|
|
@@ -333,6 +578,48 @@ const NPMRC_AUTH_OR_REGISTRY_KEYS = [
|
|
|
333
578
|
"username"
|
|
334
579
|
];
|
|
335
580
|
/**
|
|
581
|
+
* Pattern for pnpm's channel-specific Node.js mirror keys.
|
|
582
|
+
*/
|
|
583
|
+
const NODE_MIRROR_KEY_PATTERN = /^node-mirror:(?<channel>.+)$/iu;
|
|
584
|
+
/**
|
|
585
|
+
* Normalize `.npmrc` key for case-insensitive matching.
|
|
586
|
+
*
|
|
587
|
+
* @param key - Raw `.npmrc` key
|
|
588
|
+
*
|
|
589
|
+
* @returns Trimmed lower-case key without an array suffix
|
|
590
|
+
*/
|
|
591
|
+
function normalizeNpmrcKey(key) {
|
|
592
|
+
return key.trim().replace(/\[\]$/u, "").toLowerCase();
|
|
593
|
+
}
|
|
594
|
+
/**
|
|
595
|
+
* Extract key name from a raw `.npmrc` line.
|
|
596
|
+
*
|
|
597
|
+
* @param line - Raw line from an `.npmrc` file
|
|
598
|
+
*
|
|
599
|
+
* @returns Normalized key, or `undefined` for comments and invalid lines
|
|
600
|
+
*/
|
|
601
|
+
function getNpmrcLineKey(line) {
|
|
602
|
+
const trimmed = line.trim();
|
|
603
|
+
if (!trimmed || trimmed.startsWith(";") || trimmed.startsWith("#")) return;
|
|
604
|
+
const index = trimmed.indexOf("=");
|
|
605
|
+
if (index <= 0) return;
|
|
606
|
+
return normalizeNpmrcKey(trimmed.slice(0, index));
|
|
607
|
+
}
|
|
608
|
+
/**
|
|
609
|
+
* Check whether a `.npmrc` key is auth/registry-related and should stay in
|
|
610
|
+
* pnpm v11 and newer.
|
|
611
|
+
*
|
|
612
|
+
* @param key - Raw or normalized `.npmrc` key
|
|
613
|
+
*
|
|
614
|
+
* @returns `true` when the key must remain in `.npmrc`
|
|
615
|
+
*/
|
|
616
|
+
function isNpmrcAuthOrRegistryKey(key) {
|
|
617
|
+
const normalized = normalizeNpmrcKey(key);
|
|
618
|
+
if (normalized.startsWith("//")) return true;
|
|
619
|
+
if (normalized === "registry" || normalized.endsWith(":registry")) return true;
|
|
620
|
+
return NPMRC_AUTH_OR_REGISTRY_KEYS.some((authKey) => normalized === authKey || normalized.endsWith(`:${authKey}`));
|
|
621
|
+
}
|
|
622
|
+
/**
|
|
336
623
|
* Remove pnpm-related settings from `.npmrc` file.
|
|
337
624
|
*
|
|
338
625
|
* This function reads the `.npmrc` file, filters out all lines that start with
|
|
@@ -340,6 +627,8 @@ const NPMRC_AUTH_OR_REGISTRY_KEYS = [
|
|
|
340
627
|
* writes the cleaned content back to the file.
|
|
341
628
|
*
|
|
342
629
|
* @param path - Absolute path to the `.npmrc` file
|
|
630
|
+
* @param compatibility - Concrete pnpm compatibility target
|
|
631
|
+
* @param migratedKeys - Original `.npmrc` keys that were migrated
|
|
343
632
|
*
|
|
344
633
|
* @returns A promise that resolves when the file has been pruned
|
|
345
634
|
*
|
|
@@ -351,87 +640,261 @@ const NPMRC_AUTH_OR_REGISTRY_KEYS = [
|
|
|
351
640
|
* ```
|
|
352
641
|
*/
|
|
353
642
|
async function pruneNpmrc(path, compatibility = "v10", migratedKeys = []) {
|
|
354
|
-
const pnpmSettingsFields = PNPM_SETTINGS_FIELDS.map((
|
|
643
|
+
const pnpmSettingsFields = PNPM_SETTINGS_FIELDS.map((field) => kebabCase(field));
|
|
355
644
|
const migratedKeySet = new Set(migratedKeys.map(normalizeNpmrcKey));
|
|
356
|
-
|
|
645
|
+
const updatedContent = (await fsReadFile(path)).split(/\r?\n/u).filter((line) => {
|
|
357
646
|
const key = getNpmrcLineKey(line);
|
|
358
647
|
if (!key) return true;
|
|
359
|
-
if (compatibility
|
|
360
|
-
return !pnpmSettingsFields.some((
|
|
361
|
-
}).join("\n"));
|
|
648
|
+
if (compatibility !== "v10") return !migratedKeySet.has(normalizeNpmrcKey(key));
|
|
649
|
+
return !pnpmSettingsFields.some((field) => key.startsWith(field));
|
|
650
|
+
}).join("\n").trimEnd();
|
|
651
|
+
if (compatibility !== "v10" && !updatedContent.trim()) {
|
|
652
|
+
await fsRemoveFile(path);
|
|
653
|
+
return;
|
|
654
|
+
}
|
|
655
|
+
await fsWriteFile(path, updatedContent);
|
|
362
656
|
}
|
|
363
657
|
/**
|
|
364
|
-
* Read
|
|
658
|
+
* Read `.npmrc` and return settings that should be migrated into workspace config.
|
|
365
659
|
*
|
|
366
|
-
*
|
|
367
|
-
*
|
|
368
|
-
*
|
|
660
|
+
* - `v10`: returns settings from the legacy whitelist.
|
|
661
|
+
* - `v11+`: excludes auth/registry keys because pnpm still reads them from
|
|
662
|
+
* `.npmrc`.
|
|
369
663
|
*
|
|
370
664
|
* @param path - Absolute path to the `.npmrc` file
|
|
665
|
+
* @param compatibility - Concrete pnpm compatibility target
|
|
371
666
|
*
|
|
372
|
-
* @returns
|
|
667
|
+
* @returns Migratable settings and their original `.npmrc` keys
|
|
373
668
|
*
|
|
374
669
|
* @throws {Error} When file reading or INI parsing fails
|
|
375
|
-
*
|
|
376
|
-
* @example
|
|
377
|
-
* ```ts
|
|
378
|
-
* const config = await readNpmrc('/path/to/.npmrc')
|
|
379
|
-
* // config.allowBuilds, config.packageExtensions, etc.
|
|
380
|
-
* ```
|
|
381
|
-
*/
|
|
382
|
-
/**
|
|
383
|
-
* Read `.npmrc` and return settings that should be migrated into workspace config.
|
|
384
|
-
*
|
|
385
|
-
* - `v10`: returns all keys (legacy whitelist filtering happens in caller).
|
|
386
|
-
* - `v11`: excludes auth/registry keys because pnpm still reads them from `.npmrc`.
|
|
387
670
|
*/
|
|
388
671
|
async function readMigratableNpmrc(path, compatibility) {
|
|
389
672
|
const raw = await readIniFile(path);
|
|
390
|
-
if (compatibility === "v10")
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
673
|
+
if (compatibility === "v10") {
|
|
674
|
+
const settings = camelcaseKeys(raw);
|
|
675
|
+
return {
|
|
676
|
+
keys: PNPM_SETTINGS_FIELDS,
|
|
677
|
+
settings: pick(settings, PNPM_SETTINGS_FIELDS)
|
|
678
|
+
};
|
|
679
|
+
}
|
|
394
680
|
const migratable = {};
|
|
395
681
|
const keys = [];
|
|
682
|
+
const nodeDownloadMirrors = {};
|
|
396
683
|
for (const [key, value] of Object.entries(raw)) if (!isNpmrcAuthOrRegistryKey(key)) {
|
|
397
|
-
migratable[key] = value;
|
|
398
684
|
keys.push(key);
|
|
685
|
+
const nodeMirrorMatch = normalizeNpmrcKey(key).match(NODE_MIRROR_KEY_PATTERN);
|
|
686
|
+
if (nodeMirrorMatch?.groups?.channel) nodeDownloadMirrors[nodeMirrorMatch.groups.channel] = String(value);
|
|
687
|
+
else migratable[key] = value;
|
|
399
688
|
}
|
|
689
|
+
const settings = camelcaseKeys(migratable);
|
|
690
|
+
if (Object.keys(nodeDownloadMirrors).length) settings.nodeDownloadMirrors = nodeDownloadMirrors;
|
|
400
691
|
return {
|
|
401
692
|
keys,
|
|
402
|
-
settings
|
|
693
|
+
settings
|
|
403
694
|
};
|
|
404
695
|
}
|
|
405
|
-
|
|
406
|
-
|
|
696
|
+
//#endregion
|
|
697
|
+
//#region src/utils/settings-change.ts
|
|
698
|
+
/**
|
|
699
|
+
* Collect changed root settings between two workspace configurations.
|
|
700
|
+
*
|
|
701
|
+
* @param before - Workspace settings before migration
|
|
702
|
+
* @param after - Workspace settings after migration
|
|
703
|
+
*
|
|
704
|
+
* @returns Changed root settings with their before and after values
|
|
705
|
+
*/
|
|
706
|
+
function collectSettingsChanges(before, after) {
|
|
707
|
+
const beforeSettings = { ...before };
|
|
708
|
+
const afterSettings = { ...after };
|
|
709
|
+
const keys = /* @__PURE__ */ new Set([...Object.keys(beforeSettings), ...Object.keys(afterSettings)]);
|
|
710
|
+
return Array.from(keys).filter((key) => !isDeepStrictEqual(beforeSettings[key], afterSettings[key])).map((key) => ({
|
|
711
|
+
after: afterSettings[key],
|
|
712
|
+
before: beforeSettings[key],
|
|
713
|
+
key
|
|
714
|
+
}));
|
|
407
715
|
}
|
|
408
716
|
/**
|
|
409
|
-
*
|
|
717
|
+
* Create a line-based diff using the longest common subsequence.
|
|
718
|
+
*
|
|
719
|
+
* @param beforeLines - YAML lines before migration
|
|
720
|
+
* @param afterLines - YAML lines after migration
|
|
721
|
+
*
|
|
722
|
+
* @returns Classified diff lines in display order
|
|
410
723
|
*/
|
|
411
|
-
function
|
|
412
|
-
const
|
|
413
|
-
|
|
414
|
-
const
|
|
415
|
-
|
|
416
|
-
|
|
724
|
+
function createLineDiff(beforeLines, afterLines) {
|
|
725
|
+
const lengths = Array.from({ length: beforeLines.length + 1 }, () => Array(afterLines.length + 1).fill(0));
|
|
726
|
+
for (let beforeIndex = beforeLines.length - 1; beforeIndex >= 0; beforeIndex--) for (let afterIndex = afterLines.length - 1; afterIndex >= 0; afterIndex--) lengths[beforeIndex][afterIndex] = beforeLines[beforeIndex] === afterLines[afterIndex] ? lengths[beforeIndex + 1][afterIndex + 1] + 1 : Math.max(lengths[beforeIndex + 1][afterIndex], lengths[beforeIndex][afterIndex + 1]);
|
|
727
|
+
const diff = [];
|
|
728
|
+
let beforeIndex = 0;
|
|
729
|
+
let afterIndex = 0;
|
|
730
|
+
while (beforeIndex < beforeLines.length && afterIndex < afterLines.length) if (beforeLines[beforeIndex] === afterLines[afterIndex]) {
|
|
731
|
+
diff.push({
|
|
732
|
+
kind: "unchanged",
|
|
733
|
+
value: beforeLines[beforeIndex]
|
|
734
|
+
});
|
|
735
|
+
beforeIndex++;
|
|
736
|
+
afterIndex++;
|
|
737
|
+
} else if (lengths[beforeIndex + 1][afterIndex] >= lengths[beforeIndex][afterIndex + 1]) {
|
|
738
|
+
diff.push({
|
|
739
|
+
kind: "removed",
|
|
740
|
+
value: beforeLines[beforeIndex]
|
|
741
|
+
});
|
|
742
|
+
beforeIndex++;
|
|
743
|
+
} else {
|
|
744
|
+
diff.push({
|
|
745
|
+
kind: "added",
|
|
746
|
+
value: afterLines[afterIndex]
|
|
747
|
+
});
|
|
748
|
+
afterIndex++;
|
|
749
|
+
}
|
|
750
|
+
while (beforeIndex < beforeLines.length) {
|
|
751
|
+
diff.push({
|
|
752
|
+
kind: "removed",
|
|
753
|
+
value: beforeLines[beforeIndex]
|
|
754
|
+
});
|
|
755
|
+
beforeIndex++;
|
|
756
|
+
}
|
|
757
|
+
while (afterIndex < afterLines.length) {
|
|
758
|
+
diff.push({
|
|
759
|
+
kind: "added",
|
|
760
|
+
value: afterLines[afterIndex]
|
|
761
|
+
});
|
|
762
|
+
afterIndex++;
|
|
763
|
+
}
|
|
764
|
+
return diff;
|
|
417
765
|
}
|
|
418
766
|
/**
|
|
419
|
-
*
|
|
767
|
+
* Format one root setting as YAML lines.
|
|
768
|
+
*
|
|
769
|
+
* @param key - Root setting key
|
|
770
|
+
* @param value - Setting value, or `undefined` when the key is absent
|
|
771
|
+
*
|
|
772
|
+
* @returns YAML lines for the setting, or an empty array when absent
|
|
420
773
|
*/
|
|
421
|
-
function
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
if (normalized === "registry" || normalized.endsWith(":registry")) return true;
|
|
425
|
-
return NPMRC_AUTH_OR_REGISTRY_KEYS.some((v) => normalized === v || normalized.endsWith(`:${v}`));
|
|
774
|
+
function formatSettingLines(key, value) {
|
|
775
|
+
if (value === void 0) return [];
|
|
776
|
+
return stringify({ [key]: value }).trimEnd().split("\n");
|
|
426
777
|
}
|
|
427
778
|
/**
|
|
428
|
-
*
|
|
779
|
+
* Format a setting change as a GitHub-style YAML diff.
|
|
780
|
+
*
|
|
781
|
+
* @param change - Root setting change to format
|
|
782
|
+
*
|
|
783
|
+
* @returns Classified YAML diff lines
|
|
429
784
|
*/
|
|
430
|
-
function
|
|
431
|
-
return key.
|
|
785
|
+
function createSettingsDiffLines(change) {
|
|
786
|
+
return createLineDiff(formatSettingLines(change.key, change.before), formatSettingLines(change.key, change.after));
|
|
787
|
+
}
|
|
788
|
+
/**
|
|
789
|
+
* Print a summary and GitHub-style YAML diff for every changed setting.
|
|
790
|
+
*
|
|
791
|
+
* @param changes - Root setting changes to report
|
|
792
|
+
*
|
|
793
|
+
* @returns Nothing; output is written through consola
|
|
794
|
+
*/
|
|
795
|
+
function reportSettingsChanges(changes) {
|
|
796
|
+
consola$1.info(`${changes.length} ${changes.length === 1 ? "setting" : "settings"} changed`);
|
|
797
|
+
for (const [index, change] of changes.entries()) {
|
|
798
|
+
if (index > 0) consola$1.log("");
|
|
799
|
+
for (const line of createSettingsDiffLines(change)) if (line.kind === "added") consola$1.log(green(`+ ${line.value}`));
|
|
800
|
+
else if (line.kind === "removed") consola$1.log(red(`- ${line.value}`));
|
|
801
|
+
else consola$1.log(dim(` ${line.value}`));
|
|
802
|
+
}
|
|
803
|
+
}
|
|
804
|
+
//#endregion
|
|
805
|
+
//#region src/utils/config.ts
|
|
806
|
+
/**
|
|
807
|
+
* Resolve the indentation width for a YAML document.
|
|
808
|
+
*
|
|
809
|
+
* @param content - Raw YAML content
|
|
810
|
+
*
|
|
811
|
+
* @returns Detected indentation width or the project default
|
|
812
|
+
*/
|
|
813
|
+
function resolveYamlIndent(content) {
|
|
814
|
+
const detectedIndent = detectIndent(content).amount;
|
|
815
|
+
return detectedIndent > 0 ? detectedIndent : 2;
|
|
816
|
+
}
|
|
817
|
+
/**
|
|
818
|
+
* Read and parse a package manifest when it exists.
|
|
819
|
+
*
|
|
820
|
+
* @param path - Absolute path to `package.json`
|
|
821
|
+
* @param exists - Whether the package manifest exists
|
|
822
|
+
*
|
|
823
|
+
* @returns Parsed package manifest and detected indentation
|
|
824
|
+
*
|
|
825
|
+
* @throws {SyntaxError} When the package manifest contains invalid JSON
|
|
826
|
+
*/
|
|
827
|
+
async function readPackageJson(path, exists) {
|
|
828
|
+
if (!exists) return {
|
|
829
|
+
indent: 2,
|
|
830
|
+
value: {}
|
|
831
|
+
};
|
|
832
|
+
const content = await fsReadFile(path);
|
|
833
|
+
return {
|
|
834
|
+
indent: detectIndent(content).indent,
|
|
835
|
+
value: JSON.parse(content)
|
|
836
|
+
};
|
|
837
|
+
}
|
|
838
|
+
/**
|
|
839
|
+
* Read and parse a pnpm workspace manifest when it exists.
|
|
840
|
+
*
|
|
841
|
+
* @param path - Absolute path to `pnpm-workspace.yaml`
|
|
842
|
+
* @param exists - Whether the workspace manifest exists
|
|
843
|
+
*
|
|
844
|
+
* @returns Parsed workspace settings and detected indentation
|
|
845
|
+
*
|
|
846
|
+
* @throws {Error} When the workspace manifest contains invalid YAML
|
|
847
|
+
*/
|
|
848
|
+
async function readPnpmWorkspace(path, exists) {
|
|
849
|
+
if (!exists) return {
|
|
850
|
+
indent: 2,
|
|
851
|
+
value: {}
|
|
852
|
+
};
|
|
853
|
+
const content = await fsReadFile(path);
|
|
854
|
+
return {
|
|
855
|
+
indent: resolveYamlIndent(content),
|
|
856
|
+
value: parse(content) ?? {}
|
|
857
|
+
};
|
|
858
|
+
}
|
|
859
|
+
/**
|
|
860
|
+
* Resolve migratable pnpm settings from a package manifest.
|
|
861
|
+
*
|
|
862
|
+
* Yarn resolutions are merged into pnpm overrides when enabled. Empty
|
|
863
|
+
* overrides are removed from the result.
|
|
864
|
+
*
|
|
865
|
+
* @param packageJson - Parsed package manifest
|
|
866
|
+
* @param yarnResolutions - Whether to convert Yarn resolutions to pnpm overrides
|
|
867
|
+
*
|
|
868
|
+
* @returns Pnpm workspace settings resolved from the package manifest
|
|
869
|
+
*/
|
|
870
|
+
function resolvePackageJsonSettings(packageJson, yarnResolutions) {
|
|
871
|
+
const pnpmSettings = yarnResolutions && packageJson.resolutions ? {
|
|
872
|
+
...packageJson.pnpm,
|
|
873
|
+
overrides: defu(packageJson.pnpm?.overrides, packageJson.resolutions)
|
|
874
|
+
} : { ...packageJson.pnpm };
|
|
875
|
+
if (pnpmSettings.overrides && !Object.keys(pnpmSettings.overrides).length) delete pnpmSettings.overrides;
|
|
876
|
+
return pnpmSettings;
|
|
432
877
|
}
|
|
433
878
|
//#endregion
|
|
434
879
|
//#region src/core.ts
|
|
880
|
+
function hasSettingsSources(npmrcExists, packageJsonExists, pnpmWorkspaceExists) {
|
|
881
|
+
if (!npmrcExists) consola$1.info(`${dim(NPMRC)} not found`);
|
|
882
|
+
if (!packageJsonExists) consola$1.info(`${dim(PACKAGE_JSON)} not found`);
|
|
883
|
+
if (npmrcExists || packageJsonExists || pnpmWorkspaceExists) return true;
|
|
884
|
+
consola$1.warn("No pnpm settings files to migrate");
|
|
885
|
+
return false;
|
|
886
|
+
}
|
|
887
|
+
function hasMigratableSettings(sources) {
|
|
888
|
+
const { existingSettingsChanged, packageJson, pnpmSettingsInNpmrc, yarnResolutions } = sources;
|
|
889
|
+
return Boolean(existingSettingsChanged || packageJson.pnpm || yarnResolutions && packageJson.resolutions || Object.keys(pnpmSettingsInNpmrc).length);
|
|
890
|
+
}
|
|
891
|
+
function assertCanMigrateRuntime(runtimeVersion, packageJsonExists) {
|
|
892
|
+
if (runtimeVersion && !packageJsonExists) throw new Error("Cannot migrate the removed Node.js runtime setting without a package.json file.");
|
|
893
|
+
}
|
|
894
|
+
function reportMigrationChanges(showChanges, before = {}, after = {}) {
|
|
895
|
+
if (!showChanges) return;
|
|
896
|
+
reportSettingsChanges(collectSettingsChanges(before, after));
|
|
897
|
+
}
|
|
435
898
|
/**
|
|
436
899
|
* Migrate pnpm settings from legacy locations to `pnpm-workspace.yaml`.
|
|
437
900
|
*
|
|
@@ -440,6 +903,7 @@ function normalizeNpmrcKey(key) {
|
|
|
440
903
|
* - `package.json` pnpm field
|
|
441
904
|
* - `.npmrc` pnpm-related settings
|
|
442
905
|
* - `package.json` resolutions (optional, converts to pnpm overrides)
|
|
906
|
+
* - deprecated settings already present in `pnpm-workspace.yaml`
|
|
443
907
|
*
|
|
444
908
|
* @param rawOptions - Migration options
|
|
445
909
|
* @param rawOptions.cwd - Current working directory (default: process.cwd())
|
|
@@ -448,6 +912,7 @@ function normalizeNpmrcKey(key) {
|
|
|
448
912
|
* @param rawOptions.yarnResolutions - Whether to migrate resolutions field (default: true)
|
|
449
913
|
* @param rawOptions.sortKeys - Whether to sort keys in output YAML (default: false)
|
|
450
914
|
* @param rawOptions.newlineBetween - Add newlines between root keys (default: true)
|
|
915
|
+
* @param rawOptions.showChanges - Show settings changes after migration (default: true)
|
|
451
916
|
*
|
|
452
917
|
* @returns A promise that resolves when migration is complete
|
|
453
918
|
*
|
|
@@ -477,53 +942,48 @@ async function migratePnpmSettings(rawOptions = {}) {
|
|
|
477
942
|
fsExists(packageJsonPath),
|
|
478
943
|
fsExists(pnpmWorkspaceYamlPath)
|
|
479
944
|
]);
|
|
480
|
-
if (!npmrcExists)
|
|
481
|
-
|
|
482
|
-
if (!npmrcExists && !packageJsonExists) {
|
|
483
|
-
consola$1.warn("No pnpm settings files to migrate");
|
|
945
|
+
if (!hasSettingsSources(npmrcExists, packageJsonExists, pnpmWorkspaceExists)) {
|
|
946
|
+
reportMigrationChanges(options.showChanges);
|
|
484
947
|
return;
|
|
485
948
|
}
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
if (packageJsonExists) {
|
|
491
|
-
const content = await fsReadFile(packageJsonPath);
|
|
492
|
-
packageJsonIndent = detectIndent(content).indent;
|
|
493
|
-
packageJsonObject = JSON.parse(content);
|
|
494
|
-
}
|
|
495
|
-
if (pnpmWorkspaceExists) {
|
|
496
|
-
const content = await fsReadFile(pnpmWorkspaceYamlPath);
|
|
497
|
-
pnpmWorkspaceYamlIndent = resolveYamlIndent(content);
|
|
498
|
-
pnpmWorkspaceYamlObject = parse(content) ?? {};
|
|
499
|
-
}
|
|
500
|
-
const compatibility = resolveCompatibilityTarget(options.compatibility, packageJsonObject.packageManager);
|
|
501
|
-
const npmrcMigratable = npmrcExists ? compatibility === "v10" ? {
|
|
502
|
-
keys: PNPM_SETTINGS_FIELDS,
|
|
503
|
-
settings: pick(await readNpmrc(npmrcPath), PNPM_SETTINGS_FIELDS)
|
|
504
|
-
} : await readMigratableNpmrc(npmrcPath, compatibility) : {
|
|
949
|
+
const [packageJson, pnpmWorkspace] = await Promise.all([readPackageJson(packageJsonPath, packageJsonExists), readPnpmWorkspace(pnpmWorkspaceYamlPath, pnpmWorkspaceExists)]);
|
|
950
|
+
const pnpmWorkspaceBefore = structuredClone(pnpmWorkspace.value);
|
|
951
|
+
const compatibility = resolveCompatibilityTarget(options.compatibility, packageJson.value.packageManager, packageJson.value.devEngines?.packageManager);
|
|
952
|
+
const npmrcMigratable = npmrcExists ? await readMigratableNpmrc(npmrcPath, compatibility) : {
|
|
505
953
|
keys: [],
|
|
506
954
|
settings: {}
|
|
507
955
|
};
|
|
508
956
|
const pnpmSettingsInNpmrc = npmrcMigratable.settings;
|
|
509
|
-
const
|
|
510
|
-
const hasResolutions = options.yarnResolutions && !!packageJsonObject.resolutions;
|
|
511
|
-
const hasNpmrcSettings = Object.keys(pnpmSettingsInNpmrc).length > 0;
|
|
512
|
-
if (!hasPnpmInPackageJson && !hasResolutions && !hasNpmrcSettings) {
|
|
513
|
-
consola$1.warn("No pnpm settings fields to migrate");
|
|
514
|
-
return;
|
|
515
|
-
}
|
|
516
|
-
const pnpmSettingsInPackageJson = options.yarnResolutions && packageJsonObject.resolutions ? {
|
|
517
|
-
...packageJsonObject.pnpm,
|
|
518
|
-
overrides: defu(packageJsonObject.pnpm?.overrides, packageJsonObject.resolutions)
|
|
519
|
-
} : { ...packageJsonObject.pnpm };
|
|
520
|
-
if (pnpmSettingsInPackageJson.overrides && !Object.keys(pnpmSettingsInPackageJson.overrides).length) delete pnpmSettingsInPackageJson.overrides;
|
|
957
|
+
const pnpmSettingsInPackageJson = resolvePackageJsonSettings(packageJson.value, options.yarnResolutions);
|
|
521
958
|
const incomingSettings = {
|
|
522
959
|
...pnpmSettingsInNpmrc,
|
|
523
960
|
...pnpmSettingsInPackageJson
|
|
524
961
|
};
|
|
525
|
-
normalizeIncomingSettings(
|
|
526
|
-
|
|
962
|
+
const [existingNormalization, incomingNormalization] = await Promise.all([normalizeIncomingSettings(pnpmWorkspace.value, {
|
|
963
|
+
compatibility,
|
|
964
|
+
cwd: options.cwd,
|
|
965
|
+
replaceDeprecated: options.replaceDeprecated
|
|
966
|
+
}), normalizeIncomingSettings(incomingSettings, {
|
|
967
|
+
compatibility,
|
|
968
|
+
cwd: options.cwd,
|
|
969
|
+
replaceDeprecated: options.replaceDeprecated
|
|
970
|
+
})]);
|
|
971
|
+
for (const warning of [...existingNormalization.warnings, ...incomingNormalization.warnings]) consola$1.warn(warning);
|
|
972
|
+
const runtimeVersion = resolveRuntimeVersionByStrategy(existingNormalization.runtimeVersion, incomingNormalization.runtimeVersion, options.strategy);
|
|
973
|
+
assertCanMigrateRuntime(runtimeVersion, packageJsonExists);
|
|
974
|
+
const runtimeMigration = migrateRuntimeToPackageJson(packageJson.value, runtimeVersion);
|
|
975
|
+
if (runtimeMigration.warning) consola$1.warn(runtimeMigration.warning);
|
|
976
|
+
if (!hasMigratableSettings({
|
|
977
|
+
existingSettingsChanged: existingNormalization.changed,
|
|
978
|
+
packageJson: packageJson.value,
|
|
979
|
+
pnpmSettingsInNpmrc,
|
|
980
|
+
yarnResolutions: options.yarnResolutions
|
|
981
|
+
})) {
|
|
982
|
+
consola$1.warn("No pnpm settings fields to migrate");
|
|
983
|
+
reportMigrationChanges(options.showChanges);
|
|
984
|
+
return;
|
|
985
|
+
}
|
|
986
|
+
const pnpmWorkspaceResult = mergeByStrategy(pnpmWorkspace.value, incomingSettings, options.strategy);
|
|
527
987
|
const yamlDocument = new Document({}, { sortMapEntries: options.sortKeys });
|
|
528
988
|
Object.entries(pnpmWorkspaceResult).forEach(([key, value]) => {
|
|
529
989
|
yamlDocument.add({
|
|
@@ -531,58 +991,29 @@ async function migratePnpmSettings(rawOptions = {}) {
|
|
|
531
991
|
value
|
|
532
992
|
});
|
|
533
993
|
});
|
|
534
|
-
const yamlContent = yamlDocument.toString({ indent:
|
|
535
|
-
await fsWriteFile(pnpmWorkspaceYamlPath, options.newlineBetween ? yamlContent.replace(/\n(?=[^\s#][^:\n]*:)/
|
|
994
|
+
const yamlContent = yamlDocument.toString({ indent: pnpmWorkspace.indent });
|
|
995
|
+
await fsWriteFile(pnpmWorkspaceYamlPath, options.newlineBetween ? yamlContent.replace(/\n(?=[^\s#][^:\n]*:)/gu, "\n\n") : yamlContent);
|
|
536
996
|
if (npmrcExists && options.cleanNpmrc) await pruneNpmrc(npmrcPath, compatibility, npmrcMigratable.keys);
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
if (
|
|
540
|
-
|
|
997
|
+
let packageJsonChanged = runtimeMigration.changed;
|
|
998
|
+
if (packageJsonExists && options.cleanPackageJson) {
|
|
999
|
+
if (packageJson.value.pnpm) packageJsonChanged = true;
|
|
1000
|
+
delete packageJson.value.pnpm;
|
|
1001
|
+
if (options.yarnResolutions && packageJson.value.resolutions) {
|
|
1002
|
+
packageJsonChanged = true;
|
|
1003
|
+
delete packageJson.value.resolutions;
|
|
1004
|
+
}
|
|
541
1005
|
}
|
|
1006
|
+
if (packageJsonExists && packageJsonChanged) await fsWriteFile(packageJsonPath, JSON.stringify(packageJson.value, null, packageJson.indent));
|
|
1007
|
+
reportMigrationChanges(options.showChanges, pnpmWorkspaceBefore, pnpmWorkspaceResult);
|
|
542
1008
|
} catch (err) {
|
|
543
1009
|
consola$1.error("Failed to migrate pnpm settings:", err);
|
|
544
1010
|
throw err;
|
|
545
1011
|
}
|
|
546
1012
|
}
|
|
547
|
-
/**
|
|
548
|
-
* Build v11 `allowBuilds` map from legacy build-script settings.
|
|
549
|
-
*/
|
|
550
|
-
function collectAllowBuildsFromLegacy(incomingSettings) {
|
|
551
|
-
const allowBuilds = {};
|
|
552
|
-
for (const name of incomingSettings.onlyBuiltDependencies || []) allowBuilds[name] = true;
|
|
553
|
-
for (const name of incomingSettings.ignoredBuiltDependencies || []) allowBuilds[name] = false;
|
|
554
|
-
for (const name of incomingSettings.neverBuiltDependencies || []) allowBuilds[name] = false;
|
|
555
|
-
return Object.keys(allowBuilds).length ? allowBuilds : void 0;
|
|
556
|
-
}
|
|
557
|
-
/**
|
|
558
|
-
* Normalize incoming settings according to compatibility target.
|
|
559
|
-
*/
|
|
560
|
-
function normalizeIncomingSettings(incomingSettings, compatibility, replaceDeprecated) {
|
|
561
|
-
if (compatibility === "v10" && !replaceDeprecated) return;
|
|
562
|
-
if (incomingSettings.allowNonAppliedPatches !== void 0) incomingSettings.allowUnusedPatches = incomingSettings.allowUnusedPatches ?? incomingSettings.allowNonAppliedPatches;
|
|
563
|
-
const allowBuildsFromLegacy = collectAllowBuildsFromLegacy(incomingSettings);
|
|
564
|
-
if (allowBuildsFromLegacy) incomingSettings.allowBuilds = {
|
|
565
|
-
...allowBuildsFromLegacy,
|
|
566
|
-
...incomingSettings.allowBuilds || {}
|
|
567
|
-
};
|
|
568
|
-
for (const key of PNPM_V11_REMOVED_SETTINGS) delete incomingSettings[key];
|
|
569
|
-
}
|
|
570
|
-
/**
|
|
571
|
-
* Resolve final compatibility target from user option and package manager hint.
|
|
572
|
-
*/
|
|
573
|
-
function resolveCompatibilityTarget(compatibility, packageManager) {
|
|
574
|
-
if (compatibility !== "auto") return compatibility;
|
|
575
|
-
const [, major] = (packageManager || "").match(/^pnpm@(\d+)(?:\.|$)/) || [];
|
|
576
|
-
return Number(major) >= 11 ? "v11" : "v10";
|
|
577
|
-
}
|
|
578
|
-
function resolveYamlIndent(content) {
|
|
579
|
-
const detectedIndent = detectIndent(content).amount;
|
|
580
|
-
return detectedIndent > 0 ? detectedIndent : 2;
|
|
581
|
-
}
|
|
582
1013
|
//#endregion
|
|
583
1014
|
//#region src/cli.ts
|
|
584
1015
|
const cli = cac(name);
|
|
585
|
-
cli.version(version).option("--cwd [cwd]", "Current working directory").option("--sort-keys", "Sort keys when write pnpm-workspace.yaml").option("--compatibility <compatibility>", "Compatibility target (auto, v10, v11)").option("--replace-deprecated", "Replace deprecated pnpm settings with new ones and remove old keys").option("--strategy <strategy>", "Strategy to handle conflicts (discard, merge, overwrite)").option("--no-yarn-resolutions", "Disable migrating resolutions field in package.json").option("--no-newline-between", "Disable adding newlines between each root keys").option("--no-clean-npmrc", "Disable removing pnpm settings in .npmrc file").option("--no-clean-package-json", "Disable removing pnpm fields in package.json").help();
|
|
1016
|
+
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, v12)").option("--replace-deprecated", "Replace deprecated pnpm settings with new ones and remove old keys").option("--strategy <strategy>", "Strategy to handle conflicts (discard, merge, overwrite)").option("--no-yarn-resolutions", "Disable migrating resolutions field in package.json").option("--no-newline-between", "Disable adding newlines between each root keys").option("--no-show-changes", "Disable showing settings changes").option("--no-clean-npmrc", "Disable removing pnpm settings in .npmrc file").option("--no-clean-package-json", "Disable removing pnpm fields in package.json").help();
|
|
586
1017
|
cli.command("").action(async (options) => {
|
|
587
1018
|
try {
|
|
588
1019
|
consola.log(`\n${bold(magenta(name))} ${dim(`v${version}`)}`);
|