diff --git a/README.md b/README.md index f865c5e..61b4d81 100644 --- a/README.md +++ b/README.md @@ -17,6 +17,9 @@ A powerful and flexible tool to track and analyze JavaScript bundle sizes across - Console output - JSON reports - HTML reports with visualizations +- File compression analysis: + - Gzip compression + - Brotli compression - Custom size limits per bundle - Easy CI/CD integration - Full TypeScript support diff --git a/package.json b/package.json index 6a21a93..5463073 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@avixiii/bundle-size-tracker", - "version": "0.1.0", + "version": "0.1.1", "description": "A powerful and flexible tool to track and analyze JavaScript bundle sizes across different build tools", "type": "module", "main": "dist/index.js", diff --git a/src/core/analyzer.ts b/src/core/analyzer.ts index 2c680bd..cdf02ec 100644 --- a/src/core/analyzer.ts +++ b/src/core/analyzer.ts @@ -2,7 +2,8 @@ import { promises as fs } from 'node:fs'; import * as path from 'node:path'; import { filesize } from 'filesize'; import chalk from 'chalk'; -import type { BundleSizeTrackerOptions, BundleInfo, BundleReport } from '../types'; +import { gzipSync, brotliCompressSync } from 'node:zlib'; +import type { BundleSizeTrackerOptions, BundleInfo, BundleReport, BundleSize } from '../types'; export class BundleSizeAnalyzer { private options: Required; @@ -12,11 +13,12 @@ export class BundleSizeAnalyzer { maxSize: options.maxSize ?? 500, outputFormat: options.outputFormat ?? 'console', outputPath: options.outputPath ?? './report', - rules: options.rules ?? [] + rules: options.rules ?? [], + compression: options.compression ?? true }; } - private getSizeLimit(fileName: string): number { + private getSizeLimit(fileName: string) { const matchingRule = this.options.rules.find(rule => { if (rule.pattern instanceof RegExp) { return rule.pattern.test(fileName); @@ -24,35 +26,100 @@ export class BundleSizeAnalyzer { return fileName.includes(rule.pattern); }); - return matchingRule?.maxSize ?? this.options.maxSize; + return { + raw: matchingRule?.maxSize ?? this.options.maxSize, + gzip: matchingRule?.maxCompressedSize, + brotli: matchingRule?.maxCompressedSize + }; + } + + private async getFileSize(filePath: string): Promise { + const content = await fs.readFile(filePath); + const size: BundleSize = { raw: content.length }; + + const compression = this.options.compression; + if (compression === false) return size; + + const useGzip = compression === true || compression.gzip; + const useBrotli = compression === true || compression.brotli; + + if (useGzip) { + size.gzip = gzipSync(content).length; + } + + if (useBrotli) { + size.brotli = brotliCompressSync(content).length; + } + + return size; + } + + private checkSizeLimits(size: BundleSize, limits: BundleInfo['sizeLimit']): boolean { + if (size.raw > limits.raw * 1024) return true; + if (limits.gzip && size.gzip && size.gzip > limits.gzip * 1024) return true; + if (limits.brotli && size.brotli && size.brotli > limits.brotli * 1024) return true; + return false; } async analyzeBundles(files: string[]): Promise { const bundles: BundleInfo[] = await Promise.all( files.map(async (file) => { - const stats = await fs.stat(file); + const size = await this.getFileSize(file); const sizeLimit = this.getSizeLimit(path.basename(file)); return { name: path.basename(file), - size: stats.size, - exceedsLimit: stats.size > sizeLimit * 1024, // Convert KB to bytes + size, + exceedsLimit: this.checkSizeLimits(size, sizeLimit), sizeLimit }; }) ); + const totalSize: BundleSize = { + raw: bundles.reduce((sum, b) => sum + b.size.raw, 0) + }; + + if (this.options.compression !== false) { + if (bundles.some(b => b.size.gzip)) { + totalSize.gzip = bundles.reduce((sum, b) => sum + (b.size.gzip || 0), 0); + } + if (bundles.some(b => b.size.brotli)) { + totalSize.brotli = bundles.reduce((sum, b) => sum + (b.size.brotli || 0), 0); + } + } + const report: BundleReport = { timestamp: new Date().toISOString(), bundles, status: bundles.some(b => b.exceedsLimit) ? 'fail' : 'pass', - totalSize: bundles.reduce((sum, b) => sum + b.size, 0) + totalSize }; await this.generateReport(report); return report; } + private formatSize(size: BundleSize): string { + const parts = [ + `Raw: ${filesize(size.raw)}`, + size.gzip && `Gzip: ${filesize(size.gzip)}`, + size.brotli && `Brotli: ${filesize(size.brotli)}` + ].filter(Boolean); + + return parts.join(' | '); + } + + private formatLimit(limit: BundleInfo['sizeLimit']): string { + const parts = [ + `Raw: ${limit.raw}KB`, + limit.gzip && `Gzip: ${limit.gzip}KB`, + limit.brotli && `Brotli: ${limit.brotli}KB` + ].filter(Boolean); + + return parts.join(' | '); + } + private async generateReport(report: BundleReport): Promise { switch (this.options.outputFormat) { case 'json': @@ -62,71 +129,73 @@ export class BundleSizeAnalyzer { await this.generateHtmlReport(report); break; default: - this.printConsoleReport(report); + this.generateConsoleReport(report); + } + } + + private generateConsoleReport(report: BundleReport): void { + console.log('\n Bundle Size Report\n'); + console.log(`Generated: ${report.timestamp}`); + console.log(`Status: ${report.status === 'pass' ? chalk.green('PASS') : chalk.red('FAIL')}`); + console.log(`Total Size: ${this.formatSize(report.totalSize)}\n`); + + for (const bundle of report.bundles) { + console.log(bundle.name); + console.log(`Size: ${this.formatSize(bundle.size)}`); + console.log(`Limit: ${this.formatLimit(bundle.sizeLimit)}`); + console.log(`Status: ${bundle.exceedsLimit ? chalk.red('Exceeds limit') : chalk.green('Within limit')}\n`); } } private async generateJsonReport(report: BundleReport): Promise { - await fs.mkdir(this.options.outputPath, { recursive: true }); - const filePath = path.join(this.options.outputPath, 'bundle-size-report.json'); - await fs.writeFile(filePath, JSON.stringify(report, null, 2)); + const outputPath = path.resolve(this.options.outputPath, 'bundle-size-report.json'); + await fs.mkdir(path.dirname(outputPath), { recursive: true }); + await fs.writeFile(outputPath, JSON.stringify(report, null, 2)); } private async generateHtmlReport(report: BundleReport): Promise { - await fs.mkdir(this.options.outputPath, { recursive: true }); - const filePath = path.join(this.options.outputPath, 'bundle-size-report.html'); - - const html = ` - - - - Bundle Size Report - - - -

Bundle Size Report

-

Generated: ${report.timestamp}

-

Status: ${report.status}

-

Total Size: ${filesize(report.totalSize)}

- -
- ${report.bundles.map(bundle => ` -
-

${bundle.name}

-

Size: ${filesize(bundle.size)}

-

Limit: ${bundle.sizeLimit}KB

-

Status: ${bundle.exceedsLimit ? 'āŒ Exceeds limit' : 'āœ… Within limit'}

-
- `).join('')} -
- - - `; - - await fs.writeFile(filePath, html); - } + const html = ` + + + Bundle Size Report + + + +

Bundle Size Report

+

Generated: ${report.timestamp}

+

Status: ${report.status.toUpperCase()}

+

Total Size: ${this.formatSize(report.totalSize)}

+ +

Bundles

+ + + + + + + + ${report.bundles.map(bundle => ` + + + + + + + `).join('')} +
NameSizeLimitStatus
${bundle.name}${this.formatSize(bundle.size)}${this.formatLimit(bundle.sizeLimit)} + ${bundle.exceedsLimit ? 'Exceeds limit' : 'Within limit'} +
+ + `; - private printConsoleReport(report: BundleReport): void { - console.log('\nšŸ“¦ Bundle Size Report\n'); - console.log(`Generated: ${report.timestamp}`); - console.log(`Status: ${report.status === 'pass' ? chalk.green('PASS') : chalk.red('FAIL')}`); - console.log(`Total Size: ${filesize(report.totalSize)}\n`); - - report.bundles.forEach(bundle => { - const sizeText = filesize(bundle.size); - const status = bundle.exceedsLimit - ? chalk.red('āŒ Exceeds limit') - : chalk.green('āœ… Within limit'); - - console.log(`${chalk.bold(bundle.name)}`); - console.log(`Size: ${sizeText}`); - console.log(`Limit: ${bundle.sizeLimit}KB`); - console.log(`Status: ${status}\n`); - }); + const outputPath = path.resolve(this.options.outputPath, 'bundle-size-report.html'); + await fs.mkdir(path.dirname(outputPath), { recursive: true }); + await fs.writeFile(outputPath, html); } } diff --git a/src/types/index.ts b/src/types/index.ts index ebba196..666b3fc 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -21,6 +21,15 @@ export interface BundleSizeTrackerOptions { * Custom rules for specific bundles */ rules?: BundleRule[]; + + /** + * Enable compression analysis + * @default true + */ + compression?: boolean | { + gzip?: boolean; + brotli?: boolean; + }; } export interface BundleRule { @@ -28,38 +37,53 @@ export interface BundleRule { * Pattern to match bundle names */ pattern: string | RegExp; - + /** - * Maximum size in KB for matched bundles + * Maximum allowed size in KB */ maxSize: number; + + /** + * Maximum allowed compressed size in KB + */ + maxCompressedSize?: number; +} + +export interface BundleSize { + raw: number; + gzip?: number; + brotli?: number; } export interface BundleInfo { /** - * Name of the bundle file + * Bundle file name */ name: string; /** - * Size of the bundle in bytes + * Bundle sizes (raw and compressed) */ - size: number; + size: BundleSize; /** - * Whether the bundle exceeds its size limit + * Whether the bundle exceeds size limits */ exceedsLimit: boolean; /** - * The applicable size limit in KB + * Size limits in KB */ - sizeLimit: number; + sizeLimit: { + raw: number; + gzip?: number; + brotli?: number; + }; } export interface BundleReport { /** - * Timestamp of the report + * Report generation timestamp */ timestamp: string; @@ -69,12 +93,12 @@ export interface BundleReport { bundles: BundleInfo[]; /** - * Overall status of size checks + * Overall status */ status: 'pass' | 'fail'; /** - * Total size of all bundles + * Total sizes */ - totalSize: number; + totalSize: BundleSize; }