@pylonsync/create-pylon 0.3.246 → 0.3.247
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.247",
|
|
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
|
@@ -23,6 +23,9 @@ Operating rules for a coding agent in this Pylon app. Pylon is a Rails-like fram
|
|
|
23
23
|
## Key gotchas
|
|
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
|
+
- **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
|
+
- **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
|
+
- **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.
|
|
26
29
|
- **`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.
|
|
27
30
|
- **`response.*` / `response.redirect()` / `response.notFound()` must fire in the synchronous shell render**, before any `await` / `<Suspense>`. The HTTP head commits when the shell is ready — status/headers/cookies set from a suspended subtree are lost, and `redirect`/`notFound` thrown below a Suspense boundary are swallowed.
|
|
28
31
|
- **`ctx.llm` and `ctx.connections` are on mutation + action only, NOT query** (reactive purity). `action` has no direct `ctx.db` — use `ctx.runQuery` / `ctx.runMutation`.
|
|
@@ -1,11 +1,7 @@
|
|
|
1
1
|
import React from "react";
|
|
2
|
+
import type { PageProps } from "@pylonsync/react";
|
|
2
3
|
import { Button } from "@/components/ui/button";
|
|
3
4
|
|
|
4
|
-
interface PageProps {
|
|
5
|
-
url: string;
|
|
6
|
-
searchParams: Record<string, string>;
|
|
7
|
-
}
|
|
8
|
-
|
|
9
5
|
// `app/counter/page.tsx` → `/counter`. This page is server-rendered AND
|
|
10
6
|
// interactive: the HTML arrives with the initial count already in it (try
|
|
11
7
|
// /counter?start=10), then the per-route chunk hydrates and useState takes
|
|
@@ -1,20 +1,14 @@
|
|
|
1
1
|
import React from "react";
|
|
2
|
-
import { Link } from "@pylonsync/react";
|
|
3
|
-
|
|
4
|
-
// Auth shape injected by the SSR runtime. `auth.user_id` is null for
|
|
5
|
-
// anonymous visitors. Wire a sign-in flow with @pylonsync/client when
|
|
6
|
-
// you're ready — for now this just shows the session state.
|
|
7
|
-
interface AuthShape {
|
|
8
|
-
user_id: string | null;
|
|
9
|
-
is_admin: boolean;
|
|
10
|
-
tenant_id: string | null;
|
|
11
|
-
roles: string[];
|
|
12
|
-
}
|
|
2
|
+
import { Link, type PageAuth } from "@pylonsync/react";
|
|
13
3
|
|
|
4
|
+
// A layout receives the page props plus `children`. `auth.user_id` is null
|
|
5
|
+
// for anonymous visitors — wire a sign-in flow with @pylonsync/client when
|
|
6
|
+
// you're ready; for now this just shows the session state. The `PageAuth`
|
|
7
|
+
// type is exported from @pylonsync/react so you never hand-roll it.
|
|
14
8
|
interface LayoutProps {
|
|
15
9
|
children: React.ReactNode;
|
|
16
10
|
url: string;
|
|
17
|
-
auth:
|
|
11
|
+
auth: PageAuth;
|
|
18
12
|
}
|
|
19
13
|
|
|
20
14
|
// The root layout wraps every page. It receives `url` and `auth` from the
|
|
@@ -50,6 +44,9 @@ export default function RootLayout({ children, url, auth }: LayoutProps) {
|
|
|
50
44
|
<Link href="/counter" className="hover:text-foreground">
|
|
51
45
|
Counter
|
|
52
46
|
</Link>
|
|
47
|
+
<Link href="/notes" className="hover:text-foreground">
|
|
48
|
+
Notes
|
|
49
|
+
</Link>
|
|
53
50
|
<span
|
|
54
51
|
className={signedIn ? "text-emerald-600" : "text-muted-foreground/60"}
|
|
55
52
|
title={url}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import React, { Suspense, use } from "react";
|
|
2
|
+
import { type Metadata, type PageProps, type ServerData } from "@pylonsync/react";
|
|
3
|
+
import {
|
|
4
|
+
Card,
|
|
5
|
+
CardContent,
|
|
6
|
+
CardDescription,
|
|
7
|
+
CardHeader,
|
|
8
|
+
CardTitle,
|
|
9
|
+
} from "@/components/ui/card";
|
|
10
|
+
|
|
11
|
+
// SEO metadata for `/notes`. (`generateMetadata(props)` is the dynamic
|
|
12
|
+
// form when the title depends on params — e.g. a `[slug]` route.)
|
|
13
|
+
export const metadata: Metadata = {
|
|
14
|
+
title: "Notes — __APP_NAME__",
|
|
15
|
+
description:
|
|
16
|
+
"A list of notes read from the database during the server render — no client fetch, no loading flash.",
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
// The `Note` entity from app.ts. Type your rows however you like; the
|
|
20
|
+
// shape is whatever your entity declares.
|
|
21
|
+
interface Note {
|
|
22
|
+
id: string;
|
|
23
|
+
body: string;
|
|
24
|
+
done: boolean;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// This component reads the database DURING the render. `serverData.list`
|
|
28
|
+
// returns a promise; React 19 `use()` suspends the subtree until it
|
|
29
|
+
// resolves on the server, then the HTML streams with the rows already in
|
|
30
|
+
// it — no `useEffect`, no client round-trip, no loading flash on first
|
|
31
|
+
// paint. The resolved value is replayed into the hydration payload, so the
|
|
32
|
+
// browser renders the exact same markup. Reads run through the same policy
|
|
33
|
+
// gate as a query function's `ctx.db`; writes are rejected.
|
|
34
|
+
function NotesList({ serverData }: { serverData: ServerData }) {
|
|
35
|
+
const notes = use(serverData.list<Note>("Note"));
|
|
36
|
+
|
|
37
|
+
if (notes.length === 0) {
|
|
38
|
+
return (
|
|
39
|
+
<p className="text-sm text-muted-foreground">
|
|
40
|
+
No notes yet. Create one through the auto-generated API:{" "}
|
|
41
|
+
<code className="rounded bg-muted px-1">
|
|
42
|
+
curl -X POST localhost:8787/api/entities/Note -d
|
|
43
|
+
'{"{"}"body":"hello"{"}"}'
|
|
44
|
+
</code>{" "}
|
|
45
|
+
then refresh.
|
|
46
|
+
</p>
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return (
|
|
51
|
+
<ul className="space-y-2">
|
|
52
|
+
{notes.map((note) => (
|
|
53
|
+
<li
|
|
54
|
+
key={note.id}
|
|
55
|
+
className="flex items-center gap-3 rounded-md border px-3 py-2 text-sm"
|
|
56
|
+
>
|
|
57
|
+
<span
|
|
58
|
+
aria-hidden
|
|
59
|
+
className={
|
|
60
|
+
note.done
|
|
61
|
+
? "text-emerald-600"
|
|
62
|
+
: "text-muted-foreground/50"
|
|
63
|
+
}
|
|
64
|
+
>
|
|
65
|
+
{note.done ? "✓" : "○"}
|
|
66
|
+
</span>
|
|
67
|
+
<span className={note.done ? "line-through text-muted-foreground" : ""}>
|
|
68
|
+
{note.body}
|
|
69
|
+
</span>
|
|
70
|
+
</li>
|
|
71
|
+
))}
|
|
72
|
+
</ul>
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// `app/notes/page.tsx` → `/notes`. The page destructures `serverData` out
|
|
77
|
+
// of its `PageProps` and hands it to the suspending child. (The same props
|
|
78
|
+
// carry `response` for status/redirect/cookies and `params`/`searchParams`
|
|
79
|
+
// for the URL — all typed, all from @pylonsync/react.)
|
|
80
|
+
export default function NotesPage({ serverData }: PageProps) {
|
|
81
|
+
return (
|
|
82
|
+
<div className="space-y-6">
|
|
83
|
+
<section>
|
|
84
|
+
<h1 className="text-2xl font-semibold tracking-tight">Notes</h1>
|
|
85
|
+
<p className="mt-2 text-muted-foreground">
|
|
86
|
+
These rows are read from the database <em>during the server
|
|
87
|
+
render</em> with <code>serverData</code> + React's{" "}
|
|
88
|
+
<code>use()</code>. The HTML arrives with the data in it — view
|
|
89
|
+
source and you'll see the notes in the markup, not an empty
|
|
90
|
+
shell that fetches later.
|
|
91
|
+
</p>
|
|
92
|
+
</section>
|
|
93
|
+
|
|
94
|
+
<Card>
|
|
95
|
+
<CardHeader>
|
|
96
|
+
<CardTitle>From the database</CardTitle>
|
|
97
|
+
<CardDescription>
|
|
98
|
+
Server-rendered, then hydrated. Edit{" "}
|
|
99
|
+
<code className="rounded bg-muted px-1">app.ts</code> to change
|
|
100
|
+
the <code className="rounded bg-muted px-1">Note</code> shape.
|
|
101
|
+
</CardDescription>
|
|
102
|
+
</CardHeader>
|
|
103
|
+
<CardContent>
|
|
104
|
+
<Suspense
|
|
105
|
+
fallback={
|
|
106
|
+
<p className="text-sm text-muted-foreground">Loading notes…</p>
|
|
107
|
+
}
|
|
108
|
+
>
|
|
109
|
+
<NotesList serverData={serverData} />
|
|
110
|
+
</Suspense>
|
|
111
|
+
</CardContent>
|
|
112
|
+
</Card>
|
|
113
|
+
</div>
|
|
114
|
+
);
|
|
115
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import React from "react";
|
|
2
|
-
import { Link } from "@pylonsync/react";
|
|
2
|
+
import { Link, type Metadata, type PageProps } from "@pylonsync/react";
|
|
3
3
|
import { Button } from "@/components/ui/button";
|
|
4
4
|
import {
|
|
5
5
|
Card,
|
|
@@ -9,15 +9,23 @@ import {
|
|
|
9
9
|
CardTitle,
|
|
10
10
|
} from "@/components/ui/card";
|
|
11
11
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
12
|
+
// SEO metadata. Export `metadata` (static) or `generateMetadata(props)`
|
|
13
|
+
// (dynamic) from any page or layout — Pylon renders the <title>/<meta>
|
|
14
|
+
// into <head> server-side. The `Metadata` type is exported from
|
|
15
|
+
// @pylonsync/react.
|
|
16
|
+
export const metadata: Metadata = {
|
|
17
|
+
title: "__APP_NAME__ — full-stack Pylon app",
|
|
18
|
+
description:
|
|
19
|
+
"Server-rendered React, file-based routes, a synced database, and a typed client — one binary, one port.",
|
|
20
|
+
};
|
|
15
21
|
|
|
16
|
-
// `app/page.tsx` → `/`.
|
|
17
|
-
//
|
|
18
|
-
//
|
|
19
|
-
//
|
|
20
|
-
//
|
|
22
|
+
// `app/page.tsx` → `/`. Every page receives `PageProps` from the SSR
|
|
23
|
+
// runtime: `{ url, params, searchParams, auth, response, serverData }` —
|
|
24
|
+
// the type is exported from @pylonsync/react, no hand-rolled interface.
|
|
25
|
+
// This renders to HTML on the server; the per-route chunk hydrates it in
|
|
26
|
+
// the browser so interactive pages (see /counter) just work. shadcn/ui is
|
|
27
|
+
// pre-wired — `Button`/`Card` resolve through the `@/` alias; add more with
|
|
28
|
+
// `npx shadcn@latest add <component>`.
|
|
21
29
|
export default function IndexPage({ url }: PageProps) {
|
|
22
30
|
return (
|
|
23
31
|
<div className="space-y-8">
|
|
@@ -68,6 +76,9 @@ export default function IndexPage({ url }: PageProps) {
|
|
|
68
76
|
<Link href="/counter">See hydration in action →</Link>
|
|
69
77
|
</Button>
|
|
70
78
|
<Button asChild variant="outline">
|
|
79
|
+
<Link href="/notes">Server data in the render →</Link>
|
|
80
|
+
</Button>
|
|
81
|
+
<Button asChild variant="ghost">
|
|
71
82
|
<a
|
|
72
83
|
href="https://docs.pylon.dev"
|
|
73
84
|
target="_blank"
|