From 176c247fb94a38d448644c02b0a341c8a53b2698 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=A1szl=C3=B3=20K=C3=A1rolyi?= Date: Sun, 28 Jul 2024 18:43:50 +0200 Subject: [PATCH 1/4] Fix integrity calculation, fixes #126 --- lib/index.js | 122 ++++++++++++++++++++++++++++----------------------- 1 file changed, 66 insertions(+), 56 deletions(-) diff --git a/lib/index.js b/lib/index.js index 4e4c2d4..f402edf 100644 --- a/lib/index.js +++ b/lib/index.js @@ -3,12 +3,15 @@ /** @typedef {import("lodash.assign")} assign */ /** @typedef {import("lodash.get")} get */ /** @typedef {import("webpack").Compiler} Compiler */ +/** @typedef {import("webpack").AssetEmittedInfo} AssetEmittedInfo */ /** @typedef {import("webpack").Stats} Stats */ /** @typedef {import("webpack").compilation.Compilation} Compilation */ /** @typedef {import("webpack").compilation.ContextModuleFactory} ContextModuleFactory */ /** @typedef {import("webpack").ChunkData} ChunkData */ /** @typedef {import("../typings").Contents} Contents */ /** @typedef {import("../typings").Options} Options */ +/** @typedef {Contents['assets']} ContentsAssets */ +/** @typedef {ContentsAssets[keyof ContentsAssets]} ContentsAssetsValue */ const path = require('path'); const fs = require('fs'); @@ -26,11 +29,6 @@ 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' }); -} - /** * Merges the provided objects, ensuring that the resulting object has its properties in sorted order. * @template T @@ -61,6 +59,8 @@ class BundleTrackerPlugin { chunks: {}, }; this.name = 'BundleTrackerPlugin'; + /** @type {Contents} */ + this._iter_output = { status: 'compile', assets: {}, chunks: {} } this.outputChunkDir = ''; this.outputTrackerFile = ''; @@ -84,7 +84,10 @@ class BundleTrackerPlugin { integrityHashes: ['sha256', 'sha384', 'sha512'], }); - if (this.options.filename?.includes('/')) { + if ( + typeof this.options.filename === 'string' && + this.options.filename.includes('/') + ) { throw Error( "The `filename` shouldn't include a `/`. Please use the `path` parameter to " + "build the directory path and use `filename` only for the file's name itself.\n" + @@ -102,15 +105,14 @@ class BundleTrackerPlugin { return this; } /** - * Write bundle tracker stats file - * - * @param {Compiler} compiler - * @param {Partial} contents + * Write bundle tracker stats file, merging the existing content with + * the output from the latest compilation results, back to the + * `this.contents` variable. */ - _writeOutput(compiler, contents) { - assign(this.contents, contents, { - assets: mergeObjects(this.contents.assets, contents.assets), - chunks: mergeObjects(this.contents.chunks, contents.chunks), + _writeOutput() { + assign(this.contents, this._iter_output, { + assets: mergeObjects(this.contents.assets, this._iter_output.assets), + chunks: mergeObjects(this.contents.chunks, this._iter_output.chunks), }); if (this.options.publicPath) { @@ -143,8 +145,45 @@ class BundleTrackerPlugin { * @param {Compiler} compiler */ _handleCompile(compiler) { - this._writeOutput(compiler, { status: 'compile' }); + this._iter_output = { status: 'compile', assets: {}, chunks: {} } + this._writeOutput(); } + + /** + * Handle compile hook + * @param {string} compiledFile + * @param {AssetEmittedInfo} compiledDetails + */ + _handleAssetEmitted(compiledFile, compiledDetails) { + /** @type {ContentsAssetsValue} */ + const thisFile = { + name: compiledFile, + path: compiledDetails.targetPath + } + if (this.options.integrity === true) { + thisFile.integrity = this._computeIntegrity(compiledDetails.content); + } + if (this.options.publicPath) { + if (this.options.publicPath === 'auto') { + thisFile.publicPath = 'auto'; + } else { + thisFile.publicPath = this.options.publicPath + compiledFile; + } + } + if (this.options.relativePath === true) { + thisFile.path = path.relative(this.outputChunkDir, thisFile.path); + } + /** @type {Compilation} */ + const compilation = compiledDetails.compilation + // @ts-ignore: TS2339: Property 'assetsInfo' does not exist on type 'Compilation'. + if (compilation.assetsInfo) { + // @ts-ignore: TS2339: Property 'assetsInfo' does not exist on type 'Compilation'. + thisFile.sourceFilename = + compilation.assetsInfo.get(compiledFile).sourceFilename; + } + this._iter_output.assets[compiledFile] = thisFile + } + /** * Handle compile hook * @param {Compiler} compiler @@ -159,60 +198,29 @@ class BundleTrackerPlugin { return compilation.children.find(child => findError(child)); }; const error = findError(stats.compilation); - this._writeOutput(compiler, { + this._iter_output = { status: 'error', error: get(error, 'name', 'unknown-error'), message: stripAnsi(error['message']), - }); + assets: {}, chunks: {} + } + this._writeOutput(); return; } - - /** @type {Contents} */ - const output = { status: 'done', assets: {}, chunks: {} }; - each(stats.compilation.assets, (file, assetName) => { - const fileInfo = { - name: assetName, - path: getAssetPath(stats.compilation, assetName), - }; - - if (this.options.integrity === true) { - fileInfo.integrity = this._computeIntegrity(getSource(stats.compilation, assetName)); - } - - if (this.options.publicPath) { - if (this.options.publicPath === 'auto') { - fileInfo.publicPath = 'auto'; - } else { - fileInfo.publicPath = this.options.publicPath + assetName; - } - } - - if (this.options.relativePath === true) { - fileInfo.path = path.relative(this.outputChunkDir, fileInfo.path); - } - - // @ts-ignore: TS2339: Property 'assetsInfo' does not exist on type 'Compilation'. - if (stats.compilation.assetsInfo) { - // @ts-ignore: TS2339: Property 'assetsInfo' does not exist on type 'Compilation'. - fileInfo.sourceFilename = stats.compilation.assetsInfo.get(assetName).sourceFilename; - } - - output.assets[assetName] = fileInfo; - }); each(stats.compilation.chunkGroups, chunkGroup => { if (!chunkGroup.isInitial()) return; - - output.chunks[chunkGroup.name] = chunkGroup.getFiles(); + this._iter_output.chunks[chunkGroup.name] = chunkGroup.getFiles(); }); if (this.options.logTime === true) { - output.startTime = stats.startTime; - output.endTime = stats.endTime; + this._iter_output.startTime = stats.startTime; + this._iter_output.endTime = stats.endTime; } - - this._writeOutput(compiler, output); + this._iter_output.status = 'done' + this._writeOutput(); } + /** * Method called by webpack to apply plugin hook * @param {Compiler} compiler @@ -220,7 +228,9 @@ class BundleTrackerPlugin { apply(compiler) { this._setParamsFromCompiler(compiler); + // In order of hook calls: compiler.hooks.compile.tap(this.name, this._handleCompile.bind(this, compiler)); + compiler.hooks.assetEmitted.tap(this.name, this._handleAssetEmitted.bind(this)); compiler.hooks.done.tap(this.name, this._handleDone.bind(this, compiler)); } } From 9ea3b032ed78481269338ccc25dedbba00e9c715 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=A1szl=C3=B3=20K=C3=A1rolyi?= Date: Tue, 30 Jul 2024 19:36:02 +0200 Subject: [PATCH 2/4] Webpack 4 & 5 compatibility --- lib/index.js | 308 ++++++++++++++++++++++++++++++--------------------- package.json | 36 +++--- typings.d.ts | 92 +++++++++------ 3 files changed, 261 insertions(+), 175 deletions(-) diff --git a/lib/index.js b/lib/index.js index f402edf..87e3174 100644 --- a/lib/index.js +++ b/lib/index.js @@ -2,31 +2,42 @@ /** @typedef {import("lodash.defaults")} defaults */ /** @typedef {import("lodash.assign")} assign */ /** @typedef {import("lodash.get")} get */ -/** @typedef {import("webpack").Compiler} Compiler */ -/** @typedef {import("webpack").AssetEmittedInfo} AssetEmittedInfo */ -/** @typedef {import("webpack").Stats} Stats */ -/** @typedef {import("webpack").compilation.Compilation} Compilation */ -/** @typedef {import("webpack").compilation.ContextModuleFactory} ContextModuleFactory */ -/** @typedef {import("webpack").ChunkData} ChunkData */ +/** @typedef {import("webpack").Compiler} Compiler4 */ +/** @typedef {Compiler4['hooks']['assetEmitted']} assetEmitted4 */ +/** @typedef {import("webpack").Stats} Stats4 */ +/** @typedef {import("webpack").compilation.Compilation} Compilation4 */ +/** @typedef {import("webpack").ChunkData} ChunkData4 */ +/** @typedef {import("webpack5").Compiler} Compiler5 */ +/** @typedef {import("webpack5").AssetEmittedInfo} AssetEmittedInfo5 */ +/** @typedef {import("webpack5").Compilation} Compilation5 */ +/** @typedef {Compiler5['hooks']['assetEmitted']} assetEmitted5 */ /** @typedef {import("../typings").Contents} Contents */ +/** @typedef {import("../typings").ComputedOpts} ComputedOpts */ /** @typedef {import("../typings").Options} Options */ /** @typedef {Contents['assets']} ContentsAssets */ /** @typedef {ContentsAssets[keyof ContentsAssets]} ContentsAssetsValue */ +/** @typedef {Compilation4 | Compilation5} Compilation4or5 */ -const path = require('path'); -const fs = require('fs'); -const crypto = require('crypto'); +const path = require('path') +const fs = require('fs') +const crypto = require('crypto') -const defaults = require('lodash.defaults'); -const assign = require('lodash.assign'); -const get = require('lodash.get'); -const each = require('lodash.foreach'); -const fromPairs = require('lodash.frompairs'); -const toPairs = require('lodash.topairs'); -const stripAnsi = require('./utils/stripAnsi'); +const assign = require('lodash.assign') +const get = require('lodash.get') +const each = require('lodash.foreach') +const fromPairs = require('lodash.frompairs') +const toPairs = require('lodash.topairs') +const stripAnsi = require('./utils/stripAnsi') +/** + * @param {Compilation4or5} compilation + * @param {string} name + * @returns {string} + */ function getAssetPath(compilation, name) { - return path.join(compilation.getPath(compilation.compiler.outputPath), name.split('?')[0]); + return path.join( + compilation.getPath(compilation.compiler.outputPath, {}), + name.split('?')[0]) } /** @@ -37,12 +48,17 @@ function getAssetPath(compilation, name) { * @returns {T} */ function mergeObjects(obj1, obj2) { - const mergedObj = assign({}, obj1, obj2); - const sortedPairs = toPairs(mergedObj).sort((e1, e2) => e1[0].localeCompare(e2[0])); - // @ts-ignore: 2322 The Lodash typedefs aren't smart enough to be able to tell TS that we're + const mergedObj = assign({}, obj1, obj2) + const sortedPairs = toPairs(mergedObj).sort( + (e1, e2) => e1[0].localeCompare(e2[0])) + // @ts-expect-error: 2322 + // The Lodash typedefs aren't smart enough to be able to tell TS that we're // regenerating the object from the original key-value pairs. - return fromPairs(sortedPairs); + return fromPairs(sortedPairs) } +/** + * @property {Compilation4or5} _compilation + */ class BundleTrackerPlugin { /** @@ -51,188 +67,228 @@ class BundleTrackerPlugin { */ constructor(options) { /** @type {Options} */ - this.options = options; + this.options = options /** @type {Contents} */ this.contents = { status: 'initialization', assets: {}, chunks: {}, - }; - this.name = 'BundleTrackerPlugin'; + } + this.name = 'BundleTrackerPlugin' /** @type {Contents} */ this._iter_output = { status: 'compile', assets: {}, chunks: {} } - - this.outputChunkDir = ''; - this.outputTrackerFile = ''; - this.outputTrackerDir = ''; } + /** - * Setup parameter from compiler data - * @param {Compiler} compiler - * @returns this + * Setup module options from compiler data + * @param {Compiler4} compiler + * @returns ComputedOpts */ - _setParamsFromCompiler(compiler) { - this.options = defaults({}, this.options, { - path: get(compiler.options, 'output.path', process.cwd()), - publicPath: get(compiler.options, 'output.publicPath', ''), - filename: 'webpack-stats.json', - logTime: false, - relativePath: false, - integrity: false, - indent: 2, + _getComputedOptions(compiler) { + const opts = this.options + const config = { + path: opts.path || get(compiler.options, 'output.path', process.cwd()), + filename: opts.filename || 'webpack-stats.json', + publicPath: + opts.publicPath || get(compiler.options, 'output.publicPath', ''), + logTime: opts.logTime || false, + relativePath: opts.relativePath || false, + indent: opts.indent || 2, + integrity: opts.integrity || false, // https://www.w3.org/TR/SRI/#cryptographic-hash-functions - integrityHashes: ['sha256', 'sha384', 'sha512'], - }); - - if ( - typeof this.options.filename === 'string' && - this.options.filename.includes('/') - ) { - throw Error( - "The `filename` shouldn't include a `/`. Please use the `path` parameter to " + - "build the directory path and use `filename` only for the file's name itself.\n" + - 'TIP: you can use `path.join` to build the directory path in your config file.', - ); + integrityHashes: opts.integrityHashes || ['sha256', 'sha384', 'sha512'], + outputChunkDir: + path.resolve(get(compiler.options, 'output.path', process.cwd())), } - - // Set output directories - this.outputChunkDir = path.resolve(get(compiler.options, 'output.path', process.cwd())); - // @ts-ignore: TS2345 this.options.path can't be undefined here because we set a default value above - // @ts-ignore: TS2345 this.options.filename can't be undefined here because we set a default value above - this.outputTrackerFile = path.resolve(path.join(this.options.path, this.options.filename)); - this.outputTrackerDir = path.dirname(this.outputTrackerFile); - - return this; + if (config.filename.includes('/')) + throw Error( + 'The `filename` shouldn\'t include a `/`. Please use the `path` ' + + 'parameter to build the directory path and use `filename` only for ' + + 'the file\'s name itself.\nTIP: you can use `path.join` to build the ' + + 'directory path in your config file.') + const outputTrackerFile = + path.resolve(path.join(config.path, config.filename)) + /** @type {ComputedOpts} */ + const computedOpts = Object.assign(config, { + outputTrackerFile, + outputTrackerDir: path.dirname(outputTrackerFile) + }) + return computedOpts } + /** * Write bundle tracker stats file, merging the existing content with * the output from the latest compilation results, back to the * `this.contents` variable. + * @param {ComputedOpts} computedOpts */ - _writeOutput() { + _writeOutput(computedOpts) { assign(this.contents, this._iter_output, { assets: mergeObjects(this.contents.assets, this._iter_output.assets), chunks: mergeObjects(this.contents.chunks, this._iter_output.chunks), - }); + }) - if (this.options.publicPath) { - this.contents.publicPath = this.options.publicPath; - } + if (computedOpts.publicPath) + this.contents.publicPath = computedOpts.publicPath - fs.mkdirSync(this.outputTrackerDir, { recursive: true, mode: 0o755 }); - fs.writeFileSync(this.outputTrackerFile, JSON.stringify(this.contents, null, this.options.indent)); + fs.mkdirSync( + computedOpts.outputTrackerDir, + { recursive: true, mode: 0o755 }) + fs.writeFileSync( + computedOpts.outputTrackerFile, + JSON.stringify(this.contents, null, computedOpts.indent)) } + /** * Compute hash for a content - * @param {string} content + * @param {Buffer | string} content + * @param {ComputedOpts} computedOpts */ - _computeIntegrity(content) { - // @ts-ignore: TS2532 this.options.integrityHashes can't be undefined here because - // we set a default value on _setParamsFromCompiler - return this.options.integrityHashes + _computeIntegrity(content, computedOpts) { + return computedOpts.integrityHashes .map(algorithm => { - const hash = crypto - .createHash(algorithm) - .update(content, 'utf8') - .digest('base64'); - - return `${algorithm}-${hash}`; + const hash = crypto.createHash(algorithm) + if (typeof content === 'string') + hash.update(content, 'utf8') + else + hash.update(content) + return `${algorithm}-${hash.digest('base64')}` }) - .join(' '); + .join(' ') } + /** * Handle compile hook - * @param {Compiler} compiler + * @param {Compiler4} compiler + * @param {ComputedOpts} computedOpts */ - _handleCompile(compiler) { + _handleCompile(compiler, computedOpts) { this._iter_output = { status: 'compile', assets: {}, chunks: {} } - this._writeOutput(); + this._writeOutput(computedOpts) } /** * Handle compile hook + * @param {ComputedOpts} computedOpts * @param {string} compiledFile - * @param {AssetEmittedInfo} compiledDetails + * @param {AssetEmittedInfo5 | string} detailsOrContent */ - _handleAssetEmitted(compiledFile, compiledDetails) { + _handleAssetEmitted(computedOpts, compiledFile, detailsOrContent) { + /** @type {Compilation4or5} */ + let compilation + let content + let targetPath + if (typeof detailsOrContent === 'string') { + // Webpack 4 + if (!this._compilation) + throw Error('_handleAssetEmitted needs the Compilation object.') + compilation = this._compilation + content = detailsOrContent + targetPath = getAssetPath(compilation, compiledFile) + } else { + // Webpack 5 + compilation = detailsOrContent.compilation + content = detailsOrContent.content + targetPath = detailsOrContent.targetPath + } /** @type {ContentsAssetsValue} */ const thisFile = { name: compiledFile, - path: compiledDetails.targetPath - } - if (this.options.integrity === true) { - thisFile.integrity = this._computeIntegrity(compiledDetails.content); + path: targetPath } - if (this.options.publicPath) { - if (this.options.publicPath === 'auto') { - thisFile.publicPath = 'auto'; + if (computedOpts.integrity === true) + thisFile.integrity = + this._computeIntegrity(content, computedOpts) + if (computedOpts.publicPath) { + if (computedOpts.publicPath === 'auto') { + thisFile.publicPath = 'auto' } else { - thisFile.publicPath = this.options.publicPath + compiledFile; + thisFile.publicPath = computedOpts.publicPath + compiledFile } } - if (this.options.relativePath === true) { - thisFile.path = path.relative(this.outputChunkDir, thisFile.path); - } - /** @type {Compilation} */ - const compilation = compiledDetails.compilation - // @ts-ignore: TS2339: Property 'assetsInfo' does not exist on type 'Compilation'. - if (compilation.assetsInfo) { - // @ts-ignore: TS2339: Property 'assetsInfo' does not exist on type 'Compilation'. - thisFile.sourceFilename = - compilation.assetsInfo.get(compiledFile).sourceFilename; + if (computedOpts.relativePath === true) { + thisFile.path = path.relative(computedOpts.outputChunkDir, thisFile.path) } + const getAssetResponse = compilation.getAsset(compiledFile) + if ( + getAssetResponse?.info && + getAssetResponse?.info && + 'sourceFilename' in getAssetResponse.info && + getAssetResponse.info.sourceFilename + ) thisFile.sourceFilename = getAssetResponse.info.sourceFilename + this._iter_output.assets[compiledFile] = thisFile } /** * Handle compile hook - * @param {Compiler} compiler - * @param {Stats} stats + * @param {Compiler4} compiler + * @param {ComputedOpts} computedOpts + * @param {Stats4} stats */ - _handleDone(compiler, stats) { + _handleDone(compiler, computedOpts, stats) { if (stats.hasErrors()) { - const findError = compilation => { + const findError = ( + /** @type {Compilation4or5} */ compilation + ) => { if (compilation.errors.length > 0) { - return compilation.errors[0]; + return compilation.errors[0] } - return compilation.children.find(child => findError(child)); - }; - const error = findError(stats.compilation); + return compilation.children.find(child => findError(child)) + } + const error = findError(stats.compilation) this._iter_output = { status: 'error', error: get(error, 'name', 'unknown-error'), message: stripAnsi(error['message']), assets: {}, chunks: {} } - this._writeOutput(); + this._writeOutput(computedOpts) - return; + return } each(stats.compilation.chunkGroups, chunkGroup => { - if (!chunkGroup.isInitial()) return; - this._iter_output.chunks[chunkGroup.name] = chunkGroup.getFiles(); - }); + if (!chunkGroup.isInitial()) return + this._iter_output.chunks[chunkGroup.name] = chunkGroup.getFiles() + }) - if (this.options.logTime === true) { - this._iter_output.startTime = stats.startTime; - this._iter_output.endTime = stats.endTime; + if (computedOpts.logTime === true) { + this._iter_output.startTime = stats.startTime + this._iter_output.endTime = stats.endTime } this._iter_output.status = 'done' - this._writeOutput(); + this._writeOutput(computedOpts) + } + + /** + * Set the compilation object (for the webpack4 case) + * @param {Compilation4or5} compilation + */ + _handleThisCompilation(compilation) { + this._compilation = compilation } /** * Method called by webpack to apply plugin hook - * @param {Compiler} compiler + * @param {Compiler4} compiler */ apply(compiler) { - this._setParamsFromCompiler(compiler); + const computedOpts = this._getComputedOptions(compiler) + // this._setParamsFromCompiler(compiler) // In order of hook calls: - compiler.hooks.compile.tap(this.name, this._handleCompile.bind(this, compiler)); - compiler.hooks.assetEmitted.tap(this.name, this._handleAssetEmitted.bind(this)); - compiler.hooks.done.tap(this.name, this._handleDone.bind(this, compiler)); + compiler.hooks.compile + .tap(this.name, this._handleCompile.bind(this, compiler, computedOpts)) + compiler.hooks.thisCompilation + .tap(this.name, this._handleThisCompilation.bind(this)) + // Webpack 4 or 5, who knows at this point + /** @type {assetEmitted4 | assetEmitted5} */ + const assetEmitted = compiler.hooks.assetEmitted + assetEmitted + .tap(this.name, this._handleAssetEmitted.bind(this, computedOpts)) + compiler.hooks.done + .tap(this.name, this._handleDone.bind(this, compiler, computedOpts)) } } -module.exports = BundleTrackerPlugin; +module.exports = BundleTrackerPlugin diff --git a/package.json b/package.json index 7f8118f..29129a2 100644 --- a/package.json +++ b/package.json @@ -47,35 +47,35 @@ "lodash.topairs": "^4.3.0" }, "devDependencies": { - "@types/babel__traverse": "7.0.6", - "@types/lodash": "4.14.173", - "@types/lodash.assign": "^4.2.7", - "@types/lodash.defaults": "^4.2.7", - "@types/lodash.foreach": "^4.5.7", - "@types/lodash.frompairs": "^4.0.7", - "@types/lodash.get": "^4.4.7", - "@types/lodash.topairs": "^4.3.7", + "@types/babel__traverse": "7.20.6", + "@types/lodash": "4.17.7", + "@types/lodash.assign": "^4.2.9", + "@types/lodash.defaults": "^4.2.9", + "@types/lodash.foreach": "^4.5.9", + "@types/lodash.frompairs": "^4.0.9", + "@types/lodash.get": "^4.4.9", + "@types/lodash.topairs": "^4.3.9", "@types/node": "^13.13.52", - "@types/webpack": "^4.41.33", - "@typescript-eslint/eslint-plugin": "^2.34.0", - "@typescript-eslint/parser": "^2.34.0", + "@types/webpack": "^4.41.38", + "@typescript-eslint/eslint-plugin": "^7.17.0", + "@typescript-eslint/parser": "^7.17.0", "commitizen": "^4.3.0", "compression-webpack-plugin": "^6.1.1", "css-loader": "^5.2.7", "cz-conventional-changelog": "3.3.0", - "eslint": "^6.8.0", - "file-loader": "^5.1.0", + "eslint": "^8.56.0", + "file-loader": "^6.2.0", "jest": "^29.7.0", - "jest-extended": "^0.11.5", + "jest-extended": "^4.0.2", "mini-css-extract-plugin": "^1.6.2", - "prettier": "^1.19.1", + "prettier": "^3.3.3", "standard-version": "^9.5.0", - "style-loader": "^1.3.0", + "style-loader": "^2.0.0", "tslint": "^6.1.0", - "typescript": "^5.3.2", + "typescript": "^5.5.4", "webpack": "^4.47.0", "webpack-cli": "^4.10.0", - "webpack5": "npm:webpack@^5.89.0" + "webpack5": "npm:webpack@^5.93.0" }, "config": { "commitizen": { diff --git a/typings.d.ts b/typings.d.ts index a5bcc9d..e8a6e52 100644 --- a/typings.d.ts +++ b/typings.d.ts @@ -1,99 +1,129 @@ -import { Compiler } from 'webpack'; - -export = BundleTrackerPlugin; +import { Compiler } from 'webpack' +export = BundleTrackerPlugin declare class BundleTrackerPlugin { - constructor(options?: BundleTrackerPlugin.Options); - apply(compiler: Compiler): void; -} + constructor(options?: BundleTrackerPlugin.Options) + apply(compiler: Compiler): void} + declare namespace BundleTrackerPlugin { interface Options { + /** * Output directory of the bundle tracker JSON file * Default: `'.'` */ - path?: string; + path?: string + /** * Name of the bundle tracker JSON file. * Default: `'webpack-stats.json'` */ - filename?: string; + filename?: string + /** * Property to override default `output.publicPath` from Webpack config file. */ - publicPath?: string; + publicPath?: string + /** * Output `startTime` and `endTime` properties to JSON file. * Default: `false` */ - logTime?: boolean; + logTime?: boolean + /** * Output relative path from JSON file into `path` properties. * Default: `false` */ - relativePath?: boolean; + relativePath?: boolean + /** * Indent JSON output file */ - indent?: number; + indent?: number + /** * Enable subresources integrity * Default: `false` */ - integrity?: boolean; + integrity?: boolean + /** * Set subresources integrity hashes * Default: `[ 'sha256', 'sha384', 'sha512' ]` */ - integrityHashes?: string[]; + integrityHashes?: string[] } + + interface ComputedOpts { + path: string + filename: string + publicPath: string + logTime: boolean + relativePath: boolean + indent: number + integrity: boolean + integrityHashes: string[] + outputChunkDir: str + outputTrackerFile: str + outputTrackerDir: str + } + interface Contents { + /** * Status of webpack */ - status: string; + status: string + /** * Error when webpack has failure from compilation */ - error?: string; + error?: string + /** * Error message when webpack has failure from compilation */ - message?: string; + message?: string + /** * File information */ assets: { [name: string]: { - name: string; - integrity?: string; - publicPath?: string; - path: string; - }; - }; + name: string + integrity?: string + publicPath?: string + path: string + sourceFilename?: string + } } + /** * List of chunks builded */ chunks: { [name: string]: [ { - name: string; - publicPath?: string; - path: string; + name: string + publicPath?: string + path: string }, - ]; - }; + ] + } + /** * Public path of chunks */ - publicPath?: string; + publicPath?: string + /** * Start time of webpack compilation */ - startTime?: number; + startTime?: number + /** * End time of webpack compilation */ - endTime?: number; + endTime?: number } } From 972f5287639f21de1dc0e68e14d840b4104af32a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=A1szl=C3=B3=20K=C3=A1rolyi?= Date: Sun, 5 Jan 2025 15:06:18 +0100 Subject: [PATCH 3/4] Final touches --- lib/index.js | 66 +++++++++++++++++++++++++++++++--------------------- 1 file changed, 40 insertions(+), 26 deletions(-) diff --git a/lib/index.js b/lib/index.js index 87e3174..ea10bca 100644 --- a/lib/index.js +++ b/lib/index.js @@ -5,6 +5,7 @@ /** @typedef {import("webpack").Compiler} Compiler4 */ /** @typedef {Compiler4['hooks']['assetEmitted']} assetEmitted4 */ /** @typedef {import("webpack").Stats} Stats4 */ +/** @typedef {import("webpack5").Stats} Stats5 */ /** @typedef {import("webpack").compilation.Compilation} Compilation4 */ /** @typedef {import("webpack").ChunkData} ChunkData4 */ /** @typedef {import("webpack5").Compiler} Compiler5 */ @@ -16,7 +17,6 @@ /** @typedef {import("../typings").Options} Options */ /** @typedef {Contents['assets']} ContentsAssets */ /** @typedef {ContentsAssets[keyof ContentsAssets]} ContentsAssetsValue */ -/** @typedef {Compilation4 | Compilation5} Compilation4or5 */ const path = require('path') const fs = require('fs') @@ -30,7 +30,7 @@ const toPairs = require('lodash.topairs') const stripAnsi = require('./utils/stripAnsi') /** - * @param {Compilation4or5} compilation + * @param {Compilation4 | Compilation5} compilation * @param {string} name * @returns {string} */ @@ -56,9 +56,6 @@ function mergeObjects(obj1, obj2) { // regenerating the object from the original key-value pairs. return fromPairs(sortedPairs) } -/** - * @property {Compilation4or5} _compilation - */ class BundleTrackerPlugin { /** @@ -79,26 +76,40 @@ class BundleTrackerPlugin { this._iter_output = { status: 'compile', assets: {}, chunks: {} } } + /** + * @param {Compiler4 | Compiler5} compiler + * @returns {string} + */ + _getPublicPath(compiler) { + const publicPath = compiler.options.output?.publicPath + if (publicPath === undefined) + return '' + if (typeof publicPath === 'string') + return publicPath + // It is a function but no idea what to pass it + // See https://webpack.js.org/configuration/output/#outputpublicpath + return '' + } + /** * Setup module options from compiler data - * @param {Compiler4} compiler - * @returns ComputedOpts + * @param {Compiler4 | Compiler5} compiler + * @returns {ComputedOpts} */ _getComputedOptions(compiler) { const opts = this.options + const outputPath = compiler.options.output?.path || process.cwd() const config = { - path: opts.path || get(compiler.options, 'output.path', process.cwd()), + path: opts.path || outputPath, filename: opts.filename || 'webpack-stats.json', - publicPath: - opts.publicPath || get(compiler.options, 'output.publicPath', ''), + publicPath: opts.publicPath || this._getPublicPath(compiler), logTime: opts.logTime || false, relativePath: opts.relativePath || false, indent: opts.indent || 2, integrity: opts.integrity || false, // https://www.w3.org/TR/SRI/#cryptographic-hash-functions integrityHashes: opts.integrityHashes || ['sha256', 'sha384', 'sha512'], - outputChunkDir: - path.resolve(get(compiler.options, 'output.path', process.cwd())), + outputChunkDir: path.resolve(outputPath) } if (config.filename.includes('/')) throw Error( @@ -121,6 +132,7 @@ class BundleTrackerPlugin { * the output from the latest compilation results, back to the * `this.contents` variable. * @param {ComputedOpts} computedOpts + * @returns {void} */ _writeOutput(computedOpts) { assign(this.contents, this._iter_output, { @@ -159,10 +171,9 @@ class BundleTrackerPlugin { /** * Handle compile hook - * @param {Compiler4} compiler * @param {ComputedOpts} computedOpts */ - _handleCompile(compiler, computedOpts) { + _handleCompile(computedOpts) { this._iter_output = { status: 'compile', assets: {}, chunks: {} } this._writeOutput(computedOpts) } @@ -172,9 +183,10 @@ class BundleTrackerPlugin { * @param {ComputedOpts} computedOpts * @param {string} compiledFile * @param {AssetEmittedInfo5 | string} detailsOrContent + * @returns {void} */ _handleAssetEmitted(computedOpts, compiledFile, detailsOrContent) { - /** @type {Compilation4or5} */ + /** @type {Compilation4 | Compilation5} */ let compilation let content let targetPath @@ -222,14 +234,14 @@ class BundleTrackerPlugin { /** * Handle compile hook - * @param {Compiler4} compiler * @param {ComputedOpts} computedOpts - * @param {Stats4} stats + * @param {Stats4 | Stats5} stats + * @returns {void} */ - _handleDone(compiler, computedOpts, stats) { + _handleDone(computedOpts, stats) { if (stats.hasErrors()) { const findError = ( - /** @type {Compilation4or5} */ compilation + /** @type {Compilation4 | Compilation5} */ compilation ) => { if (compilation.errors.length > 0) { return compilation.errors[0] @@ -237,9 +249,11 @@ class BundleTrackerPlugin { return compilation.children.find(child => findError(child)) } const error = findError(stats.compilation) + const errorStr = + typeof error === 'string' ? error : error?.details || 'unknown-error' this._iter_output = { status: 'error', - error: get(error, 'name', 'unknown-error'), + error: errorStr, message: stripAnsi(error['message']), assets: {}, chunks: {} } @@ -262,7 +276,8 @@ class BundleTrackerPlugin { /** * Set the compilation object (for the webpack4 case) - * @param {Compilation4or5} compilation + * @param {Compilation4 | Compilation5} compilation + * @returns {void} */ _handleThisCompilation(compilation) { this._compilation = compilation @@ -270,15 +285,14 @@ class BundleTrackerPlugin { /** * Method called by webpack to apply plugin hook - * @param {Compiler4} compiler + * @param {Compiler4 | Compiler5} compiler + * @returns {void} */ apply(compiler) { const computedOpts = this._getComputedOptions(compiler) - // this._setParamsFromCompiler(compiler) - // In order of hook calls: compiler.hooks.compile - .tap(this.name, this._handleCompile.bind(this, compiler, computedOpts)) + .tap(this.name, this._handleCompile.bind(this, computedOpts)) compiler.hooks.thisCompilation .tap(this.name, this._handleThisCompilation.bind(this)) // Webpack 4 or 5, who knows at this point @@ -287,7 +301,7 @@ class BundleTrackerPlugin { assetEmitted .tap(this.name, this._handleAssetEmitted.bind(this, computedOpts)) compiler.hooks.done - .tap(this.name, this._handleDone.bind(this, compiler, computedOpts)) + .tap(this.name, this._handleDone.bind(this, computedOpts)) } } From af241ef9edd3b9963175cb95102067b5ce634594 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=A1szl=C3=B3=20K=C3=A1rolyi?= Date: Tue, 30 Jul 2024 22:53:26 +0200 Subject: [PATCH 4/4] Remove unused lodash.get() --- lib/index.js | 2 -- package.json | 2 +- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/lib/index.js b/lib/index.js index ea10bca..37efa6d 100644 --- a/lib/index.js +++ b/lib/index.js @@ -1,7 +1,6 @@ // @ts-check /** @typedef {import("lodash.defaults")} defaults */ /** @typedef {import("lodash.assign")} assign */ -/** @typedef {import("lodash.get")} get */ /** @typedef {import("webpack").Compiler} Compiler4 */ /** @typedef {Compiler4['hooks']['assetEmitted']} assetEmitted4 */ /** @typedef {import("webpack").Stats} Stats4 */ @@ -23,7 +22,6 @@ const fs = require('fs') const crypto = require('crypto') const assign = require('lodash.assign') -const get = require('lodash.get') const each = require('lodash.foreach') const fromPairs = require('lodash.frompairs') const toPairs = require('lodash.topairs') diff --git a/package.json b/package.json index 29129a2..043fba1 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "webpack-bundle-tracker", - "version": "3.1.1", + "version": "3.2.0", "description": "Spits out some stats about webpack compilation process to a file", "keywords": [ "bundle",