pnpm-settings-migrator 0.7.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 +24 -2
- package/dist/cli.mjs +549 -100
- package/dist/index.d.mts +9 -2
- package/dist/index.mjs +528 -97
- package/package.json +7 -4
package/dist/cli.mjs
CHANGED
|
@@ -1,86 +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";
|
|
14
|
+
import { isDeepStrictEqual } from "node:util";
|
|
15
|
+
import { Document, isMap, isNode, isScalar, isSeq, parseDocument, stringify } from "yaml";
|
|
13
16
|
import detectIndent from "detect-indent";
|
|
14
17
|
//#region package.json
|
|
15
18
|
var name = "pnpm-settings-migrator";
|
|
16
|
-
var version = "0.
|
|
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
|
+
}
|
|
17
33
|
//#endregion
|
|
18
34
|
//#region src/constants.ts
|
|
19
35
|
const NPMRC = ".npmrc";
|
|
20
36
|
const PACKAGE_JSON = "package.json";
|
|
21
37
|
const PNPM_WORKSPACE_YAML = "pnpm-workspace.yaml";
|
|
22
38
|
/**
|
|
23
|
-
* @see {@link https://
|
|
24
|
-
*
|
|
39
|
+
* @see {@link https://pnpm.io/10.x/settings}
|
|
40
|
+
* Documented pnpm v10 config fields that can be migrated from `.npmrc`.
|
|
25
41
|
*
|
|
26
42
|
* Intentionally excluded workspace manifest-only fields:
|
|
27
43
|
* - `catalog`
|
|
28
|
-
* - `catalogMode`
|
|
29
44
|
* - `catalogs`
|
|
30
|
-
* - `cleanupUnusedCatalogs`
|
|
31
45
|
* - `packages`
|
|
32
46
|
*/
|
|
33
|
-
const
|
|
47
|
+
const PNPM_V10_NPMRC_SETTINGS_FIELDS = [
|
|
34
48
|
"allowBuilds",
|
|
35
49
|
"allowedDeprecatedVersions",
|
|
36
50
|
"allowNonAppliedPatches",
|
|
37
51
|
"allowUnusedPatches",
|
|
38
52
|
"auditConfig",
|
|
53
|
+
"auditLevel",
|
|
54
|
+
"autoInstallPeers",
|
|
55
|
+
"blockExoticSubdeps",
|
|
56
|
+
"cacheDir",
|
|
57
|
+
"catalogMode",
|
|
58
|
+
"childConcurrency",
|
|
59
|
+
"ci",
|
|
60
|
+
"cleanupUnusedCatalogs",
|
|
61
|
+
"color",
|
|
39
62
|
"configDependencies",
|
|
63
|
+
"dangerouslyAllowAllBuilds",
|
|
40
64
|
"dedupeDirectDeps",
|
|
41
65
|
"dedupeInjectedDeps",
|
|
42
66
|
"dedupePeerDependents",
|
|
43
67
|
"deployAllFiles",
|
|
68
|
+
"disallowWorkspaceCycles",
|
|
69
|
+
"dlxCacheMaxAge",
|
|
70
|
+
"enableGlobalVirtualStore",
|
|
71
|
+
"enableModulesDir",
|
|
72
|
+
"enablePrePostScripts",
|
|
44
73
|
"engineStrict",
|
|
45
74
|
"executionEnv",
|
|
75
|
+
"extendNodePath",
|
|
76
|
+
"failIfNoMatch",
|
|
46
77
|
"fetchMinSpeedKiBps",
|
|
78
|
+
"fetchRetries",
|
|
47
79
|
"fetchRetryFactor",
|
|
48
80
|
"fetchRetryMaxtimeout",
|
|
49
81
|
"fetchRetryMintimeout",
|
|
50
|
-
"
|
|
82
|
+
"fetchTimeout",
|
|
51
83
|
"fetchWarnTimeoutMs",
|
|
52
84
|
"forceLegacyDeploy",
|
|
85
|
+
"frozenLockfile",
|
|
86
|
+
"gitBranchLockfile",
|
|
53
87
|
"gitChecks",
|
|
54
88
|
"gitShallowHosts",
|
|
55
|
-
"
|
|
89
|
+
"globalBinDir",
|
|
90
|
+
"globalDir",
|
|
91
|
+
"globalPnpmfile",
|
|
56
92
|
"globalVirtualStoreDir",
|
|
57
93
|
"hoist",
|
|
58
94
|
"hoistPattern",
|
|
59
95
|
"hoistWorkspacePackages",
|
|
60
96
|
"httpProxy",
|
|
61
97
|
"httpsProxy",
|
|
98
|
+
"ignoreCompatibilityDb",
|
|
62
99
|
"ignoredBuiltDependencies",
|
|
63
100
|
"ignoreDepScripts",
|
|
64
|
-
"ignoreDeps",
|
|
65
101
|
"ignoredOptionalDependencies",
|
|
66
102
|
"ignorePatchFailures",
|
|
103
|
+
"ignorePnpmfile",
|
|
104
|
+
"ignoreScripts",
|
|
67
105
|
"ignoreWorkspaceCycles",
|
|
106
|
+
"ignoreWorkspaceRootCheck",
|
|
107
|
+
"includeWorkspaceRoot",
|
|
68
108
|
"injectWorkspacePackages",
|
|
69
109
|
"linkWorkspacePackages",
|
|
110
|
+
"localAddress",
|
|
70
111
|
"lockfile",
|
|
71
112
|
"lockfileDir",
|
|
113
|
+
"lockfileIncludeTarballUrl",
|
|
114
|
+
"loglevel",
|
|
72
115
|
"managePackageManagerVersions",
|
|
116
|
+
"maxsockets",
|
|
73
117
|
"mergeGitBranchLockfiles",
|
|
74
118
|
"mergeGitBranchLockfilesBranchPattern",
|
|
119
|
+
"minimumReleaseAge",
|
|
120
|
+
"minimumReleaseAgeExclude",
|
|
75
121
|
"modulesCacheMaxAge",
|
|
122
|
+
"modulesDir",
|
|
76
123
|
"networkConcurrency",
|
|
77
124
|
"neverBuiltDependencies",
|
|
78
|
-
"nodeDownloadMirrors",
|
|
79
125
|
"nodeLinker",
|
|
80
|
-
"
|
|
126
|
+
"nodeOptions",
|
|
127
|
+
"nodeVersion",
|
|
81
128
|
"noProxy",
|
|
129
|
+
"noproxy",
|
|
82
130
|
"npmPath",
|
|
83
|
-
"npmrcAuthFile",
|
|
84
131
|
"onlyBuiltDependencies",
|
|
85
132
|
"onlyBuiltDependenciesFile",
|
|
86
133
|
"optimisticRepeatInstall",
|
|
@@ -94,38 +141,46 @@ const PNPM_SETTINGS_FIELDS = [
|
|
|
94
141
|
"peerDependencyRules",
|
|
95
142
|
"peersSuffixMaxLength",
|
|
96
143
|
"pnpmfile",
|
|
97
|
-
"pnpmfileIgnorePkgManifest",
|
|
98
|
-
"pmOnFail",
|
|
99
144
|
"preferFrozenLockfile",
|
|
145
|
+
"preferSymlinkedExecutables",
|
|
100
146
|
"preferWorkspacePackages",
|
|
101
147
|
"production",
|
|
102
148
|
"proxy",
|
|
103
149
|
"publicHoistPattern",
|
|
104
150
|
"recursiveInstall",
|
|
105
|
-
"
|
|
151
|
+
"registrySupportsTimeField",
|
|
106
152
|
"requiredScripts",
|
|
107
153
|
"resolutionMode",
|
|
108
154
|
"resolvePeersFromWorkspaceRoot",
|
|
109
155
|
"saveExact",
|
|
110
156
|
"savePrefix",
|
|
157
|
+
"saveWorkspaceProtocol",
|
|
111
158
|
"scriptShell",
|
|
112
159
|
"shamefullyHoist",
|
|
113
160
|
"sharedWorkspaceLockfile",
|
|
114
161
|
"shellEmulator",
|
|
115
162
|
"sideEffectsCache",
|
|
116
163
|
"sideEffectsCacheReadonly",
|
|
164
|
+
"stateDir",
|
|
117
165
|
"storeDir",
|
|
118
166
|
"strictDepBuilds",
|
|
119
167
|
"strictPeerDependencies",
|
|
120
168
|
"strictSsl",
|
|
169
|
+
"strictStorePkgContentCheck",
|
|
121
170
|
"supportedArchitectures",
|
|
171
|
+
"symlink",
|
|
122
172
|
"syncInjectedDepsAfterScripts",
|
|
123
173
|
"tag",
|
|
124
|
-
"
|
|
125
|
-
"
|
|
174
|
+
"trustPolicy",
|
|
175
|
+
"trustPolicyExclude",
|
|
176
|
+
"trustPolicyIgnoreAfter",
|
|
177
|
+
"unsafePerm",
|
|
126
178
|
"updateConfig",
|
|
179
|
+
"updateNotifier",
|
|
127
180
|
"useBetaCli",
|
|
128
181
|
"useNodeVersion",
|
|
182
|
+
"useRunningStoreServer",
|
|
183
|
+
"useStderr",
|
|
129
184
|
"verifyDepsBeforeRun",
|
|
130
185
|
"verifyStoreIntegrity",
|
|
131
186
|
"virtualStoreDir",
|
|
@@ -155,9 +210,9 @@ const DEFAULT_OPTIONS = {
|
|
|
155
210
|
cleanNpmrc: true,
|
|
156
211
|
cleanPackageJson: true,
|
|
157
212
|
compatibility: "auto",
|
|
158
|
-
cwd: process.cwd(),
|
|
159
213
|
newlineBetween: true,
|
|
160
214
|
replaceDeprecated: false,
|
|
215
|
+
showChanges: true,
|
|
161
216
|
sortKeys: false,
|
|
162
217
|
strategy: "merge",
|
|
163
218
|
yarnResolutions: true
|
|
@@ -208,8 +263,9 @@ function resolveOptions(options = {}) {
|
|
|
208
263
|
return {
|
|
209
264
|
cleanNpmrc: options.cleanNpmrc ?? DEFAULT_OPTIONS.cleanNpmrc,
|
|
210
265
|
compatibility: resolveCompatibility(options.compatibility),
|
|
211
|
-
cwd: options.cwd ??
|
|
266
|
+
cwd: options.cwd ?? process.cwd(),
|
|
212
267
|
newlineBetween: options.newlineBetween ?? DEFAULT_OPTIONS.newlineBetween,
|
|
268
|
+
showChanges: options.showChanges ?? DEFAULT_OPTIONS.showChanges,
|
|
213
269
|
sortKeys: options.sortKeys ?? DEFAULT_OPTIONS.sortKeys,
|
|
214
270
|
strategy: resolveStrategy(options.strategy),
|
|
215
271
|
yarnResolutions: options.yarnResolutions ?? DEFAULT_OPTIONS.yarnResolutions,
|
|
@@ -220,52 +276,104 @@ function resolveOptions(options = {}) {
|
|
|
220
276
|
//#endregion
|
|
221
277
|
//#region src/utils/fs.ts
|
|
222
278
|
/**
|
|
223
|
-
*
|
|
224
|
-
|
|
225
|
-
|
|
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
|
+
/**
|
|
301
|
+
* Check whether a filesystem path exists.
|
|
302
|
+
*
|
|
303
|
+
* @param path - Filesystem path to check
|
|
304
|
+
*
|
|
305
|
+
* @returns `true` when the path is accessible, otherwise `false`
|
|
226
306
|
*/
|
|
227
307
|
async function fsExists(path) {
|
|
228
308
|
return access(path).then(() => true).catch(() => false);
|
|
229
309
|
}
|
|
230
310
|
/**
|
|
231
|
-
* Read file
|
|
232
|
-
*
|
|
233
|
-
* @
|
|
311
|
+
* Read a UTF-8 text file.
|
|
312
|
+
*
|
|
313
|
+
* @param path - Filesystem path to read
|
|
314
|
+
*
|
|
315
|
+
* @returns Text content of the file
|
|
234
316
|
*/
|
|
235
317
|
async function fsReadFile(path) {
|
|
236
318
|
return await readFile(path, "utf-8");
|
|
237
319
|
}
|
|
238
320
|
/**
|
|
239
321
|
* Remove a file if it exists.
|
|
240
|
-
*
|
|
322
|
+
*
|
|
323
|
+
* @param path - Filesystem path to remove
|
|
324
|
+
*
|
|
325
|
+
* @returns A promise that resolves after the file is removed
|
|
241
326
|
*/
|
|
242
327
|
async function fsRemoveFile(path) {
|
|
243
328
|
await rm(path, { force: true });
|
|
244
329
|
}
|
|
245
330
|
/**
|
|
246
|
-
* Write file
|
|
247
|
-
*
|
|
248
|
-
* @param
|
|
331
|
+
* Write a normalized UTF-8 text file with one trailing newline.
|
|
332
|
+
*
|
|
333
|
+
* @param path - Filesystem path to write
|
|
334
|
+
* @param content - Text content to write
|
|
335
|
+
*
|
|
336
|
+
* @returns A promise that resolves after the file is written
|
|
249
337
|
*/
|
|
250
338
|
async function fsWriteFile(path, content) {
|
|
251
|
-
|
|
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
|
+
}
|
|
252
352
|
}
|
|
253
|
-
|
|
254
|
-
|
|
353
|
+
//#endregion
|
|
354
|
+
//#region src/utils/color.ts
|
|
355
|
+
/**
|
|
356
|
+
* Apply dim terminal styling to a value.
|
|
357
|
+
*/
|
|
255
358
|
const dim = getColor("dim");
|
|
359
|
+
/**
|
|
360
|
+
* Apply green terminal styling to a value.
|
|
361
|
+
*/
|
|
256
362
|
const green = getColor("green");
|
|
363
|
+
/**
|
|
364
|
+
* Apply red terminal styling to a value.
|
|
365
|
+
*/
|
|
257
366
|
const red = getColor("red");
|
|
367
|
+
/**
|
|
368
|
+
* Apply bold terminal styling to a value.
|
|
369
|
+
*/
|
|
258
370
|
const bold = getColor("bold");
|
|
371
|
+
/**
|
|
372
|
+
* Apply magenta terminal styling to a value.
|
|
373
|
+
*/
|
|
259
374
|
const magenta = getColor("magenta");
|
|
260
375
|
//#endregion
|
|
261
|
-
//#region src/utils/
|
|
262
|
-
const PNPM_REPLACEABLE_IN_V10_SETTINGS = [
|
|
263
|
-
"allowNonAppliedPatches",
|
|
264
|
-
"ignoredBuiltDependencies",
|
|
265
|
-
"neverBuiltDependencies",
|
|
266
|
-
"onlyBuiltDependencies",
|
|
267
|
-
"onlyBuiltDependenciesFile"
|
|
268
|
-
];
|
|
376
|
+
//#region src/utils/legacy-build-settings.ts
|
|
269
377
|
/**
|
|
270
378
|
* Read packages listed by the legacy `onlyBuiltDependenciesFile` setting.
|
|
271
379
|
*/
|
|
@@ -277,18 +385,50 @@ async function readOnlyBuiltDependenciesFile(cwd, file) {
|
|
|
277
385
|
return value;
|
|
278
386
|
}
|
|
279
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
|
+
/**
|
|
280
401
|
* Build an `allowBuilds` map from legacy build-script settings.
|
|
281
402
|
*/
|
|
282
403
|
async function collectAllowBuildsFromLegacy(incomingSettings, cwd) {
|
|
283
404
|
const allowBuilds = {};
|
|
405
|
+
const allowed = readLegacyBuildDependencyList(incomingSettings, "onlyBuiltDependencies");
|
|
406
|
+
const ignored = readLegacyBuildDependencyList(incomingSettings, "ignoredBuiltDependencies");
|
|
407
|
+
const neverBuilt = readLegacyBuildDependencyList(incomingSettings, "neverBuiltDependencies");
|
|
284
408
|
const allowedFromFile = await readOnlyBuiltDependenciesFile(cwd, incomingSettings.onlyBuiltDependenciesFile);
|
|
285
|
-
for (const name of [...
|
|
286
|
-
for (const name of
|
|
287
|
-
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;
|
|
288
412
|
return Object.keys(allowBuilds).length ? allowBuilds : void 0;
|
|
289
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
|
+
];
|
|
290
426
|
/**
|
|
291
427
|
* Resolve the pnpm v11 replacement for legacy package-manager settings.
|
|
428
|
+
*
|
|
429
|
+
* @param settings - Settings containing legacy package-manager declarations
|
|
430
|
+
*
|
|
431
|
+
* @returns Resolved `pmOnFail` behavior, or `undefined` when not configured
|
|
292
432
|
*/
|
|
293
433
|
function resolvePmOnFail(settings) {
|
|
294
434
|
if (settings.pmOnFail) return settings.pmOnFail;
|
|
@@ -299,6 +439,11 @@ function resolvePmOnFail(settings) {
|
|
|
299
439
|
}
|
|
300
440
|
/**
|
|
301
441
|
* Rename legacy audit settings while preserving values for manual ID updates.
|
|
442
|
+
*
|
|
443
|
+
* @param settings - Settings whose audit configuration may be normalized
|
|
444
|
+
* @param warnings - Warning collection updated with manual follow-up guidance
|
|
445
|
+
*
|
|
446
|
+
* @returns Nothing; the settings and warning collection are mutated in place
|
|
302
447
|
*/
|
|
303
448
|
function normalizeAuditConfig(settings, warnings) {
|
|
304
449
|
const { auditConfig } = settings;
|
|
@@ -310,6 +455,11 @@ function normalizeAuditConfig(settings, warnings) {
|
|
|
310
455
|
}
|
|
311
456
|
/**
|
|
312
457
|
* Resolve a removed Node.js runtime setting for migration to `package.json`.
|
|
458
|
+
*
|
|
459
|
+
* @param settings - Settings containing legacy runtime declarations
|
|
460
|
+
* @param warnings - Warning collection updated when declarations conflict
|
|
461
|
+
*
|
|
462
|
+
* @returns Runtime version to migrate, or `undefined` when not configured
|
|
313
463
|
*/
|
|
314
464
|
function resolveRuntimeVersion(settings, warnings) {
|
|
315
465
|
const { useNodeVersion } = settings;
|
|
@@ -319,6 +469,11 @@ function resolveRuntimeVersion(settings, warnings) {
|
|
|
319
469
|
}
|
|
320
470
|
/**
|
|
321
471
|
* Normalize settings according to the selected compatibility target.
|
|
472
|
+
*
|
|
473
|
+
* @param incomingSettings - Settings to normalize in place
|
|
474
|
+
* @param options - Compatibility target and normalization context
|
|
475
|
+
*
|
|
476
|
+
* @returns Normalization result, extracted runtime version, and warnings
|
|
322
477
|
*/
|
|
323
478
|
async function normalizeIncomingSettings(incomingSettings, options) {
|
|
324
479
|
const { compatibility, cwd, replaceDeprecated } = options;
|
|
@@ -354,8 +509,15 @@ async function normalizeIncomingSettings(incomingSettings, options) {
|
|
|
354
509
|
warnings
|
|
355
510
|
};
|
|
356
511
|
}
|
|
512
|
+
//#endregion
|
|
513
|
+
//#region src/utils/compatibility-runtime.ts
|
|
357
514
|
/**
|
|
358
515
|
* Move a removed Node.js runtime setting to `package.json#devEngines.runtime`.
|
|
516
|
+
*
|
|
517
|
+
* @param packageJson - Package manifest to update in place
|
|
518
|
+
* @param runtimeVersion - Node.js runtime version extracted from pnpm settings
|
|
519
|
+
*
|
|
520
|
+
* @returns Migration result indicating whether the manifest changed
|
|
359
521
|
*/
|
|
360
522
|
function migrateRuntimeToPackageJson(packageJson, runtimeVersion) {
|
|
361
523
|
if (!runtimeVersion) return { changed: false };
|
|
@@ -374,6 +536,12 @@ function migrateRuntimeToPackageJson(packageJson, runtimeVersion) {
|
|
|
374
536
|
}
|
|
375
537
|
/**
|
|
376
538
|
* Select the runtime setting that wins under the configured merge strategy.
|
|
539
|
+
*
|
|
540
|
+
* @param existingVersion - Runtime version from existing workspace settings
|
|
541
|
+
* @param incomingVersion - Runtime version from incoming settings
|
|
542
|
+
* @param strategy - Merge strategy controlling value precedence
|
|
543
|
+
*
|
|
544
|
+
* @returns Selected runtime version, or `undefined` when neither source has one
|
|
377
545
|
*/
|
|
378
546
|
function resolveRuntimeVersionByStrategy(existingVersion, incomingVersion, strategy) {
|
|
379
547
|
if (strategy === "overwrite") return incomingVersion ?? existingVersion;
|
|
@@ -381,17 +549,44 @@ function resolveRuntimeVersionByStrategy(existingVersion, incomingVersion, strat
|
|
|
381
549
|
}
|
|
382
550
|
//#endregion
|
|
383
551
|
//#region src/utils/compatibility-target.ts
|
|
552
|
+
/**
|
|
553
|
+
* pnpm major version that introduced the v11 settings schema.
|
|
554
|
+
*/
|
|
384
555
|
const PNPM_V11_MAJOR = 11;
|
|
556
|
+
/**
|
|
557
|
+
* pnpm major version that introduced the v12 compatibility target.
|
|
558
|
+
*/
|
|
385
559
|
const PNPM_V12_MAJOR = 12;
|
|
386
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
|
+
/**
|
|
387
575
|
* Resolve final compatibility target from user option and package manager hint.
|
|
576
|
+
*
|
|
577
|
+
* @param compatibility - Explicit or automatically detected compatibility target
|
|
578
|
+
* @param packageManager - `packageManager` declaration from `package.json`
|
|
579
|
+
* @param devPackageManager - Package manager declaration from `devEngines`
|
|
580
|
+
*
|
|
581
|
+
* @returns Resolved concrete compatibility target
|
|
388
582
|
*/
|
|
389
583
|
function resolveCompatibilityTarget(compatibility, packageManager, devPackageManager) {
|
|
390
584
|
if (compatibility !== "auto") return compatibility;
|
|
391
|
-
const
|
|
392
|
-
const
|
|
393
|
-
|
|
394
|
-
|
|
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";
|
|
395
590
|
}
|
|
396
591
|
//#endregion
|
|
397
592
|
//#region src/utils/merge.ts
|
|
@@ -470,6 +665,9 @@ function mergeByStrategy(existing, incoming, strategy) {
|
|
|
470
665
|
}
|
|
471
666
|
//#endregion
|
|
472
667
|
//#region src/utils/npmrc.ts
|
|
668
|
+
/**
|
|
669
|
+
* Authentication and registry keys that must remain in `.npmrc`.
|
|
670
|
+
*/
|
|
473
671
|
const NPMRC_AUTH_OR_REGISTRY_KEYS = [
|
|
474
672
|
"_auth",
|
|
475
673
|
"_authtoken",
|
|
@@ -486,15 +684,26 @@ const NPMRC_AUTH_OR_REGISTRY_KEYS = [
|
|
|
486
684
|
"tokenhelper",
|
|
487
685
|
"username"
|
|
488
686
|
];
|
|
687
|
+
/**
|
|
688
|
+
* Pattern for pnpm's channel-specific Node.js mirror keys.
|
|
689
|
+
*/
|
|
489
690
|
const NODE_MIRROR_KEY_PATTERN = /^node-mirror:(?<channel>.+)$/iu;
|
|
490
691
|
/**
|
|
491
692
|
* Normalize `.npmrc` key for case-insensitive matching.
|
|
693
|
+
*
|
|
694
|
+
* @param key - Raw `.npmrc` key
|
|
695
|
+
*
|
|
696
|
+
* @returns Trimmed lower-case key without an array suffix
|
|
492
697
|
*/
|
|
493
698
|
function normalizeNpmrcKey(key) {
|
|
494
699
|
return key.trim().replace(/\[\]$/u, "").toLowerCase();
|
|
495
700
|
}
|
|
496
701
|
/**
|
|
497
702
|
* Extract key name from a raw `.npmrc` line.
|
|
703
|
+
*
|
|
704
|
+
* @param line - Raw line from an `.npmrc` file
|
|
705
|
+
*
|
|
706
|
+
* @returns Normalized key, or `undefined` for comments and invalid lines
|
|
498
707
|
*/
|
|
499
708
|
function getNpmrcLineKey(line) {
|
|
500
709
|
const trimmed = line.trim();
|
|
@@ -506,6 +715,10 @@ function getNpmrcLineKey(line) {
|
|
|
506
715
|
/**
|
|
507
716
|
* Check whether a `.npmrc` key is auth/registry-related and should stay in
|
|
508
717
|
* pnpm v11 and newer.
|
|
718
|
+
*
|
|
719
|
+
* @param key - Raw or normalized `.npmrc` key
|
|
720
|
+
*
|
|
721
|
+
* @returns `true` when the key must remain in `.npmrc`
|
|
509
722
|
*/
|
|
510
723
|
function isNpmrcAuthOrRegistryKey(key) {
|
|
511
724
|
const normalized = normalizeNpmrcKey(key);
|
|
@@ -516,11 +729,12 @@ function isNpmrcAuthOrRegistryKey(key) {
|
|
|
516
729
|
/**
|
|
517
730
|
* Remove pnpm-related settings from `.npmrc` file.
|
|
518
731
|
*
|
|
519
|
-
* This function reads the `.npmrc` file,
|
|
520
|
-
*
|
|
521
|
-
* 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.
|
|
522
734
|
*
|
|
523
735
|
* @param path - Absolute path to the `.npmrc` file
|
|
736
|
+
* @param compatibility - Concrete pnpm compatibility target
|
|
737
|
+
* @param migratedKeys - Original `.npmrc` keys that were migrated
|
|
524
738
|
*
|
|
525
739
|
* @returns A promise that resolves when the file has been pruned
|
|
526
740
|
*
|
|
@@ -528,17 +742,15 @@ function isNpmrcAuthOrRegistryKey(key) {
|
|
|
528
742
|
*
|
|
529
743
|
* @example
|
|
530
744
|
* ```ts
|
|
531
|
-
* await pruneNpmrc('/path/to/.npmrc')
|
|
745
|
+
* await pruneNpmrc('/path/to/.npmrc', 'v10', ['node-linker'])
|
|
532
746
|
* ```
|
|
533
747
|
*/
|
|
534
|
-
async function pruneNpmrc(path, compatibility
|
|
535
|
-
const pnpmSettingsFields = PNPM_SETTINGS_FIELDS.map((field) => kebabCase(field));
|
|
748
|
+
async function pruneNpmrc(path, compatibility, migratedKeys) {
|
|
536
749
|
const migratedKeySet = new Set(migratedKeys.map(normalizeNpmrcKey));
|
|
537
750
|
const updatedContent = (await fsReadFile(path)).split(/\r?\n/u).filter((line) => {
|
|
538
751
|
const key = getNpmrcLineKey(line);
|
|
539
752
|
if (!key) return true;
|
|
540
|
-
|
|
541
|
-
return !pnpmSettingsFields.some((field) => key.startsWith(field));
|
|
753
|
+
return !migratedKeySet.has(key);
|
|
542
754
|
}).join("\n").trimEnd();
|
|
543
755
|
if (compatibility !== "v10" && !updatedContent.trim()) {
|
|
544
756
|
await fsRemoveFile(path);
|
|
@@ -547,38 +759,28 @@ async function pruneNpmrc(path, compatibility = "v10", migratedKeys = []) {
|
|
|
547
759
|
await fsWriteFile(path, updatedContent);
|
|
548
760
|
}
|
|
549
761
|
/**
|
|
550
|
-
* Read
|
|
762
|
+
* Read `.npmrc` and return settings that should be migrated into workspace config.
|
|
551
763
|
*
|
|
552
|
-
*
|
|
553
|
-
*
|
|
554
|
-
*
|
|
764
|
+
* - `v10`: returns settings from the legacy whitelist.
|
|
765
|
+
* - `v11+`: excludes auth/registry keys because pnpm still reads them from
|
|
766
|
+
* `.npmrc`.
|
|
555
767
|
*
|
|
556
768
|
* @param path - Absolute path to the `.npmrc` file
|
|
769
|
+
* @param compatibility - Concrete pnpm compatibility target
|
|
557
770
|
*
|
|
558
|
-
* @returns
|
|
771
|
+
* @returns Migratable settings and their original `.npmrc` keys
|
|
559
772
|
*
|
|
560
773
|
* @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
774
|
*/
|
|
575
775
|
async function readMigratableNpmrc(path, compatibility) {
|
|
576
776
|
const raw = await readIniFile(path);
|
|
577
777
|
if (compatibility === "v10") {
|
|
578
|
-
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]]));
|
|
579
781
|
return {
|
|
580
|
-
keys
|
|
581
|
-
settings:
|
|
782
|
+
keys,
|
|
783
|
+
settings: camelcaseKeys(migratable)
|
|
582
784
|
};
|
|
583
785
|
}
|
|
584
786
|
const migratable = {};
|
|
@@ -598,11 +800,225 @@ async function readMigratableNpmrc(path, compatibility) {
|
|
|
598
800
|
};
|
|
599
801
|
}
|
|
600
802
|
//#endregion
|
|
803
|
+
//#region src/utils/settings-change.ts
|
|
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
|
+
/**
|
|
814
|
+
* Collect changed root settings between two workspace configurations.
|
|
815
|
+
*
|
|
816
|
+
* @param before - Workspace settings before migration
|
|
817
|
+
* @param after - Workspace settings after migration
|
|
818
|
+
*
|
|
819
|
+
* @returns Changed root settings with their before and after values
|
|
820
|
+
*/
|
|
821
|
+
function collectSettingsChanges(before, after) {
|
|
822
|
+
const beforeSettings = { ...before };
|
|
823
|
+
const afterSettings = { ...after };
|
|
824
|
+
const keys = /* @__PURE__ */ new Set([...Object.keys(beforeSettings), ...Object.keys(afterSettings)]);
|
|
825
|
+
return Array.from(keys).filter((key) => !isDeepStrictEqual(beforeSettings[key], afterSettings[key])).map((key) => ({
|
|
826
|
+
after: afterSettings[key],
|
|
827
|
+
before: beforeSettings[key],
|
|
828
|
+
key
|
|
829
|
+
}));
|
|
830
|
+
}
|
|
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
|
+
/**
|
|
864
|
+
* Create a line-based diff using the longest common subsequence.
|
|
865
|
+
*
|
|
866
|
+
* @param beforeLines - YAML lines before migration
|
|
867
|
+
* @param afterLines - YAML lines after migration
|
|
868
|
+
*
|
|
869
|
+
* @returns Classified diff lines in display order
|
|
870
|
+
*/
|
|
871
|
+
function createLineDiff(beforeLines, afterLines) {
|
|
872
|
+
if (beforeLines.length * afterLines.length > MAX_LCS_CELLS) return createLinearLineDiff(beforeLines, afterLines);
|
|
873
|
+
const lengths = Array.from({ length: beforeLines.length + 1 }, () => Array(afterLines.length + 1).fill(0));
|
|
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]);
|
|
875
|
+
const diff = [];
|
|
876
|
+
let beforeIndex = 0;
|
|
877
|
+
let afterIndex = 0;
|
|
878
|
+
while (beforeIndex < beforeLines.length && afterIndex < afterLines.length) if (beforeLines[beforeIndex] === afterLines[afterIndex]) {
|
|
879
|
+
diff.push({
|
|
880
|
+
kind: "unchanged",
|
|
881
|
+
value: beforeLines[beforeIndex]
|
|
882
|
+
});
|
|
883
|
+
beforeIndex++;
|
|
884
|
+
afterIndex++;
|
|
885
|
+
} else if (lengths[beforeIndex + 1][afterIndex] >= lengths[beforeIndex][afterIndex + 1]) {
|
|
886
|
+
diff.push({
|
|
887
|
+
kind: "removed",
|
|
888
|
+
value: beforeLines[beforeIndex]
|
|
889
|
+
});
|
|
890
|
+
beforeIndex++;
|
|
891
|
+
} else {
|
|
892
|
+
diff.push({
|
|
893
|
+
kind: "added",
|
|
894
|
+
value: afterLines[afterIndex]
|
|
895
|
+
});
|
|
896
|
+
afterIndex++;
|
|
897
|
+
}
|
|
898
|
+
while (beforeIndex < beforeLines.length) {
|
|
899
|
+
diff.push({
|
|
900
|
+
kind: "removed",
|
|
901
|
+
value: beforeLines[beforeIndex]
|
|
902
|
+
});
|
|
903
|
+
beforeIndex++;
|
|
904
|
+
}
|
|
905
|
+
while (afterIndex < afterLines.length) {
|
|
906
|
+
diff.push({
|
|
907
|
+
kind: "added",
|
|
908
|
+
value: afterLines[afterIndex]
|
|
909
|
+
});
|
|
910
|
+
afterIndex++;
|
|
911
|
+
}
|
|
912
|
+
return diff;
|
|
913
|
+
}
|
|
914
|
+
/**
|
|
915
|
+
* Format one root setting as YAML lines.
|
|
916
|
+
*
|
|
917
|
+
* @param key - Root setting key
|
|
918
|
+
* @param value - Setting value, or `undefined` when the key is absent
|
|
919
|
+
*
|
|
920
|
+
* @returns YAML lines for the setting, or an empty array when absent
|
|
921
|
+
*/
|
|
922
|
+
function formatSettingLines(key, value) {
|
|
923
|
+
if (value === void 0) return [];
|
|
924
|
+
return stringify({ [key]: value }).trimEnd().split("\n");
|
|
925
|
+
}
|
|
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
|
+
/**
|
|
938
|
+
* Format a setting change as a GitHub-style YAML diff.
|
|
939
|
+
*
|
|
940
|
+
* @param change - Root setting change to format
|
|
941
|
+
*
|
|
942
|
+
* @returns Classified YAML diff lines
|
|
943
|
+
*/
|
|
944
|
+
function createSettingsDiffLines(change) {
|
|
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
|
+
}));
|
|
949
|
+
}
|
|
950
|
+
/**
|
|
951
|
+
* Print a summary and GitHub-style YAML diff for every changed setting.
|
|
952
|
+
*
|
|
953
|
+
* @param changes - Root setting changes to report
|
|
954
|
+
*
|
|
955
|
+
* @returns Nothing; output is written through consola
|
|
956
|
+
*/
|
|
957
|
+
function reportSettingsChanges(changes) {
|
|
958
|
+
consola$1.info(`${changes.length} ${changes.length === 1 ? "setting" : "settings"} changed`);
|
|
959
|
+
for (const [index, change] of changes.entries()) {
|
|
960
|
+
if (index > 0) consola$1.log("");
|
|
961
|
+
for (const line of createSettingsDiffLines(change)) if (line.kind === "added") consola$1.log(green(`+ ${line.value}`));
|
|
962
|
+
else if (line.kind === "removed") consola$1.log(red(`- ${line.value}`));
|
|
963
|
+
else consola$1.log(dim(` ${line.value}`));
|
|
964
|
+
}
|
|
965
|
+
}
|
|
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
|
|
601
1000
|
//#region src/utils/config.ts
|
|
1001
|
+
/**
|
|
1002
|
+
* Resolve the indentation width for a YAML document.
|
|
1003
|
+
*
|
|
1004
|
+
* @param content - Raw YAML content
|
|
1005
|
+
*
|
|
1006
|
+
* @returns Detected indentation width or the project default
|
|
1007
|
+
*/
|
|
602
1008
|
function resolveYamlIndent(content) {
|
|
603
1009
|
const detectedIndent = detectIndent(content).amount;
|
|
604
1010
|
return detectedIndent > 0 ? detectedIndent : 2;
|
|
605
1011
|
}
|
|
1012
|
+
/**
|
|
1013
|
+
* Read and parse a package manifest when it exists.
|
|
1014
|
+
*
|
|
1015
|
+
* @param path - Absolute path to `package.json`
|
|
1016
|
+
* @param exists - Whether the package manifest exists
|
|
1017
|
+
*
|
|
1018
|
+
* @returns Parsed package manifest and detected indentation
|
|
1019
|
+
*
|
|
1020
|
+
* @throws {SyntaxError} When the package manifest contains invalid JSON
|
|
1021
|
+
*/
|
|
606
1022
|
async function readPackageJson(path, exists) {
|
|
607
1023
|
if (!exists) return {
|
|
608
1024
|
indent: 2,
|
|
@@ -614,17 +1030,46 @@ async function readPackageJson(path, exists) {
|
|
|
614
1030
|
value: JSON.parse(content)
|
|
615
1031
|
};
|
|
616
1032
|
}
|
|
1033
|
+
/**
|
|
1034
|
+
* Read and parse a pnpm workspace manifest when it exists.
|
|
1035
|
+
*
|
|
1036
|
+
* @param path - Absolute path to `pnpm-workspace.yaml`
|
|
1037
|
+
* @param exists - Whether the workspace manifest exists
|
|
1038
|
+
*
|
|
1039
|
+
* @returns Parsed workspace settings and detected indentation
|
|
1040
|
+
*
|
|
1041
|
+
* @throws {Error} When the workspace manifest contains invalid YAML
|
|
1042
|
+
*/
|
|
617
1043
|
async function readPnpmWorkspace(path, exists) {
|
|
618
1044
|
if (!exists) return {
|
|
1045
|
+
document: new Document({}),
|
|
619
1046
|
indent: 2,
|
|
620
1047
|
value: {}
|
|
621
1048
|
};
|
|
622
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({});
|
|
623
1056
|
return {
|
|
1057
|
+
document,
|
|
624
1058
|
indent: resolveYamlIndent(content),
|
|
625
|
-
value:
|
|
1059
|
+
value: parsedValue ?? {}
|
|
626
1060
|
};
|
|
627
1061
|
}
|
|
1062
|
+
/**
|
|
1063
|
+
* Resolve migratable pnpm settings from a package manifest.
|
|
1064
|
+
*
|
|
1065
|
+
* Yarn resolutions are merged into pnpm overrides when enabled. Empty
|
|
1066
|
+
* overrides are removed from the result.
|
|
1067
|
+
*
|
|
1068
|
+
* @param packageJson - Parsed package manifest
|
|
1069
|
+
* @param yarnResolutions - Whether to convert Yarn resolutions to pnpm overrides
|
|
1070
|
+
*
|
|
1071
|
+
* @returns Pnpm workspace settings resolved from the package manifest
|
|
1072
|
+
*/
|
|
628
1073
|
function resolvePackageJsonSettings(packageJson, yarnResolutions) {
|
|
629
1074
|
const pnpmSettings = yarnResolutions && packageJson.resolutions ? {
|
|
630
1075
|
...packageJson.pnpm,
|
|
@@ -649,6 +1094,10 @@ function hasMigratableSettings(sources) {
|
|
|
649
1094
|
function assertCanMigrateRuntime(runtimeVersion, packageJsonExists) {
|
|
650
1095
|
if (runtimeVersion && !packageJsonExists) throw new Error("Cannot migrate the removed Node.js runtime setting without a package.json file.");
|
|
651
1096
|
}
|
|
1097
|
+
function reportMigrationChanges(showChanges, before = {}, after = {}) {
|
|
1098
|
+
if (!showChanges) return;
|
|
1099
|
+
reportSettingsChanges(collectSettingsChanges(before, after));
|
|
1100
|
+
}
|
|
652
1101
|
/**
|
|
653
1102
|
* Migrate pnpm settings from legacy locations to `pnpm-workspace.yaml`.
|
|
654
1103
|
*
|
|
@@ -666,6 +1115,7 @@ function assertCanMigrateRuntime(runtimeVersion, packageJsonExists) {
|
|
|
666
1115
|
* @param rawOptions.yarnResolutions - Whether to migrate resolutions field (default: true)
|
|
667
1116
|
* @param rawOptions.sortKeys - Whether to sort keys in output YAML (default: false)
|
|
668
1117
|
* @param rawOptions.newlineBetween - Add newlines between root keys (default: true)
|
|
1118
|
+
* @param rawOptions.showChanges - Show settings changes after migration (default: true)
|
|
669
1119
|
*
|
|
670
1120
|
* @returns A promise that resolves when migration is complete
|
|
671
1121
|
*
|
|
@@ -695,19 +1145,19 @@ async function migratePnpmSettings(rawOptions = {}) {
|
|
|
695
1145
|
fsExists(packageJsonPath),
|
|
696
1146
|
fsExists(pnpmWorkspaceYamlPath)
|
|
697
1147
|
]);
|
|
698
|
-
if (!hasSettingsSources(npmrcExists, packageJsonExists, pnpmWorkspaceExists))
|
|
1148
|
+
if (!hasSettingsSources(npmrcExists, packageJsonExists, pnpmWorkspaceExists)) {
|
|
1149
|
+
reportMigrationChanges(options.showChanges);
|
|
1150
|
+
return;
|
|
1151
|
+
}
|
|
699
1152
|
const [packageJson, pnpmWorkspace] = await Promise.all([readPackageJson(packageJsonPath, packageJsonExists), readPnpmWorkspace(pnpmWorkspaceYamlPath, pnpmWorkspaceExists)]);
|
|
1153
|
+
const pnpmWorkspaceBefore = structuredClone(pnpmWorkspace.value);
|
|
700
1154
|
const compatibility = resolveCompatibilityTarget(options.compatibility, packageJson.value.packageManager, packageJson.value.devEngines?.packageManager);
|
|
701
1155
|
const npmrcMigratable = npmrcExists ? await readMigratableNpmrc(npmrcPath, compatibility) : {
|
|
702
1156
|
keys: [],
|
|
703
1157
|
settings: {}
|
|
704
1158
|
};
|
|
705
1159
|
const pnpmSettingsInNpmrc = npmrcMigratable.settings;
|
|
706
|
-
const
|
|
707
|
-
const incomingSettings = {
|
|
708
|
-
...pnpmSettingsInNpmrc,
|
|
709
|
-
...pnpmSettingsInPackageJson
|
|
710
|
-
};
|
|
1160
|
+
const incomingSettings = mergeByStrategy(resolvePackageJsonSettings(packageJson.value, options.yarnResolutions), pnpmSettingsInNpmrc, "merge");
|
|
711
1161
|
const [existingNormalization, incomingNormalization] = await Promise.all([normalizeIncomingSettings(pnpmWorkspace.value, {
|
|
712
1162
|
compatibility,
|
|
713
1163
|
cwd: options.cwd,
|
|
@@ -729,19 +1179,16 @@ async function migratePnpmSettings(rawOptions = {}) {
|
|
|
729
1179
|
yarnResolutions: options.yarnResolutions
|
|
730
1180
|
})) {
|
|
731
1181
|
consola$1.warn("No pnpm settings fields to migrate");
|
|
1182
|
+
reportMigrationChanges(options.showChanges);
|
|
732
1183
|
return;
|
|
733
1184
|
}
|
|
734
1185
|
const pnpmWorkspaceResult = mergeByStrategy(pnpmWorkspace.value, incomingSettings, options.strategy);
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
value
|
|
740
|
-
});
|
|
1186
|
+
updateYamlDocument(pnpmWorkspace.document, {
|
|
1187
|
+
after: pnpmWorkspaceResult,
|
|
1188
|
+
before: pnpmWorkspaceBefore,
|
|
1189
|
+
sortKeys: options.sortKeys
|
|
741
1190
|
});
|
|
742
|
-
const
|
|
743
|
-
await fsWriteFile(pnpmWorkspaceYamlPath, options.newlineBetween ? yamlContent.replace(/\n(?=[^\s#][^:\n]*:)/gu, "\n\n") : yamlContent);
|
|
744
|
-
if (npmrcExists && options.cleanNpmrc) await pruneNpmrc(npmrcPath, compatibility, npmrcMigratable.keys);
|
|
1191
|
+
const finalYamlContent = formatRootSpacing(pnpmWorkspace.document.toString({ indent: pnpmWorkspace.indent }), options.newlineBetween);
|
|
745
1192
|
let packageJsonChanged = runtimeMigration.changed;
|
|
746
1193
|
if (packageJsonExists && options.cleanPackageJson) {
|
|
747
1194
|
if (packageJson.value.pnpm) packageJsonChanged = true;
|
|
@@ -751,7 +1198,10 @@ async function migratePnpmSettings(rawOptions = {}) {
|
|
|
751
1198
|
delete packageJson.value.resolutions;
|
|
752
1199
|
}
|
|
753
1200
|
}
|
|
1201
|
+
await fsWriteFile(pnpmWorkspaceYamlPath, finalYamlContent);
|
|
754
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);
|
|
1204
|
+
reportMigrationChanges(options.showChanges, pnpmWorkspaceBefore, pnpmWorkspaceResult);
|
|
755
1205
|
} catch (err) {
|
|
756
1206
|
consola$1.error("Failed to migrate pnpm settings:", err);
|
|
757
1207
|
throw err;
|
|
@@ -759,9 +1209,8 @@ async function migratePnpmSettings(rawOptions = {}) {
|
|
|
759
1209
|
}
|
|
760
1210
|
//#endregion
|
|
761
1211
|
//#region src/cli.ts
|
|
762
|
-
const cli =
|
|
763
|
-
cli.
|
|
764
|
-
cli.command("").action(async (options) => {
|
|
1212
|
+
const cli = createCli();
|
|
1213
|
+
cli.command("").usage("[options]").action(async (options) => {
|
|
765
1214
|
try {
|
|
766
1215
|
consola.log(`\n${bold(magenta(name))} ${dim(`v${version}`)}`);
|
|
767
1216
|
consola.log(dim("\n--------------\n"));
|