mirror of
https://github.com/callstack/agent-device.git
synced 2026-09-14 20:06:34 +08:00
b15121ffc8
* test(ios): add snapshot convergence evidence harness * fix(ios): satisfy benchmark CI guards * fix(ios): constrain benchmark proxy routes * fix(ios-benchmark): enforce cell admission evidence * fix(ios-benchmark): protect benchmark state ownership * fix(ios-benchmark): use proxy port flag * fix(ios-benchmark): let proxy choose an ephemeral port * test(ios-benchmark): keep CLI process seam local * fix(ios-benchmark): parse proxy startup envelope * fix(ios-benchmark): bind proxy lease to simulator * fix(ios-benchmark): keep fresh proxy CLI sessions isolated * fix(ios-benchmark): preserve async timeout evidence * docs(ios-benchmark): retain exact-head evidence * test(ios): reveal offscreen alert fixture controls * test(ios): reset alert between relaunch samples * test(ios): admit native alert snapshots * docs(ios): publish snapshot convergence corpus * chore(ios): format benchmark evidence * fix(ios-benchmark): admit proxy fixture anchors * docs(ios): republish exact-head benchmark corpus * fix(size): make publish asset evidence hermetic * style(size): format package evidence test * test(size): update publish preparation contracts * fix: retire stale utils layering zone * test: pin shared publish asset owner * test: verify preserved size reporter closure * fix: move mutation ownership to snapshot module * test(ios): add snapshot convergence evidence harness * fix(ios): satisfy benchmark CI guards * fix(ios): constrain benchmark proxy routes * fix(ios-benchmark): enforce cell admission evidence * fix(ios-benchmark): protect benchmark state ownership * fix(ios-benchmark): use proxy port flag * fix(ios-benchmark): let proxy choose an ephemeral port * test(ios-benchmark): keep CLI process seam local * fix(ios-benchmark): parse proxy startup envelope * fix(ios-benchmark): bind proxy lease to simulator * fix(ios-benchmark): keep fresh proxy CLI sessions isolated * fix(ios-benchmark): preserve async timeout evidence * docs(ios-benchmark): retain exact-head evidence * test(ios): reveal offscreen alert fixture controls * test(ios): reset alert between relaunch samples * test(ios): admit native alert snapshots * docs(ios): publish snapshot convergence corpus * chore(ios): format benchmark evidence * fix(ios-benchmark): admit proxy fixture anchors * docs(ios): republish exact-head benchmark corpus * fix(size): make publish asset evidence hermetic * test(size): update publish preparation contracts * fix: keep git-state gates out of mutation sandboxes
222 lines
7.2 KiB
JavaScript
222 lines
7.2 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: 'apple-snapshot-presentation',
|
|
label: 'Apple snapshot presentation source',
|
|
matches: (entryPath) =>
|
|
entryPath === 'dist/apple/snapshot-presentation' ||
|
|
entryPath.startsWith('dist/apple/snapshot-presentation/'),
|
|
},
|
|
{
|
|
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', '--ignore-scripts', '--json', '--pack-destination', cachePath, '--cache', cachePath],
|
|
{ cwd: root, encoding: 'utf8' },
|
|
);
|
|
const pack = parseNpmPackOutput(stdout);
|
|
const entries = normalizeNpmPackEntries(pack);
|
|
assertPublishPackageContents(entries);
|
|
return {
|
|
filename: pack.filename,
|
|
tarballPath: path.join(cachePath, pack.filename),
|
|
tarballBytes: pack.size,
|
|
unpackedBytes: pack.unpackedSize,
|
|
files: entries.length,
|
|
entries,
|
|
components: summarizeEntries(pack.unpackedSize, entries),
|
|
};
|
|
}
|
|
|
|
export function assertPublishPackageContents(entries) {
|
|
const paths = entries.map((entry) => entry.path);
|
|
const requiredAssets = [
|
|
{ directory: 'android/snapshot-helper/dist/', suffix: '.apk' },
|
|
{ directory: 'android/snapshot-helper/dist/', suffix: '.manifest.json' },
|
|
{ directory: 'android/ime-helper/dist/', suffix: '.apk' },
|
|
{ directory: 'android/ime-helper/dist/', suffix: '.manifest.json' },
|
|
];
|
|
const missingAssets = requiredAssets.filter(
|
|
(asset) =>
|
|
!paths.some(
|
|
(entryPath) => entryPath.startsWith(asset.directory) && entryPath.endsWith(asset.suffix),
|
|
),
|
|
);
|
|
if (missingAssets.length > 0) {
|
|
throw new Error(
|
|
`npm pack is missing publish assets: ${missingAssets
|
|
.map((asset) => `${asset.directory}*${asset.suffix}`)
|
|
.join(', ')}`,
|
|
);
|
|
}
|
|
const forbiddenPaths = paths.filter(
|
|
(entryPath) => entryPath === 'scripts' || entryPath.startsWith('scripts/'),
|
|
);
|
|
if (forbiddenPaths.length > 0) {
|
|
throw new Error(`npm pack includes benchmark or build scripts: ${forbiddenPaths.join(', ')}`);
|
|
}
|
|
}
|
|
|
|
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')}
|
|
`;
|
|
}
|