jvm-upgrade 0.1.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 +23 -9
- package/dist/apply.js +3 -2
- package/dist/changelog.js +49 -12
- package/dist/format.js +102 -19
- package/dist/http.js +22 -11
- package/dist/main.js +34 -17
- package/dist/repos.js +14 -6
- package/dist/rows.js +4 -0
- package/dist/scan/gradle.js +49 -31
- package/dist/scan/index.js +15 -5
- package/dist/scan/maven.js +1 -2
- package/dist/version.js +0 -4
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -35,17 +35,19 @@ Run jvm-upgrade -u to apply the underlined versions (target: major), or jvm-upgr
|
|
|
35
35
|
npm install -g jvm-upgrade
|
|
36
36
|
```
|
|
37
37
|
|
|
38
|
-
Or run it once with `npx jvm-upgrade`. Needs Node.js
|
|
38
|
+
Or run it once with `npx jvm-upgrade`. Needs Node.js 22.13 or newer.
|
|
39
39
|
|
|
40
40
|
## Usage
|
|
41
41
|
|
|
42
42
|
```sh
|
|
43
|
-
jvm-upgrade
|
|
44
|
-
jvm-upgrade -u
|
|
45
|
-
jvm-upgrade -u -t minor
|
|
46
|
-
jvm-upgrade -i
|
|
47
|
-
jvm-upgrade -l
|
|
48
|
-
jvm-upgrade -
|
|
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
|
|
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 |
|
|
@@ -124,7 +128,17 @@ Dynamic versions (`1.+`, `[1.0,2.0)`, `latest.release`) are shown but never rewr
|
|
|
124
128
|
|
|
125
129
|
## Changelogs
|
|
126
130
|
|
|
127
|
-
For each upgrade, jvm-upgrade
|
|
131
|
+
For each upgrade, jvm-upgrade finds the source repository:
|
|
132
|
+
|
|
133
|
+
- the POM's `<scm>` or `<url>`, following parent POMs and Gradle plugin markers,
|
|
134
|
+
- the GitHub repo itself for JitPack (`com.github.user:repo`),
|
|
135
|
+
- `gradle/gradle` for the Gradle wrapper.
|
|
136
|
+
|
|
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.
|
|
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
|
+
|
|
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.
|
|
128
142
|
|
|
129
143
|
Set `GITHUB_TOKEN` (or `GH_TOKEN`) to avoid GitHub rate limits.
|
|
130
144
|
|
package/dist/apply.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { randomBytes } from 'node:crypto';
|
|
2
|
-
import { readFile, rename, rm, stat, writeFile } from 'node:fs/promises';
|
|
2
|
+
import { readFile, realpath, rename, rm, stat, writeFile } from 'node:fs/promises';
|
|
3
3
|
export class EditConflictError extends Error {
|
|
4
4
|
}
|
|
5
5
|
/** Rewrites only the version literals. Every file is checked first, then written atomically. */
|
|
@@ -41,7 +41,8 @@ function rewrite(file, content, edits) {
|
|
|
41
41
|
}
|
|
42
42
|
return out;
|
|
43
43
|
}
|
|
44
|
-
async function writeAtomic(
|
|
44
|
+
async function writeAtomic(path, content) {
|
|
45
|
+
const file = await realpath(path);
|
|
45
46
|
const tmp = `${file}.${randomBytes(4).toString('hex')}.tmp`;
|
|
46
47
|
const { mode } = await stat(file);
|
|
47
48
|
try {
|
package/dist/changelog.js
CHANGED
|
@@ -1,7 +1,13 @@
|
|
|
1
1
|
import { fetchPom } from './repos.js';
|
|
2
|
+
import { isPrerelease } from './version.js';
|
|
2
3
|
const CHANGELOG_FILES = ['CHANGELOG.md', 'CHANGES.md', 'HISTORY.md', 'RELEASE_NOTES.md', 'docs/CHANGELOG.md'];
|
|
3
4
|
const MAX_RELEASE_PAGES = 5;
|
|
4
5
|
const MAX_PARENT_DEPTH = 3;
|
|
6
|
+
const GOOGLE_NOTES = [
|
|
7
|
+
['com.google.android.gms', 'https://developers.google.com/android/guides/releases'],
|
|
8
|
+
['com.google.gms', 'https://developers.google.com/android/guides/releases'],
|
|
9
|
+
['com.google.firebase', 'https://firebase.google.com/support/release-notes/android'],
|
|
10
|
+
];
|
|
5
11
|
const POM_NOISE = /<(parent|scm|licenses|developers|contributors|organization|distributionManagement|issueManagement|ciManagement|mailingLists|repositories|pluginRepositories|build|reporting|profiles|dependencies|dependencyManagement)\b[\s\S]*?<\/\1>/g;
|
|
6
12
|
export function normalizeScmUrl(raw) {
|
|
7
13
|
let s = raw.trim();
|
|
@@ -56,9 +62,21 @@ export function androidxReleasesUrl(group) {
|
|
|
56
62
|
const name = first === 'compose' && second ? `compose-${second}` : first;
|
|
57
63
|
return `https://developer.android.com/jetpack/androidx/releases/${name}`;
|
|
58
64
|
}
|
|
65
|
+
function googleNotesUrl(group) {
|
|
66
|
+
return GOOGLE_NOTES.find(([prefix]) => group === prefix || group.startsWith(`${prefix}.`))?.[1];
|
|
67
|
+
}
|
|
59
68
|
export async function findSourceRepo(http, dep, version, repo) {
|
|
60
|
-
if (dep.kind === 'gradle')
|
|
61
|
-
|
|
69
|
+
if (dep.kind === 'gradle') {
|
|
70
|
+
const notesUrl = `https://docs.gradle.org/${version}/release-notes.html`;
|
|
71
|
+
return { github: { owner: 'gradle', repo: 'gradle' }, url: notesUrl, notesUrl };
|
|
72
|
+
}
|
|
73
|
+
const fromPom = await sourceFromPom(http, dep, version, repo);
|
|
74
|
+
const notesUrl = googleNotesUrl(dep.group);
|
|
75
|
+
if (notesUrl)
|
|
76
|
+
return { ...fromPom, url: fromPom.url ?? notesUrl, notesUrl };
|
|
77
|
+
return fromPom;
|
|
78
|
+
}
|
|
79
|
+
async function sourceFromPom(http, dep, version, repo) {
|
|
62
80
|
const [root, , owner] = dep.group.split('.');
|
|
63
81
|
if (dep.group.startsWith('com.github.') && owner) {
|
|
64
82
|
return { github: { owner, repo: dep.artifact }, url: `https://github.com/${owner}/${dep.artifact}` };
|
|
@@ -114,11 +132,19 @@ export function normalizeTag(tag, artifact) {
|
|
|
114
132
|
return v;
|
|
115
133
|
}
|
|
116
134
|
const isVersionLike = (v) => /^\d+(\.\d+)*([-.+_][0-9A-Za-z.+_-]*)?$/.test(v);
|
|
135
|
+
const trimZeros = (v) => v.replace(/(\.0)+(?=$|-)/, '');
|
|
136
|
+
/** Trailing ".0" parts are ignored so a "v9.8.0" tag matches version "9.8". */
|
|
117
137
|
function inRange(v, from, to, compare) {
|
|
118
|
-
|
|
138
|
+
const x = trimZeros(v);
|
|
139
|
+
return compare(x, trimZeros(from)) > 0 && compare(x, trimZeros(to)) <= 0;
|
|
140
|
+
}
|
|
141
|
+
/** Some projects tag without the artifact's major, e.g. protobuf-java 4.36.2 is tag v36.2. */
|
|
142
|
+
function withoutMajor(v) {
|
|
143
|
+
const parts = v.split('.');
|
|
144
|
+
return parts.length >= 3 && /^\d+$/.test(parts[0]) ? parts.slice(1).join('.') : undefined;
|
|
119
145
|
}
|
|
120
146
|
const sortDesc = (entries, compare) => entries.sort((a, b) => compare(b.version, a.version));
|
|
121
|
-
async function releaseEntries(http, gh, artifact, from, to, compare) {
|
|
147
|
+
async function releaseEntries(http, gh, artifact, from, to, compare, includePre) {
|
|
122
148
|
const entries = [];
|
|
123
149
|
for (let page = 1; page <= MAX_RELEASE_PAGES; page++) {
|
|
124
150
|
const res = await http
|
|
@@ -135,7 +161,7 @@ async function releaseEntries(http, gh, artifact, from, to, compare) {
|
|
|
135
161
|
if (r.draft || !isVersionLike(version))
|
|
136
162
|
continue;
|
|
137
163
|
versions.push(version);
|
|
138
|
-
if (!inRange(version, from, to, compare))
|
|
164
|
+
if (!inRange(version, from, to, compare) || (r.prerelease && !includePre))
|
|
139
165
|
continue;
|
|
140
166
|
entries.push({
|
|
141
167
|
version,
|
|
@@ -196,12 +222,23 @@ export async function getChangelog(http, dep, from, to, repo, compare) {
|
|
|
196
222
|
const src = await findSourceRepo(http, dep, to, repo);
|
|
197
223
|
const gh = src.github;
|
|
198
224
|
if (!gh)
|
|
199
|
-
return { entries: [], source: src.url };
|
|
225
|
+
return { entries: [], source: src.notesUrl ?? src.url };
|
|
200
226
|
const releasesUrl = `https://github.com/${gh.owner}/${gh.repo}/releases`;
|
|
201
|
-
const
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
227
|
+
const fallback = { entries: [], source: src.notesUrl ?? releasesUrl };
|
|
228
|
+
const includePre = isPrerelease(to);
|
|
229
|
+
const ranges = [[from, to]];
|
|
230
|
+
const [shortFrom, shortTo] = [withoutMajor(from), withoutMajor(to)];
|
|
231
|
+
if (shortFrom && shortTo && compare(shortFrom, shortTo) < 0)
|
|
232
|
+
ranges.push([shortFrom, shortTo]);
|
|
233
|
+
for (const [a, b] of ranges) {
|
|
234
|
+
const releases = await releaseEntries(http, gh, dep.artifact, a, b, compare, includePre);
|
|
235
|
+
if (releases.entries.length)
|
|
236
|
+
return { entries: sortDesc(releases.entries, compare), source: releasesUrl };
|
|
237
|
+
if (releases.rateLimited)
|
|
238
|
+
return fallback;
|
|
239
|
+
const files = await changelogFileEntries(http, gh, a, b, compare);
|
|
240
|
+
if (files)
|
|
241
|
+
return files;
|
|
242
|
+
}
|
|
243
|
+
return fallback;
|
|
207
244
|
}
|
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
|
|
61
|
-
|
|
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
|
-
|
|
65
|
-
|
|
66
|
-
|
|
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(
|
|
74
|
-
if (
|
|
75
|
-
return [
|
|
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
|
|
164
|
+
for (const word of text.split(' ')) {
|
|
82
165
|
if (current && current.length + word.length + 1 > width) {
|
|
83
166
|
lines.push(current);
|
|
84
|
-
current =
|
|
167
|
+
current = word;
|
|
85
168
|
}
|
|
86
169
|
else {
|
|
87
170
|
current = current ? `${current} ${word}` : word;
|
package/dist/http.js
CHANGED
|
@@ -21,7 +21,7 @@ export async function clearCache(dir) {
|
|
|
21
21
|
}
|
|
22
22
|
export function authHeader(url, credentials) {
|
|
23
23
|
const match = credentials
|
|
24
|
-
.filter((c) => url
|
|
24
|
+
.filter((c) => underPrefix(url, c.url) && sameOrigin(url, c.url))
|
|
25
25
|
.sort((a, b) => b.url.length - a.url.length)[0];
|
|
26
26
|
if (match?.token)
|
|
27
27
|
return `Bearer ${match.token}`;
|
|
@@ -33,6 +33,10 @@ export function authHeader(url, credentials) {
|
|
|
33
33
|
return `Bearer ${gh}`;
|
|
34
34
|
return undefined;
|
|
35
35
|
}
|
|
36
|
+
function underPrefix(url, prefix) {
|
|
37
|
+
const base = prefix.endsWith('/') ? prefix : `${prefix}/`;
|
|
38
|
+
return url === prefix || url.startsWith(base);
|
|
39
|
+
}
|
|
36
40
|
function hostOf(url) {
|
|
37
41
|
try {
|
|
38
42
|
return new URL(url).host;
|
|
@@ -69,10 +73,12 @@ function limiter(max) {
|
|
|
69
73
|
}
|
|
70
74
|
};
|
|
71
75
|
}
|
|
72
|
-
async function readLocal(url) {
|
|
76
|
+
async function readLocal(url, method) {
|
|
73
77
|
try {
|
|
74
|
-
const
|
|
75
|
-
|
|
78
|
+
const path = fileURLToPath(url);
|
|
79
|
+
const { mtime } = await stat(path);
|
|
80
|
+
const body = method === 'HEAD' ? '' : await readFile(path, 'utf8');
|
|
81
|
+
return { status: 200, headers: { 'last-modified': mtime.toUTCString() }, body };
|
|
76
82
|
}
|
|
77
83
|
catch {
|
|
78
84
|
return { status: 404, headers: {}, body: '' };
|
|
@@ -89,6 +95,7 @@ async function atomicWrite(file, data) {
|
|
|
89
95
|
throw err;
|
|
90
96
|
}
|
|
91
97
|
}
|
|
98
|
+
const cacheable = (res) => res.status === 200 || res.status === 404;
|
|
92
99
|
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
93
100
|
export function createHttp(opts) {
|
|
94
101
|
const ttl = opts.ttlMs ?? DEFAULT_TTL;
|
|
@@ -104,10 +111,11 @@ export function createHttp(opts) {
|
|
|
104
111
|
const file = cacheFile(url);
|
|
105
112
|
try {
|
|
106
113
|
const entry = JSON.parse(await readFile(file, 'utf8'));
|
|
107
|
-
|
|
108
|
-
|
|
114
|
+
const age = Date.now() - entry.time;
|
|
115
|
+
// Stale entries are not deleted here: that races fresh writes from other processes.
|
|
116
|
+
if (age >= 0 && age <= ttl && typeof entry.status === 'number' && typeof entry.body === 'string') {
|
|
117
|
+
return { status: entry.status, headers: entry.headers ?? {}, body: entry.body };
|
|
109
118
|
}
|
|
110
|
-
await rm(file, { force: true });
|
|
111
119
|
}
|
|
112
120
|
catch {
|
|
113
121
|
// missing or corrupt entry
|
|
@@ -115,7 +123,7 @@ export function createHttp(opts) {
|
|
|
115
123
|
return undefined;
|
|
116
124
|
}
|
|
117
125
|
async function writeCache(url, res) {
|
|
118
|
-
if (!cacheDir || (res
|
|
126
|
+
if (!cacheDir || !cacheable(res))
|
|
119
127
|
return;
|
|
120
128
|
try {
|
|
121
129
|
dirReady ??= mkdir(cacheDir, { recursive: true });
|
|
@@ -145,6 +153,8 @@ export function createHttp(opts) {
|
|
|
145
153
|
current = new URL(location, current).toString();
|
|
146
154
|
continue;
|
|
147
155
|
}
|
|
156
|
+
if (method === 'HEAD')
|
|
157
|
+
await res.body?.cancel();
|
|
148
158
|
return {
|
|
149
159
|
status: res.status,
|
|
150
160
|
headers: Object.fromEntries(res.headers),
|
|
@@ -175,7 +185,7 @@ export function createHttp(opts) {
|
|
|
175
185
|
}
|
|
176
186
|
async function get(url) {
|
|
177
187
|
if (url.startsWith('file:'))
|
|
178
|
-
return readLocal(url);
|
|
188
|
+
return readLocal(url, 'GET');
|
|
179
189
|
const authed = authHeader(url, opts.credentials) !== undefined;
|
|
180
190
|
const cached = authed ? undefined : await readCache(url);
|
|
181
191
|
if (cached)
|
|
@@ -191,13 +201,14 @@ export function createHttp(opts) {
|
|
|
191
201
|
if (!p) {
|
|
192
202
|
p = get(url);
|
|
193
203
|
inflight.set(url, p);
|
|
194
|
-
|
|
204
|
+
// Keep good answers for the whole run; let errors and 5xx/429 be retried later.
|
|
205
|
+
p.then((res) => cacheable(res) || inflight.delete(url), () => inflight.delete(url));
|
|
195
206
|
}
|
|
196
207
|
return p;
|
|
197
208
|
},
|
|
198
209
|
async head(url) {
|
|
199
210
|
if (url.startsWith('file:'))
|
|
200
|
-
return readLocal(url);
|
|
211
|
+
return readLocal(url, 'HEAD');
|
|
201
212
|
return network('HEAD', url);
|
|
202
213
|
},
|
|
203
214
|
async close() {
|
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
|
|
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' },
|
|
@@ -150,6 +154,7 @@ export async function main(argv = process.argv.slice(2)) {
|
|
|
150
154
|
await clearCache(cacheDir);
|
|
151
155
|
const showProgress = flags.progress && process.stderr.isTTY;
|
|
152
156
|
const log = (text) => process.stderr.write(`${text}\n`);
|
|
157
|
+
const say = (text) => (json ? process.stderr : process.stdout).write(`${text}\n`);
|
|
153
158
|
let scan;
|
|
154
159
|
try {
|
|
155
160
|
scan = await scanProject(root);
|
|
@@ -160,7 +165,7 @@ export async function main(argv = process.argv.slice(2)) {
|
|
|
160
165
|
for (const warning of scan.warnings)
|
|
161
166
|
log(styleText('yellow', `warning: ${warning}`));
|
|
162
167
|
if (!scan.dependencies.length) {
|
|
163
|
-
|
|
168
|
+
say('No Gradle or Maven dependencies found.');
|
|
164
169
|
if (json)
|
|
165
170
|
process.stdout.write('[]\n');
|
|
166
171
|
return EXIT.ok;
|
|
@@ -194,6 +199,7 @@ export async function main(argv = process.argv.slice(2)) {
|
|
|
194
199
|
if (nameCount.get(row.name) > 1)
|
|
195
200
|
row.name += ` (${relative(root, row.deps[0].file)})`;
|
|
196
201
|
const outdated = rows.filter((r) => r.choices.length);
|
|
202
|
+
const selectable = outdated.filter((r) => r.selected !== undefined);
|
|
197
203
|
const failed = rows.filter((r) => !r.choices.length && r.errors.length);
|
|
198
204
|
const failedAll = rows.length > 0 && failed.length === rows.length;
|
|
199
205
|
const visible = flags.verbose ? rows : [...outdated, ...failed];
|
|
@@ -202,9 +208,9 @@ export async function main(argv = process.argv.slice(2)) {
|
|
|
202
208
|
}
|
|
203
209
|
else if (!flags.interactive) {
|
|
204
210
|
if (visible.length)
|
|
205
|
-
|
|
211
|
+
say(renderTable(visible, root));
|
|
206
212
|
if (!outdated.length)
|
|
207
|
-
|
|
213
|
+
say(styleText('green', `All ${rows.length - failed.length} dependencies are up to date.`));
|
|
208
214
|
}
|
|
209
215
|
if (failedAll)
|
|
210
216
|
return EXIT.network;
|
|
@@ -214,7 +220,7 @@ export async function main(argv = process.argv.slice(2)) {
|
|
|
214
220
|
if (flags.interactive) {
|
|
215
221
|
const picked = await (await import('./tui.js')).pick(outdated, (row, choice) => loadChangelog(http, row, choice));
|
|
216
222
|
if (!picked) {
|
|
217
|
-
|
|
223
|
+
say('Cancelled. Nothing was changed.');
|
|
218
224
|
return EXIT.ok;
|
|
219
225
|
}
|
|
220
226
|
chosen = picked;
|
|
@@ -227,12 +233,13 @@ export async function main(argv = process.argv.slice(2)) {
|
|
|
227
233
|
chosen.set(row, choice);
|
|
228
234
|
}
|
|
229
235
|
}
|
|
230
|
-
|
|
231
|
-
|
|
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);
|
|
232
239
|
const writable = [...chosen].filter(([row]) => row.location);
|
|
233
240
|
if (flags.upgrade || flags.interactive) {
|
|
234
241
|
if (!writable.length) {
|
|
235
|
-
|
|
242
|
+
say('Nothing to upgrade.');
|
|
236
243
|
return EXIT.ok;
|
|
237
244
|
}
|
|
238
245
|
const edits = writable.map(([row, choice]) => ({ location: row.location, from: row.current, to: choice.version }));
|
|
@@ -245,15 +252,15 @@ export async function main(argv = process.argv.slice(2)) {
|
|
|
245
252
|
throw e;
|
|
246
253
|
}
|
|
247
254
|
for (const [row, choice] of writable)
|
|
248
|
-
|
|
249
|
-
|
|
255
|
+
say(`${styleText('green', '✔')} ${row.name} ${row.current} → ${choice.version}`);
|
|
256
|
+
say(styleText('gray', 'Build files updated. Run your build and tests to check the upgrades.'));
|
|
250
257
|
return EXIT.ok;
|
|
251
258
|
}
|
|
252
259
|
if (!json) {
|
|
253
260
|
const target = rootOptions.target;
|
|
254
|
-
|
|
261
|
+
say(styleText('gray', `\nRun ${styleText('cyan', 'jvm-upgrade -u')} to apply the underlined versions (target: ${target}), or ${styleText('cyan', 'jvm-upgrade -i')} to pick.`));
|
|
255
262
|
}
|
|
256
|
-
return flags['error-on-outdated'] ? EXIT.outdated : EXIT.ok;
|
|
263
|
+
return flags['error-on-outdated'] && selectable.length ? EXIT.outdated : EXIT.ok;
|
|
257
264
|
}
|
|
258
265
|
finally {
|
|
259
266
|
await http.close();
|
|
@@ -263,17 +270,27 @@ function loadChangelog(http, row, choice) {
|
|
|
263
270
|
const dep = row.deps[0];
|
|
264
271
|
return getChangelog(http, dep, row.current, choice.version, row.repoOf.get(choice.version), compareVersions);
|
|
265
272
|
}
|
|
266
|
-
|
|
273
|
+
const MAX_NOTE_LINES = 40;
|
|
274
|
+
async function printChangelogs(http, chosen, mode, showProgress) {
|
|
267
275
|
const entries = [...chosen];
|
|
276
|
+
if (!entries.length)
|
|
277
|
+
return;
|
|
268
278
|
const progress = createProgress('Loading changelogs', process.stderr, showProgress);
|
|
269
279
|
progress.total(entries.length);
|
|
270
280
|
const logs = await Promise.all(entries.map(([row, choice]) => loadChangelog(http, row, choice)
|
|
271
|
-
.catch((
|
|
281
|
+
.catch(() => ({ entries: [] }))
|
|
272
282
|
.finally(() => progress.tick())));
|
|
273
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
|
+
}
|
|
274
289
|
const width = Math.min(process.stdout.columns ?? 100, 120) - 2;
|
|
275
290
|
entries.forEach(([row, choice], i) => {
|
|
276
|
-
|
|
277
|
-
|
|
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`);
|
|
278
295
|
});
|
|
279
296
|
}
|
package/dist/repos.js
CHANGED
|
@@ -41,21 +41,29 @@ function loadGradle(http) {
|
|
|
41
41
|
return p;
|
|
42
42
|
}
|
|
43
43
|
async function fetchGithubTags(http, owner, repo) {
|
|
44
|
-
const
|
|
44
|
+
const url = `https://api.github.com/repos/${owner}/${repo}/tags?per_page=100`;
|
|
45
|
+
const res = await http.get(url);
|
|
46
|
+
if (res.status === 404)
|
|
47
|
+
return [];
|
|
45
48
|
if (res.status !== 200)
|
|
46
|
-
|
|
49
|
+
throw new Error(`${url}: HTTP ${res.status}`);
|
|
47
50
|
const tags = JSON.parse(res.body);
|
|
48
51
|
return tags.map((t) => t.name.replace(/^v(?=\d)/i, ''));
|
|
49
52
|
}
|
|
50
53
|
async function versionsFromRepo(http, dep, repo) {
|
|
51
54
|
const url = `${repo}${artifactPath(dep.group, dep.artifact)}/maven-metadata.xml`;
|
|
52
55
|
const res = await http.get(url);
|
|
53
|
-
if (res.status === 200)
|
|
56
|
+
if (res.status === 200) {
|
|
57
|
+
// Some proxies answer 200 with an HTML page instead of 404 or 401.
|
|
58
|
+
if (!res.body.includes('<metadata'))
|
|
59
|
+
throw new Error(`${url}: not a maven-metadata.xml response`);
|
|
54
60
|
return parseMetadata(res.body);
|
|
61
|
+
}
|
|
55
62
|
if (res.status === 404) {
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
63
|
+
// JitPack: com.github.Owner:Repo, or com.github.Owner.Repo:Module for multi-module builds.
|
|
64
|
+
const [, , owner, ghRepo = dep.artifact] = dep.group.split('.');
|
|
65
|
+
if (repo.includes('jitpack.io') && dep.group.startsWith('com.github.') && owner) {
|
|
66
|
+
return fetchGithubTags(http, owner, ghRepo);
|
|
59
67
|
}
|
|
60
68
|
return [];
|
|
61
69
|
}
|
package/dist/rows.js
CHANGED
|
@@ -42,6 +42,10 @@ export async function buildRows(deps, opts) {
|
|
|
42
42
|
try {
|
|
43
43
|
return await buildRow(group, optionsOf.get(group[0]), opts);
|
|
44
44
|
}
|
|
45
|
+
catch (e) {
|
|
46
|
+
const first = group[0];
|
|
47
|
+
return { name: displayName(first), deps: group, current: first.version, location: first.location, choices: [], repoOf: new Map(), errors: [e.message] };
|
|
48
|
+
}
|
|
45
49
|
finally {
|
|
46
50
|
opts.progress.tick();
|
|
47
51
|
}
|
package/dist/scan/gradle.js
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import { readFile } from 'node:fs/promises';
|
|
2
2
|
import { dirname, join, resolve } from 'node:path';
|
|
3
3
|
import { dedupeRepos, GOOGLE, GRADLE_PLUGIN_PORTAL, MAVEN_CENTRAL, NAMED_REPOS } from './repos.js';
|
|
4
|
-
const COORD_RE = /(["'])([\w.\-]
|
|
4
|
+
const COORD_RE = /(["'])([A-Za-z][\w.\-]*):([\w.\-]+):((?:\$\{[^}\n]*\}|[\w.\-+!$\[\](),])+)(?::[\w.\-]+)?(?:@\w+)?\1/dg;
|
|
5
|
+
// Rejects look-alikes such as "jdbc:h2:mem:test".
|
|
6
|
+
const VERSIONISH_RE = /\d|\$|^\+$|^latest\.|SNAPSHOT/;
|
|
5
7
|
const MAP_RE = /\bgroup\s*[:=]\s*(["'])([^"']+)\1\s*,\s*name\s*[:=]\s*(["'])([^"']+)\3\s*,\s*version\s*[:=]\s*(?:(["'])([^"']*)\5|([A-Za-z_][\w.]*))/dg;
|
|
6
|
-
const KOTLIN_DEP_RE = /\bkotlin\s*\(\s*"([\w\-]+)"\s*,\s*(?:version\s*=\s*)?"([^"]*)"\s*\)/dg;
|
|
8
|
+
const KOTLIN_DEP_RE = /\bkotlin\s*\(\s*"([\w\-]+)"\s*,\s*(?:version\s*=\s*)?(?:"([^"]*)"|([A-Za-z_][\w.]*))\s*\)/dg;
|
|
7
9
|
const PLUGIN_RE = /\b(id|kotlin)\s*\(?\s*(["'])([\w.\-]+)\2\s*\)?\s*\.?\s*version\s*\(?\s*(?:(["'])([^"']*)\4|([A-Za-z_][\w.]*(?:\(\))?))/dg;
|
|
8
10
|
const REPO_RE = /\b(mavenCentral|google|gradlePluginPortal|jcenter|mavenLocal)\s*[({]|\bmaven\s*\(\s*(?:url\s*=\s*)?(?:uri\s*\(\s*)?["']([^"'$]+)["']|\b(?:url|setUrl)\s*(?:=\s*)?\(?\s*(?:uri\s*\(\s*)?["']([^"'$]+)["']/g;
|
|
9
11
|
const CATALOG_RE = /(?:create\s*\(\s*["'](\w+)["']\s*\)|(\w+))\s*\{[^{}]*?\bfrom\s*\(\s*files\s*\(\s*["']([^"']+)["']/g;
|
|
@@ -17,6 +19,8 @@ const EXT_BLOCK_RES = [
|
|
|
17
19
|
/(?<![\w.])(\w+)\s*=\s*(["'])([^"'$\n]*)\2/dg,
|
|
18
20
|
/\bset\s*\(\s*["'](\w+)["']\s*,\s*(["'])([^"'$\n]*)\2/dg,
|
|
19
21
|
];
|
|
22
|
+
const MAP_VAR_RE = /(?<![\w.])(?:ext\.)?(\w+)\s*=\s*\[/g;
|
|
23
|
+
const MAP_ENTRY_RE = /(?<![\w.])(\w+)\s*:\s*(["'])([^"'$\n]*)\2/dg;
|
|
20
24
|
const PROPERTY_RE = /^[ \t]*([\w.\-]+)[ \t]*[=:][ \t]*(.*?)[ \t]*\r?$/dgm;
|
|
21
25
|
const WRAPPER_RE = /distributionUrl\s*[=:].*?gradle-([\w.\-]+?)-(?:bin|all)\.zip/d;
|
|
22
26
|
export function isDynamic(version) {
|
|
@@ -90,8 +94,7 @@ export function stripProperties(src) {
|
|
|
90
94
|
}
|
|
91
95
|
async function readSource(file, strip) {
|
|
92
96
|
try {
|
|
93
|
-
|
|
94
|
-
return { file, content, code: strip(content) };
|
|
97
|
+
return { file, code: strip(await readFile(file, 'utf8')) };
|
|
95
98
|
}
|
|
96
99
|
catch {
|
|
97
100
|
return undefined;
|
|
@@ -101,15 +104,17 @@ function findBlocks(code, name) {
|
|
|
101
104
|
const re = new RegExp(`\\b${name}\\s*(?:\\([^()]*\\))?\\s*\\{`, 'g');
|
|
102
105
|
return [...code.matchAll(re)].map((m) => {
|
|
103
106
|
const open = m.index + m[0].length - 1;
|
|
104
|
-
return { start: open + 1, end:
|
|
107
|
+
return { start: open + 1, end: matchClose(code, open) };
|
|
105
108
|
});
|
|
106
109
|
}
|
|
107
|
-
function
|
|
110
|
+
function matchClose(code, open) {
|
|
111
|
+
const opener = code[open];
|
|
112
|
+
const closer = opener === '[' ? ']' : '}';
|
|
108
113
|
let depth = 0;
|
|
109
114
|
for (let i = open; i < code.length; i++) {
|
|
110
|
-
if (code[i] ===
|
|
115
|
+
if (code[i] === opener)
|
|
111
116
|
depth++;
|
|
112
|
-
else if (code[i] ===
|
|
117
|
+
else if (code[i] === closer && --depth === 0)
|
|
113
118
|
return i;
|
|
114
119
|
}
|
|
115
120
|
return code.length;
|
|
@@ -150,6 +155,13 @@ function collectVars(src) {
|
|
|
150
155
|
].sort((a, b) => a.index - b.index);
|
|
151
156
|
for (const m of matches)
|
|
152
157
|
vars.set(m[1], { value: m[3], loc: locOf(src, m, 3) });
|
|
158
|
+
for (const m of src.code.matchAll(MAP_VAR_RE)) {
|
|
159
|
+
const open = m.index + m[0].length - 1;
|
|
160
|
+
for (const e of src.code.slice(open, matchClose(src.code, open)).matchAll(MAP_ENTRY_RE)) {
|
|
161
|
+
const [start, end] = e.indices[3];
|
|
162
|
+
vars.set(`${m[1]}.${e[1]}`, { value: e[3], loc: { file: src.file, start: open + start, end: open + end } });
|
|
163
|
+
}
|
|
164
|
+
}
|
|
153
165
|
return vars;
|
|
154
166
|
}
|
|
155
167
|
function propertyVars(src) {
|
|
@@ -168,8 +180,8 @@ function lookup(scope, expr) {
|
|
|
168
180
|
const catalog = /^(\w+)\.versions\.([\w.]+)$/.exec(e);
|
|
169
181
|
if (catalog)
|
|
170
182
|
return scope.catalogs.get(catalog[1])?.get(normKey(catalog[2]));
|
|
171
|
-
const key = /(?:extra|properties)\s*\[\s*["']([\w.\-]+)["']\s*\]$/.exec(e)?.[1] ??
|
|
172
|
-
/property\s*\(\s*["']([\w.\-]+)["']\s*\)$/.exec(e)?.[1] ??
|
|
183
|
+
const key = /(?:ext|extra|properties)\s*\[\s*["']([\w.\-]+)["']\s*\]$/.exec(e)?.[1] ??
|
|
184
|
+
/(?:findProperty|property)\s*\(\s*["']([\w.\-]+)["']\s*\)$/.exec(e)?.[1] ??
|
|
173
185
|
e.replace(/^(?:(?:rootProject|project|ext|extra|properties)\.)+/, '');
|
|
174
186
|
for (const vars of scope.vars) {
|
|
175
187
|
const value = vars.get(key);
|
|
@@ -211,30 +223,30 @@ function addResolved(ctx, src, kind, group, artifact, raw, loc) {
|
|
|
211
223
|
}
|
|
212
224
|
ctx.out.push(makeDep(kind, group, artifact, version, src.file, ctx.repositories));
|
|
213
225
|
}
|
|
226
|
+
/** Adds a match whose version is either a quoted literal in group `literal` or a bare reference in `literal + 1`. */
|
|
227
|
+
function addLiteralOrRef(ctx, src, kind, group, artifact, m, literal) {
|
|
228
|
+
if (m[literal] !== undefined)
|
|
229
|
+
addResolved(ctx, src, kind, group, artifact, m[literal], locOf(src, m, literal));
|
|
230
|
+
else
|
|
231
|
+
addResolved(ctx, src, kind, group, artifact, `\${${m[literal + 1]}}`);
|
|
232
|
+
}
|
|
214
233
|
function libraryDeps(src, ctx) {
|
|
215
234
|
for (const m of src.code.matchAll(COORD_RE)) {
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
for (const m of src.code.matchAll(MAP_RE)) {
|
|
219
|
-
if (m[6] !== undefined)
|
|
220
|
-
addResolved(ctx, src, 'library', m[2], m[4], m[6], locOf(src, m, 6));
|
|
221
|
-
else
|
|
222
|
-
addResolved(ctx, src, 'library', m[2], m[4], `\${${m[7]}}`);
|
|
235
|
+
if (VERSIONISH_RE.test(m[4]))
|
|
236
|
+
addResolved(ctx, src, 'library', m[2], m[3], m[4], locOf(src, m, 4));
|
|
223
237
|
}
|
|
238
|
+
for (const m of src.code.matchAll(MAP_RE))
|
|
239
|
+
addLiteralOrRef(ctx, src, 'library', m[2], m[4], m, 6);
|
|
224
240
|
const plugins = findBlocks(src.code, 'plugins');
|
|
225
241
|
for (const m of src.code.matchAll(KOTLIN_DEP_RE)) {
|
|
226
|
-
if (!within(m.index, plugins))
|
|
227
|
-
|
|
228
|
-
}
|
|
242
|
+
if (!within(m.index, plugins))
|
|
243
|
+
addLiteralOrRef(ctx, src, 'library', 'org.jetbrains.kotlin', `kotlin-${m[1]}`, m, 2);
|
|
229
244
|
}
|
|
230
245
|
}
|
|
231
246
|
function pluginDeps(src, ctx) {
|
|
232
247
|
for (const m of matchesIn(src.code, PLUGIN_RE, findBlocks(src.code, 'plugins'))) {
|
|
233
248
|
const id = m[1] === 'kotlin' ? `org.jetbrains.kotlin.${m[3]}` : m[3];
|
|
234
|
-
|
|
235
|
-
addResolved(ctx, src, 'plugin', id, `${id}.gradle.plugin`, m[5], locOf(src, m, 5));
|
|
236
|
-
else
|
|
237
|
-
addResolved(ctx, src, 'plugin', id, `${id}.gradle.plugin`, `\${${m[6]}}`);
|
|
249
|
+
addLiteralOrRef(ctx, src, 'plugin', id, `${id}.gradle.plugin`, m, 5);
|
|
238
250
|
}
|
|
239
251
|
}
|
|
240
252
|
export function parseToml(code) {
|
|
@@ -305,6 +317,8 @@ export function parseToml(code) {
|
|
|
305
317
|
break;
|
|
306
318
|
if (code[i] === '[') {
|
|
307
319
|
const end = code.indexOf(']', i);
|
|
320
|
+
if (end < 0)
|
|
321
|
+
break;
|
|
308
322
|
table = code.slice(i + 1, end).trim();
|
|
309
323
|
i = end + 1;
|
|
310
324
|
skipLine();
|
|
@@ -354,8 +368,7 @@ function pickVersion(src, fields, path) {
|
|
|
354
368
|
function splitNotation(src, s) {
|
|
355
369
|
const parts = s.value.split(':');
|
|
356
370
|
const last = parts[parts.length - 1];
|
|
357
|
-
|
|
358
|
-
return { parts, version: tomlValue(src, { value: last, start, end: s.end }) };
|
|
371
|
+
return { parts, version: tomlValue(src, { value: last, start: s.end - last.length, end: s.end }) };
|
|
359
372
|
}
|
|
360
373
|
function parseCatalog(src, libraryRepos, pluginRepos, warnings) {
|
|
361
374
|
const entries = parseToml(src.code);
|
|
@@ -379,7 +392,7 @@ function parseCatalog(src, libraryRepos, pluginRepos, warnings) {
|
|
|
379
392
|
const str = e.fields.get('');
|
|
380
393
|
if (str) {
|
|
381
394
|
const { parts, version } = splitNotation(src, str);
|
|
382
|
-
if (parts.length >= 3
|
|
395
|
+
if (parts.length >= 3)
|
|
383
396
|
deps.push(makeDep('library', parts[0], parts[1], version, src.file, libraryRepos));
|
|
384
397
|
continue;
|
|
385
398
|
}
|
|
@@ -437,12 +450,17 @@ export async function scanGradle(build) {
|
|
|
437
450
|
const scope = { vars: [collectVars(settings), rootProps], catalogs };
|
|
438
451
|
pluginDeps(settings, { scope, repositories: pluginRepos, warnings, out });
|
|
439
452
|
}
|
|
440
|
-
|
|
441
|
-
const
|
|
453
|
+
const modules = await Promise.all(build.buildFiles.map(async (file) => {
|
|
454
|
+
const dir = dirname(file);
|
|
455
|
+
const [src, props] = await Promise.all([
|
|
456
|
+
file === rootBuildFile ? rootBuild : readSource(file, stripCode),
|
|
457
|
+
dir === build.root ? undefined : readSource(join(dir, 'gradle.properties'), stripProperties),
|
|
458
|
+
]);
|
|
459
|
+
return { src, moduleProps: propertyVars(props) };
|
|
460
|
+
}));
|
|
461
|
+
for (const { src, moduleProps } of modules) {
|
|
442
462
|
if (!src)
|
|
443
463
|
continue;
|
|
444
|
-
const dir = dirname(file);
|
|
445
|
-
const moduleProps = dir === build.root ? new Map() : propertyVars(await readSource(join(dir, 'gradle.properties'), stripProperties));
|
|
446
464
|
const scope = { vars: [collectVars(src), rootVars, moduleProps, rootProps], catalogs };
|
|
447
465
|
libraryDeps(src, { scope, repositories: libraryRepos(buildFileRepos(src)), warnings, out });
|
|
448
466
|
pluginDeps(src, { scope, repositories: pluginRepos, warnings, out });
|
package/dist/scan/index.js
CHANGED
|
@@ -5,21 +5,31 @@ import { scanMaven } from './maven.js';
|
|
|
5
5
|
const SKIP_DIRS = new Set(['build', 'node_modules', 'target', 'out', 'bin', 'intermediates']);
|
|
6
6
|
const SETTINGS = new Set(['settings.gradle', 'settings.gradle.kts']);
|
|
7
7
|
const BUILD_FILES = new Set(['build.gradle', 'build.gradle.kts']);
|
|
8
|
+
const isBuildFile = (name) => SETTINGS.has(name) || BUILD_FILES.has(name) || name === 'pom.xml';
|
|
9
|
+
// Symlinked directories are not followed (Dirent.isDirectory() is false for them), so link loops cannot recurse.
|
|
8
10
|
async function walk(dir, files) {
|
|
9
11
|
const entries = await readdir(dir, { withFileTypes: true }).catch(() => []);
|
|
12
|
+
// A module's src/ holds sources and test fixtures (src/it, src/test/resources), never real modules.
|
|
13
|
+
const isModule = entries.some((e) => !e.isDirectory() && isBuildFile(e.name));
|
|
10
14
|
await Promise.all(entries.map(async (entry) => {
|
|
11
15
|
const path = join(dir, entry.name);
|
|
12
16
|
if (entry.isDirectory()) {
|
|
13
|
-
|
|
17
|
+
const skip = entry.name.startsWith('.') || SKIP_DIRS.has(entry.name) || (isModule && entry.name === 'src');
|
|
18
|
+
if (!skip)
|
|
14
19
|
await walk(path, files);
|
|
15
20
|
}
|
|
16
|
-
else if (
|
|
21
|
+
else if (isBuildFile(entry.name)) {
|
|
17
22
|
files.push(path);
|
|
18
23
|
}
|
|
19
24
|
}));
|
|
20
25
|
}
|
|
21
|
-
function
|
|
22
|
-
|
|
26
|
+
function nearestBuild(dir, builds) {
|
|
27
|
+
for (let d = dir;; d = dirname(d)) {
|
|
28
|
+
if (builds.has(d))
|
|
29
|
+
return d;
|
|
30
|
+
if (dirname(d) === d)
|
|
31
|
+
return undefined;
|
|
32
|
+
}
|
|
23
33
|
}
|
|
24
34
|
function depth(path) {
|
|
25
35
|
return path.split(sep).length;
|
|
@@ -32,7 +42,7 @@ export function groupGradleBuilds(files) {
|
|
|
32
42
|
const buildFiles = files.filter((f) => BUILD_FILES.has(basename(f))).sort((a, b) => depth(a) - depth(b));
|
|
33
43
|
for (const file of buildFiles) {
|
|
34
44
|
const dir = dirname(file);
|
|
35
|
-
const owner =
|
|
45
|
+
const owner = nearestBuild(dir, builds);
|
|
36
46
|
const root = owner && basename(dir) !== 'buildSrc' ? owner : dir;
|
|
37
47
|
if (!builds.has(root))
|
|
38
48
|
builds.set(root, { root, buildFiles: [] });
|
package/dist/scan/maven.js
CHANGED
|
@@ -72,7 +72,6 @@ async function loadPom(file) {
|
|
|
72
72
|
const pom = { file, code, project, props: new Map(), parent: child(project, 'parent') };
|
|
73
73
|
pom.artifactId = childText(pom, project, 'artifactId');
|
|
74
74
|
pom.groupId = childText(pom, project, 'groupId') ?? childText(pom, pom.parent, 'groupId');
|
|
75
|
-
pom.version = childText(pom, project, 'version') ?? childText(pom, pom.parent, 'version');
|
|
76
75
|
for (const prop of child(project, 'properties')?.children ?? []) {
|
|
77
76
|
const value = textOf(pom, prop);
|
|
78
77
|
if (value)
|
|
@@ -192,7 +191,7 @@ export async function scanMaven(pomFiles) {
|
|
|
192
191
|
kind,
|
|
193
192
|
version: version.value,
|
|
194
193
|
file: pom.file,
|
|
195
|
-
repositories: kind === 'plugin' ? pluginRepos : libraryRepos,
|
|
194
|
+
repositories: kind === 'plugin' || node.parent?.parent?.name === 'plugin' ? pluginRepos : libraryRepos,
|
|
196
195
|
...(location && { location }),
|
|
197
196
|
});
|
|
198
197
|
}
|
package/dist/version.js
CHANGED
|
@@ -44,10 +44,6 @@ export function isSnapshot(v) {
|
|
|
44
44
|
export function isPrerelease(v) {
|
|
45
45
|
return tokenize(v).some((t) => PRERELEASE.has(t));
|
|
46
46
|
}
|
|
47
|
-
export function isDynamic(v) {
|
|
48
|
-
const s = v.trim();
|
|
49
|
-
return /^[[(]/.test(s) || /[\])]$/.test(s) || s.endsWith('+') || /^latest\./i.test(s) || /^(LATEST|RELEASE)$/.test(s);
|
|
50
|
-
}
|
|
51
47
|
export function upgradeLevel(from, to) {
|
|
52
48
|
if (compareVersions(to, from) <= 0)
|
|
53
49
|
return undefined;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "jvm-upgrade",
|
|
3
|
-
"version": "0.1
|
|
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": {
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
"LICENSE"
|
|
13
13
|
],
|
|
14
14
|
"engines": {
|
|
15
|
-
"node": ">=
|
|
15
|
+
"node": ">=22.13"
|
|
16
16
|
},
|
|
17
17
|
"scripts": {
|
|
18
18
|
"build": "rm -rf dist && tsc -p tsconfig.build.json",
|