@pylonsync/create-pylon 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
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pylonsync/create-pylon",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.250",
|
|
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"
|
package/templates/ssr/AGENTS.md
CHANGED
|
@@ -24,7 +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. The SAME earned render is also kept in an **origin disk cache** (`.pylon/.cache/ssr`): a cookie-less GET with no query string is served straight off disk for the TTL — skipping the render entirely — then re-rendered live when stale. The disk cache is namespaced per deploy (wiped on each new build) and OFF in `pylon dev` (so an edit is never masked by a stale entry); invalidation is by the `revalidate` TTL or the next deploy.
|
|
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`.
|
|
27
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
|
+
- **`export const streaming = true` streams a page's OWN inner `<Suspense>` boundaries.** Without it (and without a `loading.tsx`), a page is BUFFERED — the whole document, including suspended children, resolves before the first byte. Opt in and the shell + each inner `<Suspense>` fallback flush immediately, then each boundary's real content streams in as its data resolves (multi-boundary progressive streaming). It's opt-in because it changes the response timing contract: a streaming render commits its HTTP head BEFORE suspended subtrees finish, so (a) it's never CDN/disk cacheable — don't combine with `export const revalidate`; (b) `response.setStatus/setCookie/redirect/notFound` only take effect from the SYNCHRONOUS shell render — a call from inside a suspended subtree is dropped (the runtime logs a loud warning naming what was lost); (c) a `throw` from a deep `<Suspense>` child resolves via its nearest `error.tsx` at HTTP 200, not a 5xx. Hydration is clean for any number of boundaries (the data blob ships before hydration runs). Type the config with `import type { RouteSegmentConfig } from "@pylonsync/react"`.
|
|
28
31
|
- **`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.
|
|
29
32
|
- **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.
|
|
30
33
|
- **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.
|
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
import React, { Suspense, use } from "react";
|
|
2
|
-
import {
|
|
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,
|
|
@@ -16,6 +22,15 @@ export const metadata: Metadata = {
|
|
|
16
22
|
"A list of notes read from the database during the server render — no client fetch, no loading flash.",
|
|
17
23
|
};
|
|
18
24
|
|
|
25
|
+
// Progressive streaming opt-in (#278). With this, the page shell — heading,
|
|
26
|
+
// blurb, the <Form> — flushes to the browser IMMEDIATELY, and the inner
|
|
27
|
+
// <Suspense> around <NotesList> streams in its "Loading notes…" fallback,
|
|
28
|
+
// then swaps in the real rows when serverData resolves. Without it, the whole
|
|
29
|
+
// page (including the notes) is buffered and arrives in one shot. Streaming
|
|
30
|
+
// pages are never CDN/disk-cached (the head commits before the data resolves),
|
|
31
|
+
// so don't combine it with `export const revalidate`.
|
|
32
|
+
export const streaming = true;
|
|
33
|
+
|
|
19
34
|
// The `Note` entity from app.ts. Type your rows however you like; the
|
|
20
35
|
// shape is whatever your entity declares.
|
|
21
36
|
interface Note {
|
|
@@ -77,7 +92,7 @@ function NotesList({ serverData }: { serverData: ServerData }) {
|
|
|
77
92
|
// of its `PageProps` and hands it to the suspending child. (The same props
|
|
78
93
|
// carry `response` for status/redirect/cookies and `params`/`searchParams`
|
|
79
94
|
// for the URL — all typed, all from @pylonsync/react.)
|
|
80
|
-
export default function NotesPage({ serverData }: PageProps) {
|
|
95
|
+
export default function NotesPage({ serverData, searchParams }: PageProps) {
|
|
81
96
|
return (
|
|
82
97
|
<div className="space-y-6">
|
|
83
98
|
<section>
|
|
@@ -91,6 +106,29 @@ export default function NotesPage({ serverData }: PageProps) {
|
|
|
91
106
|
</p>
|
|
92
107
|
</section>
|
|
93
108
|
|
|
109
|
+
{/* No-JS form (#276). Posts to app/notes/route.ts, which creates a Note
|
|
110
|
+
and 303-redirects back here. Works with JS disabled; the runtime
|
|
111
|
+
enhances it (no full reload) when JS is on. */}
|
|
112
|
+
{searchParams.created ? (
|
|
113
|
+
<p className="rounded-md border border-emerald-600/30 bg-emerald-600/10 px-3 py-2 text-sm text-emerald-700">
|
|
114
|
+
Note added.
|
|
115
|
+
</p>
|
|
116
|
+
) : null}
|
|
117
|
+
{searchParams.error ? (
|
|
118
|
+
<p className="rounded-md border border-red-600/30 bg-red-600/10 px-3 py-2 text-sm text-red-700">
|
|
119
|
+
{searchParams.error}
|
|
120
|
+
</p>
|
|
121
|
+
) : null}
|
|
122
|
+
<Form action="/notes" className="flex items-center gap-2">
|
|
123
|
+
<input
|
|
124
|
+
name="body"
|
|
125
|
+
placeholder="Write a note…"
|
|
126
|
+
aria-label="Note"
|
|
127
|
+
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"
|
|
128
|
+
/>
|
|
129
|
+
<Button type="submit">Add</Button>
|
|
130
|
+
</Form>
|
|
131
|
+
|
|
94
132
|
<Card>
|
|
95
133
|
<CardHeader>
|
|
96
134
|
<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
|
+
};
|