@modular-vue/nuxt
Nuxt 4 integration for the @modular-vue family. Nuxt owns
the Vue app and the vue-router instance, so this package is the router-owning
seam over @modular-vue/runtime: it grafts every module's createRoutes()
subtree onto Nuxt's router and installs the modular contexts (shared
dependencies, navigation, slots, modules) on the Nuxt Vue app.
Status:
0.1.0, experimental. The runtime installer is exercised by the package test suite against a real Vue app + vue-router. The build-time Nuxt module is a thin wrapper that injects a runtime plugin. Breaking changes between 0.x minor versions are possible. For the stable SPA path, see Getting started with Vue Router.
Installation
npm install @modular-vue/nuxt @modular-vue/runtime @modular-vue/core @modular-vue/vue
@modular-vue/nuxt peer-depends on nuxt (4.4+), @modular-vue/runtime,
vue, and vue-router (all provided by a Nuxt 4.4+ app). The vue-router@^5
peer is only satisfiable on Nuxt 4.4 or newer — that's the release where
Nuxt integrated Vue Router 5; earlier Nuxt 4.x ships Vue Router 4.
Two ways to wire it
1. The Nuxt module (zero-config path)
Add the module to nuxt.config.ts and point it at a file that exports your
registry:
// nuxt.config.ts
export default defineNuxtConfig({
modules: ["@modular-vue/nuxt"],
modularVue: {
// Default-export the registry, or a factory `(nuxtApp) => registry`.
registry: "~/modular/registry",
// Optional: graft module routes under an existing Nuxt page/route.
parentRouteName: "app",
},
});
// modular/registry.ts
import { createRegistry } from "@modular-vue/runtime";
import billingModule from "./modules/billing";
import type { AppDependencies, AppSlots } from "./types";
import { createAuthStore, httpClient } from "./services";
// A FACTORY, so a fresh registry is built per request under SSR.
export default function buildRegistry() {
const registry = createRegistry<AppDependencies, AppSlots>({
stores: { auth: createAuthStore() },
services: { httpClient },
slots: { commands: [] },
});
registry.register(billingModule);
return registry;
}
The module injects a runtime plugin that calls installModularApp and exposes
the resolved manifest as useNuxtApp().$modular.
Only the serializable options (registry, parentRouteName) travel through
nuxt.config.ts. If you need authGuard, providers, slotFilter, or
onModuleExit, use the plugin path below instead.
2. Your own Nuxt plugin (full control)
Skip the module and write the plugin yourself — this is the path when you need the non-serializable options:
// plugins/modular-vue.ts
import { installModularApp } from "@modular-vue/nuxt/runtime";
import buildRegistry from "~/modular/registry";
export default defineNuxtPlugin((nuxtApp) => {
const registry = buildRegistry();
const manifest = installModularApp(nuxtApp, registry, {
parentRouteName: "app",
authGuard: (to) => (to.meta.requiresAuth && !isLoggedIn() ? "/login" : true),
onModuleExit: (event) => nuxtApp.$router.push("/"),
});
return { provide: { modular: manifest } };
});
installModularApp(nuxtApp, registry, options?)
nuxtApp— the Nuxt app (needsvueAppand$router; the structuralNuxtAppLikeinterface a realNuxtAppsatisfies).registry— a@modular-vue/runtimeregistry with your modules registered.options—parentRouteName,authGuard,providers,slotFilter,onModuleExit(all forwarded toregistry.resolve();routeris taken fromnuxtApp.$router).
Returns the ApplicationManifest (router, navigation, slots, modules,
recalculateSlots, …). Plugin extensions are preserved: a registry carrying
journeysPlugin() yields manifest.extensions.journeys (and the
manifest.journeys alias) typed as JourneyRuntime with no cast — the
extension map is inferred from the registry's resolve() return. That same
install threads the journey runtime app-wide via the plugin's appProvides hook,
so <JourneyOutlet> resolves it from context without a <JourneyProvider> (or a
manual provideJourneyRuntime).
Typing under
defineNuxtPlugin.return { provide: { modular: manifest } }can trip TS7022 ("referenced directly or indirectly in its own initializer") when the manifest carries a large plugin runtime. Annotate the binding —const manifest: ApplicationManifest<AppSlots, AppNavItem, { journeys: JourneyRuntime }> = installModularApp(…)— to break the cycle. See the Nuxt framework-mode guide.
SSR and per-request state
Under SSR, Nuxt creates a fresh app and router per request.
registry.resolve() is single-use, and shared server state must not leak
between requests, so build the registry per request — export a factory and
call it inside the plugin (both paths above do). A module-level singleton
registry is fine only for a client-only app (ssr: false), where the plugin
runs once.
Routing note
Module routes are added at runtime via router.addRoute() when the plugin runs.
They are available for client-side navigation immediately; for a module route
to render on the first server-rendered paint, add a matching Nuxt catch-all
page (or declare the shell route as parentRouteName) so Nuxt's initial route
resolution finds it. See the
Nuxt framework-mode guide for the full
walkthrough and the SSR considerations.
See also
- Framework-mode integration (Nuxt)
- Getting started with Vue Router
@modular-vue/runtime—createRegistry,resolve,resolveManifest.- Troubleshooting: duplicate
vue-routerinstances — ifvue-tscreportsRouteRecordRaw"not assignable to"RouteRecordRaw, your app resolved two copies ofvue-router; this explains the cause and the one-line dedupe.