Widget Types: bootstrap registry into the dashboard client#77917
Conversation
Emit gutenberg_get_registered_widget_modules() in the generated widgets.php so consumers read the build manifest through a memoized helper instead of requiring the registry file directly. Aligns with the registry+getter pattern used for routes and pages.
Bridge layer between the build manifest and the client data store. Reads gutenberg_get_registered_widget_modules() and exposes the discovered widgets as window.__registeredWidgetTypes so the core/widget-types resolver can import each widget's metadata module on demand. Provisional transport: a follow-up will replace this inline payload with a REST endpoint backed by a server-side widget type registry. The inline payload may remain afterwards as an optional hydration prefill the resolver consumes when present. Gated behind the gutenberg-dashboard-widgets experiment.
Trim PHPDoc and emphasize that window.__registeredWidgetTypes is a stopgap until a REST endpoint takes over.
Iterate the widget types from useWidgetTypes() and dynamically import each renderModule, mounting its default export inside the stage.
Apply {{page}}_script_module_dependencies (and the wp-admin variant)
on $boot_dependencies before the page registers its loader module, so
surfaces can add their own modules to the page's import map.
Add gutenberg_get_widget_module_dependencies() and hook it on the dashboard's script_module_dependencies filters so widget modules enter the page's import map as 'import' => 'dynamic'. Bytes are only fetched when something calls import() on the handle.
PHPDoc on gutenberg_dashboard_widgets_module_dependencies now flags that the hardcoded all-widgets approach is a stopgap until per-widget page assignment lands.
Replace the silent catch in the resolver with a @wordpress/warning call so module resolution failures are visible during development.
Temporary: registerWidgetType requires a title, but the build manifest does not yet forward widget.json metadata to the client. When that wiring lands, this declaration becomes redundant.
|
The following accounts have interacted with this PR and/or linked issues. I will continue to update these lists as activity occurs. You can also manually ask me to refresh this list by adding the If you're merging code through a pull request on GitHub, copy and paste the following into the bottom of the merge commit message. To understand the WordPress project's expectations around crediting contributors, please review the Contributor Attribution page in the Core Handbook. |
|
Size Change: -132 B (0%) Total Size: 7.94 MB 📦 View Changed
ℹ️ View Unchanged
|
|
Flaky tests detected in 30ba4c8. 🔍 Workflow run URL: https://github.com/WordPress/gutenberg/actions/runs/25373660753
|
Replace the useEffect/useState load pattern with a module-level resource cache that throws a promise while pending and the loaded module once fulfilled. Each widget gets its own Suspense boundary so they mount independently as their render modules resolve.
Match the convention already used in the resolver so non-esbuild bundlers do not try to resolve the dynamic specifier at build time.
Routes are not part of any package's tsconfig (wp-build handles them), so the IDE falls back to defaults that misreport JSX as a UMD global. This config sets jsx: react-jsx for the directory and excludes tests that rely on jest types declared elsewhere.
Keep only what differs from the defaults: jsx, noEmit, resolveJsonModule, noImplicitAny.
Iterate registered widgets in page templates the same way routes are iterated, instead of introducing a new apply_filters extension point. Drops the now-unused dependency helpers from the dashboard bridge.
Move the {prefix}/widgets/{dir}/{kind} handle convention out of the
four consumers (registration, page templates, dashboard bridge) and
into the registry generator, so the convention has a single source
of truth and consumers read $widget['render_module'] /
$widget['widget_module'] directly.
Match the routes pattern: registry file emits identity and presence
flags only, the registry getter augments each entry with the
{prefix}/widgets/{dir}/{kind} handles. Convention stays in one place
without leaking into the manifest data.
register_widget_modules read the raw registry file directly and checked render_module / widget_module fields that only exist on entries after the getter augments them. The handles were never registered as script modules, leaving the import map without them. Read through the getter so the registration uses the same shape every other consumer sees.
| wp_print_inline_script_tag( | ||
| 'window.__registeredWidgetTypes = ' . wp_json_encode( $entries ) . ';' | ||
| ); |
There was a problem hiding this comment.
Temporary implementation. Ideally, we can replace it with a new REST API endpoint or a similar alternative.
| } | ||
| } | ||
|
|
||
| // Add registered widgets as dynamic dependencies |
There was a problem hiding this comment.
Adds each registered widget to the page's import map as 'import' => 'dynamic': handles resolve for import('wp/widgets/<dir>/<kind>') calls, but bytes don't preload.
A page with N widgets incurs zero up-front network cost and fetches each widget only when it is rendered.
The handles come from gutenberg_get_registered_widget_modules() (in widget-registration.php.template), which augments each registry entry with render_module / widget_module resolved via <HANDLE_PREFIX>/widgets/<dir>/<kind>.
One place owns the naming convention. The function_exists() guard makes this block a no-op for projects without widgets.
Scaling: a pages field in widget.json plus a gutenberg_get_widget_modules_for_page( '{{PAGE_SLUG}}' ) helper would scope this loop per-page; only the function call here would change.
| */ | ||
| export default { | ||
| name: 'wordpress/hello-world', | ||
| title: __( 'Hello World' ), |
There was a problem hiding this comment.
We might wanna keep this untranslated to not add it to translate.wordpress.org, as there isn't this string yet there.
simison
left a comment
There was a problem hiding this comment.
Just a small nit about not translating "Hello World"
What
Wires the data pipeline so the dashboard client sees the registered widget types via
useWidgetTypes(). Loading and mounting each widget'srendermodule is out of scope and lands in a follow-up PR.wp-build: the build manifest carries each widget's identity (name,dir_name) and presence flags (has_render,has_widget). A memoized getter (gutenberg_get_registered_widget_modules()) returns the entries augmented withrender_module/widget_modulescript-module handles, so the convention<HANDLE_PREFIX>/widgets/<dir>/<kind>lives in one place. A guarded foreach in the page templates adds each registered widget to the page's import map as'import' => 'dynamic'.lib/experimental/dashboard-widgets/: printswindow.__registeredWidgetTypesso the resolver has data on first paint.core/widget-typesresolver populates the store from the inline payload.useWidgetTypes()and logs the result; the.dashboard-widgetscontainer is left empty on purpose.Part of #77625
Why
useWidgetTypes()returned an empty array because nothing populated the store, and anyimport('wp/widgets/.../...')would have failed because the handle was not in the page's import map. This PR closes both gaps. Splitting the data pipeline from the rendering keeps each piece reviewable on its own.For projects that use
wp-buildwithout authoring widgets, the foreach is a no-op: when nowidgets/directory exists, the build skips emitting the registry helper, and afunction_exists()guard in the page template short-circuits the loop. No bytes added, no behavior change.Approach (tentative)
window.__registeredWidgetTypesis a stopgap; the plan is a REST endpoint consumed viaapiFetch.pagesfield onwidget.json(array) will letwp-buildscope the iteration to the widgets each page owns, via a thin helper on top of the existing getter.widgets/<name>/widget.tsredeclares fields liketitleuntil the manifest forwardswidget.jsonmetadata to the client.How
packages/wp-build/:lib/build.mjs(generateWidgetRegistry): emit each widget as{ name, dir_name, has_render, has_widget }inbuild/widgets/registry.php. Same shape as routes' registry (presence flags only).templates/widget-registration.php.template:gutenberg_get_registered_widget_modules(), a memoized reader of the registry. The getter augments each entry withrender_moduleandwidget_module, applying the<HANDLE_PREFIX>/widgets/<dir>/<kind>convention once per request.nullwhen the corresponding source file is absent.$widget['render_module']/$widget['widget_module']directly to callwp_register_script_module(). No handle reconstruction.templates/page.php.templateandpage-wp-admin.php.template: after the routes foreach, add afunction_exists()-guarded foreach that pushes each widget'srender_moduleandwidget_moduleinto$boot_dependenciesas'import' => 'dynamic'.lib/experimental/dashboard-widgets/widget-types.php(new): onadmin_print_scripts, printswindow.__registeredWidgetTypesviawp_print_inline_script_tagwith the registry projected to{ name, render_module, widget_module }. Reads the same getter; returns early when the function is undefined or the projection is empty.lib/load.php: load the new file whengutenberg-dashboard-widgetsis enabled.routes/dashboard/stage.tsx: readuseWidgetTypes()andconsole.logthe result; container left empty.routes/dashboard/tsconfig.json(new): minimal IDE type-check config for the route.widgets/hello-world/widget.ts: declaretitlelocally (temporary).Testing
gutenberg-dashboard-widgetsexperiment.npm install && composer install && npm run build && npm run wp-env start.wp-admin/admin.php?page=dashboard-wp-admin.Failed to resolve module specifiererrors in the console.window.__registeredWidgetTypesis the array of registered widgets.widgetTypeswithname,title,renderModule.must have a titlewarning forhello-world..dashboard-widgetsis rendered but empty (rendering is out of scope).widgets/directory (or experiment disabled), the dashboard still renders without errors, the page's import map contains no widget handles, andwindow.__registeredWidgetTypesisundefined.Follow-ups
renderModulefrom the dashboard stage and mount it.console.loginroutes/dashboard/stage.tsx.pagesfield towidget.jsonand agutenberg_get_widget_modules_for_page( $page )helper so the page templates iterate only the widgets each page owns (declarative widget-to-page assignment).apiFetch.widget.jsonmetadata (title, description, category) through the manifest, removing the redeclaration inwidgets/<name>/widget.ts.