dep-radius 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +78 -0
- package/dist/cli.js +179 -0
- package/dist/render/progress.js +53 -0
- package/dist/render/terminal.js +121 -0
- package/package.json +64 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 prakticode
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# dep-radius
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/dep-radius)
|
|
4
|
+
[](https://github.com/prakticode/dep-radius/actions/workflows/ci.yml)
|
|
5
|
+
[](LICENSE)
|
|
6
|
+
|
|
7
|
+
Which changes in a dependency update land on code you actually wrote.
|
|
8
|
+
|
|
9
|
+
```sh
|
|
10
|
+
npx dep-radius # every direct dependency with an update waiting
|
|
11
|
+
npx dep-radius schemakit # one package
|
|
12
|
+
npx dep-radius schemakit@3.2.0 # an exact target
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
```
|
|
16
|
+
◇ Read 412 files
|
|
17
|
+
◇ Checked 38 packages in 9.4s
|
|
18
|
+
|
|
19
|
+
schemakit 3.1.4 → 3.2.0 minor published 3d ago REVIEW
|
|
20
|
+
surface changes ......... 12
|
|
21
|
+
changes you touch ....... 0
|
|
22
|
+
notes mentioning you .... 1 of 18, notes for 1/1 versions (github-release)
|
|
23
|
+
|
|
24
|
+
3.2.0 ⚠️ The email pattern no longer accepts quoted local parts
|
|
25
|
+
you use: email
|
|
26
|
+
src/signup/schema.ts:14
|
|
27
|
+
src/billing/contact.ts:9 (via src/lib/validation.ts)
|
|
28
|
+
|
|
29
|
+
...
|
|
30
|
+
|
|
31
|
+
38 with an update 31 quiet · 7 review · 0 blocked · 12 up to date
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
No account, no configuration file, no build. It reads the folder as it is: any installer, one
|
|
35
|
+
manifest or fifty, TypeScript or plain JavaScript.
|
|
36
|
+
|
|
37
|
+
## How it decides
|
|
38
|
+
|
|
39
|
+
Two nets, counted apart and never merged into one reassuring number:
|
|
40
|
+
|
|
41
|
+
1. **The type surface.** Both versions' declaration files are compared, and every changed or removed
|
|
42
|
+
export is matched against the names your code resolves into the package, through local re-export
|
|
43
|
+
files and values built in other files.
|
|
44
|
+
2. **The release notes.** GitHub releases or the changelog, split into entries, kept only when they
|
|
45
|
+
name something you use.
|
|
46
|
+
|
|
47
|
+
| Verdict | Exit | Means |
|
|
48
|
+
| ------- | ---- | -------------------------------------------------------------------- |
|
|
49
|
+
| quiet | 0 | nothing you use changed, no note mentions it: merge without reading |
|
|
50
|
+
| review | 1 | here are the lines concerned, or here is what the tool could not see |
|
|
51
|
+
| blocked | 2 | an export you call was removed |
|
|
52
|
+
|
|
53
|
+
Anything the tool cannot see pushes towards review, never towards quiet: packages used only from
|
|
54
|
+
scripts, config strings or CSS, names passed around whole, packages with neither types nor notes. A
|
|
55
|
+
quiet verdict that stood on one net only is marked `*`.
|
|
56
|
+
|
|
57
|
+
## Options
|
|
58
|
+
|
|
59
|
+
`--json`, `--markdown` (a pull request comment), `--offline`, `--min-age <duration>` (default 1d, or
|
|
60
|
+
the project's `minimumReleaseAge`), `--latest`, `--no-notes`, `--no-surface`, `--prod`, `--verbose`.
|
|
61
|
+
Set `GITHUB_TOKEN`, or log in with `gh`, to lift GitHub's limit from 60 to 5000 requests an hour.
|
|
62
|
+
|
|
63
|
+
`--json` is a stable contract, `schemaVersion: 1`, described by
|
|
64
|
+
[`brief-v1.schema.json`](../core/schema/brief-v1.schema.json), shipped in `@dep-radius/core` as
|
|
65
|
+
`@dep-radius/core/schema/brief-v1.schema.json`. Version 1 only ever gains fields; removing or
|
|
66
|
+
renaming one means version 2.
|
|
67
|
+
|
|
68
|
+
Everything downloaded is cached: a second run is offline and takes about as long as reading your
|
|
69
|
+
files. Type surfaces are keyed by tarball integrity, so they are the same for everyone.
|
|
70
|
+
|
|
71
|
+
## Limits
|
|
72
|
+
|
|
73
|
+
- Behaviour changes are only seen through release notes.
|
|
74
|
+
- No type checker runs over your code: a member reached through a callback or a value from an
|
|
75
|
+
unfollowed place is matched by name only.
|
|
76
|
+
- A package whose declarations re-export another package's types is judged from its notes only.
|
|
77
|
+
- Release notes are read from GitHub and the package's own changelog.
|
|
78
|
+
- Transitive dependencies are out of scope; a lockfile and an install cooldown own that risk.
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { resolve } from "node:path";
|
|
3
|
+
import { parseArgs } from "node:util";
|
|
4
|
+
import { createRequire } from "node:module";
|
|
5
|
+
import { existsSync, statSync } from "node:fs";
|
|
6
|
+
import { runDebug } from "@dep-radius/core/debug";
|
|
7
|
+
import { createCtx, defaultCacheDir, parseDuration, renderJson, renderMarkdown, run, } from "@dep-radius/core";
|
|
8
|
+
import { renderTerminal } from "./render/terminal.js";
|
|
9
|
+
import { createProgressView } from "./render/progress.js";
|
|
10
|
+
const HELP = `radius: which changes in a dependency update land on code you wrote
|
|
11
|
+
|
|
12
|
+
usage
|
|
13
|
+
radius [path] every direct dependency with an update waiting
|
|
14
|
+
radius <pkg>[@<version|tag>] ... only these packages (a version analyses that exact target)
|
|
15
|
+
|
|
16
|
+
options
|
|
17
|
+
--json the brief as JSON (schemaVersion 1)
|
|
18
|
+
--markdown a pull request comment
|
|
19
|
+
--offline read the cache only, never the network
|
|
20
|
+
--min-age <duration> hold back versions younger than this (default 1d, or the project's
|
|
21
|
+
minimumReleaseAge); "0" disables
|
|
22
|
+
--latest target the newest version, across majors
|
|
23
|
+
--no-notes do not read release notes
|
|
24
|
+
--no-surface do not compare type surfaces
|
|
25
|
+
--prod skip devDependencies
|
|
26
|
+
--concurrency <n> network requests at once (default 8)
|
|
27
|
+
--cache-dir <dir> default $RADIUS_CACHE_DIR, then the platform cache folder
|
|
28
|
+
--verbose every note, every site, every quiet package
|
|
29
|
+
--version, --help
|
|
30
|
+
|
|
31
|
+
exit codes
|
|
32
|
+
0 quiet: nothing you use changed 1 review: read the brief 2 blocked: something you use was removed
|
|
33
|
+
3 the command itself failed
|
|
34
|
+
|
|
35
|
+
GITHUB_TOKEN, GH_TOKEN or a logged in gh raise the GitHub limit from 60 to 5000 requests an hour.
|
|
36
|
+
`;
|
|
37
|
+
async function main(argv) {
|
|
38
|
+
if (argv[0] === "debug")
|
|
39
|
+
return runDebug(argv.slice(1));
|
|
40
|
+
let parsed;
|
|
41
|
+
try {
|
|
42
|
+
parsed = parseArgs({
|
|
43
|
+
args: argv,
|
|
44
|
+
allowPositionals: true,
|
|
45
|
+
allowNegative: true,
|
|
46
|
+
options: {
|
|
47
|
+
json: { type: "boolean" },
|
|
48
|
+
markdown: { type: "boolean" },
|
|
49
|
+
offline: { type: "boolean" },
|
|
50
|
+
"min-age": { type: "string" },
|
|
51
|
+
latest: { type: "boolean" },
|
|
52
|
+
notes: { type: "boolean", default: true },
|
|
53
|
+
surface: { type: "boolean", default: true },
|
|
54
|
+
prod: { type: "boolean" },
|
|
55
|
+
concurrency: { type: "string" },
|
|
56
|
+
"cache-dir": { type: "string" },
|
|
57
|
+
verbose: { type: "boolean", short: "v" },
|
|
58
|
+
now: { type: "string" },
|
|
59
|
+
version: { type: "boolean" },
|
|
60
|
+
help: { type: "boolean", short: "h" },
|
|
61
|
+
},
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
catch (error) {
|
|
65
|
+
process.stderr.write(`radius: ${error.message}\n\n${HELP}`);
|
|
66
|
+
return 3;
|
|
67
|
+
}
|
|
68
|
+
const v = parsed.values;
|
|
69
|
+
if (v.help) {
|
|
70
|
+
process.stdout.write(HELP);
|
|
71
|
+
return 0;
|
|
72
|
+
}
|
|
73
|
+
if (v.version) {
|
|
74
|
+
process.stdout.write(`${cliVersion()}\n`);
|
|
75
|
+
return 0;
|
|
76
|
+
}
|
|
77
|
+
if (v.json && v.markdown) {
|
|
78
|
+
process.stderr.write("radius: --json and --markdown cannot be combined\n");
|
|
79
|
+
return 3;
|
|
80
|
+
}
|
|
81
|
+
let root = process.cwd();
|
|
82
|
+
const specs = [];
|
|
83
|
+
// a path is written like one (./x, ../x, /x, a/b that exists); a bare word is a package, so a
|
|
84
|
+
// folder that shares a package's name is reached as ./name
|
|
85
|
+
for (const p of parsed.positionals) {
|
|
86
|
+
const abs = resolve(p);
|
|
87
|
+
const pathLike = p === "." ||
|
|
88
|
+
p.startsWith("./") ||
|
|
89
|
+
p.startsWith("../") ||
|
|
90
|
+
p.startsWith("/") ||
|
|
91
|
+
p.includes("\\") ||
|
|
92
|
+
(p.includes("/") && !p.startsWith("@"));
|
|
93
|
+
if (pathLike && existsSync(abs) && statSync(abs).isDirectory())
|
|
94
|
+
root = abs;
|
|
95
|
+
else if (pathLike) {
|
|
96
|
+
process.stderr.write(`radius: ${p} is not a directory\n`);
|
|
97
|
+
return 3;
|
|
98
|
+
}
|
|
99
|
+
else
|
|
100
|
+
specs.push(p);
|
|
101
|
+
}
|
|
102
|
+
if (!existsSync(root)) {
|
|
103
|
+
process.stderr.write(`radius: ${root} does not exist\n`);
|
|
104
|
+
return 3;
|
|
105
|
+
}
|
|
106
|
+
const nowRaw = v.now ?? process.env.RADIUS_NOW;
|
|
107
|
+
const now = nowRaw ? Date.parse(nowRaw) : Date.now();
|
|
108
|
+
if (!Number.isFinite(now)) {
|
|
109
|
+
process.stderr.write(`radius: invalid --now ${nowRaw}\n`);
|
|
110
|
+
return 3;
|
|
111
|
+
}
|
|
112
|
+
let minAgeMs;
|
|
113
|
+
try {
|
|
114
|
+
minAgeMs =
|
|
115
|
+
v["min-age"] !== undefined ? parseDuration(v["min-age"]) : undefined;
|
|
116
|
+
}
|
|
117
|
+
catch (error) {
|
|
118
|
+
process.stderr.write(`radius: ${error.message}\n`);
|
|
119
|
+
return 3;
|
|
120
|
+
}
|
|
121
|
+
const format = v.json ? "json" : v.markdown ? "markdown" : "terminal";
|
|
122
|
+
const opts = {
|
|
123
|
+
root,
|
|
124
|
+
specs,
|
|
125
|
+
format,
|
|
126
|
+
offline: !!v.offline,
|
|
127
|
+
minAgeMs,
|
|
128
|
+
latest: !!v.latest,
|
|
129
|
+
notes: v.notes !== false,
|
|
130
|
+
surface: v.surface !== false,
|
|
131
|
+
prod: !!v.prod,
|
|
132
|
+
concurrency: Math.max(1, Number(v.concurrency ?? 8) || 8),
|
|
133
|
+
cacheDir: v["cache-dir"]
|
|
134
|
+
? resolve(v["cache-dir"])
|
|
135
|
+
: defaultCacheDir(process.env),
|
|
136
|
+
verbose: !!v.verbose,
|
|
137
|
+
now,
|
|
138
|
+
color: format === "terminal" && !!process.stdout.isTTY && !process.env.NO_COLOR,
|
|
139
|
+
};
|
|
140
|
+
const ctx = createCtx(opts);
|
|
141
|
+
// a person watching gets progress on stderr; --json, --markdown, CI and pipes get none
|
|
142
|
+
const view = format === "terminal" &&
|
|
143
|
+
process.stderr.isTTY &&
|
|
144
|
+
// a pseudo terminal that reports no width wraps every frame after one character
|
|
145
|
+
(process.stderr.columns ?? 0) >= 40 &&
|
|
146
|
+
!process.env.CI
|
|
147
|
+
? createProgressView(process.stderr)
|
|
148
|
+
: undefined;
|
|
149
|
+
let brief;
|
|
150
|
+
try {
|
|
151
|
+
brief = await run(opts, ctx, view ? (e) => view.onEvent(e) : undefined);
|
|
152
|
+
}
|
|
153
|
+
catch (error) {
|
|
154
|
+
view?.fail();
|
|
155
|
+
throw error;
|
|
156
|
+
}
|
|
157
|
+
view?.finish(brief);
|
|
158
|
+
const text = format === "json"
|
|
159
|
+
? renderJson(brief)
|
|
160
|
+
: format === "markdown"
|
|
161
|
+
? renderMarkdown(brief, now)
|
|
162
|
+
: renderTerminal(brief, {
|
|
163
|
+
color: opts.color,
|
|
164
|
+
verbose: opts.verbose,
|
|
165
|
+
now,
|
|
166
|
+
});
|
|
167
|
+
process.stdout.write(text);
|
|
168
|
+
return brief.exitCode;
|
|
169
|
+
}
|
|
170
|
+
main(process.argv.slice(2)).then((code) => {
|
|
171
|
+
process.exitCode = code;
|
|
172
|
+
}, (error) => {
|
|
173
|
+
process.stderr.write(`radius: ${error instanceof Error ? (error.stack ?? error.message) : String(error)}\n`);
|
|
174
|
+
process.exitCode = 3;
|
|
175
|
+
});
|
|
176
|
+
// the version of this package, which is what `radius --version` reports; the brief carries the engine's
|
|
177
|
+
function cliVersion() {
|
|
178
|
+
return createRequire(import.meta.url)("../package.json").version;
|
|
179
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { progress, spinner } from "@clack/prompts";
|
|
2
|
+
// Drawn on stderr, and only when a person is watching: the report on stdout stays byte for byte
|
|
3
|
+
// what a script or an agent reads.
|
|
4
|
+
export function createProgressView(output, now = Date.now) {
|
|
5
|
+
const started = now();
|
|
6
|
+
const reading = spinner({ output, withGuide: false });
|
|
7
|
+
reading.start("Reading project files");
|
|
8
|
+
let bar;
|
|
9
|
+
let files = 0;
|
|
10
|
+
const running = new Set();
|
|
11
|
+
return {
|
|
12
|
+
onEvent(e) {
|
|
13
|
+
if (e.type === "files") {
|
|
14
|
+
files = e.total;
|
|
15
|
+
reading.message(`Reading project files ${e.done}/${e.total}`);
|
|
16
|
+
}
|
|
17
|
+
else if (e.type === "packages") {
|
|
18
|
+
reading.stop(`Read ${files} ${files === 1 ? "file" : "files"}`);
|
|
19
|
+
if (e.total === 0)
|
|
20
|
+
return;
|
|
21
|
+
bar = progress({ output, withGuide: false, max: e.total, size: 30 });
|
|
22
|
+
bar.start(`Checking ${e.total} ${e.total === 1 ? "package" : "packages"}`);
|
|
23
|
+
}
|
|
24
|
+
else if (e.type === "package-start") {
|
|
25
|
+
running.add(e.name);
|
|
26
|
+
}
|
|
27
|
+
else if (bar) {
|
|
28
|
+
running.delete(e.name);
|
|
29
|
+
const percent = Math.round((100 * e.done) / e.total);
|
|
30
|
+
// every package starts at once, so names only help once a few slow ones remain
|
|
31
|
+
const waiting = running.size > 0 && running.size <= 3
|
|
32
|
+
? ` waiting on ${[...running].join(", ")}`
|
|
33
|
+
: "";
|
|
34
|
+
bar.advance(1, `${e.done}/${e.total} ${percent}%${waiting}`);
|
|
35
|
+
}
|
|
36
|
+
},
|
|
37
|
+
finish(brief) {
|
|
38
|
+
const seconds = ((now() - started) / 1000).toFixed(1);
|
|
39
|
+
const checked = brief.packages.length + brief.upToDate;
|
|
40
|
+
const message = `Checked ${checked} ${checked === 1 ? "package" : "packages"} in ${seconds}s`;
|
|
41
|
+
if (bar)
|
|
42
|
+
bar.stop(message);
|
|
43
|
+
else
|
|
44
|
+
reading.stop(message);
|
|
45
|
+
},
|
|
46
|
+
fail() {
|
|
47
|
+
if (bar)
|
|
48
|
+
bar.error("Stopped");
|
|
49
|
+
else
|
|
50
|
+
reading.error("Stopped");
|
|
51
|
+
},
|
|
52
|
+
};
|
|
53
|
+
}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { styleText } from "node:util";
|
|
2
|
+
import { ago, coverageLabel, groupBriefs, oneNetOnly, opaqueLabel, siteLabel, sitesFor, surfaceLabel, } from "@dep-radius/core/render";
|
|
3
|
+
export function renderTerminal(brief, opts) {
|
|
4
|
+
const c = (style, s) => (opts.color ? styleText(style, s) : s);
|
|
5
|
+
const out = [];
|
|
6
|
+
const g = groupBriefs(brief);
|
|
7
|
+
const sources = [...new Set(brief.packages.map((p) => p.versionSource))].join(", ") ||
|
|
8
|
+
"none";
|
|
9
|
+
out.push(`${c("bold", "radius")} ${brief.root}`);
|
|
10
|
+
out.push(c("dim", `${brief.manifests} ${brief.manifests === 1 ? "manifest" : "manifests"} · ${brief.packages.length} with an update · ${brief.upToDate} up to date · versions from ${sources}`));
|
|
11
|
+
out.push("");
|
|
12
|
+
const siteCap = opts.verbose ? Number.POSITIVE_INFINITY : 5;
|
|
13
|
+
for (const p of g.detailed) {
|
|
14
|
+
out.push(...packageBlock(p, c, siteCap, opts));
|
|
15
|
+
out.push("");
|
|
16
|
+
}
|
|
17
|
+
if (g.quiet.length > 0) {
|
|
18
|
+
out.push(`${c("green", "quiet")} (${g.quiet.length}) ${c("dim", "merge without reading")}`);
|
|
19
|
+
const items = g.quiet.map((p) => `${p.pkg} ${p.from} → ${p.to}${oneNetOnly(p) ? "*" : ""}`);
|
|
20
|
+
if (opts.verbose)
|
|
21
|
+
for (const p of g.quiet)
|
|
22
|
+
out.push(` ${p.pkg} ${p.from} → ${p.to} ${c("dim", p.reasons.map((r) => r.detail).join("; "))}`);
|
|
23
|
+
else
|
|
24
|
+
out.push(` ${items.join(", ")}`);
|
|
25
|
+
if (!opts.verbose && g.quiet.some(oneNetOnly))
|
|
26
|
+
out.push(c("dim", " * judged on one net only: no types, or notes missing for some versions (--verbose)"));
|
|
27
|
+
out.push("");
|
|
28
|
+
}
|
|
29
|
+
if (g.unseen.length > 0) {
|
|
30
|
+
out.push(`${c("yellow", "cannot see how you use these")} (${g.unseen.length}) ${c("dim", "run your build or tests")}`);
|
|
31
|
+
for (const p of g.unseen)
|
|
32
|
+
out.push(` ${p.pkg} ${p.from} → ${p.to} ${c("dim", p.bump)} ${c("dim", opaqueLabel(p))}`);
|
|
33
|
+
out.push("");
|
|
34
|
+
}
|
|
35
|
+
if (brief.notAnalyzed.length > 0) {
|
|
36
|
+
out.push(c("dim", `not analysed (${brief.notAnalyzed.length})`));
|
|
37
|
+
for (const n of brief.notAnalyzed)
|
|
38
|
+
out.push(c("dim", ` ${n.pkg}: ${n.reason}`));
|
|
39
|
+
out.push("");
|
|
40
|
+
}
|
|
41
|
+
const globalLine = brief.global.map((x) => `${x.count} ${x.kind}`).join(", ");
|
|
42
|
+
if (globalLine)
|
|
43
|
+
out.push(c("dim", `whole project: ${globalLine}`));
|
|
44
|
+
if (brief.limits.length > 0) {
|
|
45
|
+
out.push(c("dim", "limits"));
|
|
46
|
+
for (const l of brief.limits)
|
|
47
|
+
out.push(c("dim", ` - ${l}`));
|
|
48
|
+
}
|
|
49
|
+
const rateLimited = brief.packages.some((p) => p.notes.perVersion.some((v) => v.reason === "rate-limited"));
|
|
50
|
+
if (rateLimited) {
|
|
51
|
+
out.push("");
|
|
52
|
+
out.push(c("yellow", "GitHub's rate limit was reached, so some release notes were not read: set GITHUB_TOKEN or run gh auth login."));
|
|
53
|
+
}
|
|
54
|
+
const count = (v) => brief.packages.filter((p) => p.verdict === v).length;
|
|
55
|
+
out.push("");
|
|
56
|
+
out.push(`${c("bold", `${brief.packages.length} with an update`)} ${c("green", `${count("quiet")} quiet`)} · ${c("yellow", `${count("review")} review`)} · ${c("red", `${count("blocked")} blocked`)} ${c("dim", `· ${brief.upToDate} up to date`)}`);
|
|
57
|
+
return `${out.join("\n")}\n`;
|
|
58
|
+
}
|
|
59
|
+
function packageBlock(p, c, siteCap, opts) {
|
|
60
|
+
const lines = [];
|
|
61
|
+
const verdict = p.verdict === "blocked"
|
|
62
|
+
? c(["bold", "red"], "BLOCKED")
|
|
63
|
+
: c(["bold", "yellow"], "REVIEW");
|
|
64
|
+
lines.push(`${c("bold", p.pkg)} ${p.from} → ${p.to} ${p.bump} ${c("dim", `published ${ago(p.publishedAt, opts.now)}`)} ${verdict}`);
|
|
65
|
+
const direct = p.notes.matched.filter((m) => m.direct).length;
|
|
66
|
+
const possibly = p.notes.matched.length - direct;
|
|
67
|
+
lines.push(` surface changes ......... ${surfaceLabel(p)}`);
|
|
68
|
+
if (p.surface.status === "computed")
|
|
69
|
+
lines.push(` changes you touch ....... ${p.surface.touched.length}`);
|
|
70
|
+
lines.push(` notes mentioning you .... ${p.notes.coverage === "disabled" ? "not read" : direct}${possibly > 0 ? c("dim", ` (+${possibly} possibly)`) : ""}${c("dim", ` of ${p.notes.total}, ${coverageLabel(p)}`)}`);
|
|
71
|
+
lines.push(c("dim", ` used in ${p.usage.files} ${p.usage.files === 1 ? "file" : "files"}, ${p.usage.sites.length} sites · ${p.manifests.join(", ")}`));
|
|
72
|
+
const touchCap = opts.verbose ? Number.POSITIVE_INFINITY : 10;
|
|
73
|
+
for (const t of p.surface.touched.slice(0, touchCap)) {
|
|
74
|
+
lines.push("");
|
|
75
|
+
const label = t.bucket === "removed" ? c("red", "removed") : t.bucket;
|
|
76
|
+
lines.push(` ${label} ${c("bold", t.change.path)}${t.strength === "weak" ? c("dim", " (by member name)") : ""}`);
|
|
77
|
+
if (t.change.before)
|
|
78
|
+
lines.push(c("dim", ` before ${t.change.before.join(" | ").slice(0, 200)}`));
|
|
79
|
+
if (t.change.after)
|
|
80
|
+
lines.push(c("dim", ` after ${t.change.after.join(" | ").slice(0, 200)}`));
|
|
81
|
+
for (const s of t.sites.slice(0, siteCap))
|
|
82
|
+
lines.push(` ${siteLabel(s)}`);
|
|
83
|
+
if (t.sites.length > siteCap)
|
|
84
|
+
lines.push(c("dim", ` ... ${t.sites.length - siteCap} more`));
|
|
85
|
+
}
|
|
86
|
+
if (p.surface.touched.length > touchCap)
|
|
87
|
+
lines.push("", c("dim", ` ... ${p.surface.touched.length - touchCap} more changes you touch (--verbose)`));
|
|
88
|
+
const shownNotes = opts.verbose
|
|
89
|
+
? p.notes.matched
|
|
90
|
+
: p.notes.matched.slice(0, 8);
|
|
91
|
+
if (shownNotes.length > 0)
|
|
92
|
+
lines.push("");
|
|
93
|
+
for (const m of shownNotes) {
|
|
94
|
+
const names = [...new Set(m.hits.map((h) => h.name))];
|
|
95
|
+
const tag = m.direct ? "" : c("dim", " possibly");
|
|
96
|
+
lines.push(` ${c("cyan", m.entry.version)} ${m.entry.title}${tag}`);
|
|
97
|
+
const strongNames = m.hits
|
|
98
|
+
.filter((h) => h.strength === "strong")
|
|
99
|
+
.map((h) => h.name);
|
|
100
|
+
const sites = sitesFor(p, strongNames.length > 0 ? strongNames : names);
|
|
101
|
+
lines.push(c("dim", ` you use: ${names.join(", ")}`));
|
|
102
|
+
for (const s of sites.slice(0, m.direct ? Math.min(3, siteCap) : 1))
|
|
103
|
+
lines.push(` ${siteLabel(s)}`);
|
|
104
|
+
if (sites.length > (m.direct ? Math.min(3, siteCap) : 1))
|
|
105
|
+
lines.push(c("dim", ` ... ${sites.length - (m.direct ? Math.min(3, siteCap) : 1)} more sites`));
|
|
106
|
+
}
|
|
107
|
+
if (p.notes.matched.length > shownNotes.length)
|
|
108
|
+
lines.push(c("dim", ` ... ${p.notes.matched.length - shownNotes.length} more notes (--verbose)`));
|
|
109
|
+
for (const e of p.notes.unattributedBreaking)
|
|
110
|
+
lines.push(` ${c("cyan", e.version)} ${e.title} ${c("dim", "breaking, names no API")}`);
|
|
111
|
+
lines.push("");
|
|
112
|
+
for (const r of p.reasons)
|
|
113
|
+
lines.push(c("dim", ` why: ${r.detail}`));
|
|
114
|
+
const skipped = p.skippedNewer.filter((s) => s.reason === "too-new");
|
|
115
|
+
if (skipped.length > 0) {
|
|
116
|
+
lines.push(c("dim", ` newer, held back: ${skipped.map((s) => `${s.version} (${s.publishedAt ? ago(s.publishedAt, opts.now) : "too new"})`).join(", ")}`));
|
|
117
|
+
}
|
|
118
|
+
if (p.alsoAvailable)
|
|
119
|
+
lines.push(c("dim", ` also available: ${p.alsoAvailable.version} (${p.alsoAvailable.bump}, not analysed; radius ${p.pkg}@${p.alsoAvailable.version})`));
|
|
120
|
+
return lines;
|
|
121
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "dep-radius",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Tells you which changes in a dependency update land on code you actually wrote.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"author": "prakticode",
|
|
7
|
+
"homepage": "https://github.com/prakticode/dep-radius#readme",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "git+https://github.com/prakticode/dep-radius.git",
|
|
11
|
+
"directory": "packages/cli"
|
|
12
|
+
},
|
|
13
|
+
"bugs": {
|
|
14
|
+
"url": "https://github.com/prakticode/dep-radius/issues"
|
|
15
|
+
},
|
|
16
|
+
"keywords": [
|
|
17
|
+
"dependencies",
|
|
18
|
+
"upgrade",
|
|
19
|
+
"semver",
|
|
20
|
+
"changelog",
|
|
21
|
+
"release-notes",
|
|
22
|
+
"renovate",
|
|
23
|
+
"dependabot",
|
|
24
|
+
"breaking-changes",
|
|
25
|
+
"typescript",
|
|
26
|
+
"cli"
|
|
27
|
+
],
|
|
28
|
+
"type": "module",
|
|
29
|
+
"bin": {
|
|
30
|
+
"radius": "dist/cli.js",
|
|
31
|
+
"dep-radius": "dist/cli.js"
|
|
32
|
+
},
|
|
33
|
+
"files": [
|
|
34
|
+
"dist"
|
|
35
|
+
],
|
|
36
|
+
"engines": {
|
|
37
|
+
"node": ">=22.12"
|
|
38
|
+
},
|
|
39
|
+
"publishConfig": {
|
|
40
|
+
"access": "public"
|
|
41
|
+
},
|
|
42
|
+
"dependencies": {
|
|
43
|
+
"@clack/prompts": "^1.8.0",
|
|
44
|
+
"@dep-radius/core": "^0.1.0"
|
|
45
|
+
},
|
|
46
|
+
"devDependencies": {
|
|
47
|
+
"@repo/eslint-config": "0.0.0",
|
|
48
|
+
"@repo/typescript-config": "0.0.0",
|
|
49
|
+
"@types/node": "^24.0.0",
|
|
50
|
+
"eslint": "^9.39.5",
|
|
51
|
+
"typescript": "~6.0.3",
|
|
52
|
+
"vitest": "^5.0.0"
|
|
53
|
+
},
|
|
54
|
+
"scripts": {
|
|
55
|
+
"build": "tsc -p tsconfig.build.json",
|
|
56
|
+
"dev": "node src/cli.ts",
|
|
57
|
+
"typecheck": "tsc --noEmit",
|
|
58
|
+
"lint": "eslint --max-warnings 0",
|
|
59
|
+
"lint:fix": "eslint --fix",
|
|
60
|
+
"test": "vitest run",
|
|
61
|
+
"test:watch": "vitest",
|
|
62
|
+
"clean": "rm -rf dist .turbo"
|
|
63
|
+
}
|
|
64
|
+
}
|