dep-brain 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.
@@ -0,0 +1,60 @@
1
+ import { type DepBrainConfig, type DepBrainConfigOverrides } from "../utils/config.js";
2
+ export interface AnalysisOptions {
3
+ rootDir?: string;
4
+ configPath?: string;
5
+ config?: DepBrainConfigOverrides;
6
+ }
7
+ export interface DuplicateInstance {
8
+ path: string;
9
+ version: string;
10
+ }
11
+ export interface DuplicateDependency {
12
+ name: string;
13
+ versions: string[];
14
+ instances: DuplicateInstance[];
15
+ }
16
+ export interface UnusedDependency {
17
+ name: string;
18
+ section: "dependencies" | "devDependencies";
19
+ package?: string;
20
+ }
21
+ export interface OutdatedDependency {
22
+ name: string;
23
+ current: string;
24
+ latest: string;
25
+ updateType: "major" | "minor" | "patch" | "unknown";
26
+ package?: string;
27
+ }
28
+ export interface RiskDependency {
29
+ name: string;
30
+ reasons: string[];
31
+ package?: string;
32
+ }
33
+ export interface AnalysisResult {
34
+ rootDir: string;
35
+ score: number;
36
+ policy: PolicyResult;
37
+ duplicates: DuplicateDependency[];
38
+ unused: UnusedDependency[];
39
+ outdated: OutdatedDependency[];
40
+ risks: RiskDependency[];
41
+ suggestions: string[];
42
+ config: DepBrainConfig;
43
+ packages?: PackageAnalysisResult[];
44
+ }
45
+ export interface PolicyResult {
46
+ passed: boolean;
47
+ reasons: string[];
48
+ }
49
+ export interface PackageAnalysisResult {
50
+ name: string;
51
+ rootDir: string;
52
+ score: number;
53
+ policy: PolicyResult;
54
+ duplicates: DuplicateDependency[];
55
+ unused: UnusedDependency[];
56
+ outdated: OutdatedDependency[];
57
+ risks: RiskDependency[];
58
+ suggestions: string[];
59
+ }
60
+ export declare function analyzeProject(options?: AnalysisOptions): Promise<AnalysisResult>;
@@ -0,0 +1,158 @@
1
+ import path from "node:path";
2
+ import { findDuplicateDependencies } from "../checks/duplicate.js";
3
+ import { findOutdatedDependencies } from "../checks/outdated.js";
4
+ import { findRiskDependencies } from "../checks/risk.js";
5
+ import { findUnusedDependencies } from "../checks/unused.js";
6
+ import { loadDepBrainConfig } from "../utils/config.js";
7
+ import { findWorkspacePackages } from "../utils/workspaces.js";
8
+ import { buildDependencyGraph } from "./graph-builder.js";
9
+ import { calculateHealthScore } from "./scorer.js";
10
+ export async function analyzeProject(options = {}) {
11
+ const rootDir = path.resolve(options.rootDir ?? process.cwd());
12
+ const loadedConfig = await loadDepBrainConfig(rootDir, options.configPath);
13
+ const config = mergeConfig(loadedConfig, options.config);
14
+ const workspaces = await findWorkspacePackages(rootDir);
15
+ if (workspaces.length === 0) {
16
+ return analyzeSingleProject(rootDir, config);
17
+ }
18
+ const rootGraph = await buildDependencyGraph(rootDir);
19
+ const rawDuplicates = await findDuplicateDependencies(rootGraph);
20
+ const duplicates = rawDuplicates.filter((item) => !config.ignore.duplicates.includes(item.name));
21
+ const packages = [];
22
+ for (const workspace of workspaces) {
23
+ const result = await analyzeSingleProject(workspace.rootDir, config, {
24
+ packageName: workspace.name
25
+ });
26
+ packages.push({ ...result, name: workspace.name });
27
+ }
28
+ const unused = packages.flatMap((pkg) => pkg.unused.map((item) => ({ ...item, package: pkg.name })));
29
+ const outdated = packages.flatMap((pkg) => pkg.outdated.map((item) => ({ ...item, package: pkg.name })));
30
+ const risks = packages.flatMap((pkg) => pkg.risks.map((item) => ({ ...item, package: pkg.name })));
31
+ const score = calculateHealthScore({
32
+ duplicates: duplicates.length,
33
+ unused: unused.length,
34
+ outdated: outdated.length,
35
+ risks: risks.length
36
+ });
37
+ const suggestions = [
38
+ ...packages.flatMap((pkg) => pkg.suggestions.map((suggestion) => `[${pkg.name}] ${suggestion}`))
39
+ ].slice(0, config.report.maxSuggestions);
40
+ const policy = evaluatePolicy({
41
+ score,
42
+ duplicates: duplicates.length,
43
+ unused: unused.length,
44
+ outdated: outdated.length,
45
+ risks: risks.length
46
+ }, config);
47
+ return {
48
+ rootDir,
49
+ score,
50
+ policy,
51
+ duplicates,
52
+ unused,
53
+ outdated,
54
+ risks,
55
+ suggestions,
56
+ config,
57
+ packages
58
+ };
59
+ }
60
+ function mergeConfig(base, overrides) {
61
+ if (!overrides) {
62
+ return base;
63
+ }
64
+ return {
65
+ ignore: {
66
+ dependencies: overrides.ignore?.dependencies ?? base.ignore.dependencies,
67
+ devDependencies: overrides.ignore?.devDependencies ?? base.ignore.devDependencies,
68
+ duplicates: overrides.ignore?.duplicates ?? base.ignore.duplicates,
69
+ outdated: overrides.ignore?.outdated ?? base.ignore.outdated,
70
+ risks: overrides.ignore?.risks ?? base.ignore.risks,
71
+ unused: overrides.ignore?.unused ?? base.ignore.unused
72
+ },
73
+ policy: {
74
+ minScore: overrides.policy?.minScore ?? base.policy.minScore,
75
+ failOnDuplicates: overrides.policy?.failOnDuplicates ?? base.policy.failOnDuplicates,
76
+ failOnOutdated: overrides.policy?.failOnOutdated ?? base.policy.failOnOutdated,
77
+ failOnRisks: overrides.policy?.failOnRisks ?? base.policy.failOnRisks,
78
+ failOnUnused: overrides.policy?.failOnUnused ?? base.policy.failOnUnused
79
+ },
80
+ report: {
81
+ maxSuggestions: overrides.report?.maxSuggestions ?? base.report.maxSuggestions
82
+ }
83
+ };
84
+ }
85
+ function evaluatePolicy(summary, config) {
86
+ const reasons = [];
87
+ if (summary.score < config.policy.minScore) {
88
+ reasons.push(`Score ${summary.score} is below minimum ${config.policy.minScore}`);
89
+ }
90
+ if (config.policy.failOnDuplicates && summary.duplicates > 0) {
91
+ reasons.push(`Found ${summary.duplicates} duplicate dependencies`);
92
+ }
93
+ if (config.policy.failOnUnused && summary.unused > 0) {
94
+ reasons.push(`Found ${summary.unused} unused dependencies`);
95
+ }
96
+ if (config.policy.failOnOutdated && summary.outdated > 0) {
97
+ reasons.push(`Found ${summary.outdated} outdated dependencies`);
98
+ }
99
+ if (config.policy.failOnRisks && summary.risks > 0) {
100
+ reasons.push(`Found ${summary.risks} risky dependencies`);
101
+ }
102
+ return {
103
+ passed: reasons.length === 0,
104
+ reasons
105
+ };
106
+ }
107
+ async function analyzeSingleProject(rootDir, config, options = {}) {
108
+ const graph = await buildDependencyGraph(rootDir);
109
+ const [rawDuplicates, rawUnused, rawOutdated, rawRisks] = await Promise.all([
110
+ findDuplicateDependencies(graph),
111
+ findUnusedDependencies(rootDir, graph),
112
+ findOutdatedDependencies(graph),
113
+ findRiskDependencies(graph)
114
+ ]);
115
+ const duplicates = rawDuplicates.filter((item) => !config.ignore.duplicates.includes(item.name));
116
+ const unused = rawUnused.filter((item) => !config.ignore.unused.includes(item.name) &&
117
+ !config.ignore[item.section].includes(item.name));
118
+ const outdated = rawOutdated.filter((item) => !config.ignore.outdated.includes(item.name));
119
+ const risks = rawRisks.filter((item) => !config.ignore.risks.includes(item.name));
120
+ const score = calculateHealthScore({
121
+ duplicates: duplicates.length,
122
+ unused: unused.length,
123
+ outdated: outdated.length,
124
+ risks: risks.length
125
+ });
126
+ const suggestions = [
127
+ ...unused.map((item) => `Remove ${item.name} from ${item.section}`),
128
+ ...duplicates.map((item) => `Consider consolidating ${item.name} to one version`),
129
+ ...outdated.map((item) => `Review ${item.name}: ${item.current} -> ${item.latest} (${item.updateType})`)
130
+ ].slice(0, config.report.maxSuggestions);
131
+ const policy = evaluatePolicy({
132
+ score,
133
+ duplicates: duplicates.length,
134
+ unused: unused.length,
135
+ outdated: outdated.length,
136
+ risks: risks.length
137
+ }, config);
138
+ const scopedUnused = options.packageName && options.packageName.trim().length > 0
139
+ ? unused.map((item) => ({ ...item, package: options.packageName }))
140
+ : unused;
141
+ const scopedOutdated = options.packageName && options.packageName.trim().length > 0
142
+ ? outdated.map((item) => ({ ...item, package: options.packageName }))
143
+ : outdated;
144
+ const scopedRisks = options.packageName && options.packageName.trim().length > 0
145
+ ? risks.map((item) => ({ ...item, package: options.packageName }))
146
+ : risks;
147
+ return {
148
+ rootDir,
149
+ score,
150
+ policy,
151
+ duplicates,
152
+ unused: scopedUnused,
153
+ outdated: scopedOutdated,
154
+ risks: scopedRisks,
155
+ suggestions,
156
+ config
157
+ };
158
+ }
@@ -0,0 +1,14 @@
1
+ export interface LockPackageInstance {
2
+ path: string;
3
+ version: string;
4
+ }
5
+ export interface DependencyGraph {
6
+ rootDir: string;
7
+ packageJsonPath: string;
8
+ lockfilePath?: string;
9
+ dependencies: Record<string, string>;
10
+ devDependencies: Record<string, string>;
11
+ scripts: Record<string, string>;
12
+ lockPackages: Record<string, LockPackageInstance[]>;
13
+ }
14
+ export declare function buildDependencyGraph(rootDir: string): Promise<DependencyGraph>;
@@ -0,0 +1,63 @@
1
+ import path from "node:path";
2
+ import { readJsonFile } from "../utils/file-parser.js";
3
+ export async function buildDependencyGraph(rootDir) {
4
+ const packageJsonPath = path.join(rootDir, "package.json");
5
+ const lockfilePath = path.join(rootDir, "package-lock.json");
6
+ const packageJson = await readJsonFile(packageJsonPath);
7
+ const lockPackages = new Map();
8
+ try {
9
+ const packageLock = await readJsonFile(lockfilePath);
10
+ for (const [packagePath, details] of Object.entries(packageLock.packages ?? {})) {
11
+ const name = extractPackageName(packagePath);
12
+ const version = details.version;
13
+ if (!name || !version) {
14
+ continue;
15
+ }
16
+ const instances = lockPackages.get(name) ?? new Map();
17
+ const normalizedPath = packagePath || "node_modules/" + name;
18
+ instances.set(normalizedPath, { path: normalizedPath, version });
19
+ lockPackages.set(name, instances);
20
+ }
21
+ for (const [name, details] of Object.entries(packageLock.dependencies ?? {})) {
22
+ if (!details.version) {
23
+ continue;
24
+ }
25
+ const instances = lockPackages.get(name) ?? new Map();
26
+ const normalizedPath = `node_modules/${name}`;
27
+ instances.set(normalizedPath, { path: normalizedPath, version: details.version });
28
+ lockPackages.set(name, instances);
29
+ }
30
+ }
31
+ catch {
32
+ return {
33
+ rootDir,
34
+ packageJsonPath,
35
+ dependencies: packageJson.dependencies ?? {},
36
+ devDependencies: packageJson.devDependencies ?? {},
37
+ scripts: packageJson.scripts ?? {},
38
+ lockPackages: {}
39
+ };
40
+ }
41
+ return {
42
+ rootDir,
43
+ packageJsonPath,
44
+ lockfilePath,
45
+ dependencies: packageJson.dependencies ?? {},
46
+ devDependencies: packageJson.devDependencies ?? {},
47
+ scripts: packageJson.scripts ?? {},
48
+ lockPackages: Object.fromEntries(Array.from(lockPackages.entries()).map(([name, instances]) => [
49
+ name,
50
+ Array.from(instances.values()).sort((left, right) => left.path.localeCompare(right.path))
51
+ ]))
52
+ };
53
+ }
54
+ function extractPackageName(packagePath) {
55
+ if (!packagePath) {
56
+ return null;
57
+ }
58
+ const match = packagePath.match(/(?:^|\/)node_modules\/(.+)$/);
59
+ if (!match) {
60
+ return null;
61
+ }
62
+ return match[1];
63
+ }
@@ -0,0 +1,7 @@
1
+ export interface ScoreInputs {
2
+ duplicates: number;
3
+ unused: number;
4
+ outdated: number;
5
+ risks: number;
6
+ }
7
+ export declare function calculateHealthScore(inputs: ScoreInputs): number;
@@ -0,0 +1,8 @@
1
+ export function calculateHealthScore(inputs) {
2
+ const rawScore = 100 -
3
+ inputs.duplicates * 5 -
4
+ inputs.outdated * 3 -
5
+ inputs.unused * 4 -
6
+ inputs.risks * 10;
7
+ return Math.max(0, rawScore);
8
+ }
@@ -0,0 +1,4 @@
1
+ export { analyzeProject } from "./core/analyzer.js";
2
+ export type { AnalysisOptions, AnalysisResult, DuplicateDependency, OutdatedDependency, PolicyResult, PackageAnalysisResult, RiskDependency, UnusedDependency } from "./core/analyzer.js";
3
+ export type { DepBrainConfig, DepBrainConfigOverrides } from "./utils/config.js";
4
+ export type { WorkspacePackage } from "./utils/workspaces.js";
package/dist/index.js ADDED
@@ -0,0 +1 @@
1
+ export { analyzeProject } from "./core/analyzer.js";
@@ -0,0 +1,2 @@
1
+ import type { AnalysisResult } from "../core/analyzer.js";
2
+ export declare function renderConsoleReport(result: AnalysisResult): string;
@@ -0,0 +1,51 @@
1
+ export function renderConsoleReport(result) {
2
+ const lines = [];
3
+ lines.push(`Project Health: ${result.score}/100`);
4
+ lines.push(`Path: ${result.rootDir}`);
5
+ lines.push(`Policy: ${result.policy.passed ? "PASS" : "FAIL"}`);
6
+ lines.push("");
7
+ lines.push(summaryLine("Duplicates", result.duplicates.length));
8
+ lines.push(summaryLine("Unused", result.unused.length));
9
+ lines.push(summaryLine("Outdated", result.outdated.length));
10
+ lines.push(summaryLine("Risks", result.risks.length));
11
+ if (result.packages && result.packages.length > 0) {
12
+ lines.push("");
13
+ lines.push("Packages:");
14
+ for (const pkg of result.packages) {
15
+ lines.push(`- ${pkg.name}: ${pkg.score}/100, D:${pkg.duplicates.length} U:${pkg.unused.length} O:${pkg.outdated.length} R:${pkg.risks.length}`);
16
+ }
17
+ }
18
+ appendSection(lines, "Duplicate dependencies", result.duplicates.map((item) => `${item.name}: ${item.versions.join(", ")}`));
19
+ appendSection(lines, "Unused dependencies", result.unused.map((item) => item.package
20
+ ? `${item.name} (${item.section}) [${item.package}]`
21
+ : `${item.name} (${item.section})`));
22
+ appendSection(lines, "Outdated dependencies", result.outdated.map((item) => item.package
23
+ ? `${item.name}: ${item.current} -> ${item.latest} [${item.updateType}] [${item.package}]`
24
+ : `${item.name}: ${item.current} -> ${item.latest} [${item.updateType}]`));
25
+ appendSection(lines, "Risky dependencies", result.risks.map((item) => item.package
26
+ ? `${item.name}: ${item.reasons.join("; ")} [${item.package}]`
27
+ : `${item.name}: ${item.reasons.join("; ")}`));
28
+ appendSection(lines, "Policy reasons", result.policy.reasons);
29
+ if (result.suggestions.length > 0) {
30
+ lines.push("");
31
+ lines.push("Suggestions:");
32
+ for (const suggestion of result.suggestions) {
33
+ lines.push(`- ${suggestion}`);
34
+ }
35
+ }
36
+ return lines.join("\n");
37
+ }
38
+ function summaryLine(label, count) {
39
+ const indicator = count === 0 ? "OK" : "WARN";
40
+ return `${indicator} ${label}: ${count}`;
41
+ }
42
+ function appendSection(lines, title, entries) {
43
+ if (entries.length === 0) {
44
+ return;
45
+ }
46
+ lines.push("");
47
+ lines.push(`${title}:`);
48
+ for (const entry of entries.slice(0, 10)) {
49
+ lines.push(`- ${entry}`);
50
+ }
51
+ }
@@ -0,0 +1,2 @@
1
+ import type { AnalysisResult } from "../core/analyzer.js";
2
+ export declare function renderJsonReport(result: AnalysisResult): string;
@@ -0,0 +1,3 @@
1
+ export function renderJsonReport(result) {
2
+ return JSON.stringify(result, null, 2);
3
+ }
@@ -0,0 +1,27 @@
1
+ export interface DepBrainConfig {
2
+ ignore: {
3
+ dependencies: string[];
4
+ devDependencies: string[];
5
+ duplicates: string[];
6
+ outdated: string[];
7
+ risks: string[];
8
+ unused: string[];
9
+ };
10
+ policy: {
11
+ minScore: number;
12
+ failOnDuplicates: boolean;
13
+ failOnOutdated: boolean;
14
+ failOnRisks: boolean;
15
+ failOnUnused: boolean;
16
+ };
17
+ report: {
18
+ maxSuggestions: number;
19
+ };
20
+ }
21
+ export interface DepBrainConfigOverrides {
22
+ ignore?: Partial<DepBrainConfig["ignore"]>;
23
+ policy?: Partial<DepBrainConfig["policy"]>;
24
+ report?: Partial<DepBrainConfig["report"]>;
25
+ }
26
+ export declare const defaultConfig: DepBrainConfig;
27
+ export declare function loadDepBrainConfig(rootDir: string, configPath?: string): Promise<DepBrainConfig>;
@@ -0,0 +1,66 @@
1
+ import path from "node:path";
2
+ import { readJsonFile } from "./file-parser.js";
3
+ export const defaultConfig = {
4
+ ignore: {
5
+ dependencies: [],
6
+ devDependencies: [],
7
+ duplicates: [],
8
+ outdated: [],
9
+ risks: [],
10
+ unused: []
11
+ },
12
+ policy: {
13
+ minScore: 0,
14
+ failOnDuplicates: false,
15
+ failOnOutdated: false,
16
+ failOnRisks: false,
17
+ failOnUnused: false
18
+ },
19
+ report: {
20
+ maxSuggestions: 5
21
+ }
22
+ };
23
+ export async function loadDepBrainConfig(rootDir, configPath) {
24
+ const resolvedPath = path.resolve(rootDir, configPath ?? "depbrain.config.json");
25
+ try {
26
+ const loaded = await readJsonFile(resolvedPath);
27
+ return normalizeConfig(loaded);
28
+ }
29
+ catch {
30
+ return defaultConfig;
31
+ }
32
+ }
33
+ function normalizeConfig(loaded) {
34
+ return {
35
+ ignore: {
36
+ dependencies: normalizeStringArray(loaded.ignore?.dependencies, defaultConfig.ignore.dependencies),
37
+ devDependencies: normalizeStringArray(loaded.ignore?.devDependencies, defaultConfig.ignore.devDependencies),
38
+ duplicates: normalizeStringArray(loaded.ignore?.duplicates, defaultConfig.ignore.duplicates),
39
+ outdated: normalizeStringArray(loaded.ignore?.outdated, defaultConfig.ignore.outdated),
40
+ risks: normalizeStringArray(loaded.ignore?.risks, defaultConfig.ignore.risks),
41
+ unused: normalizeStringArray(loaded.ignore?.unused, defaultConfig.ignore.unused)
42
+ },
43
+ policy: {
44
+ minScore: normalizeNumber(loaded.policy?.minScore, defaultConfig.policy.minScore),
45
+ failOnDuplicates: normalizeBoolean(loaded.policy?.failOnDuplicates, defaultConfig.policy.failOnDuplicates),
46
+ failOnOutdated: normalizeBoolean(loaded.policy?.failOnOutdated, defaultConfig.policy.failOnOutdated),
47
+ failOnRisks: normalizeBoolean(loaded.policy?.failOnRisks, defaultConfig.policy.failOnRisks),
48
+ failOnUnused: normalizeBoolean(loaded.policy?.failOnUnused, defaultConfig.policy.failOnUnused)
49
+ },
50
+ report: {
51
+ maxSuggestions: normalizeNumber(loaded.report?.maxSuggestions, defaultConfig.report.maxSuggestions)
52
+ }
53
+ };
54
+ }
55
+ function normalizeStringArray(value, fallback) {
56
+ if (!Array.isArray(value)) {
57
+ return fallback;
58
+ }
59
+ return value.filter((item) => typeof item === "string");
60
+ }
61
+ function normalizeBoolean(value, fallback) {
62
+ return typeof value === "boolean" ? value : fallback;
63
+ }
64
+ function normalizeNumber(value, fallback) {
65
+ return typeof value === "number" && Number.isFinite(value) ? value : fallback;
66
+ }
@@ -0,0 +1,3 @@
1
+ export declare function readJsonFile<T>(filePath: string): Promise<T>;
2
+ export declare function readTextFile(filePath: string): Promise<string>;
3
+ export declare function collectProjectFiles(rootDir: string, pattern: RegExp): Promise<string[]>;
@@ -0,0 +1,27 @@
1
+ import { promises as fs } from "node:fs";
2
+ import path from "node:path";
3
+ export async function readJsonFile(filePath) {
4
+ const content = await fs.readFile(filePath, "utf8");
5
+ return JSON.parse(content);
6
+ }
7
+ export async function readTextFile(filePath) {
8
+ return fs.readFile(filePath, "utf8");
9
+ }
10
+ export async function collectProjectFiles(rootDir, pattern) {
11
+ const entries = await fs.readdir(rootDir, { withFileTypes: true });
12
+ const files = [];
13
+ for (const entry of entries) {
14
+ const fullPath = path.join(rootDir, entry.name);
15
+ if (entry.isDirectory()) {
16
+ if (entry.name === ".git") {
17
+ continue;
18
+ }
19
+ files.push(...(await collectProjectFiles(fullPath, pattern)));
20
+ continue;
21
+ }
22
+ if (pattern.test(entry.name)) {
23
+ files.push(fullPath);
24
+ }
25
+ }
26
+ return files;
27
+ }
@@ -0,0 +1,8 @@
1
+ export interface PackageMetadata {
2
+ latestVersion: string | null;
3
+ repository: string | null;
4
+ downloads: number | null;
5
+ daysSincePublish: number | null;
6
+ }
7
+ export declare function getLatestVersion(name: string): Promise<string | null>;
8
+ export declare function getPackageMetadata(name: string): Promise<PackageMetadata | null>;
@@ -0,0 +1,52 @@
1
+ const metadataCache = new Map();
2
+ export async function getLatestVersion(name) {
3
+ const metadata = await getPackageMetadata(name);
4
+ return metadata?.latestVersion ?? null;
5
+ }
6
+ export async function getPackageMetadata(name) {
7
+ const existing = metadataCache.get(name);
8
+ if (existing) {
9
+ return existing;
10
+ }
11
+ const request = fetchPackageMetadata(name);
12
+ metadataCache.set(name, request);
13
+ return request;
14
+ }
15
+ async function fetchPackageMetadata(name) {
16
+ try {
17
+ const [packageResponse, downloadsResponse] = await Promise.all([
18
+ fetch(`https://registry.npmjs.org/${encodeURIComponent(name)}`, {
19
+ signal: AbortSignal.timeout(5000)
20
+ }),
21
+ fetch(`https://api.npmjs.org/downloads/point/last-week/${encodeURIComponent(name)}`, {
22
+ signal: AbortSignal.timeout(5000)
23
+ })
24
+ ]);
25
+ if (!packageResponse.ok) {
26
+ return null;
27
+ }
28
+ const packageJson = (await packageResponse.json());
29
+ const downloadsJson = downloadsResponse.ok
30
+ ? (await downloadsResponse.json())
31
+ : {};
32
+ const latestVersion = packageJson["dist-tags"]?.latest ?? null;
33
+ const latestPublishedAt = latestVersion && packageJson.time?.[latestVersion]
34
+ ? new Date(packageJson.time[latestVersion])
35
+ : null;
36
+ const daysSincePublish = latestPublishedAt === null
37
+ ? null
38
+ : Math.floor((Date.now() - latestPublishedAt.getTime()) / (1000 * 60 * 60 * 24));
39
+ const repository = typeof packageJson.repository === "string"
40
+ ? packageJson.repository
41
+ : packageJson.repository?.url ?? null;
42
+ return {
43
+ latestVersion,
44
+ repository,
45
+ downloads: downloadsJson.downloads ?? null,
46
+ daysSincePublish
47
+ };
48
+ }
49
+ catch {
50
+ return null;
51
+ }
52
+ }
@@ -0,0 +1,6 @@
1
+ export interface WorkspacePackage {
2
+ name: string;
3
+ rootDir: string;
4
+ packageJsonPath: string;
5
+ }
6
+ export declare function findWorkspacePackages(rootDir: string): Promise<WorkspacePackage[]>;
@@ -0,0 +1,68 @@
1
+ import { promises as fs } from "node:fs";
2
+ import path from "node:path";
3
+ import { readJsonFile } from "./file-parser.js";
4
+ export async function findWorkspacePackages(rootDir) {
5
+ const rootPackageJsonPath = path.join(rootDir, "package.json");
6
+ const rootPackage = await readJsonFile(rootPackageJsonPath).catch(() => null);
7
+ if (!rootPackage?.workspaces) {
8
+ return [];
9
+ }
10
+ const patterns = Array.isArray(rootPackage.workspaces)
11
+ ? rootPackage.workspaces
12
+ : rootPackage.workspaces.packages ?? [];
13
+ if (patterns.length === 0) {
14
+ return [];
15
+ }
16
+ const packageJsonFiles = await collectPackageJsonFiles(rootDir);
17
+ const matches = packageJsonFiles.filter((filePath) => matchesWorkspacePatterns(rootDir, filePath, patterns));
18
+ const packages = [];
19
+ for (const packageJsonPath of matches) {
20
+ const pkg = await readJsonFile(packageJsonPath).catch(() => null);
21
+ if (!pkg?.name) {
22
+ continue;
23
+ }
24
+ packages.push({
25
+ name: pkg.name,
26
+ rootDir: path.dirname(packageJsonPath),
27
+ packageJsonPath
28
+ });
29
+ }
30
+ return packages.sort((left, right) => left.name.localeCompare(right.name));
31
+ }
32
+ async function collectPackageJsonFiles(rootDir) {
33
+ const entries = await fs.readdir(rootDir, { withFileTypes: true });
34
+ const files = [];
35
+ for (const entry of entries) {
36
+ if (entry.name === "node_modules" || entry.name === ".git") {
37
+ continue;
38
+ }
39
+ const fullPath = path.join(rootDir, entry.name);
40
+ if (entry.isDirectory()) {
41
+ files.push(...(await collectPackageJsonFiles(fullPath)));
42
+ continue;
43
+ }
44
+ if (entry.isFile() && entry.name === "package.json") {
45
+ files.push(fullPath);
46
+ }
47
+ }
48
+ return files;
49
+ }
50
+ function matchesWorkspacePatterns(rootDir, packageJsonPath, patterns) {
51
+ const rel = normalizePath(path.relative(rootDir, path.dirname(packageJsonPath)));
52
+ return patterns.some((pattern) => {
53
+ const regex = globToRegExp(pattern);
54
+ return regex.test(rel);
55
+ });
56
+ }
57
+ function globToRegExp(pattern) {
58
+ const normalized = normalizePath(pattern)
59
+ .replace(/\/+$/, "")
60
+ .replace(/[.+^${}()|[\]\\]/g, "\\$&")
61
+ .replace(/\*\*/g, "___DEPBRAIN_GLOBSTAR___")
62
+ .replace(/\*/g, "[^/]*")
63
+ .replace(/___DEPBRAIN_GLOBSTAR___/g, ".*");
64
+ return new RegExp(`^${normalized}$`);
65
+ }
66
+ function normalizePath(value) {
67
+ return value.split(path.sep).join("/");
68
+ }