140 lines
5.8 KiB
JavaScript
140 lines
5.8 KiB
JavaScript
import path from 'path';
|
|
import { promises as fs, statSync } from 'fs';
|
|
import { APP_PATHS_MANIFEST, CLIENT_REFERENCE_MANIFEST, SERVER_DIRECTORY } from '../shared/lib/constants';
|
|
import { filterAndSortList } from './utils';
|
|
const ROUTE_BUNDLE_STATS_FILE = 'route-bundle-stats.json';
|
|
function sumFileSizes(distDir, files, cache) {
|
|
let total = 0;
|
|
for (const relPath of files){
|
|
const cached = cache.get(relPath);
|
|
if (cached !== undefined) {
|
|
total += cached;
|
|
continue;
|
|
}
|
|
try {
|
|
const size = statSync(path.join(distDir, relPath)).size;
|
|
cache.set(relPath, size);
|
|
total += size;
|
|
} catch {
|
|
// ignore missing files
|
|
}
|
|
}
|
|
return total;
|
|
}
|
|
function toProjectRelativePaths(dir, distDir, relPaths) {
|
|
return relPaths.map((f)=>path.relative(dir, path.join(distDir, f)));
|
|
}
|
|
function buildRouteToAppPathsMap(appPathsManifest) {
|
|
const { normalizeAppPath } = require('../shared/lib/router/utils/app-paths');
|
|
// Keys in appPathsManifest are app paths like /blog/[slug]/page;
|
|
// values are server bundle file paths. Normalize the key to get the route.
|
|
const routeToAppPaths = new Map();
|
|
for (const appPath of Object.keys(appPathsManifest)){
|
|
const route = normalizeAppPath(appPath);
|
|
const existing = routeToAppPaths.get(route);
|
|
if (existing) {
|
|
existing.push(appPath);
|
|
} else {
|
|
routeToAppPaths.set(route, [
|
|
appPath
|
|
]);
|
|
}
|
|
}
|
|
return routeToAppPaths;
|
|
}
|
|
// Reads the manfiest file and gets the entry JS files. The manifest file is
|
|
// a JavaScript file that sets a global variable (__RSC_MANIFEST). We require()
|
|
// it with a save/restore of the global
|
|
function readEntryJSFiles(distDir, pagePath, appRoute) {
|
|
const manifestFile = path.join(distDir, SERVER_DIRECTORY, 'app', `${pagePath}_${CLIENT_REFERENCE_MANIFEST}.js`);
|
|
try {
|
|
const g = global;
|
|
const prev = g.__RSC_MANIFEST;
|
|
g.__RSC_MANIFEST = undefined;
|
|
require(manifestFile);
|
|
const rscManifest = g.__RSC_MANIFEST;
|
|
g.__RSC_MANIFEST = prev;
|
|
// The key in __RSC_MANIFEST is the app path (e.g. /blog/[slug]/page)
|
|
const manifestEntry = (rscManifest == null ? void 0 : rscManifest[pagePath]) ?? (rscManifest == null ? void 0 : rscManifest[appRoute]);
|
|
return manifestEntry == null ? void 0 : manifestEntry.entryJSFiles;
|
|
} catch {
|
|
return undefined;
|
|
}
|
|
}
|
|
function collectPagesRouterStats(pages, buildManifest, distDir, dir, cache) {
|
|
const rows = [];
|
|
const sharedFiles = buildManifest.pages['/_app'] ?? [];
|
|
for (const page of filterAndSortList(pages, 'pages', false)){
|
|
if (page === '/_app' || page === '/_document' || page === '/_error') continue;
|
|
const allFiles = (buildManifest.pages[page] ?? []).filter((f)=>f.endsWith('.js'));
|
|
const sharedJs = sharedFiles.filter((f)=>f.endsWith('.js'));
|
|
const chunks = [
|
|
...new Set([
|
|
...allFiles,
|
|
...sharedJs
|
|
])
|
|
];
|
|
const firstLoadUncompressedJsBytes = sumFileSizes(distDir, chunks, cache);
|
|
rows.push({
|
|
route: page,
|
|
firstLoadUncompressedJsBytes,
|
|
firstLoadChunkPaths: toProjectRelativePaths(dir, distDir, chunks)
|
|
});
|
|
}
|
|
return rows;
|
|
}
|
|
async function collectAppRouterStats(appRoutes, buildManifest, distDir, dir, cache) {
|
|
let appPathsManifest = {};
|
|
try {
|
|
const manifestPath = path.join(distDir, SERVER_DIRECTORY, APP_PATHS_MANIFEST);
|
|
appPathsManifest = JSON.parse(await fs.readFile(manifestPath, 'utf8'));
|
|
} catch {
|
|
// App paths manifest not available; skip app router sizes
|
|
return [];
|
|
}
|
|
const routeToAppPaths = buildRouteToAppPathsMap(appPathsManifest);
|
|
const sharedFiles = buildManifest.rootMainFiles ?? [];
|
|
const rows = [];
|
|
for (const appRoute of filterAndSortList(appRoutes, 'app', false)){
|
|
const appPaths = routeToAppPaths.get(appRoute) ?? [];
|
|
// Find the /page entry (most specific, has the most chunks)
|
|
const pagePath = appPaths.find((p)=>p.endsWith('/page'));
|
|
if (!pagePath) continue;
|
|
const entryJSFiles = readEntryJSFiles(distDir, pagePath, appRoute);
|
|
if (!entryJSFiles) continue;
|
|
// Union JS files across all segments (page, layout, etc.) so that
|
|
// layout code's contribution is included in the First Load JS total.
|
|
const allFiles = [
|
|
...new Set(Object.values(entryJSFiles).flat().filter((f)=>f.endsWith('.js')))
|
|
];
|
|
const sharedJs = sharedFiles.filter((f)=>f.endsWith('.js'));
|
|
const chunks = [
|
|
...new Set([
|
|
...allFiles,
|
|
...sharedJs
|
|
])
|
|
];
|
|
const firstLoadUncompressedJsBytes = sumFileSizes(distDir, chunks, cache);
|
|
rows.push({
|
|
route: appRoute,
|
|
firstLoadUncompressedJsBytes,
|
|
firstLoadChunkPaths: toProjectRelativePaths(dir, distDir, chunks)
|
|
});
|
|
}
|
|
return rows;
|
|
}
|
|
export async function writeRouteBundleStats(lists, buildManifest, distDir, dir) {
|
|
const cache = new Map();
|
|
const rows = [
|
|
...lists.pages.length > 0 ? collectPagesRouterStats(lists.pages, buildManifest, distDir, dir, cache) : [],
|
|
...lists.app && lists.app.length > 0 ? await collectAppRouterStats(lists.app, buildManifest, distDir, dir, cache) : []
|
|
];
|
|
rows.sort((a, b)=>b.firstLoadUncompressedJsBytes - a.firstLoadUncompressedJsBytes);
|
|
const diagnosticsDir = path.join(distDir, 'diagnostics');
|
|
await fs.mkdir(diagnosticsDir, {
|
|
recursive: true
|
|
});
|
|
await fs.writeFile(path.join(diagnosticsDir, ROUTE_BUNDLE_STATS_FILE), JSON.stringify(rows, null, 2));
|
|
}
|
|
|
|
//# sourceMappingURL=route-bundle-stats.js.map
|