@rainy-updates/cli 0.5.1 → 0.5.2-rc.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/CHANGELOG.md +68 -1
- package/README.md +84 -25
- package/dist/bin/cli.js +30 -0
- package/dist/commands/audit/mapper.js +1 -1
- package/dist/commands/licenses/parser.d.ts +2 -0
- package/dist/commands/licenses/parser.js +116 -0
- package/dist/commands/licenses/runner.d.ts +9 -0
- package/dist/commands/licenses/runner.js +163 -0
- package/dist/commands/licenses/sbom.d.ts +10 -0
- package/dist/commands/licenses/sbom.js +70 -0
- package/dist/commands/resolve/graph/builder.d.ts +20 -0
- package/dist/commands/resolve/graph/builder.js +183 -0
- package/dist/commands/resolve/graph/conflict.d.ts +20 -0
- package/dist/commands/resolve/graph/conflict.js +52 -0
- package/dist/commands/resolve/graph/resolver.d.ts +17 -0
- package/dist/commands/resolve/graph/resolver.js +71 -0
- package/dist/commands/resolve/parser.d.ts +2 -0
- package/dist/commands/resolve/parser.js +89 -0
- package/dist/commands/resolve/runner.d.ts +13 -0
- package/dist/commands/resolve/runner.js +136 -0
- package/dist/commands/snapshot/parser.d.ts +2 -0
- package/dist/commands/snapshot/parser.js +80 -0
- package/dist/commands/snapshot/runner.d.ts +11 -0
- package/dist/commands/snapshot/runner.js +115 -0
- package/dist/commands/snapshot/store.d.ts +35 -0
- package/dist/commands/snapshot/store.js +158 -0
- package/dist/commands/unused/matcher.d.ts +22 -0
- package/dist/commands/unused/matcher.js +95 -0
- package/dist/commands/unused/parser.d.ts +2 -0
- package/dist/commands/unused/parser.js +95 -0
- package/dist/commands/unused/runner.d.ts +11 -0
- package/dist/commands/unused/runner.js +113 -0
- package/dist/commands/unused/scanner.d.ts +18 -0
- package/dist/commands/unused/scanner.js +129 -0
- package/dist/core/impact.d.ts +36 -0
- package/dist/core/impact.js +82 -0
- package/dist/core/options.d.ts +13 -1
- package/dist/core/options.js +35 -13
- package/dist/types/index.d.ts +153 -0
- package/dist/utils/semver.d.ts +18 -0
- package/dist/utils/semver.js +88 -3
- package/package.json +1 -1
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
const VALID_ACTIONS = ["save", "list", "restore", "diff"];
|
|
2
|
+
export function parseSnapshotArgs(args) {
|
|
3
|
+
const options = {
|
|
4
|
+
cwd: process.cwd(),
|
|
5
|
+
workspace: false,
|
|
6
|
+
action: "list",
|
|
7
|
+
label: undefined,
|
|
8
|
+
snapshotId: undefined,
|
|
9
|
+
storeFile: undefined,
|
|
10
|
+
};
|
|
11
|
+
// First positional arg is the action
|
|
12
|
+
let positionalIndex = 0;
|
|
13
|
+
const positionals = [];
|
|
14
|
+
for (let i = 0; i < args.length; i++) {
|
|
15
|
+
const current = args[i];
|
|
16
|
+
const next = args[i + 1];
|
|
17
|
+
if (current === "--cwd" && next) {
|
|
18
|
+
options.cwd = next;
|
|
19
|
+
i++;
|
|
20
|
+
continue;
|
|
21
|
+
}
|
|
22
|
+
if (current === "--cwd")
|
|
23
|
+
throw new Error("Missing value for --cwd");
|
|
24
|
+
if (current === "--workspace") {
|
|
25
|
+
options.workspace = true;
|
|
26
|
+
continue;
|
|
27
|
+
}
|
|
28
|
+
if (current === "--label" && next) {
|
|
29
|
+
options.label = next;
|
|
30
|
+
i++;
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
if (current === "--label")
|
|
34
|
+
throw new Error("Missing value for --label");
|
|
35
|
+
if (current === "--store" && next) {
|
|
36
|
+
options.storeFile = next;
|
|
37
|
+
i++;
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
if (current === "--store")
|
|
41
|
+
throw new Error("Missing value for --store");
|
|
42
|
+
if (current === "--help" || current === "-h") {
|
|
43
|
+
process.stdout.write(SNAPSHOT_HELP);
|
|
44
|
+
process.exit(0);
|
|
45
|
+
}
|
|
46
|
+
if (current.startsWith("-"))
|
|
47
|
+
throw new Error(`Unknown option: ${current}`);
|
|
48
|
+
// Positional arguments
|
|
49
|
+
positionals.push(current);
|
|
50
|
+
}
|
|
51
|
+
// positionals[0] = action, positionals[1] = id/label for restore/diff
|
|
52
|
+
if (positionals.length >= 1) {
|
|
53
|
+
const action = positionals[0];
|
|
54
|
+
if (!VALID_ACTIONS.includes(action)) {
|
|
55
|
+
throw new Error(`Unknown snapshot action: "${positionals[0]}". Valid: ${VALID_ACTIONS.join(", ")}`);
|
|
56
|
+
}
|
|
57
|
+
options.action = action;
|
|
58
|
+
}
|
|
59
|
+
if (positionals.length >= 2) {
|
|
60
|
+
// For restore/diff the second positional is the id/label
|
|
61
|
+
options.snapshotId = positionals[1];
|
|
62
|
+
}
|
|
63
|
+
return options;
|
|
64
|
+
}
|
|
65
|
+
const SNAPSHOT_HELP = `
|
|
66
|
+
rup snapshot — Save, list, restore, and diff dependency state snapshots
|
|
67
|
+
|
|
68
|
+
Usage:
|
|
69
|
+
rup snapshot save [--label <name>] Save current dependency state
|
|
70
|
+
rup snapshot list List all saved snapshots
|
|
71
|
+
rup snapshot restore <id|label> Restore package.json files from a snapshot
|
|
72
|
+
rup snapshot diff <id|label> Show changes since a snapshot
|
|
73
|
+
|
|
74
|
+
Options:
|
|
75
|
+
--label <name> Human-readable label for the snapshot
|
|
76
|
+
--store <path> Custom snapshot store file (default: .rup-snapshots.json)
|
|
77
|
+
--workspace Include all workspace packages
|
|
78
|
+
--cwd <path> Working directory (default: cwd)
|
|
79
|
+
--help Show this help
|
|
80
|
+
`.trimStart();
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { SnapshotOptions, SnapshotResult } from "../../types/index.js";
|
|
2
|
+
/**
|
|
3
|
+
* Entry point for `rup snapshot`. Lazy-loaded by cli.ts.
|
|
4
|
+
*
|
|
5
|
+
* Actions:
|
|
6
|
+
* save — Capture package.json + lockfile state → store
|
|
7
|
+
* list — Print all saved snapshots
|
|
8
|
+
* restore — Restore a snapshot by id or label
|
|
9
|
+
* diff — Show dependency changes since a snapshot
|
|
10
|
+
*/
|
|
11
|
+
export declare function runSnapshot(options: SnapshotOptions): Promise<SnapshotResult>;
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import process from "node:process";
|
|
2
|
+
import { discoverPackageDirs } from "../../workspace/discover.js";
|
|
3
|
+
import { SnapshotStore, captureState, restoreState, diffManifests, } from "./store.js";
|
|
4
|
+
/**
|
|
5
|
+
* Entry point for `rup snapshot`. Lazy-loaded by cli.ts.
|
|
6
|
+
*
|
|
7
|
+
* Actions:
|
|
8
|
+
* save — Capture package.json + lockfile state → store
|
|
9
|
+
* list — Print all saved snapshots
|
|
10
|
+
* restore — Restore a snapshot by id or label
|
|
11
|
+
* diff — Show dependency changes since a snapshot
|
|
12
|
+
*/
|
|
13
|
+
export async function runSnapshot(options) {
|
|
14
|
+
const result = {
|
|
15
|
+
action: options.action,
|
|
16
|
+
errors: [],
|
|
17
|
+
warnings: [],
|
|
18
|
+
};
|
|
19
|
+
const packageDirs = await discoverPackageDirs(options.cwd, options.workspace);
|
|
20
|
+
const store = new SnapshotStore(options.cwd, options.storeFile);
|
|
21
|
+
switch (options.action) {
|
|
22
|
+
// ─ save ──────────────────────────────────────────────────────────────────
|
|
23
|
+
case "save": {
|
|
24
|
+
const label = options.label ??
|
|
25
|
+
`snap-${new Date().toISOString().replace(/[:T]/g, "-").slice(0, 19)}`;
|
|
26
|
+
const { manifests, lockfileHashes } = await captureState(packageDirs);
|
|
27
|
+
const entry = await store.saveSnapshot(manifests, lockfileHashes, label);
|
|
28
|
+
result.snapshotId = entry.id;
|
|
29
|
+
result.label = entry.label;
|
|
30
|
+
process.stdout.write(`✔ Snapshot saved: ${entry.label} (${entry.id})\n`);
|
|
31
|
+
break;
|
|
32
|
+
}
|
|
33
|
+
// ─ list ──────────────────────────────────────────────────────────────────
|
|
34
|
+
case "list": {
|
|
35
|
+
const entries = await store.listSnapshots();
|
|
36
|
+
result.entries = entries.map((e) => ({
|
|
37
|
+
id: e.id,
|
|
38
|
+
label: e.label,
|
|
39
|
+
createdAt: new Date(e.createdAt).toISOString(),
|
|
40
|
+
}));
|
|
41
|
+
if (entries.length === 0) {
|
|
42
|
+
process.stdout.write("No snapshots saved yet. Use `rup snapshot save` to create one.\n");
|
|
43
|
+
}
|
|
44
|
+
else {
|
|
45
|
+
process.stdout.write(`\n${entries.length} snapshot(s):\n\n`);
|
|
46
|
+
process.stdout.write(" " + "ID".padEnd(30) + "Label".padEnd(30) + "Created\n");
|
|
47
|
+
process.stdout.write(" " + "─".repeat(75) + "\n");
|
|
48
|
+
for (const e of entries) {
|
|
49
|
+
process.stdout.write(" " +
|
|
50
|
+
e.id.padEnd(30) +
|
|
51
|
+
e.label.padEnd(30) +
|
|
52
|
+
new Date(e.createdAt).toLocaleString() +
|
|
53
|
+
"\n");
|
|
54
|
+
}
|
|
55
|
+
process.stdout.write("\n");
|
|
56
|
+
}
|
|
57
|
+
break;
|
|
58
|
+
}
|
|
59
|
+
// ─ restore ───────────────────────────────────────────────────────────────
|
|
60
|
+
case "restore": {
|
|
61
|
+
const idOrLabel = options.snapshotId ?? options.label;
|
|
62
|
+
if (!idOrLabel) {
|
|
63
|
+
result.errors.push("Restore requires a snapshot ID or label. Usage: rup snapshot restore <id|label>");
|
|
64
|
+
break;
|
|
65
|
+
}
|
|
66
|
+
const entry = await store.findSnapshot(idOrLabel);
|
|
67
|
+
if (!entry) {
|
|
68
|
+
result.errors.push(`Snapshot not found: ${idOrLabel}. Use \`rup snapshot list\` to view saved snapshots.`);
|
|
69
|
+
break;
|
|
70
|
+
}
|
|
71
|
+
await restoreState(entry);
|
|
72
|
+
result.snapshotId = entry.id;
|
|
73
|
+
result.label = entry.label;
|
|
74
|
+
const count = Object.keys(entry.manifests).length;
|
|
75
|
+
process.stdout.write(`✔ Restored ${count} package.json file(s) from snapshot "${entry.label}" (${entry.id})\n`);
|
|
76
|
+
process.stdout.write(" Re-run your package manager install to apply.\n");
|
|
77
|
+
break;
|
|
78
|
+
}
|
|
79
|
+
// ─ diff ──────────────────────────────────────────────────────────────────
|
|
80
|
+
case "diff": {
|
|
81
|
+
const idOrLabel = options.snapshotId ?? options.label;
|
|
82
|
+
if (!idOrLabel) {
|
|
83
|
+
result.errors.push("Diff requires a snapshot ID or label. Usage: rup snapshot diff <id|label>");
|
|
84
|
+
break;
|
|
85
|
+
}
|
|
86
|
+
const entry = await store.findSnapshot(idOrLabel);
|
|
87
|
+
if (!entry) {
|
|
88
|
+
result.errors.push(`Snapshot not found: ${idOrLabel}`);
|
|
89
|
+
break;
|
|
90
|
+
}
|
|
91
|
+
const { manifests: currentManifests } = await captureState(packageDirs);
|
|
92
|
+
const changes = diffManifests(entry.manifests, currentManifests);
|
|
93
|
+
result.diff = changes;
|
|
94
|
+
if (changes.length === 0) {
|
|
95
|
+
process.stdout.write(`✔ No dependency changes since snapshot "${entry.label}"\n`);
|
|
96
|
+
}
|
|
97
|
+
else {
|
|
98
|
+
process.stdout.write(`\nDependency changes since snapshot "${entry.label}":\n\n`);
|
|
99
|
+
process.stdout.write(" " + "Package".padEnd(35) + "Before".padEnd(20) + "After\n");
|
|
100
|
+
process.stdout.write(" " + "─".repeat(65) + "\n");
|
|
101
|
+
for (const c of changes) {
|
|
102
|
+
process.stdout.write(" " + c.name.padEnd(35) + c.from.padEnd(20) + c.to + "\n");
|
|
103
|
+
}
|
|
104
|
+
process.stdout.write("\n");
|
|
105
|
+
}
|
|
106
|
+
break;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
if (result.errors.length > 0) {
|
|
110
|
+
for (const err of result.errors) {
|
|
111
|
+
process.stderr.write(`[snapshot] ✖ ${err}\n`);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
return result;
|
|
115
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { SnapshotEntry } from "../../types/index.js";
|
|
2
|
+
/**
|
|
3
|
+
* Lightweight SQLite-free snapshot store (uses a JSON file in the project root).
|
|
4
|
+
*
|
|
5
|
+
* Design goals:
|
|
6
|
+
* - No extra runtime dependencies (SQLite bindings vary by runtime)
|
|
7
|
+
* - Human-readable store file (git-committable if desired)
|
|
8
|
+
* - Atomic writes via tmp-rename to prevent corruption
|
|
9
|
+
* - Fast: entire store fits in memory for typical use (< 50 snapshots)
|
|
10
|
+
*/
|
|
11
|
+
export declare class SnapshotStore {
|
|
12
|
+
private readonly storePath;
|
|
13
|
+
private entries;
|
|
14
|
+
private loaded;
|
|
15
|
+
constructor(cwd: string, storeFile?: string);
|
|
16
|
+
load(): Promise<void>;
|
|
17
|
+
save(): Promise<void>;
|
|
18
|
+
saveSnapshot(manifests: Record<string, string>, lockfileHashes: Record<string, string>, label: string): Promise<SnapshotEntry>;
|
|
19
|
+
listSnapshots(): Promise<SnapshotEntry[]>;
|
|
20
|
+
findSnapshot(idOrLabel: string): Promise<SnapshotEntry | null>;
|
|
21
|
+
deleteSnapshot(idOrLabel: string): Promise<boolean>;
|
|
22
|
+
}
|
|
23
|
+
/** Captures current package.json and lockfile state for a set of directories. */
|
|
24
|
+
export declare function captureState(packageDirs: string[]): Promise<{
|
|
25
|
+
manifests: Record<string, string>;
|
|
26
|
+
lockfileHashes: Record<string, string>;
|
|
27
|
+
}>;
|
|
28
|
+
/** Restores package.json files from a snapshot's manifest map. */
|
|
29
|
+
export declare function restoreState(entry: SnapshotEntry): Promise<void>;
|
|
30
|
+
/** Computes a diff of dependency versions between two manifest snapshots. */
|
|
31
|
+
export declare function diffManifests(before: Record<string, string>, after: Record<string, string>): Array<{
|
|
32
|
+
name: string;
|
|
33
|
+
from: string;
|
|
34
|
+
to: string;
|
|
35
|
+
}>;
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { promises as fs } from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
const DEFAULT_STORE_NAME = ".rup-snapshots.json";
|
|
5
|
+
/**
|
|
6
|
+
* Lightweight SQLite-free snapshot store (uses a JSON file in the project root).
|
|
7
|
+
*
|
|
8
|
+
* Design goals:
|
|
9
|
+
* - No extra runtime dependencies (SQLite bindings vary by runtime)
|
|
10
|
+
* - Human-readable store file (git-committable if desired)
|
|
11
|
+
* - Atomic writes via tmp-rename to prevent corruption
|
|
12
|
+
* - Fast: entire store fits in memory for typical use (< 50 snapshots)
|
|
13
|
+
*/
|
|
14
|
+
export class SnapshotStore {
|
|
15
|
+
storePath;
|
|
16
|
+
entries = [];
|
|
17
|
+
loaded = false;
|
|
18
|
+
constructor(cwd, storeFile) {
|
|
19
|
+
this.storePath = storeFile
|
|
20
|
+
? path.isAbsolute(storeFile)
|
|
21
|
+
? storeFile
|
|
22
|
+
: path.join(cwd, storeFile)
|
|
23
|
+
: path.join(cwd, DEFAULT_STORE_NAME);
|
|
24
|
+
}
|
|
25
|
+
async load() {
|
|
26
|
+
if (this.loaded)
|
|
27
|
+
return;
|
|
28
|
+
try {
|
|
29
|
+
const raw = await fs.readFile(this.storePath, "utf8");
|
|
30
|
+
const parsed = JSON.parse(raw);
|
|
31
|
+
if (Array.isArray(parsed)) {
|
|
32
|
+
this.entries = parsed;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
this.entries = [];
|
|
37
|
+
}
|
|
38
|
+
this.loaded = true;
|
|
39
|
+
}
|
|
40
|
+
async save() {
|
|
41
|
+
const tmp = this.storePath + ".tmp";
|
|
42
|
+
await fs.writeFile(tmp, JSON.stringify(this.entries, null, 2) + "\n", "utf8");
|
|
43
|
+
await fs.rename(tmp, this.storePath);
|
|
44
|
+
}
|
|
45
|
+
async saveSnapshot(manifests, lockfileHashes, label) {
|
|
46
|
+
await this.load();
|
|
47
|
+
const id = `snap-${Date.now()}-${Math.random().toString(36).slice(2, 7)}`;
|
|
48
|
+
const entry = {
|
|
49
|
+
id,
|
|
50
|
+
label,
|
|
51
|
+
createdAt: Date.now(),
|
|
52
|
+
manifests,
|
|
53
|
+
lockfileHashes,
|
|
54
|
+
};
|
|
55
|
+
this.entries.push(entry);
|
|
56
|
+
await this.save();
|
|
57
|
+
return entry;
|
|
58
|
+
}
|
|
59
|
+
async listSnapshots() {
|
|
60
|
+
await this.load();
|
|
61
|
+
return [...this.entries].sort((a, b) => b.createdAt - a.createdAt);
|
|
62
|
+
}
|
|
63
|
+
async findSnapshot(idOrLabel) {
|
|
64
|
+
await this.load();
|
|
65
|
+
return (this.entries.find((e) => e.id === idOrLabel || e.label === idOrLabel) ??
|
|
66
|
+
null);
|
|
67
|
+
}
|
|
68
|
+
async deleteSnapshot(idOrLabel) {
|
|
69
|
+
await this.load();
|
|
70
|
+
const before = this.entries.length;
|
|
71
|
+
this.entries = this.entries.filter((e) => e.id !== idOrLabel && e.label !== idOrLabel);
|
|
72
|
+
if (this.entries.length < before) {
|
|
73
|
+
await this.save();
|
|
74
|
+
return true;
|
|
75
|
+
}
|
|
76
|
+
return false;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
/** Captures current package.json and lockfile state for a set of directories. */
|
|
80
|
+
export async function captureState(packageDirs) {
|
|
81
|
+
const manifests = {};
|
|
82
|
+
const lockfileHashes = {};
|
|
83
|
+
const LOCKFILES = [
|
|
84
|
+
"package-lock.json",
|
|
85
|
+
"pnpm-lock.yaml",
|
|
86
|
+
"yarn.lock",
|
|
87
|
+
"bun.lockb",
|
|
88
|
+
];
|
|
89
|
+
await Promise.all(packageDirs.map(async (dir) => {
|
|
90
|
+
// Read package.json
|
|
91
|
+
try {
|
|
92
|
+
const content = await fs.readFile(path.join(dir, "package.json"), "utf8");
|
|
93
|
+
manifests[dir] = content;
|
|
94
|
+
}
|
|
95
|
+
catch {
|
|
96
|
+
// No package.json — skip
|
|
97
|
+
}
|
|
98
|
+
// Hash the first lockfile found
|
|
99
|
+
for (const lf of LOCKFILES) {
|
|
100
|
+
try {
|
|
101
|
+
const content = await fs.readFile(path.join(dir, lf));
|
|
102
|
+
lockfileHashes[dir] = createHash("sha256")
|
|
103
|
+
.update(content)
|
|
104
|
+
.digest("hex");
|
|
105
|
+
break;
|
|
106
|
+
}
|
|
107
|
+
catch {
|
|
108
|
+
// Try next
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}));
|
|
112
|
+
return { manifests, lockfileHashes };
|
|
113
|
+
}
|
|
114
|
+
/** Restores package.json files from a snapshot's manifest map. */
|
|
115
|
+
export async function restoreState(entry) {
|
|
116
|
+
await Promise.all(Object.entries(entry.manifests).map(async ([dir, content]) => {
|
|
117
|
+
const manifestPath = path.join(dir, "package.json");
|
|
118
|
+
const tmp = manifestPath + ".tmp";
|
|
119
|
+
await fs.writeFile(tmp, content, "utf8");
|
|
120
|
+
await fs.rename(tmp, manifestPath);
|
|
121
|
+
}));
|
|
122
|
+
}
|
|
123
|
+
/** Computes a diff of dependency versions between two manifest snapshots. */
|
|
124
|
+
export function diffManifests(before, after) {
|
|
125
|
+
const changes = [];
|
|
126
|
+
for (const [dir, afterJson] of Object.entries(after)) {
|
|
127
|
+
const beforeJson = before[dir];
|
|
128
|
+
if (!beforeJson)
|
|
129
|
+
continue;
|
|
130
|
+
let beforeManifest;
|
|
131
|
+
let afterManifest;
|
|
132
|
+
try {
|
|
133
|
+
beforeManifest = JSON.parse(beforeJson);
|
|
134
|
+
afterManifest = JSON.parse(afterJson);
|
|
135
|
+
}
|
|
136
|
+
catch {
|
|
137
|
+
continue;
|
|
138
|
+
}
|
|
139
|
+
const fields = [
|
|
140
|
+
"dependencies",
|
|
141
|
+
"devDependencies",
|
|
142
|
+
"optionalDependencies",
|
|
143
|
+
];
|
|
144
|
+
for (const field of fields) {
|
|
145
|
+
const before = beforeManifest[field] ?? {};
|
|
146
|
+
const after = afterManifest[field] ?? {};
|
|
147
|
+
const allNames = new Set([...Object.keys(before), ...Object.keys(after)]);
|
|
148
|
+
for (const name of allNames) {
|
|
149
|
+
const fromVer = before[name] ?? "(removed)";
|
|
150
|
+
const toVer = after[name] ?? "(removed)";
|
|
151
|
+
if (fromVer !== toVer) {
|
|
152
|
+
changes.push({ name, from: fromVer, to: toVer });
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
return changes;
|
|
158
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { PackageManifest } from "../../types/index.js";
|
|
2
|
+
import type { UnusedDependency } from "../../types/index.js";
|
|
3
|
+
/**
|
|
4
|
+
* Cross-references declared package.json dependencies against imported package names
|
|
5
|
+
* to surface two classes of problems:
|
|
6
|
+
*
|
|
7
|
+
* - "declared-not-imported": in package.json but never imported in source
|
|
8
|
+
* - "imported-not-declared": imported in source but absent from package.json
|
|
9
|
+
*/
|
|
10
|
+
export interface MatchOptions {
|
|
11
|
+
includeDevDependencies: boolean;
|
|
12
|
+
}
|
|
13
|
+
export declare function matchDependencies(manifest: PackageManifest, importedPackages: Set<string>, packageDir: string, options: MatchOptions): {
|
|
14
|
+
unused: UnusedDependency[];
|
|
15
|
+
missing: UnusedDependency[];
|
|
16
|
+
};
|
|
17
|
+
/**
|
|
18
|
+
* Applies the unused/missing result of `matchDependencies` to a package.json
|
|
19
|
+
* manifest in-memory, removing `unused` entries. Returns the modified manifest
|
|
20
|
+
* as a formatted JSON string ready to write back to disk.
|
|
21
|
+
*/
|
|
22
|
+
export declare function removeUnusedFromManifest(manifestJson: string, unused: UnusedDependency[]): string;
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
export function matchDependencies(manifest, importedPackages, packageDir, options) {
|
|
2
|
+
const declared = buildDeclaredMap(manifest, options);
|
|
3
|
+
const unused = [];
|
|
4
|
+
const missing = [];
|
|
5
|
+
// Find declared deps not seen in source imports
|
|
6
|
+
for (const [name, field] of declared) {
|
|
7
|
+
if (!importedPackages.has(name)) {
|
|
8
|
+
unused.push({
|
|
9
|
+
name,
|
|
10
|
+
kind: "declared-not-imported",
|
|
11
|
+
declaredIn: field,
|
|
12
|
+
});
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
// Find imports not declared in package.json
|
|
16
|
+
for (const importedName of importedPackages) {
|
|
17
|
+
if (!declared.has(importedName)) {
|
|
18
|
+
missing.push({
|
|
19
|
+
name: importedName,
|
|
20
|
+
kind: "imported-not-declared",
|
|
21
|
+
importedFrom: packageDir,
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
return { unused, missing };
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Build a map of { packageName → fieldName } from package.json.
|
|
29
|
+
* Only includes the fields requested (e.g. skip devDependencies when
|
|
30
|
+
* includeDevDependencies is false).
|
|
31
|
+
*/
|
|
32
|
+
function buildDeclaredMap(manifest, options) {
|
|
33
|
+
const result = new Map();
|
|
34
|
+
const fields = [
|
|
35
|
+
[
|
|
36
|
+
manifest.dependencies,
|
|
37
|
+
"dependencies",
|
|
38
|
+
],
|
|
39
|
+
[
|
|
40
|
+
manifest.optionalDependencies,
|
|
41
|
+
"optionalDependencies",
|
|
42
|
+
],
|
|
43
|
+
];
|
|
44
|
+
if (options.includeDevDependencies) {
|
|
45
|
+
fields.push([
|
|
46
|
+
manifest.devDependencies,
|
|
47
|
+
"devDependencies",
|
|
48
|
+
]);
|
|
49
|
+
}
|
|
50
|
+
// peerDependencies are intentionally excluded — they are rarely directly imported
|
|
51
|
+
// and are a separate concern handled by `rup resolve`.
|
|
52
|
+
for (const [deps, fieldName] of fields) {
|
|
53
|
+
if (!deps)
|
|
54
|
+
continue;
|
|
55
|
+
for (const name of Object.keys(deps)) {
|
|
56
|
+
if (!result.has(name)) {
|
|
57
|
+
result.set(name, fieldName);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return result;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Applies the unused/missing result of `matchDependencies` to a package.json
|
|
65
|
+
* manifest in-memory, removing `unused` entries. Returns the modified manifest
|
|
66
|
+
* as a formatted JSON string ready to write back to disk.
|
|
67
|
+
*/
|
|
68
|
+
export function removeUnusedFromManifest(manifestJson, unused) {
|
|
69
|
+
if (unused.length === 0)
|
|
70
|
+
return manifestJson;
|
|
71
|
+
let parsed;
|
|
72
|
+
try {
|
|
73
|
+
parsed = JSON.parse(manifestJson);
|
|
74
|
+
}
|
|
75
|
+
catch {
|
|
76
|
+
return manifestJson;
|
|
77
|
+
}
|
|
78
|
+
const unusedNames = new Set(unused.map((u) => u.name));
|
|
79
|
+
const fields = [
|
|
80
|
+
"dependencies",
|
|
81
|
+
"devDependencies",
|
|
82
|
+
"optionalDependencies",
|
|
83
|
+
];
|
|
84
|
+
for (const field of fields) {
|
|
85
|
+
const deps = parsed[field];
|
|
86
|
+
if (!deps || typeof deps !== "object")
|
|
87
|
+
continue;
|
|
88
|
+
for (const name of Object.keys(deps)) {
|
|
89
|
+
if (unusedNames.has(name)) {
|
|
90
|
+
delete deps[name];
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return JSON.stringify(parsed, null, 2) + "\n";
|
|
95
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
const DEFAULT_SRC_DIRS = ["src"];
|
|
2
|
+
export function parseUnusedArgs(args) {
|
|
3
|
+
const options = {
|
|
4
|
+
cwd: process.cwd(),
|
|
5
|
+
workspace: false,
|
|
6
|
+
srcDirs: DEFAULT_SRC_DIRS,
|
|
7
|
+
includeDevDependencies: true,
|
|
8
|
+
fix: false,
|
|
9
|
+
dryRun: false,
|
|
10
|
+
jsonFile: undefined,
|
|
11
|
+
concurrency: 16,
|
|
12
|
+
};
|
|
13
|
+
for (let i = 0; i < args.length; i++) {
|
|
14
|
+
const current = args[i];
|
|
15
|
+
const next = args[i + 1];
|
|
16
|
+
if (current === "--cwd" && next) {
|
|
17
|
+
options.cwd = next;
|
|
18
|
+
i++;
|
|
19
|
+
continue;
|
|
20
|
+
}
|
|
21
|
+
if (current === "--cwd")
|
|
22
|
+
throw new Error("Missing value for --cwd");
|
|
23
|
+
if (current === "--workspace") {
|
|
24
|
+
options.workspace = true;
|
|
25
|
+
continue;
|
|
26
|
+
}
|
|
27
|
+
if (current === "--src" && next) {
|
|
28
|
+
options.srcDirs = next
|
|
29
|
+
.split(",")
|
|
30
|
+
.map((s) => s.trim())
|
|
31
|
+
.filter(Boolean);
|
|
32
|
+
i++;
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
35
|
+
if (current === "--src")
|
|
36
|
+
throw new Error("Missing value for --src");
|
|
37
|
+
if (current === "--no-dev") {
|
|
38
|
+
options.includeDevDependencies = false;
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
if (current === "--fix") {
|
|
42
|
+
options.fix = true;
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
if (current === "--dry-run") {
|
|
46
|
+
options.dryRun = true;
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
if (current === "--json-file" && next) {
|
|
50
|
+
options.jsonFile = next;
|
|
51
|
+
i++;
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
if (current === "--json-file")
|
|
55
|
+
throw new Error("Missing value for --json-file");
|
|
56
|
+
if (current === "--concurrency" && next) {
|
|
57
|
+
const parsed = Number(next);
|
|
58
|
+
if (!Number.isInteger(parsed) || parsed <= 0)
|
|
59
|
+
throw new Error("--concurrency must be a positive integer");
|
|
60
|
+
options.concurrency = parsed;
|
|
61
|
+
i++;
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
if (current === "--concurrency")
|
|
65
|
+
throw new Error("Missing value for --concurrency");
|
|
66
|
+
if (current === "--help" || current === "-h") {
|
|
67
|
+
process.stdout.write(UNUSED_HELP);
|
|
68
|
+
process.exit(0);
|
|
69
|
+
}
|
|
70
|
+
if (current.startsWith("-"))
|
|
71
|
+
throw new Error(`Unknown option: ${current}`);
|
|
72
|
+
}
|
|
73
|
+
return options;
|
|
74
|
+
}
|
|
75
|
+
const UNUSED_HELP = `
|
|
76
|
+
rup unused — Detect unused and missing npm dependencies
|
|
77
|
+
|
|
78
|
+
Usage:
|
|
79
|
+
rup unused [options]
|
|
80
|
+
|
|
81
|
+
Options:
|
|
82
|
+
--src <dirs> Comma-separated source directories to scan (default: src)
|
|
83
|
+
--workspace Scan all workspace packages
|
|
84
|
+
--no-dev Exclude devDependencies from unused detection
|
|
85
|
+
--fix Remove unused dependencies from package.json
|
|
86
|
+
--dry-run Preview changes without writing
|
|
87
|
+
--json-file <path> Write JSON report to file
|
|
88
|
+
--cwd <path> Working directory (default: cwd)
|
|
89
|
+
--concurrency <n> Parallel file scanning concurrency (default: 16)
|
|
90
|
+
--help Show this help
|
|
91
|
+
|
|
92
|
+
Exit codes:
|
|
93
|
+
0 No unused dependencies found
|
|
94
|
+
1 Unused or missing dependencies detected
|
|
95
|
+
`.trimStart();
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { UnusedOptions, UnusedResult } from "../../types/index.js";
|
|
2
|
+
/**
|
|
3
|
+
* Entry point for `rup unused`. Lazy-loaded by cli.ts.
|
|
4
|
+
*
|
|
5
|
+
* Strategy:
|
|
6
|
+
* 1. Collect all source directories to scan
|
|
7
|
+
* 2. Scan them in parallel for imported package names
|
|
8
|
+
* 3. Cross-reference against package.json declarations
|
|
9
|
+
* 4. Optionally apply --fix (remove unused from package.json)
|
|
10
|
+
*/
|
|
11
|
+
export declare function runUnused(options: UnusedOptions): Promise<UnusedResult>;
|