diff --git a/.gitignore b/.gitignore index f35d743ea866f..7610ba4d99469 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 a924445df3e0c..70f826ac0e33c 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 0000000000000..204c67f1be50c --- /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 dfa8cab48cc6f..adc843f0c55d3 100644 --- a/src/wp-includes/default-filters.php +++ b/src/wp-includes/default-filters.php @@ -570,6 +570,7 @@ // 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 b552d07938e07..1213a5c097440 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 @@ public function get_context( ?string $store_namespace = null ): array { /** * 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 c873e5d8359cd..80831f1a54a67 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 e9349190ebdb2..8d190a93aafc1 100644 --- a/tests/phpunit/tests/interactivity-api/wpInteractivityAPI.php +++ b/tests/phpunit/tests/interactivity-api/wpInteractivityAPI.php @@ -211,6 +211,17 @@ public function test_config_not_printed_when_empty() { $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 @@ public function test_config_not_printed_when_empty() { */ 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 e365942b85c24..0000000000000 --- 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 0000000000000..2975434d6be2e --- /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 9cfd335176492..b446b0e0028cd 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 40f9b7e53bbb2..53ef8bd9ac4d9 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 ), ];