diff --git a/main.js b/main.js index d3bfe0c..7c2d0ca 100644 --- a/main.js +++ b/main.js @@ -24170,6 +24170,30 @@ async function fetchPackageMetadata(packageName, version) { return null; } } +async function calculateTotalDependencySizeIncrease(newVersions) { + let totalSize = 0; + const processedPackages = /* @__PURE__ */ new Set(); + const packageSizes = /* @__PURE__ */ new Map(); + for (const dep of newVersions) { + const packageKey = `${dep.name}@${dep.version}`; + if (processedPackages.has(packageKey)) { + continue; + } + try { + const metadata = await fetchPackageMetadata(dep.name, dep.version); + if (!metadata || metadata.dist?.unpackedSize === void 0) { + return null; + } + totalSize += metadata.dist.unpackedSize; + packageSizes.set(packageKey, metadata.dist.unpackedSize); + processedPackages.add(packageKey); + core2.info(`Added ${metadata.dist.unpackedSize} bytes for ${packageKey}`); + } catch { + return null; + } + } + return { totalSize, packageSizes }; +} // src/main.ts function formatBytes(bytes) { @@ -24180,6 +24204,21 @@ function formatBytes(bytes) { return `${parseFloat((bytes / Math.pow(k, i)).toFixed(1))} ${sizes[i]}`; } var COMMENT_TAG = ""; +function getLsCommand(lockfilePath, packageName) { + if (lockfilePath.endsWith("package-lock.json")) { + return `npm ls ${packageName}`; + } + if (lockfilePath.endsWith("pnpm-lock.yaml")) { + return `pnpm why ${packageName}`; + } + if (lockfilePath.endsWith("yarn.lock")) { + return `yarn why ${packageName}`; + } + if (lockfilePath.endsWith("bun.lock")) { + return `bun pm ls ${packageName}`; + } + return void 0; +} async function run() { try { const workspacePath = process2.env.GITHUB_WORKSPACE || process2.cwd(); @@ -24227,8 +24266,13 @@ async function run() { core3.getInput("size-threshold") || "100000", 10 ); + const duplicateThreshold = parseInt( + core3.getInput("duplicate-threshold") || "1", + 10 + ); core3.info(`Dependency threshold set to ${dependencyThreshold}`); core3.info(`Size threshold set to ${formatBytes(sizeThreshold)}`); + core3.info(`Duplicate threshold set to ${duplicateThreshold}`); const messages = []; const currentDepCount = Array.from(currentDeps.values()).reduce( (sum, versions) => sum + versions.size, @@ -24247,6 +24291,26 @@ async function run() { `\u26A0\uFE0F **Dependency Count Warning**: This PR adds ${depIncrease} new dependencies (${baseDepCount} \u2192 ${currentDepCount}), which exceeds the threshold of ${dependencyThreshold}.` ); } + const duplicateWarnings = []; + for (const [packageName, currentVersionSet] of currentDeps) { + if (currentVersionSet.size > duplicateThreshold) { + const versions = Array.from(currentVersionSet).sort(); + duplicateWarnings.push( + `\u{1F4E6} **${packageName}**: ${currentVersionSet.size} versions (${versions.join(", ")})` + ); + } + } + if (duplicateWarnings.length > 0) { + const exampleCommand = getLsCommand(lockfilePath, "example-package"); + const helpMessage = exampleCommand ? ` + +\u{1F4A1} To find out what depends on a specific package, run: \`${exampleCommand}\`` : ""; + messages.push( + `\u26A0\uFE0F **Duplicate Dependencies Warning** (threshold: ${duplicateThreshold}): + +${duplicateWarnings.join("\n")}${helpMessage}` + ); + } const newVersions = []; for (const [packageName, currentVersionSet] of currentDeps) { const baseVersionSet = baseDeps.get(packageName); @@ -24262,28 +24326,20 @@ async function run() { } core3.info(`Found ${newVersions.length} new package versions`); if (newVersions.length > 0) { - const sizeWarnings = []; - for (const dep of newVersions) { - try { - const metadata = await fetchPackageMetadata(dep.name, dep.version); - if (metadata?.dist?.unpackedSize && metadata.dist.unpackedSize >= sizeThreshold) { - const label = dep.isNewPackage ? "new package" : "new version"; - sizeWarnings.push( - `\u{1F4E6} **${dep.name}@${dep.version}** (${label}): ${formatBytes(metadata.dist.unpackedSize)}` - ); - } - } catch (err) { - core3.info( - `Failed to check size for ${dep.name}@${dep.version}: ${err}` + try { + const sizeData = await calculateTotalDependencySizeIncrease(newVersions); + if (sizeData !== null && sizeData.totalSize >= sizeThreshold) { + const packageRows = Array.from(sizeData.packageSizes.entries()).sort(([, a], [, b]) => b - a).map(([pkg, size]) => `| ${pkg} | ${formatBytes(size)} |`).join("\n"); + messages.push( + `\u26A0\uFE0F **Large Dependency Size Increase**: This PR adds ${formatBytes(sizeData.totalSize)} of new dependencies, which exceeds the threshold of ${formatBytes(sizeThreshold)}. + +| Package | Size | +|---------|------| +${packageRows}` ); } - } - if (sizeWarnings.length > 0) { - messages.push( - `\u26A0\uFE0F **Large Package Warnings** (threshold: ${formatBytes(sizeThreshold)}): - -${sizeWarnings.join("\n")}` - ); + } catch (err) { + core3.info(`Failed to calculate total dependency size increase: ${err}`); } } if (messages.length === 0) { diff --git a/src/main.ts b/src/main.ts index cdec0ce..69ecebe 100644 --- a/src/main.ts +++ b/src/main.ts @@ -3,7 +3,7 @@ import * as core from '@actions/core'; import * as github from '@actions/github'; import {parseLockfile, detectLockfile} from './lockfile.js'; import {getFileFromRef, getBaseRef} from './git.js'; -import {fetchPackageMetadata} from './npm.js'; +import {calculateTotalDependencySizeIncrease} from './npm.js'; function formatBytes(bytes: number): string { if (bytes === 0) return '0 B'; @@ -15,6 +15,25 @@ function formatBytes(bytes: number): string { const COMMENT_TAG = ''; +function getLsCommand( + lockfilePath: string, + packageName: string +): string | undefined { + if (lockfilePath.endsWith('package-lock.json')) { + return `npm ls ${packageName}`; + } + if (lockfilePath.endsWith('pnpm-lock.yaml')) { + return `pnpm why ${packageName}`; + } + if (lockfilePath.endsWith('yarn.lock')) { + return `yarn why ${packageName}`; + } + if (lockfilePath.endsWith('bun.lock')) { + return `bun pm ls ${packageName}`; + } + return undefined; +} + async function run(): Promise { try { const workspacePath = process.env.GITHUB_WORKSPACE || process.cwd(); @@ -68,9 +87,14 @@ async function run(): Promise { core.getInput('size-threshold') || '100000', 10 ); + const duplicateThreshold = parseInt( + core.getInput('duplicate-threshold') || '1', + 10 + ); core.info(`Dependency threshold set to ${dependencyThreshold}`); core.info(`Size threshold set to ${formatBytes(sizeThreshold)}`); + core.info(`Duplicate threshold set to ${duplicateThreshold}`); const messages: string[] = []; @@ -95,6 +119,26 @@ async function run(): Promise { ); } + const duplicateWarnings: string[] = []; + for (const [packageName, currentVersionSet] of currentDeps) { + if (currentVersionSet.size > duplicateThreshold) { + const versions = Array.from(currentVersionSet).sort(); + duplicateWarnings.push( + `📦 **${packageName}**: ${currentVersionSet.size} versions (${versions.join(', ')})` + ); + } + } + + if (duplicateWarnings.length > 0) { + const exampleCommand = getLsCommand(lockfilePath, 'example-package'); + const helpMessage = exampleCommand + ? `\n\n💡 To find out what depends on a specific package, run: \`${exampleCommand}\`` + : ''; + messages.push( + `⚠️ **Duplicate Dependencies Warning** (threshold: ${duplicateThreshold}):\n\n${duplicateWarnings.join('\n')}${helpMessage}` + ); + } + const newVersions: Array<{ name: string; version: string; @@ -118,31 +162,22 @@ async function run(): Promise { core.info(`Found ${newVersions.length} new package versions`); if (newVersions.length > 0) { - const sizeWarnings: string[] = []; - - for (const dep of newVersions) { - try { - const metadata = await fetchPackageMetadata(dep.name, dep.version); - if ( - metadata?.dist?.unpackedSize && - metadata.dist.unpackedSize >= sizeThreshold - ) { - const label = dep.isNewPackage ? 'new package' : 'new version'; - sizeWarnings.push( - `📦 **${dep.name}@${dep.version}** (${label}): ${formatBytes(metadata.dist.unpackedSize)}` - ); - } - } catch (err) { - core.info( - `Failed to check size for ${dep.name}@${dep.version}: ${err}` + try { + const sizeData = + await calculateTotalDependencySizeIncrease(newVersions); + + if (sizeData !== null && sizeData.totalSize >= sizeThreshold) { + const packageRows = Array.from(sizeData.packageSizes.entries()) + .sort(([, a], [, b]) => b - a) + .map(([pkg, size]) => `| ${pkg} | ${formatBytes(size)} |`) + .join('\n'); + + messages.push( + `⚠️ **Large Dependency Size Increase**: This PR adds ${formatBytes(sizeData.totalSize)} of new dependencies, which exceeds the threshold of ${formatBytes(sizeThreshold)}.\n\n| Package | Size |\n|---------|------|\n${packageRows}` ); } - } - - if (sizeWarnings.length > 0) { - messages.push( - `⚠️ **Large Package Warnings** (threshold: ${formatBytes(sizeThreshold)}):\n\n${sizeWarnings.join('\n')}` - ); + } catch (err) { + core.info(`Failed to calculate total dependency size increase: ${err}`); } } diff --git a/src/npm.ts b/src/npm.ts index 46356a2..4c2d848 100644 --- a/src/npm.ts +++ b/src/npm.ts @@ -6,6 +6,11 @@ export interface PackageMetadata { dist?: { unpackedSize?: number; }; + dependencies?: Record; +} + +export interface PackageIndex { + versions: Record; } export async function fetchPackageMetadata( @@ -24,3 +29,37 @@ export async function fetchPackageMetadata( return null; } } + +export async function calculateTotalDependencySizeIncrease( + newVersions: Array<{name: string; version: string}> +): Promise<{totalSize: number; packageSizes: Map} | null> { + let totalSize = 0; + const processedPackages = new Set(); + const packageSizes = new Map(); + + for (const dep of newVersions) { + const packageKey = `${dep.name}@${dep.version}`; + + if (processedPackages.has(packageKey)) { + continue; + } + + try { + const metadata = await fetchPackageMetadata(dep.name, dep.version); + + if (!metadata || metadata.dist?.unpackedSize === undefined) { + return null; + } + + totalSize += metadata.dist.unpackedSize; + packageSizes.set(packageKey, metadata.dist.unpackedSize); + processedPackages.add(packageKey); + + core.info(`Added ${metadata.dist.unpackedSize} bytes for ${packageKey}`); + } catch { + return null; + } + } + + return {totalSize, packageSizes}; +}