pnpm-settings-migrator 0.7.0 → 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 +22 -0
- package/dist/cli.mjs +285 -32
- package/dist/index.d.mts +7 -0
- package/dist/index.mjs +279 -32
- package/package.json +4 -4
package/README.md
CHANGED
|
@@ -15,6 +15,20 @@ Run in your workspace root:
|
|
|
15
15
|
pnpm dlx pnpm-settings-migrator
|
|
16
16
|
```
|
|
17
17
|
|
|
18
|
+
After migration, the CLI reports how many root settings changed and shows a
|
|
19
|
+
GitHub-style YAML diff. Removed lines are red and added lines are green:
|
|
20
|
+
|
|
21
|
+
```text
|
|
22
|
+
ℹ 2 settings changed
|
|
23
|
+
packages:
|
|
24
|
+
- packages/*
|
|
25
|
+
+ - apps/*
|
|
26
|
+
|
|
27
|
+
overrides:
|
|
28
|
+
foo: 1.0.0
|
|
29
|
+
+ bar: 2.0.0
|
|
30
|
+
```
|
|
31
|
+
|
|
18
32
|
## CLI Options
|
|
19
33
|
|
|
20
34
|
### `--cwd`
|
|
@@ -113,6 +127,14 @@ Strategy to handle conflicts when merging settings with existing `pnpm-workspace
|
|
|
113
127
|
|
|
114
128
|
Disable migrating `resolutions` field in `package.json`.
|
|
115
129
|
|
|
130
|
+
### `--no-show-changes`
|
|
131
|
+
|
|
132
|
+
- **Type**: `boolean`
|
|
133
|
+
- **Default behavior**: `showChanges=true` (use this flag to disable)
|
|
134
|
+
|
|
135
|
+
Disable showing the settings diff after migration. Library consumers can set
|
|
136
|
+
`showChanges: false`.
|
|
137
|
+
|
|
116
138
|
### `--no-clean-npmrc`
|
|
117
139
|
|
|
118
140
|
- **Type**: `boolean`
|
package/dist/cli.mjs
CHANGED
|
@@ -2,7 +2,7 @@ import process from "node:process";
|
|
|
2
2
|
import { cac } from "cac";
|
|
3
3
|
import consola$1, { consola } from "consola";
|
|
4
4
|
import { resolve } from "pathe";
|
|
5
|
-
import { Document, parse } from "yaml";
|
|
5
|
+
import { Document, parse, stringify } from "yaml";
|
|
6
6
|
import { access, readFile, rm, writeFile } from "node:fs/promises";
|
|
7
7
|
import { getColor } from "consola/utils";
|
|
8
8
|
import { isPlainObject, isUndefined, pick } from "@ntnyq/utils";
|
|
@@ -10,10 +10,11 @@ import { defu } from "defu";
|
|
|
10
10
|
import camelcaseKeys from "camelcase-keys";
|
|
11
11
|
import { readIniFile } from "read-ini-file";
|
|
12
12
|
import { kebabCase } from "uncase";
|
|
13
|
+
import { isDeepStrictEqual } from "node:util";
|
|
13
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";
|
|
@@ -158,6 +159,7 @@ const DEFAULT_OPTIONS = {
|
|
|
158
159
|
cwd: process.cwd(),
|
|
159
160
|
newlineBetween: true,
|
|
160
161
|
replaceDeprecated: false,
|
|
162
|
+
showChanges: true,
|
|
161
163
|
sortKeys: false,
|
|
162
164
|
strategy: "merge",
|
|
163
165
|
yarnResolutions: true
|
|
@@ -210,6 +212,7 @@ function resolveOptions(options = {}) {
|
|
|
210
212
|
compatibility: resolveCompatibility(options.compatibility),
|
|
211
213
|
cwd: options.cwd ?? DEFAULT_OPTIONS.cwd,
|
|
212
214
|
newlineBetween: options.newlineBetween ?? DEFAULT_OPTIONS.newlineBetween,
|
|
215
|
+
showChanges: options.showChanges ?? DEFAULT_OPTIONS.showChanges,
|
|
213
216
|
sortKeys: options.sortKeys ?? DEFAULT_OPTIONS.sortKeys,
|
|
214
217
|
strategy: resolveStrategy(options.strategy),
|
|
215
218
|
yarnResolutions: options.yarnResolutions ?? DEFAULT_OPTIONS.yarnResolutions,
|
|
@@ -220,45 +223,73 @@ function resolveOptions(options = {}) {
|
|
|
220
223
|
//#endregion
|
|
221
224
|
//#region src/utils/fs.ts
|
|
222
225
|
/**
|
|
223
|
-
* Check
|
|
224
|
-
*
|
|
225
|
-
* @
|
|
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`
|
|
226
231
|
*/
|
|
227
232
|
async function fsExists(path) {
|
|
228
233
|
return access(path).then(() => true).catch(() => false);
|
|
229
234
|
}
|
|
230
235
|
/**
|
|
231
|
-
* Read file
|
|
232
|
-
*
|
|
233
|
-
* @
|
|
236
|
+
* Read a UTF-8 text file.
|
|
237
|
+
*
|
|
238
|
+
* @param path - Filesystem path to read
|
|
239
|
+
*
|
|
240
|
+
* @returns Text content of the file
|
|
234
241
|
*/
|
|
235
242
|
async function fsReadFile(path) {
|
|
236
243
|
return await readFile(path, "utf-8");
|
|
237
244
|
}
|
|
238
245
|
/**
|
|
239
246
|
* Remove a file if it exists.
|
|
240
|
-
*
|
|
247
|
+
*
|
|
248
|
+
* @param path - Filesystem path to remove
|
|
249
|
+
*
|
|
250
|
+
* @returns A promise that resolves after the file is removed
|
|
241
251
|
*/
|
|
242
252
|
async function fsRemoveFile(path) {
|
|
243
253
|
await rm(path, { force: true });
|
|
244
254
|
}
|
|
245
255
|
/**
|
|
246
|
-
* Write file
|
|
247
|
-
*
|
|
248
|
-
* @param
|
|
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
|
|
249
262
|
*/
|
|
250
263
|
async function fsWriteFile(path, content) {
|
|
251
264
|
await writeFile(path, `${content.trimEnd()}\n`, "utf-8");
|
|
252
265
|
}
|
|
253
266
|
getColor("cyan");
|
|
254
267
|
getColor("yellow");
|
|
268
|
+
/**
|
|
269
|
+
* Apply dim terminal styling to a value.
|
|
270
|
+
*/
|
|
255
271
|
const dim = getColor("dim");
|
|
272
|
+
/**
|
|
273
|
+
* Apply green terminal styling to a value.
|
|
274
|
+
*/
|
|
256
275
|
const green = getColor("green");
|
|
276
|
+
/**
|
|
277
|
+
* Apply red terminal styling to a value.
|
|
278
|
+
*/
|
|
257
279
|
const red = getColor("red");
|
|
280
|
+
/**
|
|
281
|
+
* Apply bold terminal styling to a value.
|
|
282
|
+
*/
|
|
258
283
|
const bold = getColor("bold");
|
|
284
|
+
/**
|
|
285
|
+
* Apply magenta terminal styling to a value.
|
|
286
|
+
*/
|
|
259
287
|
const magenta = getColor("magenta");
|
|
260
288
|
//#endregion
|
|
261
289
|
//#region src/utils/compatibility.ts
|
|
290
|
+
/**
|
|
291
|
+
* Legacy build settings that can be replaced for pnpm v10.
|
|
292
|
+
*/
|
|
262
293
|
const PNPM_REPLACEABLE_IN_V10_SETTINGS = [
|
|
263
294
|
"allowNonAppliedPatches",
|
|
264
295
|
"ignoredBuiltDependencies",
|
|
@@ -268,6 +299,14 @@ const PNPM_REPLACEABLE_IN_V10_SETTINGS = [
|
|
|
268
299
|
];
|
|
269
300
|
/**
|
|
270
301
|
* Read packages listed by the legacy `onlyBuiltDependenciesFile` setting.
|
|
302
|
+
*
|
|
303
|
+
* @param cwd - Working directory used to resolve the referenced file
|
|
304
|
+
* @param file - Relative path stored in `onlyBuiltDependenciesFile`
|
|
305
|
+
*
|
|
306
|
+
* @returns Package names read from the referenced JSON file
|
|
307
|
+
*
|
|
308
|
+
* @throws {TypeError} When the referenced JSON value is not a string array
|
|
309
|
+
* @throws {SyntaxError} When the referenced file contains invalid JSON
|
|
271
310
|
*/
|
|
272
311
|
async function readOnlyBuiltDependenciesFile(cwd, file) {
|
|
273
312
|
if (!file) return [];
|
|
@@ -278,6 +317,11 @@ async function readOnlyBuiltDependenciesFile(cwd, file) {
|
|
|
278
317
|
}
|
|
279
318
|
/**
|
|
280
319
|
* Build an `allowBuilds` map from legacy build-script settings.
|
|
320
|
+
*
|
|
321
|
+
* @param incomingSettings - Settings containing legacy build declarations
|
|
322
|
+
* @param cwd - Working directory used to resolve referenced files
|
|
323
|
+
*
|
|
324
|
+
* @returns Package-to-permission map, or `undefined` when no entries exist
|
|
281
325
|
*/
|
|
282
326
|
async function collectAllowBuildsFromLegacy(incomingSettings, cwd) {
|
|
283
327
|
const allowBuilds = {};
|
|
@@ -289,6 +333,10 @@ async function collectAllowBuildsFromLegacy(incomingSettings, cwd) {
|
|
|
289
333
|
}
|
|
290
334
|
/**
|
|
291
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
|
|
292
340
|
*/
|
|
293
341
|
function resolvePmOnFail(settings) {
|
|
294
342
|
if (settings.pmOnFail) return settings.pmOnFail;
|
|
@@ -299,6 +347,11 @@ function resolvePmOnFail(settings) {
|
|
|
299
347
|
}
|
|
300
348
|
/**
|
|
301
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
|
|
302
355
|
*/
|
|
303
356
|
function normalizeAuditConfig(settings, warnings) {
|
|
304
357
|
const { auditConfig } = settings;
|
|
@@ -310,6 +363,11 @@ function normalizeAuditConfig(settings, warnings) {
|
|
|
310
363
|
}
|
|
311
364
|
/**
|
|
312
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
|
|
313
371
|
*/
|
|
314
372
|
function resolveRuntimeVersion(settings, warnings) {
|
|
315
373
|
const { useNodeVersion } = settings;
|
|
@@ -319,6 +377,11 @@ function resolveRuntimeVersion(settings, warnings) {
|
|
|
319
377
|
}
|
|
320
378
|
/**
|
|
321
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
|
|
322
385
|
*/
|
|
323
386
|
async function normalizeIncomingSettings(incomingSettings, options) {
|
|
324
387
|
const { compatibility, cwd, replaceDeprecated } = options;
|
|
@@ -354,8 +417,15 @@ async function normalizeIncomingSettings(incomingSettings, options) {
|
|
|
354
417
|
warnings
|
|
355
418
|
};
|
|
356
419
|
}
|
|
420
|
+
//#endregion
|
|
421
|
+
//#region src/utils/compatibility-runtime.ts
|
|
357
422
|
/**
|
|
358
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
|
|
359
429
|
*/
|
|
360
430
|
function migrateRuntimeToPackageJson(packageJson, runtimeVersion) {
|
|
361
431
|
if (!runtimeVersion) return { changed: false };
|
|
@@ -374,6 +444,12 @@ function migrateRuntimeToPackageJson(packageJson, runtimeVersion) {
|
|
|
374
444
|
}
|
|
375
445
|
/**
|
|
376
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
|
|
377
453
|
*/
|
|
378
454
|
function resolveRuntimeVersionByStrategy(existingVersion, incomingVersion, strategy) {
|
|
379
455
|
if (strategy === "overwrite") return incomingVersion ?? existingVersion;
|
|
@@ -381,10 +457,22 @@ function resolveRuntimeVersionByStrategy(existingVersion, incomingVersion, strat
|
|
|
381
457
|
}
|
|
382
458
|
//#endregion
|
|
383
459
|
//#region src/utils/compatibility-target.ts
|
|
460
|
+
/**
|
|
461
|
+
* pnpm major version that introduced the v11 settings schema.
|
|
462
|
+
*/
|
|
384
463
|
const PNPM_V11_MAJOR = 11;
|
|
464
|
+
/**
|
|
465
|
+
* pnpm major version that introduced the v12 compatibility target.
|
|
466
|
+
*/
|
|
385
467
|
const PNPM_V12_MAJOR = 12;
|
|
386
468
|
/**
|
|
387
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
|
|
388
476
|
*/
|
|
389
477
|
function resolveCompatibilityTarget(compatibility, packageManager, devPackageManager) {
|
|
390
478
|
if (compatibility !== "auto") return compatibility;
|
|
@@ -470,6 +558,9 @@ function mergeByStrategy(existing, incoming, strategy) {
|
|
|
470
558
|
}
|
|
471
559
|
//#endregion
|
|
472
560
|
//#region src/utils/npmrc.ts
|
|
561
|
+
/**
|
|
562
|
+
* Authentication and registry keys that must remain in `.npmrc`.
|
|
563
|
+
*/
|
|
473
564
|
const NPMRC_AUTH_OR_REGISTRY_KEYS = [
|
|
474
565
|
"_auth",
|
|
475
566
|
"_authtoken",
|
|
@@ -486,15 +577,26 @@ const NPMRC_AUTH_OR_REGISTRY_KEYS = [
|
|
|
486
577
|
"tokenhelper",
|
|
487
578
|
"username"
|
|
488
579
|
];
|
|
580
|
+
/**
|
|
581
|
+
* Pattern for pnpm's channel-specific Node.js mirror keys.
|
|
582
|
+
*/
|
|
489
583
|
const NODE_MIRROR_KEY_PATTERN = /^node-mirror:(?<channel>.+)$/iu;
|
|
490
584
|
/**
|
|
491
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
|
|
492
590
|
*/
|
|
493
591
|
function normalizeNpmrcKey(key) {
|
|
494
592
|
return key.trim().replace(/\[\]$/u, "").toLowerCase();
|
|
495
593
|
}
|
|
496
594
|
/**
|
|
497
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
|
|
498
600
|
*/
|
|
499
601
|
function getNpmrcLineKey(line) {
|
|
500
602
|
const trimmed = line.trim();
|
|
@@ -506,6 +608,10 @@ function getNpmrcLineKey(line) {
|
|
|
506
608
|
/**
|
|
507
609
|
* Check whether a `.npmrc` key is auth/registry-related and should stay in
|
|
508
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`
|
|
509
615
|
*/
|
|
510
616
|
function isNpmrcAuthOrRegistryKey(key) {
|
|
511
617
|
const normalized = normalizeNpmrcKey(key);
|
|
@@ -521,6 +627,8 @@ function isNpmrcAuthOrRegistryKey(key) {
|
|
|
521
627
|
* writes the cleaned content back to the file.
|
|
522
628
|
*
|
|
523
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
|
|
524
632
|
*
|
|
525
633
|
* @returns A promise that resolves when the file has been pruned
|
|
526
634
|
*
|
|
@@ -547,30 +655,18 @@ async function pruneNpmrc(path, compatibility = "v10", migratedKeys = []) {
|
|
|
547
655
|
await fsWriteFile(path, updatedContent);
|
|
548
656
|
}
|
|
549
657
|
/**
|
|
550
|
-
* Read
|
|
658
|
+
* Read `.npmrc` and return settings that should be migrated into workspace config.
|
|
551
659
|
*
|
|
552
|
-
*
|
|
553
|
-
*
|
|
554
|
-
*
|
|
660
|
+
* - `v10`: returns settings from the legacy whitelist.
|
|
661
|
+
* - `v11+`: excludes auth/registry keys because pnpm still reads them from
|
|
662
|
+
* `.npmrc`.
|
|
555
663
|
*
|
|
556
664
|
* @param path - Absolute path to the `.npmrc` file
|
|
665
|
+
* @param compatibility - Concrete pnpm compatibility target
|
|
557
666
|
*
|
|
558
|
-
* @returns
|
|
667
|
+
* @returns Migratable settings and their original `.npmrc` keys
|
|
559
668
|
*
|
|
560
669
|
* @throws {Error} When file reading or INI parsing fails
|
|
561
|
-
*
|
|
562
|
-
* @example
|
|
563
|
-
* ```ts
|
|
564
|
-
* const config = await readNpmrc('/path/to/.npmrc')
|
|
565
|
-
* // config.allowBuilds, config.packageExtensions, etc.
|
|
566
|
-
* ```
|
|
567
|
-
*/
|
|
568
|
-
/**
|
|
569
|
-
* Read `.npmrc` and return settings that should be migrated into workspace config.
|
|
570
|
-
*
|
|
571
|
-
* - `v10`: returns settings from the legacy whitelist.
|
|
572
|
-
* - `v11+`: excludes auth/registry keys because pnpm still reads them from
|
|
573
|
-
* `.npmrc`.
|
|
574
670
|
*/
|
|
575
671
|
async function readMigratableNpmrc(path, compatibility) {
|
|
576
672
|
const raw = await readIniFile(path);
|
|
@@ -598,11 +694,136 @@ async function readMigratableNpmrc(path, compatibility) {
|
|
|
598
694
|
};
|
|
599
695
|
}
|
|
600
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
|
+
}));
|
|
715
|
+
}
|
|
716
|
+
/**
|
|
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
|
|
723
|
+
*/
|
|
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;
|
|
765
|
+
}
|
|
766
|
+
/**
|
|
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
|
|
773
|
+
*/
|
|
774
|
+
function formatSettingLines(key, value) {
|
|
775
|
+
if (value === void 0) return [];
|
|
776
|
+
return stringify({ [key]: value }).trimEnd().split("\n");
|
|
777
|
+
}
|
|
778
|
+
/**
|
|
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
|
|
784
|
+
*/
|
|
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
|
|
601
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
|
+
*/
|
|
602
813
|
function resolveYamlIndent(content) {
|
|
603
814
|
const detectedIndent = detectIndent(content).amount;
|
|
604
815
|
return detectedIndent > 0 ? detectedIndent : 2;
|
|
605
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
|
+
*/
|
|
606
827
|
async function readPackageJson(path, exists) {
|
|
607
828
|
if (!exists) return {
|
|
608
829
|
indent: 2,
|
|
@@ -614,6 +835,16 @@ async function readPackageJson(path, exists) {
|
|
|
614
835
|
value: JSON.parse(content)
|
|
615
836
|
};
|
|
616
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
|
+
*/
|
|
617
848
|
async function readPnpmWorkspace(path, exists) {
|
|
618
849
|
if (!exists) return {
|
|
619
850
|
indent: 2,
|
|
@@ -625,6 +856,17 @@ async function readPnpmWorkspace(path, exists) {
|
|
|
625
856
|
value: parse(content) ?? {}
|
|
626
857
|
};
|
|
627
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
|
+
*/
|
|
628
870
|
function resolvePackageJsonSettings(packageJson, yarnResolutions) {
|
|
629
871
|
const pnpmSettings = yarnResolutions && packageJson.resolutions ? {
|
|
630
872
|
...packageJson.pnpm,
|
|
@@ -649,6 +891,10 @@ function hasMigratableSettings(sources) {
|
|
|
649
891
|
function assertCanMigrateRuntime(runtimeVersion, packageJsonExists) {
|
|
650
892
|
if (runtimeVersion && !packageJsonExists) throw new Error("Cannot migrate the removed Node.js runtime setting without a package.json file.");
|
|
651
893
|
}
|
|
894
|
+
function reportMigrationChanges(showChanges, before = {}, after = {}) {
|
|
895
|
+
if (!showChanges) return;
|
|
896
|
+
reportSettingsChanges(collectSettingsChanges(before, after));
|
|
897
|
+
}
|
|
652
898
|
/**
|
|
653
899
|
* Migrate pnpm settings from legacy locations to `pnpm-workspace.yaml`.
|
|
654
900
|
*
|
|
@@ -666,6 +912,7 @@ function assertCanMigrateRuntime(runtimeVersion, packageJsonExists) {
|
|
|
666
912
|
* @param rawOptions.yarnResolutions - Whether to migrate resolutions field (default: true)
|
|
667
913
|
* @param rawOptions.sortKeys - Whether to sort keys in output YAML (default: false)
|
|
668
914
|
* @param rawOptions.newlineBetween - Add newlines between root keys (default: true)
|
|
915
|
+
* @param rawOptions.showChanges - Show settings changes after migration (default: true)
|
|
669
916
|
*
|
|
670
917
|
* @returns A promise that resolves when migration is complete
|
|
671
918
|
*
|
|
@@ -695,8 +942,12 @@ async function migratePnpmSettings(rawOptions = {}) {
|
|
|
695
942
|
fsExists(packageJsonPath),
|
|
696
943
|
fsExists(pnpmWorkspaceYamlPath)
|
|
697
944
|
]);
|
|
698
|
-
if (!hasSettingsSources(npmrcExists, packageJsonExists, pnpmWorkspaceExists))
|
|
945
|
+
if (!hasSettingsSources(npmrcExists, packageJsonExists, pnpmWorkspaceExists)) {
|
|
946
|
+
reportMigrationChanges(options.showChanges);
|
|
947
|
+
return;
|
|
948
|
+
}
|
|
699
949
|
const [packageJson, pnpmWorkspace] = await Promise.all([readPackageJson(packageJsonPath, packageJsonExists), readPnpmWorkspace(pnpmWorkspaceYamlPath, pnpmWorkspaceExists)]);
|
|
950
|
+
const pnpmWorkspaceBefore = structuredClone(pnpmWorkspace.value);
|
|
700
951
|
const compatibility = resolveCompatibilityTarget(options.compatibility, packageJson.value.packageManager, packageJson.value.devEngines?.packageManager);
|
|
701
952
|
const npmrcMigratable = npmrcExists ? await readMigratableNpmrc(npmrcPath, compatibility) : {
|
|
702
953
|
keys: [],
|
|
@@ -729,6 +980,7 @@ async function migratePnpmSettings(rawOptions = {}) {
|
|
|
729
980
|
yarnResolutions: options.yarnResolutions
|
|
730
981
|
})) {
|
|
731
982
|
consola$1.warn("No pnpm settings fields to migrate");
|
|
983
|
+
reportMigrationChanges(options.showChanges);
|
|
732
984
|
return;
|
|
733
985
|
}
|
|
734
986
|
const pnpmWorkspaceResult = mergeByStrategy(pnpmWorkspace.value, incomingSettings, options.strategy);
|
|
@@ -752,6 +1004,7 @@ async function migratePnpmSettings(rawOptions = {}) {
|
|
|
752
1004
|
}
|
|
753
1005
|
}
|
|
754
1006
|
if (packageJsonExists && packageJsonChanged) await fsWriteFile(packageJsonPath, JSON.stringify(packageJson.value, null, packageJson.indent));
|
|
1007
|
+
reportMigrationChanges(options.showChanges, pnpmWorkspaceBefore, pnpmWorkspaceResult);
|
|
755
1008
|
} catch (err) {
|
|
756
1009
|
consola$1.error("Failed to migrate pnpm settings:", err);
|
|
757
1010
|
throw err;
|
|
@@ -760,7 +1013,7 @@ async function migratePnpmSettings(rawOptions = {}) {
|
|
|
760
1013
|
//#endregion
|
|
761
1014
|
//#region src/cli.ts
|
|
762
1015
|
const cli = cac(name);
|
|
763
|
-
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-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();
|
|
764
1017
|
cli.command("").action(async (options) => {
|
|
765
1018
|
try {
|
|
766
1019
|
consola.log(`\n${bold(magenta(name))} ${dim(`v${version}`)}`);
|
package/dist/index.d.mts
CHANGED
|
@@ -51,6 +51,12 @@ interface Options {
|
|
|
51
51
|
* @default false
|
|
52
52
|
*/
|
|
53
53
|
replaceDeprecated?: boolean;
|
|
54
|
+
/**
|
|
55
|
+
* Whether to show a settings diff after migration
|
|
56
|
+
*
|
|
57
|
+
* @default true
|
|
58
|
+
*/
|
|
59
|
+
showChanges?: boolean;
|
|
54
60
|
/**
|
|
55
61
|
* Sort keys when write `pnpm-workspace.yaml`
|
|
56
62
|
*
|
|
@@ -227,6 +233,7 @@ type PnpmWorkspace = PnpmSettings & PnpmSettingsDeprecated & PnpmSettingsV11 & P
|
|
|
227
233
|
* @param rawOptions.yarnResolutions - Whether to migrate resolutions field (default: true)
|
|
228
234
|
* @param rawOptions.sortKeys - Whether to sort keys in output YAML (default: false)
|
|
229
235
|
* @param rawOptions.newlineBetween - Add newlines between root keys (default: true)
|
|
236
|
+
* @param rawOptions.showChanges - Show settings changes after migration (default: true)
|
|
230
237
|
*
|
|
231
238
|
* @returns A promise that resolves when migration is complete
|
|
232
239
|
*
|
package/dist/index.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import consola from "consola";
|
|
2
2
|
import { resolve } from "pathe";
|
|
3
|
-
import { Document, parse } from "yaml";
|
|
3
|
+
import { Document, parse, stringify } from "yaml";
|
|
4
4
|
import process from "node:process";
|
|
5
5
|
import { access, readFile, rm, writeFile } from "node:fs/promises";
|
|
6
6
|
import { getColor } from "consola/utils";
|
|
@@ -9,6 +9,7 @@ import { defu } from "defu";
|
|
|
9
9
|
import camelcaseKeys from "camelcase-keys";
|
|
10
10
|
import { readIniFile } from "read-ini-file";
|
|
11
11
|
import { kebabCase } from "uncase";
|
|
12
|
+
import { isDeepStrictEqual } from "node:util";
|
|
12
13
|
import detectIndent from "detect-indent";
|
|
13
14
|
//#region src/constants.ts
|
|
14
15
|
const NPMRC = ".npmrc";
|
|
@@ -153,6 +154,7 @@ const DEFAULT_OPTIONS = {
|
|
|
153
154
|
cwd: process.cwd(),
|
|
154
155
|
newlineBetween: true,
|
|
155
156
|
replaceDeprecated: false,
|
|
157
|
+
showChanges: true,
|
|
156
158
|
sortKeys: false,
|
|
157
159
|
strategy: "merge",
|
|
158
160
|
yarnResolutions: true
|
|
@@ -205,6 +207,7 @@ function resolveOptions(options = {}) {
|
|
|
205
207
|
compatibility: resolveCompatibility(options.compatibility),
|
|
206
208
|
cwd: options.cwd ?? DEFAULT_OPTIONS.cwd,
|
|
207
209
|
newlineBetween: options.newlineBetween ?? DEFAULT_OPTIONS.newlineBetween,
|
|
210
|
+
showChanges: options.showChanges ?? DEFAULT_OPTIONS.showChanges,
|
|
208
211
|
sortKeys: options.sortKeys ?? DEFAULT_OPTIONS.sortKeys,
|
|
209
212
|
strategy: resolveStrategy(options.strategy),
|
|
210
213
|
yarnResolutions: options.yarnResolutions ?? DEFAULT_OPTIONS.yarnResolutions,
|
|
@@ -215,45 +218,67 @@ function resolveOptions(options = {}) {
|
|
|
215
218
|
//#endregion
|
|
216
219
|
//#region src/utils/fs.ts
|
|
217
220
|
/**
|
|
218
|
-
* Check
|
|
219
|
-
*
|
|
220
|
-
* @
|
|
221
|
+
* Check whether a filesystem path exists.
|
|
222
|
+
*
|
|
223
|
+
* @param path - Filesystem path to check
|
|
224
|
+
*
|
|
225
|
+
* @returns `true` when the path is accessible, otherwise `false`
|
|
221
226
|
*/
|
|
222
227
|
async function fsExists(path) {
|
|
223
228
|
return access(path).then(() => true).catch(() => false);
|
|
224
229
|
}
|
|
225
230
|
/**
|
|
226
|
-
* Read file
|
|
227
|
-
*
|
|
228
|
-
* @
|
|
231
|
+
* Read a UTF-8 text file.
|
|
232
|
+
*
|
|
233
|
+
* @param path - Filesystem path to read
|
|
234
|
+
*
|
|
235
|
+
* @returns Text content of the file
|
|
229
236
|
*/
|
|
230
237
|
async function fsReadFile(path) {
|
|
231
238
|
return await readFile(path, "utf-8");
|
|
232
239
|
}
|
|
233
240
|
/**
|
|
234
241
|
* Remove a file if it exists.
|
|
235
|
-
*
|
|
242
|
+
*
|
|
243
|
+
* @param path - Filesystem path to remove
|
|
244
|
+
*
|
|
245
|
+
* @returns A promise that resolves after the file is removed
|
|
236
246
|
*/
|
|
237
247
|
async function fsRemoveFile(path) {
|
|
238
248
|
await rm(path, { force: true });
|
|
239
249
|
}
|
|
240
250
|
/**
|
|
241
|
-
* Write file
|
|
242
|
-
*
|
|
243
|
-
* @param
|
|
251
|
+
* Write a normalized UTF-8 text file with one trailing newline.
|
|
252
|
+
*
|
|
253
|
+
* @param path - Filesystem path to write
|
|
254
|
+
* @param content - Text content to write
|
|
255
|
+
*
|
|
256
|
+
* @returns A promise that resolves after the file is written
|
|
244
257
|
*/
|
|
245
258
|
async function fsWriteFile(path, content) {
|
|
246
259
|
await writeFile(path, `${content.trimEnd()}\n`, "utf-8");
|
|
247
260
|
}
|
|
248
261
|
getColor("cyan");
|
|
249
262
|
getColor("yellow");
|
|
263
|
+
/**
|
|
264
|
+
* Apply dim terminal styling to a value.
|
|
265
|
+
*/
|
|
250
266
|
const dim = getColor("dim");
|
|
251
|
-
|
|
252
|
-
|
|
267
|
+
/**
|
|
268
|
+
* Apply green terminal styling to a value.
|
|
269
|
+
*/
|
|
270
|
+
const green = getColor("green");
|
|
271
|
+
/**
|
|
272
|
+
* Apply red terminal styling to a value.
|
|
273
|
+
*/
|
|
274
|
+
const red = getColor("red");
|
|
253
275
|
getColor("bold");
|
|
254
276
|
getColor("magenta");
|
|
255
277
|
//#endregion
|
|
256
278
|
//#region src/utils/compatibility.ts
|
|
279
|
+
/**
|
|
280
|
+
* Legacy build settings that can be replaced for pnpm v10.
|
|
281
|
+
*/
|
|
257
282
|
const PNPM_REPLACEABLE_IN_V10_SETTINGS = [
|
|
258
283
|
"allowNonAppliedPatches",
|
|
259
284
|
"ignoredBuiltDependencies",
|
|
@@ -263,6 +288,14 @@ const PNPM_REPLACEABLE_IN_V10_SETTINGS = [
|
|
|
263
288
|
];
|
|
264
289
|
/**
|
|
265
290
|
* Read packages listed by the legacy `onlyBuiltDependenciesFile` setting.
|
|
291
|
+
*
|
|
292
|
+
* @param cwd - Working directory used to resolve the referenced file
|
|
293
|
+
* @param file - Relative path stored in `onlyBuiltDependenciesFile`
|
|
294
|
+
*
|
|
295
|
+
* @returns Package names read from the referenced JSON file
|
|
296
|
+
*
|
|
297
|
+
* @throws {TypeError} When the referenced JSON value is not a string array
|
|
298
|
+
* @throws {SyntaxError} When the referenced file contains invalid JSON
|
|
266
299
|
*/
|
|
267
300
|
async function readOnlyBuiltDependenciesFile(cwd, file) {
|
|
268
301
|
if (!file) return [];
|
|
@@ -273,6 +306,11 @@ async function readOnlyBuiltDependenciesFile(cwd, file) {
|
|
|
273
306
|
}
|
|
274
307
|
/**
|
|
275
308
|
* Build an `allowBuilds` map from legacy build-script settings.
|
|
309
|
+
*
|
|
310
|
+
* @param incomingSettings - Settings containing legacy build declarations
|
|
311
|
+
* @param cwd - Working directory used to resolve referenced files
|
|
312
|
+
*
|
|
313
|
+
* @returns Package-to-permission map, or `undefined` when no entries exist
|
|
276
314
|
*/
|
|
277
315
|
async function collectAllowBuildsFromLegacy(incomingSettings, cwd) {
|
|
278
316
|
const allowBuilds = {};
|
|
@@ -284,6 +322,10 @@ async function collectAllowBuildsFromLegacy(incomingSettings, cwd) {
|
|
|
284
322
|
}
|
|
285
323
|
/**
|
|
286
324
|
* Resolve the pnpm v11 replacement for legacy package-manager settings.
|
|
325
|
+
*
|
|
326
|
+
* @param settings - Settings containing legacy package-manager declarations
|
|
327
|
+
*
|
|
328
|
+
* @returns Resolved `pmOnFail` behavior, or `undefined` when not configured
|
|
287
329
|
*/
|
|
288
330
|
function resolvePmOnFail(settings) {
|
|
289
331
|
if (settings.pmOnFail) return settings.pmOnFail;
|
|
@@ -294,6 +336,11 @@ function resolvePmOnFail(settings) {
|
|
|
294
336
|
}
|
|
295
337
|
/**
|
|
296
338
|
* Rename legacy audit settings while preserving values for manual ID updates.
|
|
339
|
+
*
|
|
340
|
+
* @param settings - Settings whose audit configuration may be normalized
|
|
341
|
+
* @param warnings - Warning collection updated with manual follow-up guidance
|
|
342
|
+
*
|
|
343
|
+
* @returns Nothing; the settings and warning collection are mutated in place
|
|
297
344
|
*/
|
|
298
345
|
function normalizeAuditConfig(settings, warnings) {
|
|
299
346
|
const { auditConfig } = settings;
|
|
@@ -305,6 +352,11 @@ function normalizeAuditConfig(settings, warnings) {
|
|
|
305
352
|
}
|
|
306
353
|
/**
|
|
307
354
|
* Resolve a removed Node.js runtime setting for migration to `package.json`.
|
|
355
|
+
*
|
|
356
|
+
* @param settings - Settings containing legacy runtime declarations
|
|
357
|
+
* @param warnings - Warning collection updated when declarations conflict
|
|
358
|
+
*
|
|
359
|
+
* @returns Runtime version to migrate, or `undefined` when not configured
|
|
308
360
|
*/
|
|
309
361
|
function resolveRuntimeVersion(settings, warnings) {
|
|
310
362
|
const { useNodeVersion } = settings;
|
|
@@ -314,6 +366,11 @@ function resolveRuntimeVersion(settings, warnings) {
|
|
|
314
366
|
}
|
|
315
367
|
/**
|
|
316
368
|
* Normalize settings according to the selected compatibility target.
|
|
369
|
+
*
|
|
370
|
+
* @param incomingSettings - Settings to normalize in place
|
|
371
|
+
* @param options - Compatibility target and normalization context
|
|
372
|
+
*
|
|
373
|
+
* @returns Normalization result, extracted runtime version, and warnings
|
|
317
374
|
*/
|
|
318
375
|
async function normalizeIncomingSettings(incomingSettings, options) {
|
|
319
376
|
const { compatibility, cwd, replaceDeprecated } = options;
|
|
@@ -349,8 +406,15 @@ async function normalizeIncomingSettings(incomingSettings, options) {
|
|
|
349
406
|
warnings
|
|
350
407
|
};
|
|
351
408
|
}
|
|
409
|
+
//#endregion
|
|
410
|
+
//#region src/utils/compatibility-runtime.ts
|
|
352
411
|
/**
|
|
353
412
|
* Move a removed Node.js runtime setting to `package.json#devEngines.runtime`.
|
|
413
|
+
*
|
|
414
|
+
* @param packageJson - Package manifest to update in place
|
|
415
|
+
* @param runtimeVersion - Node.js runtime version extracted from pnpm settings
|
|
416
|
+
*
|
|
417
|
+
* @returns Migration result indicating whether the manifest changed
|
|
354
418
|
*/
|
|
355
419
|
function migrateRuntimeToPackageJson(packageJson, runtimeVersion) {
|
|
356
420
|
if (!runtimeVersion) return { changed: false };
|
|
@@ -369,6 +433,12 @@ function migrateRuntimeToPackageJson(packageJson, runtimeVersion) {
|
|
|
369
433
|
}
|
|
370
434
|
/**
|
|
371
435
|
* Select the runtime setting that wins under the configured merge strategy.
|
|
436
|
+
*
|
|
437
|
+
* @param existingVersion - Runtime version from existing workspace settings
|
|
438
|
+
* @param incomingVersion - Runtime version from incoming settings
|
|
439
|
+
* @param strategy - Merge strategy controlling value precedence
|
|
440
|
+
*
|
|
441
|
+
* @returns Selected runtime version, or `undefined` when neither source has one
|
|
372
442
|
*/
|
|
373
443
|
function resolveRuntimeVersionByStrategy(existingVersion, incomingVersion, strategy) {
|
|
374
444
|
if (strategy === "overwrite") return incomingVersion ?? existingVersion;
|
|
@@ -376,10 +446,22 @@ function resolveRuntimeVersionByStrategy(existingVersion, incomingVersion, strat
|
|
|
376
446
|
}
|
|
377
447
|
//#endregion
|
|
378
448
|
//#region src/utils/compatibility-target.ts
|
|
449
|
+
/**
|
|
450
|
+
* pnpm major version that introduced the v11 settings schema.
|
|
451
|
+
*/
|
|
379
452
|
const PNPM_V11_MAJOR = 11;
|
|
453
|
+
/**
|
|
454
|
+
* pnpm major version that introduced the v12 compatibility target.
|
|
455
|
+
*/
|
|
380
456
|
const PNPM_V12_MAJOR = 12;
|
|
381
457
|
/**
|
|
382
458
|
* Resolve final compatibility target from user option and package manager hint.
|
|
459
|
+
*
|
|
460
|
+
* @param compatibility - Explicit or automatically detected compatibility target
|
|
461
|
+
* @param packageManager - `packageManager` declaration from `package.json`
|
|
462
|
+
* @param devPackageManager - Package manager declaration from `devEngines`
|
|
463
|
+
*
|
|
464
|
+
* @returns Resolved concrete compatibility target
|
|
383
465
|
*/
|
|
384
466
|
function resolveCompatibilityTarget(compatibility, packageManager, devPackageManager) {
|
|
385
467
|
if (compatibility !== "auto") return compatibility;
|
|
@@ -465,6 +547,9 @@ function mergeByStrategy(existing, incoming, strategy) {
|
|
|
465
547
|
}
|
|
466
548
|
//#endregion
|
|
467
549
|
//#region src/utils/npmrc.ts
|
|
550
|
+
/**
|
|
551
|
+
* Authentication and registry keys that must remain in `.npmrc`.
|
|
552
|
+
*/
|
|
468
553
|
const NPMRC_AUTH_OR_REGISTRY_KEYS = [
|
|
469
554
|
"_auth",
|
|
470
555
|
"_authtoken",
|
|
@@ -481,15 +566,26 @@ const NPMRC_AUTH_OR_REGISTRY_KEYS = [
|
|
|
481
566
|
"tokenhelper",
|
|
482
567
|
"username"
|
|
483
568
|
];
|
|
569
|
+
/**
|
|
570
|
+
* Pattern for pnpm's channel-specific Node.js mirror keys.
|
|
571
|
+
*/
|
|
484
572
|
const NODE_MIRROR_KEY_PATTERN = /^node-mirror:(?<channel>.+)$/iu;
|
|
485
573
|
/**
|
|
486
574
|
* Normalize `.npmrc` key for case-insensitive matching.
|
|
575
|
+
*
|
|
576
|
+
* @param key - Raw `.npmrc` key
|
|
577
|
+
*
|
|
578
|
+
* @returns Trimmed lower-case key without an array suffix
|
|
487
579
|
*/
|
|
488
580
|
function normalizeNpmrcKey(key) {
|
|
489
581
|
return key.trim().replace(/\[\]$/u, "").toLowerCase();
|
|
490
582
|
}
|
|
491
583
|
/**
|
|
492
584
|
* Extract key name from a raw `.npmrc` line.
|
|
585
|
+
*
|
|
586
|
+
* @param line - Raw line from an `.npmrc` file
|
|
587
|
+
*
|
|
588
|
+
* @returns Normalized key, or `undefined` for comments and invalid lines
|
|
493
589
|
*/
|
|
494
590
|
function getNpmrcLineKey(line) {
|
|
495
591
|
const trimmed = line.trim();
|
|
@@ -501,6 +597,10 @@ function getNpmrcLineKey(line) {
|
|
|
501
597
|
/**
|
|
502
598
|
* Check whether a `.npmrc` key is auth/registry-related and should stay in
|
|
503
599
|
* pnpm v11 and newer.
|
|
600
|
+
*
|
|
601
|
+
* @param key - Raw or normalized `.npmrc` key
|
|
602
|
+
*
|
|
603
|
+
* @returns `true` when the key must remain in `.npmrc`
|
|
504
604
|
*/
|
|
505
605
|
function isNpmrcAuthOrRegistryKey(key) {
|
|
506
606
|
const normalized = normalizeNpmrcKey(key);
|
|
@@ -516,6 +616,8 @@ function isNpmrcAuthOrRegistryKey(key) {
|
|
|
516
616
|
* writes the cleaned content back to the file.
|
|
517
617
|
*
|
|
518
618
|
* @param path - Absolute path to the `.npmrc` file
|
|
619
|
+
* @param compatibility - Concrete pnpm compatibility target
|
|
620
|
+
* @param migratedKeys - Original `.npmrc` keys that were migrated
|
|
519
621
|
*
|
|
520
622
|
* @returns A promise that resolves when the file has been pruned
|
|
521
623
|
*
|
|
@@ -542,30 +644,18 @@ async function pruneNpmrc(path, compatibility = "v10", migratedKeys = []) {
|
|
|
542
644
|
await fsWriteFile(path, updatedContent);
|
|
543
645
|
}
|
|
544
646
|
/**
|
|
545
|
-
* Read
|
|
647
|
+
* Read `.npmrc` and return settings that should be migrated into workspace config.
|
|
546
648
|
*
|
|
547
|
-
*
|
|
548
|
-
*
|
|
549
|
-
*
|
|
649
|
+
* - `v10`: returns settings from the legacy whitelist.
|
|
650
|
+
* - `v11+`: excludes auth/registry keys because pnpm still reads them from
|
|
651
|
+
* `.npmrc`.
|
|
550
652
|
*
|
|
551
653
|
* @param path - Absolute path to the `.npmrc` file
|
|
654
|
+
* @param compatibility - Concrete pnpm compatibility target
|
|
552
655
|
*
|
|
553
|
-
* @returns
|
|
656
|
+
* @returns Migratable settings and their original `.npmrc` keys
|
|
554
657
|
*
|
|
555
658
|
* @throws {Error} When file reading or INI parsing fails
|
|
556
|
-
*
|
|
557
|
-
* @example
|
|
558
|
-
* ```ts
|
|
559
|
-
* const config = await readNpmrc('/path/to/.npmrc')
|
|
560
|
-
* // config.allowBuilds, config.packageExtensions, etc.
|
|
561
|
-
* ```
|
|
562
|
-
*/
|
|
563
|
-
/**
|
|
564
|
-
* Read `.npmrc` and return settings that should be migrated into workspace config.
|
|
565
|
-
*
|
|
566
|
-
* - `v10`: returns settings from the legacy whitelist.
|
|
567
|
-
* - `v11+`: excludes auth/registry keys because pnpm still reads them from
|
|
568
|
-
* `.npmrc`.
|
|
569
659
|
*/
|
|
570
660
|
async function readMigratableNpmrc(path, compatibility) {
|
|
571
661
|
const raw = await readIniFile(path);
|
|
@@ -593,11 +683,136 @@ async function readMigratableNpmrc(path, compatibility) {
|
|
|
593
683
|
};
|
|
594
684
|
}
|
|
595
685
|
//#endregion
|
|
686
|
+
//#region src/utils/settings-change.ts
|
|
687
|
+
/**
|
|
688
|
+
* Collect changed root settings between two workspace configurations.
|
|
689
|
+
*
|
|
690
|
+
* @param before - Workspace settings before migration
|
|
691
|
+
* @param after - Workspace settings after migration
|
|
692
|
+
*
|
|
693
|
+
* @returns Changed root settings with their before and after values
|
|
694
|
+
*/
|
|
695
|
+
function collectSettingsChanges(before, after) {
|
|
696
|
+
const beforeSettings = { ...before };
|
|
697
|
+
const afterSettings = { ...after };
|
|
698
|
+
const keys = /* @__PURE__ */ new Set([...Object.keys(beforeSettings), ...Object.keys(afterSettings)]);
|
|
699
|
+
return Array.from(keys).filter((key) => !isDeepStrictEqual(beforeSettings[key], afterSettings[key])).map((key) => ({
|
|
700
|
+
after: afterSettings[key],
|
|
701
|
+
before: beforeSettings[key],
|
|
702
|
+
key
|
|
703
|
+
}));
|
|
704
|
+
}
|
|
705
|
+
/**
|
|
706
|
+
* Create a line-based diff using the longest common subsequence.
|
|
707
|
+
*
|
|
708
|
+
* @param beforeLines - YAML lines before migration
|
|
709
|
+
* @param afterLines - YAML lines after migration
|
|
710
|
+
*
|
|
711
|
+
* @returns Classified diff lines in display order
|
|
712
|
+
*/
|
|
713
|
+
function createLineDiff(beforeLines, afterLines) {
|
|
714
|
+
const lengths = Array.from({ length: beforeLines.length + 1 }, () => Array(afterLines.length + 1).fill(0));
|
|
715
|
+
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]);
|
|
716
|
+
const diff = [];
|
|
717
|
+
let beforeIndex = 0;
|
|
718
|
+
let afterIndex = 0;
|
|
719
|
+
while (beforeIndex < beforeLines.length && afterIndex < afterLines.length) if (beforeLines[beforeIndex] === afterLines[afterIndex]) {
|
|
720
|
+
diff.push({
|
|
721
|
+
kind: "unchanged",
|
|
722
|
+
value: beforeLines[beforeIndex]
|
|
723
|
+
});
|
|
724
|
+
beforeIndex++;
|
|
725
|
+
afterIndex++;
|
|
726
|
+
} else if (lengths[beforeIndex + 1][afterIndex] >= lengths[beforeIndex][afterIndex + 1]) {
|
|
727
|
+
diff.push({
|
|
728
|
+
kind: "removed",
|
|
729
|
+
value: beforeLines[beforeIndex]
|
|
730
|
+
});
|
|
731
|
+
beforeIndex++;
|
|
732
|
+
} else {
|
|
733
|
+
diff.push({
|
|
734
|
+
kind: "added",
|
|
735
|
+
value: afterLines[afterIndex]
|
|
736
|
+
});
|
|
737
|
+
afterIndex++;
|
|
738
|
+
}
|
|
739
|
+
while (beforeIndex < beforeLines.length) {
|
|
740
|
+
diff.push({
|
|
741
|
+
kind: "removed",
|
|
742
|
+
value: beforeLines[beforeIndex]
|
|
743
|
+
});
|
|
744
|
+
beforeIndex++;
|
|
745
|
+
}
|
|
746
|
+
while (afterIndex < afterLines.length) {
|
|
747
|
+
diff.push({
|
|
748
|
+
kind: "added",
|
|
749
|
+
value: afterLines[afterIndex]
|
|
750
|
+
});
|
|
751
|
+
afterIndex++;
|
|
752
|
+
}
|
|
753
|
+
return diff;
|
|
754
|
+
}
|
|
755
|
+
/**
|
|
756
|
+
* Format one root setting as YAML lines.
|
|
757
|
+
*
|
|
758
|
+
* @param key - Root setting key
|
|
759
|
+
* @param value - Setting value, or `undefined` when the key is absent
|
|
760
|
+
*
|
|
761
|
+
* @returns YAML lines for the setting, or an empty array when absent
|
|
762
|
+
*/
|
|
763
|
+
function formatSettingLines(key, value) {
|
|
764
|
+
if (value === void 0) return [];
|
|
765
|
+
return stringify({ [key]: value }).trimEnd().split("\n");
|
|
766
|
+
}
|
|
767
|
+
/**
|
|
768
|
+
* Format a setting change as a GitHub-style YAML diff.
|
|
769
|
+
*
|
|
770
|
+
* @param change - Root setting change to format
|
|
771
|
+
*
|
|
772
|
+
* @returns Classified YAML diff lines
|
|
773
|
+
*/
|
|
774
|
+
function createSettingsDiffLines(change) {
|
|
775
|
+
return createLineDiff(formatSettingLines(change.key, change.before), formatSettingLines(change.key, change.after));
|
|
776
|
+
}
|
|
777
|
+
/**
|
|
778
|
+
* Print a summary and GitHub-style YAML diff for every changed setting.
|
|
779
|
+
*
|
|
780
|
+
* @param changes - Root setting changes to report
|
|
781
|
+
*
|
|
782
|
+
* @returns Nothing; output is written through consola
|
|
783
|
+
*/
|
|
784
|
+
function reportSettingsChanges(changes) {
|
|
785
|
+
consola.info(`${changes.length} ${changes.length === 1 ? "setting" : "settings"} changed`);
|
|
786
|
+
for (const [index, change] of changes.entries()) {
|
|
787
|
+
if (index > 0) consola.log("");
|
|
788
|
+
for (const line of createSettingsDiffLines(change)) if (line.kind === "added") consola.log(green(`+ ${line.value}`));
|
|
789
|
+
else if (line.kind === "removed") consola.log(red(`- ${line.value}`));
|
|
790
|
+
else consola.log(dim(` ${line.value}`));
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
//#endregion
|
|
596
794
|
//#region src/utils/config.ts
|
|
795
|
+
/**
|
|
796
|
+
* Resolve the indentation width for a YAML document.
|
|
797
|
+
*
|
|
798
|
+
* @param content - Raw YAML content
|
|
799
|
+
*
|
|
800
|
+
* @returns Detected indentation width or the project default
|
|
801
|
+
*/
|
|
597
802
|
function resolveYamlIndent(content) {
|
|
598
803
|
const detectedIndent = detectIndent(content).amount;
|
|
599
804
|
return detectedIndent > 0 ? detectedIndent : 2;
|
|
600
805
|
}
|
|
806
|
+
/**
|
|
807
|
+
* Read and parse a package manifest when it exists.
|
|
808
|
+
*
|
|
809
|
+
* @param path - Absolute path to `package.json`
|
|
810
|
+
* @param exists - Whether the package manifest exists
|
|
811
|
+
*
|
|
812
|
+
* @returns Parsed package manifest and detected indentation
|
|
813
|
+
*
|
|
814
|
+
* @throws {SyntaxError} When the package manifest contains invalid JSON
|
|
815
|
+
*/
|
|
601
816
|
async function readPackageJson(path, exists) {
|
|
602
817
|
if (!exists) return {
|
|
603
818
|
indent: 2,
|
|
@@ -609,6 +824,16 @@ async function readPackageJson(path, exists) {
|
|
|
609
824
|
value: JSON.parse(content)
|
|
610
825
|
};
|
|
611
826
|
}
|
|
827
|
+
/**
|
|
828
|
+
* Read and parse a pnpm workspace manifest when it exists.
|
|
829
|
+
*
|
|
830
|
+
* @param path - Absolute path to `pnpm-workspace.yaml`
|
|
831
|
+
* @param exists - Whether the workspace manifest exists
|
|
832
|
+
*
|
|
833
|
+
* @returns Parsed workspace settings and detected indentation
|
|
834
|
+
*
|
|
835
|
+
* @throws {Error} When the workspace manifest contains invalid YAML
|
|
836
|
+
*/
|
|
612
837
|
async function readPnpmWorkspace(path, exists) {
|
|
613
838
|
if (!exists) return {
|
|
614
839
|
indent: 2,
|
|
@@ -620,6 +845,17 @@ async function readPnpmWorkspace(path, exists) {
|
|
|
620
845
|
value: parse(content) ?? {}
|
|
621
846
|
};
|
|
622
847
|
}
|
|
848
|
+
/**
|
|
849
|
+
* Resolve migratable pnpm settings from a package manifest.
|
|
850
|
+
*
|
|
851
|
+
* Yarn resolutions are merged into pnpm overrides when enabled. Empty
|
|
852
|
+
* overrides are removed from the result.
|
|
853
|
+
*
|
|
854
|
+
* @param packageJson - Parsed package manifest
|
|
855
|
+
* @param yarnResolutions - Whether to convert Yarn resolutions to pnpm overrides
|
|
856
|
+
*
|
|
857
|
+
* @returns Pnpm workspace settings resolved from the package manifest
|
|
858
|
+
*/
|
|
623
859
|
function resolvePackageJsonSettings(packageJson, yarnResolutions) {
|
|
624
860
|
const pnpmSettings = yarnResolutions && packageJson.resolutions ? {
|
|
625
861
|
...packageJson.pnpm,
|
|
@@ -644,6 +880,10 @@ function hasMigratableSettings(sources) {
|
|
|
644
880
|
function assertCanMigrateRuntime(runtimeVersion, packageJsonExists) {
|
|
645
881
|
if (runtimeVersion && !packageJsonExists) throw new Error("Cannot migrate the removed Node.js runtime setting without a package.json file.");
|
|
646
882
|
}
|
|
883
|
+
function reportMigrationChanges(showChanges, before = {}, after = {}) {
|
|
884
|
+
if (!showChanges) return;
|
|
885
|
+
reportSettingsChanges(collectSettingsChanges(before, after));
|
|
886
|
+
}
|
|
647
887
|
/**
|
|
648
888
|
* Migrate pnpm settings from legacy locations to `pnpm-workspace.yaml`.
|
|
649
889
|
*
|
|
@@ -661,6 +901,7 @@ function assertCanMigrateRuntime(runtimeVersion, packageJsonExists) {
|
|
|
661
901
|
* @param rawOptions.yarnResolutions - Whether to migrate resolutions field (default: true)
|
|
662
902
|
* @param rawOptions.sortKeys - Whether to sort keys in output YAML (default: false)
|
|
663
903
|
* @param rawOptions.newlineBetween - Add newlines between root keys (default: true)
|
|
904
|
+
* @param rawOptions.showChanges - Show settings changes after migration (default: true)
|
|
664
905
|
*
|
|
665
906
|
* @returns A promise that resolves when migration is complete
|
|
666
907
|
*
|
|
@@ -690,8 +931,12 @@ async function migratePnpmSettings(rawOptions = {}) {
|
|
|
690
931
|
fsExists(packageJsonPath),
|
|
691
932
|
fsExists(pnpmWorkspaceYamlPath)
|
|
692
933
|
]);
|
|
693
|
-
if (!hasSettingsSources(npmrcExists, packageJsonExists, pnpmWorkspaceExists))
|
|
934
|
+
if (!hasSettingsSources(npmrcExists, packageJsonExists, pnpmWorkspaceExists)) {
|
|
935
|
+
reportMigrationChanges(options.showChanges);
|
|
936
|
+
return;
|
|
937
|
+
}
|
|
694
938
|
const [packageJson, pnpmWorkspace] = await Promise.all([readPackageJson(packageJsonPath, packageJsonExists), readPnpmWorkspace(pnpmWorkspaceYamlPath, pnpmWorkspaceExists)]);
|
|
939
|
+
const pnpmWorkspaceBefore = structuredClone(pnpmWorkspace.value);
|
|
695
940
|
const compatibility = resolveCompatibilityTarget(options.compatibility, packageJson.value.packageManager, packageJson.value.devEngines?.packageManager);
|
|
696
941
|
const npmrcMigratable = npmrcExists ? await readMigratableNpmrc(npmrcPath, compatibility) : {
|
|
697
942
|
keys: [],
|
|
@@ -724,6 +969,7 @@ async function migratePnpmSettings(rawOptions = {}) {
|
|
|
724
969
|
yarnResolutions: options.yarnResolutions
|
|
725
970
|
})) {
|
|
726
971
|
consola.warn("No pnpm settings fields to migrate");
|
|
972
|
+
reportMigrationChanges(options.showChanges);
|
|
727
973
|
return;
|
|
728
974
|
}
|
|
729
975
|
const pnpmWorkspaceResult = mergeByStrategy(pnpmWorkspace.value, incomingSettings, options.strategy);
|
|
@@ -747,6 +993,7 @@ async function migratePnpmSettings(rawOptions = {}) {
|
|
|
747
993
|
}
|
|
748
994
|
}
|
|
749
995
|
if (packageJsonExists && packageJsonChanged) await fsWriteFile(packageJsonPath, JSON.stringify(packageJson.value, null, packageJson.indent));
|
|
996
|
+
reportMigrationChanges(options.showChanges, pnpmWorkspaceBefore, pnpmWorkspaceResult);
|
|
750
997
|
} catch (err) {
|
|
751
998
|
consola.error("Failed to migrate pnpm settings:", err);
|
|
752
999
|
throw err;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pnpm-settings-migrator",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.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",
|
|
@@ -52,13 +52,13 @@
|
|
|
52
52
|
"yaml": "^2.9.0"
|
|
53
53
|
},
|
|
54
54
|
"devDependencies": {
|
|
55
|
-
"@types/node": "^26.
|
|
55
|
+
"@types/node": "^26.3.0",
|
|
56
56
|
"bumpp": "^12.2.1",
|
|
57
57
|
"husky": "^9.1.7",
|
|
58
58
|
"nano-staged": "^1.0.2",
|
|
59
59
|
"npm-run-all2": "^9.0.3",
|
|
60
|
-
"oxfmt": "^0.
|
|
61
|
-
"oxlint": "^1.
|
|
60
|
+
"oxfmt": "^0.65.0",
|
|
61
|
+
"oxlint": "^1.80.0",
|
|
62
62
|
"tsdown": "^0.22.14",
|
|
63
63
|
"typescript": "^7.0.2",
|
|
64
64
|
"vitest": "^4.1.11"
|