@stackra/routing
v2.0.0
Published
Client-side routing for the Stackra framework — guards, middleware, SEO, analytics, breadcrumbs, route modes (page/dialog/drawer/sheet), advanced matchers (subdomain/query/header/hash), and a `defineRoute()` builder that composes with React Router v7.
Downloads
363
Maintainers
Readme
@stackra/routing
Client-side routing for the Stackra framework — guards, middleware, SEO,
analytics, breadcrumbs, route modes (page / dialog / drawer / sheet), advanced
matchers (subdomain / query / header / hash), a defineRoute() builder that
composes with React Router v7, and two auto-discovery paths that eliminate the
RoutingModule.forFeature({ routes }) boilerplate that used to live in every
Web<Pkg>Module.
Auto-discovery — two paths
Every route in a Stackra app takes one of three shapes today. Pick per file based on what the file needs:
Shape 1 — @Route class (framework packages that need DI). The class
extends BaseRoute, injects whatever config / flags / services the route needs,
and returns the record from getRecord():
// packages/frontend/rbac/src/react/routes/roles-list/roles-list.route.tsx
import { BaseRoute } from "@stackra/routing";
import { Route } from "@stackra/decorators/routing";
import { Inject } from "@stackra/container";
import { RBAC_CONFIG } from "@stackra/contracts";
@Route({ id: "rbac.roles-list", source: "rbac" })
export class RolesListRoute extends BaseRoute {
public constructor(
@Inject(RBAC_CONFIG) private readonly config: IRbacConfig,
) {
super();
}
public getRecord() {
return { path: this.config.routePaths.rolesList, Component: RolesListPage };
}
}The class lands in the package's Web<Pkg>Module.forRoot() providers array;
the RouteLoader walks the DI graph at OnApplicationBootstrap and
auto-registers every @Route-decorated class into the RouteRegistry. No
RoutingModule.forFeature(...) call needed.
Shape 2 — file-based via the Vite collectRoutes plugin (app-level routes).
For app-level static routes that don't need DI. Each route file just exports a
default record:
// apps/dashboard/src/routes/some-page.route.tsx
import { defineRoute } from "@stackra/routing";
export default defineRoute({ path: "/some-page", Component: SomePage });Enable the collector in the app's vite.config.ts:
import { router } from "@stackra/routing/vite";
export default defineConfig({
plugins: [
router({
collectRoutes: { globs: ["src/routes/**/*.route.tsx"] },
}),
],
});Then feed the collected list into the routing module:
// apps/dashboard/src/main.tsx (or app.module.ts)
import collectedRoutes from "virtual:stackra-routing/collected-routes";
import { RoutingModule } from "@stackra/routing";
@Module({
imports: [RoutingModule.forRoot({ basename: "/", routes: collectedRoutes })],
})
export class AppModule {}Add the ambient module type in apps/<name>/src/vite-env.d.ts:
/// <reference types="@stackra/routing/client" />Shape 3 — plain records + RoutingModule.forFeature({ routes }). Legacy.
Still supported for backward compat; new code should pick Shape 1 or 2.
Which shape to pick
- Framework package (
@stackra/rbac,@stackra/grants, ...) whose route path depends on injected config → Shape 1. - App-level static route with no DI need → Shape 2.
- Existing code that already works → leave alone.
Phase F.1 — core. This release ships the non-React CORE only. The React subpath (
@stackra/routing/react) lands in F.2; the Vite plugin (@stackra/routing/vite) lands in F.3. Every symbol below is framework-agnostic and safe to import from a Node build script or a browser bundle alike.
Install
pnpm add @stackra/routing @stackra/container @stackra/contracts @stackra/decorators \
@stackra/error @stackra/events @stackra/logger @stackra/pipeline @stackra/support \
react-router reflect-metadataQuick start (F.1 surface)
import { Module } from "@stackra/container";
import {
RoutingModule,
defineRoute,
defineLayout,
definePage,
} from "@stackra/routing";
import { subdomain, query } from "@stackra/routing/matchers";
import { organization, article } from "@stackra/routing/seo";
@Module({
imports: [
RoutingModule.forRoot({
basename: "/",
rootDomain: "figentra.com",
seo: {
baseUrl: "https://figentra.com",
defaults: {
jsonLd: [
organization({ name: "Stackra", url: "https://figentra.com" }),
],
},
},
}),
],
})
export class AppModule {}Native usage (@stackra/routing/native)
The native subpath ships the React Navigation v7 counterpart to the web subpath.
defineScreen() mirrors defineRoute(), and <StackraNativeRoutingProvider>
composes <NavigationContainer> with a LinkingOptions config auto-built from
every screen's path field.
Install the native peers alongside the routing package:
pnpm add @react-navigation/native @react-navigation/native-stack \
react-native-screens react-native-safe-area-contextWire it in the app module + provider stack:
// src/config/routing.config.ts
import { registerAs } from "@stackra/config";
import { defineScreen } from "@stackra/routing/native";
import type { INativeRoutingModuleOptions } from "@stackra/contracts";
import { HomeScreen } from "@/screens/home.screen";
import { SettingsScreen } from "@/screens/settings.screen";
export const routingConfig = registerAs<INativeRoutingModuleOptions>(
"routing",
() => ({
initialRouteName: "Home",
screens: [
defineScreen({ name: "Home", component: HomeScreen, path: "" }),
defineScreen({
name: "Settings",
component: SettingsScreen,
path: "settings",
}),
],
linking: {
enabled: true,
prefixes: ["stackra://", "https://app.stackra.com"],
},
}),
);// src/app.module.ts
import { Module } from "@stackra/container";
import { ConfigModule } from "@stackra/config";
import { NativeRoutingModule } from "@stackra/routing/native";
import { routingConfig } from "@/config/routing.config";
@Module({
imports: [
ConfigModule.forRoot({ isGlobal: true, load: [routingConfig] }),
NativeRoutingModule.forRoot(routingConfig()),
],
})
export class AppModule {}// App.tsx
import "reflect-metadata";
import { ApplicationFactory } from "@stackra/container";
import { ContainerProvider } from "@stackra/container/react";
import { StackraNativeRoutingProvider } from "@stackra/routing/native";
import { AppModule } from "@/app.module";
import { RootNavigator } from "@/navigation/root-navigator";
export default function App() {
const app = ApplicationFactory.create(AppModule);
return (
<ContainerProvider context={app}>
<StackraNativeRoutingProvider>
<RootNavigator />
</StackraNativeRoutingProvider>
</ContainerProvider>
);
}// src/navigation/root-navigator.tsx
import { createNativeStackNavigator } from "@react-navigation/native-stack";
import { useStackraNativeRouting } from "@stackra/routing/native";
const Stack = createNativeStackNavigator();
export function RootNavigator() {
const { screens, config } = useStackraNativeRouting();
return (
<Stack.Navigator initialRouteName={config.initialRouteName}>
{screens.map((screen) => (
<Stack.Screen
key={screen.name}
name={screen.name}
component={screen.component}
options={screen.options}
initialParams={screen.initialParams}
/>
))}
</Stack.Navigator>
);
}The provider iterates the registry as the single source of truth — adding a
screen means editing routing.config.ts only; the navigator picks it up without
a code change.
Subpaths
| Import | Purpose |
| --------------------------- | ---------------------------------------------------------------------------------------------------------- |
| @stackra/routing | Core module + services (registry, matcher, middleware/guard resolvers, SEO) |
| @stackra/routing/matchers | Callable matcher builders — subdomain, query, header, hash |
| @stackra/routing/seo | JSON-LD builders (article, organization, faqPage, …) |
| @stackra/routing/rrv7 | Type-only re-exports of RRv7 primitives (IRrvRouteObject, …) |
| @stackra/routing/native | React Navigation v7 bindings — defineScreen, <StackraNativeRoutingProvider>, useStackraNativeRouting |
| @stackra/routing/testing | Unit-test helpers (createMockGuardContext, runGuard, …) |
What's coming later
- F.2 — React subpath.
<Link>,useNavigate(),<SeoHead />,<StackraRoutingProvider>,<OverlayOutlet />,<Breadcrumbs />, dev-tools panel. - F.3 — Vite plugin.
router()plugin for dev subdomain wiring, build-time prerender pipeline, subdomain output split. - G — AI integration.
<AiRouteContext>+navigateToolbinding. - Native follow-ups.
NativeRoutingModule.forFeature({ name, screens })for cross-package screen contributions; Liquid Glass tabs via Callstack's@bottom-tabs/react-navigation— both deferred to follow-up specs.
License
MIT
