Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

FEATURE: Send vite bundle stats #16

Merged
merged 10 commits into from
Nov 1, 2024
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);
});
});
Loading