jvm-upgrade 0.2.0 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -40,12 +40,14 @@ Or run it once with `npx jvm-upgrade`. Needs Node.js 22.13 or newer.
40
40
  ## Usage
41
41
 
42
42
  ```sh
43
- jvm-upgrade # list available upgrades (nothing is changed)
44
- jvm-upgrade -u # apply the latest major versions
45
- jvm-upgrade -u -t minor # apply the latest minor versions only
46
- jvm-upgrade -i # pick versions and read changelogs interactively
47
- jvm-upgrade -l # print the changelogs of the selected upgrades
48
- jvm-upgrade -c 7 # ignore versions published in the last 7 days
43
+ jvm-upgrade # list available upgrades (nothing is changed)
44
+ jvm-upgrade -u # apply the latest major versions
45
+ jvm-upgrade -u -t minor # apply the latest minor versions only
46
+ jvm-upgrade -i # pick versions and read changelogs interactively
47
+ jvm-upgrade -l # print a changelog link for each upgrade
48
+ jvm-upgrade --changelog-latest # print the release notes of each new version
49
+ jvm-upgrade --changelog-diff # print the release notes of every version in between
50
+ jvm-upgrade -c 7 # ignore versions published in the last 7 days
49
51
  jvm-upgrade path/to/project
50
52
  ```
51
53
 
@@ -70,7 +72,9 @@ jvm-upgrade path/to/project
70
72
  | `[directory]` | `.` | Project root to scan |
71
73
  | `-u, --upgrade` | | Write the upgrades to the build files |
72
74
  | `-i, --interactive` | | Pick upgrades in a terminal UI |
73
- | `-l, --changelog` | | Print the changelog of each selected upgrade |
75
+ | `-l, --changelog` | | Print a changelog link for each selected upgrade |
76
+ | `--changelog-latest` | | Print the release notes of the new version only |
77
+ | `--changelog-diff` | | Print the release notes of every version between the old and the new one |
74
78
  | `-t, --target <level>` | `major` | Highest jump allowed: `major`, `minor` or `patch` |
75
79
  | `-c, --cooldown <days>` | `0` | Skip versions published less than N days ago |
76
80
  | `--allow-downgrade` | | With `--cooldown`, roll back versions that are too new |
@@ -132,6 +136,8 @@ For each upgrade, jvm-upgrade finds the source repository:
132
136
 
133
137
  It then reads the GitHub releases between your version and the new one. Prereleases are skipped unless you upgrade to one. If there are no releases, it reads `CHANGELOG.md` (and similar files) and cuts out the right sections. Tags without the major version (protobuf-java `4.36.2` is tag `v36.2`) are matched too.
134
138
 
139
+ Notes are cleaned up for the terminal: headings, bullets and code are styled, links and HTML are removed, pull request and commit links become `#123` and short hashes, and Renovate/Dependabot bumps are hidden. Each version shows at most 40 lines, followed by a link to the rest. In the interactive picker (`c`), you see the full notes.
140
+
135
141
  If it finds nothing, it gives you a link: the release notes page for androidx, Google Play services, Firebase and Gradle, or the project's releases page.
136
142
 
137
143
  Set `GITHUB_TOKEN` (or `GH_TOKEN`) to avoid GitHub rate limits.
package/dist/format.js CHANGED
@@ -51,37 +51,120 @@ export function toJson(rows, root) {
51
51
  };
52
52
  });
53
53
  }
