[ Interface ]
@sdxc/lazy-route
Maps a route to a module imported on the first request that reaches it
- Used by
- uptime, auth-saas, reader, blog, books
- Source
- packages/lazy-route
Maps a route to a module imported on the first request that reaches it.
Installation
npm add @sdxc/lazy-route
It plugs into the router from remix, which installs alongside it.
Usage
Defer a route
Replace the static import with a loader. The route is still mapped at startup, so matching and href() work as before; the module arrives with the first request that matches.
import { lazy } from "@sdxc/lazy-route";
import { createRouter } from "remix/router";
import { get, route } from "remix/routes";
let routes = route({ bookmarks: get("/bookmarks") });
let router = createRouter();
router.map(
routes.bookmarks,
lazy(() => import("./controllers/bookmarks.js")),
);
The loader runs once and every later request reuses its module.
Map actions and controllers alike
A module that default-exports a createAction(...) maps to a single route, and one that default-exports a createController(...) maps to a route map. The router picks the shape from the route it is given.
router.map(
routes.home,
lazy(() => import("./controllers/home.js")),
);
router.map(
routes.session,
lazy(() => import("./controllers/session.js")),
);
Every action of a controller shares one import: the first request to any of them loads the module for all.
Keep the module's own middleware
The middleware an action or controller declares travels with it and runs after the router's chain and before the handler, exactly as it does when imported statically.
// ./controllers/settings.ts
import { createController } from "remix/router";
export default createController(routes.settings, {
middleware: [requireAdmin],
actions: {
index: () => new Response("settings"),
},
});
Guard a route before it loads
Middleware passed as the second argument runs ahead of the module's own, and a guard that answers does so without importing the module.
router.map(
routes.admin,
lazy(() => import("./controllers/admin.js"), [requireAdmin]),
);
Load a named export
A loader may resolve the handler itself instead of a module with a default export.
router.map(
routes.health,
lazy(() => import("./controllers/health.js").then((module) => module.check)),
);
API
lazy(load, middleware?)
function lazy<module>(
load: () => Promise<module>,
middleware?: readonly Middleware[],
): Loaded<module>;
Returns a stand-in typed as the module's default export (or as the resolved value when it has no default), to pass wherever router.map() takes an action or a controller. load runs on the first matching request and its result serves every request after it, including a rejected import.
middleware runs before the module's own chain. It takes middleware with no context transform: publish context values from the router's chain or from inside the module.
Because the stand-in keeps the module's type, mapping a controller to a single route, an action to a route map, or a controller missing a declared action is a type error at router.map(). The router's own runtime checks for those mistakes run on the first request instead, and throw a TypeError naming the problem.
Pattern: Deferring The Whole Route Table
Each route pays one import on its first request, and a route the process never serves costs only its pattern. On a runtime that starts an isolate per cold request, deferring every route keeps startup to the router itself.
import { lazy } from "@sdxc/lazy-route";
import { createRouter } from "remix/router";
import { get, route } from "remix/routes";
let routes = route({
home: get("/"),
search: get("/search"),
account: get("/account"),
});
let router = createRouter();
router.map(
routes.home,
lazy(() => import("./controllers/home.js")),
);
router.map(
routes.search,
lazy(() => import("./controllers/search.js")),
);
router.map(
routes.account,
lazy(() => import("./controllers/account.js")),
);
export default { fetch: (request: Request) => router.fetch(request) };
Point each loader at a bare import() with a literal specifier so the bundler splits the module into its own chunk.
Pattern: One Guard For A Route Group
A section that shares its guards passes the same list to every lazy() call. An unauthorized request is answered before the controller it targets is imported, so a section nobody may enter is never loaded.
import type { Middleware } from "remix/router";
import { lazy } from "@sdxc/lazy-route";
import { createRouter } from "remix/router";
import { get, route } from "remix/routes";
let routes = route({
admin: {
dashboard: get("/admin"),
articles: get("/admin/articles"),
},
});
let requireAdmin: Middleware = (context, next) => {
if (context.request.headers.get("x-role") !== "admin") {
return new Response("Forbidden", { status: 403 });
}
return next();
};
let router = createRouter();
router.map(
routes.admin.dashboard,
lazy(() => import("./controllers/admin/dashboard.js"), [requireAdmin]),
);
router.map(
routes.admin.articles,
lazy(() => import("./controllers/admin/articles.js"), [requireAdmin]),
);
Pattern: A Route Map Spread Across Modules
A stand-in is a valid action on its own, so a route map whose actions live in different files maps one loader per action. Inside createController() they keep the route map's typing and run behind the controller's middleware, which answers before any of them loads.
import type { Middleware } from "remix/router";
import { lazy } from "@sdxc/lazy-route";
import { createController, createRouter } from "remix/router";
import { get, route } from "remix/routes";
let routes = route({
feeds: {
all: get("/feeds"),
articles: get("/feeds/articles"),
tutorials: get("/feeds/tutorials"),
},
});
let requireSignIn: Middleware = (context, next) => {
if (!context.request.headers.has("authorization")) {
return new Response("Unauthorized", { status: 401 });
}
return next();
};
let router = createRouter();
router.map(
routes.feeds,
createController(routes.feeds, {
middleware: [requireSignIn],
actions: {
all: lazy(() => import("./controllers/feeds/all.js")),
articles: lazy(() =>
import("./controllers/feeds/posts.js").then((module) => module.articles),
),
tutorials: lazy(() =>
import("./controllers/feeds/posts.js").then((module) => module.tutorials),
),
},
}),
);
articles and tutorials name the same specifier to pick different named exports; they share one chunk, imported by whichever of them is requested first.