Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 0 additions & 20 deletions .github/workflows/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,26 +7,6 @@ on:
branches: [ master ]

jobs:
# Oldest maintenance LTS, End-of-Life 2025-04-30
test-node-18:
runs-on: ubuntu-latest
container:
image: node:18.19
env:
NODE_OPTIONS: --openssl-legacy-provider
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set up Node.js
run: |
echo "Node version: $(node --version)"
echo "NPM version: $(npm --version)"
npm install
npm run ci
- name: Unset NODE_OPTIONS
run: |
unset NODE_OPTIONS

test:
runs-on: ubuntu-latest
strategy:
Expand Down
106 changes: 54 additions & 52 deletions lib/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,8 @@ function getAssetPath(compilation, name) {
return path.join(compilation.getPath(compilation.compiler.outputPath), name.split('?')[0]);
}

function getSource(compilation, name) {
const path = getAssetPath(compilation, name);
return fs.readFileSync(path, { encoding: 'utf-8' });
function getSource(asset) {
return asset.source.source();
}

/**
Expand Down Expand Up @@ -49,13 +48,14 @@ class BundleTrackerPlugin {
/** @type {Options} */
this.options = options;
/** @type {Contents} */
this.contents = {
this.output = {
status: 'initialization',
assets: {},
chunks: {},
};
this.name = 'BundleTrackerPlugin';

this.assets = {};
this.outputChunkDir = '';
this.outputTrackerFile = '';
this.outputTrackerDir = '';
Expand Down Expand Up @@ -103,22 +103,19 @@ class BundleTrackerPlugin {
}
/**
* Write bundle tracker stats file
*
* @param {Compiler} _compiler
* @param {Partial<Contents>} contents
*/
_writeOutput(_compiler, contents) {
Object.assign(this.contents, contents, {
assets: mergeObjectsAndSortKeys(this.contents.assets, contents.assets),
chunks: mergeObjectsAndSortKeys(this.contents.chunks, contents.chunks),
_writeOutput(contents) {
Object.assign(this.output, contents, {
assets: mergeObjectsAndSortKeys(this.output.assets, contents.assets),
chunks: mergeObjectsAndSortKeys(this.output.chunks, contents.chunks),
});

if (this.options.publicPath) {
this.contents.publicPath = this.options.publicPath;
this.output.publicPath = this.options.publicPath;
}

fs.mkdirSync(this.outputTrackerDir, { recursive: true, mode: 0o755 });
fs.writeFileSync(this.outputTrackerFile, JSON.stringify(this.contents, null, this.options.indent));
fs.writeFileSync(this.outputTrackerFile, JSON.stringify(this.output, null, this.options.indent));
}
/**
* Compute hash for a content
Expand All @@ -140,44 +137,25 @@ class BundleTrackerPlugin {
}
/**
* Handle compile hook
* @param {Compiler} compiler
*/
_handleCompile(compiler) {
this._writeOutput(compiler, { status: 'compile' });
_handleCompile() {
this._writeOutput({ status: 'compile' });
}

/**
* Handle compile hook
* @param {Compiler} compiler
* @param {Stats} stats
* Hook to handle the assets when they are ready to be emitted
* @param {Compilation} compilation
*/
_handleDone(compiler, stats) {
if (stats.hasErrors()) {
const findError = compilation => {
if (compilation.errors.length > 0) {
return compilation.errors[0];
}
return compilation.children.find(child => findError(child));
};
const error = findError(stats.compilation);
this._writeOutput(compiler, {
status: 'error',
error: error?.name ?? 'unknown-error',
message: stripAnsi(error['message']),
});

return;
}

/** @type {Contents} */
const output = { status: 'done', assets: {}, chunks: {} };
Object.entries(stats.compilation.assets).map(([assetName, _]) => {
_handleEmit(compilation) {
Object.keys(compilation.assets).forEach(assetName => {
const fileInfo = {
name: assetName,
path: getAssetPath(stats.compilation, assetName),
path: getAssetPath(compilation, assetName),
};

if (this.options.integrity === true) {
fileInfo.integrity = this._computeIntegrity(getSource(stats.compilation, assetName));
const asset = compilation.getAsset(assetName);
fileInfo.integrity = this._computeIntegrity(getSource(asset));
}

if (this.options.publicPath) {
Expand All @@ -193,35 +171,59 @@ class BundleTrackerPlugin {
}

// @ts-ignore: TS2339: Property 'assetsInfo' does not exist on type 'Compilation'.
if (stats.compilation.assetsInfo) {
if (compilation.assetsInfo) {
// @ts-ignore: TS2339: Property 'assetsInfo' does not exist on type 'Compilation'.
fileInfo.sourceFilename = stats.compilation.assetsInfo.get(assetName).sourceFilename;
fileInfo.sourceFilename = compilation.assetsInfo.get(assetName).sourceFilename;
}

output.assets[assetName] = fileInfo;
this.assets[assetName] = fileInfo;
});
}

/**
* Handle done hook and write output file
* @param {Stats} stats
*/
_handleDone(stats) {
if (stats.hasErrors()) {
const findError = compilation => {
if (compilation.errors.length > 0) {
return compilation.errors[0];
}
return compilation.children.find(child => findError(child));
};
const error = findError(stats.compilation);
this._writeOutput({
status: 'error',
error: error?.name ?? 'unknown-error',
message: stripAnsi(error['message']),
});
return;
}

const chunks = {};
stats.compilation.chunkGroups.forEach(chunkGroup => {
if (!chunkGroup.isInitial()) return;

output.chunks[chunkGroup.name] = chunkGroup.getFiles();
chunks[chunkGroup.name] = chunkGroup.getFiles();
});

const output = { status: 'done', chunks, assets: this.assets };
if (this.options.logTime === true) {
output.startTime = stats.startTime;
output.endTime = stats.endTime;
}

this._writeOutput(compiler, output);
this._writeOutput(output);
}

/**
* Method called by webpack to apply plugin hook
* @param {Compiler} compiler
*/
apply(compiler) {
this._setParamsFromCompiler(compiler);

compiler.hooks.compile.tap(this.name, this._handleCompile.bind(this, compiler));
compiler.hooks.done.tap(this.name, this._handleDone.bind(this, compiler));
compiler.hooks.compile.tap(this.name, this._handleCompile.bind(this));
compiler.hooks.emit.tap(this.name, this._handleEmit.bind(this));
Copy link
Contributor Author

Choose a reason for hiding this comment

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

The emit hook was the only place where we could always access the full list of compiled assets and could access their in-memory contents.

Other hooks tried:

  1. done: we could not access the in-memory content of the files
  2. compilation.processAssets: we do not have to all compiled assets, broke tests where we compressed them
  3. assetEmitted: we have access to all files + their in-memory content, however it's not always called without changing the plugin settings, so ended up causing the stats file to have missing files when not used in a dev server environment.

compiler.hooks.done.tap(this.name, this._handleDone.bind(this));
}
}

Expand Down
5 changes: 4 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "webpack-bundle-tracker",
"version": "3.2.1",
"version": "3.2.2",
"description": "Spits out some stats about webpack compilation process to a file",
"keywords": [
"bundle",
Expand All @@ -11,6 +11,9 @@
],
"homepage": "https://github.com/django-webpack/webpack-bundle-tracker",
"bugs": "https://github.com/django-webpack/webpack-bundle-tracker/issues",
"engines": {
"node": ">=20.0.0"
},
"repository": {
"type": "git",
"url": "git+https://github.com/django-webpack/webpack-bundle-tracker.git"
Expand Down
81 changes: 81 additions & 0 deletions tests/base.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,87 @@ describe('BundleTrackerPlugin bases tests', () => {
);
});

it('It should generate the stats file when the plugin runs twice and the output assets already exist', done => {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This test ensures that even if an asset didn't change between builds, the stats file will always contain its correct info.

const expectErrors = null;
const expectWarnings = getWebpack4WarningMessage();

// 1st run
testPlugin(
webpack,
{
context: __dirname,
entry: path.resolve(__dirname, 'fixtures', 'index.js'),
output: {
path: OUTPUT_DIR,
filename: 'js/[name].js',
publicPath: 'http://localhost:3000/assets/',
},
plugins: [
new BundleTrackerPlugin({
path: OUTPUT_DIR,
filename: 'webpack-stats.json',
}),
],
},
{
status: 'done',
publicPath: 'http://localhost:3000/assets/',
chunks: {
main: ['js/main.js'],
},
assets: {
'js/main.js': {
name: 'js/main.js',
path: OUTPUT_DIR + '/js/main.js',
publicPath: 'http://localhost:3000/assets/js/main.js',
},
},
},
'webpack-stats.json',
jest.fn(),
expectErrors,
expectWarnings,
);

// 2nd run
testPlugin(
webpack,
{
context: __dirname,
entry: path.resolve(__dirname, 'fixtures', 'index.js'),
output: {
path: OUTPUT_DIR,
filename: 'js/[name].js',
publicPath: 'http://localhost:3000/assets/',
},
plugins: [
new BundleTrackerPlugin({
path: OUTPUT_DIR,
filename: 'webpack-stats.json',
}),
],
},
{
status: 'done',
publicPath: 'http://localhost:3000/assets/',
chunks: {
main: ['js/main.js'],
},
assets: {
'js/main.js': {
name: 'js/main.js',
path: OUTPUT_DIR + '/js/main.js',
publicPath: 'http://localhost:3000/assets/js/main.js',
},
},
},
'webpack-stats.json',
done,
expectErrors,
expectWarnings,
);
});

it('It should add log time when option is set', done => {
const expectErrors = null;
const expectWarnings = getWebpack4WarningMessage();
Expand Down
Loading