@pylonsync/functions 0.7.0 → 0.8.0

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.
@@ -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
- /** The incoming URL path (e.g. `/blog/hello-world`). */
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pylonsync/functions",
3
- "version": "0.7.0",
3
+ "version": "0.8.0",
4
4
  "description": "TypeScript function runtime for pylon — defines server-side queries, mutations, and actions.",
5
5
  "type": "module",
6
6
  "main": "src/index.ts",
@@ -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
+ });
@@ -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
- /** The incoming URL path (e.g. `/blog/hello-world`). */
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),