update-versions 7.1.3 → 7.2.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.
Files changed (5) hide show
  1. package/CHANGELOG.md +13 -2
  2. package/LICENSE +1 -1
  3. package/README.md +2 -2
  4. package/cli.js +777 -390
  5. package/package.json +22 -29
package/cli.js CHANGED
@@ -3,33 +3,248 @@
3
3
  // VARS
4
4
  // -----------------------------------------------------------------------------
5
5
 
6
- import meow from "meow";
7
- import pacote from "pacote";
8
- import pReduce from "p-reduce";
9
- import { globby } from "globby";
10
- import isOnline from "is-online";
6
+ import { promises, realpathSync } from "node:fs";
7
+ import { createRequire } from "node:module";
8
+ import path from "node:path";
9
+ import { fileURLToPath } from "node:url";
11
10
  import diff1 from "ansi-diff-stream";
11
+ import { glob } from "codsen-glob";
12
+ import { codsenCLI, isPlainObject } from "codsen-utils";
13
+ import { del, set } from "edit-package-json";
12
14
  import objectPath from "object-path";
13
- import write from "write-file-atomic";
14
- import { createRequire } from "module";
15
- import { isPlainObject } from "codsen-utils";
16
- import { promises, readFileSync } from "fs";
17
- import { set, del } from "edit-package-json";
18
- import updateNotifier from "update-notifier";
19
15
  import pProgress, { PProgress } from "p-progress";
16
+ import pReduce from "p-reduce";
17
+ import packageJson from "package-json";
18
+ import updateNotifier from "update-notifier";
19
+ import write from "write-file-atomic";
20
20
 
21
21
  const require1 = createRequire(import.meta.url);
22
22
  const pkg = require1("./package.json");
23
23
 
24
24
  const { readFile } = promises;
25
- const diff = diff1();
26
25
 
27
26
  const { log } = console;
28
27
  const sparkles = "\u2728"; // https://emojipedia.org/sparkles/
29
28
  const messagePrefix = `\u001b[${90}m${`${sparkles} update-versions: `}\u001b[${39}m`;
30
29
 
