forked from mirrors/misskey
refactor(dev): unify frontend-bundle-visualizer-report.mjs and frontend-js-size.mjs
This commit is contained in:
parent
544c4227f7
commit
8186742c0f
4 changed files with 331 additions and 391 deletions
|
|
@ -1,276 +0,0 @@
|
|||
import { readFile, writeFile } from 'node:fs/promises';
|
||||
|
||||
const [beforeFile, afterFile, outputFile] = process.argv.slice(2);
|
||||
|
||||
if (beforeFile == null || afterFile == null || outputFile == null) {
|
||||
console.error('Usage: node .github/scripts/frontend-bundle-visualizer-report.mjs <before-stats.json> <after-stats.json> <report.md>');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const byteFormatter = new Intl.NumberFormat('en-US');
|
||||
const numberFormatter = new Intl.NumberFormat('en-US');
|
||||
|
||||
function formatBytes(value) {
|
||||
if (!Number.isFinite(value) || value <= 0) return '0 B';
|
||||
|
||||
const units = ['B', 'KiB', 'MiB', 'GiB'];
|
||||
let unitIndex = 0;
|
||||
let size = value;
|
||||
while (size >= 1024 && unitIndex < units.length - 1) {
|
||||
size /= 1024;
|
||||
unitIndex += 1;
|
||||
}
|
||||
|
||||
const maximumFractionDigits = size >= 10 || unitIndex === 0 ? 0 : 1;
|
||||
return `${byteFormatter.format(Number(size.toFixed(maximumFractionDigits)))} ${units[unitIndex]}`;
|
||||
}
|
||||
|
||||
function formatNumber(value) {
|
||||
return numberFormatter.format(value);
|
||||
}
|
||||
|
||||
function formatPercent(value) {
|
||||
return `${Math.round(value)}%`;
|
||||
}
|
||||
|
||||
function sharePercent(value, total) {
|
||||
if (total === 0) return '0%';
|
||||
return formatPercent((value / total) * 100);
|
||||
}
|
||||
|
||||
function formatMathText(text) {
|
||||
return text
|
||||
.replaceAll('\\', '\\\\')
|
||||
.replaceAll('{', '\\{')
|
||||
.replaceAll('}', '\\}')
|
||||
.replaceAll('%', '\\%');
|
||||
}
|
||||
|
||||
function formatColoredDiff(text, diff) {
|
||||
const color = diff > 0 ? 'orange' : 'green';
|
||||
return `$\\color{${color}}{\\text{${formatMathText(text)}}}$`;
|
||||
}
|
||||
|
||||
function formatDiff(before, after, formatter) {
|
||||
const diff = after - before;
|
||||
if (diff === 0) return formatter(0);
|
||||
|
||||
const sign = diff > 0 ? '+' : '-';
|
||||
return formatColoredDiff(`${sign}${formatter(Math.abs(diff))}`, diff);
|
||||
}
|
||||
|
||||
function formatDiffPercent(before, after) {
|
||||
if (before === 0 && after === 0) return '0%';
|
||||
if (before === 0) return '-';
|
||||
|
||||
const diff = after - before;
|
||||
if (diff === 0) return '0%';
|
||||
|
||||
const sign = diff > 0 ? '+' : '-';
|
||||
return formatColoredDiff(`${sign}${formatPercent(Math.abs(diff / before) * 100)}`, diff);
|
||||
}
|
||||
|
||||
function tableCell(value) {
|
||||
return String(value).replaceAll('|', '\\|').replaceAll('\r', ' ').replaceAll('\n', ' ');
|
||||
}
|
||||
|
||||
function code(value) {
|
||||
const sanitized = String(value).replaceAll('\r', ' ').replaceAll('\n', ' ');
|
||||
const backtickRuns = sanitized.match(/`+/g) ?? [];
|
||||
const fenceLength = Math.max(1, ...backtickRuns.map((run) => run.length + 1));
|
||||
const fence = '`'.repeat(fenceLength);
|
||||
const padding = sanitized.startsWith('`') || sanitized.endsWith('`') ? ' ' : '';
|
||||
|
||||
return `${fence}${padding}${sanitized}${padding}${fence}`;
|
||||
}
|
||||
|
||||
function tableCode(value) {
|
||||
return tableCell(code(value));
|
||||
}
|
||||
|
||||
function collectReport(data) {
|
||||
const nodeParts = data.nodeParts ?? {};
|
||||
const nodeMetas = Object.values(data.nodeMetas ?? {});
|
||||
const moduleRows = [];
|
||||
const bundleMap = new Map();
|
||||
|
||||
for (const meta of nodeMetas) {
|
||||
const row = {
|
||||
id: meta.id,
|
||||
bundles: 0,
|
||||
renderedLength: 0,
|
||||
gzipLength: 0,
|
||||
brotliLength: 0,
|
||||
importedByCount: meta.importedBy?.length ?? 0,
|
||||
importedCount: meta.imported?.length ?? 0,
|
||||
};
|
||||
|
||||
for (const [bundleId, partUid] of Object.entries(meta.moduleParts ?? {})) {
|
||||
const part = nodeParts[partUid];
|
||||
if (part == null) continue;
|
||||
|
||||
row.bundles += 1;
|
||||
row.renderedLength += part.renderedLength;
|
||||
row.gzipLength += part.gzipLength;
|
||||
row.brotliLength += part.brotliLength;
|
||||
|
||||
const bundle = bundleMap.get(bundleId) ?? {
|
||||
id: bundleId,
|
||||
modules: 0,
|
||||
renderedLength: 0,
|
||||
gzipLength: 0,
|
||||
brotliLength: 0,
|
||||
};
|
||||
bundle.modules += 1;
|
||||
bundle.renderedLength += part.renderedLength;
|
||||
bundle.gzipLength += part.gzipLength;
|
||||
bundle.brotliLength += part.brotliLength;
|
||||
bundleMap.set(bundleId, bundle);
|
||||
}
|
||||
|
||||
if (row.bundles > 0) {
|
||||
moduleRows.push(row);
|
||||
}
|
||||
}
|
||||
|
||||
let staticImports = 0;
|
||||
let dynamicImports = 0;
|
||||
for (const meta of nodeMetas) {
|
||||
for (const imported of meta.imported ?? []) {
|
||||
if (imported.dynamic) {
|
||||
dynamicImports += 1;
|
||||
} else {
|
||||
staticImports += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const bundleRows = [...bundleMap.values()].sort((a, b) => b.renderedLength - a.renderedLength);
|
||||
const hotModules = [...moduleRows].sort((a, b) => b.renderedLength - a.renderedLength);
|
||||
const totalRendered = moduleRows.reduce((sum, row) => sum + row.renderedLength, 0);
|
||||
const totalGzip = moduleRows.reduce((sum, row) => sum + row.gzipLength, 0);
|
||||
const totalBrotli = moduleRows.reduce((sum, row) => sum + row.brotliLength, 0);
|
||||
|
||||
return {
|
||||
options: data.options ?? {},
|
||||
summary: {
|
||||
bundles: bundleRows.length,
|
||||
modules: moduleRows.length,
|
||||
entries: nodeMetas.filter((meta) => meta.isEntry).length,
|
||||
externals: nodeMetas.filter((meta) => meta.isExternal).length,
|
||||
staticImports,
|
||||
dynamicImports,
|
||||
},
|
||||
metrics: {
|
||||
renderedLength: totalRendered,
|
||||
gzipLength: totalGzip,
|
||||
brotliLength: totalBrotli,
|
||||
},
|
||||
hotModules,
|
||||
};
|
||||
}
|
||||
|
||||
function renderSummaryTable(before, after) {
|
||||
const summary = [
|
||||
'bundles',
|
||||
'modules',
|
||||
'entries',
|
||||
//'externals',
|
||||
'staticImports',
|
||||
'dynamicImports',
|
||||
];
|
||||
|
||||
const metrics = [
|
||||
'renderedLength',
|
||||
'gzipLength',
|
||||
'brotliLength',
|
||||
];
|
||||
|
||||
return [
|
||||
`<table>`,
|
||||
`<thead>`,
|
||||
`<tr>`,
|
||||
`<th rowspan="2"></th>`,
|
||||
`<th rowspan="2">Bundles</th>`,
|
||||
`<th rowspan="2">Modules</th>`,
|
||||
`<th rowspan="2">Entries</th>`,
|
||||
`<th colspan="2">Imports</th>`,
|
||||
`<th colspan="3">Size</th>`,
|
||||
`</tr>`,
|
||||
`<tr>`,
|
||||
`<th>Static</th>`,
|
||||
`<th>Dynamic</th>`,
|
||||
`<th>Rendered</th>`,
|
||||
`<th>Gzip</th>`,
|
||||
`<th>Brotli</th>`,
|
||||
`</tr>`,
|
||||
`</thead>`,
|
||||
`<tbody>`,
|
||||
`<tr>`,
|
||||
`<th><b>Before</b></th>`,
|
||||
...summary.map((key) => `<td>${formatNumber(before.summary[key])}</td>`),
|
||||
...metrics.map((key) => `<td>${formatBytes(before.metrics[key])}</td>`),
|
||||
`</tr>`,
|
||||
`<tr>`,
|
||||
`<th><b>After</b></th>`,
|
||||
...summary.map((key) => `<td>${formatNumber(after.summary[key])}</td>`),
|
||||
...metrics.map((key) => `<td>${formatBytes(after.metrics[key])}</td>`),
|
||||
`</tr>`,
|
||||
`<tr><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td></tr>`,
|
||||
`<tr>`,
|
||||
`<th><b>Δ</b></th>`,
|
||||
...summary.map((key) => `<td>${formatDiff(before.summary[key], after.summary[key], formatNumber)}</td>`),
|
||||
...metrics.map((key) => `<td>${formatDiff(before.metrics[key], after.metrics[key], formatBytes)}</td>`),
|
||||
`</tr>`,
|
||||
`<tr>`,
|
||||
`<th><b>Δ (%)</b></th>`,
|
||||
...summary.map((key) => `<td>${formatDiffPercent(before.summary[key], after.summary[key])}</td>`),
|
||||
...metrics.map((key) => `<td>${formatDiffPercent(before.metrics[key], after.metrics[key])}</td>`),
|
||||
`</tr>`,
|
||||
`</tbody>`,
|
||||
`</table>`,
|
||||
];
|
||||
}
|
||||
|
||||
const beforeData = JSON.parse(await readFile(beforeFile, 'utf8'));
|
||||
const afterData = JSON.parse(await readFile(afterFile, 'utf8'));
|
||||
const before = collectReport(beforeData);
|
||||
const after = collectReport(afterData);
|
||||
const lines = [
|
||||
'## Frontend Bundle Report',
|
||||
'',
|
||||
...renderSummaryTable(before, after),
|
||||
'',
|
||||
'<details>',
|
||||
'<summary>Top 10</summary>',
|
||||
'',
|
||||
];
|
||||
|
||||
for (const row of after.hotModules.slice(0, 10)) {
|
||||
lines.push(`- ${code(row.id)}: ${sharePercent(row.renderedLength, after.metrics.renderedLength)} (${formatBytes(row.renderedLength)})`);
|
||||
}
|
||||
|
||||
lines.push(
|
||||
'',
|
||||
'</details>',
|
||||
);
|
||||
|
||||
lines.push(
|
||||
'',
|
||||
'<details>',
|
||||
'<summary>Hot Modules (Self Size)</summary>',
|
||||
'',
|
||||
'| Module | Bundles | Rendered | Share | Gzip | Brotli | Imports | Imported By |',
|
||||
'|---|---:|---:|---:|---:|---:|---:|---:|',
|
||||
);
|
||||
|
||||
for (const row of after.hotModules.slice(0, 15)) {
|
||||
lines.push(`| ${tableCode(row.id)} | ${row.bundles} | ${formatBytes(row.renderedLength)} | ${sharePercent(row.renderedLength, after.metrics.renderedLength)} | ${formatBytes(row.gzipLength)} | ${formatBytes(row.brotliLength)} | ${row.importedCount} | ${row.importedByCount} |`);
|
||||
}
|
||||
|
||||
lines.push(
|
||||
'',
|
||||
'</details>',
|
||||
);
|
||||
|
||||
await writeFile(outputFile, `${lines.join('\n')}\n`);
|
||||
431
.github/scripts/frontend-js-size.mjs
vendored
431
.github/scripts/frontend-js-size.mjs
vendored
|
|
@ -1,8 +1,15 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { promises as fs } from 'node:fs';
|
||||
import path from 'node:path';
|
||||
|
||||
const marker = '<!-- misskey-frontend-js-size -->';
|
||||
const locale = process.env.FRONTEND_JS_SIZE_LOCALE || 'ja-JP';
|
||||
const byteFormatter = new Intl.NumberFormat('en-US');
|
||||
const numberFormatter = new Intl.NumberFormat('en-US');
|
||||
|
||||
function normalizePath(filePath) {
|
||||
return filePath.split(path.sep).join('/');
|
||||
|
|
@ -33,18 +40,26 @@ async function* walk(dir) {
|
|||
}
|
||||
}
|
||||
|
||||
function formatNumber(value) {
|
||||
return numberFormatter.format(value);
|
||||
}
|
||||
|
||||
function formatBytes(size) {
|
||||
if (size == null) return '-';
|
||||
if (size < 1024) return `${size} B`;
|
||||
if (size < 1024 * 1024) return `${stripTrailingZeros((size / 1024).toFixed(1))} KiB`;
|
||||
return `${stripTrailingZeros((size / 1024 / 1024).toFixed(2))} MiB`;
|
||||
if (!Number.isFinite(value) || value <= 0) return '0 B';
|
||||
|
||||
const units = ['B', 'KiB', 'MiB', 'GiB'];
|
||||
let unitIndex = 0;
|
||||
let size = value;
|
||||
while (size >= 1024 && unitIndex < units.length - 1) {
|
||||
size /= 1024;
|
||||
unitIndex += 1;
|
||||
}
|
||||
|
||||
const maximumFractionDigits = size >= 10 || unitIndex === 0 ? 0 : 1;
|
||||
return `${byteFormatter.format(Number(size.toFixed(maximumFractionDigits)))} ${units[unitIndex]}`;
|
||||
}
|
||||
|
||||
function stripTrailingZeros(value) {
|
||||
return value.replace(/\.0+$/, '').replace(/(\.\d*?)0+$/, '$1');
|
||||
}
|
||||
|
||||
function formatMathText(text) {
|
||||
function escapeLatex(text) {
|
||||
return text
|
||||
.replaceAll('\\', '\\\\')
|
||||
.replaceAll('{', '\\{')
|
||||
|
|
@ -52,28 +67,60 @@ function formatMathText(text) {
|
|||
.replaceAll('%', '\\%');
|
||||
}
|
||||
|
||||
function formatDiff(diff) {
|
||||
if (diff == null) return '-';
|
||||
if (diff === 0) return '0 B';
|
||||
const sign = diff > 0 ? '+' : '-';
|
||||
const text = `${sign}${formatBytes(Math.abs(diff))}`;
|
||||
function formatColoredDiff(text, diff) {
|
||||
if (diff === 0) return text;
|
||||
const color = diff > 0 ? 'orange' : 'green';
|
||||
return `$\\color{${color}}{\\text{${formatMathText(text).replaceAll('\\%', '\\\\%')}}}$`;
|
||||
const sign = diff > 0 ? '+' : '-';
|
||||
return `$\\color{${color}}{\\text{${sign}${escapeLatex(text).replaceAll('\\%', '\\\\%')}}}$`;
|
||||
}
|
||||
|
||||
function formatDiffPercent(beforeSize, afterSize) {
|
||||
if (beforeSize == null || beforeSize === 0 || afterSize == null || afterSize === 0) return '-';
|
||||
const diff = afterSize - beforeSize;
|
||||
function formatNumberDiff(before, after) {
|
||||
if (before == null || after == null) return '-';
|
||||
const diff = after - before;
|
||||
return formatColoredDiff(formatNumber(Math.abs(diff)), diff);
|
||||
}
|
||||
|
||||
function formatBytesDiff(diff) {
|
||||
if (diff == null) return '-';
|
||||
if (diff === 0) return '0 B';
|
||||
return formatColoredDiff(formatBytes(Math.abs(diff)), diff);
|
||||
}
|
||||
|
||||
function formatDiffPercent(before, after) {
|
||||
if (before == null || before === 0 || after == null || after === 0) return '-';
|
||||
const diff = after - before;
|
||||
if (diff === 0) return `0%`;
|
||||
const percent = Math.round(diff / beforeSize * 100);
|
||||
const color = diff > 0 ? 'orange' : 'green';
|
||||
return `$\\color{${color}}{\\text{${formatMathText(percent.toString() + '%').replaceAll('\\%', '\\\\%')}}}$`;
|
||||
const percent = Math.round(diff / before * 100);
|
||||
return formatColoredDiff(`${percent}%`, diff);
|
||||
}
|
||||
|
||||
function sharePercent(value, total) {
|
||||
if (total === 0) return '0%';
|
||||
return Math.round((value / total) * 100) + '%';
|
||||
}
|
||||
|
||||
function escapeCell(value) {
|
||||
return String(value).replaceAll('|', '\\|').replaceAll('\n', '<br>');
|
||||
}
|
||||
|
||||
function tableCell(value) {
|
||||
return String(value).replaceAll('|', '\\|').replaceAll('\r', ' ').replaceAll('\n', ' ');
|
||||
}
|
||||
|
||||
function code(value) {
|
||||
const sanitized = String(value).replaceAll('\r', ' ').replaceAll('\n', ' ');
|
||||
const backtickRuns = sanitized.match(/`+/g) ?? [];
|
||||
const fenceLength = Math.max(1, ...backtickRuns.map((run) => run.length + 1));
|
||||
const fence = '`'.repeat(fenceLength);
|
||||
const padding = sanitized.startsWith('`') || sanitized.endsWith('`') ? ' ' : '';
|
||||
|
||||
return `${fence}${padding}${sanitized}${padding}${fence}`;
|
||||
}
|
||||
|
||||
function tableCode(value) {
|
||||
return tableCell(code(value));
|
||||
}
|
||||
|
||||
function entryDisplayName(entry) {
|
||||
if (!entry) return '';
|
||||
return entry.displayName || entry.file;
|
||||
|
|
@ -174,28 +221,165 @@ async function collectReport(repoDir) {
|
|||
};
|
||||
}
|
||||
|
||||
function commonKeys(before, after) {
|
||||
function collectVisualizerReport(data) {
|
||||
const nodeParts = data.nodeParts ?? {};
|
||||
const nodeMetas = Object.values(data.nodeMetas ?? {});
|
||||
const moduleRows = [];
|
||||
const bundleMap = new Map();
|
||||
|
||||
for (const meta of nodeMetas) {
|
||||
const row = {
|
||||
id: meta.id,
|
||||
bundles: 0,
|
||||
renderedLength: 0,
|
||||
gzipLength: 0,
|
||||
brotliLength: 0,
|
||||
importedByCount: meta.importedBy?.length ?? 0,
|
||||
importedCount: meta.imported?.length ?? 0,
|
||||
};
|
||||
|
||||
for (const [bundleId, partUid] of Object.entries(meta.moduleParts ?? {})) {
|
||||
const part = nodeParts[partUid];
|
||||
if (part == null) continue;
|
||||
|
||||
row.bundles += 1;
|
||||
row.renderedLength += part.renderedLength;
|
||||
row.gzipLength += part.gzipLength;
|
||||
row.brotliLength += part.brotliLength;
|
||||
|
||||
const bundle = bundleMap.get(bundleId) ?? {
|
||||
id: bundleId,
|
||||
modules: 0,
|
||||
renderedLength: 0,
|
||||
gzipLength: 0,
|
||||
brotliLength: 0,
|
||||
};
|
||||
bundle.modules += 1;
|
||||
bundle.renderedLength += part.renderedLength;
|
||||
bundle.gzipLength += part.gzipLength;
|
||||
bundle.brotliLength += part.brotliLength;
|
||||
bundleMap.set(bundleId, bundle);
|
||||
}
|
||||
|
||||
if (row.bundles > 0) {
|
||||
moduleRows.push(row);
|
||||
}
|
||||
}
|
||||
|
||||
let staticImports = 0;
|
||||
let dynamicImports = 0;
|
||||
for (const meta of nodeMetas) {
|
||||
for (const imported of meta.imported ?? []) {
|
||||
if (imported.dynamic) {
|
||||
dynamicImports += 1;
|
||||
} else {
|
||||
staticImports += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const bundleRows = [...bundleMap.values()].sort((a, b) => b.renderedLength - a.renderedLength);
|
||||
const hotModules = [...moduleRows].sort((a, b) => b.renderedLength - a.renderedLength);
|
||||
const totalRendered = moduleRows.reduce((sum, row) => sum + row.renderedLength, 0);
|
||||
const totalGzip = moduleRows.reduce((sum, row) => sum + row.gzipLength, 0);
|
||||
const totalBrotli = moduleRows.reduce((sum, row) => sum + row.brotliLength, 0);
|
||||
|
||||
return {
|
||||
options: data.options ?? {},
|
||||
summary: {
|
||||
bundles: bundleRows.length,
|
||||
modules: moduleRows.length,
|
||||
entries: nodeMetas.filter((meta) => meta.isEntry).length,
|
||||
externals: nodeMetas.filter((meta) => meta.isExternal).length,
|
||||
staticImports,
|
||||
dynamicImports,
|
||||
},
|
||||
metrics: {
|
||||
renderedLength: totalRendered,
|
||||
gzipLength: totalGzip,
|
||||
brotliLength: totalBrotli,
|
||||
},
|
||||
hotModules,
|
||||
};
|
||||
}
|
||||
|
||||
function renderVisualizerSummaryTable(before, after) {
|
||||
const summary = [
|
||||
'bundles',
|
||||
'modules',
|
||||
'entries',
|
||||
//'externals',
|
||||
'staticImports',
|
||||
'dynamicImports',
|
||||
];
|
||||
|
||||
const metrics = [
|
||||
'renderedLength',
|
||||
'gzipLength',
|
||||
'brotliLength',
|
||||
];
|
||||
|
||||
return [
|
||||
`<table>`,
|
||||
`<thead>`,
|
||||
`<tr>`,
|
||||
`<th rowspan="2"></th>`,
|
||||
`<th rowspan="2">Bundles</th>`,
|
||||
`<th rowspan="2">Modules</th>`,
|
||||
`<th rowspan="2">Entries</th>`,
|
||||
`<th colspan="2">Imports</th>`,
|
||||
`<th colspan="3">Size</th>`,
|
||||
`</tr>`,
|
||||
`<tr>`,
|
||||
`<th>Static</th>`,
|
||||
`<th>Dynamic</th>`,
|
||||
`<th>Rendered</th>`,
|
||||
`<th>Gzip</th>`,
|
||||
`<th>Brotli</th>`,
|
||||
`</tr>`,
|
||||
`</thead>`,
|
||||
`<tbody>`,
|
||||
`<tr>`,
|
||||
`<th><b>Before</b></th>`,
|
||||
...summary.map((key) => `<td>${formatNumber(before.summary[key])}</td>`),
|
||||
...metrics.map((key) => `<td>${formatBytes(before.metrics[key])}</td>`),
|
||||
`</tr>`,
|
||||
`<tr>`,
|
||||
`<th><b>After</b></th>`,
|
||||
...summary.map((key) => `<td>${formatNumber(after.summary[key])}</td>`),
|
||||
...metrics.map((key) => `<td>${formatBytes(after.metrics[key])}</td>`),
|
||||
`</tr>`,
|
||||
`<tr><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td></tr>`,
|
||||
`<tr>`,
|
||||
`<th><b>Δ</b></th>`,
|
||||
...summary.map((key) => `<td>${formatNumberDiff(before.summary[key], after.summary[key])}</td>`),
|
||||
...metrics.map((key) => `<td>${formatBytesDiff(before.metrics[key], after.metrics[key])}</td>`),
|
||||
`</tr>`,
|
||||
`<tr>`,
|
||||
`<th><b>Δ (%)</b></th>`,
|
||||
...summary.map((key) => `<td>${formatDiffPercent(before.summary[key], after.summary[key])}</td>`),
|
||||
...metrics.map((key) => `<td>${formatDiffPercent(before.metrics[key], after.metrics[key])}</td>`),
|
||||
`</tr>`,
|
||||
`</tbody>`,
|
||||
`</table>`,
|
||||
];
|
||||
}
|
||||
|
||||
function commonChunkKeys(before, after) {
|
||||
return Object.keys(before.chunks)
|
||||
.filter((key) => after.chunks[key] != null);
|
||||
}
|
||||
|
||||
function addedKeys(before, after) {
|
||||
function addedChunkKeys(before, after) {
|
||||
return Object.keys(after.chunks)
|
||||
.filter((key) => before.chunks[key] == null);
|
||||
}
|
||||
|
||||
function removedKeys(before, after) {
|
||||
function removedChunkKeys(before, after) {
|
||||
return Object.keys(before.chunks)
|
||||
.filter((key) => after.chunks[key] == null);
|
||||
}
|
||||
|
||||
function rowChangeType(beforeEntry, afterEntry, beforeSize, afterSize) {
|
||||
if (beforeEntry == null) return 'added';
|
||||
if (afterEntry == null) return 'removed';
|
||||
if (beforeSize !== afterSize) return 'updated';
|
||||
return 'unchanged';
|
||||
}
|
||||
|
||||
function getChunkComparisonRows(keys, before, after) {
|
||||
return keys.map((key) => {
|
||||
const beforeEntry = before.chunks[key];
|
||||
|
|
@ -208,13 +392,13 @@ function getChunkComparisonRows(keys, before, after) {
|
|||
chunkFile: beforeEntry?.file ?? afterEntry?.file,
|
||||
beforeSize,
|
||||
afterSize,
|
||||
changeType: rowChangeType(beforeEntry, afterEntry, beforeSize, afterSize),
|
||||
changeType: beforeEntry == null ? 'added' : afterEntry == null ? 'removed' : beforeSize !== afterSize ? 'updated' : 'unchanged',
|
||||
sortSize: Math.max(beforeSize, afterSize),
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
function summarizeChanges(rows) {
|
||||
function summarizeChunkChanges(rows) {
|
||||
return {
|
||||
updated: rows.filter((row) => row.changeType === 'updated').length,
|
||||
added: rows.filter((row) => row.changeType === 'added').length,
|
||||
|
|
@ -222,18 +406,18 @@ function summarizeChanges(rows) {
|
|||
};
|
||||
}
|
||||
|
||||
function formatChangeSummary(label, summary) {
|
||||
function formatChunkChangeSummary(label, summary) {
|
||||
return `${label} (${summary.updated} updated, ${summary.added} added, ${summary.removed} removed)`;
|
||||
}
|
||||
|
||||
function compareComparisonRows(a, b) {
|
||||
function compareChunkComparisonRows(a, b) {
|
||||
return Math.abs(b.afterSize - b.beforeSize) - Math.abs(a.afterSize - a.beforeSize)
|
||||
|| (b.afterSize - b.beforeSize) - (a.afterSize - a.beforeSize)
|
||||
|| b.sortSize - a.sortSize
|
||||
|| a.name.localeCompare(b.name);
|
||||
}
|
||||
|
||||
function markdownTable(rows, total) {
|
||||
function chunkMarkdownTable(rows, total) {
|
||||
if (rows.length === 0) return '_No data_';
|
||||
|
||||
const lines = [
|
||||
|
|
@ -241,91 +425,138 @@ function markdownTable(rows, total) {
|
|||
'| --- | ---: | ---: | ---: | ---: |',
|
||||
];
|
||||
if (total != null) {
|
||||
lines.push(`| (total) | ${formatBytes(total.beforeSize)} | ${formatBytes(total.afterSize)} | ${formatDiff(total.afterSize - total.beforeSize)} | ${formatDiffPercent(total.beforeSize, total.afterSize)} |`);
|
||||
lines.push(`| (total) | ${formatBytes(total.beforeSize)} | ${formatBytes(total.afterSize)} | ${formatBytesDiff(total.afterSize - total.beforeSize)} | ${formatDiffPercent(total.beforeSize, total.afterSize)} |`);
|
||||
lines.push('| | | | | |');
|
||||
}
|
||||
for (const row of rows) {
|
||||
if (row.changeType === 'added') {
|
||||
lines.push(`| <details><summary>\`${escapeCell(row.name)}\`</summary> \`${escapeCell(row.chunkFile)}\` </details> | ${formatBytes(row.beforeSize)} | ${formatBytes(row.afterSize)} | ${formatDiff(row.afterSize - row.beforeSize)} | $\\color{orange}{\\text{(+)}}$ |`);
|
||||
lines.push(`| <details><summary>\`${escapeCell(row.name)}\`</summary> \`${escapeCell(row.chunkFile)}\` </details> | ${formatBytes(row.beforeSize)} | ${formatBytes(row.afterSize)} | ${formatBytesDiff(row.afterSize - row.beforeSize)} | $\\color{orange}{\\text{(+)}}$ |`);
|
||||
} else if (row.changeType === 'removed') {
|
||||
lines.push(`| <details><summary>\`${escapeCell(row.name)}\`</summary> \`${escapeCell(row.chunkFile)}\` </details> | ${formatBytes(row.beforeSize)} | ${formatBytes(row.afterSize)} | ${formatDiff(row.afterSize - row.beforeSize)} | $\\color{green}{\\text{(-)}}$ |`);
|
||||
lines.push(`| <details><summary>\`${escapeCell(row.name)}\`</summary> \`${escapeCell(row.chunkFile)}\` </details> | ${formatBytes(row.beforeSize)} | ${formatBytes(row.afterSize)} | ${formatBytesDiff(row.afterSize - row.beforeSize)} | $\\color{green}{\\text{(-)}}$ |`);
|
||||
} else {
|
||||
lines.push(`| <details><summary>\`${escapeCell(row.name)}\`</summary> \`${escapeCell(row.chunkFile)}\` </details> | ${formatBytes(row.beforeSize)} | ${formatBytes(row.afterSize)} | ${formatDiff(row.afterSize - row.beforeSize)} | ${formatDiffPercent(row.beforeSize, row.afterSize)} |`);
|
||||
lines.push(`| <details><summary>\`${escapeCell(row.name)}\`</summary> \`${escapeCell(row.chunkFile)}\` </details> | ${formatBytes(row.beforeSize)} | ${formatBytes(row.afterSize)} | ${formatBytesDiff(row.afterSize - row.beforeSize)} | ${formatDiffPercent(row.beforeSize, row.afterSize)} |`);
|
||||
}
|
||||
}
|
||||
return lines.join('\n');
|
||||
}
|
||||
|
||||
const beforeDir = process.argv[2];
|
||||
const afterDir = process.argv[3];
|
||||
const outFile = process.argv[4];
|
||||
const beforeSha = process.env.BASE_SHA;
|
||||
const afterSha = process.env.HEAD_SHA;
|
||||
function renderFrontendChunkReport(before, after) {
|
||||
const commonChunkKeys = commonChunkKeys(before, after);
|
||||
const allChunkKeys = [
|
||||
...commonChunkKeys,
|
||||
...addedChunkKeys(before, after),
|
||||
...removedChunkKeys(before, after),
|
||||
];
|
||||
//const comparisonRows = getChunkComparisonRows(commonChunkKeys, before, after);
|
||||
const allComparisonRows = getChunkComparisonRows(allChunkKeys, before, after);
|
||||
|
||||
const changedRows = allComparisonRows.filter((row) => row.changeType !== 'unchanged');
|
||||
const diffSummary = summarizeChunkChanges(changedRows);
|
||||
const diffTotal = {
|
||||
beforeSize: allComparisonRows.reduce((sum, row) => sum + row.beforeSize, 0),
|
||||
afterSize: allComparisonRows.reduce((sum, row) => sum + row.afterSize, 0),
|
||||
};
|
||||
const diffRows = changedRows.sort(compareChunkComparisonRows).slice(0, 30); // TODO: 実際に30を超えて切り捨てられたrowがあった場合はその旨をmarkdown内に表示するようにする
|
||||
|
||||
const startupKeys = new Set([
|
||||
...before.startupKeys,
|
||||
...after.startupKeys,
|
||||
]);
|
||||
const startupComparisonRows = getChunkComparisonRows([...startupKeys], before, after);
|
||||
const startupRows = startupComparisonRows.sort(compareChunkComparisonRows);
|
||||
const startupSummary = summarizeChunkChanges(startupComparisonRows);
|
||||
const startupTotal = {
|
||||
beforeSize: startupComparisonRows.reduce((sum, row) => sum + row.beforeSize, 0),
|
||||
afterSize: startupComparisonRows.reduce((sum, row) => sum + row.afterSize, 0),
|
||||
};
|
||||
|
||||
//const largeRows = comparisonRows
|
||||
// .sort((a, b) => b.sortSize - a.sortSize || a.name.localeCompare(b.name))
|
||||
// .slice(0, 30);
|
||||
|
||||
return [
|
||||
'<details open>',
|
||||
`<summary>${formatChunkChangeSummary('Diffs', diffSummary)}</summary>`,
|
||||
'',
|
||||
chunkMarkdownTable(diffRows, diffTotal),
|
||||
'',
|
||||
'</details>',
|
||||
'',
|
||||
'<details>',
|
||||
`<summary>${formatChunkChangeSummary('Startup', startupSummary)}</summary>`,
|
||||
'',
|
||||
chunkMarkdownTable(startupRows, startupTotal),
|
||||
'',
|
||||
`_Startup chunks are the Vite entry for \`src/_boot_.ts\` and its static imports._`,
|
||||
'',
|
||||
'</details>',
|
||||
'',
|
||||
//'<details>',
|
||||
//`<summary>Largest</summary>`,
|
||||
//'',
|
||||
//markdownTable(largeRows),
|
||||
//'',
|
||||
//'</details>',
|
||||
//'',
|
||||
].join('\n');
|
||||
}
|
||||
|
||||
function renderFrontendBundleReport(before, after) {
|
||||
const lines = [
|
||||
...renderVisualizerSummaryTable(before, after),
|
||||
'',
|
||||
'<details>',
|
||||
'<summary>Top 10</summary>',
|
||||
'',
|
||||
];
|
||||
|
||||
for (const row of after.hotModules.slice(0, 10)) {
|
||||
lines.push(`- ${code(row.id)}: ${sharePercent(row.renderedLength, after.metrics.renderedLength)} (${formatBytes(row.renderedLength)})`);
|
||||
}
|
||||
|
||||
lines.push(
|
||||
'',
|
||||
'</details>',
|
||||
);
|
||||
|
||||
lines.push(
|
||||
'',
|
||||
'<details>',
|
||||
'<summary>Hot Modules (Self Size)</summary>',
|
||||
'',
|
||||
'| Module | Bundles | Rendered | Share | Gzip | Brotli | Imports | Imported By |',
|
||||
'|---|---:|---:|---:|---:|---:|---:|---:|',
|
||||
);
|
||||
|
||||
for (const row of after.hotModules.slice(0, 15)) {
|
||||
lines.push(`| ${tableCode(row.id)} | ${row.bundles} | ${formatBytes(row.renderedLength)} | ${sharePercent(row.renderedLength, after.metrics.renderedLength)} | ${formatBytes(row.gzipLength)} | ${formatBytes(row.brotliLength)} | ${row.importedCount} | ${row.importedByCount} |`);
|
||||
}
|
||||
|
||||
lines.push(
|
||||
'',
|
||||
'</details>',
|
||||
);
|
||||
|
||||
return lines.join('\n');
|
||||
}
|
||||
|
||||
const args = process.argv.slice(2);
|
||||
const [beforeDir, afterDir, beforeStatsFile, afterStatsFile, outFile] = args;
|
||||
const before = await collectReport(beforeDir);
|
||||
const after = await collectReport(afterDir);
|
||||
|
||||
const commonChunkKeys = commonKeys(before, after);
|
||||
const allChunkKeys = [
|
||||
...commonChunkKeys,
|
||||
...addedKeys(before, after),
|
||||
...removedKeys(before, after),
|
||||
];
|
||||
//const comparisonRows = getChunkComparisonRows(commonChunkKeys, before, after);
|
||||
const allComparisonRows = getChunkComparisonRows(allChunkKeys, before, after);
|
||||
|
||||
const changedRows = allComparisonRows.filter((row) => row.changeType !== 'unchanged');
|
||||
const diffSummary = summarizeChanges(changedRows);
|
||||
const diffTotal = {
|
||||
beforeSize: allComparisonRows.reduce((sum, row) => sum + row.beforeSize, 0),
|
||||
afterSize: allComparisonRows.reduce((sum, row) => sum + row.afterSize, 0),
|
||||
};
|
||||
const diffRows = changedRows.sort(compareComparisonRows).slice(0, 30); // TODO: 実際に30を超えて切り捨てられたrowがあった場合はその旨をmarkdown内に表示するようにする
|
||||
|
||||
const startupKeys = new Set([
|
||||
...before.startupKeys,
|
||||
...after.startupKeys,
|
||||
]);
|
||||
const startupComparisonRows = getChunkComparisonRows([...startupKeys], before, after);
|
||||
const startupRows = startupComparisonRows
|
||||
.sort(compareComparisonRows);
|
||||
const startupSummary = summarizeChanges(startupComparisonRows);
|
||||
const startupTotal = {
|
||||
beforeSize: startupComparisonRows.reduce((sum, row) => sum + row.beforeSize, 0),
|
||||
afterSize: startupComparisonRows.reduce((sum, row) => sum + row.afterSize, 0),
|
||||
};
|
||||
|
||||
//const largeRows = comparisonRows
|
||||
// .sort((a, b) => b.sortSize - a.sortSize || a.name.localeCompare(b.name))
|
||||
// .slice(0, 30);
|
||||
const beforeStats = JSON.parse(await fs.readFile(beforeStatsFile, 'utf8'));
|
||||
const afterStats = JSON.parse(await fs.readFile(afterStatsFile, 'utf8'));
|
||||
|
||||
const body = [
|
||||
marker,
|
||||
'',
|
||||
`## Frontend Chunk Report`,
|
||||
'',
|
||||
'<details open>',
|
||||
`<summary>${formatChangeSummary('Diffs', diffSummary)}</summary>`,
|
||||
renderFrontendChunkReport(before, after),
|
||||
'',
|
||||
markdownTable(diffRows, diffTotal),
|
||||
'## Frontend Bundle Report',
|
||||
'',
|
||||
'</details>',
|
||||
'',
|
||||
'<details>',
|
||||
`<summary>${formatChangeSummary('Startup', startupSummary)}</summary>`,
|
||||
'',
|
||||
markdownTable(startupRows, startupTotal),
|
||||
'',
|
||||
`_Startup chunks are the Vite entry for \`src/_boot_.ts\` and its static imports._`,
|
||||
'',
|
||||
'</details>',
|
||||
'',
|
||||
//'<details>',
|
||||
//`<summary>Largest</summary>`,
|
||||
//'',
|
||||
//markdownTable(largeRows),
|
||||
//'',
|
||||
//'</details>',
|
||||
//'',
|
||||
renderFrontendBundleReport(collectVisualizerReport(beforeStats), collectVisualizerReport(afterStats)),
|
||||
].join('\n');
|
||||
|
||||
await fs.writeFile(outFile, body);
|
||||
|
|
|
|||
|
|
@ -26,7 +26,6 @@ on:
|
|||
- pnpm-workspace.yaml
|
||||
- .node-version
|
||||
- .github/scripts/frontend-js-size.mjs
|
||||
- .github/scripts/frontend-bundle-visualizer-report.mjs
|
||||
- .github/workflows/frontend-bundle-report.yml
|
||||
- .github/workflows/frontend-bundle-report-comment.yml
|
||||
|
||||
|
|
@ -185,7 +184,6 @@ jobs:
|
|||
const reportMarkers = [jsSizeMarker, visualizerMarker];
|
||||
const reportDir = path.join(process.env.RUNNER_TEMP, 'frontend-bundle-report');
|
||||
const jsSizeReportPath = path.join(reportDir, 'frontend-js-size-report.md');
|
||||
const visualizerReportPath = path.join(reportDir, 'frontend-bundle-visualizer-report.md');
|
||||
const prNumberPath = path.join(reportDir, 'pr-number.txt');
|
||||
const headShaPath = path.join(reportDir, 'head-sha.txt');
|
||||
const workflowRun = context.payload.workflow_run;
|
||||
|
|
@ -197,10 +195,6 @@ jobs:
|
|||
core.setFailed('The frontend bundle report artifact does not contain frontend-js-size-report.md.');
|
||||
return;
|
||||
}
|
||||
if (!fs.existsSync(visualizerReportPath)) {
|
||||
core.setFailed('The frontend bundle report artifact does not contain frontend-bundle-visualizer-report.md.');
|
||||
return;
|
||||
}
|
||||
|
||||
const artifactHeadSha = fs.existsSync(headShaPath)
|
||||
? fs.readFileSync(headShaPath, 'utf8').trim()
|
||||
|
|
@ -274,11 +268,7 @@ jobs:
|
|||
core.setFailed('The frontend JS size report is missing the expected marker.');
|
||||
return;
|
||||
}
|
||||
const visualizerReport = fs.readFileSync(visualizerReportPath, 'utf8').trim();
|
||||
let body = [
|
||||
jsSizeReport,
|
||||
visualizerReport,
|
||||
].join('\n\n') + '\n';
|
||||
let body = `${jsSizeReport}\n`;
|
||||
|
||||
const maxCommentLength = 65_000;
|
||||
if (body.length > maxCommentLength) {
|
||||
|
|
|
|||
7
.github/workflows/frontend-bundle-report.yml
vendored
7
.github/workflows/frontend-bundle-report.yml
vendored
|
|
@ -21,7 +21,6 @@ on:
|
|||
- pnpm-workspace.yaml
|
||||
- .node-version
|
||||
- .github/scripts/frontend-js-size.mjs
|
||||
- .github/scripts/frontend-bundle-visualizer-report.mjs
|
||||
- .github/workflows/frontend-bundle-report.yml
|
||||
- .github/workflows/frontend-bundle-report-comment.yml
|
||||
|
||||
|
|
@ -134,8 +133,7 @@ jobs:
|
|||
PR_NUMBER: ${{ github.event.pull_request.number }}
|
||||
run: |
|
||||
REPORT_DIR="$RUNNER_TEMP/frontend-bundle-report"
|
||||
node after/.github/scripts/frontend-js-size.mjs before after "$REPORT_DIR/frontend-js-size-report.md"
|
||||
node after/.github/scripts/frontend-bundle-visualizer-report.mjs "$REPORT_DIR/before-stats.json" "$REPORT_DIR/after-stats.json" "$REPORT_DIR/frontend-bundle-visualizer-report.md"
|
||||
node after/.github/scripts/frontend-js-size.mjs before after "$REPORT_DIR/before-stats.json" "$REPORT_DIR/after-stats.json" "$REPORT_DIR/frontend-js-size-report.md"
|
||||
printf '%s\n' "$PR_NUMBER" > "$REPORT_DIR/pr-number.txt"
|
||||
printf '%s\n' "$BASE_SHA" > "$REPORT_DIR/base-sha.txt"
|
||||
printf '%s\n' "$HEAD_SHA" > "$REPORT_DIR/head-sha.txt"
|
||||
|
|
@ -148,10 +146,7 @@ jobs:
|
|||
test -s "$REPORT_DIR/before-stats.json"
|
||||
test -s "$REPORT_DIR/after-stats.json"
|
||||
test -s "$REPORT_DIR/frontend-js-size-report.md"
|
||||
test -s "$REPORT_DIR/frontend-bundle-visualizer-report.md"
|
||||
cat "$REPORT_DIR/frontend-js-size-report.md" >> "$GITHUB_STEP_SUMMARY"
|
||||
printf '\n\n' >> "$GITHUB_STEP_SUMMARY"
|
||||
cat "$REPORT_DIR/frontend-bundle-visualizer-report.md" >> "$GITHUB_STEP_SUMMARY"
|
||||
|
||||
- name: Upload bundle report
|
||||
if: steps.check-base-visualizer.outputs.supported == 'true'
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue