pnpm-settings-migrator 0.8.0 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +33 -16
- package/dist/cli.mjs +1346 -220
- package/dist/index.d.mts +50 -14
- package/dist/index.mjs +1323 -219
- package/package.json +10 -5
package/dist/cli.mjs
CHANGED
|
@@ -1,87 +1,136 @@
|
|
|
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, relative, resolve } from "pathe";
|
|
5
|
+
import camelcaseKeys from "camelcase-keys";
|
|
6
|
+
import { randomUUID } from "node:crypto";
|
|
7
|
+
import { access, chmod, readFile, rename, rm, stat, writeFile } from "node:fs/promises";
|
|
8
|
+
import { fileURLToPath } from "node:url";
|
|
7
9
|
import { getColor } from "consola/utils";
|
|
8
|
-
import { isPlainObject, isUndefined
|
|
10
|
+
import { isPlainObject, isUndefined } from "@ntnyq/utils";
|
|
9
11
|
import { defu } from "defu";
|
|
10
|
-
import camelcaseKeys from "camelcase-keys";
|
|
11
12
|
import { readIniFile } from "read-ini-file";
|
|
12
13
|
import { kebabCase } from "uncase";
|
|
13
|
-
import {
|
|
14
|
+
import { glob } from "tinyglobby";
|
|
14
15
|
import detectIndent from "detect-indent";
|
|
16
|
+
import { Document, isMap, isNode, isScalar, isSeq, parseDocument, stringify } from "yaml";
|
|
17
|
+
import { isDeepStrictEqual } from "node:util";
|
|
15
18
|
//#region package.json
|
|
16
19
|
var name = "pnpm-settings-migrator";
|
|
17
|
-
var version = "0.
|
|
20
|
+
var version = "0.9.0";
|
|
21
|
+
//#endregion
|
|
22
|
+
//#region src/cli-options.ts
|
|
23
|
+
/**
|
|
24
|
+
* Create the command-line parser shared by the executable and parser tests.
|
|
25
|
+
*
|
|
26
|
+
* @returns Configured command-line parser
|
|
27
|
+
*/
|
|
28
|
+
function createCli() {
|
|
29
|
+
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");
|
|
30
|
+
cli.help((sections) => {
|
|
31
|
+
if (!(cli.commands.length > 0 && cli.commands.every((command) => command.isDefaultCommand))) return sections;
|
|
32
|
+
return sections.filter((section) => section.title !== "Commands" && !section.title?.startsWith("For more info"));
|
|
33
|
+
});
|
|
34
|
+
return cli;
|
|
35
|
+
}
|
|
18
36
|
//#endregion
|
|
19
37
|
//#region src/constants.ts
|
|
20
38
|
const NPMRC = ".npmrc";
|
|
21
39
|
const PACKAGE_JSON = "package.json";
|
|
22
40
|
const PNPM_WORKSPACE_YAML = "pnpm-workspace.yaml";
|
|
23
41
|
/**
|
|
24
|
-
* @see {@link https://
|
|
25
|
-
*
|
|
42
|
+
* @see {@link https://pnpm.io/10.x/settings}
|
|
43
|
+
* Documented pnpm v10 config fields that can be migrated from `.npmrc`.
|
|
26
44
|
*
|
|
27
45
|
* Intentionally excluded workspace manifest-only fields:
|
|
28
46
|
* - `catalog`
|
|
29
|
-
* - `catalogMode`
|
|
30
47
|
* - `catalogs`
|
|
31
|
-
* - `cleanupUnusedCatalogs`
|
|
32
48
|
* - `packages`
|
|
33
49
|
*/
|
|
34
|
-
const
|
|
50
|
+
const PNPM_V10_NPMRC_SETTINGS_FIELDS = [
|
|
35
51
|
"allowBuilds",
|
|
36
52
|
"allowedDeprecatedVersions",
|
|
37
53
|
"allowNonAppliedPatches",
|
|
38
54
|
"allowUnusedPatches",
|
|
39
55
|
"auditConfig",
|
|
56
|
+
"auditLevel",
|
|
57
|
+
"autoInstallPeers",
|
|
58
|
+
"blockExoticSubdeps",
|
|
59
|
+
"cacheDir",
|
|
60
|
+
"catalogMode",
|
|
61
|
+
"childConcurrency",
|
|
62
|
+
"ci",
|
|
63
|
+
"cleanupUnusedCatalogs",
|
|
64
|
+
"color",
|
|
40
65
|
"configDependencies",
|
|
66
|
+
"dangerouslyAllowAllBuilds",
|
|
41
67
|
"dedupeDirectDeps",
|
|
42
68
|
"dedupeInjectedDeps",
|
|
43
69
|
"dedupePeerDependents",
|
|
44
70
|
"deployAllFiles",
|
|
71
|
+
"disallowWorkspaceCycles",
|
|
72
|
+
"dlxCacheMaxAge",
|
|
73
|
+
"enableGlobalVirtualStore",
|
|
74
|
+
"enableModulesDir",
|
|
75
|
+
"enablePrePostScripts",
|
|
45
76
|
"engineStrict",
|
|
46
77
|
"executionEnv",
|
|
78
|
+
"extendNodePath",
|
|
79
|
+
"failIfNoMatch",
|
|
47
80
|
"fetchMinSpeedKiBps",
|
|
81
|
+
"fetchRetries",
|
|
48
82
|
"fetchRetryFactor",
|
|
49
83
|
"fetchRetryMaxtimeout",
|
|
50
84
|
"fetchRetryMintimeout",
|
|
51
|
-
"
|
|
85
|
+
"fetchTimeout",
|
|
52
86
|
"fetchWarnTimeoutMs",
|
|
53
87
|
"forceLegacyDeploy",
|
|
88
|
+
"frozenLockfile",
|
|
89
|
+
"gitBranchLockfile",
|
|
54
90
|
"gitChecks",
|
|
55
91
|
"gitShallowHosts",
|
|
56
|
-
"
|
|
92
|
+
"globalBinDir",
|
|
93
|
+
"globalDir",
|
|
94
|
+
"globalPnpmfile",
|
|
57
95
|
"globalVirtualStoreDir",
|
|
58
96
|
"hoist",
|
|
59
97
|
"hoistPattern",
|
|
60
98
|
"hoistWorkspacePackages",
|
|
61
99
|
"httpProxy",
|
|
62
100
|
"httpsProxy",
|
|
101
|
+
"ignoreCompatibilityDb",
|
|
63
102
|
"ignoredBuiltDependencies",
|
|
64
103
|
"ignoreDepScripts",
|
|
65
|
-
"ignoreDeps",
|
|
66
104
|
"ignoredOptionalDependencies",
|
|
67
105
|
"ignorePatchFailures",
|
|
106
|
+
"ignorePnpmfile",
|
|
107
|
+
"ignoreScripts",
|
|
68
108
|
"ignoreWorkspaceCycles",
|
|
109
|
+
"ignoreWorkspaceRootCheck",
|
|
110
|
+
"includeWorkspaceRoot",
|
|
69
111
|
"injectWorkspacePackages",
|
|
70
112
|
"linkWorkspacePackages",
|
|
113
|
+
"localAddress",
|
|
71
114
|
"lockfile",
|
|
72
115
|
"lockfileDir",
|
|
116
|
+
"lockfileIncludeTarballUrl",
|
|
117
|
+
"loglevel",
|
|
73
118
|
"managePackageManagerVersions",
|
|
119
|
+
"maxsockets",
|
|
74
120
|
"mergeGitBranchLockfiles",
|
|
75
121
|
"mergeGitBranchLockfilesBranchPattern",
|
|
122
|
+
"minimumReleaseAge",
|
|
123
|
+
"minimumReleaseAgeExclude",
|
|
76
124
|
"modulesCacheMaxAge",
|
|
125
|
+
"modulesDir",
|
|
77
126
|
"networkConcurrency",
|
|
78
127
|
"neverBuiltDependencies",
|
|
79
|
-
"nodeDownloadMirrors",
|
|
80
128
|
"nodeLinker",
|
|
81
|
-
"
|
|
129
|
+
"nodeOptions",
|
|
130
|
+
"nodeVersion",
|
|
82
131
|
"noProxy",
|
|
132
|
+
"noproxy",
|
|
83
133
|
"npmPath",
|
|
84
|
-
"npmrcAuthFile",
|
|
85
134
|
"onlyBuiltDependencies",
|
|
86
135
|
"onlyBuiltDependenciesFile",
|
|
87
136
|
"optimisticRepeatInstall",
|
|
@@ -95,38 +144,46 @@ const PNPM_SETTINGS_FIELDS = [
|
|
|
95
144
|
"peerDependencyRules",
|
|
96
145
|
"peersSuffixMaxLength",
|
|
97
146
|
"pnpmfile",
|
|
98
|
-
"pnpmfileIgnorePkgManifest",
|
|
99
|
-
"pmOnFail",
|
|
100
147
|
"preferFrozenLockfile",
|
|
148
|
+
"preferSymlinkedExecutables",
|
|
101
149
|
"preferWorkspacePackages",
|
|
102
150
|
"production",
|
|
103
151
|
"proxy",
|
|
104
152
|
"publicHoistPattern",
|
|
105
153
|
"recursiveInstall",
|
|
106
|
-
"
|
|
154
|
+
"registrySupportsTimeField",
|
|
107
155
|
"requiredScripts",
|
|
108
156
|
"resolutionMode",
|
|
109
157
|
"resolvePeersFromWorkspaceRoot",
|
|
110
158
|
"saveExact",
|
|
111
159
|
"savePrefix",
|
|
160
|
+
"saveWorkspaceProtocol",
|
|
112
161
|
"scriptShell",
|
|
113
162
|
"shamefullyHoist",
|
|
114
163
|
"sharedWorkspaceLockfile",
|
|
115
164
|
"shellEmulator",
|
|
116
165
|
"sideEffectsCache",
|
|
117
166
|
"sideEffectsCacheReadonly",
|
|
167
|
+
"stateDir",
|
|
118
168
|
"storeDir",
|
|
119
169
|
"strictDepBuilds",
|
|
120
170
|
"strictPeerDependencies",
|
|
121
171
|
"strictSsl",
|
|
172
|
+
"strictStorePkgContentCheck",
|
|
122
173
|
"supportedArchitectures",
|
|
174
|
+
"symlink",
|
|
123
175
|
"syncInjectedDepsAfterScripts",
|
|
124
176
|
"tag",
|
|
125
|
-
"
|
|
126
|
-
"
|
|
177
|
+
"trustPolicy",
|
|
178
|
+
"trustPolicyExclude",
|
|
179
|
+
"trustPolicyIgnoreAfter",
|
|
180
|
+
"unsafePerm",
|
|
127
181
|
"updateConfig",
|
|
182
|
+
"updateNotifier",
|
|
128
183
|
"useBetaCli",
|
|
129
184
|
"useNodeVersion",
|
|
185
|
+
"useRunningStoreServer",
|
|
186
|
+
"useStderr",
|
|
130
187
|
"verifyDepsBeforeRun",
|
|
131
188
|
"verifyStoreIntegrity",
|
|
132
189
|
"virtualStoreDir",
|
|
@@ -148,80 +205,37 @@ const PNPM_V11_REMOVED_SETTINGS = [
|
|
|
148
205
|
"useNodeVersion"
|
|
149
206
|
];
|
|
150
207
|
//#endregion
|
|
151
|
-
//#region src/
|
|
208
|
+
//#region src/utils/fs.ts
|
|
152
209
|
/**
|
|
153
|
-
*
|
|
210
|
+
* Modulus that isolates Unix permission bits from a stat mode.
|
|
154
211
|
*/
|
|
155
|
-
const
|
|
156
|
-
cleanNpmrc: true,
|
|
157
|
-
cleanPackageJson: true,
|
|
158
|
-
compatibility: "auto",
|
|
159
|
-
cwd: process.cwd(),
|
|
160
|
-
newlineBetween: true,
|
|
161
|
-
replaceDeprecated: false,
|
|
162
|
-
showChanges: true,
|
|
163
|
-
sortKeys: false,
|
|
164
|
-
strategy: "merge",
|
|
165
|
-
yarnResolutions: true
|
|
166
|
-
};
|
|
167
|
-
const VALID_STRATEGIES = [
|
|
168
|
-
"discard",
|
|
169
|
-
"merge",
|
|
170
|
-
"overwrite"
|
|
171
|
-
];
|
|
172
|
-
const VALID_COMPATIBILITIES = [
|
|
173
|
-
"auto",
|
|
174
|
-
"v10",
|
|
175
|
-
"v11",
|
|
176
|
-
"v12"
|
|
177
|
-
];
|
|
178
|
-
function resolveCompatibility(compatibility) {
|
|
179
|
-
if (!compatibility) return DEFAULT_OPTIONS.compatibility;
|
|
180
|
-
if (VALID_COMPATIBILITIES.includes(compatibility)) return compatibility;
|
|
181
|
-
throw new Error(`Invalid compatibility: ${compatibility}. Expected one of: ${VALID_COMPATIBILITIES.join(", ")}`);
|
|
182
|
-
}
|
|
183
|
-
function resolveStrategy(strategy) {
|
|
184
|
-
if (!strategy) return DEFAULT_OPTIONS.strategy;
|
|
185
|
-
if (VALID_STRATEGIES.includes(strategy)) return strategy;
|
|
186
|
-
throw new Error(`Invalid strategy: ${strategy}. Expected one of: ${VALID_STRATEGIES.join(", ")}`);
|
|
187
|
-
}
|
|
212
|
+
const FILE_PERMISSION_MODULUS = 512;
|
|
188
213
|
/**
|
|
189
|
-
* Resolve
|
|
190
|
-
*
|
|
191
|
-
* This function takes partial options and returns a complete options object
|
|
192
|
-
* with all properties set to either the provided value or the default value.
|
|
214
|
+
* Resolve a filesystem path to a string suitable for a sibling temp file.
|
|
193
215
|
*
|
|
194
|
-
* @param
|
|
216
|
+
* @param path - Filesystem path to normalize
|
|
195
217
|
*
|
|
196
|
-
* @returns
|
|
218
|
+
* @returns String path suitable for path manipulation
|
|
219
|
+
*/
|
|
220
|
+
function resolvePathString(path) {
|
|
221
|
+
if (path instanceof URL) return fileURLToPath(path);
|
|
222
|
+
return typeof path === "string" ? path : path.toString();
|
|
223
|
+
}
|
|
224
|
+
/**
|
|
225
|
+
* Read an existing file's permission bits, if the file exists.
|
|
197
226
|
*
|
|
198
|
-
* @
|
|
199
|
-
* ```ts
|
|
200
|
-
* // Use all defaults
|
|
201
|
-
* const opts = resolveOptions()
|
|
202
|
-
* // { cleanNpmrc: true, cleanPackageJson: true, cwd: '/current/dir', ... }
|
|
227
|
+
* @param path - Filesystem path to inspect
|
|
203
228
|
*
|
|
204
|
-
*
|
|
205
|
-
* const opts = resolveOptions({ sortKeys: true, cleanNpmrc: false })
|
|
206
|
-
* // { cleanNpmrc: false, cleanPackageJson: true, sortKeys: true, ... }
|
|
207
|
-
* ```
|
|
229
|
+
* @returns Unix permission bits, or `undefined` when the file does not exist
|
|
208
230
|
*/
|
|
209
|
-
function
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
sortKeys: options.sortKeys ?? DEFAULT_OPTIONS.sortKeys,
|
|
217
|
-
strategy: resolveStrategy(options.strategy),
|
|
218
|
-
yarnResolutions: options.yarnResolutions ?? DEFAULT_OPTIONS.yarnResolutions,
|
|
219
|
-
cleanPackageJson: options.cleanPackageJson ?? DEFAULT_OPTIONS.cleanPackageJson,
|
|
220
|
-
replaceDeprecated: options.replaceDeprecated ?? DEFAULT_OPTIONS.replaceDeprecated
|
|
221
|
-
};
|
|
231
|
+
async function resolveExistingMode(path) {
|
|
232
|
+
try {
|
|
233
|
+
return (await stat(path)).mode % FILE_PERMISSION_MODULUS;
|
|
234
|
+
} catch (error) {
|
|
235
|
+
if (error.code === "ENOENT") return;
|
|
236
|
+
throw error;
|
|
237
|
+
}
|
|
222
238
|
}
|
|
223
|
-
//#endregion
|
|
224
|
-
//#region src/utils/fs.ts
|
|
225
239
|
/**
|
|
226
240
|
* Check whether a filesystem path exists.
|
|
227
241
|
*
|
|
@@ -261,52 +275,208 @@ async function fsRemoveFile(path) {
|
|
|
261
275
|
* @returns A promise that resolves after the file is written
|
|
262
276
|
*/
|
|
263
277
|
async function fsWriteFile(path, content) {
|
|
264
|
-
|
|
278
|
+
const pathString = resolvePathString(path);
|
|
279
|
+
const temporaryPath = join(dirname(pathString), `.${basename(pathString)}.${process.pid}.${randomUUID()}.tmp`);
|
|
280
|
+
const mode = await resolveExistingMode(path);
|
|
281
|
+
try {
|
|
282
|
+
await writeFile(temporaryPath, `${content.trimEnd()}\n`, {
|
|
283
|
+
encoding: "utf-8",
|
|
284
|
+
mode
|
|
285
|
+
});
|
|
286
|
+
if (mode !== void 0) await chmod(temporaryPath, mode);
|
|
287
|
+
await rename(temporaryPath, pathString);
|
|
288
|
+
} finally {
|
|
289
|
+
await rm(temporaryPath, { force: true });
|
|
290
|
+
}
|
|
265
291
|
}
|
|
266
|
-
|
|
267
|
-
|
|
292
|
+
//#endregion
|
|
293
|
+
//#region src/utils/color.ts
|
|
268
294
|
/**
|
|
269
295
|
* Apply dim terminal styling to a value.
|
|
296
|
+
*
|
|
297
|
+
* @param text - Text or number to style
|
|
298
|
+
*
|
|
299
|
+
* @returns ANSI-styled string
|
|
270
300
|
*/
|
|
271
301
|
const dim = getColor("dim");
|
|
272
302
|
/**
|
|
273
303
|
* Apply green terminal styling to a value.
|
|
304
|
+
*
|
|
305
|
+
* @param text - Text or number to style
|
|
306
|
+
*
|
|
307
|
+
* @returns ANSI-styled string
|
|
274
308
|
*/
|
|
275
309
|
const green = getColor("green");
|
|
276
310
|
/**
|
|
277
311
|
* Apply red terminal styling to a value.
|
|
312
|
+
*
|
|
313
|
+
* @param text - Text or number to style
|
|
314
|
+
*
|
|
315
|
+
* @returns ANSI-styled string
|
|
278
316
|
*/
|
|
279
317
|
const red = getColor("red");
|
|
280
318
|
/**
|
|
281
319
|
* Apply bold terminal styling to a value.
|
|
320
|
+
*
|
|
321
|
+
* @param text - Text or number to style
|
|
322
|
+
*
|
|
323
|
+
* @returns ANSI-styled string
|
|
282
324
|
*/
|
|
283
325
|
const bold = getColor("bold");
|
|
284
326
|
/**
|
|
285
327
|
* Apply magenta terminal styling to a value.
|
|
328
|
+
*
|
|
329
|
+
* @param text - Text or number to style
|
|
330
|
+
*
|
|
331
|
+
* @returns ANSI-styled string
|
|
286
332
|
*/
|
|
287
333
|
const magenta = getColor("magenta");
|
|
288
334
|
//#endregion
|
|
289
|
-
//#region src/utils/
|
|
335
|
+
//#region src/utils/deprecated-settings.ts
|
|
290
336
|
/**
|
|
291
|
-
*
|
|
337
|
+
* Replace deprecated audit fields with the structured `audit` setting.
|
|
338
|
+
*
|
|
339
|
+
* @param settings - Workspace settings to normalize in place
|
|
292
340
|
*/
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
341
|
+
function normalizeAuditSettings(settings) {
|
|
342
|
+
const level = settings.audit?.level ?? settings.auditLevel;
|
|
343
|
+
const ignore = settings.audit?.ignore ?? settings.auditConfig?.ignoreGhsas;
|
|
344
|
+
if (level !== void 0 || ignore !== void 0) settings.audit = {
|
|
345
|
+
...settings.audit,
|
|
346
|
+
...level === void 0 ? {} : { level },
|
|
347
|
+
...ignore === void 0 ? {} : { ignore }
|
|
348
|
+
};
|
|
349
|
+
Reflect.deleteProperty(settings, "auditConfig");
|
|
350
|
+
Reflect.deleteProperty(settings, "auditLevel");
|
|
351
|
+
}
|
|
352
|
+
/**
|
|
353
|
+
* Replace deprecated update fields with the structured `update` setting.
|
|
354
|
+
*
|
|
355
|
+
* @param settings - Workspace settings to normalize in place
|
|
356
|
+
*/
|
|
357
|
+
function normalizeUpdateSettings(settings) {
|
|
358
|
+
const { updateConfig } = settings;
|
|
359
|
+
if (!updateConfig) return;
|
|
360
|
+
settings.update = {
|
|
361
|
+
...updateConfig.ignoreDependencies === void 0 ? {} : { ignoreDeps: updateConfig.ignoreDependencies },
|
|
362
|
+
...updateConfig.changeset === void 0 ? {} : { changeset: updateConfig.changeset },
|
|
363
|
+
...updateConfig.githubActions === void 0 ? {} : { githubActions: updateConfig.githubActions },
|
|
364
|
+
...updateConfig.githubActionsServer === void 0 ? {} : { githubActionsServer: updateConfig.githubActionsServer },
|
|
365
|
+
...settings.update
|
|
366
|
+
};
|
|
367
|
+
Reflect.deleteProperty(settings, "updateConfig");
|
|
368
|
+
}
|
|
369
|
+
/**
|
|
370
|
+
* Replace former scalar aliases with their canonical v11+ settings.
|
|
371
|
+
*
|
|
372
|
+
* @param settings - Workspace settings to normalize in place
|
|
373
|
+
*/
|
|
374
|
+
function normalizeScalarAliases(settings) {
|
|
375
|
+
if (settings.cleanupUnusedCatalogs !== void 0) {
|
|
376
|
+
settings.catalogPrune ??= settings.cleanupUnusedCatalogs;
|
|
377
|
+
Reflect.deleteProperty(settings, "cleanupUnusedCatalogs");
|
|
378
|
+
}
|
|
379
|
+
if (settings.enableGlobalVirtualStore !== void 0) {
|
|
380
|
+
settings.virtualStoreType ??= settings.enableGlobalVirtualStore ? "global" : "project";
|
|
381
|
+
Reflect.deleteProperty(settings, "enableGlobalVirtualStore");
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
/**
|
|
385
|
+
* Replace the older side-effects cache spellings with one structured value.
|
|
386
|
+
*
|
|
387
|
+
* @param settings - Workspace settings to normalize in place
|
|
388
|
+
*/
|
|
389
|
+
function normalizeSideEffectsCache(settings) {
|
|
390
|
+
const declared = settings.sideEffectsCache;
|
|
391
|
+
const readonly = settings.sideEffectsCacheReadonly;
|
|
392
|
+
const remote = settings.remoteSideEffectsCache;
|
|
393
|
+
if (readonly === void 0 && remote === void 0) return;
|
|
394
|
+
const structured = typeof declared === "object" && declared !== null ? declared : void 0;
|
|
395
|
+
const shorthand = typeof declared === "boolean" ? declared : void 0;
|
|
396
|
+
settings.sideEffectsCache = {
|
|
397
|
+
...shorthand === void 0 ? {} : {
|
|
398
|
+
read: readonly === true ? true : shorthand,
|
|
399
|
+
write: readonly === true ? false : shorthand
|
|
400
|
+
},
|
|
401
|
+
...readonly === void 0 || shorthand !== void 0 ? {} : {
|
|
402
|
+
read: readonly,
|
|
403
|
+
...readonly ? { write: false } : {}
|
|
404
|
+
},
|
|
405
|
+
...remote === void 0 ? {} : { remote },
|
|
406
|
+
...structured
|
|
407
|
+
};
|
|
408
|
+
Reflect.deleteProperty(settings, "remoteSideEffectsCache");
|
|
409
|
+
Reflect.deleteProperty(settings, "sideEffectsCacheReadonly");
|
|
410
|
+
}
|
|
411
|
+
function addRegistryScope(declarations, url, scope) {
|
|
412
|
+
declarations[url] ??= {};
|
|
413
|
+
const declaration = declarations[url];
|
|
414
|
+
declaration.scopes = [.../* @__PURE__ */ new Set([...declaration.scopes ?? [], scope])];
|
|
415
|
+
}
|
|
416
|
+
/**
|
|
417
|
+
* Replace `namedRegistries` with URL-keyed registry declarations.
|
|
418
|
+
*
|
|
419
|
+
* A URL can expose only one canonical prefix. Conflicting legacy aliases are
|
|
420
|
+
* therefore retained for manual resolution instead of being discarded.
|
|
421
|
+
*
|
|
422
|
+
* @param settings - Workspace settings to normalize in place
|
|
423
|
+
* @param warnings - Collection that receives unresolved conflict warnings
|
|
424
|
+
*/
|
|
425
|
+
function normalizeNamedRegistries(settings, warnings) {
|
|
426
|
+
const { namedRegistries } = settings;
|
|
427
|
+
if (!namedRegistries) return;
|
|
428
|
+
const prefixesByUrl = /* @__PURE__ */ new Map();
|
|
429
|
+
for (const [prefix, url] of Object.entries(namedRegistries)) {
|
|
430
|
+
const prefixes = prefixesByUrl.get(url) ?? [];
|
|
431
|
+
prefixes.push(prefix);
|
|
432
|
+
prefixesByUrl.set(url, prefixes);
|
|
433
|
+
}
|
|
434
|
+
const conflicts = [...prefixesByUrl.entries()].filter(([, prefixes]) => prefixes.length > 1);
|
|
435
|
+
if (conflicts.length) {
|
|
436
|
+
warnings.push(`namedRegistries was kept because the new registries format supports one prefix per URL: ${conflicts.map(([url, prefixes]) => `${url} (${prefixes.join(", ")})`).join("; ")}.`);
|
|
437
|
+
return;
|
|
438
|
+
}
|
|
439
|
+
const declarations = {};
|
|
440
|
+
for (const [key, value] of Object.entries(settings.registries ?? {})) if (typeof value === "string") addRegistryScope(declarations, value, key === "default" ? "@" : key);
|
|
441
|
+
else declarations[key] = { ...value };
|
|
442
|
+
for (const [url, [prefix]] of prefixesByUrl) {
|
|
443
|
+
declarations[url] ??= {};
|
|
444
|
+
const declaration = declarations[url];
|
|
445
|
+
if (declaration.prefix && declaration.prefix !== prefix) {
|
|
446
|
+
warnings.push(`namedRegistries was kept because ${url} already declares prefix ${declaration.prefix}.`);
|
|
447
|
+
return;
|
|
448
|
+
}
|
|
449
|
+
declaration.prefix = prefix;
|
|
450
|
+
}
|
|
451
|
+
settings.registries = declarations;
|
|
452
|
+
Reflect.deleteProperty(settings, "namedRegistries");
|
|
453
|
+
}
|
|
454
|
+
/**
|
|
455
|
+
* Replace aliases that remain accepted but are no longer canonical.
|
|
456
|
+
*
|
|
457
|
+
* @param settings - Workspace settings to normalize in place
|
|
458
|
+
* @param warnings - Collection that receives unresolved conflict warnings
|
|
459
|
+
*
|
|
460
|
+
* @returns Nothing; settings and warnings are updated in place
|
|
461
|
+
*/
|
|
462
|
+
function normalizeCurrentAliases(settings, warnings) {
|
|
463
|
+
normalizeAuditSettings(settings);
|
|
464
|
+
normalizeUpdateSettings(settings);
|
|
465
|
+
normalizeScalarAliases(settings);
|
|
466
|
+
normalizeSideEffectsCache(settings);
|
|
467
|
+
normalizeNamedRegistries(settings, warnings);
|
|
468
|
+
}
|
|
469
|
+
//#endregion
|
|
470
|
+
//#region src/utils/legacy-build-settings.ts
|
|
300
471
|
/**
|
|
301
472
|
* Read packages listed by the legacy `onlyBuiltDependenciesFile` setting.
|
|
302
473
|
*
|
|
303
|
-
* @param cwd -
|
|
304
|
-
* @param file -
|
|
474
|
+
* @param cwd - Workspace root directory
|
|
475
|
+
* @param file - Path declared by the legacy setting
|
|
305
476
|
*
|
|
306
|
-
* @returns
|
|
477
|
+
* @returns Validated package names, or an empty list when no file is declared
|
|
307
478
|
*
|
|
308
|
-
* @throws {TypeError} When the
|
|
309
|
-
* @throws {SyntaxError} When the referenced file contains invalid JSON
|
|
479
|
+
* @throws {TypeError} When the file does not contain an array of package names
|
|
310
480
|
*/
|
|
311
481
|
async function readOnlyBuiltDependenciesFile(cwd, file) {
|
|
312
482
|
if (!file) return [];
|
|
@@ -316,21 +486,56 @@ async function readOnlyBuiltDependenciesFile(cwd, file) {
|
|
|
316
486
|
return value;
|
|
317
487
|
}
|
|
318
488
|
/**
|
|
489
|
+
* Read and validate one legacy build dependency list. Configuration files are
|
|
490
|
+
* external input, so their runtime shape can differ from its declared type.
|
|
491
|
+
*
|
|
492
|
+
* @param settings - Workspace settings containing the legacy list
|
|
493
|
+
* @param key - Legacy setting name to read
|
|
494
|
+
*
|
|
495
|
+
* @returns Validated package names, or an empty list when the setting is absent
|
|
496
|
+
*
|
|
497
|
+
* @throws {TypeError} When the setting is not an array of package names
|
|
498
|
+
*/
|
|
499
|
+
function readLegacyBuildDependencyList(settings, key) {
|
|
500
|
+
const value = settings[key];
|
|
501
|
+
if (value === void 0) return [];
|
|
502
|
+
if (!Array.isArray(value) || !value.every((item) => typeof item === "string")) {
|
|
503
|
+
const npmrcKey = key.replace(/[A-Z]/gu, (character) => `-${character.toLowerCase()}`);
|
|
504
|
+
throw new TypeError(`Invalid ${key}: expected an array of package names. In .npmrc, use ${npmrcKey}[]=<package>.`);
|
|
505
|
+
}
|
|
506
|
+
return value;
|
|
507
|
+
}
|
|
508
|
+
/**
|
|
319
509
|
* Build an `allowBuilds` map from legacy build-script settings.
|
|
320
510
|
*
|
|
321
|
-
* @param incomingSettings -
|
|
322
|
-
* @param cwd -
|
|
511
|
+
* @param incomingSettings - Legacy settings to read and normalize
|
|
512
|
+
* @param cwd - Workspace root used to resolve dependency list files
|
|
323
513
|
*
|
|
324
|
-
* @returns Package
|
|
514
|
+
* @returns Package permissions, or `undefined` when no legacy values exist
|
|
325
515
|
*/
|
|
326
516
|
async function collectAllowBuildsFromLegacy(incomingSettings, cwd) {
|
|
327
517
|
const allowBuilds = {};
|
|
518
|
+
const allowed = readLegacyBuildDependencyList(incomingSettings, "onlyBuiltDependencies");
|
|
519
|
+
const ignored = readLegacyBuildDependencyList(incomingSettings, "ignoredBuiltDependencies");
|
|
520
|
+
const neverBuilt = readLegacyBuildDependencyList(incomingSettings, "neverBuiltDependencies");
|
|
328
521
|
const allowedFromFile = await readOnlyBuiltDependenciesFile(cwd, incomingSettings.onlyBuiltDependenciesFile);
|
|
329
|
-
for (const name of [...
|
|
330
|
-
for (const name of
|
|
331
|
-
for (const name of
|
|
522
|
+
for (const name of [...allowed, ...allowedFromFile]) allowBuilds[name] = true;
|
|
523
|
+
for (const name of ignored) allowBuilds[name] = false;
|
|
524
|
+
for (const name of neverBuilt) allowBuilds[name] = false;
|
|
332
525
|
return Object.keys(allowBuilds).length ? allowBuilds : void 0;
|
|
333
526
|
}
|
|
527
|
+
//#endregion
|
|
528
|
+
//#region src/utils/compatibility.ts
|
|
529
|
+
/**
|
|
530
|
+
* Legacy build settings that can be replaced for pnpm v10.
|
|
531
|
+
*/
|
|
532
|
+
const PNPM_REPLACEABLE_IN_V10_SETTINGS = [
|
|
533
|
+
"allowNonAppliedPatches",
|
|
534
|
+
"ignoredBuiltDependencies",
|
|
535
|
+
"neverBuiltDependencies",
|
|
536
|
+
"onlyBuiltDependencies",
|
|
537
|
+
"onlyBuiltDependenciesFile"
|
|
538
|
+
];
|
|
334
539
|
/**
|
|
335
540
|
* Resolve the pnpm v11 replacement for legacy package-manager settings.
|
|
336
541
|
*
|
|
@@ -407,6 +612,7 @@ async function normalizeIncomingSettings(incomingSettings, options) {
|
|
|
407
612
|
const pmOnFail = resolvePmOnFail(incomingSettings);
|
|
408
613
|
if (pmOnFail) incomingSettings.pmOnFail = pmOnFail;
|
|
409
614
|
normalizeAuditConfig(incomingSettings, warnings);
|
|
615
|
+
if (replaceDeprecated) normalizeCurrentAliases(incomingSettings, warnings);
|
|
410
616
|
const runtimeVersion = resolveRuntimeVersion(incomingSettings, warnings);
|
|
411
617
|
if (incomingSettings.ignoreDepScripts !== void 0) warnings.push("ignoreDepScripts was removed in pnpm v11 and has no direct replacement.");
|
|
412
618
|
if (incomingSettings.ignorePatchFailures !== void 0) warnings.push("ignorePatchFailures was removed in pnpm v11; patch failures now always throw.");
|
|
@@ -466,6 +672,28 @@ const PNPM_V11_MAJOR = 11;
|
|
|
466
672
|
*/
|
|
467
673
|
const PNPM_V12_MAJOR = 12;
|
|
468
674
|
/**
|
|
675
|
+
* Resolve a pnpm major from a package manager declaration.
|
|
676
|
+
*
|
|
677
|
+
* @param hint - Package manager declaration to inspect
|
|
678
|
+
*
|
|
679
|
+
* @returns Detected pnpm major, or `undefined` when the hint does not match
|
|
680
|
+
*/
|
|
681
|
+
function resolvePnpmMajor(hint) {
|
|
682
|
+
const match = hint?.match(/^pnpm@[\s<=>^~]*(?<major>\d+)(?:\.|\s|$)/u);
|
|
683
|
+
return match?.groups?.major ? Number(match.groups.major) : void 0;
|
|
684
|
+
}
|
|
685
|
+
/**
|
|
686
|
+
* Normalize the single-or-array devEngines declaration.
|
|
687
|
+
*
|
|
688
|
+
* @param value - Optional single or multiple package manager declarations
|
|
689
|
+
*
|
|
690
|
+
* @returns Package manager declarations normalized to an array
|
|
691
|
+
*/
|
|
692
|
+
function normalizePackageManagers(value) {
|
|
693
|
+
if (Array.isArray(value)) return value;
|
|
694
|
+
return value ? [value] : [];
|
|
695
|
+
}
|
|
696
|
+
/**
|
|
469
697
|
* Resolve final compatibility target from user option and package manager hint.
|
|
470
698
|
*
|
|
471
699
|
* @param compatibility - Explicit or automatically detected compatibility target
|
|
@@ -476,10 +704,11 @@ const PNPM_V12_MAJOR = 12;
|
|
|
476
704
|
*/
|
|
477
705
|
function resolveCompatibilityTarget(compatibility, packageManager, devPackageManager) {
|
|
478
706
|
if (compatibility !== "auto") return compatibility;
|
|
479
|
-
const
|
|
480
|
-
const
|
|
481
|
-
|
|
482
|
-
|
|
707
|
+
const packageManagerMajor = resolvePnpmMajor(packageManager);
|
|
708
|
+
const devPackageManagerMajors = normalizePackageManagers(devPackageManager).filter((item) => item.name === "pnpm").map((item) => resolvePnpmMajor(`pnpm@${item.version ?? ""}`)).filter((major) => major !== void 0);
|
|
709
|
+
const major = packageManagerMajor ?? (devPackageManagerMajors.length ? Math.max(...devPackageManagerMajors) : void 0);
|
|
710
|
+
if (major !== void 0 && major >= PNPM_V12_MAJOR) return "v12";
|
|
711
|
+
return major !== void 0 && major >= PNPM_V11_MAJOR ? "v11" : "v10";
|
|
483
712
|
}
|
|
484
713
|
//#endregion
|
|
485
714
|
//#region src/utils/merge.ts
|
|
@@ -557,6 +786,380 @@ function mergeByStrategy(existing, incoming, strategy) {
|
|
|
557
786
|
}
|
|
558
787
|
}
|
|
559
788
|
//#endregion
|
|
789
|
+
//#region src/settings-fields.ts
|
|
790
|
+
/**
|
|
791
|
+
* Settings that a project-level `pnpm-workspace.yaml` must never control.
|
|
792
|
+
*
|
|
793
|
+
* @see https://github.com/pnpm/pnpm/blob/main/pnpm11/config/reader/src/index.ts
|
|
794
|
+
* @see https://github.com/pnpm/pnpm/blob/v12.2.1/pnpm/crates/config/src/refused_keys.rs
|
|
795
|
+
*/
|
|
796
|
+
const PNPM_PROJECT_REFUSED_SETTINGS = [
|
|
797
|
+
"allProjects",
|
|
798
|
+
"allProjectsGraph",
|
|
799
|
+
"authConfig",
|
|
800
|
+
"bin",
|
|
801
|
+
"cliOptions",
|
|
802
|
+
"configByUri",
|
|
803
|
+
"configDir",
|
|
804
|
+
"explicitlySetKeys",
|
|
805
|
+
"finders",
|
|
806
|
+
"globalBinDir",
|
|
807
|
+
"globalDir",
|
|
808
|
+
"globalPkgDir",
|
|
809
|
+
"hooks",
|
|
810
|
+
"npmrcAuthFile",
|
|
811
|
+
"packageManager",
|
|
812
|
+
"packageManagerNetworkConfig",
|
|
813
|
+
"packageManagerRegistries",
|
|
814
|
+
"pnpmHomeDir",
|
|
815
|
+
"prodAllProjectsGraph",
|
|
816
|
+
"prodOnlySelectedProjectDirs",
|
|
817
|
+
"rootProjectManifest",
|
|
818
|
+
"rootProjectManifestDir",
|
|
819
|
+
"scope",
|
|
820
|
+
"selectedProjectsGraph",
|
|
821
|
+
"stateDir",
|
|
822
|
+
"userConfig",
|
|
823
|
+
"userconfig",
|
|
824
|
+
"wantedPackageManager",
|
|
825
|
+
"workspaceDir"
|
|
826
|
+
];
|
|
827
|
+
/**
|
|
828
|
+
* Settings added to, or recognized by, pnpm's v11 workspace config schema.
|
|
829
|
+
* Combined with the v10 settings below to form the complete v11 allowlist.
|
|
830
|
+
*
|
|
831
|
+
* @see https://github.com/pnpm/pnpm/blob/main/pnpm11/config/reader/src/types.ts
|
|
832
|
+
* @see https://github.com/pnpm/pnpm/blob/main/pnpm11/config/reader/src/unknownSettings.ts
|
|
833
|
+
*/
|
|
834
|
+
const PNPM_V11_ADDITIONAL_WORKSPACE_SETTINGS_FIELDS = [
|
|
835
|
+
"access",
|
|
836
|
+
"aggregateOutput",
|
|
837
|
+
"allowNew",
|
|
838
|
+
"allowSameVersion",
|
|
839
|
+
"audit",
|
|
840
|
+
"auditIgnorePrune",
|
|
841
|
+
"autoConfirmAllPrompts",
|
|
842
|
+
"bail",
|
|
843
|
+
"binLinks",
|
|
844
|
+
"catalog",
|
|
845
|
+
"catalogPrune",
|
|
846
|
+
"catalogs",
|
|
847
|
+
"changedFilesIgnorePattern",
|
|
848
|
+
"commitHooks",
|
|
849
|
+
"confirmModulesPurge",
|
|
850
|
+
"cpu",
|
|
851
|
+
"dedupePeers",
|
|
852
|
+
"depth",
|
|
853
|
+
"description",
|
|
854
|
+
"dev",
|
|
855
|
+
"dryRun",
|
|
856
|
+
"embedReadme",
|
|
857
|
+
"enablePnp",
|
|
858
|
+
"excludeLinksFromLockfile",
|
|
859
|
+
"extraBinPaths",
|
|
860
|
+
"extraEnv",
|
|
861
|
+
"fetchingConcurrency",
|
|
862
|
+
"filter",
|
|
863
|
+
"filterProd",
|
|
864
|
+
"force",
|
|
865
|
+
"frozenStore",
|
|
866
|
+
"git",
|
|
867
|
+
"gitTagVersion",
|
|
868
|
+
"global",
|
|
869
|
+
"globalPath",
|
|
870
|
+
"globalPrefix",
|
|
871
|
+
"hoistingLimits",
|
|
872
|
+
"ignoreCurrentSpecifiers",
|
|
873
|
+
"ignoreWorkspace",
|
|
874
|
+
"initAuthorEmail",
|
|
875
|
+
"initAuthorName",
|
|
876
|
+
"initAuthorUrl",
|
|
877
|
+
"initLicense",
|
|
878
|
+
"initPackageManager",
|
|
879
|
+
"initType",
|
|
880
|
+
"initVersion",
|
|
881
|
+
"json",
|
|
882
|
+
"legacyDirFiltering",
|
|
883
|
+
"libc",
|
|
884
|
+
"lockfileOnly",
|
|
885
|
+
"long",
|
|
886
|
+
"maxSockets",
|
|
887
|
+
"message",
|
|
888
|
+
"minimumReleaseAgeExcludePrune",
|
|
889
|
+
"minimumReleaseAgeIgnoreMissingTime",
|
|
890
|
+
"minimumReleaseAgeStrict",
|
|
891
|
+
"namedRegistries",
|
|
892
|
+
"nodeDownloadMirrors",
|
|
893
|
+
"nodeExperimentalPackageMap",
|
|
894
|
+
"nodePackageMapType",
|
|
895
|
+
"offline",
|
|
896
|
+
"only",
|
|
897
|
+
"os",
|
|
898
|
+
"packDestination",
|
|
899
|
+
"packGzipLevel",
|
|
900
|
+
"packageConfigs",
|
|
901
|
+
"packageLock",
|
|
902
|
+
"packages",
|
|
903
|
+
"parseable",
|
|
904
|
+
"patchesDir",
|
|
905
|
+
"pending",
|
|
906
|
+
"pmOnFail",
|
|
907
|
+
"pnpmExecPath",
|
|
908
|
+
"pnprServer",
|
|
909
|
+
"preferOffline",
|
|
910
|
+
"preserveAbsolutePaths",
|
|
911
|
+
"progress",
|
|
912
|
+
"provenance",
|
|
913
|
+
"publishBranch",
|
|
914
|
+
"recursive",
|
|
915
|
+
"registries",
|
|
916
|
+
"registriesByPrefix",
|
|
917
|
+
"registriesByScope",
|
|
918
|
+
"registry",
|
|
919
|
+
"registryOptionsByUrl",
|
|
920
|
+
"remoteSideEffectsCache",
|
|
921
|
+
"reporter",
|
|
922
|
+
"reporterHidePrefix",
|
|
923
|
+
"reverse",
|
|
924
|
+
"runtime",
|
|
925
|
+
"runtimeOnFail",
|
|
926
|
+
"save",
|
|
927
|
+
"saveCatalogName",
|
|
928
|
+
"saveDev",
|
|
929
|
+
"saveOptional",
|
|
930
|
+
"savePeer",
|
|
931
|
+
"saveProd",
|
|
932
|
+
"scriptsPrependNodePath",
|
|
933
|
+
"sideEffectsCacheRead",
|
|
934
|
+
"sideEffectsCacheWrite",
|
|
935
|
+
"signGitTag",
|
|
936
|
+
"skipManifestObfuscation",
|
|
937
|
+
"sort",
|
|
938
|
+
"stream",
|
|
939
|
+
"tasks",
|
|
940
|
+
"testPattern",
|
|
941
|
+
"trustLockfile",
|
|
942
|
+
"tryLoadDefaultPnpmfile",
|
|
943
|
+
"umask",
|
|
944
|
+
"update",
|
|
945
|
+
"useGitBranchLockfile",
|
|
946
|
+
"useLockfile",
|
|
947
|
+
"userAgent",
|
|
948
|
+
"version",
|
|
949
|
+
"versioning",
|
|
950
|
+
"virtualStoreOnly",
|
|
951
|
+
"virtualStoreType",
|
|
952
|
+
"workspacePackagePatterns",
|
|
953
|
+
"workspacePackages",
|
|
954
|
+
"workspacePrefix",
|
|
955
|
+
"workspaceRoot",
|
|
956
|
+
"yes"
|
|
957
|
+
];
|
|
958
|
+
const refusedProjectSettings = new Set(PNPM_PROJECT_REFUSED_SETTINGS);
|
|
959
|
+
/**
|
|
960
|
+
* Settings accepted by a pnpm v11 project workspace manifest.
|
|
961
|
+
*/
|
|
962
|
+
const PNPM_V11_WORKSPACE_SETTINGS_FIELDS = [.../* @__PURE__ */ new Set([...PNPM_V10_NPMRC_SETTINGS_FIELDS, ...PNPM_V11_ADDITIONAL_WORKSPACE_SETTINGS_FIELDS])].filter((field) => !refusedProjectSettings.has(field));
|
|
963
|
+
/**
|
|
964
|
+
* Settings that are supported by v11 but not by pnpm v12.
|
|
965
|
+
*/
|
|
966
|
+
const PNPM_V11_ONLY_WORKSPACE_SETTINGS = ["confirmModulesPurge", "packageConfigs"];
|
|
967
|
+
/**
|
|
968
|
+
* Settings introduced by pnpm v12.
|
|
969
|
+
*/
|
|
970
|
+
const PNPM_V12_ONLY_WORKSPACE_SETTINGS = [
|
|
971
|
+
"autoInstallPeersFromHighestMatch",
|
|
972
|
+
"externalDependencies",
|
|
973
|
+
"globalShims"
|
|
974
|
+
];
|
|
975
|
+
const v11OnlyWorkspaceSettings = new Set(PNPM_V11_ONLY_WORKSPACE_SETTINGS);
|
|
976
|
+
/**
|
|
977
|
+
* Settings accepted by a pnpm v12 project workspace manifest.
|
|
978
|
+
*/
|
|
979
|
+
const PNPM_V12_WORKSPACE_SETTINGS_FIELDS = [...PNPM_V11_WORKSPACE_SETTINGS_FIELDS.filter((field) => !v11OnlyWorkspaceSettings.has(field)), ...PNPM_V12_ONLY_WORKSPACE_SETTINGS];
|
|
980
|
+
/**
|
|
981
|
+
* Fields pnpm v11 accepts inside a `packageConfigs` entry.
|
|
982
|
+
*/
|
|
983
|
+
const PNPM_V11_PACKAGE_CONFIG_FIELDS = [
|
|
984
|
+
"hoist",
|
|
985
|
+
"modulesDir",
|
|
986
|
+
"overrides",
|
|
987
|
+
"saveExact",
|
|
988
|
+
"savePrefix"
|
|
989
|
+
];
|
|
990
|
+
//#endregion
|
|
991
|
+
//#region src/utils/settings-schema.ts
|
|
992
|
+
const PNPM_V10_WORKSPACE_SETTINGS_FIELDS = [
|
|
993
|
+
...PNPM_V10_NPMRC_SETTINGS_FIELDS,
|
|
994
|
+
"catalog",
|
|
995
|
+
"catalogs",
|
|
996
|
+
"packages"
|
|
997
|
+
];
|
|
998
|
+
const PROJECT_REFUSED_SETTINGS = new Set(PNPM_PROJECT_REFUSED_SETTINGS);
|
|
999
|
+
const PNPM_V11_ONLY_SETTINGS = new Set(PNPM_V11_ONLY_WORKSPACE_SETTINGS);
|
|
1000
|
+
const PNPM_V11_SETTINGS = new Set(PNPM_V11_WORKSPACE_SETTINGS_FIELDS);
|
|
1001
|
+
const PNPM_V12_ONLY_SETTINGS = new Set(PNPM_V12_ONLY_WORKSPACE_SETTINGS);
|
|
1002
|
+
const PNPM_V12_SETTINGS = new Set(PNPM_V12_WORKSPACE_SETTINGS_FIELDS);
|
|
1003
|
+
const PNPM_V10_SETTINGS = new Set(PNPM_V10_WORKSPACE_SETTINGS_FIELDS);
|
|
1004
|
+
const REGISTRY_SETTINGS = /* @__PURE__ */ new Set([
|
|
1005
|
+
"namedRegistries",
|
|
1006
|
+
"registries",
|
|
1007
|
+
"registriesByPrefix",
|
|
1008
|
+
"registriesByScope",
|
|
1009
|
+
"registry",
|
|
1010
|
+
"registryOptionsByUrl"
|
|
1011
|
+
]);
|
|
1012
|
+
const REGISTRY_CREDENTIAL_KEYS = /* @__PURE__ */ new Set([
|
|
1013
|
+
"auth",
|
|
1014
|
+
"authtoken",
|
|
1015
|
+
"password",
|
|
1016
|
+
"token",
|
|
1017
|
+
"tokenhelper",
|
|
1018
|
+
"username"
|
|
1019
|
+
]);
|
|
1020
|
+
const WORKSPACE_SCHEMA_DIRECTIVE = "$schema";
|
|
1021
|
+
/**
|
|
1022
|
+
* Create an empty collection of schema selection issues.
|
|
1023
|
+
*
|
|
1024
|
+
* @returns Empty issue lists for every supported issue category
|
|
1025
|
+
*/
|
|
1026
|
+
function createSettingsIssues() {
|
|
1027
|
+
return {
|
|
1028
|
+
incompatible: [],
|
|
1029
|
+
nonCamelCase: [],
|
|
1030
|
+
refused: [],
|
|
1031
|
+
unsafe: [],
|
|
1032
|
+
unknown: [],
|
|
1033
|
+
unsupported: []
|
|
1034
|
+
};
|
|
1035
|
+
}
|
|
1036
|
+
function hasUrlCredentials(value) {
|
|
1037
|
+
try {
|
|
1038
|
+
const url = new URL(value);
|
|
1039
|
+
return Boolean(url.username || url.password);
|
|
1040
|
+
} catch {
|
|
1041
|
+
return false;
|
|
1042
|
+
}
|
|
1043
|
+
}
|
|
1044
|
+
function isRegistryCredentialKey(key) {
|
|
1045
|
+
return REGISTRY_CREDENTIAL_KEYS.has(key.replaceAll("-", "").replaceAll("_", "").toLowerCase());
|
|
1046
|
+
}
|
|
1047
|
+
function isUnsafeRegistryUrl(value) {
|
|
1048
|
+
return value.includes("${") || hasUrlCredentials(value);
|
|
1049
|
+
}
|
|
1050
|
+
function containsUnsafeRegistryValue(value, checkCredentialKeys = false) {
|
|
1051
|
+
if (typeof value === "string") return isUnsafeRegistryUrl(value);
|
|
1052
|
+
if (!value || typeof value !== "object") return false;
|
|
1053
|
+
return Object.entries(value).some(([key, entryValue]) => checkCredentialKeys && isRegistryCredentialKey(key) || isUnsafeRegistryUrl(key) || containsUnsafeRegistryValue(entryValue, true));
|
|
1054
|
+
}
|
|
1055
|
+
function resolveCamelCaseKey(key) {
|
|
1056
|
+
return Object.keys(camelcaseKeys({ [key]: true }))[0] ?? key;
|
|
1057
|
+
}
|
|
1058
|
+
function resolveTargetSettings(compatibility) {
|
|
1059
|
+
switch (compatibility) {
|
|
1060
|
+
case "v10": return PNPM_V10_SETTINGS;
|
|
1061
|
+
case "v11": return PNPM_V11_SETTINGS;
|
|
1062
|
+
case "v12": return PNPM_V12_SETTINGS;
|
|
1063
|
+
default: throw new TypeError(`Unsupported compatibility target: ${compatibility}`);
|
|
1064
|
+
}
|
|
1065
|
+
}
|
|
1066
|
+
function isSettingFromAnotherMajor(key, compatibility) {
|
|
1067
|
+
if (compatibility === "v11") return PNPM_V12_ONLY_SETTINGS.has(key);
|
|
1068
|
+
if (compatibility === "v12") return PNPM_V11_ONLY_SETTINGS.has(key);
|
|
1069
|
+
return PNPM_V11_SETTINGS.has(key) || PNPM_V12_SETTINGS.has(key);
|
|
1070
|
+
}
|
|
1071
|
+
function resolveSettingIssue({ allowedFields, compatibility, key, npmrc, targetSettings, value }) {
|
|
1072
|
+
if (!npmrc && key !== resolveCamelCaseKey(key)) return "nonCamelCase";
|
|
1073
|
+
if (compatibility !== "v10" && PROJECT_REFUSED_SETTINGS.has(key)) return "refused";
|
|
1074
|
+
if (REGISTRY_SETTINGS.has(key) && containsUnsafeRegistryValue(value)) return "unsafe";
|
|
1075
|
+
if (!targetSettings.has(key)) return isSettingFromAnotherMajor(key, compatibility) ? "incompatible" : "unknown";
|
|
1076
|
+
if (allowedFields && !allowedFields.has(key)) return "unsupported";
|
|
1077
|
+
}
|
|
1078
|
+
/**
|
|
1079
|
+
* Select settings that are valid for a concrete pnpm workspace schema.
|
|
1080
|
+
*
|
|
1081
|
+
* Unrecognized, refused, incompatible, and destination-specific unsupported
|
|
1082
|
+
* settings are reported but never returned for migration.
|
|
1083
|
+
*
|
|
1084
|
+
* @param rawSettings - Settings read from a legacy configuration source
|
|
1085
|
+
* @param compatibility - Concrete pnpm compatibility target
|
|
1086
|
+
* @param options - Source spelling and destination field restrictions
|
|
1087
|
+
*
|
|
1088
|
+
* @returns Selected settings, original source keys, and rejected field groups
|
|
1089
|
+
*/
|
|
1090
|
+
function selectPnpmSettings(rawSettings, compatibility, options = {}) {
|
|
1091
|
+
const issues = createSettingsIssues();
|
|
1092
|
+
const keys = [];
|
|
1093
|
+
const settings = {};
|
|
1094
|
+
const targetSettings = resolveTargetSettings(compatibility);
|
|
1095
|
+
const allowedFields = options.allowedFields ? new Set(options.allowedFields) : void 0;
|
|
1096
|
+
for (const [originalKey, value] of Object.entries(rawSettings)) {
|
|
1097
|
+
const key = options.npmrc ? resolveCamelCaseKey(originalKey) : originalKey;
|
|
1098
|
+
const issue = resolveSettingIssue({
|
|
1099
|
+
allowedFields,
|
|
1100
|
+
compatibility,
|
|
1101
|
+
key,
|
|
1102
|
+
npmrc: Boolean(options.npmrc),
|
|
1103
|
+
targetSettings,
|
|
1104
|
+
value
|
|
1105
|
+
});
|
|
1106
|
+
if (issue) issues[issue].push(originalKey);
|
|
1107
|
+
else {
|
|
1108
|
+
keys.push(originalKey);
|
|
1109
|
+
settings[key] = value;
|
|
1110
|
+
}
|
|
1111
|
+
}
|
|
1112
|
+
return {
|
|
1113
|
+
issues,
|
|
1114
|
+
keys,
|
|
1115
|
+
settings
|
|
1116
|
+
};
|
|
1117
|
+
}
|
|
1118
|
+
function assertPackageConfigFields(settings) {
|
|
1119
|
+
const { packageConfigs } = settings;
|
|
1120
|
+
if (packageConfigs === void 0) return;
|
|
1121
|
+
const allowedFields = new Set(PNPM_V11_PACKAGE_CONFIG_FIELDS);
|
|
1122
|
+
const entries = Array.isArray(packageConfigs) ? packageConfigs.flatMap((entry, index) => {
|
|
1123
|
+
if (!entry || typeof entry !== "object" || Array.isArray(entry)) throw new TypeError(`packageConfigs[${index}] must be an object with a match array.`);
|
|
1124
|
+
const { match, ...config } = entry;
|
|
1125
|
+
if (!Array.isArray(match) || !match.every((projectName) => typeof projectName === "string")) throw new TypeError(`packageConfigs[${index}].match must be an array of package names.`);
|
|
1126
|
+
return [[`packageConfigs[${index}]`, config]];
|
|
1127
|
+
}) : Object.entries(packageConfigs);
|
|
1128
|
+
for (const [name, config] of entries) {
|
|
1129
|
+
if (!config || typeof config !== "object" || Array.isArray(config)) throw new TypeError(`${name} must contain a project settings object.`);
|
|
1130
|
+
const unsupported = Object.keys(config).filter((key) => !allowedFields.has(key));
|
|
1131
|
+
if (unsupported.length) throw new TypeError(`${name} contains unsupported project settings: ${unsupported.map((key) => JSON.stringify(key)).join(", ")}.`);
|
|
1132
|
+
}
|
|
1133
|
+
}
|
|
1134
|
+
function formatIssueList(keys) {
|
|
1135
|
+
return keys.map((key) => JSON.stringify(key)).join(", ");
|
|
1136
|
+
}
|
|
1137
|
+
/**
|
|
1138
|
+
* Assert that an existing workspace manifest matches the selected pnpm major.
|
|
1139
|
+
*
|
|
1140
|
+
* The migrator refuses to rewrite a manifest containing ignored settings so a
|
|
1141
|
+
* migration cannot silently preserve invalid output.
|
|
1142
|
+
*
|
|
1143
|
+
* @param settings - Existing workspace settings to validate
|
|
1144
|
+
* @param compatibility - Concrete pnpm compatibility target
|
|
1145
|
+
*
|
|
1146
|
+
* @returns Nothing when all settings match the target schema
|
|
1147
|
+
*
|
|
1148
|
+
* @throws {TypeError} When the manifest contains incompatible settings
|
|
1149
|
+
*/
|
|
1150
|
+
function assertCompatibleWorkspaceSettings(settings, compatibility) {
|
|
1151
|
+
if (compatibility === "v10") return;
|
|
1152
|
+
const { issues } = selectPnpmSettings(Object.fromEntries(Object.entries(settings).filter(([key]) => key !== WORKSPACE_SCHEMA_DIRECTIVE)), compatibility);
|
|
1153
|
+
const problems = [];
|
|
1154
|
+
if (issues.refused.length) problems.push(`refused: ${formatIssueList(issues.refused)}`);
|
|
1155
|
+
if (issues.incompatible.length) problems.push(`other pnpm major: ${formatIssueList(issues.incompatible)}`);
|
|
1156
|
+
if (issues.nonCamelCase.length) problems.push(`not camelCase: ${formatIssueList(issues.nonCamelCase)}`);
|
|
1157
|
+
if (issues.unknown.length) problems.push(`unrecognized: ${formatIssueList(issues.unknown)}`);
|
|
1158
|
+
if (issues.unsafe.length) problems.push(`unsafe registry URL: ${formatIssueList(issues.unsafe)}`);
|
|
1159
|
+
if (problems.length) throw new TypeError(`pnpm-workspace.yaml is incompatible with pnpm ${compatibility.slice(1)} (${problems.join("; ")}).`);
|
|
1160
|
+
if (compatibility === "v11") assertPackageConfigFields(settings);
|
|
1161
|
+
}
|
|
1162
|
+
//#endregion
|
|
560
1163
|
//#region src/utils/npmrc.ts
|
|
561
1164
|
/**
|
|
562
1165
|
* Authentication and registry keys that must remain in `.npmrc`.
|
|
@@ -581,6 +1184,9 @@ const NPMRC_AUTH_OR_REGISTRY_KEYS = [
|
|
|
581
1184
|
* Pattern for pnpm's channel-specific Node.js mirror keys.
|
|
582
1185
|
*/
|
|
583
1186
|
const NODE_MIRROR_KEY_PATTERN = /^node-mirror:(?<channel>.+)$/iu;
|
|
1187
|
+
function mergeSettingsIssues(target, source, sourceKeys) {
|
|
1188
|
+
for (const reason of Object.keys(target)) if (source[reason].length) target[reason].push(...sourceKeys ?? source[reason]);
|
|
1189
|
+
}
|
|
584
1190
|
/**
|
|
585
1191
|
* Normalize `.npmrc` key for case-insensitive matching.
|
|
586
1192
|
*
|
|
@@ -622,9 +1228,8 @@ function isNpmrcAuthOrRegistryKey(key) {
|
|
|
622
1228
|
/**
|
|
623
1229
|
* Remove pnpm-related settings from `.npmrc` file.
|
|
624
1230
|
*
|
|
625
|
-
* This function reads the `.npmrc` file,
|
|
626
|
-
*
|
|
627
|
-
* writes the cleaned content back to the file.
|
|
1231
|
+
* This function reads the `.npmrc` file, removes lines whose exact normalized
|
|
1232
|
+
* keys were migrated, and writes the cleaned content back to the file.
|
|
628
1233
|
*
|
|
629
1234
|
* @param path - Absolute path to the `.npmrc` file
|
|
630
1235
|
* @param compatibility - Concrete pnpm compatibility target
|
|
@@ -636,17 +1241,15 @@ function isNpmrcAuthOrRegistryKey(key) {
|
|
|
636
1241
|
*
|
|
637
1242
|
* @example
|
|
638
1243
|
* ```ts
|
|
639
|
-
* await pruneNpmrc('/path/to/.npmrc')
|
|
1244
|
+
* await pruneNpmrc('/path/to/.npmrc', 'v10', ['node-linker'])
|
|
640
1245
|
* ```
|
|
641
1246
|
*/
|
|
642
|
-
async function pruneNpmrc(path, compatibility
|
|
643
|
-
const pnpmSettingsFields = PNPM_SETTINGS_FIELDS.map((field) => kebabCase(field));
|
|
1247
|
+
async function pruneNpmrc(path, compatibility, migratedKeys) {
|
|
644
1248
|
const migratedKeySet = new Set(migratedKeys.map(normalizeNpmrcKey));
|
|
645
1249
|
const updatedContent = (await fsReadFile(path)).split(/\r?\n/u).filter((line) => {
|
|
646
1250
|
const key = getNpmrcLineKey(line);
|
|
647
1251
|
if (!key) return true;
|
|
648
|
-
|
|
649
|
-
return !pnpmSettingsFields.some((field) => key.startsWith(field));
|
|
1252
|
+
return !migratedKeySet.has(key);
|
|
650
1253
|
}).join("\n").trimEnd();
|
|
651
1254
|
if (compatibility !== "v10" && !updatedContent.trim()) {
|
|
652
1255
|
await fsRemoveFile(path);
|
|
@@ -658,44 +1261,271 @@ async function pruneNpmrc(path, compatibility = "v10", migratedKeys = []) {
|
|
|
658
1261
|
* Read `.npmrc` and return settings that should be migrated into workspace config.
|
|
659
1262
|
*
|
|
660
1263
|
* - `v10`: returns settings from the legacy whitelist.
|
|
661
|
-
* - `v11+`:
|
|
662
|
-
*
|
|
1264
|
+
* - `v11+`: selects target-schema settings and retains auth, registry,
|
|
1265
|
+
* refused, incompatible, and unknown keys.
|
|
663
1266
|
*
|
|
664
1267
|
* @param path - Absolute path to the `.npmrc` file
|
|
665
1268
|
* @param compatibility - Concrete pnpm compatibility target
|
|
1269
|
+
* @param options - Optional destination-specific field restrictions
|
|
666
1270
|
*
|
|
667
1271
|
* @returns Migratable settings and their original `.npmrc` keys
|
|
668
1272
|
*
|
|
669
1273
|
* @throws {Error} When file reading or INI parsing fails
|
|
670
1274
|
*/
|
|
671
|
-
async function readMigratableNpmrc(path, compatibility) {
|
|
1275
|
+
async function readMigratableNpmrc(path, compatibility, options = {}) {
|
|
672
1276
|
const raw = await readIniFile(path);
|
|
1277
|
+
const issues = createSettingsIssues();
|
|
673
1278
|
if (compatibility === "v10") {
|
|
674
|
-
const
|
|
1279
|
+
const pnpmSettingsFields = new Set(PNPM_V10_NPMRC_SETTINGS_FIELDS.map((field) => normalizeNpmrcKey(kebabCase(field))));
|
|
1280
|
+
const keys = Object.keys(raw).filter((key) => pnpmSettingsFields.has(normalizeNpmrcKey(key)));
|
|
1281
|
+
const migratable = Object.fromEntries(keys.map((key) => [key, raw[key]]));
|
|
675
1282
|
return {
|
|
676
|
-
|
|
677
|
-
|
|
1283
|
+
issues,
|
|
1284
|
+
keys,
|
|
1285
|
+
settings: camelcaseKeys(migratable)
|
|
678
1286
|
};
|
|
679
1287
|
}
|
|
680
1288
|
const migratable = {};
|
|
681
1289
|
const keys = [];
|
|
682
1290
|
const nodeDownloadMirrors = {};
|
|
1291
|
+
const nodeDownloadMirrorKeys = [];
|
|
683
1292
|
for (const [key, value] of Object.entries(raw)) if (!isNpmrcAuthOrRegistryKey(key)) {
|
|
684
|
-
keys.push(key);
|
|
685
1293
|
const nodeMirrorMatch = normalizeNpmrcKey(key).match(NODE_MIRROR_KEY_PATTERN);
|
|
686
|
-
if (nodeMirrorMatch?.groups?.channel)
|
|
687
|
-
|
|
688
|
-
|
|
1294
|
+
if (nodeMirrorMatch?.groups?.channel) {
|
|
1295
|
+
nodeDownloadMirrorKeys.push(key);
|
|
1296
|
+
nodeDownloadMirrors[nodeMirrorMatch.groups.channel] = String(value);
|
|
1297
|
+
} else {
|
|
1298
|
+
const selected = selectPnpmSettings({ [key]: value }, compatibility, {
|
|
1299
|
+
allowedFields: options.allowedFields,
|
|
1300
|
+
npmrc: true
|
|
1301
|
+
});
|
|
1302
|
+
keys.push(...selected.keys);
|
|
1303
|
+
Object.assign(migratable, selected.settings);
|
|
1304
|
+
mergeSettingsIssues(issues, selected.issues);
|
|
1305
|
+
}
|
|
1306
|
+
}
|
|
689
1307
|
const settings = camelcaseKeys(migratable);
|
|
690
|
-
if (Object.keys(nodeDownloadMirrors).length)
|
|
1308
|
+
if (Object.keys(nodeDownloadMirrors).length) {
|
|
1309
|
+
const selected = selectPnpmSettings({ nodeDownloadMirrors }, compatibility, { allowedFields: options.allowedFields });
|
|
1310
|
+
if (selected.keys.length) {
|
|
1311
|
+
keys.push(...nodeDownloadMirrorKeys);
|
|
1312
|
+
settings.nodeDownloadMirrors = nodeDownloadMirrors;
|
|
1313
|
+
} else mergeSettingsIssues(issues, selected.issues, nodeDownloadMirrorKeys);
|
|
1314
|
+
}
|
|
691
1315
|
return {
|
|
1316
|
+
issues,
|
|
692
1317
|
keys,
|
|
693
1318
|
settings
|
|
694
1319
|
};
|
|
695
1320
|
}
|
|
696
1321
|
//#endregion
|
|
1322
|
+
//#region src/utils/config.ts
|
|
1323
|
+
/**
|
|
1324
|
+
* Resolve the indentation width for a YAML document.
|
|
1325
|
+
*
|
|
1326
|
+
* @param content - Raw YAML content
|
|
1327
|
+
*
|
|
1328
|
+
* @returns Detected indentation width or the project default
|
|
1329
|
+
*/
|
|
1330
|
+
function resolveYamlIndent(content) {
|
|
1331
|
+
const detectedIndent = detectIndent(content).amount;
|
|
1332
|
+
return detectedIndent > 0 ? detectedIndent : 2;
|
|
1333
|
+
}
|
|
1334
|
+
/**
|
|
1335
|
+
* Read and parse a package manifest when it exists.
|
|
1336
|
+
*
|
|
1337
|
+
* @param path - Absolute path to `package.json`
|
|
1338
|
+
* @param exists - Whether the package manifest exists
|
|
1339
|
+
*
|
|
1340
|
+
* @returns Parsed package manifest and detected indentation
|
|
1341
|
+
*
|
|
1342
|
+
* @throws {SyntaxError} When the package manifest contains invalid JSON
|
|
1343
|
+
*/
|
|
1344
|
+
async function readPackageJson(path, exists) {
|
|
1345
|
+
if (!exists) return {
|
|
1346
|
+
indent: 2,
|
|
1347
|
+
value: {}
|
|
1348
|
+
};
|
|
1349
|
+
const content = await fsReadFile(path);
|
|
1350
|
+
return {
|
|
1351
|
+
indent: detectIndent(content).indent,
|
|
1352
|
+
value: JSON.parse(content)
|
|
1353
|
+
};
|
|
1354
|
+
}
|
|
1355
|
+
/**
|
|
1356
|
+
* Read and parse a pnpm workspace manifest when it exists.
|
|
1357
|
+
*
|
|
1358
|
+
* @param path - Absolute path to `pnpm-workspace.yaml`
|
|
1359
|
+
* @param exists - Whether the workspace manifest exists
|
|
1360
|
+
*
|
|
1361
|
+
* @returns Parsed workspace settings and detected indentation
|
|
1362
|
+
*
|
|
1363
|
+
* @throws {Error} When the workspace manifest contains invalid YAML
|
|
1364
|
+
*/
|
|
1365
|
+
async function readPnpmWorkspace(path, exists) {
|
|
1366
|
+
if (!exists) return {
|
|
1367
|
+
document: new Document({}),
|
|
1368
|
+
indent: 2,
|
|
1369
|
+
value: {}
|
|
1370
|
+
};
|
|
1371
|
+
const content = await fsReadFile(path);
|
|
1372
|
+
const document = parseDocument(content);
|
|
1373
|
+
const [parseError] = document.errors;
|
|
1374
|
+
if (parseError) throw parseError;
|
|
1375
|
+
const parsedValue = document.toJS();
|
|
1376
|
+
if (parsedValue !== null && !isMap(document.contents)) throw new TypeError("pnpm-workspace.yaml must contain a root mapping.");
|
|
1377
|
+
if (parsedValue === null) document.contents = document.createNode({});
|
|
1378
|
+
return {
|
|
1379
|
+
document,
|
|
1380
|
+
indent: resolveYamlIndent(content),
|
|
1381
|
+
value: parsedValue ?? {}
|
|
1382
|
+
};
|
|
1383
|
+
}
|
|
1384
|
+
/**
|
|
1385
|
+
* Resolve migratable pnpm settings from a package manifest.
|
|
1386
|
+
*
|
|
1387
|
+
* Yarn resolutions are merged into pnpm overrides when enabled. Empty
|
|
1388
|
+
* overrides are removed from the result.
|
|
1389
|
+
*
|
|
1390
|
+
* @param packageJson - Parsed package manifest
|
|
1391
|
+
* @param yarnResolutions - Whether to convert Yarn resolutions to pnpm overrides
|
|
1392
|
+
* @param compatibility - Concrete pnpm compatibility target
|
|
1393
|
+
*
|
|
1394
|
+
* @returns Selected workspace settings and source cleanup metadata
|
|
1395
|
+
*/
|
|
1396
|
+
function resolvePackageJsonSettings(packageJson, yarnResolutions, compatibility) {
|
|
1397
|
+
const selected = selectPnpmSettings(Object.fromEntries(Object.entries(packageJson.pnpm ?? {})), compatibility);
|
|
1398
|
+
const migrateYarnResolutions = Boolean(yarnResolutions && packageJson.resolutions);
|
|
1399
|
+
const pnpmSettings = migrateYarnResolutions ? {
|
|
1400
|
+
...selected.settings,
|
|
1401
|
+
overrides: defu(selected.settings.overrides, packageJson.resolutions)
|
|
1402
|
+
} : { ...selected.settings };
|
|
1403
|
+
if (pnpmSettings.overrides && !Object.keys(pnpmSettings.overrides).length) delete pnpmSettings.overrides;
|
|
1404
|
+
return {
|
|
1405
|
+
issues: selected.issues,
|
|
1406
|
+
keys: selected.keys,
|
|
1407
|
+
settings: pnpmSettings,
|
|
1408
|
+
yarnResolutions: migrateYarnResolutions
|
|
1409
|
+
};
|
|
1410
|
+
}
|
|
1411
|
+
//#endregion
|
|
1412
|
+
//#region src/utils/project-npmrc.ts
|
|
1413
|
+
function resolvePackageJsonPatterns(patterns) {
|
|
1414
|
+
return patterns.map((pattern) => {
|
|
1415
|
+
const negated = pattern.startsWith("!");
|
|
1416
|
+
const workspacePattern = (negated ? pattern.slice(1) : pattern).replace(/\/+$/u, "");
|
|
1417
|
+
const packageJsonPattern = workspacePattern === "." ? PACKAGE_JSON : `${workspacePattern}/${PACKAGE_JSON}`;
|
|
1418
|
+
return negated ? `!${packageJsonPattern}` : packageJsonPattern;
|
|
1419
|
+
});
|
|
1420
|
+
}
|
|
1421
|
+
/**
|
|
1422
|
+
* Collect unique workspace package patterns from multiple settings sources.
|
|
1423
|
+
*
|
|
1424
|
+
* @param settingsSources - Workspace settings that may declare package patterns
|
|
1425
|
+
*
|
|
1426
|
+
* @returns Unique package patterns in source order
|
|
1427
|
+
*/
|
|
1428
|
+
function collectWorkspacePackagePatterns(...settingsSources) {
|
|
1429
|
+
return [...new Set(settingsSources.flatMap((settings) => Array.isArray(settings.packages) ? settings.packages : []))];
|
|
1430
|
+
}
|
|
1431
|
+
async function readProjectManifestCandidates(cwd, patterns) {
|
|
1432
|
+
if (!patterns.some((pattern) => !pattern.startsWith("!"))) return {
|
|
1433
|
+
candidates: [],
|
|
1434
|
+
warnings: []
|
|
1435
|
+
};
|
|
1436
|
+
const rootPackageJsonPath = resolve(cwd, PACKAGE_JSON);
|
|
1437
|
+
const packageJsonPaths = await glob(resolvePackageJsonPatterns(patterns), {
|
|
1438
|
+
absolute: true,
|
|
1439
|
+
cwd,
|
|
1440
|
+
dot: true,
|
|
1441
|
+
ignore: ["**/node_modules/**"],
|
|
1442
|
+
onlyFiles: true
|
|
1443
|
+
});
|
|
1444
|
+
const candidateResults = await Promise.all([...new Set(packageJsonPaths)].sort().filter((packageJsonPath) => resolve(packageJsonPath) !== rootPackageJsonPath).map(async (packageJsonPath) => {
|
|
1445
|
+
const projectDir = dirname(packageJsonPath);
|
|
1446
|
+
const npmrcPath = resolve(projectDir, NPMRC);
|
|
1447
|
+
if (!await fsExists(npmrcPath)) return {};
|
|
1448
|
+
const packageJson = await readPackageJson(packageJsonPath, true);
|
|
1449
|
+
if (!packageJson.value.name) return { warning: `${relative(cwd, npmrcPath)} was kept because its package.json has no name for packageConfigs matching.` };
|
|
1450
|
+
return { candidate: {
|
|
1451
|
+
npmrcPath,
|
|
1452
|
+
packageJsonPath,
|
|
1453
|
+
projectName: packageJson.value.name
|
|
1454
|
+
} };
|
|
1455
|
+
}));
|
|
1456
|
+
const candidates = [];
|
|
1457
|
+
const warnings = [];
|
|
1458
|
+
for (const result of candidateResults) {
|
|
1459
|
+
if (result.candidate) candidates.push(result.candidate);
|
|
1460
|
+
if (result.warning) warnings.push(result.warning);
|
|
1461
|
+
}
|
|
1462
|
+
return {
|
|
1463
|
+
candidates,
|
|
1464
|
+
warnings
|
|
1465
|
+
};
|
|
1466
|
+
}
|
|
1467
|
+
/**
|
|
1468
|
+
* Collect supported settings from subproject `.npmrc` files.
|
|
1469
|
+
*
|
|
1470
|
+
* pnpm v11 accepts five fields in `packageConfigs`. pnpm v12 does not support
|
|
1471
|
+
* `packageConfigs`, so its subproject settings are only reported and retained.
|
|
1472
|
+
*
|
|
1473
|
+
* @param cwd - Workspace root directory
|
|
1474
|
+
* @param patterns - Workspace package patterns used to discover subprojects
|
|
1475
|
+
* @param compatibility - Concrete pnpm compatibility target
|
|
1476
|
+
*
|
|
1477
|
+
* @returns Migratable project settings, source metadata, and warnings
|
|
1478
|
+
*/
|
|
1479
|
+
async function readProjectNpmrcMigrations(cwd, patterns, compatibility) {
|
|
1480
|
+
if (compatibility === "v10") return {
|
|
1481
|
+
packageConfigs: {},
|
|
1482
|
+
projects: [],
|
|
1483
|
+
warnings: []
|
|
1484
|
+
};
|
|
1485
|
+
const { candidates, warnings } = await readProjectManifestCandidates(cwd, patterns);
|
|
1486
|
+
const candidatesByName = /* @__PURE__ */ new Map();
|
|
1487
|
+
for (const candidate of candidates) {
|
|
1488
|
+
const namedCandidates = candidatesByName.get(candidate.projectName) ?? [];
|
|
1489
|
+
namedCandidates.push(candidate);
|
|
1490
|
+
candidatesByName.set(candidate.projectName, namedCandidates);
|
|
1491
|
+
}
|
|
1492
|
+
const packageConfigs = {};
|
|
1493
|
+
const projects = [];
|
|
1494
|
+
const allowedFields = compatibility === "v11" ? PNPM_V11_PACKAGE_CONFIG_FIELDS : [];
|
|
1495
|
+
const migrationResults = await Promise.all([...candidatesByName].map(async ([projectName, namedCandidates]) => {
|
|
1496
|
+
if (namedCandidates.length > 1) return { warning: `Subproject .npmrc files for duplicate package name ${JSON.stringify(projectName)} were kept: ${namedCandidates.map((candidate) => relative(cwd, candidate.packageJsonPath)).join(", ")}.` };
|
|
1497
|
+
const [candidate] = namedCandidates;
|
|
1498
|
+
return { project: {
|
|
1499
|
+
migratable: await readMigratableNpmrc(candidate.npmrcPath, compatibility, { allowedFields }),
|
|
1500
|
+
npmrcPath: candidate.npmrcPath,
|
|
1501
|
+
projectName
|
|
1502
|
+
} };
|
|
1503
|
+
}));
|
|
1504
|
+
for (const result of migrationResults) {
|
|
1505
|
+
if (result.warning) warnings.push(result.warning);
|
|
1506
|
+
if (result.project) {
|
|
1507
|
+
projects.push(result.project);
|
|
1508
|
+
if (result.project.migratable.keys.length) packageConfigs[result.project.projectName] = result.project.migratable.settings;
|
|
1509
|
+
}
|
|
1510
|
+
}
|
|
1511
|
+
return {
|
|
1512
|
+
packageConfigs,
|
|
1513
|
+
projects,
|
|
1514
|
+
warnings
|
|
1515
|
+
};
|
|
1516
|
+
}
|
|
1517
|
+
//#endregion
|
|
697
1518
|
//#region src/utils/settings-change.ts
|
|
698
1519
|
/**
|
|
1520
|
+
* Maximum dynamic-programming cells used for an exact line diff.
|
|
1521
|
+
*/
|
|
1522
|
+
const MAX_LCS_CELLS = 1e6;
|
|
1523
|
+
/**
|
|
1524
|
+
* URL userinfo can contain proxy usernames and passwords. It must never be
|
|
1525
|
+
* printed in a migration report.
|
|
1526
|
+
*/
|
|
1527
|
+
const URL_USERINFO_PATTERN = /(?<scheme>[a-z][a-z\d+.-]*:\/\/)[^/\s@]+@/giu;
|
|
1528
|
+
/**
|
|
699
1529
|
* Collect changed root settings between two workspace configurations.
|
|
700
1530
|
*
|
|
701
1531
|
* @param before - Workspace settings before migration
|
|
@@ -714,6 +1544,43 @@ function collectSettingsChanges(before, after) {
|
|
|
714
1544
|
}));
|
|
715
1545
|
}
|
|
716
1546
|
/**
|
|
1547
|
+
* Create a bounded-memory diff that retains shared prefixes and suffixes.
|
|
1548
|
+
*
|
|
1549
|
+
* This fallback deliberately gives up the exact LCS for unusually large
|
|
1550
|
+
* settings to keep reporting from exhausting memory after migration writes.
|
|
1551
|
+
*
|
|
1552
|
+
* @param beforeLines - YAML lines before migration
|
|
1553
|
+
* @param afterLines - YAML lines after migration
|
|
1554
|
+
*
|
|
1555
|
+
* @returns Classified diff lines in display order
|
|
1556
|
+
*/
|
|
1557
|
+
function createLinearLineDiff(beforeLines, afterLines) {
|
|
1558
|
+
let prefixLength = 0;
|
|
1559
|
+
const maximumPrefixLength = Math.min(beforeLines.length, afterLines.length);
|
|
1560
|
+
while (prefixLength < maximumPrefixLength && beforeLines[prefixLength] === afterLines[prefixLength]) prefixLength++;
|
|
1561
|
+
let suffixLength = 0;
|
|
1562
|
+
const maximumSuffixLength = maximumPrefixLength - prefixLength;
|
|
1563
|
+
while (suffixLength < maximumSuffixLength && beforeLines[beforeLines.length - suffixLength - 1] === afterLines[afterLines.length - suffixLength - 1]) suffixLength++;
|
|
1564
|
+
return [
|
|
1565
|
+
...beforeLines.slice(0, prefixLength).map((value) => ({
|
|
1566
|
+
kind: "unchanged",
|
|
1567
|
+
value
|
|
1568
|
+
})),
|
|
1569
|
+
...beforeLines.slice(prefixLength, beforeLines.length - suffixLength).map((value) => ({
|
|
1570
|
+
kind: "removed",
|
|
1571
|
+
value
|
|
1572
|
+
})),
|
|
1573
|
+
...afterLines.slice(prefixLength, afterLines.length - suffixLength).map((value) => ({
|
|
1574
|
+
kind: "added",
|
|
1575
|
+
value
|
|
1576
|
+
})),
|
|
1577
|
+
...beforeLines.slice(beforeLines.length - suffixLength).map((value) => ({
|
|
1578
|
+
kind: "unchanged",
|
|
1579
|
+
value
|
|
1580
|
+
}))
|
|
1581
|
+
];
|
|
1582
|
+
}
|
|
1583
|
+
/**
|
|
717
1584
|
* Create a line-based diff using the longest common subsequence.
|
|
718
1585
|
*
|
|
719
1586
|
* @param beforeLines - YAML lines before migration
|
|
@@ -722,6 +1589,7 @@ function collectSettingsChanges(before, after) {
|
|
|
722
1589
|
* @returns Classified diff lines in display order
|
|
723
1590
|
*/
|
|
724
1591
|
function createLineDiff(beforeLines, afterLines) {
|
|
1592
|
+
if (beforeLines.length * afterLines.length > MAX_LCS_CELLS) return createLinearLineDiff(beforeLines, afterLines);
|
|
725
1593
|
const lengths = Array.from({ length: beforeLines.length + 1 }, () => Array(afterLines.length + 1).fill(0));
|
|
726
1594
|
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
1595
|
const diff = [];
|
|
@@ -776,6 +1644,17 @@ function formatSettingLines(key, value) {
|
|
|
776
1644
|
return stringify({ [key]: value }).trimEnd().split("\n");
|
|
777
1645
|
}
|
|
778
1646
|
/**
|
|
1647
|
+
* Remove credentials embedded in URLs while preserving enough context to
|
|
1648
|
+
* identify the changed setting.
|
|
1649
|
+
*
|
|
1650
|
+
* @param value - Rendered YAML diff line
|
|
1651
|
+
*
|
|
1652
|
+
* @returns Diff line safe to print to a terminal or CI log
|
|
1653
|
+
*/
|
|
1654
|
+
function redactUrlCredentials(value) {
|
|
1655
|
+
return value.replace(URL_USERINFO_PATTERN, "$<scheme>***@");
|
|
1656
|
+
}
|
|
1657
|
+
/**
|
|
779
1658
|
* Format a setting change as a GitHub-style YAML diff.
|
|
780
1659
|
*
|
|
781
1660
|
* @param change - Root setting change to format
|
|
@@ -783,7 +1662,10 @@ function formatSettingLines(key, value) {
|
|
|
783
1662
|
* @returns Classified YAML diff lines
|
|
784
1663
|
*/
|
|
785
1664
|
function createSettingsDiffLines(change) {
|
|
786
|
-
return createLineDiff(formatSettingLines(change.key, change.before), formatSettingLines(change.key, change.after))
|
|
1665
|
+
return createLineDiff(formatSettingLines(change.key, change.before), formatSettingLines(change.key, change.after)).map((line) => ({
|
|
1666
|
+
kind: line.kind,
|
|
1667
|
+
value: redactUrlCredentials(line.value)
|
|
1668
|
+
}));
|
|
787
1669
|
}
|
|
788
1670
|
/**
|
|
789
1671
|
* Print a summary and GitHub-style YAML diff for every changed setting.
|
|
@@ -802,78 +1684,312 @@ function reportSettingsChanges(changes) {
|
|
|
802
1684
|
}
|
|
803
1685
|
}
|
|
804
1686
|
//#endregion
|
|
805
|
-
//#region src/utils/
|
|
1687
|
+
//#region src/utils/settings-issue-report.ts
|
|
1688
|
+
function formatIssueKeys(keys) {
|
|
1689
|
+
return keys.map((key) => JSON.stringify(key)).join(", ");
|
|
1690
|
+
}
|
|
806
1691
|
/**
|
|
807
|
-
*
|
|
1692
|
+
* Report settings that were intentionally left in their source.
|
|
808
1693
|
*
|
|
809
|
-
* @param
|
|
1694
|
+
* @param options - Retained settings and destination context
|
|
810
1695
|
*
|
|
811
|
-
* @returns
|
|
1696
|
+
* @returns Nothing; warnings are written through consola
|
|
812
1697
|
*/
|
|
813
|
-
function
|
|
814
|
-
const
|
|
815
|
-
|
|
1698
|
+
function reportSettingsIssues(options) {
|
|
1699
|
+
const { compatibility, issues, projectConfig = false, source } = options;
|
|
1700
|
+
if (issues.refused.length) consola$1.warn(`Kept project-refused settings in ${source}: ${formatIssueKeys(issues.refused)}. Configure machine settings globally and current-run paths on the command line.`);
|
|
1701
|
+
if (issues.incompatible.length) consola$1.warn(`Kept settings in ${source} that are incompatible with pnpm ${compatibility.slice(1)}: ${formatIssueKeys(issues.incompatible)}.`);
|
|
1702
|
+
if (issues.nonCamelCase.length) consola$1.warn(`Kept non-camelCase settings in ${source}: ${formatIssueKeys(issues.nonCamelCase)}. Workspace manifest keys must use camelCase.`);
|
|
1703
|
+
if (issues.unknown.length) consola$1.warn(`Kept settings in ${source} that pnpm ${compatibility.slice(1)} does not recognize: ${formatIssueKeys(issues.unknown)}.`);
|
|
1704
|
+
if (issues.unsupported.length) {
|
|
1705
|
+
const destination = projectConfig && compatibility === "v12" ? "pnpm v12 does not support packageConfigs" : "packageConfigs only accepts hoist, modulesDir, overrides, saveExact, and savePrefix";
|
|
1706
|
+
consola$1.warn(`Kept subproject settings in ${source}: ${formatIssueKeys(issues.unsupported)}; ${destination}.`);
|
|
1707
|
+
}
|
|
1708
|
+
if (issues.unsafe.length) consola$1.warn(`Kept unsafe registry settings in ${source}: ${formatIssueKeys(issues.unsafe)}. Remove credentials and dynamic URL interpolation before migrating them.`);
|
|
816
1709
|
}
|
|
1710
|
+
//#endregion
|
|
1711
|
+
//#region src/utils/yaml-document.ts
|
|
817
1712
|
/**
|
|
818
|
-
*
|
|
1713
|
+
* Sort every mapping in a YAML node without replacing its comments or anchors.
|
|
819
1714
|
*
|
|
820
|
-
* @param
|
|
821
|
-
|
|
1715
|
+
* @param node - YAML node whose nested mappings should be sorted
|
|
1716
|
+
*/
|
|
1717
|
+
function sortYamlMappings(node) {
|
|
1718
|
+
if (isMap(node)) {
|
|
1719
|
+
node.items.sort((left, right) => {
|
|
1720
|
+
const leftKey = isScalar(left.key) ? String(left.key.value) : "";
|
|
1721
|
+
const rightKey = isScalar(right.key) ? String(right.key.value) : "";
|
|
1722
|
+
return leftKey.localeCompare(rightKey);
|
|
1723
|
+
});
|
|
1724
|
+
for (const pair of node.items) if (isNode(pair.value)) sortYamlMappings(pair.value);
|
|
1725
|
+
} else if (isSeq(node)) {
|
|
1726
|
+
for (const item of node.items) if (isNode(item)) sortYamlMappings(item);
|
|
1727
|
+
}
|
|
1728
|
+
}
|
|
1729
|
+
/**
|
|
1730
|
+
* Apply semantic root changes to the parsed document while retaining untouched
|
|
1731
|
+
* YAML nodes and their source metadata.
|
|
822
1732
|
*
|
|
823
|
-
* @
|
|
1733
|
+
* @param document - Parsed YAML document to update in place
|
|
1734
|
+
* @param options - Before/after settings and sorting preference
|
|
824
1735
|
*
|
|
825
|
-
* @
|
|
1736
|
+
* @returns Nothing; the document is updated in place
|
|
826
1737
|
*/
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
const content = await fsReadFile(path);
|
|
833
|
-
return {
|
|
834
|
-
indent: detectIndent(content).indent,
|
|
835
|
-
value: JSON.parse(content)
|
|
836
|
-
};
|
|
1738
|
+
function updateYamlDocument(document, options) {
|
|
1739
|
+
const { after, before, sortKeys } = options;
|
|
1740
|
+
for (const key of Object.keys(before)) if (!Object.hasOwn(after, key)) document.delete(key);
|
|
1741
|
+
for (const [key, value] of Object.entries(after)) if (!Object.hasOwn(before, key) || !isDeepStrictEqual(Reflect.get(before, key), value)) document.set(key, value);
|
|
1742
|
+
if (sortKeys) sortYamlMappings(document.contents);
|
|
837
1743
|
}
|
|
838
1744
|
/**
|
|
839
|
-
*
|
|
1745
|
+
* Normalize blank lines immediately before root mapping keys.
|
|
840
1746
|
*
|
|
841
|
-
* @param
|
|
842
|
-
* @param
|
|
1747
|
+
* @param content - Serialized workspace YAML
|
|
1748
|
+
* @param newlineBetween - Whether root keys should be separated by blank lines
|
|
843
1749
|
*
|
|
844
|
-
* @returns
|
|
1750
|
+
* @returns YAML with normalized root-key spacing
|
|
1751
|
+
*/
|
|
1752
|
+
function formatRootSpacing(content, newlineBetween) {
|
|
1753
|
+
return content.replace(/\n+(?=[^\s#][^:\n]*:)/gu, newlineBetween ? "\n\n" : "\n");
|
|
1754
|
+
}
|
|
1755
|
+
//#endregion
|
|
1756
|
+
//#region src/migration-persistence.ts
|
|
1757
|
+
const SETTINGS_WITHOUT_REPLACEMENT = /* @__PURE__ */ new Set(["ignoreDepScripts", "ignorePatchFailures"]);
|
|
1758
|
+
const REPLACEMENT_SETTING_KEYS = {
|
|
1759
|
+
allowNonAppliedPatches: "allowUnusedPatches",
|
|
1760
|
+
auditConfig: "audit",
|
|
1761
|
+
auditLevel: "audit",
|
|
1762
|
+
cleanupUnusedCatalogs: "catalogPrune",
|
|
1763
|
+
enableGlobalVirtualStore: "virtualStoreType",
|
|
1764
|
+
ignoredBuiltDependencies: "allowBuilds",
|
|
1765
|
+
managePackageManagerVersions: "pmOnFail",
|
|
1766
|
+
namedRegistries: "registries",
|
|
1767
|
+
neverBuiltDependencies: "allowBuilds",
|
|
1768
|
+
onlyBuiltDependencies: "allowBuilds",
|
|
1769
|
+
onlyBuiltDependenciesFile: "allowBuilds",
|
|
1770
|
+
packageManagerStrict: "pmOnFail",
|
|
1771
|
+
packageManagerStrictVersion: "pmOnFail",
|
|
1772
|
+
remoteSideEffectsCache: "sideEffectsCache",
|
|
1773
|
+
sideEffectsCacheReadonly: "sideEffectsCache",
|
|
1774
|
+
updateConfig: "update"
|
|
1775
|
+
};
|
|
1776
|
+
function cleanPackageJsonSettings({ migratedKeys, packageJson, settings, yarnResolutionsApplied }) {
|
|
1777
|
+
let changed = false;
|
|
1778
|
+
if (packageJson.value.pnpm) {
|
|
1779
|
+
for (const key of migratedKeys) {
|
|
1780
|
+
Reflect.deleteProperty(packageJson.value.pnpm, key);
|
|
1781
|
+
changed = true;
|
|
1782
|
+
}
|
|
1783
|
+
if (!Object.keys(packageJson.value.pnpm).length) delete packageJson.value.pnpm;
|
|
1784
|
+
}
|
|
1785
|
+
if (settings.yarnResolutions && yarnResolutionsApplied) {
|
|
1786
|
+
changed = true;
|
|
1787
|
+
delete packageJson.value.resolutions;
|
|
1788
|
+
}
|
|
1789
|
+
return changed;
|
|
1790
|
+
}
|
|
1791
|
+
function containsMigratedValue(actual, expected) {
|
|
1792
|
+
if (Array.isArray(actual) && Array.isArray(expected)) return expected.every((expectedItem) => actual.some((actualItem) => containsMigratedValue(actualItem, expectedItem)));
|
|
1793
|
+
if (actual && expected && typeof actual === "object" && typeof expected === "object") return Object.entries(expected).every(([key, expectedValue]) => containsMigratedValue(actual[key], expectedValue));
|
|
1794
|
+
return Object.is(actual, expected);
|
|
1795
|
+
}
|
|
1796
|
+
function resolveSourceRuntimeVersion(sourceSettings, targetKey) {
|
|
1797
|
+
if (targetKey === "useNodeVersion") return Reflect.get(sourceSettings, "useNodeVersion");
|
|
1798
|
+
const executionEnv = Reflect.get(sourceSettings, "executionEnv");
|
|
1799
|
+
if (!executionEnv || typeof executionEnv !== "object" || Array.isArray(executionEnv)) return;
|
|
1800
|
+
return executionEnv.nodeVersion;
|
|
1801
|
+
}
|
|
1802
|
+
function resolveReplacementSettingKey(sourceKey, targetKey, npmrc) {
|
|
1803
|
+
if (npmrc && /^node-mirror:/iu.test(sourceKey)) return "nodeDownloadMirrors";
|
|
1804
|
+
return REPLACEMENT_SETTING_KEYS[targetKey];
|
|
1805
|
+
}
|
|
1806
|
+
function selectAppliedRootKeys({ finalSettings, incomingSettings, keys, npmrc = false, runtimeApplied, runtimeVersion, sourceSettings }) {
|
|
1807
|
+
return keys.filter((sourceKey) => {
|
|
1808
|
+
const targetKey = npmrc ? Object.keys(camelcaseKeys({ [sourceKey]: true }))[0] ?? sourceKey : sourceKey;
|
|
1809
|
+
if (!Object.hasOwn(incomingSettings, targetKey)) {
|
|
1810
|
+
if (targetKey === "executionEnv" || targetKey === "useNodeVersion") {
|
|
1811
|
+
const sourceRuntimeVersion = resolveSourceRuntimeVersion(sourceSettings, targetKey);
|
|
1812
|
+
return runtimeApplied && typeof sourceRuntimeVersion === "string" && sourceRuntimeVersion === runtimeVersion;
|
|
1813
|
+
}
|
|
1814
|
+
if (SETTINGS_WITHOUT_REPLACEMENT.has(targetKey)) return false;
|
|
1815
|
+
const replacementKey = resolveReplacementSettingKey(sourceKey, targetKey, npmrc);
|
|
1816
|
+
if (!replacementKey || !Object.hasOwn(incomingSettings, replacementKey)) return true;
|
|
1817
|
+
return containsMigratedValue(Reflect.get(finalSettings, replacementKey), Reflect.get(incomingSettings, replacementKey));
|
|
1818
|
+
}
|
|
1819
|
+
return containsMigratedValue(Reflect.get(finalSettings, targetKey), Reflect.get(incomingSettings, targetKey));
|
|
1820
|
+
});
|
|
1821
|
+
}
|
|
1822
|
+
function resolveProjectConfig(settings, projectName) {
|
|
1823
|
+
const { packageConfigs } = settings;
|
|
1824
|
+
if (!packageConfigs || typeof packageConfigs !== "object" || Array.isArray(packageConfigs)) return;
|
|
1825
|
+
const projectConfig = packageConfigs[projectName];
|
|
1826
|
+
return projectConfig && typeof projectConfig === "object" && !Array.isArray(projectConfig) ? projectConfig : void 0;
|
|
1827
|
+
}
|
|
1828
|
+
function selectAppliedProjectKeys(project, finalSettings) {
|
|
1829
|
+
const finalProjectConfig = resolveProjectConfig(finalSettings, project.projectName);
|
|
1830
|
+
if (!finalProjectConfig) return [];
|
|
1831
|
+
return project.migratable.keys.filter((sourceKey) => {
|
|
1832
|
+
const targetKey = Object.keys(camelcaseKeys({ [sourceKey]: true }))[0] ?? sourceKey;
|
|
1833
|
+
return containsMigratedValue(finalProjectConfig[targetKey], project.migratable.settings[targetKey]);
|
|
1834
|
+
});
|
|
1835
|
+
}
|
|
1836
|
+
/**
|
|
1837
|
+
* Persist destinations before pruning any legacy source file.
|
|
845
1838
|
*
|
|
846
|
-
* @
|
|
1839
|
+
* @param options - Destination files, selected sources, and cleanup policy
|
|
1840
|
+
*
|
|
1841
|
+
* @returns A promise that resolves after destinations and sources are persisted
|
|
847
1842
|
*/
|
|
848
|
-
async function
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
1843
|
+
async function persistMigration(options) {
|
|
1844
|
+
const { cleanNpmrc, cleanPackageJson, compatibility, finalSettings, incomingSettings, npmrc, npmrcExists, npmrcPath, packageJson, packageJsonExists, packageJsonPath, packageJsonRuntimeChanged, packageJsonSettings, pnpmWorkspaceContent, pnpmWorkspacePath, projectNpmrcs, runtimeVersion } = options;
|
|
1845
|
+
const appliedPackageJsonKeys = selectAppliedRootKeys({
|
|
1846
|
+
finalSettings,
|
|
1847
|
+
incomingSettings,
|
|
1848
|
+
keys: packageJsonSettings.keys,
|
|
1849
|
+
runtimeApplied: packageJsonRuntimeChanged,
|
|
1850
|
+
runtimeVersion,
|
|
1851
|
+
sourceSettings: packageJsonSettings.settings
|
|
1852
|
+
});
|
|
1853
|
+
const appliedNpmrcKeys = selectAppliedRootKeys({
|
|
1854
|
+
finalSettings,
|
|
1855
|
+
incomingSettings,
|
|
1856
|
+
keys: npmrc.keys,
|
|
1857
|
+
npmrc: true,
|
|
1858
|
+
runtimeApplied: packageJsonRuntimeChanged,
|
|
1859
|
+
runtimeVersion,
|
|
1860
|
+
sourceSettings: npmrc.settings
|
|
1861
|
+
});
|
|
1862
|
+
const yarnResolutionsApplied = packageJsonSettings.yarnResolutions && containsMigratedValue(finalSettings.overrides, incomingSettings.overrides);
|
|
1863
|
+
const packageJsonSettingsChanged = packageJsonExists && cleanPackageJson ? cleanPackageJsonSettings({
|
|
1864
|
+
migratedKeys: appliedPackageJsonKeys,
|
|
1865
|
+
packageJson,
|
|
1866
|
+
settings: packageJsonSettings,
|
|
1867
|
+
yarnResolutionsApplied
|
|
1868
|
+
}) : false;
|
|
1869
|
+
await fsWriteFile(pnpmWorkspacePath, pnpmWorkspaceContent);
|
|
1870
|
+
if (packageJsonExists && (packageJsonRuntimeChanged || packageJsonSettingsChanged)) await fsWriteFile(packageJsonPath, JSON.stringify(packageJson.value, null, packageJson.indent));
|
|
1871
|
+
if (!cleanNpmrc) return;
|
|
1872
|
+
const pruneTasks = projectNpmrcs.projects.flatMap((project) => {
|
|
1873
|
+
const appliedKeys = selectAppliedProjectKeys(project, finalSettings);
|
|
1874
|
+
return appliedKeys.length ? [pruneNpmrc(project.npmrcPath, compatibility, appliedKeys)] : [];
|
|
1875
|
+
});
|
|
1876
|
+
if (npmrcExists && appliedNpmrcKeys.length) pruneTasks.push(pruneNpmrc(npmrcPath, compatibility, appliedNpmrcKeys));
|
|
1877
|
+
await Promise.all(pruneTasks);
|
|
1878
|
+
}
|
|
1879
|
+
//#endregion
|
|
1880
|
+
//#region src/migration-sources.ts
|
|
1881
|
+
/**
|
|
1882
|
+
* Read, schema-filter, report, and merge all legacy settings sources.
|
|
1883
|
+
*
|
|
1884
|
+
* @param options - Migration target and source configuration
|
|
1885
|
+
*
|
|
1886
|
+
* @returns Resolved source metadata and merged incoming settings
|
|
1887
|
+
*/
|
|
1888
|
+
async function resolveMigrationSources(options) {
|
|
1889
|
+
const { compatibility, cwd, npmrcExists, npmrcPath, packageJson, pnpmWorkspace, strategy, yarnResolutions } = options;
|
|
1890
|
+
const npmrc = npmrcExists ? await readMigratableNpmrc(npmrcPath, compatibility) : {
|
|
1891
|
+
issues: createSettingsIssues(),
|
|
1892
|
+
keys: [],
|
|
1893
|
+
settings: {}
|
|
852
1894
|
};
|
|
853
|
-
|
|
1895
|
+
reportSettingsIssues({
|
|
1896
|
+
compatibility,
|
|
1897
|
+
issues: npmrc.issues,
|
|
1898
|
+
source: NPMRC
|
|
1899
|
+
});
|
|
1900
|
+
const packageJsonSettings = resolvePackageJsonSettings(packageJson, yarnResolutions, compatibility);
|
|
1901
|
+
reportSettingsIssues({
|
|
1902
|
+
compatibility,
|
|
1903
|
+
issues: packageJsonSettings.issues,
|
|
1904
|
+
source: `${PACKAGE_JSON}#pnpm`
|
|
1905
|
+
});
|
|
1906
|
+
const projectNpmrcs = await readProjectNpmrcMigrations(cwd, collectWorkspacePackagePatterns(mergeByStrategy(pnpmWorkspace, mergeByStrategy(packageJsonSettings.settings, npmrc.settings, "merge"), strategy)), compatibility);
|
|
1907
|
+
for (const warning of projectNpmrcs.warnings) consola$1.warn(warning);
|
|
1908
|
+
for (const project of projectNpmrcs.projects) reportSettingsIssues({
|
|
1909
|
+
compatibility,
|
|
1910
|
+
issues: project.migratable.issues,
|
|
1911
|
+
projectConfig: true,
|
|
1912
|
+
source: relative(cwd, project.npmrcPath)
|
|
1913
|
+
});
|
|
1914
|
+
const npmrcSettings = mergeByStrategy(npmrc.settings, Object.keys(projectNpmrcs.packageConfigs).length ? { packageConfigs: projectNpmrcs.packageConfigs } : {}, "merge");
|
|
854
1915
|
return {
|
|
855
|
-
|
|
856
|
-
|
|
1916
|
+
incomingSettings: mergeByStrategy(packageJsonSettings.settings, npmrcSettings, "merge"),
|
|
1917
|
+
npmrc,
|
|
1918
|
+
packageJson: packageJsonSettings,
|
|
1919
|
+
projectNpmrcs
|
|
857
1920
|
};
|
|
858
1921
|
}
|
|
1922
|
+
//#endregion
|
|
1923
|
+
//#region src/options.ts
|
|
859
1924
|
/**
|
|
860
|
-
*
|
|
1925
|
+
* Default values for migration options.
|
|
1926
|
+
*/
|
|
1927
|
+
const DEFAULT_OPTIONS = {
|
|
1928
|
+
cleanNpmrc: true,
|
|
1929
|
+
cleanPackageJson: true,
|
|
1930
|
+
compatibility: "auto",
|
|
1931
|
+
newlineBetween: true,
|
|
1932
|
+
replaceDeprecated: false,
|
|
1933
|
+
showChanges: true,
|
|
1934
|
+
sortKeys: false,
|
|
1935
|
+
strategy: "merge",
|
|
1936
|
+
yarnResolutions: true
|
|
1937
|
+
};
|
|
1938
|
+
const VALID_STRATEGIES = [
|
|
1939
|
+
"discard",
|
|
1940
|
+
"merge",
|
|
1941
|
+
"overwrite"
|
|
1942
|
+
];
|
|
1943
|
+
const VALID_COMPATIBILITIES = [
|
|
1944
|
+
"auto",
|
|
1945
|
+
"v10",
|
|
1946
|
+
"v11",
|
|
1947
|
+
"v12"
|
|
1948
|
+
];
|
|
1949
|
+
function resolveCompatibility(compatibility) {
|
|
1950
|
+
if (!compatibility) return DEFAULT_OPTIONS.compatibility;
|
|
1951
|
+
if (VALID_COMPATIBILITIES.includes(compatibility)) return compatibility;
|
|
1952
|
+
throw new Error(`Invalid compatibility: ${compatibility}. Expected one of: ${VALID_COMPATIBILITIES.join(", ")}`);
|
|
1953
|
+
}
|
|
1954
|
+
function resolveStrategy(strategy) {
|
|
1955
|
+
if (!strategy) return DEFAULT_OPTIONS.strategy;
|
|
1956
|
+
if (VALID_STRATEGIES.includes(strategy)) return strategy;
|
|
1957
|
+
throw new Error(`Invalid strategy: ${strategy}. Expected one of: ${VALID_STRATEGIES.join(", ")}`);
|
|
1958
|
+
}
|
|
1959
|
+
/**
|
|
1960
|
+
* Resolve and normalize migration options with defaults.
|
|
861
1961
|
*
|
|
862
|
-
*
|
|
863
|
-
*
|
|
1962
|
+
* This function takes partial options and returns a complete options object
|
|
1963
|
+
* with all properties set to either the provided value or the default value.
|
|
864
1964
|
*
|
|
865
|
-
* @param
|
|
866
|
-
*
|
|
1965
|
+
* @param options - Partial migration options
|
|
1966
|
+
*
|
|
1967
|
+
* @returns Complete options object with all required properties
|
|
867
1968
|
*
|
|
868
|
-
* @
|
|
1969
|
+
* @example
|
|
1970
|
+
* ```ts
|
|
1971
|
+
* // Use all defaults
|
|
1972
|
+
* const opts = resolveOptions()
|
|
1973
|
+
* // { cleanNpmrc: true, cleanPackageJson: true, cwd: '/current/dir', ... }
|
|
1974
|
+
*
|
|
1975
|
+
* // Override specific options
|
|
1976
|
+
* const opts = resolveOptions({ sortKeys: true, cleanNpmrc: false })
|
|
1977
|
+
* // { cleanNpmrc: false, cleanPackageJson: true, sortKeys: true, ... }
|
|
1978
|
+
* ```
|
|
869
1979
|
*/
|
|
870
|
-
function
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
1980
|
+
function resolveOptions(options = {}) {
|
|
1981
|
+
return {
|
|
1982
|
+
cleanNpmrc: options.cleanNpmrc ?? DEFAULT_OPTIONS.cleanNpmrc,
|
|
1983
|
+
compatibility: resolveCompatibility(options.compatibility),
|
|
1984
|
+
cwd: options.cwd ?? process.cwd(),
|
|
1985
|
+
newlineBetween: options.newlineBetween ?? DEFAULT_OPTIONS.newlineBetween,
|
|
1986
|
+
showChanges: options.showChanges ?? DEFAULT_OPTIONS.showChanges,
|
|
1987
|
+
sortKeys: options.sortKeys ?? DEFAULT_OPTIONS.sortKeys,
|
|
1988
|
+
strategy: resolveStrategy(options.strategy),
|
|
1989
|
+
yarnResolutions: options.yarnResolutions ?? DEFAULT_OPTIONS.yarnResolutions,
|
|
1990
|
+
cleanPackageJson: options.cleanPackageJson ?? DEFAULT_OPTIONS.cleanPackageJson,
|
|
1991
|
+
replaceDeprecated: options.replaceDeprecated ?? DEFAULT_OPTIONS.replaceDeprecated
|
|
1992
|
+
};
|
|
877
1993
|
}
|
|
878
1994
|
//#endregion
|
|
879
1995
|
//#region src/core.ts
|
|
@@ -885,8 +2001,8 @@ function hasSettingsSources(npmrcExists, packageJsonExists, pnpmWorkspaceExists)
|
|
|
885
2001
|
return false;
|
|
886
2002
|
}
|
|
887
2003
|
function hasMigratableSettings(sources) {
|
|
888
|
-
const { existingSettingsChanged,
|
|
889
|
-
return Boolean(existingSettingsChanged ||
|
|
2004
|
+
const { existingSettingsChanged, npmrcKeys, packageJsonKeys, projectNpmrcKeys, yarnResolutions } = sources;
|
|
2005
|
+
return Boolean(existingSettingsChanged || npmrcKeys.length || packageJsonKeys.length || projectNpmrcKeys.length || yarnResolutions);
|
|
890
2006
|
}
|
|
891
2007
|
function assertCanMigrateRuntime(runtimeVersion, packageJsonExists) {
|
|
892
2008
|
if (runtimeVersion && !packageJsonExists) throw new Error("Cannot migrate the removed Node.js runtime setting without a package.json file.");
|
|
@@ -909,10 +2025,13 @@ function reportMigrationChanges(showChanges, before = {}, after = {}) {
|
|
|
909
2025
|
* @param rawOptions.cwd - Current working directory (default: process.cwd())
|
|
910
2026
|
* @param rawOptions.cleanNpmrc - Whether to remove pnpm settings from `.npmrc` (default: true)
|
|
911
2027
|
* @param rawOptions.cleanPackageJson - Whether to remove pnpm field from `package.json` (default: true)
|
|
2028
|
+
* @param rawOptions.compatibility - Target pnpm major, or automatic detection (default: auto)
|
|
912
2029
|
* @param rawOptions.yarnResolutions - Whether to migrate resolutions field (default: true)
|
|
913
2030
|
* @param rawOptions.sortKeys - Whether to sort keys in output YAML (default: false)
|
|
914
2031
|
* @param rawOptions.newlineBetween - Add newlines between root keys (default: true)
|
|
2032
|
+
* @param rawOptions.replaceDeprecated - Whether to replace deprecated settings (default: false)
|
|
915
2033
|
* @param rawOptions.showChanges - Show settings changes after migration (default: true)
|
|
2034
|
+
* @param rawOptions.strategy - Conflict handling strategy (default: merge)
|
|
916
2035
|
*
|
|
917
2036
|
* @returns A promise that resolves when migration is complete
|
|
918
2037
|
*
|
|
@@ -949,16 +2068,18 @@ async function migratePnpmSettings(rawOptions = {}) {
|
|
|
949
2068
|
const [packageJson, pnpmWorkspace] = await Promise.all([readPackageJson(packageJsonPath, packageJsonExists), readPnpmWorkspace(pnpmWorkspaceYamlPath, pnpmWorkspaceExists)]);
|
|
950
2069
|
const pnpmWorkspaceBefore = structuredClone(pnpmWorkspace.value);
|
|
951
2070
|
const compatibility = resolveCompatibilityTarget(options.compatibility, packageJson.value.packageManager, packageJson.value.devEngines?.packageManager);
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
2071
|
+
assertCompatibleWorkspaceSettings(pnpmWorkspace.value, compatibility);
|
|
2072
|
+
const sources = await resolveMigrationSources({
|
|
2073
|
+
compatibility,
|
|
2074
|
+
cwd: options.cwd,
|
|
2075
|
+
npmrcExists,
|
|
2076
|
+
npmrcPath,
|
|
2077
|
+
packageJson: packageJson.value,
|
|
2078
|
+
pnpmWorkspace: pnpmWorkspace.value,
|
|
2079
|
+
strategy: options.strategy,
|
|
2080
|
+
yarnResolutions: options.yarnResolutions
|
|
2081
|
+
});
|
|
2082
|
+
const { incomingSettings } = sources;
|
|
962
2083
|
const [existingNormalization, incomingNormalization] = await Promise.all([normalizeIncomingSettings(pnpmWorkspace.value, {
|
|
963
2084
|
compatibility,
|
|
964
2085
|
cwd: options.cwd,
|
|
@@ -975,35 +2096,41 @@ async function migratePnpmSettings(rawOptions = {}) {
|
|
|
975
2096
|
if (runtimeMigration.warning) consola$1.warn(runtimeMigration.warning);
|
|
976
2097
|
if (!hasMigratableSettings({
|
|
977
2098
|
existingSettingsChanged: existingNormalization.changed,
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
2099
|
+
npmrcKeys: sources.npmrc.keys,
|
|
2100
|
+
packageJsonKeys: sources.packageJson.keys,
|
|
2101
|
+
projectNpmrcKeys: sources.projectNpmrcs.projects.flatMap((project) => project.migratable.keys),
|
|
2102
|
+
yarnResolutions: sources.packageJson.yarnResolutions
|
|
981
2103
|
})) {
|
|
982
2104
|
consola$1.warn("No pnpm settings fields to migrate");
|
|
983
2105
|
reportMigrationChanges(options.showChanges);
|
|
984
2106
|
return;
|
|
985
2107
|
}
|
|
986
2108
|
const pnpmWorkspaceResult = mergeByStrategy(pnpmWorkspace.value, incomingSettings, options.strategy);
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
2109
|
+
updateYamlDocument(pnpmWorkspace.document, {
|
|
2110
|
+
after: pnpmWorkspaceResult,
|
|
2111
|
+
before: pnpmWorkspaceBefore,
|
|
2112
|
+
sortKeys: options.sortKeys
|
|
2113
|
+
});
|
|
2114
|
+
const finalYamlContent = formatRootSpacing(pnpmWorkspace.document.toString({ indent: pnpmWorkspace.indent }), options.newlineBetween);
|
|
2115
|
+
await persistMigration({
|
|
2116
|
+
cleanNpmrc: options.cleanNpmrc,
|
|
2117
|
+
cleanPackageJson: options.cleanPackageJson,
|
|
2118
|
+
compatibility,
|
|
2119
|
+
finalSettings: pnpmWorkspaceResult,
|
|
2120
|
+
incomingSettings,
|
|
2121
|
+
npmrc: sources.npmrc,
|
|
2122
|
+
npmrcExists,
|
|
2123
|
+
npmrcPath,
|
|
2124
|
+
packageJson,
|
|
2125
|
+
packageJsonExists,
|
|
2126
|
+
packageJsonPath,
|
|
2127
|
+
packageJsonRuntimeChanged: runtimeMigration.changed,
|
|
2128
|
+
packageJsonSettings: sources.packageJson,
|
|
2129
|
+
pnpmWorkspaceContent: finalYamlContent,
|
|
2130
|
+
pnpmWorkspacePath: pnpmWorkspaceYamlPath,
|
|
2131
|
+
projectNpmrcs: sources.projectNpmrcs,
|
|
2132
|
+
runtimeVersion
|
|
993
2133
|
});
|
|
994
|
-
const yamlContent = yamlDocument.toString({ indent: pnpmWorkspace.indent });
|
|
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);
|
|
997
|
-
let packageJsonChanged = runtimeMigration.changed;
|
|
998
|
-
if (packageJsonExists && options.cleanPackageJson) {
|
|
999
|
-
if (packageJson.value.pnpm) packageJsonChanged = true;
|
|
1000
|
-
delete packageJson.value.pnpm;
|
|
1001
|
-
if (options.yarnResolutions && packageJson.value.resolutions) {
|
|
1002
|
-
packageJsonChanged = true;
|
|
1003
|
-
delete packageJson.value.resolutions;
|
|
1004
|
-
}
|
|
1005
|
-
}
|
|
1006
|
-
if (packageJsonExists && packageJsonChanged) await fsWriteFile(packageJsonPath, JSON.stringify(packageJson.value, null, packageJson.indent));
|
|
1007
2134
|
reportMigrationChanges(options.showChanges, pnpmWorkspaceBefore, pnpmWorkspaceResult);
|
|
1008
2135
|
} catch (err) {
|
|
1009
2136
|
consola$1.error("Failed to migrate pnpm settings:", err);
|
|
@@ -1012,9 +2139,8 @@ async function migratePnpmSettings(rawOptions = {}) {
|
|
|
1012
2139
|
}
|
|
1013
2140
|
//#endregion
|
|
1014
2141
|
//#region src/cli.ts
|
|
1015
|
-
const cli =
|
|
1016
|
-
cli.
|
|
1017
|
-
cli.command("").action(async (options) => {
|
|
2142
|
+
const cli = createCli();
|
|
2143
|
+
cli.command("").usage("[options]").action(async (options) => {
|
|
1018
2144
|
try {
|
|
1019
2145
|
consola.log(`\n${bold(magenta(name))} ${dim(`v${version}`)}`);
|
|
1020
2146
|
consola.log(dim("\n--------------\n"));
|