From 1e34129a7edaa4f6b889a9554ab299af46cb7e80 Mon Sep 17 00:00:00 2001 From: Adnan Haque Date: Sun, 16 Aug 2026 15:54:55 -0700 Subject: [PATCH 1/6] Script Loader: Disable script and style concatenation by default. Flips the fallback in `script_concat_settings()` so an undefined `CONCATENATE_SCRIPTS` resolves to `false` instead of `true`, making `load-scripts.php` and `load-styles.php` opt-in. Sites that want the previous behaviour can define the constant as `true`. The concatenation code paths are unchanged, and the surrounding guard already forces the global to `false` outside of `is_admin()` and `login_init`, so only wp-admin and the login screen are affected. See #57548. --- src/wp-includes/script-loader.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/wp-includes/script-loader.php b/src/wp-includes/script-loader.php index a364439f0abbb..6b87367be522a 100644 --- a/src/wp-includes/script-loader.php +++ b/src/wp-includes/script-loader.php @@ -4,7 +4,7 @@ * * Several constants are used to manage the loading, concatenating and compression of scripts and CSS: * define('SCRIPT_DEBUG', true); loads the development (non-minified) versions of all scripts and CSS, and disables compression and concatenation, - * define('CONCATENATE_SCRIPTS', false); disables compression and concatenation of scripts and CSS, + * define('CONCATENATE_SCRIPTS', true); enables concatenation of scripts and CSS in the admin and on the login screen (disabled by default), * define('COMPRESS_SCRIPTS', false); disables compression of scripts, * define('COMPRESS_CSS', false); disables compression of CSS, * define('ENFORCE_GZIP', true); forces gzip for compression (default is deflate). @@ -2476,7 +2476,7 @@ function script_concat_settings() { $can_compress_scripts = ! wp_installing() && get_site_option( 'can_compress_scripts' ); if ( ! isset( $concatenate_scripts ) ) { - $concatenate_scripts = defined( 'CONCATENATE_SCRIPTS' ) ? CONCATENATE_SCRIPTS : true; + $concatenate_scripts = defined( 'CONCATENATE_SCRIPTS' ) ? CONCATENATE_SCRIPTS : false; if ( ( ! is_admin() && ! did_action( 'login_init' ) ) || ( defined( 'SCRIPT_DEBUG' ) && SCRIPT_DEBUG ) ) { $concatenate_scripts = false; } From c20d7fca4d2e5929c6d679000f4a1aded297f894 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Tue, 6 Oct 2026 00:40:21 -0700 Subject: [PATCH 2/6] Update the wp_should_concatenate_admin_scripts() tests for the new default Two tests still expected an undefined `CONCATENATE_SCRIPTS` to mean concatenation unless `SCRIPT_DEBUG` is on. They passed when run from `src/`, where `SCRIPT_DEBUG` is on and turns concatenation off whatever the default, but would fail where it is off. They now expect no concatenation unless the constant is defined as true. Co-Authored-By: Claude Opus 5.5 --- .../tests/dependencies/wpShouldConcatenateAdminScripts.php | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/tests/phpunit/tests/dependencies/wpShouldConcatenateAdminScripts.php b/tests/phpunit/tests/dependencies/wpShouldConcatenateAdminScripts.php index 6030e3965314a..8a577edf3228b 100644 --- a/tests/phpunit/tests/dependencies/wpShouldConcatenateAdminScripts.php +++ b/tests/phpunit/tests/dependencies/wpShouldConcatenateAdminScripts.php @@ -74,8 +74,7 @@ public function test_constant_off(): void { } /** - * Tests the default when `CONCATENATE_SCRIPTS` is not defined, which is to concatenate unless - * `SCRIPT_DEBUG` is on. + * Tests the default when `CONCATENATE_SCRIPTS` is not defined, which is not to concatenate. * * @ticket 57548 */ @@ -84,7 +83,7 @@ public function test_default(): void { $this->markTestSkipped( 'CONCATENATE_SCRIPTS is defined.' ); } - $this->assertSame( ! SCRIPT_DEBUG, wp_should_concatenate_admin_scripts() ); + $this->assertFalse( wp_should_concatenate_admin_scripts() ); } /** @@ -96,7 +95,7 @@ public function test_filter(): void { $filter = new MockAction(); add_filter( 'wp_should_concatenate_admin_scripts', array( $filter, 'filter' ) ); - $this->assertSame( ! SCRIPT_DEBUG && ( ! defined( 'CONCATENATE_SCRIPTS' ) || CONCATENATE_SCRIPTS ), wp_should_concatenate_admin_scripts() ); + $this->assertSame( ! SCRIPT_DEBUG && defined( 'CONCATENATE_SCRIPTS' ) && CONCATENATE_SCRIPTS, wp_should_concatenate_admin_scripts() ); $this->assertSame( 1, $filter->get_call_count() ); add_filter( 'wp_should_concatenate_admin_scripts', '__return_true', 20 ); From 4e13d295f3f027b25edccbe0d2cad0ab545c2685 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Fri, 9 Oct 2026 16:03:33 -0700 Subject: [PATCH 3/6] Always register TinyMCE as separate files TinyMCE was registered as the prebuilt `wp-tinymce.js` bundle only when scripts were both concatenated and compressed, and otherwise as TinyMCE core plus the compat3x plugin. Both conditions are on their way out: concatenation is now off by default, and `$compress_scripts` has not compressed anything since PHP compression was removed from the loaders in r43580 and from `wp-tinymce.php` in r44651. TinyMCE is now registered as the separate files regardless, which is how it is served on the front end and in the admin by default. The bundle holds the modern theme and all 22 plugins, which TinyMCE otherwise loads itself only when an editor is initialized. The block editor loads TinyMCE on every screen but only initializes it for a Classic block, so most screens skip about 300 KB. The `$force_uncompressed` parameter is now unused. The script_concat_settings() call stays, since this is where an admin screen settles the concatenation settings, as the `wp_should_concatenate_admin_scripts` filter documents. A site can still register the bundle under the `wp-tinymce` handle itself. Co-Authored-By: Claude Opus 5.5 --- src/wp-includes/script-loader.php | 32 +++++----- tests/phpunit/tests/dependencies/scripts.php | 66 ++++++++++++++++++++ 2 files changed, 82 insertions(+), 16 deletions(-) diff --git a/src/wp-includes/script-loader.php b/src/wp-includes/script-loader.php index 7240f246f352e..803c50d7c65a8 100644 --- a/src/wp-includes/script-loader.php +++ b/src/wp-includes/script-loader.php @@ -37,35 +37,35 @@ /** * Registers TinyMCE scripts. * + * TinyMCE core and the compat3x plugin are registered as separate files. TinyMCE loads its theme + * and any other plugins itself when an editor is initialized. The `wp-tinymce.js` bundle of all of + * them is no longer registered, but it can still be registered in their place under the + * `wp-tinymce` handle. + * * @since 5.0.0 + * @since 7.2.0 The `wp-tinymce.js` bundle is no longer registered, regardless of whether scripts + * are concatenated or compressed, and the `$force_uncompressed` parameter is unused. * * @global string $tinymce_version - * @global bool $concatenate_scripts - * @global bool $compress_scripts * * @param WP_Scripts $scripts WP_Scripts object. - * @param bool $force_uncompressed Whether to forcibly prevent gzip compression. Default false. + * @param bool $force_uncompressed Unused. */ function wp_register_tinymce_scripts( $scripts, $force_uncompressed = false ) { - global $tinymce_version, $concatenate_scripts, $compress_scripts; + global $tinymce_version; $suffix = wp_scripts_get_suffix(); $dev_suffix = wp_scripts_get_suffix( 'dev' ); - script_concat_settings(); - - $compressed = $compress_scripts && $concatenate_scripts && ! $force_uncompressed; - /* - * Load tinymce.js when running from /src, otherwise load wp-tinymce.js (in production) - * or tinymce.min.js (when SCRIPT_DEBUG is true). + * This no longer depends on the concatenation settings, but it is where an admin screen has + * settled them until now, which the 'wp_should_concatenate_admin_scripts' filter documents. */ - if ( $compressed ) { - $scripts->add( 'wp-tinymce', includes_url( 'js/tinymce/' ) . 'wp-tinymce.js', array(), $tinymce_version ); - } else { - $scripts->add( 'wp-tinymce-root', includes_url( 'js/tinymce/' ) . "tinymce$dev_suffix.js", array(), $tinymce_version ); - $scripts->add( 'wp-tinymce', includes_url( 'js/tinymce/' ) . "plugins/compat3x/plugin$dev_suffix.js", array( 'wp-tinymce-root' ), $tinymce_version ); - } + script_concat_settings(); + + // Load tinymce.js when running from /src, otherwise tinymce.min.js. + $scripts->add( 'wp-tinymce-root', includes_url( 'js/tinymce/' ) . "tinymce$dev_suffix.js", array(), $tinymce_version ); + $scripts->add( 'wp-tinymce', includes_url( 'js/tinymce/' ) . "plugins/compat3x/plugin$dev_suffix.js", array( 'wp-tinymce-root' ), $tinymce_version ); $scripts->add( 'wp-tinymce-lists', includes_url( "js/tinymce/plugins/lists/plugin$suffix.js" ), array( 'wp-tinymce' ), $tinymce_version ); } diff --git a/tests/phpunit/tests/dependencies/scripts.php b/tests/phpunit/tests/dependencies/scripts.php index 000838f0fbd0c..ce37a80f33483 100644 --- a/tests/phpunit/tests/dependencies/scripts.php +++ b/tests/phpunit/tests/dependencies/scripts.php @@ -35,6 +35,11 @@ class Tests_Dependencies_Scripts extends WP_UnitTestCase { */ protected $old_concatenate_scripts; + /** + * @var mixed + */ + protected $old_compress_scripts; + /** * @var WP_Styles */ @@ -54,6 +59,7 @@ public function set_up() { $this->old_wp_scripts = $GLOBALS['wp_scripts'] ?? null; $this->old_wp_styles = $GLOBALS['wp_styles'] ?? null; $this->old_concatenate_scripts = $GLOBALS['concatenate_scripts'] ?? null; + $this->old_compress_scripts = $GLOBALS['compress_scripts'] ?? null; remove_action( 'wp_default_scripts', 'wp_default_scripts' ); remove_action( 'wp_default_scripts', 'wp_default_packages' ); $GLOBALS['wp_scripts'] = new WP_Scripts(); @@ -77,6 +83,7 @@ public function tear_down() { $GLOBALS['wp_scripts'] = $this->old_wp_scripts; $GLOBALS['wp_styles'] = $this->old_wp_styles; $GLOBALS['concatenate_scripts'] = $this->old_concatenate_scripts; + $GLOBALS['compress_scripts'] = $this->old_compress_scripts; add_action( 'wp_default_scripts', 'wp_default_scripts' ); parent::tear_down(); } @@ -3840,6 +3847,65 @@ public function test_printing_tinymce_scripts() { $this->assertStringNotContainsString( 'defer', $actual, 'TinyMCE should not have a defer attribute.' ); } + /** + * Tests that TinyMCE is registered as separate files, never as the `wp-tinymce.js` bundle. + * + * @ticket 57548 + * + * @covers ::wp_register_tinymce_scripts + * + * @dataProvider data_register_tinymce_scripts_unbundled + * + * @param bool $concatenate Value of the `$concatenate_scripts` global. + * @param bool $compress Value of the `$compress_scripts` global. + */ + public function test_register_tinymce_scripts_unbundled( bool $concatenate, bool $compress ): void { + global $concatenate_scripts, $compress_scripts; + + $concatenate_scripts = $concatenate; + $compress_scripts = $compress; + + $wp_scripts = wp_scripts(); + wp_register_tinymce_scripts( $wp_scripts ); + + $root = $wp_scripts->query( 'wp-tinymce-root' ); + $this->assertInstanceOf( _WP_Dependency::class, $root, 'Expected TinyMCE core to be registered.' ); + $this->assertIsString( $root->src ); + $this->assertMatchesRegularExpression( '#/js/tinymce/tinymce(\.min)?\.js$#', $root->src ); + + $tinymce = $wp_scripts->query( 'wp-tinymce' ); + $this->assertInstanceOf( _WP_Dependency::class, $tinymce, 'Expected the wp-tinymce handle to be registered.' ); + $this->assertIsString( $tinymce->src ); + $this->assertMatchesRegularExpression( '#/js/tinymce/plugins/compat3x/plugin(\.min)?\.js$#', $tinymce->src ); + $this->assertSame( array( 'wp-tinymce-root' ), $tinymce->deps ); + } + + /** + * Data provider for test_register_tinymce_scripts_unbundled(). + * + * @return array + */ + public function data_register_tinymce_scripts_unbundled(): array { + return array( + 'concatenated and compressed' => array( + 'concatenate' => true, + 'compress' => true, + ), + 'concatenated only' => array( + 'concatenate' => true, + 'compress' => false, + ), + 'compressed only' => array( + 'concatenate' => false, + 'compress' => true, + ), + 'neither' => array( + 'concatenate' => false, + 'compress' => false, + ), + ); + } + /** * Make sure scripts with a loading strategy that are printed * without being enqueued are handled properly. From a9adc336d3e19f5e09095fbecbd1d04123ca57a6 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Fri, 9 Oct 2026 23:52:46 -0700 Subject: [PATCH 4/6] Use wp_scripts() in the TinyMCE printing test The `$wp_scripts` global is typed as `mixed`, which PHPStan rejects as the `WP_Scripts` argument to wp_register_tinymce_scripts(). The wp_scripts() accessor returns a `WP_Scripts` instance, initializing the global first if needed. The `true` passed as the second argument is dropped too, since that parameter is no longer used. Co-Authored-By: Claude Opus 5.5 --- tests/phpunit/tests/dependencies/scripts.php | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/tests/phpunit/tests/dependencies/scripts.php b/tests/phpunit/tests/dependencies/scripts.php index ce37a80f33483..f80fd7c9c8e2c 100644 --- a/tests/phpunit/tests/dependencies/scripts.php +++ b/tests/phpunit/tests/dependencies/scripts.php @@ -3837,9 +3837,7 @@ static function () { * @ticket 58648 */ public function test_printing_tinymce_scripts() { - global $wp_scripts; - - wp_register_tinymce_scripts( $wp_scripts, true ); + wp_register_tinymce_scripts( wp_scripts() ); $actual = get_echo( 'wp_print_scripts', array( array( 'wp-tinymce' ) ) ); From 0e7d9ed202350db6b147ea2f19b17f8b5a34c1e8 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Fri, 9 Oct 2026 23:53:29 -0700 Subject: [PATCH 5/6] Deprecate the second parameter of wp_register_tinymce_scripts() Since TinyMCE is always registered as separate files, the `$force_uncompressed` parameter has nothing left to force. It is renamed to `$deprecated` and typed with `@phpstan-param false`, as for other deprecated parameters, so that PHPStan reports any caller still passing `true`. Passing a truthy value also triggers _deprecated_argument(), which uses the parameter and so resolves its `function.unusedParameter` error. _WP_Editors::force_uncompressed_tinymce() no longer passes `true`, so it does not trigger the notice. The signature also gains a `void` return type, which resolves the `missingType.return` error reported on the changed line. Co-Authored-By: Claude Opus 5.5 --- src/wp-includes/class-wp-editor.php | 2 +- src/wp-includes/script-loader.php | 15 +++++++++++---- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/src/wp-includes/class-wp-editor.php b/src/wp-includes/class-wp-editor.php index 5709dd2ef5c0b..2f1649a35a878 100644 --- a/src/wp-includes/class-wp-editor.php +++ b/src/wp-includes/class-wp-editor.php @@ -1541,7 +1541,7 @@ public static function force_uncompressed_tinymce() { $wp_scripts = wp_scripts(); $wp_scripts->remove( 'wp-tinymce' ); - wp_register_tinymce_scripts( $wp_scripts, true ); + wp_register_tinymce_scripts( $wp_scripts ); } /** diff --git a/src/wp-includes/script-loader.php b/src/wp-includes/script-loader.php index 803c50d7c65a8..aca7a0bea20ac 100644 --- a/src/wp-includes/script-loader.php +++ b/src/wp-includes/script-loader.php @@ -44,16 +44,23 @@ * * @since 5.0.0 * @since 7.2.0 The `wp-tinymce.js` bundle is no longer registered, regardless of whether scripts - * are concatenated or compressed, and the `$force_uncompressed` parameter is unused. + * are concatenated or compressed. + * @since 7.2.0 The `$force_uncompressed` parameter was deprecated and renamed to `$deprecated`. * * @global string $tinymce_version * - * @param WP_Scripts $scripts WP_Scripts object. - * @param bool $force_uncompressed Unused. + * @param WP_Scripts $scripts WP_Scripts object. + * @param bool $deprecated Not used. + * + * @phpstan-param false $deprecated */ -function wp_register_tinymce_scripts( $scripts, $force_uncompressed = false ) { +function wp_register_tinymce_scripts( $scripts, $deprecated = false ): void { global $tinymce_version; + if ( ! empty( $deprecated ) ) { + _deprecated_argument( __FUNCTION__, '7.2.0' ); + } + $suffix = wp_scripts_get_suffix(); $dev_suffix = wp_scripts_get_suffix( 'dev' ); From df55aff2ef083965b7515a63b464786cf0586ca1 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Sat, 10 Oct 2026 00:58:53 -0700 Subject: [PATCH 6/6] Strictly compare the deprecated TinyMCE parameter to false The deprecation notice in wp_register_tinymce_scripts() was triggered only for a non-empty `$deprecated`, so a caller passing `null`, `0` or an empty string went unnoticed. Since `false` is the only value the parameter accepts, as its `@phpstan-param` says, any other value now triggers the notice. Co-Authored-By: Claude Opus 5.5 --- src/wp-includes/script-loader.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/wp-includes/script-loader.php b/src/wp-includes/script-loader.php index aca7a0bea20ac..28772b6534337 100644 --- a/src/wp-includes/script-loader.php +++ b/src/wp-includes/script-loader.php @@ -57,7 +57,7 @@ function wp_register_tinymce_scripts( $scripts, $deprecated = false ): void { global $tinymce_version; - if ( ! empty( $deprecated ) ) { + if ( false !== $deprecated ) { _deprecated_argument( __FUNCTION__, '7.2.0' ); }