update-versions 7.1.3 → 7.2.1

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