// ── — where core renders a module's content ───────────────────────── // // Phase 3, slice 2 of docs/website/MODULE_SYSTEM.md §2.7.1; the normative // contract is docs/website/MODULE_API.md §3.7. // // The read side of registry.js's extension slots. Core puts one of these where a // module may contribute to a core page, and gets back either the filling // component with the props core passed, or nothing at all. // // **Nothing at all is the important half.** An instance with no module installed // renders the identical page it renders today, which is the same untouched-path // guarantee `withModuleNav` makes for nav — and the reason a core layout can // place a slot without also acquiring an empty-state to design. import React from 'react' import { extensionFor } from './registry.js' /** * Contain a module's render failure to the module's own section. * * This is where the client differs from the server, deliberately. A module * *route* that throws costs the module's own page and core does not need to care. * An extension throws inside CORE's page — the admin's user detail, the site * footer — and the whole reason core keeps ownership of that page is that it * stays usable. So a slot renders nothing and logs, rather than taking the * surrounding page down with it. * * A class because that is what React gives us: there is no hook form of * componentDidCatch, and this is the only error boundary core has. */ class SlotBoundary extends React.Component { constructor(props) { super(props) this.state = { failed: false } } static getDerivedStateFromError() { return { failed: true } } componentDidCatch(error) { // Named so the console says whose fault it is: a blank section with an // anonymous stack is how a module bug becomes core's support ticket. console.error(`[modules] extension in slot "${this.props.name}" threw and was dropped`, error) } render() { return this.state.failed ? null : this.props.children } } /** * @param {string} name the slot id, declared by core in main.jsx * @param {function} [wrap] core markup that only makes sense AROUND a rendered * extension — a separator, a heading, a rule. Called with the extension's * element and rendered inside the boundary, so it shares the extension's fate: * an unfilled slot and a failed one both render nothing at all, decoration * included. Found in a browser, because the obvious alternative — asking * whether the slot is filled and rendering the separator alongside — is right * about the unfilled case and leaves a stray separator behind on the failed one. * @param {object} props everything else is handed to the filling component */ export default function Slot({ name, wrap, ...props }) { const Extension = extensionFor(name) if (!Extension) return null const element = return {wrap ? wrap(element) : element} }