fix(tanstack-start): address review findings

This commit is contained in:
Chris Tate
2026-09-08 10:11:46 -05:00
parent 6b095856f2
commit e13ffc53fc
15 changed files with 405 additions and 42 deletions
+14 -1
View File
@@ -541,7 +541,13 @@ const app = createNextApp({ spec });
```tsx
import { createFileRoute, notFound } from "@tanstack/react-router";
import { PageRenderer, type StartAppSpec } from "@json-render/tanstack-start";
import {
PageRenderer,
StartErrorBoundary,
StartLoading,
StartNotFound,
type StartAppSpec,
} from "@json-render/tanstack-start";
import { createStartApp } from "@json-render/tanstack-start/server";
const spec: StartAppSpec = {
@@ -569,9 +575,16 @@ export const Route = createFileRoute("/$")({
},
head: ({ match }) => getHead({ pathname: match.pathname }),
component: () => <PageRenderer {...Route.useLoaderData()} />,
pendingComponent: StartLoading,
errorComponent: StartErrorBoundary,
notFoundComponent: StartNotFound,
});
```
Wrap the root route's outlet with `<StartAppProvider spec={spec}>` so route
fallback components can resolve the current route. Pass named `$computed`
implementations through its `functions` prop.
### shadcn-svelte (Svelte)
```typescript
@@ -213,6 +213,10 @@ loader, loading/error/not-found specs, and static parameters.
</tbody>
</table>
Loader parameters are URL-decoded before they reach named loaders. Parameter
values supplied through `staticParams` are URL-encoded in the paths returned by
`getStaticPaths()`.
For prerendered dynamic routes, provide `staticParams`:
```typescript
@@ -241,7 +245,12 @@ Wire the helpers to a file-based `$` splat route:
```tsx
// src/routes/$.tsx
import { createFileRoute, notFound } from "@tanstack/react-router";
import { PageRenderer } from "@json-render/tanstack-start";
import {
PageRenderer,
StartErrorBoundary,
StartLoading,
StartNotFound,
} from "@json-render/tanstack-start";
import { getHead, getPageData } from "@/lib/json-app";
export const Route = createFileRoute("/$")({
@@ -252,6 +261,9 @@ export const Route = createFileRoute("/$")({
},
head: ({ match }) => getHead({ pathname: match.pathname }),
component: Page,
pendingComponent: StartLoading,
errorComponent: StartErrorBoundary,
notFoundComponent: StartNotFound,
});
function Page() {
@@ -277,6 +289,7 @@ import {
Scripts,
} from "@tanstack/react-router";
import { StartAppProvider } from "@json-render/tanstack-start";
import { spec } from "@/lib/spec";
export const Route = createRootRoute({
component: () => (
@@ -285,7 +298,11 @@ export const Route = createRootRoute({
<HeadContent />
</head>
<body>
<StartAppProvider registry={registry} handlers={handlers}>
<StartAppProvider
registry={registry}
handlers={handlers}
spec={spec}
>
<Outlet />
</StartAppProvider>
<Scripts />
@@ -295,13 +312,32 @@ export const Route = createRootRoute({
});
```
Passing `spec` lets `StartLoading`, `StartErrorBoundary`, and `StartNotFound`
automatically select the matched route's fallback specs. Their explicit
`loadingSpec`, `errorSpec`, and `notFoundSpec` props take precedence. For a
server-only application spec, omit `spec` and pass client-safe fallback specs
explicitly.
Pass named functions through `functions` when props use `$computed`:
```tsx
<StartAppProvider
registry={registry}
spec={spec}
functions={{ uppercase: ({ value }) => String(value).toUpperCase() }}
>
<Outlet />
</StartAppProvider>
```
## Built-ins
- `Slot` inserts page content into a JSON-defined layout.
- `Link` wraps TanStack Router's `Link`; generated specs use an `href` prop.
- `navigate` performs client-side navigation from action bindings.
- `StartLoading`, `StartErrorBoundary`, and `StartNotFound` can be used for
TanStack Router's route fallback component options.
- `StartLoading`, `StartErrorBoundary`, and `StartNotFound` resolve the matched
route's fallback specs when they are used as TanStack Router boundary
components and the provider receives `spec`.
`Slot` and `Link` are automatically added to the page registry.
+36 -3
View File
@@ -112,7 +112,12 @@ export const { getPageData, getHead, getStaticPaths } = createStartApp({
```tsx
// src/routes/$.tsx
import { createFileRoute, notFound } from "@tanstack/react-router";
import { PageRenderer } from "@json-render/tanstack-start";
import {
PageRenderer,
StartErrorBoundary,
StartLoading,
StartNotFound,
} from "@json-render/tanstack-start";
import { getHead, getPageData } from "@/lib/json-app";
export const Route = createFileRoute("/$")({
@@ -123,6 +128,9 @@ export const Route = createFileRoute("/$")({
},
head: ({ match }) => getHead({ pathname: match.pathname }),
component: Page,
pendingComponent: StartLoading,
errorComponent: StartErrorBoundary,
notFoundComponent: StartNotFound,
});
function Page() {
@@ -147,6 +155,7 @@ import {
} from "@tanstack/react-router";
import { StartAppProvider } from "@json-render/tanstack-start";
import { registry, handlers } from "@/lib/registry";
import { spec } from "@/lib/spec";
export const Route = createRootRoute({ component: Root });
@@ -157,7 +166,11 @@ function Root() {
<HeadContent />
</head>
<body>
<StartAppProvider registry={registry} handlers={handlers}>
<StartAppProvider
registry={registry}
handlers={handlers}
spec={spec}
>
<Outlet />
</StartAppProvider>
<Scripts />
@@ -167,6 +180,25 @@ function Root() {
}
```
Passing `spec` lets the Router boundary components automatically render the
matched route's `loading`, `error`, and `notFound` specs. An explicit
`loadingSpec`, `errorSpec`, or `notFoundSpec` prop overrides this lookup. If the
application spec is server-only, omit `spec` from the provider and supply those
explicit props from client-safe fallback specs.
Pass named functions through the provider when generated props use
`$computed`:
```tsx
<StartAppProvider
registry={registry}
spec={spec}
functions={{ uppercase: ({ value }) => String(value).toUpperCase() }}
>
<Outlet />
</StartAppProvider>
```
## Route Patterns
| Pattern | Matches | Loader params |
@@ -177,7 +209,8 @@ function Root() {
| `/docs/$` | `/docs/a/b` | `{ _splat: ["a", "b"] }` |
Static routes are included in `getStaticPaths()`. Dynamic routes are included
when their route spec supplies `staticParams`.
when their route spec supplies `staticParams`. Loader parameters are URL-decoded,
and parameter values emitted by `getStaticPaths()` are URL-encoded.
Map the paths to TanStack Start's top-level `pages` option when prerendering:
@@ -1,12 +1,14 @@
import React from "react";
import type { Spec } from "@json-render/core";
import { PageRenderer } from "./page-renderer";
import { resolveRouteFallback } from "./route-fallback";
import { useOptionalStartApp } from "./provider";
/** Props accepted by TanStack Router's `errorComponent`. */
export interface StartErrorBoundaryProps {
error: Error;
reset: () => void;
/** Explicit fallback override; otherwise the matched route's spec is used. */
errorSpec?: Spec | null;
}
@@ -17,8 +19,14 @@ export function StartErrorBoundary({
errorSpec,
}: StartErrorBoundaryProps) {
const context = useOptionalStartApp();
if (errorSpec && context) {
return <PageRenderer spec={errorSpec} />;
const resolvedSpec = resolveRouteFallback(
context?.spec,
context?.pathname,
"error",
errorSpec,
);
if (resolvedSpec && context) {
return <PageRenderer spec={resolvedSpec} />;
}
return (
@@ -1,17 +1,25 @@
import React from "react";
import type { Spec } from "@json-render/core";
import { PageRenderer } from "./page-renderer";
import { resolveRouteFallback } from "./route-fallback";
import { useOptionalStartApp } from "./provider";
export interface StartLoadingProps {
/** Explicit fallback override; otherwise the matched route's spec is used. */
loadingSpec?: Spec | null;
}
/** Render a route-specific pending spec or a small default spinner. */
export function StartLoading({ loadingSpec }: StartLoadingProps = {}) {
const context = useOptionalStartApp();
if (loadingSpec && context) {
return <PageRenderer spec={loadingSpec} loading />;
const resolvedSpec = resolveRouteFallback(
context?.spec,
context?.pathname,
"loading",
loadingSpec,
);
if (resolvedSpec && context) {
return <PageRenderer spec={resolvedSpec} loading />;
}
return (
@@ -2,17 +2,25 @@ import React from "react";
import type { NotFoundRouteProps } from "@tanstack/react-router";
import type { Spec } from "@json-render/core";
import { PageRenderer } from "./page-renderer";
import { resolveRouteFallback } from "./route-fallback";
import { useOptionalStartApp } from "./provider";
export interface StartNotFoundProps extends Partial<NotFoundRouteProps> {
/** Explicit fallback override; otherwise the matched route's spec is used. */
notFoundSpec?: Spec | null;
}
/** Render a route-specific not-found spec or a small default 404 view. */
export function StartNotFound({ notFoundSpec }: StartNotFoundProps = {}) {
const context = useOptionalStartApp();
if (notFoundSpec && context) {
return <PageRenderer spec={notFoundSpec} />;
const resolvedSpec = resolveRouteFallback(
context?.spec,
context?.pathname,
"notFound",
notFoundSpec,
);
if (resolvedSpec && context) {
return <PageRenderer spec={resolvedSpec} />;
}
return (
@@ -27,7 +27,13 @@ export function PageRenderer({
layoutSpec,
loading,
}: PageRendererProps) {
const { registry, handlers, navigate } = useStartApp();
const {
registry,
handlers,
spec: appSpec,
functions,
navigate,
} = useStartApp();
const augmentedRegistry: ComponentRegistry = useMemo(
() => ({ ...registry, Link, Slot }),
[registry],
@@ -42,6 +48,11 @@ export function PageRenderer({
}),
[handlers, navigate],
);
const resolvedInitialState = useMemo(() => {
if (initialState !== undefined) return initialState;
if (!appSpec?.state && !spec.state) return undefined;
return { ...appSpec?.state, ...spec.state };
}, [appSpec?.state, initialState, spec.state]);
const page = (
<Renderer spec={spec} registry={augmentedRegistry} loading={loading} />
@@ -50,9 +61,10 @@ export function PageRenderer({
return (
<JSONUIProvider
registry={augmentedRegistry}
initialState={initialState}
initialState={resolvedInitialState}
handlers={actionHandlers}
navigate={navigate}
functions={functions}
>
{layoutSpec ? (
<LayoutWithSlot
@@ -0,0 +1,93 @@
import React from "react";
import {
createMemoryHistory,
createRootRoute,
createRouter,
RouterProvider,
} from "@tanstack/react-router";
import { cleanup, render, screen } from "@testing-library/react";
import { afterEach, beforeAll, describe, expect, it } from "vitest";
import type { Spec } from "@json-render/core";
import type { ComponentRenderProps } from "@json-render/react";
import type { StartAppSpec } from "../types";
import { StartLoading } from "./loading-renderer";
import { PageRenderer } from "./page-renderer";
import { StartAppProvider } from "./provider";
afterEach(cleanup);
beforeAll(() => {
window.scrollTo = () => {};
});
function Text({ element }: ComponentRenderProps<{ value: unknown }>) {
return <span>{String(element.props.value)}</span>;
}
async function renderInRouter(component: React.ReactNode) {
const rootRoute = createRootRoute({ component: () => component });
const router = createRouter({
routeTree: rootRoute,
history: createMemoryHistory({ initialEntries: ["/"] }),
});
await router.load();
return render(<RouterProvider router={router} />);
}
describe("StartAppProvider", () => {
it("forwards named functions to $computed expressions", async () => {
const page: Spec = {
root: "root",
elements: {
root: {
type: "Text",
props: {
value: { $computed: "uppercase", args: { value: "hello" } },
},
children: [],
},
},
};
await renderInRouter(
<StartAppProvider
registry={{ Text }}
functions={{ uppercase: ({ value }) => String(value).toUpperCase() }}
>
<PageRenderer spec={page} />
</StartAppProvider>,
);
expect(screen.getByText("HELLO")).toBeTruthy();
});
it("renders the matched route's loading spec", async () => {
const loading: Spec = {
root: "root",
state: { message: "Loading route" },
elements: {
root: {
type: "Text",
props: { value: { $state: "/message" } },
children: [],
},
},
};
const spec: StartAppSpec = {
state: { message: "Application" },
routes: {
"/": {
page: loading,
loading,
},
},
};
await renderInRouter(
<StartAppProvider registry={{ Text }} spec={spec}>
<StartLoading />
</StartAppProvider>,
);
expect(screen.getByText("Loading route")).toBeTruthy();
});
});
@@ -1,6 +1,8 @@
import React, { createContext, useContext, type ReactNode } from "react";
import { useRouter } from "@tanstack/react-router";
import { useLocation, useRouter } from "@tanstack/react-router";
import type { ComputedFunction } from "@json-render/core";
import type { ComponentRegistry } from "@json-render/react";
import type { StartAppSpec } from "../types";
export interface StartAppContextValue {
registry: ComponentRegistry;
@@ -8,6 +10,9 @@ export interface StartAppContextValue {
string,
(params: Record<string, unknown>) => Promise<unknown> | unknown
>;
spec?: StartAppSpec;
functions?: Record<string, ComputedFunction>;
pathname: string;
navigate: (href: string) => void;
}
@@ -19,16 +24,23 @@ export interface StartAppProviderProps {
string,
(params: Record<string, unknown>) => Promise<unknown> | unknown
>;
/** Application spec used to resolve route-specific fallback components. */
spec?: StartAppSpec;
/** Named functions available to `$computed` prop expressions. */
functions?: Record<string, ComputedFunction>;
children: ReactNode;
}
/** Provide the component registry, actions, and TanStack navigation. */
/** Provide rendering dependencies, route fallbacks, and TanStack navigation. */
export function StartAppProvider({
registry,
handlers,
spec,
functions,
children,
}: StartAppProviderProps) {
const router = useRouter();
const pathname = useLocation({ select: (location) => location.pathname });
const navigate = React.useCallback(
(href: string) => {
void router.navigate({ to: href });
@@ -36,8 +48,8 @@ export function StartAppProvider({
[router],
);
const value = React.useMemo(
() => ({ registry, handlers, navigate }),
[registry, handlers, navigate],
() => ({ registry, handlers, spec, functions, pathname, navigate }),
[registry, handlers, spec, functions, pathname, navigate],
);
return (
@@ -0,0 +1,49 @@
import { describe, expect, it } from "vitest";
import type { Spec } from "@json-render/core";
import type { StartAppSpec } from "../types";
import { resolveRouteFallback } from "./route-fallback";
function page(type: string): Spec {
return {
root: "root",
elements: { root: { type, props: {}, children: [] } },
};
}
describe("resolveRouteFallback", () => {
const loading = page("Loading");
const error = page("Error");
const notFound = page("NotFound");
const spec: StartAppSpec = {
routes: {
"/blog/$slug": {
page: page("Page"),
loading,
error,
notFound,
},
},
};
it("resolves each fallback from the matched route", () => {
expect(resolveRouteFallback(spec, "/blog/post", "loading", undefined)).toBe(
loading,
);
expect(resolveRouteFallback(spec, "/blog/post", "error", undefined)).toBe(
error,
);
expect(
resolveRouteFallback(spec, "/blog/post", "notFound", undefined),
).toBe(notFound);
});
it("prefers an explicit fallback and allows null to disable one", () => {
const explicit = page("Explicit");
expect(resolveRouteFallback(spec, "/blog/post", "loading", explicit)).toBe(
explicit,
);
expect(
resolveRouteFallback(spec, "/blog/post", "loading", null),
).toBeNull();
});
});
@@ -0,0 +1,17 @@
import type { Spec } from "@json-render/core";
import { matchRoute } from "../router";
import type { StartAppSpec } from "../types";
export type RouteFallbackKind = "loading" | "error" | "notFound";
/** Resolve an explicit fallback or the fallback on the currently matched route. */
export function resolveRouteFallback(
spec: StartAppSpec | undefined,
pathname: string | undefined,
kind: RouteFallbackKind,
explicitSpec: Spec | null | undefined,
): Spec | null | undefined {
if (explicitSpec !== undefined) return explicitSpec;
if (!spec || pathname === undefined) return undefined;
return matchRoute(spec, pathname)?.route[kind];
}
+1 -1
View File
@@ -46,7 +46,7 @@ export type {
SetState,
StateModel,
} from "./catalog-types";
export type { Spec, StateStore } from "@json-render/core";
export type { ComputedFunction, Spec, StateStore } from "@json-render/core";
export { createStateStore } from "@json-render/core";
export type {
ComponentRegistry,
@@ -35,6 +35,23 @@ describe("matchRoute", () => {
expect(matched?.params).toEqual({ slug: "hello" });
});
it("decodes dynamic and splat parameters", () => {
expect(
matchRoute(specWith({ "/blog/$slug": {} }), "/blog/hello%20world")
?.params,
).toEqual({ slug: "hello world" });
expect(
matchRoute(specWith({ "/docs/$": {} }), "/docs/guides/hello%20world")
?.params,
).toEqual({ _splat: ["guides", "hello world"] });
});
it("does not match malformed encoded parameters", () => {
expect(
matchRoute(specWith({ "/blog/$slug": {} }), "/blog/%E0%A4%A"),
).toBeNull();
});
it("captures zero or more splat segments under _splat", () => {
const spec = specWith({ "/docs/$": {} });
expect(matchRoute(spec, "/docs")?.params).toEqual({ _splat: [] });
@@ -54,6 +71,28 @@ describe("matchRoute", () => {
expect(matchRoute(spec, "/blog/2026/post")?.pattern).toBe("/blog/$");
});
it("ranks earlier static segments ahead of later static segments", () => {
const spec = specWith({
"/$type/edit": {},
"/posts/$id": {},
});
expect(matchRoute(spec, "/posts/edit")?.pattern).toBe("/posts/$id");
});
it("lets an earlier static segment outrank a later splat", () => {
const spec = specWith({
"/$type/edit": {},
"/posts/$": {},
});
expect(matchRoute(spec, "/posts/edit")?.pattern).toBe("/posts/$");
});
it("matches static segments case-insensitively like TanStack Router", () => {
expect(matchRoute(specWith({ "/about": {} }), "/ABOUT")?.pattern).toBe(
"/about",
);
});
it("returns null for an unmatched path", () => {
expect(matchRoute(specWith({ "/": {} }), "/missing")).toBeNull();
});
@@ -73,6 +112,7 @@ describe("static paths", () => {
staticParams: [{ slug: "hello" }, { slug: "world" }],
},
"/docs/$": { staticParams: [{ _splat: "guides/intro" }] },
"/search/$query": { staticParams: [{ query: "hello world" }] },
"/users/$id": {},
});
expect(collectStaticPaths(spec)).toEqual([
@@ -81,6 +121,7 @@ describe("static paths", () => {
"/blog/hello",
"/blog/world",
"/docs/guides/intro",
"/search/hello%20world",
]);
});
});
+34 -17
View File
@@ -4,40 +4,44 @@ interface CompiledRoute {
pattern: string;
regex: RegExp;
paramNames: string[];
splat: boolean;
specificity: number;
segmentRanks: number[];
}
const SPLAT_PARAM = "_splat";
const STATIC_SEGMENT_RANK = 3;
const DYNAMIC_SEGMENT_RANK = 2;
const SPLAT_SEGMENT_RANK = 1;
/** Compile a TanStack Router pattern into a pathname matcher. */
function compileRoute(pattern: string): CompiledRoute {
const paramNames: string[] = [];
let splat = false;
let specificity = 0;
const segments = pattern === "/" ? [""] : pattern.split("/").slice(1);
const regexParts: string[] = [];
const segmentRanks: number[] = [];
for (const segment of segments) {
if (segment === "$") {
paramNames.push(SPLAT_PARAM);
splat = true;
segmentRanks.push(SPLAT_SEGMENT_RANK);
regexParts.push("(?:/(.+))?");
} else if (segment.startsWith("$") && segment.length > 1) {
paramNames.push(segment.slice(1));
segmentRanks.push(DYNAMIC_SEGMENT_RANK);
regexParts.push("/([^/]+)");
} else {
specificity++;
segmentRanks.push(STATIC_SEGMENT_RANK);
regexParts.push(`/${escapeRegExp(segment)}`);
}
}
return {
pattern,
regex: new RegExp(pattern === "/" ? "^/$" : `^${regexParts.join("")}$`),
regex: new RegExp(
pattern === "/" ? "^/$" : `^${regexParts.join("")}$`,
"i",
),
paramNames,
splat,
specificity,
segmentRanks,
};
}
@@ -54,11 +58,13 @@ export function matchRoute(
const compiled = Object.keys(spec.routes).map(compileRoute);
compiled.sort((a, b) => {
if (a.splat !== b.splat) return a.splat ? 1 : -1;
if (a.specificity !== b.specificity) {
return b.specificity - a.specificity;
const segmentCount = Math.max(a.segmentRanks.length, b.segmentRanks.length);
for (let index = 0; index < segmentCount; index++) {
const aRank = a.segmentRanks[index] ?? 0;
const bRank = b.segmentRanks[index] ?? 0;
if (aRank !== bRank) return bRank - aRank;
}
return a.paramNames.length - b.paramNames.length;
return 0;
});
for (const candidate of compiled) {
@@ -66,12 +72,23 @@ export function matchRoute(
if (!match) continue;
const params: Record<string, string | string[]> = {};
let validParams = true;
for (let index = 0; index < candidate.paramNames.length; index++) {
const name = candidate.paramNames[index]!;
const value = match[index + 1];
params[name] =
name === SPLAT_PARAM ? (value ? value.split("/") : []) : (value ?? "");
try {
params[name] =
name === SPLAT_PARAM
? value
? value.split("/").map(decodeURIComponent)
: []
: decodeURIComponent(value ?? "");
} catch {
validParams = false;
break;
}
}
if (!validParams) continue;
return {
route: spec.routes[candidate.pattern]!,
@@ -117,11 +134,11 @@ function buildPathFromPattern(
for (const segment of pattern.split("/").slice(1)) {
if (segment === "$") {
const value = params[SPLAT_PARAM];
if (value) result.push(...value.split("/"));
if (value) result.push(...value.split("/").map(encodeURIComponent));
} else if (segment.startsWith("$") && segment.length > 1) {
const value = params[segment.slice(1)];
if (!value) return null;
result.push(value);
result.push(encodeURIComponent(value));
} else {
result.push(segment);
}
+20 -4
View File
@@ -113,13 +113,19 @@ State merge precedence is application state, page state, then loader data.
`getHead` merges app and route metadata into TanStack `meta` and `links`
descriptors. `getStaticPaths` includes static routes plus dynamic routes with
`staticParams`. Convert its strings to `{ path }` objects for TanStack Start's
top-level `pages` plugin option.
top-level `pages` plugin option. Loader params are URL-decoded, while values
from `staticParams` are URL-encoded in generated paths.
## Route Wiring
```tsx
import { createFileRoute, notFound } from "@tanstack/react-router";
import { PageRenderer } from "@json-render/tanstack-start";
import {
PageRenderer,
StartErrorBoundary,
StartLoading,
StartNotFound,
} from "@json-render/tanstack-start";
import { getHead, getPageData } from "@/lib/json-app";
export const Route = createFileRoute("/$")({
@@ -130,6 +136,9 @@ export const Route = createFileRoute("/$")({
},
head: ({ match }) => getHead({ pathname: match.pathname }),
component: () => <PageRenderer {...Route.useLoaderData()} />,
pendingComponent: StartLoading,
errorComponent: StartErrorBoundary,
notFoundComponent: StartNotFound,
});
```
@@ -151,6 +160,7 @@ import {
Scripts,
} from "@tanstack/react-router";
import { StartAppProvider } from "@json-render/tanstack-start";
import { spec } from "@/lib/spec";
export const Route = createRootRoute({
component: () => (
@@ -159,7 +169,11 @@ export const Route = createRootRoute({
<HeadContent />
</head>
<body>
<StartAppProvider registry={registry} handlers={handlers}>
<StartAppProvider
registry={registry}
handlers={handlers}
spec={spec}
>
<Outlet />
</StartAppProvider>
<Scripts />
@@ -171,7 +185,9 @@ export const Route = createRootRoute({
Use `StartLoading`, `StartErrorBoundary`, and `StartNotFound` for TanStack
Router's `pendingComponent`, `errorComponent`, and `notFoundComponent` options.
Each accepts an optional json-render fallback spec.
When `StartAppProvider` receives `spec`, each component selects the matched
route's corresponding fallback. Explicit fallback props override that lookup.
Pass named `$computed` implementations through `StartAppProvider.functions`.
Import React components from `@json-render/tanstack-start`. Import `schema`,
`createStartApp`, `matchRoute`, `resolveMetadata`, and static path helpers from