Skip to content

Commit

Permalink
Merge pull request #243 from relative-ci/stats-options
Browse files Browse the repository at this point in the history
Stats options
  • Loading branch information
vio authored Jun 10, 2024
2 parents 97398d8 + d37fe30 commit ebea444
Show file tree
Hide file tree
Showing 8 changed files with 527 additions and 203 deletions.
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,9 @@ export default defineConfig((env) => ({
### Options

- `fileName` - JSON stats file inside rollup/vite output directory
- `excludeAssets` - exclude matching assets: `string | RegExp | ((filepath: string) => boolean) | Array<string | RegExp | ((filepath: string) => boolean)>`
- `excludeModules` - exclude matching modules: `string | RegExp | ((filepath: string) => boolean) | Array<string | RegExp | ((filepath: string) => boolean)>`


## Resources

Expand Down
9 changes: 9 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { Plugin, OutputOptions } from 'rollup';

import type { ExcludeFilepathOption } from './types';
import { BundleTransformOptions, bundleToWebpackStats } from './transform';

export { bundleToWebpackStats } from './transform';
Expand All @@ -12,6 +13,14 @@ interface WebpackStatsOptions extends BundleTransformOptions {
* default: webpack-stats.json
*/
fileName?: string;
/**
* Exclude matching assets
*/
excludeAssets?: ExcludeFilepathOption;
/**
* Exclude matching modules
*/
excludeModules?: ExcludeFilepathOption;
}

type WebpackStatsOptionsOrBuilder =
Expand Down
56 changes: 25 additions & 31 deletions src/transform.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import crypto from 'crypto';
import path from 'path';
import { OutputBundle, OutputChunk } from 'rollup';
import type { OutputBundle } from 'rollup';

const HASH_LENGTH = 7;
import type { ExcludeFilepathOption } from "./types";
import { checkExcludeFilepath, getByteSize, getChunkId } from "./utils";

// https://github.com/relative-ci/bundle-stats/blob/master/packages/plugin-webpack-filter/src/index.ts
export type WebpackStatsFilteredAsset = {
Expand Down Expand Up @@ -42,47 +42,29 @@ export interface WebpackStatsFiltered {
modules?: Array<WebpackStatsFilteredRootModule>;
}

const getByteSize = (content: string | Buffer): number => {
if (typeof content === 'string') {
return Buffer.from(content).length;
}

return content?.length || 0;
};


const getHash = (text: string): string => {
const digest = crypto.createHash('sha256');
return digest.update(Buffer.from(text)).digest('hex').substr(0, HASH_LENGTH);
};

const getChunkId = (chunk: OutputChunk): string => {
let value = chunk.name;

// Use entry module relative path
if (chunk.moduleIds?.length > 0) {
const absoluteModulePath = chunk.moduleIds[chunk.moduleIds.length - 1];
value = path.relative(process.cwd(), absoluteModulePath);
}

return getHash([chunk, value].join('-'));
}

export type BundleTransformOptions = {
/**
* Extract module original size or rendered size
* default: false
*/
moduleOriginalSize?: boolean;
/**
* Exclude asset
*/
excludeAssets?: ExcludeFilepathOption;
/**
* Exclude module
*/
excludeModules?: ExcludeFilepathOption;
};

export const bundleToWebpackStats = (
bundle: OutputBundle,
customOptions?: BundleTransformOptions
pluginOptions?: BundleTransformOptions
): WebpackStatsFiltered => {
const options = {
moduleOriginalSize: false,
...customOptions,
...pluginOptions,
};

const items = Object.values(bundle);
Expand All @@ -94,6 +76,10 @@ export const bundleToWebpackStats = (

items.forEach(item => {
if (item.type === 'chunk') {
if (checkExcludeFilepath(item.fileName, options.excludeAssets)) {
return;
}

assets.push({
name: item.fileName,
size: getByteSize(item.code),
Expand All @@ -110,6 +96,10 @@ export const bundleToWebpackStats = (
});

Object.entries(item.modules).forEach(([modulePath, moduleInfo]) => {
if (checkExcludeFilepath(modulePath, options.excludeModules)) {
return;
}

// Remove unexpected rollup null prefix
const normalizedModulePath = modulePath.replace('\u0000', '');

Expand Down Expand Up @@ -138,6 +128,10 @@ export const bundleToWebpackStats = (
}
});
} else if (item.type === 'asset') {
if (checkExcludeFilepath(item.fileName, options.excludeAssets)) {
return;
}

assets.push({
name: item.fileName,
size: getByteSize(item.source.toString()),
Expand Down
3 changes: 3 additions & 0 deletions src/types.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export type ExcludeFilepathParam = string | RegExp | ((filepath: string) => boolean);

export type ExcludeFilepathOption = ExcludeFilepathParam | Array<ExcludeFilepathParam>;
74 changes: 74 additions & 0 deletions src/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import path from 'path';
import crypto from 'crypto';
import type { OutputChunk } from 'rollup';

import type { ExcludeFilepathOption } from './types';

const HASH_LENGTH = 7;

/**
* Get content byte size
*/
export function getByteSize(content: string | Buffer): number {
if (typeof content === 'string') {
return Buffer.from(content).length;
}

return content?.length || 0;
}

/**
* Generate a 7 chars hash from a filepath
*/
export function getHash(filepath: string): string {
const digest = crypto.createHash('sha256');
return digest.update(Buffer.from(filepath)).digest('hex').substr(0, HASH_LENGTH);
}

export function getChunkId(chunk: OutputChunk): string {
let value = chunk.name;

// Use entry module relative path
if (chunk.moduleIds?.length > 0) {
const absoluteModulePath = chunk.moduleIds[chunk.moduleIds.length - 1];
value = path.relative(process.cwd(), absoluteModulePath);
}

return getHash([chunk, value].join('-'));
}

/**
* Check if filepath should be excluded based on a config
*/
export function checkExcludeFilepath(
filepath: string,
option?: ExcludeFilepathOption,
): boolean {
if (!option) {
return false;
}

if (Array.isArray(option)) {
let res = false;

for (let i = 0; i <= option.length - 1 && res === false; i++) {
res = checkExcludeFilepath(filepath, option[i]);
}

return res;
}

if (typeof option === 'function') {
return option(filepath);
}

if (typeof option === 'string') {
return Boolean(filepath.match(option));
}

if ('test' in option) {
return option.test(filepath);
}

return false;
}
176 changes: 176 additions & 0 deletions test/unit/fixtures/rollup-bundle-stats.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
import path from 'path';
import type { OutputBundle } from 'rollup';

const ROOT_DIR = path.join(__dirname, '../../../');

export default {
'assets/logo-abcd1234.svg': {
name: undefined,
fileName: 'assets/logo-abcd1234.svg',
type: 'asset',
source: '<svg></svg>',
needsCodeReference: true,
},
'assets/main-abcd1234.js': {
name: 'main',
fileName: 'assets/main-abcd1234.js',
preliminaryFileName: 'assets/main-abcd1234.js',
sourcemapFileName: 'assets/main-abcd1234.js.map',
type: 'chunk',
code: 'export default function () {}',
isEntry: true,
isDynamicEntry: false,
facadeModuleId: null,
map: null,
isImplicitEntry: false,
implicitlyLoadedBefore: [],
importedBindings: {},
referencedFiles: [],
moduleIds: [
path.join(ROOT_DIR, 'src/component-a.js'),
path.join(ROOT_DIR, 'src/index.js'),
],
modules: {
[path.join(ROOT_DIR, 'src/component-a.js')]: {
code: 'export default A = 1;',
originalLength: 10,
renderedLength: 8,
removedExports: [],
renderedExports: [],
},
[path.join(ROOT_DIR, 'src/index.js')]: {
code: '',
originalLength: 100,
renderedLength: 80,
removedExports: [],
renderedExports: [],
},
},
imports: [],
exports: [],
dynamicImports: [],
},
'assets/vendors-abcd1234.js': {
name: 'vendors',
fileName: 'assets/vendors-abcd1234.js',
preliminaryFileName: 'assets/vendors-abcd1234.js',
sourcemapFileName: 'assets/vendors-abcd1234.js.map',
type: 'chunk',
code: 'export default function () {}',
isEntry: true,
isDynamicEntry: false,
facadeModuleId: null,
map: null,
isImplicitEntry: false,
implicitlyLoadedBefore: [],
importedBindings: {},
referencedFiles: [],
moduleIds: [
path.join(ROOT_DIR, 'node_modules', 'package-a', 'index.js'),
path.join(ROOT_DIR, 'node_modules', 'package-b', 'index.js'),
],
modules: {
[path.join(
ROOT_DIR,
'node_modules',
'package-a',
'index.js'
)]: {
code: '',
originalLength: 10,
renderedLength: 8,
removedExports: [],
renderedExports: [],
},
[path.join(
ROOT_DIR,
'node_modules',
'package-b',
'index.js'
)]: {
code: '',
originalLength: 100,
renderedLength: 80,
removedExports: [],
renderedExports: [],
},
},
imports: [],
exports: [],
dynamicImports: [],
},
'assets/index-abcd1234.js': {
name: 'index',
fileName: 'assets/index-abcd1234.js',
preliminaryFileName: 'assets/index-abcd1234.js',
sourcemapFileName: 'assets/index-abcd1234.js.map',
type: 'chunk',
code: 'export default function () {}',
isEntry: false,
isDynamicEntry: true,
facadeModuleId: null,
map: null,
isImplicitEntry: false,
implicitlyLoadedBefore: [],
importedBindings: {},
referencedFiles: [],
moduleIds: [
path.join(ROOT_DIR, 'src', 'components/component-b/index.js'),
],
modules: {
[path.join(
ROOT_DIR,
'src',
'components',
'component-b',
'index.js',
)]: {
code: '',
originalLength: 10,
renderedLength: 8,
removedExports: [],
renderedExports: [],
},
},
imports: [],
exports: [],
dynamicImports: [],
},
'assets/index-efab5678.js': {
name: 'index',
fileName: 'assets/index-efab5678.js',
preliminaryFileName: 'assets/index-efab5678.js',
sourcemapFileName: 'assets/index-efab5678.js.map',
type: 'chunk',
code: 'export default function () {}',
isEntry: false,
isDynamicEntry: true,
facadeModuleId: null,
map: null,
isImplicitEntry: false,
implicitlyLoadedBefore: [],
importedBindings: {},
referencedFiles: [],
moduleIds: [
path.join(ROOT_DIR, 'src', 'components/component-c/index.js'),
],
modules: {
[path.join(
ROOT_DIR,
'src',
'components',
'component-c',
'index.js',
)]: {
code: '',
originalLength: 10,
renderedLength: 8,
removedExports: [],
renderedExports: [],
},
},
imports: [],
exports: [],
dynamicImports: [],
},
} satisfies OutputBundle;
Loading

0 comments on commit ebea444

Please sign in to comment.