@pylonsync/react 0.3.248 → 0.3.250

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/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "publishConfig": {
4
4
  "access": "public"
5
5
  },
6
- "version": "0.3.248",
6
+ "version": "0.3.250",
7
7
  "type": "module",
8
8
  "main": "src/index.ts",
9
9
  "types": "src/index.ts",
@@ -12,8 +12,8 @@
12
12
  "check": "tsc -p tsconfig.json --noEmit"
13
13
  },
14
14
  "dependencies": {
15
- "@pylonsync/sdk": "0.3.248",
16
- "@pylonsync/sync": "0.3.248"
15
+ "@pylonsync/sdk": "0.3.250",
16
+ "@pylonsync/sync": "0.3.250"
17
17
  },
18
18
  "peerDependencies": {
19
19
  "react": ">=19.0.0"
package/src/Form.tsx ADDED
@@ -0,0 +1,41 @@
1
+ import React from "react";
2
+
3
+ // Pylon's progressive-enhancement form. Renders a plain `<form method="post">`
4
+ // that works WITHOUT client JS: the browser POSTs to `action` (a route.ts
5
+ // path), the handler processes + redirects (303 See Other), and the browser
6
+ // follows with a GET (POST-redirect-GET). WITH JS, the runtime's global submit
7
+ // handler intercepts (via `data-pylon-form`), `fetch()`es the same endpoint,
8
+ // and follows the redirect through client-side navigation — no full reload.
9
+ //
10
+ // `<form method>` is POST-only without JS (browsers only do GET/POST natively),
11
+ // so POST is the default + the no-JS-safe choice. Set `navigate={false}` to
12
+ // force a native full-page submit even when JS is available.
13
+ export interface FormProps
14
+ extends Omit<React.FormHTMLAttributes<HTMLFormElement>, "method" | "action"> {
15
+ /** The `route.ts` handler path (e.g. "/notes"). */
16
+ action: string;
17
+ /** HTTP method. Default "post" (the only method usable without JS). */
18
+ method?: "post" | "put" | "patch" | "delete";
19
+ /** Opt out of client interception — force a native full-page submit. */
20
+ navigate?: boolean;
21
+ children?: React.ReactNode;
22
+ }
23
+
24
+ export function Form({
25
+ action,
26
+ method = "post",
27
+ navigate = true,
28
+ children,
29
+ ...rest
30
+ }: FormProps) {
31
+ return (
32
+ <form
33
+ method={method}
34
+ action={action}
35
+ {...(navigate ? { "data-pylon-form": "" } : {})}
36
+ {...rest}
37
+ >
38
+ {children}
39
+ </form>
40
+ );
41
+ }
package/src/index.ts CHANGED
@@ -7,6 +7,8 @@ export { Link } from "./Link";
7
7
  export type { LinkProps } from "./Link";
8
8
  export { Image } from "./Image";
9
9
  export type { ImageProps } from "./Image";
10
+ export { Form } from "./Form";
11
+ export type { FormProps } from "./Form";
10
12
 
11
13
  // SSR page-author types — the contract every `app/**/page.tsx` is handed
12
14
  // in props, plus `metadata` / `generateMetadata`. Type-only.
@@ -18,8 +20,13 @@ export type {
18
20
  SsrCookieOptions,
19
21
  Metadata,
20
22
  GenerateMetadata,
23
+ RouteSegmentConfig,
21
24
  ErrorBoundaryProps,
22
25
  NotFoundProps,
26
+ FormFields,
27
+ FormDb,
28
+ FormRequest,
29
+ RouteHandler,
23
30
  } from "./ssr";
24
31
 
25
32
  // Client navigation hooks for SSR pages (Next-style).
package/src/ssr.ts CHANGED
@@ -206,6 +206,40 @@ export type GenerateMetadata<
206
206
  props: PageProps<TParams, TSearchParams>,
207
207
  ) => Metadata | Promise<Metadata>;
208
208
 
