Skip to content

Commit f2dbebd

Browse files
committed
ci: extract script from workflow
1 parent 517851f commit f2dbebd

File tree

2 files changed

+183
-35
lines changed

2 files changed

+183
-35
lines changed

.github/workflows/size-comment.yml

Lines changed: 27 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -37,47 +37,29 @@ jobs:
3737
with:
3838
github-token: ${{ secrets.GITHUB_TOKEN }}
3939
run-id: ${{ github.event.workflow_run.id }}
40+
41+
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
42+
if: steps.pr.outputs.result
43+
with:
44+
sparse-checkout: scripts/parse-sizes.ts
45+
sparse-checkout-cone-mode: false
46+
47+
- name: 📊 Generate size comparison
48+
if: steps.pr.outputs.result
49+
id: sizes
50+
run: |
51+
COMMENT=$(node scripts/parse-sizes.ts nuxi nuxt-cli create-nuxt)
52+
echo "comment<<EOF" >> $GITHUB_OUTPUT
53+
echo "$COMMENT" >> $GITHUB_OUTPUT
54+
echo "EOF" >> $GITHUB_OUTPUT
4055
41-
- name: 📊 Compare and comment on sizes
56+
- name: 💬 Post or update comment
4257
if: steps.pr.outputs.result
4358
uses: actions/github-script@v8
4459
with:
4560
script: |
46-
const fs = require('fs');
47-
const path = require('path');
4861
const prNumber = ${{ steps.pr.outputs.result }};
49-
50-
if (!prNumber) {
51-
console.log('No PR number found, skipping comment');
52-
return;
53-
}
54-
55-
const packages = ['nuxi', 'nuxt-cli', 'create-nuxt'];
56-
let commentBody = '## 📦 Bundle Size Comparison\n\n';
57-
58-
for (const pkg of packages) {
59-
try {
60-
const headStats = JSON.parse(fs.readFileSync(`./head-stats/${pkg}/stats.json`, 'utf8'));
61-
const baseStats = JSON.parse(fs.readFileSync(`./base-stats/${pkg}/stats.json`, 'utf8'));
62-
63-
// Simple size comparison - adjust based on your stats.json structure
64-
const headSize = headStats.bundleSize || headStats.size || 0;
65-
const baseSize = baseStats.bundleSize || baseStats.size || 0;
66-
const diff = headSize - baseSize;
67-
const diffPercent = baseSize ? ((diff / baseSize) * 100).toFixed(2) : 0;
68-
69-
const icon = diff > 0 ? '📈' : diff < 0 ? '📉' : '➡️';
70-
const sign = diff > 0 ? '+' : '';
71-
72-
commentBody += `### ${icon} ${pkg}\n`;
73-
commentBody += `- Base: ${(baseSize / 1024).toFixed(2)} KB\n`;
74-
commentBody += `- Head: ${(headSize / 1024).toFixed(2)} KB\n`;
75-
commentBody += `- Diff: ${sign}${(diff / 1024).toFixed(2)} KB (${sign}${diffPercent}%)\n\n`;
76-
} catch (error) {
77-
console.log(`Error processing ${pkg}:`, error.message);
78-
commentBody += `### ⚠️ ${pkg}\nCould not compare sizes\n\n`;
79-
}
80-
}
62+
const commentBody = `${{ steps.sizes.outputs.comment }}`;
8163
8264
// Find existing comment
8365
const comments = await github.rest.issues.listComments({
@@ -108,3 +90,13 @@ jobs:
10890
body: commentBody
10991
});
11092
}
93+
});
94+
} else {
95+
// Create new comment
96+
await github.rest.issues.createComment({
97+
owner: context.repo.owner,
98+
repo: context.repo.repo,
99+
issue_number: prNumber,
100+
body: commentBody
101+
});
102+
}

