From 0b8b80449fb25e0242ad53262fcbabc08ea3ecb9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Greg=20Zi=C3=83=C2=B3=C3=85=E2=80=9Akowski?= Date: Tue, 24 Sep 2024 07:33:55 +0000 Subject: [PATCH] Build: Prepare for more Script Modules This is a companion to https://github.com/WordPress/gutenberg/pull/65460 that requires syncing in WordPress Core. Namely, the block-library changes require registration with their updated script module IDs so that the blocks continue to work correctly. They key improvement is script modules registration is handled in one central place, and a combined asset file is used to improve the performance by avoiding multiple disk operations for every individual file. Props jonsurrell, gziolo, wildworks, noisysocks. See #60647, #59462. git-svn-id: https://develop.svn.wordpress.org/trunk@59083 602fd350-edb4-49c9-b593-d223f7449a82 --- .gitignore | 1 + Gruntfile.js | 1 + .../assets/script-modules-packages.min.php | 1 + src/wp-includes/default-filters.php | 1 + .../class-wp-interactivity-api.php | 19 +-- src/wp-includes/script-modules.php | 50 ++++++++ .../interactivity-api/wpInteractivityAPI.php | 12 +- tools/webpack/modules.js | 76 ----------- tools/webpack/script-modules.js | 119 ++++++++++++++++++ tools/webpack/shared.js | 4 + webpack.config.js | 4 +- 11 files changed, 193 insertions(+), 95 deletions(-) create mode 100644 src/wp-includes/assets/script-modules-packages.min.php delete mode 100644 tools/webpack/modules.js create mode 100644 tools/webpack/script-modules.js diff --git a/.gitignore b/.gitignore index f35d743ea8..7610ba4d99 100644 --- a/.gitignore +++ b/.gitignore @@ -31,6 +31,7 @@ wp-tests-config.php /src/wp-admin/js /src/wp-includes/assets/* !/src/wp-includes/assets/script-loader-packages.min.php +!/src/wp-includes/assets/script-modules-packages.min.php /src/wp-includes/js /src/wp-includes/css/dist /src/wp-includes/css/*.min.css diff --git a/Gruntfile.js b/Gruntfile.js index a924445df3..70f826ac0e 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -56,6 +56,7 @@ module.exports = function(grunt) { 'wp-includes/css/dist', 'wp-includes/blocks/**/*.css', '!wp-includes/assets/script-loader-packages.min.php', + '!wp-includes/assets/script-modules-packages.min.php', ], // Prepend `dir` to `file`, and keep `!` in place. diff --git a/src/wp-includes/assets/script-modules-packages.min.php b/src/wp-includes/assets/script-modules-packages.min.php new file mode 100644 index 0000000000..204c67f1be --- /dev/null +++ b/src/wp-includes/assets/script-modules-packages.min.php @@ -0,0 +1 @@ + array('dependencies' => array(), 'version' => '2d6d1fdbcb3fda39c768', 'type' => 'module'), 'interactivity/debug.min.js' => array('dependencies' => array(), 'version' => '1ccc67b05c275e51a8f8', 'type' => 'module'), 'interactivity-router/index.min.js' => array('dependencies' => array('@wordpress/interactivity'), 'version' => '64645ef3cd2d32860d7d', 'type' => 'module'), 'block-library/file/view.min.js' => array('dependencies' => array('@wordpress/interactivity'), 'version' => 'fdc2f6842e015af83140', 'type' => 'module'), 'block-library/image/view.min.js' => array('dependencies' => array('@wordpress/interactivity'), 'version' => 'acfec7b3c0be4a859b31', 'type' => 'module'), 'block-library/navigation/view.min.js' => array('dependencies' => array('@wordpress/interactivity'), 'version' => '8ff192874fc8910a284c', 'type' => 'module'), 'block-library/query/view.min.js' => array('dependencies' => array('@wordpress/interactivity', array('id' => '@wordpress/interactivity-router', 'import' => 'dynamic')), 'version' => 'f4c91c89fa5271f3dad9', 'type' => 'module'), 'block-library/search/view.min.js' => array('dependencies' => array('@wordpress/interactivity'), 'version' => '2a73400a693958f604de', 'type' => 'module')); diff --git a/src/wp-includes/default-filters.php b/src/wp-includes/default-filters.php index dfa8cab48c..adc843f0c5 100644 --- a/src/wp-includes/default-filters.php +++ b/src/wp-includes/default-filters.php @@ -570,6 +570,7 @@ add_action( 'set_current_user', 'kses_init' ); // Script Loader. add_action( 'wp_default_scripts', 'wp_default_scripts' ); add_action( 'wp_default_scripts', 'wp_default_packages' ); +add_action( 'wp_default_scripts', 'wp_default_script_modules' ); add_action( 'wp_enqueue_scripts', 'wp_localize_jquery_ui_datepicker', 1000 ); add_action( 'wp_enqueue_scripts', 'wp_common_block_scripts_and_styles' ); diff --git a/src/wp-includes/interactivity-api/class-wp-interactivity-api.php b/src/wp-includes/interactivity-api/class-wp-interactivity-api.php index b552d07938..1213a5c097 100644 --- a/src/wp-includes/interactivity-api/class-wp-interactivity-api.php +++ b/src/wp-includes/interactivity-api/class-wp-interactivity-api.php @@ -281,33 +281,20 @@ final class WP_Interactivity_API { /** * Registers the `@wordpress/interactivity` script modules. * + * @deprecated 6.7.0 Script Modules registration is handled by {@see wp_default_script_modules()}. + * * @since 6.5.0 */ public function register_script_modules() { - $suffix = wp_scripts_get_suffix(); - - wp_register_script_module( - '@wordpress/interactivity', - includes_url( "js/dist/interactivity$suffix.js" ) - ); - - wp_register_script_module( - '@wordpress/interactivity-router', - includes_url( "js/dist/interactivity-router$suffix.js" ), - array( '@wordpress/interactivity' ) - ); + _deprecated_function( __METHOD__, '6.7.0', 'wp_default_script_modules' ); } /** * Adds the necessary hooks for the Interactivity API. * * @since 6.5.0 - * @since 6.7.0 Use the {@see "script_module_data_{$module_id}"} filter to pass client-side data. */ public function add_hooks() { - add_action( 'wp_enqueue_scripts', array( $this, 'register_script_modules' ) ); - add_action( 'admin_enqueue_scripts', array( $this, 'register_script_modules' ) ); - add_filter( 'script_module_data_@wordpress/interactivity', array( $this, 'filter_script_module_interactivity_data' ) ); } diff --git a/src/wp-includes/script-modules.php b/src/wp-includes/script-modules.php index c873e5d835..80831f1a54 100644 --- a/src/wp-includes/script-modules.php +++ b/src/wp-includes/script-modules.php @@ -123,3 +123,53 @@ function wp_dequeue_script_module( string $id ) { function wp_deregister_script_module( string $id ) { wp_script_modules()->deregister( $id ); } + +/** + * Registers all the default WordPress Script Modules. + * + * @since 6.7.0 + */ +function wp_default_script_modules() { + $suffix = defined( 'WP_RUN_CORE_TESTS' ) ? '.min' : wp_scripts_get_suffix(); + + /* + * Expects multidimensional array like: + * + * 'interactivity/index.min.js' => array('dependencies' => array(…), 'version' => '…'), + * 'interactivity/debug.min.js' => array('dependencies' => array(…), 'version' => '…'), + * 'interactivity-router/index.min.js' => … + */ + $assets = include ABSPATH . WPINC . "/assets/script-modules-packages{$suffix}.php"; + + foreach ( $assets as $file_name => $script_module_data ) { + /* + * Build the WordPress Script Module ID from the file name. + * Prepend `@wordpress/` and remove extensions and `/index` if present: + * - interactivity/index.min.js => @wordpress/interactivity + * - interactivity/debug.min.js => @wordpress/interactivity/debug + * - block-library/query/view.js => @wordpress/block-library/query/view + */ + $script_module_id = '@wordpress/' . preg_replace( '~(?:/index)?(?:\.min)?\.js$~D', '', $file_name, 1 ); + + switch ( $script_module_id ) { + /* + * Interactivity exposes two entrypoints, "/index" and "/debug". + * "/debug" should replalce "/index" in devlopment. + */ + case '@wordpress/interactivity/debug': + if ( ! SCRIPT_DEBUG ) { + continue 2; + } + $script_module_id = '@wordpress/interactivity'; + break; + case '@wordpress/interactivity': + if ( SCRIPT_DEBUG ) { + continue 2; + } + break; + } + + $path = "/wp-includes/js/dist/script-modules/{$file_name}"; + wp_register_script_module( $script_module_id, $path, $script_module_data['dependencies'], $script_module_data['version'] ); + } +} diff --git a/tests/phpunit/tests/interactivity-api/wpInteractivityAPI.php b/tests/phpunit/tests/interactivity-api/wpInteractivityAPI.php index e9349190eb..8d190a93aa 100644 --- a/tests/phpunit/tests/interactivity-api/wpInteractivityAPI.php +++ b/tests/phpunit/tests/interactivity-api/wpInteractivityAPI.php @@ -211,6 +211,17 @@ class Tests_Interactivity_API_WpInteractivityAPI extends WP_UnitTestCase { $this->expectOutputString( '' ); } + /** + * Test that the deprecated register_script_modules method is deprecated but does not throw. + * + * @ticket 60647 + * + * @expectedDeprecated WP_Interactivity_API::register_script_modules + */ + public function test_register_script_modules_deprecated() { + $this->interactivity->register_script_modules(); + } + /** * Sets up an activity, runs an optional callback, and returns a MockAction for inspection. * @@ -221,7 +232,6 @@ class Tests_Interactivity_API_WpInteractivityAPI extends WP_UnitTestCase { */ private function get_script_data_filter_result( ?Closure $callback = null ): MockAction { $this->interactivity->add_hooks(); - $this->interactivity->register_script_modules(); wp_enqueue_script_module( '@wordpress/interactivity' ); $filter = new MockAction(); add_filter( 'script_module_data_@wordpress/interactivity', array( $filter, 'filter' ) ); diff --git a/tools/webpack/modules.js b/tools/webpack/modules.js deleted file mode 100644 index e365942b85..0000000000 --- a/tools/webpack/modules.js +++ /dev/null @@ -1,76 +0,0 @@ -/** - * WordPress dependencies - */ -const DependencyExtractionPlugin = require( '@wordpress/dependency-extraction-webpack-plugin' ); - -/** - * Internal dependencies - */ -const { - baseDir, - getBaseConfig, - normalizeJoin, - MODULES, - WORDPRESS_NAMESPACE, -} = require( './shared' ); - -module.exports = function ( - env = { environment: 'production', watch: false, buildTarget: false } -) { - const mode = env.environment; - const suffix = mode === 'production' ? '.min' : ''; - let buildTarget = env.buildTarget - ? env.buildTarget - : mode === 'production' - ? 'build' - : 'src'; - buildTarget = buildTarget + '/wp-includes'; - - const baseConfig = getBaseConfig( env ); - const config = { - ...baseConfig, - entry: MODULES.map( ( packageName ) => - packageName.replace( WORDPRESS_NAMESPACE, '' ) - ).reduce( ( memo, packageName ) => { - const path = - 'development' === mode && 'interactivity' === packageName - ? 'interactivity/build-module/debug' - : packageName; - memo[ packageName ] = { - import: normalizeJoin( - baseDir, - `node_modules/@wordpress/${ path }` - ), - }; - - return memo; - }, {} ), - experiments: { - outputModule: true, - }, - output: { - devtoolNamespace: 'wp', - filename: `[name]${ suffix }.js`, - path: normalizeJoin( baseDir, `${ buildTarget }/js/dist` ), - library: { - type: 'module', - }, - environment: { module: true }, - }, - externalsType: 'module', - externals: { - '@wordpress/interactivity': '@wordpress/interactivity', - '@wordpress/interactivity-router': - 'import @wordpress/interactivity-router', - }, - plugins: [ - ...baseConfig.plugins, - new DependencyExtractionPlugin( { - injectPolyfill: false, - useDefaults: false, - } ), - ], - }; - - return config; -}; diff --git a/tools/webpack/script-modules.js b/tools/webpack/script-modules.js new file mode 100644 index 0000000000..2975434d6b --- /dev/null +++ b/tools/webpack/script-modules.js @@ -0,0 +1,119 @@ +/** + * External dependencies + */ +const { createRequire } = require( 'node:module' ); +const { dirname } = require( 'node:path' ); + +/** + * WordPress dependencies + */ +const DependencyExtractionPlugin = require( '@wordpress/dependency-extraction-webpack-plugin' ); + +/** + * Internal dependencies + */ +const { + baseDir, + getBaseConfig, + normalizeJoin, + MODULES, + SCRIPT_AND_MODULE_DUAL_PACKAGES, + WORDPRESS_NAMESPACE, +} = require( './shared' ); + +/** @type {Map} */ +const scriptModules = new Map(); +for ( const packageName of MODULES.concat( SCRIPT_AND_MODULE_DUAL_PACKAGES ) ) { + const packageRequire = createRequire( + `${ dirname( require.resolve( `${ packageName }/package.json` ) ) }/` + ); + + const depPackageJson = packageRequire( './package.json' ); + if ( ! Object.hasOwn( depPackageJson, 'wpScriptModuleExports' ) ) { + continue; + } + + const moduleName = packageName.substring( WORDPRESS_NAMESPACE.length ); + let { wpScriptModuleExports } = depPackageJson; + + // Special handling for { "wpScriptModuleExports": "./build-module/index.js" }. + if ( typeof wpScriptModuleExports === 'string' ) { + wpScriptModuleExports = { '.': wpScriptModuleExports }; + } + + if ( Object.getPrototypeOf( wpScriptModuleExports ) !== Object.prototype ) { + throw new Error( 'wpScriptModuleExports must be an object' ); + } + + for ( const [ exportName, exportPath ] of Object.entries( + wpScriptModuleExports + ) ) { + if ( typeof exportPath !== 'string' ) { + throw new Error( 'wpScriptModuleExports paths must be strings' ); + } + + if ( ! exportPath.startsWith( './' ) ) { + throw new Error( + 'wpScriptModuleExports paths must start with "./"' + ); + } + + const name = + exportName === '.' ? 'index' : exportName.replace( /^\.\/?/, '' ); + + scriptModules.set( + `${ moduleName }/${ name }`, + packageRequire.resolve( exportPath ) + ); + } +} + +module.exports = function ( + env = { environment: 'production', watch: false, buildTarget: false } +) { + const mode = env.environment; + const suffix = mode === 'production' ? '.min' : ''; + let buildTarget = env.buildTarget + ? env.buildTarget + : mode === 'production' + ? 'build' + : 'src'; + buildTarget = buildTarget + '/wp-includes'; + + const baseConfig = getBaseConfig( env ); + const config = { + ...baseConfig, + entry: Object.fromEntries( scriptModules.entries() ), + experiments: { + outputModule: true, + }, + output: { + devtoolNamespace: 'wp', + filename: `[name]${ suffix }.js`, + path: normalizeJoin( + baseDir, + `${ buildTarget }/js/dist/script-modules` + ), + library: { + type: 'module', + }, + environment: { module: true }, + module: true, + chunkFormat: 'module', + asyncChunks: false, + }, + plugins: [ + ...baseConfig.plugins, + new DependencyExtractionPlugin( { + injectPolyfill: false, + combineAssets: true, + combinedOutputFile: normalizeJoin( + baseDir, + `${ buildTarget }/assets/script-modules-packages${ suffix }.php` + ), + } ), + ], + }; + + return config; +}; diff --git a/tools/webpack/shared.js b/tools/webpack/shared.js index 9cfd335176..b446b0e002 100644 --- a/tools/webpack/shared.js +++ b/tools/webpack/shared.js @@ -102,6 +102,9 @@ const MODULES = [ '@wordpress/interactivity', '@wordpress/interactivity-router', ]; +const SCRIPT_AND_MODULE_DUAL_PACKAGES = [ + '@wordpress/block-library', +]; const WORDPRESS_NAMESPACE = '@wordpress/'; module.exports = { @@ -111,5 +114,6 @@ module.exports = { stylesTransform, BUNDLED_PACKAGES, MODULES, + SCRIPT_AND_MODULE_DUAL_PACKAGES, WORDPRESS_NAMESPACE, }; diff --git a/webpack.config.js b/webpack.config.js index 40f9b7e53b..53ef8bd9ac 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -2,7 +2,7 @@ const blocksConfig = require( './tools/webpack/blocks' ); const developmentConfig = require( './tools/webpack/development' ); const mediaConfig = require( './tools/webpack/media' ); const packagesConfig = require( './tools/webpack/packages' ); -const modulesConfig = require( './tools/webpack/modules' ); +const scriptModulesConfig = require( './tools/webpack/script-modules' ); const vendorsConfig = require( './tools/webpack/vendors' ); module.exports = function( env = { environment: "production", watch: false, buildTarget: false } ) { @@ -19,7 +19,7 @@ module.exports = function( env = { environment: "production", watch: false, buil ...developmentConfig( env ), mediaConfig( env ), packagesConfig( env ), - modulesConfig( env ), + scriptModulesConfig( env ), ...vendorsConfig( env ), ];