@luwiostack/registry
v0.1.0
Published
The shared registry + Vite auto-discovery primitive behind @luwiostack/router and @luwiostack/translations: a tiny module-singleton registry (add/get/all, dedup by id) and a Vite plugin factory that scans suffixed files and self-registers their exports.
Readme
@luwiostack/registry
The shared registry + Vite auto-discovery primitive behind @luwiostack/router and
@luwiostack/translations. Two small pieces give a package the "define it in a
file, and it registers itself" ergonomics:
createModuleRegistry({ idOf, match })— a module-singleton registry:add/get/all/clear, dedup by id, andregisterModules()to collect items out of bundler module namespaces.createRegistryPlugin(config)— a Vite plugin factory that scans suffixed files (*.route,*.catalog, …) and emits a virtual module handing their exports to yourregisterModules. Lives at the Node-only./vitesubpath, so the runtime entry stays dependency-free.
Reach for it when a package's items are genuinely consumed as a whole set — the router must
enumerate every route to match a URL, and @luwiostack/translations loads every catalog into its
store. (@luwiostack/http deliberately does not build on this: an endpoint is a leaf you import
and call, so it stays a plain callable with no registry.) You rarely install this directly — you use
@luwiostack/router / @luwiostack/translations, which build on it.
The registry
import { createModuleRegistry } from '@luwiostack/registry'
interface RouteBuilder {
id: string /* … */
}
const isRoute = (v: unknown): v is RouteBuilder => typeof v === 'object' && v !== null && 'id' in v
export const routeRegistry = createModuleRegistry<RouteBuilder>({
idOf: (r) => r.id,
match: isRoute,
onDuplicate: 'throw', // 'replace' (console.warn + swap) is the other option
label: 'route', // used in the error / warning text
})Re-adding the same instance is always a silent no-op, so a factory that self-registers stays
compatible with a plugin that re-collects the same exports. A different item on a taken id
follows onDuplicate: 'throw' (default) or 'replace' (console.warn + swap).
The Vite plugin
Wrap the factory so your package exposes a named plugin with its own option names:
// @luwiostack/router/vite
import { createRegistryPlugin } from '@luwiostack/registry/vite'
import type { Plugin } from 'vite'
export function luwioRouter(options: { routesDir?: string } = {}): Plugin {
return createRegistryPlugin({
name: '@luwiostack/router',
dir: options.routesDir ?? 'src',
suffix: '.route',
virtualId: 'virtual:@luwiostack/router/routes',
registerImport: '@luwiostack/router', // must export `registerModules`
})
}The generated virtual module imports every *.route.{ts,tsx,js,jsx} under dir and passes them to
registerModules. Editing, adding, or removing a matched file triggers a full reload so the registry
rebuilds cleanly — keep matched files thin (export the item; put components elsewhere) for fast HMR.
API
| Export | What it does |
| --- | --- |
| createModuleRegistry({ idOf, match, onDuplicate?, label? }) | Build a registry: add / get / all / clear / registerModules. |
| collectModules(modules, match) | Pull matching values (default + named exports) out of module namespaces. |
| createRegistryPlugin(config) | (./vite) The Vite scanner that emits a self-registering virtual module. |
