Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions packages/boot/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
4 changes: 2 additions & 2 deletions packages/boot/src/components/save-button/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IMO, this component doesn't belong in "editor" package. So instead of adding complexity, maybe the right solution is to move it to the right place. This is what I tried to do in this old PR #71948 (worth reviving?)

import { Button, Modal } from '@wordpress/components';
import { Tooltip } from '@wordpress/ui';

Expand All @@ -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 );
Expand Down Expand Up @@ -120,7 +120,7 @@ export default function SaveButton() {
onRequestClose={ () => setIsSaveViewOpened( false ) }
size="small"
>
<EntitiesSavedStates
<LazyEntitiesSavedStates
close={ () => setIsSaveViewOpened( false ) }
variant="inline"
/>
Expand Down
4 changes: 2 additions & 2 deletions packages/boot/src/components/save-panel/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 );
Expand All @@ -26,7 +26,7 @@ export default function SavePanel() {
title={ __( 'Review changes' ) }
size="small"
>
<EntitiesSavedStates
<LazyEntitiesSavedStates
close={ () => setIsOpen( false ) }
variant="inline"
/>
Expand Down
Original file line number Diff line number Diff line change
@@ -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 (
<Suspense fallback={ <Spinner /> }>
<EntitiesSavedStates { ...props } />
</Suspense>
);
}
23 changes: 15 additions & 8 deletions packages/boot/src/components/save-panel/use-save-shortcut.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*
Expand All @@ -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 );

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we replace these with things from "core-data" instead. Core Data should already have all what's needed to check dirtiness and things like that.

One thing that might be missing in "core-data" is the "save lock" behavior which probably belongs there, rather than "editor". I wonder if we should do a small refactor to move things there?

const { registerShortcut, unregisterShortcut } = useDispatch(
keyboardShortcutsStore
);
Expand Down Expand Up @@ -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();
}
} );
}
4 changes: 4 additions & 0 deletions packages/lazy-editor/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
8 changes: 8 additions & 0 deletions packages/lazy-editor/src/index.tsx
Original file line number Diff line number Diff line change
@@ -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
*/
Expand Down
4 changes: 4 additions & 0 deletions packages/wp-build/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
43 changes: 31 additions & 12 deletions packages/wp-build/templates/page-wp-admin.php.template
Original file line number Diff line number Diff line change
Expand Up @@ -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 ) ) {
Expand All @@ -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
);
}

/**
Expand Down Expand Up @@ -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}};

Expand Down Expand Up @@ -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' );
}
Expand Down
43 changes: 31 additions & 12 deletions packages/wp-build/templates/page.php.template
Original file line number Diff line number Diff line change
Expand Up @@ -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 ) ) {
Expand All @@ -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
);
}

/**
Expand Down Expand Up @@ -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}};
Expand All @@ -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' );
}
Expand Down
9 changes: 8 additions & 1 deletion packages/wp-build/templates/routes-registration.php.template
Original file line number Diff line number Diff line change
Expand Up @@ -46,13 +46,19 @@ 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";
if ( file_exists( $content_asset_path ) ) {
$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 );
Expand All @@ -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 );
Expand All @@ -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 );
}
}
}
Expand Down
Loading