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
This commit is contained in:
Greg Ziółkowski 2024-09-24 07:33:55 +00:00
parent a39079946a
commit 0b8b80449f
11 changed files with 193 additions and 95 deletions

1
.gitignore vendored
View File

@ -31,6 +31,7 @@ wp-tests-config.php
/src/wp-admin/js /src/wp-admin/js
/src/wp-includes/assets/* /src/wp-includes/assets/*
!/src/wp-includes/assets/script-loader-packages.min.php !/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/js
/src/wp-includes/css/dist /src/wp-includes/css/dist
/src/wp-includes/css/*.min.css /src/wp-includes/css/*.min.css

View File

@ -56,6 +56,7 @@ module.exports = function(grunt) {
'wp-includes/css/dist', 'wp-includes/css/dist',
'wp-includes/blocks/**/*.css', 'wp-includes/blocks/**/*.css',
'!wp-includes/assets/script-loader-packages.min.php', '!wp-includes/assets/script-loader-packages.min.php',
'!wp-includes/assets/script-modules-packages.min.php',
], ],
// Prepend `dir` to `file`, and keep `!` in place. // Prepend `dir` to `file`, and keep `!` in place.

View File

@ -0,0 +1 @@
<?php return array('interactivity/index.min.js' => 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'));

View File

@ -570,6 +570,7 @@ add_action( 'set_current_user', 'kses_init' );
// Script Loader. // Script Loader.
add_action( 'wp_default_scripts', 'wp_default_scripts' ); add_action( 'wp_default_scripts', 'wp_default_scripts' );
add_action( 'wp_default_scripts', 'wp_default_packages' ); 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_localize_jquery_ui_datepicker', 1000 );
add_action( 'wp_enqueue_scripts', 'wp_common_block_scripts_and_styles' ); add_action( 'wp_enqueue_scripts', 'wp_common_block_scripts_and_styles' );

View File

@ -281,33 +281,20 @@ final class WP_Interactivity_API {
/** /**
* Registers the `@wordpress/interactivity` script modules. * 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 * @since 6.5.0
*/ */
public function register_script_modules() { public function register_script_modules() {
$suffix = wp_scripts_get_suffix(); _deprecated_function( __METHOD__, '6.7.0', 'wp_default_script_modules' );
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' )
);
} }
/** /**
* Adds the necessary hooks for the Interactivity API. * Adds the necessary hooks for the Interactivity API.
* *
* @since 6.5.0 * @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() { 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' ) ); add_filter( 'script_module_data_@wordpress/interactivity', array( $this, 'filter_script_module_interactivity_data' ) );
} }

View File

@ -123,3 +123,53 @@ function wp_dequeue_script_module( string $id ) {
function wp_deregister_script_module( string $id ) { function wp_deregister_script_module( string $id ) {
wp_script_modules()->deregister( $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'] );
}
}

View File

@ -211,6 +211,17 @@ class Tests_Interactivity_API_WpInteractivityAPI extends WP_UnitTestCase {
$this->expectOutputString( '' ); $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. * 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 { private function get_script_data_filter_result( ?Closure $callback = null ): MockAction {
$this->interactivity->add_hooks(); $this->interactivity->add_hooks();
$this->interactivity->register_script_modules();
wp_enqueue_script_module( '@wordpress/interactivity' ); wp_enqueue_script_module( '@wordpress/interactivity' );
$filter = new MockAction(); $filter = new MockAction();
add_filter( 'script_module_data_@wordpress/interactivity', array( $filter, 'filter' ) ); add_filter( 'script_module_data_@wordpress/interactivity', array( $filter, 'filter' ) );

View File

@ -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;
};

View File

@ -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<string, string>} */
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;
};

View File

@ -102,6 +102,9 @@ const MODULES = [
'@wordpress/interactivity', '@wordpress/interactivity',
'@wordpress/interactivity-router', '@wordpress/interactivity-router',
]; ];
const SCRIPT_AND_MODULE_DUAL_PACKAGES = [
'@wordpress/block-library',
];
const WORDPRESS_NAMESPACE = '@wordpress/'; const WORDPRESS_NAMESPACE = '@wordpress/';
module.exports = { module.exports = {
@ -111,5 +114,6 @@ module.exports = {
stylesTransform, stylesTransform,
BUNDLED_PACKAGES, BUNDLED_PACKAGES,
MODULES, MODULES,
SCRIPT_AND_MODULE_DUAL_PACKAGES,
WORDPRESS_NAMESPACE, WORDPRESS_NAMESPACE,
}; };

View File

@ -2,7 +2,7 @@ const blocksConfig = require( './tools/webpack/blocks' );
const developmentConfig = require( './tools/webpack/development' ); const developmentConfig = require( './tools/webpack/development' );
const mediaConfig = require( './tools/webpack/media' ); const mediaConfig = require( './tools/webpack/media' );
const packagesConfig = require( './tools/webpack/packages' ); 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' ); const vendorsConfig = require( './tools/webpack/vendors' );
module.exports = function( env = { environment: "production", watch: false, buildTarget: false } ) { 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 ), ...developmentConfig( env ),
mediaConfig( env ), mediaConfig( env ),
packagesConfig( env ), packagesConfig( env ),
modulesConfig( env ), scriptModulesConfig( env ),
...vendorsConfig( env ), ...vendorsConfig( env ),
]; ];