Merge branch 'develop' into fix-17588

This commit is contained in:
syuilo 2026-06-20 21:08:42 +09:00 committed by GitHub
commit fc4ef8ebc6
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -92,7 +92,6 @@ jobs:
const marker = '<!-- misskey-frontend-js-size -->';
const locale = process.env.FRONTEND_JS_SIZE_LOCALE || 'ja-JP';
const topLimit = 10;
function normalizePath(filePath) {
return filePath.split(path.sep).join('/');
@ -126,20 +125,38 @@ jobs:
function formatBytes(size) {
if (size == null) return '-';
if (size < 1024) return `${size} B`;
if (size < 1024 * 1024) return `${(size / 1024).toFixed(1)} KiB`;
return `${(size / 1024 / 1024).toFixed(2)} MiB`;
if (size < 1024 * 1024) return `${stripTrailingZeros((size / 1024).toFixed(1))} KiB`;
return `${stripTrailingZeros((size / 1024 / 1024).toFixed(2))} MiB`;
}
function stripTrailingZeros(value) {
return value.replace(/\.0+$/, '').replace(/(\.\d*?)0+$/, '$1');
}
function formatMathText(text) {
return text
.replaceAll('\\', '\\\\')
.replaceAll('{', '\\{')
.replaceAll('}', '\\}')
.replaceAll('%', '\\\\%');
}
function formatDiff(diff) {
if (diff == null) return '-';
if (diff === 0) return '0 B';
const sign = diff > 0 ? '+' : '-';
return `${sign}${formatBytes(Math.abs(diff))}`;
const text = `${sign}${formatBytes(Math.abs(diff))}`;
const color = diff > 0 ? 'orange' : 'green';
return `$\\color{${color}}{\\text{${formatMathText(text)}}}$`;
}
function sizeDiff(beforeSize, afterSize) {
if (beforeSize == null && afterSize == null) return null;
return (afterSize ?? 0) - (beforeSize ?? 0);
function formatDiffPercent(beforeSize, afterSize) {
if (beforeSize == null || beforeSize === 0 || afterSize == null || afterSize === 0) return '-';
const diff = afterSize - beforeSize;
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() + '%')}}}$`;
}
function escapeCell(value) {
@ -148,9 +165,7 @@ jobs:
function entryDisplayName(entry) {
if (!entry) return '';
return entry.displayName === entry.file
? entry.displayName
: `${entry.displayName} (${entry.file})`;
return entry.displayName || entry.file;
}
function findEntryKey(manifest) {
@ -185,7 +200,6 @@ jobs:
}
async function resolveBuiltFile(outDir, file) {
const originalPath = path.join(outDir, file);
if (file.startsWith('scripts/')) {
const localizedFile = file.slice('scripts/'.length);
const localizedPath = path.join(outDir, locale, localizedFile);
@ -195,9 +209,11 @@ jobs:
relativePath: `${locale}/${localizedFile}`,
};
}
throw new Error(`Expected ${locale} localized chunk for ${file}, but ${localizedPath} was not found.`);
}
return {
absolutePath: originalPath,
absolutePath: path.join(outDir, file),
relativePath: file,
};
}
@ -224,18 +240,20 @@ jobs:
byFile.add(builtFile.relativePath);
}
for await (const fullPath of walk(outDir)) {
if (!fullPath.endsWith('.js')) continue;
const relativePath = normalizePath(path.relative(outDir, fullPath));
if (byFile.has(relativePath)) continue;
if (relativePath.startsWith('scripts/') || relativePath.startsWith(`${locale}/`)) continue;
const size = await fileSize(fullPath);
byKey.set(relativePath, {
key: relativePath,
displayName: relativePath,
file: relativePath,
size,
});
const localeDir = path.join(outDir, locale);
if (await exists(localeDir)) {
for await (const fullPath of walk(localeDir)) {
if (!fullPath.endsWith('.js')) continue;
const relativePath = normalizePath(path.relative(outDir, fullPath));
if (byFile.has(relativePath)) continue;
const size = await fileSize(fullPath);
byKey.set(relativePath, {
key: relativePath,
displayName: relativePath,
file: relativePath,
size,
});
}
}
return {
@ -245,66 +263,78 @@ jobs:
};
}
function compareRows(keys, before, after) {
function commonKeys(before, after) {
return Object.keys(before.chunks)
.filter((key) => after.chunks[key] != null);
}
function addedKeys(before, after) {
return Object.keys(after.chunks)
.filter((key) => before.chunks[key] == null);
}
function removedKeys(before, after) {
return Object.keys(before.chunks)
.filter((key) => after.chunks[key] == null);
}
function getChunkComparisonRows(keys, before, after) {
return keys.map((key) => {
const beforeEntry = before.chunks[key];
const afterEntry = after.chunks[key];
const beforeSize = beforeEntry?.size ?? null;
const afterSize = afterEntry?.size ?? null;
const beforeSize = beforeEntry?.size ?? 0;
const afterSize = afterEntry?.size ?? 0;
return {
key,
file: entryDisplayName(afterEntry ?? beforeEntry),
name: entryDisplayName(beforeEntry ?? afterEntry),
chunkFile: beforeEntry?.file ?? afterEntry?.file,
beforeSize,
afterSize,
diff: sizeDiff(beforeSize, afterSize),
sortSize: Math.max(beforeSize ?? 0, afterSize ?? 0),
sortSize: Math.max(beforeSize, afterSize),
};
});
}
function markdownTable(rows, emptyMessage = '_No JavaScript chunks found._') {
if (rows.length === 0) {
return emptyMessage;
}
function markdownTable(rows, total) {
if (rows.length === 0) return '_No data_';
const lines = [
'| File | Before | After | Diff |',
'| --- | ---: | ---: | ---: |',
'| Chunk | Before | After | Diff | Diff (%) |',
'| --- | ---: | ---: | ---: | ---: |',
];
if (total != null) {
lines.push(`| (total) | ${formatBytes(total.beforeSize)} | ${formatBytes(total.afterSize)} | ${formatDiff(total.afterSize - total.beforeSize)} | ${formatDiffPercent(total.beforeSize, total.afterSize)} |`);
lines.push('| | | | | |');
}
for (const row of rows) {
lines.push(`| ${escapeCell(row.file)} | ${formatBytes(row.beforeSize)} | ${formatBytes(row.afterSize)} | ${formatDiff(row.diff)} |`);
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)} |`);
}
return lines.join('\n');
}
function unionTopKeys(before, after) {
const allKeys = new Set([
...Object.keys(before.chunks),
...Object.keys(after.chunks),
]);
return compareRows([...allKeys], before, after)
.sort((a, b) => b.sortSize - a.sortSize || a.file.localeCompare(b.file))
.slice(0, topLimit)
.map((row) => row.key);
function chunkRows(keys, report) {
return keys.map((key) => {
const entry = report.chunks[key];
return {
key,
name: entryDisplayName(entry),
chunkFile: entry.file,
size: entry.size,
};
});
}
function compareDiffRows(a, b) {
return Math.abs(b.diff ?? 0) - Math.abs(a.diff ?? 0)
|| (b.diff ?? 0) - (a.diff ?? 0)
|| b.sortSize - a.sortSize
|| a.file.localeCompare(b.file);
}
function markdownChunkTable(rows) {
if (rows.length === 0) return '_No data_';
function topDiffKeys(before, after) {
const allKeys = new Set([
...Object.keys(before.chunks),
...Object.keys(after.chunks),
]);
return compareRows([...allKeys], before, after)
.filter((row) => row.diff !== 0 && row.diff != null)
.sort(compareDiffRows)
.slice(0, topLimit)
.map((row) => row.key);
const lines = [
'| Chunk | Size |',
'| --- | ---: |',
];
for (const row of rows) {
lines.push(`| <details><summary>\`${escapeCell(row.name)}\`</summary> \`${escapeCell(row.chunkFile)}\` </details> | ${formatBytes(row.size)} |`);
}
return lines.join('\n');
}
const beforeDir = process.argv[2];
@ -316,37 +346,86 @@ jobs:
const before = await collectReport(beforeDir);
const after = await collectReport(afterDir);
const topRows = compareRows(unionTopKeys(before, after), before, after)
.sort((a, b) => b.sortSize - a.sortSize || a.file.localeCompare(b.file));
const commonChunkKeys = commonKeys(before, after);
const comparisonRows = getChunkComparisonRows(commonChunkKeys, before, after);
const diffRows = compareRows(topDiffKeys(before, after), before, after)
.sort(compareDiffRows);
const diffRows = comparisonRows
.filter((row) => row.beforeSize !== row.afterSize)
.sort((a, b) => 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))
.slice(0, 30);
const diffTotal = {
beforeSize: comparisonRows.reduce((sum, row) => sum + row.beforeSize, 0),
afterSize: comparisonRows.reduce((sum, row) => sum + row.afterSize, 0),
};
const addedRows = chunkRows(addedKeys(before, after), after)
.sort((a, b) => b.size - a.size || a.name.localeCompare(b.name));
const removedRows = chunkRows(removedKeys(before, after), before)
.sort((a, b) => b.size - a.size || a.name.localeCompare(b.name));
const startupKeys = new Set([
...before.startupKeys,
...after.startupKeys,
]);
const startupRows = compareRows([...startupKeys], before, after)
.sort((a, b) => b.sortSize - a.sortSize || a.file.localeCompare(b.file));
const startupComparisonRows = getChunkComparisonRows([...startupKeys], before, after);
const startupRows = startupComparisonRows
.sort((a, b) => 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));
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 body = [
marker,
'## Frontend size report',
`## Frontend chunk size report (${locale})`,
'',
'### Top 10 largest chunk diffs',
'<details open>',
`<summary>Diffs</summary>`,
'',
markdownTable(diffRows, '_No JavaScript chunk size changes found._'),
markdownTable(diffRows, diffTotal),
'',
'### Top 10 largest chunks',
'<details>',
markdownTable(topRows),
'</details>',
'',
'### Startup chunks',
'<details>',
markdownTable(startupRows),
`<summary>Added (${addedRows.length})</summary>`,
'',
markdownChunkTable(addedRows),
'',
'</details>',
'',
'<details>',
`<summary>Removed (${removedRows.length})</summary>`,
'',
markdownChunkTable(removedRows),
'',
'</details>',
'',
'<details>',
`<summary>Startup</summary>`,
'',
markdownTable(startupRows, startupTotal),
'',
`_Only ${locale} localized chunks are reported. Size comparison tables include chunks that exist in both builds. Added and removed chunks are listed separately. Top 10 is sorted by max(before, after) size. Diff top 10 is sorted by absolute size diff. Startup chunks are the Vite entry for \`src/_boot_.ts\` and its static imports._`,
'',
'</details>',
'',
'<details>',
`<summary>Largest</summary>`,
'',
markdownTable(largeRows),
'',
'_Top 10 is sorted by max(before, after) size. Diff top 10 is sorted by absolute size diff, with missing chunks compared against 0 B. Startup chunks are the Vite entry for `src/_boot_.ts` and its static imports._',
'</details>',
'',
].join('\n');