Skip to content

Commit

Permalink
Merge pull request xanaawakens#1 from avixiii-dev/develop
Browse files Browse the repository at this point in the history
Develop
  • Loading branch information
xanaawakens authored Dec 29, 2024
2 parents b463663 + 7b53fa0 commit 67230f5
Show file tree
Hide file tree
Showing 4 changed files with 175 additions and 79 deletions.
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
201 changes: 135 additions & 66 deletions src/core/analyzer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<BundleSizeTrackerOptions>;
Expand All @@ -12,47 +13,113 @@ 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);
}
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<BundleSize> {
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<BundleReport> {
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<void> {
switch (this.options.outputFormat) {
case 'json':
Expand All @@ -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<void> {
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<void> {
await fs.mkdir(this.options.outputPath, { recursive: true });
const filePath = path.join(this.options.outputPath, 'bundle-size-report.html');

const html = `
<!DOCTYPE html>
<html>
<head>
<title>Bundle Size Report</title>
<style>
body { font-family: -apple-system, system-ui, sans-serif; margin: 2rem; }
.bundle { margin: 1rem 0; padding: 1rem; border: 1px solid #ddd; }
.exceeded { background-color: #fff0f0; border-color: #ff8080; }
.ok { background-color: #f0fff0; border-color: #80ff80; }
</style>
</head>
<body>
<h1>Bundle Size Report</h1>
<p>Generated: ${report.timestamp}</p>
<p>Status: <strong>${report.status}</strong></p>
<p>Total Size: ${filesize(report.totalSize)}</p>
<div class="bundles">
${report.bundles.map(bundle => `
<div class="bundle ${bundle.exceedsLimit ? 'exceeded' : 'ok'}">
<h3>${bundle.name}</h3>
<p>Size: ${filesize(bundle.size)}</p>
<p>Limit: ${bundle.sizeLimit}KB</p>
<p>Status: ${bundle.exceedsLimit ? '❌ Exceeds limit' : '✅ Within limit'}</p>
</div>
`).join('')}
</div>
</body>
</html>
`;

await fs.writeFile(filePath, html);
}
const html = `<!DOCTYPE html>
<html>
<head>
<title>Bundle Size Report</title>
<style>
body { font-family: -apple-system, system-ui, sans-serif; padding: 20px; }
.pass { color: green; } .fail { color: red; }
table { border-collapse: collapse; width: 100%; }
th, td { border: 1px solid #ddd; padding: 8px; text-align: left; }
th { background-color: #f5f5f5; }
</style>
</head>
<body>
<h1>Bundle Size Report</h1>
<p>Generated: ${report.timestamp}</p>
<p>Status: <span class="${report.status}">${report.status.toUpperCase()}</span></p>
<p>Total Size: ${this.formatSize(report.totalSize)}</p>
<h2>Bundles</h2>
<table>
<tr>
<th>Name</th>
<th>Size</th>
<th>Limit</th>
<th>Status</th>
</tr>
${report.bundles.map(bundle => `
<tr>
<td>${bundle.name}</td>
<td>${this.formatSize(bundle.size)}</td>
<td>${this.formatLimit(bundle.sizeLimit)}</td>
<td class="${bundle.exceedsLimit ? 'fail' : 'pass'}">
${bundle.exceedsLimit ? 'Exceeds limit' : 'Within limit'}
</td>
</tr>
`).join('')}
</table>
</body>
</html>`;

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);
}
}
48 changes: 36 additions & 12 deletions src/types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,45 +21,69 @@ export interface BundleSizeTrackerOptions {
* Custom rules for specific bundles
*/
rules?: BundleRule[];

/**
* Enable compression analysis
* @default true
*/
compression?: boolean | {
gzip?: boolean;
brotli?: boolean;
};
}

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;

Expand All @@ -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;
}

0 comments on commit 67230f5

Please sign in to comment.