diff --git a/.github/workflows/nextjs_bundle_analysis.yml b/.github/workflows/nextjs_bundle_analysis.yml index 68756b6..d88714a 100644 --- a/.github/workflows/nextjs_bundle_analysis.yml +++ b/.github/workflows/nextjs_bundle_analysis.yml @@ -51,7 +51,7 @@ jobs: # Here's the first place where next-bundle-analysis' own script is used # This step pulls the raw bundle stats for the current bundle - name: Analyze bundle - run: npx -p nextjs-bundle-analysis report + run: node report-bundle-size.js - name: Upload bundle uses: actions/upload-artifact@v3 diff --git a/package.json b/package.json index ebb87ff..97ace9f 100644 --- a/package.json +++ b/package.json @@ -97,8 +97,10 @@ "eslint-plugin-storybook": "^0.6.15", "eslint-plugin-tailwindcss": "^3.13.0", "fetch-mock": "^9.11.0", + "gzip-size": "6", "jest": "^29.7.0", "jest-environment-jsdom": "^29.7.0", + "mkdirp": "^3.0.1", "npm-only-allow": "^1.2.6", "patch-package": "^8.0.0", "postcss": "^8.4.31", diff --git a/report-bundle-size.js b/report-bundle-size.js new file mode 100644 index 0000000..75c9a56 --- /dev/null +++ b/report-bundle-size.js @@ -0,0 +1,133 @@ +#!/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 gzSize = require("gzip-size") +const mkdirp = require("mkdirp") +const fs = require("fs") +const path = require("path") + +// 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" +} diff --git a/yarn.lock b/yarn.lock index c693f88..4d6e1b9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9419,7 +9419,7 @@ gunzip-maybe@^1.4.2: pumpify "^1.3.3" through2 "^2.0.3" -gzip-size@^6.0.0: +gzip-size@6, gzip-size@^6.0.0: version "6.0.0" resolved "https://registry.yarnpkg.com/gzip-size/-/gzip-size-6.0.0.tgz#065367fd50c239c0671cbcbad5be3e2eeb10e462" integrity sha512-ax7ZYomf6jqPTQ4+XCpUGyXKHk5WweS+e05MBO4/y3WJ5RkmPXNKvX+bx1behVILVwr6JSQvZAku021CHPXG3Q== @@ -11813,6 +11813,11 @@ mkdirp@^1.0.3, mkdirp@^1.0.4: resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e" integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw== +mkdirp@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-3.0.1.tgz#e44e4c5607fb279c168241713cc6e0fea9adcb50" + integrity sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg== + module-details-from-path@^1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/module-details-from-path/-/module-details-from-path-1.0.3.tgz#114c949673e2a8a35e9d35788527aa37b679da2b"