scripts/parse-sizes.ts

Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
import { readFileSync } from 'node:fs'
2+
import process from 'node:process'
3+
4+
interface RollupStatsNode {
5+
renderedLength?: number
6+
gzipLength?: number
7+
brotliLength?: number
8+
}
9+
10+
interface RollupStats {
11+
nodeParts?: Record<string, RollupStatsNode>
12+
}
13+
14+
interface BundleSize {
15+
rendered: number
16+
gzip: number
17+
brotli: number
18+
}
19+
20+
interface PackageComparison {
21+
name: string
22+
base: BundleSize
23+
head: BundleSize
24+
diff: {
25+
rendered: number
26+
gzip: number
27+
brotli: number
28+
}
29+
error?: string
30+
}
31+
32+
/**
33+
* Calculate total size from Rollup stats
34+
*/
35+
function calculateTotalSize(stats: RollupStats): BundleSize {
36+
if (!stats.nodeParts) {
37+
return { rendered: 0, gzip: 0, brotli: 0 }
38+
}
39+
40+
let totalRendered = 0
41+
let totalGzip = 0
42+
let totalBrotli = 0
43+
44+
for (const node of Object.values(stats.nodeParts)) {
45+
totalRendered += node.renderedLength || 0
46+
totalGzip += node.gzipLength || 0
47+
totalBrotli += node.brotliLength || 0
48+
}
49+
50+
return { rendered: totalRendered, gzip: totalGzip, brotli: totalBrotli }
51+
}
52+
53+
/**
54+
* Format bytes to KB with 2 decimal places
55+
*/
56+
function formatBytes(bytes: number): string {
57+
return (bytes / 1024).toFixed(2)
58+
}
59+
60+
/**
61+
* Format diff with sign and percentage
62+
*/
63+
function formatDiff(diff: number, base: number): { icon: string, sign: string, percent: string } {
64+
const percent = base ? ((diff / base) * 100).toFixed(2) : '0.00'
65+
const sign = diff > 0 ? '+' : ''
66+
const icon = diff > 0 ? '📈' : diff < 0 ? '📉' : '➡️'
67+
return { icon, sign, percent }
68+
}
69+
70+
/**
71+
* Compare sizes for a single package
72+
*/
73+
function comparePackage(name: string, headPath: string, basePath: string): PackageComparison {
74+
try {
75+
const headStats: RollupStats = JSON.parse(readFileSync(headPath, 'utf8'))
76+
const baseStats: RollupStats = JSON.parse(readFileSync(basePath, 'utf8'))
77+
78+
const head = calculateTotalSize(headStats)
79+
const base = calculateTotalSize(baseStats)
80+
81+
return {
82+
name,
83+
base,
84+
head,
85+
diff: {
86+
rendered: head.rendered - base.rendered,
87+
gzip: head.gzip - base.gzip,
88+
brotli: head.brotli - base.brotli,
89+
},
90+
}
91+
}
92+
catch (error) {
93+
return {
94+
name,
95+
base: { rendered: 0, gzip: 0, brotli: 0 },
96+
head: { rendered: 0, gzip: 0, brotli: 0 },
97+
diff: { rendered: 0, gzip: 0, brotli: 0 },
98+
error: error instanceof Error ? error.message : String(error),
99+
}
100+
}
101+
}
102+
103+
/**
104+
* Generate markdown comment for size comparison
105+
*/
106+
export function generateSizeComment(packages: string[], statsDir = '.'): string {
107+
let commentBody = '## 📦 Bundle Size Comparison\n\n'
108+
109+
for (const pkg of packages) {
110+
const headPath = `${statsDir}/head-stats/${pkg}/stats.json`
111+
const basePath = `${statsDir}/base-stats/${pkg}/stats.json`
112+
113+
const comparison = comparePackage(pkg, headPath, basePath)
114+
115+
if (comparison.error) {
116+
console.error(`Error processing ${pkg}:`, comparison.error)
117+
commentBody += `### ⚠️ **${pkg}**\n\nCould not compare sizes: ${comparison.error}\n\n`
118+
continue
119+
}
120+
121+
const { icon, sign, percent } = formatDiff(comparison.diff.rendered, comparison.base.rendered)
122+
123+
commentBody += `### ${icon} **${pkg}**\n\n`
124+
commentBody += `| Metric | Base | Head | Diff |\n`
125+
commentBody += `|--------|------|------|------|\n`
126+
commentBody += `| Rendered | ${formatBytes(comparison.base.rendered)} KB | ${formatBytes(comparison.head.rendered)} KB | ${sign}${formatBytes(comparison.diff.rendered)} KB (${sign}${percent}%) |\n`
127+
128+
if (comparison.base.gzip > 0 || comparison.head.gzip > 0) {
129+
const gzipFmt = formatDiff(comparison.diff.gzip, comparison.base.gzip)
130+
commentBody += `| Gzip | ${formatBytes(comparison.base.gzip)} KB | ${formatBytes(comparison.head.gzip)} KB | ${gzipFmt.sign}${formatBytes(comparison.diff.gzip)} KB (${gzipFmt.sign}${gzipFmt.percent}%) |\n`
131+
}
132+
133+
commentBody += '\n'
134+
}
135+
136+
return commentBody
137+
}
138+
139+
// CLI usage
140+
const isMainModule = process.argv[1] && (
141+
import.meta.url === `file://${process.argv[1]}`
142+
|| import.meta.url.endsWith(process.argv[1])
143+
)
144+
145+
if (isMainModule) {
146+
const packages = process.argv.slice(2)
147+
if (packages.length === 0) {
148+
console.error('Usage: node scripts/parse-sizes.ts <package1> <package2> ...')
149+
console.error('')
150+
console.error('Example: node scripts/parse-sizes.ts nuxi nuxt-cli create-nuxt')
151+
process.exit(1)
152+
}
153+
154+
const comment = generateSizeComment(packages)
155+
console.log(comment)
156+
}

0 commit comments

Comments
 (0)