Docs/API Reference/Plugins

Virtual router

@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.…

6 min read

@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:

FieldMeaningFile equivalent
segmentURL segment; "" is the rootfolder name
pagePage componentpage.tsx
layoutLayout wrapping childrenlayout.tsx
loadingLoading componentloading.tsx
errorError component ({ error, reset })error.tsx
notFoundNot-found componentnot-found.tsx
defaultParallel slot defaultdefault.tsx
routeHTTP method handlers (GET, POST, …)route.ts
generateMetadataMetadata generator ({ params, searchParams })generateMetadata export
middlewaresMiddleware pipeline for this nodemiddlewares export
auth / roles / permissionsAuth/RBAC requirementsmodule exports
breadcrumbBreadcrumbValuemodule export
idRoute id for template overrides—
childrenChild nodessubfolders
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#

ts
const tree = new RouteTree(initialRoot?, initialRewrites?);
tree.addTree(pluginTree, "/prefix");
const result = tree.match("/blog/hello");
MethodBehavior
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.

MethodBehavior
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() (alias getModulesWithHomepage).

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?) — scans page/layout/loading/error/not-found/default/route files and folder conventions into a RouteNode; dynamicImport(relPath) lets a bundled host import modules. Skips dot and underscore directories.
  • globToTree(globResult, baseDir?) — builds a tree from an import.meta.glob-style record.
  • generateRouteManifest(appDir, importPrefix) — emits TypeScript source for a static routeTree (codegen alternative to runtime discovery).
ts
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:

ExportBehavior
EnsuredGuestRedirects authenticated users away (to referer or /).
EnsuredUserRedirects unauthenticated users to authRoutes.signIn.
EnsuredAuthRequires a session and passes checkSecurity (2FA/onboarding gates); redirects on failure.
ApiEnsuredAuthAPI variant; returns 401 JSON.
SameOriginPage middleware rejecting cross-origin state-changing requests (AppError.Forbidden).
ApiSameOriginAPI variant; returns 403 JSON.
SkipSecurityMarks 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; needsAuth is true when any level requires auth or a role/permission).
  • handleVeapApiRequest(request, { tree, path, searchParams }) — matches, resolves the HTTP method handler, prepends ApiEnsuredAuth when required, builds the context, and runs the API pipeline. Returns 404, 405 or the handler's Response.

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, runs runPipeline, resolves parallel slots, and renders the layout chain with RouterErrorBoundary.
  • withRouter(Component, configOrPath?) — HOC wrapping a physical page so it inherits virtual layouts/parallel slots/middleware; WithRouterConfig is { path?, roles?, permissions?, middlewares? }.
  • RouterErrorBoundary — client error boundary with an optional fallback render prop ({ error, reset }); reset remounts the subtree.
  • SoftNavigationInterceptor — client component that intercepts internal link clicks and calls onNavigate(href) (or pushes history when absent).

Tokens ROUTER_SERVICE and SITE_LAYOUT are exported from the router entry point as well as common/tokens.

veap
Preparing0%