54
- export function formatChangelog(log, width) {
54
+ export function formatChangelog(log, width, opts = {}) {
55
55
  if (!log.entries.length) {
56
- return log.source ? `No release notes found here. Read them at:\n${log.source}` : 'No release notes found.';
56
+ return log.source ? ` No release notes found here. Read them at:\n ${styleText('cyan', log.source)}` : ' No release notes found.';
57
57
  }
58
58
  const out = [];
59
59
  for (const entry of log.entries) {
60
- const date = entry.date ? styleText('gray', ` ${entry.date.slice(0, 10)}`) : '';
61
- out.push(styleText(['bold', 'cyan'], entry.title && entry.title !== entry.version ? `${entry.version} ${entry.title}` : entry.version) + date);
60
+ const title = entry.title && !isJustVersion(entry.title, entry.version) ? styleText('gray', ` ${entry.title}`) : '';
61
+ const date = entry.date ? styleText('gray', ` · ${entry.date.slice(0, 10)}`) : '';
62
+ out.push(` ${styleText(['bold', 'green'], `● ${entry.version}`)}${title}${date}`);
62
63
  if (entry.url)
63
- out.push(styleText('gray', entry.url));
64
- out.push('');
65
- for (const line of entry.body.replace(/\r/g, '').trim().split('\n'))
66
- out.push(...wrap(line, width));
64
+ out.push(` ${styleText('gray', entry.url)}`);
65
+ const body = renderMarkdown(entry.body, width - 4);
66
+ const shown = opts.maxLines && body.length > opts.maxLines ? body.slice(0, opts.maxLines) : body;
67
+ if (shown.length)
68
+ out.push('', ...shown.map((line) => (line ? ` ${line}` : '')));
69
+ if (shown.length < body.length) {
70
+ out.push(styleText('gray', ` … ${body.length - shown.length} more lines${entry.url ? ` at ${entry.url}` : ''}`));
71
+ }
67
72
  out.push('');
68
73
  }
69
- if (log.source)
70
- out.push(styleText('gray', `Source: ${log.source}`));
71
- return out.join('\n');
74
+ if (log.source && log.entries.some((e) => !e.url))
75
+ out.push(styleText('gray', ` Source: ${log.source}`));
76
+ return out.join('\n').trimEnd();
77
+ }
78
+ export function formatChangelogHeader(name, from, to, width) {
79
+ const title = ` ${name} ${from} → ${to.version} `;
80
+ const rule = '━'.repeat(Math.max(3, Math.min(width, 100) - title.length - 3));
81
+ return `${styleText('gray', '━━')}${styleText('bold', ` ${name} `)}${from} → ${styleText(['bold', levelColor(to.level)], to.version)} ${styleText('gray', rule)}`;
82
+ }
83
+ export function formatChangelogLinks(links) {
84
+ const nameWidth = Math.max(...links.map((l) => l.name.length));
85
+ const jumpWidth = Math.max(...links.map((l) => `${l.from} → ${l.to.version}`.length));
86
+ const lines = links.map((l) => {
87
+ const jump = `${l.from} → ${l.to.version}`;
88
+ const colored = `${l.from} → ${styleText(levelColor(l.to.level), l.to.version)}${' '.repeat(jumpWidth - jump.length)}`;
89
+ return `${l.name.padEnd(nameWidth)} ${colored} ${l.url ? styleText('cyan', l.url) : styleText('gray', 'no release notes found')}`;
90
+ });
91
+ return [styleText('bold', 'Changelogs'), ...lines].join('\n');
92
+ }
93
+ /** Link to the notes of the target version when found, else to the general notes page. */
94
+ export function changelogUrl(log) {
95
+ return log.entries[0]?.url ?? log.source;
96
+ }
97
+ function isJustVersion(title, version) {
98
+ return title.replace(/^v/i, '') === version || !title.replace(new RegExp(version.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'g'), '').replace(/[\sv]/gi, '');
99
+ }
100
+ /** Turns release-note markdown into short, styled terminal lines. */
101
+ export function renderMarkdown(markdown, width) {
102
+ const text = markdown
103
+ .replace(/\r/g, '')
104
+ .replace(/<!--[\s\S]*?-->/g, '')
105
+ .replace(/<\/?[a-z][^>]*>/gi, '')
106
+ .replace(/!\[[^\]]*\]\([^)]*\)/g, '')
107
+ .replace(/\[([^\]]+)\]\((?:https?:[^)\s]+)\)/g, '$1')
108
+ .replace(/https:\/\/github\.com\/[^/\s]+\/[^/\s]+\/(?:pull|issues)\/(\d+)/g, '#$1')
109
+ .replace(/\(?https:\/\/github\.com\/[^/\s]+\/[^/\s]+\/commit\/([0-9a-f]{8})[0-9a-f]*\)?/g, '($1)')
110
+ .replace(/^\s*(?:\*\*|__)([^*_\n]+)(?:\*\*|__):?\s*$/gm, '### $1')
111
+ .replace(/^\s*[-*+]\s.*@(?:renovate|dependabot)\[bot\].*$\n?/gim, '')
112
+ .replace(/\*\*|__/g, '');
113
+ const out = [];
114
+ let fence = false;
115
+ let afterHeading = false;
116
+ for (const raw of text.split('\n')) {
117
+ const line = raw.trimEnd();
118
+ const wasHeading = afterHeading;
119
+ afterHeading = false;
120
+ if (/^\s*```/.test(line)) {
121
+ fence = !fence;
122
+ continue;
123
+ }
124
+ if (fence) {
125
+ out.push(styleText('cyan', line));
126
+ continue;
127
+ }
128
+ if (/^\s*\[[^\]]+\]:\s*\S+$/.test(line) || /^\s*([-*_])(\s*\1){2,}\s*$/.test(line))
129
+ continue;
130
+ const heading = /^#{1,6}\s+(.*?)\s*#*$/.exec(line);
131
+ if (heading) {
132
+ if (out.length && out.at(-1) !== '')
133
+ out.push('');
134
+ out.push(styleText(['bold', 'yellow'], heading[1]));
135
+ afterHeading = true;
136
+ continue;
137
+ }
138
+ if (!line.trim()) {
139
+ if (out.length && out.at(-1) !== '' && !wasHeading)
140
+ out.push('');
141
+ afterHeading = wasHeading;
142
+ continue;
143
+ }
144
+ const bullet = /^(\s*)[-*+]\s+(.*)$/.exec(line);
145
+ if (bullet) {
146
+ const indent = ' '.repeat(Math.min(Math.floor(bullet[1].length / 2), 3));
147
+ out.push(...wrap(bullet[2], width - indent.length - 2).map((l, i) => `${indent}${i ? ' ' : '• '}${code(l)}`));
148
+ continue;
149
+ }
150
+ out.push(...wrap(line.trim(), width).map(code));
151
+ }
152
+ while (out.at(-1) === '')
153
+ out.pop();
154
+ return out;
155
+ }
156
+ function code(line) {
157
+ return line.replace(/`([^`]+)`/g, (_, c) => styleText('cyan', c));
72
158
  }
73
- function wrap(line, width) {
74
- if (line.length <= width || width < 20)
75
- return [line];
76
- const indent = /^\s*([-*+]\s+|\d+\.\s+)?/.exec(line)[0].length;
77
- const pad = ' '.repeat(Math.min(indent, 8));
78
- const words = line.split(' ');
159
+ function wrap(text, width) {
160
+ if (text.length <= width || width < 20)
161
+ return [text];
79
162
  const lines = [];
80
163
  let current = '';
81
- for (const word of words) {
164
+ for (const word of text.split(' ')) {
82
165
  if (current && current.length + word.length + 1 > width) {
83
166
  lines.push(current);
84
- current = pad + word;
167
+ current = word;
85
168
  }
86
169
  else {
87
170
  current = current ? `${current} ${word}` : word;
package/dist/main.js CHANGED
@@ -5,7 +5,7 @@ import { parseArgs, styleText } from 'node:util';
5
5
  import { applyEdits, EditConflictError } from './apply.js';
6
6
  import { getChangelog } from './changelog.js';
7
7
  import { ConfigError, loadCredentials, loadUserConfig, projectConfigResolver, splitPatterns } from './config.js';
8
- import { formatChangelog, renderTable, toJson } from './format.js';
8
+ import { changelogUrl, formatChangelog, formatChangelogHeader, formatChangelogLinks, renderTable, toJson } from './format.js';
9
9
  import { clearCache, createHttp, defaultCacheDir } from './http.js';
10
10
  import { createProgress } from './progress.js';
11
11
  import { buildRows } from './rows.js';
@@ -20,7 +20,9 @@ Find outdated libraries in Gradle and Maven projects and upgrade them.
20
20
  Actions
21
21
  -u, --upgrade Write the selected upgrades to the build files
22
22
  -i, --interactive Pick upgrades (and read changelogs) in a terminal UI
23
- -l, --changelog Print the changelog of every selected upgrade
23
+ -l, --changelog Print a changelog link for every selected upgrade
24
+ --changelog-latest Print the release notes of the new version only
25
+ --changelog-diff Print the release notes of every version between old and new
24
26
 
25
27
  Policy
26
28
  -t, --target <level> Highest jump allowed: major, minor or patch (default: major)
@@ -59,6 +61,8 @@ function parseCli(argv) {
59
61
  upgrade: { type: 'boolean', short: 'u' },
60
62
  interactive: { type: 'boolean', short: 'i' },
61
63
  changelog: { type: 'boolean', short: 'l' },
64
+ 'changelog-latest': { type: 'boolean' },
65
+ 'changelog-diff': { type: 'boolean' },
62
66
  target: { type: 'string', short: 't' },
63
67
  cooldown: { type: 'string', short: 'c' },
64
68
  'allow-downgrade': { type: 'boolean' },
@@ -229,8 +233,9 @@ export async function main(argv = process.argv.slice(2)) {
229
233
  chosen.set(row, choice);
230
234
  }
231
235
  }
232
- if (flags.changelog && !json)
233
- await printChangelogs(http, chosen, showProgress);
236
+ const changelogMode = flags['changelog-diff'] ? 'diff' : flags['changelog-latest'] ? 'latest' : flags.changelog ? 'links' : undefined;
237
+ if (changelogMode && !json)
238
+ await printChangelogs(http, chosen, changelogMode, showProgress);
234
239
  const writable = [...chosen].filter(([row]) => row.location);
235
240
  if (flags.upgrade || flags.interactive) {
236
241
  if (!writable.length) {
@@ -265,17 +270,27 @@ function loadChangelog(http, row, choice) {
265
270
  const dep = row.deps[0];
266
271
  return getChangelog(http, dep, row.current, choice.version, row.repoOf.get(choice.version), compareVersions);
267
272
  }
268
- async function printChangelogs(http, chosen, showProgress) {
273
+ const MAX_NOTE_LINES = 40;
274
+ async function printChangelogs(http, chosen, mode, showProgress) {
269
275
  const entries = [...chosen];
276
+ if (!entries.length)
277
+ return;
270
278
  const progress = createProgress('Loading changelogs', process.stderr, showProgress);
271
279
  progress.total(entries.length);
272
280
  const logs = await Promise.all(entries.map(([row, choice]) => loadChangelog(http, row, choice)
273
- .catch((e) => ({ entries: [], source: `could not load: ${e.message}` }))
281
+ .catch(() => ({ entries: [] }))
274
282
  .finally(() => progress.tick())));
275
283
  progress.done();
284
+ if (mode === 'links') {
285
+ const links = entries.map(([row, choice], i) => ({ name: row.name, from: row.current, to: choice, url: changelogUrl(logs[i]) }));
286
+ process.stdout.write(`\n${formatChangelogLinks(links)}\n`);
287
+ return;
288
+ }
276
289
  const width = Math.min(process.stdout.columns ?? 100, 120) - 2;
277
290
  entries.forEach(([row, choice], i) => {
278
- process.stdout.write(`\n${styleText(['bold', 'underline'], `${row.name} ${row.current} → ${choice.version}`)}\n\n`);
279
- process.stdout.write(`${formatChangelog(logs[i], width)}\n`);
291
+ const log = logs[i];
292
+ const shown = mode === 'latest' ? { ...log, entries: log.entries.slice(0, 1) } : log;
293
+ process.stdout.write(`\n${formatChangelogHeader(row.name, row.current, choice, width)}\n\n`);
294
+ process.stdout.write(`${formatChangelog(shown, width, { maxLines: MAX_NOTE_LINES })}\n`);
280
295
  });
281
296
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jvm-upgrade",
3
- "version": "0.2.0",
3
+ "version": "0.3.1",
4
4
  "description": "Find and apply library upgrades in Java, Kotlin and Android projects (Gradle and Maven), with changelogs.",
5
5
  "type": "module",
6
6
  "bin": {