Skip to content

Commit

Permalink
Add dynamic bundle size calculations (#47)
Browse files Browse the repository at this point in the history
* Add dynamic bundle size calculations

* Fix significant changes and node_modules check

* Only show fallback text if both tables are empty
  • Loading branch information
canac authored Apr 25, 2024
1 parent aa7c3f9 commit a1da747
Show file tree
Hide file tree
Showing 7 changed files with 170 additions and 27 deletions.
81 changes: 68 additions & 13 deletions dist/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -127755,6 +127755,11 @@ function getStaticBundleSizes(workingDir) {
const manifest = loadBuildManifest(workingDir);
return getPageSizesFromManifest(manifest, workingDir);
}
function getDynamicBundleSizes(workingDir) {
const staticManifest = loadBuildManifest(workingDir);
const manifest = loadReactLoadableManifest(staticManifest.pages['/_app'], workingDir);
return getPageSizesFromManifest(manifest, workingDir);
}
function getPageSizesFromManifest(manifest, workingDir) {
return Object.entries(manifest.pages).map(([page, files]) => {
const size = files
Expand All @@ -127772,6 +127777,28 @@ function loadBuildManifest(workingDir) {
const file = external_fs_default().readFileSync(external_path_default().join(process.cwd(), workingDir, '.next', 'build-manifest.json'), 'utf-8');
return JSON.parse(file);
}
function loadReactLoadableManifest(appChunks, workingDir) {
const file = external_fs_default().readFileSync(external_path_default().join(process.cwd(), workingDir, '.next', 'react-loadable-manifest.json'), 'utf-8');
const content = JSON.parse(file);
const pages = {};
Object.keys(content).forEach((item) => {
if (item.includes('/node_modules/')) {
return;
}
const fileList = getFiles(content[item]);
const uniqueFileList = Array.from(new Set(fileList));
pages[item] = uniqueFileList.filter((file) => !appChunks.find((chunkFile) => file === chunkFile));
});
return {
pages,
};
}
function getFiles(chunks) {
if (chunks.files) {
return chunks.files;
}
return chunks.map(({ file }) => file);
}
function getSingleColumnMarkdownTable({ bundleSizes, name, }) {
const rows = getPageChangeInfo([], bundleSizes);
return formatTableNoDiff(name, rows);
Expand Down Expand Up @@ -127851,7 +127878,7 @@ function getSign(bytes) {
;// CONCATENATED MODULE: ./src/text-format.ts
function formatTextFragments(...text) {
return text
.map((fragment) => fragment.trim())
.map((fragment) => fragment === null || fragment === void 0 ? void 0 : fragment.trim())
.filter(Boolean)
.join('\n\n');
}
Expand All @@ -127864,19 +127891,22 @@ async function findCommentByTextMatch({ octokit, issueNumber, text, }) {
const { data: comments } = await octokit.rest.issues.listComments(Object.assign(Object.assign({}, github.context.repo), { issue_number: issueNumber }));
return comments.find((comment) => { var _a; return (_a = comment.body) === null || _a === void 0 ? void 0 : _a.includes(text); });
}
async function createOrReplaceComment({ octokit, issueNumber, title, shaInfo, routesTable, strategy, }) {
async function createOrReplaceComment({ octokit, issueNumber, title, shaInfo, routesTable, dynamicTable, strategy, }) {
const existingComment = await findCommentByTextMatch({
octokit,
issueNumber,
text: title,
});
const body = formatTextFragments(title, shaInfo, routesTable !== null && routesTable !== void 0 ? routesTable : FALLBACK_COMPARISON_TEXT);
const body = formatTextFragments(title, shaInfo, routesTable, dynamicTable, !(routesTable === null || routesTable === void 0 ? void 0 : routesTable.trim()) && !(dynamicTable === null || dynamicTable === void 0 ? void 0 : dynamicTable.trim()) ? FALLBACK_COMPARISON_TEXT : null);
if (existingComment) {
console.log(`Updating comment ${existingComment.id}`);
const response = await octokit.rest.issues.updateComment(Object.assign(Object.assign({}, github.context.repo), { comment_id: existingComment.id, body }));
console.log(`Done with status ${response.status}`);
}
else if (!existingComment && !routesTable && strategy === 'skip-insignificant') {
else if (!existingComment &&
!routesTable &&
!dynamicTable &&
strategy === 'skip-insignificant') {
console.log(`Skipping comment [${title}]: no significant changes`);
}
else {
Expand Down Expand Up @@ -127991,20 +128021,23 @@ function getInputs() {

;// CONCATENATED MODULE: ./src/issue.ts


async function findIssueByTitleMatch({ octokit, title }) {
const { data: issues } = await octokit.rest.issues.listForRepo(github.context.repo);
return issues.find((issue) => issue.title === title);
}
async function createOrReplaceIssue({ octokit, title, routesTable, }) {
async function createOrReplaceIssue({ octokit, title, routesTable, dynamicTable, }) {
const existingIssue = await findIssueByTitleMatch({ octokit, title });
const body = formatTextFragments(routesTable, dynamicTable);
if (existingIssue) {
console.log(`Updating issue ${existingIssue.number} with latest bundle sizes`);
const response = await octokit.rest.issues.update(Object.assign(Object.assign({}, github.context.repo), { body: routesTable, issue_number: existingIssue.number }));
const response = await octokit.rest.issues.update(Object.assign(Object.assign({}, github.context.repo), { body, issue_number: existingIssue.number }));
console.log(`Issue update response status ${response.status}`);
}
else {
console.log(`Creating issue "${title}" to show latest bundle sizes`);
const response = await octokit.rest.issues.create(Object.assign(Object.assign({}, github.context.repo), { body: routesTable, title }));
const response = await octokit.rest.issues.create(Object.assign(Object.assign({}, github.context.repo), { body,
title }));
console.log(`Issue creation response status ${response.status}`);
}
}
Expand All @@ -128017,13 +128050,16 @@ var tmp = __nccwpck_require__(38766);



async function uploadJsonAsArtifact(artifactName, fileName, data) {
async function uploadJsonAsArtifact(artifactName, artifactFiles) {
const artifactClient = new artifact.DefaultArtifactClient();
const dir = tmp/* dirSync */.op();
const file = tmp/* fileSync */.yd({ name: fileName, dir: dir.name });
external_fs_.writeFileSync(file.name, JSON.stringify(data, null, 2));
console.log(`Uploading ${file.name}`);
const response = await artifactClient.uploadArtifact(artifactName, [file.name], dir.name);
const filenames = artifactFiles.map(({ fileName, data }) => {
const file = tmp/* fileSync */.yd({ name: fileName, dir: dir.name });
external_fs_.writeFileSync(file.name, JSON.stringify(data, null, 2));
return file.name;
});
console.log(`Uploading ${filenames.join(', ')}`);
const response = await artifactClient.uploadArtifact(artifactName, filenames, dir.name);
console.log('Artifact uploaded', response);
}

Expand All @@ -128039,6 +128075,7 @@ async function uploadJsonAsArtifact(artifactName, fileName, data) {

const ARTIFACT_NAME_PREFIX = 'next-bundle-analyzer__';
const FILE_NAME = 'bundle-sizes.json';
const DYNAMIC_FILE_NAME = 'dynamic-bundle-sizes.json';
async function run() {
var _a;
try {
Expand All @@ -128051,11 +128088,18 @@ async function run() {
console.log(`> Downloading bundle sizes from ${default_branch}`);
const referenceBundleSizes = (await downloadArtifactAsJson(octokit, default_branch, artifactName, FILE_NAME)) || { sha: 'none', data: [] };
console.log(referenceBundleSizes);
const referenceDynamicBundleSizes = (await downloadArtifactAsJson(octokit, default_branch, artifactName, DYNAMIC_FILE_NAME)) || { sha: 'none', data: [] };
console.log(referenceDynamicBundleSizes);
console.log('> Calculating local bundle sizes');
const bundleSizes = getStaticBundleSizes(inputs.workingDirectory);
console.log(bundleSizes);
const dynamicBundleSizes = getDynamicBundleSizes(inputs.workingDirectory);
console.log(dynamicBundleSizes);
console.log('> Uploading local bundle sizes');
await uploadJsonAsArtifact(artifactName, FILE_NAME, bundleSizes);
await uploadJsonAsArtifact(artifactName, [
{ fileName: FILE_NAME, data: bundleSizes },
{ fileName: DYNAMIC_FILE_NAME, data: dynamicBundleSizes },
]);
if (issueNumber) {
const title = `### Bundle sizes [${appName}]`;
const shaInfo = `Compared against ${referenceBundleSizes.sha}`;
Expand All @@ -128064,23 +128108,34 @@ async function run() {
actualBundleSizes: bundleSizes,
name: 'Route',
});
const dynamicTable = getComparisonMarkdownTable({
referenceBundleSizes: referenceDynamicBundleSizes.data,
actualBundleSizes: dynamicBundleSizes,
name: 'Dynamic import',
});
createOrReplaceComment({
octokit,
issueNumber,
title,
shaInfo,
routesTable,
dynamicTable,
strategy: inputs.commentStrategy,
});
}
else if (github.context.ref === `refs/heads/${default_branch}` && inputs.createIssue) {
console.log('> Creating/updating bundle size issue');
const title = `Bundle sizes [${appName}]`;
const routesTable = getSingleColumnMarkdownTable({ bundleSizes, name: 'Route' });
const dynamicTable = getSingleColumnMarkdownTable({
bundleSizes: dynamicBundleSizes,
name: 'Dynamic import',
});
createOrReplaceIssue({
octokit,
title,
routesTable,
dynamicTable,
});
}
}
Expand Down
40 changes: 40 additions & 0 deletions src/bundle-size.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ type BuildManifest = {
pages: Record<string, string[]>;
};

type ReactLoadableManifest = Record<string, Next10Chunks | Next12Chunks>;
type Next10Chunks = { id: string; file: string }[];
type Next12Chunks = { id: string; files: string[] };

export type PageBundleSizes = { page: string; size: number }[];

export function getStaticBundleSizes(workingDir: string): PageBundleSizes {
Expand All @@ -14,6 +18,13 @@ export function getStaticBundleSizes(workingDir: string): PageBundleSizes {
return getPageSizesFromManifest(manifest, workingDir);
}

export function getDynamicBundleSizes(workingDir: string): PageBundleSizes {
const staticManifest = loadBuildManifest(workingDir);
const manifest = loadReactLoadableManifest(staticManifest.pages['/_app'], workingDir);

return getPageSizesFromManifest(manifest, workingDir);
}

function getPageSizesFromManifest(manifest: BuildManifest, workingDir: string): PageBundleSizes {
return Object.entries(manifest.pages).map(([page, files]) => {
const size = files
Expand All @@ -37,6 +48,35 @@ function loadBuildManifest(workingDir: string): BuildManifest {
return JSON.parse(file);
}

function loadReactLoadableManifest(appChunks: string[], workingDir: string): BuildManifest {
const file = fs.readFileSync(
path.join(process.cwd(), workingDir, '.next', 'react-loadable-manifest.json'),
'utf-8',
);
const content = JSON.parse(file) as ReactLoadableManifest;
const pages: BuildManifest['pages'] = {};
Object.keys(content).forEach((item) => {
if (item.includes('/node_modules/')) {
return;
}
const fileList = getFiles(content[item]);
const uniqueFileList = Array.from(new Set(fileList));
pages[item] = uniqueFileList.filter(
(file) => !appChunks.find((chunkFile) => file === chunkFile),
);
});
return {
pages,
};
}

function getFiles(chunks: Next10Chunks | Next12Chunks): string[] {
if ((chunks as Next12Chunks).files) {
return (chunks as Next12Chunks).files;
}
return (chunks as Next10Chunks).map(({ file }) => file);
}

export function getSingleColumnMarkdownTable({
bundleSizes,
name,
Expand Down
17 changes: 15 additions & 2 deletions src/comments.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,13 +28,15 @@ export async function createOrReplaceComment({
title,
shaInfo,
routesTable,
dynamicTable,
strategy,
}: {
octokit: Octokit;
issueNumber: number;
title: string;
shaInfo: string;
routesTable: string | null;
dynamicTable: string | null;
strategy: ActionInputs['commentStrategy'];
}): Promise<void> {
const existingComment = await findCommentByTextMatch({
Expand All @@ -43,7 +45,13 @@ export async function createOrReplaceComment({
text: title,
});

const body = formatTextFragments(title, shaInfo, routesTable ?? FALLBACK_COMPARISON_TEXT);
const body = formatTextFragments(
title,
shaInfo,
routesTable,
dynamicTable,
!routesTable?.trim() && !dynamicTable?.trim() ? FALLBACK_COMPARISON_TEXT : null,
);

if (existingComment) {
console.log(`Updating comment ${existingComment.id}`);
Expand All @@ -53,7 +61,12 @@ export async function createOrReplaceComment({
body,
});
console.log(`Done with status ${response.status}`);
} else if (!existingComment && !routesTable && strategy === 'skip-insignificant') {
} else if (
!existingComment &&
!routesTable &&
!dynamicTable &&
strategy === 'skip-insignificant'
) {
console.log(`Skipping comment [${title}]: no significant changes`);
} else {
console.log(`Creating comment on PR ${issueNumber}`);
Expand Down
27 changes: 26 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import * as core from '@actions/core';
import { context, getOctokit } from '@actions/github';
import {
getComparisonMarkdownTable,
getDynamicBundleSizes,
getSingleColumnMarkdownTable,
getStaticBundleSizes,
} from './bundle-size';
Expand All @@ -15,6 +16,7 @@ import { uploadJsonAsArtifact } from './upload-artifacts';

const ARTIFACT_NAME_PREFIX = 'next-bundle-analyzer__';
const FILE_NAME = 'bundle-sizes.json';
const DYNAMIC_FILE_NAME = 'dynamic-bundle-sizes.json';

async function run() {
try {
Expand All @@ -38,13 +40,25 @@ async function run() {
FILE_NAME,
)) || { sha: 'none', data: [] };
console.log(referenceBundleSizes);
const referenceDynamicBundleSizes = (await downloadArtifactAsJson(
octokit,
default_branch,
artifactName,
DYNAMIC_FILE_NAME,
)) || { sha: 'none', data: [] };
console.log(referenceDynamicBundleSizes);

console.log('> Calculating local bundle sizes');
const bundleSizes = getStaticBundleSizes(inputs.workingDirectory);
console.log(bundleSizes);
const dynamicBundleSizes = getDynamicBundleSizes(inputs.workingDirectory);
console.log(dynamicBundleSizes);

console.log('> Uploading local bundle sizes');
await uploadJsonAsArtifact(artifactName, FILE_NAME, bundleSizes);
await uploadJsonAsArtifact(artifactName, [
{ fileName: FILE_NAME, data: bundleSizes },
{ fileName: DYNAMIC_FILE_NAME, data: dynamicBundleSizes },
]);

if (issueNumber) {
const title = `### Bundle sizes [${appName}]`;
Expand All @@ -54,22 +68,33 @@ async function run() {
actualBundleSizes: bundleSizes,
name: 'Route',
});
const dynamicTable = getComparisonMarkdownTable({
referenceBundleSizes: referenceDynamicBundleSizes.data,
actualBundleSizes: dynamicBundleSizes,
name: 'Dynamic import',
});
createOrReplaceComment({
octokit,
issueNumber,
title,
shaInfo,
routesTable,
dynamicTable,
strategy: inputs.commentStrategy,
});
} else if (context.ref === `refs/heads/${default_branch}` && inputs.createIssue) {
console.log('> Creating/updating bundle size issue');
const title = `Bundle sizes [${appName}]`;
const routesTable = getSingleColumnMarkdownTable({ bundleSizes, name: 'Route' });
const dynamicTable = getSingleColumnMarkdownTable({
bundleSizes: dynamicBundleSizes,
name: 'Dynamic import',
});
createOrReplaceIssue({
octokit,
title,
routesTable,
dynamicTable,
});
}
} catch (e) {
Expand Down
Loading

0 comments on commit a1da747

Please sign in to comment.