-
Notifications
You must be signed in to change notification settings - Fork 107
Change how stats file assets are build using emit hook #132
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
Changes from 6 commits
2da54ab
c3eae8b
4a38afb
7aed240
278cc18
9d56786
31eaa92
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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(); | ||
| } | ||
|
|
||
| /** | ||
|
|
@@ -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 = ''; | ||
|
|
@@ -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 | ||
|
|
@@ -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) { | ||
|
|
@@ -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)); | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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:
|
||
| compiler.hooks.done.tap(this.name, this._handleDone.bind(this)); | ||
| } | ||
| } | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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 => { | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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(); | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
can we keep the this.contents name to avoid changing the API?