Widget Types: replace bootstrap with resolver#77847
Conversation
Init runs lazily on first selector read; consumers read via useWidgetTypes or getWidgetTypes(). bootstrap.ts is removed.
Drop manual ResolverApi/ForwardResolverApi interfaces in favor of the @ts-expect-error pattern already used in abilities and editor dataviews, pending registry types from @wordpress/data.
Add a fixture-backed test that verifies the first read populates the store via dynamic import, and a second test that mutates the source between reads to prove the resolver does not fire twice. Reset resolution metadata in beforeEach so each test exercises the resolver from scratch.
routes/ has no tsconfig in this repo, so resolver files are not type checked under strict; the directives were not suppressing anything.
|
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: 0 B Total Size: 7.87 MB ℹ️ View Unchanged
|
|
Flaky tests detected in 7c914a1. 🔍 Workflow run URL: https://github.com/WordPress/gutenberg/actions/runs/25254219936
|
Part of #77625.
What?
Replaces the explicit
bootstrapWidgetTypes()call site inroutes/dashboard/widget-types/with agetWidgetTypesresolver on thecore/widget-typesstore.Adds a
useWidgetTypes()hook for React consumers and removesbootstrap.ts.The public surface (
registerWidgetType,unregisterWidgetType, thewidgets.registerWidgetTypefilter, selectors, and the store handle) is unchanged.Why?
Before this PR, populating the
core/widget-typesstore required a caller to invokeawait bootstrapWidgetTypes()before any consumer read from the store. Two problems with that shape:1. The init step is invisible from the consumer side.
The bootstrap call is a precondition that lives outside the data flow consumers actually read from. Nothing in the consumer's code expresses the dependency, so the requirement only exists in the README and in whoever wired it up. If the bootstrap script has not run, the store is empty and there is no signal at the consumer.
2. The init step forces a strict script-load order.
The bootstrap has to run before any consumer mounts. The moment a second surface (sidebar, picker, additional page) consumes widget types, it has to coordinate around it. The constraint is not enforced anywhere in the type system or runtime contract.
Moving the work into a resolver puts widget type initialization where the rest of
@wordpress/dataputs comparable work: behind the selector that reads it.@wordpress/dataowns the "populate on first read, no-op afterwards" behavior. Consumers just read.Because reading is now the entire entry point, consumers pick whichever access pattern fits their context, with no extra setup:
The hook is one of several entry points, not a required wrapper.
The resolver fires the first time the selector is read with empty state, dynamically imports each widget's metadata module, merges the snake_case payload from PHP into the camelCase
WidgetTypeshape, and dispatchesregisterWidgetTypeper entry.A follow-up (out of scope here) replaces the PHP-injected global with a REST-driven source. Because the new shape is a resolver, that swap is a change of source inside the resolver, not a change in the public surface above.
How?
store/resolvers.tsgetWidgetTypesdoes whatbootstrap.tsdid: readswindow.__registeredWidgetTypes, dynamic-imports eachwidget_module, applies the snake-to-camel mapping, and dispatchesregisterWidgetTypeper entry.getWidgetTypeforward-resolves togetWidgetTypesso either selector triggers initialization.The thunk shape
() => async ( { dispatch } ) => { … }matches the rest of the resolver code in core.store/index.tsRegisters
resolversalongsideactionsandselectorsin thecreateReduxStoreconfig.hooks/use-widget-types.tsThin wrapper over
useSelect( ( s ) => s( store ).getWidgetTypes(), [] ). NouseEffect. No dispatch from the consumer.bootstrap.tsRemoved. The
index.tsbarrel exportsuseWidgetTypesinstead ofbootstrapWidgetTypes.README.mdUsage section, diagram, function reference, and the "Known concerns" entry on the global all updated to reflect the resolver-based init.
Testing
npm run test:unit -- routes/dashboard/widget-types/store/test/24 tests pass (19 prior action tests, plus 5 new resolver tests):
widget_moduleare skippedwidget_modulefails to import are skippedstore/test/fixtures/widget-with-default.tsprovides the imported default export)window.__registeredWidgetTypesbetween reads does not re-fire the resolver; the second read returns the cached stateThe
beforeEachresetswindow.__registeredWidgetTypesand callsinvalidateResolutionForStore()so each test exercises the resolver from a clean slate.End-to-end verification in the dashboard is not feasible in this PR.
routes/dashboard/stage.tsxis still a placeholder in trunk, and the PHP loader that injectswindow.__registeredWidgetTypesis not fully wired here yet. The change is verified by the unit tests above; the first surface to consume widget types will exercise the resolver in context.Follow-ups
window.__registeredWidgetTypeswith a REST-driven source so the widget type list reaches the client through the standard data layer.getWidgetType( name )should fetch a single widget when the metadata moves to REST, or keep forward-resolving togetWidgetTypes().