@pylonsync/create-pylon 0.3.247 → 0.3.249

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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pylonsync/create-pylon",
3
- "version": "0.3.247",
3
+ "version": "0.3.249",
4
4
  "description": "Scaffold a new Pylon app — realtime backend + web/mobile/expo frontends in one command. Run via `npm create @pylonsync/pylon@latest`.",
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -24,6 +24,10 @@ Operating rules for a coding agent in this Pylon app. Pylon is a Rails-like fram
24
24
 
25
25
  - **Policies deny by default; server functions BYPASS them.** Direct client CRUD (`/api/entities/*`) and sync are policy-checked. Functions run with full DB access — enforce trust with `ctx.auth` checks inside the handler, not policies.
26
26
  - **Type page props from the SDK, don't hand-roll them.** `import type { PageProps, Metadata } from "@pylonsync/react"`. Every page/layout gets `{ url, params, searchParams, auth, response, serverData }`; `PageProps<{ slug: string }>` types a `[slug]` route's params. Request headers/cookies are intentionally NOT on `PageProps` — they're server-only and stripped from hydration, so reading them in the render would mismatch.
27
+ - **Anonymous output caching is opt-in + earned.** `export const revalidate = 60` (seconds) on a page makes it CDN-cacheable (`public, s-maxage=60`) — but ONLY if the render is auth-INDEPENDENT: it must NOT read `props.auth` (reading it at all opts out, even for anonymous), set no cookie, and the app must not run strict per-caller policies (`PYLON_STRICT_FN_POLICIES`). `export const dynamic = "force-static"` caches until the next deploy; `"force-dynamic"` never caches. Fail-closed: without the opt-in (or if any condition fails) the page is `no-cache`. A page that reads `auth` or sets a cookie is never shared.
28
+ - **No-JS forms use `route.ts` + `<Form>`.** Drop `app/.../route.ts` exporting `export const POST: RouteHandler = async ({ form, db, response, auth }) => { await db.insert("X", {...}); response.redirect("/x?ok=1"); }` (303 POST-redirect-GET by default). Render `<Form action="/x">` (from @pylonsync/react) with plain `<input name=...>` — works with JS off (native POST→handler→redirect) and is enhanced to no-reload when JS is on. The handler's `db` is read+write (mutation trust model — gate on `auth`); CSRF is automatic (Origin gate + SameSite=Lax). Multipart/file uploads aren't supported yet — use urlencoded forms + `/api/files`.
29
+ - **`loading.tsx` streams a skeleton while the page's data resolves.** Drop `app/.../loading.tsx` (default export, page props) and the nearest one becomes a route-level Suspense fallback: Pylon flushes the shell + skeleton immediately, then reveals the real page when its top-level `use(serverData…)` resolves (no blank page). It only shows when the PAGE suspends — a page that wraps its own `<Suspense>` around a child (like `/notes`) handles that itself. The skeleton is SERVER-ONLY: don't read `serverData` in it. A page with no `loading.tsx` is buffered (unchanged).
30
+ - **`error.tsx` / `not-found.tsx` boundaries are HYDRATED (interactive).** `app/.../error.tsx` catches a throw below it (HTTP 500) and receives `{ error: { message, digest }, reset }` (`import type { ErrorBoundaryProps }`) — `reset()` re-attempts the route; the stack NEVER reaches the client (dev overlay + logs only). `app/.../not-found.tsx` renders at 404 (also for `response.notFound()`) and gets the page props (`NotFoundProps`), no `reset`. Both run useState/onClick/hooks.
27
31
  - **Client navigation hooks live in @pylonsync/react.** `useRouter()` → `{ push, replace, back, forward, refresh, prefetch }`; `useSearchParams()` → reactive `URLSearchParams`; `usePathname()` → reactive pathname. The hooks are CLIENT-reactive — during SSR they return defaults (empty params / "/"); for server-side URL values read the `url` / `searchParams` page props.
28
32
  - **Dynamic + catch-all routes follow Next conventions.** `app/blog/[slug]/page.tsx` → `params.slug`. `app/docs/[...path]/page.tsx` is a catch-all (matches `/docs/a/b/c`; `params.path === "a/b/c"` — `.split("/")` for segments). `app/shop/[[...filters]]/page.tsx` is an optional catch-all (also matches the bare `/shop`, with `params.filters === ""`). A catch-all must be the last segment; static beats dynamic beats catch-all on overlap.
29
33
  - **`serverData` (SSR) is READ-ONLY.** No write methods; the runtime rejects write frames (`SSR_WRITE_FORBIDDEN`). Mutations belong in actions/functions, never in a page render.
