How the music store is put together
The plugins
- catalog-plugin — the base plugin. Exposes the
useCatalogSongsdata hook and acatalogslot UI. Ships themusic-catalog-pluginPython package — a reactor plugin (manifest namecatalog) servingGET /api/catalog/songs. - header-plugin — declares
dependencies: [CatalogPlugin, ShopPlugin]and consumesuseCatalogSongsplus the shared cart store. Contributes the store header to theheaderslot, including a cart summary that reveals cart details in an overlay on hover. It offers acart-actionsslot inside that overlay and does not know or care who fills it — which is why it does not depend on the checkout plugin. - shop-plugin — declares
dependencies: [CatalogPlugin]. Owns the shareduseCartstore and contributes the purchasable song cards plus cart to themainslot, offering the samecart-actionsslot underneath them. - checkout-plugin — declares
dependencies: [ShopPlugin]and consumes the shared cart store. Contributes everything it owns and exports nothing for another plugin to draw:CheckoutButtontocart-actions,CheckoutPagetocheckout, andCheckoutAsidetocheckout-aside. - playlist-plugin — the plugin that offers a contribution point. It owns
a playlist view and opens
music.playlistRulefor other plugins to fill; it ships no rules itself, so on its own it renders a playlist that says so. - mood-plugin — the plugin that uses that point. It declares
dependencies: [PlaylistPlugin]and contributes three rules (Chill, Energetic, A to Z); it contributes to no slot and renders nothing of its own. - plugins-panel-plugin — contributes the Python plugins as a group in the
plugins manager, driven by
GET /pluginsandPOST /plugins/{name}/toggle.
Every plugin, on both tiers, declares a displayName/display_name,
description, octicon and emoji. That is why one overlay draws either.
The checkout plugin is the one to untick first
The header used to import the checkout button and render it itself, so
switching this plugin off left a button that opened a page that was no longer
there.
Now the header and the shop each offer a cart-actions slot and the checkout
plugin fills both, so unticking it removes the button from the overlay and from
under the songs, and the rest of the store carries on.
Nothing depends on it any more, which is why the app mounts it deliberately in
StoreExtension — a capability of the store, not an implementation detail of its
header.
Two plugins, one contribution point
playlist-plugin and mood-plugin exist to show the shape a slot cannot
express. A slot answers "render everything plugins put here". A
contribution point answers a different question: "what do plugins offer, so
the application can choose?" — here, a set of ways to fill a playlist, of which
one is on screen at a time.
-
The playlist plugin declares the point and hosts it. It reads the contributions with
useContributions(PlaylistRulePoint), draws a chooser from them, and applies the chosen rule to the catalog. -
The mood plugin contributes to it, declaratively:
contributes: [contribution(PlaylistRulePoint, CHILL, { id: 'chill', order: 0 }),…]
Nothing points from the playlist plugin to the mood plugin. That is what makes this a contribution point rather than an import: a fourth plugin can add a rule tomorrow without the playlist plugin changing.
The same relationship exists on the Python side, between the playlist and
mood backend plugins, over music.playlistRule.
Loaded after the first paint
@music/mood is mounted with defineLazyPlugin, so its module is fetched
after the platform starts rather than before the first paint:
const MoodPlugin = defineLazyPlugin({
name: '@music/mood',
displayName: 'Moods',
octicon: 'sun',
emoji: '🌤️',
dependencies: [PlaylistPlugin],
activationEvents: [onContributionPoint('music.playlistRule')],
load: () => import('@datalayer-examples/reactor-music-mood-plugin')
.then(module => module.MoodPlugin),
});
The store and an empty playlist render immediately; the playlist asks what rules exist, and that read fetches the module. Nobody had to name this plugin to cause it.
It is a fair candidate for laziness precisely because it renders no UI of its
own — its absence costs a chooser, not a page. Everything the sidebar needs to
list and describe it is declared on the reference rather than inside the module,
so it appears in the plugin list from the first frame with a loading… marker
rather than popping into existence when its module arrives.
The application, in full
The app is purely declarative. It mounts plugins, renders slots, and asks the slots what is in them so the layout can adapt when one is switched off:
const hasShop = useSlotComponents('main').length > 0;
With the shop off there is no first column, and what is left takes the whole width rather than sitting beside a hole.