31
- const cli = meow(
32
- `
30
+ const defaultConfig = Object.freeze({
31
+ noMajorBumping: Object.freeze([]),
32
+ pin: Object.freeze({}),
33
+ });
34
+
35
+ function makeFailure(phase, filename, cause) {
36
+ let causeMessage = cause instanceof Error ? cause.message : String(cause);
37
+ let error = new Error(`${phase} failed for "${filename}": ${causeMessage}`);
38
+ error.name = "UpdateVersionsOperationError";
39
+ error.phase = phase;
40
+ error.path = filename;
41
+ error.cause = cause;
42
+ return error;
43
+ }
44
+
45
+ export class UpdateVersionsError extends AggregateError {
46
+ constructor(
47
+ errors,
48
+ { updatedFiles = [], unchangedFiles = [], updatedPackages = {} } = {},
49
+ ) {
50
+ let failureCount = errors.length;
51
+ let message = `update-versions failed with ${failureCount} ${
52
+ failureCount === 1 ? "error" : "errors"
53
+ }; ${updatedFiles.length} ${
54
+ updatedFiles.length === 1 ? "file was" : "files were"
55
+ } updated and ${unchangedFiles.length} ${
56
+ unchangedFiles.length === 1 ? "file was" : "files were"
57
+ } unchanged.`;
58
+ if (updatedFiles.length === 0) {
59
+ message += " Nothing was written.";
60
+ }
61
+ super(errors, message);
62
+ this.name = "UpdateVersionsError";
63
+ this.code = "UPDATE_VERSIONS_FAILED";
64
+ this.updatedFiles = [...updatedFiles];
65
+ this.unchangedFiles = [...unchangedFiles];
66
+ this.updatedPackages = { ...updatedPackages };
67
+ }
68
+ }
69
+
70
+ function parseConfig(configSource, configPath) {
71
+ let parsed;
72
+ try {
73
+ parsed = JSON.parse(configSource);
74
+ } catch (error) {
75
+ throw new TypeError(
76
+ `update-versions/updateVersions(): [THROW_ID_01] Could not parse "${configPath}" as JSON: ${error.message}`,
77
+ );
78
+ }
79
+
80
+ if (!isPlainObject(parsed)) {
81
+ throw new TypeError(
82
+ `update-versions/updateVersions(): [THROW_ID_02] "${configPath}" must contain a JSON object.`,
83
+ );
84
+ }
85
+
86
+ let unknownKeys = Object.keys(parsed).filter(
87
+ (key) => !Object.hasOwn(defaultConfig, key),
88
+ );
89
+ if (unknownKeys.length > 0) {
90
+ throw new TypeError(
91
+ `update-versions/updateVersions(): [THROW_ID_03] "${configPath}" contains unsupported ${
92
+ unknownKeys.length === 1 ? "property" : "properties"
93
+ }: ${unknownKeys.sort().join(", ")}.`,
94
+ );
95
+ }
96
+
97
+ if (
98
+ parsed.noMajorBumping !== undefined &&
99
+ (!Array.isArray(parsed.noMajorBumping) ||
100
+ parsed.noMajorBumping.some(
101
+ (name) =>
102
+ typeof name !== "string" ||
103
+ name.trim().length === 0 ||
104
+ name !== name.trim(),
105
+ ))
106
+ ) {
107
+ throw new TypeError(
108
+ `update-versions/updateVersions(): [THROW_ID_04] "noMajorBumping" in "${configPath}" must be an array of trimmed, non-empty package-name strings.`,
109
+ );
110
+ }
111
+
112
+ if (parsed.pin !== undefined && !isPlainObject(parsed.pin)) {
113
+ throw new TypeError(
114
+ `update-versions/updateVersions(): [THROW_ID_05] "pin" in "${configPath}" must be a plain object.`,
115
+ );
116
+ }
117
+
118
+ if (
119
+ parsed.pin !== undefined &&
120
+ Object.entries(parsed.pin).some(
121
+ ([name, version]) =>
122
+ name.trim().length === 0 ||
123
+ name !== name.trim() ||
124
+ typeof version !== "string" ||
125
+ version.trim().length === 0 ||
126
+ version !== version.trim(),
127
+ )
128
+ ) {
129
+ throw new TypeError(
130
+ `update-versions/updateVersions(): [THROW_ID_06] Every "pin" entry in "${configPath}" must map a trimmed, non-empty package name to a trimmed, non-empty string.`,
131
+ );
132
+ }
133
+
134
+ return {
135
+ noMajorBumping: [
136
+ ...new Set(parsed.noMajorBumping ?? defaultConfig.noMajorBumping),
137
+ ],
138
+ pin: { ...(parsed.pin ?? defaultConfig.pin) },
139
+ };
140
+ }
141
+
142
+ async function loadConfig(configPath, readTextFile) {
143
+ let configSource;
144
+ try {
145
+ configSource = await readTextFile(configPath, "utf8");
146
+ } catch (error) {
147
+ if (error?.code === "ENOENT") {
148
+ return {
149
+ noMajorBumping: [...defaultConfig.noMajorBumping],
150
+ pin: { ...defaultConfig.pin },
151
+ };
152
+ }
153
+ throw makeFailure("config read", configPath, error);
154
+ }
155
+ try {
156
+ return parseConfig(configSource, configPath);
157
+ } catch (error) {
158
+ throw makeFailure("config validation", configPath, error);
159
+ }
160
+ }
161
+
162
+ function parseDependencySpec(dependencyName, currentSpec) {
163
+ if (typeof currentSpec !== "string") {
164
+ throw new TypeError(
165
+ `update-versions/updateVersions(): [THROW_ID_07] Dependency "${dependencyName}" must use a string version specifier; received ${typeof currentSpec}.`,
166
+ );
167
+ }
168
+ if (!currentSpec.startsWith("workspace:")) {
169
+ return {
170
+ dependencyName,
171
+ kind: "registry",
172
+ selector: currentSpec,
173
+ targetName: dependencyName,
174
+ };
175
+ }
176
+
177
+ let workspaceValue = currentSpec.slice("workspace:".length);
178
+ if (/^\.\.?\//.test(workspaceValue)) {
179
+ return {
180
+ dependencyName,
181
+ kind: "workspace-path",
182
+ path: workspaceValue,
183
+ selector: null,
184
+ targetName: null,
185
+ };
186
+ }
187
+
188
+ let aliasSeparator = workspaceValue.lastIndexOf("@");
189
+ if (aliasSeparator > 0) {
190
+ return {
191
+ dependencyName,
192
+ kind: "workspace-alias",
193
+ selector: workspaceValue.slice(aliasSeparator + 1),
194
+ targetName: workspaceValue.slice(0, aliasSeparator),
195
+ };
196
+ }
197
+
198
+ return {
199
+ dependencyName,
200
+ kind: "workspace-selector",
201
+ selector: workspaceValue,
202
+ targetName: dependencyName,
203
+ };
204
+ }
205
+
206
+ function workspaceSpecPrefix(parsedSpec) {
207
+ return parsedSpec.kind === "workspace-alias"
208
+ ? `workspace:${parsedSpec.targetName}@`
209
+ : "workspace:";
210
+ }
211
+
212
+ function pinnedDependencySpec(parsedSpec, currentSpec, pinnedSpec) {
213
+ if (parsedSpec.kind === "registry") {
214
+ return pinnedSpec;
215
+ }
216
+ if (parsedSpec.kind === "workspace-path") {
217
+ return currentSpec;
218
+ }
219
+ let selector = pinnedSpec;
220
+ if (pinnedSpec.startsWith("workspace:")) {
221
+ let parsedPin = parseDependencySpec(parsedSpec.dependencyName, pinnedSpec);
222
+ selector = parsedPin.selector ?? parsedSpec.selector;
223
+ }
224
+ return `${workspaceSpecPrefix(parsedSpec)}${selector}`;
225
+ }
226
+
227
+ function updatedDependencySpec(parsedSpec, currentSpec, version) {
228
+ if (parsedSpec.kind === "registry") {
229
+ return `^${version}`;
230
+ }
231
+ if (parsedSpec.kind === "workspace-path") {
232
+ return currentSpec;
233
+ }
234
+
235
+ let workspaceRange = parsedSpec.selector;
236
+ if (["*", "^", "~"].includes(workspaceRange)) {
237
+ return currentSpec;
238
+ }
239
+ let firstVersionDigit = workspaceRange.search(/\d/);
240
+ if (firstVersionDigit === -1) {
241
+ return currentSpec;
242
+ }
243
+ let rangePrefix = workspaceRange.slice(0, firstVersionDigit);
244
+ return `${workspaceSpecPrefix(parsedSpec)}${rangePrefix}${version}`;
245
+ }
246
+
247
+ const helpText = `
33
248
  Usage:
34
249
  $ upd
35
250
  $ or...
@@ -39,44 +254,44 @@ const cli = meow(
39
254
  -m, --module Blacklist against bumping major any type=module packages
40
255
  -h, --help Shows this help
41
256
  -v, --version Shows the current installed version
42
- `,
43
- {
44
- importMeta: import.meta,
45
- },
46
- );
47
- updateNotifier({ pkg }).notify();
48
-
49
- // Step #0. take care of -v and -h flags that are left out in meow.
50
- // -----------------------------------------------------------------------------
51
-
52
- if (cli.flags.v) {
53
- log(pkg.version);
54
- process.exit(0);
55
- } else if (cli.flags.h) {
56
- log(cli.help);
57
- process.exit(0);
58
- }
59
-
60
- // Step #1. set up the cli
61
- // -----------------------------------------------------------------------------
62
257
 
63
- let { input } = cli;
64
- // if the folder/file name follows the flag (for example "-d templates1"),
65
- // that name will be put under the flag's key value, not into cli.input.
66
- // That's handy for certain types of CLI apps, but not this one, as in our case
67
- // the flags position does not matter, they don't affect the keywords that follow.
68
- if (cli.flags) {
69
- Object.keys(cli.flags).forEach((flag) => {
70
- if (typeof cli.flags[flag] === "string") {
71
- input = input.concat(cli.flags[flag]);
258
+ Optional upd.config.json:
259
+ {
260
+ "noMajorBumping": ["package-name"],
261
+ "pin": { "package-name": "1.2.3" }
72
262
  }
263
+ `;
264
+
265
+ function parseCli(argv = process.argv.slice(2)) {
266
+ return codsenCLI(helpText, {
267
+ pkg,
268
+ argv,
269
+ flags: {
270
+ module: { type: "boolean", shortFlag: "m" },
271
+ help: { type: "boolean", shortFlag: "h" },
272
+ version: { type: "boolean", shortFlag: "v" },
273
+ },
73
274
  });
74
275
  }
75
276
 
76
- // Step #2. the main function
277
+ // Step #1. the main function
77
278
  // -----------------------------------------------------------------------------
78
279
 
79
- (async () => {
280
+ export async function updateVersions({
281
+ cwd = process.cwd(),
282
+ effects = {},
283
+ fetchPackage = packageJson,
284
+ moduleMode = false,
285
+ reportProgress = false,
286
+ } = {}) {
287
+ let {
288
+ deleteJsonValue = del,
289
+ findPackageJsons = glob,
290
+ readTextFile = readFile,
291
+ setJsonValue = set,
292
+ writeTextFile = write,
293
+ } = effects;
294
+
80
295
  // we'll use the object below to distil all unique package updates
81
296
  let updatedPackages = {};
82
297
  function printUpdated() {
@@ -86,69 +301,159 @@ if (cli.flags) {
86
301
  .join("\n");
87
302
  }
88
303
  function major(versNum) {
89
- if (typeof versNum === "string" && versNum.includes(".")) {
90
- return versNum.split(".")[0];
304
+ if (typeof versNum === "string") {
305
+ return (
306
+ versNum.match(/^(?:workspace:)?[^\d]*(\d+)(?:\.|$)/)?.[1] ?? versNum
307
+ );
91
308
  }
92
309
  return versNum;
93
310
  }
94
311
 
95
- let confLocation = "./upd.config.json";
96
- let newConfig = {
97
- noMajorBumping: [],
98
- pin: {},
99
- };
100
-
101
- let online = await isOnline();
102
- if (!online) {
103
- console.error(
104
- `\n${messagePrefix}${`\u001b[${31}m${"Please check your internet connection."}\u001b[${39}m`}\n`,
105
- );
106
- process.exit(1);
312
+ let configPath = path.join(cwd, "upd.config.json");
313
+ let newConfig;
314
+ try {
315
+ newConfig = await loadConfig(configPath, readTextFile);
316
+ } catch (error) {
317
+ throw new UpdateVersionsError([error]);
107
318
  }
108
319
 
109
- // try to read the local config if it's present
320
+ let packagePaths;
110
321
  try {
111
- newConfig = JSON.parse(readFileSync(confLocation, "utf8"));
112
- } catch (e) {
113
- console.log(
114
- `\n${messagePrefix}${`\u001b[${90}m${"No config found, moving on."}\u001b[${39}m`}\n`,
322
+ packagePaths = await findPackageJsons(
323
+ ["**/package.json", "!**/node_modules/**", "!**/test/**"],
324
+ { cwd },
115
325
  );
326
+ } catch (error) {
327
+ throw new UpdateVersionsError([
328
+ makeFailure("package discovery", cwd, error),
329
+ ]);
116
330
  }
117
331
 
118
- let pathsPromise = await globby([
119
- "**/package.json",
120
- "!**/node_modules/**",
121
- "!**/test/**",
122
- ]).then((paths) =>
123
- pReduce(
124
- paths,
125
- (mapReceived, currentPath) =>
126
- readFile(currentPath, "utf8")
127
- .then((packContentsStr) => {
128
- let parsedContents = JSON.parse(packContentsStr);
129
- mapReceived.namesList.push(parsedContents.name);
130
- mapReceived.pathsList.push(currentPath);
131
- mapReceived.pathsByName[parsedContents.name] = currentPath;
132
- mapReceived.contentsStr[currentPath] = packContentsStr;
133
- mapReceived.contentsObj[currentPath] = parsedContents;
134
- return mapReceived;
135
- })
136
- .catch((err) => {
137
- log(
138
- `${messagePrefix}${`\u001b[${31}m${`Couldn't read and parse the package.json at "${currentPath}": (${err})`}\u001b[${39}m`}`,
139
- );
140
- return mapReceived;
141
- }),
142
- {
143
- namesList: [],
144
- pathsList: [],
145
- pathsByName: {},
146
- contentsObj: {},
147
- contentsStr: {},
148
- },
149
- ),
332
+ let inventoryFailures = [];
333
+ let pathsPromise = await pReduce(
334
+ packagePaths,
335
+ async (mapReceived, currentPath) => {
336
+ let packagePath = path.join(cwd, currentPath);
337
+ let packContentsStr;
338
+ try {
339
+ packContentsStr = await readTextFile(packagePath, "utf8");
340
+ } catch (error) {
341
+ inventoryFailures.push(makeFailure("package read", currentPath, error));
342
+ return mapReceived;
343
+ }
344
+
345
+ let parsedContents;
346
+ try {
347
+ parsedContents = JSON.parse(packContentsStr);
348
+ if (!isPlainObject(parsedContents)) {
349
+ throw new TypeError(
350
+ "update-versions/updateVersions(): [THROW_ID_08] package.json must contain a JSON object.",
351
+ );
352
+ }
353
+ } catch (error) {
354
+ inventoryFailures.push(
355
+ makeFailure("package parse", currentPath, error),
356
+ );
357
+ return mapReceived;
358
+ }
359
+
360
+ mapReceived.namesList.push(parsedContents.name);
361
+ mapReceived.pathsList.push(currentPath);
362
+ mapReceived.pathsByName[parsedContents.name] = currentPath;
363
+ mapReceived.contentsStr[currentPath] = packContentsStr;
364
+ mapReceived.contentsObj[currentPath] = parsedContents;
365
+ return mapReceived;
366
+ },
367
+ {
368
+ namesList: [],
369
+ pathsList: [],
370
+ pathsByName: {},
371
+ contentsObj: {},
372
+ contentsStr: {},
373
+ },
150
374
  );
151
375
 
376
+ if (inventoryFailures.length > 0) {
377
+ throw new UpdateVersionsError(inventoryFailures, {
378
+ unchangedFiles: pathsPromise.pathsList,
379
+ });
380
+ }
381
+
382
+ // Resolve the complete registry view before touching any package.json. This
383
+ // makes a failed registry run atomic from the caller's point of view and also
384
+ // deduplicates lookups shared by packages in a monorepo.
385
+ let externalNames = new Set();
386
+ for (let oneOfPaths of pathsPromise.pathsList) {
387
+ let parsedContents = pathsPromise.contentsObj[oneOfPaths];
388
+ for (let dependencyKey of ["dependencies", "devDependencies"]) {
389
+ if (isPlainObject(parsedContents[dependencyKey])) {
390
+ for (let [name, spec] of Object.entries(
391
+ parsedContents[dependencyKey],
392
+ )) {
393
+ if (
394
+ typeof spec === "string" &&
395
+ !spec.startsWith("file:") &&
396
+ !Object.hasOwn(newConfig.pin, name)
397
+ ) {
398
+ let parsedSpec = parseDependencySpec(name, spec);
399
+ if (
400
+ parsedSpec.targetName &&
401
+ !pathsPromise.namesList.includes(parsedSpec.targetName)
402
+ ) {
403
+ externalNames.add(parsedSpec.targetName);
404
+ }
405
+ }
406
+ }
407
+ }
408
+ }
409
+ }
410
+
411
+ let registryFailures = [];
412
+ let registryMetadata = new Map();
413
+ let registryResults = await Promise.all(
414
+ [...externalNames].map(async (name) => {
415
+ try {
416
+ let metadata = await fetchPackage(name, { fullMetadata: true });
417
+ if (
418
+ !metadata ||
419
+ typeof metadata.version !== "string" ||
420
+ metadata.version.length === 0
421
+ ) {
422
+ throw new TypeError(`${name} returned no version`);
423
+ }
424
+ return { metadata, name };
425
+ } catch (error) {
426
+ return { error, name };
427
+ }
428
+ }),
429
+ );
430
+ for (let result of registryResults) {
431
+ if (result.error) {
432
+ registryFailures.push(
433
+ makeFailure("registry lookup", result.name, result.error),
434
+ );
435
+ } else {
436
+ registryMetadata.set(result.name, result.metadata);
437
+ }
438
+ }
439
+
440
+ if (registryFailures.length > 0) {
441
+ throw new UpdateVersionsError(registryFailures, {
442
+ unchangedFiles: pathsPromise.pathsList,
443
+ });
444
+ }
445
+
446
+ if (moduleMode) {
447
+ for (let [name, metadata] of registryMetadata) {
448
+ if (
449
+ metadata?.type === "module" &&
450
+ !newConfig.noMajorBumping.includes(name)
451
+ ) {
452
+ newConfig.noMajorBumping.push(name);
453
+ }
454
+ }
455
+ }
456
+
152
457
  let allProgressPromise = PProgress.all(
153
458
  pathsPromise.pathsList.map((oneOfPaths) =>
154
459
  pProgress(async (progress) => {
@@ -157,329 +462,411 @@ if (cli.flags) {
157
462
  let amended = false;
158
463
  let finalContents = pathsPromise.contentsStr[oneOfPaths];
159
464
  let parsedContents = pathsPromise.contentsObj[oneOfPaths];
160
-
161
- let totalDeps = (
162
- isPlainObject(parsedContents.dependencies)
163
- ? Object.keys(parsedContents.dependencies)
164
- : []
165
- ).concat(
166
- isPlainObject(parsedContents.devDependencies)
167
- ? Object.keys(parsedContents.devDependencies)
168
- : [],
169
- );
170
-
171
- //
172
- //
173
- //
174
- //
175
- //
176
- //
177
- //
178
- // 1. LOOKUP OF ALL DEPS & DEV-DEPS ALL AT ONCE
179
- //
180
- //
181
- //
182
- //
183
- //
184
- //
185
- //
186
-
187
- // As dependency lookup is process-heavy and will take time, we need
188
- // to track it. The total progress of this single package we're processing
189
- // is divided 75% to compile new versions, 25% to write/skip
190
-
191
- // this is the first 75% of per-package progress
192
- // https://github.com/sindresorhus/p-progress#pprogressallpromises-options
193
-
194
- let compiledDepNameVersionPairs = {};
195
- let allProgressPromise2 = PProgress.all(
196
- totalDeps.map(async (singleDepName) => {
197
- if (pathsPromise.namesList.includes(singleDepName)) {
198
- compiledDepNameVersionPairs[singleDepName] =
465
+ let fileUpdates = {};
466
+
467
+ try {
468
+ let totalDeps = (
469
+ isPlainObject(parsedContents.dependencies)
470
+ ? Object.keys(parsedContents.dependencies)
471
+ : []
472
+ ).concat(
473
+ isPlainObject(parsedContents.devDependencies)
474
+ ? Object.keys(parsedContents.devDependencies)
475
+ : [],
476
+ );
477
+
478
+ //
479
+ //
480
+ //
481
+ //
482
+ //
483
+ //
484
+ //
485
+ // 1. LOOKUP OF ALL DEPS & DEV-DEPS ALL AT ONCE
486
+ //
487
+ //
488
+ //
489
+ //
490
+ //
491
+ //
492
+ //
493
+
494
+ // All external metadata was resolved before this processing phase, so
495
+ // no package can be written while another registry request is pending.
496
+ let compiledDepNameVersionPairs = {};
497
+ for (let singleDepName of totalDeps) {
498
+ let singleDepValue = Object.hasOwn(
499
+ parsedContents.dependencies ?? {},
500
+ singleDepName,
501
+ )
502
+ ? parsedContents.dependencies[singleDepName]
503
+ : parsedContents.devDependencies[singleDepName];
504
+ let parsedSpec = parseDependencySpec(singleDepName, singleDepValue);
505
+ if (pathsPromise.namesList.includes(parsedSpec.targetName)) {
506
+ let localVersion =
199
507
  pathsPromise.contentsObj[
200
- pathsPromise.pathsByName[singleDepName]
508
+ pathsPromise.pathsByName[parsedSpec.targetName]
201
509
  ].version;
202
- return;
203
- }
204
- try {
205
- await pacote
206
- .manifest(singleDepName, {
207
- fullMetadata: true,
208
- })
209
- .then((pkg1) => {
210
- if (pkg1.version === null) {
211
- throw new Error(
212
- `${messagePrefix}${singleDepName} version from npm came as null, CLI will exit now, nothing was written.`,
213
- );
214
- } else {
215
- compiledDepNameVersionPairs[singleDepName] = pkg1.version;
216
-
217
- if (
218
- (cli.flags.m || cli.flags.module) &&
219
- pkg1.type === "module"
220
- ) {
221
- newConfig.noMajorBumping.push(pkg1.name);
222
- }
223
- }
224
- });
225
- } catch (e) {
226
- // no response from npm
227
- compiledDepNameVersionPairs[singleDepName] = null;
228
- }
229
- }),
230
- );
231
- allProgressPromise2.onProgress((val) => {
232
- // console.log(
233
- // `197 ${`\u001b[${32}m${`CALL PROGRESS():`} ${val *
234
- // 0.75}\u001b[${39}m`}`
235
- // );
236
- progress(val * 0.75);
237
- });
238
- await allProgressPromise2;
239
-
240
- // Now we need to simultaneously query all the deps, dev and normal ones.
241
- // We rely on pacote's caching mechanism.
242
-
243
- // The plan is to query all the deps at once, then await the result,
244
- // then process received result, picking values we need from it.
245
-
246
- //
247
- //
248
- //
249
- //
250
- //
251
- //
252
- //
253
- // 2. DEPS
254
- //
255
- //
256
- //
257
- //
258
- //
259
- //
260
- //
261
-
262
- if (isPlainObject(parsedContents.dependencies)) {
263
- let keys = Object.keys(parsedContents.dependencies);
264
- for (let y = 0, len2 = keys.length; y < len2; y++) {
265
- // delete this dependency from lect.various.devDependencies if present
266
- // ---------------------
267
- if (
268
- objectPath.has(parsedContents, "lect.various.devDependencies") &&
269
- Array.isArray(parsedContents.lect.various.devDependencies) &&
270
- parsedContents.lect.various.devDependencies.includes(keys[y])
271
- ) {
272
- let foundIdx;
273
- let newVal = parsedContents.lect.various.devDependencies.filter(
274
- (dep, z) => {
275
- if (dep === keys[y]) {
276
- foundIdx = z;
277
- return false;
278
- }
279
- return true;
280
- },
281
- );
282
- parsedContents.lect.various.devDependencies = newVal;
283
- finalContents = del(
284
- finalContents,
285
- `lect.various.devDependencies.${foundIdx}`,
286
- );
510
+ compiledDepNameVersionPairs[singleDepName] =
511
+ typeof localVersion === "string" && localVersion.length > 0
512
+ ? localVersion
513
+ : null;
514
+ } else {
515
+ compiledDepNameVersionPairs[singleDepName] =
516
+ registryMetadata.get(parsedSpec.targetName)?.version ?? null;
287
517
  }
518
+ }
519
+ progress(0.75);
520
+
521
+ //
522
+ //
523
+ //
524
+ //
525
+ //
526
+ //
527
+ //
528
+ // 2. DEPS
529
+ //
530
+ //
531
+ //
532
+ //
533
+ //
534
+ //
535
+ //
288
536
 
289
- // tackle the deps list:
290
- // ---------------------
291
-
292
- let singleDepName = keys[y];
293
- let singleDepValue = parsedContents.dependencies[keys[y]];
294
- if (singleDepValue.startsWith("file:")) {
295
- continue;
296
- }
297
- let workspacePrefix = singleDepValue.startsWith("workspace:")
298
- ? "workspace:"
299
- : "";
300
-
301
- if (Array.isArray(newConfig?.pin) && newConfig.pin[singleDepName]) {
302
- finalContents = set(
303
- finalContents,
304
- `dependencies.${singleDepName}`,
305
- newConfig.pin[singleDepName],
306
- );
307
- amended = true;
537
+ if (isPlainObject(parsedContents.dependencies)) {
538
+ let keys = Object.keys(parsedContents.dependencies);
539
+ for (let y = 0, len2 = keys.length; y < len2; y++) {
540
+ // delete this dependency from lect.various.devDependencies if present
541
+ // ---------------------
308
542
  if (
309
- !Object.prototype.hasOwnProperty.call(
310
- updatedPackages,
311
- singleDepName,
312
- )
543
+ objectPath.has(
544
+ parsedContents,
545
+ "lect.various.devDependencies",
546
+ ) &&
547
+ Array.isArray(parsedContents.lect.various.devDependencies) &&
548
+ parsedContents.lect.various.devDependencies.includes(keys[y])
313
549
  ) {
314
- updatedPackages[singleDepName] = newConfig.pin[singleDepName];
550
+ let foundIdx;
551
+ let newVal = parsedContents.lect.various.devDependencies.filter(
552
+ (dep, z) => {
553
+ if (dep === keys[y]) {
554
+ foundIdx = z;
555
+ return false;
556
+ }
557
+ return true;
558
+ },
559
+ );
560
+ parsedContents.lect.various.devDependencies = newVal;
561
+ finalContents = deleteJsonValue(
562
+ finalContents,
563
+ `lect.various.devDependencies.${foundIdx}`,
564
+ );
565
+ amended = true;
315
566
  }
316
- } else if (
317
- compiledDepNameVersionPairs[singleDepName] !== null &&
318
- singleDepValue !==
319
- `${workspacePrefix}^${compiledDepNameVersionPairs[singleDepName]}` &&
320
- // either dependency is not blacklisted (so we don't care)
321
- (!newConfig.noMajorBumping.includes(singleDepName) ||
322
- // or it is blacklisted but the bump is within the same major semver digit
323
- major(compiledDepNameVersionPairs[singleDepName]) ===
324
- major(singleDepValue))
325
- ) {
326
- finalContents = set(
327
- finalContents,
328
- `dependencies.${singleDepName}`,
329
- `${workspacePrefix}^${compiledDepNameVersionPairs[singleDepName]}`,
567
+
568
+ // tackle the deps list:
569
+ // ---------------------
570
+
571
+ let singleDepName = keys[y];
572
+ let singleDepValue = parsedContents.dependencies[keys[y]];
573
+ if (singleDepValue.startsWith("file:")) {
574
+ continue;
575
+ }
576
+ let parsedSpec = parseDependencySpec(
577
+ singleDepName,
578
+ singleDepValue,
330
579
  );
331
- amended = true;
332
- if (
333
- !Object.prototype.hasOwnProperty.call(
334
- updatedPackages,
335
- singleDepName,
336
- )
337
- ) {
338
- updatedPackages[singleDepName] =
339
- compiledDepNameVersionPairs[singleDepName];
580
+ if (Object.hasOwn(newConfig.pin, singleDepName)) {
581
+ let nextSpec = pinnedDependencySpec(
582
+ parsedSpec,
583
+ singleDepValue,
584
+ newConfig.pin[singleDepName],
585
+ );
586
+ if (singleDepValue !== nextSpec) {
587
+ finalContents = setJsonValue(
588
+ finalContents,
589
+ `dependencies.${singleDepName}`,
590
+ nextSpec,
591
+ );
592
+ amended = true;
593
+ fileUpdates[singleDepName] = nextSpec;
594
+ }
595
+ } else if (compiledDepNameVersionPairs[singleDepName] !== null) {
596
+ let nextSpec = updatedDependencySpec(
597
+ parsedSpec,
598
+ singleDepValue,
599
+ compiledDepNameVersionPairs[singleDepName],
600
+ );
601
+ if (
602
+ singleDepValue !== nextSpec &&
603
+ // either dependency is not blacklisted (so we don't care)
604
+ (!newConfig.noMajorBumping.some((name) =>
605
+ [singleDepName, parsedSpec.targetName].includes(name),
606
+ ) ||
607
+ // or it is blacklisted but the bump is within the same major semver digit
608
+ major(compiledDepNameVersionPairs[singleDepName]) ===
609
+ major(parsedSpec.selector))
610
+ ) {
611
+ finalContents = setJsonValue(
612
+ finalContents,
613
+ `dependencies.${singleDepName}`,
614
+ nextSpec,
615
+ );
616
+ amended = true;
617
+ fileUpdates[singleDepName] =
618
+ compiledDepNameVersionPairs[singleDepName];
619
+ }
340
620
  }
341
- }
342
621
 
343
- // report progress
344
- // ---------------------
622
+ // report progress
623
+ // ---------------------
345
624
 
346
- // total: totalDeps, current chunk total: len2
347
- progress(0.75 + 0.24 * (y / totalDeps.length));
625
+ // total: totalDeps, current chunk total: len2
626
+ progress(0.75 + 0.24 * (y / totalDeps.length));
627
+ }
348
628
  }
349
- }
350
629
 
351
- //
352
- //
353
- //
354
- //
355
- //
356
- //
357
- //
358
- // 3. DEV-DEPS
359
- //
360
- //
361
- //
362
- //
363
- //
364
- //
365
- //
366
-
367
- if (isPlainObject(parsedContents.devDependencies)) {
368
- let keys = Object.keys(parsedContents.devDependencies);
369
- // 1. first, remove deps which if they are in normal dependencies in
370
- // package.json, that's our value parsedContents.dependencies
371
- if (isPlainObject(parsedContents.dependencies)) {
372
- Object.keys(parsedContents.dependencies).forEach((depName) => {
373
- if (keys.includes(depName)) {
374
- // 1. delete dev-dep entry on JSON string
375
- finalContents = del(
376
- finalContents,
377
- `devDependencies.${depName}`,
378
- );
379
- // 2. delete the dev-dep from parsedContents.devDependencies
380
- // key array which will be used to traverse in the loop later
381
- keys = keys.filter((val) => val !== depName);
382
- // 3. set the flag to activate the file write operation later
383
- amended = true;
384
- }
385
- });
386
- }
387
- for (let y = 0, len2 = keys.length; y < len2; y++) {
388
- let singleDepName = keys[y];
389
- let singleDepValue = parsedContents.devDependencies[keys[y]];
390
- if (singleDepValue.startsWith("file:")) {
391
- continue;
630
+ //
631
+ //
632
+ //
633
+ //
634
+ //
635
+ //
636
+ //
637
+ // 3. DEV-DEPS
638
+ //
639
+ //
640
+ //
641
+ //
642
+ //
643
+ //
644
+ //
645
+
646
+ if (isPlainObject(parsedContents.devDependencies)) {
647
+ let keys = Object.keys(parsedContents.devDependencies);
648
+ // 1. first, remove deps which if they are in normal dependencies in
649
+ // package.json, that's our value parsedContents.dependencies
650
+ if (isPlainObject(parsedContents.dependencies)) {
651
+ Object.keys(parsedContents.dependencies).forEach((depName) => {
652
+ if (keys.includes(depName)) {
653
+ // 1. delete dev-dep entry on JSON string
654
+ finalContents = deleteJsonValue(
655
+ finalContents,
656
+ `devDependencies.${depName}`,
657
+ );
658
+ // 2. delete the dev-dep from parsedContents.devDependencies
659
+ // key array which will be used to traverse in the loop later
660
+ keys = keys.filter((val) => val !== depName);
661
+ // 3. set the flag to activate the file write operation later
662
+ amended = true;
663
+ }
664
+ });
392
665
  }
393
- let workspacePrefix = singleDepValue.startsWith("workspace:")
394
- ? "workspace:"
395
- : "";
396
-
397
- if (Array.isArray(newConfig?.pin) && newConfig.pin[singleDepName]) {
398
- finalContents = set(
399
- finalContents,
400
- `dependencies.${singleDepName}`,
401
- newConfig.pin[singleDepName],
402
- );
403
- amended = true;
404
- if (
405
- !Object.prototype.hasOwnProperty.call(
406
- updatedPackages,
407
- singleDepName,
408
- )
409
- ) {
410
- updatedPackages[singleDepName] = newConfig.pin[singleDepName];
666
+ for (let y = 0, len2 = keys.length; y < len2; y++) {
667
+ let singleDepName = keys[y];
668
+ let singleDepValue = parsedContents.devDependencies[keys[y]];
669
+ if (singleDepValue.startsWith("file:")) {
670
+ continue;
411
671
  }
412
- } else if (
413
- compiledDepNameVersionPairs[singleDepName] !== null &&
414
- singleDepValue !==
415
- `${workspacePrefix}^${compiledDepNameVersionPairs[singleDepName]}` &&
416
- // either dependency is not blacklisted (so we don't care)
417
- (!newConfig.noMajorBumping.includes(singleDepName) ||
418
- // or it is blacklisted but the bump is within the same major semver digit
419
- major(compiledDepNameVersionPairs[singleDepName]) ===
420
- major(singleDepValue))
421
- ) {
422
- finalContents = set(
423
- finalContents,
424
- `devDependencies.${singleDepName}`,
425
- `${workspacePrefix}^${compiledDepNameVersionPairs[singleDepName]}`,
672
+ let parsedSpec = parseDependencySpec(
673
+ singleDepName,
674
+ singleDepValue,
426
675
  );
427
- amended = true;
428
-
429
- // update logging:
430
- if (
431
- !Object.prototype.hasOwnProperty.call(
432
- updatedPackages,
433
- singleDepName,
434
- )
435
- ) {
436
- updatedPackages[singleDepName] =
437
- `${compiledDepNameVersionPairs[singleDepName]}`;
676
+ if (Object.hasOwn(newConfig.pin, singleDepName)) {
677
+ let nextSpec = pinnedDependencySpec(
678
+ parsedSpec,
679
+ singleDepValue,
680
+ newConfig.pin[singleDepName],
681
+ );
682
+ if (singleDepValue !== nextSpec) {
683
+ finalContents = setJsonValue(
684
+ finalContents,
685
+ `devDependencies.${singleDepName}`,
686
+ nextSpec,
687
+ );
688
+ amended = true;
689
+ fileUpdates[singleDepName] = nextSpec;
690
+ }
691
+ } else if (compiledDepNameVersionPairs[singleDepName] !== null) {
692
+ let nextSpec = updatedDependencySpec(
693
+ parsedSpec,
694
+ singleDepValue,
695
+ compiledDepNameVersionPairs[singleDepName],
696
+ );
697
+ if (
698
+ singleDepValue !== nextSpec &&
699
+ // either dependency is not blacklisted (so we don't care)
700
+ (!newConfig.noMajorBumping.some((name) =>
701
+ [singleDepName, parsedSpec.targetName].includes(name),
702
+ ) ||
703
+ // or it is blacklisted but the bump is within the same major semver digit
704
+ major(compiledDepNameVersionPairs[singleDepName]) ===
705
+ major(parsedSpec.selector))
706
+ ) {
707
+ finalContents = setJsonValue(
708
+ finalContents,
709
+ `devDependencies.${singleDepName}`,
710
+ nextSpec,
711
+ );
712
+ amended = true;
713
+ fileUpdates[singleDepName] =
714
+ compiledDepNameVersionPairs[singleDepName];
715
+ }
438
716
  }
439
- }
440
717
 
441
- progress(
442
- 0.75 + 0.24 * ((totalDeps.length - len2 + y) / totalDeps.length),
443
- );
718
+ progress(
719
+ 0.75 +
720
+ 0.24 * ((totalDeps.length - len2 + y) / totalDeps.length),
721
+ );
722
+ }
444
723
  }
445
- }
446
724
 
447
- if (
448
- isPlainObject(parsedContents) &&
449
- Object.prototype.hasOwnProperty.call(parsedContents, "gitHead")
450
- ) {
451
- finalContents = del(finalContents, "gitHead");
725
+ if (Object.hasOwn(parsedContents, "gitHead")) {
726
+ finalContents = deleteJsonValue(finalContents, "gitHead");
727
+ amended = true;
728
+ }
729
+ } catch (error) {
730
+ progress(1);
731
+ return {
732
+ error: makeFailure("package transform", oneOfPaths, error),
733
+ path: oneOfPaths,
734
+ };
452
735
  }
453
736
 
454
737
  if (amended) {
455
738
  try {
456
- await write(oneOfPaths, finalContents);
457
- } catch (e) {
458
- console.error(
459
- `${messagePrefix}error happened when writing package.json:\n${e}`,
460
- );
739
+ await writeTextFile(path.join(cwd, oneOfPaths), finalContents);
740
+ } catch (error) {
741
+ progress(1);
742
+ return {
743
+ error: makeFailure("package write", oneOfPaths, error),
744
+ path: oneOfPaths,
745
+ };
461
746
  }
747
+ progress(1);
748
+ return { path: oneOfPaths, status: "updated", updates: fileUpdates };
462
749
  }
750
+
751
+ progress(1);
752
+ return { path: oneOfPaths, status: "unchanged", updates: fileUpdates };
463
753
  }),
464
754
  ),
465
755
  );
466
756
 
467
- allProgressPromise.onProgress((val) =>
468
- diff.write(
469
- val === 1
470
- ? `${messagePrefix}${
471
- Object.keys(updatedPackages).length
472
- ? `all updated:\n${printUpdated()}`
757
+ let diff;
758
+ if (reportProgress) {
759
+ diff = diff1();
760
+ allProgressPromise.onProgress((val) => {
761
+ if (val < 1) {
762
+ diff.write(`${messagePrefix}${Math.floor(val * 100)}% done`);
763
+ }
764
+ });
765
+ diff.pipe(process.stdout);
766
+ }
767
+
768
+ let processingResults = await allProgressPromise;
769
+ let processingFailures = [];
770
+ let unchangedFiles = [];
771
+ let updatedFiles = [];
772
+
773
+ for (let result of processingResults) {
774
+ if (result.error) {
775
+ processingFailures.push(result.error);
776
+ } else if (result.status === "updated") {
777
+ updatedFiles.push(result.path);
778
+ for (let [name, version] of Object.entries(result.updates)) {
779
+ if (!Object.hasOwn(updatedPackages, name)) {
780
+ updatedPackages[name] = version;
781
+ }
782
+ }
783
+ } else {
784
+ unchangedFiles.push(result.path);
785
+ }
786
+ }
787
+
788
+ if (diff) {
789
+ if (processingFailures.length > 0) {
790
+ diff.write(
791
+ `${messagePrefix}completed with ${processingFailures.length} ${
792
+ processingFailures.length === 1 ? "failure" : "failures"
793
+ }; ${updatedFiles.length} updated, ${unchangedFiles.length} unchanged${
794
+ Object.keys(updatedPackages).length ? `:\n${printUpdated()}` : ""
795
+ }`,
796
+ );
797
+ } else {
798
+ diff.write(
799
+ `${messagePrefix}${
800
+ updatedFiles.length > 0 && Object.keys(updatedPackages).length
801
+ ? `all updated:\n${printUpdated()}`
802
+ : updatedFiles.length > 0
803
+ ? `${updatedFiles.length} package.json ${
804
+ updatedFiles.length === 1 ? "file" : "files"
805
+ } updated (metadata cleanup only)`
473
806
  : "everything was already up-to-date"
474
- }`
475
- : `${messagePrefix}${Math.floor(val * 100)}% ${
476
- Object.keys(updatedPackages).length
477
- ? `updated:\n${printUpdated()}`
478
- : "done"
479
- }`,
480
- ),
481
- );
482
- diff.pipe(process.stdout);
807
+ }`,
808
+ );
809
+ }
810
+ diff.end();
811
+ }
812
+
813
+ if (processingFailures.length > 0) {
814
+ throw new UpdateVersionsError(processingFailures, {
815
+ unchangedFiles,
816
+ updatedFiles,
817
+ updatedPackages,
818
+ });
819
+ }
820
+
821
+ return updatedPackages;
822
+ }
823
+
824
+ async function runCli() {
825
+ const cli = parseCli();
826
+
827
+ // Honour help/version even when another argument is also present. codsenCLI
828
+ // handles either flag automatically when it is the sole argument.
829
+ if (cli.flags.version) {
830
+ log(pkg.version);
831
+ return;
832
+ }
833
+ if (cli.flags.help) {
834
+ log(cli.help);
835
+ return;
836
+ }
483
837
 
484
- await allProgressPromise;
485
- })();
838
+ await updateVersions({
839
+ moduleMode: Boolean(cli.flags.module),
840
+ reportProgress: true,
841
+ });
842
+ updateNotifier({ pkg }).notify();
843
+ }
844
+
845
+ function isDirectExecution() {
846
+ if (!process.argv[1]) {
847
+ return false;
848
+ }
849
+ try {
850
+ return realpathSync(process.argv[1]) === fileURLToPath(import.meta.url);
851
+ } catch (_e) {
852
+ return false;
853
+ }
854
+ }
855
+
856
+ if (isDirectExecution()) {
857
+ runCli().catch((error) => {
858
+ let details =
859
+ error instanceof AggregateError
860
+ ? error.errors
861
+ .map(
862
+ (failure) =>
863
+ `\n- [${failure.phase ?? "unknown"}] ${failure.path ?? "unknown"}: ${failure.cause?.message ?? failure.message}`,
864
+ )
865
+ .join("")
866
+ : "";
867
+ console.error(
868
+ `\n${messagePrefix}${`\u001b[${31}m${error.message}${details}\u001b[${39}m`}\n`,
869
+ );
870
+ process.exitCode = 1;
871
+ });
872
+ }