@richie-router/server 0.1.7 → 0.1.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -40,6 +40,7 @@ var __export = (target, all) => {
40
40
  var exports_src = {};
41
41
  __export(exports_src, {
42
42
  matchesSpaPath: () => matchesSpaPath,
43
+ matchesPassthroughPath: () => matchesPassthroughPath,
43
44
  handleSpaRequest: () => handleSpaRequest,
44
45
  handleRequest: () => handleRequest,
45
46
  handleHeadTagRequest: () => handleHeadTagRequest,
@@ -236,6 +237,15 @@ function resolveSpaRoutes(spaRoutesManifest) {
236
237
  }
237
238
  return spaRoutes;
238
239
  }
240
+ function resolveHostedRouting(options) {
241
+ if ("routeManifest" in options) {
242
+ return import_core.resolveHostedRoutingConfig(options.routeManifest.hostedRouting);
243
+ }
244
+ return import_core.resolveHostedRoutingConfig(options.spaRoutesManifest.hostedRouting);
245
+ }
246
+ function matchesPassthroughLocation(options, location) {
247
+ return resolveHostedRouting(options).passthrough.some((route) => import_core.matchPathname(route, location.pathname) !== null);
248
+ }
239
249
  function matchesSpaLocation(options, location) {
240
250
  if ("routeManifest" in options) {
241
251
  return buildMatches(options.routeManifest, location).length > 0;
@@ -249,9 +259,16 @@ function matchesSpaPath(path, options) {
249
259
  }
250
260
  return matchesSpaLocation(options, documentRequest.location);
251
261
  }
262
+ function matchesPassthroughPath(path, options) {
263
+ const documentRequest = resolveDocumentPath(path, options.basePath);
264
+ if (documentRequest === null) {
265
+ return false;
266
+ }
267
+ return matchesPassthroughLocation(options, documentRequest.location);
268
+ }
252
269
  async function executeHeadTag(request, headTags, routeId, params, rawSearch) {
253
270
  const definition = headTags.definitions[routeId];
254
- const schemaEntry = headTags.routerSchema[routeId];
271
+ const schemaEntry = import_core.getRouteSchemaEntry(headTags.routerSchema, routeId);
255
272
  if (!definition) {
256
273
  throw new Error(`Unknown server head route "${routeId}".`);
257
274
  }
@@ -372,7 +389,7 @@ async function handleDocumentHeadRequest(request, options, href) {
372
389
  async function handleHeadRequest(request, options) {
373
390
  const url = new URL(request.url);
374
391
  const basePath = normalizeBasePath(options.basePath);
375
- const headBasePath = options.headBasePath ?? prependBasePathToPathname("/head-api", basePath);
392
+ const headBasePath = prependBasePathToPathname(import_core.getRouterSchemaHostedRouting(options.headTags.routerSchema).headBasePath, basePath);
376
393
  if (url.pathname !== headBasePath) {
377
394
  return {
378
395
  matched: false,
@@ -445,8 +462,7 @@ async function handleRequest(request, options) {
445
462
  const basePath = normalizeBasePath(options.basePath);
446
463
  const handledHeadTagRequest = await handleHeadTagRequest(request, {
447
464
  headTags: options.headTags,
448
- basePath,
449
- headBasePath: options.headBasePath
465
+ basePath
450
466
  });
451
467
  if (handledHeadTagRequest.matched) {
452
468
  return handledHeadTagRequest;
@@ -29,6 +29,9 @@ function createTestArtifacts(options) {
29
29
  "/about": {
30
30
  serverHead: true
31
31
  }
32
+ }, {
33
+ passthrough: ["/api/$"],
34
+ headBasePath: options?.headBasePath
32
35
  });
33
36
  const headTags = import__.defineHeadTags(rootRoute, routerSchema, {
34
37
  "/about": {
@@ -49,6 +52,10 @@ function createTestArtifacts(options) {
49
52
  }
50
53
  }
51
54
  });
55
+ rootRoute._setHostedRouting({
56
+ headBasePath: options?.headBasePath ?? "/head-api",
57
+ passthrough: [options?.headBasePath ?? "/head-api", "/api/$"]
58
+ });
52
59
  return {
53
60
  routeManifest: rootRoute,
54
61
  headTags,
@@ -63,7 +70,11 @@ function createTestArtifacts(options) {
63
70
  { id: "/posts/", to: "/posts", parentId: "/posts", isRoot: false },
64
71
  { id: "/posts/$postId", to: "/posts/$postId", parentId: "/posts", isRoot: false }
65
72
  ],
66
- spaRoutes: ["/", "/about", "/dashboard", "/posts", "/posts/$postId"]
73
+ spaRoutes: ["/", "/about", "/dashboard", "/posts", "/posts/$postId"],
74
+ hostedRouting: {
75
+ headBasePath: options?.headBasePath ?? "/head-api",
76
+ passthrough: [options?.headBasePath ?? "/head-api", "/api/$"]
77
+ }
67
78
  }
68
79
  };
69
80
  }
@@ -166,6 +177,21 @@ import_bun_test.describe("handleSpaRequest", () => {
166
177
  basePath: "/project"
167
178
  })).toBe(false);
168
179
  });
180
+ import_bun_test.test("exposes passthrough matching for host-side routing decisions", () => {
181
+ const { spaRoutesManifest, routeManifest } = createTestArtifacts();
182
+ import_bun_test.expect(import__.matchesPassthroughPath("/project/head-api", {
183
+ spaRoutesManifest,
184
+ basePath: "/project"
185
+ })).toBe(true);
186
+ import_bun_test.expect(import__.matchesPassthroughPath("/project/api/health", {
187
+ routeManifest,
188
+ basePath: "/project"
189
+ })).toBe(true);
190
+ import_bun_test.expect(import__.matchesPassthroughPath("/project/about", {
191
+ spaRoutesManifest,
192
+ basePath: "/project"
193
+ })).toBe(false);
194
+ });
169
195
  import_bun_test.test("matches document requests under the basePath with a routeManifest", async () => {
170
196
  const { routeManifest } = createTestArtifacts();
171
197
  const result = await import__.handleSpaRequest(new Request("https://example.com/project/about"), {
@@ -314,6 +340,22 @@ import_bun_test.describe("handleRequest basePath", () => {
314
340
  ]
315
341
  });
316
342
  });
343
+ import_bun_test.test("reads a custom headBasePath from the router schema", async () => {
344
+ const { headTags } = createTestArtifacts({
345
+ headBasePath: "/meta"
346
+ });
347
+ const result = await import__.handleHeadTagRequest(new Request("https://example.com/project/meta?routeId=%2Fabout&params=%7B%7D&search=%7B%7D"), {
348
+ headTags,
349
+ basePath: "/project"
350
+ });
351
+ import_bun_test.expect(result.matched).toBe(true);
352
+ import_bun_test.expect(result.response.status).toBe(200);
353
+ import_bun_test.expect(await result.response.json()).toEqual({
354
+ head: [
355
+ { tag: "title", children: "About" }
356
+ ]
357
+ });
358
+ });
317
359
  import_bun_test.test("resolves merged document head payloads for host-rendered HTML shells", async () => {
318
360
  const { headTags } = createTestArtifacts();
319
361
  const result = await import__.handleHeadRequest(new Request("https://example.com/project/head-api?href=%2Fproject%2Fabout"), {
@@ -4,11 +4,14 @@ import {
4
4
  createParsedLocation,
5
5
  defaultParseSearch,
6
6
  defaultStringifySearch,
7
+ getRouteSchemaEntry,
8
+ getRouterSchemaHostedRouting,
7
9
  isNotFound,
8
10
  isRedirect,
9
11
  matchPathname,
10
12
  matchRouteTree,
11
13
  resolveHeadConfig,
14
+ resolveHostedRoutingConfig,
12
15
  serializeHeadConfig
13
16
  } from "@richie-router/core";
14
17
  function defineHeadTags(routeManifest, routerSchema, definitions) {
@@ -199,6 +202,15 @@ function resolveSpaRoutes(spaRoutesManifest) {
199
202
  }
200
203
  return spaRoutes;
201
204
  }
205
+ function resolveHostedRouting(options) {
206
+ if ("routeManifest" in options) {
207
+ return resolveHostedRoutingConfig(options.routeManifest.hostedRouting);
208
+ }
209
+ return resolveHostedRoutingConfig(options.spaRoutesManifest.hostedRouting);
210
+ }
211
+ function matchesPassthroughLocation(options, location) {
212
+ return resolveHostedRouting(options).passthrough.some((route) => matchPathname(route, location.pathname) !== null);
213
+ }
202
214
  function matchesSpaLocation(options, location) {
203
215
  if ("routeManifest" in options) {
204
216
  return buildMatches(options.routeManifest, location).length > 0;
@@ -212,9 +224,16 @@ function matchesSpaPath(path, options) {
212
224
  }
213
225
  return matchesSpaLocation(options, documentRequest.location);
214
226
  }
227
+ function matchesPassthroughPath(path, options) {
228
+ const documentRequest = resolveDocumentPath(path, options.basePath);
229
+ if (documentRequest === null) {
230
+ return false;
231
+ }
232
+ return matchesPassthroughLocation(options, documentRequest.location);
233
+ }
215
234
  async function executeHeadTag(request, headTags, routeId, params, rawSearch) {
216
235
  const definition = headTags.definitions[routeId];
217
- const schemaEntry = headTags.routerSchema[routeId];
236
+ const schemaEntry = getRouteSchemaEntry(headTags.routerSchema, routeId);
218
237
  if (!definition) {
219
238
  throw new Error(`Unknown server head route "${routeId}".`);
220
239
  }
@@ -335,7 +354,7 @@ async function handleDocumentHeadRequest(request, options, href) {
335
354
  async function handleHeadRequest(request, options) {
336
355
  const url = new URL(request.url);
337
356
  const basePath = normalizeBasePath(options.basePath);
338
- const headBasePath = options.headBasePath ?? prependBasePathToPathname("/head-api", basePath);
357
+ const headBasePath = prependBasePathToPathname(getRouterSchemaHostedRouting(options.headTags.routerSchema).headBasePath, basePath);
339
358
  if (url.pathname !== headBasePath) {
340
359
  return {
341
360
  matched: false,
@@ -408,8 +427,7 @@ async function handleRequest(request, options) {
408
427
  const basePath = normalizeBasePath(options.basePath);
409
428
  const handledHeadTagRequest = await handleHeadTagRequest(request, {
410
429
  headTags: options.headTags,
411
- basePath,
412
- headBasePath: options.headBasePath
430
+ basePath
413
431
  });
414
432
  if (handledHeadTagRequest.matched) {
415
433
  return handledHeadTagRequest;
@@ -461,6 +479,7 @@ async function handleRequest(request, options) {
461
479
  }
462
480
  export {
463
481
  matchesSpaPath,
482
+ matchesPassthroughPath,
464
483
  handleSpaRequest,
465
484
  handleRequest,
466
485
  handleHeadTagRequest,
@@ -1,7 +1,7 @@
1
1
  // packages/server/src/index.test.ts
2
2
  import { describe, expect, test } from "bun:test";
3
3
  import { defineRouterSchema, redirect, createRouteNode } from "@richie-router/core";
4
- import { defineHeadTags, handleHeadRequest, handleHeadTagRequest, handleRequest, handleSpaRequest, matchesSpaPath } from "./index.mjs";
4
+ import { defineHeadTags, handleHeadRequest, handleHeadTagRequest, handleRequest, handleSpaRequest, matchesPassthroughPath, matchesSpaPath } from "./index.mjs";
5
5
  function createTestArtifacts(options) {
6
6
  const rootRoute = createRouteNode("__root__", {}, { isRoot: true });
7
7
  const indexRoute = createRouteNode("/", {});
@@ -29,6 +29,9 @@ function createTestArtifacts(options) {
29
29
  "/about": {
30
30
  serverHead: true
31
31
  }
32
+ }, {
33
+ passthrough: ["/api/$"],
34
+ headBasePath: options?.headBasePath
32
35
  });
33
36
  const headTags = defineHeadTags(rootRoute, routerSchema, {
34
37
  "/about": {
@@ -49,6 +52,10 @@ function createTestArtifacts(options) {
49
52
  }
50
53
  }
51
54
  });
55
+ rootRoute._setHostedRouting({
56
+ headBasePath: options?.headBasePath ?? "/head-api",
57
+ passthrough: [options?.headBasePath ?? "/head-api", "/api/$"]
58
+ });
52
59
  return {
53
60
  routeManifest: rootRoute,
54
61
  headTags,
@@ -63,7 +70,11 @@ function createTestArtifacts(options) {
63
70
  { id: "/posts/", to: "/posts", parentId: "/posts", isRoot: false },
64
71
  { id: "/posts/$postId", to: "/posts/$postId", parentId: "/posts", isRoot: false }
65
72
  ],
66
- spaRoutes: ["/", "/about", "/dashboard", "/posts", "/posts/$postId"]
73
+ spaRoutes: ["/", "/about", "/dashboard", "/posts", "/posts/$postId"],
74
+ hostedRouting: {
75
+ headBasePath: options?.headBasePath ?? "/head-api",
76
+ passthrough: [options?.headBasePath ?? "/head-api", "/api/$"]
77
+ }
67
78
  }
68
79
  };
69
80
  }
@@ -166,6 +177,21 @@ describe("handleSpaRequest", () => {
166
177
  basePath: "/project"
167
178
  })).toBe(false);
168
179
  });
180
+ test("exposes passthrough matching for host-side routing decisions", () => {
181
+ const { spaRoutesManifest, routeManifest } = createTestArtifacts();
182
+ expect(matchesPassthroughPath("/project/head-api", {
183
+ spaRoutesManifest,
184
+ basePath: "/project"
185
+ })).toBe(true);
186
+ expect(matchesPassthroughPath("/project/api/health", {
187
+ routeManifest,
188
+ basePath: "/project"
189
+ })).toBe(true);
190
+ expect(matchesPassthroughPath("/project/about", {
191
+ spaRoutesManifest,
192
+ basePath: "/project"
193
+ })).toBe(false);
194
+ });
169
195
  test("matches document requests under the basePath with a routeManifest", async () => {
170
196
  const { routeManifest } = createTestArtifacts();
171
197
  const result = await handleSpaRequest(new Request("https://example.com/project/about"), {
@@ -314,6 +340,22 @@ describe("handleRequest basePath", () => {
314
340
  ]
315
341
  });
316
342
  });
343
+ test("reads a custom headBasePath from the router schema", async () => {
344
+ const { headTags } = createTestArtifacts({
345
+ headBasePath: "/meta"
346
+ });
347
+ const result = await handleHeadTagRequest(new Request("https://example.com/project/meta?routeId=%2Fabout&params=%7B%7D&search=%7B%7D"), {
348
+ headTags,
349
+ basePath: "/project"
350
+ });
351
+ expect(result.matched).toBe(true);
352
+ expect(result.response.status).toBe(200);
353
+ expect(await result.response.json()).toEqual({
354
+ head: [
355
+ { tag: "title", children: "About" }
356
+ ]
357
+ });
358
+ });
317
359
  test("resolves merged document head payloads for host-rendered HTML shells", async () => {
318
360
  const { headTags } = createTestArtifacts();
319
361
  const result = await handleHeadRequest(new Request("https://example.com/project/head-api?href=%2Fproject%2Fabout"), {
@@ -1,4 +1,4 @@
1
- import { type ResolveAllParamsForRouteId, type RouteIdsWithServerHead, type RouterSchemaShape, type InferRouterSearchSchema, type AnyRoute, type HeadConfig, type RouteHeadEntry } from '@richie-router/core';
1
+ import { type ResolveAllParamsForRouteId, type RouteIdsWithServerHead, type AnyRouterSchema, type InferRouterSearchSchema, type AnyRoute, type HeadConfig, type HostedRoutingConfig, type RouteHeadEntry } from '@richie-router/core';
2
2
  export interface HeadTagContext<TParams extends Record<string, string>, TSearch> {
3
3
  request: Request;
4
4
  params: TParams;
@@ -8,15 +8,15 @@ export interface HeadTagDefinition<TParams extends Record<string, string>, TSear
8
8
  staleTime?: number;
9
9
  head: (ctx: HeadTagContext<TParams, TSearch>) => Promise<HeadConfig> | HeadConfig;
10
10
  }
11
- export type HeadTagDefinitions<TRouterSchema extends RouterSchemaShape> = {
11
+ export type HeadTagDefinitions<TRouterSchema extends AnyRouterSchema> = {
12
12
  [TRouteId in RouteIdsWithServerHead<TRouterSchema>]: HeadTagDefinition<ResolveAllParamsForRouteId<TRouteId>, InferRouterSearchSchema<TRouterSchema, TRouteId>>;
13
13
  };
14
- export interface DefinedHeadTags<TRouteManifest extends AnyRoute, TRouterSchema extends RouterSchemaShape> {
14
+ export interface DefinedHeadTags<TRouteManifest extends AnyRoute, TRouterSchema extends AnyRouterSchema> {
15
15
  routeManifest: TRouteManifest;
16
16
  routerSchema: TRouterSchema;
17
17
  definitions: HeadTagDefinitions<TRouterSchema>;
18
18
  }
19
- export declare function defineHeadTags<TRouteManifest extends AnyRoute, TRouterSchema extends RouterSchemaShape>(routeManifest: TRouteManifest, routerSchema: TRouterSchema, definitions: HeadTagDefinitions<TRouterSchema>): DefinedHeadTags<TRouteManifest, TRouterSchema>;
19
+ export declare function defineHeadTags<TRouteManifest extends AnyRoute, TRouterSchema extends AnyRouterSchema>(routeManifest: TRouteManifest, routerSchema: TRouterSchema, definitions: HeadTagDefinitions<TRouterSchema>): DefinedHeadTags<TRouteManifest, TRouterSchema>;
20
20
  export interface HtmlOptions {
21
21
  template: string | ((ctx: {
22
22
  request: Request;
@@ -33,6 +33,7 @@ export interface SpaRoutesManifestRoute {
33
33
  export interface SpaRoutesManifest {
34
34
  routes?: SpaRoutesManifestRoute[];
35
35
  spaRoutes: string[];
36
+ hostedRouting?: HostedRoutingConfig;
36
37
  }
37
38
  interface BaseMatchSpaPathOptions {
38
39
  basePath?: string;
@@ -47,18 +48,16 @@ interface DocumentResponseOptions {
47
48
  headers?: HeadersInit;
48
49
  }
49
50
  export type HandleSpaRequestOptions = MatchSpaPathOptions & DocumentResponseOptions;
50
- export interface HandleRequestOptions<TRouteManifest extends AnyRoute, TRouterSchema extends RouterSchemaShape> {
51
+ export interface HandleRequestOptions<TRouteManifest extends AnyRoute, TRouterSchema extends AnyRouterSchema> {
51
52
  routeManifest: TRouteManifest;
52
53
  headTags: DefinedHeadTags<TRouteManifest, TRouterSchema>;
53
54
  html: HtmlOptions;
54
55
  basePath?: string;
55
56
  headers?: HeadersInit;
56
- headBasePath?: string;
57
57
  }
58
- export interface HandleHeadTagRequestOptions<TRouteManifest extends AnyRoute, TRouterSchema extends RouterSchemaShape> {
58
+ export interface HandleHeadTagRequestOptions<TRouteManifest extends AnyRoute, TRouterSchema extends AnyRouterSchema> {
59
59
  headTags: DefinedHeadTags<TRouteManifest, TRouterSchema>;
60
60
  basePath?: string;
61
- headBasePath?: string;
62
61
  }
63
62
  export interface HandleRequestResult {
64
63
  matched: boolean;
@@ -74,8 +73,9 @@ export interface DocumentHeadResponsePayload extends RouteHeadResponsePayload {
74
73
  routeHeads: RouteHeadEntry[];
75
74
  }
76
75
  export declare function matchesSpaPath(path: string, options: MatchSpaPathOptions): boolean;
77
- export declare function handleHeadRequest<TRouteManifest extends AnyRoute, TRouterSchema extends RouterSchemaShape>(request: Request, options: HandleHeadTagRequestOptions<TRouteManifest, TRouterSchema>): Promise<HandleRequestResult>;
78
- export declare function handleHeadTagRequest<TRouteManifest extends AnyRoute, TRouterSchema extends RouterSchemaShape>(request: Request, options: HandleHeadTagRequestOptions<TRouteManifest, TRouterSchema>): Promise<HandleRequestResult>;
76
+ export declare function matchesPassthroughPath(path: string, options: MatchSpaPathOptions): boolean;
77
+ export declare function handleHeadRequest<TRouteManifest extends AnyRoute, TRouterSchema extends AnyRouterSchema>(request: Request, options: HandleHeadTagRequestOptions<TRouteManifest, TRouterSchema>): Promise<HandleRequestResult>;
78
+ export declare function handleHeadTagRequest<TRouteManifest extends AnyRoute, TRouterSchema extends AnyRouterSchema>(request: Request, options: HandleHeadTagRequestOptions<TRouteManifest, TRouterSchema>): Promise<HandleRequestResult>;
79
79
  export declare function handleSpaRequest(request: Request, options: HandleSpaRequestOptions): Promise<HandleRequestResult>;
80
- export declare function handleRequest<TRouteManifest extends AnyRoute, TRouterSchema extends RouterSchemaShape>(request: Request, options: HandleRequestOptions<TRouteManifest, TRouterSchema>): Promise<HandleRequestResult>;
80
+ export declare function handleRequest<TRouteManifest extends AnyRoute, TRouterSchema extends AnyRouterSchema>(request: Request, options: HandleRequestOptions<TRouteManifest, TRouterSchema>): Promise<HandleRequestResult>;
81
81
  export {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@richie-router/server",
3
- "version": "0.1.7",
3
+ "version": "0.1.8",
4
4
  "description": "Server helpers for Richie Router head tags and document handling",
5
5
  "sideEffects": false,
6
6
  "exports": {
@@ -13,7 +13,7 @@
13
13
  }
14
14
  },
15
15
  "dependencies": {
16
- "@richie-router/core": "^0.1.4"
16
+ "@richie-router/core": "^0.1.5"
17
17
  },
18
18
  "author": "Richie <oss@ricsam.dev>",
19
19
  "homepage": "https://docs.ricsam.dev/richie-router",