Skip to content

Commit

Permalink
Merge pull request #16 from agoda-com/AVAILABILI-3470
Browse files Browse the repository at this point in the history
FEATURE: Send vite bundle stats
  • Loading branch information
sofen-ag authored Nov 1, 2024
2 parents a534172 + bd536b4 commit bb52189
Show file tree
Hide file tree
Showing 7 changed files with 139 additions and 8 deletions.
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,12 @@ or for Vite:
viteBuildStatsPlugin('vite-build-extraordinaire');
```

Wait, there's even more! If you are limiting the bootstrap chunk size of your js bundle (see [anti-chonk](https://www.npmjs.com/package/vite-plugin-anti-chonk)), then you can pass that limit here as well. This will help you track how this limit has changed on your project.

```javascript
viteBuildStatsPlugin('vite-build-extraordinaire', 1000); // 1 mega byte
```

## The F5 Experience: Because Waiting is So Last Year

What is the F5 Experience? have a read [here](https://beerandserversdontmix.com/2024/08/15/an-introduction-to-the-f5-experience/)
Expand Down
7 changes: 5 additions & 2 deletions examples/vite.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,9 @@
"v8Version": "9.4.146.24",
"viteVersion": "4.0.0",
"commitSha": "a1b2c3d4e5f678901234567890abcdef12345678",
"customIdentifier": "build-job-12345"
"customIdentifier": "build-job-12345",
"bundleStats": {
"bootstrapChunkSizeBytes": 1000000,
"bootstrapChunkSizeLimitBytes": 2500000
}
}

2 changes: 1 addition & 1 deletion jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,6 @@ module.exports = {
transform: {
'^.+\\.(t|j)sx?$': 'ts-jest',
},
testRegex: '(/tests/.*|(\\.|/)(test|spec))\\.(jsx?|tsx?)$',
testRegex: '/tests/.+\\.spec\\.(jsx?|tsx?)$',
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'],
};
6 changes: 6 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,13 @@ export interface WebpackBuildData extends CommonMetadata {
nbrOfRebuiltModules: number;
}

export interface ViteBundleStats {
bootstrapChunkSizeBytes?: number
bootstrapChunkSizeLimitBytes?: number
}

export interface ViteBuildData extends CommonMetadata {
type: 'vite';
viteVersion: string | null;
bundleStats?: ViteBundleStats
}
27 changes: 26 additions & 1 deletion src/viteBuildStatsPlugin.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,17 @@
import type { ViteBuildData } from './types';
import { type Plugin } from 'vite';
import { NormalizedOutputOptions, OutputBundle } from 'rollup';
import { Blob } from 'node:buffer';

import type { ViteBuildData, ViteBundleStats } from './types';
import { getCommonMetadata, sendBuildData } from './common';

export function viteBuildStatsPlugin(
customIdentifier: string | undefined = process.env.npm_lifecycle_event,
bootstrapBundleSizeLimitKb?: number,
): Plugin {
let buildStart: number;
let buildEnd: number;
let bootstrapChunkSizeBytes: number | undefined = undefined;
let rollupVersion: string | undefined = undefined;

return {
Expand All @@ -18,11 +23,31 @@ export function viteBuildStatsPlugin(
buildEnd: function () {
buildEnd = Date.now();
},
generateBundle: (
outputOptions: NormalizedOutputOptions,
outputBundle: OutputBundle
) => {
try {
for (const [_, bundle] of Object.entries(outputBundle)) {
if (bundle.name === 'bootstrap' && bundle.type === 'chunk') {
bootstrapChunkSizeBytes = new Blob([bundle.code]).size;
}
}
} catch (err) {
console.warn('Failed to measure bootstrap chunk size because of error', err)
}
},
closeBundle: async function () {
const bundleStats: ViteBundleStats = {
bootstrapChunkSizeBytes: bootstrapChunkSizeBytes,
bootstrapChunkSizeLimitBytes: bootstrapBundleSizeLimitKb != null ? bootstrapBundleSizeLimitKb * 1000 : undefined,
}

const buildStats: ViteBuildData = {
...getCommonMetadata(buildEnd - buildStart, customIdentifier),
type: 'vite',
viteVersion: rollupVersion ?? null,
bundleStats,
};

sendBuildData(buildStats);
Expand Down
45 changes: 45 additions & 0 deletions tests/utils/test-data-generator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { OutputAsset, OutputBundle, OutputChunk } from "rollup";

export function generateViteOutputBundleData(includeBootstrapChunk: boolean): OutputBundle {
const bootstrapChunk: OutputChunk = {
name: 'bootstrap',
type: 'chunk',
code: 'console.log("Bygone visions of life asunder, long since quelled by newfound wonder.");' +
'console.log("Bygone visions of life asunder, long since quelled by newfound wonder.");' +
'console.log("Bygone visions of life asunder, long since quelled by newfound wonder.");' +
'console.log("Bygone visions of life asunder, long since quelled by newfound wonder.");' +
'console.log("Bygone visions of life asunder, long since quelled by newfound wonder.");'
} as unknown as OutputChunk;

const nonBootstrapChunkA: OutputChunk = {
name: 'notBootstrapA',
type: 'chunk',
code: 'random sentence to pass my time'
} as unknown as OutputChunk;

const nonBootstrapChunkB: OutputChunk = {
name: 'notBootstrapB',
type: 'chunk',
code: 'random sentence to pass my time again'
} as unknown as OutputChunk;

const assetNamedBootstrap: OutputAsset = {
name: 'bootstrap',
fileName: 'bootstrap.jpg',
type: 'asset',
source: '../../assets/bootstrap.jpg',
needsCodeReference: false
};

const bundle = {
'a-not-bootstrap.js': nonBootstrapChunkA,
'b-not-bootstrap.js': nonBootstrapChunkB,
'bootstrap.jpg': assetNamedBootstrap
}

if (includeBootstrapChunk) {
bundle['bootstrap.xyz123.js'] = bootstrapChunk
}

return bundle;
}
54 changes: 50 additions & 4 deletions tests/viteBuildStatsPlugin.spec.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import { NormalizedOutputOptions, OutputBundle } from "rollup";

import { viteBuildStatsPlugin } from '../src';
import type { CommonMetadata, ViteBuildData } from '../src/types';
import { viteBuildStatsPlugin } from '../src/viteBuildStatsPlugin';
import { getCommonMetadata, sendBuildData } from '../src/common';
import { generateViteOutputBundleData } from "./utils/test-data-generator";

jest.mock('../src/common', () => ({
getCommonMetadata: jest.fn(),
Expand All @@ -13,16 +16,21 @@ const mockedGetCommonMetadata = getCommonMetadata as jest.MockedFunction<
const mockedSendBuildData = sendBuildData as jest.MockedFunction<typeof sendBuildData>;

describe('viteBuildStatsPlugin', () => {
const bootstrapChunkSizeLimitKb = 2_000;
const expected: ViteBuildData = {
type: 'vite',
viteVersion: '1.2.3',
bundleStats: {
bootstrapChunkSizeBytes: 430,
bootstrapChunkSizeLimitBytes: bootstrapChunkSizeLimitKb * 1000,
}
} as ViteBuildData;

beforeEach(() => {
jest.resetAllMocks();
});

it('should send the correct data', async () => {
it('should send the correct data - happy path', async () => {
// mock measurement
global.Date = {
now: jest.fn().mockReturnValueOnce(0).mockReturnValueOnce(100),
Expand All @@ -31,14 +39,45 @@ describe('viteBuildStatsPlugin', () => {
// mock common utils
mockedGetCommonMetadata.mockReturnValue({} as CommonMetadata);
mockedSendBuildData.mockReturnValue(Promise.resolve());
const bundle = generateViteOutputBundleData(true);

const plugin = viteBuildStatsPlugin('my custom identifier', bootstrapChunkSizeLimitKb);
(plugin.buildStart as () => void).bind({ meta: { rollupVersion: '1.2.3' } })();
(plugin.generateBundle as (opts: NormalizedOutputOptions, bundle: OutputBundle) => void)({} as NormalizedOutputOptions, bundle);
(plugin.buildEnd as () => void)();
await (plugin.closeBundle as () => Promise<void>)();

expect(mockedGetCommonMetadata).toBeCalledWith(100, 'my custom identifier');
expect(mockedSendBuildData).toBeCalledWith(expected);
});

it('should send the correct data - bootstrap chunk not found', async () => {
// mock measurement
global.Date = {
now: jest.fn().mockReturnValueOnce(0).mockReturnValueOnce(100),
} as unknown as typeof Date;

// mock common utils
mockedGetCommonMetadata.mockReturnValue({} as CommonMetadata);
mockedSendBuildData.mockReturnValue(Promise.resolve());
const bundle = generateViteOutputBundleData(false);

const plugin = viteBuildStatsPlugin('my custom identifier');
(plugin.buildStart as () => void).bind({ meta: { rollupVersion: '1.2.3' } })();
(plugin.generateBundle as (opts: NormalizedOutputOptions, bundle: OutputBundle) => void)({} as NormalizedOutputOptions, bundle);
(plugin.buildEnd as () => void)();
await (plugin.closeBundle as () => Promise<void>)();

const caseSpecificExpected = {
...expected,
bundleStats: {
generateOutputBundleData: undefined,
bootstrapChunkSizeLimitBytes: undefined
}
}

expect(mockedGetCommonMetadata).toBeCalledWith(100, 'my custom identifier');
expect(mockedSendBuildData).toBeCalledWith(expect.objectContaining(expected));
expect(mockedSendBuildData).toBeCalledWith(caseSpecificExpected);
});

it('should use process.env.npm_lifecycle_event as default custom identifier', async () => {
Expand All @@ -50,6 +89,7 @@ describe('viteBuildStatsPlugin', () => {
// mock common utils
mockedGetCommonMetadata.mockReturnValue({} as CommonMetadata);
mockedSendBuildData.mockReturnValue(Promise.resolve());
const bundle = generateViteOutputBundleData(true);

// mock process object
global.process = {
Expand All @@ -60,10 +100,16 @@ describe('viteBuildStatsPlugin', () => {

const plugin = viteBuildStatsPlugin();
(plugin.buildStart as () => void).bind({ meta: { rollupVersion: '1.2.3' } })();
(plugin.generateBundle as (opts: NormalizedOutputOptions, bundle: OutputBundle) => void)({} as NormalizedOutputOptions, bundle);
(plugin.buildEnd as () => void)();
await (plugin.closeBundle as () => Promise<void>)();

const caseSpecificExpected = {
...expected,
bundleStats: { ...expected.bundleStats, bootstrapChunkSizeLimitBytes: undefined }
}

expect(mockedGetCommonMetadata).toBeCalledWith(100, 'default_value');
expect(mockedSendBuildData).toBeCalledWith(expect.objectContaining(expected));
expect(mockedSendBuildData).toBeCalledWith(caseSpecificExpected);
});
});

0 comments on commit bb52189

Please sign in to comment.