update-versions 7.2.4 → 7.2.5

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/CHANGELOG.md CHANGED
@@ -3,6 +3,13 @@
3
3
  All notable changes to this project will be documented in this file.
4
4
  See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
5
5
 
6
+ ## 7.2.5 (2026-09-06)
7
+
8
+ ### Bug Fixes
9
+
10
+ - **update-versions:** remove p-reduce from package inventory ([22fbf9a](https://github.com/codsen/codsen/commit/22fbf9afe447a3b33c8c6aaee47a28769b7f5f6c))
11
+ - **update-versions:** replace external update notifier ([aee67ab](https://github.com/codsen/codsen/commit/aee67abd0b123e73a89003af55a4ffea04f46881))
12
+
6
13
  ## 7.2.0 (2026-08-19)
7
14
 
8
15
  ### Bug Fixes
package/README.md CHANGED
@@ -35,7 +35,7 @@ upd
35
35
 
36
36
  ## Documentation
37
37
 
38
- Please [visit codsen.com](https://codsen.com/os/update-versions/) for a full description of the API. If you’re looking for the **Changelog**, it’s [here](https://github.com/codsen/codsen/blob/main/packages/update-versions/CHANGELOG.md).
38
+ Please [visit codsen.com](https://codsen.com/os/update-versions/) for a full description of the API. For **Changelog**, see [raw md on GitHub](https://github.com/codsen/codsen/blob/main/packages/update-versions/CHANGELOG.md) or [the website](https://codsen.com/os/update-versions/changelog).
39
39
 
40
40
  ## Contributing
41
41
 
@@ -0,0 +1,793 @@
1
+ // This is the canonical source. `lect` copies it verbatim into every CLI.
2
+
3
+ import { spawn } from "node:child_process";
4
+ import {
5
+ mkdirSync,
6
+ readFileSync,
7
+ renameSync,
8
+ statSync,
9
+ unlinkSync,
10
+ writeFileSync,
11
+ } from "node:fs";
12
+ import { homedir } from "node:os";
13
+ import path from "node:path";
14
+ import { fileURLToPath } from "node:url";
15
+
16
+ const CHECK_INTERVAL = 24 * 60 * 60 * 1000;
17
+ const FAILURE_BACKOFF = 60 * 60 * 1000;
18
+ const FETCH_TIMEOUT = 10 * 1000;
19
+ const LOCK_STALE_AFTER = FETCH_TIMEOUT + 20 * 1000;
20
+ const MAX_JSON_BYTES = 64 * 1024;
21
+ const MODULE_FILENAME = fileURLToPath(import.meta.url);
22
+ const NOTIFICATION_LEASE = 60 * 60 * 1000;
23
+ const STATE_SCHEMA_VERSION = 1;
24
+ const WORKER_FLAG = "--codsen-update-check-worker";
25
+
26
+ const SEMVER_PATTERN =
27
+ /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$/;
28
+ const PACKAGE_NAME_PART = /^[a-z0-9][a-z0-9._~-]*$/;
29
+
30
+ function parseSemver(version) {
31
+ if (typeof version !== "string") {
32
+ return null;
33
+ }
34
+ const match = SEMVER_PATTERN.exec(version);
35
+ if (!match) {
36
+ return null;
37
+ }
38
+ const prerelease = match[4] ? match[4].split(".") : [];
39
+ if (
40
+ prerelease.some(
41
+ (identifier) => /^\d+$/.test(identifier) && /^0\d+/.test(identifier),
42
+ )
43
+ ) {
44
+ return null;
45
+ }
46
+ return {
47
+ core: [match[1], match[2], match[3]],
48
+ prerelease,
49
+ };
50
+ }
51
+
52
+ function compareNumericStrings(left, right) {
53
+ if (left.length !== right.length) {
54
+ return left.length < right.length ? -1 : 1;
55
+ }
56
+ if (left === right) {
57
+ return 0;
58
+ }
59
+ return left < right ? -1 : 1;
60
+ }
61
+
62
+ function compareSemver(leftVersion, rightVersion) {
63
+ const left = parseSemver(leftVersion);
64
+ const right = parseSemver(rightVersion);
65
+ if (!left || !right) {
66
+ return null;
67
+ }
68
+ for (let index = 0; index < left.core.length; index += 1) {
69
+ const comparison = compareNumericStrings(
70
+ left.core[index],
71
+ right.core[index],
72
+ );
73
+ if (comparison) {
74
+ return comparison;
75
+ }
76
+ }
77
+ if (!left.prerelease.length || !right.prerelease.length) {
78
+ if (left.prerelease.length === right.prerelease.length) {
79
+ return 0;
80
+ }
81
+ return left.prerelease.length ? -1 : 1;
82
+ }
83
+ const length = Math.max(left.prerelease.length, right.prerelease.length);
84
+ for (let index = 0; index < length; index += 1) {
85
+ const leftIdentifier = left.prerelease[index];
86
+ const rightIdentifier = right.prerelease[index];
87
+ if (leftIdentifier === undefined || rightIdentifier === undefined) {
88
+ return leftIdentifier === undefined ? -1 : 1;
89
+ }
90
+ if (leftIdentifier === rightIdentifier) {
91
+ continue;
92
+ }
93
+ const leftIsNumeric = /^\d+$/.test(leftIdentifier);
94
+ const rightIsNumeric = /^\d+$/.test(rightIdentifier);
95
+ if (leftIsNumeric && rightIsNumeric) {
96
+ return compareNumericStrings(leftIdentifier, rightIdentifier);
97
+ }
98
+ if (leftIsNumeric !== rightIsNumeric) {
99
+ return leftIsNumeric ? -1 : 1;
100
+ }
101
+ return leftIdentifier < rightIdentifier ? -1 : 1;
102
+ }
103
+ return 0;
104
+ }
105
+
106
+ function isValidPackageName(packageName) {
107
+ if (
108
+ typeof packageName !== "string" ||
109
+ !packageName ||
110
+ packageName.length > 214
111
+ ) {
112
+ return false;
113
+ }
114
+ if (!packageName.startsWith("@")) {
115
+ return PACKAGE_NAME_PART.test(packageName);
116
+ }
117
+ const parts = packageName.slice(1).split("/");
118
+ return (
119
+ parts.length === 2 && parts.every((part) => PACKAGE_NAME_PART.test(part))
120
+ );
121
+ }
122
+
123
+ function validTimestamp(value) {
124
+ return Number.isSafeInteger(value) && value >= 0 ? value : 0;
125
+ }
126
+
127
+ function initialState(now, legacy = {}) {
128
+ const legacyCheck = validTimestamp(legacy.lastUpdateCheck);
129
+ return {
130
+ schemaVersion: STATE_SCHEMA_VERSION,
131
+ createdAt: legacyCheck || now,
132
+ lastAttempt: 0,
133
+ lastSuccess: 0,
134
+ latestVersion: parseSemver(legacy.latestVersion)
135
+ ? legacy.latestVersion
136
+ : null,
137
+ lastNotification: null,
138
+ pendingNotification: null,
139
+ };
140
+ }
141
+
142
+ function normalizeState(value, now) {
143
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
144
+ return null;
145
+ }
146
+ const latestVersion = parseSemver(value.latestVersion)
147
+ ? value.latestVersion
148
+ : null;
149
+ const notification = value.lastNotification;
150
+ const lastNotification =
151
+ notification &&
152
+ typeof notification === "object" &&
153
+ parseSemver(notification.current) &&
154
+ parseSemver(notification.latest)
155
+ ? {
156
+ current: notification.current,
157
+ latest: notification.latest,
158
+ at: validTimestamp(notification.at),
159
+ }
160
+ : null;
161
+ const pending = value.pendingNotification;
162
+ const pendingNotification =
163
+ pending &&
164
+ typeof pending === "object" &&
165
+ parseSemver(pending.current) &&
166
+ parseSemver(pending.latest) &&
167
+ typeof pending.token === "string" &&
168
+ pending.token.length <= 256
169
+ ? {
170
+ current: pending.current,
171
+ latest: pending.latest,
172
+ token: pending.token,
173
+ at: validTimestamp(pending.at),
174
+ }
175
+ : null;
176
+ return {
177
+ schemaVersion: STATE_SCHEMA_VERSION,
178
+ createdAt: validTimestamp(value.createdAt) || now,
179
+ lastAttempt: validTimestamp(value.lastAttempt),
180
+ lastSuccess: validTimestamp(value.lastSuccess),
181
+ latestVersion,
182
+ lastNotification,
183
+ pendingNotification,
184
+ };
185
+ }
186
+
187
+ function readSmallJson(filename) {
188
+ if (!filename) {
189
+ return null;
190
+ }
191
+ try {
192
+ const stats = statSync(filename);
193
+ if (!stats.isFile() || stats.size > MAX_JSON_BYTES) {
194
+ return null;
195
+ }
196
+ return JSON.parse(readFileSync(filename, "utf8"));
197
+ } catch {
198
+ return null;
199
+ }
200
+ }
201
+
202
+ function readState(filename, now) {
203
+ return normalizeState(readSmallJson(filename), now);
204
+ }
205
+
206
+ function writeState(filename, state) {
207
+ let temporaryFilename;
208
+ try {
209
+ mkdirSync(path.dirname(filename), { mode: 0o700, recursive: true });
210
+ temporaryFilename = `${filename}.${process.pid}.${Date.now().toString(36)}.${Math.random().toString(36).slice(2)}.tmp`;
211
+ writeFileSync(temporaryFilename, `${JSON.stringify(state, null, 2)}\n`, {
212
+ encoding: "utf8",
213
+ flag: "wx",
214
+ mode: 0o600,
215
+ });
216
+ renameSync(temporaryFilename, filename);
217
+ return true;
218
+ } catch {
219
+ if (temporaryFilename) {
220
+ try {
221
+ unlinkSync(temporaryFilename);
222
+ } catch {}
223
+ }
224
+ return false;
225
+ }
226
+ }
227
+
228
+ function acquireLock(lockFile, now, staleAfter = LOCK_STALE_AFTER) {
229
+ try {
230
+ mkdirSync(path.dirname(lockFile), { mode: 0o700, recursive: true });
231
+ } catch {
232
+ return false;
233
+ }
234
+ const token = `${process.pid}:${Date.now()}:${Math.random()}`;
235
+ for (let attempt = 0; attempt < 2; attempt += 1) {
236
+ try {
237
+ writeFileSync(lockFile, token, {
238
+ encoding: "utf8",
239
+ flag: "wx",
240
+ mode: 0o600,
241
+ });
242
+ return token;
243
+ } catch (error) {
244
+ if (error?.code !== "EEXIST") {
245
+ return false;
246
+ }
247
+ try {
248
+ const observedToken = readFileSync(lockFile, "utf8");
249
+ const observedStats = statSync(lockFile);
250
+ const age = now - observedStats.mtimeMs;
251
+ if (!Number.isFinite(age) || age <= staleAfter) {
252
+ return false;
253
+ }
254
+ const currentStats = statSync(lockFile);
255
+ if (
256
+ readFileSync(lockFile, "utf8") !== observedToken ||
257
+ currentStats.dev !== observedStats.dev ||
258
+ currentStats.ino !== observedStats.ino ||
259
+ currentStats.mtimeMs !== observedStats.mtimeMs
260
+ ) {
261
+ return false;
262
+ }
263
+ if (readFileSync(lockFile, "utf8") !== observedToken) {
264
+ return false;
265
+ }
266
+ unlinkSync(lockFile);
267
+ } catch {
268
+ return false;
269
+ }
270
+ }
271
+ }
272
+ return false;
273
+ }
274
+
275
+ function releaseLock(lockFile, token) {
276
+ try {
277
+ if (readFileSync(lockFile, "utf8") === token) {
278
+ unlinkSync(lockFile);
279
+ }
280
+ } catch {}
281
+ }
282
+
283
+ function updateStateWithLock(
284
+ paths,
285
+ now,
286
+ update,
287
+ staleAfter = LOCK_STALE_AFTER,
288
+ ) {
289
+ const lockToken = acquireLock(paths.lockFile, now, staleAfter);
290
+ if (!lockToken) {
291
+ return null;
292
+ }
293
+ try {
294
+ const existingState = readState(paths.cacheFile, now);
295
+ const nextState = update(existingState || initialState(now), {
296
+ exists: Boolean(existingState),
297
+ });
298
+ return nextState && writeState(paths.cacheFile, nextState)
299
+ ? nextState
300
+ : null;
301
+ } finally {
302
+ releaseLock(paths.lockFile, lockToken);
303
+ }
304
+ }
305
+
306
+ function resolveCacheRoot(env, home, platform) {
307
+ if (
308
+ typeof env.XDG_CACHE_HOME === "string" &&
309
+ path.isAbsolute(env.XDG_CACHE_HOME)
310
+ ) {
311
+ return env.XDG_CACHE_HOME;
312
+ }
313
+ if (platform === "win32") {
314
+ if (
315
+ typeof env.LOCALAPPDATA === "string" &&
316
+ path.isAbsolute(env.LOCALAPPDATA)
317
+ ) {
318
+ return env.LOCALAPPDATA;
319
+ }
320
+ return home && path.isAbsolute(home)
321
+ ? path.join(home, "AppData", "Local")
322
+ : null;
323
+ }
324
+ if (!home || !path.isAbsolute(home)) {
325
+ return null;
326
+ }
327
+ return platform === "darwin"
328
+ ? path.join(home, "Library", "Caches")
329
+ : path.join(home, ".cache");
330
+ }
331
+
332
+ function resolvePaths(packageName, runtime = {}) {
333
+ const env = runtime.env ?? process.env;
334
+ const home = runtime.home ?? homedir();
335
+ const platform = runtime.platform ?? process.platform;
336
+ const cacheRoot = runtime.cacheRoot ?? resolveCacheRoot(env, home, platform);
337
+ if (!cacheRoot || !path.isAbsolute(cacheRoot)) {
338
+ return null;
339
+ }
340
+ const encodedName = Buffer.from(packageName).toString("base64url");
341
+ const directory = path.join(cacheRoot, "codsen", "update-notifier");
342
+ const cacheFile =
343
+ runtime.cacheFile ?? path.join(directory, `${encodedName}.json`);
344
+ const lockFile = runtime.lockFile ?? `${cacheFile}.lock`;
345
+ const configRoot =
346
+ env.XDG_CONFIG_HOME || (home && path.join(home, ".config"));
347
+ const legacyConfigFile =
348
+ "legacyConfigFile" in runtime
349
+ ? runtime.legacyConfigFile
350
+ : configRoot &&
351
+ path.join(
352
+ configRoot,
353
+ "configstore",
354
+ `update-notifier-${packageName}.json`,
355
+ );
356
+ return { cacheFile, legacyConfigFile, lockFile };
357
+ }
358
+
359
+ function readLegacyConfig(filename) {
360
+ const value = readSmallJson(filename);
361
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
362
+ return {};
363
+ }
364
+ return {
365
+ optOut: Boolean(value.optOut),
366
+ lastUpdateCheck: validTimestamp(value.lastUpdateCheck),
367
+ latestVersion: value.update?.latest,
368
+ };
369
+ }
370
+
371
+ function isCi(env) {
372
+ return (
373
+ env.CI !== "0" &&
374
+ env.CI !== "false" &&
375
+ ("CI" in env ||
376
+ "CONTINUOUS_INTEGRATION" in env ||
377
+ Object.keys(env).some((key) => key.startsWith("CI_")))
378
+ );
379
+ }
380
+
381
+ function isPackageManagerInvocation(env) {
382
+ const userAgent = env.npm_config_user_agent;
383
+ return (
384
+ (typeof env.npm_package_json === "string" &&
385
+ env.npm_package_json.endsWith("package.json")) ||
386
+ (typeof userAgent === "string" &&
387
+ ["npm", "yarn", "pnpm", "bun"].some((name) => userAgent.startsWith(name)))
388
+ );
389
+ }
390
+
391
+ function isDisabled({
392
+ argv,
393
+ enabled,
394
+ env,
395
+ packageName,
396
+ packageVersion,
397
+ stderr,
398
+ }) {
399
+ return (
400
+ enabled === false ||
401
+ !isValidPackageName(packageName) ||
402
+ !parseSemver(packageVersion) ||
403
+ stderr?.isTTY !== true ||
404
+ "NO_UPDATE_NOTIFIER" in env ||
405
+ env.NODE_ENV === "test" ||
406
+ argv.includes("--no-update-notifier") ||
407
+ isCi(env) ||
408
+ isPackageManagerInvocation(env)
409
+ );
410
+ }
411
+
412
+ function isCheckDue(state, now, options = {}) {
413
+ const checkInterval = options.checkInterval ?? CHECK_INTERVAL;
414
+ const failureBackoff = options.failureBackoff ?? FAILURE_BACKOFF;
415
+ const successOrCreation = state.lastSuccess || state.createdAt;
416
+ return (
417
+ now - successOrCreation >= checkInterval &&
418
+ (!state.lastAttempt || now - state.lastAttempt >= failureBackoff)
419
+ );
420
+ }
421
+
422
+ function isNotificationDue(state, currentVersion) {
423
+ const latestVersion = state.latestVersion;
424
+ if (compareSemver(latestVersion, currentVersion) !== 1) {
425
+ return false;
426
+ }
427
+ return !(
428
+ state.lastNotification?.current === currentVersion &&
429
+ state.lastNotification?.latest === latestVersion
430
+ );
431
+ }
432
+
433
+ function formatCliUpdateNotification({
434
+ currentVersion,
435
+ latestVersion,
436
+ packageName,
437
+ }) {
438
+ return `\nUpdate available for ${packageName}: ${currentVersion} → ${latestVersion}\nhttps://www.npmjs.com/package/${encodeURIComponent(packageName)}\n\n`;
439
+ }
440
+
441
+ function hasActiveNotificationReservation(
442
+ state,
443
+ currentVersion,
444
+ latestVersion,
445
+ now,
446
+ lease,
447
+ ) {
448
+ const pending = state.pendingNotification;
449
+ return (
450
+ pending?.current === currentVersion &&
451
+ pending.latest === latestVersion &&
452
+ now - pending.at < lease
453
+ );
454
+ }
455
+
456
+ function registerNotification({
457
+ currentVersion,
458
+ now,
459
+ onExit,
460
+ packageName,
461
+ paths,
462
+ reservationToken,
463
+ staleAfter,
464
+ stderr,
465
+ }) {
466
+ const finish = (code) => {
467
+ const notificationTime = now();
468
+ updateStateWithLock(
469
+ paths,
470
+ notificationTime,
471
+ (state) => {
472
+ if (state.pendingNotification?.token !== reservationToken) {
473
+ return null;
474
+ }
475
+ if (code !== 0 || stderr?.isTTY !== true) {
476
+ return { ...state, pendingNotification: null };
477
+ }
478
+ const notificationLatest = state.latestVersion;
479
+ if (compareSemver(notificationLatest, currentVersion) !== 1) {
480
+ return { ...state, pendingNotification: null };
481
+ }
482
+ try {
483
+ stderr.write(
484
+ formatCliUpdateNotification({
485
+ currentVersion,
486
+ latestVersion: notificationLatest,
487
+ packageName,
488
+ }),
489
+ );
490
+ } catch {
491
+ return { ...state, pendingNotification: null };
492
+ }
493
+ return {
494
+ ...state,
495
+ lastNotification: {
496
+ current: currentVersion,
497
+ latest: notificationLatest,
498
+ at: notificationTime,
499
+ },
500
+ pendingNotification: null,
501
+ };
502
+ },
503
+ staleAfter,
504
+ );
505
+ };
506
+ try {
507
+ onExit(finish);
508
+ return true;
509
+ } catch {
510
+ finish(1);
511
+ return false;
512
+ }
513
+ }
514
+
515
+ function spawnWorker(packageName, attemptToken, spawnImpl = spawn) {
516
+ try {
517
+ const child = spawnImpl(
518
+ process.execPath,
519
+ [MODULE_FILENAME, WORKER_FLAG, packageName, String(attemptToken)],
520
+ {
521
+ detached: true,
522
+ stdio: "ignore",
523
+ windowsHide: true,
524
+ },
525
+ );
526
+ child.once?.("error", () => {});
527
+ child.unref?.();
528
+ return true;
529
+ } catch {
530
+ return false;
531
+ }
532
+ }
533
+
534
+ function notifyOfCliUpdate({ enabled = true, pkg } = {}, runtime = {}) {
535
+ try {
536
+ const argv = runtime.argv ?? process.argv.slice(2);
537
+ const env = runtime.env ?? process.env;
538
+ const stderr = runtime.stderr ?? process.stderr;
539
+ const packageName = pkg?.name;
540
+ const packageVersion = pkg?.version;
541
+ if (
542
+ isDisabled({
543
+ argv,
544
+ enabled,
545
+ env,
546
+ packageName,
547
+ packageVersion,
548
+ stderr,
549
+ })
550
+ ) {
551
+ return false;
552
+ }
553
+ const paths = resolvePaths(packageName, runtime);
554
+ if (!paths) {
555
+ return false;
556
+ }
557
+ const legacy = readLegacyConfig(paths.legacyConfigFile);
558
+ if (legacy.optOut) {
559
+ return false;
560
+ }
561
+ const now = runtime.now ?? Date.now;
562
+ const currentTime = now();
563
+ const staleAfter = runtime.lockStaleAfter ?? LOCK_STALE_AFTER;
564
+ let state = readState(paths.cacheFile, currentTime);
565
+ if (!state) {
566
+ state = updateStateWithLock(
567
+ paths,
568
+ currentTime,
569
+ (current, { exists }) =>
570
+ exists ? current : initialState(currentTime, legacy),
571
+ staleAfter,
572
+ );
573
+ if (!state) {
574
+ return false;
575
+ }
576
+ }
577
+ if (isNotificationDue(state, packageVersion)) {
578
+ const reservationToken = `${process.pid}:${currentTime}:${Math.random()}`;
579
+ const claimedState = updateStateWithLock(
580
+ paths,
581
+ currentTime,
582
+ (current) => {
583
+ const currentLatest = current.latestVersion;
584
+ return isNotificationDue(current, packageVersion) &&
585
+ !hasActiveNotificationReservation(
586
+ current,
587
+ packageVersion,
588
+ currentLatest,
589
+ currentTime,
590
+ runtime.notificationLease ?? NOTIFICATION_LEASE,
591
+ )
592
+ ? {
593
+ ...current,
594
+ pendingNotification: {
595
+ current: packageVersion,
596
+ latest: currentLatest,
597
+ token: reservationToken,
598
+ at: currentTime,
599
+ },
600
+ }
601
+ : null;
602
+ },
603
+ staleAfter,
604
+ );
605
+ if (!claimedState) {
606
+ return false;
607
+ }
608
+ return registerNotification({
609
+ currentVersion: packageVersion,
610
+ now,
611
+ onExit:
612
+ runtime.onExit ?? ((listener) => process.once("exit", listener)),
613
+ packageName,
614
+ paths,
615
+ reservationToken,
616
+ staleAfter,
617
+ stderr,
618
+ });
619
+ }
620
+ if (
621
+ !isCheckDue(state, currentTime, {
622
+ checkInterval: runtime.checkInterval,
623
+ failureBackoff: runtime.failureBackoff,
624
+ })
625
+ ) {
626
+ return false;
627
+ }
628
+ const scheduledState = updateStateWithLock(
629
+ paths,
630
+ currentTime,
631
+ (current) =>
632
+ isCheckDue(current, currentTime, {
633
+ checkInterval: runtime.checkInterval,
634
+ failureBackoff: runtime.failureBackoff,
635
+ })
636
+ ? { ...current, lastAttempt: currentTime }
637
+ : null,
638
+ staleAfter,
639
+ );
640
+ if (!scheduledState) {
641
+ return false;
642
+ }
643
+ const startWorker = runtime.spawnWorker ?? spawnWorker;
644
+ return Boolean(startWorker(packageName, currentTime, runtime.spawnImpl));
645
+ } catch {
646
+ return false;
647
+ }
648
+ }
649
+
650
+ async function readLimitedBody(response) {
651
+ const contentLength = Number(response.headers?.get?.("content-length"));
652
+ if (Number.isFinite(contentLength) && contentLength > MAX_JSON_BYTES) {
653
+ return null;
654
+ }
655
+ if (!response.body?.getReader) {
656
+ const text = await response.text();
657
+ return Buffer.byteLength(text) <= MAX_JSON_BYTES ? text : null;
658
+ }
659
+ const reader = response.body.getReader();
660
+ const chunks = [];
661
+ let total = 0;
662
+ try {
663
+ while (true) {
664
+ const { done, value } = await reader.read();
665
+ if (done) {
666
+ break;
667
+ }
668
+ const chunk = Buffer.from(value);
669
+ total += chunk.length;
670
+ if (total > MAX_JSON_BYTES) {
671
+ await reader.cancel();
672
+ return null;
673
+ }
674
+ chunks.push(chunk);
675
+ }
676
+ } finally {
677
+ reader.releaseLock();
678
+ }
679
+ return Buffer.concat(chunks, total).toString("utf8");
680
+ }
681
+
682
+ async function fetchLatestVersion(packageName, runtime = {}) {
683
+ const fetchImpl = runtime.fetchImpl ?? globalThis.fetch;
684
+ if (typeof fetchImpl !== "function") {
685
+ return null;
686
+ }
687
+ const timeoutMs = runtime.fetchTimeout ?? FETCH_TIMEOUT;
688
+ const controller = new AbortController();
689
+ let timeout;
690
+ const request = Promise.resolve()
691
+ .then(async () => {
692
+ const response = await fetchImpl(
693
+ `https://registry.npmjs.org/${encodeURIComponent(packageName)}/latest`,
694
+ {
695
+ headers: { accept: "application/json" },
696
+ redirect: "follow",
697
+ signal: controller.signal,
698
+ },
699
+ );
700
+ if (!response?.ok) {
701
+ return null;
702
+ }
703
+ const body = await readLimitedBody(response);
704
+ if (body === null) {
705
+ return null;
706
+ }
707
+ const version = JSON.parse(body).version;
708
+ return parseSemver(version) ? version : null;
709
+ })
710
+ .catch(() => null);
711
+ const timedOut = new Promise((resolve) => {
712
+ timeout = setTimeout(() => {
713
+ controller.abort();
714
+ resolve(null);
715
+ }, timeoutMs);
716
+ });
717
+ try {
718
+ return await Promise.race([request, timedOut]);
719
+ } finally {
720
+ clearTimeout(timeout);
721
+ }
722
+ }
723
+
724
+ async function runUpdateCheck(packageName, runtime = {}) {
725
+ try {
726
+ if (!isValidPackageName(packageName)) {
727
+ return false;
728
+ }
729
+ const paths = resolvePaths(packageName, runtime);
730
+ if (!paths) {
731
+ return false;
732
+ }
733
+ const latestVersion = await fetchLatestVersion(packageName, runtime);
734
+ if (!latestVersion) {
735
+ return false;
736
+ }
737
+ const now = runtime.now ?? Date.now;
738
+ const currentTime = now();
739
+ return Boolean(
740
+ updateStateWithLock(
741
+ paths,
742
+ currentTime,
743
+ (state) =>
744
+ runtime.expectedAttempt !== undefined &&
745
+ state.lastAttempt !== runtime.expectedAttempt
746
+ ? null
747
+ : {
748
+ ...state,
749
+ lastSuccess: currentTime,
750
+ latestVersion,
751
+ },
752
+ runtime.lockStaleAfter,
753
+ ),
754
+ );
755
+ } catch {
756
+ return false;
757
+ }
758
+ }
759
+
760
+ function isWorkerInvocation(argv = process.argv) {
761
+ return (
762
+ argv[1] &&
763
+ path.resolve(argv[1]) === path.resolve(MODULE_FILENAME) &&
764
+ argv[2] === WORKER_FLAG
765
+ );
766
+ }
767
+
768
+ if (isWorkerInvocation()) {
769
+ const attemptToken = Number(process.argv[4]);
770
+ await runUpdateCheck(process.argv[3], {
771
+ expectedAttempt: Number.isSafeInteger(attemptToken)
772
+ ? attemptToken
773
+ : undefined,
774
+ });
775
+ }
776
+
777
+ export {
778
+ compareSemver,
779
+ fetchLatestVersion,
780
+ formatCliUpdateNotification,
781
+ isCheckDue,
782
+ isCi,
783
+ isDisabled,
784
+ isNotificationDue,
785
+ isPackageManagerInvocation,
786
+ notifyOfCliUpdate,
787
+ parseSemver,
788
+ readLegacyConfig,
789
+ readState,
790
+ resolveCacheRoot,
791
+ resolvePaths,
792
+ runUpdateCheck,
793
+ };
package/cli.js CHANGED
@@ -13,9 +13,13 @@ import { codsenCLI, isPlainObject } from "codsen-utils";
13
13
  import { del, set } from "edit-package-json";
14
14
  import objectPath from "object-path";
15
15
  import pProgress, { PProgress } from "p-progress";
16
- import pReduce from "p-reduce";
17
16
  import packageJson from "package-json";
18
- import updateNotifier from "update-notifier";
17
+ import { notifyOfCliUpdate } from "./cli-update-notifier.js";
18
+ import {
19
+ major,
20
+ updatedDependencySpec,
21
+ workspaceSpecPrefix,
22
+ } from "./dependency-spec.js";
19
23
 
20
24
  const require1 = createRequire(import.meta.url);
21
25
  const pkg = require1("./package.json");
@@ -202,12 +206,6 @@ function parseDependencySpec(dependencyName, currentSpec) {
202
206
  };
203
207
  }
204
208
 
205
- function workspaceSpecPrefix(parsedSpec) {
206
- return parsedSpec.kind === "workspace-alias"
207
- ? `workspace:${parsedSpec.targetName}@`
208
- : "workspace:";
209
- }
210
-
211
209
  function pinnedDependencySpec(parsedSpec, currentSpec, pinnedSpec) {
212
210
  if (parsedSpec.kind === "registry") {
213
211
  return pinnedSpec;
@@ -223,26 +221,6 @@ function pinnedDependencySpec(parsedSpec, currentSpec, pinnedSpec) {
223
221
  return `${workspaceSpecPrefix(parsedSpec)}${selector}`;
224
222
  }
225
223
 
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
224
  const helpText = `
247
225
  Usage:
248
226
  $ upd
@@ -325,15 +303,6 @@ export async function updateVersions({
325
303
  .map((n) => `${n} ${updatedPackages[n]}`)
326
304
  .join("\n");
327
305
  }
328
- function major(versNum) {
329
- if (typeof versNum === "string") {
330
- return (
331
- versNum.match(/^(?:workspace:)?[^\d]*(\d+)(?:\.|$)/)?.[1] ?? versNum
332
- );
333
- }
334
- return versNum;
335
- }
336
-
337
306
  let configPath = path.join(cwd, "upd.config.json");
338
307
  let newConfig;
339
308
  try {
@@ -355,52 +324,46 @@ export async function updateVersions({
355
324
  }
356
325
 
357
326
  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
- }
327
+ let inventory = {
328
+ namesList: [],
329
+ pathsList: [],
330
+ pathsByName: {},
331
+ contentsObj: {},
332
+ contentsStr: {},
333
+ };
334
+ for (const currentPath of packagePaths) {
335
+ let packagePath = path.join(cwd, currentPath);
336
+ let packContentsStr;
337
+ try {
338
+ packContentsStr = await readTextFile(packagePath, "utf8");
339
+ } catch (error) {
340
+ inventoryFailures.push(makeFailure("package read", currentPath, error));
341
+ continue;
342
+ }
369
343
 
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),
344
+ let parsedContents;
345
+ try {
346
+ parsedContents = JSON.parse(packContentsStr);
347
+ if (!isPlainObject(parsedContents)) {
348
+ throw new TypeError(
349
+ "update-versions/updateVersions(): [THROW_ID_08] package.json must contain a JSON object.",
381
350
  );
382
- return mapReceived;
383
351
  }
352
+ } catch (error) {
353
+ inventoryFailures.push(makeFailure("package parse", currentPath, error));
354
+ continue;
355
+ }
384
356
 
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
- );
357
+ inventory.namesList.push(parsedContents.name);
358
+ inventory.pathsList.push(currentPath);
359
+ inventory.pathsByName[parsedContents.name] = currentPath;
360
+ inventory.contentsStr[currentPath] = packContentsStr;
361
+ inventory.contentsObj[currentPath] = parsedContents;
362
+ }
400
363
 
401
364
  if (inventoryFailures.length > 0) {
402
365
  throw new UpdateVersionsError(inventoryFailures, {
403
- unchangedFiles: pathsPromise.pathsList,
366
+ unchangedFiles: inventory.pathsList,
404
367
  });
405
368
  }
406
369
 
@@ -408,8 +371,8 @@ export async function updateVersions({
408
371
  // makes a failed registry run atomic from the caller's point of view and also
409
372
  // deduplicates lookups shared by packages in a monorepo.
410
373
  let externalNames = new Set();
411
- for (let oneOfPaths of pathsPromise.pathsList) {
412
- let parsedContents = pathsPromise.contentsObj[oneOfPaths];
374
+ for (let oneOfPaths of inventory.pathsList) {
375
+ let parsedContents = inventory.contentsObj[oneOfPaths];
413
376
  for (let dependencyKey of ["dependencies", "devDependencies"]) {
414
377
  if (isPlainObject(parsedContents[dependencyKey])) {
415
378
  for (let [name, spec] of Object.entries(
@@ -423,7 +386,7 @@ export async function updateVersions({
423
386
  let parsedSpec = parseDependencySpec(name, spec);
424
387
  if (
425
388
  parsedSpec.targetName &&
426
- !pathsPromise.namesList.includes(parsedSpec.targetName)
389
+ !inventory.namesList.includes(parsedSpec.targetName)
427
390
  ) {
428
391
  externalNames.add(parsedSpec.targetName);
429
392
  }
@@ -464,7 +427,7 @@ export async function updateVersions({
464
427
 
465
428
  if (registryFailures.length > 0) {
466
429
  throw new UpdateVersionsError(registryFailures, {
467
- unchangedFiles: pathsPromise.pathsList,
430
+ unchangedFiles: inventory.pathsList,
468
431
  });
469
432
  }
470
433
 
@@ -480,13 +443,13 @@ export async function updateVersions({
480
443
  }
481
444
 
482
445
  let allProgressPromise = PProgress.all(
483
- pathsPromise.pathsList.map((oneOfPaths) =>
446
+ inventory.pathsList.map((oneOfPaths) =>
484
447
  pProgress(async (progress) => {
485
448
  // call progress() like progress(0.14);
486
449
 
487
450
  let amended = false;
488
- let finalContents = pathsPromise.contentsStr[oneOfPaths];
489
- let parsedContents = pathsPromise.contentsObj[oneOfPaths];
451
+ let finalContents = inventory.contentsStr[oneOfPaths];
452
+ let parsedContents = inventory.contentsObj[oneOfPaths];
490
453
  let fileUpdates = {};
491
454
 
492
455
  try {
@@ -527,10 +490,10 @@ export async function updateVersions({
527
490
  ? parsedContents.dependencies[singleDepName]
528
491
  : parsedContents.devDependencies[singleDepName];
529
492
  let parsedSpec = parseDependencySpec(singleDepName, singleDepValue);
530
- if (pathsPromise.namesList.includes(parsedSpec.targetName)) {
493
+ if (inventory.namesList.includes(parsedSpec.targetName)) {
531
494
  let localVersion =
532
- pathsPromise.contentsObj[
533
- pathsPromise.pathsByName[parsedSpec.targetName]
495
+ inventory.contentsObj[
496
+ inventory.pathsByName[parsedSpec.targetName]
534
497
  ].version;
535
498
  compiledDepNameVersionPairs[singleDepName] =
536
499
  typeof localVersion === "string" && localVersion.length > 0
@@ -864,7 +827,7 @@ async function runCli() {
864
827
  moduleMode: Boolean(cli.flags.module),
865
828
  reportProgress: true,
866
829
  });
867
- updateNotifier({ pkg }).notify();
830
+ notifyOfCliUpdate({ pkg });
868
831
  }
869
832
 
870
833
  function isDirectExecution() {
@@ -0,0 +1,32 @@
1
+ export function workspaceSpecPrefix(parsedSpec) {
2
+ return parsedSpec.kind === "workspace-alias"
3
+ ? `workspace:${parsedSpec.targetName}@`
4
+ : "workspace:";
5
+ }
6
+
7
+ export function updatedDependencySpec(parsedSpec, currentSpec, version) {
8
+ if (parsedSpec.kind === "registry") {
9
+ return `^${version}`;
10
+ }
11
+ if (parsedSpec.kind === "workspace-path") {
12
+ return currentSpec;
13
+ }
14
+
15
+ let workspaceRange = parsedSpec.selector;
16
+ if (["*", "^", "~"].includes(workspaceRange)) {
17
+ return currentSpec;
18
+ }
19
+ let firstVersionDigit = workspaceRange.search(/\d/);
20
+ if (firstVersionDigit === -1) {
21
+ return currentSpec;
22
+ }
23
+ let rangePrefix = workspaceRange.slice(0, firstVersionDigit);
24
+ return `${workspaceSpecPrefix(parsedSpec)}${rangePrefix}${version}`;
25
+ }
26
+
27
+ export function major(versNum) {
28
+ if (typeof versNum === "string") {
29
+ return versNum.match(/^(?:workspace:)?[^\d]*(\d+)(?:\.|$)/)?.[1] ?? versNum;
30
+ }
31
+ return versNum;
32
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "update-versions",
3
- "version": "7.2.4",
3
+ "version": "7.2.5",
4
4
  "description": "Like npm-check-updates but supports Lerna monorepos and enforces strict semver values",
5
5
  "keywords": [
6
6
  "app",
@@ -60,9 +60,12 @@
60
60
  },
61
61
  "c8": {
62
62
  "all": true,
63
+ "branches": 100,
63
64
  "check-coverage": true,
64
- "exclude": ["**/test/**/*.*"],
65
- "lines": 80
65
+ "exclude": ["**/test/**/*.*", "cli-update-notifier.js"],
66
+ "functions": 100,
67
+ "lines": 100,
68
+ "statements": 100
66
69
  },
67
70
  "lect": {
68
71
  "licence": {
@@ -71,14 +74,12 @@
71
74
  },
72
75
  "dependencies": {
73
76
  "ansi-diff-stream": "^1.2.1",
74
- "codsen-glob": "^1.1.1",
75
- "codsen-utils": "^1.10.0",
76
- "edit-package-json": "^0.10.4",
77
+ "codsen-glob": "^1.1.2",
78
+ "codsen-utils": "^1.10.1",
79
+ "edit-package-json": "^0.10.5",
77
80
  "object-path": "^0.11.8",
78
81
  "p-progress": "^1.0.0",
79
- "p-reduce": "^3.0.0",
80
- "package-json": "^10.0.1",
81
- "update-notifier": "^7.3.1"
82
+ "package-json": "^10.0.1"
82
83
  },
83
84
  "devDependencies": {
84
85
  "p-map": "^7.0.7"