@veap/plugins/router is a file-convention router that mirrors the Next.js App Router tree in memory and renders plugin-contributed pages from a single physical catch-all route. Plugins expose routes through IPlugin.routeTree; the router merges every enabled plugin's tree, matches a URL, runs a middleware pipeline, and renders the matched layout chain.
The docs describe this router as unstable because its semantics are tightly coupled to the host application's Next.js integration and can change.
Route tree types#
RouteNode mirrors the App Router file conventions:
| Field | Meaning | File equivalent |
|---|---|---|
segment | URL segment; "" is the root | folder name |
page | Page component | page.tsx |
layout | Layout wrapping children | layout.tsx |
loading | Loading component | loading.tsx |
error | Error component ({ error, reset }) | error.tsx |
notFound | Not-found component | not-found.tsx |
default | Parallel slot default | default.tsx |
route | HTTP method handlers (GET, POST, …) | route.ts |
generateMetadata | Metadata generator ({ params, searchParams }) | generateMetadata export |
middlewares | Middleware pipeline for this node | middlewares export |
auth / roles / permissions | Auth/RBAC requirements | module exports |
breadcrumb | BreadcrumbValue | module export |
id | Route id for template overrides | — |
children | Child nodes | subfolders |
parallelRoutes | @slot subtrees | @slot folders |
Segment kinds: static, [id] dynamic, [...path] catch-all, [[...path]] optional catch-all, (group) transparent route group, @slot parallel slot.
MatchResult is { node, params, layoutChain, matchedSegments, isExact }. LayoutChainEntry carries one level's layout/loading/error/notFound/middlewares/auth/roles/permissions/parallelSlots/id.
Segment helpers#
isGroupSegment, isDynamicSegment, isCatchAllSegment, isOptionalCatchAllSegment, isParallelSlot, extractParamName, segmentMatchesUrlPart, and resolveMagicPrefix(node, prefixPath) (resolves a [prefix] segment to the configured private path segments).
RouteTree#
const tree = new RouteTree(initialRoot?, initialRewrites?);
tree.addTree(pluginTree, "/prefix");
const result = tree.match("/blog/hello");| Method | Behavior |
|---|---|
addTree(source, prefix?) | Merges a RouteNode/RouteTree under an optional path prefix; also imports the source's rewrites. |
mergeNode(target, source) | Merges page/layout/boundaries, unions roles/permissions, concatenates middlewares and children, and recurses into parallel routes. |
addRewrite(from, to) / setRewrites(rewrites) / getRewrites() | Manage rewrite rules; from supports :name, :name(.*), [...name], *. |
resolveRewrite(path) | Applies the first matching rewrite (exact, then pattern). |
match(path) | Returns a MatchResult (isExact: true for a leaf page/route match, false for the deepest partial match) or null. |
generateMetadata(path, searchParams?) | Merges metadata from the root-to-leaf generators and the leaf node. |
getRoot() | The root RouteNode. |
match() first resolves a rewrite, then walks the tree preferring static → group → dynamic → catch-all segments and returns the best partial match when no exact leaf is found.
RouterService#
@Injectable() service constructed with CACHE_PROVIDER, CONFIG_SERVICE, PluginRegistry and NavigationService.
| Method | Behavior |
|---|---|
setSiteLayout(layout) / getSiteLayout() | The layout attached to the (site) route group. |
addRewrite(from, to) / getRewrites() | Rewrite registration. |
clearCache() | Deletes the router:tree cache entry. |
getPluginsWithHomepage() | Plugins whose tree matches /. |
buildRouteTree(includePrivate = false) | Merges enabled plugins' trees (resolving [prefix]), attaches the site layout to (site), and caches the tree under router:tree in production only. |
Discovery helpers#
discovery.ts wraps the service with React cache() and app resolution:
buildRouteTree(includePrivate = false)—cache(...)-wrapped; the usual way to build the tree in a request.addRouteRewrite(from, to)/getRouteRewrites().matchRoute(pattern, path)— re-exported from the matcher.getPluginsWithHomepage()(aliasgetModulesWithHomepage).
The matcher matchRoute(pattern, path) supports :id and :name(.*) and returns Record<string, string> | null; it is distinct from RouteTree.match.
File scanning#
scanner.ts builds trees from a plugin's app/ directory:
discoverRoutes(appDir, dynamicImport?)— scanspage/layout/loading/error/not-found/default/routefiles and folder conventions into aRouteNode;dynamicImport(relPath)lets a bundled host import modules. Skips dot and underscore directories.globToTree(globResult, baseDir?)— builds a tree from animport.meta.glob-style record.generateRouteManifest(appDir, importPrefix)— emits TypeScript source for a staticrouteTree(codegen alternative to runtime discovery).
routeTree: async () => {
const { discoverRoutes } = await import("@veap/plugins/router");
const appDir = path.join(path.dirname(fileURLToPath(import.meta.url)), "app");
return discoverRoutes(appDir, (rel) => import(`./app/${rel}`));
},Middleware#
middlewares.ts exports page (VeapMiddleware) and API (ApiMiddleware) middleware and two runners:
| Export | Behavior |
|---|---|
EnsuredGuest | Redirects authenticated users away (to referer or /). |
EnsuredUser | Redirects unauthenticated users to authRoutes.signIn. |
EnsuredAuth | Requires a session and passes checkSecurity (2FA/onboarding gates); redirects on failure. |
ApiEnsuredAuth | API variant; returns 401 JSON. |
SameOrigin | Page middleware rejecting cross-origin state-changing requests (AppError.Forbidden). |
ApiSameOrigin | API variant; returns 403 JSON. |
SkipSecurity | Marks the context so the renderer skips EnsuredAuth for this route (explicit auth/roles/permissions still apply). |
runPipeline(middlewares, context, final) | Runs a page middleware chain, ending in final. |
runApiPipeline(middlewares, request, context, final) | Runs an API middleware chain. |
SameOrigin/ApiSameOrigin delegate to the kernel's verifySameOrigin (Sec-Fetch-Site plus Origin/Host/X-Forwarded-Host).
Helpers and API handler#
collectMiddlewares(chain, node)/collectApiMiddlewares(chain, node)— flatten the layout chain plus the node's middlewares.collectAuthRequirements(chain, node)—{ needsAuth, roles, permissions }(deduplicated;needsAuthis true when any level requires auth or a role/permission).handleVeapApiRequest(request, { tree, path, searchParams })— matches, resolves the HTTP method handler, prependsApiEnsuredAuthwhen required, builds the context, and runs the API pipeline. Returns404,405or the handler'sResponse.
React integration#
@veap/plugins/router (root) and the /router subpath also export the React layer used by the host's catch-all:
VeapRouter— the renderer: matches the tree, runsrunPipeline, resolves parallel slots, and renders the layout chain withRouterErrorBoundary.withRouter(Component, configOrPath?)— HOC wrapping a physical page so it inherits virtual layouts/parallel slots/middleware;WithRouterConfigis{ path?, roles?, permissions?, middlewares? }.RouterErrorBoundary— client error boundary with an optionalfallbackrender prop ({ error, reset }); reset remounts the subtree.SoftNavigationInterceptor— client component that intercepts internal link clicks and callsonNavigate(href)(or pushes history when absent).
Tokens ROUTER_SERVICE and SITE_LAYOUT are exported from the router entry point as well as common/tokens.