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

Add bundle size stats #6

Open
wants to merge 13 commits into
base: master
Choose a base branch
from
55 changes: 0 additions & 55 deletions BUILD_TIME.md

This file was deleted.

66 changes: 65 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,4 +24,68 @@ You might need to add this file to `.gitignore` to avoid committing it to the re
### Build Time (Compilation Time)

This package supports collecting the build time (compilation time) of projects that are using Webpack (4.x or 5.x) or Vite (4.x).
Follow this [instruction](BUILD_TIME.md) to get started.

### Bundle Size

This package also supports collecting the bundle size of your project. This helps in understanding the size of the final output and optimizing it for better performance.

## Usage

### Configuration

You can define an endpoint in the environment variable and the stats data will be sent there via HTTP POST Request

| Environment Variable | Default Value |
| -------------------- | ------------------------------------ |
| WEBPACK_ENDPOINT | <http://compilation-metrics/webpack> |
| VITE_ENDPOINT | <http://compilation-metrics/vite> |

### Basic usage

#### Webpack

If you use **Webpack**, you can add the following to your `webpack.config.js` file:

```javascript
const { WebpackBuildStatsPlugin } = require('agoda-devfeedback');

module.exports = {
// ...
plugins: [
// ...
new WebpackBuildStatsPlugin(),
],
};
```

#### Vite

If you use **Vite** you can add the following to your `vite.config.js` file:

```javascript
import { viteBuildStatsPlugin } from 'agoda-devfeedback';

export default defineConfig({
// ...
plugins: [
// ...
viteBuildStatsPlugin(),
],
});
```

### Advanced usage

Both Webpack and Vite plugins will not only send the build data but also send the command that you used to run the build like `yarn dev` or `yarn build` to be the build identifier which should work in most cases in order to help you distinguish between different build configurations.

However, if you would like to define your own identifier, you can do so by passing it as a parameter to the plugin.

```javascript
new WebpackBuildStatsPlugin('production');
```

or

```javascript
viteBuildStatsPlugin('production');
```
17 changes: 17 additions & 0 deletions src/WebpackBuildStatsPlugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,26 @@ import type { Compiler, Stats, StatsCompilation } from 'webpack';

