@richie-router/server 0.1.6 → 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.
- package/dist/cjs/index.cjs +20 -4
- package/dist/cjs/index.test.cjs +148 -1
- package/dist/esm/index.mjs +23 -4
- package/dist/esm/index.test.mjs +149 -2
- package/dist/types/index.d.ts +11 -11
- package/package.json +2 -2
package/dist/cjs/index.cjs
CHANGED
|
@@ -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
|
|
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
|
|
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;
|
package/dist/cjs/index.test.cjs
CHANGED
|
@@ -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,10 +70,86 @@ 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
|
}
|
|
81
|
+
function createCompetingHeadArtifacts() {
|
|
82
|
+
const rootRoute = import_core.createRouteNode("__root__", {}, { isRoot: true });
|
|
83
|
+
const usernameRoute = import_core.createRouteNode("/$username", {});
|
|
84
|
+
const usernameIndexRoute = import_core.createRouteNode("/$username/", {});
|
|
85
|
+
const usernameSlugRoute = import_core.createRouteNode("/$username/$slug", {});
|
|
86
|
+
const loginRoute = import_core.createRouteNode("/login", {});
|
|
87
|
+
const registerRoute = import_core.createRouteNode("/register", {});
|
|
88
|
+
const tagsTagRoute = import_core.createRouteNode("/tags/$tag", {});
|
|
89
|
+
usernameRoute._setServerHead(true);
|
|
90
|
+
usernameIndexRoute._setServerHead(true);
|
|
91
|
+
usernameSlugRoute._setServerHead(true);
|
|
92
|
+
loginRoute._setServerHead(true);
|
|
93
|
+
registerRoute._setServerHead(true);
|
|
94
|
+
tagsTagRoute._setServerHead(true);
|
|
95
|
+
usernameRoute._addFileChildren({
|
|
96
|
+
index: usernameIndexRoute,
|
|
97
|
+
slug: usernameSlugRoute
|
|
98
|
+
});
|
|
99
|
+
rootRoute._addFileChildren({
|
|
100
|
+
username: usernameRoute,
|
|
101
|
+
login: loginRoute,
|
|
102
|
+
register: registerRoute,
|
|
103
|
+
tags: tagsTagRoute
|
|
104
|
+
});
|
|
105
|
+
const routerSchema = import_core.defineRouterSchema({
|
|
106
|
+
"/$username/": {
|
|
107
|
+
serverHead: true
|
|
108
|
+
},
|
|
109
|
+
"/$username/$slug": {
|
|
110
|
+
serverHead: true
|
|
111
|
+
},
|
|
112
|
+
"/login": {
|
|
113
|
+
serverHead: true
|
|
114
|
+
},
|
|
115
|
+
"/register": {
|
|
116
|
+
serverHead: true
|
|
117
|
+
},
|
|
118
|
+
"/tags/$tag": {
|
|
119
|
+
serverHead: true
|
|
120
|
+
}
|
|
121
|
+
});
|
|
122
|
+
const headTags = import__.defineHeadTags(rootRoute, routerSchema, {
|
|
123
|
+
"/$username/": {
|
|
124
|
+
head: ({ params }) => [
|
|
125
|
+
{ tag: "title", children: `User ${params.username}` }
|
|
126
|
+
]
|
|
127
|
+
},
|
|
128
|
+
"/$username/$slug": {
|
|
129
|
+
head: ({ params }) => [
|
|
130
|
+
{ tag: "title", children: `Post ${params.username}/${params.slug}` }
|
|
131
|
+
]
|
|
132
|
+
},
|
|
133
|
+
"/login": {
|
|
134
|
+
head: () => [
|
|
135
|
+
{ tag: "title", children: "Login" }
|
|
136
|
+
]
|
|
137
|
+
},
|
|
138
|
+
"/register": {
|
|
139
|
+
head: () => [
|
|
140
|
+
{ tag: "title", children: "Register" }
|
|
141
|
+
]
|
|
142
|
+
},
|
|
143
|
+
"/tags/$tag": {
|
|
144
|
+
head: ({ params }) => [
|
|
145
|
+
{ tag: "title", children: `Tag ${params.tag}` }
|
|
146
|
+
]
|
|
147
|
+
}
|
|
148
|
+
});
|
|
149
|
+
return {
|
|
150
|
+
headTags
|
|
151
|
+
};
|
|
152
|
+
}
|
|
70
153
|
import_bun_test.describe("handleSpaRequest", () => {
|
|
71
154
|
import_bun_test.test('treats "/" as the root basePath and trims trailing slashes', () => {
|
|
72
155
|
const { spaRoutesManifest } = createTestArtifacts();
|
|
@@ -94,6 +177,21 @@ import_bun_test.describe("handleSpaRequest", () => {
|
|
|
94
177
|
basePath: "/project"
|
|
95
178
|
})).toBe(false);
|
|
96
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
|
+
});
|
|
97
195
|
import_bun_test.test("matches document requests under the basePath with a routeManifest", async () => {
|
|
98
196
|
const { routeManifest } = createTestArtifacts();
|
|
99
197
|
const result = await import__.handleSpaRequest(new Request("https://example.com/project/about"), {
|
|
@@ -242,6 +340,22 @@ import_bun_test.describe("handleRequest basePath", () => {
|
|
|
242
340
|
]
|
|
243
341
|
});
|
|
244
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¶ms=%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
|
+
});
|
|
245
359
|
import_bun_test.test("resolves merged document head payloads for host-rendered HTML shells", async () => {
|
|
246
360
|
const { headTags } = createTestArtifacts();
|
|
247
361
|
const result = await import__.handleHeadRequest(new Request("https://example.com/project/head-api?href=%2Fproject%2Fabout"), {
|
|
@@ -267,6 +381,39 @@ import_bun_test.describe("handleRequest basePath", () => {
|
|
|
267
381
|
import_bun_test.expect(payload.richieRouterHead).toContain("window.__RICHIE_ROUTER_HEAD__");
|
|
268
382
|
import_bun_test.expect(result.response.headers.get("cache-control")).toBe("private, no-store");
|
|
269
383
|
});
|
|
384
|
+
import_bun_test.test("prefers static routes over dynamic siblings when resolving document head payloads", async () => {
|
|
385
|
+
const { headTags } = createCompetingHeadArtifacts();
|
|
386
|
+
const tagResult = await import__.handleHeadRequest(new Request("https://example.com/head-api?href=%2Ftags%2Ftesting"), {
|
|
387
|
+
headTags
|
|
388
|
+
});
|
|
389
|
+
import_bun_test.expect(tagResult.matched).toBe(true);
|
|
390
|
+
import_bun_test.expect(await tagResult.response.json()).toMatchObject({
|
|
391
|
+
href: "/tags/testing",
|
|
392
|
+
routeHeads: [
|
|
393
|
+
{
|
|
394
|
+
routeId: "/tags/$tag",
|
|
395
|
+
head: [
|
|
396
|
+
{ tag: "title", children: "Tag testing" }
|
|
397
|
+
]
|
|
398
|
+
}
|
|
399
|
+
]
|
|
400
|
+
});
|
|
401
|
+
const loginResult = await import__.handleHeadRequest(new Request("https://example.com/head-api?href=%2Flogin"), {
|
|
402
|
+
headTags
|
|
403
|
+
});
|
|
404
|
+
import_bun_test.expect(loginResult.matched).toBe(true);
|
|
405
|
+
import_bun_test.expect(await loginResult.response.json()).toMatchObject({
|
|
406
|
+
href: "/login",
|
|
407
|
+
routeHeads: [
|
|
408
|
+
{
|
|
409
|
+
routeId: "/login",
|
|
410
|
+
head: [
|
|
411
|
+
{ tag: "title", children: "Login" }
|
|
412
|
+
]
|
|
413
|
+
}
|
|
414
|
+
]
|
|
415
|
+
});
|
|
416
|
+
});
|
|
270
417
|
import_bun_test.test("returns redirect responses for document head payload requests", async () => {
|
|
271
418
|
const { headTags } = createTestArtifacts({
|
|
272
419
|
redirectAbout: true
|
package/dist/esm/index.mjs
CHANGED
|
@@ -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
|
|
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
|
|
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,
|
package/dist/esm/index.test.mjs
CHANGED
|
@@ -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,10 +70,86 @@ 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
|
}
|
|
81
|
+
function createCompetingHeadArtifacts() {
|
|
82
|
+
const rootRoute = createRouteNode("__root__", {}, { isRoot: true });
|
|
83
|
+
const usernameRoute = createRouteNode("/$username", {});
|
|
84
|
+
const usernameIndexRoute = createRouteNode("/$username/", {});
|
|
85
|
+
const usernameSlugRoute = createRouteNode("/$username/$slug", {});
|
|
86
|
+
const loginRoute = createRouteNode("/login", {});
|
|
87
|
+
const registerRoute = createRouteNode("/register", {});
|
|
88
|
+
const tagsTagRoute = createRouteNode("/tags/$tag", {});
|
|
89
|
+
usernameRoute._setServerHead(true);
|
|
90
|
+
usernameIndexRoute._setServerHead(true);
|
|
91
|
+
usernameSlugRoute._setServerHead(true);
|
|
92
|
+
loginRoute._setServerHead(true);
|
|
93
|
+
registerRoute._setServerHead(true);
|
|
94
|
+
tagsTagRoute._setServerHead(true);
|
|
95
|
+
usernameRoute._addFileChildren({
|
|
96
|
+
index: usernameIndexRoute,
|
|
97
|
+
slug: usernameSlugRoute
|
|
98
|
+
});
|
|
99
|
+
rootRoute._addFileChildren({
|
|
100
|
+
username: usernameRoute,
|
|
101
|
+
login: loginRoute,
|
|
102
|
+
register: registerRoute,
|
|
103
|
+
tags: tagsTagRoute
|
|
104
|
+
});
|
|
105
|
+
const routerSchema = defineRouterSchema({
|
|
106
|
+
"/$username/": {
|
|
107
|
+
serverHead: true
|
|
108
|
+
},
|
|
109
|
+
"/$username/$slug": {
|
|
110
|
+
serverHead: true
|
|
111
|
+
},
|
|
112
|
+
"/login": {
|
|
113
|
+
serverHead: true
|
|
114
|
+
},
|
|
115
|
+
"/register": {
|
|
116
|
+
serverHead: true
|
|
117
|
+
},
|
|
118
|
+
"/tags/$tag": {
|
|
119
|
+
serverHead: true
|
|
120
|
+
}
|
|
121
|
+
});
|
|
122
|
+
const headTags = defineHeadTags(rootRoute, routerSchema, {
|
|
123
|
+
"/$username/": {
|
|
124
|
+
head: ({ params }) => [
|
|
125
|
+
{ tag: "title", children: `User ${params.username}` }
|
|
126
|
+
]
|
|
127
|
+
},
|
|
128
|
+
"/$username/$slug": {
|
|
129
|
+
head: ({ params }) => [
|
|
130
|
+
{ tag: "title", children: `Post ${params.username}/${params.slug}` }
|
|
131
|
+
]
|
|
132
|
+
},
|
|
133
|
+
"/login": {
|
|
134
|
+
head: () => [
|
|
135
|
+
{ tag: "title", children: "Login" }
|
|
136
|
+
]
|
|
137
|
+
},
|
|
138
|
+
"/register": {
|
|
139
|
+
head: () => [
|
|
140
|
+
{ tag: "title", children: "Register" }
|
|
141
|
+
]
|
|
142
|
+
},
|
|
143
|
+
"/tags/$tag": {
|
|
144
|
+
head: ({ params }) => [
|
|
145
|
+
{ tag: "title", children: `Tag ${params.tag}` }
|
|
146
|
+
]
|
|
147
|
+
}
|
|
148
|
+
});
|
|
149
|
+
return {
|
|
150
|
+
headTags
|
|
151
|
+
};
|
|
152
|
+
}
|
|
70
153
|
describe("handleSpaRequest", () => {
|
|
71
154
|
test('treats "/" as the root basePath and trims trailing slashes', () => {
|
|
72
155
|
const { spaRoutesManifest } = createTestArtifacts();
|
|
@@ -94,6 +177,21 @@ describe("handleSpaRequest", () => {
|
|
|
94
177
|
basePath: "/project"
|
|
95
178
|
})).toBe(false);
|
|
96
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
|
+
});
|
|
97
195
|
test("matches document requests under the basePath with a routeManifest", async () => {
|
|
98
196
|
const { routeManifest } = createTestArtifacts();
|
|
99
197
|
const result = await handleSpaRequest(new Request("https://example.com/project/about"), {
|
|
@@ -242,6 +340,22 @@ describe("handleRequest basePath", () => {
|
|
|
242
340
|
]
|
|
243
341
|
});
|
|
244
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¶ms=%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
|
+
});
|
|
245
359
|
test("resolves merged document head payloads for host-rendered HTML shells", async () => {
|
|
246
360
|
const { headTags } = createTestArtifacts();
|
|
247
361
|
const result = await handleHeadRequest(new Request("https://example.com/project/head-api?href=%2Fproject%2Fabout"), {
|
|
@@ -267,6 +381,39 @@ describe("handleRequest basePath", () => {
|
|
|
267
381
|
expect(payload.richieRouterHead).toContain("window.__RICHIE_ROUTER_HEAD__");
|
|
268
382
|
expect(result.response.headers.get("cache-control")).toBe("private, no-store");
|
|
269
383
|
});
|
|
384
|
+
test("prefers static routes over dynamic siblings when resolving document head payloads", async () => {
|
|
385
|
+
const { headTags } = createCompetingHeadArtifacts();
|
|
386
|
+
const tagResult = await handleHeadRequest(new Request("https://example.com/head-api?href=%2Ftags%2Ftesting"), {
|
|
387
|
+
headTags
|
|
388
|
+
});
|
|
389
|
+
expect(tagResult.matched).toBe(true);
|
|
390
|
+
expect(await tagResult.response.json()).toMatchObject({
|
|
391
|
+
href: "/tags/testing",
|
|
392
|
+
routeHeads: [
|
|
393
|
+
{
|
|
394
|
+
routeId: "/tags/$tag",
|
|
395
|
+
head: [
|
|
396
|
+
{ tag: "title", children: "Tag testing" }
|
|
397
|
+
]
|
|
398
|
+
}
|
|
399
|
+
]
|
|
400
|
+
});
|
|
401
|
+
const loginResult = await handleHeadRequest(new Request("https://example.com/head-api?href=%2Flogin"), {
|
|
402
|
+
headTags
|
|
403
|
+
});
|
|
404
|
+
expect(loginResult.matched).toBe(true);
|
|
405
|
+
expect(await loginResult.response.json()).toMatchObject({
|
|
406
|
+
href: "/login",
|
|
407
|
+
routeHeads: [
|
|
408
|
+
{
|
|
409
|
+
routeId: "/login",
|
|
410
|
+
head: [
|
|
411
|
+
{ tag: "title", children: "Login" }
|
|
412
|
+
]
|
|
413
|
+
}
|
|
414
|
+
]
|
|
415
|
+
});
|
|
416
|
+
});
|
|
270
417
|
test("returns redirect responses for document head payload requests", async () => {
|
|
271
418
|
const { headTags } = createTestArtifacts({
|
|
272
419
|
redirectAbout: true
|
package/dist/types/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type ResolveAllParamsForRouteId, type RouteIdsWithServerHead, type
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
78
|
-
export declare function
|
|
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
|
|
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.
|
|
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.
|
|
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",
|