209
+ /**
210
+ * Per-route configuration, declared as top-level `export const` in a
211
+ * `page.tsx` (Next-shaped). All optional. The runtime reads these statically
212
+ * before the render.
213
+ *
214
+ * ```ts
215
+ * export const revalidate = 60; // CDN + origin-disk cache for 60s
216
+ * export const dynamic = "force-dynamic"; // never cache
217
+ * export const streaming = true; // progressive Suspense streaming
218
+ * ```
219
+ *
220
+ * - `revalidate` — seconds an auth-independent render stays cacheable
221
+ * (`public, s-maxage=N`); also kept in the origin disk cache. See the
222
+ * "Anonymous output caching" rules — it only takes effect if the render
223
+ * never reads `auth`, sets no cookie, and isn't running strict policies.
224
+ * - `dynamic` — `"force-static"` caches until the next deploy; `"force-dynamic"`
225
+ * opts out of all caching.
226
+ * - `streaming` — opt into PROGRESSIVE streaming: the shell + each inner
227
+ * `<Suspense>` fallback flush immediately, then each boundary's real content
228
+ * streams in as its data resolves (instead of buffering the whole document).
229
+ * A page with a `loading.tsx` already streams at the route level; this
230
+ * extends it to a page's OWN inner boundaries. Mutually exclusive with
231
+ * caching: a streaming render commits its HTTP head before suspended
232
+ * subtrees finish, so it can never be marked cacheable (and a deep
233
+ * `<Suspense>` throw resolves via its `error.tsx` at HTTP 200, not a 5xx).
234
+ * Set `response.*` (status/cookies) only in the synchronous shell — late
235
+ * calls from a suspended subtree are dropped (the runtime warns).
236
+ */
237
+ export interface RouteSegmentConfig {
238
+ revalidate?: number;
239
+ dynamic?: "force-static" | "force-dynamic";
240
+ streaming?: boolean;
241
+ }
242
+
209
243
  /**
210
244
  * Props an `app/.../error.tsx` boundary receives. Error boundaries are now
211
245
  * HYDRATED (interactive — useState/onClick/effects work), so `reset` is a
@@ -239,3 +273,72 @@ export interface ErrorBoundaryProps {
239
273
  * Same shape as a page.
240
274
  */
241
275
  export type NotFoundProps = PageProps;
276
+
277
+ /**
278
+ * Parsed form fields handed to a `route.ts` handler. Mirrors URLSearchParams
279
+ * `get`/`getAll`/`has` semantics over the submitted body.
280
+ */
281
+ export interface FormFields {
282
+ /** First value for `name`, or null. */
283
+ get(name: string): string | null;
284
+ /** All values for `name` (empty array if none). */
285
+ getAll(name: string): string[];
286
+ has(name: string): boolean;
287
+ /** Raw map: name → value (single) or values (repeated field). */
288
+ readonly fields: Record<string, string | string[]>;
289
+ }
290
+
291
+ /**
292
+ * Read + write DB handle for a `route.ts` form handler (mutation-shaped). The
293
+ * read surface is `ServerData`; writes go through the same policy-checked,
294
+ * broadcast-firing path a mutation's `ctx.db` uses.
295
+ */
296
+ export interface FormDb extends ServerData {
297
+ insert<T = Record<string, unknown>>(
298
+ entity: string,
299
+ data: Record<string, unknown>,
300
+ ): Promise<T>;
301
+ update<T = Record<string, unknown>>(
302
+ entity: string,
303
+ id: string,
304
+ data: Record<string, unknown>,
305
+ ): Promise<T>;
306
+ delete(entity: string, id: string): Promise<void>;
307
+ }
308
+
309
+ /**
310
+ * The request a `route.ts` POST/PUT/PATCH/DELETE handler receives. Shape the
311
+ * reply through `response` — usually `response.redirect("/x?ok=1")` (303
312
+ * POST-redirect-GET, the default) after a write, so a no-JS browser follows
313
+ * with a GET. Enforce trust with `auth` inside the handler (forms run with the
314
+ * standard function trust model).
315
+ *
316
+ * ```ts
317
+ * import type { RouteHandler } from "@pylonsync/react";
318
+ * export const POST: RouteHandler = async ({ form, db, response, auth }) => {
319
+ * const body = form.get("body");
320
+ * if (!body) return response.redirect("/notes?error=empty");
321
+ * await db.insert("Note", { body });
322
+ * response.redirect("/notes?created=1");
323
+ * };
324
+ * ```
325
+ */
326
+ export interface FormRequest<
327
+ TParams extends Record<string, string> = Record<string, string>,
328
+ TSearchParams extends Record<string, string> = Record<string, string>,
329
+ > {
330
+ form: FormFields;
331
+ params: TParams;
332
+ searchParams: TSearchParams;
333
+ auth: PageAuth;
334
+ cookies: Record<string, string>;
335
+ headers: Record<string, string>;
336
+ db: FormDb;
337
+ response: SsrResponse;
338
+ }
339
+
340
+ /** Signature of a `route.ts` method handler export (POST/PUT/PATCH/DELETE). */
341
+ export type RouteHandler<
342
+ TParams extends Record<string, string> = Record<string, string>,
343
+ TSearchParams extends Record<string, string> = Record<string, string>,
344
+ > = (req: FormRequest<TParams, TSearchParams>) => void | Promise<void>;