@reliverse/dler 2.3.3 → 2.3.4

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.
@@ -1,34 +1,31 @@
1
1
  import { existsSync, statSync } from "node:fs";
2
- import { mkdir, readFile, writeFile } from "node:fs/promises";
3
2
  import { join } from "node:path";
3
+ import { loadCache, saveCache } from "../../utils/cache.js";
4
4
  const CACHE_VERSION = "1.0.0";
5
- const DEFAULT_CACHE_DIR = "node_modules/.cache/dler-tsc";
6
5
  const DEFAULT_MAX_AGE = 24 * 60 * 60 * 1e3;
7
6
  export class TscCache {
8
7
  options;
9
8
  metadata = null;
10
- metadataPath;
11
9
  constructor(options = {}) {
12
10
  this.options = {
13
11
  enabled: options.enabled ?? true,
14
- cacheDir: options.cacheDir ?? DEFAULT_CACHE_DIR,
12
+ cacheDir: options.cacheDir ?? "node_modules/.cache/dler",
15
13
  maxAge: options.maxAge ?? DEFAULT_MAX_AGE
16
14
  };
17
- this.metadataPath = join(this.options.cacheDir, "metadata.json");
18
15
  }
19
16
  async initialize() {
20
17
  if (!this.options.enabled) {
21
18
  return;
22
19
  }
23
20
  try {
24
- await mkdir(this.options.cacheDir, { recursive: true });
25
21
  await this.loadMetadata();
26
22
  } catch {
27
23
  this.options.enabled = false;
28
24
  }
29
25
  }
30
26
  async loadMetadata() {
31
- if (!existsSync(this.metadataPath)) {
27
+ const cachedMetadata = await loadCache("tsc");
28
+ if (!cachedMetadata) {
32
29
  this.metadata = {
33
30
  version: CACHE_VERSION,
34
31
  lastUpdated: Date.now(),
@@ -36,22 +33,14 @@ export class TscCache {
36
33
  };
37
34
  return;
38
35
  }
39
- try {
40
- const content = await readFile(this.metadataPath, "utf-8");
41
- this.metadata = JSON.parse(content);
42
- if (Date.now() - this.metadata.lastUpdated > this.options.maxAge) {
43
- this.metadata = {
44
- version: CACHE_VERSION,
45
- lastUpdated: Date.now(),
46
- packages: {}
47
- };
48
- }
49
- } catch {
36
+ if (Date.now() - cachedMetadata.lastUpdated > this.options.maxAge) {
50
37
  this.metadata = {
51
38
  version: CACHE_VERSION,
52
39
  lastUpdated: Date.now(),
53
40
  packages: {}
54
41
  };
42
+ } else {
43
+ this.metadata = cachedMetadata;
55
44
  }
56
45
  }
57
46
  async saveMetadata() {
@@ -60,7 +49,7 @@ export class TscCache {
60
49
  }
61
50
  try {
62
51
  this.metadata.lastUpdated = Date.now();
63
- await writeFile(this.metadataPath, JSON.stringify(this.metadata, null, 2), "utf-8");
52
+ await saveCache("tsc", this.metadata);
64
53
  } catch {
65
54
  }
66
55
  }
@@ -466,14 +466,11 @@ const collectAllResults = async (packages, monorepoRoot, options = {}, cache) =>
466
466
  incremental = true,
467
467
  buildMode = false
468
468
  } = options;
469
- if (!verbose) {
470
- logger.info(`Processing ${packages.length} packages...`);
471
- }
472
469
  try {
473
470
  const tscResults = await pMap(
474
471
  packages,
475
472
  async (pkg, index) => {
476
- if (!verbose) {
473
+ if (verbose) {
477
474
  logger.info(`Processing ${pkg.name} (${index + 1}/${packages.length})...`);
478
475
  }
479
476
  return runTscOnPackage(pkg, monorepoRoot, {
@@ -675,8 +672,7 @@ export const runTscOnAllPackages = async (ignore, cwd, options = {}) => {
675
672
  }
676
673
  const { stopOnError = false } = options;
677
674
  logger.info(
678
- ` Checking ${packages.length} packages (concurrency: ${concurrency}, stopOnError: ${stopOnError})...
679
- `
675
+ `Checking ${packages.length} packages (concurrency: ${concurrency}, stopOnError: ${stopOnError})...`
680
676
  );
681
677
  if (verbose) {
682
678
  logger.info("\u{1F680} Starting TypeScript checks...\n");
@@ -0,0 +1,2 @@
1
+ declare const _default: any;
2
+ export default _default;
@@ -0,0 +1,105 @@
1
+ import { logger } from "@reliverse/relinka";
2
+ import { defineCommand, option } from "@reliverse/rempts-core";
3
+ import { type } from "arktype";
4
+ import { findUnusedDependencies } from "./impl.js";
5
+ export default defineCommand({
6
+ description: "Find unused dependencies in package.json files",
7
+ options: {
8
+ // Target package selection
9
+ target: option(type("string | undefined"), {
10
+ short: "t",
11
+ description: "Target workspace package(s) (from workspaces.packages). Use '.' for current directory package. Supports multiple packages (space-separated) and glob patterns."
12
+ }),
13
+ package: option(type("string | undefined"), {
14
+ description: "Target workspace package(s) (alias for --target). Use '.' for current directory package. Supports multiple packages (space-separated) and glob patterns."
15
+ }),
16
+ pkg: option(type("string | undefined"), {
17
+ description: "Target workspace package(s) (alias for --target). Use '.' for current directory package. Supports multiple packages (space-separated) and glob patterns."
18
+ }),
19
+ w: option(type("boolean | undefined"), {
20
+ description: "Check unused dependencies in root package.json"
21
+ }),
22
+ // Scope filtering
23
+ scope: option(type("'dev'|'prod'|'peer'|'optional' | undefined"), {
24
+ short: "s",
25
+ description: "Check specific dependency scope: dev, prod, peer, optional. If not specified, checks all scopes."
26
+ }),
27
+ dev: option(type("boolean | undefined"), {
28
+ short: "D",
29
+ description: "Check only devDependencies (shorthand for --scope dev)"
30
+ }),
31
+ prod: option(type("boolean | undefined"), {
32
+ short: "P",
33
+ description: "Check only dependencies (shorthand for --scope prod)"
34
+ }),
35
+ peer: option(type("boolean | undefined"), {
36
+ short: "R",
37
+ description: "Check only peerDependencies (shorthand for --scope peer)"
38
+ }),
39
+ optional: option(type("boolean | undefined"), {
40
+ short: "O",
41
+ description: "Check only optionalDependencies (shorthand for --scope optional)"
42
+ }),
43
+ // Analysis options
44
+ ignore: option(type("string | undefined"), {
45
+ short: "i",
46
+ description: "Comma-separated list of package names to ignore when checking for unused dependencies"
47
+ }),
48
+ includePeer: option(type("boolean | undefined"), {
49
+ description: "Include peerDependencies in the analysis (default: false)",
50
+ default: false
51
+ }),
52
+ // Other options
53
+ cwd: option(type("string | undefined"), {
54
+ description: "Working directory (monorepo root)"
55
+ }),
56
+ verbose: option(type("boolean | undefined"), {
57
+ short: "v",
58
+ description: "Verbose output"
59
+ })
60
+ },
61
+ handler: async ({ flags }) => {
62
+ try {
63
+ if (typeof process.versions.bun === "undefined") {
64
+ logger.error("\u274C This command requires Bun runtime.");
65
+ logger.error("Please run this command using Bun: bun dler unused");
66
+ process.exit(1);
67
+ }
68
+ logger.log("\u{1F50D} Finding unused dependencies...");
69
+ let scope;
70
+ if (flags.scope) {
71
+ scope = flags.scope;
72
+ } else if (flags.dev) {
73
+ scope = "dev";
74
+ } else if (flags.peer) {
75
+ scope = "peer";
76
+ } else if (flags.optional) {
77
+ scope = "optional";
78
+ }
79
+ const ignoreList = flags.ignore ? flags.ignore.split(",").map((pkg) => pkg.trim()).filter(Boolean) : void 0;
80
+ const options = {
81
+ target: flags.target || flags.package || flags.pkg,
82
+ w: flags.w,
83
+ scope,
84
+ ignore: ignoreList,
85
+ includePeer: flags.includePeer ?? false,
86
+ cwd: flags.cwd || void 0,
87
+ verbose: flags.verbose ?? false
88
+ };
89
+ await findUnusedDependencies(options);
90
+ } catch (error) {
91
+ logger.error("\n\u274C Failed to find unused dependencies:");
92
+ if (error instanceof Error) {
93
+ logger.error(error.message);
94
+ } else {
95
+ logger.error(String(error));
96
+ }
97
+ logger.log("");
98
+ logger.log("\u{1F4A1} Tips:");
99
+ logger.log(" \u2022 Ensure you're in a valid project directory with package.json");
100
+ logger.log(" \u2022 Use --verbose flag for more detailed output");
101
+ logger.log(" \u2022 Use --ignore flag to exclude specific packages from analysis");
102
+ process.exit(1);
103
+ }
104
+ }
105
+ });
@@ -0,0 +1,16 @@
1
+ export interface UnusedOptions {
2
+ target?: string;
3
+ w?: boolean;
4
+ scope?: "dev" | "prod" | "peer" | "optional";
5
+ ignore?: string[];
6
+ includePeer: boolean;
7
+ cwd?: string;
8
+ verbose: boolean;
9
+ }
10
+ export interface MonorepoInfo {
11
+ isMonorepo: boolean;
12
+ rootPath: string;
13
+ rootPackageJson: any;
14
+ workspacePackages?: string[];
15
+ }
16
+ export declare function findUnusedDependencies(options: UnusedOptions): Promise<void>;
@@ -0,0 +1,415 @@
1
+ import { existsSync, readdirSync, statSync } from "node:fs";
2
+ import { relative, resolve } from "node:path";
3
+ import { createIncludeFilter } from "@reliverse/matcha";
4
+ import path from "@reliverse/pathkit";
5
+ import { logger } from "@reliverse/relinka";
6
+ import { hasWorkspaces, readPackageJSON } from "@reliverse/typerso";
7
+ const monorepoCache = /* @__PURE__ */ new Map();
8
+ async function detectMonorepo(startDir) {
9
+ const cwd = resolve(startDir ?? process.cwd());
10
+ if (monorepoCache.has(cwd)) {
11
+ return monorepoCache.get(cwd);
12
+ }
13
+ let currentDir = cwd;
14
+ while (true) {
15
+ const packageJsonPath2 = path.join(currentDir, "package.json");
16
+ if (existsSync(packageJsonPath2)) {
17
+ const packageJson2 = await readPackageJSON(currentDir);
18
+ if (packageJson2 && hasWorkspaces(packageJson2)) {
19
+ const workspacePackages = await discoverWorkspacePackages(currentDir, packageJson2);
20
+ const result2 = {
21
+ isMonorepo: true,
22
+ rootPath: currentDir,
23
+ rootPackageJson: packageJson2,
24
+ workspacePackages
25
+ };
26
+ monorepoCache.set(cwd, result2);
27
+ return result2;
28
+ }
29
+ }
30
+ const parentDir = path.dirname(currentDir);
31
+ if (parentDir === currentDir) {
32
+ break;
33
+ }
34
+ currentDir = parentDir;
35
+ }
36
+ const packageJsonPath = path.join(cwd, "package.json");
37
+ if (!existsSync(packageJsonPath)) {
38
+ throw new Error("No package.json found in current directory or any parent directory");
39
+ }
40
+ const packageJson = await readPackageJSON(cwd);
41
+ if (!packageJson) {
42
+ throw new Error("Could not read package.json");
43
+ }
44
+ const result = {
45
+ isMonorepo: false,
46
+ rootPath: cwd,
47
+ rootPackageJson: packageJson
48
+ };
49
+ monorepoCache.set(cwd, result);
50
+ return result;
51
+ }
52
+ const workspaceCache = /* @__PURE__ */ new Map();
53
+ function hasBiomeConfig(rootPath) {
54
+ return existsSync(path.join(rootPath, "biome.json")) || existsSync(path.join(rootPath, "biome.jsonc"));
55
+ }
56
+ function hasTurboConfig(rootPath) {
57
+ return existsSync(path.join(rootPath, "turbo.json"));
58
+ }
59
+ async function discoverWorkspacePackages(monorepoRoot, rootPackageJson) {
60
+ if (workspaceCache.has(monorepoRoot)) {
61
+ return workspaceCache.get(monorepoRoot);
62
+ }
63
+ const packages = [];
64
+ if (!rootPackageJson.workspaces?.packages) {
65
+ workspaceCache.set(monorepoRoot, packages);
66
+ return packages;
67
+ }
68
+ const patterns = Array.isArray(rootPackageJson.workspaces.packages) ? rootPackageJson.workspaces.packages : [];
69
+ const packagePaths = [];
70
+ const validationPromises = [];
71
+ for (const pattern of patterns) {
72
+ if (pattern.includes("*")) {
73
+ const glob = new Bun.Glob(pattern);
74
+ const matches = glob.scanSync({ cwd: monorepoRoot, onlyFiles: false });
75
+ for (const match of matches) {
76
+ const packagePath = resolve(monorepoRoot, match);
77
+ packagePaths.push(packagePath);
78
+ validationPromises.push(isValidWorkspacePackage(packagePath));
79
+ }
80
+ } else {
81
+ const packagePath = resolve(monorepoRoot, pattern);
82
+ packagePaths.push(packagePath);
83
+ validationPromises.push(isValidWorkspacePackage(packagePath));
84
+ }
85
+ }
86
+ const validationResults = await Promise.all(validationPromises);
87
+ for (let i = 0; i < packagePaths.length; i++) {
88
+ if (validationResults[i]) {
89
+ packages.push(packagePaths[i]);
90
+ }
91
+ }
92
+ workspaceCache.set(monorepoRoot, packages);
93
+ return packages;
94
+ }
95
+ async function isValidWorkspacePackage(packagePath) {
96
+ const packageJsonPath = path.join(packagePath, "package.json");
97
+ if (!existsSync(packageJsonPath)) {
98
+ return false;
99
+ }
100
+ const packageJson = await readPackageJSON(packagePath);
101
+ return !!packageJson?.name;
102
+ }
103
+ function containsGlobPattern(str) {
104
+ return str.includes("*") || str.includes("?") || str.includes("[") || str.includes("{");
105
+ }
106
+ function containsMultipleTargets(str) {
107
+ return str.includes(" ") && str.trim().split(/\s+/).length > 1;
108
+ }
109
+ const packageInfoCache = /* @__PURE__ */ new Map();
110
+ async function getPackageInfo(monorepoInfo) {
111
+ if (packageInfoCache.has(monorepoInfo.rootPath)) {
112
+ return packageInfoCache.get(monorepoInfo.rootPath);
113
+ }
114
+ const packages = [];
115
+ if (monorepoInfo.isMonorepo) {
116
+ if (hasDependencies(monorepoInfo.rootPackageJson)) {
117
+ packages.push({
118
+ name: monorepoInfo.rootPackageJson.name || "root",
119
+ path: monorepoInfo.rootPath,
120
+ json: monorepoInfo.rootPackageJson,
121
+ packagePath: monorepoInfo.rootPath,
122
+ packageJson: monorepoInfo.rootPackageJson
123
+ });
124
+ }
125
+ if (monorepoInfo.workspacePackages) {
126
+ for (const packagePath of monorepoInfo.workspacePackages) {
127
+ const packageJson = await readPackageJSON(packagePath);
128
+ if (packageJson && hasDependencies(packageJson)) {
129
+ packages.push({
130
+ name: packageJson.name || "unnamed",
131
+ path: packagePath,
132
+ json: packageJson,
133
+ packagePath,
134
+ packageJson
135
+ });
136
+ }
137
+ }
138
+ }
139
+ } else {
140
+ packages.push({
141
+ name: monorepoInfo.rootPackageJson.name || "project",
142
+ path: monorepoInfo.rootPath,
143
+ json: monorepoInfo.rootPackageJson,
144
+ packagePath: monorepoInfo.rootPath,
145
+ packageJson: monorepoInfo.rootPackageJson
146
+ });
147
+ }
148
+ packageInfoCache.set(monorepoInfo.rootPath, packages);
149
+ return packages;
150
+ }
151
+ function hasDependencies(packageJson) {
152
+ return !!(packageJson.dependencies || packageJson.devDependencies || packageJson.peerDependencies || packageJson.optionalDependencies);
153
+ }
154
+ async function resolveTargetPackages(monorepoInfo, targetOption, w) {
155
+ const allPackages = await getPackageInfo(monorepoInfo);
156
+ if (!(targetOption || w)) {
157
+ const currentDir = process.cwd();
158
+ const currentPackage = allPackages.find((pkg) => pkg.path === currentDir);
159
+ if (currentPackage) {
160
+ return [currentPackage];
161
+ }
162
+ return allPackages;
163
+ }
164
+ if (w) {
165
+ const rootPackage = allPackages.find((pkg) => pkg.path === monorepoInfo.rootPath);
166
+ return rootPackage ? [rootPackage] : [];
167
+ }
168
+ if (targetOption) {
169
+ if (containsGlobPattern(targetOption) || containsMultipleTargets(targetOption)) {
170
+ const targets = [];
171
+ if (containsMultipleTargets(targetOption)) {
172
+ targets.push(...targetOption.trim().split(/\s+/));
173
+ } else {
174
+ targets.push(targetOption);
175
+ }
176
+ const matchedPackages = [];
177
+ const filter = createIncludeFilter(targets);
178
+ const matchingPackages = filter(allPackages);
179
+ matchedPackages.push(...matchingPackages);
180
+ return matchedPackages;
181
+ } else {
182
+ const pkg = allPackages.find(
183
+ (p) => p.name === targetOption || relative(monorepoInfo.rootPath, p.path) === targetOption || p.path === resolve(monorepoInfo.rootPath, targetOption)
184
+ );
185
+ return pkg ? [pkg] : [];
186
+ }
187
+ }
188
+ return [];
189
+ }
190
+ function getSourceFiles(packagePath, rootPath) {
191
+ const files = [];
192
+ const extensions = [".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs"];
193
+ const skipDirs = ["node_modules", ".git", "dist", "build", ".cache", "coverage", ".turbo"];
194
+ function scanDir(dir) {
195
+ try {
196
+ const entries = readdirSync(dir);
197
+ for (const entry of entries) {
198
+ const fullPath = path.join(dir, entry);
199
+ const stat = statSync(fullPath);
200
+ if (stat.isDirectory()) {
201
+ if (!skipDirs.includes(entry)) {
202
+ scanDir(fullPath);
203
+ }
204
+ } else if (stat.isFile()) {
205
+ const ext = path.extname(entry);
206
+ if (extensions.includes(ext)) {
207
+ files.push(fullPath);
208
+ }
209
+ }
210
+ }
211
+ } catch (error) {
212
+ }
213
+ }
214
+ scanDir(packagePath);
215
+ return files;
216
+ }
217
+ function analyzePackageScripts(packageJson) {
218
+ const usedPackages = /* @__PURE__ */ new Set();
219
+ if (packageJson.scripts) {
220
+ for (const script of Object.values(packageJson.scripts)) {
221
+ const buildTools = ["biome", "turbo", "ultracite"];
222
+ for (const tool of buildTools) {
223
+ if (script.includes(tool)) {
224
+ if (tool === "biome") {
225
+ usedPackages.add("@biomejs/biome");
226
+ } else {
227
+ usedPackages.add(tool);
228
+ }
229
+ }
230
+ }
231
+ }
232
+ }
233
+ return usedPackages;
234
+ }
235
+ function analyzeSourceFile(filePath, packageJson) {
236
+ const usedPackages = /* @__PURE__ */ new Set();
237
+ const packageName = packageJson.name || "";
238
+ try {
239
+ const content = require("node:fs").readFileSync(filePath, "utf8");
240
+ const importPatterns = [
241
+ // ES6 imports
242
+ /import\s+.*?\s+from\s+['"]([^'"]+)['"]/g,
243
+ // Dynamic imports
244
+ /import\s*\(\s*['"]([^'"]+)['"]\s*\)/g,
245
+ // Require statements
246
+ /require\s*\(\s*['"]([^'"]+)['"]\s*\)/g,
247
+ // TypeScript type imports
248
+ /import\s*\(\s*['"]([^'"]+)['"]\s*\)/g
249
+ ];
250
+ for (const pattern of importPatterns) {
251
+ let match = pattern.exec(content);
252
+ while (match !== null) {
253
+ const importedPackage = match[1];
254
+ if (!importedPackage) continue;
255
+ if (!(importedPackage.startsWith(".") || path.extname(importedPackage))) {
256
+ const basePackage = importedPackage.split("/")[0];
257
+ if (basePackage && basePackage.startsWith("@")) {
258
+ const parts = importedPackage.split("/");
259
+ if (parts.length >= 2) {
260
+ usedPackages.add(`${parts[0]}/${parts[1]}`);
261
+ }
262
+ } else if (basePackage) {
263
+ usedPackages.add(basePackage);
264
+ }
265
+ }
266
+ match = pattern.exec(content);
267
+ }
268
+ }
269
+ if (content.includes(packageName)) {
270
+ usedPackages.delete(packageName);
271
+ }
272
+ } catch (error) {
273
+ }
274
+ return usedPackages;
275
+ }
276
+ function collectAllDependencies(packageJson, scope, includePeer = false) {
277
+ const allDeps = {};
278
+ const depTypes = [
279
+ { key: "dependencies", type: "prod" },
280
+ { key: "devDependencies", type: "dev" },
281
+ { key: "peerDependencies", type: "peer" },
282
+ { key: "optionalDependencies", type: "optional" }
283
+ ];
284
+ for (const { key, type } of depTypes) {
285
+ if (packageJson[key] && (!scope || scope === type)) {
286
+ if (type === "peer" && !includePeer) {
287
+ continue;
288
+ }
289
+ Object.assign(allDeps, packageJson[key]);
290
+ }
291
+ }
292
+ return allDeps;
293
+ }
294
+ export async function findUnusedDependencies(options) {
295
+ const monorepoInfo = await detectMonorepo(options.cwd);
296
+ const targetPackages = await resolveTargetPackages(monorepoInfo, options.target, options.w);
297
+ if (targetPackages.length === 0) {
298
+ logger.warn("No packages found to analyze");
299
+ return;
300
+ }
301
+ const hasBiome = hasBiomeConfig(monorepoInfo.rootPath);
302
+ const hasTurbo = hasTurboConfig(monorepoInfo.rootPath);
303
+ let totalUnused = 0;
304
+ const results = [];
305
+ for (const targetPackage of targetPackages) {
306
+ if (options.verbose) {
307
+ logger.log(`Analyzing ${targetPackage.packageJson.name || "unnamed package"}...`);
308
+ }
309
+ const sourceFiles = getSourceFiles(targetPackage.packagePath, monorepoInfo.rootPath);
310
+ const usedPackages = /* @__PURE__ */ new Set();
311
+ for (const file of sourceFiles) {
312
+ const fileUsedPackages = analyzeSourceFile(file, targetPackage.packageJson);
313
+ for (const pkg of fileUsedPackages) {
314
+ usedPackages.add(pkg);
315
+ }
316
+ }
317
+ const scriptUsedPackages = analyzePackageScripts(targetPackage.packageJson);
318
+ for (const pkg of scriptUsedPackages) {
319
+ usedPackages.add(pkg);
320
+ }
321
+ if (hasBiome) {
322
+ usedPackages.add("@biomejs/biome");
323
+ usedPackages.add("ultracite");
324
+ }
325
+ if (hasTurbo) {
326
+ usedPackages.add("turbo");
327
+ }
328
+ const allDeps = collectAllDependencies(
329
+ targetPackage.packageJson,
330
+ options.scope,
331
+ options.includePeer
332
+ );
333
+ const unusedDeps = [];
334
+ for (const [depName, depVersion] of Object.entries(allDeps)) {
335
+ if (options.ignore?.includes(depName)) {
336
+ continue;
337
+ }
338
+ if (!usedPackages.has(depName)) {
339
+ let scope = "unknown";
340
+ if (targetPackage.packageJson.dependencies?.[depName]) {
341
+ scope = "prod";
342
+ } else if (targetPackage.packageJson.devDependencies?.[depName]) {
343
+ scope = "dev";
344
+ } else if (targetPackage.packageJson.peerDependencies?.[depName]) {
345
+ scope = "peer";
346
+ } else if (targetPackage.packageJson.optionalDependencies?.[depName]) {
347
+ scope = "optional";
348
+ }
349
+ unusedDeps.push({
350
+ name: depName,
351
+ scope,
352
+ version: depVersion
353
+ });
354
+ }
355
+ }
356
+ if (unusedDeps.length > 0) {
357
+ results.push({
358
+ packageName: targetPackage.packageJson.name || "unnamed package",
359
+ packagePath: targetPackage.packagePath,
360
+ unusedDeps
361
+ });
362
+ totalUnused += unusedDeps.length;
363
+ }
364
+ }
365
+ if (results.length === 0) {
366
+ logger.success("\u2705 No unused dependencies found!");
367
+ return;
368
+ }
369
+ logger.log(`
370
+ \u{1F4E6} Found ${totalUnused} unused dependencies across ${results.length} packages:
371
+ `);
372
+ const packageSuggestions = [];
373
+ for (const result of results) {
374
+ const relativePath = relative(process.cwd(), result.packagePath);
375
+ const depsByScope = {};
376
+ const allDeps = [];
377
+ for (const dep of result.unusedDeps) {
378
+ if (!depsByScope[dep.scope]) {
379
+ depsByScope[dep.scope] = [];
380
+ }
381
+ depsByScope[dep.scope].push(dep.name);
382
+ allDeps.push(dep);
383
+ }
384
+ packageSuggestions.push({
385
+ packageName: result.packageName,
386
+ packagePath: result.packagePath,
387
+ relativePath,
388
+ depsByScope,
389
+ allDeps
390
+ });
391
+ }
392
+ for (const suggestion of packageSuggestions) {
393
+ logger.log(`\u{1F4C1} ${suggestion.packageName} (${suggestion.relativePath}):`);
394
+ for (const dep of suggestion.allDeps) {
395
+ logger.log(` \u274C ${dep.name}@${dep.version} (${dep.scope})`);
396
+ }
397
+ logger.log("");
398
+ }
399
+ logger.log("\u{1F527} Suggested removal commands:");
400
+ for (const suggestion of packageSuggestions) {
401
+ const packageTarget = suggestion.relativePath === "package.json" ? "." : suggestion.relativePath;
402
+ for (const [scope, deps] of Object.entries(suggestion.depsByScope)) {
403
+ if (deps.length > 0) {
404
+ const scopeFlag = scope === "prod" ? "" : ` --${scope}`;
405
+ const depList = deps.join(" ");
406
+ logger.log(` bun dler rm ${depList} --target "${packageTarget}"${scopeFlag}`);
407
+ }
408
+ }
409
+ logger.log("");
410
+ }
411
+ logger.log(`\u{1F4A1} Use --ignore flag to exclude specific packages from analysis`);
412
+ logger.log(
413
+ `\u{1F4A1} If some dep is used, but was marked as unused, please create an issue: https://github.com/reliverse/dler/issues`
414
+ );
415
+ }