Skip to content
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

Improve webpack cache-busting version parameter by using file contents hash #44838

Merged
merged 14 commits into from
Feb 27, 2024
Merged
Show file tree
Hide file tree
Changes from 13 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
5 changes: 4 additions & 1 deletion packages/js/internal-style-build/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ const path = require( 'path' );
const WebpackRTLPlugin = require( 'webpack-rtl-plugin' );
const RemoveEmptyScriptsPlugin = require( 'webpack-remove-empty-scripts' );
const postcssPlugins = require( '@wordpress/postcss-plugins-preset' );
const StyleAssetPlugin = require( './style-asset-plugin' );

const NODE_ENV = process.env.NODE_ENV || 'development';

Expand Down Expand Up @@ -69,12 +70,14 @@ module.exports = {
new RemoveEmptyScriptsPlugin(),
new MiniCssExtractPlugin( {
filename: '[name]/style.css',
chunkFilename: 'chunks/[id].style.css',
chunkFilename: 'chunks/[id].style.css?ver=[contenthash]',
} ),
new WebpackRTLPlugin( {
filename: '[name]/style-rtl.css',
minify: NODE_ENV === 'development' ? false : { safe: true },
} ),
new StyleAssetPlugin(),
],
},
StyleAssetPlugin,
};
1 change: 1 addition & 0 deletions packages/js/internal-style-build/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
"@wordpress/base-styles": "wp-6.0",
"@wordpress/postcss-plugins-preset": "wp-6.0",
"css-loader": "^3.6.0",
"json2php": "^0.0.7",
"mini-css-extract-plugin": "^2.7.6",
"postcss-loader": "^4.3.0",
"sass-loader": "^10.5.0",
Expand Down
219 changes: 219 additions & 0 deletions packages/js/internal-style-build/style-asset-plugin.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,219 @@
/**
* Add an asset file for each entry point that contains the current version calculated for the current source code.
*
* This is modified from WP dependency-extraction-webpack-plugin plugin:
* https://github.com/WordPress/gutenberg/tree/a04a8e94e8b93ba60441c6534e21f4c3c26ff1bc/packages/dependency-extraction-webpack-plugin
*
* We can contribute this back to the original plugin in the future and remove this file.
*/
Copy link
Contributor

Choose a reason for hiding this comment

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

To support CSS files, I added a new plugin called style-asset-plugin to extract the CSS file hash and write it to the asset manifest file. It's just a simple modification of the dependency-extraction-webpack-plugin.

Seems like a great upstream contribution. If I'm reading correctly, you've inserted logic to find CSS files in the loop over entrypointChunks and assigned a content hash, similar to the one used for JS.

Copy link
Member Author

Choose a reason for hiding this comment

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

Correct! And I also removed some unnecessary extraction logic. 🙂


/**
* External dependencies
*/
const path = require( 'path' );
const webpack = require( 'webpack' );
const json2php = require( 'json2php' );
const { createHash } = webpack.util;

const { RawSource } = webpack.sources;
const { AsyncDependenciesBlock } = webpack;

class AssetDataPlugin {
constructor( options ) {
this.options = Object.assign(
{
combineAssets: false,
combinedOutputFile: null,
outputFormat: 'php',
outputFilename: null,
},
options
);
}

/**
* @param {any} asset Asset Data
* @return {string} Stringified asset data suitable for output
*/
stringify( asset ) {
if ( this.options.outputFormat === 'php' ) {
return `<?php return ${ json2php(
JSON.parse( JSON.stringify( asset ) )
) };\n`;
}

return JSON.stringify( asset );
}

apply( compiler ) {
compiler.hooks.thisCompilation.tap(
this.constructor.name,
( compilation ) => {
compilation.hooks.processAssets.tap(
{
name: this.constructor.name,
stage: compiler.webpack.Compilation
.PROCESS_ASSETS_STAGE_ANALYSE,
},
() => this.addAssets( compilation )
);
}
);
}

/** @param {webpack.Compilation} compilation */
addAssets( compilation ) {
const {
combineAssets,
combinedOutputFile,
outputFormat,
outputFilename,
} = this.options;

const combinedAssetsData = {};

// 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 styleExtensionRegExp = /\.s?css$/i;

const chunkStyleFile = chunkFiles.find( ( f ) =>
styleExtensionRegExp.test( f )
);
if ( ! chunkStyleFile ) {
// No style file, skip
continue;
}

// Go through the assets and hash the sources. We can't just use
// `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 { 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 assetData = {
version: contentHash,
};

if ( combineAssets ) {
combinedAssetsData[ chunkStyleFile ] = assetData;
continue;
}

let assetFilename;
if ( outputFilename ) {
assetFilename = compilation.getPath( outputFilename, {
chunk,
filename: chunkStyleFile,
contentHash,
} );
} else {
const suffix =
'.asset.' + ( outputFormat === 'php' ? 'php' : 'json' );
assetFilename = compilation
.getPath( '[file]', { filename: chunkStyleFile } )
.replace( styleExtensionRegExp, suffix );
}

// Add source and file into compilation for webpack to output.
compilation.assets[ assetFilename ] = new RawSource(
this.stringify( assetData )
);
chunk.files.add( assetFilename );
}

if ( combineAssets ) {
const outputFolder = compilation.outputOptions.path;

const assetsFilePath = path.resolve(
outputFolder,
combinedOutputFile ||
'assets.' + ( outputFormat === 'php' ? 'php' : 'json' )
);
const assetsFilename = path.relative(
outputFolder,
assetsFilePath
);

// Add source into compilation for webpack to output.
compilation.assets[ assetsFilename ] = new RawSource(
this.stringify( combinedAssetsData )
);
}
}

/**
* Can we trace a line of static dependencies from an entry to a module
*
* @param {webpack.Compilation} compilation
* @param {webpack.DependenciesBlock} block
*
* @return {boolean} True if there is a static import path to the root
*/
static hasStaticDependencyPathToRoot( compilation, block ) {
const incomingConnections = [
...compilation.moduleGraph.getIncomingConnections( block ),
].filter(
( connection ) =>
// Library connections don't have a dependency, this is a root
connection.dependency &&
// Entry dependencies are another root
connection.dependency.constructor.name !== 'EntryDependency'
);

// If we don't have non-entry, non-library incoming connections,
// we've reached a root of
if ( ! incomingConnections.length ) {
return true;
}

const staticDependentModules = incomingConnections.flatMap(
( connection ) => {
const { dependency } = connection;
const parentBlock =
compilation.moduleGraph.getParentBlock( dependency );

return parentBlock.constructor.name !==
AsyncDependenciesBlock.name
? [ compilation.moduleGraph.getParentModule( dependency ) ]
: [];
}
);

// All the dependencies were Async, the module was reached via a dynamic import
if ( ! staticDependentModules.length ) {
return false;
}

// Continue to explore any static dependencies
return staticDependentModules.some( ( parentStaticDependentModule ) =>
AssetDataPlugin.hasStaticDependencyPathToRoot(
compilation,
parentStaticDependentModule
)
);
}
}

module.exports = AssetDataPlugin;
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Significance: patch
Type: dev

Improve webpack cache-busting version parameter by using file contents hash
6 changes: 5 additions & 1 deletion packages/js/product-editor/webpack.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,10 @@ const WebpackRTLPlugin = require( 'webpack-rtl-plugin' );
/**
* Internal dependencies
*/
const { webpackConfig } = require( '@woocommerce/internal-style-build' );
const {
webpackConfig,
StyleAssetPlugin,
} = require( '@woocommerce/internal-style-build' );
const {
blockEntryPoints,
getBlockMetaData,
Expand Down Expand Up @@ -69,5 +72,6 @@ module.exports = {
},
],
} ),
new StyleAssetPlugin(),
],
};
23 changes: 0 additions & 23 deletions plugins/woocommerce-admin/client/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,29 +21,6 @@ import { WcAdminConflictErrorSlot } from './settings/conflict-error-slotfill.js'
import './xstate.js';
import { deriveWpAdminBackgroundColours } from './utils/derive-wp-admin-background-colours';

// Modify webpack pubilcPath at runtime based on location of WordPress Plugin.
// eslint-disable-next-line no-undef,camelcase
__webpack_public_path__ = global.wcAdminAssets.path;

// Modify webpack to append the ?ver parameter to JS chunk
// https://webpack.js.org/api/module-variables/#__webpack_get_script_filename__-webpack-specific
// eslint-disable-next-line no-undef,camelcase
const oldGetScriptFileNameFn = __webpack_get_script_filename__;
// eslint-disable-next-line no-undef,camelcase
__webpack_get_script_filename__ = ( chunk ) => {
const filename = oldGetScriptFileNameFn( chunk );
return `${ filename }?ver=${ window.wcAdminAssets.version }`;
};

// Modify webpack to append the ?ver parameter to CSS chunk hrefs generated by mini-css-extract-plugin
// eslint-disable-next-line no-undef,camelcase
const oldMinCssFn = __webpack_require__.miniCssF;
// eslint-disable-next-line no-undef,camelcase
__webpack_require__.miniCssF = ( chunkId ) => {
const filename = oldMinCssFn( chunkId );
return `${ filename }?ver=${ window.wcAdminAssets.version }`;
};

const appRoot = document.getElementById( 'root' );
const embeddedRoot = document.getElementById( 'woocommerce-embedded-root' );
const settingsGroup = 'wc_admin';
Expand Down
6 changes: 4 additions & 2 deletions plugins/woocommerce-admin/unminify.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
* 2. Remove check for development mode - we always want unminified files.
* 3. Remove BannerPlugin support - we don't use it.
* 4. Remove the 'min' suffix from the chunk loaded in the new `mainEntry` option.
* 5. Hook into compilation later so we're running after Source Map generation. (https://webpack.js.org/api/compilation-hooks/: PROCESS_ASSETS_STAGE_OPTIMIZE_INLINE)
* 5. Hook into compilation later so we're running after Source Map generation. (https://webpack.js.org/api/compilation-hooks/: PROCESS_ASSETS_STAGE_OPTIMIZE_INLINE)
*/
const path = require( 'path' );
const ModuleFilenameHelpers = require( 'webpack/lib/ModuleFilenameHelpers' );
Expand Down Expand Up @@ -39,7 +39,7 @@ class UnminifyWebpackPlugin {

apply( compiler ) {
const options = this.options;
const outputNormal = {};
let outputNormal = {};

compiler.hooks.compilation.tap(
'UnminifyWebpackPlugin',
Expand Down Expand Up @@ -107,6 +107,8 @@ class UnminifyWebpackPlugin {
value.filename,
new webpack.sources.RawSource( value.content )
);
// Reset the outputNormal object to avoid writing to file that only differs in casing or query string from already written file.
outputNormal = {};
}
}
);
Expand Down
5 changes: 3 additions & 2 deletions plugins/woocommerce-admin/webpack.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ const webpackConfig = {
? `wp-admin-scripts/[name]${ outputSuffix }.js`
: `[name]/index${ outputSuffix }.js`;
},
chunkFilename: `chunks/[name]${ outputSuffix }.js`,
chunkFilename: `chunks/[name]${ outputSuffix }.js?ver=[contenthash]`,
path: path.join( __dirname, '/build' ),
library: {
// Expose the exports of entry points so we can consume the libraries in window.wc.[modulename] with WooCommerceDependencyExtractionWebpackPlugin.
Expand Down Expand Up @@ -200,7 +200,8 @@ const webpackConfig = {
// The package build process doesn't handle extracting CSS from JS files, so we copy them separately.
new CopyWebpackPlugin( {
patterns: wcAdminPackages.map( ( packageName ) => ( {
from: `../../packages/js/${ packageName }/build-style/*.css`,
// Copy css and style.asset.php files.
from: `../../packages/js/${ packageName }/build-style/*.{css,php}`,
to: `./${ packageName }/[name][ext]`,
noErrorOnMissing: true,
// Overwrites files already in compilation.assets to ensure we use the assets from the build-style.
Expand Down
4 changes: 4 additions & 0 deletions plugins/woocommerce/changelog/44838-dev-use-content-hash
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Significance: patch
Type: dev

Improve webpack cache-busting version parameter by using file contents hash
Original file line number Diff line number Diff line change
Expand Up @@ -586,7 +586,7 @@ private function enqueue_block_editor_script( $script_path_name, $script_name )
'wc-admin-' . $script_name,
WCAdminAssets::get_url( $script_path_name . '/' . $script_name, 'js' ),
$script_assets['dependencies'],
WCAdminAssets::get_file_version( 'js' ),
WCAdminAssets::get_file_version( 'js', $script_assets['version'] ),
true
);
}
Expand Down