@pylonsync/create-pylon 0.3.248 → 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.
|
|
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"
|
package/templates/ssr/AGENTS.md
CHANGED
|
@@ -24,6 +24,8 @@ 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`.
|
|
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).
|
|
28
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.
|
|
29
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.
|
|
@@ -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,
|
|
@@ -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
|
+
};
|