@pylonsync/create-pylon 0.3.54 → 0.3.57
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/bin/create-pylon.js +28 -51
- package/package.json +1 -1
- package/templates/_root/turbo.json +18 -0
- package/templates/backend/b2b/apps/api/package.json +3 -3
- package/templates/backend/barebones/apps/api/package.json +3 -3
- package/templates/backend/chat/apps/api/package.json +3 -3
- package/templates/backend/consumer/apps/api/package.json +3 -3
- package/templates/backend/todo/apps/api/package.json +3 -3
- package/templates/expo/barebones/apps/expo/package.json +1 -0
- package/templates/expo/chat/apps/expo/App.tsx +126 -156
- package/templates/expo/chat/apps/expo/package.json +1 -0
- package/templates/expo/consumer/apps/expo/App.tsx +211 -179
- package/templates/expo/consumer/apps/expo/package.json +1 -0
- package/templates/expo/todo/apps/expo/package.json +1 -0
- package/templates/ios/chat/apps/ios/Package.swift +1 -11
- package/templates/ios/chat/apps/ios/Sources/__APP_NAME_PASCAL__/ChatRootView.swift +26 -30
- package/templates/ios/chat/apps/ios/Sources/__APP_NAME_PASCAL__/RoomView.swift +35 -36
- package/templates/ios/chat/apps/ios/Sources/__APP_NAME_PASCAL__/__APP_NAME_PASCAL__App.swift +27 -4
- package/templates/ios/chat/apps/ios/project.yml +2 -0
- package/templates/ios/consumer/apps/ios/Package.swift +1 -0
- package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/FeedView.swift +81 -52
- package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/Models.swift +6 -14
- package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/ProfileSetupView.swift +14 -6
- package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/RootView.swift +25 -15
- package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/__APP_NAME_PASCAL__App.swift +33 -5
- package/templates/ios/consumer/apps/ios/project.yml +2 -0
- package/templates/mac/chat/apps/mac/Package.swift +1 -0
- package/templates/mac/chat/apps/mac/Sources/__APP_NAME_PASCAL__/ChatRootView.swift +30 -27
- package/templates/mac/chat/apps/mac/Sources/__APP_NAME_PASCAL__/RoomView.swift +35 -36
- package/templates/mac/chat/apps/mac/Sources/__APP_NAME_PASCAL__/__APP_NAME_PASCAL__App.swift +26 -5
- package/templates/mac/chat/apps/mac/project.yml +2 -0
- package/templates/mac/consumer/apps/mac/Package.swift +1 -0
- package/templates/mac/consumer/apps/mac/Sources/__APP_NAME_PASCAL__/FeedView.swift +81 -52
- package/templates/mac/consumer/apps/mac/Sources/__APP_NAME_PASCAL__/Models.swift +6 -14
- package/templates/mac/consumer/apps/mac/Sources/__APP_NAME_PASCAL__/ProfileSetupView.swift +14 -6
- package/templates/mac/consumer/apps/mac/Sources/__APP_NAME_PASCAL__/RootView.swift +25 -15
- package/templates/mac/consumer/apps/mac/Sources/__APP_NAME_PASCAL__/__APP_NAME_PASCAL__App.swift +34 -6
- package/templates/mac/consumer/apps/mac/project.yml +2 -0
- package/templates/web/chat/apps/web/src/app/components/ChatRoom.tsx +52 -71
- package/templates/web/chat/apps/web/src/app/layout.tsx +3 -2
- package/templates/web/chat/apps/web/src/app/page.tsx +5 -44
- package/templates/web/chat/apps/web/src/app/providers.tsx +25 -0
- package/templates/web/consumer/apps/web/src/app/components/Feed.tsx +139 -119
- package/templates/web/consumer/apps/web/src/app/layout.tsx +3 -2
- package/templates/web/consumer/apps/web/src/app/page.tsx +8 -42
- package/templates/web/consumer/apps/web/src/app/providers.tsx +25 -0
package/templates/mac/consumer/apps/mac/Sources/__APP_NAME_PASCAL__/__APP_NAME_PASCAL__App.swift
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import SwiftUI
|
|
2
2
|
import PylonClient
|
|
3
|
+
import PylonSync
|
|
3
4
|
|
|
4
5
|
@main
|
|
5
6
|
struct __APP_NAME_PASCAL__App: App {
|
|
@@ -7,9 +8,17 @@ struct __APP_NAME_PASCAL__App: App {
|
|
|
7
8
|
|
|
8
9
|
var body: some Scene {
|
|
9
10
|
Window("__APP_NAME__", id: "main") {
|
|
10
|
-
|
|
11
|
-
.
|
|
12
|
-
|
|
11
|
+
Group {
|
|
12
|
+
if let engine = session.engine {
|
|
13
|
+
RootView(engine: engine)
|
|
14
|
+
.environmentObject(session)
|
|
15
|
+
.frame(minWidth: 480, minHeight: 600)
|
|
16
|
+
} else {
|
|
17
|
+
ProgressView()
|
|
18
|
+
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
.task { await session.bootIfNeeded() }
|
|
13
22
|
}
|
|
14
23
|
.windowResizability(.contentMinSize)
|
|
15
24
|
}
|
|
@@ -17,8 +26,9 @@ struct __APP_NAME_PASCAL__App: App {
|
|
|
17
26
|
|
|
18
27
|
@MainActor
|
|
19
28
|
final class AppSession: ObservableObject {
|
|
20
|
-
let
|
|
21
|
-
@Published var
|
|
29
|
+
let client: PylonClient
|
|
30
|
+
@Published private(set) var engine: SyncEngine?
|
|
31
|
+
@Published var myProfileId: String?
|
|
22
32
|
|
|
23
33
|
init() {
|
|
24
34
|
let baseURLString = ProcessInfo.processInfo.environment["PYLON_BASE_URL"]
|
|
@@ -26,6 +36,24 @@ final class AppSession: ObservableObject {
|
|
|
26
36
|
guard let url = URL(string: baseURLString) else {
|
|
27
37
|
fatalError("Invalid PYLON_BASE_URL: \(baseURLString)")
|
|
28
38
|
}
|
|
29
|
-
self.
|
|
39
|
+
self.client = PylonClient(baseURL: url, appName: "__APP_NAME_SNAKE__")
|
|
40
|
+
self.myProfileId = UserDefaults.standard.string(forKey: "myProfileId")
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
func bootIfNeeded() async {
|
|
44
|
+
guard engine == nil else { return }
|
|
45
|
+
let baseURLString = ProcessInfo.processInfo.environment["PYLON_BASE_URL"]
|
|
46
|
+
?? "http://localhost:4321"
|
|
47
|
+
guard let url = URL(string: baseURLString) else { return }
|
|
48
|
+
let config = SyncEngineConfig(baseURL: url, appName: "__APP_NAME_SNAKE__")
|
|
49
|
+
let engine = await SyncEngine(config: config, client: client)
|
|
50
|
+
await engine.start()
|
|
51
|
+
self.engine = engine
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
func setMyProfileId(_ id: String?) {
|
|
55
|
+
myProfileId = id
|
|
56
|
+
if let id { UserDefaults.standard.set(id, forKey: "myProfileId") }
|
|
57
|
+
else { UserDefaults.standard.removeObject(forKey: "myProfileId") }
|
|
30
58
|
}
|
|
31
59
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
-
import { useEffect, useRef, useState
|
|
3
|
+
import { useEffect, useRef, useState } from "react";
|
|
4
|
+
import { db, callFn } from "@pylonsync/react";
|
|
4
5
|
import { Button, Input } from "@__APP_NAME_KEBAB__/ui";
|
|
5
6
|
|
|
6
7
|
type Room = {
|
|
@@ -21,47 +22,29 @@ type Message = {
|
|
|
21
22
|
|
|
22
23
|
const NAME_KEY = "__APP_NAME_SNAKE___author_name";
|
|
23
24
|
|
|
24
|
-
export function ChatRoom({
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
25
|
+
export function ChatRoom() {
|
|
26
|
+
// Live subscriptions. The sync engine pushes diffs over WebSocket
|
|
27
|
+
// — every Room insert + every Message append from any other client
|
|
28
|
+
// re-renders these components without a fetch / poll / refresh.
|
|
29
|
+
const { data: rooms = [] } = db.useQuery<Room>("Room", {
|
|
30
|
+
orderBy: { createdAt: "asc" },
|
|
31
|
+
});
|
|
32
|
+
const [activeId, setActiveId] = useState<string | null>(null);
|
|
33
|
+
useEffect(() => {
|
|
34
|
+
if (!activeId && rooms.length > 0) setActiveId(rooms[0].id);
|
|
35
|
+
}, [rooms, activeId]);
|
|
36
|
+
|
|
37
|
+
const { data: messages = [] } = db.useQuery<Message>("Message", {
|
|
38
|
+
where: activeId ? { roomId: activeId } : undefined,
|
|
39
|
+
orderBy: { createdAt: "asc" },
|
|
40
|
+
limit: 200,
|
|
41
|
+
});
|
|
42
|
+
|
|
36
43
|
const [body, setBody] = useState("");
|
|
37
44
|
const [authorName, setAuthorName] = useState("anonymous");
|
|
38
|
-
const [
|
|
39
|
-
const [pollIdx, setPollIdx] = useState(0);
|
|
45
|
+
const [sending, setSending] = useState(false);
|
|
40
46
|
const scrollerRef = useRef<HTMLDivElement>(null);
|
|
41
47
|
|
|
42
|
-
// Poll the active room every 1.5s. The framework supports a
|
|
43
|
-
// WebSocket subscription path (db.useQuery) — we use polling here
|
|
44
|
-
// so the scaffold has zero front-end SDK setup. Swap in
|
|
45
|
-
// db.useQuery("Message", { roomId }) when you wire up
|
|
46
|
-
// `@pylonsync/react`'s init() in your layout.
|
|
47
|
-
useEffect(() => {
|
|
48
|
-
if (!active) return;
|
|
49
|
-
const t = setInterval(() => setPollIdx((n) => n + 1), 1500);
|
|
50
|
-
return () => clearInterval(t);
|
|
51
|
-
}, [active]);
|
|
52
|
-
|
|
53
|
-
useEffect(() => {
|
|
54
|
-
if (!active) return;
|
|
55
|
-
void (async () => {
|
|
56
|
-
const res = await fetch("/api/fn/roomMessages", {
|
|
57
|
-
method: "POST",
|
|
58
|
-
headers: { "Content-Type": "application/json" },
|
|
59
|
-
body: JSON.stringify({ roomId: active.id }),
|
|
60
|
-
});
|
|
61
|
-
if (res.ok) setMessages(await res.json());
|
|
62
|
-
})();
|
|
63
|
-
}, [active, pollIdx]);
|
|
64
|
-
|
|
65
48
|
useEffect(() => {
|
|
66
49
|
const stored =
|
|
67
50
|
typeof window !== "undefined"
|
|
@@ -71,37 +54,38 @@ export function ChatRoom({
|
|
|
71
54
|
}, []);
|
|
72
55
|
|
|
73
56
|
useEffect(() => {
|
|
74
|
-
// Auto-scroll to bottom
|
|
57
|
+
// Auto-scroll to bottom when message count changes.
|
|
75
58
|
scrollerRef.current?.scrollTo({
|
|
76
59
|
top: scrollerRef.current.scrollHeight,
|
|
77
60
|
behavior: "smooth",
|
|
78
61
|
});
|
|
79
|
-
}, [messages]);
|
|
62
|
+
}, [messages.length, activeId]);
|
|
80
63
|
|
|
81
64
|
function persistName(next: string) {
|
|
82
65
|
setAuthorName(next);
|
|
83
66
|
window.localStorage.setItem(NAME_KEY, next);
|
|
84
67
|
}
|
|
85
68
|
|
|
69
|
+
const active = rooms.find((r) => r.id === activeId) ?? null;
|
|
70
|
+
|
|
86
71
|
async function send() {
|
|
87
72
|
const trimmed = body.trim();
|
|
88
73
|
if (!trimmed || !active) return;
|
|
89
74
|
setBody("");
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
75
|
+
setSending(true);
|
|
76
|
+
try {
|
|
77
|
+
// Server validates body length / auth, runs ctx.db.insert,
|
|
78
|
+
// the resulting change_event hits every subscriber's local
|
|
79
|
+
// store. We don't append to local state ourselves — the
|
|
80
|
+
// useQuery above re-renders when the new row lands.
|
|
81
|
+
await callFn("sendMessage", {
|
|
82
|
+
roomId: active.id,
|
|
83
|
+
body: trimmed,
|
|
84
|
+
authorName,
|
|
99
85
|
});
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
}
|
|
104
|
-
});
|
|
86
|
+
} finally {
|
|
87
|
+
setSending(false);
|
|
88
|
+
}
|
|
105
89
|
}
|
|
106
90
|
|
|
107
91
|
async function createRoom() {
|
|
@@ -111,18 +95,12 @@ export function ChatRoom({
|
|
|
111
95
|
.toLowerCase()
|
|
112
96
|
.replace(/[^a-z0-9]+/g, "-")
|
|
113
97
|
.replace(/^-|-$/g, "");
|
|
114
|
-
|
|
115
|
-
const
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
if (res.ok) {
|
|
121
|
-
const room = (await res.json()) as Room;
|
|
122
|
-
setRooms((prev) => [...prev, room]);
|
|
123
|
-
setActive(room);
|
|
124
|
-
}
|
|
125
|
-
});
|
|
98
|
+
try {
|
|
99
|
+
const room = (await callFn("createRoom", { slug, name })) as Room;
|
|
100
|
+
setActiveId(room.id);
|
|
101
|
+
} catch (e) {
|
|
102
|
+
window.alert(`Create failed: ${String(e)}`);
|
|
103
|
+
}
|
|
126
104
|
}
|
|
127
105
|
|
|
128
106
|
return (
|
|
@@ -149,9 +127,9 @@ export function ChatRoom({
|
|
|
149
127
|
{rooms.map((r) => (
|
|
150
128
|
<li key={r.id}>
|
|
151
129
|
<button
|
|
152
|
-
onClick={() =>
|
|
130
|
+
onClick={() => setActiveId(r.id)}
|
|
153
131
|
className={`w-full text-left px-4 py-2.5 text-sm hover:bg-neutral-50 dark:hover:bg-neutral-900 ${
|
|
154
|
-
r.id ===
|
|
132
|
+
r.id === activeId
|
|
155
133
|
? "bg-neutral-100 dark:bg-neutral-800 font-medium"
|
|
156
134
|
: ""
|
|
157
135
|
}`}
|
|
@@ -181,7 +159,10 @@ export function ChatRoom({
|
|
|
181
159
|
</p>
|
|
182
160
|
</header>
|
|
183
161
|
|
|
184
|
-
<div
|
|
162
|
+
<div
|
|
163
|
+
ref={scrollerRef}
|
|
164
|
+
className="flex-1 overflow-auto px-6 py-4 space-y-3"
|
|
165
|
+
>
|
|
185
166
|
{messages.length === 0 ? (
|
|
186
167
|
<p className="text-sm text-neutral-500 text-center py-12">
|
|
187
168
|
No messages yet. Say hi.
|
|
@@ -219,13 +200,13 @@ export function ChatRoom({
|
|
|
219
200
|
value={body}
|
|
220
201
|
onChange={(e) => setBody(e.target.value)}
|
|
221
202
|
placeholder={`Message ${active.name}…`}
|
|
222
|
-
disabled={
|
|
203
|
+
disabled={sending}
|
|
223
204
|
className="flex-1"
|
|
224
205
|
/>
|
|
225
206
|
<Button
|
|
226
207
|
type="submit"
|
|
227
208
|
variant="primary"
|
|
228
|
-
disabled={
|
|
209
|
+
disabled={sending || !body.trim()}
|
|
229
210
|
>
|
|
230
211
|
Send
|
|
231
212
|
</Button>
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import type { Metadata } from "next";
|
|
2
2
|
import "./globals.css";
|
|
3
|
+
import { Providers } from "./providers";
|
|
3
4
|
|
|
4
5
|
export const metadata: Metadata = {
|
|
5
6
|
title: "__APP_NAME__",
|
|
6
|
-
description: "
|
|
7
|
+
description: "Realtime chat powered by Pylon's sync engine",
|
|
7
8
|
};
|
|
8
9
|
|
|
9
10
|
export default function RootLayout({
|
|
@@ -14,7 +15,7 @@ export default function RootLayout({
|
|
|
14
15
|
return (
|
|
15
16
|
<html lang="en">
|
|
16
17
|
<body className="antialiased min-h-screen bg-white dark:bg-neutral-950 text-neutral-900 dark:text-neutral-100">
|
|
17
|
-
{children}
|
|
18
|
+
<Providers>{children}</Providers>
|
|
18
19
|
</body>
|
|
19
20
|
</html>
|
|
20
21
|
);
|
|
@@ -1,51 +1,12 @@
|
|
|
1
|
-
import { pylon } from "@/lib/pylon";
|
|
2
1
|
import { ChatRoom } from "./components/ChatRoom";
|
|
3
2
|
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
slug: string;
|
|
9
|
-
name: string;
|
|
10
|
-
createdAt: string;
|
|
11
|
-
};
|
|
12
|
-
|
|
13
|
-
type Message = {
|
|
14
|
-
id: string;
|
|
15
|
-
roomId: string;
|
|
16
|
-
authorId: string;
|
|
17
|
-
authorName: string;
|
|
18
|
-
body: string;
|
|
19
|
-
createdAt: string;
|
|
20
|
-
};
|
|
21
|
-
|
|
22
|
-
export default async function HomePage() {
|
|
23
|
-
const rooms = await pylon
|
|
24
|
-
.json<Room[]>("/api/fn/listRooms", {
|
|
25
|
-
method: "POST",
|
|
26
|
-
body: "{}",
|
|
27
|
-
headers: { "Content-Type": "application/json" },
|
|
28
|
-
})
|
|
29
|
-
.catch(() => [] as Room[]);
|
|
30
|
-
|
|
31
|
-
const initialRoom = rooms[0] ?? null;
|
|
32
|
-
const initialMessages = initialRoom
|
|
33
|
-
? await pylon
|
|
34
|
-
.json<Message[]>("/api/fn/roomMessages", {
|
|
35
|
-
method: "POST",
|
|
36
|
-
body: JSON.stringify({ roomId: initialRoom.id }),
|
|
37
|
-
headers: { "Content-Type": "application/json" },
|
|
38
|
-
})
|
|
39
|
-
.catch(() => [] as Message[])
|
|
40
|
-
: [];
|
|
41
|
-
|
|
3
|
+
// All data flows through `db.useQuery` in the client component below —
|
|
4
|
+
// no server-side initial fetch needed. The sync engine handles initial
|
|
5
|
+
// hydration + every subsequent change over the same WebSocket.
|
|
6
|
+
export default function HomePage() {
|
|
42
7
|
return (
|
|
43
8
|
<main className="h-screen flex">
|
|
44
|
-
<ChatRoom
|
|
45
|
-
initialRooms={rooms}
|
|
46
|
-
initialActiveRoom={initialRoom}
|
|
47
|
-
initialMessages={initialMessages}
|
|
48
|
-
/>
|
|
9
|
+
<ChatRoom />
|
|
49
10
|
</main>
|
|
50
11
|
);
|
|
51
12
|
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useEffect, useState } from "react";
|
|
4
|
+
import { init } from "@pylonsync/react";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Boots the Pylon sync engine on first mount. Every `db.useQuery`
|
|
8
|
+
* downstream renders against the same engine instance, so a single
|
|
9
|
+
* top-level Providers wrapper is enough — no need to thread a client
|
|
10
|
+
* through context.
|
|
11
|
+
*
|
|
12
|
+
* `baseUrl` is the same-origin Next path because next.config.ts
|
|
13
|
+
* proxies /api/* to the Pylon binary on PYLON_API_URL. Browser →
|
|
14
|
+
* same-origin → Next server → Pylon: no CORS, no extra DNS, the
|
|
15
|
+
* cookie travels naturally.
|
|
16
|
+
*/
|
|
17
|
+
export function Providers({ children }: { children: React.ReactNode }) {
|
|
18
|
+
const [ready, setReady] = useState(false);
|
|
19
|
+
useEffect(() => {
|
|
20
|
+
init({ baseUrl: "", appName: "__APP_NAME_SNAKE__" });
|
|
21
|
+
setReady(true);
|
|
22
|
+
}, []);
|
|
23
|
+
if (!ready) return null;
|
|
24
|
+
return <>{children}</>;
|
|
25
|
+
}
|