pnpm-settings-migrator 0.8.0 → 0.8.1
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 +2 -2
- package/dist/cli.mjs +284 -88
- package/dist/index.d.mts +2 -2
- package/dist/index.mjs +267 -83
- package/package.json +4 -1
package/README.md
CHANGED
|
@@ -57,8 +57,8 @@ Compatibility target for migrated settings:
|
|
|
57
57
|
- `v10`: keep legacy settings as-is and migrate schema-aligned pnpm config keys from `.npmrc`
|
|
58
58
|
- `v11`: normalize to v11-compatible settings (`allowBuilds`, `allowUnusedPatches`, etc.)
|
|
59
59
|
and migrate all non auth/registry `.npmrc` entries to `pnpm-workspace.yaml`
|
|
60
|
-
- `v12`:
|
|
61
|
-
|
|
60
|
+
- `v12`: use the same settings-key migration as `v11`. Lockfile migration and
|
|
61
|
+
validation are outside this tool's scope.
|
|
62
62
|
|
|
63
63
|
In `v11` and `v12` modes, normalization is applied both to settings read from
|
|
64
64
|
legacy files and to deprecated settings already present in
|
package/dist/cli.mjs
CHANGED
|
@@ -1,87 +1,133 @@
|
|
|
1
1
|
import process from "node:process";
|
|
2
|
-
import { cac } from "cac";
|
|
3
2
|
import consola$1, { consola } from "consola";
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
3
|
+
import { cac } from "cac";
|
|
4
|
+
import { basename, dirname, join, resolve } from "pathe";
|
|
5
|
+
import { randomUUID } from "node:crypto";
|
|
6
|
+
import { access, chmod, readFile, rename, rm, stat, writeFile } from "node:fs/promises";
|
|
7
|
+
import { fileURLToPath } from "node:url";
|
|
7
8
|
import { getColor } from "consola/utils";
|
|
8
|
-
import { isPlainObject, isUndefined
|
|
9
|
+
import { isPlainObject, isUndefined } from "@ntnyq/utils";
|
|
9
10
|
import { defu } from "defu";
|
|
10
11
|
import camelcaseKeys from "camelcase-keys";
|
|
11
12
|
import { readIniFile } from "read-ini-file";
|
|
12
13
|
import { kebabCase } from "uncase";
|
|
13
14
|
import { isDeepStrictEqual } from "node:util";
|
|
15
|
+
import { Document, isMap, isNode, isScalar, isSeq, parseDocument, stringify } from "yaml";
|
|
14
16
|
import detectIndent from "detect-indent";
|
|
15
17
|
//#region package.json
|
|
16
18
|
var name = "pnpm-settings-migrator";
|
|
17
|
-
var version = "0.8.
|
|
19
|
+
var version = "0.8.1";
|
|
20
|
+
//#endregion
|
|
21
|
+
//#region src/cli-options.ts
|
|
22
|
+
/**
|
|
23
|
+
* Create the command-line parser shared by the executable and parser tests.
|
|
24
|
+
*/
|
|
25
|
+
function createCli() {
|
|
26
|
+
const cli = cac(name).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");
|
|
27
|
+
cli.help((sections) => {
|
|
28
|
+
if (!(cli.commands.length > 0 && cli.commands.every((command) => command.isDefaultCommand))) return sections;
|
|
29
|
+
return sections.filter((section) => section.title !== "Commands" && !section.title?.startsWith("For more info"));
|
|
30
|
+
});
|
|
31
|
+
return cli;
|
|
32
|
+
}
|
|
18
33
|
//#endregion
|
|
19
34
|
//#region src/constants.ts
|
|
20
35
|
const NPMRC = ".npmrc";
|
|
21
36
|
const PACKAGE_JSON = "package.json";
|
|
22
37
|
const PNPM_WORKSPACE_YAML = "pnpm-workspace.yaml";
|
|
23
38
|
/**
|
|
24
|
-
* @see {@link https://
|
|
25
|
-
*
|
|
39
|
+
* @see {@link https://pnpm.io/10.x/settings}
|
|
40
|
+
* Documented pnpm v10 config fields that can be migrated from `.npmrc`.
|
|
26
41
|
*
|
|
27
42
|
* Intentionally excluded workspace manifest-only fields:
|
|
28
43
|
* - `catalog`
|
|
29
|
-
* - `catalogMode`
|
|
30
44
|
* - `catalogs`
|
|
31
|
-
* - `cleanupUnusedCatalogs`
|
|
32
45
|
* - `packages`
|
|
33
46
|
*/
|
|
34
|
-
const
|
|
47
|
+
const PNPM_V10_NPMRC_SETTINGS_FIELDS = [
|
|
35
48
|
"allowBuilds",
|
|
36
49
|
"allowedDeprecatedVersions",
|
|
37
50
|
"allowNonAppliedPatches",
|
|
38
51
|
"allowUnusedPatches",
|
|
39
52
|
"auditConfig",
|
|
53
|
+
"auditLevel",
|
|
54
|
+
"autoInstallPeers",
|
|
55
|
+
"blockExoticSubdeps",
|
|
56
|
+
"cacheDir",
|
|
57
|
+
"catalogMode",
|
|
58
|
+
"childConcurrency",
|
|
59
|
+
"ci",
|
|
60
|
+
"cleanupUnusedCatalogs",
|
|
61
|
+
"color",
|
|
40
62
|
"configDependencies",
|
|
63
|
+
"dangerouslyAllowAllBuilds",
|
|
41
64
|
"dedupeDirectDeps",
|
|
42
65
|
"dedupeInjectedDeps",
|
|
43
66
|
"dedupePeerDependents",
|
|
44
67
|
"deployAllFiles",
|
|
68
|
+
"disallowWorkspaceCycles",
|
|
69
|
+
"dlxCacheMaxAge",
|
|
70
|
+
"enableGlobalVirtualStore",
|
|
71
|
+
"enableModulesDir",
|
|
72
|
+
"enablePrePostScripts",
|
|
45
73
|
"engineStrict",
|
|
46
74
|
"executionEnv",
|
|
75
|
+
"extendNodePath",
|
|
76
|
+
"failIfNoMatch",
|
|
47
77
|
"fetchMinSpeedKiBps",
|
|
78
|
+
"fetchRetries",
|
|
48
79
|
"fetchRetryFactor",
|
|
49
80
|
"fetchRetryMaxtimeout",
|
|
50
81
|
"fetchRetryMintimeout",
|
|
51
|
-
"
|
|
82
|
+
"fetchTimeout",
|
|
52
83
|
"fetchWarnTimeoutMs",
|
|
53
84
|
"forceLegacyDeploy",
|
|
85
|
+
"frozenLockfile",
|
|
86
|
+
"gitBranchLockfile",
|
|
54
87
|
"gitChecks",
|
|
55
88
|
"gitShallowHosts",
|
|
56
|
-
"
|
|
89
|
+
"globalBinDir",
|
|
90
|
+
"globalDir",
|
|
91
|
+
"globalPnpmfile",
|
|
57
92
|
"globalVirtualStoreDir",
|
|
58
93
|
"hoist",
|
|
59
94
|
"hoistPattern",
|
|
60
95
|
"hoistWorkspacePackages",
|
|
61
96
|
"httpProxy",
|
|
62
97
|
"httpsProxy",
|
|
98
|
+
"ignoreCompatibilityDb",
|
|
63
99
|
"ignoredBuiltDependencies",
|
|
64
100
|
"ignoreDepScripts",
|
|
65
|
-
"ignoreDeps",
|
|
66
101
|
"ignoredOptionalDependencies",
|
|
67
102
|
"ignorePatchFailures",
|
|
103
|
+
"ignorePnpmfile",
|
|
104
|
+
"ignoreScripts",
|
|
68
105
|
"ignoreWorkspaceCycles",
|
|
106
|
+
"ignoreWorkspaceRootCheck",
|
|
107
|
+
"includeWorkspaceRoot",
|
|
69
108
|
"injectWorkspacePackages",
|
|
70
109
|
"linkWorkspacePackages",
|
|
110
|
+
"localAddress",
|
|
71
111
|
"lockfile",
|
|
72
112
|
"lockfileDir",
|
|
113
|
+
"lockfileIncludeTarballUrl",
|
|
114
|
+
"loglevel",
|
|
73
115
|
"managePackageManagerVersions",
|
|
116
|
+
"maxsockets",
|
|
74
117
|
"mergeGitBranchLockfiles",
|
|
75
118
|
"mergeGitBranchLockfilesBranchPattern",
|
|
119
|
+
"minimumReleaseAge",
|
|
120
|
+
"minimumReleaseAgeExclude",
|
|
76
121
|
"modulesCacheMaxAge",
|
|
122
|
+
"modulesDir",
|
|
77
123
|
"networkConcurrency",
|
|
78
124
|
"neverBuiltDependencies",
|
|
79
|
-
"nodeDownloadMirrors",
|
|
80
125
|
"nodeLinker",
|
|
81
|
-
"
|
|
126
|
+
"nodeOptions",
|
|
127
|
+
"nodeVersion",
|
|
82
128
|
"noProxy",
|
|
129
|
+
"noproxy",
|
|
83
130
|
"npmPath",
|
|
84
|
-
"npmrcAuthFile",
|
|
85
131
|
"onlyBuiltDependencies",
|
|
86
132
|
"onlyBuiltDependenciesFile",
|
|
87
133
|
"optimisticRepeatInstall",
|
|
@@ -95,38 +141,46 @@ const PNPM_SETTINGS_FIELDS = [
|
|
|
95
141
|
"peerDependencyRules",
|
|
96
142
|
"peersSuffixMaxLength",
|
|
97
143
|
"pnpmfile",
|
|
98
|
-
"pnpmfileIgnorePkgManifest",
|
|
99
|
-
"pmOnFail",
|
|
100
144
|
"preferFrozenLockfile",
|
|
145
|
+
"preferSymlinkedExecutables",
|
|
101
146
|
"preferWorkspacePackages",
|
|
102
147
|
"production",
|
|
103
148
|
"proxy",
|
|
104
149
|
"publicHoistPattern",
|
|
105
150
|
"recursiveInstall",
|
|
106
|
-
"
|
|
151
|
+
"registrySupportsTimeField",
|
|
107
152
|
"requiredScripts",
|
|
108
153
|
"resolutionMode",
|
|
109
154
|
"resolvePeersFromWorkspaceRoot",
|
|
110
155
|
"saveExact",
|
|
111
156
|
"savePrefix",
|
|
157
|
+
"saveWorkspaceProtocol",
|
|
112
158
|
"scriptShell",
|
|
113
159
|
"shamefullyHoist",
|
|
114
160
|
"sharedWorkspaceLockfile",
|
|
115
161
|
"shellEmulator",
|
|
116
162
|
"sideEffectsCache",
|
|
117
163
|
"sideEffectsCacheReadonly",
|
|
164
|
+
"stateDir",
|
|
118
165
|
"storeDir",
|
|
119
166
|
"strictDepBuilds",
|
|
120
167
|
"strictPeerDependencies",
|
|
121
168
|
"strictSsl",
|
|
169
|
+
"strictStorePkgContentCheck",
|
|
122
170
|
"supportedArchitectures",
|
|
171
|
+
"symlink",
|
|
123
172
|
"syncInjectedDepsAfterScripts",
|
|
124
173
|
"tag",
|
|
125
|
-
"
|
|
126
|
-
"
|
|
174
|
+
"trustPolicy",
|
|
175
|
+
"trustPolicyExclude",
|
|
176
|
+
"trustPolicyIgnoreAfter",
|
|
177
|
+
"unsafePerm",
|
|
127
178
|
"updateConfig",
|
|
179
|
+
"updateNotifier",
|
|
128
180
|
"useBetaCli",
|
|
129
181
|
"useNodeVersion",
|
|
182
|
+
"useRunningStoreServer",
|
|
183
|
+
"useStderr",
|
|
130
184
|
"verifyDepsBeforeRun",
|
|
131
185
|
"verifyStoreIntegrity",
|
|
132
186
|
"virtualStoreDir",
|
|
@@ -156,7 +210,6 @@ const DEFAULT_OPTIONS = {
|
|
|
156
210
|
cleanNpmrc: true,
|
|
157
211
|
cleanPackageJson: true,
|
|
158
212
|
compatibility: "auto",
|
|
159
|
-
cwd: process.cwd(),
|
|
160
213
|
newlineBetween: true,
|
|
161
214
|
replaceDeprecated: false,
|
|
162
215
|
showChanges: true,
|
|
@@ -210,7 +263,7 @@ function resolveOptions(options = {}) {
|
|
|
210
263
|
return {
|
|
211
264
|
cleanNpmrc: options.cleanNpmrc ?? DEFAULT_OPTIONS.cleanNpmrc,
|
|
212
265
|
compatibility: resolveCompatibility(options.compatibility),
|
|
213
|
-
cwd: options.cwd ??
|
|
266
|
+
cwd: options.cwd ?? process.cwd(),
|
|
214
267
|
newlineBetween: options.newlineBetween ?? DEFAULT_OPTIONS.newlineBetween,
|
|
215
268
|
showChanges: options.showChanges ?? DEFAULT_OPTIONS.showChanges,
|
|
216
269
|
sortKeys: options.sortKeys ?? DEFAULT_OPTIONS.sortKeys,
|
|
@@ -223,6 +276,28 @@ function resolveOptions(options = {}) {
|
|
|
223
276
|
//#endregion
|
|
224
277
|
//#region src/utils/fs.ts
|
|
225
278
|
/**
|
|
279
|
+
* Modulus that isolates Unix permission bits from a stat mode.
|
|
280
|
+
*/
|
|
281
|
+
const FILE_PERMISSION_MODULUS = 512;
|
|
282
|
+
/**
|
|
283
|
+
* Resolve a filesystem path to a string suitable for a sibling temp file.
|
|
284
|
+
*/
|
|
285
|
+
function resolvePathString(path) {
|
|
286
|
+
if (path instanceof URL) return fileURLToPath(path);
|
|
287
|
+
return typeof path === "string" ? path : path.toString();
|
|
288
|
+
}
|
|
289
|
+
/**
|
|
290
|
+
* Read an existing file's permission bits, if the file exists.
|
|
291
|
+
*/
|
|
292
|
+
async function resolveExistingMode(path) {
|
|
293
|
+
try {
|
|
294
|
+
return (await stat(path)).mode % FILE_PERMISSION_MODULUS;
|
|
295
|
+
} catch (error) {
|
|
296
|
+
if (error.code === "ENOENT") return;
|
|
297
|
+
throw error;
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
/**
|
|
226
301
|
* Check whether a filesystem path exists.
|
|
227
302
|
*
|
|
228
303
|
* @param path - Filesystem path to check
|
|
@@ -261,10 +336,22 @@ async function fsRemoveFile(path) {
|
|
|
261
336
|
* @returns A promise that resolves after the file is written
|
|
262
337
|
*/
|
|
263
338
|
async function fsWriteFile(path, content) {
|
|
264
|
-
|
|
339
|
+
const pathString = resolvePathString(path);
|
|
340
|
+
const temporaryPath = join(dirname(pathString), `.${basename(pathString)}.${process.pid}.${randomUUID()}.tmp`);
|
|
341
|
+
const mode = await resolveExistingMode(path);
|
|
342
|
+
try {
|
|
343
|
+
await writeFile(temporaryPath, `${content.trimEnd()}\n`, {
|
|
344
|
+
encoding: "utf-8",
|
|
345
|
+
mode
|
|
346
|
+
});
|
|
347
|
+
if (mode !== void 0) await chmod(temporaryPath, mode);
|
|
348
|
+
await rename(temporaryPath, pathString);
|
|
349
|
+
} finally {
|
|
350
|
+
await rm(temporaryPath, { force: true });
|
|
351
|
+
}
|
|
265
352
|
}
|
|
266
|
-
|
|
267
|
-
|
|
353
|
+
//#endregion
|
|
354
|
+
//#region src/utils/color.ts
|
|
268
355
|
/**
|
|
269
356
|
* Apply dim terminal styling to a value.
|
|
270
357
|
*/
|
|
@@ -286,27 +373,9 @@ const bold = getColor("bold");
|
|
|
286
373
|
*/
|
|
287
374
|
const magenta = getColor("magenta");
|
|
288
375
|
//#endregion
|
|
289
|
-
//#region src/utils/
|
|
290
|
-
/**
|
|
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
|
-
];
|
|
376
|
+
//#region src/utils/legacy-build-settings.ts
|
|
300
377
|
/**
|
|
301
378
|
* 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
|
|
310
379
|
*/
|
|
311
380
|
async function readOnlyBuiltDependenciesFile(cwd, file) {
|
|
312
381
|
if (!file) return [];
|
|
@@ -316,21 +385,44 @@ async function readOnlyBuiltDependenciesFile(cwd, file) {
|
|
|
316
385
|
return value;
|
|
317
386
|
}
|
|
318
387
|
/**
|
|
388
|
+
* Read and validate one legacy build dependency list. Configuration files are
|
|
389
|
+
* external input, so their runtime shape can differ from its declared type.
|
|
390
|
+
*/
|
|
391
|
+
function readLegacyBuildDependencyList(settings, key) {
|
|
392
|
+
const value = settings[key];
|
|
393
|
+
if (value === void 0) return [];
|
|
394
|
+
if (!Array.isArray(value) || !value.every((item) => typeof item === "string")) {
|
|
395
|
+
const npmrcKey = key.replace(/[A-Z]/gu, (character) => `-${character.toLowerCase()}`);
|
|
396
|
+
throw new TypeError(`Invalid ${key}: expected an array of package names. In .npmrc, use ${npmrcKey}[]=<package>.`);
|
|
397
|
+
}
|
|
398
|
+
return value;
|
|
399
|
+
}
|
|
400
|
+
/**
|
|
319
401
|
* 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
|
|
325
402
|
*/
|
|
326
403
|
async function collectAllowBuildsFromLegacy(incomingSettings, cwd) {
|
|
327
404
|
const allowBuilds = {};
|
|
405
|
+
const allowed = readLegacyBuildDependencyList(incomingSettings, "onlyBuiltDependencies");
|
|
406
|
+
const ignored = readLegacyBuildDependencyList(incomingSettings, "ignoredBuiltDependencies");
|
|
407
|
+
const neverBuilt = readLegacyBuildDependencyList(incomingSettings, "neverBuiltDependencies");
|
|
328
408
|
const allowedFromFile = await readOnlyBuiltDependenciesFile(cwd, incomingSettings.onlyBuiltDependenciesFile);
|
|
329
|
-
for (const name of [...
|
|
330
|
-
for (const name of
|
|
331
|
-
for (const name of
|
|
409
|
+
for (const name of [...allowed, ...allowedFromFile]) allowBuilds[name] = true;
|
|
410
|
+
for (const name of ignored) allowBuilds[name] = false;
|
|
411
|
+
for (const name of neverBuilt) allowBuilds[name] = false;
|
|
332
412
|
return Object.keys(allowBuilds).length ? allowBuilds : void 0;
|
|
333
413
|
}
|
|
414
|
+
//#endregion
|
|
415
|
+
//#region src/utils/compatibility.ts
|
|
416
|
+
/**
|
|
417
|
+
* Legacy build settings that can be replaced for pnpm v10.
|
|
418
|
+
*/
|
|
419
|
+
const PNPM_REPLACEABLE_IN_V10_SETTINGS = [
|
|
420
|
+
"allowNonAppliedPatches",
|
|
421
|
+
"ignoredBuiltDependencies",
|
|
422
|
+
"neverBuiltDependencies",
|
|
423
|
+
"onlyBuiltDependencies",
|
|
424
|
+
"onlyBuiltDependenciesFile"
|
|
425
|
+
];
|
|
334
426
|
/**
|
|
335
427
|
* Resolve the pnpm v11 replacement for legacy package-manager settings.
|
|
336
428
|
*
|
|
@@ -466,6 +558,20 @@ const PNPM_V11_MAJOR = 11;
|
|
|
466
558
|
*/
|
|
467
559
|
const PNPM_V12_MAJOR = 12;
|
|
468
560
|
/**
|
|
561
|
+
* Resolve a pnpm major from a package manager declaration.
|
|
562
|
+
*/
|
|
563
|
+
function resolvePnpmMajor(hint) {
|
|
564
|
+
const match = hint?.match(/^pnpm@[\s<=>^~]*(?<major>\d+)(?:\.|\s|$)/u);
|
|
565
|
+
return match?.groups?.major ? Number(match.groups.major) : void 0;
|
|
566
|
+
}
|
|
567
|
+
/**
|
|
568
|
+
* Normalize the single-or-array devEngines declaration.
|
|
569
|
+
*/
|
|
570
|
+
function normalizePackageManagers(value) {
|
|
571
|
+
if (Array.isArray(value)) return value;
|
|
572
|
+
return value ? [value] : [];
|
|
573
|
+
}
|
|
574
|
+
/**
|
|
469
575
|
* Resolve final compatibility target from user option and package manager hint.
|
|
470
576
|
*
|
|
471
577
|
* @param compatibility - Explicit or automatically detected compatibility target
|
|
@@ -476,10 +582,11 @@ const PNPM_V12_MAJOR = 12;
|
|
|
476
582
|
*/
|
|
477
583
|
function resolveCompatibilityTarget(compatibility, packageManager, devPackageManager) {
|
|
478
584
|
if (compatibility !== "auto") return compatibility;
|
|
479
|
-
const
|
|
480
|
-
const
|
|
481
|
-
|
|
482
|
-
|
|
585
|
+
const packageManagerMajor = resolvePnpmMajor(packageManager);
|
|
586
|
+
const devPackageManagerMajors = normalizePackageManagers(devPackageManager).filter((item) => item.name === "pnpm").map((item) => resolvePnpmMajor(`pnpm@${item.version ?? ""}`)).filter((major) => major !== void 0);
|
|
587
|
+
const major = packageManagerMajor ?? (devPackageManagerMajors.length ? Math.max(...devPackageManagerMajors) : void 0);
|
|
588
|
+
if (major !== void 0 && major >= PNPM_V12_MAJOR) return "v12";
|
|
589
|
+
return major !== void 0 && major >= PNPM_V11_MAJOR ? "v11" : "v10";
|
|
483
590
|
}
|
|
484
591
|
//#endregion
|
|
485
592
|
//#region src/utils/merge.ts
|
|
@@ -622,9 +729,8 @@ function isNpmrcAuthOrRegistryKey(key) {
|
|
|
622
729
|
/**
|
|
623
730
|
* Remove pnpm-related settings from `.npmrc` file.
|
|
624
731
|
*
|
|
625
|
-
* This function reads the `.npmrc` file,
|
|
626
|
-
*
|
|
627
|
-
* writes the cleaned content back to the file.
|
|
732
|
+
* This function reads the `.npmrc` file, removes lines whose exact normalized
|
|
733
|
+
* keys were migrated, and writes the cleaned content back to the file.
|
|
628
734
|
*
|
|
629
735
|
* @param path - Absolute path to the `.npmrc` file
|
|
630
736
|
* @param compatibility - Concrete pnpm compatibility target
|
|
@@ -636,17 +742,15 @@ function isNpmrcAuthOrRegistryKey(key) {
|
|
|
636
742
|
*
|
|
637
743
|
* @example
|
|
638
744
|
* ```ts
|
|
639
|
-
* await pruneNpmrc('/path/to/.npmrc')
|
|
745
|
+
* await pruneNpmrc('/path/to/.npmrc', 'v10', ['node-linker'])
|
|
640
746
|
* ```
|
|
641
747
|
*/
|
|
642
|
-
async function pruneNpmrc(path, compatibility
|
|
643
|
-
const pnpmSettingsFields = PNPM_SETTINGS_FIELDS.map((field) => kebabCase(field));
|
|
748
|
+
async function pruneNpmrc(path, compatibility, migratedKeys) {
|
|
644
749
|
const migratedKeySet = new Set(migratedKeys.map(normalizeNpmrcKey));
|
|
645
750
|
const updatedContent = (await fsReadFile(path)).split(/\r?\n/u).filter((line) => {
|
|
646
751
|
const key = getNpmrcLineKey(line);
|
|
647
752
|
if (!key) return true;
|
|
648
|
-
|
|
649
|
-
return !pnpmSettingsFields.some((field) => key.startsWith(field));
|
|
753
|
+
return !migratedKeySet.has(key);
|
|
650
754
|
}).join("\n").trimEnd();
|
|
651
755
|
if (compatibility !== "v10" && !updatedContent.trim()) {
|
|
652
756
|
await fsRemoveFile(path);
|
|
@@ -671,10 +775,12 @@ async function pruneNpmrc(path, compatibility = "v10", migratedKeys = []) {
|
|
|
671
775
|
async function readMigratableNpmrc(path, compatibility) {
|
|
672
776
|
const raw = await readIniFile(path);
|
|
673
777
|
if (compatibility === "v10") {
|
|
674
|
-
const
|
|
778
|
+
const pnpmSettingsFields = new Set(PNPM_V10_NPMRC_SETTINGS_FIELDS.map((field) => normalizeNpmrcKey(kebabCase(field))));
|
|
779
|
+
const keys = Object.keys(raw).filter((key) => pnpmSettingsFields.has(normalizeNpmrcKey(key)));
|
|
780
|
+
const migratable = Object.fromEntries(keys.map((key) => [key, raw[key]]));
|
|
675
781
|
return {
|
|
676
|
-
keys
|
|
677
|
-
settings:
|
|
782
|
+
keys,
|
|
783
|
+
settings: camelcaseKeys(migratable)
|
|
678
784
|
};
|
|
679
785
|
}
|
|
680
786
|
const migratable = {};
|
|
@@ -696,6 +802,15 @@ async function readMigratableNpmrc(path, compatibility) {
|
|
|
696
802
|
//#endregion
|
|
697
803
|
//#region src/utils/settings-change.ts
|
|
698
804
|
/**
|
|
805
|
+
* Maximum dynamic-programming cells used for an exact line diff.
|
|
806
|
+
*/
|
|
807
|
+
const MAX_LCS_CELLS = 1e6;
|
|
808
|
+
/**
|
|
809
|
+
* URL userinfo can contain proxy usernames and passwords. It must never be
|
|
810
|
+
* printed in a migration report.
|
|
811
|
+
*/
|
|
812
|
+
const URL_USERINFO_PATTERN = /(?<scheme>[a-z][a-z\d+.-]*:\/\/)[^/\s@]+@/giu;
|
|
813
|
+
/**
|
|
699
814
|
* Collect changed root settings between two workspace configurations.
|
|
700
815
|
*
|
|
701
816
|
* @param before - Workspace settings before migration
|
|
@@ -714,6 +829,38 @@ function collectSettingsChanges(before, after) {
|
|
|
714
829
|
}));
|
|
715
830
|
}
|
|
716
831
|
/**
|
|
832
|
+
* Create a bounded-memory diff that retains shared prefixes and suffixes.
|
|
833
|
+
*
|
|
834
|
+
* This fallback deliberately gives up the exact LCS for unusually large
|
|
835
|
+
* settings to keep reporting from exhausting memory after migration writes.
|
|
836
|
+
*/
|
|
837
|
+
function createLinearLineDiff(beforeLines, afterLines) {
|
|
838
|
+
let prefixLength = 0;
|
|
839
|
+
const maximumPrefixLength = Math.min(beforeLines.length, afterLines.length);
|
|
840
|
+
while (prefixLength < maximumPrefixLength && beforeLines[prefixLength] === afterLines[prefixLength]) prefixLength++;
|
|
841
|
+
let suffixLength = 0;
|
|
842
|
+
const maximumSuffixLength = maximumPrefixLength - prefixLength;
|
|
843
|
+
while (suffixLength < maximumSuffixLength && beforeLines[beforeLines.length - suffixLength - 1] === afterLines[afterLines.length - suffixLength - 1]) suffixLength++;
|
|
844
|
+
return [
|
|
845
|
+
...beforeLines.slice(0, prefixLength).map((value) => ({
|
|
846
|
+
kind: "unchanged",
|
|
847
|
+
value
|
|
848
|
+
})),
|
|
849
|
+
...beforeLines.slice(prefixLength, beforeLines.length - suffixLength).map((value) => ({
|
|
850
|
+
kind: "removed",
|
|
851
|
+
value
|
|
852
|
+
})),
|
|
853
|
+
...afterLines.slice(prefixLength, afterLines.length - suffixLength).map((value) => ({
|
|
854
|
+
kind: "added",
|
|
855
|
+
value
|
|
856
|
+
})),
|
|
857
|
+
...beforeLines.slice(beforeLines.length - suffixLength).map((value) => ({
|
|
858
|
+
kind: "unchanged",
|
|
859
|
+
value
|
|
860
|
+
}))
|
|
861
|
+
];
|
|
862
|
+
}
|
|
863
|
+
/**
|
|
717
864
|
* Create a line-based diff using the longest common subsequence.
|
|
718
865
|
*
|
|
719
866
|
* @param beforeLines - YAML lines before migration
|
|
@@ -722,6 +869,7 @@ function collectSettingsChanges(before, after) {
|
|
|
722
869
|
* @returns Classified diff lines in display order
|
|
723
870
|
*/
|
|
724
871
|
function createLineDiff(beforeLines, afterLines) {
|
|
872
|
+
if (beforeLines.length * afterLines.length > MAX_LCS_CELLS) return createLinearLineDiff(beforeLines, afterLines);
|
|
725
873
|
const lengths = Array.from({ length: beforeLines.length + 1 }, () => Array(afterLines.length + 1).fill(0));
|
|
726
874
|
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
875
|
const diff = [];
|
|
@@ -776,6 +924,17 @@ function formatSettingLines(key, value) {
|
|
|
776
924
|
return stringify({ [key]: value }).trimEnd().split("\n");
|
|
777
925
|
}
|
|
778
926
|
/**
|
|
927
|
+
* Remove credentials embedded in URLs while preserving enough context to
|
|
928
|
+
* identify the changed setting.
|
|
929
|
+
*
|
|
930
|
+
* @param value - Rendered YAML diff line
|
|
931
|
+
*
|
|
932
|
+
* @returns Diff line safe to print to a terminal or CI log
|
|
933
|
+
*/
|
|
934
|
+
function redactUrlCredentials(value) {
|
|
935
|
+
return value.replace(URL_USERINFO_PATTERN, "$<scheme>***@");
|
|
936
|
+
}
|
|
937
|
+
/**
|
|
779
938
|
* Format a setting change as a GitHub-style YAML diff.
|
|
780
939
|
*
|
|
781
940
|
* @param change - Root setting change to format
|
|
@@ -783,7 +942,10 @@ function formatSettingLines(key, value) {
|
|
|
783
942
|
* @returns Classified YAML diff lines
|
|
784
943
|
*/
|
|
785
944
|
function createSettingsDiffLines(change) {
|
|
786
|
-
return createLineDiff(formatSettingLines(change.key, change.before), formatSettingLines(change.key, change.after))
|
|
945
|
+
return createLineDiff(formatSettingLines(change.key, change.before), formatSettingLines(change.key, change.after)).map((line) => ({
|
|
946
|
+
kind: line.kind,
|
|
947
|
+
value: redactUrlCredentials(line.value)
|
|
948
|
+
}));
|
|
787
949
|
}
|
|
788
950
|
/**
|
|
789
951
|
* Print a summary and GitHub-style YAML diff for every changed setting.
|
|
@@ -802,6 +964,39 @@ function reportSettingsChanges(changes) {
|
|
|
802
964
|
}
|
|
803
965
|
}
|
|
804
966
|
//#endregion
|
|
967
|
+
//#region src/utils/yaml-document.ts
|
|
968
|
+
/**
|
|
969
|
+
* Sort every mapping in a YAML node without replacing its comments or anchors.
|
|
970
|
+
*/
|
|
971
|
+
function sortYamlMappings(node) {
|
|
972
|
+
if (isMap(node)) {
|
|
973
|
+
node.items.sort((left, right) => {
|
|
974
|
+
const leftKey = isScalar(left.key) ? String(left.key.value) : "";
|
|
975
|
+
const rightKey = isScalar(right.key) ? String(right.key.value) : "";
|
|
976
|
+
return leftKey.localeCompare(rightKey);
|
|
977
|
+
});
|
|
978
|
+
for (const pair of node.items) if (isNode(pair.value)) sortYamlMappings(pair.value);
|
|
979
|
+
} else if (isSeq(node)) {
|
|
980
|
+
for (const item of node.items) if (isNode(item)) sortYamlMappings(item);
|
|
981
|
+
}
|
|
982
|
+
}
|
|
983
|
+
/**
|
|
984
|
+
* Apply semantic root changes to the parsed document while retaining untouched
|
|
985
|
+
* YAML nodes and their source metadata.
|
|
986
|
+
*/
|
|
987
|
+
function updateYamlDocument(document, options) {
|
|
988
|
+
const { after, before, sortKeys } = options;
|
|
989
|
+
for (const key of Object.keys(before)) if (!Object.hasOwn(after, key)) document.delete(key);
|
|
990
|
+
for (const [key, value] of Object.entries(after)) if (!Object.hasOwn(before, key) || !isDeepStrictEqual(Reflect.get(before, key), value)) document.set(key, value);
|
|
991
|
+
if (sortKeys) sortYamlMappings(document.contents);
|
|
992
|
+
}
|
|
993
|
+
/**
|
|
994
|
+
* Normalize blank lines immediately before root mapping keys.
|
|
995
|
+
*/
|
|
996
|
+
function formatRootSpacing(content, newlineBetween) {
|
|
997
|
+
return content.replace(/\n+(?=[^\s#][^:\n]*:)/gu, newlineBetween ? "\n\n" : "\n");
|
|
998
|
+
}
|
|
999
|
+
//#endregion
|
|
805
1000
|
//#region src/utils/config.ts
|
|
806
1001
|
/**
|
|
807
1002
|
* Resolve the indentation width for a YAML document.
|
|
@@ -847,13 +1042,21 @@ async function readPackageJson(path, exists) {
|
|
|
847
1042
|
*/
|
|
848
1043
|
async function readPnpmWorkspace(path, exists) {
|
|
849
1044
|
if (!exists) return {
|
|
1045
|
+
document: new Document({}),
|
|
850
1046
|
indent: 2,
|
|
851
1047
|
value: {}
|
|
852
1048
|
};
|
|
853
1049
|
const content = await fsReadFile(path);
|
|
1050
|
+
const document = parseDocument(content);
|
|
1051
|
+
const [parseError] = document.errors;
|
|
1052
|
+
if (parseError) throw parseError;
|
|
1053
|
+
const parsedValue = document.toJS();
|
|
1054
|
+
if (parsedValue !== null && !isMap(document.contents)) throw new TypeError("pnpm-workspace.yaml must contain a root mapping.");
|
|
1055
|
+
if (parsedValue === null) document.contents = document.createNode({});
|
|
854
1056
|
return {
|
|
1057
|
+
document,
|
|
855
1058
|
indent: resolveYamlIndent(content),
|
|
856
|
-
value:
|
|
1059
|
+
value: parsedValue ?? {}
|
|
857
1060
|
};
|
|
858
1061
|
}
|
|
859
1062
|
/**
|
|
@@ -954,11 +1157,7 @@ async function migratePnpmSettings(rawOptions = {}) {
|
|
|
954
1157
|
settings: {}
|
|
955
1158
|
};
|
|
956
1159
|
const pnpmSettingsInNpmrc = npmrcMigratable.settings;
|
|
957
|
-
const
|
|
958
|
-
const incomingSettings = {
|
|
959
|
-
...pnpmSettingsInNpmrc,
|
|
960
|
-
...pnpmSettingsInPackageJson
|
|
961
|
-
};
|
|
1160
|
+
const incomingSettings = mergeByStrategy(resolvePackageJsonSettings(packageJson.value, options.yarnResolutions), pnpmSettingsInNpmrc, "merge");
|
|
962
1161
|
const [existingNormalization, incomingNormalization] = await Promise.all([normalizeIncomingSettings(pnpmWorkspace.value, {
|
|
963
1162
|
compatibility,
|
|
964
1163
|
cwd: options.cwd,
|
|
@@ -984,16 +1183,12 @@ async function migratePnpmSettings(rawOptions = {}) {
|
|
|
984
1183
|
return;
|
|
985
1184
|
}
|
|
986
1185
|
const pnpmWorkspaceResult = mergeByStrategy(pnpmWorkspace.value, incomingSettings, options.strategy);
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
value
|
|
992
|
-
});
|
|
1186
|
+
updateYamlDocument(pnpmWorkspace.document, {
|
|
1187
|
+
after: pnpmWorkspaceResult,
|
|
1188
|
+
before: pnpmWorkspaceBefore,
|
|
1189
|
+
sortKeys: options.sortKeys
|
|
993
1190
|
});
|
|
994
|
-
const
|
|
995
|
-
await fsWriteFile(pnpmWorkspaceYamlPath, options.newlineBetween ? yamlContent.replace(/\n(?=[^\s#][^:\n]*:)/gu, "\n\n") : yamlContent);
|
|
996
|
-
if (npmrcExists && options.cleanNpmrc) await pruneNpmrc(npmrcPath, compatibility, npmrcMigratable.keys);
|
|
1191
|
+
const finalYamlContent = formatRootSpacing(pnpmWorkspace.document.toString({ indent: pnpmWorkspace.indent }), options.newlineBetween);
|
|
997
1192
|
let packageJsonChanged = runtimeMigration.changed;
|
|
998
1193
|
if (packageJsonExists && options.cleanPackageJson) {
|
|
999
1194
|
if (packageJson.value.pnpm) packageJsonChanged = true;
|
|
@@ -1003,7 +1198,9 @@ async function migratePnpmSettings(rawOptions = {}) {
|
|
|
1003
1198
|
delete packageJson.value.resolutions;
|
|
1004
1199
|
}
|
|
1005
1200
|
}
|
|
1201
|
+
await fsWriteFile(pnpmWorkspaceYamlPath, finalYamlContent);
|
|
1006
1202
|
if (packageJsonExists && packageJsonChanged) await fsWriteFile(packageJsonPath, JSON.stringify(packageJson.value, null, packageJson.indent));
|
|
1203
|
+
if (npmrcExists && options.cleanNpmrc) await pruneNpmrc(npmrcPath, compatibility, npmrcMigratable.keys);
|
|
1007
1204
|
reportMigrationChanges(options.showChanges, pnpmWorkspaceBefore, pnpmWorkspaceResult);
|
|
1008
1205
|
} catch (err) {
|
|
1009
1206
|
consola$1.error("Failed to migrate pnpm settings:", err);
|
|
@@ -1012,9 +1209,8 @@ async function migratePnpmSettings(rawOptions = {}) {
|
|
|
1012
1209
|
}
|
|
1013
1210
|
//#endregion
|
|
1014
1211
|
//#region src/cli.ts
|
|
1015
|
-
const cli =
|
|
1016
|
-
cli.
|
|
1017
|
-
cli.command("").action(async (options) => {
|
|
1212
|
+
const cli = createCli();
|
|
1213
|
+
cli.command("").usage("[options]").action(async (options) => {
|
|
1018
1214
|
try {
|
|
1019
1215
|
consola.log(`\n${bold(magenta(name))} ${dim(`v${version}`)}`);
|
|
1020
1216
|
consola.log(dim("\n--------------\n"));
|
package/dist/index.d.mts
CHANGED
|
@@ -91,7 +91,7 @@ interface PackageJson {
|
|
|
91
91
|
* Development tool declarations.
|
|
92
92
|
*/
|
|
93
93
|
devEngines?: {
|
|
94
|
-
packageManager?: PackageManagerEngine;
|
|
94
|
+
packageManager?: PackageManagerEngine | PackageManagerEngine[];
|
|
95
95
|
runtime?: RuntimeEngine | RuntimeEngine[];
|
|
96
96
|
[key: string]: unknown;
|
|
97
97
|
};
|
|
@@ -128,7 +128,7 @@ type NpmRC = Record<string, any>;
|
|
|
128
128
|
interface PackageManagerEngine {
|
|
129
129
|
name: string;
|
|
130
130
|
onFail?: 'download' | 'error' | 'ignore' | 'warn';
|
|
131
|
-
version
|
|
131
|
+
version?: string;
|
|
132
132
|
}
|
|
133
133
|
/**
|
|
134
134
|
* Runtime declaration stored in `package.json#devEngines.runtime`.
|
package/dist/index.mjs
CHANGED
|
@@ -1,82 +1,115 @@
|
|
|
1
1
|
import consola from "consola";
|
|
2
|
-
import { resolve } from "pathe";
|
|
3
|
-
import { Document, parse, stringify } from "yaml";
|
|
2
|
+
import { basename, dirname, join, resolve } from "pathe";
|
|
4
3
|
import process from "node:process";
|
|
5
|
-
import {
|
|
4
|
+
import { randomUUID } from "node:crypto";
|
|
5
|
+
import { access, chmod, readFile, rename, rm, stat, writeFile } from "node:fs/promises";
|
|
6
|
+
import { fileURLToPath } from "node:url";
|
|
6
7
|
import { getColor } from "consola/utils";
|
|
7
|
-
import { isPlainObject, isUndefined
|
|
8
|
+
import { isPlainObject, isUndefined } from "@ntnyq/utils";
|
|
8
9
|
import { defu } from "defu";
|
|
9
10
|
import camelcaseKeys from "camelcase-keys";
|
|
10
11
|
import { readIniFile } from "read-ini-file";
|
|
11
12
|
import { kebabCase } from "uncase";
|
|
12
13
|
import { isDeepStrictEqual } from "node:util";
|
|
14
|
+
import { Document, isMap, isNode, isScalar, isSeq, parseDocument, stringify } from "yaml";
|
|
13
15
|
import detectIndent from "detect-indent";
|
|
14
16
|
//#region src/constants.ts
|
|
15
17
|
const NPMRC = ".npmrc";
|
|
16
18
|
const PACKAGE_JSON = "package.json";
|
|
17
19
|
const PNPM_WORKSPACE_YAML = "pnpm-workspace.yaml";
|
|
18
20
|
/**
|
|
19
|
-
* @see {@link https://
|
|
20
|
-
*
|
|
21
|
+
* @see {@link https://pnpm.io/10.x/settings}
|
|
22
|
+
* Documented pnpm v10 config fields that can be migrated from `.npmrc`.
|
|
21
23
|
*
|
|
22
24
|
* Intentionally excluded workspace manifest-only fields:
|
|
23
25
|
* - `catalog`
|
|
24
|
-
* - `catalogMode`
|
|
25
26
|
* - `catalogs`
|
|
26
|
-
* - `cleanupUnusedCatalogs`
|
|
27
27
|
* - `packages`
|
|
28
28
|
*/
|
|
29
|
-
const
|
|
29
|
+
const PNPM_V10_NPMRC_SETTINGS_FIELDS = [
|
|
30
30
|
"allowBuilds",
|
|
31
31
|
"allowedDeprecatedVersions",
|
|
32
32
|
"allowNonAppliedPatches",
|
|
33
33
|
"allowUnusedPatches",
|
|
34
34
|
"auditConfig",
|
|
35
|
+
"auditLevel",
|
|
36
|
+
"autoInstallPeers",
|
|
37
|
+
"blockExoticSubdeps",
|
|
38
|
+
"cacheDir",
|
|
39
|
+
"catalogMode",
|
|
40
|
+
"childConcurrency",
|
|
41
|
+
"ci",
|
|
42
|
+
"cleanupUnusedCatalogs",
|
|
43
|
+
"color",
|
|
35
44
|
"configDependencies",
|
|
45
|
+
"dangerouslyAllowAllBuilds",
|
|
36
46
|
"dedupeDirectDeps",
|
|
37
47
|
"dedupeInjectedDeps",
|
|
38
48
|
"dedupePeerDependents",
|
|
39
49
|
"deployAllFiles",
|
|
50
|
+
"disallowWorkspaceCycles",
|
|
51
|
+
"dlxCacheMaxAge",
|
|
52
|
+
"enableGlobalVirtualStore",
|
|
53
|
+
"enableModulesDir",
|
|
54
|
+
"enablePrePostScripts",
|
|
40
55
|
"engineStrict",
|
|
41
56
|
"executionEnv",
|
|
57
|
+
"extendNodePath",
|
|
58
|
+
"failIfNoMatch",
|
|
42
59
|
"fetchMinSpeedKiBps",
|
|
60
|
+
"fetchRetries",
|
|
43
61
|
"fetchRetryFactor",
|
|
44
62
|
"fetchRetryMaxtimeout",
|
|
45
63
|
"fetchRetryMintimeout",
|
|
46
|
-
"
|
|
64
|
+
"fetchTimeout",
|
|
47
65
|
"fetchWarnTimeoutMs",
|
|
48
66
|
"forceLegacyDeploy",
|
|
67
|
+
"frozenLockfile",
|
|
68
|
+
"gitBranchLockfile",
|
|
49
69
|
"gitChecks",
|
|
50
70
|
"gitShallowHosts",
|
|
51
|
-
"
|
|
71
|
+
"globalBinDir",
|
|
72
|
+
"globalDir",
|
|
73
|
+
"globalPnpmfile",
|
|
52
74
|
"globalVirtualStoreDir",
|
|
53
75
|
"hoist",
|
|
54
76
|
"hoistPattern",
|
|
55
77
|
"hoistWorkspacePackages",
|
|
56
78
|
"httpProxy",
|
|
57
79
|
"httpsProxy",
|
|
80
|
+
"ignoreCompatibilityDb",
|
|
58
81
|
"ignoredBuiltDependencies",
|
|
59
82
|
"ignoreDepScripts",
|
|
60
|
-
"ignoreDeps",
|
|
61
83
|
"ignoredOptionalDependencies",
|
|
62
84
|
"ignorePatchFailures",
|
|
85
|
+
"ignorePnpmfile",
|
|
86
|
+
"ignoreScripts",
|
|
63
87
|
"ignoreWorkspaceCycles",
|
|
88
|
+
"ignoreWorkspaceRootCheck",
|
|
89
|
+
"includeWorkspaceRoot",
|
|
64
90
|
"injectWorkspacePackages",
|
|
65
91
|
"linkWorkspacePackages",
|
|
92
|
+
"localAddress",
|
|
66
93
|
"lockfile",
|
|
67
94
|
"lockfileDir",
|
|
95
|
+
"lockfileIncludeTarballUrl",
|
|
96
|
+
"loglevel",
|
|
68
97
|
"managePackageManagerVersions",
|
|
98
|
+
"maxsockets",
|
|
69
99
|
"mergeGitBranchLockfiles",
|
|
70
100
|
"mergeGitBranchLockfilesBranchPattern",
|
|
101
|
+
"minimumReleaseAge",
|
|
102
|
+
"minimumReleaseAgeExclude",
|
|
71
103
|
"modulesCacheMaxAge",
|
|
104
|
+
"modulesDir",
|
|
72
105
|
"networkConcurrency",
|
|
73
106
|
"neverBuiltDependencies",
|
|
74
|
-
"nodeDownloadMirrors",
|
|
75
107
|
"nodeLinker",
|
|
76
|
-
"
|
|
108
|
+
"nodeOptions",
|
|
109
|
+
"nodeVersion",
|
|
77
110
|
"noProxy",
|
|
111
|
+
"noproxy",
|
|
78
112
|
"npmPath",
|
|
79
|
-
"npmrcAuthFile",
|
|
80
113
|
"onlyBuiltDependencies",
|
|
81
114
|
"onlyBuiltDependenciesFile",
|
|
82
115
|
"optimisticRepeatInstall",
|
|
@@ -90,38 +123,46 @@ const PNPM_SETTINGS_FIELDS = [
|
|
|
90
123
|
"peerDependencyRules",
|
|
91
124
|
"peersSuffixMaxLength",
|
|
92
125
|
"pnpmfile",
|
|
93
|
-
"pnpmfileIgnorePkgManifest",
|
|
94
|
-
"pmOnFail",
|
|
95
126
|
"preferFrozenLockfile",
|
|
127
|
+
"preferSymlinkedExecutables",
|
|
96
128
|
"preferWorkspacePackages",
|
|
97
129
|
"production",
|
|
98
130
|
"proxy",
|
|
99
131
|
"publicHoistPattern",
|
|
100
132
|
"recursiveInstall",
|
|
101
|
-
"
|
|
133
|
+
"registrySupportsTimeField",
|
|
102
134
|
"requiredScripts",
|
|
103
135
|
"resolutionMode",
|
|
104
136
|
"resolvePeersFromWorkspaceRoot",
|
|
105
137
|
"saveExact",
|
|
106
138
|
"savePrefix",
|
|
139
|
+
"saveWorkspaceProtocol",
|
|
107
140
|
"scriptShell",
|
|
108
141
|
"shamefullyHoist",
|
|
109
142
|
"sharedWorkspaceLockfile",
|
|
110
143
|
"shellEmulator",
|
|
111
144
|
"sideEffectsCache",
|
|
112
145
|
"sideEffectsCacheReadonly",
|
|
146
|
+
"stateDir",
|
|
113
147
|
"storeDir",
|
|
114
148
|
"strictDepBuilds",
|
|
115
149
|
"strictPeerDependencies",
|
|
116
150
|
"strictSsl",
|
|
151
|
+
"strictStorePkgContentCheck",
|
|
117
152
|
"supportedArchitectures",
|
|
153
|
+
"symlink",
|
|
118
154
|
"syncInjectedDepsAfterScripts",
|
|
119
155
|
"tag",
|
|
120
|
-
"
|
|
121
|
-
"
|
|
156
|
+
"trustPolicy",
|
|
157
|
+
"trustPolicyExclude",
|
|
158
|
+
"trustPolicyIgnoreAfter",
|
|
159
|
+
"unsafePerm",
|
|
122
160
|
"updateConfig",
|
|
161
|
+
"updateNotifier",
|
|
123
162
|
"useBetaCli",
|
|
124
163
|
"useNodeVersion",
|
|
164
|
+
"useRunningStoreServer",
|
|
165
|
+
"useStderr",
|
|
125
166
|
"verifyDepsBeforeRun",
|
|
126
167
|
"verifyStoreIntegrity",
|
|
127
168
|
"virtualStoreDir",
|
|
@@ -151,7 +192,6 @@ const DEFAULT_OPTIONS = {
|
|
|
151
192
|
cleanNpmrc: true,
|
|
152
193
|
cleanPackageJson: true,
|
|
153
194
|
compatibility: "auto",
|
|
154
|
-
cwd: process.cwd(),
|
|
155
195
|
newlineBetween: true,
|
|
156
196
|
replaceDeprecated: false,
|
|
157
197
|
showChanges: true,
|
|
@@ -205,7 +245,7 @@ function resolveOptions(options = {}) {
|
|
|
205
245
|
return {
|
|
206
246
|
cleanNpmrc: options.cleanNpmrc ?? DEFAULT_OPTIONS.cleanNpmrc,
|
|
207
247
|
compatibility: resolveCompatibility(options.compatibility),
|
|
208
|
-
cwd: options.cwd ??
|
|
248
|
+
cwd: options.cwd ?? process.cwd(),
|
|
209
249
|
newlineBetween: options.newlineBetween ?? DEFAULT_OPTIONS.newlineBetween,
|
|
210
250
|
showChanges: options.showChanges ?? DEFAULT_OPTIONS.showChanges,
|
|
211
251
|
sortKeys: options.sortKeys ?? DEFAULT_OPTIONS.sortKeys,
|
|
@@ -218,6 +258,28 @@ function resolveOptions(options = {}) {
|
|
|
218
258
|
//#endregion
|
|
219
259
|
//#region src/utils/fs.ts
|
|
220
260
|
/**
|
|
261
|
+
* Modulus that isolates Unix permission bits from a stat mode.
|
|
262
|
+
*/
|
|
263
|
+
const FILE_PERMISSION_MODULUS = 512;
|
|
264
|
+
/**
|
|
265
|
+
* Resolve a filesystem path to a string suitable for a sibling temp file.
|
|
266
|
+
*/
|
|
267
|
+
function resolvePathString(path) {
|
|
268
|
+
if (path instanceof URL) return fileURLToPath(path);
|
|
269
|
+
return typeof path === "string" ? path : path.toString();
|
|
270
|
+
}
|
|
271
|
+
/**
|
|
272
|
+
* Read an existing file's permission bits, if the file exists.
|
|
273
|
+
*/
|
|
274
|
+
async function resolveExistingMode(path) {
|
|
275
|
+
try {
|
|
276
|
+
return (await stat(path)).mode % FILE_PERMISSION_MODULUS;
|
|
277
|
+
} catch (error) {
|
|
278
|
+
if (error.code === "ENOENT") return;
|
|
279
|
+
throw error;
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
/**
|
|
221
283
|
* Check whether a filesystem path exists.
|
|
222
284
|
*
|
|
223
285
|
* @param path - Filesystem path to check
|
|
@@ -256,10 +318,22 @@ async function fsRemoveFile(path) {
|
|
|
256
318
|
* @returns A promise that resolves after the file is written
|
|
257
319
|
*/
|
|
258
320
|
async function fsWriteFile(path, content) {
|
|
259
|
-
|
|
321
|
+
const pathString = resolvePathString(path);
|
|
322
|
+
const temporaryPath = join(dirname(pathString), `.${basename(pathString)}.${process.pid}.${randomUUID()}.tmp`);
|
|
323
|
+
const mode = await resolveExistingMode(path);
|
|
324
|
+
try {
|
|
325
|
+
await writeFile(temporaryPath, `${content.trimEnd()}\n`, {
|
|
326
|
+
encoding: "utf-8",
|
|
327
|
+
mode
|
|
328
|
+
});
|
|
329
|
+
if (mode !== void 0) await chmod(temporaryPath, mode);
|
|
330
|
+
await rename(temporaryPath, pathString);
|
|
331
|
+
} finally {
|
|
332
|
+
await rm(temporaryPath, { force: true });
|
|
333
|
+
}
|
|
260
334
|
}
|
|
261
|
-
|
|
262
|
-
|
|
335
|
+
//#endregion
|
|
336
|
+
//#region src/utils/color.ts
|
|
263
337
|
/**
|
|
264
338
|
* Apply dim terminal styling to a value.
|
|
265
339
|
*/
|
|
@@ -275,27 +349,9 @@ const red = getColor("red");
|
|
|
275
349
|
getColor("bold");
|
|
276
350
|
getColor("magenta");
|
|
277
351
|
//#endregion
|
|
278
|
-
//#region src/utils/
|
|
279
|
-
/**
|
|
280
|
-
* Legacy build settings that can be replaced for pnpm v10.
|
|
281
|
-
*/
|
|
282
|
-
const PNPM_REPLACEABLE_IN_V10_SETTINGS = [
|
|
283
|
-
"allowNonAppliedPatches",
|
|
284
|
-
"ignoredBuiltDependencies",
|
|
285
|
-
"neverBuiltDependencies",
|
|
286
|
-
"onlyBuiltDependencies",
|
|
287
|
-
"onlyBuiltDependenciesFile"
|
|
288
|
-
];
|
|
352
|
+
//#region src/utils/legacy-build-settings.ts
|
|
289
353
|
/**
|
|
290
354
|
* 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
|
|
299
355
|
*/
|
|
300
356
|
async function readOnlyBuiltDependenciesFile(cwd, file) {
|
|
301
357
|
if (!file) return [];
|
|
@@ -305,21 +361,44 @@ async function readOnlyBuiltDependenciesFile(cwd, file) {
|
|
|
305
361
|
return value;
|
|
306
362
|
}
|
|
307
363
|
/**
|
|
364
|
+
* Read and validate one legacy build dependency list. Configuration files are
|
|
365
|
+
* external input, so their runtime shape can differ from its declared type.
|
|
366
|
+
*/
|
|
367
|
+
function readLegacyBuildDependencyList(settings, key) {
|
|
368
|
+
const value = settings[key];
|
|
369
|
+
if (value === void 0) return [];
|
|
370
|
+
if (!Array.isArray(value) || !value.every((item) => typeof item === "string")) {
|
|
371
|
+
const npmrcKey = key.replace(/[A-Z]/gu, (character) => `-${character.toLowerCase()}`);
|
|
372
|
+
throw new TypeError(`Invalid ${key}: expected an array of package names. In .npmrc, use ${npmrcKey}[]=<package>.`);
|
|
373
|
+
}
|
|
374
|
+
return value;
|
|
375
|
+
}
|
|
376
|
+
/**
|
|
308
377
|
* 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
|
|
314
378
|
*/
|
|
315
379
|
async function collectAllowBuildsFromLegacy(incomingSettings, cwd) {
|
|
316
380
|
const allowBuilds = {};
|
|
381
|
+
const allowed = readLegacyBuildDependencyList(incomingSettings, "onlyBuiltDependencies");
|
|
382
|
+
const ignored = readLegacyBuildDependencyList(incomingSettings, "ignoredBuiltDependencies");
|
|
383
|
+
const neverBuilt = readLegacyBuildDependencyList(incomingSettings, "neverBuiltDependencies");
|
|
317
384
|
const allowedFromFile = await readOnlyBuiltDependenciesFile(cwd, incomingSettings.onlyBuiltDependenciesFile);
|
|
318
|
-
for (const name of [...
|
|
319
|
-
for (const name of
|
|
320
|
-
for (const name of
|
|
385
|
+
for (const name of [...allowed, ...allowedFromFile]) allowBuilds[name] = true;
|
|
386
|
+
for (const name of ignored) allowBuilds[name] = false;
|
|
387
|
+
for (const name of neverBuilt) allowBuilds[name] = false;
|
|
321
388
|
return Object.keys(allowBuilds).length ? allowBuilds : void 0;
|
|
322
389
|
}
|
|
390
|
+
//#endregion
|
|
391
|
+
//#region src/utils/compatibility.ts
|
|
392
|
+
/**
|
|
393
|
+
* Legacy build settings that can be replaced for pnpm v10.
|
|
394
|
+
*/
|
|
395
|
+
const PNPM_REPLACEABLE_IN_V10_SETTINGS = [
|
|
396
|
+
"allowNonAppliedPatches",
|
|
397
|
+
"ignoredBuiltDependencies",
|
|
398
|
+
"neverBuiltDependencies",
|
|
399
|
+
"onlyBuiltDependencies",
|
|
400
|
+
"onlyBuiltDependenciesFile"
|
|
401
|
+
];
|
|
323
402
|
/**
|
|
324
403
|
* Resolve the pnpm v11 replacement for legacy package-manager settings.
|
|
325
404
|
*
|
|
@@ -455,6 +534,20 @@ const PNPM_V11_MAJOR = 11;
|
|
|
455
534
|
*/
|
|
456
535
|
const PNPM_V12_MAJOR = 12;
|
|
457
536
|
/**
|
|
537
|
+
* Resolve a pnpm major from a package manager declaration.
|
|
538
|
+
*/
|
|
539
|
+
function resolvePnpmMajor(hint) {
|
|
540
|
+
const match = hint?.match(/^pnpm@[\s<=>^~]*(?<major>\d+)(?:\.|\s|$)/u);
|
|
541
|
+
return match?.groups?.major ? Number(match.groups.major) : void 0;
|
|
542
|
+
}
|
|
543
|
+
/**
|
|
544
|
+
* Normalize the single-or-array devEngines declaration.
|
|
545
|
+
*/
|
|
546
|
+
function normalizePackageManagers(value) {
|
|
547
|
+
if (Array.isArray(value)) return value;
|
|
548
|
+
return value ? [value] : [];
|
|
549
|
+
}
|
|
550
|
+
/**
|
|
458
551
|
* Resolve final compatibility target from user option and package manager hint.
|
|
459
552
|
*
|
|
460
553
|
* @param compatibility - Explicit or automatically detected compatibility target
|
|
@@ -465,10 +558,11 @@ const PNPM_V12_MAJOR = 12;
|
|
|
465
558
|
*/
|
|
466
559
|
function resolveCompatibilityTarget(compatibility, packageManager, devPackageManager) {
|
|
467
560
|
if (compatibility !== "auto") return compatibility;
|
|
468
|
-
const
|
|
469
|
-
const
|
|
470
|
-
|
|
471
|
-
|
|
561
|
+
const packageManagerMajor = resolvePnpmMajor(packageManager);
|
|
562
|
+
const devPackageManagerMajors = normalizePackageManagers(devPackageManager).filter((item) => item.name === "pnpm").map((item) => resolvePnpmMajor(`pnpm@${item.version ?? ""}`)).filter((major) => major !== void 0);
|
|
563
|
+
const major = packageManagerMajor ?? (devPackageManagerMajors.length ? Math.max(...devPackageManagerMajors) : void 0);
|
|
564
|
+
if (major !== void 0 && major >= PNPM_V12_MAJOR) return "v12";
|
|
565
|
+
return major !== void 0 && major >= PNPM_V11_MAJOR ? "v11" : "v10";
|
|
472
566
|
}
|
|
473
567
|
//#endregion
|
|
474
568
|
//#region src/utils/merge.ts
|
|
@@ -611,9 +705,8 @@ function isNpmrcAuthOrRegistryKey(key) {
|
|
|
611
705
|
/**
|
|
612
706
|
* Remove pnpm-related settings from `.npmrc` file.
|
|
613
707
|
*
|
|
614
|
-
* This function reads the `.npmrc` file,
|
|
615
|
-
*
|
|
616
|
-
* writes the cleaned content back to the file.
|
|
708
|
+
* This function reads the `.npmrc` file, removes lines whose exact normalized
|
|
709
|
+
* keys were migrated, and writes the cleaned content back to the file.
|
|
617
710
|
*
|
|
618
711
|
* @param path - Absolute path to the `.npmrc` file
|
|
619
712
|
* @param compatibility - Concrete pnpm compatibility target
|
|
@@ -625,17 +718,15 @@ function isNpmrcAuthOrRegistryKey(key) {
|
|
|
625
718
|
*
|
|
626
719
|
* @example
|
|
627
720
|
* ```ts
|
|
628
|
-
* await pruneNpmrc('/path/to/.npmrc')
|
|
721
|
+
* await pruneNpmrc('/path/to/.npmrc', 'v10', ['node-linker'])
|
|
629
722
|
* ```
|
|
630
723
|
*/
|
|
631
|
-
async function pruneNpmrc(path, compatibility
|
|
632
|
-
const pnpmSettingsFields = PNPM_SETTINGS_FIELDS.map((field) => kebabCase(field));
|
|
724
|
+
async function pruneNpmrc(path, compatibility, migratedKeys) {
|
|
633
725
|
const migratedKeySet = new Set(migratedKeys.map(normalizeNpmrcKey));
|
|
634
726
|
const updatedContent = (await fsReadFile(path)).split(/\r?\n/u).filter((line) => {
|
|
635
727
|
const key = getNpmrcLineKey(line);
|
|
636
728
|
if (!key) return true;
|
|
637
|
-
|
|
638
|
-
return !pnpmSettingsFields.some((field) => key.startsWith(field));
|
|
729
|
+
return !migratedKeySet.has(key);
|
|
639
730
|
}).join("\n").trimEnd();
|
|
640
731
|
if (compatibility !== "v10" && !updatedContent.trim()) {
|
|
641
732
|
await fsRemoveFile(path);
|
|
@@ -660,10 +751,12 @@ async function pruneNpmrc(path, compatibility = "v10", migratedKeys = []) {
|
|
|
660
751
|
async function readMigratableNpmrc(path, compatibility) {
|
|
661
752
|
const raw = await readIniFile(path);
|
|
662
753
|
if (compatibility === "v10") {
|
|
663
|
-
const
|
|
754
|
+
const pnpmSettingsFields = new Set(PNPM_V10_NPMRC_SETTINGS_FIELDS.map((field) => normalizeNpmrcKey(kebabCase(field))));
|
|
755
|
+
const keys = Object.keys(raw).filter((key) => pnpmSettingsFields.has(normalizeNpmrcKey(key)));
|
|
756
|
+
const migratable = Object.fromEntries(keys.map((key) => [key, raw[key]]));
|
|
664
757
|
return {
|
|
665
|
-
keys
|
|
666
|
-
settings:
|
|
758
|
+
keys,
|
|
759
|
+
settings: camelcaseKeys(migratable)
|
|
667
760
|
};
|
|
668
761
|
}
|
|
669
762
|
const migratable = {};
|
|
@@ -685,6 +778,15 @@ async function readMigratableNpmrc(path, compatibility) {
|
|
|
685
778
|
//#endregion
|
|
686
779
|
//#region src/utils/settings-change.ts
|
|
687
780
|
/**
|
|
781
|
+
* Maximum dynamic-programming cells used for an exact line diff.
|
|
782
|
+
*/
|
|
783
|
+
const MAX_LCS_CELLS = 1e6;
|
|
784
|
+
/**
|
|
785
|
+
* URL userinfo can contain proxy usernames and passwords. It must never be
|
|
786
|
+
* printed in a migration report.
|
|
787
|
+
*/
|
|
788
|
+
const URL_USERINFO_PATTERN = /(?<scheme>[a-z][a-z\d+.-]*:\/\/)[^/\s@]+@/giu;
|
|
789
|
+
/**
|
|
688
790
|
* Collect changed root settings between two workspace configurations.
|
|
689
791
|
*
|
|
690
792
|
* @param before - Workspace settings before migration
|
|
@@ -703,6 +805,38 @@ function collectSettingsChanges(before, after) {
|
|
|
703
805
|
}));
|
|
704
806
|
}
|
|
705
807
|
/**
|
|
808
|
+
* Create a bounded-memory diff that retains shared prefixes and suffixes.
|
|
809
|
+
*
|
|
810
|
+
* This fallback deliberately gives up the exact LCS for unusually large
|
|
811
|
+
* settings to keep reporting from exhausting memory after migration writes.
|
|
812
|
+
*/
|
|
813
|
+
function createLinearLineDiff(beforeLines, afterLines) {
|
|
814
|
+
let prefixLength = 0;
|
|
815
|
+
const maximumPrefixLength = Math.min(beforeLines.length, afterLines.length);
|
|
816
|
+
while (prefixLength < maximumPrefixLength && beforeLines[prefixLength] === afterLines[prefixLength]) prefixLength++;
|
|
817
|
+
let suffixLength = 0;
|
|
818
|
+
const maximumSuffixLength = maximumPrefixLength - prefixLength;
|
|
819
|
+
while (suffixLength < maximumSuffixLength && beforeLines[beforeLines.length - suffixLength - 1] === afterLines[afterLines.length - suffixLength - 1]) suffixLength++;
|
|
820
|
+
return [
|
|
821
|
+
...beforeLines.slice(0, prefixLength).map((value) => ({
|
|
822
|
+
kind: "unchanged",
|
|
823
|
+
value
|
|
824
|
+
})),
|
|
825
|
+
...beforeLines.slice(prefixLength, beforeLines.length - suffixLength).map((value) => ({
|
|
826
|
+
kind: "removed",
|
|
827
|
+
value
|
|
828
|
+
})),
|
|
829
|
+
...afterLines.slice(prefixLength, afterLines.length - suffixLength).map((value) => ({
|
|
830
|
+
kind: "added",
|
|
831
|
+
value
|
|
832
|
+
})),
|
|
833
|
+
...beforeLines.slice(beforeLines.length - suffixLength).map((value) => ({
|
|
834
|
+
kind: "unchanged",
|
|
835
|
+
value
|
|
836
|
+
}))
|
|
837
|
+
];
|
|
838
|
+
}
|
|
839
|
+
/**
|
|
706
840
|
* Create a line-based diff using the longest common subsequence.
|
|
707
841
|
*
|
|
708
842
|
* @param beforeLines - YAML lines before migration
|
|
@@ -711,6 +845,7 @@ function collectSettingsChanges(before, after) {
|
|
|
711
845
|
* @returns Classified diff lines in display order
|
|
712
846
|
*/
|
|
713
847
|
function createLineDiff(beforeLines, afterLines) {
|
|
848
|
+
if (beforeLines.length * afterLines.length > MAX_LCS_CELLS) return createLinearLineDiff(beforeLines, afterLines);
|
|
714
849
|
const lengths = Array.from({ length: beforeLines.length + 1 }, () => Array(afterLines.length + 1).fill(0));
|
|
715
850
|
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
851
|
const diff = [];
|
|
@@ -765,6 +900,17 @@ function formatSettingLines(key, value) {
|
|
|
765
900
|
return stringify({ [key]: value }).trimEnd().split("\n");
|
|
766
901
|
}
|
|
767
902
|
/**
|
|
903
|
+
* Remove credentials embedded in URLs while preserving enough context to
|
|
904
|
+
* identify the changed setting.
|
|
905
|
+
*
|
|
906
|
+
* @param value - Rendered YAML diff line
|
|
907
|
+
*
|
|
908
|
+
* @returns Diff line safe to print to a terminal or CI log
|
|
909
|
+
*/
|
|
910
|
+
function redactUrlCredentials(value) {
|
|
911
|
+
return value.replace(URL_USERINFO_PATTERN, "$<scheme>***@");
|
|
912
|
+
}
|
|
913
|
+
/**
|
|
768
914
|
* Format a setting change as a GitHub-style YAML diff.
|
|
769
915
|
*
|
|
770
916
|
* @param change - Root setting change to format
|
|
@@ -772,7 +918,10 @@ function formatSettingLines(key, value) {
|
|
|
772
918
|
* @returns Classified YAML diff lines
|
|
773
919
|
*/
|
|
774
920
|
function createSettingsDiffLines(change) {
|
|
775
|
-
return createLineDiff(formatSettingLines(change.key, change.before), formatSettingLines(change.key, change.after))
|
|
921
|
+
return createLineDiff(formatSettingLines(change.key, change.before), formatSettingLines(change.key, change.after)).map((line) => ({
|
|
922
|
+
kind: line.kind,
|
|
923
|
+
value: redactUrlCredentials(line.value)
|
|
924
|
+
}));
|
|
776
925
|
}
|
|
777
926
|
/**
|
|
778
927
|
* Print a summary and GitHub-style YAML diff for every changed setting.
|
|
@@ -791,6 +940,39 @@ function reportSettingsChanges(changes) {
|
|
|
791
940
|
}
|
|
792
941
|
}
|
|
793
942
|
//#endregion
|
|
943
|
+
//#region src/utils/yaml-document.ts
|
|
944
|
+
/**
|
|
945
|
+
* Sort every mapping in a YAML node without replacing its comments or anchors.
|
|
946
|
+
*/
|
|
947
|
+
function sortYamlMappings(node) {
|
|
948
|
+
if (isMap(node)) {
|
|
949
|
+
node.items.sort((left, right) => {
|
|
950
|
+
const leftKey = isScalar(left.key) ? String(left.key.value) : "";
|
|
951
|
+
const rightKey = isScalar(right.key) ? String(right.key.value) : "";
|
|
952
|
+
return leftKey.localeCompare(rightKey);
|
|
953
|
+
});
|
|
954
|
+
for (const pair of node.items) if (isNode(pair.value)) sortYamlMappings(pair.value);
|
|
955
|
+
} else if (isSeq(node)) {
|
|
956
|
+
for (const item of node.items) if (isNode(item)) sortYamlMappings(item);
|
|
957
|
+
}
|
|
958
|
+
}
|
|
959
|
+
/**
|
|
960
|
+
* Apply semantic root changes to the parsed document while retaining untouched
|
|
961
|
+
* YAML nodes and their source metadata.
|
|
962
|
+
*/
|
|
963
|
+
function updateYamlDocument(document, options) {
|
|
964
|
+
const { after, before, sortKeys } = options;
|
|
965
|
+
for (const key of Object.keys(before)) if (!Object.hasOwn(after, key)) document.delete(key);
|
|
966
|
+
for (const [key, value] of Object.entries(after)) if (!Object.hasOwn(before, key) || !isDeepStrictEqual(Reflect.get(before, key), value)) document.set(key, value);
|
|
967
|
+
if (sortKeys) sortYamlMappings(document.contents);
|
|
968
|
+
}
|
|
969
|
+
/**
|
|
970
|
+
* Normalize blank lines immediately before root mapping keys.
|
|
971
|
+
*/
|
|
972
|
+
function formatRootSpacing(content, newlineBetween) {
|
|
973
|
+
return content.replace(/\n+(?=[^\s#][^:\n]*:)/gu, newlineBetween ? "\n\n" : "\n");
|
|
974
|
+
}
|
|
975
|
+
//#endregion
|
|
794
976
|
//#region src/utils/config.ts
|
|
795
977
|
/**
|
|
796
978
|
* Resolve the indentation width for a YAML document.
|
|
@@ -836,13 +1018,21 @@ async function readPackageJson(path, exists) {
|
|
|
836
1018
|
*/
|
|
837
1019
|
async function readPnpmWorkspace(path, exists) {
|
|
838
1020
|
if (!exists) return {
|
|
1021
|
+
document: new Document({}),
|
|
839
1022
|
indent: 2,
|
|
840
1023
|
value: {}
|
|
841
1024
|
};
|
|
842
1025
|
const content = await fsReadFile(path);
|
|
1026
|
+
const document = parseDocument(content);
|
|
1027
|
+
const [parseError] = document.errors;
|
|
1028
|
+
if (parseError) throw parseError;
|
|
1029
|
+
const parsedValue = document.toJS();
|
|
1030
|
+
if (parsedValue !== null && !isMap(document.contents)) throw new TypeError("pnpm-workspace.yaml must contain a root mapping.");
|
|
1031
|
+
if (parsedValue === null) document.contents = document.createNode({});
|
|
843
1032
|
return {
|
|
1033
|
+
document,
|
|
844
1034
|
indent: resolveYamlIndent(content),
|
|
845
|
-
value:
|
|
1035
|
+
value: parsedValue ?? {}
|
|
846
1036
|
};
|
|
847
1037
|
}
|
|
848
1038
|
/**
|
|
@@ -943,11 +1133,7 @@ async function migratePnpmSettings(rawOptions = {}) {
|
|
|
943
1133
|
settings: {}
|
|
944
1134
|
};
|
|
945
1135
|
const pnpmSettingsInNpmrc = npmrcMigratable.settings;
|
|
946
|
-
const
|
|
947
|
-
const incomingSettings = {
|
|
948
|
-
...pnpmSettingsInNpmrc,
|
|
949
|
-
...pnpmSettingsInPackageJson
|
|
950
|
-
};
|
|
1136
|
+
const incomingSettings = mergeByStrategy(resolvePackageJsonSettings(packageJson.value, options.yarnResolutions), pnpmSettingsInNpmrc, "merge");
|
|
951
1137
|
const [existingNormalization, incomingNormalization] = await Promise.all([normalizeIncomingSettings(pnpmWorkspace.value, {
|
|
952
1138
|
compatibility,
|
|
953
1139
|
cwd: options.cwd,
|
|
@@ -973,16 +1159,12 @@ async function migratePnpmSettings(rawOptions = {}) {
|
|
|
973
1159
|
return;
|
|
974
1160
|
}
|
|
975
1161
|
const pnpmWorkspaceResult = mergeByStrategy(pnpmWorkspace.value, incomingSettings, options.strategy);
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
value
|
|
981
|
-
});
|
|
1162
|
+
updateYamlDocument(pnpmWorkspace.document, {
|
|
1163
|
+
after: pnpmWorkspaceResult,
|
|
1164
|
+
before: pnpmWorkspaceBefore,
|
|
1165
|
+
sortKeys: options.sortKeys
|
|
982
1166
|
});
|
|
983
|
-
const
|
|
984
|
-
await fsWriteFile(pnpmWorkspaceYamlPath, options.newlineBetween ? yamlContent.replace(/\n(?=[^\s#][^:\n]*:)/gu, "\n\n") : yamlContent);
|
|
985
|
-
if (npmrcExists && options.cleanNpmrc) await pruneNpmrc(npmrcPath, compatibility, npmrcMigratable.keys);
|
|
1167
|
+
const finalYamlContent = formatRootSpacing(pnpmWorkspace.document.toString({ indent: pnpmWorkspace.indent }), options.newlineBetween);
|
|
986
1168
|
let packageJsonChanged = runtimeMigration.changed;
|
|
987
1169
|
if (packageJsonExists && options.cleanPackageJson) {
|
|
988
1170
|
if (packageJson.value.pnpm) packageJsonChanged = true;
|
|
@@ -992,7 +1174,9 @@ async function migratePnpmSettings(rawOptions = {}) {
|
|
|
992
1174
|
delete packageJson.value.resolutions;
|
|
993
1175
|
}
|
|
994
1176
|
}
|
|
1177
|
+
await fsWriteFile(pnpmWorkspaceYamlPath, finalYamlContent);
|
|
995
1178
|
if (packageJsonExists && packageJsonChanged) await fsWriteFile(packageJsonPath, JSON.stringify(packageJson.value, null, packageJson.indent));
|
|
1179
|
+
if (npmrcExists && options.cleanNpmrc) await pruneNpmrc(npmrcPath, compatibility, npmrcMigratable.keys);
|
|
996
1180
|
reportMigrationChanges(options.showChanges, pnpmWorkspaceBefore, pnpmWorkspaceResult);
|
|
997
1181
|
} catch (err) {
|
|
998
1182
|
consola.error("Failed to migrate pnpm settings:", err);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pnpm-settings-migrator",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.1",
|
|
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",
|
|
@@ -67,6 +67,9 @@
|
|
|
67
67
|
"*.{js,ts,mjs,tsx}": "oxlint --fix",
|
|
68
68
|
"*": "oxfmt --no-error-on-unmatched-pattern"
|
|
69
69
|
},
|
|
70
|
+
"engines": {
|
|
71
|
+
"node": "^22.19.0 || >=24.11.0"
|
|
72
|
+
},
|
|
70
73
|
"scripts": {
|
|
71
74
|
"build": "tsdown",
|
|
72
75
|
"dev": "tsdown --watch",
|