diff --git a/packages/dependency-extraction-webpack-plugin/CHANGELOG.md b/packages/dependency-extraction-webpack-plugin/CHANGELOG.md index 7fb035bf1bed1..5a8de8c78920e 100644 --- a/packages/dependency-extraction-webpack-plugin/CHANGELOG.md +++ b/packages/dependency-extraction-webpack-plugin/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +### New Features + +- Output asset files for shared chunks, too ([#41002](https://github.com/WordPress/gutenberg/pull/41002)). + ## 3.5.0 (2022-05-18) ### Bug Fix diff --git a/packages/dependency-extraction-webpack-plugin/lib/index.js b/packages/dependency-extraction-webpack-plugin/lib/index.js index 48ece4ff0a3e4..581274c3684f9 100644 --- a/packages/dependency-extraction-webpack-plugin/lib/index.js +++ b/packages/dependency-extraction-webpack-plugin/lib/index.js @@ -117,7 +117,7 @@ class DependencyExtractionWebpackPlugin { if ( isWebpack4 ) { compiler.hooks.emit.tap( this.constructor.name, ( compilation ) => - this.addAssets( compilation, compiler ) + this.addAssets( compilation ) ); } else { compiler.hooks.thisCompilation.tap( @@ -129,14 +129,14 @@ class DependencyExtractionWebpackPlugin { stage: compiler.webpack.Compilation .PROCESS_ASSETS_STAGE_ANALYSE, }, - () => this.addAssets( compilation, compiler ) + () => this.addAssets( compilation ) ); } ); } } - addAssets( compilation, compiler ) { + addAssets( compilation ) { const { combineAssets, combinedOutputFile, @@ -162,124 +162,102 @@ class DependencyExtractionWebpackPlugin { const combinedAssetsData = {}; - // Process each entry point independently. - for ( const [ - entrypointName, - entrypoint, - ] of compilation.entrypoints.entries() ) { - const entrypointExternalizedWpDeps = new Set(); + // Accumulate all entrypoint chunks, some of them shared + const entrypointChunks = new Set(); + for ( const entrypoint of compilation.entrypoints.values() ) { + for ( const chunk of entrypoint.chunks ) { + entrypointChunks.add( chunk ); + } + } + + // Process each entrypoint chunk independently + for ( const chunk of entrypointChunks ) { + const chunkFiles = Array.from( chunk.files ); + + const chunkJSFile = chunkFiles.find( ( f ) => /\.js$/i.test( f ) ); + if ( ! chunkJSFile ) { + // There's no JS file in this chunk, no work for us. Typically a `style.css` from cache group. + continue; + } + + const chunkDeps = new Set(); if ( injectPolyfill ) { - entrypointExternalizedWpDeps.add( 'wp-polyfill' ); + chunkDeps.add( 'wp-polyfill' ); } const processModule = ( { userRequest } ) => { if ( this.externalizedDeps.has( userRequest ) ) { - const scriptDependency = - this.mapRequestToDependency( userRequest ); - entrypointExternalizedWpDeps.add( scriptDependency ); + chunkDeps.add( this.mapRequestToDependency( userRequest ) ); } }; // Search for externalized modules in all chunks. - for ( const chunk of entrypoint.chunks ) { - const modulesIterable = isWebpack4 - ? chunk.modulesIterable - : compilation.chunkGraph.getChunkModules( chunk ); - for ( const chunkModule of modulesIterable ) { - processModule( chunkModule ); - // Loop through submodules of ConcatenatedModule. - if ( chunkModule.modules ) { - for ( const concatModule of chunkModule.modules ) { - processModule( concatModule ); - } + const modulesIterable = isWebpack4 + ? chunk.modulesIterable + : compilation.chunkGraph.getChunkModules( chunk ); + for ( const chunkModule of modulesIterable ) { + processModule( chunkModule ); + // Loop through submodules of ConcatenatedModule. + if ( chunkModule.modules ) { + for ( const concatModule of chunkModule.modules ) { + processModule( concatModule ); } } } - const { hashFunction, hashDigest, hashDigestLength } = - compilation.outputOptions; - // Go through the assets and hash the sources. We can't just use - // `entrypointChunk.contentHash` because that's not updated when + // `chunk.contentHash` because that's not updated when // assets are minified. In practice the hash is updated by // `RealContentHashPlugin` after minification, but it only modifies // already-produced asset filenames and the updated hash is not // available to plugins. - const hash = createHash( hashFunction ); - for ( const filename of entrypoint.getFiles().sort() ) { - const asset = compilation.getAsset( filename ); - hash.update( asset.source.buffer() ); - } - const version = hash + const { hashFunction, hashDigest, hashDigestLength } = + compilation.outputOptions; + + const contentHash = chunkFiles + .sort() + .reduce( ( hash, filename ) => { + const asset = compilation.getAsset( filename ); + return hash.update( asset.source.buffer() ); + }, createHash( hashFunction ) ) .digest( hashDigest ) .slice( 0, hashDigestLength ); - const entrypointChunk = isWebpack4 - ? entrypoint.chunks.find( ( c ) => c.name === entrypointName ) - : entrypoint.getEntrypointChunk(); - const assetData = { // Get a sorted array so we can produce a stable, stringified representation. - dependencies: Array.from( entrypointExternalizedWpDeps ).sort(), - version, + dependencies: Array.from( chunkDeps ).sort(), + version: contentHash, }; - const assetString = this.stringify( assetData ); - const contentHash = createHash( hashFunction ) - .update( assetString ) - .digest( hashDigest ) - .slice( 0, hashDigestLength ); - - // Determine a filename for the asset file. - const [ filename, query ] = entrypointName.split( '?', 2 ); - const buildFilename = compilation.getPath( - compiler.options.output.filename, - { - chunk: entrypointChunk, - filename, - query, - basename: basename( filename ), - contentHash, - } - ); - if ( combineAssets ) { - combinedAssetsData[ buildFilename ] = assetData; + combinedAssetsData[ chunkJSFile ] = assetData; continue; } let assetFilename; - if ( outputFilename ) { assetFilename = compilation.getPath( outputFilename, { - chunk: entrypointChunk, - filename, - query, - basename: basename( filename ), + chunk, + filename: chunkJSFile, contentHash, } ); } else { - assetFilename = buildFilename.replace( - /\.js$/i, - '.asset.' + ( outputFormat === 'php' ? 'php' : 'json' ) - ); + const suffix = + '.asset.' + ( outputFormat === 'php' ? 'php' : 'json' ); + assetFilename = compilation + .getPath( '[file]', { filename: chunkJSFile } ) + .replace( /\.js$/i, suffix ); } // Add source and file into compilation for webpack to output. - compilation.assets[ assetFilename ] = new RawSource( assetString ); - entrypointChunk.files[ isWebpack4 ? 'push' : 'add' ]( - assetFilename + compilation.assets[ assetFilename ] = new RawSource( + this.stringify( assetData ) ); + chunk.files[ isWebpack4 ? 'push' : 'add' ]( assetFilename ); } if ( combineAssets ) { - // Assert the `string` type for output path. - // The type indicates the option may be `undefined`. - // However, at this point in compilation, webpack has filled the options in if - // they were not provided. - const outputFolder = /** @type {{path:string}} */ ( - compiler.options.output - ).path; + const outputFolder = compilation.outputOptions.path; const assetsFilePath = path.resolve( outputFolder, @@ -299,11 +277,4 @@ class DependencyExtractionWebpackPlugin { } } -function basename( name ) { - if ( ! name.includes( '/' ) ) { - return name; - } - return name.substr( name.lastIndexOf( '/' ) + 1 ); -} - module.exports = DependencyExtractionWebpackPlugin; diff --git a/packages/dependency-extraction-webpack-plugin/test/__snapshots__/build.js.snap b/packages/dependency-extraction-webpack-plugin/test/__snapshots__/build.js.snap index 4d7ba179f8112..3feaba84de033 100644 --- a/packages/dependency-extraction-webpack-plugin/test/__snapshots__/build.js.snap +++ b/packages/dependency-extraction-webpack-plugin/test/__snapshots__/build.js.snap @@ -207,12 +207,17 @@ Array [ `; exports[`DependencyExtractionWebpackPlugin Webpack \`runtime-chunk-single\` should produce expected output: Asset file 'a.asset.php' should match snapshot 1`] = ` -" array('wp-blob'), 'version' => '39c05211520759f42c9d'); +" array('wp-blob'), 'version' => '09a0c551770a351c5ca7'); " `; exports[`DependencyExtractionWebpackPlugin Webpack \`runtime-chunk-single\` should produce expected output: Asset file 'b.asset.php' should match snapshot 1`] = ` -" array('lodash', 'wp-blob'), 'version' => '70fbf918dd6a71b65cf6'); +" array('lodash', 'wp-blob'), 'version' => 'c9f00d690a9f72438910'); +" +`; + +exports[`DependencyExtractionWebpackPlugin Webpack \`runtime-chunk-single\` should produce expected output: Asset file 'runtime.asset.php' should match snapshot 1`] = ` +" array(), 'version' => '46ea0ff11ac53fa5e88b'); " `; @@ -234,6 +239,29 @@ Array [ ] `; +exports[`DependencyExtractionWebpackPlugin Webpack \`style-imports\` should produce expected output: Asset file 'main.asset.php' should match snapshot 1`] = ` +" array('lodash', 'wp-blob'), 'version' => 'd8c0ee89d933a3809c0e'); +" +`; + +exports[`DependencyExtractionWebpackPlugin Webpack \`style-imports\` should produce expected output: External modules should match snapshot 1`] = ` +Array [ + Object { + "externalType": "window", + "request": "lodash", + "userRequest": "lodash", + }, + Object { + "externalType": "window", + "request": Array [ + "wp", + "blob", + ], + "userRequest": "@wordpress/blob", + }, +] +`; + exports[`DependencyExtractionWebpackPlugin Webpack \`wordpress\` should produce expected output: Asset file 'main.asset.php' should match snapshot 1`] = ` " array('lodash', 'wp-blob'), 'version' => '9b7ebe61044661fdabda'); " diff --git a/packages/dependency-extraction-webpack-plugin/test/build.js b/packages/dependency-extraction-webpack-plugin/test/build.js index ef543d0a3f9d6..b749b66f1e74b 100644 --- a/packages/dependency-extraction-webpack-plugin/test/build.js +++ b/packages/dependency-extraction-webpack-plugin/test/build.js @@ -52,17 +52,8 @@ describe( 'DependencyExtractionWebpackPlugin', () => { const assetFiles = glob( `${ outputDirectory }/+(*.asset|assets).@(json|php)` ); - const hasCombinedAssets = ( options.plugins || [] ).some( - ( plugin ) => !! ( plugin.options || {} ).combineAssets - ); - const entrypointCount = - typeof options.entry === 'object' - ? Object.keys( options.entry ).length - : 1; - const expectedLength = hasCombinedAssets - ? 1 - : entrypointCount; - expect( assetFiles ).toHaveLength( expectedLength ); + + expect( assetFiles.length ).toBeGreaterThan( 0 ); // Asset files should match. assetFiles.forEach( ( assetFile ) => { diff --git a/packages/dependency-extraction-webpack-plugin/test/fixtures/style-imports/index.js b/packages/dependency-extraction-webpack-plugin/test/fixtures/style-imports/index.js new file mode 100644 index 0000000000000..df02e0b35e6f8 --- /dev/null +++ b/packages/dependency-extraction-webpack-plugin/test/fixtures/style-imports/index.js @@ -0,0 +1,16 @@ +/** + * WordPress dependencies + */ +import { isBlobURL } from '@wordpress/blob'; + +/** + * External dependencies + */ +import _ from 'lodash'; + +/** + * Internal dependencies + */ +import './style.css'; + +_.isEmpty( isBlobURL( '' ) ); diff --git a/packages/dependency-extraction-webpack-plugin/test/fixtures/style-imports/style.css b/packages/dependency-extraction-webpack-plugin/test/fixtures/style-imports/style.css new file mode 100644 index 0000000000000..c5489eafcbc3d --- /dev/null +++ b/packages/dependency-extraction-webpack-plugin/test/fixtures/style-imports/style.css @@ -0,0 +1 @@ +body { color: white; } diff --git a/packages/dependency-extraction-webpack-plugin/test/fixtures/style-imports/webpack.config.js b/packages/dependency-extraction-webpack-plugin/test/fixtures/style-imports/webpack.config.js new file mode 100644 index 0000000000000..52cb718a579de --- /dev/null +++ b/packages/dependency-extraction-webpack-plugin/test/fixtures/style-imports/webpack.config.js @@ -0,0 +1,31 @@ +/** + * External dependencies + */ +const MiniCSSExtractPlugin = require( 'mini-css-extract-plugin' ); + +/** + * Internal dependencies + */ +const DependencyExtractionWebpackPlugin = require( '../../..' ); + +module.exports = { + plugins: [ + new DependencyExtractionWebpackPlugin(), + new MiniCSSExtractPlugin(), + ], + module: { + rules: [ + { + test: /\.css$/, + use: [ + { + loader: MiniCSSExtractPlugin.loader, + }, + { + loader: require.resolve( 'css-loader' ), + }, + ], + }, + ], + }, +};