create-fluxstack 1.21.1 → 1.22.1

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.
Files changed (55) hide show
  1. package/app/client/src/App.tsx +9 -11
  2. package/app/client/src/components/AppLayout.tsx +291 -290
  3. package/app/client/src/components/BackButton.tsx +17 -16
  4. package/app/client/src/components/ColorWheel.tsx +1 -0
  5. package/app/client/src/components/DemoPage.tsx +136 -135
  6. package/app/client/src/components/ErrorBoundary.tsx +2 -1
  7. package/app/client/src/components/LiveErrorBoundary.tsx +2 -1
  8. package/app/client/src/components/ThemePicker.tsx +1 -0
  9. package/app/client/src/framework/ClientOnly.tsx +14 -0
  10. package/app/client/src/framework/LivePage.tsx +55 -0
  11. package/app/client/src/framework/RscHomePage.tsx +125 -0
  12. package/app/client/src/framework/RscLink.tsx +31 -0
  13. package/app/client/src/framework/RscNav.tsx +42 -0
  14. package/app/client/src/framework/RscRoot.tsx +54 -0
  15. package/app/client/src/framework/csrf.ts +22 -0
  16. package/app/client/src/framework/entry.browser.tsx +51 -0
  17. package/app/client/src/framework/entry.rsc.tsx +41 -0
  18. package/app/client/src/framework/entry.ssr.tsx +12 -0
  19. package/app/client/src/framework/navigation.ts +29 -0
  20. package/app/client/src/framework/params.tsx +22 -0
  21. package/app/client/src/framework/routes.ts +143 -0
  22. package/app/client/src/framework/vite-rsc.d.ts +21 -0
  23. package/app/client/src/hooks/useThemeClock.ts +16 -1
  24. package/app/client/src/live/AuthDemo.tsx +271 -270
  25. package/app/client/src/live/CounterDemo.tsx +152 -151
  26. package/app/client/src/live/FormDemo.tsx +141 -140
  27. package/app/client/src/live/PingPongDemo.tsx +181 -180
  28. package/app/client/src/live/RoomChatDemo.tsx +398 -397
  29. package/app/client/src/live/SharedCounterDemo.tsx +2 -1
  30. package/app/client/src/pages/ApiTestPage.tsx +1 -0
  31. package/app/client/src/pages/auth.tsx +13 -0
  32. package/app/client/src/pages/blog/[slug].tsx +23 -0
  33. package/app/client/src/pages/counter.tsx +15 -0
  34. package/app/client/src/pages/form.tsx +13 -0
  35. package/app/client/src/pages/index.tsx +9 -0
  36. package/app/client/src/pages/ping-pong.tsx +13 -0
  37. package/app/client/src/pages/room-chat.tsx +13 -0
  38. package/app/client/src/pages/shared-counter.tsx +13 -0
  39. package/app/client/src/pages/sobre.tsx +19 -0
  40. package/app/server/index.ts +5 -1
  41. package/app/server/live/rooms/index.ts +14 -0
  42. package/config/system/plugins.config.ts +12 -2
  43. package/core/cli/commands/dev.ts +9 -1
  44. package/core/plugins/built-in/rsc/index.ts +156 -0
  45. package/core/plugins/built-in/ssr/bun-asset-loader.ts +46 -0
  46. package/core/plugins/built-in/ssr/index.ts +143 -0
  47. package/core/plugins/built-in/ssr/registry.ts +38 -0
  48. package/core/plugins/built-in/vite/index.ts +7 -0
  49. package/core/server/live/websocket-plugin.ts +29 -10
  50. package/core/utils/version.ts +6 -6
  51. package/create-fluxstack.ts +64 -5
  52. package/package.json +112 -108
  53. package/playwright.config.ts +6 -2
  54. package/vite.config.ts +22 -0
  55. package/app/client/.live-stubs/LiveUpload.js +0 -15
