@pylonsync/create-pylon 0.3.246 → 0.3.248

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.246",
3
+ "version": "0.3.248",
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"
@@ -23,6 +23,11 @@ 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
+ - **`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
+ - **`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
+ - **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
+ - **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
31
  - **`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
32
  - **`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
33
  - **`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
@@ -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
+ }
@@ -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: AuthShape;
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,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
+ }
@@ -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&apos;s{" "}
88
+ <code>use()</code>. The HTML arrives with the data in it — view
89
+ source and you&apos;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
- interface PageProps {
13
- url: string;
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` → `/`. Pages receive `{ url, auth, searchParams }` from
17
- // the SSR runtime. This renders to HTML on the server; the per-route
18
- // chunk hydrates it in the browser so interactive pages (see /counter)
19
- // just work. shadcn/ui is pre-wired `Button`/`Card` resolve through the
20
- // `@/` alias and add more with `npx shadcn@latest add <component>`.
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"