mirror of
https://github.com/actualbudget/actual.git
synced 2026-03-11 12:43:09 -05:00
733 lines
18 KiB
JavaScript
733 lines
18 KiB
JavaScript
#!/usr/bin/env node
|
|
|
|
/**
|
|
* Generates a combined bundle stats comment for GitHub Actions.
|
|
* Heavily inspired by https://github.com/twk3/rollup-size-compare-action (MIT).
|
|
*/
|
|
|
|
import { readFile } from 'node:fs/promises';
|
|
import path from 'node:path';
|
|
import process from 'node:process';
|
|
|
|
const REQUIRED_ARGS = new Map([
|
|
['base', 'Mapping of bundle names to base stats JSON'],
|
|
['head', 'Mapping of bundle names to head stats JSON'],
|
|
]);
|
|
|
|
function parseRawArgs(argv) {
|
|
const args = new Map();
|
|
|
|
for (let index = 2; index < argv.length; index += 1) {
|
|
const key = argv[index];
|
|
|
|
if (!key?.startsWith('--')) {
|
|
throw new Error(
|
|
`Unexpected argument "${key ?? ''}". Use --key value pairs.`,
|
|
);
|
|
}
|
|
|
|
const values = [];
|
|
|
|
while (index + 1 < argv.length && !argv[index + 1].startsWith('--')) {
|
|
values.push(argv[index + 1]);
|
|
index += 1;
|
|
}
|
|
|
|
if (values.length === 0) {
|
|
throw new Error(`Missing value for argument "${key}".`);
|
|
}
|
|
|
|
const keyName = key.slice(2);
|
|
// Accumulate values if the key already exists
|
|
if (args.has(keyName)) {
|
|
args.set(keyName, [...args.get(keyName), ...values]);
|
|
} else {
|
|
args.set(keyName, values);
|
|
}
|
|
}
|
|
|
|
return args;
|
|
}
|
|
|
|
function getSingleValue(args, key) {
|
|
const values = args.get(key);
|
|
if (!values) {
|
|
return undefined;
|
|
}
|
|
if (values.length !== 1) {
|
|
throw new Error(`Argument "--${key}" must have exactly one value.`);
|
|
}
|
|
return values[0];
|
|
}
|
|
|
|
function parseMapping(values, key, description) {
|
|
if (!values || values.length === 0) {
|
|
throw new Error(`Missing required argument "--${key}" (${description}).`);
|
|
}
|
|
|
|
if (values.length === 1) {
|
|
const [rawValue] = values;
|
|
const trimmed = rawValue.trim();
|
|
|
|
if (trimmed.startsWith('{')) {
|
|
try {
|
|
const parsed = JSON.parse(trimmed);
|
|
|
|
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
throw new Error('Value must be a JSON object.');
|
|
}
|
|
|
|
return new Map(
|
|
Object.entries(parsed).map(([name, pathValue]) => {
|
|
if (typeof pathValue !== 'string') {
|
|
throw new Error(
|
|
`Value for "${name}" in "--${key}" must be a string path.`,
|
|
);
|
|
}
|
|
return [name, pathValue];
|
|
}),
|
|
);
|
|
} catch (error) {
|
|
const message =
|
|
error instanceof Error ? error.message : 'Unknown parsing error';
|
|
throw new Error(
|
|
`Failed to parse "--${key}" value as JSON object: ${message}`,
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
const entries = new Map();
|
|
|
|
for (const value of values) {
|
|
const [rawName, ...rawPathParts] = value.split('=');
|
|
|
|
if (!rawName || rawPathParts.length === 0) {
|
|
throw new Error(
|
|
`Argument "--${key}" must be provided as name=path pairs or a JSON object.`,
|
|
);
|
|
}
|
|
|
|
const name = rawName.trim();
|
|
const pathValue = rawPathParts.join('=').trim();
|
|
|
|
if (!name) {
|
|
throw new Error(`Argument "--${key}" contains an empty bundle name.`);
|
|
}
|
|
|
|
if (!pathValue) {
|
|
throw new Error(
|
|
`Argument "--${key}" for bundle "${name}" must include a non-empty path.`,
|
|
);
|
|
}
|
|
|
|
entries.set(name, pathValue);
|
|
}
|
|
|
|
if (entries.size === 0) {
|
|
throw new Error(`Argument "--${key}" must define at least one bundle.`);
|
|
}
|
|
|
|
return entries;
|
|
}
|
|
|
|
function parseArgs(argv) {
|
|
const args = parseRawArgs(argv);
|
|
|
|
const baseMap = parseMapping(
|
|
args.get('base'),
|
|
'base',
|
|
REQUIRED_ARGS.get('base'),
|
|
);
|
|
const headMap = parseMapping(
|
|
args.get('head'),
|
|
'head',
|
|
REQUIRED_ARGS.get('head'),
|
|
);
|
|
|
|
const sections = [];
|
|
|
|
for (const [name, basePath] of baseMap.entries()) {
|
|
const headPath = headMap.get(name);
|
|
|
|
if (!headPath) {
|
|
throw new Error(
|
|
`Bundle "${name}" is missing a corresponding "--head" entry.`,
|
|
);
|
|
}
|
|
|
|
sections.push({
|
|
name,
|
|
basePath,
|
|
headPath,
|
|
});
|
|
}
|
|
|
|
for (const name of headMap.keys()) {
|
|
if (!baseMap.has(name)) {
|
|
throw new Error(
|
|
`Bundle "${name}" is missing a corresponding "--base" entry.`,
|
|
);
|
|
}
|
|
}
|
|
|
|
return {
|
|
sections,
|
|
identifier: getSingleValue(args, 'identifier') ?? 'bundle-stats',
|
|
format: getSingleValue(args, 'format') ?? 'pr-body',
|
|
};
|
|
}
|
|
|
|
async function loadStats(filePath) {
|
|
try {
|
|
const absolutePath = path.resolve(process.cwd(), filePath);
|
|
const fileContents = await readFile(absolutePath, 'utf8');
|
|
const parsed = JSON.parse(fileContents);
|
|
|
|
// Validate that we got a meaningful stats object
|
|
if (!parsed || typeof parsed !== 'object') {
|
|
throw new Error('Stats file does not contain a valid JSON object');
|
|
}
|
|
|
|
return parsed;
|
|
} catch (error) {
|
|
const message =
|
|
error instanceof Error
|
|
? error.message
|
|
: 'Unknown error while parsing stats file';
|
|
console.error(`[bundle-stats] Failed to parse "${filePath}": ${message}`);
|
|
throw new Error(`Failed to load stats file "${filePath}": ${message}`);
|
|
}
|
|
}
|
|
|
|
function findAllChildren(node = {}) {
|
|
if (Array.isArray(node.children)) {
|
|
return node.children.flatMap(findAllChildren);
|
|
}
|
|
return [node];
|
|
}
|
|
|
|
function trimPath(input) {
|
|
if (!input) {
|
|
return '';
|
|
}
|
|
return input.replace(/.*node_modules/, '/node_modules');
|
|
}
|
|
|
|
function assetNameToSizeMap(statAssets = {}) {
|
|
const children = statAssets?.tree?.children;
|
|
|
|
if (!Array.isArray(children) || children.length === 0) {
|
|
return new Map();
|
|
}
|
|
|
|
return new Map(
|
|
children.map(asset => {
|
|
const descendants = findAllChildren(asset);
|
|
let size = 0;
|
|
let gzipSize = statAssets?.options?.gzip ? 0 : null;
|
|
|
|
for (const mod of descendants) {
|
|
const nodePart = statAssets?.nodeParts?.[mod.uid];
|
|
|
|
if (!nodePart) {
|
|
continue;
|
|
}
|
|
|
|
size += nodePart.renderedLength ?? 0;
|
|
|
|
if (gzipSize !== null) {
|
|
gzipSize += nodePart.gzipLength ?? 0;
|
|
}
|
|
}
|
|
|
|
return [trimPath(asset.name), { size, gzipSize }];
|
|
}),
|
|
);
|
|
}
|
|
|
|
function chunkModuleNameToSizeMap(statChunks = {}) {
|
|
if (!statChunks?.tree) {
|
|
return new Map();
|
|
}
|
|
|
|
return new Map(
|
|
findAllChildren(statChunks.tree).map(mod => {
|
|
const modInfo = statChunks?.nodeParts?.[mod.uid] ?? {};
|
|
const meta = statChunks?.nodeMetas?.[modInfo.metaUid] ?? {};
|
|
const id = trimPath(meta.id ?? '');
|
|
|
|
return [
|
|
id,
|
|
{
|
|
size: modInfo.renderedLength ?? 0,
|
|
gzipSize: statChunks?.options?.gzip
|
|
? (modInfo.gzipLength ?? 0)
|
|
: null,
|
|
},
|
|
];
|
|
}),
|
|
);
|
|
}
|
|
|
|
function sortDiffDescending(items) {
|
|
return items.sort((a, b) => Math.abs(b.diff) - Math.abs(a.diff));
|
|
}
|
|
|
|
function normaliseGzip(value) {
|
|
if (value == null || Number.isNaN(value)) {
|
|
return NaN;
|
|
}
|
|
return value;
|
|
}
|
|
|
|
function getAssetDiff(name, oldSize, newSize) {
|
|
const diff = newSize.size - oldSize.size;
|
|
|
|
const percent =
|
|
oldSize.size === 0
|
|
? newSize.size === 0
|
|
? 0
|
|
: Infinity
|
|
: +((1 - newSize.size / oldSize.size) * -100).toFixed(5) || 0;
|
|
|
|
return {
|
|
name,
|
|
new: {
|
|
size: newSize.size,
|
|
gzipSize: normaliseGzip(newSize.gzipSize),
|
|
},
|
|
old: {
|
|
size: oldSize.size,
|
|
gzipSize: normaliseGzip(oldSize.gzipSize),
|
|
},
|
|
diff,
|
|
diffPercentage: percent,
|
|
};
|
|
}
|
|
|
|
function webpackStatsDiff(oldAssets, newAssets) {
|
|
const added = [];
|
|
const removed = [];
|
|
const bigger = [];
|
|
const smaller = [];
|
|
const unchanged = [];
|
|
|
|
let newSizeTotal = 0;
|
|
let oldSizeTotal = 0;
|
|
let newGzipSizeTotal = 0;
|
|
let oldGzipSizeTotal = 0;
|
|
|
|
for (const [name, oldAssetSizes] of oldAssets) {
|
|
oldSizeTotal += oldAssetSizes.size;
|
|
oldGzipSizeTotal += oldAssetSizes.gzipSize ?? NaN;
|
|
|
|
const newAsset = newAssets.get(name);
|
|
|
|
if (!newAsset) {
|
|
removed.push(getAssetDiff(name, oldAssetSizes, { size: 0, gzipSize: 0 }));
|
|
continue;
|
|
}
|
|
|
|
const diff = getAssetDiff(name, oldAssetSizes, newAsset);
|
|
|
|
if (diff.diffPercentage > 0) {
|
|
bigger.push(diff);
|
|
} else if (diff.diffPercentage < 0) {
|
|
smaller.push(diff);
|
|
} else {
|
|
unchanged.push(diff);
|
|
}
|
|
}
|
|
|
|
for (const [name, newAssetSizes] of newAssets) {
|
|
newSizeTotal += newAssetSizes.size;
|
|
newGzipSizeTotal += newAssetSizes.gzipSize ?? NaN;
|
|
|
|
if (!oldAssets.has(name)) {
|
|
added.push(getAssetDiff(name, { size: 0, gzipSize: 0 }, newAssetSizes));
|
|
}
|
|
}
|
|
|
|
const oldFilesCount = oldAssets.size;
|
|
const newFilesCount = newAssets.size;
|
|
|
|
return {
|
|
added: sortDiffDescending(added),
|
|
removed: sortDiffDescending(removed),
|
|
bigger: sortDiffDescending(bigger),
|
|
smaller: sortDiffDescending(smaller),
|
|
unchanged,
|
|
total: getAssetDiff(
|
|
oldFilesCount === newFilesCount
|
|
? `${newFilesCount}`
|
|
: `${oldFilesCount} → ${newFilesCount}`,
|
|
{ size: oldSizeTotal, gzipSize: oldGzipSizeTotal },
|
|
{ size: newSizeTotal, gzipSize: newGzipSizeTotal },
|
|
),
|
|
};
|
|
}
|
|
|
|
function getStatsDiff(oldStats, newStats) {
|
|
return webpackStatsDiff(
|
|
assetNameToSizeMap(oldStats),
|
|
assetNameToSizeMap(newStats),
|
|
);
|
|
}
|
|
|
|
function getChunkModuleDiff(oldStats, newStats) {
|
|
const diff = webpackStatsDiff(
|
|
chunkModuleNameToSizeMap(oldStats),
|
|
chunkModuleNameToSizeMap(newStats),
|
|
);
|
|
|
|
if (
|
|
diff.added.length === 0 &&
|
|
diff.removed.length === 0 &&
|
|
diff.bigger.length === 0 &&
|
|
diff.smaller.length === 0
|
|
) {
|
|
return null;
|
|
}
|
|
|
|
return diff;
|
|
}
|
|
|
|
const BYTES_PER_KILOBYTE = 1024;
|
|
const FILE_SIZE_DENOMINATIONS = [
|
|
'B',
|
|
'kB',
|
|
'MB',
|
|
'GB',
|
|
'TB',
|
|
'PB',
|
|
'EB',
|
|
'ZB',
|
|
'YB',
|
|
'BB',
|
|
];
|
|
|
|
function formatFileSizeIEC(bytes, precision = 2) {
|
|
if (bytes == null || Number.isNaN(bytes)) {
|
|
return 'N/A';
|
|
}
|
|
|
|
if (bytes === 0) {
|
|
return `0 ${FILE_SIZE_DENOMINATIONS[0]}`;
|
|
}
|
|
|
|
const absBytes = Math.abs(bytes);
|
|
const denominationIndex = Math.floor(
|
|
Math.log(absBytes) / Math.log(BYTES_PER_KILOBYTE),
|
|
);
|
|
const value = absBytes / Math.pow(BYTES_PER_KILOBYTE, denominationIndex);
|
|
const stripped = parseFloat(value.toFixed(precision));
|
|
|
|
return `${stripped} ${FILE_SIZE_DENOMINATIONS[denominationIndex]}`;
|
|
}
|
|
|
|
function conditionalPercentage(number) {
|
|
if (number === Infinity || number === -Infinity) {
|
|
return '-';
|
|
}
|
|
|
|
const absValue = Math.abs(number);
|
|
|
|
if (absValue === 0 || absValue === 100) {
|
|
return `${number}%`;
|
|
}
|
|
|
|
const value = Number.isFinite(absValue) ? absValue.toFixed(2) : absValue;
|
|
return `${signFor(number)}${value}%`;
|
|
}
|
|
|
|
function capitalize(text) {
|
|
if (!text) return '';
|
|
return `${text[0].toUpperCase()}${text.slice(1)}`;
|
|
}
|
|
|
|
function makeHeader(columns) {
|
|
const header = columns.join(' | ');
|
|
const separator = columns
|
|
.map(column =>
|
|
Array.from({ length: column.length })
|
|
.map(() => '-')
|
|
.join(''),
|
|
)
|
|
.join(' | ');
|
|
|
|
return `${header}\n${separator}`;
|
|
}
|
|
|
|
const TOTAL_HEADERS = makeHeader([
|
|
'Files count',
|
|
'Total bundle size',
|
|
'% Changed',
|
|
]);
|
|
const SUMMARY_HEADERS = makeHeader([
|
|
'Bundle',
|
|
'Files count',
|
|
'Total bundle size',
|
|
'% Changed',
|
|
]);
|
|
const TABLE_HEADERS = makeHeader(['Asset', 'File Size', '% Changed']);
|
|
const CHUNK_TABLE_HEADERS = makeHeader(['File', 'Δ', 'Size']);
|
|
|
|
function signFor(num) {
|
|
if (num === 0) return '';
|
|
return num > 0 ? '+' : '-';
|
|
}
|
|
|
|
function toFileSizeDiff(oldSize, newSize, diff) {
|
|
const diffLine = [
|
|
`${formatFileSizeIEC(oldSize)} → ${formatFileSizeIEC(newSize)}`,
|
|
];
|
|
|
|
if (typeof diff !== 'undefined') {
|
|
diffLine.push(`(${signFor(diff)}${formatFileSizeIEC(diff)})`);
|
|
}
|
|
|
|
return diffLine.join(' ');
|
|
}
|
|
|
|
function toFileSizeDiffCell(asset) {
|
|
const lines = [];
|
|
|
|
if (asset.diff === 0) {
|
|
lines.push(formatFileSizeIEC(asset.new.size));
|
|
|
|
if (asset.new.gzipSize) {
|
|
lines.push(formatFileSizeIEC(asset.new.gzipSize));
|
|
}
|
|
} else {
|
|
lines.push(toFileSizeDiff(asset.old.size, asset.new.size, asset.diff));
|
|
|
|
if (asset.old.gzipSize || asset.new.gzipSize) {
|
|
lines.push(
|
|
`${toFileSizeDiff(asset.old.gzipSize, asset.new.gzipSize)} (gzip)`,
|
|
);
|
|
}
|
|
}
|
|
|
|
return lines.join('<br />');
|
|
}
|
|
|
|
function printAssetTableRow(asset) {
|
|
return [
|
|
asset.name,
|
|
toFileSizeDiffCell(asset),
|
|
conditionalPercentage(asset.diffPercentage),
|
|
].join(' | ');
|
|
}
|
|
|
|
function printAssetTablesByGroup(statsDiff) {
|
|
const statsFields = ['added', 'removed', 'bigger', 'smaller', 'unchanged'];
|
|
|
|
return statsFields
|
|
.map(field => {
|
|
const assets = statsDiff[field] ?? [];
|
|
|
|
if (assets.length === 0) {
|
|
return `**${capitalize(field)}**\nNo assets were ${field}`;
|
|
}
|
|
|
|
return `**${capitalize(field)}**\n${TABLE_HEADERS}\n${assets
|
|
.map(asset => printAssetTableRow(asset))
|
|
.join('\n')}`;
|
|
})
|
|
.join('\n\n');
|
|
}
|
|
|
|
function getDiffEmoji(diff) {
|
|
if (diff.diffPercentage === Infinity) return '🆕';
|
|
if (diff.diffPercentage <= -100) return '🔥';
|
|
if (diff.diffPercentage > 0) return '📈';
|
|
if (diff.diffPercentage < 0) return '📉';
|
|
return ' ';
|
|
}
|
|
|
|
function getTrimmedChunkName(chunkModule) {
|
|
const chunkName = chunkModule.name ?? '';
|
|
if (chunkName.startsWith('./')) {
|
|
return chunkName.substring(2);
|
|
}
|
|
if (chunkName.startsWith('/')) {
|
|
return chunkName.substring(1);
|
|
}
|
|
return chunkName;
|
|
}
|
|
|
|
function printChunkModuleRow(chunkModule) {
|
|
const emoji = getDiffEmoji(chunkModule);
|
|
const chunkName = getTrimmedChunkName(chunkModule);
|
|
const diffPart = `${chunkModule.diff >= 0 ? '+' : '-'}${formatFileSizeIEC(chunkModule.diff)}`;
|
|
const percentPart = Number.isFinite(chunkModule.diffPercentage)
|
|
? ` (${conditionalPercentage(chunkModule.diffPercentage)})`
|
|
: '';
|
|
|
|
return [
|
|
`\`${chunkName}\``,
|
|
`${emoji} ${diffPart}${percentPart}`,
|
|
`${formatFileSizeIEC(chunkModule.old.size)} → ${formatFileSizeIEC(chunkModule.new.size)}`,
|
|
].join(' | ');
|
|
}
|
|
|
|
function printChunkModulesTable(statsDiff) {
|
|
if (!statsDiff) {
|
|
return '';
|
|
}
|
|
|
|
const changedModules = [
|
|
...(statsDiff.added ?? []),
|
|
...(statsDiff.removed ?? []),
|
|
...(statsDiff.bigger ?? []),
|
|
...(statsDiff.smaller ?? []),
|
|
].sort((a, b) => b.diffPercentage - a.diffPercentage);
|
|
|
|
if (changedModules.length === 0) {
|
|
return `<details>\n<summary>Changeset</summary>\nNo files were changed\n</details>`;
|
|
}
|
|
|
|
const rows = changedModules
|
|
.slice(0, 100)
|
|
.map(chunkModule => printChunkModuleRow(chunkModule))
|
|
.join('\n');
|
|
|
|
const summarySuffix =
|
|
changedModules.length > 100 ? ' (largest 100 files by percent change)' : '';
|
|
|
|
return `<details>\n<summary>Changeset${summarySuffix}</summary>\n\n${CHUNK_TABLE_HEADERS}\n${rows}\n</details>`;
|
|
}
|
|
|
|
function printTotalAssetTable(statsDiff) {
|
|
return `**Total**\n${TOTAL_HEADERS}\n${printAssetTableRow(statsDiff.total)}`;
|
|
}
|
|
|
|
function printSummaryTable(sections) {
|
|
if (sections.length === 0) {
|
|
return `${SUMMARY_HEADERS}\nNo bundle stats were generated.`;
|
|
}
|
|
|
|
const rows = sections.map(section => {
|
|
const total = section.statsDiff.total;
|
|
return [
|
|
section.name,
|
|
total.name,
|
|
toFileSizeDiffCell(total),
|
|
conditionalPercentage(total.diffPercentage),
|
|
].join(' | ');
|
|
});
|
|
|
|
return `${SUMMARY_HEADERS}\n${rows.join('\n')}`;
|
|
}
|
|
|
|
function renderSection(title, statsDiff, chunkModuleDiff) {
|
|
const { total, ...groups } = statsDiff;
|
|
const parts = [`#### ${title}`, '', printTotalAssetTable({ total })];
|
|
|
|
const chunkTable = printChunkModulesTable(chunkModuleDiff);
|
|
if (chunkTable) {
|
|
parts.push('', chunkTable);
|
|
}
|
|
|
|
parts.push(
|
|
'',
|
|
`<details>\n<summary>View detailed bundle breakdown</summary>\n<div>\n\n${printAssetTablesByGroup(
|
|
groups,
|
|
)}\n</div>\n</details>`,
|
|
);
|
|
|
|
return parts.join('\n');
|
|
}
|
|
|
|
function renderSections(sections) {
|
|
return sections
|
|
.map(section =>
|
|
renderSection(section.name, section.statsDiff, section.chunkDiff),
|
|
)
|
|
.join('\n\n---\n\n');
|
|
}
|
|
|
|
function getIdentifierMarkers(key) {
|
|
const label = 'bundlestats-action-comment';
|
|
return {
|
|
start: `<!--- ${label} key:${key} start --->`,
|
|
end: `<!--- ${label} key:${key} end --->`,
|
|
};
|
|
}
|
|
|
|
async function main() {
|
|
const args = parseArgs(process.argv);
|
|
const allowedFormats = new Set(['comment', 'pr-body']);
|
|
if (!allowedFormats.has(args.format)) {
|
|
throw new Error(
|
|
`Invalid format "${args.format}". Use "comment" or "pr-body".`,
|
|
);
|
|
}
|
|
|
|
console.error(
|
|
`[bundle-stats] Found ${args.sections.length} sections to process`,
|
|
);
|
|
args.sections.forEach((section, index) => {
|
|
console.error(
|
|
`[bundle-stats] Section ${index + 1}: ${section.name} (base: ${section.basePath}, head: ${section.headPath})`,
|
|
);
|
|
});
|
|
|
|
const sections = [];
|
|
|
|
for (const section of args.sections) {
|
|
console.error(`[bundle-stats] Processing section: ${section.name}`);
|
|
console.error(
|
|
`[bundle-stats] Loading base stats from: ${section.basePath}`,
|
|
);
|
|
const baseStats = await loadStats(section.basePath);
|
|
console.error(
|
|
`[bundle-stats] Loading head stats from: ${section.headPath}`,
|
|
);
|
|
const headStats = await loadStats(section.headPath);
|
|
|
|
const statsDiff = getStatsDiff(baseStats, headStats);
|
|
const chunkDiff = getChunkModuleDiff(baseStats, headStats);
|
|
|
|
console.error(
|
|
`[bundle-stats] Section ${section.name}: ${statsDiff.total.name} files, total size ${statsDiff.total.old.size} → ${statsDiff.total.new.size}`,
|
|
);
|
|
|
|
sections.push({
|
|
name: section.name,
|
|
statsDiff,
|
|
chunkDiff,
|
|
});
|
|
}
|
|
|
|
const markers = getIdentifierMarkers(args.identifier);
|
|
const sectionsContent = renderSections(sections);
|
|
const summaryTable = printSummaryTable(sections);
|
|
|
|
const detailedBody = ['### Bundle Stats', '', sectionsContent].join('\n');
|
|
|
|
const commentBody = [markers.start, detailedBody, '', markers.end, ''].join(
|
|
'\n',
|
|
);
|
|
|
|
const prBody = [
|
|
markers.start,
|
|
'### Bundle Stats',
|
|
'',
|
|
summaryTable,
|
|
'',
|
|
`<details>\n<summary>View detailed bundle stats</summary>\n\n${sectionsContent}\n</details>`,
|
|
'',
|
|
markers.end,
|
|
'',
|
|
].join('\n');
|
|
|
|
process.stdout.write(args.format === 'comment' ? commentBody : prBody);
|
|
}
|
|
|
|
main().catch(error => {
|
|
console.error(error);
|
|
process.exitCode = 1;
|
|
});
|