@pylonsync/functions 0.7.0 → 0.8.1
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/ssr-runtime.d.ts +5 -1
- package/package.json +1 -1
- package/src/ssr-pathname-prop.test.ts +60 -0
- package/src/ssr-runtime.ts +17 -1
package/dist/ssr-runtime.d.ts
CHANGED
|
@@ -39,7 +39,11 @@ export interface RenderRouteMessage {
|
|
|
39
39
|
layouts?: string[];
|
|
40
40
|
/** The matched route pattern (e.g. `/blog/:slug`). */
|
|
41
41
|
route_path: string;
|
|
42
|
-
/**
|
|
42
|
+
/**
|
|
43
|
+
* The incoming request PATH, query already stripped by the router (it
|
|
44
|
+
* passes `path_only_owned`). Reaches userland as BOTH `pathname` (use
|
|
45
|
+
* this) and `url` (deprecated); the query is `search_params`.
|
|
46
|
+
*/
|
|
43
47
|
url: string;
|
|
44
48
|
/** Dynamic-segment matches keyed by name (e.g. `{slug: "hello-world"}`). */
|
|
45
49
|
params: Record<string, string>;
|
package/package.json
CHANGED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
// The `url` page prop carries the request PATH, never the query — the router
|
|
2
|
+
// hands over `path_only_owned` and the query arrives separately as
|
|
3
|
+
// `search_params`. The name does not say that, and the gap has a cost: a
|
|
4
|
+
// login page did `new URL(props.url).searchParams.get("next")`, which
|
|
5
|
+
// compiles, looks right, and returns null on every request. It silently
|
|
6
|
+
// dropped the pending OIDC authorize request, so signing in to one app in the
|
|
7
|
+
// fleet landed on the identity provider's dashboard with no error.
|
|
8
|
+
//
|
|
9
|
+
// `pathname` is the honest name. These pin that both exist, agree, and reach
|
|
10
|
+
// a bucketed page's hydrated tail together — a page that got one but not the
|
|
11
|
+
// other would be the same trap in a new place.
|
|
12
|
+
|
|
13
|
+
import { expect, test } from "bun:test";
|
|
14
|
+
import { readFileSync } from "node:fs";
|
|
15
|
+
|
|
16
|
+
const runtime = readFileSync(new URL("./ssr-runtime.ts", import.meta.url), "utf8");
|
|
17
|
+
const publicTypes = readFileSync(
|
|
18
|
+
new URL("../../react/src/ssr.ts", import.meta.url),
|
|
19
|
+
"utf8",
|
|
20
|
+
);
|
|
21
|
+
|
|
22
|
+
function block(src: string, start: string, end: string): string {
|
|
23
|
+
const a = src.indexOf(start);
|
|
24
|
+
expect(a).toBeGreaterThan(-1);
|
|
25
|
+
const b = src.indexOf(end, a);
|
|
26
|
+
return src.slice(a, b > a ? b : undefined);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
test("page props carry pathname and url, both from the same value", () => {
|
|
30
|
+
const props = block(runtime, "props = {", "params: msg.params");
|
|
31
|
+
expect(props).toContain("pathname: msg.url");
|
|
32
|
+
expect(props).toContain("url: msg.url");
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
test("a bucketed page's serialized tail carries both", () => {
|
|
36
|
+
const tail = block(runtime, "serializableProps = {", "auth: {");
|
|
37
|
+
expect(tail).toContain("pathname:");
|
|
38
|
+
expect(tail).toContain("url: restProps.url");
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
test("the PPR snapshot carries both", () => {
|
|
42
|
+
const snap = block(runtime, "bucketTailBase = bucketOptIn", "searchParams: jsonClone");
|
|
43
|
+
expect(snap).toContain("pathname: msg.url");
|
|
44
|
+
expect(snap).toContain("url: msg.url");
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
test("PageProps declares pathname and marks url deprecated", () => {
|
|
48
|
+
expect(publicTypes).toContain("pathname: string;");
|
|
49
|
+
const urlDoc = block(publicTypes, "The request path. Same value as `pathname`", "url: string;");
|
|
50
|
+
expect(urlDoc).toContain("@deprecated");
|
|
51
|
+
// The doc has to name the replacement, or it just says "don't" without
|
|
52
|
+
// saying what instead.
|
|
53
|
+
expect(urlDoc).toContain("searchParams");
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
test("the deprecation explains the failure, not just the preference", () => {
|
|
57
|
+
const urlDoc = block(publicTypes, "The request path. Same value as `pathname`", "url: string;");
|
|
58
|
+
expect(urlDoc.toLowerCase()).toContain("query");
|
|
59
|
+
expect(urlDoc).toMatch(/null|silently/);
|
|
60
|
+
});
|
package/src/ssr-runtime.ts
CHANGED
|
@@ -58,7 +58,11 @@ export interface RenderRouteMessage {
|
|
|
58
58
|
layouts?: string[];
|
|
59
59
|
/** The matched route pattern (e.g. `/blog/:slug`). */
|
|
60
60
|
route_path: string;
|
|
61
|
-
/**
|
|
61
|
+
/**
|
|
62
|
+
* The incoming request PATH, query already stripped by the router (it
|
|
63
|
+
* passes `path_only_owned`). Reaches userland as BOTH `pathname` (use
|
|
64
|
+
* this) and `url` (deprecated); the query is `search_params`.
|
|
65
|
+
*/
|
|
62
66
|
url: string;
|
|
63
67
|
/** Dynamic-segment matches keyed by name (e.g. `{slug: "hello-world"}`). */
|
|
64
68
|
params: Record<string, string>;
|
|
@@ -2071,6 +2075,10 @@ export function buildHydrationTail(args: {
|
|
|
2071
2075
|
// collapsed binary auth bit. (params/searchParams are keyed by pathname; a
|
|
2072
2076
|
// bucket request has no query, so searchParams is empty.)
|
|
2073
2077
|
serializableProps = {
|
|
2078
|
+
// Both, and for the same reason they are both on props: a bucketed page
|
|
2079
|
+
// that got `url` but not `pathname` would hydrate with one of the two
|
|
2080
|
+
// undefined, which is the same trap in a new place.
|
|
2081
|
+
pathname: restProps.pathname ?? restProps.url,
|
|
2074
2082
|
url: restProps.url,
|
|
2075
2083
|
params: restProps.params,
|
|
2076
2084
|
searchParams: restProps.searchParams,
|
|
@@ -3245,6 +3253,13 @@ export async function handleRenderRoute(
|
|
|
3245
3253
|
);
|
|
3246
3254
|
|
|
3247
3255
|
props = {
|
|
3256
|
+
// Both name the same value: the request PATH, with no query string.
|
|
3257
|
+
// `pathname` is the name to use; `url` is kept for compatibility and
|
|
3258
|
+
// is deprecated — see the doc comments on PageProps. The old name is a
|
|
3259
|
+
// trap, because a prop called `url` reads as if it carries the query,
|
|
3260
|
+
// and code that does `new URL(props.url).searchParams.get(x)` silently
|
|
3261
|
+
// finds nothing forever instead of failing.
|
|
3262
|
+
pathname: msg.url,
|
|
3248
3263
|
url: msg.url,
|
|
3249
3264
|
params: msg.params,
|
|
3250
3265
|
searchParams: msg.search_params,
|
|
@@ -3270,6 +3285,7 @@ export async function handleRenderRoute(
|
|
|
3270
3285
|
const bucketOptIn = (mod as any).cache === "auth-bucketed";
|
|
3271
3286
|
const bucketTailBase = bucketOptIn
|
|
3272
3287
|
? {
|
|
3288
|
+
pathname: msg.url,
|
|
3273
3289
|
url: msg.url,
|
|
3274
3290
|
params: jsonClone(msg.params),
|
|
3275
3291
|
searchParams: jsonClone(msg.search_params),
|