@@ -1,180 +1,181 @@
1
- import { useMemo, useState, useEffect, useRef, useCallback } from 'react'
2
- import { Live } from '@/core/client'
3
- import { LivePingPong } from '@server/live/LivePingPong'
4
- import type { PingRoom } from '@server/live/rooms/PingRoom'
5
- import { FaGaugeHigh, FaPlay, FaSignal, FaStopwatch } from 'react-icons/fa6'
6
-
7
- interface PingEntry {
8
- seq: number
9
- sentAt: number
10
- rtt: number | null
11
- }
12
-
13
- function StatCard({ label, value, tone = 'white' }: { label: string; value: string; tone?: string }) {
14
- return (
15
- <div className="rounded-lg border border-white/10 bg-white/[0.025] p-4">
16
- <div className={`text-2xl font-semibold tabular-nums ${tone}`}>{value}</div>
17
- <div className="mt-1 text-xs font-medium uppercase tracking-[0.16em] text-gray-600">{label}</div>
18
- </div>
19
- )
20
- }
21
-
22
- export function PingPongDemo() {
23
- const username = useMemo(() => {
24
- const prefix = ['Edge', 'Core', 'Node', 'Wire', 'Frame'][Math.floor(Math.random() * 5)]
25
- const suffix = Math.floor(Math.random() * 100)
26
- return `${prefix}-${suffix}`
27
- }, [])
28
-
29
- const live = Live.use(LivePingPong, {
30
- initialState: { ...LivePingPong.defaultState, username },
31
- })
32
-
33
- const [pings, setPings] = useState<PingEntry[]>([])
34
- const [avgRtt, setAvgRtt] = useState<number | null>(null)
35
- const [minRtt, setMinRtt] = useState<number | null>(null)
36
- const [maxRtt, setMaxRtt] = useState<number | null>(null)
37
- const [autoPing, setAutoPing] = useState(false)
38
- const seqRef = useRef(0)
39
- const pendingRef = useRef<Map<number, number>>(new Map())
40
- const intervalRef = useRef<ReturnType<typeof setInterval> | null>(null)
41
-
42
- useEffect(() => {
43
- const unsub = live.$room<PingRoom>('ping:global').on('pong', (data) => {
44
- const sentAt = pendingRef.current.get(data.seq)
45
- if (sentAt == null) return
46
- pendingRef.current.delete(data.seq)
47
-
48
- const rtt = Date.now() - sentAt
49
- setPings(prev => {
50
- const updated = [{ seq: data.seq, sentAt, rtt }, ...prev].slice(0, 18)
51
- const rtts = updated.filter(p => p.rtt != null).map(p => p.rtt!)
52
- if (rtts.length > 0) {
53
- setAvgRtt(Math.round(rtts.reduce((a, b) => a + b, 0) / rtts.length))
54
- setMinRtt(Math.min(...rtts))
55
- setMaxRtt(Math.max(...rtts))
56
- }
57
- return updated
58
- })
59
- })
60
- return unsub
61
- }, [live])
62
-
63
- const sendPing = useCallback(() => {
64
- const seq = ++seqRef.current
65
- pendingRef.current.set(seq, Date.now())
66
- live.ping({ seq })
67
- }, [live])
68
-
69
- useEffect(() => {
70
- if (autoPing && live.$connected) {
71
- intervalRef.current = setInterval(sendPing, 500)
72
- }
73
- return () => {
74
- if (intervalRef.current != null) {
75
- clearInterval(intervalRef.current)
76
- intervalRef.current = null
77
- }
78
- }
79
- }, [autoPing, live.$connected, sendPing])
80
-
81
- const rttColor = (rtt: number) => {
82
- if (rtt < 10) return 'text-emerald-300'
83
- if (rtt < 50) return 'text-amber-300'
84
- return 'text-red-300'
85
- }
86
-
87
- return (
88
- <div className="grid w-full max-w-5xl gap-4 lg:grid-cols-[380px_1fr]">
89
- <section className="rounded-lg border border-white/10 bg-[#07070b]/85 p-5 shadow-2xl shadow-black/20">
90
- <div className="mb-6 flex items-start justify-between gap-4">
91
- <div>
92
- <h2 className="text-2xl font-semibold tracking-tight text-white">Latency probe</h2>
93
- <p className="mt-2 text-sm leading-6 text-gray-500">
94
- Send binary msgpack events and measure round-trip time through the Live room.
95
- </p>
96
- </div>
97
- <span className={`inline-flex items-center gap-2 rounded-full border px-3 py-1 text-xs ${
98
- live.$connected
99
- ? 'border-emerald-400/25 bg-emerald-400/10 text-emerald-200'
100
- : 'border-red-400/25 bg-red-400/10 text-red-200'
101
- }`}>
102
- <span className={`h-1.5 w-1.5 rounded-full ${live.$connected ? 'bg-emerald-300' : 'bg-red-300'}`} />
103
- {live.$connected ? 'Connected' : 'Offline'}
104
- </span>
105
- </div>
106
-
107
- <div className="grid gap-3">
108
- <StatCard label="Average" value={avgRtt != null ? `${avgRtt}ms` : '--'} />
109
- <div className="grid grid-cols-2 gap-3">
110
- <StatCard label="Min" value={minRtt != null ? `${minRtt}ms` : '--'} tone="text-emerald-300" />
111
- <StatCard label="Max" value={maxRtt != null ? `${maxRtt}ms` : '--'} tone="text-red-300" />
112
- </div>
113
- </div>
114
-
115
- <div className="mt-6 grid grid-cols-2 gap-3">
116
- <button
117
- onClick={sendPing}
118
- disabled={!live.$connected || live.$loading}
119
- className="inline-flex h-12 items-center justify-center gap-2 rounded-lg bg-white px-4 text-sm font-semibold text-black transition hover:bg-gray-200 disabled:opacity-50"
120
- >
121
- <FaPlay className="h-3.5 w-3.5" />
122
- Ping
123
- </button>
124
- <button
125
- onClick={() => setAutoPing(!autoPing)}
126
- disabled={!live.$connected}
127
- className={`inline-flex h-12 items-center justify-center gap-2 rounded-lg border px-4 text-sm font-semibold transition disabled:opacity-50 ${
128
- autoPing
129
- ? 'border-amber-400/25 bg-amber-400/10 text-amber-200'
130
- : 'border-white/10 bg-white/[0.03] text-white hover:bg-white/[0.06]'
131
- }`}
132
- >
133
- <FaStopwatch className="h-3.5 w-3.5" />
134
- {autoPing ? 'Auto on' : 'Auto off'}
135
- </button>
136
- </div>
137
-
138
- <div className="mt-6 rounded-lg border border-white/10 bg-white/[0.025] p-4 text-sm text-gray-400">
139
- <div className="mb-2 flex items-center gap-2 text-white">
140
- <FaSignal className="text-theme" />
141
- Session
142
- </div>
143
- <div className="grid gap-2 text-xs">
144
- <div className="flex justify-between"><span>Client</span><span className="font-mono text-gray-200">{username}</span></div>
145
- <div className="flex justify-between"><span>Online</span><span className="font-mono text-gray-200">{live.$state.onlineCount}</span></div>
146
- <div className="flex justify-between"><span>Total pings</span><span className="font-mono text-gray-200">{live.$state.totalPings}</span></div>
147
- </div>
148
- </div>
149
- </section>
150
-
151
- <section className="rounded-lg border border-white/10 bg-black/30">
152
- <div className="flex items-center justify-between border-b border-white/10 px-4 py-3">
153
- <div className="inline-flex items-center gap-2 text-sm font-semibold text-white">
154
- <FaGaugeHigh className="text-theme" />
155
- Event log
156
- </div>
157
- <span className="text-xs text-gray-500">wire: msgpack binary frames</span>
158
- </div>
159
-
160
- <div className="max-h-[560px] overflow-auto">
161
- {pings.length === 0 ? (
162
- <div className="px-4 py-14 text-center text-sm text-gray-600">
163
- Send a ping to populate the event log.
164
- </div>
165
- ) : (
166
- pings.map((p) => (
167
- <div key={p.seq} className="grid grid-cols-[90px_1fr_auto] items-center gap-3 border-b border-white/5 px-4 py-3 text-sm">
168
- <span className="font-mono text-gray-500">#{p.seq}</span>
169
- <span className="text-gray-500">{new Date(p.sentAt).toLocaleTimeString()}</span>
170
- <span className={`font-mono font-semibold ${p.rtt != null ? rttColor(p.rtt) : 'text-gray-600'}`}>
171
- {p.rtt != null ? `${p.rtt}ms` : 'pending'}
172
- </span>
173
- </div>
174
- ))
175
- )}
176
- </div>
177
- </section>
178
- </div>
179
- )
180
- }
1
+ 'use client'
2
+ import { useMemo, useState, useEffect, useRef, useCallback } from 'react'
3
+ import { Live } from '@/core/client'
4
+ import { LivePingPong } from '@server/live/LivePingPong'
5
+ import type { PingRoom } from '@server/live/rooms/PingRoom'
6
+ import { FaGaugeHigh, FaPlay, FaSignal, FaStopwatch } from 'react-icons/fa6'
7
+
8
+ interface PingEntry {
9
+ seq: number
10
+ sentAt: number
11
+ rtt: number | null
12
+ }
13
+
14
+ function StatCard({ label, value, tone = 'white' }: { label: string; value: string; tone?: string }) {
15
+ return (
16
+ <div className="rounded-lg border border-white/10 bg-white/[0.025] p-4">
17
+ <div className={`text-2xl font-semibold tabular-nums ${tone}`}>{value}</div>
18
+ <div className="mt-1 text-xs font-medium uppercase tracking-[0.16em] text-gray-600">{label}</div>
19
+ </div>
20
+ )
21
+ }
22
+
23
+ export function PingPongDemo() {
24
+ const username = useMemo(() => {
25
+ const prefix = ['Edge', 'Core', 'Node', 'Wire', 'Frame'][Math.floor(Math.random() * 5)]
26
+ const suffix = Math.floor(Math.random() * 100)
27
+ return `${prefix}-${suffix}`
28
+ }, [])
29
+
30
+ const live = Live.use(LivePingPong, {
31
+ initialState: { ...LivePingPong.defaultState, username },
32
+ })
33
+
34
+ const [pings, setPings] = useState<PingEntry[]>([])
35
+ const [avgRtt, setAvgRtt] = useState<number | null>(null)
36
+ const [minRtt, setMinRtt] = useState<number | null>(null)
37
+ const [maxRtt, setMaxRtt] = useState<number | null>(null)
38
+ const [autoPing, setAutoPing] = useState(false)
39
+ const seqRef = useRef(0)
40
+ const pendingRef = useRef<Map<number, number>>(new Map())
41
+ const intervalRef = useRef<ReturnType<typeof setInterval> | null>(null)
42
+
43
+ useEffect(() => {
44
+ const unsub = live.$room<PingRoom>('ping:global').on('pong', (data) => {
45
+ const sentAt = pendingRef.current.get(data.seq)
46
+ if (sentAt == null) return
47
+ pendingRef.current.delete(data.seq)
48
+
49
+ const rtt = Date.now() - sentAt
50
+ setPings(prev => {
51
+ const updated = [{ seq: data.seq, sentAt, rtt }, ...prev].slice(0, 18)
52
+ const rtts = updated.filter(p => p.rtt != null).map(p => p.rtt!)
53
+ if (rtts.length > 0) {
54
+ setAvgRtt(Math.round(rtts.reduce((a, b) => a + b, 0) / rtts.length))
55
+ setMinRtt(Math.min(...rtts))
56
+ setMaxRtt(Math.max(...rtts))
57
+ }
58
+ return updated
59
+ })
60
+ })
61
+ return unsub
62
+ }, [live])
63
+
64
+ const sendPing = useCallback(() => {
65
+ const seq = ++seqRef.current
66
+ pendingRef.current.set(seq, Date.now())
67
+ live.ping({ seq })
68
+ }, [live])
69
+
70
+ useEffect(() => {
71
+ if (autoPing && live.$connected) {
72
+ intervalRef.current = setInterval(sendPing, 500)
73
+ }
74
+ return () => {
75
+ if (intervalRef.current != null) {
76
+ clearInterval(intervalRef.current)
77
+ intervalRef.current = null
78
+ }
79
+ }
80
+ }, [autoPing, live.$connected, sendPing])
81
+
82
+ const rttColor = (rtt: number) => {
83
+ if (rtt < 10) return 'text-emerald-300'
84
+ if (rtt < 50) return 'text-amber-300'
85
+ return 'text-red-300'
86
+ }
87
+
88
+ return (
89
+ <div className="grid w-full max-w-5xl gap-4 lg:grid-cols-[380px_1fr]">
90
+ <section className="rounded-lg border border-white/10 bg-[#07070b]/85 p-5 shadow-2xl shadow-black/20">
91
+ <div className="mb-6 flex items-start justify-between gap-4">
92
+ <div>
93
+ <h2 className="text-2xl font-semibold tracking-tight text-white">Latency probe</h2>
94
+ <p className="mt-2 text-sm leading-6 text-gray-500">
95
+ Send binary msgpack events and measure round-trip time through the Live room.
96
+ </p>
97
+ </div>
98
+ <span className={`inline-flex items-center gap-2 rounded-full border px-3 py-1 text-xs ${
99
+ live.$connected
100
+ ? 'border-emerald-400/25 bg-emerald-400/10 text-emerald-200'
101
+ : 'border-red-400/25 bg-red-400/10 text-red-200'
102
+ }`}>
103
+ <span className={`h-1.5 w-1.5 rounded-full ${live.$connected ? 'bg-emerald-300' : 'bg-red-300'}`} />
104
+ {live.$connected ? 'Connected' : 'Offline'}
105
+ </span>
106
+ </div>
107
+
108
+ <div className="grid gap-3">
109
+ <StatCard label="Average" value={avgRtt != null ? `${avgRtt}ms` : '--'} />
110
+ <div className="grid grid-cols-2 gap-3">
111
+ <StatCard label="Min" value={minRtt != null ? `${minRtt}ms` : '--'} tone="text-emerald-300" />
112
+ <StatCard label="Max" value={maxRtt != null ? `${maxRtt}ms` : '--'} tone="text-red-300" />
113
+ </div>
114
+ </div>
115
+
116
+ <div className="mt-6 grid grid-cols-2 gap-3">
117
+ <button
118
+ onClick={sendPing}
119
+ disabled={!live.$connected || live.$loading}
120
+ className="inline-flex h-12 items-center justify-center gap-2 rounded-lg bg-white px-4 text-sm font-semibold text-black transition hover:bg-gray-200 disabled:opacity-50"
121
+ >
122
+ <FaPlay className="h-3.5 w-3.5" />
123
+ Ping
124
+ </button>
125
+ <button
126
+ onClick={() => setAutoPing(!autoPing)}
127
+ disabled={!live.$connected}
128
+ className={`inline-flex h-12 items-center justify-center gap-2 rounded-lg border px-4 text-sm font-semibold transition disabled:opacity-50 ${
129
+ autoPing
130
+ ? 'border-amber-400/25 bg-amber-400/10 text-amber-200'
131
+ : 'border-white/10 bg-white/[0.03] text-white hover:bg-white/[0.06]'
132
+ }`}
133
+ >
134
+ <FaStopwatch className="h-3.5 w-3.5" />
135
+ {autoPing ? 'Auto on' : 'Auto off'}
136
+ </button>
137
+ </div>
138
+
139
+ <div className="mt-6 rounded-lg border border-white/10 bg-white/[0.025] p-4 text-sm text-gray-400">
140
+ <div className="mb-2 flex items-center gap-2 text-white">
141
+ <FaSignal className="text-theme" />
142
+ Session
143
+ </div>
144
+ <div className="grid gap-2 text-xs">
145
+ <div className="flex justify-between"><span>Client</span><span className="font-mono text-gray-200">{username}</span></div>
146
+ <div className="flex justify-between"><span>Online</span><span className="font-mono text-gray-200">{live.$state.onlineCount}</span></div>
147
+ <div className="flex justify-between"><span>Total pings</span><span className="font-mono text-gray-200">{live.$state.totalPings}</span></div>
148
+ </div>
149
+ </div>
150
+ </section>
151
+
152
+ <section className="rounded-lg border border-white/10 bg-black/30">
153
+ <div className="flex items-center justify-between border-b border-white/10 px-4 py-3">
154
+ <div className="inline-flex items-center gap-2 text-sm font-semibold text-white">
155
+ <FaGaugeHigh className="text-theme" />
156
+ Event log
157
+ </div>
158
+ <span className="text-xs text-gray-500">wire: msgpack binary frames</span>
159
+ </div>
160
+
161
+ <div className="max-h-[560px] overflow-auto">
162
+ {pings.length === 0 ? (
163
+ <div className="px-4 py-14 text-center text-sm text-gray-600">
164
+ Send a ping to populate the event log.
165
+ </div>
166
+ ) : (
167
+ pings.map((p) => (
168
+ <div key={p.seq} className="grid grid-cols-[90px_1fr_auto] items-center gap-3 border-b border-white/5 px-4 py-3 text-sm">
169
+ <span className="font-mono text-gray-500">#{p.seq}</span>
170
+ <span className="text-gray-500">{new Date(p.sentAt).toLocaleTimeString()}</span>
171
+ <span className={`font-mono font-semibold ${p.rtt != null ? rttColor(p.rtt) : 'text-gray-600'}`}>
172
+ {p.rtt != null ? `${p.rtt}ms` : 'pending'}
173
+ </span>
174
+ </div>
175
+ ))
176
+ )}
177
+ </div>
178
+ </section>
179
+ </div>
180
+ )
181
+ }