Skip to content

Instantly share code, notes, and snippets.

@raphaelbadia
Created September 4, 2023 10:04
Show Gist options
  • Save raphaelbadia/1fbc948928378a4ce0e1ce90923263ae to your computer and use it in GitHub Desktop.
Save raphaelbadia/1fbc948928378a4ce0e1ce90923263ae to your computer and use it in GitHub Desktop.
#!/usr/bin/env node
/* eslint-disable no-console */
/**
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: MPL-2.0
*/
// edited to work with the appdir by @raphaelbadia
const path = require('path');
const fs = require('fs');
const gzSize = require('gzip-size');
const mkdirp = require('mkdirp');
// Pull options from `package.json`
const options = getOptions();
const BUILD_OUTPUT_DIRECTORY = getBuildOutputDirectory(options);
// first we check to make sure that the build output directory exists
const nextMetaRoot = path.join(process.cwd(), BUILD_OUTPUT_DIRECTORY);
try {
fs.accessSync(nextMetaRoot, fs.constants.R_OK);
} catch (err) {
console.error(
`No build output found at "${nextMetaRoot}" - you may not have your working directory set correctly, or not have run "next build".`,
);
process.exit(1);
}
// if so, we can import the build manifest
const buildMeta = require(path.join(nextMetaRoot, 'build-manifest.json'));
const appDirMeta = require(path.join(nextMetaRoot, 'app-build-manifest.json'));
// this memory cache ensures we dont read any script file more than once
// bundles are often shared between pages
const memoryCache = {};
// since _app is the template that all other pages are rendered into,
// every page must load its scripts. we'll measure its size here
const globalBundle = buildMeta.pages['/_app'];
const globalBundleSizes = getScriptSizes(globalBundle);
// next, we calculate the size of each page's scripts, after
// subtracting out the global scripts
const allPageSizes = Object.values(buildMeta.pages).reduce((acc, scriptPaths, i) => {
const pagePath = Object.keys(buildMeta.pages)[i];
const scriptSizes = getScriptSizes(
scriptPaths.filter((scriptPath) => !globalBundle.includes(scriptPath)),
);
acc[pagePath] = scriptSizes;
return acc;
}, {});
const globalAppDirBundle = buildMeta.rootMainFiles;
const globalAppDirBundleSizes = getScriptSizes(globalAppDirBundle);
const allAppDirSizes = Object.values(appDirMeta.pages).reduce((acc, scriptPaths, i) => {
const pagePath = Object.keys(appDirMeta.pages)[i];
const scriptSizes = getScriptSizes(
scriptPaths.filter((scriptPath) => !globalAppDirBundle.includes(scriptPath)),
);
acc[pagePath] = scriptSizes;
return acc;
}, {});
// format and write the output
const rawData = JSON.stringify({
...allAppDirSizes,
__global: globalAppDirBundleSizes,
});
// log ouputs to the gh actions panel
console.log(rawData);
mkdirp.sync(path.join(nextMetaRoot, 'analyze/'));
fs.writeFileSync(path.join(nextMetaRoot, 'analyze/__bundle_analysis.json'), rawData);
// --------------
// Util Functions
// --------------
// given an array of scripts, return the total of their combined file sizes
function getScriptSizes(scriptPaths) {
const res = scriptPaths.reduce(
(acc, scriptPath) => {
const [rawSize, gzipSize] = getScriptSize(scriptPath);
acc.raw += rawSize;
acc.gzip += gzipSize;
return acc;
},
{ raw: 0, gzip: 0 },
);
return res;
}
// given an individual path to a script, return its file size
function getScriptSize(scriptPath) {
const encoding = 'utf8';
const p = path.join(nextMetaRoot, scriptPath);
let rawSize, gzipSize;
if (Object.keys(memoryCache).includes(p)) {
rawSize = memoryCache[p][0];
gzipSize = memoryCache[p][1];
} else {
const textContent = fs.readFileSync(p, encoding);
rawSize = Buffer.byteLength(textContent, encoding);
gzipSize = gzSize.sync(textContent);
memoryCache[p] = [rawSize, gzipSize];
}
return [rawSize, gzipSize];
}
/**
* Reads options from `package.json`
*/
function getOptions(pathPrefix = process.cwd()) {
const pkg = require(path.join(pathPrefix, 'package.json'));
return { ...pkg.nextBundleAnalysis, name: pkg.name };
}
/**
* Gets the output build directory, defaults to `.next`
*
* @param {object} options the options parsed from package.json.nextBundleAnalysis using `getOptions`
* @returns {string}
*/
function getBuildOutputDirectory(options) {
return options.buildOutputDirectory || '.next';
}
@Thanaen
Copy link

Thanaen commented Sep 15, 2023

It seems that the globalBundleSize and allPageSizes objects are not being used! Is this expected?

PS: Ah, I guess you've edited the original script, and these methods were the ones used for the "pages" router!

@raphaelbadia
Copy link
Author

@Thanaen indeed, I I only converted it to app router and forgot to clean them - it won't work for pages route anymore!

@Thanaen
Copy link

Thanaen commented Sep 15, 2023

👌
Thanks for this handy script!

@asaam7373
Copy link

رائع جدا

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment