Commit 4f907fe225a for woocommerce

commit 4f907fe225a847849c0a639700f81461d6b30dd6
Author: Daniel Mallory <daniel.mallory@automattic.com>
Date:   Tue Apr 14 07:13:58 2026 +0100

    Cache admin and blocks webpack builds outside watch mode (#64080)

    * perf(build): cache webpack bundles outside watch mode

    * fix(build): address webpack cache review feedback

    * fix(build): refine webpack cache configuration

    * build(admin,blocks): skip filesystem cache in ci

    * Monorepo: fix webpack caching issues in admin.

    . build:project:bundle: orphan modules 12.2 MiB (javascript) 139 KiB (asset) 159 KiB (css/mini-extract) 30.3 KiB (runtime) [orphan] 6824 modules
    . build:project:bundle: runtime modules 269 KiB 1020 modules
    . build:project:bundle: built modules 12.3 MiB (javascript) 648 KiB (css/mini-extract) 417 KiB (asset) [built]

    vs

    . build:project:bundle: cached modules 16.5 MiB (javascript) 807 KiB (css/mini-extract) 448 KiB (asset) 80.2 KiB (runtime) [cached] 7852 modules
    . build:project:bundle: orphan modules 546 bytes [orphan] 13 modules
    . build:project:bundle: built modules 6.97 MiB [built]

    * Monorepo: fix webpack caching issues in admin.

    * Monorepo: fix webpack caching issues in blocks.

    * Monorepo: account for more dependencies in updated configurations.

    * Monorepo: cleanup.

    * Monorepo: cleanup.

    * Monorepo: cleanup.

    * Monorepo: cleanup.

    * Monorepo: account for more dependencies in updated configurations.

    * chore: update changelog entry for webpack filesystem cache

    ---------

    Co-authored-by: Vladimir Reznichenko <kalessil@gmail.com>

diff --git a/plugins/woocommerce/changelog/build-webpack-filesystem-cache b/plugins/woocommerce/changelog/build-webpack-filesystem-cache
new file mode 100644
index 00000000000..707d3dd60dc
--- /dev/null
+++ b/plugins/woocommerce/changelog/build-webpack-filesystem-cache
@@ -0,0 +1,4 @@
+Significance: minor
+Type: dev
+
+Improve repeated WooCommerce admin and blocks production builds by using webpack filesystem cache outside watch mode.
diff --git a/plugins/woocommerce/client/admin/bin/custom-templated-path-webpack-plugin.js b/plugins/woocommerce/client/admin/bin/custom-templated-path-webpack-plugin.js
index 00e69a4ea31..01ac82563bd 100644
--- a/plugins/woocommerce/client/admin/bin/custom-templated-path-webpack-plugin.js
+++ b/plugins/woocommerce/client/admin/bin/custom-templated-path-webpack-plugin.js
@@ -45,7 +45,7 @@ class CustomTemplatedPathPlugin {
 		compiler.hooks.compilation.tap(
 			'CustomTemplatedPathPlugin',
 			( compilation ) => {
-				compilation.mainTemplate.hooks.assetPath.tap(
+				compilation.hooks.assetPath.tap(
 					'CustomTemplatedPathPlugin',
 					( path, data ) => {
 						for ( let i = 0; i < this.handlers.length; i++ ) {
diff --git a/plugins/woocommerce/client/admin/bin/filesystem-cache-warnings-webpack-plugin.js b/plugins/woocommerce/client/admin/bin/filesystem-cache-warnings-webpack-plugin.js
new file mode 100644
index 00000000000..2e4df624c1c
--- /dev/null
+++ b/plugins/woocommerce/client/admin/bin/filesystem-cache-warnings-webpack-plugin.js
@@ -0,0 +1,21 @@
+// ExternalModule instances (wp.*, wc.* externals) are not serializable by webpack's PackFileCacheStrategy.
+// Suppress here since ignoreWarnings only affects compilation warnings, not infrastructure logs.
+
+function FilesystemCacheWarningsPlugin() {}
+
+FilesystemCacheWarningsPlugin.prototype.apply = function ( compiler ) {
+	compiler.hooks.infrastructureLog.tap(
+		'SuppressExternalModuleCacheWarning',
+		( name, type, args ) => {
+			if ( type === 'warn' && name === 'webpack.cache.PackFileCacheStrategy' ) {
+				return (
+					args[ 0 ]?.includes?.( 'No serializer registered for ModuleExternalInitFragment' ) ||
+					args[ 0 ]?.includes?.( 'No serializer registered for ExternalModule' ) ||
+					args[ 0 ]?.includes?.( 'No serializer registered for Warning' )
+				);
+			}
+		}
+	);
+};
+
+module.exports = FilesystemCacheWarningsPlugin;
diff --git a/plugins/woocommerce/client/admin/webpack.config.js b/plugins/woocommerce/client/admin/webpack.config.js
index c724e0d2035..d66e9b01c07 100644
--- a/plugins/woocommerce/client/admin/webpack.config.js
+++ b/plugins/woocommerce/client/admin/webpack.config.js
@@ -14,6 +14,7 @@ const ReactRefreshWebpackPlugin = require( '@pmmmwh/react-refresh-webpack-plugin
  * Internal dependencies
  */
 const CustomTemplatedPathPlugin = require( './bin/custom-templated-path-webpack-plugin' );
+const FilesystemCacheWarningsPlugin = require( './bin/filesystem-cache-warnings-webpack-plugin.js' );
 const UnminifyWebpackPlugin = require( './bin/unminify-webpack-plugin.js' );
 const {
 	webpackConfig: styleConfig,
@@ -24,6 +25,7 @@ const NODE_ENV = process.env.NODE_ENV || 'development';
 const WC_ADMIN_PHASE = process.env.WC_ADMIN_PHASE || 'development';
 const isHot = Boolean( process.env.HOT );
 const isProduction = NODE_ENV === 'production';
+const isWatch = ! isProduction && process.argv.includes( '--watch' );

 const getSubdirectoriesAt = ( searchPath ) => {
 	const dir = path.resolve( __dirname, searchPath );
@@ -90,6 +92,18 @@ require( 'fs-extra' ).ensureSymlinkSync(

 const webpackConfig = {
 	mode: NODE_ENV,
+	cache: ( isWatch || process.env.CI || process.env.HOT || process.env.STORYBOOK )
+		? { type: 'memory' }
+		: {
+				type: 'filesystem',
+				cacheDirectory: path.resolve(
+					__dirname,
+					`node_modules/.cache/webpack-${ WC_ADMIN_PHASE }`
+				),
+				buildDependencies: {
+					config: [ __filename ],
+				},
+		  },
 	entry: getEntryPoints(),
 	output: {
 		filename: ( data ) => {
@@ -277,6 +291,8 @@ const webpackConfig = {
 				test: /\.js($|\?)/i,
 				mainEntry: 'app/index.min.js',
 			} ),
+		// Suppress file system cache warnings (unsupported serialization related).
+		new FilesystemCacheWarningsPlugin(),
 	].filter( Boolean ),
 	optimization: {
 		minimize: NODE_ENV !== 'development',
diff --git a/plugins/woocommerce/client/blocks/bin/filesystem-cache-warnings-webpack-plugin.js b/plugins/woocommerce/client/blocks/bin/filesystem-cache-warnings-webpack-plugin.js
new file mode 100644
index 00000000000..7158a75e58a
--- /dev/null
+++ b/plugins/woocommerce/client/blocks/bin/filesystem-cache-warnings-webpack-plugin.js
@@ -0,0 +1,30 @@
+// ExternalModule instances (wp.*, wc.* externals) are not serializable by webpack's PackFileCacheStrategy.
+// Suppress here since ignoreWarnings only affects compilation warnings, not infrastructure logs.
+
+function FilesystemCacheWarningsPlugin() {}
+
+FilesystemCacheWarningsPlugin.prototype.apply = function ( compiler ) {
+	compiler.hooks.infrastructureLog.tap(
+		'SuppressExternalModuleCacheWarning',
+		( name, type, args ) => {
+			if (
+				type === 'warn' &&
+				name === 'webpack.cache.PackFileCacheStrategy'
+			) {
+				return (
+					args[ 0 ]?.includes?.(
+						'No serializer registered for ModuleExternalInitFragment'
+					) ||
+					args[ 0 ]?.includes?.(
+						'No serializer registered for ExternalModule'
+					) ||
+					args[ 0 ]?.includes?.(
+						'No serializer registered for Warning'
+					)
+				);
+			}
+		}
+	);
+};
+
+module.exports = FilesystemCacheWarningsPlugin;
diff --git a/plugins/woocommerce/client/blocks/bin/webpack-config-dependency-detection.js b/plugins/woocommerce/client/blocks/bin/webpack-config-dependency-detection.js
index f9a9c23743f..f2a8d78acdd 100644
--- a/plugins/woocommerce/client/blocks/bin/webpack-config-dependency-detection.js
+++ b/plugins/woocommerce/client/blocks/bin/webpack-config-dependency-detection.js
@@ -11,6 +11,7 @@ const TerserPlugin = require( 'terser-webpack-plugin' );
 /**
  * Internal dependencies
  */
+const FilesystemCacheWarningsPlugin = require( './filesystem-cache-warnings-webpack-plugin.js' );
 const { getProgressBarPluginConfig } = require( './webpack-helpers' );

 const ROOT_DIR = path.resolve( __dirname, '../../../../../' );
@@ -65,6 +66,8 @@ module.exports = {
 		new ProgressBarPlugin(
 			getProgressBarPluginConfig( 'Dependency Detection' )
 		),
+		// Suppress file system cache warnings (unsupported serialization related).
+		new FilesystemCacheWarningsPlugin(),
 	],
 	optimization: {
 		// Always minimize - this is an inline script embedded in page HTML.
diff --git a/plugins/woocommerce/client/blocks/bin/webpack-config-interactive-blocks.js b/plugins/woocommerce/client/blocks/bin/webpack-config-interactive-blocks.js
index 527ea850b56..e9f023fed18 100644
--- a/plugins/woocommerce/client/blocks/bin/webpack-config-interactive-blocks.js
+++ b/plugins/woocommerce/client/blocks/bin/webpack-config-interactive-blocks.js
@@ -14,6 +14,7 @@ const RemoveFilesPlugin = require( './remove-files-webpack-plugin' );
 /**
  * Internal dependencies
  */
+const FilesystemCacheWarningsPlugin = require( './filesystem-cache-warnings-webpack-plugin.js' );
 const { sharedOptimizationConfig } = require( './webpack-shared-config' );
 const {
 	scriptModuleEntries,
@@ -95,6 +96,8 @@ module.exports = {
 		} ),
 		// Remove JS files generated by MiniCssExtractPlugin.
 		new RemoveFilesPlugin( './build/**/*-@(editor|style).js' ),
+		// Suppress file system cache warnings (unsupported serialization related).
+		new FilesystemCacheWarningsPlugin(),
 	],
 	module: {
 		rules: [
diff --git a/plugins/woocommerce/client/blocks/bin/webpack-config-interactivity.js b/plugins/woocommerce/client/blocks/bin/webpack-config-interactivity.js
index 963cf6d948f..ad244e3ac70 100644
--- a/plugins/woocommerce/client/blocks/bin/webpack-config-interactivity.js
+++ b/plugins/woocommerce/client/blocks/bin/webpack-config-interactivity.js
@@ -8,6 +8,7 @@ const { DefinePlugin } = require( 'webpack' );
 /**
  * Internal dependencies
  */
+const FilesystemCacheWarningsPlugin = require( './filesystem-cache-warnings-webpack-plugin.js' );
 const { sharedOptimizationConfig } = require( './webpack-shared-config' );

 const { NODE_ENV: mode = 'development' } = process.env;
@@ -55,6 +56,8 @@ module.exports = {
 		new DefinePlugin( {
 			'globalThis.SCRIPT_DEBUG': JSON.stringify( mode === 'development' ),
 		} ),
+		// Suppress file system cache warnings (unsupported serialization related).
+		new FilesystemCacheWarningsPlugin(),
 	],
 	module: {
 		rules: [
diff --git a/plugins/woocommerce/client/blocks/bin/webpack-configs.js b/plugins/woocommerce/client/blocks/bin/webpack-configs.js
index 27b7d64ad22..fe9feda129b 100644
--- a/plugins/woocommerce/client/blocks/bin/webpack-configs.js
+++ b/plugins/woocommerce/client/blocks/bin/webpack-configs.js
@@ -16,6 +16,7 @@ const CopyWebpackPlugin = require( 'copy-webpack-plugin' );
 /**
  * Internal dependencies
  */
+const FilesystemCacheWarningsPlugin = require( './filesystem-cache-warnings-webpack-plugin.js' );
 const { getEntryConfig, genericBlocks } = require( './webpack-entries' );
 const {
 	ASSET_CHECK,
@@ -67,6 +68,8 @@ const getSharedPlugins = ( {
 			requestToExternal,
 			requestToHandle,
 		} ),
+		// Suppress file system cache warnings (unsupported serialization related).
+		new FilesystemCacheWarningsPlugin(),
 	].filter( Boolean );

 /**
diff --git a/plugins/woocommerce/client/blocks/webpack.config.js b/plugins/woocommerce/client/blocks/webpack.config.js
index 2c71a476c00..4b6eca5e4d1 100644
--- a/plugins/woocommerce/client/blocks/webpack.config.js
+++ b/plugins/woocommerce/client/blocks/webpack.config.js
@@ -1,3 +1,8 @@
+/**
+ * External dependencies
+ */
+const path = require( 'path' );
+
 /**
  * Internal dependencies
  */
@@ -16,6 +21,29 @@ const {
 const interactivityBlocksConfig = require( './bin/webpack-config-interactive-blocks.js' );
 const interactivityAPIConfig = require( './bin/webpack-config-interactivity.js' );
 const dependencyDetectionConfig = require( './bin/webpack-config-dependency-detection.js' );
+const isWatch =
+	NODE_ENV === 'development' && process.argv.includes( '--watch' );
+
+const getCacheConfig = ( name, configPaths = [] ) =>
+	isWatch || process.env.CI
+		? { type: 'memory' }
+		: {
+				type: 'filesystem',
+				cacheDirectory: path.resolve(
+					__dirname,
+					`node_modules/.cache/webpack-${ name }`
+				),
+				buildDependencies: {
+					config: [
+						__filename,
+						path.resolve( __dirname, 'bin/webpack-configs.js' ),
+						path.resolve( __dirname, 'bin/webpack-helpers.js' ),
+						...configPaths.map( ( configPath ) =>
+							path.resolve( __dirname, configPath )
+						),
+					],
+				},
+		  };

 // Only options shared between all configs should be defined here.
 const sharedConfig = {
@@ -40,26 +68,28 @@ const sharedConfig = {

 const CartAndCheckoutFrontendConfig = {
 	...sharedConfig,
+	cache: getCacheConfig( 'cart-and-checkout-frontend', [] ),
 	...getCartAndCheckoutFrontendConfig( { alias: getAlias() } ),
 };

 // Core config for shared libraries.
 const CoreConfig = {
 	...sharedConfig,
+	cache: getCacheConfig( 'core', [] ),
 	...getCoreConfig( { alias: getAlias() } ),
 };

 // Main Blocks config for registering Blocks and for the Editor.
 const MainConfig = {
 	...sharedConfig,
-	...getMainConfig( {
-		alias: getAlias(),
-	} ),
+	cache: getCacheConfig( 'main', [] ),
+	...getMainConfig( { alias: getAlias() } ),
 };

 // Frontend config for scripts used in the store itself.
 const FrontendConfig = {
 	...sharedConfig,
+	cache: getCacheConfig( 'frontend', [] ),
 	...getFrontConfig( { alias: getAlias() } ),
 };

@@ -68,6 +98,7 @@ const FrontendConfig = {
  */
 const ExtensionsConfig = {
 	...sharedConfig,
+	cache: getCacheConfig( 'extensions', [] ),
 	...getExtensionsConfig( { alias: getAlias() } ),
 };

@@ -76,6 +107,7 @@ const ExtensionsConfig = {
  */
 const PaymentsConfig = {
 	...sharedConfig,
+	cache: getCacheConfig( 'payments', [] ),
 	...getPaymentsConfig( { alias: getAlias() } ),
 };

@@ -84,6 +116,7 @@ const PaymentsConfig = {
  */
 const StylingConfig = {
 	...sharedConfig,
+	cache: getCacheConfig( 'styling', [] ),
 	...getStylingConfig( { alias: getAlias() } ),
 };

@@ -92,16 +125,23 @@ const StylingConfig = {
  */
 const SiteEditorConfig = {
 	...sharedConfig,
+	cache: getCacheConfig( 'site-editor', [] ),
 	...getSiteEditorConfig( { alias: getAlias() } ),
 };

 const InteractivityBlocksConfig = {
 	...sharedConfig,
+	cache: getCacheConfig( 'interactivity-blocks', [
+		'bin/webpack-config-interactive-blocks.js',
+	] ),
 	...interactivityBlocksConfig,
 };

 const InteractivityAPIConfig = {
 	...sharedConfig,
+	cache: getCacheConfig( 'interactivity-api', [
+		'bin/webpack-config-interactivity.js',
+	] ),
 	...interactivityAPIConfig,
 };

@@ -111,6 +151,9 @@ const InteractivityAPIConfig = {
  */
 const DependencyDetectionConfig = {
 	...sharedConfig,
+	cache: getCacheConfig( 'dependency-detection', [
+		'bin/webpack-config-dependency-detection.js',
+	] ),
 	...dependencyDetectionConfig,
 };