diff --git a/packages/boot/CHANGELOG.md b/packages/boot/CHANGELOG.md index 8dcb3ad6a4bc97..5f532fc75423a5 100644 --- a/packages/boot/CHANGELOG.md +++ b/packages/boot/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +### Enhancements + +- Load the editor lazily. The save/"review changes" UI no longer statically imports `@wordpress/editor`, so the editor (and its `block-editor`/`block-library`/`media-utils` dependencies) is only loaded on demand instead of eagerly on every view, including list views that never edit. [#79913](https://github.com/WordPress/gutenberg/pull/79913). + ## 0.17.0 (2026-07-01) ## 0.16.0 (2026-06-24) diff --git a/packages/boot/src/components/save-button/index.tsx b/packages/boot/src/components/save-button/index.tsx index 09d0cdfb6e3c14..484745c7000386 100644 --- a/packages/boot/src/components/save-button/index.tsx +++ b/packages/boot/src/components/save-button/index.tsx @@ -7,7 +7,6 @@ import { _n, __, sprintf } from '@wordpress/i18n'; import { store as coreStore } from '@wordpress/core-data'; import { displayShortcut, rawShortcut } from '@wordpress/keycodes'; import { check } from '@wordpress/icons'; -import { EntitiesSavedStates } from '@wordpress/editor'; import { Button, Modal } from '@wordpress/components'; import { Tooltip } from '@wordpress/ui'; @@ -16,6 +15,7 @@ import { Tooltip } from '@wordpress/ui'; */ import './style.scss'; import useSaveShortcut from '../save-panel/use-save-shortcut'; +import LazyEntitiesSavedStates from '../save-panel/lazy-entities-saved-states'; export default function SaveButton() { const [ isSaveViewOpen, setIsSaveViewOpened ] = useState( false ); @@ -120,7 +120,7 @@ export default function SaveButton() { onRequestClose={ () => setIsSaveViewOpened( false ) } size="small" > - setIsSaveViewOpened( false ) } variant="inline" /> diff --git a/packages/boot/src/components/save-panel/index.tsx b/packages/boot/src/components/save-panel/index.tsx index 32e654f95d3645..d6e0eee252b8ca 100644 --- a/packages/boot/src/components/save-panel/index.tsx +++ b/packages/boot/src/components/save-panel/index.tsx @@ -3,13 +3,13 @@ */ import { useState } from '@wordpress/element'; import { Modal } from '@wordpress/components'; -import { EntitiesSavedStates } from '@wordpress/editor'; import { __ } from '@wordpress/i18n'; /** * Internal dependencies */ import useSaveShortcut from './use-save-shortcut'; +import LazyEntitiesSavedStates from './lazy-entities-saved-states'; export default function SavePanel() { const [ isOpen, setIsOpen ] = useState< boolean >( false ); @@ -26,7 +26,7 @@ export default function SavePanel() { title={ __( 'Review changes' ) } size="small" > - setIsOpen( false ) } variant="inline" /> diff --git a/packages/boot/src/components/save-panel/lazy-entities-saved-states.tsx b/packages/boot/src/components/save-panel/lazy-entities-saved-states.tsx new file mode 100644 index 00000000000000..42764e51aa5b13 --- /dev/null +++ b/packages/boot/src/components/save-panel/lazy-entities-saved-states.tsx @@ -0,0 +1,35 @@ +/** + * External dependencies + */ +import type { ComponentProps } from 'react'; + +/** + * WordPress dependencies + */ +import { lazy, Suspense } from '@wordpress/element'; +import { Spinner } from '@wordpress/components'; + +/** + * `EntitiesSavedStates` lives in `@wordpress/editor`, a classic script that would + * otherwise be pulled into boot's eager dependencies and force the whole editor + * (block-editor, block-library, media-utils, …) to load on every view. + * + * Loading it through `@wordpress/lazy-editor` — a script module — defers the editor + * until the "Review changes" modal actually opens, which only happens while editing, + * when the editor has already been loaded by the canvas. + */ +const EntitiesSavedStates = lazy( () => + import( '@wordpress/lazy-editor' ).then( ( module ) => ( { + default: module.EntitiesSavedStates, + } ) ) +); + +export default function LazyEntitiesSavedStates( + props: ComponentProps< typeof EntitiesSavedStates > +) { + return ( + }> + + + ); +} diff --git a/packages/boot/src/components/save-panel/use-save-shortcut.ts b/packages/boot/src/components/save-panel/use-save-shortcut.ts index 6840aa7b5c23ae..90d2db3d199dba 100644 --- a/packages/boot/src/components/save-panel/use-save-shortcut.ts +++ b/packages/boot/src/components/save-panel/use-save-shortcut.ts @@ -7,12 +7,17 @@ import { store as keyboardShortcutsStore, } from '@wordpress/keyboard-shortcuts'; import { __ } from '@wordpress/i18n'; -import { useDispatch, useSelect } from '@wordpress/data'; +import { useDispatch, useSelect, useRegistry } from '@wordpress/data'; import { store as coreStore } from '@wordpress/core-data'; -import { store as editorStore } from '@wordpress/editor'; const shortcutName = 'core/boot/save'; +// The editor store is provided by the classic @wordpress/editor script, which is +// loaded lazily (via the canvas / lazy-editor). Referencing it by name here — rather +// than importing the store descriptor — keeps @wordpress/editor out of boot's eager +// dependency graph so the editor only loads when there is something to edit. +const EDITOR_STORE_NAME = 'core/editor'; + /** * Register the save keyboard shortcut in view mode. * @@ -24,11 +29,9 @@ export default function useSaveShortcut( { }: { openSavePanel: () => void; } ) { + const registry = useRegistry(); const { __experimentalGetDirtyEntityRecords, isSavingEntityRecord } = useSelect( coreStore ); - const { hasNonPostEntityChanges, isPostSavingLocked } = - useSelect( editorStore ); - const { savePost } = useDispatch( editorStore ); const { registerShortcut, unregisterShortcut } = useDispatch( keyboardShortcutsStore ); @@ -57,10 +60,14 @@ export default function useSaveShortcut( { if ( ! hasDirtyEntities || isSaving ) { return; } - if ( hasNonPostEntityChanges() ) { + // The editor store is only registered once the editor has been loaded. + // When it isn't present there is nothing post-related to save, so fall + // back to the entity-only save panel. + const editorSelectors = registry.select( EDITOR_STORE_NAME ); + if ( ! editorSelectors || editorSelectors.hasNonPostEntityChanges() ) { openSavePanel(); - } else if ( ! isPostSavingLocked() ) { - savePost(); + } else if ( ! editorSelectors.isPostSavingLocked() ) { + registry.dispatch( EDITOR_STORE_NAME ).savePost(); } } ); } diff --git a/packages/lazy-editor/CHANGELOG.md b/packages/lazy-editor/CHANGELOG.md index 5e904be26de314..2e17143300b2da 100644 --- a/packages/lazy-editor/CHANGELOG.md +++ b/packages/lazy-editor/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +### New Features + +- Export `EntitiesSavedStates` so consumers can render the editor's "review changes" UI on demand through this script module instead of statically depending on `@wordpress/editor`. [#79913](https://github.com/WordPress/gutenberg/pull/79913). + ## 1.16.0 (2026-07-01) ## 1.15.0 (2026-06-24) diff --git a/packages/lazy-editor/src/index.tsx b/packages/lazy-editor/src/index.tsx index 19248dbed8c8db..538a3f04258311 100644 --- a/packages/lazy-editor/src/index.tsx +++ b/packages/lazy-editor/src/index.tsx @@ -1,3 +1,11 @@ +/** + * WordPress dependencies + */ +// Re-exported through this script module so consumers (e.g. @wordpress/boot) can load +// the editor-backed "review changes" UI on demand instead of pulling the classic +// @wordpress/editor script into their eager dependencies. +export { EntitiesSavedStates } from '@wordpress/editor'; + /** * Internal dependencies */ diff --git a/packages/wp-build/CHANGELOG.md b/packages/wp-build/CHANGELOG.md index 8535bdeeccc495..42a2ccadbb67f6 100644 --- a/packages/wp-build/CHANGELOG.md +++ b/packages/wp-build/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +### Bug Fixes + +- Generated pages now enqueue the classic script dependencies declared by each route's content and route modules, merged into the prerequisites script. Previously a route relied on whatever classic scripts `@wordpress/boot` happened to load eagerly, so a route using e.g. `@wordpress/viewport` could break if those were not loaded. [#79913](https://github.com/WordPress/gutenberg/pull/79913). + ### Enhancements - Widgets: carry the optional metadata fields from `widget.json` into diff --git a/packages/wp-build/templates/page-wp-admin.php.template b/packages/wp-build/templates/page-wp-admin.php.template index 96c46272fbc971..a9bf6cee83e6eb 100644 --- a/packages/wp-build/templates/page-wp-admin.php.template +++ b/packages/wp-build/templates/page-wp-admin.php.template @@ -10,20 +10,22 @@ * @package {{PREFIX}} */ -// Global storage for {{PAGE_SLUG}} routes and menu items -global ${{PREFIX}}_{{PAGE_SLUG_UNDERSCORE}}_wp_admin_routes, ${{PREFIX}}_{{PAGE_SLUG_UNDERSCORE}}_wp_admin_menu_items; -${{PREFIX}}_{{PAGE_SLUG_UNDERSCORE}}_wp_admin_routes = array(); -${{PREFIX}}_{{PAGE_SLUG_UNDERSCORE}}_wp_admin_menu_items = array(); +// Global storage for {{PAGE_SLUG}} routes, menu items, and route classic dependencies +global ${{PREFIX}}_{{PAGE_SLUG_UNDERSCORE}}_wp_admin_routes, ${{PREFIX}}_{{PAGE_SLUG_UNDERSCORE}}_wp_admin_menu_items, ${{PREFIX}}_{{PAGE_SLUG_UNDERSCORE}}_wp_admin_route_dependencies; +${{PREFIX}}_{{PAGE_SLUG_UNDERSCORE}}_wp_admin_routes = array(); +${{PREFIX}}_{{PAGE_SLUG_UNDERSCORE}}_wp_admin_menu_items = array(); +${{PREFIX}}_{{PAGE_SLUG_UNDERSCORE}}_wp_admin_route_dependencies = array(); /** * Register a route for the {{PAGE_SLUG}}-wp-admin page. * - * @param string $path Route path (e.g., '/types/$type/edit/$id'). - * @param string|null $content_module Script module ID for content (stage/inspector). - * @param string|null $route_module Script module ID for route lifecycle hooks. + * @param string $path Route path (e.g., '/types/$type/edit/$id'). + * @param string|null $content_module Script module ID for content (stage/inspector). + * @param string|null $route_module Script module ID for route lifecycle hooks. + * @param array $classic_dependencies Classic script handles the route's modules depend on. */ -function {{PREFIX}}_register_{{PAGE_SLUG_UNDERSCORE}}_wp_admin_route( $path, $content_module = null, $route_module = null ) { - global ${{PREFIX}}_{{PAGE_SLUG_UNDERSCORE}}_wp_admin_routes; +function {{PREFIX}}_register_{{PAGE_SLUG_UNDERSCORE}}_wp_admin_route( $path, $content_module = null, $route_module = null, $classic_dependencies = array() ) { + global ${{PREFIX}}_{{PAGE_SLUG_UNDERSCORE}}_wp_admin_routes, ${{PREFIX}}_{{PAGE_SLUG_UNDERSCORE}}_wp_admin_route_dependencies; $route = array( 'path' => $path ); if ( ! empty( $content_module ) ) { @@ -33,7 +35,11 @@ function {{PREFIX}}_register_{{PAGE_SLUG_UNDERSCORE}}_wp_admin_route( $path, $co $route['route_module'] = $route_module; } - ${{PREFIX}}_{{PAGE_SLUG_UNDERSCORE}}_wp_admin_routes[] = $route; + ${{PREFIX}}_{{PAGE_SLUG_UNDERSCORE}}_wp_admin_routes[] = $route; + ${{PREFIX}}_{{PAGE_SLUG_UNDERSCORE}}_wp_admin_route_dependencies = array_merge( + ${{PREFIX}}_{{PAGE_SLUG_UNDERSCORE}}_wp_admin_route_dependencies, + $classic_dependencies + ); } /** @@ -150,10 +156,23 @@ function {{PREFIX}}_{{PAGE_SLUG_UNDERSCORE}}_wp_admin_enqueue_scripts( $hook_suf if ( file_exists( $asset_file ) ) { $asset = require $asset_file; + // Route content/route modules declare their own classic script dependencies + // (e.g. wp-viewport) that must be present before the module evaluates. Script + // modules cannot load classic-script dependencies themselves, so the deps + // collected during route registration are merged into the prerequisites script. + // This lets each route reliably receive its classic dependencies without relying + // on whatever @wordpress/boot happens to load eagerly. + global ${{PREFIX}}_{{PAGE_SLUG_UNDERSCORE}}_wp_admin_route_dependencies; + $prerequisite_dependencies = array_values( + array_unique( + array_merge( $asset['dependencies'], ${{PREFIX}}_{{PAGE_SLUG_UNDERSCORE}}_wp_admin_route_dependencies ) + ) + ); + // This script serves two purposes: // 1. It ensures all the globals that are made available to the modules are loaded. // 2. It initializes the boot module as an inline script. - wp_register_script( '{{PAGE_SLUG}}-wp-admin-prerequisites', '', $asset['dependencies'], $asset['version'], true ); + wp_register_script( '{{PAGE_SLUG}}-wp-admin-prerequisites', '', $prerequisite_dependencies, $asset['version'], true ); $init_modules = {{INIT_MODULES_JSON}}; @@ -193,7 +212,7 @@ function {{PREFIX}}_{{PAGE_SLUG_UNDERSCORE}}_wp_admin_enqueue_scripts( $hook_suf // Register prerequisites style by filtering script dependencies to find registered styles $style_dependencies = array_filter( - $asset['dependencies'], + $prerequisite_dependencies, function ( $handle ) { return wp_style_is( $handle, 'registered' ); } diff --git a/packages/wp-build/templates/page.php.template b/packages/wp-build/templates/page.php.template index adc7274a74056f..7f48643560e0e3 100644 --- a/packages/wp-build/templates/page.php.template +++ b/packages/wp-build/templates/page.php.template @@ -7,20 +7,22 @@ * @package {{PREFIX}} */ -// Global storage for {{PAGE_SLUG}} routes and menu items -global ${{PREFIX}}_{{PAGE_SLUG_UNDERSCORE}}_routes, ${{PREFIX}}_{{PAGE_SLUG_UNDERSCORE}}_menu_items; -${{PREFIX}}_{{PAGE_SLUG_UNDERSCORE}}_routes = array(); -${{PREFIX}}_{{PAGE_SLUG_UNDERSCORE}}_menu_items = array(); +// Global storage for {{PAGE_SLUG}} routes, menu items, and route classic dependencies +global ${{PREFIX}}_{{PAGE_SLUG_UNDERSCORE}}_routes, ${{PREFIX}}_{{PAGE_SLUG_UNDERSCORE}}_menu_items, ${{PREFIX}}_{{PAGE_SLUG_UNDERSCORE}}_route_dependencies; +${{PREFIX}}_{{PAGE_SLUG_UNDERSCORE}}_routes = array(); +${{PREFIX}}_{{PAGE_SLUG_UNDERSCORE}}_menu_items = array(); +${{PREFIX}}_{{PAGE_SLUG_UNDERSCORE}}_route_dependencies = array(); /** * Register a route for the {{PAGE_SLUG}} page. * - * @param string $path Route path (e.g., '/types/$type/edit/$id'). - * @param string|null $content_module Script module ID for content (stage/inspector). - * @param string|null $route_module Script module ID for route lifecycle hooks. + * @param string $path Route path (e.g., '/types/$type/edit/$id'). + * @param string|null $content_module Script module ID for content (stage/inspector). + * @param string|null $route_module Script module ID for route lifecycle hooks. + * @param array $classic_dependencies Classic script handles the route's modules depend on. */ -function {{PREFIX}}_register_{{PAGE_SLUG_UNDERSCORE}}_route( $path, $content_module = null, $route_module = null ) { - global ${{PREFIX}}_{{PAGE_SLUG_UNDERSCORE}}_routes; +function {{PREFIX}}_register_{{PAGE_SLUG_UNDERSCORE}}_route( $path, $content_module = null, $route_module = null, $classic_dependencies = array() ) { + global ${{PREFIX}}_{{PAGE_SLUG_UNDERSCORE}}_routes, ${{PREFIX}}_{{PAGE_SLUG_UNDERSCORE}}_route_dependencies; $route = array( 'path' => $path ); if ( ! empty( $content_module ) ) { @@ -30,7 +32,11 @@ function {{PREFIX}}_register_{{PAGE_SLUG_UNDERSCORE}}_route( $path, $content_mod $route['route_module'] = $route_module; } - ${{PREFIX}}_{{PAGE_SLUG_UNDERSCORE}}_routes[] = $route; + ${{PREFIX}}_{{PAGE_SLUG_UNDERSCORE}}_routes[] = $route; + ${{PREFIX}}_{{PAGE_SLUG_UNDERSCORE}}_route_dependencies = array_merge( + ${{PREFIX}}_{{PAGE_SLUG_UNDERSCORE}}_route_dependencies, + $classic_dependencies + ); } /** @@ -156,10 +162,23 @@ function {{PREFIX}}_{{PAGE_SLUG_UNDERSCORE}}_render_page() { if ( file_exists( $asset_file ) ) { $asset = require $asset_file; + // Route content/route modules declare their own classic script dependencies + // (e.g. wp-viewport) that must be present before the module evaluates. Script + // modules cannot load classic-script dependencies themselves, so the deps + // collected during route registration are merged into the prerequisites script. + // This lets each route reliably receive its classic dependencies without relying + // on whatever @wordpress/boot happens to load eagerly. + global ${{PREFIX}}_{{PAGE_SLUG_UNDERSCORE}}_route_dependencies; + $prerequisite_dependencies = array_values( + array_unique( + array_merge( $asset['dependencies'], ${{PREFIX}}_{{PAGE_SLUG_UNDERSCORE}}_route_dependencies ) + ) + ); + // This script serves two purposes: // 1. It ensures all the globals that are made available to the modules are loaded. // 2. It initializes the boot module as an inline script. - wp_register_script( '{{PAGE_SLUG}}-prerequisites', '', $asset['dependencies'], $asset['version'], true ); + wp_register_script( '{{PAGE_SLUG}}-prerequisites', '', $prerequisite_dependencies, $asset['version'], true ); // Add inline script to initialize the app $init_modules = {{INIT_MODULES_JSON}}; @@ -177,7 +196,7 @@ function {{PREFIX}}_{{PAGE_SLUG_UNDERSCORE}}_render_page() { // Register prerequisites style by filtering script dependencies to find registered styles $style_dependencies = array_filter( - $asset['dependencies'], + $prerequisite_dependencies, function ( $handle ) { return wp_style_is( $handle, 'registered' ); } diff --git a/packages/wp-build/templates/routes-registration.php.template b/packages/wp-build/templates/routes-registration.php.template index 7ab43f1eb10be7..36d190fa7ec54c 100644 --- a/packages/wp-build/templates/routes-registration.php.template +++ b/packages/wp-build/templates/routes-registration.php.template @@ -46,6 +46,11 @@ function {{PREFIX}}_register_page_routes( $page_routes, $register_function_name $content_handle = null; $route_handle = null; + // Classic script dependencies declared by this route's modules. Script modules + // cannot load classic-script dependencies themselves, so they are collected here + // and enqueued by the page's prerequisites script (see the page render function). + $classic_dependencies = array(); + // Register content module if exists if ( $route['has_content'] ) { $content_asset_path = __DIR__ . "/routes/{$route['name']}/content.min.asset.php"; @@ -53,6 +58,7 @@ function {{PREFIX}}_register_page_routes( $page_routes, $register_function_name $content_asset = require $content_asset_path; $content_handle = '{{HANDLE_PREFIX}}/routes/' . $route['name'] . '/content'; $extension = defined( 'SCRIPT_DEBUG' ) && SCRIPT_DEBUG ? '.js' : '.min.js'; + $classic_dependencies = array_merge( $classic_dependencies, $content_asset['dependencies'] ?? array() ); // Deregister first to override any previously registered version // (e.g., Core's default modules when running as a plugin). wp_deregister_script_module( $content_handle ); @@ -72,6 +78,7 @@ function {{PREFIX}}_register_page_routes( $page_routes, $register_function_name $route_asset = require $route_asset_path; $route_handle = '{{HANDLE_PREFIX}}/routes/' . $route['name'] . '/route'; $extension = defined( 'SCRIPT_DEBUG' ) && SCRIPT_DEBUG ? '.js' : '.min.js'; + $classic_dependencies = array_merge( $classic_dependencies, $route_asset['dependencies'] ?? array() ); // Deregister first to override any previously registered version // (e.g., Core's default modules when running as a plugin). wp_deregister_script_module( $route_handle ); @@ -86,7 +93,7 @@ function {{PREFIX}}_register_page_routes( $page_routes, $register_function_name // Register route with page if ( function_exists( $register_function_name ) ) { - call_user_func( $register_function_name, $route['path'], $content_handle, $route_handle ); + call_user_func( $register_function_name, $route['path'], $content_handle, $route_handle, $classic_dependencies ); } } }