mirror of
https://github.com/callstack/agent-device.git
synced 2026-09-14 20:06:34 +08:00
30de1597d3
* ci: attribute npm package size by shipped component * refactor: modularize size reporting and trim Apple runner * ci: preserve size reporter modules across base checkout
184 lines
5.7 KiB
JavaScript
184 lines
5.7 KiB
JavaScript
import { execFileSync } from 'node:child_process';
|
|
import fs from 'node:fs';
|
|
import path from 'node:path';
|
|
import {
|
|
formatBytes,
|
|
formatDiff,
|
|
formatMaybeBytes,
|
|
formatSignedBytes,
|
|
} from './size-report-format.mjs';
|
|
|
|
const PACKAGE_COMPONENTS = [
|
|
{
|
|
id: 'js',
|
|
label: 'JS / dist source',
|
|
matches: (entryPath) => entryPath === 'dist/src' || entryPath.startsWith('dist/src/'),
|
|
},
|
|
{
|
|
id: 'apple-runner',
|
|
label: 'Apple runner source/project',
|
|
matches: (entryPath) =>
|
|
entryPath === 'dist/apple/runner' || entryPath.startsWith('dist/apple/runner/'),
|
|
},
|
|
{
|
|
id: 'macos-helper',
|
|
label: 'macOS helper source',
|
|
matches: (entryPath) =>
|
|
entryPath === 'apple/macos-helper' || entryPath.startsWith('apple/macos-helper/'),
|
|
},
|
|
{
|
|
id: 'android-helpers',
|
|
label: 'Android helper artifacts',
|
|
matches: (entryPath) =>
|
|
/^android\/(?:snapshot-helper|ime-helper)\/dist(?:\/|$)/.test(entryPath),
|
|
},
|
|
{ id: 'other', label: 'Other package files' },
|
|
];
|
|
|
|
export function collectNpmPack(root) {
|
|
const cachePath = path.join(root, '.tmp', 'npm-cache');
|
|
fs.mkdirSync(cachePath, { recursive: true });
|
|
const stdout = execFileSync(
|
|
'npm',
|
|
['pack', '--dry-run', '--ignore-scripts', '--json', '--cache', cachePath],
|
|
{ cwd: root, encoding: 'utf8' },
|
|
);
|
|
const pack = parseNpmPackOutput(stdout);
|
|
const entries = normalizeNpmPackEntries(pack);
|
|
return {
|
|
filename: pack.filename,
|
|
tarballBytes: pack.size,
|
|
unpackedBytes: pack.unpackedSize,
|
|
files: entries.length,
|
|
entries,
|
|
components: summarizeEntries(pack.unpackedSize, entries),
|
|
};
|
|
}
|
|
|
|
function parseNpmPackOutput(stdout) {
|
|
const parsed = JSON.parse(stdout);
|
|
return Array.isArray(parsed) ? parsed[0] : parsed;
|
|
}
|
|
|
|
function normalizeNpmPackEntries(pack) {
|
|
if (!Array.isArray(pack.files)) {
|
|
throw new Error('npm pack did not return per-file path/size data in files[]');
|
|
}
|
|
return pack.files.map((entry) => {
|
|
if (
|
|
!entry ||
|
|
typeof entry.path !== 'string' ||
|
|
!Number.isSafeInteger(entry.size) ||
|
|
entry.size < 0
|
|
) {
|
|
throw new Error(`npm pack returned an invalid file entry: ${JSON.stringify(entry)}`);
|
|
}
|
|
return { path: entry.path, size: entry.size };
|
|
});
|
|
}
|
|
|
|
export function classifyNpmPackEntry(entry) {
|
|
const matches = PACKAGE_COMPONENTS.filter((component) => component.matches?.(entry.path));
|
|
if (matches.length > 1) {
|
|
throw new Error(
|
|
`Package entry ${JSON.stringify(entry.path)} matched ${matches.length} size components`,
|
|
);
|
|
}
|
|
return { ...entry, component: matches[0]?.id ?? 'other' };
|
|
}
|
|
|
|
export function summarizeNpmPackComponents(pack) {
|
|
return summarizeEntries(pack.unpackedSize, normalizeNpmPackEntries(pack));
|
|
}
|
|
|
|
function summarizeEntries(unpackedSize, entries) {
|
|
if (!Number.isSafeInteger(unpackedSize) || unpackedSize < 0) {
|
|
throw new Error(`npm pack returned an invalid unpackedSize: ${unpackedSize}`);
|
|
}
|
|
const totals = new Map(
|
|
PACKAGE_COMPONENTS.map((component) => [component.id, { files: 0, unpackedBytes: 0 }]),
|
|
);
|
|
for (const entry of entries.map(classifyNpmPackEntry)) {
|
|
const total = totals.get(entry.component);
|
|
total.files += 1;
|
|
total.unpackedBytes += entry.size;
|
|
}
|
|
const components = PACKAGE_COMPONENTS.map((component) => ({
|
|
id: component.id,
|
|
label: component.label,
|
|
...totals.get(component.id),
|
|
}));
|
|
const componentBytes = components.reduce(
|
|
(total, component) => total + component.unpackedBytes,
|
|
0,
|
|
);
|
|
if (componentBytes !== unpackedSize) {
|
|
throw new Error(
|
|
`Package component byte sum ${componentBytes} does not match npm pack unpackedSize ${unpackedSize}`,
|
|
);
|
|
}
|
|
return components;
|
|
}
|
|
|
|
export function formatPackageComponents(currentPack, basePack) {
|
|
const currentById = new Map(
|
|
(currentPack.components ?? []).map((component) => [component.id, component]),
|
|
);
|
|
const baseById = new Map(
|
|
(basePack?.components ?? []).map((component) => [component.id, component]),
|
|
);
|
|
const rows = PACKAGE_COMPONENTS.map((component) => {
|
|
const current = currentById.get(component.id)?.unpackedBytes ?? 0;
|
|
const base = baseById.get(component.id)?.unpackedBytes;
|
|
return `| ${component.label} | ${formatMaybeBytes(base)} | ${formatBytes(current)} | ${formatDiff(base, current)} |`;
|
|
});
|
|
return `### npm unpacked components
|
|
|
|
| Component | Base | Current | Diff |
|
|
|---|---:|---:|---:|
|
|
${rows.join('\n')}
|
|
`;
|
|
}
|
|
|
|
export function formatPackedFiles(currentEntries, baseEntries) {
|
|
if (!baseEntries) return formatTopPackedFiles(currentEntries);
|
|
const currentByPath = new Map(currentEntries.map((entry) => [entry.path, entry.size]));
|
|
const baseByPath = new Map(baseEntries.map((entry) => [entry.path, entry.size]));
|
|
const paths = new Set([...currentByPath.keys(), ...baseByPath.keys()]);
|
|
const rows = [...paths]
|
|
.map((filePath) => {
|
|
const current = currentByPath.get(filePath) ?? 0;
|
|
const base = baseByPath.get(filePath) ?? 0;
|
|
return { path: filePath, base, current, diff: current - base };
|
|
})
|
|
.filter((entry) => entry.diff !== 0)
|
|
.sort((left, right) => Math.abs(right.diff) - Math.abs(left.diff))
|
|
.slice(0, 10)
|
|
.map(
|
|
(entry) =>
|
|
`| \`${entry.path}\` | ${formatBytes(entry.base)} | ${formatBytes(entry.current)} | ${formatSignedBytes(entry.diff)} |`,
|
|
);
|
|
if (rows.length === 0) {
|
|
return '### Top changed packed files\n\nNo changed packed files.\n';
|
|
}
|
|
return `### Top changed packed files
|
|
|
|
| Packed file | Base | Current | Diff |
|
|
|---|---:|---:|---:|
|
|
${rows.join('\n')}
|
|
`;
|
|
}
|
|
|
|
function formatTopPackedFiles(entries) {
|
|
const rows = [...entries]
|
|
.sort((left, right) => right.size - left.size)
|
|
.slice(0, 10)
|
|
.map((entry) => `| \`${entry.path}\` | ${formatBytes(entry.size)} |`);
|
|
return `### Top packed files
|
|
|
|
| Packed file | Unpacked |
|
|
|---|---:|
|
|
${rows.join('\n')}
|
|
`;
|
|
}
|