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 a0ce5fb..8624b78 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 } }