export class WebpackBuildStatsPlugin {
private readonly customIdentifier: string | undefined;
private bundleFiles: Record<string, number> = {};

constructor(customIdentifier: string | undefined = process.env.npm_lifecycle_event) {
this.customIdentifier = customIdentifier;
}

apply(compiler: Compiler) {
compiler.hooks.emit.tapAsync('AgodaBuildStatsPlugin', (compilation, callback) => {
this.bundleFiles = {};

for (const assetName in compilation.assets) {
if (compilation.assets.hasOwnProperty(assetName)) {
const asset = compilation.assets[assetName];
this.bundleFiles[assetName] = asset.size();
}
}

callback();
});

compiler.hooks.done.tap('AgodaBuildStatsPlugin', async (stats: Stats) => {
const jsonStats: StatsCompilation = stats.toJson();

Expand All @@ -19,6 +34,8 @@ export class WebpackBuildStatsPlugin {
webpackVersion: jsonStats.version ?? null,
nbrOfCachedModules: jsonStats.modules?.filter((m) => m.cached).length ?? 0,
nbrOfRebuiltModules: jsonStats.modules?.filter((m) => m.built).length ?? 0,
bundleFiles: this.bundleFiles ?? {},
bundleSize: Object.values(this.bundleFiles).reduce((total, size) => total + size, 0) ?? 0,
};

sendBuildData(buildStats);
Expand Down
27 changes: 17 additions & 10 deletions src/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,11 +56,23 @@ export const getCommonMetadata = (
};
};

const getEndpointFromType = (type: string) => {
return {
webpack: process.env.WEBPACK_ENDPOINT,
vite: process.env.VITE_ENDPOINT,
}[type];
const getEndpointFromType = (type: WebpackBuildData['type'] | ViteBuildData['type']): string => {
const endpointsFromEnv: Record<typeof type, string> = {
webpack: process.env.WEBPACK_ENDPOINT ?? '',
vite: process.env.VITE_ENDPOINT ?? '',
};

const defaultEndpoints: Record<typeof type, string> = {
webpack: "http://compilation-metrics/webpack",
vite: "http://compilation-metrics/vite",
}

if (endpointsFromEnv[type] === '') {
console.warn(`No endpoint found for type "${type}" from environment variable, using default: ${defaultEndpoints[type]}`);
return defaultEndpoints[type];
}

return endpointsFromEnv[type];
};

const LOG_FILE = 'devfeedback.log';
Expand All @@ -77,11 +89,6 @@ const sendData = async (endpoint: string, data: CommonMetadata): Promise<boolean
export const sendBuildData = async (buildStats: WebpackBuildData | ViteBuildData) => {
const endpoint = getEndpointFromType(buildStats.type);

if (!endpoint) {
console.log(`No endpoint found for type ${buildStats.type}. Please set the environment variable.`);
return;
}

console.log(`Your build time was ${buildStats.timeTaken.toFixed(2)}ms.`);

const sent = await sendData(endpoint, buildStats);
Expand Down
4 changes: 4 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,13 @@ export interface WebpackBuildData extends CommonMetadata {
compilationHash: string | null;
nbrOfCachedModules: number;
nbrOfRebuiltModules: number;
bundleFiles: Record<string, number>;
bundleSize: number;
}

export interface ViteBuildData extends CommonMetadata {
type: 'vite';
viteVersion: string | null;
bundleFiles: Record<string, number>;
bundleSize: number;
}
16 changes: 16 additions & 0 deletions src/viteBuildStatsPlugin.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
import type { ViteBuildData } from './types';
import { type Plugin } from 'vite';
import { getCommonMetadata, sendBuildData } from './common';
import { promises as fs } from 'fs';
import path from 'path';

export function viteBuildStatsPlugin(
customIdentifier: string | undefined = process.env.npm_lifecycle_event,
): Plugin {
let buildStart: number;
let buildEnd: number;
let rollupVersion: string | undefined = undefined;
let bundleFiles: Record<string, number> = {};

return {
name: 'vite-plugin-agoda-build-reporter',
Expand All @@ -18,11 +21,24 @@ export function viteBuildStatsPlugin(
buildEnd: function () {
buildEnd = Date.now();
},
writeBundle: async function (options, bundle) {
for (const [fileName, assetInfo] of Object.entries(bundle)) {
const filePath = path.join(options.dir || '', fileName);
try {
const stats = await fs.stat(filePath);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How much slower has this plugin become? Did you do any benchmarks?

bundleFiles[fileName] = stats.size;
} catch (err) {
console.error(`Error reading file size for ${fileName}:`, err);
}
}
},
closeBundle: async function () {
const buildStats: ViteBuildData = {
...getCommonMetadata(buildEnd - buildStart, customIdentifier),
type: 'vite',
viteVersion: rollupVersion ?? null,
bundleFiles,
bundleSize: Object.values(bundleFiles).reduce((total, size) => total + size, 0),
};

sendBuildData(buildStats);
Expand Down
44 changes: 43 additions & 1 deletion tests/WebpackBuildStatsPlugin.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@ const mockedSendBuildData = sendBuildData as jest.MockedFunction<typeof sendBuil

const mockedCompiler = {
hooks: {
emit: {
tapAsync: jest.fn(),
},
done: {
tap: jest.fn(),
},
Expand All @@ -28,7 +31,12 @@ describe('WebpackBuildStatsPlugin', () => {
compilationHash: 'blahblahblacksheep',
nbrOfCachedModules: 1,
nbrOfRebuiltModules: 1,
} as WebpackBuildData;
bundleFiles: {
'file1.js': 1000,
'file2.js': 2000,
},
bundleSize: 3000,
} as unknown as WebpackBuildData;

beforeEach(() => {
jest.resetAllMocks();
Expand All @@ -52,9 +60,22 @@ describe('WebpackBuildStatsPlugin', () => {
}),
};

// mock compilation
const mockedCompilation = {
assets: {
'file1.js': { size: () => 1000 },
'file2.js': { size: () => 2000 },
},
};

const plugin = new WebpackBuildStatsPlugin('my custom identifier');
plugin.apply(mockedCompiler as unknown as Compiler);

// simulate emit hook
const emitCallback = mockedCompiler.hooks.emit.tapAsync.mock.calls[0][1];
await emitCallback(mockedCompilation, () => { });

// simulate done hook
const callback = mockedCompiler.hooks.done.tap.mock.calls[0][1];
await callback(mockedStats as unknown as import('webpack').Stats);

Expand All @@ -76,6 +97,14 @@ describe('WebpackBuildStatsPlugin', () => {
}),
};

// mock compilation
const mockedCompilation = {
assets: {
'file1.js': { size: () => 1000 },
'file2.js': { size: () => 2000 },
},
};

// mock process object
global.process = {
env: {
Expand All @@ -86,9 +115,22 @@ describe('WebpackBuildStatsPlugin', () => {
const plugin = new WebpackBuildStatsPlugin();
plugin.apply(mockedCompiler as unknown as Compiler);

// simulate emit hook
const emitCallback = mockedCompiler.hooks.emit.tapAsync.mock.calls[0][1];
await emitCallback(mockedCompilation, () => { });

// simulate done hook
const callback = mockedCompiler.hooks.done.tap.mock.calls[0][1];
await callback(mockedStats as unknown as import('webpack').Stats);

expect(mockedGetCommonMetadata).toBeCalledWith(123, 'default_value');
expect(mockedSendBuildData).toBeCalledWith(expect.objectContaining({
...expected,
bundleFiles: {
'file1.js': 1000,
'file2.js': 2000,
},
bundleSize: 3000,
}));
});
});
Loading
Loading