@@ -0,0 +1,43 @@
1
+ import React from "react";
2
+ import { type ErrorBoundaryProps } from "@pylonsync/react";
3
+ import { Button } from "@/components/ui/button";
4
+
5
+ // `app/error.tsx` → the error boundary for this segment. It catches a throw
6
+ // in any page/layout below it and renders at HTTP 500. It's HYDRATED, so
7
+ // this is a real interactive client component: `reset()` re-attempts the
8
+ // route, and useState/onClick work. The thrown error reaches the client as
9
+ // `{ message, digest }` only — the stack stays in the dev overlay
10
+ // (PYLON_DEV_MODE) and the server logs, never in the page.
11
+ export default function Error({ error, reset }: ErrorBoundaryProps) {
12
+ const [tries, setTries] = React.useState(0);
13
+ return (
14
+ <div className="space-y-6">
15
+ <section>
16
+ <h1 className="text-2xl font-semibold tracking-tight">
17
+ Something went wrong
18
+ </h1>
19
+ <p className="mt-2 text-muted-foreground">{error.message}</p>
20
+ {error.digest ? (
21
+ <p className="mt-1 text-xs text-muted-foreground/70">
22
+ Reference: <code>{error.digest}</code>
23
+ </p>
24
+ ) : null}
25
+ </section>
26
+ <div className="flex items-center gap-3">
27
+ <Button
28
+ onClick={() => {
29
+ setTries((n) => n + 1);
30
+ reset();
31
+ }}
32
+ >
33
+ Try again
34
+ </Button>
35
+ {tries > 0 ? (
36
+ <span className="text-sm text-muted-foreground">
37
+ Retried {tries} {tries === 1 ? "time" : "times"}
38
+ </span>
39
+ ) : null}
40
+ </div>
41
+ </div>
42
+ );
43
+ }
@@ -0,0 +1,29 @@
1
+ import React from "react";
2
+ import { Link, useRouter, type NotFoundProps } from "@pylonsync/react";
3
+ import { Button } from "@/components/ui/button";
4
+
5
+ // `app/not-found.tsx` → rendered at HTTP 404 for any unmatched URL (and when
6
+ // a page calls `response.notFound()`). It's HYDRATED, so it's interactive:
7
+ // the buttons below use the client router. Not-found boundaries receive the
8
+ // standard page props (and, matching Next, no `reset`).
9
+ export default function NotFound(_props: NotFoundProps) {
10
+ const router = useRouter();
11
+ return (
12
+ <div className="space-y-6">
13
+ <section>
14
+ <h1 className="text-2xl font-semibold tracking-tight">404</h1>
15
+ <p className="mt-2 text-muted-foreground">
16
+ We couldn&apos;t find that page.
17
+ </p>
18
+ </section>
19
+ <div className="flex items-center gap-3">
20
+ <Button onClick={() => router.back()} variant="outline">
21
+ ← Go back
22
+ </Button>
23
+ <Button asChild>
24
+ <Link href="/">Home</Link>
25
+ </Button>
26
+ </div>
27
+ </div>
28
+ );
29
+ }
@@ -1,5 +1,11 @@
1
1
  import React, { Suspense, use } from "react";
2
- import { type Metadata, type PageProps, type ServerData } from "@pylonsync/react";
2
+ import {
3
+ Form,
4
+ type Metadata,
5
+ type PageProps,
6
+ type ServerData,
7
+ } from "@pylonsync/react";
8
+ import { Button } from "@/components/ui/button";
3
9
  import {
4
10
  Card,
5
11
  CardContent,
@@ -77,7 +83,7 @@ function NotesList({ serverData }: { serverData: ServerData }) {
77
83
  // of its `PageProps` and hands it to the suspending child. (The same props
78
84
  // carry `response` for status/redirect/cookies and `params`/`searchParams`
79
85
  // for the URL — all typed, all from @pylonsync/react.)
80
- export default function NotesPage({ serverData }: PageProps) {
86
+ export default function NotesPage({ serverData, searchParams }: PageProps) {
81
87
  return (
82
88
  <div className="space-y-6">
83
89
  <section>
@@ -91,6 +97,29 @@ export default function NotesPage({ serverData }: PageProps) {
91
97
  </p>
92
98
  </section>
93
99
 
100
+ {/* No-JS form (#276). Posts to app/notes/route.ts, which creates a Note
101
+ and 303-redirects back here. Works with JS disabled; the runtime
102
+ enhances it (no full reload) when JS is on. */}
103
+ {searchParams.created ? (
104
+ <p className="rounded-md border border-emerald-600/30 bg-emerald-600/10 px-3 py-2 text-sm text-emerald-700">
105
+ Note added.
106
+ </p>
107
+ ) : null}
108
+ {searchParams.error ? (
109
+ <p className="rounded-md border border-red-600/30 bg-red-600/10 px-3 py-2 text-sm text-red-700">
110
+ {searchParams.error}
111
+ </p>
112
+ ) : null}
113
+ <Form action="/notes" className="flex items-center gap-2">
114
+ <input
115
+ name="body"
116
+ placeholder="Write a note…"
117
+ aria-label="Note"
118
+ className="flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-sm outline-none focus-visible:ring-1 focus-visible:ring-ring"
119
+ />
120
+ <Button type="submit">Add</Button>
121
+ </Form>
122
+
94
123
  <Card>
95
124
  <CardHeader>
96
125
  <CardTitle>From the database</CardTitle>
@@ -0,0 +1,21 @@
1
+ import { type RouteHandler } from "@pylonsync/react";
2
+
3
+ // `app/notes/route.ts` → handles POST /notes (the same path the page lives at).
4
+ // A `<Form action="/notes">` posts here; we create a Note, then 303-redirect
5
+ // back to /notes (POST-redirect-GET) so a no-JS browser re-renders with the new
6
+ // note. Validation errors round-trip through a query param. With JS, the
7
+ // runtime intercepts the submit + swaps the page in without a full reload —
8
+ // same handler, no extra code.
9
+ //
10
+ // CSRF is automatic: Pylon's Origin gate rejects cross-site POSTs before this
11
+ // runs, and the session cookie is SameSite=Lax. Writes here use the normal
12
+ // function trust model — gate sensitive actions on `auth` inside the handler.
13
+ export const POST: RouteHandler = async ({ form, db, response }) => {
14
+ const body = (form.get("body") ?? "").trim();
15
+ if (!body) {
16
+ response.redirect("/notes?error=" + encodeURIComponent("Note can't be empty"));
17
+ return;
18
+ }
19
+ await db.insert("Note", { body });
20
+ response.redirect("/notes?created=1");
21
+ };