Module Kit registries and menus
This page shows how your module adds to another module without importing it: menu items, entries in a registry, and data for an area's single bootstrap request. It is for module authors. Set up the module first: Overview.
Extension points explains when to use a registry and when a hook.
Menu helpers
These helpers add items to a Menu without importing it.
| Helper | Signature | Purpose |
|---|---|---|
addMenuSource | (nuxt: Nuxt, options: MenuSourceOptions) => void | Registers a menu source file. Duplicates are ignored. |
addOptionalMenuSource | (nuxt: Nuxt, options: MenuSourceOptions) => boolean | The same, only when Menu is installed. Returns whether it registered. |
interface MenuSourceOptions {
/** The menu the items belong to, for example `dashboard.account`. */
menu: string
/** Absolute path of the source file (a `defineMenuSource` default export). */
file: string
}
addOptionalMenuSource(nuxt, {
menu: 'dashboard.account',
file: resolver.resolve('./runtime/menus'),
})
Menu reads the sources once every module is done. The source file imports @frigear-nu/menu/utils itself, which
resolves only when the file runs.
addOptionalMenuSource, and declare @frigear-nu/menu as an optional moduleDependencies entry and an optional
peer dependency. Your module then works without Menu.Module Kit depends on neither Menu nor Widget. For widgets and tabs, guard the call in
setup() yourself: check hasNuxtModule('@frigear-nu/widget'), then await import('@frigear-nu/widget/utils') and
call extendWidgets or extendTabs.
Registries
defineVirtualRegistry lets your module collect entries from other modules at build time. Other modules register
entries during their own setup(), in any order. Once every module is done (modules:done), the entries are bundled
into one virtual module that your runtime code imports. Nothing is scanned at runtime, so it works on Cloudflare
Workers. Call it from the setup() of the module that owns the registry.
function defineVirtualRegistry<T>(nuxt: Nuxt, options: VirtualRegistryOptions<T>): void
interface VirtualRegistryOptions<T> {
/** Template file name, for example `#widgets/registry.mjs`. */
filename: string
/** `app` writes the template and aliases it; `server` adds a Nitro virtual module. Defaults to `app`. */
target?: 'app' | 'server'
/** The import path of the virtual module (`app` target only), for example `#widgets/registry`. */
alias?: string
/** Reads what modules registered during their own `setup()`. */
collect: () => T
/** The source of the virtual module. */
contents: (data: T) => string
/** Called with the collected data after the template is registered. */
resolved?: (data: T) => void | Promise<void>
}
Bootstrap providers
A bootstrap provider adds your module's data to an area's single request, so your composable needs no request of its own. An area such as the account pages shows data from several modules for the signed-in user. Without providers, each module's composable makes its own request.
The area loads GET /api/auth/bootstrap?scope=<scope> before its pages set up. That request primes the
useCachedFetch cache under each provider's key.
function registerBootstrapProvider(nuxt: Nuxt, provider: {
/** The area, e.g. `account`. */
scope: string
/** The response key, and the `useCachedFetch` key your composable already uses for the same data. */
key: string
/** Server file whose default export is `(event: H3Event) => Promise<data>`; absolute or relative to rootDir. */
handler: string
}): void
getBootstrapProviders(nuxt: Nuxt, scope?: string): BootstrapProvider[] // what is registered so far
defineBootstrapRegistry(nuxt: Nuxt): void // bundles them into `#bootstrap/providers` (called by @frigear-nu/auth)
Share one function between your endpoint and the provider, so both return the same shape. Events does it like this:
export default async function myTickets(event: H3Event): Promise<TicketDto[]> {
const { user } = await authorize(event, 'tickets:read')
return listUserTickets(user.id)
}
import myTickets from '../../bootstrap/my-tickets'
export default defineEventHandler(myTickets)
registerBootstrapProvider(nuxt, {
scope: 'account',
key: 'events:tickets:mine', // what useMyTickets() passes to useCachedFetch
handler: resolver.resolve('./runtime/server/bootstrap/my-tickets'),
})
Your composable does not change. useCachedFetch finds the primed entry under its key, on the server, while hydrating
and on client navigation.
How it fits together
| Part | Behaviour |
|---|---|
| The route | Auth serves it. It calls requireAuth once (authentication only), runs the scope's providers side by side and returns { [key]: data }. |
| Route errors | 401 when signed out, 400 without scope, 404 for a scope with no providers. The response is private, no-store. |
| Authorization | The principal is cached on the event. Call authorize(event, '<permission>') in the provider, so a token without the permission gets nothing, as on your own route. |
| Failures | A provider that throws is left out and logged. Your composable then fetches from your own endpoint. |
| The client | The area's owner calls loadBootstrap(scope) from a route middleware. It refetches when the last load is over a minute old. |
The account scope | Account adds the middleware to every account page and resets it when the signed-in user changes. |
Module Kit cannot serve the route itself, because Auth already depends on Module Kit. Today Subscriptions
(subscriptions:me), Events (events:tickets:mine) and OAuth Server (oauth-server:mine) provide account data.
One request for an area covers the client.
Next steps
- Module Kit server toolkit – serve data from your own routes.
- Menu – write the menu source file your module registers.
- Widget – add widgets and tabs to another module's pages.